diff --git a/java/test/org/openqa/selenium/javascript/BUILD.bazel b/java/test/org/openqa/selenium/javascript/BUILD.bazel new file mode 100644 index 0000000000000..0c3afd1a4f820 --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//java:defs.bzl", "java_library") + +java_library( + name = "javascript", + testonly = 1, + srcs = glob(["*.java"]), + visibility = ["//javascript:__subpackages__"], + deps = [ + "//java/src/org/openqa/selenium:core", + "//java/test/org/openqa/selenium/build", + "//java/test/org/openqa/selenium/environment", + "//java/test/org/openqa/selenium/testing:test-base", + "//java/test/org/openqa/selenium/testing/drivers", + artifact("com.google.guava:guava"), + artifact("junit:junit"), + artifact("org.assertj:assertj-core"), + ], +) diff --git a/java/test/org/openqa/selenium/javascript/ClosureTestStatement.java b/java/test/org/openqa/selenium/javascript/ClosureTestStatement.java new file mode 100644 index 0000000000000..9363ec97ed3d7 --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/ClosureTestStatement.java @@ -0,0 +1,122 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC 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.openqa.selenium.javascript; + +import static org.junit.Assert.fail; +import static org.openqa.selenium.testing.TestUtilities.isOnTravis; + +import com.google.common.base.Stopwatch; + +import org.junit.runners.model.Statement; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; + +import java.net.URL; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Logger; + +public class ClosureTestStatement extends Statement { + + private static final Logger LOG = Logger.getLogger(ClosureTestStatement.class.getName()); + + private final Supplier driverSupplier; + private final String testPath; + private final Function filePathToUrlFn; + private final long timeoutSeconds; + + public ClosureTestStatement( + Supplier driverSupplier, + String testPath, + Function filePathToUrlFn, + long timeoutSeconds) { + this.driverSupplier = driverSupplier; + this.testPath = testPath; + this.filePathToUrlFn = filePathToUrlFn; + this.timeoutSeconds = Math.max(0, timeoutSeconds); + } + + @Override + public void evaluate() throws Throwable { + URL testUrl = filePathToUrlFn.apply(testPath); + LOG.info("Running: " + testUrl); + + Stopwatch stopwatch = Stopwatch.createStarted(); + + WebDriver driver = driverSupplier.get(); + + if (!isOnTravis()) { + // Attempt to make the window as big as possible. + try { + driver.manage().window().maximize(); + } catch (RuntimeException ignored) { + // We tried. + } + } + + JavascriptExecutor executor = (JavascriptExecutor) driver; + // Avoid Safari JS leak between tests. + executor.executeScript("if (window && window.top) window.top.G_testRunner = null"); + + try { + driver.get(testUrl.toString()); + } catch (WebDriverException e) { + fail("Test failed to load: " + e.getMessage()); + } + + while (!getBoolean(executor, Query.IS_FINISHED)) { + long elapsedTime = stopwatch.elapsed(TimeUnit.SECONDS); + if (timeoutSeconds > 0 && elapsedTime > timeoutSeconds) { + throw new JavaScriptAssertionError("Tests timed out after " + elapsedTime + " s. \nCaptured Errors: " + + ((JavascriptExecutor) driver).executeScript("return window.errors;") + + "\nPageSource: " + driver.getPageSource() + "\nScreenshot: " + + ((TakesScreenshot)driver).getScreenshotAs(OutputType.BASE64)); + } + TimeUnit.MILLISECONDS.sleep(100); + } + + if (!getBoolean(executor, Query.IS_SUCCESS)) { + String report = getString(executor, Query.GET_REPORT); + throw new JavaScriptAssertionError(report); + } + } + + private boolean getBoolean(JavascriptExecutor executor, Query query) { + return (Boolean) executor.executeScript(query.script); + } + + private String getString(JavascriptExecutor executor, Query query) { + return (String) executor.executeScript(query.script); + } + + private enum Query { + IS_FINISHED("return !!tr && tr.isFinished();"), + IS_SUCCESS("return !!tr && tr.isSuccess();"), + GET_REPORT("return tr.getReport(true);"); + + private final String script; + + Query(String script) { + this.script = "var tr = window.top.G_testRunner;" + script; + } + } +} diff --git a/java/test/org/openqa/selenium/javascript/ClosureTestSuite.java b/java/test/org/openqa/selenium/javascript/ClosureTestSuite.java new file mode 100644 index 0000000000000..ff3988ad4b25a --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/ClosureTestSuite.java @@ -0,0 +1,34 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC 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.openqa.selenium.javascript; + +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +import static org.assertj.core.api.Assumptions.assumeThat; + + +@RunWith(JavaScriptTestSuite.class) +public class ClosureTestSuite { + + @BeforeClass + public static void checkShouldRun() { + assumeThat(Boolean.getBoolean("selenium.skiptest")).isFalse(); + } + +} diff --git a/java/test/org/openqa/selenium/javascript/JavaScriptAssertionError.java b/java/test/org/openqa/selenium/javascript/JavaScriptAssertionError.java new file mode 100644 index 0000000000000..c368cbe47e41b --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/JavaScriptAssertionError.java @@ -0,0 +1,35 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC 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.openqa.selenium.javascript; + +class JavaScriptAssertionError extends AssertionError { + + public JavaScriptAssertionError(String message) { + super(message); + } + + @Override + public Throwable fillInStackTrace() { + return this; // No java stack traces. + } + + @Override + public void setStackTrace(StackTraceElement[] stackTraceElements) { + // No java stack traces. + } +} diff --git a/java/test/org/openqa/selenium/javascript/JavaScriptTestSuite.java b/java/test/org/openqa/selenium/javascript/JavaScriptTestSuite.java new file mode 100644 index 0000000000000..f9e99a0e07e94 --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/JavaScriptTestSuite.java @@ -0,0 +1,190 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC 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.openqa.selenium.javascript; + +import static java.util.stream.Collectors.toList; + +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.ParentRunner; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.environment.GlobalTestEnvironment; +import org.openqa.selenium.environment.InProcessTestEnvironment; +import org.openqa.selenium.environment.TestEnvironment; +import org.openqa.selenium.environment.webserver.AppServer; +import org.openqa.selenium.build.InProject; +import org.openqa.selenium.testing.drivers.WebDriverBuilder; + +import java.io.Closeable; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * JUnit4 test runner for Closure-based JavaScript tests. + */ +public class JavaScriptTestSuite extends ParentRunner { + + private final List children; + private final Supplier driverSupplier; + + public JavaScriptTestSuite(Class testClass) throws InitializationError, IOException { + super(testClass); + + long timeout = Math.max(0, Long.getLong("js.test.timeout", 0)); + + driverSupplier = new DriverSupplier(); + + children = createChildren(driverSupplier, timeout); + } + + private static boolean isBazel() { + return InProject.findRunfilesRoot() != null; + } + + private static List createChildren( + final Supplier driverSupplier, final long timeout) throws IOException { + final Path baseDir = InProject.findProjectRoot(); + final Function pathToUrlFn = s -> { + AppServer appServer = GlobalTestEnvironment.get().getAppServer(); + try { + String url = "/" + s; + if (isBazel() && !url.startsWith("/common/generated/")) { + url = "/filez/selenium" + url; + } + return new URL(appServer.whereIs(url)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }; + + List tests = TestFileLocator.findTestFiles(); + return tests.stream() + .map(file -> { + final String path = TestFileLocator.getTestFilePath(baseDir, file); + Description description = Description.createSuiteDescription( + path.replaceAll(".html$", "")); + + Statement testStatement = new ClosureTestStatement( + driverSupplier, path, pathToUrlFn, timeout); + return new StatementRunner(testStatement, description); + }) + .collect(toList()); + } + + @Override + protected List getChildren() { + return children; + } + + @Override + protected Description describeChild(Runner child) { + return child.getDescription(); + } + + @Override + protected void runChild(Runner child, RunNotifier notifier) { + child.run(notifier); + } + + @Override + protected Statement classBlock(RunNotifier notifier) { + final Statement suite = super.classBlock(notifier); + + return new Statement() { + @Override + public void evaluate() throws Throwable { + TestEnvironment testEnvironment = null; + try { + testEnvironment = GlobalTestEnvironment.getOrCreate(InProcessTestEnvironment::new); + suite.evaluate(); + } finally { + if (testEnvironment != null) { + testEnvironment.stop(); + } + if (driverSupplier instanceof Closeable) { + ((Closeable) driverSupplier).close(); + } + } + } + }; + } + + private static class StatementRunner extends Runner { + + private final Description description; + private final Statement testStatement; + + private StatementRunner(Statement testStatement, Description description) { + this.testStatement = testStatement; + this.description = description; + } + + @Override + public Description getDescription() { + return description; + } + + @Override + public void run(RunNotifier notifier) { + notifier.fireTestStarted(description); + try { + testStatement.evaluate(); + } catch (Throwable throwable) { + Failure failure = new Failure(description, throwable); + notifier.fireTestFailure(failure); + } finally { + notifier.fireTestFinished(description); + } + } + } + + private static class DriverSupplier implements Supplier, Closeable { + + private volatile WebDriver driver; + + @Override + public WebDriver get() { + if (driver != null) { + return driver; + } + + synchronized (this) { + if (driver == null) { + driver = new WebDriverBuilder().get(); + } + } + + return driver; + } + + public void close() { + if (driver != null) { + driver.quit(); + } + } + } +} diff --git a/java/test/org/openqa/selenium/javascript/TestFileLocator.java b/java/test/org/openqa/selenium/javascript/TestFileLocator.java new file mode 100644 index 0000000000000..53817d5c1c231 --- /dev/null +++ b/java/test/org/openqa/selenium/javascript/TestFileLocator.java @@ -0,0 +1,110 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC 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.openqa.selenium.javascript; + +import static java.lang.System.getProperty; +import static java.util.Collections.emptySet; + +import com.google.common.base.Splitter; + +import org.openqa.selenium.build.InProject; +import org.openqa.selenium.internal.Require; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + + +/** + * Builder for test suites that run JavaScript tests. + */ +class TestFileLocator { + + private static final String TEST_DIRECTORY_PROPERTY = "js.test.dir"; + private static final String TEST_EXCLUDES_PROPERTY = "js.test.excludes"; + + public static List findTestFiles() throws IOException { + Path directory = getTestDirectory(); + Set excludedFiles = getExcludedFiles(directory); + return findTestFiles(directory, excludedFiles); + } + + private static List findTestFiles(Path directory, Set excludedFiles) + throws IOException { + return Files.find( + directory, + Integer.MAX_VALUE, + (path, basicFileAttributes) -> { + String name = path.getFileName().toString(); + return name.endsWith("_test.html"); + // TODO: revive support for _test.js files. +// Path sibling = path.resolveSibling(name.replace(".js", ".html")); +// return name.endsWith("_test.html") +// || (name.endsWith("_test.js") && !Files.exists(sibling)); + }) + .filter(path -> !excludedFiles.contains(path)) + .collect(Collectors.toList()); + } + + private static Path getTestDirectory() { + String testDirName = Require.state("Test directory", getProperty(TEST_DIRECTORY_PROPERTY)).nonNull( + "You must specify the test directory with the %s system property", + TEST_DIRECTORY_PROPERTY); + + Path runfiles = InProject.findRunfilesRoot(); + Path testDir; + if (runfiles != null) { + // Running with bazel. + testDir = runfiles.resolve("selenium").resolve(testDirName); + } else { + // Legacy. + testDir = InProject.locate(testDirName); + } + + Require.state("Test directory", testDir.toFile()).isDirectory(); + + return testDir; + } + + private static Set getExcludedFiles(final Path testDirectory) { + String excludedFiles = getProperty(TEST_EXCLUDES_PROPERTY); + if (excludedFiles == null) { + return emptySet(); + } + + Iterable splitExcludes = Splitter.on(',').omitEmptyStrings().split(excludedFiles); + + return StreamSupport.stream(splitExcludes.spliterator(), false) + .map(testDirectory::resolve).collect(Collectors.toSet()); + } + + public static String getTestFilePath(Path baseDir, Path testFile) { + String path = testFile.toAbsolutePath().toString() + .replace(baseDir.toAbsolutePath().toString() + File.separator, "") + .replace(File.separator, "/"); + if (path.endsWith(".js")) { + path = "common/generated/" + path; + } + return path; + } +}