You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by bo...@apache.org on 2021/01/11 21:09:35 UTC

[myfaces-tobago] 03/11: Integration tests with testcontainers

This is an automated email from the ASF dual-hosted git repository.

bommel pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit 9313c7f445f9453a781922dd7cdd352f888a3406
Author: Henning Nöth <hn...@apache.org>
AuthorDate: Tue Dec 15 20:59:28 2020 +0100

    Integration tests with testcontainers
    
    * update junit5; add testcontainers dependencies
    * add TestRule fake class, which is needed for testcontainers
    * better naming package, class, tests
    * refactor integration tests to work with jasmine
---
 pom.xml                                            |  30 +-
 tobago-example/tobago-example-demo/pom.xml         |   2 +-
 .../tobago-example-demo/src/main/webapp/test.xhtml |   2 +-
 .../src/main/webapp/testAccessAllPages.xhtml       |   4 +-
 .../tobago/example/demo/integration/BasicTest.java | 161 ++++++++++
 .../example/demo/integration/FrontendBase.java     | 119 +++++++
 .../example/demo/integration/FrontendTest.java     |  93 ++++++
 .../example/demo/qunit/AccessAllPagesTest.java     | 158 ---------
 .../tobago/example/demo/qunit/SeleniumBase.java    | 354 ---------------------
 .../tobago/example/demo/qunit/StandardTest.java    |  81 -----
 .../src/test/java/org/junit/rules/TestRule.java    |  28 ++
 11 files changed, 433 insertions(+), 599 deletions(-)

diff --git a/pom.xml b/pom.xml
index fbf27d2..79f749f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -448,13 +448,31 @@
       <dependency>
         <groupId>org.junit.jupiter</groupId>
         <artifactId>junit-jupiter-engine</artifactId>
-        <version>5.6.2</version>
+        <version>5.7.0</version>
         <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>org.junit.jupiter</groupId>
         <artifactId>junit-jupiter-params</artifactId>
-        <version>5.6.2</version>
+        <version>5.7.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-api</artifactId>
+        <version>5.7.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>testcontainers</artifactId>
+        <version>1.15.1</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.testcontainers</groupId>
+        <artifactId>junit-jupiter</artifactId>
+        <version>1.15.1</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -532,6 +550,14 @@
       <artifactId>junit-jupiter-params</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>junit-jupiter</artifactId>
+    </dependency>
+    <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
       <scope>test</scope>
diff --git a/tobago-example/tobago-example-demo/pom.xml b/tobago-example/tobago-example-demo/pom.xml
index 3f0d1dc..d9ab0c5 100644
--- a/tobago-example/tobago-example-demo/pom.xml
+++ b/tobago-example/tobago-example-demo/pom.xml
@@ -144,7 +144,7 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
           <excludes>
