diff --git a/mps/modules/org.modelix.mps.react/generator/templates/org.modelix.mps.react.generator.templates@generator.mps b/mps/modules/org.modelix.mps.react/generator/templates/org.modelix.mps.react.generator.templates@generator.mps
index a483cf9b..6ee7bd36 100644
--- a/mps/modules/org.modelix.mps.react/generator/templates/org.modelix.mps.react.generator.templates@generator.mps
+++ b/mps/modules/org.modelix.mps.react/generator/templates/org.modelix.mps.react.generator.templates@generator.mps
@@ -361,6 +361,13 @@
+
+
+
+
+
+
+
@@ -423,6 +430,7 @@
+
@@ -3238,48 +3246,59 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3289,7 +3308,7 @@
-
+
@@ -6527,5 +6546,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mps/modules/test.org.modelix.webaspect/models/test.org.modelix.webaspect.modelix.mps b/mps/modules/test.org.modelix.webaspect/models/test.org.modelix.webaspect.modelix.mps
index e91da524..b212e962 100644
--- a/mps/modules/test.org.modelix.webaspect/models/test.org.modelix.webaspect.modelix.mps
+++ b/mps/modules/test.org.modelix.webaspect/models/test.org.modelix.webaspect.modelix.mps
@@ -7,12 +7,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -64,6 +80,7 @@
+
@@ -71,13 +88,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -165,5 +216,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/react-ssr-client/src/components/DefaultComponents.ts b/react-ssr-client/src/components/DefaultComponents.ts
index 374fabb5..37a4b66e 100644
--- a/react-ssr-client/src/components/DefaultComponents.ts
+++ b/react-ssr-client/src/components/DefaultComponents.ts
@@ -100,7 +100,7 @@ export function registerDefaultComponents() {
registerComponentConstructor("modelix.ImageBasedEditor", ModelixImageBasedEditor)
const xhr = new XMLHttpRequest();
- xhr.open("POST", window.location.origin + window.location.pathname + "../../../known-components", true);
+ xhr.open("POST", "known-components", true);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.send(Array.from(componentConstructors.keys()).join("\n"));
}
diff --git a/react-ssr-mps-test/build.gradle.kts b/react-ssr-mps-test/build.gradle.kts
index f79be35a..be1a5c38 100644
--- a/react-ssr-mps-test/build.gradle.kts
+++ b/react-ssr-mps-test/build.gradle.kts
@@ -6,10 +6,12 @@ plugins {
dependencies {
testImplementation(kotlin("stdlib"))
+ testImplementation(kotlin("test"))
testImplementation(libs.playwright)
testImplementation(coreLibs.kotlin.coroutines.core)
testImplementation(coreLibs.kotlin.coroutines.test)
testImplementation(libs.testcontainers)
+ testImplementation(coreLibs.logback.classic)
}
val pluginsDir = layout.buildDirectory.dir("plugins")
@@ -21,13 +23,14 @@ val collectPlugins by tasks.registering(Sync::class) {
val testLanguagesDir = layout.buildDirectory.dir("test-languages")
val copyTestLanguages by tasks.registering(Sync::class) {
- dependsOn(":packageAllPlugins")
- from(zipTree({ project(":mps").layout.buildDirectory.file("mpsbuild/publications/tests.zip") }))
- into(testLanguagesDir)
+ dependsOn(":mps:assembleMpsModules")
+ from(project(":mps").layout.projectDirectory.dir("modules/test.org.modelix.webaspect"))
+ into(testLanguagesDir.map { it.dir("test.org.modelix.webaspect") })
}
tasks {
test {
+ useJUnitPlatform()
dependsOn(collectPlugins)
dependsOn(copyTestLanguages)
environment("MPS_VERSION", mpsVersion)
diff --git a/react-ssr-mps-test/src/test/kotlin/PagesTest.kt b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt
index ca0eedc5..e53279f1 100644
--- a/react-ssr-mps-test/src/test/kotlin/PagesTest.kt
+++ b/react-ssr-mps-test/src/test/kotlin/PagesTest.kt
@@ -1,58 +1,96 @@
package org.modelix.react.ssr.mps.test
+import com.microsoft.playwright.Browser
+import com.microsoft.playwright.Page
import com.microsoft.playwright.Playwright
-import junit.framework.TestCase
import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.utility.MountableFile
import java.io.File
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
-import kotlin.time.ExperimentalTime
import kotlin.time.toJavaDuration
-class PagesTest : TestCase() {
-
- fun `test custom page`() = runWithMPS { port ->
- Playwright.create().use { playwright ->
- val browser = playwright.chromium().launch()
- val page = browser.newPage()
- page.navigate("http://localhost:$port/pages/modelix/test/modules-list/")
- for (i in 1..10) {
- delay(1.seconds)
- if (page.content().contains("""""")) break
- }
- val content = page.content()
- println(content)
- assertTrue(content.contains(""""""))
- assertTrue(content.contains("""Module: org.modelix.mps.react"""))
+class PagesTest {
+
+ companion object {
+ var mps: GenericContainer<*>? = null
+ var playwright: Playwright? = null
+ var browser: Browser? = null
+
+ @BeforeAll
+ @JvmStatic
+ fun beforeAll() {
+ mps = GenericContainer("modelix/mps-vnc-baseimage:0.9.4-mps${System.getenv("MPS_VERSION")}")
+ .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_MPS_PLUGINS_PATH")).toPath()), "/mps/plugins")
+ .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_TEST_LANGUAGES_PATH")).toPath()), "/mps-languages")
+ .withExposedPorts(43595)
+// .waitingFor(Wait.forListeningPort().withStartupTimeout(3.minutes.toJavaDuration()))
+ .waitingFor(Wait.forHttp("/pages/modelix/test/modules-list/").withStartupTimeout(3.minutes.toJavaDuration()))
+ .withLogConsumer {
+ println(it.utf8StringWithoutLineEnding)
+ }
+ .also { it.start() }
+ playwright = Playwright.create()
+ browser = playwright!!.chromium().launch()
+ }
+
+ @AfterAll
+ @JvmStatic
+ fun afterAll() {
+ browser?.close()
+ browser = null
+ playwright?.close()
+ playwright = null
+ mps?.stop()
+ mps = null
}
}
-}
-fun runWithMPS(
- body: suspend (port: Int) -> Unit,
-) = runBlocking {
- @OptIn(ExperimentalTime::class)
- withTimeout(5.minutes) {
- val mps: GenericContainer<*> = GenericContainer("modelix/mps-vnc-baseimage:0.9.4-mps${System.getenv("MPS_VERSION")}")
- .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_MPS_PLUGINS_PATH")).toPath()), "/mps/plugins")
- .withCopyFileToContainer(MountableFile.forHostPath(File(System.getenv("MODELIX_TEST_LANGUAGES_PATH")).toPath()), "/mps-languages")
- .withExposedPorts(43595)
-// .waitingFor(Wait.forListeningPort().withStartupTimeout(3.minutes.toJavaDuration()))
- .waitingFor(Wait.forHttp("/pages/modelix/test/modules-list/").withStartupTimeout(3.minutes.toJavaDuration()))
- .withLogConsumer {
- println(it.utf8StringWithoutLineEnding)
- }
-
- mps.start()
- try {
- body(mps.firstMappedPort)
- } finally {
- mps.stop()
+ @Test
+ fun `custom page is available`() = runBrowserTest("pages/modelix/test/modules-list/") { page ->
+ page.locator("ul").waitFor()
+ val content = page.content()
+ println(content)
+ assertTrue(content.contains(""""""))
+ assertTrue(content.contains("""Module: org.modelix.mps.react"""))
+ }
+
+ @Test
+ fun `text field is editable`() = runBrowserTest("pages/modelix/test/text-field/") { page ->
+ val textField = page.locator("input")
+ val readOnlyText = page.locator("div[class='name']")
+ textField.waitFor()
+ val content = page.content()
+ println(content)
+ assertEquals("MyClass", textField.getAttribute("value"))
+ assertEquals("MyClass", readOnlyText.textContent())
+
+ textField.fill("MyChangedClass")
+ page.locator("div:has-text('MyChangedClass')[class='name']").waitFor()
+ assertEquals("MyChangedClass", textField.getAttribute("value"))
+ assertEquals("MyChangedClass", readOnlyText.textContent())
+ }
+
+ suspend fun Page.waitForContent(expected: String) {
+ for (i in 1..10) {
+ delay(1.seconds)
+ if (content().contains(expected)) return
+ }
+ error("Content not found.\n\n${content()}")
+ }
+
+ private fun runBrowserTest(path: String, body: suspend (Page) -> Unit) = runTest {
+ browser!!.newPage().use { page ->
+ page.navigate("http://localhost:${mps!!.firstMappedPort}/$path")
+ body(page)
}
}
}
diff --git a/react-ssr-mps-test/src/test/resources/logback-test.xml b/react-ssr-mps-test/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..7c480bf3
--- /dev/null
+++ b/react-ssr-mps-test/src/test/resources/logback-test.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt
index 96a11f45..889b02f7 100644
--- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt
+++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/CompiledMPSRenderer.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
+import org.jetbrains.mps.openapi.model.SNode
import org.jetbrains.mps.openapi.module.SRepository
import org.modelix.incremental.IIncrementalEngine
import org.modelix.incremental.TrackableValue
@@ -20,6 +21,7 @@ import org.modelix.model.api.getAllConcepts
import org.modelix.model.mpsadapters.MPSArea
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
import org.modelix.model.mpsadapters.computeRead
+import org.modelix.model.mpsadapters.tomps.ModelixNodeAsMPSNode
import org.modelix.react.ssr.mps.aspect.CompositeReactSSRAspectDescriptor
import org.modelix.react.ssr.mps.aspect.IReactNodeRenderer
import org.modelix.react.ssr.mps.aspect.IReactSSRAspectDescriptor
@@ -156,20 +158,33 @@ class CompiledMPSRenderer(
}
}
+ private fun ensureIsTracked(obj: T): T {
+ return when (obj) {
+ is NodeRendererCall -> obj.copy(node = ensureIsTracked(obj.node))
+ is NamedRendererCall -> obj.copy(parameterValues = ensureIsTracked(obj.parameterValues))
+ is SNode -> ModelixNodeAsMPSNode.ensureIsTracked(obj)
+ is List<*> -> obj.map { ensureIsTracked(it) }
+ else -> obj
+ } as T
+ }
+
fun renderMPSNode(call: RendererCall, descriptor: IReactSSRAspectDescriptor): IComponentOrList = renderMPSNodeIncremental(call, descriptor)
private val renderMPSNodeIncremental: (RendererCall, IReactSSRAspectDescriptor) -> IComponentOrList = incremenentalEngine.incrementalFunction("renderMPSNode") { _, call: RendererCall, descriptor: IReactSSRAspectDescriptor ->
if (call is NodeRefRendererCall) {
val node = MPSArea(repository()).asModel().resolveNode(call.node)
return@incrementalFunction renderMPSNode(NodeRendererCall(node), descriptor)
}
+
+ val call = ensureIsTracked(call)
+
val renderers = resolveRenderers(call, descriptor)
val renderer = renderers.firstOrNull() // TODO resolve conflict if multiple renderers are applicable
?: return@incrementalFunction renderNode(call)
val context = object : IRenderContext {
override fun getIncrementalEngine(): IIncrementalEngine = incrementalEngine
- override fun renderNode(node: INode): IComponentOrList {
- return renderMPSNode(NodeRendererCall(node.asReadableNode()), descriptor)
+ override fun callRenderer(call: RendererCall): IComponentOrList {
+ return renderMPSNode(call, descriptor)
}
override fun getState(id: String, defaultValue: Boolean): Boolean {
diff --git a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt
index 309b389c..b5065c89 100644
--- a/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt
+++ b/react-ssr-mps/src/main/kotlin/org/modelix/react/ssr/mps/aspect/ReactSSRAspectDescriptor.kt
@@ -9,6 +9,7 @@ import org.modelix.model.api.INode
import org.modelix.model.mpsadapters.computeRead
import org.modelix.react.ssr.server.ConceptRendererSignature
import org.modelix.react.ssr.server.IComponentOrList
+import org.modelix.react.ssr.server.NodeRendererCall
import org.modelix.react.ssr.server.RendererCall
import org.modelix.react.ssr.server.RendererSignature
@@ -79,7 +80,8 @@ interface IReactNodeRenderer {
interface IRenderContext {
fun getIncrementalEngine(): IIncrementalEngine
- fun renderNode(node: INode): IComponentOrList
+ fun renderNode(node: INode): IComponentOrList = callRenderer(NodeRendererCall(node.asReadableNode()))
+ fun callRenderer(call: RendererCall): IComponentOrList
fun getState(id: String, defaultValue: String?): String?
fun setState(id: String, value: String?): String?
fun getState(id: String, defaultValue: Boolean): Boolean
diff --git a/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt b/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt
index 092a59b0..a18a4a33 100644
--- a/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt
+++ b/reverse-mpsadapters/src/main/kotlin/org/modelix/model/mpsadapters/tomps/ModelixNodeAsMPSNode.kt
@@ -72,6 +72,20 @@ data class ModelixNodeAsMPSNode(val node: IReadableNode) : SNode {
return ModelixNodeAsMPSNode(node)
}
+ @JvmStatic
+ fun ensureIsTracked(node: SNode): SNode {
+ return when (node) {
+ is ModelixNodeAsMPSNode -> node
+ else -> ModelixNodeAsMPSNode(MPSWritableNode(node))
+ }
+ }
+
+ @JvmStatic
+ @JvmName("ensureIsTrackedNullable")
+ fun ensureIsTracked(node: SNode?): SNode? {
+ return if (node == null) null else ensureIsTracked(node)
+ }
+
private fun unwrapMPSNode(node: SNode): SNode {
return ((node as? ModelixNodeAsMPSNode)?.node as? MPSWritableNode)?.node
?: node