diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b5abb92..6af49f6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,12 +21,35 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest - + services: + hub: + image: selenium/hub:4.16.1-20231219 + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" + firefox: + image: selenium/node-firefox:4.16.1-20231219 + env: + SE_EVENT_BUS_HOST: hub + SE_EVENT_BUS_PUBLISH_PORT: 4442 + SE_EVENT_BUS_SUBSCRIBE_PORT: 4443 + chrome: + image: selenium/node-chrome:4.16.1-20231219 + env: + SE_EVENT_BUS_HOST: hub + SE_EVENT_BUS_PUBLISH_PORT: 4442 + SE_EVENT_BUS_SUBSCRIBE_PORT: 4443 steps: - - uses: actions/checkout@v2 - - # TODO: cache dependencies (taking into accounts: Maven plugins, snapshots, etc.) - - - name: Build with Maven - run: JAVA_HOME=$JAVA_HOME_8_X64 mvn -V -B -ntp -U -e verify + - name: Checkout + uses: actions/checkout@v4 + - name: Setup JDK 11 + id: setup-java-11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + cache: 'maven' + - name: Build and test with Maven + run: mvn -V -B -ntp -U -e verify -Pwebdriver-tests -Dwebdriver.test.host=$(hostname) diff --git a/gwt-core-gwt2-tests/pom.xml b/gwt-core-gwt2-tests/pom.xml index 6d9f504..2343bf8 100644 --- a/gwt-core-gwt2-tests/pom.xml +++ b/gwt-core-gwt2-tests/pom.xml @@ -76,6 +76,13 @@ gwt-core test + + + org.seleniumhq.selenium + selenium-remote-driver + 4.16.1 + test + @@ -100,4 +107,40 @@ + + + + + webdriver-tests + + localhost + localhost + 4444 + firefox,chrome + + + + + net.ltgt.gwt.maven + gwt-maven-plugin + + + ${webdriver.test.host} + + + -runStyle + org.gwtproject.junit.RunStyleRemoteWebDriver:http://${webdriver.hub.host}:${webdriver.hub.port}?${webdriver.browsers} + + + + + + + diff --git a/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleAbstractRemoteWebDriver.java b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleAbstractRemoteWebDriver.java new file mode 100644 index 0000000..1a8ebce --- /dev/null +++ b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleAbstractRemoteWebDriver.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2019 The GWT Project Authors + * + * Licensed 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.gwtproject.junit; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.junit.JUnitShell; +import com.google.gwt.junit.RunStyle; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.RemoteWebDriver; + +public abstract class RunStyleAbstractRemoteWebDriver extends RunStyle { + + public static class RemoteWebDriverConfiguration { + private String remoteWebDriverUrl; + private List> browserCapabilities; + + public String getRemoteWebDriverUrl() { + return remoteWebDriverUrl; + } + + public void setRemoteWebDriverUrl(String remoteWebDriverUrl) { + this.remoteWebDriverUrl = remoteWebDriverUrl; + } + + public List> getBrowserCapabilities() { + return browserCapabilities; + } + + public void setBrowserCapabilities(List> browserCapabilities) { + this.browserCapabilities = browserCapabilities; + } + } + + public class ConfigurationException extends Exception {} + + private List browsers = new ArrayList<>(); + private Thread keepalive; + + public RunStyleAbstractRemoteWebDriver(JUnitShell shell) { + super(shell); + } + + /** + * Validates the arguments for the specific subclass, and creates a configuration that describes + * how to run the tests. + * + * @param args the command line argument string passed from JUnitShell + * @return the configuration to use when running these tests + */ + protected abstract RemoteWebDriverConfiguration readConfiguration(String args) + throws ConfigurationException; + + @Override + public final int initialize(String args) { + final RemoteWebDriverConfiguration config; + try { + config = readConfiguration(args); + } catch (ConfigurationException failed) { + // log should already have details about what went wrong, we will just return the failure + // value + return -1; + } + + final URL remoteAddress; + try { + remoteAddress = new URL(config.getRemoteWebDriverUrl()); + } catch (MalformedURLException e) { + getLogger().log(TreeLogger.ERROR, e.getMessage(), e); + return -1; + } + + for (Map capabilityMap : config.getBrowserCapabilities()) { + DesiredCapabilities capabilities = new DesiredCapabilities(capabilityMap); + + try { + RemoteWebDriver wd = new RemoteWebDriver(remoteAddress, capabilities); + browsers.add(wd); + } catch (Exception exception) { + getLogger().log(TreeLogger.ERROR, "Failed to find desired browser", exception); + return -1; + } + } + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + if (keepalive != null) { + keepalive.interrupt(); + } + for (RemoteWebDriver browser : browsers) { + try { + browser.quit(); + } catch (Exception ignored) { + // ignore, we're shutting down, continue shutting down others + } + } + })); + return browsers.size(); + } + + @Override + public void launchModule(String moduleName) throws UnableToCompleteException { + // since WebDriver.get is blocking, start a keepalive thread first + keepalive = + new Thread( + () -> { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + for (RemoteWebDriver browser : browsers) { + browser.getTitle(); // as in RunStyleSelenium, simple way to poll the browser + } + } + }); + keepalive.setDaemon(true); + keepalive.start(); + for (RemoteWebDriver browser : browsers) { + browser.get(shell.getModuleUrl(moduleName)); + } + } + + /** + * Work-around until GWT's JUnitShell handles IPv6 addresses correctly. + * https://groups.google.com/d/msg/google-web-toolkit/jLGhwUrKVRY/eQaDO6EUqdYJ + */ + public String getLocalHostName() { + String host = System.getProperty("webdriver.test.host"); + if (host != null) { + return host; + } + InetAddress a; + try { + a = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new RuntimeException("Unable to determine my ip address", e); + } + if (a instanceof Inet6Address) { + return "[" + a.getHostAddress() + "]"; + } else { + return a.getHostAddress(); + } + } +} diff --git a/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriver.java b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriver.java new file mode 100644 index 0000000..f4f9969 --- /dev/null +++ b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriver.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2019 The GWT Project Authors + * + * Licensed 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.gwtproject.junit; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.junit.JUnitShell; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import org.openqa.selenium.remote.DesiredCapabilities; + +public class RunStyleRemoteWebDriver extends RunStyleAbstractRemoteWebDriver { + + public RunStyleRemoteWebDriver(JUnitShell shell) { + super(shell); + } + + @Override + protected RemoteWebDriverConfiguration readConfiguration(String args) + throws ConfigurationException { + RemoteWebDriverConfiguration config = new RemoteWebDriverConfiguration(); + if (args == null || args.length() == 0) { + getLogger() + .log( + TreeLogger.ERROR, + "RemoteWebDriver runstyle requires a parameter of the form protocol://hostname:port?browser1[,browser2]"); + throw new ConfigurationException(); + } + + String[] parts = args.split("\\?"); + String url = parts[0]; + URL remoteAddress = null; + try { + remoteAddress = new URL(url); + if (remoteAddress.getPath().equals("") + || (remoteAddress.getPath().equals("/") && !url.endsWith("/"))) { + getLogger() + .log( + TreeLogger.INFO, + "No path specified in webdriver remote url, using default of /wd/hub"); + config.setRemoteWebDriverUrl(url + "/wd/hub"); + } else { + config.setRemoteWebDriverUrl(url); + } + } catch (MalformedURLException e) { + getLogger().log(TreeLogger.ERROR, e.getMessage(), e); + throw new ConfigurationException(); + } + + // build each driver based on parts[1].split(",") + String[] browserNames = parts[1].split(","); + config.setBrowserCapabilities(new ArrayList<>()); + for (String browserName : browserNames) { + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setBrowserName(browserName); + config.getBrowserCapabilities().add(capabilities.asMap()); + } + + return config; + } +} diff --git a/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriverWithConfig.java b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriverWithConfig.java new file mode 100644 index 0000000..ededc00 --- /dev/null +++ b/gwt-core-gwt2-tests/src/test/java/org/gwtproject/junit/RunStyleRemoteWebDriverWithConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2019 The GWT Project Authors + * + * Licensed 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.gwtproject.junit; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.junit.JUnitShell; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; + +public class RunStyleRemoteWebDriverWithConfig extends RunStyleAbstractRemoteWebDriver { + public RunStyleRemoteWebDriverWithConfig(JUnitShell shell) { + super(shell); + } + + @Override + protected RemoteWebDriverConfiguration readConfiguration(String args) + throws ConfigurationException { + File jsonConfigFile = new File(args); + + try { + RemoteWebDriverConfiguration config = + new Gson().fromJson(new FileReader(jsonConfigFile), RemoteWebDriverConfiguration.class); + return config; + } catch (FileNotFoundException e) { + getLogger().log(TreeLogger.Type.ERROR, "Configuration file not found: " + args, e); + throw new ConfigurationException(); + } catch (JsonIOException e) { + getLogger().log(TreeLogger.Type.ERROR, "Error reading config file: " + args, e); + throw new ConfigurationException(); + } catch (JsonSyntaxException e) { + getLogger().log(TreeLogger.Type.ERROR, "Error parsing config file: " + args, e); + throw new ConfigurationException(); + } + } +}