-            <exclude>qunit/*</exclude>
+            <exclude>integration/*</exclude>
           </excludes>
         </configuration>
       </plugin>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/test.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/test.xhtml
index d06d97b..5b857f6 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/test.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/test.xhtml
@@ -28,7 +28,7 @@
   <tc:script file="#{request.contextPath}/tobago/test/tobago-test-tool.js" type="module"/>
   <tc:script file="#{request.contextPath}/script/tobago-test.js" type="module"/>
   <tc:style file="#{request.contextPath}/style/jasmine-3.5.0.css"/>
-  <tc:script file="#{request.contextPath}/#{param['accessTest'] ? 'error/error' : param['base']}.test.js"
+  <tc:script file="#{request.contextPath}/#{param['basicTest'] ? 'error/error' : param['base']}.test.js"
              type="module"/>
 
   <div id="jasmine-container"></div>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/testAccessAllPages.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/testAccessAllPages.xhtml
index 87e5511..7aa327d 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/testAccessAllPages.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/testAccessAllPages.xhtml
@@ -29,9 +29,9 @@
     <c:forEach items="#{testController.allTestPages}" var="testPage">
       <div class="testframe-wrapper">
         <div>
-          <tc:link label="open in new tab" link="test.xhtml?base=#{testPage.base}&amp;accessTest=true" target="_blank"/>
+          <tc:link label="open in new tab" link="test.xhtml?base=#{testPage.base}&amp;basicTest=true" target="_blank"/>
         </div>
-        <tc:object id="#{testPage.id}" name="test.xhtml?base=#{testPage.base}&amp;accessTest=true">
+        <tc:object id="#{testPage.id}" name="test.xhtml?base=#{testPage.base}&amp;basicTest=true">
           <tc:style height="300px"/>
         </tc:object>
       </div>
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/BasicTest.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/BasicTest.java
new file mode 100644
index 0000000..7216c7b
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/BasicTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo.integration;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalTime;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Testcontainers
+class BasicTest extends FrontendBase {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  static LocalTime startTime;
+
+  @BeforeAll
+  public static void setup() {
+    startTime = LocalTime.now();
+  }
+
+  @Test
+  void verifyExceptionTest() throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
+    final String host = InetAddress.getLocalHost().getHostAddress();
+    final int tomcatPort = tomcat.getFirstMappedPort();
+    final String path = "error/exception.xhtml";
+
+    final String base = path.substring(0, path.length() - 6);
+    final String url = "http://" + host + ":" + tomcatPort + "/test.xhtml?base="
+        + URLEncoder.encode(base, "UTF-8") + "&basicTest=true";
+
+    WebDriver webDriver = getWebDriver(host, seleniumChrome.getFirstMappedPort());
+    webDriver.get(url);
+
+    List<WebElement> results = getJasmineResults(webDriver);
+    Assertions.assertTrue(results.size() > 0, "no results detected");
+    for (WebElement result : results) {
+      if ("has no exception".equals(result.getAttribute("title"))) {
+        Assertions.assertEquals("jasmine-failed", result.getAttribute("class"), result.getAttribute("title"));
+      } else {
+        Assertions.assertEquals("jasmine-passed", result.getAttribute("class"), result.getAttribute("title"));
+      }
+    }
+  }
+
+  @Test
+  void verify404Test() throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
+    final String host = InetAddress.getLocalHost().getHostAddress();
+    final int tomcatPort = tomcat.getFirstMappedPort();
+    final String path = "error/404.xhtml";
+
+    final String base = path.substring(0, path.length() - 6);
+    final String url = "http://" + host + ":" + tomcatPort + "/test.xhtml?base="
+        + URLEncoder.encode(base, "UTF-8") + "&basicTest=true";
+
+    WebDriver webDriver = getWebDriver(host, seleniumChrome.getFirstMappedPort());
+    webDriver.get(url);
+
+    List<WebElement> results = getJasmineResults(webDriver);
+    Assertions.assertTrue(results.size() > 0, "no results detected");
+    for (WebElement result : results) {
+      if ("has no 404".equals(result.getAttribute("title"))) {
+        Assertions.assertEquals("jasmine-failed", result.getAttribute("class"), result.getAttribute("title"));
+      } else {
+        Assertions.assertEquals("jasmine-passed", result.getAttribute("class"), result.getAttribute("title"));
+      }
+    }
+  }
+
+  /**
+   * Call every page without a specific *.test.js and run general tests (duplicated Ids, 404, exception, ...).
+   */
+  @ParameterizedTest
+  @MethodSource("basicTestProvider")
+  void basicTest(String path, int testNumber, int testSize)
+      throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
+
+    final String timeLeft = getTimeLeft(startTime, testSize, testNumber);
+    final String host = InetAddress.getLocalHost().getHostAddress();
+    final int tomcatPort = tomcat.getFirstMappedPort();
+
+    LOG.info("(" + testNumber + "/" + testSize + " | time left: " + timeLeft + ")"
+        + " - url: http://" + host + ":" + tomcatPort + "/" + path);
+
+    final String base = path.substring(0, path.length() - 6);
+    final String url = "http://" + host + ":" + tomcatPort + "/test.xhtml?base="
+        + URLEncoder.encode(base, "UTF-8") + "&basicTest=true";
+
+    WebDriver webDriver = getWebDriver(host, seleniumChrome.getFirstMappedPort());
+    webDriver.get(url);
+
+    List<WebElement> results = getJasmineResults(webDriver);
+    parseJasmineResults(results);
+  }
+
+  private static Stream<Arguments> basicTestProvider() throws IOException {
+    final List<String> paths = Files.walk(Paths.get("src/main/webapp/content/"))
+        .filter(Files::isRegularFile)
+        .map(Path::toString)
+        .filter(s -> s.endsWith(".xhtml"))
+        .filter(s -> !s.contains("/x-"))
+        .map(s -> s.substring("src/main/webapp/".length()))
+        .sorted()
+        .collect(Collectors.toList());
+
+    for (String standardTestPath : getStandardTestPaths()) {
+      paths.remove(standardTestPath); // already tested by standard test
+    }
+
+    final int testSize = paths.size();
+
+    int testNumber = 1;
+    List<Arguments> arguments = new LinkedList<>();
+    for (String path : paths) {
+      arguments.add(Arguments.of(path, testNumber, testSize));
+      testNumber++;
+    }
+    return arguments.stream();
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendBase.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendBase.java
new file mode 100644
index 0000000..a613c8e
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendBase.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo.integration;
+
+import org.apache.commons.lang3.time.DurationFormatUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.openqa.selenium.By;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.ui.FluentWait;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+abstract class FrontendBase {
+
+  @SuppressWarnings("rawtypes") // this is how to use testcontainers
+  @Container
+  public static GenericContainer seleniumChrome =
+      new GenericContainer<>(DockerImageName.parse("selenium/standalone-chrome")).withExposedPorts(4444);
+
+  @SuppressWarnings("rawtypes") // this is how to use testcontainers
+  @Container
+  public static GenericContainer tomcat =
+      new GenericContainer(DockerImageName.parse("myfaces/tobago-example-demo")).withExposedPorts(8080);
+
+  private static WebDriver chromeDriver;
+
+  @AfterAll
+  static void tearDown() {
+    if (chromeDriver != null) {
+      chromeDriver.quit();
+    }
+  }
+
+  static List<String> getStandardTestPaths() throws IOException {
+    return Files.walk(Paths.get("src/main/webapp/content/"))
+        .filter(Files::isRegularFile)
+        .map(Path::toString)
+        .filter(s -> s.endsWith(".test.js"))
+        .map(s -> s.substring("src/main/webapp/".length()))
+        .sorted()
+        .map(s -> s.substring(0, s.length() - 8) + ".xhtml")
+        .collect(Collectors.toList());
+  }
+
+  WebDriver getWebDriver(final String host, final Integer port) throws MalformedURLException {
+    if (chromeDriver == null || ((RemoteWebDriver) chromeDriver).getSessionId() == null) {
+      chromeDriver = new RemoteWebDriver(new URL("http://" + host + ":" + port + "/wd/hub"), new ChromeOptions());
+    }
+    return chromeDriver;
+  }
+
+  List<WebElement> getJasmineResults(WebDriver webDriver) {
+    final FluentWait<WebDriver> fluentWait = new FluentWait<>(webDriver)
+        .withTimeout(Duration.ofSeconds(60))
+        .pollingEvery(Duration.ofSeconds(1))
+        .ignoring(NoSuchElementException.class);
+    fluentWait.until(driver -> driver.findElement(By.className("jasmine-overall-result")));
+
+    return webDriver.findElements(By.cssSelector(".jasmine-symbol-summary li"));
+  }
+
+  void parseJasmineResults(List<WebElement> results) {
+    Assertions.assertTrue(results.size() > 0, "no results detected");
+    for (WebElement result : results) {
+      Assertions.assertEquals("jasmine-passed", result.getAttribute("class"), result.getAttribute("title"));
+    }
+  }
+
+  String getTimeLeft(final LocalTime startTime, final int testSize, final int testNo) {
+    final LocalTime now = LocalTime.now();
+    final Duration completeWaitTime = Duration.between(startTime, now).dividedBy(testNo).multipliedBy(testSize);
+    final LocalTime endTime = LocalTime.from(startTime).plus(completeWaitTime);
+    final Duration timeLeft = Duration.between(LocalTime.now(), endTime);
+
+    if (timeLeft.toHours() > 0) {
+      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "H'h' m'm' s's'");
+    } else if (timeLeft.toMinutes() > 0) {
+      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "m'm' s's'");
+    } else if (timeLeft.toMillis() >= 0) {
+      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "s's'");
+    } else {
+      return "---";
+    }
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendTest.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendTest.java
new file mode 100644
index 0000000..db7d116
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/integration/FrontendTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.myfaces.tobago.example.demo.integration;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.time.LocalTime;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Stream;
+
+@Testcontainers
+class FrontendTest extends FrontendBase {
+
+  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  static LocalTime startTime;
+
+  @BeforeAll
+  public static void setup() {
+    startTime = LocalTime.now();
+  }
+
+  /**
+   * Call every page with a specific *.test.js.
+   */
+  @ParameterizedTest
+  @MethodSource("standardTestProvider")
+  void frontendTest(String path, int testNumber, int testSize)
+      throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
+
+    final String timeLeft = getTimeLeft(FrontendTest.startTime, testSize, testNumber);
+    final String host = InetAddress.getLocalHost().getHostAddress();
+    final int tomcatPort = tomcat.getFirstMappedPort();
+
+    LOG.info("(" + testNumber + "/" + testSize + " | time left: " + timeLeft + ")"
+        + " - url: http://" + host + ":" + tomcatPort + "/" + path);
+
+    final String base = path.substring(0, path.length() - 6);
+    final String url = "http://" + host + ":" + tomcatPort + "/test.xhtml?base=" + URLEncoder.encode(base, "UTF-8");
+
+    WebDriver webDriver = getWebDriver(host, seleniumChrome.getFirstMappedPort());
+    webDriver.get(url);
+
+    List<WebElement> results = getJasmineResults(webDriver);
+    parseJasmineResults(results);
+  }
+
+  private static Stream<Arguments> standardTestProvider() throws IOException {
+    final List<String> paths = getStandardTestPaths();
+    final int testSize = paths.size();
+
+    int testNumber = 1;
+    List<Arguments> arguments = new LinkedList<>();
+    for (String path : paths) {
+      arguments.add(Arguments.of(path, testNumber, testSize));
+      testNumber++;
+    }
+    return arguments.stream();
+  }
+}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/AccessAllPagesTest.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/AccessAllPagesTest.java
deleted file mode 100644
index 8b610f2..0000000
--- a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/AccessAllPagesTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.myfaces.tobago.example.demo.qunit;
-
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.openqa.selenium.By;
-import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.WebElement;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.lang.invoke.MethodHandles;
-import java.net.MalformedURLException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.LocalTime;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-class AccessAllPagesTest extends SeleniumBase {
-
-  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
-  /**
-   * Verify qunit test for "no exception".
-   */
-  @ParameterizedTest
-  @MethodSource("verifyNoException404TestProvider")
-  void verifyNoExceptionTest(Browser browser, String serverUrl)
-      throws MalformedURLException, UnsupportedEncodingException {
-    final String path = "error/exception.xhtml";
-    LOG.info("browser: " + browser + " - url: " + serverUrl + "/" + path);
-
-    if (isIgnored(serverUrl, path)) {
-      logIgnoreMessage(serverUrl, path);
-    } else {
-      setupWebDriver(browser, serverUrl, path, true);
-
-      final WebDriver webDriver = getWebDriver(browser);
-      waitForQUnitBanner(webDriver);
-      WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
-
-      List<WebElement> results = qunitTests.findElements(By.xpath("li"));
-      boolean testException = false;
-      for (final WebElement result : results) {
-        if ("has no exception".equals(result.findElement(By.className("test-name")).getText())) {
-          Assertions.assertEquals(result.getAttribute("class"), "fail");
-          testException = true;
-        }
-      }
-      Assertions.assertTrue(testException, "Could not verify 'has no exception' test.");
-    }
-  }
-
-  /**
-   * Verify qunit test for "no 404".
-   */
-  @ParameterizedTest
-  @MethodSource("verifyNoException404TestProvider")
-  void verifyNo404Test(Browser browser, String serverUrl) throws MalformedURLException, UnsupportedEncodingException {
-    final String path = "error/404.xhtml";
-    LOG.info("browser: " + browser + " - url: " + serverUrl + "/" + path);
-
-    if (isIgnored(serverUrl, path)) {
-      logIgnoreMessage(serverUrl, path);
-    } else {
-      setupWebDriver(browser, serverUrl, path, true);
-
-      final WebDriver webDriver = getWebDriver(browser);
-      waitForQUnitBanner(webDriver);
-      WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
-
-      List<WebElement> results = qunitTests.findElements(By.xpath("li"));
-      boolean test404 = false;
-      for (final WebElement result : results) {
-        if ("has no 404".equals(result.findElement(By.className("test-name")).getText())) {
-          Assertions.assertEquals(result.getAttribute("class"), "fail");
-          test404 = true;
-        }
-      }
-      Assertions.assertTrue(test404, "Could not verify 'has no 404' test.");
-    }
-  }
-
-  private static Stream<Arguments> verifyNoException404TestProvider() throws IOException {
-    List<Arguments> arguments = new LinkedList<>();
-
-    for (Browser browser : Browser.values()) {
-      for (String serverUrl : getServerUrls()) {
-        arguments.add(Arguments.of(browser, serverUrl));
-      }
-    }
-
-    return arguments.stream();
-  }
-
-  @ParameterizedTest
-  @MethodSource("accessAllPagesProvider")
-  void testAccessAllPages(Browser browser, String serverUrl, String path, LocalTime startTime, int testSize, int testNo)
-      throws MalformedURLException, UnsupportedEncodingException {
-
-    double percent = 100 * (double) testNo / testSize;
-    final String timeLeft = getTimeLeft(startTime, testSize, testNo);
-
-    LOG.info("(" + String.format("%.2f", percent) + " % complete" + " | time left: " + timeLeft + ")"
-        + " browser: " + browser + " - url: " + serverUrl + "/" + path);
-
-    if (isIgnored(serverUrl, path)) {
-      logIgnoreMessage(serverUrl, path);
-    } else {
-      setupWebDriver(browser, serverUrl, path, true);
-      parseQUnitResults(browser, serverUrl, path);
-    }
-  }
-
-  private static Stream<Arguments> accessAllPagesProvider() throws IOException {
-    final List<String> paths = Files.walk(Paths.get("src/main/webapp/content/"))
-        .filter(Files::isRegularFile)
-        .map(Path::toString)
-        .filter(s -> s.endsWith(".xhtml"))
-        .filter(s -> !s.contains("/x-"))
-        .map(s -> s.substring("src/main/webapp/".length()))
-        .sorted()
-        .collect(Collectors.toList());
-
-    for (String standardTestPath : getStandardTestPaths()) {
-      paths.remove(standardTestPath); // already tested by standard test
-    }
-
-    return getArguments(paths);
-  }
-}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/SeleniumBase.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/SeleniumBase.java
deleted file mode 100644
index d025d93..0000000
--- a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/SeleniumBase.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.myfaces.tobago.example.demo.qunit;
-
-import org.apache.commons.lang3.time.DurationFormatUtils;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.params.provider.Arguments;
-import org.openqa.selenium.By;
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.WebElement;
-import org.openqa.selenium.chrome.ChromeOptions;
-import org.openqa.selenium.remote.RemoteWebDriver;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-import org.openqa.selenium.support.ui.FluentWait;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.lang.invoke.MethodHandles;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.net.UnknownHostException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.LocalTime;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-abstract class SeleniumBase {
-
-  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
-  private static WebDriver chromeDriver;
-  private static List<String> serverUrls = new ArrayList<>();
-  private static Map<String, String> ignores = new HashMap<>();
-
-  @BeforeAll
-  static void setUp() {
-    ignores.put(":8083/tobago-example-demo-myfaces-2.3",
-        "MyFaces 2.3 don't work with Tomcat 8.5 and openjdk10");
-    ignores.put("tobago-example-demo-mojarra-2.0",
-        "Ajax events don't work with Mojarra 2.0: https://issues.apache.org/jira/browse/TOBAGO-1589");
-    ignores.put("tobago-example-demo-mojarra-2.3",
-        "Currently Tobago demo don't run with Mojarra 2.3 on Tomcat 8.5");
-
-    ignores.put("content/40-test/6000-event/event.xhtml",
-        "Focus/blur event can only be fired if the browser window is in foreground."
-            + " This cannot be guaranteed in selenium tests."
-            + " event.test.js contain focus/blur events");
-
-    final String tobago1910 = "TreeSelect: Single selection nodes are not deselected correctly with mojarra: "
-        + "https://issues.apache.org/jira/browse/TOBAGO-1910";
-    ignores.put("tobago-example-demo-mojarra-2.1/content/20-component/090-tree/01-select/tree-select.xhtml",
-        tobago1910);
-    ignores.put("tobago-example-demo-mojarra-2.2/content/20-component/090-tree/01-select/tree-select.xhtml",
-        tobago1910);
-  }
-
-  @AfterAll
-  static void tearDown() {
-    if (chromeDriver != null) {
-      chromeDriver.quit();
-    }
-  }
-
-  enum Browser {
-    chrome
-    //, firefox // TODO implement firefox
-  }
-
-  static List<String> getServerUrls() throws UnknownHostException, MalformedURLException {
-    if (serverUrls.size() <= 0) {
-      final String hostAddress = InetAddress.getLocalHost().getHostAddress();
-
-      List<String> ports = new ArrayList<>();
-      ports.add("8082"); // Tomcat JRE 8
-      ports.add("8083"); // Tomcat JRE 10
-
-      List<String> contextPaths = new ArrayList<>();
-      contextPaths.add("tobago-example-demo"); // MyFaces 2.0
-      contextPaths.add("tobago-example-demo-myfaces-2.1");
-      contextPaths.add("tobago-example-demo-myfaces-2.2");
-      contextPaths.add("tobago-example-demo-myfaces-2.3");
-      contextPaths.add("tobago-example-demo-mojarra-2.0");
-      contextPaths.add("tobago-example-demo-mojarra-2.1");
-      contextPaths.add("tobago-example-demo-mojarra-2.2");
-      contextPaths.add("tobago-example-demo-mojarra-2.3");
-
-      for (String port : ports) {
-        for (String contextPath : contextPaths) {
-          String url = "http://" + hostAddress + ":" + port + "/" + contextPath;
-          final int status = getStatus(url);
-          if (status == 200) {
-            serverUrls.add(url);
-          } else {
-            LOG.warn("\n⚠️ IGNORED: Tests for " + url + ":\n Server status: " + status);
-          }
-        }
-      }
-    }
-
-    return serverUrls;
-  }
-
-  private static int getStatus(String url) throws MalformedURLException {
-    URL siteURL = new URL(url);
-    try {
-      HttpURLConnection connection = (HttpURLConnection) siteURL.openConnection();
-      connection.setRequestMethod("GET");
-      connection.connect();
-      return connection.getResponseCode();
-    } catch (IOException e) {
-      return -1;
-    }
-  }
-
-  static List<String> getStandardTestPaths() throws IOException {
-    return Files.walk(Paths.get("src/main/webapp/content/"))
-        .filter(Files::isRegularFile)
-        .map(Path::toString)
-        .filter(s -> s.endsWith(".test.js"))
-        .map(s -> s.substring("src/main/webapp/".length()))
-        .sorted()
-        .map(s -> s.substring(0, s.length() - 8) + ".xhtml")
-        .collect(Collectors.toList());
-  }
-
-  boolean isIgnored(final String serverUrl, final String path) {
-    final String url = serverUrl + "/" + path;
-    for (String key : ignores.keySet()) {
-      if (url.contains(key)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  void logIgnoreMessage(final String serverUrl, final String path) {
-    final String url = serverUrl + "/" + path;
-    for (final Map.Entry<String, String> ignore : ignores.entrySet()) {
-      if (url.contains(ignore.getKey())) {
-        final String message = ignore.getValue();
-        LOG.info("\n⚠️ IGNORED: Test for " + url + ":\n" + message);
-        return;
-      }
-    }
-  }
-
-  void setupWebDriver(final Browser browser, final String serverUrl, final String path, final boolean accessTest)
-      throws MalformedURLException, UnsupportedEncodingException {
-    if (Browser.chrome.equals(browser)
-        && (chromeDriver == null) || ((RemoteWebDriver) chromeDriver).getSessionId() == null) {
-      chromeDriver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), new ChromeOptions());
-    }
-
-    final String base = path.substring(0, path.length() - 6);
-    final String url = serverUrl + "/test.xhtml?base="
-        + URLEncoder.encode(base, "UTF-8") + (accessTest ? "&accessTest=true" : "");
-    getWebDriver(browser).get(url);
-  }
-
-  WebDriver getWebDriver(Browser browser) {
-    if (Browser.chrome.equals(browser)) {
-      return chromeDriver;
-    } else {
-      return null;
-    }
-  }
-
-  static Stream<Arguments> getArguments(final List<String> paths) throws MalformedURLException, UnknownHostException {
-    final LocalTime startTime = LocalTime.now();
-    final int testSize = Browser.values().length * getServerUrls().size() * paths.size();
-
-    List<Arguments> arguments = new LinkedList<>();
-
-    int testNo = 1;
-    for (String serverUrl : getServerUrls()) {
-      for (String path : paths) {
-        for (Browser browser : Browser.values()) {
-          arguments.add(Arguments.of(browser, serverUrl, path, startTime, testSize, testNo));
-          testNo++;
-        }
-      }
-    }
-
-    return arguments.stream();
-  }
-
-  String getTimeLeft(final LocalTime startTime, final int testSize, final int testNo) {
-    final LocalTime now = LocalTime.now();
-    final Duration completeWaitTime = Duration.between(startTime, now).dividedBy(testNo).multipliedBy(testSize);
-    final LocalTime endTime = LocalTime.from(startTime).plus(completeWaitTime);
-    final Duration timeLeft = Duration.between(LocalTime.now(), endTime);
-
-    if (timeLeft.toHours() > 0) {
-      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "H'h' m'm' s's'");
-    } else if (timeLeft.toMinutes() > 0) {
-      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "m'm' s's'");
-    } else if (timeLeft.toMillis() >= 0) {
-      return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "s's'");
-    } else {
-      return "---";
-    }
-  }
-
-  /**
-   * Wait for the qunit-banner web element and return it.
-   * If the web element is available, the execution of qunit test should be done and it is safe to parse the results.
-   *
-   * @return qunit-banner web element
-   */
-  WebElement waitForQUnitBanner(final WebDriver webDriver) {
-    final FluentWait<WebDriver> fluentWait = new FluentWait<>(webDriver)
-        .withTimeout(Duration.ofSeconds(90))
-        .pollingEvery(Duration.ofSeconds(1))
-        .ignoring(NoSuchElementException.class);
-
-    WebElement qunitBanner = fluentWait.until(driver -> driver.findElement(By.id("qunit-banner")));
-    fluentWait.until(ExpectedConditions.attributeToBeNotEmpty(qunitBanner, "class"));
-
-    return qunitBanner;
-  }
-
-  void parseQUnitResults(final Browser browser, final String serverUrl, final String path) {
-    final WebDriver webDriver = getWebDriver(browser);
-    WebElement qunitBanner;
-    try {
-      qunitBanner = waitForQUnitBanner(webDriver);
-    } catch (Exception e) {
-      qunitBanner = webDriver.findElement(By.id("qunit-banner"));
-    }
-
-    WebElement qunitTestResult = webDriver.findElement(By.id("qunit-testresult"));
-    WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
-
-    final List<WebElement> testCases = qunitTests.findElements(By.xpath("li"));
-    Assertions.assertTrue(testCases.size() > 0, "There must be at least one test case.");
-
-    final boolean testFailed = !qunitBanner.getAttribute("class").equals("qunit-pass");
-
-    int testCaseCount = 1;
-    final StringBuilder stringBuilder = new StringBuilder();
-    stringBuilder.append(qunitTestResult.getAttribute("textContent"));
-    stringBuilder.append("\n");
-
-    if (testFailed) {
-      for (final WebElement testCase : testCases) {
-        final String testName = getText(testCase, "test-name");
-        final String testStatus = testCase.getAttribute("class").toUpperCase();
-
-        stringBuilder.append(testCaseCount++);
-        stringBuilder.append(". ");
-        stringBuilder.append(testStatus);
-        stringBuilder.append(": ");
-        stringBuilder.append(testName);
-        stringBuilder.append(" (");
-        stringBuilder.append(getText(testCase, "runtime"));
-        stringBuilder.append(")\n");
-
-        final WebElement assertList = testCase.findElement(By.className("qunit-assert-list"));
-        final List<WebElement> asserts = assertList.findElements(By.tagName("li"));
-        int assertCount = 1;
-        for (final WebElement assertion : asserts) {
-          final String assertStatus = assertion.getAttribute("class");
-
-          stringBuilder.append("- ");
-          if (assertCount <= 9) {
-            stringBuilder.append("0");
-          }
-          stringBuilder.append(assertCount++);
-          stringBuilder.append(". ");
-          stringBuilder.append(assertStatus);
-          stringBuilder.append(": ");
-          stringBuilder.append(getText(assertion, "test-message"));
-          stringBuilder.append(getText(assertion, "runtime"));
-          stringBuilder.append("\n");
-
-          final String assertExpected = getText(assertion, "test-expected");
-          if (!"null".equals(assertExpected)) {
-            stringBuilder.append("-- ");
-            stringBuilder.append(assertExpected);
-            stringBuilder.append("\n");
-          }
-          final String assertResult = getText(assertion, "test-actual");
-          if (!"null".equals(assertResult)) {
-            stringBuilder.append("-- ");
-            stringBuilder.append(assertResult);
-            stringBuilder.append("\n");
-          }
-          final String assertSource = getText(assertion, "test-source");
-          if (!"null".equals(assertSource)) {
-            stringBuilder.append("-- ");
-            stringBuilder.append(assertSource);
-            stringBuilder.append("\n");
-          }
-        }
-
-        stringBuilder.append(getText(testCase, "qunit-source"));
-        stringBuilder.append("\n\n");
-      }
-    }
-
-    final String url = serverUrl + "/" + path;
-    if (testFailed) {
-      final String message = "\n❌ FAILED: Test with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
-      LOG.warn(message);
-      Assertions.fail(message);
-    } else {
-      final String message = "\n✅ PASSED: Test with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
-      LOG.info(message);
-      Assertions.assertTrue(true, message);
-    }
-  }
-
-  private String getText(final WebElement webElement, final String className) {
-    final List<WebElement> elements = webElement.findElements(By.className(className));
-    if (elements.size() > 0) {
-      return elements.get(0).getAttribute("textContent");
-    } else {
-      return "null";
-    }
-  }
-}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/StandardTest.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/StandardTest.java
deleted file mode 100644
index b174f0a..0000000
--- a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/StandardTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.myfaces.tobago.example.demo.qunit;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.lang.invoke.MethodHandles;
-import java.net.MalformedURLException;
-import java.net.UnknownHostException;
-import java.time.LocalTime;
-import java.util.List;
-import java.util.stream.Stream;
-
-class StandardTest extends SeleniumBase {
-
-  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
-  /**
-   * To test only a singe page, just change browser setup, 'portContextPath' and/or 'path'.
-   * Start the docker container with mvn -Pdocker-qunit-tests docker:start
-   */
-  @Test
-  void testSinglePage() throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
-    final Browser browser = Browser.chrome;
-    final String serverUrl = getServerUrls().get(0);
-    final String path = "content/10-intro/intro.xhtml";
-
-    LOG.info("browser: " + browser + " - url: " + serverUrl + "/" + path);
-
-    setupWebDriver(browser, serverUrl, path, false);
-    parseQUnitResults(browser, serverUrl, path);
-  }
-
-  @ParameterizedTest
-  @MethodSource("standardTestProvider")
-  void testStandard(Browser browser, String serverUrl, String path, LocalTime startTime, int testSize, int testNo)
-      throws MalformedURLException, UnsupportedEncodingException {
-
-    double percent = 100 * (double) testNo / testSize;
-    final String timeLeft = getTimeLeft(startTime, testSize, testNo);
-
-    LOG.info("(" + String.format("%.2f", percent) + " % complete" + " | time left: " + timeLeft + ")"
-        + " browser: " + browser + " - url: " + serverUrl + "/" + path);
-
-    if (isIgnored(serverUrl, path)) {
-      logIgnoreMessage(serverUrl, path);
-    } else {
-      setupWebDriver(browser, serverUrl, path, false);
-      parseQUnitResults(browser, serverUrl, path);
-    }
-  }
-
-  private static Stream<Arguments> standardTestProvider() throws IOException {
-    final List<String> paths = getStandardTestPaths();
-    return getArguments(paths);
-  }
-}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/junit/rules/TestRule.java b/tobago-example/tobago-example-demo/src/test/java/org/junit/rules/TestRule.java
new file mode 100644
index 0000000..f0f6c69
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/junit/rules/TestRule.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.junit.rules;
+
+/*
+ * the fake class is needed for testcontainers, because of an dependency to junit4
+ * https://github.com/testcontainers/testcontainers-java/issues/970
+ */
+@SuppressWarnings("unused")
+public interface TestRule {
+}