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