You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2018/05/24 17:15:12 UTC

[myfaces-tobago] branch master updated: QUnitTests reimplemented

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9dcacb5  QUnitTests reimplemented
9dcacb5 is described below

commit 9dcacb5fca466a23b6073276a28626ffbed0558c
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Thu May 24 19:15:02 2018 +0200

    QUnitTests reimplemented
    
    * former qunit-integration-tests are reimplemented with junit5, selenium-grid and docker.
    ** to start the tests, docker is needed.
    ** to run the tests: mvn -Pdocker-qunit-tests
    ** to just start docker container: mvn -Pdocker-qunit-tests docker:start
    ** to stop docker container: mvn -Pdocker-qunit-tests docker:stop
    * arquillian dependencies removed (including ArquillianTest example)
    * simple test for intro.xhtml added
    * 'wait for test' updated, so testing 'error/exception.xhtml' and 'error/404.xhtml' is possible
---
 pom.xml                                            |  22 +-
 tobago-example/tobago-example-demo/pom.xml         | 239 +++++---
 .../src/main/webapp/content/10-intro/intro.test.js |  24 +
 .../src/main/webapp/script/tobago-test.js          |   3 +-
 .../tobago/example/demo/ArquillianTest.java        | 106 ----
 .../myfaces/tobago/example/demo/QUnitTests.java    | 600 ---------------------
 .../example/demo/qunit/AccessAllPagesTest.java     | 169 ++++++
 .../tobago/example/demo/qunit/SeleniumBase.java    | 188 +++++++
 .../tobago/example/demo/qunit/StandardTest.java    | 100 ++++
 9 files changed, 656 insertions(+), 795 deletions(-)

diff --git a/pom.xml b/pom.xml
index 94252b0..c708d5c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -438,6 +438,18 @@
         <scope>test</scope>
       </dependency>
       <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-engine</artifactId>
+        <version>5.2.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.junit.jupiter</groupId>
+        <artifactId>junit-jupiter-params</artifactId>
+        <version>5.2.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
         <groupId>org.apache.portals</groupId>
         <artifactId>portlet-api_2.0_spec</artifactId>
         <version>1.0</version>
@@ -500,6 +512,14 @@
       <artifactId>junit</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+    </dependency>
+    <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
       <scope>test</scope>
@@ -810,7 +830,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-failsafe-plugin</artifactId>
-          <version>2.20.1</version>
+          <version>2.21.0</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
diff --git a/tobago-example/tobago-example-demo/pom.xml b/tobago-example/tobago-example-demo/pom.xml
index 982fd2a..55f7fd1 100644
--- a/tobago-example/tobago-example-demo/pom.xml
+++ b/tobago-example/tobago-example-demo/pom.xml
@@ -34,10 +34,7 @@
   <properties>
     <openwebbeans.version>2.0.4</openwebbeans.version>
     <deltaspike.version>1.8.1</deltaspike.version>
-    <!--<arquillian.browser>firefox</arquillian.browser>-->
-    <!-- phantomjs works not with CSP (report-only) -->
-    <arquillian.browser>phantomjs</arquillian.browser>
-    <selenium.version>3.7.1</selenium.version>
+    <selenium.version>3.12.0</selenium.version>
     <tomee.version>7.0.4</tomee.version>
     <meecrowave.version>1.2.1</meecrowave.version>
     <tomcat.version>9.0.5</tomcat.version>
@@ -139,81 +136,20 @@
       <plugin>
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
-          <excludes><!-- TODO define group for ArquillianTests and use excludeGroups -->
-            <exclude>**/ArquillianTest.java</exclude>
+          <excludes>
+            <exclude>qunit/*</exclude>
           </excludes>
         </configuration>
       </plugin>
-      <plugin>
-        <artifactId>maven-failsafe-plugin</artifactId>
-        <configuration>
-          <includes><!-- TODO define group for ArquillianTests or use convention *IT.java, *ITCase.java -->
-            <include>**/ArquillianTest.java</include>
-          </includes>
-        </configuration>
-      </plugin>
-      <plugin>
-        <groupId>io.fabric8</groupId>
-        <artifactId>docker-maven-plugin</artifactId>
-        <version>0.22.1</version>
-        <configuration>
-          <images>
-            <image>
-              <name>selenium/standalone-firefox:${selenium.version}</name>
-              <run>
-                <ports>
-                  <port>4444:4444</port>
-                </ports>
-              </run>
-            </image>
-          </images>
-        </configuration>
-      </plugin>
     </plugins>
   </build>
 
   <dependencyManagement>
     <dependencies>
       <dependency>
