Accessibility Testing with Google Lighthouse

Using Google Lighthouse to provide accessibility testing.

Accessibility reports are crucial when testing web pages because they help to ensure that a site is usable for people of all abilities. That is why we introduced Google Lighthouse to Neodymium, an open-source tool to improve the quality of web pages.

Install Lighthouse CLI

First of all we recommend installing a package manager like npm, which we are also going to use in order to install Lighthouse CLI. After you went through the installation process of npm, open a terminal and enter the command below.

npm install -g lighthouse

To make sure the Lighthouse installation was successful, you can run the following command.

lighthouse --version

Use Lighthouse Inside Neodymium

With the objective of creating Lighthouse reports inside Neodymium we implemented the class LighthouseUtils, containing the function createLightHouseReport(String reportName). By calling this function, a Lighthouse report of the current web page is generated and automatically added to the Allure report with the name specified in the reportName parameter. Keep in mind that creating a Lighthouse report only works while using Chrome or Chromium-based browsers.

The method call can be added at every point of your test and will create a report of the currently opened page. This also works for pages, which rely on session data like a login state or products that should have been added to a shopping cart. Keep in mind that it may not work on certain pages that, if refreshed, load a different page, like checkout pages for example.

Here is an example how to add createLightHouseReport() inside a test case:


@NeodymiumTest
public void testLoginAsRegisteredUser() throws Exception
{
    // go to homepage
    var homePage = OpenHomePageFlow.flow();
    LighthouseUtils.createLightHouseReport("Homepage");

    // go to register page
    var registerPage = homePage.header.userMenu.openRegisterPage();

    // send register form
    var loginPage = registerPage.sendRegisterForm(registeredOrderTestData.getUser());

    // send login form
    var accountOverviewPage = loginPage.sendLoginForm(registeredOrderTestData.getUser());
    accountOverviewPage.validateSuccessfulLogin(registeredOrderTestData.getUser().getFirstName());

    LighthouseUtils.createLightHouseReport("Account Overview Page");
}

Please note that this example uses page object model classes which are not shipped with neodymium.

Limitations

Please note, that Lighthouse does not work with modals, fly-ins, hover etc. The site will be newly loaded for the report, so every manually opened modal will be closed after a Lighthouse report. This needs to be considered when generating reports and might require changes to the test flow (e.g. if a modal is open which contains the next button the scripts wants to click).

Lighthouse Reports

The generated Lighthouse reports can be found in the target directory of the user’s repository and in the Allure report as an attachment, visualized in the following image.

Lighthouse report inside an Allure report.

A Lighthouse report consists of the following four categories.

  1. Performance
  2. Accessibility
  3. Best Practices
  4. Search Engine Optimisation (SEO)

Each of those categories is scored between 1 and 100, which reveals how well the web page performed in every category. Therefore, Google defines the following ranges.

  • 0 to 49: Poor
  • 50 to 89: Needs Improvement
  • 90 to 100: Good

Lighthouse Report Validation

This section is about how to assert metrics from the Lighthouse report.

Lighthouse Category Score Validation

To enable validating the scores of all four categories, we implemented the following score thresholds in the neodymium.properties file.

  • neodymium.lighthouse.assert.thresholdScore.performance
  • neodymium.lighthouse.assert.thresholdScore.accessibility
  • neodymium.lighthouse.assert.thresholdScore.bestPractices
  • neodymium.lighthouse.assert.thresholdScore.seo

All of those configuration properties are set to 0.5 per default, which sets all category score threshold to 50. That means each and every category needs to match or exceed a score of 50 or otherwise the test will fail. All the score thresholds can be changed depending on the user’s wishes.

Please be aware that the Lighthouse performance score is not the most stable value and is affected by many factors like network load on th the test machine. This can lead to random outliers in the measurement making your tests flaky. A check against the 75th percentile is recommended to give the test some stability (see for example here).

Lighthouse Audit Validation

We also implemented the property neodymium.lighthouse.assert.audits in the neodymium.properties file. This property makes it possible to validate Lighthouse audits. In order to do that the user has to specify the id of all audits that should be validated as the property itself. For example: neodymium.lighthouse.assert.audits = aria-roles aria-text validates that no error occurs in the Lighthouse audits aria-roles and aria-text. All existing audit ID’s and their corresponding titles are visualized in the table below.

