Accessibility Scan Workshop

Learn to scan web applications for WCAG 2.2 accessibility violations using axe-core, IBM Equal Access, and custom Playwright checks.

View the Project on GitHub devopsabcs-engineering/accessibility-scan-workshop

Lab 04: Custom Playwright Checks — Manual Inspection Automation

   
Duration 35 minutes
Level Intermediate
Prerequisites Lab 01

Learning Objectives

By the end of this lab, you will be able to:

Exercises

Exercise 4.1: Review Custom Checks Source

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.

  1. Open the custom checks source file in your editor:

    src/lib/scanner/custom-checks.ts
    
  2. Review the existing checks:

    Check Function What It Detects WCAG Criterion
    checkAmbiguousLinkText Links with vague text like “click here,” “read more,” or “learn more” 2.4.4 Link Purpose
    checkAriaCurrentPage Navigation elements missing aria-current="page" on the active link 1.3.1 Info and Relationships
    checkEmphasisStrongSemantics Presentational use of <b> / <i> instead of semantic <strong> / <em> 1.3.1 Info and Relationships
    checkDiscountPriceAccessibility Prices marked with strikethrough (<del> / <s>) missing screen reader context 1.1.1 Non-text Content
    checkStickyElementOverlap Sticky headers or footers that could overlap content when scrolling 2.4.11 Focus Not Obscured
  3. Note the check function pattern. Each function:

    • Takes a Playwright Page object
    • Returns a CustomCheckResult or null (null if no violation found)
    • Uses page.evaluate() to query the DOM
    • Includes impact level, help text, and affected element selectors

    Custom checks source code

Exercise 4.2: Run Scanner with Custom Checks

You will run a scan that includes custom checks and examine the additional findings.

  1. 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
    
  2. Open results/demo-001-custom.json and search for findings with the custom- prefix in their rule IDs. These are the custom check results.

  3. 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

    Custom check results

[!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.

Exercise 4.3: Understand Keyboard Navigation Testing

Many accessibility issues only appear during keyboard interaction. You will review how the scanner tests keyboard accessibility.

  1. 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 Tab key and does nothing, trapping keyboard users on the page.

  2. Additionally, all interactive elements (buttons) are implemented as <div> elements with onclick handlers instead of <button> elements:

    <!-- Inaccessible -->
    <div class="btn" onclick="bookFlight()">Book Now</div>
    
    <!-- Accessible -->
    <button onclick="bookFlight()">Book Now</button>
    
  3. The scanner’s custom checks can detect some keyboard issues by:

    • Evaluating whether interactive elements have proper roles
    • Checking for tabindex on non-interactive elements used as controls
    • Detecting event listeners that suppress default keyboard behaviour

    Keyboard navigation testing

[!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.

Exercise 4.4: Write a New Custom Check

You will create a custom check to detect <marquee> elements, which are deprecated and cause WCAG 2.3.1 violations.

  1. Open src/lib/scanner/custom-checks.ts in your editor.

  2. Add a new check function before the runCustomChecks function:

    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,
        })),
      };
    }
    

    New custom check code

  3. Add the new check to the runCustomChecks function’s check array:

    const checks = [
      checkAmbiguousLinkText,
      checkAriaCurrentPage,
      checkEmphasisStrongSemantics,
      checkDiscountPriceAccessibility,
      checkStickyElementOverlap,
      checkDeprecatedMarquee,  // Add this line
    ];
    
  4. Save the file.

Exercise 4.5: Run Updated Scanner

You will verify that your new custom check detects the <marquee> element in demo app 001.

  1. 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
    
  2. Search the output for custom-deprecated-marquee:

    grep "custom-deprecated-marquee" results/demo-001-marquee.json
    

    On PowerShell:

    Select-String -Path results/demo-001-marquee.json -Pattern "custom-deprecated-marquee"
    
  3. The check should detect the <marquee> element that demo app 001 uses for its scrolling banner.

    New check results

[!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.

Verification Checkpoint

Before proceeding, verify:

Next Steps

Proceed to Lab 05: SARIF Output and GitHub Security Tab.