From 078d31e8fb573e137a4d64d695e01151932928a0 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 14 Apr 2026 09:37:33 -0700 Subject: [PATCH 1/2] Add accessibility check to Selenium tests --- build.gradle | 3 ++ gradle.properties | 2 + src/org/labkey/test/TestProperties.java | 5 +++ src/org/labkey/test/WebDriverWrapper.java | 3 ++ src/org/labkey/test/components/Component.java | 6 +++ src/org/labkey/test/pages/LabKeyPage.java | 6 +++ .../labkey/test/util/AccessibilityUtils.java | 38 +++++++++++++++++++ 7 files changed, 63 insertions(+) create mode 100644 src/org/labkey/test/util/AccessibilityUtils.java diff --git a/build.gradle b/build.gradle index aa002ab263..fbb5ca94d0 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,9 @@ project.dependencies { implementation("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}") implementation("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}") implementation "org.seleniumhq.selenium:selenium-ie-driver:${seleniumVersion}" + + implementation("com.deque.html.axe-core:selenium:${axeCoreSeleniumVersion}") + implementation("org.eclipse.jetty:jetty-util:${jettyVersion}") implementation("com.google.guava:guava:${guavaVersion}") diff --git a/gradle.properties b/gradle.properties index c1d3217e49..8779c6e8e2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,8 @@ assertjVersion=3.27.7 awaitilityVersion=4.3.0 +axeCoreSeleniumVersion=4.11.1 + lookfirstSardineVersion=5.13 jettyVersion=12.1.5 diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index 82ed8da3d2..4ab4c3cac3 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -352,6 +352,11 @@ public static boolean ignoreDatabaseNotSupportedException() return "true".equals(System.getProperty("webtest.ignoreDatabaseNotSupportedException")); } + public static boolean isAccessibilityCheckEnabled() + { + return "true".equals(System.getProperty("webtest.enableAccessibilityCheck", "true")); // TODO: default to "false" if lots of TeamCity noise + } + /** * Parses system property 'webtest.server.startup.timeout' to determine maximum allowed server startup time. * If property is not defined or is not an integer, it defaults to 120 seconds. diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index af3e2b6a67..280b8a022a 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -47,6 +47,7 @@ import org.labkey.test.pages.study.ManageStudyPage; import org.labkey.test.pages.user.ShowUsersPage; import org.labkey.test.selenium.EphemeralWebElement; +import org.labkey.test.util.AccessibilityUtils; import org.labkey.test.util.CodeMirrorHelper; import org.labkey.test.util.Crawler; import org.labkey.test.util.EscapeUtil; @@ -2134,6 +2135,8 @@ public long doAndMaybeWaitForPageToLoad(int msWait, Supplier action) String warning = "Action doesn't define a page title"; addActionWarning(warning, getDriver().getCurrentUrl()); } + + AccessibilityUtils.scanPage(getDriver()); } return loadTimer.elapsed().toMillis(); diff --git a/src/org/labkey/test/components/Component.java b/src/org/labkey/test/components/Component.java index 11a1b5057d..8d3c86bbc7 100644 --- a/src/org/labkey/test/components/Component.java +++ b/src/org/labkey/test/components/Component.java @@ -15,11 +15,15 @@ */ package org.labkey.test.components; +import com.deque.html.axecore.results.Results; +import com.deque.html.axecore.selenium.AxeBuilder; import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.NotNull; import org.labkey.test.Locator; import org.labkey.test.selenium.RefindingWebElement; +import org.labkey.test.util.AccessibilityUtils; import org.labkey.test.util.TestLogger; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; @@ -79,6 +83,8 @@ protected EC elementCache() // It succeeded, so it should be safe to get a fresh `newElementCache` _elementCache = Objects.requireNonNull(newElementCache()); } + + AccessibilityUtils.scanComponent(this); } return _elementCache; } diff --git a/src/org/labkey/test/pages/LabKeyPage.java b/src/org/labkey/test/pages/LabKeyPage.java index bf253fabf0..a9a4dcf6d9 100644 --- a/src/org/labkey/test/pages/LabKeyPage.java +++ b/src/org/labkey/test/pages/LabKeyPage.java @@ -15,9 +15,13 @@ */ package org.labkey.test.pages; +import com.deque.html.axecore.results.Results; +import com.deque.html.axecore.selenium.AxeBuilder; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locators; import org.labkey.test.WebDriverWrapper; +import org.labkey.test.util.AccessibilityUtils; +import org.labkey.test.util.TestLogger; import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; @@ -45,6 +49,8 @@ public LabKeyPage(WrapsDriver test) _test = (BaseWebDriverTest)test; _wrapsDriver = test; waitForPage(); + + AccessibilityUtils.scanPage(this); } public LabKeyPage(WebDriver driver) diff --git a/src/org/labkey/test/util/AccessibilityUtils.java b/src/org/labkey/test/util/AccessibilityUtils.java new file mode 100644 index 0000000000..ea957428e4 --- /dev/null +++ b/src/org/labkey/test/util/AccessibilityUtils.java @@ -0,0 +1,38 @@ +package org.labkey.test.util; + +import com.deque.html.axecore.results.Results; +import com.deque.html.axecore.selenium.AxeBuilder; +import org.labkey.test.components.Component; +import org.labkey.test.pages.LabKeyPage; +import org.labkey.test.util.selenium.WebDriverUtils; +import org.openqa.selenium.WebDriver; + +public class AccessibilityUtils +{ + public static void scanPage(WebDriver driver) + { + Results results = new AxeBuilder().analyze(driver); + recordViolations(results, "page"); + } + + public static void scanPage(LabKeyPage page) + { + Results results = new AxeBuilder().analyze(WebDriverUtils.extractWrappedDriver(page.getWrappedDriver())); + recordViolations(results, page.getClass().getSimpleName()); + } + + public static void scanComponent(Component component) + { + Results results = new AxeBuilder().analyze(WebDriverUtils.extractWrappedDriver(component.getComponentElement()), component.getComponentElement()); + recordViolations(results, component.getClass().getSimpleName()); + } + + private static void recordViolations(Results results, String component) + { + if (results.isErrored()) + { + TestLogger.error("Accessibility violations found on " + component); + results.getViolations().forEach(violation -> TestLogger.error(violation.getDescription())); + } + } +} From 7c25955523603e4457785424de14bb842e1f8425 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 15 Apr 2026 09:19:27 -0700 Subject: [PATCH 2/2] Don't scan all components --- src/org/labkey/test/components/Component.java | 8 +++- .../components/bootstrap/ModalDialog.java | 6 +++ .../components/ui/grids/GridFilterModal.java | 1 + .../labkey/test/util/AccessibilityUtils.java | 45 +++++++++++++++++-- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/components/Component.java b/src/org/labkey/test/components/Component.java index 8d3c86bbc7..589e66c7bd 100644 --- a/src/org/labkey/test/components/Component.java +++ b/src/org/labkey/test/components/Component.java @@ -84,13 +84,19 @@ protected EC elementCache() _elementCache = Objects.requireNonNull(newElementCache()); } - AccessibilityUtils.scanComponent(this); + if (shouldScanAfterReady()) + AccessibilityUtils.scanComponent(this); } return _elementCache; } protected void waitForReady() { } + protected boolean shouldScanAfterReady() + { + return false; + } + protected EC newElementCache() { throw new NotImplementedException("Please override newElementCache() in your component class"); diff --git a/src/org/labkey/test/components/bootstrap/ModalDialog.java b/src/org/labkey/test/components/bootstrap/ModalDialog.java index 2afdb12439..ad7c766302 100644 --- a/src/org/labkey/test/components/bootstrap/ModalDialog.java +++ b/src/org/labkey/test/components/bootstrap/ModalDialog.java @@ -64,6 +64,12 @@ protected void waitForReady() !elementCache().body.getText().isEmpty(), "Modal dialog not ready.", 2_500); } + @Override + protected boolean shouldScanAfterReady() + { + return true; + } + @Override public WebElement getComponentElement() { diff --git a/src/org/labkey/test/components/ui/grids/GridFilterModal.java b/src/org/labkey/test/components/ui/grids/GridFilterModal.java index b597356abf..84da92d23b 100644 --- a/src/org/labkey/test/components/ui/grids/GridFilterModal.java +++ b/src/org/labkey/test/components/ui/grids/GridFilterModal.java @@ -43,6 +43,7 @@ protected void waitForReady() */ public GridFilterModal selectField(CharSequence fieldIdentifier) { + getWrapper().shortWait().until(ExpectedConditions.invisibilityOfElementLocated(Locator.byClass("field-modal__empty-msg"))); WebElement fieldItem = elementCache().findFieldOption(fieldIdentifier); getWrapper().scrollIntoView(fieldItem); String fieldLabel = WebElementUtils.getTextContent(fieldItem); diff --git a/src/org/labkey/test/util/AccessibilityUtils.java b/src/org/labkey/test/util/AccessibilityUtils.java index ea957428e4..e406ff641d 100644 --- a/src/org/labkey/test/util/AccessibilityUtils.java +++ b/src/org/labkey/test/util/AccessibilityUtils.java @@ -2,31 +2,42 @@ import com.deque.html.axecore.results.Results; import com.deque.html.axecore.selenium.AxeBuilder; +import org.jspecify.annotations.NonNull; +import org.labkey.test.TestProperties; import org.labkey.test.components.Component; import org.labkey.test.pages.LabKeyPage; import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; public class AccessibilityUtils { public static void scanPage(WebDriver driver) { - Results results = new AxeBuilder().analyze(driver); + Results results = getAnalyzer().analyze(driver); recordViolations(results, "page"); } public static void scanPage(LabKeyPage page) { - Results results = new AxeBuilder().analyze(WebDriverUtils.extractWrappedDriver(page.getWrappedDriver())); + Results results = getAnalyzer().analyze(WebDriverUtils.extractWrappedDriver(page.getWrappedDriver())); recordViolations(results, page.getClass().getSimpleName()); } public static void scanComponent(Component component) { - Results results = new AxeBuilder().analyze(WebDriverUtils.extractWrappedDriver(component.getComponentElement()), component.getComponentElement()); + Results results = getAnalyzer().analyze(WebDriverUtils.extractWrappedDriver(component.getComponentElement()), component.getComponentElement()); recordViolations(results, component.getClass().getSimpleName()); } + private static @NonNull AxeBuilder getAnalyzer() + { + if (TestProperties.isAccessibilityCheckEnabled()) + return new AxeBuilder(); + else + return NoOpAxeBuilder.get(); + } + private static void recordViolations(Results results, String component) { if (results.isErrored()) @@ -36,3 +47,31 @@ private static void recordViolations(Results results, String component) } } } + +class NoOpAxeBuilder extends AxeBuilder +{ + private static final CachingSupplier INSTANCE = new CachingSupplier<>(NoOpAxeBuilder::new); + + static NoOpAxeBuilder get() + { + return INSTANCE.get(); + } + + @Override + public Results analyze(WebDriver webDriver, WebElement... context) + { + return new Results(); + } + + @Override + public Results analyze(WebDriver webDriver) + { + return new Results(); + } + + @Override + public Results analyze(WebDriver webDriver, boolean injectAxe) + { + return new Results(); + } +} \ No newline at end of file