Skip to content

Commit

Permalink
Add screenshot tests for the new registrar console
Browse files Browse the repository at this point in the history
This required updating to a newer version of Selenium, building the
console dist/ folder, and serving that folder.
  • Loading branch information
gbrodman committed Oct 1, 2024
1 parent c68d54a commit fe91046
Show file tree
Hide file tree
Showing 91 changed files with 870 additions and 391 deletions.
3 changes: 3 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,9 @@ task fragileTest(type: FilteringTest) {
// Common exclude pattern. See README in parent directory for explanation.
tests = fragileTestPatterns

// Screenshot tests depend on the console being built
dependsOn(rootProject.project('console-webapp').tasks.named('buildConsoleWebapp'))

if (rootProject.findProperty("skipDockerIncompatibleTests") == "true") {
exclude dockerIncompatibleTestPatterns
}
Expand Down
210 changes: 132 additions & 78 deletions core/gradle.lockfile

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@ public final class RegistryTestServer {

public static final ImmutableMap<String, Path> RUNFILES =
new ImmutableMap.Builder<String, Path>()
.put("/index.html",
.put(
"/index.html",
PROJECT_ROOT.resolve("core/src/main/java/google/registry/ui/html/index.html"))
.put("/error.html",
.put(
"/error.html",
PROJECT_ROOT.resolve("core/src/main/java/google/registry/ui/html/error.html"))
.put("/assets/js/*", RESOURCES_DIR.resolve("google/registry/ui"))
.put("/assets/css/*", RESOURCES_DIR.resolve("google/registry/ui/css"))
.put("/assets/sources/*", PROJECT_ROOT)
.put("/assets/*", PROJECT_ROOT.resolve("core/src/main/java/google/registry/ui/assets"))
.put("/console/*", PROJECT_ROOT.resolve("console-webapp/staged/dist"))
.build();

private static final ImmutableList<Route> ROUTES =
public static final ImmutableList<Route> ROUTES =
ImmutableList.of(
// Frontend Services
route("/whois/*", FrontendServlet.class),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.webdriver;

import static com.google.common.truth.Truth.assertThat;
import static google.registry.server.Fixture.BASIC;

import com.google.common.collect.ImmutableMap;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.server.RegistryTestServer;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junitpioneer.jupiter.RetryingTest;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

/**
* Tests for the 2024 registrar console, served from the Angular staged distribution.
*
* <p>When running the tests locally, be sure to build the console webapp first (the
* "buildConsoleWebapp" Gradle command, or the "npm run build" command) otherwise the tests will
* fail.
*
* <p>In general, for tests, we load the home page then click on elements to transfer pages. This is
* because we aren't really serving the webapp the same way that an actual webserver (e.g. Express)
* would, we're just statically serving the distribution. As a result, we cannot load URLs, e.g.
* "/#/settings", directly.
*/
public class ConsoleScreenshotTest extends WebDriverTestCase {

@RegisterExtension
final TestServerExtension server =
new TestServerExtension.Builder()
.setRunfiles(RegistryTestServer.RUNFILES)
.setRoutes(RegistryTestServer.ROUTES)
.setFixtures(BASIC)
.setEmail("[email protected]") // from makeRegistrarContact3
.setRegistryLockEmail("[email protected]")
.build();

@BeforeEach
void beforeEach() throws Exception {
server.setRegistrarRoles(ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER));
loadHomePage();
}

@RetryingTest(3)
void index() throws Exception {
driver.diffPage("page");
}

@RetryingTest(3)
void index_registrarSelectDropdown() throws Exception {
selectRegistrar();
driver.diffPage("selectedRegistrar");
assertThat(driver.getCurrentUrl()).endsWith("?registrarId=TheRegistrar");
}

@RetryingTest(3)
void dums_mainPage() throws Exception {
clickSidebarElementByName("Domains");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.waitForElementToNotExist(By.tagName("mat-spinner"));
driver.diffPage("registrarSelected");
driver.findElement(By.cssSelector("mat-table button")).click();
Thread.sleep(100);
driver.diffPage("actionsButtonClicked");
}

@RetryingTest(3)
void settingsPage() throws Exception {
clickSidebarElementByName("Settings");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.diffPage("registrarSelected_contacts");
driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_whois");
driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_security");
}

@RetryingTest(3)
void billingInfo() throws Exception {
clickSidebarElementByName("Billing Info");
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.diffPage("registrarSelected");
}

@RetryingTest(3)
void resources() throws Exception {
clickSidebarElementByName("Resources");
driver.diffPage("page");
}

@RetryingTest(3)
void support() throws Exception {
clickSidebarElementByName("Support");
driver.diffPage("page");
}

@RetryingTest(3)
void globalRole_registrars() throws Exception {
server.setGlobalRole(GlobalRole.SUPPORT_LEAD);
loadHomePage();
driver.diffPage("homePage");
clickSidebarElementByName("Registrars");
driver.diffPage("registrarsPage");
}

private void clickSidebarElementByName(String name) throws Exception {
WebElement appContainer =
driver.findElement(By.cssSelector("mat-sidenav-container.console-app__container"));
List<WebElement> sidebarElems =
appContainer.findElements(By.cssSelector("mat-tree-node,mat-nested-tree-node"));
for (WebElement elem : sidebarElems) {
if (elem.getText().contains(name)) {
elem.click();
break;
}
}
Thread.sleep(100);
}

private void loadHomePage() throws InterruptedException {
driver.get(server.getUrl("/console/index.html"));
driver.waitForElementToNotExist(By.tagName("mat-progress-bar"));
}

private void selectRegistrar() throws InterruptedException {
driver.findElement(By.cssSelector("div.console-app__registrar input")).click();
Thread.sleep(150);
driver.diffPage("selectorOpen");
driver.findElement(By.cssSelector("div.mat-mdc-autocomplete-panel mat-option")).click();
Thread.sleep(100);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class DockerWebDriverExtension implements BeforeAllCallback, AfterAllCallback {
private static URL getWebDriverUrl() {
// TODO(#209): Find a way to automatically detect the version of docker image
GenericContainer container =
new GenericContainer("selenium/standalone-chrome:3.141.59-mercury")
new GenericContainer("selenium/standalone-chrome:4.25")
.withFileSystemBind("/dev/shm", "/dev/shm", BindMode.READ_WRITE)
.withExposedPorts(CHROME_DRIVER_SERVICE_PORT)
.waitingFor(Wait.forHttp("/").withStartupTimeout(Duration.of(20, ChronoUnit.SECONDS)));
Expand All @@ -53,7 +53,7 @@ private static URL getWebDriverUrl() {
url =
new URL(
String.format(
"http://%s:%d/wd/hub",
"http://%s:%d",
container.getContainerIpAddress(),
container.getMappedPort(CHROME_DRIVER_SERVICE_PORT)));
} catch (MalformedURLException e) {
Expand All @@ -65,7 +65,7 @@ private static URL getWebDriverUrl() {

@Override
public void beforeAll(ExtensionContext context) {
ChromeOptions chromeOptions = new ChromeOptions().setHeadless(true);
ChromeOptions chromeOptions = new ChromeOptions().addArguments("--headless=new");
webDriver = new RemoteWebDriver(WEB_DRIVER_URL, chromeOptions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.HostAndPort;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
Expand Down Expand Up @@ -139,6 +140,15 @@ public void setRegistrarRoles(Map<String, RegistrarRole> registrarRoles) {
OidcTokenAuthenticationMechanism.setAuthResultForTesting(AuthResult.createUser(user));
}

/** Sets the current user's global role. */
public void setGlobalRole(GlobalRole globalRole) {
user =
user.asBuilder()
.setUserRoles(new UserRoles.Builder().setGlobalRole(globalRole).build())
.build();
OidcTokenAuthenticationMechanism.setAuthResultForTesting(AuthResult.createUser(user));
}

/**
* @see TestServer#getUrl(String)
*/
Expand Down Expand Up @@ -231,11 +241,15 @@ Builder setRunfiles(ImmutableMap<String, Path> runfiles) {
return this;
}

public Builder setRoutes(ImmutableList<Route> routes) {
checkArgument(!routes.isEmpty(), "Must include at least one route");
this.routes = routes;
return this;
}

/** Sets the list of servlet {@link Route} objects for {@link TestServer}. */
public Builder setRoutes(Route... routes) {
checkArgument(routes.length > 0);
this.routes = ImmutableList.copyOf(routes);
return this;
return setRoutes(ImmutableList.copyOf(routes));
}

/** Sets an ordered list of fixtures that should be loaded on startup. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public final class WebDriverPlusScreenDifferExtension
implements BeforeEachCallback,
AfterEachCallback,
WebDriver,
org.openqa.selenium.interactions.HasInputDevices,
TakesScreenshot,
JavascriptExecutor,
HasCapabilities {
Expand All @@ -72,7 +71,7 @@ public final class WebDriverPlusScreenDifferExtension

// Default size of the browser window when taking screenshot. Having a fixed size of window can
// help make visual regression test deterministic.
private static final Dimension DEFAULT_WINDOW_SIZE = new Dimension(1200, 2000);
private static final Dimension DEFAULT_WINDOW_SIZE = new Dimension(1920, 1200);

private static final String GOLDENS_PATH =
getResource(WebDriverPlusScreenDifferExtension.class, "goldens/chrome-linux").getFile();
Expand Down Expand Up @@ -133,6 +132,16 @@ WebElement waitForElement(By by) throws InterruptedException {
return waitForElementWithCondition(by, Predicates.alwaysTrue());
}

/** Waits for the removal of an element (e.g. a loading bar). */
void waitForElementToNotExist(By by) throws InterruptedException {
while (true) {
if (findElements(by).isEmpty()) {
return;
}
Thread.sleep(WAIT_FOR_ELEMENTS_POLLING_INTERVAL_MS);
}
}

/**
* Executes an action and waits indefinitely for the replacement of an <em>existing</em> element.
*/
Expand Down Expand Up @@ -283,16 +292,6 @@ public Options manage() {
return driver.manage();
}

@Override
public org.openqa.selenium.interactions.Keyboard getKeyboard() {
return ((org.openqa.selenium.interactions.HasInputDevices) driver).getKeyboard();
}

@Override
public org.openqa.selenium.interactions.Mouse getMouse() {
return ((org.openqa.selenium.interactions.HasInputDevices) driver).getMouse();
}

@Override
public <X> X getScreenshotAs(OutputType<X> target) throws WebDriverException {
return ((TakesScreenshot) driver).getScreenshotAs(target);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ ext {
// TODO: Remove after the legacy console is deleted.
'com.google.closure-stylesheets:closure-stylesheets:1.5.0',
'com.google.javascript:closure-compiler:v20210505',
'org.seleniumhq.selenium:selenium-api:3.141.59',
'org.seleniumhq.selenium:selenium-chrome-driver:3.141.59',
'org.seleniumhq.selenium:selenium-java:3.141.59',
'org.seleniumhq.selenium:selenium-remote-driver:3.141.59',
'org.seleniumhq.selenium:selenium-api:4.25.0',
'org.seleniumhq.selenium:selenium-chrome-driver:4.25.0',
'org.seleniumhq.selenium:selenium-java:4.25.0',
'org.seleniumhq.selenium:selenium-remote-driver:4.25.0',

// TODO: Migrate Soy Tofu to Soy Sauce (go/soysauce-migration)
'com.google.template:soy:2021-02-01',
Expand Down
9 changes: 6 additions & 3 deletions prober/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,10 @@ javax.annotation:javax.annotation-api:1.3.2=deploy_jar,runtimeClasspath,testRunt
javax.inject:javax.inject:1=annotationProcessor,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
joda-time:joda-time:2.13.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.0=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.15.0=testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.0=testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.15.3=testCompileClasspath
net.bytebuddy:byte-buddy:1.15.0=testRuntimeClasspath
net.bytebuddy:byte-buddy:1.15.3=testCompileClasspath
net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath
net.ltgt.gradle.incap:incap:0.2=annotationProcessor,testAnnotationProcessor
net.sf.saxon:Saxon-HE:10.6=checkstyle
Expand Down Expand Up @@ -168,7 +170,8 @@ org.junit.platform:junit-platform-runner:1.11.1=testCompileClasspath,testRuntime
org.junit.platform:junit-platform-suite-api:1.11.1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-suite-commons:1.11.1=testRuntimeClasspath
org.junit:junit-bom:5.11.1=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.13.0=testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.13.0=testRuntimeClasspath
org.mockito:mockito-core:5.14.1=testCompileClasspath
org.objenesis:objenesis:3.3=testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.7=jacocoAnt
Expand Down
Loading

0 comments on commit fe91046

Please sign in to comment.