KotlinJsBoxSurfaceSmokeTest.kt
src/jvmMain/kotlin/afield/examples/KotlinJsBoxSurfaceSmokeTest.kt · 107 lines
package afield.examples
import afield.lang.remotejs.KotlinJsHeadProfiles
import afield.lang.remotejs.KotlinJsSourceMap
import afield.lang.remotejs.KotlinToJsCompiler
import afield.media.image.util.NodeTool
import org.json.JSONObject
import java.nio.file.Files
import java.util.concurrent.TimeUnit
FIELD_TEST id: remotejs.kotlinjs-box-surface runner: jvm-main entrypoint: afield.examples.KotlinJsBoxSurfaceSmokeTest tags: [smoke, remotejs, kotlinjs] timeoutSec: 120 expect: exitCode: 0 stdoutContains: - "KotlinJsBoxSurfaceSmokeTest ok"
object KotlinJsBoxSurfaceSmokeTest
The Kotlin→JS half of dev_docs/REMOTEJS_BOX_PARITY.md: a // {{ remoteAR }} box compiles
against display, slider, checkbox and setTimeout (bound to the head's field.js globals),
and an exception thrown inside the Kotlin stdlib maps, through the module's source map, to the
editor line of the box's own code rather than to the generated JavaScript line (an inline stdlib function marks the line that called
the user function it was expanded into). The compiled
module runs in Node with stand-ins for the head's globals and a real stack.
fun main(args: Array<String>)
body — 74 lines, from line 33
@JvmStatic
fun main(args: Array<String>) {
val source = """
// {{ remoteAR }}
display("main", "<b>hello</b>")
slider("frequency", 1.0, 10.0, 0.1, 3.0) { v -> display("main", "f=" + v) }
checkbox("square", true) { v -> println("square " + v) }
setTimeout({ println("later") }, 10)
fun mean(values: List<Double>): Double {
return values.reduce { a, b -> a + b } / values.size
}
println(mean(emptyList()))
""".trimIndent()
val selection = KotlinJsHeadProfiles.resolve(source)
val moduleName = "box_surface_remotear"
val prepared = selection.profile.prepareSourceMapped(selection.source, stateKey = moduleName)
val result = KotlinToJsCompiler.compile(prepared.text, moduleName = moduleName, wrapImplicitMain = false)
require(result.success && result.js != null) { "box surface sample did not compile:\n${result.rawLog.takeLast(1800)}" }
val mapText = requireNotNull(result.sourceMap) { "the compiler wrote no source map" }
val map = KotlinJsSourceMap(mapText, "$moduleName.kt", prepared::userLineFor)
val node = NodeTool.resolveNode() ?: run {
println("skipping head execution: ${NodeTool.unavailableMessage()}")
println("KotlinJsBoxSurfaceSmokeTest ok")
return
}
val codeName = "box_surface|uid"
val nodeSource = """
const calls = [];
globalThis.display = (channel, html) => calls.push(["display", channel, html]);
globalThis.slider = (label, cb, o) => { calls.push(["slider", label, o.min, o.max, o.step, o.value, o.channel]); cb(4.5); return o.value; };
globalThis.checkbox = (label, cb, o) => { calls.push(["checkbox", label, o.value, o.channel]); return o.value; };
globalThis.print = (message, line) => calls.push(["print", String(message), line]);
globalThis.__fieldKotlinStateRestore = (name, init) => init();
let stack = "";
try {
require("vm").runInThisContext(${JSONObject.quote(result.js)}, {filename: ${JSONObject.quote(codeName)}});
} catch (error) {
stack = String(error.stack);
calls.push(["error", error.message]);
}
setTimeout(() => console.log(JSON.stringify({calls, stack})), 50);
""".trimIndent()
val script = Files.createTempFile("field-kotlinjs-surface", ".js").toFile()
val output = try {
script.writeText(nodeSource)
val process = ProcessBuilder(node, script.absolutePath).redirectErrorStream(true).start()
val finished = process.waitFor(30, TimeUnit.SECONDS)
if (!finished) process.destroyForcibly()
process.inputStream.bufferedReader().readText().trim().also {
require(finished && process.exitValue() == 0) { "node failed:\n$it" }
}
} finally {
script.delete()
}
val report = JSONObject(output.lines().last { it.startsWith("{") })
val calls = report.getJSONArray("calls").toString()
println("calls: $calls")
require("""["display","main","<b>hello<\/b>"]""" in calls || """["display","main","<b>hello</b>"]""" in calls) { "display missing: $calls" }
require("""["slider","frequency",1,10,0.1,3,"controls"]""" in calls) { "slider spec wrong: $calls" }
require("""["display","main","f=4.5"]""" in calls) { "slider callback did not reach display: $calls" }
require("""["checkbox","square",true,"controls"]""" in calls) { "checkbox spec wrong: $calls" }
require("""["print","later",""" in calls) { "setTimeout callback did not print: $calls" }
require("""["error",""" in calls) { "mean(emptyList()) did not throw: $calls" }
val stack = report.getString("stack")
val line = map.editorLineForStack(stack, codeName)
println("error stack maps to editor line $line")
// reduce is inline: its throw is stdlib code expanded inside mean, so the innermost frame of
// the box's own code is the call on line 9 (the generated line was 856).
require(line == 9) { "runtime error mapped to line $line, expected 9 (the call of mean):\n$stack" }
println("KotlinJsBoxSurfaceSmokeTest ok")
}
}