Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -435,11 +435,13 @@ private void processGenericNode(final Node node) throws IOException {
private String escapeInAttribute(final String unescaped) {
// FIXME ask JSoup's maintainer to expose a public method to escape the
// value of an attribute
return escapeHtmlPreservingApostrophe(unescaped).replace("\"", """);
return escapeHtmlPreservingApostrophe(unescaped)
.replace("\"", """);
}

private String escapeText(final String unescaped) {
return escapeHtmlPreservingApostrophe(unescaped).replace(""", "\"");
return escapeHtmlPreservingApostrophe(unescaped)
.replace(""", "\"");
}

private String escapeHtmlPreservingApostrophe(final String unescaped) {
Expand Down
38 changes: 38 additions & 0 deletions htmlflow-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@
<version>${junit.jupiter-version}</version>
<scope>test</scope>
</dependency>

<!-- Browser automation for the MFE engine e2e tests -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.63.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -123,4 +131,34 @@
</plugin>
</plugins>
</build>

<profiles>
<!-- End-to-end tests of htmlflow-mfe-engine: mvn -pl htmlflow-core -am -Pe2e verify -->
<profile>
<id>e2e</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<argLine>-Xmx32m</argLine>
<systemPropertyVariables>
<mfe.engine.dist>${project.basedir}/../htmlflow-mfe-engine/dist</mfe.engine.dist>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
13 changes: 6 additions & 7 deletions htmlflow-core/src/main/java/htmlflow/visitor/HtmlVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@

package htmlflow.visitor;

import static htmlflow.visitor.Tags.*;

import htmlflow.HtmlMfeConfig;
import htmlflow.exceptions.HtmlFlowAppendException;
import htmlflow.visitor.escape.HtmlEscapers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.xmlet.htmlapifaster.Area;
import org.xmlet.htmlapifaster.Base;
import org.xmlet.htmlapifaster.Br;
Expand All @@ -46,13 +52,6 @@
import org.xmlet.htmlapifaster.Source;
import org.xmlet.htmlapifaster.Text;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import static htmlflow.visitor.Tags.*;

/**
* This is the base implementation of the ElementVisitor (from HtmlApiFaster library).
*
Expand Down
61 changes: 61 additions & 0 deletions htmlflow-core/src/test/java/htmlflow/test/e2e/MfeBasicIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package htmlflow.test.e2e;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.Assert.assertEquals;

import com.microsoft.playwright.Locator;
import htmlflow.HtmlFlow;
import htmlflow.HtmlView;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xmlet.htmlapifaster.EnumTypeScriptType;

/** End-to-end test of the compiled htmlflow-mfe-engine (dist/) rendering HtmlFlow MFE views. */
public class MfeBasicIT extends MfeE2EBase {

private static final HtmlView<?> HOST_PAGE = HtmlFlow.ViewFactory.builder()
.mfeEnabled(true)
.preEncoding(true)
.build()
.view(page -> page
.html()
.head()
.script().attrType(EnumTypeScriptType.MODULE).attrSrc(ENGINE_URL).__()
.__()
.body()
.div()
.mfe(cfg -> {
cfg.setMfeUrlResource("/fragment");
cfg.setMfeName("hello");
})
.__()
.__()
.__());

private static MfeTestServer server;

@BeforeClass
public static void startServer() throws Exception {
server = new MfeTestServer()
.html("/", HOST_PAGE::render)
.html("/fragment", () -> "<p id=\"greeting\">Hello World</p>")
.start();
}

@AfterClass
public static void stopServer() {
if (server != null) server.stop();
}

@Test
public void shouldRenderHelloWorldInsideMicroFrontend() {
page.navigate(server.url("/"));

// Playwright CSS locators pierce open shadow roots
Locator mfe = page.locator("micro-frontend[mfe-name='hello']");
assertThat(mfe.locator("#greeting")).hasText("Hello World");
assertEquals(List.of(), pageErrors);
}
}
88 changes: 88 additions & 0 deletions htmlflow-core/src/test/java/htmlflow/test/e2e/MfeE2EBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package htmlflow.test.e2e;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.Route;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;

/**
* Shared Playwright setup for MFE e2e tests.
* <ul>
* <li>{@link #ENGINE_URL} is fulfilled from the compiled engine in {@code htmlflow-mfe-engine/dist}.</li>
* <li>{@code /e2e/<name>} is fulfilled from the test classpath resource {@code e2e/<name>} (JS or CSS).</li>
* </ul>
* Subclasses own their {@link MfeTestServer} (host page and fragments).
*/
public abstract class MfeE2EBase {

protected static final String ENGINE_URL = "/engine/htmlflow-mfe-engine.js";
protected static final String TEST_JS_ROUTE = "/e2e/";

private static final Path ENGINE_FILE = Path.of(
System.getProperty("mfe.engine.dist", "../htmlflow-mfe-engine/dist"),
"htmlflow-mfe-engine.js"
);

private static Playwright playwright;
protected static Browser browser;

protected BrowserContext context;
protected Page page;
protected final List<String> pageErrors = new ArrayList<>();

@BeforeClass
public static void launchBrowser() {
playwright = Playwright.create();
browser = playwright.chromium().launch();
}

@AfterClass
public static void closeBrowser() {
if (browser != null) browser.close();
if (playwright != null) playwright.close();
}

@Before
public void openPage() {
context = browser.newContext();
context.route("**" + ENGINE_URL, route -> route.fulfill(new Route.FulfillOptions()
.setContentType("text/javascript")
.setPath(ENGINE_FILE)));
context.route("**" + TEST_JS_ROUTE + "*", MfeE2EBase::fulfillTestScript);
page = context.newPage();
page.onPageError(pageErrors::add);
}

@After
public void closePage() {
if (context != null) context.close();
}

private static void fulfillTestScript(Route route) {
String path = URI.create(route.request().url()).getPath();
String resource = "e2e/" + path.substring(path.lastIndexOf('/') + 1);
try (InputStream in = MfeE2EBase.class.getClassLoader().getResourceAsStream(resource)) {
if (in == null) {
route.fulfill(new Route.FulfillOptions().setStatus(404).setBody("Not found: " + resource));
return;
}
route.fulfill(new Route.FulfillOptions()
.setContentType(resource.endsWith(".css") ? "text/css" : "text/javascript")
.setBodyBytes(in.readAllBytes()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
144 changes: 144 additions & 0 deletions htmlflow-core/src/test/java/htmlflow/test/e2e/MfeEventsIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package htmlflow.test.e2e;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import static org.junit.Assert.assertEquals;

import com.microsoft.playwright.Locator;
import htmlflow.HtmlFlow;
import htmlflow.HtmlView;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xmlet.htmlapifaster.EnumTypeScriptType;

/**
* Two MFEs talking over the engine's event bus:
* <ul>
* <li>microfrontend_1 button click -> payload rendered as a list item in microfrontend_2;</li>
* <li>microfrontend_1 button click -> microfrontend_2 reloads itself and shows the new server content.</li>
* </ul>
*/
public class MfeEventsIT extends MfeE2EBase {

private static final String EVENT_NAME = "itemAdded";

private static final HtmlView<?> HOST_PAGE = HtmlFlow.ViewFactory.builder()
.mfeEnabled(true)
.preEncoding(true)
.build()
.view(page -> page
.html()
.head()
.script().attrType(EnumTypeScriptType.MODULE).attrSrc(ENGINE_URL).__()
.script().attrType(EnumTypeScriptType.MODULE).attrSrc(TEST_JS_ROUTE + "mfe-events.js").__()
.__()
.body()
.div()
.mfe(cfg -> {
cfg.setMfeUrlResource("/fragments/microfrontend_1");
cfg.setMfeName("microfrontend_1");
cfg.setMfeTriggersEventName(EVENT_NAME);
})
.mfe(cfg -> {
cfg.setMfeUrlResource("/fragments/microfrontend_2");
cfg.setMfeName("microfrontend_2");
cfg.setMfeListeningEventName(EVENT_NAME);
})
.__()
.__()
.__());

private static final String RELOAD_EVENT_NAME = "reloadRequested";

private static final HtmlView<?> RELOAD_HOST_PAGE = HtmlFlow.ViewFactory.builder()
.mfeEnabled(true)
.preEncoding(true)
.build()
.view(page -> page
.html()
.head()
.script().attrType(EnumTypeScriptType.MODULE).attrSrc(ENGINE_URL).__()
.script().attrType(EnumTypeScriptType.MODULE).attrSrc(TEST_JS_ROUTE + "mfe-reload.js").__()
.__()
.body()
.div()
.mfe(cfg -> {
cfg.setMfeUrlResource("/fragments/reload/microfrontend_1");
cfg.setMfeName("microfrontend_1");
cfg.setMfeTriggersEventName(RELOAD_EVENT_NAME);
})
.mfe(cfg -> {
cfg.setMfeUrlResource("/fragments/reload/microfrontend_2");
cfg.setMfeName("microfrontend_2");
cfg.setMfeListeningEventName(RELOAD_EVENT_NAME);
})
.__()
.__()
.__());

// Bumped on every microfrontend_2 fetch, so each (re)load returns different content.
// AtomicInteger because the server answers on its own threads.
private static final AtomicInteger version = new AtomicInteger();

private static MfeTestServer server;

@BeforeClass
public static void startServer() throws Exception {
server = new MfeTestServer()
.html("/", HOST_PAGE::render)
.html("/fragments/microfrontend_1", () -> "<button id=\"send\">Send item</button>")
.html("/fragments/microfrontend_2", () -> "<ul id=\"items\"></ul>")
.html("/reload", RELOAD_HOST_PAGE::render)
.html("/fragments/reload/microfrontend_1", () -> "<button id=\"reload\">Reload</button>")
.html("/fragments/reload/microfrontend_2", () -> "<p id=\"version\">Version " + version.incrementAndGet() + "</p>")
.start();
}

@Before
public void resetVersion() {
version.set(0);
}

@AfterClass
public static void stopServer() {
if (server != null) server.stop();
}

@Test
public void shouldSendEventFromMicrofrontend1AndRenderItemInMicrofrontend2() {
page.navigate(server.url("/"));

Locator mfe1 = page.locator("micro-frontend[mfe-name='microfrontend_1']");
Locator mfe2 = page.locator("micro-frontend[mfe-name='microfrontend_2']");
assertThat(mfe1.locator("#send")).isVisible();
assertThat(mfe2.locator("#items")).isAttached();

mfe1.locator("#send").click();

Locator items = mfe2.locator("#items li");
assertThat(items).hasCount(1);
assertThat(items.first()).hasText("test 1");
assertThat(items.first()).hasAttribute("data-id", "1");
assertThat(mfe1.locator("li")).hasCount(0);
assertEquals(List.of(), pageErrors);
}

@Test
public void shouldReloadMicrofrontend2WhenEventIsReceived() {
page.navigate(server.url("/reload"));

Locator mfe1 = page.locator("micro-frontend[mfe-name='microfrontend_1']");
Locator mfe2 = page.locator("micro-frontend[mfe-name='microfrontend_2']");
assertThat(mfe2.locator("#version")).hasText("Version 1");

mfe1.locator("#reload").click();

// New content can only come from re-fetching the fragment
assertThat(mfe2.locator("#version")).hasText("Version 2");
assertThat(mfe2.locator("#version")).hasCount(1);
assertEquals(List.of(), pageErrors);
}
}
Loading
Loading