[{"data":1,"prerenderedAt":5903},["ShallowReactive",2],{"content:/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations":3,"category:/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations":6,"read-next:/software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan":4856},{"id":4,"title":5,"bmcUsername":6,"body":7,"cover":4846,"date":4847,"description":4848,"draft":4849,"extension":4850,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":4851,"navigation":743,"npmPackage":6,"order":6,"path":4852,"seo":4853,"stem":4854,"__hash__":4855},"content/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations.md","Playwright Accessibility Testing: What axe and Lighthouse Miss",null,{"type":8,"value":9,"toc":4816},"minimark",[10,19,22,25,30,33,109,112,115,117,121,124,127,159,162,173,179,182,184,188,193,212,215,222,235,249,252,254,258,269,276,283,288,358,365,372,374,385,393,398,409,415,526,595,606,608,612,619,629,637,648,654,656,660,663,683,689,696,806,809,814,816,820,823,826,838,850,853,863,956,958,962,965,968,994,997,1005,1008,1090,1108,1116,1118,1122,1132,1135,1141,1213,1216,1225,1227,1231,1238,1249,1252,1301,1306,1308,1312,1323,1326,1329,1370,1375,1378,2397,2399,2403,2410,2413,2417,2598,2602,2605,2777,2781,2784,2955,2959,2962,3257,3261,3264,3516,3520,3523,3853,3857,3860,4138,4142,4145,4388,4392,4395,4714,4716,4720,4726,4729,4787,4790,4792,4796,4802,4805,4808,4812],[11,12,13,14,18],"p",{},"Automated accessibility tools like axe, Lighthouse, and Playwright's ",[15,16,17],"code",{},"@axe-core/playwright"," integration only catch an estimated 30–40% of real WCAG violations. That gap is dangerous in an environment where developers are already suffering tool fatigue managing a queue of linters, security scanners, and CI checks competing for attention. When an AI assistant suggests a fix that turns a failing accessibility check green, it's tempting to accept it and move on — the fix looks reasonable, the build passes, and the queue gets shorter.",[11,20,21],{},"But greening the scanner isn't the same as fixing the problem. AI-suggested fixes are generated from patterns. Consequently, they may satisfy the scanner without addressing the underlying accessibility intent. Manual accessibility testing and context awareness are still essential to verify that detected violations aren't false positives, that proposed fixes are appropriate for the actual context, and to catch the 60–70% of accessibility bugs that automated tools never surface at all.",[23,24],"hr",{},[26,27,29],"h2",{"id":28},"automated-accessibility-testing-coverage-what-the-research-shows","Automated Accessibility Testing Coverage: What the Research Shows",[11,31,32],{},"This isn't a criticism of axe (Deque), WAVE (WebAIM), or the tools that leverage their engine like Lighthouse and Playwright. The vendors themselves and independent research agree: there are structural limits to what any automated engine can evaluate. The exact figures vary across sources, but the conclusion is consistent:",[34,35,36,49],"table",{},[37,38,39],"thead",{},[40,41,42,46],"tr",{},[43,44,45],"th",{},"Source",[43,47,48],{},"Finding",[50,51,52,65,76,87,98],"tbody",{},[40,53,54,62],{},[55,56,57],"td",{},[58,59],"external-link",{"href":60,"text":61},"https://webaim.org/projects/million/","WebAIM",[55,63,64],{},"~30% of real WCAG failures are detectable by automation",[40,66,67,73],{},[55,68,69],{},[58,70],{"href":71,"text":72},"https://www.w3.org/WAI/test-evaluate/","W3C / WAI",[55,74,75],{},"~20–30% of WCAG Success Criteria are fully automatable",[40,77,78,84],{},[55,79,80],{},[58,81],{"href":82,"text":83},"https://www.section508.gov/test/","U.S. GSA – Section 508 Program",[55,85,86],{},"Some materials state 1 in 3 issues are caught by automated accessibility tests",[40,88,89,95],{},[55,90,91],{},[58,92],{"href":93,"text":94},"https://www.deque.com/automated-accessibility-coverage-report/","Deque (axe)",[55,96,97],{},"57.38% of known violations detected when axe-core was run against a large sample of real-world audited pages",[40,99,100,106],{},[55,101,102],{},[58,103],{"href":104,"text":105},"https://accessible.org/automated-scans-wcag/","Accessible.org",[55,107,108],{},"13% fully automatable, 45% partially, 42% not automatable (WCAG 2.2 AA)",[11,110,111],{},"Deque arrived at the 57.38% figure by taking a more pragmatic, real-world approach rather than a theoretical one. They sampled a large number of sites and measured how many of the actual documented accessibility defects would have been detected using axe-core. This is good news: in a practical sense, automated accessibility tools find a higher percentage of reported defects than the theoretical estimates suggest. However, even with this optimistic approach, it shows a 43% gap — proving you still need a mix of both manual and automated testing.",[11,113,114],{},"Guided manual accessibility audits — also called semi-automated testing, or Intelligent Guided Testing (IGT) in Deque's platform — sit between fully manual and fully automated testing, narrowing the coverage gap. Some estimates put coverage as high as 80% with this approach. Guided audits are typically a premium service that uses AI and machine learning to filter what automation can handle, then provides a structured checklist to walk the tester through the remaining checks that still require human judgment.",[23,116],{},[26,118,120],{"id":119},"what-automated-accessibility-tools-reliably-detect","What Automated Accessibility Tools Reliably Detect",[11,122,123],{},"Before covering the limitations and gaps let's acknowledge what automated tool checks are good at so we know when to use them to complement accessibility testing.",[11,125,126],{},"Automated tools reliably catch mechanical rule violations:",[128,129,130,138,141,144,153,156],"ul",{},[131,132,133,134,137],"li",{},"Missing ",[15,135,136],{},"alt"," attributes on images",[131,139,140],{},"Missing form labels",[131,142,143],{},"Insufficient color contrast ratios on static text",[131,145,133,146,149,150],{},[15,147,148],{},"lang"," attribute on ",[15,151,152],{},"\u003Chtml>",[131,154,155],{},"Missing document title",[131,157,158],{},"Invalid ARIA roles and attribute values",[11,160,161],{},"These are structural checks — things that can be evaluated by reading the DOM without understanding intent or context. They're also exactly the kind of thing that's easy to accidentally break in a refactor.",[11,163,164,168,169,172],{},[165,166,167],"strong",{},"Regression prevention"," is where automated checks earn their place in a CI pipeline. Accessibility defects don't announce themselves the way functional bugs do. A UI change that removes an ",[15,170,171],{},"aria-label",", breaks focus order, or drops a landmark region won't throw an error or cause a visual test failure without an explicit check. A Playwright accessibility scan catches these silently introduced regressions before they ship — something a periodic manual review will inevitably miss between cycles.",[11,174,175,178],{},[165,176,177],{},"Surface area at scale"," is the other advantage. A Playwright accessibility scan covers every page in your test suite on every CI run. A thorough manual pass takes hours and realistically happens infrequently. Automated checks don't replace the manual pass — they cover ground at a speed and scale no manual accessibility audit can match.",[11,180,181],{},"The goal isn't to position automated and manual testing against each other. Each contributes something the other can't. Automated checks are the always-on floor; manual review is the deeper, periodic pass that catches what automation is structurally unable to evaluate.",[23,183],{},[26,185,187],{"id":186},"what-automated-accessibility-checks-cant-evaluate","What Automated Accessibility Checks Can't Evaluate",[189,190,192],"h3",{"id":191},"_1-ambiguous-link-text","1. Ambiguous Link Text",[11,194,195,196,200,201,200,204,207,208,211],{},"Links like ",[197,198,199],"em",{},"Read more",", ",[197,202,203],{},"Click here",[197,205,206],{},"Browse all",", and ",[197,209,210],{},"Learn more"," all pass automated checks without complaint. There's nothing structurally wrong with them — they're valid anchor elements with text content.",[11,213,214],{},"The problem is context. Screen reader users frequently navigate a page by tabbing through links in isolation, without the surrounding paragraph to provide meaning. Hearing \"read more\" three times in a row tells you nothing about where each link goes.",[11,216,217,218,221],{},"Automated tools can't evaluate whether link text is ",[197,219,220],{},"meaningful"," — only whether it exists. A page full of \"Read more\" links passes axe just as cleanly as a page with fully descriptive link text.",[11,223,224,227,228,230,231,234],{},[165,225,226],{},"Fix:"," Write descriptive link text that makes sense out of context — \"Read more about Playwright accessibility testing\" rather than \"Read more.\" When that's not possible due to design constraints, use ",[15,229,171],{}," to provide the full context: ",[15,232,233],{},"aria-label=\"Read more about Playwright accessibility testing\"",".",[11,236,237,240,241,244,245,248],{},[165,238,239],{},"Fix (static analysis):"," For React projects, ",[15,242,243],{},"eslint-plugin-jsx-a11y"," includes a built-in ",[15,246,247],{},"anchor-ambiguous-text"," rule. For other stacks, ESLint accessibility plugins exist for Vue and HTML but may require a custom rule for this specific check — the Playwright test later in this article is the framework-agnostic alternative.",[11,250,251],{},"Later in this article, I'll show a Playwright test pattern to catch this in your UI tests — especially useful when link text is generated dynamically and static analysis can't reach it.",[23,253],{},[189,255,257],{"id":256},"_2-missing-skip-navigation-links","2. Missing Skip Navigation Links",[11,259,260,261,264,265,268],{},"Landmark elements like ",[15,262,263],{},"\u003Cmain>"," and ",[15,266,267],{},"\u003Cnav>"," satisfy axe's bypass-block check (WCAG 2.4.1). The rule is technically met — there's a mechanism to skip repeated content.",[11,270,271,272,275],{},"But keyboard-only users who ",[197,273,274],{},"don't"," use a screen reader still tab through every item in the header before reaching the main content. Landmarks help screen reader users jump between regions; they don't help a keyboard user skip a 12-item navigation menu on every page load.",[11,277,278,279,282],{},"A skip link — a visually hidden anchor as the first focusable element that jumps to ",[15,280,281],{},"#main-content"," — serves both groups. Landmarks alone don't.",[11,284,285,287],{},[165,286,226],{}," Add a skip link as the first focusable element on every page, even if your landmark structure is correct.",[289,290,295],"pre",{"className":291,"code":292,"language":293,"meta":294,"style":294},"language-html shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","\u003Ca href=\"#main-content\" class=\"sr-only focus:not-sr-only\">\n  Skip to main content\n\u003C/a>\n","html","",[15,296,297,341,348],{"__ignoreMap":294},[298,299,302,306,310,314,317,321,324,326,329,331,333,336,338],"span",{"class":300,"line":301},"line",1,[298,303,305],{"class":304},"sPJuK","\u003C",[298,307,309],{"class":308},"saWzx","a",[298,311,313],{"class":312},"sM74w"," href",[298,315,316],{"class":304},"=",[298,318,320],{"class":319},"sZi47","\"",[298,322,281],{"class":323},"srGNg",[298,325,320],{"class":319},[298,327,328],{"class":312}," class",[298,330,316],{"class":304},[298,332,320],{"class":319},[298,334,335],{"class":323},"sr-only focus:not-sr-only",[298,337,320],{"class":319},[298,339,340],{"class":304},">\n",[298,342,344],{"class":300,"line":343},2,[298,345,347],{"class":346},"sZ-rw","  Skip to main content\n",[298,349,351,354,356],{"class":300,"line":350},3,[298,352,353],{"class":304},"\u003C/",[298,355,309],{"class":308},[298,357,340],{"class":304},[11,359,360,361,364],{},"Below is a screenshot showing how it works on this site. When a keyboard-only user visits, the first focusable element is a ",[197,362,363],{},"Skip to main content"," link that jumps them directly to the main body — saving them from pressing Tab 9 times to cycle through the header elements (crossed out in red) and social links before reaching the page's main content (highlighted in green).",[11,366,367],{},[368,369],"img",{"alt":370,"src":371},"Screenshot of www.davidmello.com showing the skip link focused at the top of the page, header navigation elements marked in red to bypass, and the main content area highlighted in green as the destination","images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/skip-link-example.webp",[23,373],{},[189,375,377,378,381,382,384],{"id":376},"_3-aria-labelledby-vs-aria-label-both-pass-one-is-better","3. ",[15,379,380],{},"aria-labelledby"," vs ",[15,383,171],{}," — Both Pass, One Is Better",[11,386,387,388,264,390,392],{},"Both ",[15,389,380],{},[15,391,171],{}," satisfy automated checks equally. The accessibility tools verify presence and valid syntax — they don't evaluate which approach is more appropriate for your context.",[11,394,395,397],{},[15,396,171],{}," strings are invisible — they exist only in the accessibility tree. This creates two problems:",[128,399,400,403],{},[131,401,402],{},"For sighted users who also use assistive technology, there can be a mismatch between what they see and what their screen reader announces",[131,404,405,406,408],{},"Translation tools frequently miss ",[15,407,171],{}," attributes, leaving non-English users with untranslated accessible names",[11,410,411,412,414],{},"When a visible heading or label already exists on the page, ",[15,413,380],{}," pointing to that element is almost always the better choice — it reuses the visible text so both representations stay in sync automatically.",[289,416,419],{"className":291,"code":417,"filename":418,"language":293,"meta":294,"style":294},"\u003Cbody>\n  \u003C!-- A visible header exists with text to label the section with, so aria-labelledby is correct here -->\n  \u003Csection aria-labelledby=\"aria-header\">\n    \u003Ch2 id=\"aria-header\">Using aria attributes correctly\u003C/h2>\n    \u003Cp>...\u003C/p>\n  \u003C/section>\n\u003C/body>\n","aria-labelledby-example.html",[15,420,421,430,436,458,489,507,517],{"__ignoreMap":294},[298,422,423,425,428],{"class":300,"line":301},[298,424,305],{"class":304},[298,426,427],{"class":308},"body",[298,429,340],{"class":304},[298,431,432],{"class":300,"line":343},[298,433,435],{"class":434},"s_gjE","  \u003C!-- A visible header exists with text to label the section with, so aria-labelledby is correct here -->\n",[298,437,438,441,444,447,449,451,454,456],{"class":300,"line":350},[298,439,440],{"class":304},"  \u003C",[298,442,443],{"class":308},"section",[298,445,446],{"class":312}," aria-labelledby",[298,448,316],{"class":304},[298,450,320],{"class":319},[298,452,453],{"class":323},"aria-header",[298,455,320],{"class":319},[298,457,340],{"class":304},[298,459,461,464,466,469,471,473,475,477,480,483,485,487],{"class":300,"line":460},4,[298,462,463],{"class":304},"    \u003C",[298,465,26],{"class":308},[298,467,468],{"class":312}," id",[298,470,316],{"class":304},[298,472,320],{"class":319},[298,474,453],{"class":323},[298,476,320],{"class":319},[298,478,479],{"class":304},">",[298,481,482],{"class":346},"Using aria attributes correctly",[298,484,353],{"class":304},[298,486,26],{"class":308},[298,488,340],{"class":304},[298,490,492,494,496,498,501,503,505],{"class":300,"line":491},5,[298,493,463],{"class":304},[298,495,11],{"class":308},[298,497,479],{"class":304},[298,499,500],{"class":346},"...",[298,502,353],{"class":304},[298,504,11],{"class":308},[298,506,340],{"class":304},[298,508,510,513,515],{"class":300,"line":509},6,[298,511,512],{"class":304},"  \u003C/",[298,514,443],{"class":308},[298,516,340],{"class":304},[298,518,520,522,524],{"class":300,"line":519},7,[298,521,353],{"class":304},[298,523,427],{"class":308},[298,525,340],{"class":304},[289,527,530],{"className":291,"code":528,"filename":529,"language":293,"meta":294,"style":294},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n\u003Cbutton aria-label=\"Close dialog\">\n  \u003Csvg aria-hidden=\"true\">...\u003C/svg>\n\u003C/button>\n","aria-label-example.html",[15,531,532,537,558,587],{"__ignoreMap":294},[298,533,534],{"class":300,"line":301},[298,535,536],{"class":434},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n",[298,538,539,541,544,547,549,551,554,556],{"class":300,"line":343},[298,540,305],{"class":304},[298,542,543],{"class":308},"button",[298,545,546],{"class":312}," aria-label",[298,548,316],{"class":304},[298,550,320],{"class":319},[298,552,553],{"class":323},"Close dialog",[298,555,320],{"class":319},[298,557,340],{"class":304},[298,559,560,562,565,568,570,572,575,577,579,581,583,585],{"class":300,"line":350},[298,561,440],{"class":304},[298,563,564],{"class":308},"svg",[298,566,567],{"class":312}," aria-hidden",[298,569,316],{"class":304},[298,571,320],{"class":319},[298,573,574],{"class":323},"true",[298,576,320],{"class":319},[298,578,479],{"class":304},[298,580,500],{"class":346},[298,582,353],{"class":304},[298,584,564],{"class":308},[298,586,340],{"class":304},[298,588,589,591,593],{"class":300,"line":460},[298,590,353],{"class":304},[298,592,543],{"class":308},[298,594,340],{"class":304},[11,596,597,599,600,602,603,605],{},[165,598,226],{}," Prefer ",[15,601,380],{}," when a visible text element can serve as the label. Reserve ",[15,604,171],{}," for cases where no visible text exists.",[23,607],{},[189,609,611],{"id":610},"_4-static-aria-labels-on-dynamic-controls","4. Static ARIA Labels on Dynamic Controls",[11,613,614,615,618],{},"A toggle button with ",[15,616,617],{},"aria-label=\"Switch to dark mode\""," passes automated checks regardless of the current state. The label is present and well-formed — but if the page is already in dark mode, the label is factually wrong. It should read \"Switch to light mode.\"",[11,620,621,622,264,625,628],{},"Automated tools check ",[197,623,624],{},"presence",[197,626,627],{},"format",", not accuracy or correctness over time. A static label on a stateful control is invisible to a scanner.",[11,630,631,633,634,636],{},[165,632,226],{}," Bind ",[15,635,171],{}," dynamically to reflect the current state:",[289,638,642],{"className":639,"code":640,"language":641,"meta":294,"style":294},"language-vue shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast",":aria-label=\"isDark ? 'Switch to light mode' : 'Switch to dark mode'\"\n","vue",[15,643,644],{"__ignoreMap":294},[298,645,646],{"class":300,"line":301},[298,647,640],{"class":346},[11,649,650,651,653],{},"Later in this article, I'll show a Playwright test that verifies a toggle's ",[15,652,171],{}," updates after interaction — catching stale labels before they ship.",[23,655],{},[189,657,659],{"id":658},"_5-low-quality-alt-text-and-aria-labels-automated-tools-cant-detect","5. Low Quality Alt Text and ARIA Labels Automated Tools Can't Detect",[11,661,662],{},"axe-core validates presence and format. It does not validate quality.",[11,664,665,200,668,207,671,674,675,678,679,682],{},[15,666,667],{},"aria-label=\"nav\"",[15,669,670],{},"aria-label=\"section 1\"",[15,672,673],{},"alt=\"image\""," all pass automated checks. So does ",[15,676,677],{},"role=\"button\""," on a ",[15,680,681],{},"\u003Cdiv>"," that isn't keyboard focusable. A screen reader user hearing \"image\" or \"section 1\" is no better off than they were before the attribute was added — in some cases they're worse off because the presence of the attribute signals to the developer that the defect has been addressed.",[11,684,685,686,688],{},"The same trap applies to landmarks: adding ",[15,687,171],{}," satisfies the rule, but a bad label is as useless as no label at all. Automated tools cannot ask \"does this label actually help someone who cannot see the page?\" That question requires a human.",[11,690,691,692,695],{},"The same principle extends to image alt text. ",[15,693,694],{},"alt=\"logo\""," on a company logo passes axe — but says nothing useful. The correct alt text depends on context that automation can't evaluate:",[289,697,699],{"className":291,"code":698,"language":293,"meta":294,"style":294},"\u003C!-- Standalone logo — identify the brand -->\n\u003Cimg src=\"logo.svg\" alt=\"Acme Corp\" />\n\n\u003C!-- Logo as a link — describe the destination, not the image -->\n\u003Ca href=\"/\">\n  \u003Cimg src=\"logo.svg\" alt=\"Acme Corp — Home\" />\n\u003C/a>\n",[15,700,701,706,739,745,750,769,798],{"__ignoreMap":294},[298,702,703],{"class":300,"line":301},[298,704,705],{"class":434},"\u003C!-- Standalone logo — identify the brand -->\n",[298,707,708,710,712,715,717,719,722,724,727,729,731,734,736],{"class":300,"line":343},[298,709,305],{"class":304},[298,711,368],{"class":308},[298,713,714],{"class":312}," src",[298,716,316],{"class":304},[298,718,320],{"class":319},[298,720,721],{"class":323},"logo.svg",[298,723,320],{"class":319},[298,725,726],{"class":312}," alt",[298,728,316],{"class":304},[298,730,320],{"class":319},[298,732,733],{"class":323},"Acme Corp",[298,735,320],{"class":319},[298,737,738],{"class":304}," />\n",[298,740,741],{"class":300,"line":350},[298,742,744],{"emptyLinePlaceholder":743},true,"\n",[298,746,747],{"class":300,"line":460},[298,748,749],{"class":434},"\u003C!-- Logo as a link — describe the destination, not the image -->\n",[298,751,752,754,756,758,760,762,765,767],{"class":300,"line":491},[298,753,305],{"class":304},[298,755,309],{"class":308},[298,757,313],{"class":312},[298,759,316],{"class":304},[298,761,320],{"class":319},[298,763,764],{"class":323},"/",[298,766,320],{"class":319},[298,768,340],{"class":304},[298,770,771,773,775,777,779,781,783,785,787,789,791,794,796],{"class":300,"line":509},[298,772,440],{"class":304},[298,774,368],{"class":308},[298,776,714],{"class":312},[298,778,316],{"class":304},[298,780,320],{"class":319},[298,782,721],{"class":323},[298,784,320],{"class":319},[298,786,726],{"class":312},[298,788,316],{"class":304},[298,790,320],{"class":319},[298,792,793],{"class":323},"Acme Corp — Home",[298,795,320],{"class":319},[298,797,738],{"class":304},[298,799,800,802,804],{"class":300,"line":519},[298,801,353],{"class":304},[298,803,309],{"class":308},[298,805,340],{"class":304},[11,807,808],{},"Both pass axe equally. Only one communicates the right thing in each context.",[11,810,811,813],{},[165,812,226],{}," Before adding any ARIA attribute, ask whether it communicates something meaningful to a user who can't see the visual context. If the answer is no, the attribute isn't a fix — it's noise. Apply the same question to alt text — describe the role the image plays on the page, not just what it depicts.",[23,815],{},[189,817,819],{"id":818},"_6-accessibility-regressions-automated-checks-wont-catch","6. Accessibility Regressions Automated Checks Won't Catch",[11,821,822],{},"Let's imagine a developer reads the WCAG documentation, applies the correct fix for an issue, and later a linter or well-meaning colleague, not having read the same best practice, \"fixes\" the fix, thinking the first developer made a mistake.",[11,824,825],{},"Two examples that come up regularly:",[11,827,828,833,834,837],{},[165,829,830],{},[15,831,832],{},"alt=\"\""," is actually the correct attribute for decorative images. It explicitly tells screen readers to skip the element. A linter that flags missing alt text, or a developer who sees an empty alt and assumes it's an oversight, changes it to ",[15,835,836],{},"alt=\"decorative image\"",". Now screen readers announce \"decorative image\" on every decorative element in the page — noise that wasn't there before.",[11,839,840,845,846,849],{},[165,841,842],{},[15,843,844],{},"aria-hidden=\"true\""," on icons inside labeled buttons is correct when the button's accessible name is provided by its visible text. Removing it and replacing it with ",[15,847,848],{},"aria-label=\"icon\""," passes tooling, but pollutes the accessible name computation and potentially overrides the button's actual label.",[11,851,852],{},"In both cases, a passing automated check actively masks a regression. The original state was more accessible than the \"fixed\" state.",[11,854,855,857,858,264,860,862],{},[165,856,226],{}," Treat ",[15,859,832],{},[15,861,844],{}," as intentional signals, not oversights. Add a comment explaining the intent so your team doesn't correct them:",[289,864,866],{"className":291,"code":865,"language":293,"meta":294,"style":294},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n\u003Cimg src=\"divider.svg\" alt=\"\" />\n\n\u003C!-- aria-hidden intentional: button label provided by visible text -->\n\u003Cbutton>\n  \u003Csvg aria-hidden=\"true\">...\u003C/svg>\n  Submit\n\u003C/button>\n",[15,867,868,873,899,903,908,916,942,947],{"__ignoreMap":294},[298,869,870],{"class":300,"line":301},[298,871,872],{"class":434},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n",[298,874,875,877,879,881,883,885,888,890,892,894,897],{"class":300,"line":343},[298,876,305],{"class":304},[298,878,368],{"class":308},[298,880,714],{"class":312},[298,882,316],{"class":304},[298,884,320],{"class":319},[298,886,887],{"class":323},"divider.svg",[298,889,320],{"class":319},[298,891,726],{"class":312},[298,893,316],{"class":304},[298,895,896],{"class":319},"\"\"",[298,898,738],{"class":304},[298,900,901],{"class":300,"line":350},[298,902,744],{"emptyLinePlaceholder":743},[298,904,905],{"class":300,"line":460},[298,906,907],{"class":434},"\u003C!-- aria-hidden intentional: button label provided by visible text -->\n",[298,909,910,912,914],{"class":300,"line":491},[298,911,305],{"class":304},[298,913,543],{"class":308},[298,915,340],{"class":304},[298,917,918,920,922,924,926,928,930,932,934,936,938,940],{"class":300,"line":509},[298,919,440],{"class":304},[298,921,564],{"class":308},[298,923,567],{"class":312},[298,925,316],{"class":304},[298,927,320],{"class":319},[298,929,574],{"class":323},[298,931,320],{"class":319},[298,933,479],{"class":304},[298,935,500],{"class":346},[298,937,353],{"class":304},[298,939,564],{"class":308},[298,941,340],{"class":304},[298,943,944],{"class":300,"line":519},[298,945,946],{"class":346},"  Submit\n",[298,948,950,952,954],{"class":300,"line":949},8,[298,951,353],{"class":304},[298,953,543],{"class":308},[298,955,340],{"class":304},[23,957],{},[189,959,961],{"id":960},"_7-automated-wcag-color-contrast-testing-limitations","7. Automated WCAG Color Contrast Testing Limitations",[11,963,964],{},"Lighthouse and axe now catch contrast violations for standard text on solid backgrounds — this is a genuine improvement and one of the areas where automated tooling has meaningfully advanced.",[11,966,967],{},"But gaps remain:",[128,969,970,976,982,988],{},[131,971,972,975],{},[165,973,974],{},"Text over images or gradients"," — axe-core may not be able to reliably report contrast issues when working with gradient or image backgrounds",[131,977,978,981],{},[165,979,980],{},"Semi-transparent backgrounds"," — the computed color may not be accurate",[131,983,984,987],{},[165,985,986],{},"Hover and focus states"," — the scan evaluates the element as it exists at scan time; insufficient contrast on focus rings or hover styles is invisible unless that state is active when the scan runs",[131,989,990,993],{},[165,991,992],{},"Dark mode"," — a scan in light mode won't catch dark mode contrast failures, and vice versa",[11,995,996],{},"Later in this article, I'll show Playwright tests that trigger hover, focus, and dark mode states before scanning — catching the contrast failures a default scan misses.",[11,998,999,1000,1004],{},"For the contrast issues automated tools do catch, a dedicated checker gives you more control than waiting for a CI scan. The ",[309,1001,1003],{"href":1002},"/tools/color-contrast-checker","WCAG Color Contrast Checker"," I built lets you test specific color pairs against AA and AAA thresholds, simulate how the combination appears across four types of colorblindness, and generate a ready-to-paste bug report including recommended closest-matching color fixes for your issue tracker.",[11,1006,1007],{},"For those harder-to-detect gradient issues, one approach to manually verify contrast over a gradient is to check against each end of the gradient. Given:",[289,1009,1013],{"className":1010,"code":1011,"language":1012,"meta":294,"style":294},"language-css shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast",".hero-text {\n  background: linear-gradient(to bottom, #ffffff, #000000);\n  color: #544f4f;\n}\n","css",[15,1014,1015,1027,1070,1085],{"__ignoreMap":294},[298,1016,1017,1020,1024],{"class":300,"line":301},[298,1018,234],{"class":1019},"sPxkN",[298,1021,1023],{"class":1022},"slOWO","hero-text",[298,1025,1026],{"class":304}," {\n",[298,1028,1029,1033,1036,1040,1043,1047,1051,1054,1057,1060,1062,1064,1067],{"class":300,"line":343},[298,1030,1032],{"class":1031},"shxsR","  background",[298,1034,1035],{"class":304},":",[298,1037,1039],{"class":1038},"slPND"," linear-gradient",[298,1041,1042],{"class":304},"(",[298,1044,1046],{"class":1045},"sE6rD","to",[298,1048,1050],{"class":1049},"sQ79N"," bottom",[298,1052,1053],{"class":304},",",[298,1055,1056],{"class":1019}," #",[298,1058,1059],{"class":1049},"ffffff",[298,1061,1053],{"class":304},[298,1063,1056],{"class":1019},[298,1065,1066],{"class":1049},"000000",[298,1068,1069],{"class":304},");\n",[298,1071,1072,1075,1077,1079,1082],{"class":300,"line":350},[298,1073,1074],{"class":1031},"  color",[298,1076,1035],{"class":304},[298,1078,1056],{"class":1019},[298,1080,1081],{"class":1049},"544f4f",[298,1083,1084],{"class":304},";\n",[298,1086,1087],{"class":300,"line":460},[298,1088,1089],{"class":304},"}\n",[11,1091,1092,1093,1096,1097,1100,1101,1103,1104,1107],{},"Enter ",[15,1094,1095],{},"#544f4f"," as the foreground color and ",[15,1098,1099],{},"#ffffff"," as the background in the ",[309,1102,1003],{"href":1002},", verify it passes, then repeat with ",[15,1105,1106],{},"#000000"," as the background. For multi-stop gradients, check each color stop. If the text passes at every stop, it passes across the full gradient.",[11,1109,1110],{},[368,1111],{"alt":1112,"src":1113,"className":1114},"Screenshot of color contrast interface with #544f4f and #000000 combination showing a contrast violation","images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/wcag-contrast-color-checker-example.webp",[1115],"portrait",[23,1117],{},[189,1119,1121],{"id":1120},"_8-when-lighthouse-landmark-recommendations-break-legacy-html","8. When Lighthouse Landmark Recommendations Break Legacy HTML",[11,1123,1124,1125,200,1127,200,1129,234],{},"Lighthouse flags missing landmark regions as accessibility violations. In a modern HTML5 codebase that recommendation is straightforward — add ",[15,1126,263],{},[15,1128,267],{},[15,1130,1131],{},"\u003Cheader>",[11,1133,1134],{},"In pre-HTML5 doctypes (XHTML strict, HTML 4.01 strict), those elements are invalid markup. Following the recommendation without understanding the doctype constraint produces markup that is simultaneously invalid and appears to satisfy the accessibility rule.",[11,1136,1137,1138,1140],{},"The correct fix in legacy markup contexts is ARIA landmark roles on ",[15,1139,681],{}," elements:",[289,1142,1144],{"className":291,"code":1143,"language":293,"meta":294,"style":294},"\u003Cdiv role=\"main\">...\u003C/div>\n\u003Cdiv role=\"navigation\" aria-label=\"Main navigation\">...\u003C/div>\n",[15,1145,1146,1175],{"__ignoreMap":294},[298,1147,1148,1150,1153,1156,1158,1160,1163,1165,1167,1169,1171,1173],{"class":300,"line":301},[298,1149,305],{"class":304},[298,1151,1152],{"class":308},"div",[298,1154,1155],{"class":312}," role",[298,1157,316],{"class":304},[298,1159,320],{"class":319},[298,1161,1162],{"class":323},"main",[298,1164,320],{"class":319},[298,1166,479],{"class":304},[298,1168,500],{"class":346},[298,1170,353],{"class":304},[298,1172,1152],{"class":308},[298,1174,340],{"class":304},[298,1176,1177,1179,1181,1183,1185,1187,1190,1192,1194,1196,1198,1201,1203,1205,1207,1209,1211],{"class":300,"line":343},[298,1178,305],{"class":304},[298,1180,1152],{"class":308},[298,1182,1155],{"class":312},[298,1184,316],{"class":304},[298,1186,320],{"class":319},[298,1188,1189],{"class":323},"navigation",[298,1191,320],{"class":319},[298,1193,546],{"class":312},[298,1195,316],{"class":304},[298,1197,320],{"class":319},[298,1199,1200],{"class":323},"Main navigation",[298,1202,320],{"class":319},[298,1204,479],{"class":304},[298,1206,500],{"class":346},[298,1208,353],{"class":304},[298,1210,1152],{"class":308},[298,1212,340],{"class":304},[11,1214,1215],{},"Automated tools evaluate against an idealized modern HTML5 context. They have no awareness of your doctype constraints and cannot distinguish between \"missing landmark\" and \"correctly implemented ARIA landmark role for a legacy codebase.\"",[11,1217,1218,1220,1221,1224],{},[165,1219,226],{}," In pre-HTML5 contexts, use ARIA landmark roles rather than HTML5 landmark elements. In HTML5 codebases, use the native elements — they carry the landmark role implicitly and don't require the explicit ",[15,1222,1223],{},"role"," attribute.",[23,1226],{},[189,1228,1230],{"id":1229},"_9-form-accessibility-labels-instructions-and-error-messages-automated-tools-miss","9. Form Accessibility: Labels, Instructions, and Error Messages Automated Tools Miss",[11,1232,1233,1234,1237],{},"Automated tools reliably catch a missing ",[15,1235,1236],{},"\u003Clabel>"," or a form field with no accessible name. What they can't evaluate is whether the label, instruction, or error message is actually useful.",[11,1239,1240,1241,1244,1245,1248],{},"A ",[15,1242,1243],{},"\u003Clabel>First name\u003C/label>"," associated with a text input passes every automated check. So does ",[15,1246,1247],{},"\u003Clabel>Field 1\u003C/label>",". The tool sees a label — it doesn't read it.",[11,1250,1251],{},"The same gap extends across the full form experience:",[128,1253,1254,1260,1271,1277,1291],{},[131,1255,1256,1259],{},[165,1257,1258],{},"Placeholder text as the only instruction"," — placeholder text disappears when the user starts typing, leaving no reminder of what the field expects. Automated tools don't flag this pattern.",[131,1261,1262,1265,1266,1270],{},[165,1263,1264],{},"Vague error messages"," — \"Invalid input\" and \"Your email address must be in the format ",[309,1267,1269],{"href":1268},"mailto:name@example.com","name@example.com","\" both pass automated checks. Only one helps the user recover.",[131,1272,1273,1276],{},[165,1274,1275],{},"Error proximity"," — errors that appear only at the top of the page satisfy WCAG's error identification requirement but are disorienting for keyboard and screen reader users navigating a long form.",[131,1278,1279,1282,1283,1286,1287,1290],{},[165,1280,1281],{},"Error announcement timing"," — whether an error is surfaced via ",[15,1284,1285],{},"aria-live"," or ",[15,1288,1289],{},"aria-describedby"," in a way screen readers actually pick up requires testing with real assistive technology, not a DOM scan.",[131,1292,1293,1296,1297,1300],{},[165,1294,1295],{},"Required field indicators"," — a red asterisk is a visual convention. Without a text equivalent or ",[15,1298,1299],{},"aria-required=\"true\"",", screen reader users have no indication a field is mandatory until a failed submission tells them so.",[11,1302,1303,1305],{},[165,1304,226],{}," Test every form manually with a keyboard and a real screen reader. Trigger error states deliberately — submit with empty required fields, enter invalid formats — and verify that the announced error tells you exactly what went wrong and what to do to correct it. Confirm that placeholder text is supplemented by a persistent visible label, not used as a substitute for one.",[23,1307],{},[189,1309,1311],{"id":1310},"_10-keyboard-accessibility-dynamic-content-modals-and-interactive-components","10. Keyboard Accessibility: Dynamic Content, Modals, and Interactive Components",[11,1313,1314,1315,1318,1319,1322],{},"Automated tools can verify that a modal has ",[15,1316,1317],{},"role=\"dialog\""," and an accessible name, or that an accordion button has ",[15,1320,1321],{},"aria-expanded",". What they can't do is interact with those components the way a keyboard user would — to verify they actually behave correctly under navigation.",[11,1324,1325],{},"This is a structural limitation of static DOM scanning. A scanner evaluates the page as it loads — it doesn't click buttons, open dialogs, or press keys. It can confirm that a modal has the right ARIA attributes in the markup, but it has no way to verify what happens when a user actually opens it. That gap is exactly where Playwright earns its place: it can drive real keyboard interactions and assert on the result.",[11,1327,1328],{},"The most common failures in this area:",[128,1330,1331,1337,1343,1349,1355,1361],{},[131,1332,1333,1336],{},[165,1334,1335],{},"Focus not moving to the modal on open"," — the dialog appears visually but keyboard focus stays behind it, leaving keyboard users stranded outside the content they need",[131,1338,1339,1342],{},[165,1340,1341],{},"Focus not trapped inside the modal"," — Tab should cycle through focusable elements within the dialog; if it escapes into the page behind, keyboard users lose their place without realizing it",[131,1344,1345,1348],{},[165,1346,1347],{},"No Escape to close"," — keyboard users expect Esc to dismiss a modal; relying solely on a close button forces them to Tab to find it",[131,1350,1351,1354],{},[165,1352,1353],{},"Focus not returned on close"," — when a modal closes, focus should return to the element that triggered it; dropping focus elsewhere forces keyboard users to re-navigate from scratch",[131,1356,1357,1360],{},[165,1358,1359],{},"Expand/collapse keyboard activation"," — an accordion that only responds to mouse clicks isn't keyboard accessible; Enter and Space should both activate toggle controls",[131,1362,1363,1366,1367,1369],{},[165,1364,1365],{},"Dynamically added content not announced"," — content that appears after an interaction (search results, inline validation, notifications) needs ",[15,1368,1285],{}," regions or explicit focus management to reach screen reader users",[11,1371,1372,1374],{},[165,1373,226],{}," Test every interactive component with keyboard only — no mouse. Open modals with Enter, navigate within them with Tab and Shift+Tab, close with Escape, and verify focus returns to the trigger. Activate expand/collapse controls with both Enter and Space and confirm the revealed content is reachable by Tab.",[11,1376,1377],{},"These keyboard paths can then be automated in Playwright:",[289,1379,1384],{"className":1380,"code":1381,"filename":1382,"language":1383,"meta":294,"style":294},"language-typescript shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","test('modal traps focus and closes on Escape', async ({ page }) => {\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Open dialog' }).click();\n  const dialog = page.getByRole('dialog');\n  await expect(dialog).toBeVisible();\n\n  // Tab multiple times — focus should remain within the dialog\n  for (let i = 0; i \u003C 10; i++) {\n    await page.keyboard.press('Tab');\n    const isInsideDialog = await page.evaluate(() =>\n      document.activeElement?.closest('[role=\"dialog\"]') !== null,\n    );\n    expect(isInsideDialog).toBe(true);\n  }\n\n  // Escape should close the modal and it should no longer be visible\n  await page.keyboard.press('Escape');\n  await expect(dialog).not.toBeVisible();\n});\n\ntest('modal returns focus to trigger on close', async ({ page }) => {\n  await page.goto('/');\n  const trigger = page.getByRole('button', { name: 'Open dialog' });\n  await trigger.click();\n  await page.keyboard.press('Escape');\n  await expect(trigger).toBeFocused();\n});\n\ntest('expand/collapse toggle is keyboard accessible', async ({ page }) => {\n  await page.goto('/');\n  const toggle = page.getByRole('button', { name: 'Show details' });\n  const content = page.getByRole('region', { name: 'Show details' });\n\n  await expect(toggle).toHaveAttribute('aria-expanded', 'false');\n  await expect(content).not.toBeVisible();\n\n  await toggle.press('Enter');\n  await expect(toggle).toHaveAttribute('aria-expanded', 'true');\n  await expect(content).toBeVisible();\n\n  await toggle.press('Space');\n  await expect(toggle).toHaveAttribute('aria-expanded', 'false');\n  await expect(content).not.toBeVisible();\n});\n","accessibility.spec.ts","typescript",[15,1385,1386,1423,1450,1502,1532,1554,1558,1563,1607,1638,1666,1703,1711,1738,1744,1749,1755,1783,1809,1819,1824,1852,1875,1919,1934,1961,1984,1993,1998,2026,2049,2094,2139,2144,2184,2210,2215,2239,2276,2297,2302,2326,2363,2388],{"__ignoreMap":294},[298,1387,1388,1392,1394,1397,1400,1402,1404,1408,1411,1415,1418,1421],{"class":300,"line":301},[298,1389,1391],{"class":1390},"sb1SK","test",[298,1393,1042],{"class":346},[298,1395,1396],{"class":319},"'",[298,1398,1399],{"class":323},"modal traps focus and closes on Escape",[298,1401,1396],{"class":319},[298,1403,1053],{"class":304},[298,1405,1407],{"class":1406},"stWsX"," async",[298,1409,1410],{"class":304}," ({",[298,1412,1414],{"class":1413},"s2xgV"," page",[298,1416,1417],{"class":304}," })",[298,1419,1420],{"class":1406}," =>",[298,1422,1026],{"class":304},[298,1424,1425,1429,1431,1433,1436,1439,1441,1443,1445,1448],{"class":300,"line":343},[298,1426,1428],{"class":1427},"sZTni","  await",[298,1430,1414],{"class":346},[298,1432,234],{"class":304},[298,1434,1435],{"class":1390},"goto",[298,1437,1042],{"class":1438},"sq0XF",[298,1440,1396],{"class":319},[298,1442,764],{"class":323},[298,1444,1396],{"class":319},[298,1446,1447],{"class":1438},")",[298,1449,1084],{"class":304},[298,1451,1452,1454,1456,1458,1461,1463,1465,1467,1469,1471,1474,1477,1479,1482,1485,1487,1490,1492,1494,1497,1500],{"class":300,"line":350},[298,1453,1428],{"class":1427},[298,1455,1414],{"class":346},[298,1457,234],{"class":304},[298,1459,1460],{"class":1390},"getByRole",[298,1462,1042],{"class":1438},[298,1464,1396],{"class":319},[298,1466,543],{"class":323},[298,1468,1396],{"class":319},[298,1470,1053],{"class":304},[298,1472,1473],{"class":304}," {",[298,1475,1476],{"class":1438}," name",[298,1478,1035],{"class":304},[298,1480,1481],{"class":319}," '",[298,1483,1484],{"class":323},"Open dialog",[298,1486,1396],{"class":319},[298,1488,1489],{"class":304}," }",[298,1491,1447],{"class":1438},[298,1493,234],{"class":304},[298,1495,1496],{"class":1390},"click",[298,1498,1499],{"class":1438},"()",[298,1501,1084],{"class":304},[298,1503,1504,1507,1510,1513,1515,1517,1519,1521,1523,1526,1528,1530],{"class":300,"line":460},[298,1505,1506],{"class":1406},"  const",[298,1508,1509],{"class":1049}," dialog",[298,1511,1512],{"class":1045}," =",[298,1514,1414],{"class":346},[298,1516,234],{"class":304},[298,1518,1460],{"class":1390},[298,1520,1042],{"class":1438},[298,1522,1396],{"class":319},[298,1524,1525],{"class":323},"dialog",[298,1527,1396],{"class":319},[298,1529,1447],{"class":1438},[298,1531,1084],{"class":304},[298,1533,1534,1536,1539,1541,1543,1545,1547,1550,1552],{"class":300,"line":491},[298,1535,1428],{"class":1427},[298,1537,1538],{"class":1390}," expect",[298,1540,1042],{"class":1438},[298,1542,1525],{"class":346},[298,1544,1447],{"class":1438},[298,1546,234],{"class":304},[298,1548,1549],{"class":1390},"toBeVisible",[298,1551,1499],{"class":1438},[298,1553,1084],{"class":304},[298,1555,1556],{"class":300,"line":509},[298,1557,744],{"emptyLinePlaceholder":743},[298,1559,1560],{"class":300,"line":519},[298,1561,1562],{"class":434},"  // Tab multiple times — focus should remain within the dialog\n",[298,1564,1565,1568,1571,1574,1577,1579,1583,1586,1588,1591,1594,1596,1598,1601,1604],{"class":300,"line":949},[298,1566,1567],{"class":1427},"  for",[298,1569,1570],{"class":1438}," (",[298,1572,1573],{"class":1406},"let",[298,1575,1576],{"class":346}," i",[298,1578,1512],{"class":1045},[298,1580,1582],{"class":1581},"s6g51"," 0",[298,1584,1585],{"class":304},";",[298,1587,1576],{"class":346},[298,1589,1590],{"class":1045}," \u003C",[298,1592,1593],{"class":1581}," 10",[298,1595,1585],{"class":304},[298,1597,1576],{"class":346},[298,1599,1600],{"class":1045},"++",[298,1602,1603],{"class":1438},") ",[298,1605,1606],{"class":304},"{\n",[298,1608,1610,1613,1615,1617,1620,1622,1625,1627,1629,1632,1634,1636],{"class":300,"line":1609},9,[298,1611,1612],{"class":1427},"    await",[298,1614,1414],{"class":346},[298,1616,234],{"class":304},[298,1618,1619],{"class":346},"keyboard",[298,1621,234],{"class":304},[298,1623,1624],{"class":1390},"press",[298,1626,1042],{"class":1438},[298,1628,1396],{"class":319},[298,1630,1631],{"class":323},"Tab",[298,1633,1396],{"class":319},[298,1635,1447],{"class":1438},[298,1637,1084],{"class":304},[298,1639,1641,1644,1647,1649,1652,1654,1656,1659,1661,1663],{"class":300,"line":1640},10,[298,1642,1643],{"class":1406},"    const",[298,1645,1646],{"class":1049}," isInsideDialog",[298,1648,1512],{"class":1045},[298,1650,1651],{"class":1427}," await",[298,1653,1414],{"class":346},[298,1655,234],{"class":304},[298,1657,1658],{"class":1390},"evaluate",[298,1660,1042],{"class":1438},[298,1662,1499],{"class":304},[298,1664,1665],{"class":1406}," =>\n",[298,1667,1669,1672,1674,1677,1680,1683,1685,1687,1690,1692,1694,1697,1700],{"class":300,"line":1668},11,[298,1670,1671],{"class":346},"      document",[298,1673,234],{"class":304},[298,1675,1676],{"class":346},"activeElement",[298,1678,1679],{"class":304},"?.",[298,1681,1682],{"class":1390},"closest",[298,1684,1042],{"class":1438},[298,1686,1396],{"class":319},[298,1688,1689],{"class":323},"[role=\"dialog\"]",[298,1691,1396],{"class":319},[298,1693,1603],{"class":1438},[298,1695,1696],{"class":1045},"!==",[298,1698,1699],{"class":1019}," null",[298,1701,1702],{"class":304},",\n",[298,1704,1706,1709],{"class":300,"line":1705},12,[298,1707,1708],{"class":1438},"    )",[298,1710,1084],{"class":304},[298,1712,1714,1717,1719,1722,1724,1726,1729,1731,1734,1736],{"class":300,"line":1713},13,[298,1715,1716],{"class":1390},"    expect",[298,1718,1042],{"class":1438},[298,1720,1721],{"class":346},"isInsideDialog",[298,1723,1447],{"class":1438},[298,1725,234],{"class":304},[298,1727,1728],{"class":1390},"toBe",[298,1730,1042],{"class":1438},[298,1732,574],{"class":1733},"sTqCK",[298,1735,1447],{"class":1438},[298,1737,1084],{"class":304},[298,1739,1741],{"class":300,"line":1740},14,[298,1742,1743],{"class":304},"  }\n",[298,1745,1747],{"class":300,"line":1746},15,[298,1748,744],{"emptyLinePlaceholder":743},[298,1750,1752],{"class":300,"line":1751},16,[298,1753,1754],{"class":434},"  // Escape should close the modal and it should no longer be visible\n",[298,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774,1777,1779,1781],{"class":300,"line":1757},17,[298,1759,1428],{"class":1427},[298,1761,1414],{"class":346},[298,1763,234],{"class":304},[298,1765,1619],{"class":346},[298,1767,234],{"class":304},[298,1769,1624],{"class":1390},[298,1771,1042],{"class":1438},[298,1773,1396],{"class":319},[298,1775,1776],{"class":323},"Escape",[298,1778,1396],{"class":319},[298,1780,1447],{"class":1438},[298,1782,1084],{"class":304},[298,1784,1786,1788,1790,1792,1794,1796,1798,1801,1803,1805,1807],{"class":300,"line":1785},18,[298,1787,1428],{"class":1427},[298,1789,1538],{"class":1390},[298,1791,1042],{"class":1438},[298,1793,1525],{"class":346},[298,1795,1447],{"class":1438},[298,1797,234],{"class":304},[298,1799,1800],{"class":346},"not",[298,1802,234],{"class":304},[298,1804,1549],{"class":1390},[298,1806,1499],{"class":1438},[298,1808,1084],{"class":304},[298,1810,1812,1815,1817],{"class":300,"line":1811},19,[298,1813,1814],{"class":304},"}",[298,1816,1447],{"class":346},[298,1818,1084],{"class":304},[298,1820,1822],{"class":300,"line":1821},20,[298,1823,744],{"emptyLinePlaceholder":743},[298,1825,1827,1829,1831,1833,1836,1838,1840,1842,1844,1846,1848,1850],{"class":300,"line":1826},21,[298,1828,1391],{"class":1390},[298,1830,1042],{"class":346},[298,1832,1396],{"class":319},[298,1834,1835],{"class":323},"modal returns focus to trigger on close",[298,1837,1396],{"class":319},[298,1839,1053],{"class":304},[298,1841,1407],{"class":1406},[298,1843,1410],{"class":304},[298,1845,1414],{"class":1413},[298,1847,1417],{"class":304},[298,1849,1420],{"class":1406},[298,1851,1026],{"class":304},[298,1853,1855,1857,1859,1861,1863,1865,1867,1869,1871,1873],{"class":300,"line":1854},22,[298,1856,1428],{"class":1427},[298,1858,1414],{"class":346},[298,1860,234],{"class":304},[298,1862,1435],{"class":1390},[298,1864,1042],{"class":1438},[298,1866,1396],{"class":319},[298,1868,764],{"class":323},[298,1870,1396],{"class":319},[298,1872,1447],{"class":1438},[298,1874,1084],{"class":304},[298,1876,1878,1880,1883,1885,1887,1889,1891,1893,1895,1897,1899,1901,1903,1905,1907,1909,1911,1913,1915,1917],{"class":300,"line":1877},23,[298,1879,1506],{"class":1406},[298,1881,1882],{"class":1049}," trigger",[298,1884,1512],{"class":1045},[298,1886,1414],{"class":346},[298,1888,234],{"class":304},[298,1890,1460],{"class":1390},[298,1892,1042],{"class":1438},[298,1894,1396],{"class":319},[298,1896,543],{"class":323},[298,1898,1396],{"class":319},[298,1900,1053],{"class":304},[298,1902,1473],{"class":304},[298,1904,1476],{"class":1438},[298,1906,1035],{"class":304},[298,1908,1481],{"class":319},[298,1910,1484],{"class":323},[298,1912,1396],{"class":319},[298,1914,1489],{"class":304},[298,1916,1447],{"class":1438},[298,1918,1084],{"class":304},[298,1920,1922,1924,1926,1928,1930,1932],{"class":300,"line":1921},24,[298,1923,1428],{"class":1427},[298,1925,1882],{"class":346},[298,1927,234],{"class":304},[298,1929,1496],{"class":1390},[298,1931,1499],{"class":1438},[298,1933,1084],{"class":304},[298,1935,1937,1939,1941,1943,1945,1947,1949,1951,1953,1955,1957,1959],{"class":300,"line":1936},25,[298,1938,1428],{"class":1427},[298,1940,1414],{"class":346},[298,1942,234],{"class":304},[298,1944,1619],{"class":346},[298,1946,234],{"class":304},[298,1948,1624],{"class":1390},[298,1950,1042],{"class":1438},[298,1952,1396],{"class":319},[298,1954,1776],{"class":323},[298,1956,1396],{"class":319},[298,1958,1447],{"class":1438},[298,1960,1084],{"class":304},[298,1962,1964,1966,1968,1970,1973,1975,1977,1980,1982],{"class":300,"line":1963},26,[298,1965,1428],{"class":1427},[298,1967,1538],{"class":1390},[298,1969,1042],{"class":1438},[298,1971,1972],{"class":346},"trigger",[298,1974,1447],{"class":1438},[298,1976,234],{"class":304},[298,1978,1979],{"class":1390},"toBeFocused",[298,1981,1499],{"class":1438},[298,1983,1084],{"class":304},[298,1985,1987,1989,1991],{"class":300,"line":1986},27,[298,1988,1814],{"class":304},[298,1990,1447],{"class":346},[298,1992,1084],{"class":304},[298,1994,1996],{"class":300,"line":1995},28,[298,1997,744],{"emptyLinePlaceholder":743},[298,1999,2001,2003,2005,2007,2010,2012,2014,2016,2018,2020,2022,2024],{"class":300,"line":2000},29,[298,2002,1391],{"class":1390},[298,2004,1042],{"class":346},[298,2006,1396],{"class":319},[298,2008,2009],{"class":323},"expand/collapse toggle is keyboard accessible",[298,2011,1396],{"class":319},[298,2013,1053],{"class":304},[298,2015,1407],{"class":1406},[298,2017,1410],{"class":304},[298,2019,1414],{"class":1413},[298,2021,1417],{"class":304},[298,2023,1420],{"class":1406},[298,2025,1026],{"class":304},[298,2027,2029,2031,2033,2035,2037,2039,2041,2043,2045,2047],{"class":300,"line":2028},30,[298,2030,1428],{"class":1427},[298,2032,1414],{"class":346},[298,2034,234],{"class":304},[298,2036,1435],{"class":1390},[298,2038,1042],{"class":1438},[298,2040,1396],{"class":319},[298,2042,764],{"class":323},[298,2044,1396],{"class":319},[298,2046,1447],{"class":1438},[298,2048,1084],{"class":304},[298,2050,2052,2054,2057,2059,2061,2063,2065,2067,2069,2071,2073,2075,2077,2079,2081,2083,2086,2088,2090,2092],{"class":300,"line":2051},31,[298,2053,1506],{"class":1406},[298,2055,2056],{"class":1049}," toggle",[298,2058,1512],{"class":1045},[298,2060,1414],{"class":346},[298,2062,234],{"class":304},[298,2064,1460],{"class":1390},[298,2066,1042],{"class":1438},[298,2068,1396],{"class":319},[298,2070,543],{"class":323},[298,2072,1396],{"class":319},[298,2074,1053],{"class":304},[298,2076,1473],{"class":304},[298,2078,1476],{"class":1438},[298,2080,1035],{"class":304},[298,2082,1481],{"class":319},[298,2084,2085],{"class":323},"Show details",[298,2087,1396],{"class":319},[298,2089,1489],{"class":304},[298,2091,1447],{"class":1438},[298,2093,1084],{"class":304},[298,2095,2097,2099,2102,2104,2106,2108,2110,2112,2114,2117,2119,2121,2123,2125,2127,2129,2131,2133,2135,2137],{"class":300,"line":2096},32,[298,2098,1506],{"class":1406},[298,2100,2101],{"class":1049}," content",[298,2103,1512],{"class":1045},[298,2105,1414],{"class":346},[298,2107,234],{"class":304},[298,2109,1460],{"class":1390},[298,2111,1042],{"class":1438},[298,2113,1396],{"class":319},[298,2115,2116],{"class":323},"region",[298,2118,1396],{"class":319},[298,2120,1053],{"class":304},[298,2122,1473],{"class":304},[298,2124,1476],{"class":1438},[298,2126,1035],{"class":304},[298,2128,1481],{"class":319},[298,2130,2085],{"class":323},[298,2132,1396],{"class":319},[298,2134,1489],{"class":304},[298,2136,1447],{"class":1438},[298,2138,1084],{"class":304},[298,2140,2142],{"class":300,"line":2141},33,[298,2143,744],{"emptyLinePlaceholder":743},[298,2145,2147,2149,2151,2153,2156,2158,2160,2163,2165,2167,2169,2171,2173,2175,2178,2180,2182],{"class":300,"line":2146},34,[298,2148,1428],{"class":1427},[298,2150,1538],{"class":1390},[298,2152,1042],{"class":1438},[298,2154,2155],{"class":346},"toggle",[298,2157,1447],{"class":1438},[298,2159,234],{"class":304},[298,2161,2162],{"class":1390},"toHaveAttribute",[298,2164,1042],{"class":1438},[298,2166,1396],{"class":319},[298,2168,1321],{"class":323},[298,2170,1396],{"class":319},[298,2172,1053],{"class":304},[298,2174,1481],{"class":319},[298,2176,2177],{"class":323},"false",[298,2179,1396],{"class":319},[298,2181,1447],{"class":1438},[298,2183,1084],{"class":304},[298,2185,2187,2189,2191,2193,2196,2198,2200,2202,2204,2206,2208],{"class":300,"line":2186},35,[298,2188,1428],{"class":1427},[298,2190,1538],{"class":1390},[298,2192,1042],{"class":1438},[298,2194,2195],{"class":346},"content",[298,2197,1447],{"class":1438},[298,2199,234],{"class":304},[298,2201,1800],{"class":346},[298,2203,234],{"class":304},[298,2205,1549],{"class":1390},[298,2207,1499],{"class":1438},[298,2209,1084],{"class":304},[298,2211,2213],{"class":300,"line":2212},36,[298,2214,744],{"emptyLinePlaceholder":743},[298,2216,2218,2220,2222,2224,2226,2228,2230,2233,2235,2237],{"class":300,"line":2217},37,[298,2219,1428],{"class":1427},[298,2221,2056],{"class":346},[298,2223,234],{"class":304},[298,2225,1624],{"class":1390},[298,2227,1042],{"class":1438},[298,2229,1396],{"class":319},[298,2231,2232],{"class":323},"Enter",[298,2234,1396],{"class":319},[298,2236,1447],{"class":1438},[298,2238,1084],{"class":304},[298,2240,2242,2244,2246,2248,2250,2252,2254,2256,2258,2260,2262,2264,2266,2268,2270,2272,2274],{"class":300,"line":2241},38,[298,2243,1428],{"class":1427},[298,2245,1538],{"class":1390},[298,2247,1042],{"class":1438},[298,2249,2155],{"class":346},[298,2251,1447],{"class":1438},[298,2253,234],{"class":304},[298,2255,2162],{"class":1390},[298,2257,1042],{"class":1438},[298,2259,1396],{"class":319},[298,2261,1321],{"class":323},[298,2263,1396],{"class":319},[298,2265,1053],{"class":304},[298,2267,1481],{"class":319},[298,2269,574],{"class":323},[298,2271,1396],{"class":319},[298,2273,1447],{"class":1438},[298,2275,1084],{"class":304},[298,2277,2279,2281,2283,2285,2287,2289,2291,2293,2295],{"class":300,"line":2278},39,[298,2280,1428],{"class":1427},[298,2282,1538],{"class":1390},[298,2284,1042],{"class":1438},[298,2286,2195],{"class":346},[298,2288,1447],{"class":1438},[298,2290,234],{"class":304},[298,2292,1549],{"class":1390},[298,2294,1499],{"class":1438},[298,2296,1084],{"class":304},[298,2298,2300],{"class":300,"line":2299},40,[298,2301,744],{"emptyLinePlaceholder":743},[298,2303,2305,2307,2309,2311,2313,2315,2317,2320,2322,2324],{"class":300,"line":2304},41,[298,2306,1428],{"class":1427},[298,2308,2056],{"class":346},[298,2310,234],{"class":304},[298,2312,1624],{"class":1390},[298,2314,1042],{"class":1438},[298,2316,1396],{"class":319},[298,2318,2319],{"class":323},"Space",[298,2321,1396],{"class":319},[298,2323,1447],{"class":1438},[298,2325,1084],{"class":304},[298,2327,2329,2331,2333,2335,2337,2339,2341,2343,2345,2347,2349,2351,2353,2355,2357,2359,2361],{"class":300,"line":2328},42,[298,2330,1428],{"class":1427},[298,2332,1538],{"class":1390},[298,2334,1042],{"class":1438},[298,2336,2155],{"class":346},[298,2338,1447],{"class":1438},[298,2340,234],{"class":304},[298,2342,2162],{"class":1390},[298,2344,1042],{"class":1438},[298,2346,1396],{"class":319},[298,2348,1321],{"class":323},[298,2350,1396],{"class":319},[298,2352,1053],{"class":304},[298,2354,1481],{"class":319},[298,2356,2177],{"class":323},[298,2358,1396],{"class":319},[298,2360,1447],{"class":1438},[298,2362,1084],{"class":304},[298,2364,2366,2368,2370,2372,2374,2376,2378,2380,2382,2384,2386],{"class":300,"line":2365},43,[298,2367,1428],{"class":1427},[298,2369,1538],{"class":1390},[298,2371,1042],{"class":1438},[298,2373,2195],{"class":346},[298,2375,1447],{"class":1438},[298,2377,234],{"class":304},[298,2379,1800],{"class":346},[298,2381,234],{"class":304},[298,2383,1549],{"class":1390},[298,2385,1499],{"class":1438},[298,2387,1084],{"class":304},[298,2389,2391,2393,2395],{"class":300,"line":2390},44,[298,2392,1814],{"class":304},[298,2394,1447],{"class":346},[298,2396,1084],{"class":304},[23,2398],{},[26,2400,2402],{"id":2401},"writing-smarter-playwright-tests-that-go-beyond-a-lighthouse-report","Writing Smarter Playwright Tests That Go Beyond a Lighthouse Report",[11,2404,2405,2406,2409],{},"The answer to these gaps isn't to abandon automated testing — it's to write tests that go further than a default axe pass or green circle in Chrome's Lighthouse Accessibility report. A standard ",[15,2407,2408],{},"AxeBuilder"," scan is the starting point, not the finish line.",[11,2411,2412],{},"These test patterns cover defects a default scan misses.",[189,2414,2416],{"id":2415},"default-axe-scan-your-baseline","Default axe scan (your baseline)",[289,2418,2420],{"className":1380,"code":2419,"filename":1382,"language":1383,"meta":294,"style":294},"import { test, expect } from '@playwright/test';\nimport AxeBuilder from '@axe-core/playwright';\n\ntest('accessibility scan', async ({ page }) => {\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  // Ensure the default page state has no violations\n  expect(results.violations).toEqual([]);\n});\n",[15,2421,2422,2450,2468,2472,2499,2521,2558,2563,2590],{"__ignoreMap":294},[298,2423,2424,2427,2429,2432,2434,2436,2438,2441,2443,2446,2448],{"class":300,"line":301},[298,2425,2426],{"class":1427},"import",[298,2428,1473],{"class":304},[298,2430,2431],{"class":346}," test",[298,2433,1053],{"class":304},[298,2435,1538],{"class":346},[298,2437,1489],{"class":304},[298,2439,2440],{"class":1427}," from",[298,2442,1481],{"class":319},[298,2444,2445],{"class":323},"@playwright/test",[298,2447,1396],{"class":319},[298,2449,1084],{"class":304},[298,2451,2452,2454,2457,2460,2462,2464,2466],{"class":300,"line":343},[298,2453,2426],{"class":1427},[298,2455,2456],{"class":346}," AxeBuilder ",[298,2458,2459],{"class":1427},"from",[298,2461,1481],{"class":319},[298,2463,17],{"class":323},[298,2465,1396],{"class":319},[298,2467,1084],{"class":304},[298,2469,2470],{"class":300,"line":350},[298,2471,744],{"emptyLinePlaceholder":743},[298,2473,2474,2476,2478,2480,2483,2485,2487,2489,2491,2493,2495,2497],{"class":300,"line":460},[298,2475,1391],{"class":1390},[298,2477,1042],{"class":346},[298,2479,1396],{"class":319},[298,2481,2482],{"class":323},"accessibility scan",[298,2484,1396],{"class":319},[298,2486,1053],{"class":304},[298,2488,1407],{"class":1406},[298,2490,1410],{"class":304},[298,2492,1414],{"class":1413},[298,2494,1417],{"class":304},[298,2496,1420],{"class":1406},[298,2498,1026],{"class":304},[298,2500,2501,2503,2505,2507,2509,2511,2513,2515,2517,2519],{"class":300,"line":491},[298,2502,1428],{"class":1427},[298,2504,1414],{"class":346},[298,2506,234],{"class":304},[298,2508,1435],{"class":1390},[298,2510,1042],{"class":1438},[298,2512,1396],{"class":319},[298,2514,764],{"class":323},[298,2516,1396],{"class":319},[298,2518,1447],{"class":1438},[298,2520,1084],{"class":304},[298,2522,2523,2525,2528,2530,2532,2535,2538,2540,2543,2545,2547,2549,2551,2554,2556],{"class":300,"line":509},[298,2524,1506],{"class":1406},[298,2526,2527],{"class":1049}," results",[298,2529,1512],{"class":1045},[298,2531,1651],{"class":1427},[298,2533,2534],{"class":1045}," new",[298,2536,2537],{"class":1390}," AxeBuilder",[298,2539,1042],{"class":1438},[298,2541,2542],{"class":304},"{",[298,2544,1414],{"class":346},[298,2546,1489],{"class":304},[298,2548,1447],{"class":1438},[298,2550,234],{"class":304},[298,2552,2553],{"class":1390},"analyze",[298,2555,1499],{"class":1438},[298,2557,1084],{"class":304},[298,2559,2560],{"class":300,"line":519},[298,2561,2562],{"class":434},"  // Ensure the default page state has no violations\n",[298,2564,2565,2568,2570,2573,2575,2578,2580,2582,2585,2588],{"class":300,"line":949},[298,2566,2567],{"class":1390},"  expect",[298,2569,1042],{"class":1438},[298,2571,2572],{"class":346},"results",[298,2574,234],{"class":304},[298,2576,2577],{"class":346},"violations",[298,2579,1447],{"class":1438},[298,2581,234],{"class":304},[298,2583,2584],{"class":1390},"toEqual",[298,2586,2587],{"class":1438},"([])",[298,2589,1084],{"class":304},[298,2591,2592,2594,2596],{"class":300,"line":1609},[298,2593,1814],{"class":304},[298,2595,1447],{"class":346},[298,2597,1084],{"class":304},[189,2599,2601],{"id":2600},"hover-state-contrast","Hover state contrast",[11,2603,2604],{},"A standard scan misses contrast issues that only appear on hover. Trigger the state first, then scan.",[289,2606,2608],{"className":1380,"code":2607,"filename":1382,"language":1383,"meta":294,"style":294},"test('accessibility scan on hovered button', async ({ page }) => {\n  await page.goto('/');\n  // Trigger hover state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).hover();\n  const results = await new AxeBuilder({ page }).analyze();\n  // Verify with the button hovered we still have no violations present\n  expect(results.violations).toEqual([]);\n});\n",[15,2609,2610,2637,2659,2664,2710,2742,2747,2769],{"__ignoreMap":294},[298,2611,2612,2614,2616,2618,2621,2623,2625,2627,2629,2631,2633,2635],{"class":300,"line":301},[298,2613,1391],{"class":1390},[298,2615,1042],{"class":346},[298,2617,1396],{"class":319},[298,2619,2620],{"class":323},"accessibility scan on hovered button",[298,2622,1396],{"class":319},[298,2624,1053],{"class":304},[298,2626,1407],{"class":1406},[298,2628,1410],{"class":304},[298,2630,1414],{"class":1413},[298,2632,1417],{"class":304},[298,2634,1420],{"class":1406},[298,2636,1026],{"class":304},[298,2638,2639,2641,2643,2645,2647,2649,2651,2653,2655,2657],{"class":300,"line":343},[298,2640,1428],{"class":1427},[298,2642,1414],{"class":346},[298,2644,234],{"class":304},[298,2646,1435],{"class":1390},[298,2648,1042],{"class":1438},[298,2650,1396],{"class":319},[298,2652,764],{"class":323},[298,2654,1396],{"class":319},[298,2656,1447],{"class":1438},[298,2658,1084],{"class":304},[298,2660,2661],{"class":300,"line":350},[298,2662,2663],{"class":434},"  // Trigger hover state on the button\n",[298,2665,2666,2668,2670,2672,2674,2676,2678,2680,2682,2684,2686,2688,2690,2692,2695,2697,2699,2701,2703,2706,2708],{"class":300,"line":460},[298,2667,1428],{"class":1427},[298,2669,1414],{"class":346},[298,2671,234],{"class":304},[298,2673,1460],{"class":1390},[298,2675,1042],{"class":1438},[298,2677,1396],{"class":319},[298,2679,543],{"class":323},[298,2681,1396],{"class":319},[298,2683,1053],{"class":304},[298,2685,1473],{"class":304},[298,2687,1476],{"class":1438},[298,2689,1035],{"class":304},[298,2691,1481],{"class":319},[298,2693,2694],{"class":323},"Subscribe",[298,2696,1396],{"class":319},[298,2698,1489],{"class":304},[298,2700,1447],{"class":1438},[298,2702,234],{"class":304},[298,2704,2705],{"class":1390},"hover",[298,2707,1499],{"class":1438},[298,2709,1084],{"class":304},[298,2711,2712,2714,2716,2718,2720,2722,2724,2726,2728,2730,2732,2734,2736,2738,2740],{"class":300,"line":491},[298,2713,1506],{"class":1406},[298,2715,2527],{"class":1049},[298,2717,1512],{"class":1045},[298,2719,1651],{"class":1427},[298,2721,2534],{"class":1045},[298,2723,2537],{"class":1390},[298,2725,1042],{"class":1438},[298,2727,2542],{"class":304},[298,2729,1414],{"class":346},[298,2731,1489],{"class":304},[298,2733,1447],{"class":1438},[298,2735,234],{"class":304},[298,2737,2553],{"class":1390},[298,2739,1499],{"class":1438},[298,2741,1084],{"class":304},[298,2743,2744],{"class":300,"line":509},[298,2745,2746],{"class":434},"  // Verify with the button hovered we still have no violations present\n",[298,2748,2749,2751,2753,2755,2757,2759,2761,2763,2765,2767],{"class":300,"line":519},[298,2750,2567],{"class":1390},[298,2752,1042],{"class":1438},[298,2754,2572],{"class":346},[298,2756,234],{"class":304},[298,2758,2577],{"class":346},[298,2760,1447],{"class":1438},[298,2762,234],{"class":304},[298,2764,2584],{"class":1390},[298,2766,2587],{"class":1438},[298,2768,1084],{"class":304},[298,2770,2771,2773,2775],{"class":300,"line":949},[298,2772,1814],{"class":304},[298,2774,1447],{"class":346},[298,2776,1084],{"class":304},[189,2778,2780],{"id":2779},"focus-state-contrast","Focus state contrast",[11,2782,2783],{},"Same principle — trigger focus before scanning so axe evaluates the focused styles.",[289,2785,2787],{"className":1380,"code":2786,"filename":1382,"language":1383,"meta":294,"style":294},"test('accessibility scan on focused button', async ({ page }) => {\n  await page.goto('/');\n  // Trigger focus state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).focus();\n  const results = await new AxeBuilder({ page }).analyze();\n  // Except no violations\n  expect(results.violations).toEqual([]);\n});\n",[15,2788,2789,2816,2838,2843,2888,2920,2925,2947],{"__ignoreMap":294},[298,2790,2791,2793,2795,2797,2800,2802,2804,2806,2808,2810,2812,2814],{"class":300,"line":301},[298,2792,1391],{"class":1390},[298,2794,1042],{"class":346},[298,2796,1396],{"class":319},[298,2798,2799],{"class":323},"accessibility scan on focused button",[298,2801,1396],{"class":319},[298,2803,1053],{"class":304},[298,2805,1407],{"class":1406},[298,2807,1410],{"class":304},[298,2809,1414],{"class":1413},[298,2811,1417],{"class":304},[298,2813,1420],{"class":1406},[298,2815,1026],{"class":304},[298,2817,2818,2820,2822,2824,2826,2828,2830,2832,2834,2836],{"class":300,"line":343},[298,2819,1428],{"class":1427},[298,2821,1414],{"class":346},[298,2823,234],{"class":304},[298,2825,1435],{"class":1390},[298,2827,1042],{"class":1438},[298,2829,1396],{"class":319},[298,2831,764],{"class":323},[298,2833,1396],{"class":319},[298,2835,1447],{"class":1438},[298,2837,1084],{"class":304},[298,2839,2840],{"class":300,"line":350},[298,2841,2842],{"class":434},"  // Trigger focus state on the button\n",[298,2844,2845,2847,2849,2851,2853,2855,2857,2859,2861,2863,2865,2867,2869,2871,2873,2875,2877,2879,2881,2884,2886],{"class":300,"line":460},[298,2846,1428],{"class":1427},[298,2848,1414],{"class":346},[298,2850,234],{"class":304},[298,2852,1460],{"class":1390},[298,2854,1042],{"class":1438},[298,2856,1396],{"class":319},[298,2858,543],{"class":323},[298,2860,1396],{"class":319},[298,2862,1053],{"class":304},[298,2864,1473],{"class":304},[298,2866,1476],{"class":1438},[298,2868,1035],{"class":304},[298,2870,1481],{"class":319},[298,2872,2694],{"class":323},[298,2874,1396],{"class":319},[298,2876,1489],{"class":304},[298,2878,1447],{"class":1438},[298,2880,234],{"class":304},[298,2882,2883],{"class":1390},"focus",[298,2885,1499],{"class":1438},[298,2887,1084],{"class":304},[298,2889,2890,2892,2894,2896,2898,2900,2902,2904,2906,2908,2910,2912,2914,2916,2918],{"class":300,"line":491},[298,2891,1506],{"class":1406},[298,2893,2527],{"class":1049},[298,2895,1512],{"class":1045},[298,2897,1651],{"class":1427},[298,2899,2534],{"class":1045},[298,2901,2537],{"class":1390},[298,2903,1042],{"class":1438},[298,2905,2542],{"class":304},[298,2907,1414],{"class":346},[298,2909,1489],{"class":304},[298,2911,1447],{"class":1438},[298,2913,234],{"class":304},[298,2915,2553],{"class":1390},[298,2917,1499],{"class":1438},[298,2919,1084],{"class":304},[298,2921,2922],{"class":300,"line":509},[298,2923,2924],{"class":434},"  // Except no violations\n",[298,2926,2927,2929,2931,2933,2935,2937,2939,2941,2943,2945],{"class":300,"line":519},[298,2928,2567],{"class":1390},[298,2930,1042],{"class":1438},[298,2932,2572],{"class":346},[298,2934,234],{"class":304},[298,2936,2577],{"class":346},[298,2938,1447],{"class":1438},[298,2940,234],{"class":304},[298,2942,2584],{"class":1390},[298,2944,2587],{"class":1438},[298,2946,1084],{"class":304},[298,2948,2949,2951,2953],{"class":300,"line":949},[298,2950,1814],{"class":304},[298,2952,1447],{"class":346},[298,2954,1084],{"class":304},[189,2956,2958],{"id":2957},"dark-mode-contrast","Dark mode contrast",[11,2960,2961],{},"Contrast issues may appear on light mode vs dark mode or vice-versa.",[289,2963,2965],{"className":1380,"code":2964,"filename":1382,"language":1383,"meta":294,"style":294},"test('accessibility scan in dark mode', async ({ page }) => {\n  await page.emulateMedia({ colorScheme: 'dark' });\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n\ntest('accessibility scan in light mode', async ({ page }) => {\n  await page.emulateMedia({ colorScheme: 'light' });\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n",[15,2966,2967,2994,3027,3049,3081,3103,3111,3115,3142,3173,3195,3227,3249],{"__ignoreMap":294},[298,2968,2969,2971,2973,2975,2978,2980,2982,2984,2986,2988,2990,2992],{"class":300,"line":301},[298,2970,1391],{"class":1390},[298,2972,1042],{"class":346},[298,2974,1396],{"class":319},[298,2976,2977],{"class":323},"accessibility scan in dark mode",[298,2979,1396],{"class":319},[298,2981,1053],{"class":304},[298,2983,1407],{"class":1406},[298,2985,1410],{"class":304},[298,2987,1414],{"class":1413},[298,2989,1417],{"class":304},[298,2991,1420],{"class":1406},[298,2993,1026],{"class":304},[298,2995,2996,2998,3000,3002,3005,3007,3009,3012,3014,3016,3019,3021,3023,3025],{"class":300,"line":343},[298,2997,1428],{"class":1427},[298,2999,1414],{"class":346},[298,3001,234],{"class":304},[298,3003,3004],{"class":1390},"emulateMedia",[298,3006,1042],{"class":1438},[298,3008,2542],{"class":304},[298,3010,3011],{"class":1438}," colorScheme",[298,3013,1035],{"class":304},[298,3015,1481],{"class":319},[298,3017,3018],{"class":323},"dark",[298,3020,1396],{"class":319},[298,3022,1489],{"class":304},[298,3024,1447],{"class":1438},[298,3026,1084],{"class":304},[298,3028,3029,3031,3033,3035,3037,3039,3041,3043,3045,3047],{"class":300,"line":350},[298,3030,1428],{"class":1427},[298,3032,1414],{"class":346},[298,3034,234],{"class":304},[298,3036,1435],{"class":1390},[298,3038,1042],{"class":1438},[298,3040,1396],{"class":319},[298,3042,764],{"class":323},[298,3044,1396],{"class":319},[298,3046,1447],{"class":1438},[298,3048,1084],{"class":304},[298,3050,3051,3053,3055,3057,3059,3061,3063,3065,3067,3069,3071,3073,3075,3077,3079],{"class":300,"line":460},[298,3052,1506],{"class":1406},[298,3054,2527],{"class":1049},[298,3056,1512],{"class":1045},[298,3058,1651],{"class":1427},[298,3060,2534],{"class":1045},[298,3062,2537],{"class":1390},[298,3064,1042],{"class":1438},[298,3066,2542],{"class":304},[298,3068,1414],{"class":346},[298,3070,1489],{"class":304},[298,3072,1447],{"class":1438},[298,3074,234],{"class":304},[298,3076,2553],{"class":1390},[298,3078,1499],{"class":1438},[298,3080,1084],{"class":304},[298,3082,3083,3085,3087,3089,3091,3093,3095,3097,3099,3101],{"class":300,"line":491},[298,3084,2567],{"class":1390},[298,3086,1042],{"class":1438},[298,3088,2572],{"class":346},[298,3090,234],{"class":304},[298,3092,2577],{"class":346},[298,3094,1447],{"class":1438},[298,3096,234],{"class":304},[298,3098,2584],{"class":1390},[298,3100,2587],{"class":1438},[298,3102,1084],{"class":304},[298,3104,3105,3107,3109],{"class":300,"line":509},[298,3106,1814],{"class":304},[298,3108,1447],{"class":346},[298,3110,1084],{"class":304},[298,3112,3113],{"class":300,"line":519},[298,3114,744],{"emptyLinePlaceholder":743},[298,3116,3117,3119,3121,3123,3126,3128,3130,3132,3134,3136,3138,3140],{"class":300,"line":949},[298,3118,1391],{"class":1390},[298,3120,1042],{"class":346},[298,3122,1396],{"class":319},[298,3124,3125],{"class":323},"accessibility scan in light mode",[298,3127,1396],{"class":319},[298,3129,1053],{"class":304},[298,3131,1407],{"class":1406},[298,3133,1410],{"class":304},[298,3135,1414],{"class":1413},[298,3137,1417],{"class":304},[298,3139,1420],{"class":1406},[298,3141,1026],{"class":304},[298,3143,3144,3146,3148,3150,3152,3154,3156,3158,3160,3162,3165,3167,3169,3171],{"class":300,"line":1609},[298,3145,1428],{"class":1427},[298,3147,1414],{"class":346},[298,3149,234],{"class":304},[298,3151,3004],{"class":1390},[298,3153,1042],{"class":1438},[298,3155,2542],{"class":304},[298,3157,3011],{"class":1438},[298,3159,1035],{"class":304},[298,3161,1481],{"class":319},[298,3163,3164],{"class":323},"light",[298,3166,1396],{"class":319},[298,3168,1489],{"class":304},[298,3170,1447],{"class":1438},[298,3172,1084],{"class":304},[298,3174,3175,3177,3179,3181,3183,3185,3187,3189,3191,3193],{"class":300,"line":1640},[298,3176,1428],{"class":1427},[298,3178,1414],{"class":346},[298,3180,234],{"class":304},[298,3182,1435],{"class":1390},[298,3184,1042],{"class":1438},[298,3186,1396],{"class":319},[298,3188,764],{"class":323},[298,3190,1396],{"class":319},[298,3192,1447],{"class":1438},[298,3194,1084],{"class":304},[298,3196,3197,3199,3201,3203,3205,3207,3209,3211,3213,3215,3217,3219,3221,3223,3225],{"class":300,"line":1668},[298,3198,1506],{"class":1406},[298,3200,2527],{"class":1049},[298,3202,1512],{"class":1045},[298,3204,1651],{"class":1427},[298,3206,2534],{"class":1045},[298,3208,2537],{"class":1390},[298,3210,1042],{"class":1438},[298,3212,2542],{"class":304},[298,3214,1414],{"class":346},[298,3216,1489],{"class":304},[298,3218,1447],{"class":1438},[298,3220,234],{"class":304},[298,3222,2553],{"class":1390},[298,3224,1499],{"class":1438},[298,3226,1084],{"class":304},[298,3228,3229,3231,3233,3235,3237,3239,3241,3243,3245,3247],{"class":300,"line":1705},[298,3230,2567],{"class":1390},[298,3232,1042],{"class":1438},[298,3234,2572],{"class":346},[298,3236,234],{"class":304},[298,3238,2577],{"class":346},[298,3240,1447],{"class":1438},[298,3242,234],{"class":304},[298,3244,2584],{"class":1390},[298,3246,2587],{"class":1438},[298,3248,1084],{"class":304},[298,3250,3251,3253,3255],{"class":300,"line":1713},[298,3252,1814],{"class":304},[298,3254,1447],{"class":346},[298,3256,1084],{"class":304},[189,3258,3260],{"id":3259},"skip-link-first-focusable-element","Skip link — first focusable element",[11,3262,3263],{},"Verify the skip link exists, is the first element reached by keyboard, and actually moves focus to the main content when activated.",[289,3265,3267],{"className":1380,"code":3266,"filename":1382,"language":1383,"meta":294,"style":294},"test('first focusable element is a skip link that navigates to main content', async ({ page }) => {\n  await page.goto('/');\n  await page.keyboard.press('Tab');\n  const focused = page.locator(':focus');\n  await expect(focused).toHaveAttribute('href', '#main-content');\n  await expect(focused).toHaveText('Skip to main content');\n\n  // Activate the skip link and verify it navigated to main content\n  await page.keyboard.press('Enter');\n  await expect(page).toHaveURL(/#main-content$/);\n});\n",[15,3268,3269,3296,3318,3344,3373,3411,3440,3444,3449,3475,3508],{"__ignoreMap":294},[298,3270,3271,3273,3275,3277,3280,3282,3284,3286,3288,3290,3292,3294],{"class":300,"line":301},[298,3272,1391],{"class":1390},[298,3274,1042],{"class":346},[298,3276,1396],{"class":319},[298,3278,3279],{"class":323},"first focusable element is a skip link that navigates to main content",[298,3281,1396],{"class":319},[298,3283,1053],{"class":304},[298,3285,1407],{"class":1406},[298,3287,1410],{"class":304},[298,3289,1414],{"class":1413},[298,3291,1417],{"class":304},[298,3293,1420],{"class":1406},[298,3295,1026],{"class":304},[298,3297,3298,3300,3302,3304,3306,3308,3310,3312,3314,3316],{"class":300,"line":343},[298,3299,1428],{"class":1427},[298,3301,1414],{"class":346},[298,3303,234],{"class":304},[298,3305,1435],{"class":1390},[298,3307,1042],{"class":1438},[298,3309,1396],{"class":319},[298,3311,764],{"class":323},[298,3313,1396],{"class":319},[298,3315,1447],{"class":1438},[298,3317,1084],{"class":304},[298,3319,3320,3322,3324,3326,3328,3330,3332,3334,3336,3338,3340,3342],{"class":300,"line":350},[298,3321,1428],{"class":1427},[298,3323,1414],{"class":346},[298,3325,234],{"class":304},[298,3327,1619],{"class":346},[298,3329,234],{"class":304},[298,3331,1624],{"class":1390},[298,3333,1042],{"class":1438},[298,3335,1396],{"class":319},[298,3337,1631],{"class":323},[298,3339,1396],{"class":319},[298,3341,1447],{"class":1438},[298,3343,1084],{"class":304},[298,3345,3346,3348,3351,3353,3355,3357,3360,3362,3364,3367,3369,3371],{"class":300,"line":460},[298,3347,1506],{"class":1406},[298,3349,3350],{"class":1049}," focused",[298,3352,1512],{"class":1045},[298,3354,1414],{"class":346},[298,3356,234],{"class":304},[298,3358,3359],{"class":1390},"locator",[298,3361,1042],{"class":1438},[298,3363,1396],{"class":319},[298,3365,3366],{"class":323},":focus",[298,3368,1396],{"class":319},[298,3370,1447],{"class":1438},[298,3372,1084],{"class":304},[298,3374,3375,3377,3379,3381,3384,3386,3388,3390,3392,3394,3397,3399,3401,3403,3405,3407,3409],{"class":300,"line":491},[298,3376,1428],{"class":1427},[298,3378,1538],{"class":1390},[298,3380,1042],{"class":1438},[298,3382,3383],{"class":346},"focused",[298,3385,1447],{"class":1438},[298,3387,234],{"class":304},[298,3389,2162],{"class":1390},[298,3391,1042],{"class":1438},[298,3393,1396],{"class":319},[298,3395,3396],{"class":323},"href",[298,3398,1396],{"class":319},[298,3400,1053],{"class":304},[298,3402,1481],{"class":319},[298,3404,281],{"class":323},[298,3406,1396],{"class":319},[298,3408,1447],{"class":1438},[298,3410,1084],{"class":304},[298,3412,3413,3415,3417,3419,3421,3423,3425,3428,3430,3432,3434,3436,3438],{"class":300,"line":509},[298,3414,1428],{"class":1427},[298,3416,1538],{"class":1390},[298,3418,1042],{"class":1438},[298,3420,3383],{"class":346},[298,3422,1447],{"class":1438},[298,3424,234],{"class":304},[298,3426,3427],{"class":1390},"toHaveText",[298,3429,1042],{"class":1438},[298,3431,1396],{"class":319},[298,3433,363],{"class":323},[298,3435,1396],{"class":319},[298,3437,1447],{"class":1438},[298,3439,1084],{"class":304},[298,3441,3442],{"class":300,"line":519},[298,3443,744],{"emptyLinePlaceholder":743},[298,3445,3446],{"class":300,"line":949},[298,3447,3448],{"class":434},"  // Activate the skip link and verify it navigated to main content\n",[298,3450,3451,3453,3455,3457,3459,3461,3463,3465,3467,3469,3471,3473],{"class":300,"line":1609},[298,3452,1428],{"class":1427},[298,3454,1414],{"class":346},[298,3456,234],{"class":304},[298,3458,1619],{"class":346},[298,3460,234],{"class":304},[298,3462,1624],{"class":1390},[298,3464,1042],{"class":1438},[298,3466,1396],{"class":319},[298,3468,2232],{"class":323},[298,3470,1396],{"class":319},[298,3472,1447],{"class":1438},[298,3474,1084],{"class":304},[298,3476,3477,3479,3481,3483,3486,3488,3490,3493,3495,3497,3499,3502,3504,3506],{"class":300,"line":1640},[298,3478,1428],{"class":1427},[298,3480,1538],{"class":1390},[298,3482,1042],{"class":1438},[298,3484,3485],{"class":346},"page",[298,3487,1447],{"class":1438},[298,3489,234],{"class":304},[298,3491,3492],{"class":1390},"toHaveURL",[298,3494,1042],{"class":1438},[298,3496,764],{"class":319},[298,3498,281],{"class":323},[298,3500,3501],{"class":1427},"$",[298,3503,764],{"class":319},[298,3505,1447],{"class":1438},[298,3507,1084],{"class":304},[298,3509,3510,3512,3514],{"class":300,"line":1668},[298,3511,1814],{"class":304},[298,3513,1447],{"class":346},[298,3515,1084],{"class":304},[189,3517,3519],{"id":3518},"unique-landmark-labels","Unique landmark labels",[11,3521,3522],{},"Catch duplicate landmark names (best practive to have programmatically unique landmarks)",[289,3524,3526],{"className":1380,"code":3525,"filename":1382,"language":1383,"meta":294,"style":294},"test('all nav landmarks have unique accessible names', async ({ page }) => {\n  await page.goto('/');\n  const navs = await page.getByRole('navigation').all();\n  const labels = await Promise.all(\n    navs.map((nav) => nav.getAttribute('aria-label')),\n  );\n\n  // Verify every nav has a label\n  labels.forEach((label, index) => {\n    expect(label, `Navigation element ${index + 1} is missing an aria-label`).not.toBeNull();\n  });\n\n  // Verify all labels are unique\n  const uniqueLabels = new Set(labels);\n  expect(uniqueLabels.size).toBe(labels.length);\n});\n",[15,3527,3528,3555,3577,3613,3634,3676,3683,3687,3692,3720,3771,3780,3784,3789,3812,3845],{"__ignoreMap":294},[298,3529,3530,3532,3534,3536,3539,3541,3543,3545,3547,3549,3551,3553],{"class":300,"line":301},[298,3531,1391],{"class":1390},[298,3533,1042],{"class":346},[298,3535,1396],{"class":319},[298,3537,3538],{"class":323},"all nav landmarks have unique accessible names",[298,3540,1396],{"class":319},[298,3542,1053],{"class":304},[298,3544,1407],{"class":1406},[298,3546,1410],{"class":304},[298,3548,1414],{"class":1413},[298,3550,1417],{"class":304},[298,3552,1420],{"class":1406},[298,3554,1026],{"class":304},[298,3556,3557,3559,3561,3563,3565,3567,3569,3571,3573,3575],{"class":300,"line":343},[298,3558,1428],{"class":1427},[298,3560,1414],{"class":346},[298,3562,234],{"class":304},[298,3564,1435],{"class":1390},[298,3566,1042],{"class":1438},[298,3568,1396],{"class":319},[298,3570,764],{"class":323},[298,3572,1396],{"class":319},[298,3574,1447],{"class":1438},[298,3576,1084],{"class":304},[298,3578,3579,3581,3584,3586,3588,3590,3592,3594,3596,3598,3600,3602,3604,3606,3609,3611],{"class":300,"line":350},[298,3580,1506],{"class":1406},[298,3582,3583],{"class":1049}," navs",[298,3585,1512],{"class":1045},[298,3587,1651],{"class":1427},[298,3589,1414],{"class":346},[298,3591,234],{"class":304},[298,3593,1460],{"class":1390},[298,3595,1042],{"class":1438},[298,3597,1396],{"class":319},[298,3599,1189],{"class":323},[298,3601,1396],{"class":319},[298,3603,1447],{"class":1438},[298,3605,234],{"class":304},[298,3607,3608],{"class":1390},"all",[298,3610,1499],{"class":1438},[298,3612,1084],{"class":304},[298,3614,3615,3617,3620,3622,3624,3627,3629,3631],{"class":300,"line":460},[298,3616,1506],{"class":1406},[298,3618,3619],{"class":1049}," labels",[298,3621,1512],{"class":1045},[298,3623,1651],{"class":1427},[298,3625,3626],{"class":1022}," Promise",[298,3628,234],{"class":304},[298,3630,3608],{"class":1390},[298,3632,3633],{"class":1438},"(\n",[298,3635,3636,3639,3641,3644,3646,3648,3651,3653,3655,3658,3660,3663,3665,3667,3669,3671,3674],{"class":300,"line":491},[298,3637,3638],{"class":346},"    navs",[298,3640,234],{"class":304},[298,3642,3643],{"class":1390},"map",[298,3645,1042],{"class":1438},[298,3647,1042],{"class":304},[298,3649,3650],{"class":1413},"nav",[298,3652,1447],{"class":304},[298,3654,1420],{"class":1406},[298,3656,3657],{"class":346}," nav",[298,3659,234],{"class":304},[298,3661,3662],{"class":1390},"getAttribute",[298,3664,1042],{"class":1438},[298,3666,1396],{"class":319},[298,3668,171],{"class":323},[298,3670,1396],{"class":319},[298,3672,3673],{"class":1438},"))",[298,3675,1702],{"class":304},[298,3677,3678,3681],{"class":300,"line":509},[298,3679,3680],{"class":1438},"  )",[298,3682,1084],{"class":304},[298,3684,3685],{"class":300,"line":519},[298,3686,744],{"emptyLinePlaceholder":743},[298,3688,3689],{"class":300,"line":949},[298,3690,3691],{"class":434},"  // Verify every nav has a label\n",[298,3693,3694,3697,3699,3702,3704,3706,3709,3711,3714,3716,3718],{"class":300,"line":1609},[298,3695,3696],{"class":346},"  labels",[298,3698,234],{"class":304},[298,3700,3701],{"class":1390},"forEach",[298,3703,1042],{"class":1438},[298,3705,1042],{"class":304},[298,3707,3708],{"class":1413},"label",[298,3710,1053],{"class":304},[298,3712,3713],{"class":1413}," index",[298,3715,1447],{"class":304},[298,3717,1420],{"class":1406},[298,3719,1026],{"class":304},[298,3721,3722,3724,3726,3728,3730,3733,3736,3739,3742,3745,3748,3750,3753,3756,3758,3760,3762,3764,3767,3769],{"class":300,"line":1640},[298,3723,1716],{"class":1390},[298,3725,1042],{"class":1438},[298,3727,3708],{"class":346},[298,3729,1053],{"class":304},[298,3731,3732],{"class":319}," `",[298,3734,3735],{"class":323},"Navigation element ",[298,3737,3738],{"class":319},"${",[298,3740,3741],{"class":346},"index",[298,3743,3744],{"class":1045}," +",[298,3746,3747],{"class":1581}," 1",[298,3749,1814],{"class":319},[298,3751,3752],{"class":323}," is missing an aria-label",[298,3754,3755],{"class":319},"`",[298,3757,1447],{"class":1438},[298,3759,234],{"class":304},[298,3761,1800],{"class":346},[298,3763,234],{"class":304},[298,3765,3766],{"class":1390},"toBeNull",[298,3768,1499],{"class":1438},[298,3770,1084],{"class":304},[298,3772,3773,3776,3778],{"class":300,"line":1668},[298,3774,3775],{"class":304},"  }",[298,3777,1447],{"class":1438},[298,3779,1084],{"class":304},[298,3781,3782],{"class":300,"line":1705},[298,3783,744],{"emptyLinePlaceholder":743},[298,3785,3786],{"class":300,"line":1713},[298,3787,3788],{"class":434},"  // Verify all labels are unique\n",[298,3790,3791,3793,3796,3798,3800,3803,3805,3808,3810],{"class":300,"line":1740},[298,3792,1506],{"class":1406},[298,3794,3795],{"class":1049}," uniqueLabels",[298,3797,1512],{"class":1045},[298,3799,2534],{"class":1045},[298,3801,3802],{"class":1390}," Set",[298,3804,1042],{"class":1438},[298,3806,3807],{"class":346},"labels",[298,3809,1447],{"class":1438},[298,3811,1084],{"class":304},[298,3813,3814,3816,3818,3821,3823,3826,3828,3830,3832,3834,3836,3838,3841,3843],{"class":300,"line":1746},[298,3815,2567],{"class":1390},[298,3817,1042],{"class":1438},[298,3819,3820],{"class":346},"uniqueLabels",[298,3822,234],{"class":304},[298,3824,3825],{"class":346},"size",[298,3827,1447],{"class":1438},[298,3829,234],{"class":304},[298,3831,1728],{"class":1390},[298,3833,1042],{"class":1438},[298,3835,3807],{"class":346},[298,3837,234],{"class":304},[298,3839,3840],{"class":1049},"length",[298,3842,1447],{"class":1438},[298,3844,1084],{"class":304},[298,3846,3847,3849,3851],{"class":300,"line":1751},[298,3848,1814],{"class":304},[298,3850,1447],{"class":346},[298,3852,1084],{"class":304},[189,3854,3856],{"id":3855},"ambiguous-link-text","Ambiguous link text",[11,3858,3859],{},"Check for known offenders that pass structural validation but fail in context.",[289,3861,3863],{"className":1380,"code":3862,"filename":1382,"language":1383,"meta":294,"style":294},"test('no ambiguous link text', async ({ page }) => {\n  await page.goto('/');\n  const ambiguous = ['read more', 'click here', 'browse all', 'learn more', 'here', 'more'];\n  const links = await page.getByRole('link').all();\n  for (const link of links) {\n    const text = (await link.innerText()).toLowerCase().trim();\n    expect(ambiguous, `Ambiguous link text found: \"${text}\"`).not.toContain(text);\n  }\n});\n",[15,3864,3865,3892,3914,3983,4019,4040,4080,4126,4130],{"__ignoreMap":294},[298,3866,3867,3869,3871,3873,3876,3878,3880,3882,3884,3886,3888,3890],{"class":300,"line":301},[298,3868,1391],{"class":1390},[298,3870,1042],{"class":346},[298,3872,1396],{"class":319},[298,3874,3875],{"class":323},"no ambiguous link text",[298,3877,1396],{"class":319},[298,3879,1053],{"class":304},[298,3881,1407],{"class":1406},[298,3883,1410],{"class":304},[298,3885,1414],{"class":1413},[298,3887,1417],{"class":304},[298,3889,1420],{"class":1406},[298,3891,1026],{"class":304},[298,3893,3894,3896,3898,3900,3902,3904,3906,3908,3910,3912],{"class":300,"line":343},[298,3895,1428],{"class":1427},[298,3897,1414],{"class":346},[298,3899,234],{"class":304},[298,3901,1435],{"class":1390},[298,3903,1042],{"class":1438},[298,3905,1396],{"class":319},[298,3907,764],{"class":323},[298,3909,1396],{"class":319},[298,3911,1447],{"class":1438},[298,3913,1084],{"class":304},[298,3915,3916,3918,3921,3923,3926,3928,3931,3933,3935,3937,3940,3942,3944,3946,3949,3951,3953,3955,3958,3960,3962,3964,3967,3969,3971,3973,3976,3978,3981],{"class":300,"line":350},[298,3917,1506],{"class":1406},[298,3919,3920],{"class":1049}," ambiguous",[298,3922,1512],{"class":1045},[298,3924,3925],{"class":1438}," [",[298,3927,1396],{"class":319},[298,3929,3930],{"class":323},"read more",[298,3932,1396],{"class":319},[298,3934,1053],{"class":304},[298,3936,1481],{"class":319},[298,3938,3939],{"class":323},"click here",[298,3941,1396],{"class":319},[298,3943,1053],{"class":304},[298,3945,1481],{"class":319},[298,3947,3948],{"class":323},"browse all",[298,3950,1396],{"class":319},[298,3952,1053],{"class":304},[298,3954,1481],{"class":319},[298,3956,3957],{"class":323},"learn more",[298,3959,1396],{"class":319},[298,3961,1053],{"class":304},[298,3963,1481],{"class":319},[298,3965,3966],{"class":323},"here",[298,3968,1396],{"class":319},[298,3970,1053],{"class":304},[298,3972,1481],{"class":319},[298,3974,3975],{"class":323},"more",[298,3977,1396],{"class":319},[298,3979,3980],{"class":1438},"]",[298,3982,1084],{"class":304},[298,3984,3985,3987,3990,3992,3994,3996,3998,4000,4002,4004,4007,4009,4011,4013,4015,4017],{"class":300,"line":460},[298,3986,1506],{"class":1406},[298,3988,3989],{"class":1049}," links",[298,3991,1512],{"class":1045},[298,3993,1651],{"class":1427},[298,3995,1414],{"class":346},[298,3997,234],{"class":304},[298,3999,1460],{"class":1390},[298,4001,1042],{"class":1438},[298,4003,1396],{"class":319},[298,4005,4006],{"class":323},"link",[298,4008,1396],{"class":319},[298,4010,1447],{"class":1438},[298,4012,234],{"class":304},[298,4014,3608],{"class":1390},[298,4016,1499],{"class":1438},[298,4018,1084],{"class":304},[298,4020,4021,4023,4025,4028,4031,4034,4036,4038],{"class":300,"line":491},[298,4022,1567],{"class":1427},[298,4024,1570],{"class":1438},[298,4026,4027],{"class":1406},"const",[298,4029,4030],{"class":1049}," link",[298,4032,4033],{"class":1045}," of",[298,4035,3989],{"class":346},[298,4037,1603],{"class":1438},[298,4039,1606],{"class":304},[298,4041,4042,4044,4047,4049,4051,4054,4056,4058,4061,4064,4066,4069,4071,4073,4076,4078],{"class":300,"line":509},[298,4043,1643],{"class":1406},[298,4045,4046],{"class":1049}," text",[298,4048,1512],{"class":1045},[298,4050,1570],{"class":1438},[298,4052,4053],{"class":1427},"await",[298,4055,4030],{"class":346},[298,4057,234],{"class":304},[298,4059,4060],{"class":1390},"innerText",[298,4062,4063],{"class":1438},"())",[298,4065,234],{"class":304},[298,4067,4068],{"class":1390},"toLowerCase",[298,4070,1499],{"class":1438},[298,4072,234],{"class":304},[298,4074,4075],{"class":1390},"trim",[298,4077,1499],{"class":1438},[298,4079,1084],{"class":304},[298,4081,4082,4084,4086,4089,4091,4093,4096,4098,4101,4103,4105,4107,4109,4111,4113,4115,4118,4120,4122,4124],{"class":300,"line":519},[298,4083,1716],{"class":1390},[298,4085,1042],{"class":1438},[298,4087,4088],{"class":346},"ambiguous",[298,4090,1053],{"class":304},[298,4092,3732],{"class":319},[298,4094,4095],{"class":323},"Ambiguous link text found: \"",[298,4097,3738],{"class":319},[298,4099,4100],{"class":346},"text",[298,4102,1814],{"class":319},[298,4104,320],{"class":323},[298,4106,3755],{"class":319},[298,4108,1447],{"class":1438},[298,4110,234],{"class":304},[298,4112,1800],{"class":346},[298,4114,234],{"class":304},[298,4116,4117],{"class":1390},"toContain",[298,4119,1042],{"class":1438},[298,4121,4100],{"class":346},[298,4123,1447],{"class":1438},[298,4125,1084],{"class":304},[298,4127,4128],{"class":300,"line":949},[298,4129,1743],{"class":304},[298,4131,4132,4134,4136],{"class":300,"line":1609},[298,4133,1814],{"class":304},[298,4135,1447],{"class":346},[298,4137,1084],{"class":304},[189,4139,4141],{"id":4140},"dynamic-aria-label-on-stateful-controls","Dynamic aria-label on stateful controls",[11,4143,4144],{},"Verify that a toggle's accessible name reflects the action it will perform, not just that it changes after interaction. A color mode toggle in light mode should read \"Switch to dark mode\" — and once clicked, \"Switch to light mode.\" A label that says \"Switch to light mode\" when the page is already in light mode is wrong/misleading.",[289,4146,4149],{"className":1380,"code":4147,"filename":4148,"language":1383,"meta":294,"style":294},"test('color mode toggle label reflects current state', async ({ page }) => {\n  // Start in light mode — toggle should offer to switch to dark\n  await page.emulateMedia({ colorScheme: 'light' });\n  await page.goto('/');\n  const toggle = page.getByRole('button', { name: /switch to/i });\n  await expect(toggle).toHaveAttribute('aria-label', 'Switch to dark mode');\n\n  // After clicking, page is in dark mode — toggle should offer to switch to light\n  await toggle.click();\n  await expect(toggle).toHaveAttribute('aria-label', 'Switch to light mode');\n});\n","color-mode-toggle-label-test.spec.ts",[15,4150,4151,4178,4183,4213,4235,4283,4320,4324,4329,4343,4380],{"__ignoreMap":294},[298,4152,4153,4155,4157,4159,4162,4164,4166,4168,4170,4172,4174,4176],{"class":300,"line":301},[298,4154,1391],{"class":1390},[298,4156,1042],{"class":346},[298,4158,1396],{"class":319},[298,4160,4161],{"class":323},"color mode toggle label reflects current state",[298,4163,1396],{"class":319},[298,4165,1053],{"class":304},[298,4167,1407],{"class":1406},[298,4169,1410],{"class":304},[298,4171,1414],{"class":1413},[298,4173,1417],{"class":304},[298,4175,1420],{"class":1406},[298,4177,1026],{"class":304},[298,4179,4180],{"class":300,"line":343},[298,4181,4182],{"class":434},"  // Start in light mode — toggle should offer to switch to dark\n",[298,4184,4185,4187,4189,4191,4193,4195,4197,4199,4201,4203,4205,4207,4209,4211],{"class":300,"line":350},[298,4186,1428],{"class":1427},[298,4188,1414],{"class":346},[298,4190,234],{"class":304},[298,4192,3004],{"class":1390},[298,4194,1042],{"class":1438},[298,4196,2542],{"class":304},[298,4198,3011],{"class":1438},[298,4200,1035],{"class":304},[298,4202,1481],{"class":319},[298,4204,3164],{"class":323},[298,4206,1396],{"class":319},[298,4208,1489],{"class":304},[298,4210,1447],{"class":1438},[298,4212,1084],{"class":304},[298,4214,4215,4217,4219,4221,4223,4225,4227,4229,4231,4233],{"class":300,"line":460},[298,4216,1428],{"class":1427},[298,4218,1414],{"class":346},[298,4220,234],{"class":304},[298,4222,1435],{"class":1390},[298,4224,1042],{"class":1438},[298,4226,1396],{"class":319},[298,4228,764],{"class":323},[298,4230,1396],{"class":319},[298,4232,1447],{"class":1438},[298,4234,1084],{"class":304},[298,4236,4237,4239,4241,4243,4245,4247,4249,4251,4253,4255,4257,4259,4261,4263,4265,4268,4271,4273,4277,4279,4281],{"class":300,"line":491},[298,4238,1506],{"class":1406},[298,4240,2056],{"class":1049},[298,4242,1512],{"class":1045},[298,4244,1414],{"class":346},[298,4246,234],{"class":304},[298,4248,1460],{"class":1390},[298,4250,1042],{"class":1438},[298,4252,1396],{"class":319},[298,4254,543],{"class":323},[298,4256,1396],{"class":319},[298,4258,1053],{"class":304},[298,4260,1473],{"class":304},[298,4262,1476],{"class":1438},[298,4264,1035],{"class":304},[298,4266,4267],{"class":319}," /",[298,4269,4270],{"class":323},"switch to",[298,4272,764],{"class":319},[298,4274,4276],{"class":4275},"sPY_W","i",[298,4278,1489],{"class":304},[298,4280,1447],{"class":1438},[298,4282,1084],{"class":304},[298,4284,4285,4287,4289,4291,4293,4295,4297,4299,4301,4303,4305,4307,4309,4311,4314,4316,4318],{"class":300,"line":509},[298,4286,1428],{"class":1427},[298,4288,1538],{"class":1390},[298,4290,1042],{"class":1438},[298,4292,2155],{"class":346},[298,4294,1447],{"class":1438},[298,4296,234],{"class":304},[298,4298,2162],{"class":1390},[298,4300,1042],{"class":1438},[298,4302,1396],{"class":319},[298,4304,171],{"class":323},[298,4306,1396],{"class":319},[298,4308,1053],{"class":304},[298,4310,1481],{"class":319},[298,4312,4313],{"class":323},"Switch to dark mode",[298,4315,1396],{"class":319},[298,4317,1447],{"class":1438},[298,4319,1084],{"class":304},[298,4321,4322],{"class":300,"line":519},[298,4323,744],{"emptyLinePlaceholder":743},[298,4325,4326],{"class":300,"line":949},[298,4327,4328],{"class":434},"  // After clicking, page is in dark mode — toggle should offer to switch to light\n",[298,4330,4331,4333,4335,4337,4339,4341],{"class":300,"line":1609},[298,4332,1428],{"class":1427},[298,4334,2056],{"class":346},[298,4336,234],{"class":304},[298,4338,1496],{"class":1390},[298,4340,1499],{"class":1438},[298,4342,1084],{"class":304},[298,4344,4345,4347,4349,4351,4353,4355,4357,4359,4361,4363,4365,4367,4369,4371,4374,4376,4378],{"class":300,"line":1640},[298,4346,1428],{"class":1427},[298,4348,1538],{"class":1390},[298,4350,1042],{"class":1438},[298,4352,2155],{"class":346},[298,4354,1447],{"class":1438},[298,4356,234],{"class":304},[298,4358,2162],{"class":1390},[298,4360,1042],{"class":1438},[298,4362,1396],{"class":319},[298,4364,171],{"class":323},[298,4366,1396],{"class":319},[298,4368,1053],{"class":304},[298,4370,1481],{"class":319},[298,4372,4373],{"class":323},"Switch to light mode",[298,4375,1396],{"class":319},[298,4377,1447],{"class":1438},[298,4379,1084],{"class":304},[298,4381,4382,4384,4386],{"class":300,"line":1668},[298,4383,1814],{"class":304},[298,4385,1447],{"class":346},[298,4387,1084],{"class":304},[189,4389,4391],{"id":4390},"generic-alt-text","Generic alt text",[11,4393,4394],{},"Catch alt text that was added to silence a linter but communicates nothing.",[289,4396,4398],{"className":1380,"code":4397,"filename":1382,"language":1383,"meta":294,"style":294},"test('no generic alt text on images', async ({ page }) => {\n  await page.goto('/');\n  const genericAlts = ['image', 'photo', 'picture', 'icon', 'decorative image', 'banner', 'img'];\n  const images = await page.locator('img[alt]').all();\n  for (const img of images) {\n    const alt = (await img.getAttribute('alt') ?? '').toLowerCase().trim();\n    if (alt !== '') {\n      // Empty alt is valid and intentional for decorative images — don't flag it\n      expect(genericAlts, `Generic alt text found: \"${alt}\"`).not.toContain(alt);\n    }\n  }\n});\n",[15,4399,4400,4427,4449,4524,4560,4579,4629,4647,4652,4697,4702,4706],{"__ignoreMap":294},[298,4401,4402,4404,4406,4408,4411,4413,4415,4417,4419,4421,4423,4425],{"class":300,"line":301},[298,4403,1391],{"class":1390},[298,4405,1042],{"class":346},[298,4407,1396],{"class":319},[298,4409,4410],{"class":323},"no generic alt text on images",[298,4412,1396],{"class":319},[298,4414,1053],{"class":304},[298,4416,1407],{"class":1406},[298,4418,1410],{"class":304},[298,4420,1414],{"class":1413},[298,4422,1417],{"class":304},[298,4424,1420],{"class":1406},[298,4426,1026],{"class":304},[298,4428,4429,4431,4433,4435,4437,4439,4441,4443,4445,4447],{"class":300,"line":343},[298,4430,1428],{"class":1427},[298,4432,1414],{"class":346},[298,4434,234],{"class":304},[298,4436,1435],{"class":1390},[298,4438,1042],{"class":1438},[298,4440,1396],{"class":319},[298,4442,764],{"class":323},[298,4444,1396],{"class":319},[298,4446,1447],{"class":1438},[298,4448,1084],{"class":304},[298,4450,4451,4453,4456,4458,4460,4462,4465,4467,4469,4471,4474,4476,4478,4480,4483,4485,4487,4489,4492,4494,4496,4498,4501,4503,4505,4507,4510,4512,4514,4516,4518,4520,4522],{"class":300,"line":350},[298,4452,1506],{"class":1406},[298,4454,4455],{"class":1049}," genericAlts",[298,4457,1512],{"class":1045},[298,4459,3925],{"class":1438},[298,4461,1396],{"class":319},[298,4463,4464],{"class":323},"image",[298,4466,1396],{"class":319},[298,4468,1053],{"class":304},[298,4470,1481],{"class":319},[298,4472,4473],{"class":323},"photo",[298,4475,1396],{"class":319},[298,4477,1053],{"class":304},[298,4479,1481],{"class":319},[298,4481,4482],{"class":323},"picture",[298,4484,1396],{"class":319},[298,4486,1053],{"class":304},[298,4488,1481],{"class":319},[298,4490,4491],{"class":323},"icon",[298,4493,1396],{"class":319},[298,4495,1053],{"class":304},[298,4497,1481],{"class":319},[298,4499,4500],{"class":323},"decorative image",[298,4502,1396],{"class":319},[298,4504,1053],{"class":304},[298,4506,1481],{"class":319},[298,4508,4509],{"class":323},"banner",[298,4511,1396],{"class":319},[298,4513,1053],{"class":304},[298,4515,1481],{"class":319},[298,4517,368],{"class":323},[298,4519,1396],{"class":319},[298,4521,3980],{"class":1438},[298,4523,1084],{"class":304},[298,4525,4526,4528,4531,4533,4535,4537,4539,4541,4543,4545,4548,4550,4552,4554,4556,4558],{"class":300,"line":460},[298,4527,1506],{"class":1406},[298,4529,4530],{"class":1049}," images",[298,4532,1512],{"class":1045},[298,4534,1651],{"class":1427},[298,4536,1414],{"class":346},[298,4538,234],{"class":304},[298,4540,3359],{"class":1390},[298,4542,1042],{"class":1438},[298,4544,1396],{"class":319},[298,4546,4547],{"class":323},"img[alt]",[298,4549,1396],{"class":319},[298,4551,1447],{"class":1438},[298,4553,234],{"class":304},[298,4555,3608],{"class":1390},[298,4557,1499],{"class":1438},[298,4559,1084],{"class":304},[298,4561,4562,4564,4566,4568,4571,4573,4575,4577],{"class":300,"line":491},[298,4563,1567],{"class":1427},[298,4565,1570],{"class":1438},[298,4567,4027],{"class":1406},[298,4569,4570],{"class":1049}," img",[298,4572,4033],{"class":1045},[298,4574,4530],{"class":346},[298,4576,1603],{"class":1438},[298,4578,1606],{"class":304},[298,4580,4581,4583,4585,4587,4589,4591,4593,4595,4597,4599,4601,4603,4605,4607,4610,4613,4615,4617,4619,4621,4623,4625,4627],{"class":300,"line":509},[298,4582,1643],{"class":1406},[298,4584,726],{"class":1049},[298,4586,1512],{"class":1045},[298,4588,1570],{"class":1438},[298,4590,4053],{"class":1427},[298,4592,4570],{"class":346},[298,4594,234],{"class":304},[298,4596,3662],{"class":1390},[298,4598,1042],{"class":1438},[298,4600,1396],{"class":319},[298,4602,136],{"class":323},[298,4604,1396],{"class":319},[298,4606,1603],{"class":1438},[298,4608,4609],{"class":1045},"??",[298,4611,4612],{"class":319}," ''",[298,4614,1447],{"class":1438},[298,4616,234],{"class":304},[298,4618,4068],{"class":1390},[298,4620,1499],{"class":1438},[298,4622,234],{"class":304},[298,4624,4075],{"class":1390},[298,4626,1499],{"class":1438},[298,4628,1084],{"class":304},[298,4630,4631,4634,4636,4638,4641,4643,4645],{"class":300,"line":519},[298,4632,4633],{"class":1427},"    if",[298,4635,1570],{"class":1438},[298,4637,136],{"class":346},[298,4639,4640],{"class":1045}," !==",[298,4642,4612],{"class":319},[298,4644,1603],{"class":1438},[298,4646,1606],{"class":304},[298,4648,4649],{"class":300,"line":949},[298,4650,4651],{"class":434},"      // Empty alt is valid and intentional for decorative images — don't flag it\n",[298,4653,4654,4657,4659,4662,4664,4666,4669,4671,4673,4675,4677,4679,4681,4683,4685,4687,4689,4691,4693,4695],{"class":300,"line":1609},[298,4655,4656],{"class":1390},"      expect",[298,4658,1042],{"class":1438},[298,4660,4661],{"class":346},"genericAlts",[298,4663,1053],{"class":304},[298,4665,3732],{"class":319},[298,4667,4668],{"class":323},"Generic alt text found: \"",[298,4670,3738],{"class":319},[298,4672,136],{"class":346},[298,4674,1814],{"class":319},[298,4676,320],{"class":323},[298,4678,3755],{"class":319},[298,4680,1447],{"class":1438},[298,4682,234],{"class":304},[298,4684,1800],{"class":346},[298,4686,234],{"class":304},[298,4688,4117],{"class":1390},[298,4690,1042],{"class":1438},[298,4692,136],{"class":346},[298,4694,1447],{"class":1438},[298,4696,1084],{"class":304},[298,4698,4699],{"class":300,"line":1640},[298,4700,4701],{"class":304},"    }\n",[298,4703,4704],{"class":300,"line":1668},[298,4705,1743],{"class":304},[298,4707,4708,4710,4712],{"class":300,"line":1705},[298,4709,1814],{"class":304},[298,4711,1447],{"class":346},[298,4713,1084],{"class":304},[23,4715],{},[26,4717,4719],{"id":4718},"manual-accessibility-tests-to-supplement-automated-tests","Manual Accessibility Tests to Supplement Automated Tests",[11,4721,4722,4723,4725],{},"Some accessibility defects cannot be detected through test automation alone. Whether a label is ",[197,4724,220],{}," vs. merely present, whether focus order feels logical to a real user, whether a screen reader announces content in a way that makes sense — these all require a human judgment call.",[11,4727,4728],{},"Use this checklist as part of your periodic accessibility audit — the checks automation structurally cannot perform:",[128,4730,4733,4742,4748,4754,4760,4766,4775,4781],{"className":4731},[4732],"contains-task-list",[131,4734,4737,4741],{"className":4735},[4736],"task-list-item",[4738,4739],"input",{"disabled":743,"type":4740},"checkbox"," Tab through the full page with no mouse — does focus order make sense?",[131,4743,4745,4747],{"className":4744},[4736],[4738,4746],{"disabled":743,"type":4740}," Is a skip link the first focusable element?",[131,4749,4751,4753],{"className":4750},[4736],[4738,4752],{"disabled":743,"type":4740}," Are all landmark regions labeled and distinct?",[131,4755,4757,4759],{"className":4756},[4736],[4738,4758],{"disabled":743,"type":4740}," Does every link make sense read aloud out of context?",[131,4761,4763,4765],{"className":4762},[4736],[4738,4764],{"disabled":743,"type":4740}," Do dynamic controls update their accessible name when state changes?",[131,4767,4769,4771,4772,4774],{"className":4768},[4736],[4738,4770],{"disabled":743,"type":4740}," Do ",[15,4773,380],{}," references point to visible, accurate text?",[131,4776,4778,4780],{"className":4777},[4736],[4738,4779],{"disabled":743,"type":4740}," Test with a real screen reader — NVDA (free, Windows), JAWS, or VoiceOver (built into macOS and iOS)",[131,4782,4784,4786],{"className":4783},[4736],[4738,4785],{"disabled":743,"type":4740}," Navigate by landmarks, headings, and links — not just linearly",[11,4788,4789],{},"Run the screen reader in a way that reflects how actual users navigate. Screen reader users may not read a page top to bottom. Instead, they may jump between headings, landmarks, and links to orient themselves. If that navigation doesn't make sense, the page isn't accessible regardless of what the automated scan says.",[23,4791],{},[26,4793,4795],{"id":4794},"conclusion","Conclusion",[11,4797,4798,4799,4801],{},"Automated accessibility tools like Lighthouse, axe, and Playwright tests (leveraging axe-core) are a floor, not a ceiling. Use them to catch mechanical violations quickly, run accessibility tests on every CI build to guard against regressions, and write tests that go beyond the default scan to cover the states and conditions a basic pass misses. Unlike functional bugs that break visible behavior, many accessibility defects are invisible to sighted developers — a stale ",[15,4800,171],{},", a dropped landmark, or a broken focus order leaves no visual trace and no failing test without an explicit check.",[11,4803,4804],{},"Think of them as a robot vacuum. They cover large areas automatically and pick up the dirt that accumulates between manual sessions, but you still need to manually vacuum the spots they can't reach.",[11,4806,4807],{},"Leverage automated tools as both a coverage floor and a regression backstop, freeing you to focus your manual testing time on the accessibility defects that require human judgment and real-user navigation.",[4809,4810],"read-next",{":items":4811},"[\"/software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan\"]",[4813,4814,4815],"style",{},"html pre.shiki code .sb1SK, html code.shiki .sb1SK{--shiki-light:#6182B8;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}html pre.shiki code .sZ-rw, html code.shiki .sZ-rw{--shiki-light:#90A4AE;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sZi47, html code.shiki .sZi47{--shiki-light:#39ADB5;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .srGNg, html code.shiki .srGNg{--shiki-light:#91B859;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sPJuK, html code.shiki .sPJuK{--shiki-light:#39ADB5;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .stWsX, html code.shiki .stWsX{--shiki-light:#9C3EDA;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .s2xgV, html code.shiki .s2xgV{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#702C00;--shiki-default-font-style:inherit;--shiki-dark:#FFB757;--shiki-dark-font-style:inherit}html pre.shiki code .sZTni, html code.shiki .sZTni{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#A0111F;--shiki-default-font-style:inherit;--shiki-dark:#FF9492;--shiki-dark-font-style:inherit}html pre.shiki code .sq0XF, html code.shiki .sq0XF{--shiki-light:#E53935;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sQ79N, html code.shiki .sQ79N{--shiki-light:#90A4AE;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sE6rD, html code.shiki .sE6rD{--shiki-light:#39ADB5;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .s_gjE, html code.shiki .s_gjE{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#66707B;--shiki-default-font-style:inherit;--shiki-dark:#BDC4CC;--shiki-dark-font-style:inherit}html pre.shiki code .s6g51, html code.shiki .s6g51{--shiki-light:#F76D47;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sPxkN, html code.shiki .sPxkN{--shiki-light:#39ADB5;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sTqCK, html code.shiki .sTqCK{--shiki-light:#FF5370;--shiki-default:#023B95;--shiki-dark:#91CBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .slOWO, html code.shiki .slOWO{--shiki-light:#E2931D;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sPY_W, html code.shiki .sPY_W{--shiki-light:#F76D47;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .saWzx, html code.shiki .saWzx{--shiki-light:#E53935;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .sM74w, html code.shiki .sM74w{--shiki-light:#9C3EDA;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .shxsR, html code.shiki .shxsR{--shiki-light:#8796B0;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .slPND, html code.shiki .slPND{--shiki-light:#6182B8;--shiki-default:#023B95;--shiki-dark:#91CBFF}",{"title":294,"searchDepth":343,"depth":343,"links":4817},[4818,4819,4820,4833,4844,4845],{"id":28,"depth":343,"text":29},{"id":119,"depth":343,"text":120},{"id":186,"depth":343,"text":187,"children":4821},[4822,4823,4824,4826,4827,4828,4829,4830,4831,4832],{"id":191,"depth":350,"text":192},{"id":256,"depth":350,"text":257},{"id":376,"depth":350,"text":4825},"3. aria-labelledby vs aria-label — Both Pass, One Is Better",{"id":610,"depth":350,"text":611},{"id":658,"depth":350,"text":659},{"id":818,"depth":350,"text":819},{"id":960,"depth":350,"text":961},{"id":1120,"depth":350,"text":1121},{"id":1229,"depth":350,"text":1230},{"id":1310,"depth":350,"text":1311},{"id":2401,"depth":343,"text":2402,"children":4834},[4835,4836,4837,4838,4839,4840,4841,4842,4843],{"id":2415,"depth":350,"text":2416},{"id":2600,"depth":350,"text":2601},{"id":2779,"depth":350,"text":2780},{"id":2957,"depth":350,"text":2958},{"id":3259,"depth":350,"text":3260},{"id":3518,"depth":350,"text":3519},{"id":3855,"depth":350,"text":3856},{"id":4140,"depth":350,"text":4141},{"id":4390,"depth":350,"text":4391},{"id":4718,"depth":343,"text":4719},{"id":4794,"depth":343,"text":4795},"/images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/what-automated-accessibility-tests-miss-cover.webp","2026-04-10","axe and Lighthouse miss 60–70% of WCAG violations. Learn the limitations of automated accessibility testing and how to write smarter Playwright tests.",false,"md",{},"/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations",{"title":5,"description":4848},"software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations","6adQE-xkFRndFQwFyvJsZ3OriqWmvud_MK3vfJ14sJE",[4857],{"id":4858,"title":4859,"bmcUsername":6,"body":4860,"cover":5895,"date":5896,"description":5897,"draft":4849,"extension":4850,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":5898,"navigation":743,"npmPackage":6,"order":6,"path":5899,"seo":5900,"stem":5901,"__hash__":5902},"content/software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan.md","Implementing a Minimum Accessibility Test Plan",{"type":8,"value":4861,"toc":5881},[4862,4866,4869,4872,4879,4889,4892,4896,4900,4909,4912,4932,4935,4938,4942,4990,4993,4997,5000,5007,5010,5013,5016,5024,5027,5031,5207,5211,5330,5344,5348,5351,5354,5357,5360,5363,5456,5459,5799,5802,5806,5809,5812,5819,5823,5826,5829,5832,5835,5842,5845,5849,5852,5855,5858,5866,5875,5878],[26,4863,4865],{"id":4864},"implementing-a-test-plan-for-accessibility-testing","Implementing a Test Plan for Accessibility Testing",[11,4867,4868],{},"Accessibility testing is similar to functional testing where you test defined expected behavior against the software under test. However, there are some unique challenges with automated accessibility testing.",[11,4870,4871],{},"For one, accessibility for websites has not been a focus of many companies until recently. This means you have a testing model where the product is already built against different expected requirements and behaviors. At minimum this means there is going to be rework for the developers.",[11,4873,4874,4875,4878],{},"I don't advocate for surprising developers with your \"gotcha\" test cases, the ones you bring out to find those ",[197,4876,4877],{},"really awesome"," defects at the last minute. It leads to having an adversarial relationship between developers and test engineers. Agreeing on the requirements, what, and how you will be testing before developers get started leads to more collaboration and less rework.",[11,4880,4881,4882,4888],{},"As of writing, there are 90 rules in ",[309,4883,4887],{"href":4884,"rel":4885},"https://dequeuniversity.com/rules/axe/4.1",[4886],"nofollow","aXe 4.1"," and just testing against the full list isn't an efficient way to do this. You'd end up flooding your backlog with accessibility defects and probably stress out your developers and analysts.",[11,4890,4891],{},"The solution I'd propose is to create a minimum accessibility test plan, get agreement that this is the minimum expected behavior of the software under test, and then automate those test cases to ensure the accessibility issues don't come back.",[4893,4894],"u-video",{"src":4895},"https://www.youtube.com/embed/lsv_lwxu2tI",[26,4897,4899],{"id":4898},"creating-a-minimum-accessibility-test-plan","Creating a Minimum Accessibility Test Plan",[11,4901,4902,4903,4908],{},"A minimum accessibility test plan will establish your foundation for making your website more accessible to users. You should have an end goal established such as eventually reaching ",[309,4904,4907],{"href":4905,"rel":4906},"https://www.w3.org/TR/WCAG21/",[4886],"WCAG 2.1 AA"," compliance. The minimum accessibility test plan will be your first milestone along the way.",[11,4910,4911],{},"I'm not a huge fan of formal test plans and strategies in an agile SDLC, lengthy formal documentation takes too long, but I believe your test plan should answer",[128,4913,4914,4917,4920,4923,4926,4929],{},[131,4915,4916],{},"Who the user(s) is/are (e.g., someone reliant on a screen-reader)",[131,4918,4919],{},"How they would use the system",[131,4921,4922],{},"Scope of testing (entire site? or certain logical flows?)",[131,4924,4925],{},"How will the tests be run? (automated, manual, both)",[131,4927,4928],{},"How often will the tests be run?",[131,4930,4931],{},"Who will run them? (pipeline, STE, developer?)",[11,4933,4934],{},"Once you have the test plan you can iterate on it with the product team and developers to ensure common understanding and gain agreement. This is a healthier collaborative approach to testing that should lead to less rework.",[11,4936,4937],{},"I'd recommend using the most impactful rules and guidance from the axe rule set. Have a goal like improving the navigation experience for users that rely on screen readers in mind.",[189,4939,4941],{"id":4940},"example-requirements-for-minimum-accessibility-test-plan","Example requirements for minimum accessibility test plan",[128,4943,4944,4952,4960,4968,4976,4984,4987],{},[131,4945,4946,4947,1447],{},"Each page should have a relevant title (",[309,4948,4951],{"href":4949,"rel":4950},"https://dequeuniversity.com/rules/axe/4.1/document-title",[4886],"Axe rule 42",[131,4953,4954,4955,1447],{},"Each page should have relevant, to the content, section headers (",[309,4956,4959],{"href":4957,"rel":4958},"https://dequeuniversity.com/rules/axe/4.1/page-has-heading-one",[4886],"Axe rule 73",[131,4961,4962,4963,1447],{},"Verify text has proper color contrast (",[309,4964,4967],{"href":4965,"rel":4966},"https://dequeuniversity.com/rules/axe/4.1/color-contrast",[4886],"Axe rule 83",[131,4969,4970,4971,1447],{},"Verify site is navigable by keyboard alone (",[309,4972,4975],{"href":4973,"rel":4974},"https://dequeuniversity.com/rules/axe/4.1/tabindex",[4886],"Axe rule 46",[131,4977,4978,4979,1447],{},"Verify images have descriptive alternative text (",[309,4980,4983],{"href":4981,"rel":4982},"https://dequeuniversity.com/rules/axe/4.1/image-alt",[4886],"Axe rule 64",[131,4985,4986],{},"Ensure forms can be filled in by keyboard alone",[131,4988,4989],{},"Ensure forms are not confusing when navigated with screen reading software",[11,4991,4992],{},"Once you've established an agreed upon minimum accessibility test plan you can turn them into automated accessibility test cases.",[26,4994,4996],{"id":4995},"automating-your-accessibility-test-plan","Automating your Accessibility Test Plan",[11,4998,4999],{},"Automating your accessibility tests is important because many accessibility violations are not as obvious as functional defects. For example, many of the accessibility features are not visible in the rendered markup of the page which is where automation can save you a lot of time. In addition, if you run your automation regularly your tests will ensure regression defects around your established accessibility requirements don't sneak in later.",[11,5001,5002,5003,234],{},"My favorite tool for implementing automating accessibility testing is Nightwatch.js combined with the nightwatch-axe-verbose NPM package. I cover installing them in my article ",[309,5004,5006],{"href":5005},"./accessibility-testing-with-nightwatchjs","accessibility testing with Nightwatch.js",[11,5008,5009],{},"The nightwatch-axe-verbose framework, unlike other accessibility assertion libraries, will not halt execution on the first rule failure. It will report on all rule violations you specify within the context of page you specify. This is great because it gives you a full picture of what needs to be remediated.",[11,5011,5012],{},"Further, the combination of Nightwatch.js and the nightwatch-axe-verbose package allows you to run a subset of the 90 or so axe rules in your automated tests which allows it to conform to your minimum accessibility test plan requirements.",[11,5014,5015],{},"You can accomplish running a subset of accessibility assertions using two different ways.",[128,5017,5018,5021],{},[131,5019,5020],{},"Disable specific tests from the axe ruleset",[131,5022,5023],{},"Run only specific rules from the ruleset",[11,5025,5026],{},"The former more aligns with the minimum accessibility test plan approach. Below are both examples.",[189,5028,5030],{"id":5029},"passing-rules-to-exclude-or-disable","Passing rules to exclude or disable",[289,5032,5036],{"className":5033,"code":5034,"language":5035,"meta":294,"style":294},"language-js shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","'Run everything except contrast and region': function (browser) {\n        browser\n            .url('@homePage')\n            .axeInject()\n            .axeRun('body', {\n                rules: {\n                    'color-contrast': {\n                        enabled: false\n                    },\n                    'region': {\n                        enabled: false\n                    }\n                }\n            })\n            .end();\n    }\n","js",[15,5037,5038,5062,5067,5087,5097,5116,5125,5140,5150,5155,5167,5175,5180,5185,5192,5203],{"__ignoreMap":294},[298,5039,5040,5042,5045,5047,5050,5053,5055,5058,5060],{"class":300,"line":301},[298,5041,1396],{"class":319},[298,5043,5044],{"class":323},"Run everything except contrast and region",[298,5046,1396],{"class":319},[298,5048,5049],{"class":346},": ",[298,5051,5052],{"class":1406},"function",[298,5054,1570],{"class":304},[298,5056,5057],{"class":1413},"browser",[298,5059,1447],{"class":304},[298,5061,1026],{"class":304},[298,5063,5064],{"class":300,"line":343},[298,5065,5066],{"class":346},"        browser\n",[298,5068,5069,5072,5075,5077,5079,5082,5084],{"class":300,"line":350},[298,5070,5071],{"class":304},"            .",[298,5073,5074],{"class":1390},"url",[298,5076,1042],{"class":1438},[298,5078,1396],{"class":319},[298,5080,5081],{"class":323},"@homePage",[298,5083,1396],{"class":319},[298,5085,5086],{"class":1438},")\n",[298,5088,5089,5091,5094],{"class":300,"line":460},[298,5090,5071],{"class":304},[298,5092,5093],{"class":1390},"axeInject",[298,5095,5096],{"class":1438},"()\n",[298,5098,5099,5101,5104,5106,5108,5110,5112,5114],{"class":300,"line":491},[298,5100,5071],{"class":304},[298,5102,5103],{"class":1390},"axeRun",[298,5105,1042],{"class":1438},[298,5107,1396],{"class":319},[298,5109,427],{"class":323},[298,5111,1396],{"class":319},[298,5113,1053],{"class":304},[298,5115,1026],{"class":304},[298,5117,5118,5121,5123],{"class":300,"line":509},[298,5119,5120],{"class":1438},"                rules",[298,5122,1035],{"class":304},[298,5124,1026],{"class":304},[298,5126,5127,5130,5134,5136,5138],{"class":300,"line":519},[298,5128,5129],{"class":319},"                    '",[298,5131,5133],{"class":5132},"sqmHM","color-contrast",[298,5135,1396],{"class":319},[298,5137,1035],{"class":304},[298,5139,1026],{"class":304},[298,5141,5142,5145,5147],{"class":300,"line":949},[298,5143,5144],{"class":1438},"                        enabled",[298,5146,1035],{"class":304},[298,5148,5149],{"class":1733}," false\n",[298,5151,5152],{"class":300,"line":1609},[298,5153,5154],{"class":304},"                    },\n",[298,5156,5157,5159,5161,5163,5165],{"class":300,"line":1640},[298,5158,5129],{"class":319},[298,5160,2116],{"class":5132},[298,5162,1396],{"class":319},[298,5164,1035],{"class":304},[298,5166,1026],{"class":304},[298,5168,5169,5171,5173],{"class":300,"line":1668},[298,5170,5144],{"class":1438},[298,5172,1035],{"class":304},[298,5174,5149],{"class":1733},[298,5176,5177],{"class":300,"line":1705},[298,5178,5179],{"class":304},"                    }\n",[298,5181,5182],{"class":300,"line":1713},[298,5183,5184],{"class":304},"                }\n",[298,5186,5187,5190],{"class":300,"line":1740},[298,5188,5189],{"class":304},"            }",[298,5191,5086],{"class":1438},[298,5193,5194,5196,5199,5201],{"class":300,"line":1746},[298,5195,5071],{"class":304},[298,5197,5198],{"class":1390},"end",[298,5200,1499],{"class":1438},[298,5202,1084],{"class":304},[298,5204,5205],{"class":300,"line":1751},[298,5206,4701],{"class":304},[189,5208,5210],{"id":5209},"passing-specific-rules-to-include","Passing specific rules to include",[289,5212,5214],{"className":5033,"code":5213,"language":5035,"meta":294,"style":294},"'Run these rules only': function (browser) {\n        browser\n            .url('@homePage')\n            .axeInject()\n            .axeRun('body', {\n                runOnly: ['color-contrast', 'image-alt']\n            })\n            .end();\n    }\n",[15,5215,5216,5237,5241,5257,5265,5283,5310,5316,5326],{"__ignoreMap":294},[298,5217,5218,5220,5223,5225,5227,5229,5231,5233,5235],{"class":300,"line":301},[298,5219,1396],{"class":319},[298,5221,5222],{"class":323},"Run these rules only",[298,5224,1396],{"class":319},[298,5226,5049],{"class":346},[298,5228,5052],{"class":1406},[298,5230,1570],{"class":304},[298,5232,5057],{"class":1413},[298,5234,1447],{"class":304},[298,5236,1026],{"class":304},[298,5238,5239],{"class":300,"line":343},[298,5240,5066],{"class":346},[298,5242,5243,5245,5247,5249,5251,5253,5255],{"class":300,"line":350},[298,5244,5071],{"class":304},[298,5246,5074],{"class":1390},[298,5248,1042],{"class":1438},[298,5250,1396],{"class":319},[298,5252,5081],{"class":323},[298,5254,1396],{"class":319},[298,5256,5086],{"class":1438},[298,5258,5259,5261,5263],{"class":300,"line":460},[298,5260,5071],{"class":304},[298,5262,5093],{"class":1390},[298,5264,5096],{"class":1438},[298,5266,5267,5269,5271,5273,5275,5277,5279,5281],{"class":300,"line":491},[298,5268,5071],{"class":304},[298,5270,5103],{"class":1390},[298,5272,1042],{"class":1438},[298,5274,1396],{"class":319},[298,5276,427],{"class":323},[298,5278,1396],{"class":319},[298,5280,1053],{"class":304},[298,5282,1026],{"class":304},[298,5284,5285,5288,5290,5292,5294,5296,5298,5300,5302,5305,5307],{"class":300,"line":509},[298,5286,5287],{"class":1438},"                runOnly",[298,5289,1035],{"class":304},[298,5291,3925],{"class":1438},[298,5293,1396],{"class":319},[298,5295,5133],{"class":323},[298,5297,1396],{"class":319},[298,5299,1053],{"class":304},[298,5301,1481],{"class":319},[298,5303,5304],{"class":323},"image-alt",[298,5306,1396],{"class":319},[298,5308,5309],{"class":1438},"]\n",[298,5311,5312,5314],{"class":300,"line":519},[298,5313,5189],{"class":304},[298,5315,5086],{"class":1438},[298,5317,5318,5320,5322,5324],{"class":300,"line":949},[298,5319,5071],{"class":304},[298,5321,5198],{"class":1390},[298,5323,1499],{"class":1438},[298,5325,1084],{"class":304},[298,5327,5328],{"class":300,"line":1609},[298,5329,4701],{"class":304},[11,5331,5332,5333,5338,5339,234],{},"The full list of rule names can be found in the ",[309,5334,5337],{"href":5335,"rel":5336},"https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md",[4886],"axe core rule descriptions",". To learn more about configuring your test project to run Nightwatch.js with nightwatch-axe-verbose watch this video on ",[309,5340,5343],{"href":5341,"rel":5342},"https://youtu.be/nSodkqB-838",[4886],"Nightwatch.js accessibility testing",[189,5345,5347],{"id":5346},"test-suite-style-considerations","Test suite style considerations",[11,5349,5350],{},"Good test suites should be readable, their intention clear, and allow for easy maintenance. To keep a test case's, intention clear I typically try to balance testing one thing per test where it makes sense balancing that with efficiency. With accessibility tests using the axe framework you can cover the entire page, subsections, or specific html elements in just one test.",[11,5352,5353],{},"In addition, as shown above, you can run one or many tests against that element. In the examples above the test assertions were run against the body element of the page so all the axe rules are cascaded and run against all the html elements inside body, effectively running one test against the entire page.",[11,5355,5356],{},"This provides you with a lot of assertion coverage using fewer tests.",[11,5358,5359],{},"nightwatch-axe-verbose has good reporting so if you do go with that style it tells you how many elements in the page passed.",[11,5361,5362],{},"It breaks out failures individually with the element identifier so you get a complete picture of each rule violation per element which is useful for remediation.",[289,5364,5368],{"className":5365,"code":5366,"language":5367,"meta":294,"style":294},"language-sh shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","√ Passed [ok]: aXe rule: region (296 elements checked)\n× Failed [fail]: (aXe rule: region - All page content must be contained by landmarks\n        In element: h1)\n...\n","sh",[15,5369,5370,5393,5438,5451],{"__ignoreMap":294},[298,5371,5372,5376,5379,5382,5385,5388,5391],{"class":300,"line":301},[298,5373,5375],{"class":5374},"sA8fK","√",[298,5377,5378],{"class":323}," Passed",[298,5380,5381],{"class":346}," [ok]: aXe rule: region (",[298,5383,5384],{"class":5374},"296",[298,5386,5387],{"class":323}," elements",[298,5389,5390],{"class":323}," checked",[298,5392,5086],{"class":346},[298,5394,5395,5398,5401,5404,5407,5410,5413,5416,5419,5421,5423,5426,5429,5432,5435],{"class":300,"line":343},[298,5396,5397],{"class":5374},"×",[298,5399,5400],{"class":323}," Failed",[298,5402,5403],{"class":346}," [fail]: (",[298,5405,5406],{"class":5374},"aXe",[298,5408,5409],{"class":323}," rule:",[298,5411,5412],{"class":323}," region",[298,5414,5415],{"class":323}," -",[298,5417,5418],{"class":323}," All",[298,5420,1414],{"class":323},[298,5422,2101],{"class":323},[298,5424,5425],{"class":323}," must",[298,5427,5428],{"class":323}," be",[298,5430,5431],{"class":323}," contained",[298,5433,5434],{"class":323}," by",[298,5436,5437],{"class":323}," landmarks\n",[298,5439,5440,5443,5446,5449],{"class":300,"line":350},[298,5441,5442],{"class":5374},"        In",[298,5444,5445],{"class":323}," element:",[298,5447,5448],{"class":323}," h1",[298,5450,5086],{"class":346},[298,5452,5453],{"class":300,"line":460},[298,5454,5455],{"class":1038},"...\n",[11,5457,5458],{},"Still, this can be a lot of information so maybe it makes sense to write our automated minimum accessibility tests in a style like the one below where it is more clear what rules the tests are looking at by their test name and the tests themselves are run against more specific sections or elements of the page.",[289,5460,5462],{"className":5033,"code":5461,"language":5035,"meta":294,"style":294},"module.exports = {\n        beforeEach: function (browser) {\n            browser.page.login().navigate();\n        },\n        'Login page has descriptive title': function (browser) {\n            browser\n                .assert.title('Please login to BizCorp');\n        },\n        'Logo has alt text': function (browser) {\n            browser\n                .axeInject()\n                .axeRun('#mainLogo', {\n                    runOnly: ['image-alt']\n                })\n                .end();\n        },\n        'Login page has accessible headers': function (browser) {\n            browser\n                .axeInject()\n                .axeRun('body', {\n                    runOnly: ['empty-heading', 'heading-order', 'page-has-heading-one', \n                             'p-as-heading']\n                })\n                .end();\n        }\n}\n",[15,5463,5464,5478,5496,5521,5526,5548,5553,5579,5583,5604,5608,5616,5635,5652,5659,5669,5673,5694,5698,5706,5724,5762,5774,5780,5790,5795],{"__ignoreMap":294},[298,5465,5466,5469,5471,5474,5476],{"class":300,"line":301},[298,5467,5468],{"class":1019},"module",[298,5470,234],{"class":304},[298,5472,5473],{"class":1019},"exports",[298,5475,1512],{"class":1045},[298,5477,1026],{"class":304},[298,5479,5480,5483,5485,5488,5490,5492,5494],{"class":300,"line":343},[298,5481,5482],{"class":1390},"        beforeEach",[298,5484,1035],{"class":304},[298,5486,5487],{"class":1406}," function",[298,5489,1570],{"class":304},[298,5491,5057],{"class":1413},[298,5493,1447],{"class":304},[298,5495,1026],{"class":304},[298,5497,5498,5501,5503,5505,5507,5510,5512,5514,5517,5519],{"class":300,"line":350},[298,5499,5500],{"class":346},"            browser",[298,5502,234],{"class":304},[298,5504,3485],{"class":346},[298,5506,234],{"class":304},[298,5508,5509],{"class":1390},"login",[298,5511,1499],{"class":1438},[298,5513,234],{"class":304},[298,5515,5516],{"class":1390},"navigate",[298,5518,1499],{"class":1438},[298,5520,1084],{"class":304},[298,5522,5523],{"class":300,"line":460},[298,5524,5525],{"class":304},"        },\n",[298,5527,5528,5531,5534,5536,5538,5540,5542,5544,5546],{"class":300,"line":491},[298,5529,5530],{"class":319},"        '",[298,5532,5533],{"class":5132},"Login page has descriptive title",[298,5535,1396],{"class":319},[298,5537,1035],{"class":304},[298,5539,5487],{"class":1406},[298,5541,1570],{"class":304},[298,5543,5057],{"class":1413},[298,5545,1447],{"class":304},[298,5547,1026],{"class":304},[298,5549,5550],{"class":300,"line":509},[298,5551,5552],{"class":346},"            browser\n",[298,5554,5555,5558,5561,5563,5566,5568,5570,5573,5575,5577],{"class":300,"line":519},[298,5556,5557],{"class":304},"                .",[298,5559,5560],{"class":346},"assert",[298,5562,234],{"class":304},[298,5564,5565],{"class":1390},"title",[298,5567,1042],{"class":1438},[298,5569,1396],{"class":319},[298,5571,5572],{"class":323},"Please login to BizCorp",[298,5574,1396],{"class":319},[298,5576,1447],{"class":1438},[298,5578,1084],{"class":304},[298,5580,5581],{"class":300,"line":949},[298,5582,5525],{"class":304},[298,5584,5585,5587,5590,5592,5594,5596,5598,5600,5602],{"class":300,"line":1609},[298,5586,5530],{"class":319},[298,5588,5589],{"class":5132},"Logo has alt text",[298,5591,1396],{"class":319},[298,5593,1035],{"class":304},[298,5595,5487],{"class":1406},[298,5597,1570],{"class":304},[298,5599,5057],{"class":1413},[298,5601,1447],{"class":304},[298,5603,1026],{"class":304},[298,5605,5606],{"class":300,"line":1640},[298,5607,5552],{"class":346},[298,5609,5610,5612,5614],{"class":300,"line":1668},[298,5611,5557],{"class":304},[298,5613,5093],{"class":1390},[298,5615,5096],{"class":1438},[298,5617,5618,5620,5622,5624,5626,5629,5631,5633],{"class":300,"line":1705},[298,5619,5557],{"class":304},[298,5621,5103],{"class":1390},[298,5623,1042],{"class":1438},[298,5625,1396],{"class":319},[298,5627,5628],{"class":323},"#mainLogo",[298,5630,1396],{"class":319},[298,5632,1053],{"class":304},[298,5634,1026],{"class":304},[298,5636,5637,5640,5642,5644,5646,5648,5650],{"class":300,"line":1713},[298,5638,5639],{"class":1438},"                    runOnly",[298,5641,1035],{"class":304},[298,5643,3925],{"class":1438},[298,5645,1396],{"class":319},[298,5647,5304],{"class":323},[298,5649,1396],{"class":319},[298,5651,5309],{"class":1438},[298,5653,5654,5657],{"class":300,"line":1740},[298,5655,5656],{"class":304},"                }",[298,5658,5086],{"class":1438},[298,5660,5661,5663,5665,5667],{"class":300,"line":1746},[298,5662,5557],{"class":304},[298,5664,5198],{"class":1390},[298,5666,1499],{"class":1438},[298,5668,1084],{"class":304},[298,5670,5671],{"class":300,"line":1751},[298,5672,5525],{"class":304},[298,5674,5675,5677,5680,5682,5684,5686,5688,5690,5692],{"class":300,"line":1757},[298,5676,5530],{"class":319},[298,5678,5679],{"class":5132},"Login page has accessible headers",[298,5681,1396],{"class":319},[298,5683,1035],{"class":304},[298,5685,5487],{"class":1406},[298,5687,1570],{"class":304},[298,5689,5057],{"class":1413},[298,5691,1447],{"class":304},[298,5693,1026],{"class":304},[298,5695,5696],{"class":300,"line":1785},[298,5697,5552],{"class":346},[298,5699,5700,5702,5704],{"class":300,"line":1811},[298,5701,5557],{"class":304},[298,5703,5093],{"class":1390},[298,5705,5096],{"class":1438},[298,5707,5708,5710,5712,5714,5716,5718,5720,5722],{"class":300,"line":1821},[298,5709,5557],{"class":304},[298,5711,5103],{"class":1390},[298,5713,1042],{"class":1438},[298,5715,1396],{"class":319},[298,5717,427],{"class":323},[298,5719,1396],{"class":319},[298,5721,1053],{"class":304},[298,5723,1026],{"class":304},[298,5725,5726,5728,5730,5732,5734,5737,5739,5741,5743,5746,5748,5750,5752,5755,5757,5759],{"class":300,"line":1826},[298,5727,5639],{"class":1438},[298,5729,1035],{"class":304},[298,5731,3925],{"class":1438},[298,5733,1396],{"class":319},[298,5735,5736],{"class":323},"empty-heading",[298,5738,1396],{"class":319},[298,5740,1053],{"class":304},[298,5742,1481],{"class":319},[298,5744,5745],{"class":323},"heading-order",[298,5747,1396],{"class":319},[298,5749,1053],{"class":304},[298,5751,1481],{"class":319},[298,5753,5754],{"class":323},"page-has-heading-one",[298,5756,1396],{"class":319},[298,5758,1053],{"class":304},[298,5760,5761],{"class":1438}," \n",[298,5763,5764,5767,5770,5772],{"class":300,"line":1854},[298,5765,5766],{"class":319},"                             '",[298,5768,5769],{"class":323},"p-as-heading",[298,5771,1396],{"class":319},[298,5773,5309],{"class":1438},[298,5775,5776,5778],{"class":300,"line":1877},[298,5777,5656],{"class":304},[298,5779,5086],{"class":1438},[298,5781,5782,5784,5786,5788],{"class":300,"line":1921},[298,5783,5557],{"class":304},[298,5785,5198],{"class":1390},[298,5787,1499],{"class":1438},[298,5789,1084],{"class":304},[298,5791,5792],{"class":300,"line":1936},[298,5793,5794],{"class":304},"        }\n",[298,5796,5797],{"class":300,"line":1963},[298,5798,1089],{"class":304},[11,5800,5801],{},"There probably isn't a one-size fits all approach to this. Weigh your current time constaints for implementing the automation against future costs of maintenance and who will be running the tests behind you to find the balance.",[189,5803,5805],{"id":5804},"dont-forget-state-changes","Don't forget state changes",[11,5807,5808],{},"Most likely you have pages that have javascript that will update the look and feel of the page based on user interaction. You will want to ensure you test those scenarios too.",[11,5810,5811],{},"For example, if you submit a form with invalid or missing information does a validation callout appear on screen? That page state should be tested as well.",[11,5813,5814,5815,5818],{},"For example, is the error marked up appropriately where it makes sense to someone using a screen reader? Would someone know what field is in error from the message shown? An error stating ",[15,5816,5817],{},"this field is required"," without visual context may be confusing.",[26,5820,5822],{"id":5821},"workflow-considerations-do-i-file-a-defect","Workflow considerations - Do I file a defect?",[11,5824,5825],{},"So what does one do if they are testing a legacy product, one without prior accessibility requirements, and find accessibility defects? I'd argue that if there was no accessibility requirement when product was developed it should be treated as a new feature request and not a bug.",[11,5827,5828],{},"It makes more sense to me to play stories enhancing the accessibility of the product as a feature for legacy products instead of just burying the backlog with defects.",[11,5830,5831],{},"This allows you and your team to work the story like any other piece of functionality. The requirements can come from the newly agreed upon accessibility test plan, coded, and verified by the tester.",[11,5833,5834],{},"However, it should be considered a defect if the automated accessibility tests catch a regression after the requirements are in place.",[11,5836,5837,5838,5841],{},"Any ",[197,5839,5840],{},"new"," features made after the minimum accessibility plan is in place should have this as an assumed requirement. Perhaps consider attaching the a11y requirements to stories as a reminder.",[11,5843,5844],{},"A11y violations on these new features not caught in development should be treated as defects.",[26,5846,5848],{"id":5847},"final-thoughts","Final Thoughts",[11,5850,5851],{},"Creating a minimum accessibility test plan is an important first step in making your website a11y compliant. Having a visible document your team can collaborate on will help with buy in. Many of the most impactful changes are easy to implement and make your site design better for all users.",[11,5853,5854],{},"Use automated tests to efficiently scan your site for violations against your accessibility test plan and to prevent from backsliding with regression defects.",[11,5856,5857],{},"Finally, find or be the accessibility advocate for your company and show how making your site more accessible leads to higher usage and protects the company from perils of non-compliance.",[11,5859,5860,5861],{},"I've posted the a working example of what I described in this article on my GitHub\n👉 ",[309,5862,5865],{"href":5863,"rel":5864},"https://github.com/reallymello/nightwatchTutorials/tree/master/a11yTestPlanExample",[4886],"Implementing a minimum accessibility test plan using Nightwatch, aXe, and nightwatch-axe-verbose tutorial",[11,5867,5868,5869,5874],{},"To stay in touch, subscribe to the ",[309,5870,5873],{"href":5871,"rel":5872},"https://youtube.com/playlist?list=PLLS_Ef55N6hmkt3-JlW40GAGpXSlp8t_D",[4886],"reallyMello channel on YouTube"," and check our my social links below. Thanks!",[4809,5876],{"to":5877},"/software-testing/frameworks/nightwatch/accessibility-testing-with-nightwatchjs",[4813,5879,5880],{},"html pre.shiki code .sZi47, html code.shiki .sZi47{--shiki-light:#39ADB5;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .srGNg, html code.shiki .srGNg{--shiki-light:#91B859;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sZ-rw, html code.shiki .sZ-rw{--shiki-light:#90A4AE;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .stWsX, html code.shiki .stWsX{--shiki-light:#9C3EDA;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .sPJuK, html code.shiki .sPJuK{--shiki-light:#39ADB5;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .s2xgV, html code.shiki .s2xgV{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#702C00;--shiki-default-font-style:inherit;--shiki-dark:#FFB757;--shiki-dark-font-style:inherit}html pre.shiki code .sb1SK, html code.shiki .sb1SK{--shiki-light:#6182B8;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}html pre.shiki code .sq0XF, html code.shiki .sq0XF{--shiki-light:#E53935;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sqmHM, html code.shiki .sqmHM{--shiki-light:#E53935;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sTqCK, html code.shiki .sTqCK{--shiki-light:#FF5370;--shiki-default:#023B95;--shiki-dark:#91CBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sA8fK, html code.shiki .sA8fK{--shiki-light:#E2931D;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .slPND, html code.shiki .slPND{--shiki-light:#6182B8;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sPxkN, html code.shiki .sPxkN{--shiki-light:#39ADB5;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sE6rD, html code.shiki .sE6rD{--shiki-light:#39ADB5;--shiki-default:#A0111F;--shiki-dark:#FF9492}",{"title":294,"searchDepth":343,"depth":343,"links":5882},[5883,5884,5887,5893,5894],{"id":4864,"depth":343,"text":4865},{"id":4898,"depth":343,"text":4899,"children":5885},[5886],{"id":4940,"depth":350,"text":4941},{"id":4995,"depth":343,"text":4996,"children":5888},[5889,5890,5891,5892],{"id":5029,"depth":350,"text":5030},{"id":5209,"depth":350,"text":5210},{"id":5346,"depth":350,"text":5347},{"id":5804,"depth":350,"text":5805},{"id":5821,"depth":343,"text":5822},{"id":5847,"depth":343,"text":5848},"/images/posts/implementing-a-minimum-accessibility-test-plan/a11y-nightwatch-test-planning.png","2021-01-11","This tutorial will cover how to implement a minimum accessibility test plan against a website using Nightwatch.js and aXe.",{},"/software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan",{"title":4859,"description":5897},"software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan","kThHgeCOMYY9PjopPGFJ9CTkX6X-eeMHBJ5iefzL_B8",1776040195144]