-        <groupId>org.jboss.arquillian</groupId>
-        <artifactId>arquillian-bom</artifactId>
-        <version>1.1.15.Final</version>
-        <scope>import</scope>
-        <type>pom</type>
-      </dependency>
-      <dependency>
-        <groupId>org.jboss.arquillian.selenium</groupId>
-        <artifactId>selenium-bom</artifactId>
+        <groupId>org.seleniumhq.selenium</groupId>
+        <artifactId>selenium-java</artifactId>
         <version>${selenium.version}</version>
-        <type>pom</type>
-        <scope>import</scope>
-      </dependency>
-      <dependency>
-        <groupId>org.jboss.arquillian.extension</groupId>
-        <artifactId>arquillian-drone-bom</artifactId>
-        <version>2.4.5</version>
-        <scope>import</scope>
-        <type>pom</type>
-      </dependency>
-      <!-- this is needed for arquillian drone (QUnitTests) -->
-      <dependency>
-        <groupId>commons-codec</groupId>
-        <artifactId>commons-codec</artifactId>
-        <version>1.11</version>
-        <!-- scope must not be "test", because myfaces-impl also uses commons-codec -->
-      </dependency>
-      <dependency>
-        <groupId>org.jboss.arquillian.graphene</groupId>
-        <artifactId>graphene-webdriver</artifactId>
-        <version>2.3.2</version>
-        <type>pom</type>
-        <scope>test</scope>
-        <exclusions>
-          <exclusion>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-          </exclusion>
-        </exclusions>
       </dependency>
       <dependency>
         <groupId>com.google.guava</groupId>
@@ -278,24 +214,8 @@
       <version>4.4.8</version>
     </dependency>
     <dependency>
-      <groupId>org.jboss.arquillian.junit</groupId>
-      <artifactId>arquillian-junit-container</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.jboss.shrinkwrap.resolver</groupId>
-      <artifactId>shrinkwrap-resolver-api-maven-archive</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.jboss.shrinkwrap.resolver</groupId>
-      <artifactId>shrinkwrap-resolver-impl-maven-archive</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.jboss.arquillian.graphene</groupId>
-      <artifactId>graphene-webdriver</artifactId>
-      <type>pom</type>
+      <groupId>org.seleniumhq.selenium</groupId>
+      <artifactId>selenium-java</artifactId>
       <scope>test</scope>
     </dependency>
   </dependencies>
@@ -865,6 +785,151 @@
     </profile>
 
     <profile>