idtitle
is-on-httpsUses HTTPS
redirects-httpRedirects HTTP traffic to HTTPS
viewportHas a <meta name="viewport"> tag with width or initial-scale
first-contentful-paintFirst Contentful Paint
largest-contentful-paintLargest Contentful Paint
first-meaningful-paintFirst Meaningful Paint
speed-indexSpeed Index
screenshot-thumbnailsScreenshot Thumbnails
final-screenshotFinal Screenshot
total-blocking-timeTotal Blocking Time
max-potential-fidMax Potential First Input Delay
cumulative-layout-shiftCumulative Layout Shift
errors-in-consoleNo browser errors logged to the console
server-response-timeInitial server response time was short
interactiveTime to Interactive
user-timingsUser Timing marks and measures
critical-request-chainsAvoid chaining critical requests
redirectsAvoid multiple page redirects
image-aspect-ratioDisplays images with correct aspect ratio
image-size-responsiveServes images with appropriate resolution
deprecationsAvoids deprecated APIs
third-party-cookiesAvoids third-party cookies
mainthread-work-breakdownMinimizes main-thread work
bootup-timeJavaScript execution time
uses-rel-preconnectPreconnect to required origins
font-displayEnsure text remains visible during webfont load
diagnosticsDiagnostics
network-requestsNetwork Requests
network-rttNetwork Round Trip Times
network-server-latencyServer Backend Latencies
main-thread-tasksTasks
metricsMetrics
resource-summaryResources Summary
third-party-summaryMinimize third-party usage
third-party-facadesLazy load third-party resources with facades
largest-contentful-paint-elementLargest Contentful Paint element
lcp-lazy-loadedLargest Contentful Paint image was not lazily loaded
layout-shiftsAvoid large layout shifts
long-tasksAvoid long main-thread tasks
non-composited-animationsAvoid non-composited animations
unsized-imagesImage elements do not have explicit width and height
valid-source-mapsPage has valid source maps
prioritize-lcp-imagePreload Largest Contentful Paint image
csp-xssEnsure CSP is effective against XSS attacks
script-treemap-dataScript Treemap Data
accesskeys[accesskey] values are unique
aria-allowed-attr[aria-*] attributes match their roles
aria-allowed-roleUses ARIA roles only on compatible elements
aria-command-namebutton, link, and menuitem elements have accessible names
aria-conditional-attrARIA attributes are used as specified for the element’s role
aria-deprecated-roleDeprecated ARIA roles were not used
aria-dialog-nameElements with role="dialog" or role="alertdialog" have accessible names.
aria-hidden-body[aria-hidden="true"] is not present on the document <body>
aria-hidden-focus[aria-hidden="true"] elements do not contain focusable descendents
aria-input-field-nameARIA input fields have accessible names
aria-meter-nameARIA meter elements have accessible names
aria-progressbar-nameARIA progressbar elements have accessible names
aria-prohibited-attrElements use only permitted ARIA attributes
aria-required-attr[role]s have all required [aria-*] attributes
aria-required-childrenElements with an ARIA [role] that require children to contain a specific [role] have all required children.
aria-required-parent[role]s are contained by their required parent element
aria-roles[role] values are valid
aria-textElements with the role=text attribute do not have focusable descendents.
aria-toggle-field-nameARIA toggle fields have accessible names
aria-tooltip-nameARIA tooltip elements have accessible names
aria-treeitem-nameARIA treeitem elements have accessible names
aria-valid-attr-value[aria-*] attributes have valid values
aria-valid-attr[aria-*] attributes are valid and not misspelled
button-nameButtons do not have an accessible name
bypassThe page contains a heading, skip link, or landmark region
color-contrastBackground and foreground colors have a sufficient contrast ratio
definition-list<dl>’s contain only properly-ordered <dt> and <dd> groups, <script>, <template> or <div> elements.
dlitemDefinition list items are wrapped in <dl> elements
document-titleDocument has a <title> element
duplicate-id-ariaARIA IDs are unique
empty-headingAll heading elements contain content.
form-field-multiple-labelsNo form fields have multiple labels
frame-title<frame> or <iframe> elements have a title
heading-orderHeading elements are not in a sequentially-descending order
html-has-lang<html> element has a [lang] attribute
html-lang-valid<html> element has a valid value for its [lang] attribute
html-xml-lang-mismatch<html> element has an [xml:lang] attribute with the same base language as the [lang] attribute.
identical-links-same-purposeIdentical links have the same purpose.
image-altImage elements have [alt] attributes
image-redundant-altImage elements do not have [alt] attributes that are redundant text.
input-button-nameInput buttons have discernible text.
input-image-alt<input type="image"> elements have [alt] text
label-content-name-mismatchElements with visible text labels have matching accessible names.
labelForm elements have associated labels
landmark-one-mainDocument has a main landmark.
link-nameLinks do not have a discernible name
link-in-text-blockLinks are distinguishable without relying on color.
listLists contain only <li> elements and script supporting elements (<script> and <template>).
listitemList items (<li>) are contained within <ul>, <ol> or <menu> parent elements
meta-refreshThe document does not use <meta http-equiv="refresh">
meta-viewport[user-scalable="no"] is not used in the <meta name="viewport"> element and the [maximum-scale] attribute is not less than 5.
object-alt<object> elements have alternate text
select-nameSelect elements have associated label elements.
skip-linkSkip links are focusable.
tabindexNo element has a [tabindex] value greater than 0
table-duplicate-nameTables have different content in the summary attribute and <caption>.
table-fake-captionTables use <caption> instead of cells with the [colspan] attribute to indicate a caption.
target-sizeTouch targets do not have sufficient size or spacing.
td-has-header<td> elements in a large <table> have one or more table headers.
td-headers-attrCells in a <table> element that use the [headers] attribute refer to table cells within the same table.
th-has-data-cells<th> elements and elements with [role="columnheader"/"rowheader"] have data cells they describe.
valid-lang[lang] attributes have a valid value
video-caption<video> elements contain a <track> element with [kind="captions"]
custom-controls-labelsCustom controls have associated labels
custom-controls-rolesCustom controls have ARIA roles
focus-trapsUser focus is not accidentally trapped in a region
focusable-controlsInteractive controls are keyboard focusable
interactive-element-affordanceInteractive elements indicate their purpose and state
logical-tab-orderThe page has a logical tab order
managed-focusThe user’s focus is directed to new content added to the page
offscreen-content-hiddenOffscreen content is hidden from assistive technology
use-landmarksHTML5 landmark elements are used to improve navigation
visual-order-follows-domVisual order on the page follows DOM order
uses-long-cache-ttlServe static assets with an efficient cache policy
total-byte-weightAvoids enormous network payloads
offscreen-imagesDefer offscreen images
render-blocking-resourcesEliminate render-blocking resources
unminified-cssMinify CSS
unminified-javascriptMinify JavaScript
unused-css-rulesReduce unused CSS
unused-javascriptReduce unused JavaScript
modern-image-formatsServe images in next-gen formats
uses-optimized-imagesEfficiently encode images
uses-text-compressionEnable text compression
uses-responsive-imagesProperly size images
efficient-animated-contentUse video formats for animated content
duplicated-javascriptRemove duplicate modules in JavaScript bundles
legacy-javascriptAvoid serving legacy JavaScript to modern browsers
doctypePage has the HTML doctype
charsetProperly defines charset
dom-sizeAvoids an excessive DOM size
geolocation-on-startAvoids requesting the geolocation permission on page load
inspector-issuesNo issues in the Issues panel in Chrome Devtools
no-document-writeAvoids document.write()
js-librariesDetected JavaScript libraries
notification-on-startAvoids requesting the notification permission on page load
paste-preventing-inputsAllows users to paste into input fields
uses-http2Use HTTP/2
uses-passive-event-listenersUses passive listeners to improve scrolling performance
meta-descriptionDocument does not have a meta description
http-status-codePage has successful HTTP status code
font-sizeDocument uses legible font sizes
link-textLinks have descriptive text
crawlable-anchorsLinks are crawlable
is-crawlablePage isn’t blocked from indexing
robots-txtrobots.txt is not valid
hreflangDocument has a valid hreflang
canonicalDocument has a valid rel=canonical
structured-dataStructured data is valid
bf-cachePage prevented back/forward cache restoration
Last modified December 16, 2025: fix image links (4abbede9)