Visibility, color and overlay extraction (jsExtractor.js)
Asqatasun injects JavaScript extractors into the rendered page via Selenium to capture runtime information that cannot be derived from static HTML alone: element visibility, computed foreground/background colors, image overlays and focus state. This page documents the full logic of these extractors.
Overview
Two JavaScript files work together during page load:
| File | Purpose | Used by |
|---|---|---|
jsExtractor.js |
Text nodes + colors + overlays + focus state | Color contrast rules (3.2.x, 3.3.x) |
visibilityExtractor.js |
Full visibility map with reason tracking | Scenario audits |
Both run before driver.pageSource is captured — this ordering is critical
because the scripts may trigger scrollIntoView and focus/blur, and the
CSS paths they produce must match the saved HTML
(ScenarioLoaderImpl.kt proceedNewResult).
Output
For each traversed DOM element, jsExtractor.js emits a JSON object:
{
"path": "body > div:eq(0) > p:eq(2)",
"bgColor": "rgb(255; 255; 255)",
"color": "rgb(0; 0; 0)",
"fontSize": "16px",
"fontWeight": "400",
"textAlign": "start",
"isHidden": false,
"isTextNode": true,
"hasPotentialImageOverlay": false,
"isFocusable": false
}When an overlay is detected, three extra fields are populated:
overlayImageType, overlayImageSrc, and hasPotentialImageOverlay: true.
When the element is focusable, three focus-related fields are added:
outlineColorFocus, outlineStyleFocus, outlineWidthFocus.
1. Element traversal
The traversal starts at <body> and walks children recursively
(extractInfo).
Two guards prevent runaway recursion:
MAX_DEPTH = 500— stops traversal on abnormally deep DOMs- Null / missing
tagNamecheck — defensive against malformed nodes
Excluded tags
isAllowedElement()
returns false for tags that never carry visible text content or whose content
is handled by other rules:
| Tag | Reason |
|---|---|
<script>, <noscript>, <style>, <meta>, <link> |
Not rendered |
<head>, <title> |
Document metadata |
<svg> |
Handled by image rules, not by text contrast |
<br> |
No text content |
<option> |
Not rendered as inline text |
Text node detection
isTextNode()
identifies elements that carry text users can actually read:
<input type="text">— the user types text into it<img alt="...">with a non-emptyalt— the alternative text is “visible” to assistive technologies- Any element with at least one direct
#textchild whosetextContent(after trimming) is non-empty
<iframe>, <noscript>, <script>, <style> are explicitly excluded.
The isTextNode flag is important downstream: overlay detection
(§ 4) only runs on text nodes, and the Java
ContrastChecker only applies WCAG ratios to text nodes.
2. Hidden element detection
isHidden()
combines several CSS-level and visual-level criteria. An element is considered
hidden if any of the following is true, OR if its parent is hidden
(recursive check up to MAX_DEPTH).
Rendering-level criteria
| Criterion | CSS / HTML | Description |
|---|---|---|
| hidden attribute | <element hidden> |
HTML5 standard boolean attribute |
| display: none | display: none |
Not rendered in layout |
| visibility: hidden | visibility: hidden |
Invisible, occupies space |
| visibility: collapse | visibility: collapse |
Table row/column collapsed |
Transparent foreground with no visible effect
Lines 155-173.
color: transparent usually hides text, but there are legitimate CSS tricks to
make text visible despite a transparent color. The script detects these and
keeps the element visible in those cases:
| Property | Rescue condition |
|---|---|
-webkit-text-stroke-width |
Non-zero stroke width paints the text outline |
text-shadow |
A shadow paints the text glyph |
-webkit-text-fill-color |
Non-transparent fill color |
background-clip: text |
Solid or image background clipped to text glyphs |
Visually imperceptible patterns (sr-only / visually-hidden)
isVisuallyImperceptible()
detects the W3C / Bootstrap / Tailwind pattern used to hide content visually
while keeping it available to screen readers. These elements cannot display
perceivable text and are treated as hidden.
| Pattern | Detection |
|---|---|
| Bounding rect ≤ 0 | rect.width <= 0 or rect.height <= 0 |
| Too small to display a glyph | rect.width <= 3 AND rect.height <= 3 |
| Positioned far off-screen | rect.right < -1000 or rect.bottom < -1000 or rect.left > 10000 or rect.top > 10000 |
clip: rect(0,0,0,0) |
Matches rect(0, 0, 0, 0) with any spacing |
clip: rect(1px,1px,1px,1px) |
The older sr-only variant |
clip-path: inset(50%) |
Modern sr-only variant |
clip-path: inset(100%) |
Full inset variant |
Without this check, tracking pixels, skip links, screen-reader-only labels and other 1×1 elements produced false positives in rule 3.2.1 (contrast).
Parent inheritance
When an element itself is not hidden, the check walks up to its parent
(lines 181-186).
A child of a display: none ancestor is hidden even if it has
display: block itself. Traversal stops at <html> or when MAX_DEPTH is
reached.
3. Color extraction
Background color
getBackgroundColor()
returns one of three values:
"background-image:url(...)"when abackground-imageis set — the contrast checker cannot compute a ratio in this case and raises NMI"rgb(r; g; b)"when a solidbackground-coloris set (commas are replaced with;to simplify JSON parsing — seeformatColor())"transparent"or"rgba(0; 0; 0; 0)"when no background is defined
When the computed value is transparent, the extractor substitutes the
parent’s resolved background color so the contrast checker sees the color
that will actually be painted behind the text
(lines 463-473).
If no parent has an opaque background, the root <body> falls back to white
rgb(255; 255; 255) — this matches the browser’s default rendering.
Foreground color
getForegroundColor()
simply reads the computed color property and normalizes commas.
Typography
fontSize, fontWeight and textAlign are captured via getComputedStyle.
These feed the ContrastChecker thresholds — normal text is checked at 4.5:1
below 24 px; bold text at 3:1 below 18.5 px.
4. Overlay image detection
Text on top of images or gradients has no computable contrast ratio. Rather
than marking every transparent-bg text as FAILED, jsExtractor.js tries two
heuristics to detect images “behind” the text and routes such elements to NMI
instead.
4.1 Browser hit-testing
checkImageBehind()
uses document.elementsFromPoint() at 5 points on the element (center +
4 inset corners):
- Skip if bounding rect is 0 or ≤ 1 px (already covered by
isVisuallyImperceptible, but redundancy is cheap) - Skip if the element is far off-screen
- If the element is outside the viewport,
scrollIntoView({block: 'center'})is called to make it hit-testable - For each point, walk the element stack returned by
elementsFromPoint:- Skip self and its descendants (
elem.contains(el)) - Skip ancestors (
el.contains(elem)) - Match
<img>,<picture>,<video>,<canvas>, or any element with abackground-image: url(...)
- Skip self and its descendants (
- Return
{type, src}on first match
Only called when isTextNode is true AND the computed bgColor is transparent
(line 468).
4.2 Pseudo-element sibling overlays
checkPseudoOverlay()
catches a pattern elementsFromPoint cannot see: a sibling <div> with
::before / ::after pseudo-elements containing a gradient or image that
overlays text. Walks up to 5 levels up the parent chain and inspects siblings
for ::before/::after with content, absolute/fixed positioning, and
background. Pure DOM traversal — no reflow, no side effects.
Only called on text nodes when the primary overlay detection returned nothing (line 480-482).
4.3 Decision flow
The Java
ContrastChecker.createRemarkOnBadContrastElement
uses the overlay fields like this:
| Condition | Outcome | Message |
|---|---|---|
isHidden |
NMI | BadContrastHiddenElement |
hasPotentialImageOverlay |
NMI | BadContrastPositionedElementWithImageInContext |
Contrast ratio exactly 1.00 (fg == bg) on visible element |
NMI | SuspectContrastCheckImageBehindText — heuristic: text invisibly matching its background is likely meant to be on an image the hit-test missed |
| Alternative contrast mechanism enabled | NMI | BadContrastButAlternativeContrastMechanismOnPage |
| Default | FAILED | BadContrast |
See
OVERLAY_DETECTION_NOTES.md
for the design rationale and abandoned approaches.
5. Focusability
isPotentiallyFocusable()
is a performance gate: focus() / blur() calls trigger layout and event
firing, so they are only issued on elements that can actually take focus:
<a href>/<area href><button>,<input>,<select>,<textarea><details>,<summary>- Any element with
tabindex - Any
contenteditableelement
When focus is accepted (document.activeElement === elem), the focus-state
outline is captured (outline-color, outline-style, outline-width) for use
by RGAA rule 10.7 (focus indicator).
6. Path building
buildPath()
produces a jQuery-style path such as body > div:eq(0) > p:eq(2). The path
is the contract between the JS extractor and the Java side: after loading,
DomElementExtractor
uses SSPHandler.domCssLikeSelectNodeSet(path) to re-locate the element in
the jsoup-parsed HTML.
This is why script execution must happen before pageSource capture: any
DOM mutation after extraction (e.g. from scrollIntoView triggering lazy
rendering frameworks) would invalidate the paths.
7. JSON escaping
escapeForJson()
escapes backslashes, quotes, newlines, carriage returns and tabs before
values are concatenated into the result array. Without this, URLs containing
quotes in background-image:url("..."), or text content with special
characters, would break JSON parsing in
DomElementExtractor.
8. Observability
The extractor logs execution stats (lines 554-564) captured by Selenium’s console log handler:
jsExtractor stats: time=42ms, maxDepthReached=18,
elementsProcessed=1234, focusOperations=56The first entry of the result array is a _stats object with the same
information for Kotlin-side parsing.
When -Dasqatasun.debug.domelement=true is set, a detailed overlay trace is
written to /tmp/asqatasun-debug/{auditId}_{url}_overlayDebug.json by
ScenarioLoaderImpl.saveDebugSource, capturing the element stack returned by
elementsFromPoint at each check. See
DEBUG_OVERLAY
in the source.
Related
- Components — Engine architecture overview
- Rule 3.2.1 — Text contrast (4.5:1)
- Rule 3.3.1 — UI component contrast (3:1)
- WCAG 2.1 contrast ratio definition