+      <id>docker-qunit-tests</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <version>0.25.2</version>
+            <configuration>
+              <images>
+                <image>
+                  <name>tomcat:8.5-jre8</name>
+                  <run>
+                    <ports>
+                      <port>8081:8080</port>
+                    </ports>
+                    <volumes>
+                      <bind>
+                        <volume>
+                          ${project.build.directory}/${project.artifactId}:/usr/local/tomcat/webapps/${project.artifactId}
+                        </volume>
+                      </bind>
+                    </volumes>
+                    <wait>
+                      <http>
+                        <url>http://localhost:8081/${project.artifactId}</url>
+                      </http>
+                      <time>60000</time>
+                    </wait>
+                  </run>
+                </image>
+                <image>
+                  <alias>selenium-hub</alias>
+                  <name>selenium/hub:${selenium.version}</name>
+                  <run>
+                    <ports>
+                      <port>4444:4444</port>
+                    </ports>
+                    <wait>
+                      <http>
+                        <url>http://localhost:4444</url>
+                        <status>200</status>
+                      </http>
+                      <time>20000</time>
+                    </wait>
+                  </run>
+                </image>
+                <image>
+                  <name>selenium/node-chrome:${selenium.version}</name>
+                  <run>
+                    <dependsOn>
+                      <container>selenium-hub</container>
+                    </dependsOn>
+                    <links>
+                      <link>selenium-hub</link>
+                    </links>
+                    <env>
+                      <HUB_HOST>selenium-hub</HUB_HOST>
+                      <HOB_PORT>4444</HOB_PORT>
+                    </env>
+                    <ports>
+                      <port>5556:5555</port>
+                    </ports>
+                    <wait>
+                      <http>
+                        <url>http://localhost:5556</url>
+                        <status>200</status>
+                      </http>
+                      <time>20000</time>
+                    </wait>
+                  </run>
+                </image>
+                <image>
+                  <name>selenium/node-firefox:${selenium.version}</name>
+                  <run>
+                    <dependsOn>
+                      <container>selenium-hub</container>
+                    </dependsOn>
+                    <links>
+                      <link>selenium-hub</link>
+                    </links>
+                    <env>
+                      <HUB_HOST>selenium-hub</HUB_HOST>
+                      <HOB_PORT>4444</HOB_PORT>
+                    </env>
+                    <ports>
+                      <port>5557:5555</port>
+                    </ports>
+                    <wait>
+                      <http>
+                        <url>http://localhost:5557</url>
+                        <status>200</status>
+                      </http>
+                      <time>20000</time>
+                    </wait>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+            <executions>
+              <execution>
+                <id>start-container</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                  <goal>build</goal>
+                  <goal>start</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop-container</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <dependencies>
+              <dependency>
+                <groupId>org.junit.platform</groupId>
+                <artifactId>junit-platform-surefire-provider</artifactId>
+                <version>1.2.0</version>
+              </dependency>
+            </dependencies>
+            <configuration>
+              <includes>
+                <include>qunit/*</include>
+              </includes>
+            </configuration>
+            <executions>
+              <execution>
+                <phase>integration-test</phase>
+                <goals>
+                  <goal>integration-test</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <profile>
       <id>qunit-integration-tests</id>
       <build>
         <plugins>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/10-intro/intro.test.js b/tobago-example/tobago-example-demo/src/main/webapp/content/10-intro/intro.test.js
new file mode 100644
index 0000000..d2e6110
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/10-intro/intro.test.js
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+QUnit.test("First section title is 'Intro'", function (assert) {
+  assert.expect(1);
+
+  var titleOfFirstSectionHeader = jQueryFrame(".tobago-section-header:first span").text();
+
+  assert.equal(titleOfFirstSectionHeader, "Intro");
+});
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/script/tobago-test.js b/tobago-example/tobago-example-demo/src/main/webapp/script/tobago-test.js
index 2829cf5..3974cfa 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/script/tobago-test.js
+++ b/tobago-example/tobago-example-demo/src/main/webapp/script/tobago-test.js
@@ -33,7 +33,8 @@ QUnit.test("wait for test", function (assert) {
   var waitingDone = false;
   var interval = setInterval(function () {
     contentWindowName = document.getElementById("page:testframe").contentWindow.name;
-    waitingDone = contentWindowName !== "page:testframe" && contentWindowName !== "ds-tempWindowId";
+    waitingDone = (contentWindowName !== "page:testframe" && contentWindowName !== "ds-tempWindowId")
+        || new RegExp('[\?&]base=([^&#]*)').exec(window.location.href)[1].indexOf("error%2F") === 0;
     if (new Date().getTime() - startTime >= 20000 || waitingDone) {
       clearInterval(interval);
       assert.ok(waitingDone);
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/ArquillianTest.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/ArquillianTest.java
deleted file mode 100644
index 7f2a74c..0000000
--- a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/ArquillianTest.java
+++ /dev/null
@@ -1,106 +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;
-
-import org.jboss.arquillian.container.test.api.Deployment;
-import org.jboss.arquillian.container.test.api.RunAsClient;
-import org.jboss.arquillian.drone.api.annotation.Drone;
-import org.jboss.arquillian.graphene.findby.FindByJQuery;
-import org.jboss.arquillian.junit.Arquillian;
-import org.jboss.arquillian.test.api.ArquillianResource;
-import org.jboss.shrinkwrap.api.ShrinkWrap;
-import org.jboss.shrinkwrap.api.spec.WebArchive;
-import org.jboss.shrinkwrap.resolver.api.maven.archive.importer.MavenImporter;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.openqa.selenium.WebDriver;
-import org.openqa.selenium.WebElement;
-import org.openqa.selenium.support.FindBy;
-
-import java.io.File;
-import java.net.URL;
-
-/**
- * if PhantomJS is configured, ensure that content-security-policy mode="off" is in tobago-config.xml otherwise
- * it will result in "Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed
- * source of script in the following Content Security Policy directive"
- * see: https://github.com/ariya/phantomjs/issues/13114
- */
-@RunWith(Arquillian.class)
-@RunAsClient
-public class ArquillianTest {
-
-  @Drone
-  private WebDriver browser;
-
-  @ArquillianResource
-  private URL contextPath;
-
-  @FindBy(id = "page:iajax::field")
-  private WebElement input;
-
-  @FindBy(id = "page:oajax")
-  private WebElement output;
-
-  @FindByJQuery("#page\\:oajax > label")
-  private WebElement outputLabel;
-
-  @FindByJQuery("#page\\:oajax > span")
-  private WebElement outputField;
-
-  @Deployment(testable = false)
-  public static WebArchive createDeployment() {
-    final File pom = new File("tobago-example/tobago-example-demo/pom.xml");
-    final WebArchive webArchive = ShrinkWrap.create(MavenImporter.class).
-            loadPomFromFile(pom, "jsf-provided", "!myfaces-2.0").importBuildOutput()
-            .as(WebArchive.class);
-    // XXX there should be a proper profile in POM for that
-    webArchive.delete("/WEB-INF/lib/hibernate-validator-4.3.2.Final.jar");
-    return webArchive;
-  }
-
-  @Test
-  public void ajaxOnChange() {
-    browser.get(contextPath + "/content/20-component/010-input/10-in/in.xhtml");
-
-    Assert.assertEquals("On Server", outputLabel.getText());
-    Assert.assertEquals("", outputField.getText());
-
-    final String text = "Testtext";
-    input.sendKeys(text);
-    output.click();
-//    Graphene.waitAjax();
-/*
-    try {
-      Thread.sleep(30000);
-    } catch (InterruptedException e) {
-      e.printStackTrace();
-    }
-*/
-
-    Assert.assertEquals(text, outputField.getText());
-
-
-//    JavascriptExecutor js = (JavascriptExecutor) browser;
-//    js.executeScript("$(Tobago.Utils.escapeClientId('{}')).blur();", output.getAttribute("id"));
-
-  }
-}
diff --git a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/QUnitTests.java b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/QUnitTests.java
deleted file mode 100644
index 5b8edf4..0000000
--- a/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/QUnitTests.java
+++ /dev/null
@@ -1,600 +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;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.jboss.arquillian.container.test.api.Deployment;
-import org.jboss.arquillian.container.test.api.RunAsClient;
-import org.jboss.arquillian.drone.api.annotation.Drone;
-import org.jboss.arquillian.graphene.findby.FindByJQuery;
-import org.jboss.arquillian.junit.Arquillian;
-import org.jboss.arquillian.test.api.ArquillianResource;
-import org.jboss.shrinkwrap.api.ShrinkWrap;
-import org.jboss.shrinkwrap.api.spec.WebArchive;
-import org.jboss.shrinkwrap.resolver.api.maven.archive.importer.MavenImporter;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-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.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-
-@RunWith(Arquillian.class)
-@RunAsClient
-public class QUnitTests {
-
-  private static final Logger LOG = LoggerFactory.getLogger(QUnitTests.class);
-  private static List<String> testedPages = new LinkedList<>();
-
-  @Drone
-  private WebDriver browser;
-  @ArquillianResource
-  private URL contextPath;
-  @FindByJQuery("#qunit-testresult")
-  private WebElement qunitTestresult;
-  @FindByJQuery("#qunit-tests")
-  private WebElement qunitTests;
-
-  @Deployment
-  public static WebArchive createDeployment() {
-    WebArchive webArchive;
-    try {
-      webArchive = createWebArchive(new File("tobago-example/tobago-example-demo/pom.xml"));
-    } catch (final Exception e) {
-      webArchive = createWebArchive(new File("pom.xml")); // Jenkins
-    }
-    return disableCSP(webArchive);
-  }
-
-  private static WebArchive createWebArchive(final File pom) {
-    final WebArchive webArchive = ShrinkWrap.create(MavenImporter.class).
-        loadPomFromFile(pom, "jsf-provided", "!myfaces-2.0").importBuildOutput()
-        .as(WebArchive.class);
-    // XXX there should be a proper profile in POM for that
-    webArchive.delete("/WEB-INF/lib/hibernate-validator-4.3.2.Final.jar");
-    return webArchive;
-  }
-
-  private static WebArchive disableCSP(final WebArchive webArchive) {
-    try {
-      final InputStream inputStream = webArchive.get("WEB-INF/tobago-config.xml").getAsset().openStream();
-
-      final StringWriter stringWriter = new StringWriter();
-      IOUtils.copy(inputStream, stringWriter, Charset.defaultCharset());
-      final String modifiedTobagoConfig = stringWriter.toString()
-          .replace("<content-security-policy mode=\"on\">", "<content-security-policy mode=\"off\">");
-
-      final File tobagoConfigXml = new File("target/tobago-config.xml");
-      FileUtils.writeStringToFile(tobagoConfigXml, modifiedTobagoConfig, Charset.defaultCharset());
-      webArchive.addAsWebInfResource(tobagoConfigXml);
-    } catch (final IOException e) {
-      LOG.error("could not modify tobago-config.xml", e);
-    }
-    return webArchive;
-  }
-
-  private void setupBrowser(final String base, final boolean accessTest) throws UnsupportedEncodingException {
-    LOG.info("setup browser for: " + base + ".xhtml | accessTest=" + accessTest);
-    browser.get(contextPath + "test.xhtml?base=" + URLEncoder.encode(base, "UTF-8")
-        + (accessTest ? "&accessTest=true" : ""));
-  }
-
-  private void runStandardTest(final String page) throws UnsupportedEncodingException, InterruptedException {
-    testedPages.add(page);
-
-    if (!ignorePages().contains(page)) {
-      final String base = page.substring(0, page.length() - 6);
-      setupBrowser(base, false);
-
-      checkQUnitResults(page);
-    }
-  }
-
-  private void checkQUnitResults(final String page) throws InterruptedException {
-    final boolean timeout = waitForTest(page);
-    final List<WebElement> testCases = qunitTests.findElements(By.xpath("li"));
-    Assert.assertTrue("There must be at least one test case.", testCases.size() > 0);
-
-    final String textContent = qunitTests.getAttribute("textContent");
-
-    final boolean testFailed = timeout || (textContent != null
-        && textContent.contains(" msfailed@ ") || textContent.contains("Expected:") || textContent.contains("Result:"));
-
-    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");
-      }
-    }
-
-    if (testFailed) {
-      final String message = "Tests for " + page + " FAILED:\n" + stringBuilder.toString();
-      LOG.warn(message);
-      Assert.fail(message);
-    } else {
-      final String message = "Tests for " + page + " PASSED:\n" + stringBuilder.toString();
-      LOG.info(message);
-      Assert.assertTrue(message, true);
-    }
-  }
-
-  private boolean waitForTest(final String page) throws InterruptedException {
-    final long endTime = System.currentTimeMillis() + 90000;
-    String lastStatus = null;
-    while (System.currentTimeMillis() < endTime) {
-      try {
-        final String status = qunitTestresult.getAttribute("textContent");
-        if (status == null) {
-          LOG.info(page + " status=null");
-        } else if (!status.equals(lastStatus)) {
-          lastStatus = status;
-          LOG.info(page + " " + status + " (" + (endTime - System.currentTimeMillis()) + "ms left)");
-          if (status.startsWith("Tests completed")) {
-            return false;
-          }
-        }
-      } catch (final Exception e) {
-        Thread.sleep(200);
-      }
-    }
-    LOG.warn(page + " timeout");
-    return true;
-  }
-
-  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";
-    }
-  }
-
-  @AfterClass
-  public static void checkMissingTests() {
-    if (testedPages.size() > 1) {
-      final List<String> pages = getXHTMLs();
-      int testablePagesCount = 0;
-
-      final StringBuilder stringBuilder = new StringBuilder();
-      for (final String page : pages) {
-        if (new File(page.substring(0, page.length() - 6) + ".test.js").exists()) {
-          testablePagesCount++;
-
-          final String pathForBrowser = page.substring(page.indexOf("src/main/webapp/") + "src/main/webapp/".length());
-          if (!testedPages.contains(pathForBrowser)) {
-            final String errorString = "missing testmethod for " + pathForBrowser;
-            LOG.warn(errorString);
-            stringBuilder.append("\n");
-            stringBuilder.append(errorString);
-          }
-        }
-      }
-      Assert.assertEquals(stringBuilder.toString(), testablePagesCount, testedPages.size());
-    }
-  }
-
-  @Test
-  public void testAccessAllPages() throws UnsupportedEncodingException, InterruptedException {
-    final List<String> pages = getXHTMLs();
-    List<WebElement> results;
-
-    // Test if 'has no exception' test is correct.
-    setupBrowser("error/exception", true);
-    final boolean timeoutException = waitForTest("error/exception");
-    Assert.assertFalse("Could not verify 'has no exception' test: TIMEOUT", timeoutException);
-    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())) {
-        Assert.assertEquals(result.getAttribute("class"), "fail");
-        testException = true;
-      }
-    }
-    Assert.assertTrue("Could not verify 'has no exception' test.", testException);
-
-    // Test if 'has no 404' test is correct.
-    setupBrowser("error/404", true);
-    final boolean timeout404 = waitForTest("error/404");
-    Assert.assertFalse("Could not verify 'has no 404' test. TIMEOUT", timeout404);
-    boolean test404 = false;
-    results = qunitTests.findElements(By.xpath("li"));
-    for (final WebElement result : results) {
-      if ("has no 404".equals(result.findElement(By.className("test-name")).getText())) {
-        Assert.assertEquals(result.getAttribute("class"), "fail");
-        test404 = true;
-      }
-    }
-    Assert.assertTrue("Could not verify 'has no 404' test.", test404);
-
-    for (final String page : pages) {
-      final String pathForBrowser = page.substring(page.indexOf("src/main/webapp/") + "src/main/webapp/".length(),
-          page.length() - 6);
-
-      //TODO: reimplement/remove/cleanup attic tests - currently too much failed
-      if (!pathForBrowser.contains("content/40-test/90000-attic/")) {
-        setupBrowser(pathForBrowser, true);
-        checkQUnitResults(page);
-      }
-    }
-  }
-
-  private static List<String> getXHTMLs() {
-    File rootDir = new File("tobago-example/tobago-example-demo/src/main/webapp/content");
-    if (!rootDir.exists()) {
-      rootDir = new File("src/main/webapp/content"); // Jenkins.
-    }
-    return getXHTMLs(rootDir);
-  }
-
-  private static List<String> getXHTMLs(final File dir) {
-    final List<String> xhtmls = new ArrayList<>();
-    for (final File file : dir.listFiles()) {
-      if (file.isDirectory()) {
-        xhtmls.addAll(getXHTMLs(file));
-      } else if (!file.getName().startsWith("x-") && file.getName().endsWith(".xhtml")) {
-        xhtmls.add(file.getPath());
-      }
-    }
-    return xhtmls;
-  }
-
-  private List<String> ignorePages() {
-    final List<String> ignore = new ArrayList<>();
-    //ajaxListener doesn't work for <tc:in> events: focus, blur, click, dblclick
-    ignore.add("content/40-test/6000-event/event.xhtml");
-    return ignore;
-  }
-
-  @Test
-  public void in() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/010-input/10-in/in.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void suggest() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/010-input/20-suggest/suggest.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void date() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/010-input/40-date/date.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void group() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/010-input/50-input-group/group.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void selectBooleanCheckbox() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/030-select/10-selectBooleanCheckbox/selectBooleanCheckbox.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void selectOneChoice() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/030-select/20-selectOneChoice/selectOneChoice.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void selectOneRadio() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/030-select/30-selectOneRadio/selectOneRadio.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void selectOneListbox() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/030-select/40-selectOneListbox/selectOneListbox.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void selectManyCheckbox() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/030-select/50-selectManyCheckbox/selectManyCheckbox.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void buttons() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/040-command/20-buttons/buttons.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void box() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/050-container/10-box/box.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void section() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/050-container/40-section/section.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void popup() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/060-popup/popup.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void tab() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/070-tab/tab.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void sheetSorting() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/080-sheet/10-sort/sheet-sorting.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void sheetEvent() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/080-sheet/30-event/sheet-event.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void treeSelect() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/20-component/090-tree/01-select/tree-select.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void contentValidation() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/06-validation/00/content-validation.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void validationJsr303() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/06-validation/01/validation-jsr303.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void form() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/08-form/form.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void formRequired() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/08-form/10-required/form-required.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void formAjax() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/08-form/20-ajax/form-ajax.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void forEach() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/51-for-each/for-each.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void collapsibleBox() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/53-collapsible/00-collapsible-box/collapsible-box.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void collapsiblePopup() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/53-collapsible/10-collapsible-popup/collapsible-popup.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void collapsiblePanel() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/53-collapsible/20-collapsible-panel/collapsible-panel.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void collapsibleSection() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/30-concept/53-collapsible/30-collapsible-section/collapsible-section.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void suggestMethod() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/35-deprecated/15-suggest-method/suggest-method.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testSuggestQuotMark() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/1020-suggest-quotMark/suggest-quotMark.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testDate() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/1040-date/date.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testDatePattern() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/1041-date-pattern/date-pattern.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testTabgroupStyle() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/2500-tab/tabgroup-style.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testSheetTypes() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/3000-sheet/10-sheet-types/sheet-types.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testButtonLink() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/4000-button+link/button+link.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testAjaxDropdown() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/4000-button+link/4050-ajax-dropdown/ajax-dropdown.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testIdMarkup() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/4800-labelLayout/100-id-markup/id-markup.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testLabelLayoutTop() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/4810-labelLayoutTop/labelLayoutTop.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testEvent() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/6000-event/event.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testModeValueIfSet() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/9010-mode-valueIfSet/mode-valueIfSet.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testHeadings() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/40000-style/100-headings/headings.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void rendererBaseGetCurrentValue() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/50000-java/10-rendererBase-getCurrentValue/rendererBase-getCurrentValue.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void ajaxExecute() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/50000-java/20-ajax-execute/ajax-execute.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void ajaxSpecialCharacter() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/50000-java/30-ajax-special-character/ajax-special-character.xhtml";
-    runStandardTest(page);
-  }
-
-  @Test
-  public void testBehavior() throws UnsupportedEncodingException, InterruptedException {
-    final String page = "content/40-test/6500-behavior/behavior.xhtml";
-    runStandardTest(page);
-  }
-}
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
new file mode 100644
index 0000000..9485052
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/AccessAllPagesTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.Assert;
+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.WebElement;
+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.net.MalformedURLException;
+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.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(AccessAllPagesTest.class);
+
+  /**
+   * Verify if qunit test for "no exception".
+   */
+  @ParameterizedTest
+  @MethodSource("verifyNoException404TestProvider")
+  void verifyNoExceptionTest(Browser browser, String portContextPath) throws MalformedURLException,
+      UnsupportedEncodingException, UnknownHostException {
+    LOG.info("browser: " + browser + " - port and context path: " + portContextPath);
+
+    setupBrowser(browser);
+
+    final String path = "error/exception.xhtml";
+    setupWebDriver(portContextPath, path, true);
+
+    WebElement qunitBanner = webDriver.findElement(By.id("qunit-banner"));
+    WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
+
+    new FluentWait(webDriver)
+        .withTimeout(Duration.ofSeconds(90))
+        .pollingEvery(Duration.ofSeconds(1))
+        .until(ExpectedConditions.attributeToBeNotEmpty(qunitBanner, "class"));
+
+    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())) {
+        Assert.assertEquals(result.getAttribute("class"), "fail");
+        testException = true;
+      }
+    }
+    Assert.assertTrue("Could not verify 'has no exception' test.", testException);
+
+    webDriver.quit();
+  }
+
+  /**
+   * Verify if qunit test for "no 404".
+   */
+  @ParameterizedTest
+  @MethodSource("verifyNoException404TestProvider")
+  void verifyNo404Test(Browser browser, String portContextPath) throws MalformedURLException,
+      UnsupportedEncodingException, UnknownHostException {
+    LOG.info("browser: " + browser + " - port and context path: " + portContextPath);
+
+    setupBrowser(browser);
+
+    final String path = "error/404.xhtml";
+    setupWebDriver(portContextPath, path, true);
+
+    WebElement qunitBanner = webDriver.findElement(By.id("qunit-banner"));
+    WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
+
+    new FluentWait(webDriver)
+        .withTimeout(Duration.ofSeconds(90))
+        .pollingEvery(Duration.ofSeconds(1))
+        .until(ExpectedConditions.attributeToBeNotEmpty(qunitBanner, "class"));
+
+    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())) {
+        Assert.assertEquals(result.getAttribute("class"), "fail");
+        test404 = true;
+      }
+    }
+    Assert.assertTrue("Could not verify 'has no 404' test.", test404);
+
+    webDriver.quit();
+  }
+
+  @ParameterizedTest
+  @MethodSource("accessAllPagesProvider")
+  void testAccessAllPages(Browser browser, String portContextPath, String path) throws MalformedURLException,
+      UnsupportedEncodingException, UnknownHostException {
+    LOG.info("browser: " + browser + " - port and context path: " + portContextPath + " - path: " + path);
+
+    setupBrowser(browser);
+    setupWebDriver(portContextPath, path, true);
+
+    parseQUnitResults(browser, portContextPath, path);
+
+    webDriver.quit();
+  }
+
+  private static Stream<Arguments> verifyNoException404TestProvider() throws IOException {
+    List<Arguments> arguments = new LinkedList<>();
+
+    for (Browser browser : Browser.values()) {
+      for (String portWithContextPath : serverPortWithContextPath) {
+        arguments.add(Arguments.of(browser, portWithContextPath));
+      }
+    }
+
+    return arguments.stream();
+  }
+
+  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()))
+        .filter(s -> !s.startsWith("content/40-test/90000-attic")) // ignore 90000-attic
+        .sorted()
+        .collect(Collectors.toList());
+
+    List<Arguments> arguments = new LinkedList<>();
+
+    for (Browser browser : Browser.values()) {
+      for (String portWithContextPath : serverPortWithContextPath) {
+        for (String path : paths) {
+          arguments.add(Arguments.of(browser, portWithContextPath, path));
+        }
+      }
+    }
+
+    return arguments.stream();
+  }
+}
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
new file mode 100644
index 0000000..fa9224d
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/SeleniumBase.java
@@ -0,0 +1,188 @@
+/*
+ * 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.Assert;
+import org.junit.jupiter.api.AfterEach;
+import org.openqa.selenium.By;
+import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.remote.DesiredCapabilities;
+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.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.List;
+
+abstract class SeleniumBase {
+
+  private static final Logger LOG = LoggerFactory.getLogger(SeleniumBase.class);
+
+  WebDriver webDriver;
+
+  @AfterEach
+  void tearDown() {
+    if (webDriver != null) {
+      webDriver.quit();
+    }
+  }
+
+  enum Browser {
+    chrome, firefox
+  }
+
+  static String[] serverPortWithContextPath = {
+      "8081/tobago-example-demo"
+  };
+
+  void setupBrowser(final Browser browser) throws MalformedURLException {
+    MutableCapabilities options = null;
+    switch (browser) {
+      case chrome:
+        options = new ChromeOptions();
+        break;
+      case firefox:
+        options = new FirefoxOptions();
+        break;
+    }
+
+    webDriver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), new DesiredCapabilities(options));
+  }
+
+  void setupWebDriver(final String portContextPath, final String path, final boolean accessTest)
+      throws UnknownHostException, UnsupportedEncodingException {
+    final String hostAddress = InetAddress.getLocalHost().getHostAddress();
+    final String base = path.substring(0, path.length() - 6);
+    final String url = "http://" + hostAddress + ":" + portContextPath + "/test.xhtml?base="
+        + URLEncoder.encode(base, "UTF-8") + (accessTest ? "&accessTest=true" : "");
+    webDriver.get(url);
+  }
+
+  void parseQUnitResults(final Browser browser, final String portContextPath, final String path) throws UnknownHostException {
+    WebElement qunitBanner = webDriver.findElement(By.id("qunit-banner"));
+    WebElement qunitTestResult = webDriver.findElement(By.id("qunit-testresult"));
+    WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
+
+    new FluentWait(webDriver)
+        .withTimeout(Duration.ofSeconds(90))
+        .pollingEvery(Duration.ofSeconds(1))
+        .until(ExpectedConditions.attributeToBeNotEmpty(qunitBanner, "class"));
+
+    final List<WebElement> testCases = qunitTests.findElements(By.xpath("li"));
+    Assert.assertTrue("There must be at least one test case.", testCases.size() > 0);
+
+    final boolean testFailed = qunitBanner.getAttribute("class").equals("qunit-fail");
+
+    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 hostAddress = InetAddress.getLocalHost().getHostAddress();
+    final String url = "http://" + hostAddress + ":" + portContextPath + "/" + path;
+    if (testFailed) {
+      final String message = "\n❌ FAILED: Tests with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
+      LOG.warn(message);
+      Assert.fail(message);
+    } else {
+      final String message = "\n✅ PASSED: Tests with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
+      LOG.info(message);
+      Assert.assertTrue(message, true);
+    }
+  }
+
+  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
new file mode 100644
index 0000000..370429e
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/test/java/org/apache/myfaces/tobago/example/demo/qunit/StandardTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.net.MalformedURLException;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+class StandardTest extends SeleniumBase {
+
+  private static final Logger LOG = LoggerFactory.getLogger(StandardTest.class);
+
+  /**
+   * To test only a singe page, just change browser setup, 'portContextPath' and/or 'path'.
+   */
+  @Test
+  void testSinglePage() throws MalformedURLException, UnknownHostException, UnsupportedEncodingException {
+    final Browser browser = Browser.chrome;
+    final String portContextPath = serverPortWithContextPath[0];
+    final String path = "content/10-intro/intro.xhtml";
+
+    LOG.info("browser: " + browser + " - port and context path: " + portContextPath + " - path: " + path);
+
+    setupBrowser(browser);
+    setupWebDriver(portContextPath, path, false);
+
+    parseQUnitResults(browser, portContextPath, path);
+
+    webDriver.quit();
+  }
+
+  @ParameterizedTest
+  @MethodSource("standardTestProvider")
+  void testStandard(Browser browser, String portContextPath, String path) throws MalformedURLException,
+      UnknownHostException, UnsupportedEncodingException {
+    LOG.info("browser: " + browser + " - port and context path: " + portContextPath + " - path: " + path);
+
+    setupBrowser(browser);
+    setupWebDriver(portContextPath, path, false);
+
+    parseQUnitResults(browser, portContextPath, path);
+
+    webDriver.quit();
+  }
+
+  private static Stream<Arguments> standardTestProvider() throws IOException {
+    final List<String> paths = 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());
+
+    List<Arguments> arguments = new LinkedList<>();
+
+    for (Browser browser : Browser.values()) {
+      for (String portWithContextPath : serverPortWithContextPath) {
+        for (String path : paths) {
+          arguments.add(Arguments.of(browser, portWithContextPath, path));
+        }
+      }
+    }
+
+    return arguments.stream();
+  }
+}

-- 
To stop receiving notification emails like this one, please contact
hnoeth@apache.org.