| permalink | /labs/lab-04 |
|---|---|
| title | Lab 04: Custom Playwright Checks — Manual Inspection Automation |
| description | Extend scanner coverage with custom Playwright-based accessibility checks beyond automated engines. |
| Duration | 35 minutes |
| Level | Intermediate |
| Prerequisites | Lab 01 |
By the end of this lab, you will be able to:
- Explain why custom checks are needed beyond axe-core and IBM Equal Access
- Review existing custom checks in the scanner source code
- Understand how Playwright tests keyboard navigation and focus management
- Write a new custom check to detect deprecated HTML elements
- Run the updated scanner and verify the new check produces findings
Automated engines like axe-core cannot catch every accessibility issue. The scanner includes custom Playwright-based checks for issues that require DOM interaction or visual inspection.
-
Open the custom checks source file in your editor:
src/lib/scanner/custom-checks.ts -
Review the existing checks:
Check Function What It Detects WCAG Criterion checkAmbiguousLinkTextLinks with vague text like "click here," "read more," or "learn more" 2.4.4 Link Purpose checkAriaCurrentPageNavigation elements missing aria-current="page"on the active link1.3.1 Info and Relationships checkEmphasisStrongSemanticsPresentational use of <b>/<i>instead of semantic<strong>/<em>1.3.1 Info and Relationships checkDiscountPriceAccessibilityPrices marked with strikethrough ( <del>/<s>) missing screen reader context1.1.1 Non-text Content checkStickyElementOverlapSticky headers or footers that could overlap content when scrolling 2.4.11 Focus Not Obscured -
Note the check function pattern. Each function:
- Takes a Playwright
Pageobject - Returns a
CustomCheckResultornull(null if no violation found) - Uses
page.evaluate()to query the DOM - Includes impact level, help text, and affected element selectors
- Takes a Playwright
You will run a scan that includes custom checks and examine the additional findings.
-
Scan demo app 001 with the scanner (custom checks run automatically):
npx ts-node src/cli/commands/scan.ts --url http://localhost:8001 --format json --output results/demo-001-custom.json
-
Open
results/demo-001-custom.jsonand search for findings with thecustom-prefix in their rule IDs. These are the custom check results. -
You should see findings for:
- Ambiguous link text — Demo app 001 uses "click here" links throughout
- Missing aria-current — The navigation bar does not mark the active page
Note
Custom checks complement automated engines. axe-core checks link-name (whether a link has accessible text at all), while the custom check checkAmbiguousLinkText goes further to flag links that have text but the text is not descriptive enough.
Many accessibility issues only appear during keyboard interaction. You will review how the scanner tests keyboard accessibility.
-
The demo apps include a deliberate keyboard trap. Demo app 001 contains this JavaScript:
document.addEventListener('keydown', function(e) { if (e.key === 'Tab') { } });
This intercepts the
Tabkey and does nothing, trapping keyboard users on the page. -
Additionally, all interactive elements (buttons) are implemented as
<div>elements withonclickhandlers instead of<button>elements:<!-- Inaccessible --> <div class="btn" onclick="bookFlight()">Book Now</div> <!-- Accessible --> <button onclick="bookFlight()">Book Now</button>
-
The scanner's custom checks can detect some keyboard issues by:
- Evaluating whether interactive elements have proper roles
- Checking for
tabindexon non-interactive elements used as controls - Detecting event listeners that suppress default keyboard behaviour
Tip
For manual keyboard testing, press Tab to move forward, Shift+Tab to move backward, Enter to activate buttons and links, and Space to toggle checkboxes and buttons. Every interactive element should be reachable and operable via keyboard alone.
You will create a custom check to detect <marquee> elements, which are deprecated and cause WCAG 2.3.1 violations.
-
Open
src/lib/scanner/custom-checks.tsin your editor. -
Add a new check function before the
runCustomChecksfunction:async function checkDeprecatedMarquee(page: Page): Promise<CustomCheckResult | null> { const marquees = await page.evaluate(() => { const elements = document.querySelectorAll('marquee'); if (elements.length === 0) return null; return Array.from(elements).map((el) => ({ selector: 'marquee', html: el.outerHTML.substring(0, 200), })); }); if (!marquees) return null; return { id: 'custom-deprecated-marquee', impact: 'serious', description: 'Page contains deprecated <marquee> elements that cause distracting motion', help: 'Remove <marquee> elements and use CSS animations with prefers-reduced-motion support instead', helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html', wcag: ['2.2.2', '2.3.1'], nodes: marquees.map((m) => ({ target: [m.selector], html: m.html, })), }; }
-
Add the new check to the
runCustomChecksfunction's check array:const checks = [ checkAmbiguousLinkText, checkAriaCurrentPage, checkEmphasisStrongSemantics, checkDiscountPriceAccessibility, checkStickyElementOverlap, checkDeprecatedMarquee, // Add this line ];
-
Save the file.
You will verify that your new custom check detects the <marquee> element in demo app 001.
-
Run the scanner against demo app 001:
npx ts-node src/cli/commands/scan.ts --url http://localhost:8001 --format json --output results/demo-001-marquee.json
-
Search the output for
custom-deprecated-marquee:grep "custom-deprecated-marquee" results/demo-001-marquee.jsonOn PowerShell:
Select-String -Path results/demo-001-marquee.json -Pattern "custom-deprecated-marquee"
-
The check should detect the
<marquee>element that demo app 001 uses for its scrolling banner.
Warning
Revert your changes to custom-checks.ts after this exercise if you do not want to keep the custom check, or commit the change to your fork. The remaining labs use the original scanner code.
Before proceeding, verify:
- Reviewed the existing custom checks in
custom-checks.ts - Ran a scan and identified custom check findings in the output
- Can explain why custom checks complement automated engines
- Successfully wrote and tested a new custom check for
<marquee>elements - New check produced findings when scanning demo app 001
Proceed to Lab 05: SARIF Output and GitHub Security Tab.




