[{"data":1,"prerenderedAt":13135},["ShallowReactive",2],{"content:\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright":3,"category:\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright":6,"read-next:\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations,\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan,\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright,\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation":2997},{"id":4,"title":5,"bmcUsername":6,"body":7,"cover":2986,"date":2987,"description":2988,"draft":2989,"extension":2990,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":2991,"navigation":193,"npmPackage":6,"order":6,"path":2992,"review":6,"seo":2993,"sitemap":2994,"stem":2995,"updated":6,"__hash__":2996},"content\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright.md","How to Disable, Exclude & Phase In axe Rules in Playwright",null,{"type":8,"value":9,"toc":2963},"minimark",[10,14,32,40,45,48,73,76,82,85,115,122,345,351,355,363,429,432,440,447,450,512,519,525,535,540,596,599,610,631,636,693,700,705,715,723,729,812,823,829,913,917,948,952,955,1247,1253,1257,1268,1271,1450,1456,1630,1633,1641,1644,1647,1657,1664,1671,1681,1688,1816,1820,1823,2016,2019,2037,2044,2860,2868,2872,2875,2926,2932,2936,2945,2951,2955,2959],[11,12,13],"p",{},"Turn on a full axe scan against an existing product and you'll usually get a wall of violations on day one. File them all as bugs and you bury the backlog, frustrate developers, and the accessibility work stalls. The pragmatic alternative is to agree on a minimum set of rules, automate those, record the issues you already know about, and phase in stricter checks as the team fixes things.",[11,15,16,17,22,23,27,28,31],{},"I wrote about this approach years ago in ",[18,19,21],"a",{"href":20},"\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan","Implementing a Minimum Accessibility Test Plan"," using Nightwatch. This is the Playwright version, using ",[24,25,26],"code",{},"@axe-core\u002Fplaywright"," and ",[24,29,30],{},"AxeBuilder",", and instead of a made-up example I used this site. Every number and failure below came from scanning davidmello.com.",[11,33,34,35,39],{},"None of this replaces manual testing. Automated scans miss most WCAG failures, which I covered in ",[18,36,38],{"href":37},"\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations","Playwright Accessibility Testing: What axe and Lighthouse Miss",". But they're the cheapest way to stop fixed issues from coming back, as long as the results don't bury the team.",[41,42,44],"h2",{"id":43},"pick-a-starting-point-for-an-existing-product","Pick a Starting Point for an Existing Product",[11,46,47],{},"There are two reasonable places to start, depending on how much accessibility debt you have:",[49,50,51,63],"ul",{},[52,53,54,58,59,62],"li",{},[55,56,57],"strong",{},"The site has extremely poor accessibility:"," start with a short, agreed list of rules using ",[24,60,61],{},"withRules()",", like the minimum test plan from my Nightwatch article (page titles, headings, image alt text, color contrast). A full scan would only produce noise. A short list gives the team somewhere to start and a clear definition of done.",[52,64,65,68,69,72],{},[55,66,67],{},"The site is mostly in good shape:"," scan against a standard like WCAG 2.1 AA with ",[24,70,71],{},"withTags()",", exclude the areas you've decided to defer, and record the remaining known issues so only new violations fail the build.",[11,74,75],{},"This site falls in the second group, so that's what the rest of the article builds. Both are stages of the same approach: start where the product is, then tighten.",[41,77,79,80],{"id":78},"install-and-run-axe-coreplaywright","Install and Run ",[24,81,26],{},[11,83,84],{},"Install the axe integration alongside Playwright:",[86,87,93],"pre",{"className":88,"code":89,"filename":90,"language":91,"meta":92,"style":92},"language-zsh shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","npm install -D @axe-core\u002Fplaywright\n","Terminal","zsh","",[24,94,95],{"__ignoreMap":92},[96,97,100,104,108,112],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sA8fK","npm",[96,105,107],{"class":106},"srGNg"," install",[96,109,111],{"class":110},"spMcu"," -D",[96,113,114],{"class":106}," @axe-core\u002Fplaywright\n",[11,116,117,118,121],{},"Once axe-core is installed ",[24,119,120],{},"import AxeBuilder"," at the top of your test spec as shown in the code example below. Now the simplest scan runs every rule axe has against the page:",[86,123,128],{"className":124,"code":125,"filename":126,"language":127,"meta":92,"style":92},"language-ts shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","import { test, expect } from '@playwright\u002Ftest';\nimport AxeBuilder from '@axe-core\u002Fplaywright';\n\ntest('home page has no accessibility violations', async ({ page }) => {\n  await page.goto('\u002F');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n","accessibility.spec.ts","ts",[24,129,130,169,188,195,234,263,307,335],{"__ignoreMap":92},[96,131,132,136,140,144,147,150,153,156,160,163,166],{"class":98,"line":99},[96,133,135],{"class":134},"sZTni","import",[96,137,139],{"class":138},"sPJuK"," {",[96,141,143],{"class":142},"sZ-rw"," test",[96,145,146],{"class":138},",",[96,148,149],{"class":142}," expect",[96,151,152],{"class":138}," }",[96,154,155],{"class":134}," from",[96,157,159],{"class":158},"sZi47"," '",[96,161,162],{"class":106},"@playwright\u002Ftest",[96,164,165],{"class":158},"'",[96,167,168],{"class":138},";\n",[96,170,172,174,177,180,182,184,186],{"class":98,"line":171},2,[96,173,135],{"class":134},[96,175,176],{"class":142}," AxeBuilder ",[96,178,179],{"class":134},"from",[96,181,159],{"class":158},[96,183,26],{"class":106},[96,185,165],{"class":158},[96,187,168],{"class":138},[96,189,191],{"class":98,"line":190},3,[96,192,194],{"emptyLinePlaceholder":193},true,"\n",[96,196,198,202,205,207,210,212,214,218,221,225,228,231],{"class":98,"line":197},4,[96,199,201],{"class":200},"sb1SK","test",[96,203,204],{"class":142},"(",[96,206,165],{"class":158},[96,208,209],{"class":106},"home page has no accessibility violations",[96,211,165],{"class":158},[96,213,146],{"class":138},[96,215,217],{"class":216},"stWsX"," async",[96,219,220],{"class":138}," ({",[96,222,224],{"class":223},"s2xgV"," page",[96,226,227],{"class":138}," })",[96,229,230],{"class":216}," =>",[96,232,233],{"class":138}," {\n",[96,235,237,240,242,245,248,251,253,256,258,261],{"class":98,"line":236},5,[96,238,239],{"class":134},"  await",[96,241,224],{"class":142},[96,243,244],{"class":138},".",[96,246,247],{"class":200},"goto",[96,249,204],{"class":250},"sq0XF",[96,252,165],{"class":158},[96,254,255],{"class":106},"\u002F",[96,257,165],{"class":158},[96,259,260],{"class":250},")",[96,262,168],{"class":138},[96,264,266,269,273,277,280,283,286,288,291,293,295,297,299,302,305],{"class":98,"line":265},6,[96,267,268],{"class":216},"  const",[96,270,272],{"class":271},"sQ79N"," results",[96,274,276],{"class":275},"sE6rD"," =",[96,278,279],{"class":134}," await",[96,281,282],{"class":275}," new",[96,284,285],{"class":200}," AxeBuilder",[96,287,204],{"class":250},[96,289,290],{"class":138},"{",[96,292,224],{"class":142},[96,294,152],{"class":138},[96,296,260],{"class":250},[96,298,244],{"class":138},[96,300,301],{"class":200},"analyze",[96,303,304],{"class":250},"()",[96,306,168],{"class":138},[96,308,310,313,315,318,320,323,325,327,330,333],{"class":98,"line":309},7,[96,311,312],{"class":200},"  expect",[96,314,204],{"class":250},[96,316,317],{"class":142},"results",[96,319,244],{"class":138},[96,321,322],{"class":142},"violations",[96,324,260],{"class":250},[96,326,244],{"class":138},[96,328,329],{"class":200},"toEqual",[96,331,332],{"class":250},"([])",[96,334,168],{"class":138},[96,336,338,341,343],{"class":98,"line":337},8,[96,339,340],{"class":138},"}",[96,342,260],{"class":142},[96,344,168],{"class":138},[11,346,347,350],{},[24,348,349],{},"analyze()"," returns every violation it finds rather than stopping at the first one, so a failing test shows you the full picture.",[41,352,354],{"id":353},"what-a-default-scan-found-on-this-site","What a Default Scan Found on This Site",[11,356,357,358,362],{},"I ran that default scan, with axe-core 4.13, against my article on ",[18,359,361],{"href":360},"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright","running parallel and serial tests in Playwright",", in both light and dark mode:",[364,365,366,382],"table",{},[367,368,369],"thead",{},[370,371,372,376,379],"tr",{},[373,374,375],"th",{},"Rule",[373,377,378],{},"Light mode",[373,380,381],{},"Dark mode",[383,384,385,402,415],"tbody",{},[370,386,387,396,399],{},[388,389,390,395],"td",{},[391,392],"external-link",{"href":393,"text":394},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.13\u002Fcolor-contrast","color-contrast"," (serious)",[388,397,398],{},"435 elements",[388,400,401],{},"0",[370,403,404,410,413],{},[388,405,406,395],{},[391,407],{"href":408,"text":409},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.13\u002Flink-in-text-block","link-in-text-block",[388,411,412],{},"9",[388,414,412],{},[370,416,417,424,427],{},[388,418,419,423],{},[391,420],{"href":421,"text":422},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.13\u002Fempty-table-header","empty-table-header"," (minor)",[388,425,426],{},"1",[388,428,426],{},[11,430,431],{},"All 435 contrast failures were inside code blocks. In light mode, the syntax highlighting colors fail against the code block background: comments at 2.47:1, strings at 2.18:1, and one light gray at 1.51:1, against a required 4.5:1. Dark mode had none.",[11,433,434,435,439],{},"One problem, a code block theme, produced 435 of the 445 violations, and it buried two smaller issues that mattered just as much. I didn't want to fix the code block theme that day, so I needed to set it aside without losing everything else. Agile development teams would have this same issue; you can't fix all incoming issues immediately, especially mid-sprint, so you need to be able to silence or disable known failures until they can be prioritized, similar to my article ",[18,436,438],{"href":437},"\u002Fsoftware-testing\u002Ftest-automation\u002Fhow-to-handle-failing-tests-caused-by-known-bugs","how to handle failing tests caused by known bugs",", but here we leverage axe features to do so.",[41,441,443,444],{"id":442},"disable-an-axe-rule-with-disablerules","Disable an axe Rule with ",[24,445,446],{},"disableRules()",[11,448,449],{},"The bluntest tool is turning the rule off:",[86,451,453],{"className":124,"code":452,"filename":126,"language":127,"meta":92,"style":92},"const results = await new AxeBuilder({ page })\n  .disableRules(['color-contrast'])\n  .analyze();\n",[24,454,455,482,502],{"__ignoreMap":92},[96,456,457,460,462,464,466,468,470,472,474,477,479],{"class":98,"line":99},[96,458,459],{"class":216},"const",[96,461,272],{"class":271},[96,463,276],{"class":275},[96,465,279],{"class":134},[96,467,282],{"class":275},[96,469,285],{"class":200},[96,471,204],{"class":142},[96,473,290],{"class":138},[96,475,476],{"class":142}," page ",[96,478,340],{"class":138},[96,480,481],{"class":142},")\n",[96,483,484,487,490,493,495,497,499],{"class":98,"line":171},[96,485,486],{"class":138},"  .",[96,488,489],{"class":200},"disableRules",[96,491,492],{"class":142},"([",[96,494,165],{"class":158},[96,496,394],{"class":106},[96,498,165],{"class":158},[96,500,501],{"class":142},"])\n",[96,503,504,506,508,510],{"class":98,"line":190},[96,505,486],{"class":138},[96,507,301],{"class":200},[96,509,304],{"class":142},[96,511,168],{"class":138},[11,513,514,515,518],{},"On this page the 435 contrast violations disappear, leaving only the link and table issues. The catch is that contrast checking is now off ",[55,516,517],{},"everywhere",", not just in code blocks. A low-contrast button added next month would pass without anyone noticing.",[11,520,521,522,524],{},"Use ",[24,523,446],{}," when a rule genuinely doesn't apply to your product, or as a short-lived step while you work through a backlog. For a known issue in one area of the page, there's a better option.",[41,526,528,529,27,532],{"id":527},"exclude-or-include-elements-with-exclude-and-include","Exclude or Include Elements with ",[24,530,531],{},"exclude()",[24,533,534],{},"include()",[11,536,537,539],{},[24,538,531],{}," takes a CSS selector and skips those elements and everything inside them:",[86,541,543],{"className":124,"code":542,"filename":126,"language":127,"meta":92,"style":92},"const results = await new AxeBuilder({ page })\n  .exclude('pre')\n  .analyze();\n",[24,544,545,569,586],{"__ignoreMap":92},[96,546,547,549,551,553,555,557,559,561,563,565,567],{"class":98,"line":99},[96,548,459],{"class":216},[96,550,272],{"class":271},[96,552,276],{"class":275},[96,554,279],{"class":134},[96,556,282],{"class":275},[96,558,285],{"class":200},[96,560,204],{"class":142},[96,562,290],{"class":138},[96,564,476],{"class":142},[96,566,340],{"class":138},[96,568,481],{"class":142},[96,570,571,573,576,578,580,582,584],{"class":98,"line":171},[96,572,486],{"class":138},[96,574,575],{"class":200},"exclude",[96,577,204],{"class":142},[96,579,165],{"class":158},[96,581,86],{"class":106},[96,583,165],{"class":158},[96,585,481],{"class":142},[96,587,588,590,592,594],{"class":98,"line":190},[96,589,486],{"class":138},[96,591,301],{"class":200},[96,593,304],{"class":142},[96,595,168],{"class":138},[11,597,598],{},"Same result on this page: the 435 code block violations are gone. The difference is that contrast is still checked everywhere else, so a new low-contrast button would fail the test.",[11,600,601,602,604,605,609],{},"The tradeoff is that ",[24,603,531],{}," skips ",[606,607,608],"em",{},"every"," rule inside the excluded elements, not just contrast. That's acceptable for code blocks, but keep exclusions as narrow as you can.",[11,611,612,614,615,618,619,622,623,626,627,630],{},[24,613,531],{}," is also the right tool for content you can't fix. When I scanned pages with an embedded YouTube video, axe reported ",[24,616,617],{},"aria-allowed-attr",", ",[24,620,621],{},"aria-prohibited-attr",", and ",[24,624,625],{},"button-name"," violations, all from inside YouTube's own player iframe. Excluding ",[24,628,629],{},"iframe"," keeps third-party embeds from failing your build over markup you don't control.",[11,632,633,635],{},[24,634,534],{}," works the other way: it limits the scan to part of the page. That's useful for holding new work to the full rule set before the rest of the page is ready. When I added a pros and cons box to my review articles, I could scan just that component:",[86,637,639],{"className":124,"code":638,"filename":126,"language":127,"meta":92,"style":92},"const results = await new AxeBuilder({ page })\n  .include('[data-testid=\"review-pros-cons\"]')\n  .analyze();\n",[24,640,641,665,683],{"__ignoreMap":92},[96,642,643,645,647,649,651,653,655,657,659,661,663],{"class":98,"line":99},[96,644,459],{"class":216},[96,646,272],{"class":271},[96,648,276],{"class":275},[96,650,279],{"class":134},[96,652,282],{"class":275},[96,654,285],{"class":200},[96,656,204],{"class":142},[96,658,290],{"class":138},[96,660,476],{"class":142},[96,662,340],{"class":138},[96,664,481],{"class":142},[96,666,667,669,672,674,676,679,681],{"class":98,"line":171},[96,668,486],{"class":138},[96,670,671],{"class":200},"include",[96,673,204],{"class":142},[96,675,165],{"class":158},[96,677,678],{"class":106},"[data-testid=\"review-pros-cons\"]",[96,680,165],{"class":158},[96,682,481],{"class":142},[96,684,685,687,689,691],{"class":98,"line":190},[96,686,486],{"class":138},[96,688,301],{"class":200},[96,690,304],{"class":142},[96,692,168],{"class":138},[11,694,695,696,699],{},"You can also combine the two, for example ",[24,697,698],{},".include('main').exclude('pre')"," to scan the page content but skip its code blocks.",[701,702,704],"h3",{"id":703},"finding-the-root-cause","Finding the Root Cause",[11,706,707,708,711,712,714],{},"Excluding the code blocks bought time, and it also prompted me to find out why light mode failed when I'd configured a high-contrast theme. The code blocks were being rendered with three themes, including ",[24,709,710],{},"material-theme-lighter",", which my UI library adds as the default light theme. My config only set the default and dark themes, so the library's light theme won in light mode. The fix is one line of config. When I apply it, the ",[24,713,86],{}," exclusion comes out of the test.",[41,716,718,719,27,721],{"id":717},"run-only-specific-rules-with-withrules-and-withtags","Run Only Specific Rules with ",[24,720,61],{},[24,722,71],{},[11,724,725,726,728],{},"To scan for a short, agreed list of rules, use ",[24,727,61],{},":",[86,730,732],{"className":124,"code":731,"filename":126,"language":127,"meta":92,"style":92},"const results = await new AxeBuilder({ page })\n  .withRules(['document-title', 'page-has-heading-one', 'image-alt', 'color-contrast'])\n  .analyze();\n",[24,733,734,758,802],{"__ignoreMap":92},[96,735,736,738,740,742,744,746,748,750,752,754,756],{"class":98,"line":99},[96,737,459],{"class":216},[96,739,272],{"class":271},[96,741,276],{"class":275},[96,743,279],{"class":134},[96,745,282],{"class":275},[96,747,285],{"class":200},[96,749,204],{"class":142},[96,751,290],{"class":138},[96,753,476],{"class":142},[96,755,340],{"class":138},[96,757,481],{"class":142},[96,759,760,762,765,767,769,772,774,776,778,781,783,785,787,790,792,794,796,798,800],{"class":98,"line":171},[96,761,486],{"class":138},[96,763,764],{"class":200},"withRules",[96,766,492],{"class":142},[96,768,165],{"class":158},[96,770,771],{"class":106},"document-title",[96,773,165],{"class":158},[96,775,146],{"class":138},[96,777,159],{"class":158},[96,779,780],{"class":106},"page-has-heading-one",[96,782,165],{"class":158},[96,784,146],{"class":138},[96,786,159],{"class":158},[96,788,789],{"class":106},"image-alt",[96,791,165],{"class":158},[96,793,146],{"class":138},[96,795,159],{"class":158},[96,797,394],{"class":106},[96,799,165],{"class":158},[96,801,501],{"class":142},[96,803,804,806,808,810],{"class":98,"line":190},[96,805,486],{"class":138},[96,807,301],{"class":200},[96,809,304],{"class":142},[96,811,168],{"class":138},[11,813,814,815,818,819,244],{},"This is the Playwright equivalent of ",[24,816,817],{},"runOnly"," in my Nightwatch test plan, and it's the starting point I'd use for a site with very poor accessibility. The full list of rule IDs is in axe-core's ",[391,820],{"href":821,"text":822},"https:\u002F\u002Fgithub.com\u002Fdequelabs\u002Faxe-core\u002Fblob\u002Fdevelop\u002Fdoc\u002Frule-descriptions.md","rule descriptions",[11,824,825,826,828],{},"To scan against a standard instead, use ",[24,827,71],{},". axe tags each rule with the WCAG criteria it covers:",[86,830,832],{"className":124,"code":831,"filename":126,"language":127,"meta":92,"style":92},"const results = await new AxeBuilder({ page })\n  .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])\n  .analyze();\n",[24,833,834,858,903],{"__ignoreMap":92},[96,835,836,838,840,842,844,846,848,850,852,854,856],{"class":98,"line":99},[96,837,459],{"class":216},[96,839,272],{"class":271},[96,841,276],{"class":275},[96,843,279],{"class":134},[96,845,282],{"class":275},[96,847,285],{"class":200},[96,849,204],{"class":142},[96,851,290],{"class":138},[96,853,476],{"class":142},[96,855,340],{"class":138},[96,857,481],{"class":142},[96,859,860,862,865,867,869,872,874,876,878,881,883,885,887,890,892,894,896,899,901],{"class":98,"line":171},[96,861,486],{"class":138},[96,863,864],{"class":200},"withTags",[96,866,492],{"class":142},[96,868,165],{"class":158},[96,870,871],{"class":106},"wcag2a",[96,873,165],{"class":158},[96,875,146],{"class":138},[96,877,159],{"class":158},[96,879,880],{"class":106},"wcag2aa",[96,882,165],{"class":158},[96,884,146],{"class":138},[96,886,159],{"class":158},[96,888,889],{"class":106},"wcag21a",[96,891,165],{"class":158},[96,893,146],{"class":138},[96,895,159],{"class":158},[96,897,898],{"class":106},"wcag21aa",[96,900,165],{"class":158},[96,902,501],{"class":142},[96,904,905,907,909,911],{"class":98,"line":190},[96,906,486],{"class":138},[96,908,301],{"class":200},[96,910,304],{"class":142},[96,912,168],{"class":138},[701,914,916],{"id":915},"wcag-tags-skip-axe-best-practice-rules","WCAG Tags Skip axe Best-Practice Rules",[11,918,919,920,923,924,618,926,618,929,618,932,618,934,622,937,940,941,944,945,947],{},"Note ",[55,921,922],{},"WCAG tags don't include axe's best-practice rules."," For example, ",[24,925,422],{},[24,927,928],{},"landmark-unique",[24,930,931],{},"landmark-no-duplicate-main",[24,933,780],{},[24,935,936],{},"heading-order",[24,938,939],{},"region"," are all tagged ",[24,942,943],{},"best-practice",", not WCAG. A WCAG-only scan would have missed several of the issues I fixed on this site (more on those below). Once those were fixed, I added the ",[24,946,943],{}," tag so they can't come back.",[41,949,951],{"id":950},"scaling-share-an-axebuilder-configuration-with-a-playwright-fixture","Scaling: Share an AxeBuilder Configuration with a Playwright Fixture",[11,953,954],{},"Repeating the same builder calls in every test makes the rule set hard to change and scale. Playwright's recommended pattern is a fixture that returns a preconfigured builder, so the agreed rules live in one place:",[86,956,958],{"className":124,"code":957,"filename":126,"language":127,"meta":92,"style":92},"import { test as base, expect } from '@playwright\u002Ftest';\nimport AxeBuilder from '@axe-core\u002Fplaywright';\n\nconst RULE_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'];\n\nconst test = base.extend\u003C{ makeAxeBuilder: () => AxeBuilder }>({\n  makeAxeBuilder: async ({ page }, use) => {\n    await use(() =>\n      new AxeBuilder({ page })\n        .withTags(RULE_TAGS)\n        .exclude('pre')     \u002F\u002F known issue: code block theme contrast (deferred)\n        .exclude('iframe'), \u002F\u002F third-party embeds\n    );\n  },\n});\n",[24,959,960,990,1006,1010,1065,1069,1108,1133,1147,1165,1180,1202,1224,1232,1238],{"__ignoreMap":92},[96,961,962,964,966,968,971,974,976,978,980,982,984,986,988],{"class":98,"line":99},[96,963,135],{"class":134},[96,965,139],{"class":138},[96,967,143],{"class":142},[96,969,970],{"class":134}," as",[96,972,973],{"class":142}," base",[96,975,146],{"class":138},[96,977,149],{"class":142},[96,979,152],{"class":138},[96,981,155],{"class":134},[96,983,159],{"class":158},[96,985,162],{"class":106},[96,987,165],{"class":158},[96,989,168],{"class":138},[96,991,992,994,996,998,1000,1002,1004],{"class":98,"line":171},[96,993,135],{"class":134},[96,995,176],{"class":142},[96,997,179],{"class":134},[96,999,159],{"class":158},[96,1001,26],{"class":106},[96,1003,165],{"class":158},[96,1005,168],{"class":138},[96,1007,1008],{"class":98,"line":190},[96,1009,194],{"emptyLinePlaceholder":193},[96,1011,1012,1014,1017,1019,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058,1060,1063],{"class":98,"line":197},[96,1013,459],{"class":216},[96,1015,1016],{"class":271}," RULE_TAGS",[96,1018,276],{"class":275},[96,1020,1021],{"class":142}," [",[96,1023,165],{"class":158},[96,1025,871],{"class":106},[96,1027,165],{"class":158},[96,1029,146],{"class":138},[96,1031,159],{"class":158},[96,1033,880],{"class":106},[96,1035,165],{"class":158},[96,1037,146],{"class":138},[96,1039,159],{"class":158},[96,1041,889],{"class":106},[96,1043,165],{"class":158},[96,1045,146],{"class":138},[96,1047,159],{"class":158},[96,1049,898],{"class":106},[96,1051,165],{"class":158},[96,1053,146],{"class":138},[96,1055,159],{"class":158},[96,1057,943],{"class":106},[96,1059,165],{"class":158},[96,1061,1062],{"class":142},"]",[96,1064,168],{"class":138},[96,1066,1067],{"class":98,"line":236},[96,1068,194],{"emptyLinePlaceholder":193},[96,1070,1071,1073,1075,1077,1079,1081,1084,1087,1091,1093,1096,1098,1100,1103,1105],{"class":98,"line":265},[96,1072,459],{"class":216},[96,1074,143],{"class":271},[96,1076,276],{"class":275},[96,1078,973],{"class":142},[96,1080,244],{"class":138},[96,1082,1083],{"class":200},"extend",[96,1085,1086],{"class":138},"\u003C{",[96,1088,1090],{"class":1089},"stzDA"," makeAxeBuilder",[96,1092,728],{"class":275},[96,1094,1095],{"class":138}," ()",[96,1097,230],{"class":216},[96,1099,285],{"class":102},[96,1101,1102],{"class":138}," }>",[96,1104,204],{"class":142},[96,1106,1107],{"class":138},"{\n",[96,1109,1110,1113,1115,1117,1119,1121,1124,1127,1129,1131],{"class":98,"line":309},[96,1111,1112],{"class":200},"  makeAxeBuilder",[96,1114,728],{"class":138},[96,1116,217],{"class":216},[96,1118,220],{"class":138},[96,1120,224],{"class":223},[96,1122,1123],{"class":138}," },",[96,1125,1126],{"class":223}," use",[96,1128,260],{"class":138},[96,1130,230],{"class":216},[96,1132,233],{"class":138},[96,1134,1135,1138,1140,1142,1144],{"class":98,"line":337},[96,1136,1137],{"class":134},"    await",[96,1139,1126],{"class":200},[96,1141,204],{"class":250},[96,1143,304],{"class":138},[96,1145,1146],{"class":216}," =>\n",[96,1148,1150,1153,1155,1157,1159,1161,1163],{"class":98,"line":1149},9,[96,1151,1152],{"class":275},"      new",[96,1154,285],{"class":200},[96,1156,204],{"class":250},[96,1158,290],{"class":138},[96,1160,224],{"class":142},[96,1162,152],{"class":138},[96,1164,481],{"class":250},[96,1166,1168,1171,1173,1175,1178],{"class":98,"line":1167},10,[96,1169,1170],{"class":138},"        .",[96,1172,864],{"class":200},[96,1174,204],{"class":250},[96,1176,1177],{"class":271},"RULE_TAGS",[96,1179,481],{"class":250},[96,1181,1183,1185,1187,1189,1191,1193,1195,1198],{"class":98,"line":1182},11,[96,1184,1170],{"class":138},[96,1186,575],{"class":200},[96,1188,204],{"class":250},[96,1190,165],{"class":158},[96,1192,86],{"class":106},[96,1194,165],{"class":158},[96,1196,1197],{"class":250},")     ",[96,1199,1201],{"class":1200},"s_gjE","\u002F\u002F known issue: code block theme contrast (deferred)\n",[96,1203,1205,1207,1209,1211,1213,1215,1217,1219,1221],{"class":98,"line":1204},12,[96,1206,1170],{"class":138},[96,1208,575],{"class":200},[96,1210,204],{"class":250},[96,1212,165],{"class":158},[96,1214,629],{"class":106},[96,1216,165],{"class":158},[96,1218,260],{"class":250},[96,1220,146],{"class":138},[96,1222,1223],{"class":1200}," \u002F\u002F third-party embeds\n",[96,1225,1227,1230],{"class":98,"line":1226},13,[96,1228,1229],{"class":250},"    )",[96,1231,168],{"class":138},[96,1233,1235],{"class":98,"line":1234},14,[96,1236,1237],{"class":138},"  },\n",[96,1239,1241,1243,1245],{"class":98,"line":1240},15,[96,1242,340],{"class":138},[96,1244,260],{"class":142},[96,1246,168],{"class":138},[11,1248,1249,1250,1252],{},"Phasing in a new check is now a one-line change to ",[24,1251,1177],{}," or the exclusions, and every test picks it up.",[41,1254,1256],{"id":1255},"handle-known-axe-violations-without-failing-the-build","Handle Known axe Violations Without Failing the Build",[11,1258,1259,1260,1263,1264,1267],{},"After the exclusions, two contrast issues were left that I'd decided to defer: the dates on my About page timeline, and some green \"Pass\" badges on one of my tool pages. Excluding those elements would turn off every rule for them. I wanted something narrower: tolerate ",[606,1261,1262],{},"that rule"," on ",[606,1265,1266],{},"those elements",", and check everything else as normal.",[11,1269,1270],{},"So the test keeps an explicit list of known issues, each with a reason:",[86,1272,1274],{"className":124,"code":1273,"filename":126,"language":127,"meta":92,"style":92},"const KNOWN_ISSUES = [\n  {\n    path: '\u002Fabout',\n    rule: 'color-contrast',\n    selector: '[data-slot=\"date\"]',\n    reason: 'Timeline dates use a dimmed text color (2.63:1)',\n  },\n  {\n    path: '\u002Ftools\u002Fcolor-contrast-checker',\n    rule: 'color-contrast',\n    selector: '.bg-success',\n    reason: 'Solid green \"Pass\" badges',\n    colorScheme: 'light',\n  },\n];\n",[24,1275,1276,1288,1293,1310,1325,1341,1357,1361,1365,1380,1394,1409,1424,1440,1444],{"__ignoreMap":92},[96,1277,1278,1280,1283,1285],{"class":98,"line":99},[96,1279,459],{"class":216},[96,1281,1282],{"class":271}," KNOWN_ISSUES",[96,1284,276],{"class":275},[96,1286,1287],{"class":142}," [\n",[96,1289,1290],{"class":98,"line":171},[96,1291,1292],{"class":138},"  {\n",[96,1294,1295,1298,1300,1302,1305,1307],{"class":98,"line":190},[96,1296,1297],{"class":250},"    path",[96,1299,728],{"class":138},[96,1301,159],{"class":158},[96,1303,1304],{"class":106},"\u002Fabout",[96,1306,165],{"class":158},[96,1308,1309],{"class":138},",\n",[96,1311,1312,1315,1317,1319,1321,1323],{"class":98,"line":197},[96,1313,1314],{"class":250},"    rule",[96,1316,728],{"class":138},[96,1318,159],{"class":158},[96,1320,394],{"class":106},[96,1322,165],{"class":158},[96,1324,1309],{"class":138},[96,1326,1327,1330,1332,1334,1337,1339],{"class":98,"line":236},[96,1328,1329],{"class":250},"    selector",[96,1331,728],{"class":138},[96,1333,159],{"class":158},[96,1335,1336],{"class":106},"[data-slot=\"date\"]",[96,1338,165],{"class":158},[96,1340,1309],{"class":138},[96,1342,1343,1346,1348,1350,1353,1355],{"class":98,"line":265},[96,1344,1345],{"class":250},"    reason",[96,1347,728],{"class":138},[96,1349,159],{"class":158},[96,1351,1352],{"class":106},"Timeline dates use a dimmed text color (2.63:1)",[96,1354,165],{"class":158},[96,1356,1309],{"class":138},[96,1358,1359],{"class":98,"line":309},[96,1360,1237],{"class":138},[96,1362,1363],{"class":98,"line":337},[96,1364,1292],{"class":138},[96,1366,1367,1369,1371,1373,1376,1378],{"class":98,"line":1149},[96,1368,1297],{"class":250},[96,1370,728],{"class":138},[96,1372,159],{"class":158},[96,1374,1375],{"class":106},"\u002Ftools\u002Fcolor-contrast-checker",[96,1377,165],{"class":158},[96,1379,1309],{"class":138},[96,1381,1382,1384,1386,1388,1390,1392],{"class":98,"line":1167},[96,1383,1314],{"class":250},[96,1385,728],{"class":138},[96,1387,159],{"class":158},[96,1389,394],{"class":106},[96,1391,165],{"class":158},[96,1393,1309],{"class":138},[96,1395,1396,1398,1400,1402,1405,1407],{"class":98,"line":1182},[96,1397,1329],{"class":250},[96,1399,728],{"class":138},[96,1401,159],{"class":158},[96,1403,1404],{"class":106},".bg-success",[96,1406,165],{"class":158},[96,1408,1309],{"class":138},[96,1410,1411,1413,1415,1417,1420,1422],{"class":98,"line":1204},[96,1412,1345],{"class":250},[96,1414,728],{"class":138},[96,1416,159],{"class":158},[96,1418,1419],{"class":106},"Solid green \"Pass\" badges",[96,1421,165],{"class":158},[96,1423,1309],{"class":138},[96,1425,1426,1429,1431,1433,1436,1438],{"class":98,"line":1226},[96,1427,1428],{"class":250},"    colorScheme",[96,1430,728],{"class":138},[96,1432,159],{"class":158},[96,1434,1435],{"class":106},"light",[96,1437,165],{"class":158},[96,1439,1309],{"class":138},[96,1441,1442],{"class":98,"line":1234},[96,1443,1237],{"class":138},[96,1445,1446,1448],{"class":98,"line":1240},[96,1447,1062],{"class":142},[96,1449,168],{"class":138},[11,1451,1452,1453,1455],{},"After ",[24,1454,349],{},", a small helper removes violations that match a known issue's page, rule, and selector, and the test asserts that nothing else is left:",[86,1457,1459],{"className":124,"code":1458,"filename":126,"language":127,"meta":92,"style":92},"const violations = await withoutKnownIssues(page, path, colorScheme, results.violations);\nexpect(\n  violations.map((v) => ({ rule: v.id, impact: v.impact, targets: v.nodes.map((n) => n.target.join(' ')) })),\n).toEqual([]);\n",[24,1460,1461,1499,1507,1618],{"__ignoreMap":92},[96,1462,1463,1465,1468,1470,1472,1475,1478,1480,1483,1485,1488,1490,1492,1494,1497],{"class":98,"line":99},[96,1464,459],{"class":216},[96,1466,1467],{"class":271}," violations",[96,1469,276],{"class":275},[96,1471,279],{"class":134},[96,1473,1474],{"class":200}," withoutKnownIssues",[96,1476,1477],{"class":142},"(page",[96,1479,146],{"class":138},[96,1481,1482],{"class":142}," path",[96,1484,146],{"class":138},[96,1486,1487],{"class":142}," colorScheme",[96,1489,146],{"class":138},[96,1491,272],{"class":142},[96,1493,244],{"class":138},[96,1495,1496],{"class":142},"violations)",[96,1498,168],{"class":138},[96,1500,1501,1504],{"class":98,"line":171},[96,1502,1503],{"class":200},"expect",[96,1505,1506],{"class":142},"(\n",[96,1508,1509,1512,1514,1517,1519,1521,1524,1526,1528,1531,1533,1536,1538,1541,1543,1546,1548,1551,1553,1555,1557,1560,1562,1565,1567,1569,1571,1574,1576,1578,1580,1582,1585,1587,1589,1592,1594,1597,1599,1602,1604,1606,1608,1611,1613,1616],{"class":98,"line":190},[96,1510,1511],{"class":142},"  violations",[96,1513,244],{"class":138},[96,1515,1516],{"class":200},"map",[96,1518,204],{"class":142},[96,1520,204],{"class":138},[96,1522,1523],{"class":223},"v",[96,1525,260],{"class":138},[96,1527,230],{"class":216},[96,1529,1530],{"class":142}," (",[96,1532,290],{"class":138},[96,1534,1535],{"class":250}," rule",[96,1537,728],{"class":138},[96,1539,1540],{"class":142}," v",[96,1542,244],{"class":138},[96,1544,1545],{"class":142},"id",[96,1547,146],{"class":138},[96,1549,1550],{"class":250}," impact",[96,1552,728],{"class":138},[96,1554,1540],{"class":142},[96,1556,244],{"class":138},[96,1558,1559],{"class":142},"impact",[96,1561,146],{"class":138},[96,1563,1564],{"class":250}," targets",[96,1566,728],{"class":138},[96,1568,1540],{"class":142},[96,1570,244],{"class":138},[96,1572,1573],{"class":142},"nodes",[96,1575,244],{"class":138},[96,1577,1516],{"class":200},[96,1579,204],{"class":142},[96,1581,204],{"class":138},[96,1583,1584],{"class":223},"n",[96,1586,260],{"class":138},[96,1588,230],{"class":216},[96,1590,1591],{"class":142}," n",[96,1593,244],{"class":138},[96,1595,1596],{"class":142},"target",[96,1598,244],{"class":138},[96,1600,1601],{"class":200},"join",[96,1603,204],{"class":142},[96,1605,165],{"class":158},[96,1607,159],{"class":158},[96,1609,1610],{"class":142},")) ",[96,1612,340],{"class":138},[96,1614,1615],{"class":142},"))",[96,1617,1309],{"class":138},[96,1619,1620,1622,1624,1626,1628],{"class":98,"line":197},[96,1621,260],{"class":142},[96,1623,244],{"class":138},[96,1625,329],{"class":200},[96,1627,332],{"class":142},[96,1629,168],{"class":138},[11,1631,1632],{},"Mapping the violations to rule, impact, and targets keeps the failure message readable. To prove the guard works, I removed the About page entry and ran the test:",[86,1634,1639],{"className":1635,"code":1637,"language":1638,"meta":92},[1636],"language-text","- Expected  -  1\n+ Received  + 13\n\n- Array []\n+ Array [\n+   Object {\n+     \"impact\": \"serious\",\n+     \"rule\": \"color-contrast\",\n+     \"targets\": Array [\n+       \".gap-3.group[data-slot=\\\"item\\\"]:nth-child(1) > … > .text-dimmed.text-xs\\\\\u002F5[data-slot=\\\"date\\\"]\",\n+       … 4 more timeline dates\n+     ],\n+   },\n+ ]\n","text",[24,1640,1637],{"__ignoreMap":92},[11,1642,1643],{},"Both the light and dark mode tests failed and named exactly the five timeline dates. When I fix them, I delete the entry, and the test then guards against the problem coming back.",[11,1645,1646],{},"This list is also where the workflow from my Nightwatch article fits in. Issues that existed before the team agreed on a rule set are known issues and get planned as feature work.",[1648,1649,1650],"blockquote",{},[11,1651,1652,1653,1656],{},"A violation that appears ",[606,1654,1655],{},"after"," the agreement is a regression and should be treated as a defect and fail the build",[11,1658,1659,1660,1663],{},"Playwright's documentation suggests a different approach: snapshotting a fingerprint of known violations with ",[24,1661,1662],{},"toMatchSnapshot()",". That works, but Playwright adds the operating system to text snapshot file names, so a snapshot generated on my Windows machine wouldn't match in Linux CI without extra setup. An explicit list with reasons is also easier to review in a pull request, at least in my case.",[41,1665,1667,1668],{"id":1666},"run-axe-in-light-and-dark-mode-with-colorscheme","Run axe in Light and Dark Mode with ",[24,1669,1670],{},"colorScheme",[11,1672,1673,1674],{},"The code block failures only existed in light mode, and the badge issue was light-only too. A scan in one color scheme would have missed half the picture. This underscores the importance of test automation to cover workflows and use cases not normally exercised. I didn't catch this manually because I normally use the site in dark mode. Further, contrast violations are not always very obvious without accessibility tooling to check. ",[606,1675,1676,1677,1680],{},"When designing your site or checking fixes you can use my ",[18,1678,1679],{"href":1375},"WCAG color contrast checker"," to verify color-contrast rule conformance.",[11,1682,1683,1684,1687],{},"This example leverages Playwright's ability to use a ",[24,1685,1686],{},"for"," loop to parameterize in test variations to cut down on test code duplication to run the checks in light and dark mode with the same test body:",[86,1689,1691],{"className":124,"code":1690,"filename":126,"language":127,"meta":92,"style":92},"for (const colorScheme of ['light', 'dark'] as const) {\n  test.describe(`Accessibility (${colorScheme} mode)`, () => {\n    test.use({ colorScheme });\n    \u002F\u002F ...one test per page\n  });\n}\n",[24,1692,1693,1737,1775,1797,1802,1811],{"__ignoreMap":92},[96,1694,1695,1697,1699,1701,1703,1706,1708,1710,1712,1714,1716,1718,1721,1723,1726,1729,1732,1735],{"class":98,"line":99},[96,1696,1686],{"class":134},[96,1698,1530],{"class":142},[96,1700,459],{"class":216},[96,1702,1487],{"class":271},[96,1704,1705],{"class":275}," of",[96,1707,1021],{"class":142},[96,1709,165],{"class":158},[96,1711,1435],{"class":106},[96,1713,165],{"class":158},[96,1715,146],{"class":138},[96,1717,159],{"class":158},[96,1719,1720],{"class":106},"dark",[96,1722,165],{"class":158},[96,1724,1725],{"class":142},"] ",[96,1727,1728],{"class":134},"as",[96,1730,1731],{"class":216}," const",[96,1733,1734],{"class":142},") ",[96,1736,1107],{"class":138},[96,1738,1739,1742,1744,1747,1749,1752,1755,1758,1760,1762,1765,1767,1769,1771,1773],{"class":98,"line":171},[96,1740,1741],{"class":142},"  test",[96,1743,244],{"class":138},[96,1745,1746],{"class":200},"describe",[96,1748,204],{"class":250},[96,1750,1751],{"class":158},"`",[96,1753,1754],{"class":106},"Accessibility (",[96,1756,1757],{"class":158},"${",[96,1759,1670],{"class":142},[96,1761,340],{"class":158},[96,1763,1764],{"class":106}," mode)",[96,1766,1751],{"class":158},[96,1768,146],{"class":138},[96,1770,1095],{"class":138},[96,1772,230],{"class":216},[96,1774,233],{"class":138},[96,1776,1777,1780,1782,1785,1787,1789,1791,1793,1795],{"class":98,"line":190},[96,1778,1779],{"class":142},"    test",[96,1781,244],{"class":138},[96,1783,1784],{"class":200},"use",[96,1786,204],{"class":250},[96,1788,290],{"class":138},[96,1790,1487],{"class":142},[96,1792,152],{"class":138},[96,1794,260],{"class":250},[96,1796,168],{"class":138},[96,1798,1799],{"class":98,"line":197},[96,1800,1801],{"class":1200},"    \u002F\u002F ...one test per page\n",[96,1803,1804,1807,1809],{"class":98,"line":236},[96,1805,1806],{"class":138},"  }",[96,1808,260],{"class":250},[96,1810,168],{"class":138},[96,1812,1813],{"class":98,"line":265},[96,1814,1815],{"class":138},"}\n",[41,1817,1819],{"id":1818},"attach-axe-results-to-the-playwright-html-report","Attach axe Results to the Playwright HTML Report",[11,1821,1822],{},"The assertion shows a summary, but when you're fixing something you want everything axe returned. Attach the full results to the test so they appear in Playwright's HTML report:",[86,1824,1826],{"className":124,"code":1825,"filename":126,"language":127,"meta":92,"style":92},"test('home has no new accessibility violations', async ({ page, makeAxeBuilder }, testInfo) => {\n  await page.goto('\u002F');\n  const results = await makeAxeBuilder().analyze();\n\n  await testInfo.attach('axe-results-home', {\n    body: JSON.stringify(results.violations, null, 2),\n    contentType: 'application\u002Fjson',\n  });\n\n  \u002F\u002F ...known issues filter and assertion\n});\n",[24,1827,1828,1864,1886,1908,1912,1936,1975,1991,1999,2003,2008],{"__ignoreMap":92},[96,1829,1830,1832,1834,1836,1839,1841,1843,1845,1847,1849,1851,1853,1855,1858,1860,1862],{"class":98,"line":99},[96,1831,201],{"class":200},[96,1833,204],{"class":142},[96,1835,165],{"class":158},[96,1837,1838],{"class":106},"home has no new accessibility violations",[96,1840,165],{"class":158},[96,1842,146],{"class":138},[96,1844,217],{"class":216},[96,1846,220],{"class":138},[96,1848,224],{"class":223},[96,1850,146],{"class":138},[96,1852,1090],{"class":223},[96,1854,1123],{"class":138},[96,1856,1857],{"class":223}," testInfo",[96,1859,260],{"class":138},[96,1861,230],{"class":216},[96,1863,233],{"class":138},[96,1865,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884],{"class":98,"line":171},[96,1867,239],{"class":134},[96,1869,224],{"class":142},[96,1871,244],{"class":138},[96,1873,247],{"class":200},[96,1875,204],{"class":250},[96,1877,165],{"class":158},[96,1879,255],{"class":106},[96,1881,165],{"class":158},[96,1883,260],{"class":250},[96,1885,168],{"class":138},[96,1887,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906],{"class":98,"line":190},[96,1889,268],{"class":216},[96,1891,272],{"class":271},[96,1893,276],{"class":275},[96,1895,279],{"class":134},[96,1897,1090],{"class":200},[96,1899,304],{"class":250},[96,1901,244],{"class":138},[96,1903,301],{"class":200},[96,1905,304],{"class":250},[96,1907,168],{"class":138},[96,1909,1910],{"class":98,"line":197},[96,1911,194],{"emptyLinePlaceholder":193},[96,1913,1914,1916,1918,1920,1923,1925,1927,1930,1932,1934],{"class":98,"line":236},[96,1915,239],{"class":134},[96,1917,1857],{"class":142},[96,1919,244],{"class":138},[96,1921,1922],{"class":200},"attach",[96,1924,204],{"class":250},[96,1926,165],{"class":158},[96,1928,1929],{"class":106},"axe-results-home",[96,1931,165],{"class":158},[96,1933,146],{"class":138},[96,1935,233],{"class":138},[96,1937,1938,1941,1943,1946,1948,1951,1953,1955,1957,1959,1961,1965,1967,1971,1973],{"class":98,"line":265},[96,1939,1940],{"class":250},"    body",[96,1942,728],{"class":138},[96,1944,1945],{"class":271}," JSON",[96,1947,244],{"class":138},[96,1949,1950],{"class":200},"stringify",[96,1952,204],{"class":250},[96,1954,317],{"class":142},[96,1956,244],{"class":138},[96,1958,322],{"class":142},[96,1960,146],{"class":138},[96,1962,1964],{"class":1963},"sPxkN"," null",[96,1966,146],{"class":138},[96,1968,1970],{"class":1969},"s6g51"," 2",[96,1972,260],{"class":250},[96,1974,1309],{"class":138},[96,1976,1977,1980,1982,1984,1987,1989],{"class":98,"line":309},[96,1978,1979],{"class":250},"    contentType",[96,1981,728],{"class":138},[96,1983,159],{"class":158},[96,1985,1986],{"class":106},"application\u002Fjson",[96,1988,165],{"class":158},[96,1990,1309],{"class":138},[96,1992,1993,1995,1997],{"class":98,"line":337},[96,1994,1806],{"class":138},[96,1996,260],{"class":250},[96,1998,168],{"class":138},[96,2000,2001],{"class":98,"line":1149},[96,2002,194],{"emptyLinePlaceholder":193},[96,2004,2005],{"class":98,"line":1167},[96,2006,2007],{"class":1200},"  \u002F\u002F ...known issues filter and assertion\n",[96,2009,2010,2012,2014],{"class":98,"line":1182},[96,2011,340],{"class":138},[96,2013,260],{"class":142},[96,2015,168],{"class":138},[11,2017,2018],{},"To show what a failure looks like, this report is from the guard check earlier, where I deliberately removed the About page's timeline dates from the known issues list. With that entry gone, both About tests fail, one per color scheme, and the other eight pass. With the entry restored, all ten pass:",[2020,2021,2022,2029],"figure",{},[11,2023,2024],{},[2025,2026],"img",{"alt":2027,"src":2028},"Playwright HTML report listing 10 accessibility tests, 8 passed and 2 failed: the About page fails in both light and dark mode, while home, article, product review, and tool pass in both modes","\u002Fimages\u002Fposts\u002Fdisable-exclude-phase-in-axe-rules-playwright\u002Fplaywright-html-report-accessibility-tests-light-dark.webp",[2030,2031,2036],"figcaption",{"className":2032},[2033,2034,2035],"text-sm","text-muted","mt-2","With the About page's known issue removed on purpose, its tests fail in both light and dark mode",[11,2038,2039,2040,2043],{},"Open a failing test and the attachment is under ",[55,2041,2042],{},"Attachments",". Each violation includes the rule, its impact, the WCAG criteria it maps to, a link to Deque's explanation, and for contrast failures, the exact colors and ratio. Here's the first of the five timeline dates from that attachment:",[86,2045,2050],{"className":2046,"code":2047,"filename":2048,"language":2049,"meta":92,"style":92},"language-json shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","[\n  {\n    \"id\": \"color-contrast\",\n    \"impact\": \"serious\",\n    \"tags\": [\n      \"cat.color\",\n      \"wcag2aa\",\n      \"wcag143\",\n      \"TTv5\",\n      \"TT13.c\",\n      \"EN-301-549\",\n      \"EN-9.1.4.3\",\n      \"ACT\",\n      \"RGAAv4\",\n      \"RGAA-3.2.1\"\n    ],\n    \"description\": \"Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds\",\n    \"help\": \"Elements must meet minimum color contrast ratio thresholds\",\n    \"helpUrl\": \"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.13\u002Fcolor-contrast?application=playwright\",\n    \"nodes\": [\n      {\n        \"any\": [\n          {\n            \"id\": \"color-contrast\",\n            \"data\": {\n              \"fgColor\": \"#90a1b9\",\n              \"bgColor\": \"#ffffff\",\n              \"contrastRatio\": 2.63,\n              \"fontSize\": \"9.0pt (12px)\",\n              \"fontWeight\": \"normal\",\n              \"messageKey\": null,\n              \"expectedContrastRatio\": \"4.5:1\"\n            },\n            \"relatedNodes\": [\n              {\n                \"html\": \"\u003Cbody>\",\n                \"target\": [\n                  \"body\"\n                ]\n              }\n            ],\n            \"impact\": \"serious\",\n            \"message\": \"Element has insufficient color contrast of 2.63 (foreground color: #90a1b9, background color: #ffffff, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1\"\n          }\n        ],\n        \"all\": [],\n        \"none\": [],\n        \"impact\": \"serious\",\n        \"html\": \"\u003Cdiv data-slot=\\\"date\\\" class=\\\"text-dimmed text-xs\u002F5\\\">\u003C!--[-->2023 – Present\u003C!--]-->\u003C\u002Fdiv>\",\n        \"target\": [\n          \".gap-3.group[data-slot=\\\"item\\\"]:nth-child(1) > .mt-1\\\\.5.pb-6\\\\.5[data-slot=\\\"wrapper\\\"] > .text-dimmed.text-xs\\\\\u002F5[data-slot=\\\"date\\\"]\"\n        ],\n        \"failureSummary\": \"Fix any of the following:\\n  Element has insufficient color contrast of 2.63 (foreground color: #90a1b9, background color: #ffffff, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1\"\n      }\n    ]\n  }\n]\n","axe-results-about-light.json","json",[24,2051,2052,2057,2061,2084,2103,2116,2128,2138,2149,2160,2171,2182,2193,2204,2215,2225,2231,2252,2273,2294,2307,2313,2329,2335,2356,2370,2393,2414,2431,2452,2473,2489,2508,2514,2528,2534,2556,2569,2580,2586,2592,2598,2617,2636,2642,2648,2663,2677,2696,2738,2751,2806,2811,2836,2842,2848,2854],{"__ignoreMap":92},[96,2053,2054],{"class":98,"line":99},[96,2055,2056],{"class":138},"[\n",[96,2058,2059],{"class":98,"line":171},[96,2060,1292],{"class":138},[96,2062,2063,2067,2070,2073,2075,2078,2080,2082],{"class":98,"line":190},[96,2064,2066],{"class":2065},"sYyCj","    \"",[96,2068,1545],{"class":2069},"svTos",[96,2071,2072],{"class":2065},"\"",[96,2074,728],{"class":138},[96,2076,2077],{"class":158}," \"",[96,2079,394],{"class":106},[96,2081,2072],{"class":158},[96,2083,1309],{"class":138},[96,2085,2086,2088,2090,2092,2094,2096,2099,2101],{"class":98,"line":197},[96,2087,2066],{"class":2065},[96,2089,1559],{"class":2069},[96,2091,2072],{"class":2065},[96,2093,728],{"class":138},[96,2095,2077],{"class":158},[96,2097,2098],{"class":106},"serious",[96,2100,2072],{"class":158},[96,2102,1309],{"class":138},[96,2104,2105,2107,2110,2112,2114],{"class":98,"line":236},[96,2106,2066],{"class":2065},[96,2108,2109],{"class":2069},"tags",[96,2111,2072],{"class":2065},[96,2113,728],{"class":138},[96,2115,1287],{"class":138},[96,2117,2118,2121,2124,2126],{"class":98,"line":265},[96,2119,2120],{"class":158},"      \"",[96,2122,2123],{"class":106},"cat.color",[96,2125,2072],{"class":158},[96,2127,1309],{"class":138},[96,2129,2130,2132,2134,2136],{"class":98,"line":309},[96,2131,2120],{"class":158},[96,2133,880],{"class":106},[96,2135,2072],{"class":158},[96,2137,1309],{"class":138},[96,2139,2140,2142,2145,2147],{"class":98,"line":337},[96,2141,2120],{"class":158},[96,2143,2144],{"class":106},"wcag143",[96,2146,2072],{"class":158},[96,2148,1309],{"class":138},[96,2150,2151,2153,2156,2158],{"class":98,"line":1149},[96,2152,2120],{"class":158},[96,2154,2155],{"class":106},"TTv5",[96,2157,2072],{"class":158},[96,2159,1309],{"class":138},[96,2161,2162,2164,2167,2169],{"class":98,"line":1167},[96,2163,2120],{"class":158},[96,2165,2166],{"class":106},"TT13.c",[96,2168,2072],{"class":158},[96,2170,1309],{"class":138},[96,2172,2173,2175,2178,2180],{"class":98,"line":1182},[96,2174,2120],{"class":158},[96,2176,2177],{"class":106},"EN-301-549",[96,2179,2072],{"class":158},[96,2181,1309],{"class":138},[96,2183,2184,2186,2189,2191],{"class":98,"line":1204},[96,2185,2120],{"class":158},[96,2187,2188],{"class":106},"EN-9.1.4.3",[96,2190,2072],{"class":158},[96,2192,1309],{"class":138},[96,2194,2195,2197,2200,2202],{"class":98,"line":1226},[96,2196,2120],{"class":158},[96,2198,2199],{"class":106},"ACT",[96,2201,2072],{"class":158},[96,2203,1309],{"class":138},[96,2205,2206,2208,2211,2213],{"class":98,"line":1234},[96,2207,2120],{"class":158},[96,2209,2210],{"class":106},"RGAAv4",[96,2212,2072],{"class":158},[96,2214,1309],{"class":138},[96,2216,2217,2219,2222],{"class":98,"line":1240},[96,2218,2120],{"class":158},[96,2220,2221],{"class":106},"RGAA-3.2.1",[96,2223,2224],{"class":158},"\"\n",[96,2226,2228],{"class":98,"line":2227},16,[96,2229,2230],{"class":138},"    ],\n",[96,2232,2234,2236,2239,2241,2243,2245,2248,2250],{"class":98,"line":2233},17,[96,2235,2066],{"class":2065},[96,2237,2238],{"class":2069},"description",[96,2240,2072],{"class":2065},[96,2242,728],{"class":138},[96,2244,2077],{"class":158},[96,2246,2247],{"class":106},"Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds",[96,2249,2072],{"class":158},[96,2251,1309],{"class":138},[96,2253,2255,2257,2260,2262,2264,2266,2269,2271],{"class":98,"line":2254},18,[96,2256,2066],{"class":2065},[96,2258,2259],{"class":2069},"help",[96,2261,2072],{"class":2065},[96,2263,728],{"class":138},[96,2265,2077],{"class":158},[96,2267,2268],{"class":106},"Elements must meet minimum color contrast ratio thresholds",[96,2270,2072],{"class":158},[96,2272,1309],{"class":138},[96,2274,2276,2278,2281,2283,2285,2287,2290,2292],{"class":98,"line":2275},19,[96,2277,2066],{"class":2065},[96,2279,2280],{"class":2069},"helpUrl",[96,2282,2072],{"class":2065},[96,2284,728],{"class":138},[96,2286,2077],{"class":158},[96,2288,2289],{"class":106},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.13\u002Fcolor-contrast?application=playwright",[96,2291,2072],{"class":158},[96,2293,1309],{"class":138},[96,2295,2297,2299,2301,2303,2305],{"class":98,"line":2296},20,[96,2298,2066],{"class":2065},[96,2300,1573],{"class":2069},[96,2302,2072],{"class":2065},[96,2304,728],{"class":138},[96,2306,1287],{"class":138},[96,2308,2310],{"class":98,"line":2309},21,[96,2311,2312],{"class":138},"      {\n",[96,2314,2316,2319,2323,2325,2327],{"class":98,"line":2315},22,[96,2317,2318],{"class":2065},"        \"",[96,2320,2322],{"class":2321},"smdAf","any",[96,2324,2072],{"class":2065},[96,2326,728],{"class":138},[96,2328,1287],{"class":138},[96,2330,2332],{"class":98,"line":2331},23,[96,2333,2334],{"class":138},"          {\n",[96,2336,2338,2341,2344,2346,2348,2350,2352,2354],{"class":98,"line":2337},24,[96,2339,2340],{"class":2065},"            \"",[96,2342,1545],{"class":2343},"sDDhh",[96,2345,2072],{"class":2065},[96,2347,728],{"class":138},[96,2349,2077],{"class":158},[96,2351,394],{"class":106},[96,2353,2072],{"class":158},[96,2355,1309],{"class":138},[96,2357,2359,2361,2364,2366,2368],{"class":98,"line":2358},25,[96,2360,2340],{"class":2065},[96,2362,2363],{"class":2343},"data",[96,2365,2072],{"class":2065},[96,2367,728],{"class":138},[96,2369,233],{"class":138},[96,2371,2373,2376,2380,2382,2384,2386,2389,2391],{"class":98,"line":2372},26,[96,2374,2375],{"class":2065},"              \"",[96,2377,2379],{"class":2378},"saWzx","fgColor",[96,2381,2072],{"class":2065},[96,2383,728],{"class":138},[96,2385,2077],{"class":158},[96,2387,2388],{"class":106},"#90a1b9",[96,2390,2072],{"class":158},[96,2392,1309],{"class":138},[96,2394,2396,2398,2401,2403,2405,2407,2410,2412],{"class":98,"line":2395},27,[96,2397,2375],{"class":2065},[96,2399,2400],{"class":2378},"bgColor",[96,2402,2072],{"class":2065},[96,2404,728],{"class":138},[96,2406,2077],{"class":158},[96,2408,2409],{"class":106},"#ffffff",[96,2411,2072],{"class":158},[96,2413,1309],{"class":138},[96,2415,2417,2419,2422,2424,2426,2429],{"class":98,"line":2416},28,[96,2418,2375],{"class":2065},[96,2420,2421],{"class":2378},"contrastRatio",[96,2423,2072],{"class":2065},[96,2425,728],{"class":138},[96,2427,2428],{"class":1969}," 2.63",[96,2430,1309],{"class":138},[96,2432,2434,2436,2439,2441,2443,2445,2448,2450],{"class":98,"line":2433},29,[96,2435,2375],{"class":2065},[96,2437,2438],{"class":2378},"fontSize",[96,2440,2072],{"class":2065},[96,2442,728],{"class":138},[96,2444,2077],{"class":158},[96,2446,2447],{"class":106},"9.0pt (12px)",[96,2449,2072],{"class":158},[96,2451,1309],{"class":138},[96,2453,2455,2457,2460,2462,2464,2466,2469,2471],{"class":98,"line":2454},30,[96,2456,2375],{"class":2065},[96,2458,2459],{"class":2378},"fontWeight",[96,2461,2072],{"class":2065},[96,2463,728],{"class":138},[96,2465,2077],{"class":158},[96,2467,2468],{"class":106},"normal",[96,2470,2072],{"class":158},[96,2472,1309],{"class":138},[96,2474,2476,2478,2481,2483,2485,2487],{"class":98,"line":2475},31,[96,2477,2375],{"class":2065},[96,2479,2480],{"class":2378},"messageKey",[96,2482,2072],{"class":2065},[96,2484,728],{"class":138},[96,2486,1964],{"class":1963},[96,2488,1309],{"class":138},[96,2490,2492,2494,2497,2499,2501,2503,2506],{"class":98,"line":2491},32,[96,2493,2375],{"class":2065},[96,2495,2496],{"class":2378},"expectedContrastRatio",[96,2498,2072],{"class":2065},[96,2500,728],{"class":138},[96,2502,2077],{"class":158},[96,2504,2505],{"class":106},"4.5:1",[96,2507,2224],{"class":158},[96,2509,2511],{"class":98,"line":2510},33,[96,2512,2513],{"class":138},"            },\n",[96,2515,2517,2519,2522,2524,2526],{"class":98,"line":2516},34,[96,2518,2340],{"class":2065},[96,2520,2521],{"class":2343},"relatedNodes",[96,2523,2072],{"class":2065},[96,2525,728],{"class":138},[96,2527,1287],{"class":138},[96,2529,2531],{"class":98,"line":2530},35,[96,2532,2533],{"class":138},"              {\n",[96,2535,2537,2540,2543,2545,2547,2549,2552,2554],{"class":98,"line":2536},36,[96,2538,2539],{"class":2065},"                \"",[96,2541,2542],{"class":2378},"html",[96,2544,2072],{"class":2065},[96,2546,728],{"class":138},[96,2548,2077],{"class":158},[96,2550,2551],{"class":106},"\u003Cbody>",[96,2553,2072],{"class":158},[96,2555,1309],{"class":138},[96,2557,2559,2561,2563,2565,2567],{"class":98,"line":2558},37,[96,2560,2539],{"class":2065},[96,2562,1596],{"class":2378},[96,2564,2072],{"class":2065},[96,2566,728],{"class":138},[96,2568,1287],{"class":138},[96,2570,2572,2575,2578],{"class":98,"line":2571},38,[96,2573,2574],{"class":158},"                  \"",[96,2576,2577],{"class":106},"body",[96,2579,2224],{"class":158},[96,2581,2583],{"class":98,"line":2582},39,[96,2584,2585],{"class":138},"                ]\n",[96,2587,2589],{"class":98,"line":2588},40,[96,2590,2591],{"class":138},"              }\n",[96,2593,2595],{"class":98,"line":2594},41,[96,2596,2597],{"class":138},"            ],\n",[96,2599,2601,2603,2605,2607,2609,2611,2613,2615],{"class":98,"line":2600},42,[96,2602,2340],{"class":2065},[96,2604,1559],{"class":2343},[96,2606,2072],{"class":2065},[96,2608,728],{"class":138},[96,2610,2077],{"class":158},[96,2612,2098],{"class":106},[96,2614,2072],{"class":158},[96,2616,1309],{"class":138},[96,2618,2620,2622,2625,2627,2629,2631,2634],{"class":98,"line":2619},43,[96,2621,2340],{"class":2065},[96,2623,2624],{"class":2343},"message",[96,2626,2072],{"class":2065},[96,2628,728],{"class":138},[96,2630,2077],{"class":158},[96,2632,2633],{"class":106},"Element has insufficient color contrast of 2.63 (foreground color: #90a1b9, background color: #ffffff, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",[96,2635,2224],{"class":158},[96,2637,2639],{"class":98,"line":2638},44,[96,2640,2641],{"class":138},"          }\n",[96,2643,2645],{"class":98,"line":2644},45,[96,2646,2647],{"class":138},"        ],\n",[96,2649,2651,2653,2656,2658,2660],{"class":98,"line":2650},46,[96,2652,2318],{"class":2065},[96,2654,2655],{"class":2321},"all",[96,2657,2072],{"class":2065},[96,2659,728],{"class":138},[96,2661,2662],{"class":138}," [],\n",[96,2664,2666,2668,2671,2673,2675],{"class":98,"line":2665},47,[96,2667,2318],{"class":2065},[96,2669,2670],{"class":2321},"none",[96,2672,2072],{"class":2065},[96,2674,728],{"class":138},[96,2676,2662],{"class":138},[96,2678,2680,2682,2684,2686,2688,2690,2692,2694],{"class":98,"line":2679},48,[96,2681,2318],{"class":2065},[96,2683,1559],{"class":2321},[96,2685,2072],{"class":2065},[96,2687,728],{"class":138},[96,2689,2077],{"class":158},[96,2691,2098],{"class":106},[96,2693,2072],{"class":158},[96,2695,1309],{"class":138},[96,2697,2699,2701,2703,2705,2707,2709,2712,2716,2719,2721,2724,2726,2729,2731,2734,2736],{"class":98,"line":2698},49,[96,2700,2318],{"class":2065},[96,2702,2542],{"class":2321},[96,2704,2072],{"class":2065},[96,2706,728],{"class":138},[96,2708,2077],{"class":158},[96,2710,2711],{"class":106},"\u003Cdiv data-slot=",[96,2713,2715],{"class":2714},"sVYJ7","\\\"",[96,2717,2718],{"class":106},"date",[96,2720,2715],{"class":2714},[96,2722,2723],{"class":106}," class=",[96,2725,2715],{"class":2714},[96,2727,2728],{"class":106},"text-dimmed text-xs\u002F5",[96,2730,2715],{"class":2714},[96,2732,2733],{"class":106},">\u003C!--[-->2023 – Present\u003C!--]-->\u003C\u002Fdiv>",[96,2735,2072],{"class":158},[96,2737,1309],{"class":138},[96,2739,2741,2743,2745,2747,2749],{"class":98,"line":2740},50,[96,2742,2318],{"class":2065},[96,2744,1596],{"class":2321},[96,2746,2072],{"class":2065},[96,2748,728],{"class":138},[96,2750,1287],{"class":138},[96,2752,2754,2757,2760,2762,2765,2767,2770,2773,2776,2778,2781,2783,2786,2788,2791,2793,2796,2798,2800,2802,2804],{"class":98,"line":2753},51,[96,2755,2756],{"class":158},"          \"",[96,2758,2759],{"class":106},".gap-3.group[data-slot=",[96,2761,2715],{"class":2714},[96,2763,2764],{"class":106},"item",[96,2766,2715],{"class":2714},[96,2768,2769],{"class":106},"]:nth-child(1) > .mt-1",[96,2771,2772],{"class":2714},"\\\\",[96,2774,2775],{"class":106},".5.pb-6",[96,2777,2772],{"class":2714},[96,2779,2780],{"class":106},".5[data-slot=",[96,2782,2715],{"class":2714},[96,2784,2785],{"class":106},"wrapper",[96,2787,2715],{"class":2714},[96,2789,2790],{"class":106},"] > .text-dimmed.text-xs",[96,2792,2772],{"class":2714},[96,2794,2795],{"class":106},"\u002F5[data-slot=",[96,2797,2715],{"class":2714},[96,2799,2718],{"class":106},[96,2801,2715],{"class":2714},[96,2803,1062],{"class":106},[96,2805,2224],{"class":158},[96,2807,2809],{"class":98,"line":2808},52,[96,2810,2647],{"class":138},[96,2812,2814,2816,2819,2821,2823,2825,2828,2831,2834],{"class":98,"line":2813},53,[96,2815,2318],{"class":2065},[96,2817,2818],{"class":2321},"failureSummary",[96,2820,2072],{"class":2065},[96,2822,728],{"class":138},[96,2824,2077],{"class":158},[96,2826,2827],{"class":106},"Fix any of the following:",[96,2829,2830],{"class":2714},"\\n",[96,2832,2833],{"class":106},"  Element has insufficient color contrast of 2.63 (foreground color: #90a1b9, background color: #ffffff, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1",[96,2835,2224],{"class":158},[96,2837,2839],{"class":98,"line":2838},54,[96,2840,2841],{"class":138},"      }\n",[96,2843,2845],{"class":98,"line":2844},55,[96,2846,2847],{"class":138},"    ]\n",[96,2849,2851],{"class":98,"line":2850},56,[96,2852,2853],{"class":138},"  }\n",[96,2855,2857],{"class":98,"line":2856},57,[96,2858,2859],{"class":138},"]\n",[11,2861,2862,2863,27,2865,2867],{},"The ",[24,2864,2818],{},[24,2866,2363],{}," fields are what you hand to whoever fixes it: the element, the colors involved, and the ratio it needs to reach.",[41,2869,2871],{"id":2870},"accessibility-issues-the-phased-in-approach-caught-on-this-site","Accessibility Issues the Phased In Approach Caught On This Site",[11,2873,2874],{},"Once the code block noise was out of the way it was easy to see the other accessibility issues. Here's what I fixed, in order:",[2876,2877,2878,2887,2895,2901,2909],"ol",{},[52,2879,2880,2886],{},[55,2881,2882,2883,2885],{},"Links that relied on color alone (",[24,2884,409],{},")."," My external and affiliate link components used the brand color with no underline, at 2.35:1 against body text, and links distinguished only by color need at least 3:1. Regular markdown links passed because they're rendered in a heavier font weight, which axe accepts as a second cue. I gave the link components the same styling.",[52,2888,2889,2894],{},[55,2890,2891,2892,2885],{},"Empty table headers (",[24,2893,422],{}," Five comparison tables had a blank first header cell. Each got a label like \"Spec\" or \"Feature\".",[52,2896,2897,2900],{},[55,2898,2899],{},"A contrast failure I'd added that same day."," A new pros and cons box used green and red headings that failed contrast in light mode. The scan caught it within hours.",[52,2902,2903,2908],{},[55,2904,2905,2906,2885],{},"Duplicate landmarks (",[24,2907,928],{}," My home and About pages had two navigation landmarks both labeled \"Social media links\". The hero section's copy now has its own label.",[52,2910,2911,2918,2919,2921,2922,2925],{},[55,2912,2913,2914,2917],{},"Nested ",[24,2915,2916],{},"\u003Cmain>"," landmarks."," My site layout already provides the page's ",[24,2920,2916],{},", but four pages wrapped their content in another one. Changing the inner one to a ",[24,2923,2924],{},"\u003Cdiv>"," fixed three best-practice violations per page.",[11,2927,2928,2929,2931],{},"With those accessibility issues fixed, I added ",[24,2930,943],{}," to the rule tags. Two known contrast issues and the code block theme are still on the list, and each will come off as it's fixed.",[41,2933,2935],{"id":2934},"conclusion","Conclusion",[11,2937,2938,2939,2941,2942,2944],{},"You don't need a perfect accessibility score before automated checks are useful. Agree on where to start, use ",[24,2940,446],{}," sparingly, prefer narrow ",[24,2943,531],{}," selectors for known areas, keep a reviewable list of known issues, and let the build fail on anything new. Then tighten the rule set as the team fixes things.",[11,2946,2947,2948,244],{},"The automated scan is still only part of the job. Pair it with the manual checks from ",[18,2949,2950],{"href":37},"What axe and Lighthouse Miss",[2952,2953],"faq-section",{":items":2954},"[{\"label\":\"How do I disable an axe rule in Playwright?\",\"content\":\"Call disableRules() with the rule IDs before analyze(), for example new AxeBuilder({ page }).disableRules(['color-contrast']).analyze(). This turns the rule off for the whole page, so prefer exclude() when the problem is limited to specific elements.\"},{\"label\":\"How do I exclude an element from an axe scan in Playwright?\",\"content\":\"Call exclude() with a CSS selector, for example .exclude('pre') to skip code blocks or .exclude('iframe') to skip third-party embeds. axe skips the matched elements and everything inside them for all rules.\"},{\"label\":\"What is the difference between disableRules and exclude?\",\"content\":\"disableRules() turns specific rules off everywhere on the page. exclude() turns every rule off for specific elements. To tolerate one rule on specific elements while keeping every other check active, filter the matching violations after analyze() against a list of known issues.\"},{\"label\":\"How do I scan only part of a page with axe in Playwright?\",\"content\":\"Call include() with a CSS selector, for example new AxeBuilder({ page }).include('main').analyze(). Only the matched elements and their children are scanned. You can combine it with exclude(), such as include('main').exclude('pre'), to scan page content while skipping code blocks.\"},{\"label\":\"How do I run only specific axe rules in Playwright?\",\"content\":\"Use withRules() with a list of rule IDs to run only those rules, or withTags() to run every rule tagged with a standard such as wcag2a, wcag2aa, wcag21a, and wcag21aa. Note that WCAG tags do not include axe best-practice rules like landmark-unique or empty-table-header; add the best-practice tag to include them.\"},{\"label\":\"Should I fix every axe violation before adding accessibility tests?\",\"content\":\"No. For an existing product, agree on a starting rule set, record the violations you already know about as known issues, and fail the build only on new ones. Then fix known issues over time and tighten the rule set. For a site with very poor accessibility, start with a short list of rules using withRules().\"}]",[2956,2957],"read-next",{":items":2958},"[\"\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\",\"\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan\",\"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation\"]",[2960,2961,2962],"style",{},"html pre.shiki code .sA8fK,html code.shiki .sA8fK{--shiki-light:#E2931D;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .srGNg,html code.shiki .srGNg{--shiki-light:#91B859;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .spMcu,html code.shiki .spMcu{--shiki-light:#91B859;--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 .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 .sPJuK,html code.shiki .sPJuK{--shiki-light:#39ADB5;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}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 .sb1SK,html code.shiki .sb1SK{--shiki-light:#6182B8;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}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 .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 .stzDA,html code.shiki .stzDA{--shiki-light:#E53935;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}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 .sPxkN,html code.shiki .sPxkN{--shiki-light:#39ADB5;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .s6g51,html code.shiki .s6g51{--shiki-light:#F76D47;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sYyCj,html code.shiki .sYyCj{--shiki-light:#39ADB5;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .svTos,html code.shiki .svTos{--shiki-light:#9C3EDA;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .smdAf,html code.shiki .smdAf{--shiki-light:#E2931D;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .sDDhh,html code.shiki .sDDhh{--shiki-light:#F76D47;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .saWzx,html code.shiki .saWzx{--shiki-light:#E53935;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .sVYJ7,html code.shiki .sVYJ7{--shiki-light:#90A4AE;--shiki-default:#A0111F;--shiki-dark:#FF9492}",{"title":92,"searchDepth":171,"depth":171,"links":2964},[2965,2966,2968,2969,2971,2975,2979,2980,2981,2983,2984,2985],{"id":43,"depth":171,"text":44},{"id":78,"depth":171,"text":2967},"Install and Run @axe-core\u002Fplaywright",{"id":353,"depth":171,"text":354},{"id":442,"depth":171,"text":2970},"Disable an axe Rule with disableRules()",{"id":527,"depth":171,"text":2972,"children":2973},"Exclude or Include Elements with exclude() and include()",[2974],{"id":703,"depth":190,"text":704},{"id":717,"depth":171,"text":2976,"children":2977},"Run Only Specific Rules with withRules() and withTags()",[2978],{"id":915,"depth":190,"text":916},{"id":950,"depth":171,"text":951},{"id":1255,"depth":171,"text":1256},{"id":1666,"depth":171,"text":2982},"Run axe in Light and Dark Mode with colorScheme",{"id":1818,"depth":171,"text":1819},{"id":2870,"depth":171,"text":2871},{"id":2934,"depth":171,"text":2935},"\u002Fimages\u002Fposts\u002Fdisable-exclude-phase-in-axe-rules-playwright\u002Fdisable-exclude-phase-in-axe-rules-playwright-cover.webp","2026-10-03","How to gradually phase in Playwright accessibility testing on existing websites with known violations, using axe rules, exclusions, and real results.",false,"md",{},"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright",{"title":5,"description":2988},{"loc":2992},"software-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright","MIVDCxo7Nx4yO7YyJTkDqltcJhnelyZhasUzeK1b4wE",[2998,7690,8732,12486],{"id":2999,"title":38,"bmcUsername":6,"body":3000,"cover":7682,"date":7683,"description":7684,"draft":2989,"extension":2990,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":7685,"navigation":193,"npmPackage":6,"order":6,"path":37,"review":6,"seo":7686,"sitemap":7687,"stem":7688,"updated":6,"__hash__":7689},"content\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations.md",{"type":8,"value":3001,"toc":7652},[3002,3008,3011,3014,3018,3021,3090,3093,3096,3098,3102,3105,3108,3138,3141,3151,3157,3160,3162,3166,3170,3186,3189,3196,3208,3222,3225,3227,3231,3240,3247,3254,3259,3314,3321,3327,3329,3340,3348,3353,3364,3370,3475,3544,3555,3557,3561,3568,3578,3586,3597,3603,3605,3609,3612,3631,3637,3644,3751,3754,3759,3761,3765,3768,3771,3783,3795,3798,3808,3900,3902,3906,3909,3912,3937,3940,3947,3950,4023,4040,4048,4050,4054,4064,4067,4073,4145,4148,4157,4159,4163,4170,4181,4184,4233,4238,4240,4244,4255,4258,4261,4302,4307,4310,5257,5259,5263,5269,5275,5278,5282,5447,5451,5454,5626,5630,5633,5804,5808,5811,6103,6107,6110,6362,6366,6369,6692,6696,6699,6972,6976,6979,7222,7226,7229,7548,7550,7554,7560,7566,7569,7627,7630,7632,7634,7640,7643,7646,7649],[11,3003,3004,3005,3007],{},"Automated accessibility tools like axe, Lighthouse, and Playwright's ",[24,3006,26],{}," 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,3009,3010],{},"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.",[3012,3013],"hr",{},[41,3015,3017],{"id":3016},"automated-accessibility-testing-coverage-what-the-research-shows","Automated Accessibility Testing Coverage: What the Research Shows",[11,3019,3020],{},"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:",[364,3022,3023,3033],{},[367,3024,3025],{},[370,3026,3027,3030],{},[373,3028,3029],{},"Source",[373,3031,3032],{},"Finding",[383,3034,3035,3046,3057,3068,3079],{},[370,3036,3037,3043],{},[388,3038,3039],{},[391,3040],{"href":3041,"text":3042},"https:\u002F\u002Fwebaim.org\u002Fprojects\u002Fmillion\u002F","WebAIM",[388,3044,3045],{},"~30% of real WCAG failures are detectable by automation",[370,3047,3048,3054],{},[388,3049,3050],{},[391,3051],{"href":3052,"text":3053},"https:\u002F\u002Fwww.w3.org\u002FWAI\u002Ftest-evaluate\u002F","W3C \u002F WAI",[388,3055,3056],{},"~20–30% of WCAG Success Criteria are fully automatable",[370,3058,3059,3065],{},[388,3060,3061],{},[391,3062],{"href":3063,"text":3064},"https:\u002F\u002Fwww.section508.gov\u002Ftest\u002F","U.S. GSA – Section 508 Program",[388,3066,3067],{},"Some materials state 1 in 3 issues are caught by automated accessibility tests",[370,3069,3070,3076],{},[388,3071,3072],{},[391,3073],{"href":3074,"text":3075},"https:\u002F\u002Fwww.deque.com\u002Fautomated-accessibility-coverage-report\u002F","Deque (axe)",[388,3077,3078],{},"57.38% of known violations detected when axe-core was run against a large sample of real-world audited pages",[370,3080,3081,3087],{},[388,3082,3083],{},[391,3084],{"href":3085,"text":3086},"https:\u002F\u002Faccessible.org\u002Fautomated-scans-wcag\u002F","Accessible.org",[388,3088,3089],{},"13% fully automatable, 45% partially, 42% not automatable (WCAG 2.2 AA)",[11,3091,3092],{},"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,3094,3095],{},"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.",[3012,3097],{},[41,3099,3101],{"id":3100},"what-automated-accessibility-tools-reliably-detect","What Automated Accessibility Tools Reliably Detect",[11,3103,3104],{},"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,3106,3107],{},"Automated tools reliably catch mechanical rule violations:",[49,3109,3110,3117,3120,3123,3132,3135],{},[52,3111,3112,3113,3116],{},"Missing ",[24,3114,3115],{},"alt"," attributes on images",[52,3118,3119],{},"Missing form labels",[52,3121,3122],{},"Insufficient color contrast ratios on static text",[52,3124,3112,3125,3128,3129],{},[24,3126,3127],{},"lang"," attribute on ",[24,3130,3131],{},"\u003Chtml>",[52,3133,3134],{},"Missing document title",[52,3136,3137],{},"Invalid ARIA roles and attribute values",[11,3139,3140],{},"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,3142,3143,3146,3147,3150],{},[55,3144,3145],{},"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 ",[24,3148,3149],{},"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,3152,3153,3156],{},[55,3154,3155],{},"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,3158,3159],{},"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.",[3012,3161],{},[41,3163,3165],{"id":3164},"what-automated-accessibility-checks-cant-evaluate","What Automated Accessibility Checks Can't Evaluate",[701,3167,3169],{"id":3168},"_1-ambiguous-link-text","1. Ambiguous Link Text",[11,3171,3172,3173,618,3176,618,3179,622,3182,3185],{},"Links like ",[606,3174,3175],{},"Read more",[606,3177,3178],{},"Click here",[606,3180,3181],{},"Browse all",[606,3183,3184],{},"Learn more"," all pass automated checks without complaint. There's nothing structurally wrong with them — they're valid anchor elements with text content.",[11,3187,3188],{},"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,3190,3191,3192,3195],{},"Automated tools can't evaluate whether link text is ",[606,3193,3194],{},"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,3197,3198,3201,3202,3204,3205,244],{},[55,3199,3200],{},"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 ",[24,3203,3149],{}," to provide the full context: ",[24,3206,3207],{},"aria-label=\"Read more about Playwright accessibility testing\"",[11,3209,3210,3213,3214,3217,3218,3221],{},[55,3211,3212],{},"Fix (static analysis):"," For React projects, ",[24,3215,3216],{},"eslint-plugin-jsx-a11y"," includes a built-in ",[24,3219,3220],{},"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,3223,3224],{},"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.",[3012,3226],{},[701,3228,3230],{"id":3229},"_2-missing-skip-navigation-links","2. Missing Skip Navigation Links",[11,3232,3233,3234,27,3236,3239],{},"Landmark elements like ",[24,3235,2916],{},[24,3237,3238],{},"\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,3241,3242,3243,3246],{},"But keyboard-only users who ",[606,3244,3245],{},"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,3248,3249,3250,3253],{},"A skip link — a visually hidden anchor as the first focusable element that jumps to ",[24,3251,3252],{},"#main-content"," — serves both groups. Landmarks alone don't.",[11,3255,3256,3258],{},[55,3257,3200],{}," Add a skip link as the first focusable element on every page, even if your landmark structure is correct.",[86,3260,3263],{"className":3261,"code":3262,"language":2542,"meta":92,"style":92},"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\u002Fa>\n",[24,3264,3265,3300,3305],{"__ignoreMap":92},[96,3266,3267,3270,3272,3276,3279,3281,3283,3285,3288,3290,3292,3295,3297],{"class":98,"line":99},[96,3268,3269],{"class":138},"\u003C",[96,3271,18],{"class":2378},[96,3273,3275],{"class":3274},"sM74w"," href",[96,3277,3278],{"class":138},"=",[96,3280,2072],{"class":158},[96,3282,3252],{"class":106},[96,3284,2072],{"class":158},[96,3286,3287],{"class":3274}," class",[96,3289,3278],{"class":138},[96,3291,2072],{"class":158},[96,3293,3294],{"class":106},"sr-only focus:not-sr-only",[96,3296,2072],{"class":158},[96,3298,3299],{"class":138},">\n",[96,3301,3302],{"class":98,"line":171},[96,3303,3304],{"class":142},"  Skip to main content\n",[96,3306,3307,3310,3312],{"class":98,"line":190},[96,3308,3309],{"class":138},"\u003C\u002F",[96,3311,18],{"class":2378},[96,3313,3299],{"class":138},[11,3315,3316,3317,3320],{},"Below is a screenshot showing how it works on this site. When a keyboard-only user visits, the first focusable element is a ",[606,3318,3319],{},"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,3322,3323],{},[2025,3324],{"alt":3325,"src":3326},"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\u002Fposts\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\u002Fskip-link-example.webp",[3012,3328],{},[701,3330,3332,3333,3336,3337,3339],{"id":3331},"_3-aria-labelledby-vs-aria-label-both-pass-one-is-better","3. ",[24,3334,3335],{},"aria-labelledby"," vs ",[24,3338,3149],{}," — Both Pass, One Is Better",[11,3341,3342,3343,27,3345,3347],{},"Both ",[24,3344,3335],{},[24,3346,3149],{}," 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,3349,3350,3352],{},[24,3351,3149],{}," strings are invisible — they exist only in the accessibility tree. This creates two problems:",[49,3354,3355,3358],{},[52,3356,3357],{},"For sighted users who also use assistive technology, there can be a mismatch between what they see and what their screen reader announces",[52,3359,3360,3361,3363],{},"Translation tools frequently miss ",[24,3362,3149],{}," attributes, leaving non-English users with untranslated accessible names",[11,3365,3366,3367,3369],{},"When a visible heading or label already exists on the page, ",[24,3368,3335],{}," pointing to that element is almost always the better choice — it reuses the visible text so both representations stay in sync automatically.",[86,3371,3374],{"className":3261,"code":3372,"filename":3373,"language":2542,"meta":92,"style":92},"\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\u002Fh2>\n    \u003Cp>...\u003C\u002Fp>\n  \u003C\u002Fsection>\n\u003C\u002Fbody>\n","aria-labelledby-example.html",[24,3375,3376,3384,3389,3411,3441,3458,3467],{"__ignoreMap":92},[96,3377,3378,3380,3382],{"class":98,"line":99},[96,3379,3269],{"class":138},[96,3381,2577],{"class":2378},[96,3383,3299],{"class":138},[96,3385,3386],{"class":98,"line":171},[96,3387,3388],{"class":1200},"  \u003C!-- A visible header exists with text to label the section with, so aria-labelledby is correct here -->\n",[96,3390,3391,3394,3397,3400,3402,3404,3407,3409],{"class":98,"line":190},[96,3392,3393],{"class":138},"  \u003C",[96,3395,3396],{"class":2378},"section",[96,3398,3399],{"class":3274}," aria-labelledby",[96,3401,3278],{"class":138},[96,3403,2072],{"class":158},[96,3405,3406],{"class":106},"aria-header",[96,3408,2072],{"class":158},[96,3410,3299],{"class":138},[96,3412,3413,3416,3418,3421,3423,3425,3427,3429,3432,3435,3437,3439],{"class":98,"line":197},[96,3414,3415],{"class":138},"    \u003C",[96,3417,41],{"class":2378},[96,3419,3420],{"class":3274}," id",[96,3422,3278],{"class":138},[96,3424,2072],{"class":158},[96,3426,3406],{"class":106},[96,3428,2072],{"class":158},[96,3430,3431],{"class":138},">",[96,3433,3434],{"class":142},"Using aria attributes correctly",[96,3436,3309],{"class":138},[96,3438,41],{"class":2378},[96,3440,3299],{"class":138},[96,3442,3443,3445,3447,3449,3452,3454,3456],{"class":98,"line":236},[96,3444,3415],{"class":138},[96,3446,11],{"class":2378},[96,3448,3431],{"class":138},[96,3450,3451],{"class":142},"...",[96,3453,3309],{"class":138},[96,3455,11],{"class":2378},[96,3457,3299],{"class":138},[96,3459,3460,3463,3465],{"class":98,"line":265},[96,3461,3462],{"class":138},"  \u003C\u002F",[96,3464,3396],{"class":2378},[96,3466,3299],{"class":138},[96,3468,3469,3471,3473],{"class":98,"line":309},[96,3470,3309],{"class":138},[96,3472,2577],{"class":2378},[96,3474,3299],{"class":138},[86,3476,3479],{"className":3261,"code":3477,"filename":3478,"language":2542,"meta":92,"style":92},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n\u003Cbutton aria-label=\"Close dialog\">\n  \u003Csvg aria-hidden=\"true\">...\u003C\u002Fsvg>\n\u003C\u002Fbutton>\n","aria-label-example.html",[24,3480,3481,3486,3507,3536],{"__ignoreMap":92},[96,3482,3483],{"class":98,"line":99},[96,3484,3485],{"class":1200},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n",[96,3487,3488,3490,3493,3496,3498,3500,3503,3505],{"class":98,"line":171},[96,3489,3269],{"class":138},[96,3491,3492],{"class":2378},"button",[96,3494,3495],{"class":3274}," aria-label",[96,3497,3278],{"class":138},[96,3499,2072],{"class":158},[96,3501,3502],{"class":106},"Close dialog",[96,3504,2072],{"class":158},[96,3506,3299],{"class":138},[96,3508,3509,3511,3514,3517,3519,3521,3524,3526,3528,3530,3532,3534],{"class":98,"line":190},[96,3510,3393],{"class":138},[96,3512,3513],{"class":2378},"svg",[96,3515,3516],{"class":3274}," aria-hidden",[96,3518,3278],{"class":138},[96,3520,2072],{"class":158},[96,3522,3523],{"class":106},"true",[96,3525,2072],{"class":158},[96,3527,3431],{"class":138},[96,3529,3451],{"class":142},[96,3531,3309],{"class":138},[96,3533,3513],{"class":2378},[96,3535,3299],{"class":138},[96,3537,3538,3540,3542],{"class":98,"line":197},[96,3539,3309],{"class":138},[96,3541,3492],{"class":2378},[96,3543,3299],{"class":138},[11,3545,3546,3548,3549,3551,3552,3554],{},[55,3547,3200],{}," Prefer ",[24,3550,3335],{}," when a visible text element can serve as the label. Reserve ",[24,3553,3149],{}," for cases where no visible text exists.",[3012,3556],{},[701,3558,3560],{"id":3559},"_4-static-aria-labels-on-dynamic-controls","4. Static ARIA Labels on Dynamic Controls",[11,3562,3563,3564,3567],{},"A toggle button with ",[24,3565,3566],{},"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,3569,3570,3571,27,3574,3577],{},"Automated tools check ",[606,3572,3573],{},"presence",[606,3575,3576],{},"format",", not accuracy or correctness over time. A static label on a stateful control is invisible to a scanner.",[11,3579,3580,3582,3583,3585],{},[55,3581,3200],{}," Bind ",[24,3584,3149],{}," dynamically to reflect the current state:",[86,3587,3591],{"className":3588,"code":3589,"language":3590,"meta":92,"style":92},"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",[24,3592,3593],{"__ignoreMap":92},[96,3594,3595],{"class":98,"line":99},[96,3596,3589],{"class":142},[11,3598,3599,3600,3602],{},"Later in this article, I'll show a Playwright test that verifies a toggle's ",[24,3601,3149],{}," updates after interaction — catching stale labels before they ship.",[3012,3604],{},[701,3606,3608],{"id":3607},"_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,3610,3611],{},"axe-core validates presence and format. It does not validate quality.",[11,3613,3614,618,3617,622,3620,3623,3624,3627,3628,3630],{},[24,3615,3616],{},"aria-label=\"nav\"",[24,3618,3619],{},"aria-label=\"section 1\"",[24,3621,3622],{},"alt=\"image\""," all pass automated checks. So does ",[24,3625,3626],{},"role=\"button\""," on a ",[24,3629,2924],{}," 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,3632,3633,3634,3636],{},"The same trap applies to landmarks: adding ",[24,3635,3149],{}," 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,3638,3639,3640,3643],{},"The same principle extends to image alt text. ",[24,3641,3642],{},"alt=\"logo\""," on a company logo passes axe — but says nothing useful. The correct alt text depends on context that automation can't evaluate:",[86,3645,3647],{"className":3261,"code":3646,"language":2542,"meta":92,"style":92},"\u003C!-- Standalone logo — identify the brand -->\n\u003Cimg src=\"logo.svg\" alt=\"Acme Corp\" \u002F>\n\n\u003C!-- Logo as a link — describe the destination, not the image -->\n\u003Ca href=\"\u002F\">\n  \u003Cimg src=\"logo.svg\" alt=\"Acme Corp — Home\" \u002F>\n\u003C\u002Fa>\n",[24,3648,3649,3654,3687,3691,3696,3714,3743],{"__ignoreMap":92},[96,3650,3651],{"class":98,"line":99},[96,3652,3653],{"class":1200},"\u003C!-- Standalone logo — identify the brand -->\n",[96,3655,3656,3658,3660,3663,3665,3667,3670,3672,3675,3677,3679,3682,3684],{"class":98,"line":171},[96,3657,3269],{"class":138},[96,3659,2025],{"class":2378},[96,3661,3662],{"class":3274}," src",[96,3664,3278],{"class":138},[96,3666,2072],{"class":158},[96,3668,3669],{"class":106},"logo.svg",[96,3671,2072],{"class":158},[96,3673,3674],{"class":3274}," alt",[96,3676,3278],{"class":138},[96,3678,2072],{"class":158},[96,3680,3681],{"class":106},"Acme Corp",[96,3683,2072],{"class":158},[96,3685,3686],{"class":138}," \u002F>\n",[96,3688,3689],{"class":98,"line":190},[96,3690,194],{"emptyLinePlaceholder":193},[96,3692,3693],{"class":98,"line":197},[96,3694,3695],{"class":1200},"\u003C!-- Logo as a link — describe the destination, not the image -->\n",[96,3697,3698,3700,3702,3704,3706,3708,3710,3712],{"class":98,"line":236},[96,3699,3269],{"class":138},[96,3701,18],{"class":2378},[96,3703,3275],{"class":3274},[96,3705,3278],{"class":138},[96,3707,2072],{"class":158},[96,3709,255],{"class":106},[96,3711,2072],{"class":158},[96,3713,3299],{"class":138},[96,3715,3716,3718,3720,3722,3724,3726,3728,3730,3732,3734,3736,3739,3741],{"class":98,"line":265},[96,3717,3393],{"class":138},[96,3719,2025],{"class":2378},[96,3721,3662],{"class":3274},[96,3723,3278],{"class":138},[96,3725,2072],{"class":158},[96,3727,3669],{"class":106},[96,3729,2072],{"class":158},[96,3731,3674],{"class":3274},[96,3733,3278],{"class":138},[96,3735,2072],{"class":158},[96,3737,3738],{"class":106},"Acme Corp — Home",[96,3740,2072],{"class":158},[96,3742,3686],{"class":138},[96,3744,3745,3747,3749],{"class":98,"line":309},[96,3746,3309],{"class":138},[96,3748,18],{"class":2378},[96,3750,3299],{"class":138},[11,3752,3753],{},"Both pass axe equally. Only one communicates the right thing in each context.",[11,3755,3756,3758],{},[55,3757,3200],{}," 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.",[3012,3760],{},[701,3762,3764],{"id":3763},"_6-accessibility-regressions-automated-checks-wont-catch","6. Accessibility Regressions Automated Checks Won't Catch",[11,3766,3767],{},"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,3769,3770],{},"Two examples that come up regularly:",[11,3772,3773,3778,3779,3782],{},[55,3774,3775],{},[24,3776,3777],{},"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 ",[24,3780,3781],{},"alt=\"decorative image\"",". Now screen readers announce \"decorative image\" on every decorative element in the page — noise that wasn't there before.",[11,3784,3785,3790,3791,3794],{},[55,3786,3787],{},[24,3788,3789],{},"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 ",[24,3792,3793],{},"aria-label=\"icon\""," passes tooling, but pollutes the accessible name computation and potentially overrides the button's actual label.",[11,3796,3797],{},"In both cases, a passing automated check actively masks a regression. The original state was more accessible than the \"fixed\" state.",[11,3799,3800,3802,3803,27,3805,3807],{},[55,3801,3200],{}," Treat ",[24,3804,3777],{},[24,3806,3789],{}," as intentional signals, not oversights. Add a comment explaining the intent so your team doesn't correct them:",[86,3809,3811],{"className":3261,"code":3810,"language":2542,"meta":92,"style":92},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n\u003Cimg src=\"divider.svg\" alt=\"\" \u002F>\n\n\u003C!-- aria-hidden intentional: button label provided by visible text -->\n\u003Cbutton>\n  \u003Csvg aria-hidden=\"true\">...\u003C\u002Fsvg>\n  Submit\n\u003C\u002Fbutton>\n",[24,3812,3813,3818,3844,3848,3853,3861,3887,3892],{"__ignoreMap":92},[96,3814,3815],{"class":98,"line":99},[96,3816,3817],{"class":1200},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n",[96,3819,3820,3822,3824,3826,3828,3830,3833,3835,3837,3839,3842],{"class":98,"line":171},[96,3821,3269],{"class":138},[96,3823,2025],{"class":2378},[96,3825,3662],{"class":3274},[96,3827,3278],{"class":138},[96,3829,2072],{"class":158},[96,3831,3832],{"class":106},"divider.svg",[96,3834,2072],{"class":158},[96,3836,3674],{"class":3274},[96,3838,3278],{"class":138},[96,3840,3841],{"class":158},"\"\"",[96,3843,3686],{"class":138},[96,3845,3846],{"class":98,"line":190},[96,3847,194],{"emptyLinePlaceholder":193},[96,3849,3850],{"class":98,"line":197},[96,3851,3852],{"class":1200},"\u003C!-- aria-hidden intentional: button label provided by visible text -->\n",[96,3854,3855,3857,3859],{"class":98,"line":236},[96,3856,3269],{"class":138},[96,3858,3492],{"class":2378},[96,3860,3299],{"class":138},[96,3862,3863,3865,3867,3869,3871,3873,3875,3877,3879,3881,3883,3885],{"class":98,"line":265},[96,3864,3393],{"class":138},[96,3866,3513],{"class":2378},[96,3868,3516],{"class":3274},[96,3870,3278],{"class":138},[96,3872,2072],{"class":158},[96,3874,3523],{"class":106},[96,3876,2072],{"class":158},[96,3878,3431],{"class":138},[96,3880,3451],{"class":142},[96,3882,3309],{"class":138},[96,3884,3513],{"class":2378},[96,3886,3299],{"class":138},[96,3888,3889],{"class":98,"line":309},[96,3890,3891],{"class":142},"  Submit\n",[96,3893,3894,3896,3898],{"class":98,"line":337},[96,3895,3309],{"class":138},[96,3897,3492],{"class":2378},[96,3899,3299],{"class":138},[3012,3901],{},[701,3903,3905],{"id":3904},"_7-automated-wcag-color-contrast-testing-limitations","7. Automated WCAG Color Contrast Testing Limitations",[11,3907,3908],{},"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,3910,3911],{},"But gaps remain:",[49,3913,3914,3920,3926,3932],{},[52,3915,3916,3919],{},[55,3917,3918],{},"Text over images or gradients"," — axe-core may not be able to reliably report contrast issues when working with gradient or image backgrounds",[52,3921,3922,3925],{},[55,3923,3924],{},"Semi-transparent backgrounds"," — the computed color may not be accurate",[52,3927,3928,3931],{},[55,3929,3930],{},"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",[52,3933,3934,3936],{},[55,3935,381],{}," — a scan in light mode won't catch dark mode contrast failures, and vice versa",[11,3938,3939],{},"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,3941,3942,3943,3946],{},"For the contrast issues automated tools do catch, a dedicated checker gives you more control than waiting for a CI scan. The ",[18,3944,3945],{"href":1375},"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,3948,3949],{},"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:",[86,3951,3955],{"className":3952,"code":3953,"language":3954,"meta":92,"style":92},"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",[24,3956,3957,3967,4005,4019],{"__ignoreMap":92},[96,3958,3959,3961,3965],{"class":98,"line":99},[96,3960,244],{"class":1963},[96,3962,3964],{"class":3963},"slOWO","hero-text",[96,3966,233],{"class":138},[96,3968,3969,3973,3975,3979,3981,3984,3987,3989,3992,3995,3997,3999,4002],{"class":98,"line":171},[96,3970,3972],{"class":3971},"shxsR","  background",[96,3974,728],{"class":138},[96,3976,3978],{"class":3977},"slPND"," linear-gradient",[96,3980,204],{"class":138},[96,3982,3983],{"class":275},"to",[96,3985,3986],{"class":271}," bottom",[96,3988,146],{"class":138},[96,3990,3991],{"class":1963}," #",[96,3993,3994],{"class":271},"ffffff",[96,3996,146],{"class":138},[96,3998,3991],{"class":1963},[96,4000,4001],{"class":271},"000000",[96,4003,4004],{"class":138},");\n",[96,4006,4007,4010,4012,4014,4017],{"class":98,"line":190},[96,4008,4009],{"class":3971},"  color",[96,4011,728],{"class":138},[96,4013,3991],{"class":1963},[96,4015,4016],{"class":271},"544f4f",[96,4018,168],{"class":138},[96,4020,4021],{"class":98,"line":197},[96,4022,1815],{"class":138},[11,4024,4025,4026,4029,4030,4032,4033,4035,4036,4039],{},"Enter ",[24,4027,4028],{},"#544f4f"," as the foreground color and ",[24,4031,2409],{}," as the background in the ",[18,4034,3945],{"href":1375},", verify it passes, then repeat with ",[24,4037,4038],{},"#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,4041,4042],{},[2025,4043],{"alt":4044,"src":4045,"className":4046},"Screenshot of color contrast interface with #544f4f and #000000 combination showing a contrast violation","images\u002Fposts\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\u002Fwcag-contrast-color-checker-example.webp",[4047],"portrait",[3012,4049],{},[701,4051,4053],{"id":4052},"_8-when-lighthouse-landmark-recommendations-break-legacy-html","8. When Lighthouse Landmark Recommendations Break Legacy HTML",[11,4055,4056,4057,618,4059,618,4061,244],{},"Lighthouse flags missing landmark regions as accessibility violations. In a modern HTML5 codebase that recommendation is straightforward — add ",[24,4058,2916],{},[24,4060,3238],{},[24,4062,4063],{},"\u003Cheader>",[11,4065,4066],{},"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,4068,4069,4070,4072],{},"The correct fix in legacy markup contexts is ARIA landmark roles on ",[24,4071,2924],{}," elements:",[86,4074,4076],{"className":3261,"code":4075,"language":2542,"meta":92,"style":92},"\u003Cdiv role=\"main\">...\u003C\u002Fdiv>\n\u003Cdiv role=\"navigation\" aria-label=\"Main navigation\">...\u003C\u002Fdiv>\n",[24,4077,4078,4107],{"__ignoreMap":92},[96,4079,4080,4082,4085,4088,4090,4092,4095,4097,4099,4101,4103,4105],{"class":98,"line":99},[96,4081,3269],{"class":138},[96,4083,4084],{"class":2378},"div",[96,4086,4087],{"class":3274}," role",[96,4089,3278],{"class":138},[96,4091,2072],{"class":158},[96,4093,4094],{"class":106},"main",[96,4096,2072],{"class":158},[96,4098,3431],{"class":138},[96,4100,3451],{"class":142},[96,4102,3309],{"class":138},[96,4104,4084],{"class":2378},[96,4106,3299],{"class":138},[96,4108,4109,4111,4113,4115,4117,4119,4122,4124,4126,4128,4130,4133,4135,4137,4139,4141,4143],{"class":98,"line":171},[96,4110,3269],{"class":138},[96,4112,4084],{"class":2378},[96,4114,4087],{"class":3274},[96,4116,3278],{"class":138},[96,4118,2072],{"class":158},[96,4120,4121],{"class":106},"navigation",[96,4123,2072],{"class":158},[96,4125,3495],{"class":3274},[96,4127,3278],{"class":138},[96,4129,2072],{"class":158},[96,4131,4132],{"class":106},"Main navigation",[96,4134,2072],{"class":158},[96,4136,3431],{"class":138},[96,4138,3451],{"class":142},[96,4140,3309],{"class":138},[96,4142,4084],{"class":2378},[96,4144,3299],{"class":138},[11,4146,4147],{},"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,4149,4150,4152,4153,4156],{},[55,4151,3200],{}," 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 ",[24,4154,4155],{},"role"," attribute.",[3012,4158],{},[701,4160,4162],{"id":4161},"_9-form-accessibility-labels-instructions-and-error-messages-automated-tools-miss","9. Form Accessibility: Labels, Instructions, and Error Messages Automated Tools Miss",[11,4164,4165,4166,4169],{},"Automated tools reliably catch a missing ",[24,4167,4168],{},"\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,4171,4172,4173,4176,4177,4180],{},"A ",[24,4174,4175],{},"\u003Clabel>First name\u003C\u002Flabel>"," associated with a text input passes every automated check. So does ",[24,4178,4179],{},"\u003Clabel>Field 1\u003C\u002Flabel>",". The tool sees a label — it doesn't read it.",[11,4182,4183],{},"The same gap extends across the full form experience:",[49,4185,4186,4192,4203,4209,4223],{},[52,4187,4188,4191],{},[55,4189,4190],{},"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.",[52,4193,4194,4197,4198,4202],{},[55,4195,4196],{},"Vague error messages"," — \"Invalid input\" and \"Your email address must be in the format ",[18,4199,4201],{"href":4200},"mailto:name@example.com","name@example.com","\" both pass automated checks. Only one helps the user recover.",[52,4204,4205,4208],{},[55,4206,4207],{},"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.",[52,4210,4211,4214,4215,4218,4219,4222],{},[55,4212,4213],{},"Error announcement timing"," — whether an error is surfaced via ",[24,4216,4217],{},"aria-live"," or ",[24,4220,4221],{},"aria-describedby"," in a way screen readers actually pick up requires testing with real assistive technology, not a DOM scan.",[52,4224,4225,4228,4229,4232],{},[55,4226,4227],{},"Required field indicators"," — a red asterisk is a visual convention. Without a text equivalent or ",[24,4230,4231],{},"aria-required=\"true\"",", screen reader users have no indication a field is mandatory until a failed submission tells them so.",[11,4234,4235,4237],{},[55,4236,3200],{}," 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.",[3012,4239],{},[701,4241,4243],{"id":4242},"_10-keyboard-accessibility-dynamic-content-modals-and-interactive-components","10. Keyboard Accessibility: Dynamic Content, Modals, and Interactive Components",[11,4245,4246,4247,4250,4251,4254],{},"Automated tools can verify that a modal has ",[24,4248,4249],{},"role=\"dialog\""," and an accessible name, or that an accordion button has ",[24,4252,4253],{},"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,4256,4257],{},"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,4259,4260],{},"The most common failures in this area:",[49,4262,4263,4269,4275,4281,4287,4293],{},[52,4264,4265,4268],{},[55,4266,4267],{},"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",[52,4270,4271,4274],{},[55,4272,4273],{},"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",[52,4276,4277,4280],{},[55,4278,4279],{},"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",[52,4282,4283,4286],{},[55,4284,4285],{},"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",[52,4288,4289,4292],{},[55,4290,4291],{},"Expand\u002Fcollapse keyboard activation"," — an accordion that only responds to mouse clicks isn't keyboard accessible; Enter and Space should both activate toggle controls",[52,4294,4295,4298,4299,4301],{},[55,4296,4297],{},"Dynamically added content not announced"," — content that appears after an interaction (search results, inline validation, notifications) needs ",[24,4300,4217],{}," regions or explicit focus management to reach screen reader users",[11,4303,4304,4306],{},[55,4305,3200],{}," 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\u002Fcollapse controls with both Enter and Space and confirm the revealed content is reachable by Tab.",[11,4308,4309],{},"These keyboard paths can then be automated in Playwright:",[86,4311,4315],{"className":4312,"code":4313,"filename":126,"language":4314,"meta":92,"style":92},"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('\u002F');\n  await page.getByRole('button', { name: 'Open dialog' }).click();\n  const dialog = page.getByRole('dialog');\n  await expect(dialog).toBeVisible();\n\n  \u002F\u002F 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  \u002F\u002F 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('\u002F');\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\u002Fcollapse toggle is keyboard accessible', async ({ page }) => {\n  await page.goto('\u002F');\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","typescript",[24,4316,4317,4344,4366,4414,4442,4463,4467,4472,4512,4541,4566,4600,4606,4632,4636,4640,4645,4672,4697,4705,4709,4736,4758,4801,4815,4841,4863,4871,4875,4902,4924,4968,5011,5015,5054,5079,5083,5106,5142,5162,5166,5189,5225,5249],{"__ignoreMap":92},[96,4318,4319,4321,4323,4325,4328,4330,4332,4334,4336,4338,4340,4342],{"class":98,"line":99},[96,4320,201],{"class":200},[96,4322,204],{"class":142},[96,4324,165],{"class":158},[96,4326,4327],{"class":106},"modal traps focus and closes on Escape",[96,4329,165],{"class":158},[96,4331,146],{"class":138},[96,4333,217],{"class":216},[96,4335,220],{"class":138},[96,4337,224],{"class":223},[96,4339,227],{"class":138},[96,4341,230],{"class":216},[96,4343,233],{"class":138},[96,4345,4346,4348,4350,4352,4354,4356,4358,4360,4362,4364],{"class":98,"line":171},[96,4347,239],{"class":134},[96,4349,224],{"class":142},[96,4351,244],{"class":138},[96,4353,247],{"class":200},[96,4355,204],{"class":250},[96,4357,165],{"class":158},[96,4359,255],{"class":106},[96,4361,165],{"class":158},[96,4363,260],{"class":250},[96,4365,168],{"class":138},[96,4367,4368,4370,4372,4374,4377,4379,4381,4383,4385,4387,4389,4392,4394,4396,4399,4401,4403,4405,4407,4410,4412],{"class":98,"line":190},[96,4369,239],{"class":134},[96,4371,224],{"class":142},[96,4373,244],{"class":138},[96,4375,4376],{"class":200},"getByRole",[96,4378,204],{"class":250},[96,4380,165],{"class":158},[96,4382,3492],{"class":106},[96,4384,165],{"class":158},[96,4386,146],{"class":138},[96,4388,139],{"class":138},[96,4390,4391],{"class":250}," name",[96,4393,728],{"class":138},[96,4395,159],{"class":158},[96,4397,4398],{"class":106},"Open dialog",[96,4400,165],{"class":158},[96,4402,152],{"class":138},[96,4404,260],{"class":250},[96,4406,244],{"class":138},[96,4408,4409],{"class":200},"click",[96,4411,304],{"class":250},[96,4413,168],{"class":138},[96,4415,4416,4418,4421,4423,4425,4427,4429,4431,4433,4436,4438,4440],{"class":98,"line":197},[96,4417,268],{"class":216},[96,4419,4420],{"class":271}," dialog",[96,4422,276],{"class":275},[96,4424,224],{"class":142},[96,4426,244],{"class":138},[96,4428,4376],{"class":200},[96,4430,204],{"class":250},[96,4432,165],{"class":158},[96,4434,4435],{"class":106},"dialog",[96,4437,165],{"class":158},[96,4439,260],{"class":250},[96,4441,168],{"class":138},[96,4443,4444,4446,4448,4450,4452,4454,4456,4459,4461],{"class":98,"line":236},[96,4445,239],{"class":134},[96,4447,149],{"class":200},[96,4449,204],{"class":250},[96,4451,4435],{"class":142},[96,4453,260],{"class":250},[96,4455,244],{"class":138},[96,4457,4458],{"class":200},"toBeVisible",[96,4460,304],{"class":250},[96,4462,168],{"class":138},[96,4464,4465],{"class":98,"line":265},[96,4466,194],{"emptyLinePlaceholder":193},[96,4468,4469],{"class":98,"line":309},[96,4470,4471],{"class":1200},"  \u002F\u002F Tab multiple times — focus should remain within the dialog\n",[96,4473,4474,4477,4479,4482,4485,4487,4490,4493,4495,4498,4501,4503,4505,4508,4510],{"class":98,"line":337},[96,4475,4476],{"class":134},"  for",[96,4478,1530],{"class":250},[96,4480,4481],{"class":216},"let",[96,4483,4484],{"class":142}," i",[96,4486,276],{"class":275},[96,4488,4489],{"class":1969}," 0",[96,4491,4492],{"class":138},";",[96,4494,4484],{"class":142},[96,4496,4497],{"class":275}," \u003C",[96,4499,4500],{"class":1969}," 10",[96,4502,4492],{"class":138},[96,4504,4484],{"class":142},[96,4506,4507],{"class":275},"++",[96,4509,1734],{"class":250},[96,4511,1107],{"class":138},[96,4513,4514,4516,4518,4520,4523,4525,4528,4530,4532,4535,4537,4539],{"class":98,"line":1149},[96,4515,1137],{"class":134},[96,4517,224],{"class":142},[96,4519,244],{"class":138},[96,4521,4522],{"class":142},"keyboard",[96,4524,244],{"class":138},[96,4526,4527],{"class":200},"press",[96,4529,204],{"class":250},[96,4531,165],{"class":158},[96,4533,4534],{"class":106},"Tab",[96,4536,165],{"class":158},[96,4538,260],{"class":250},[96,4540,168],{"class":138},[96,4542,4543,4546,4549,4551,4553,4555,4557,4560,4562,4564],{"class":98,"line":1167},[96,4544,4545],{"class":216},"    const",[96,4547,4548],{"class":271}," isInsideDialog",[96,4550,276],{"class":275},[96,4552,279],{"class":134},[96,4554,224],{"class":142},[96,4556,244],{"class":138},[96,4558,4559],{"class":200},"evaluate",[96,4561,204],{"class":250},[96,4563,304],{"class":138},[96,4565,1146],{"class":216},[96,4567,4568,4571,4573,4576,4579,4582,4584,4586,4589,4591,4593,4596,4598],{"class":98,"line":1182},[96,4569,4570],{"class":142},"      document",[96,4572,244],{"class":138},[96,4574,4575],{"class":142},"activeElement",[96,4577,4578],{"class":138},"?.",[96,4580,4581],{"class":200},"closest",[96,4583,204],{"class":250},[96,4585,165],{"class":158},[96,4587,4588],{"class":106},"[role=\"dialog\"]",[96,4590,165],{"class":158},[96,4592,1734],{"class":250},[96,4594,4595],{"class":275},"!==",[96,4597,1964],{"class":1963},[96,4599,1309],{"class":138},[96,4601,4602,4604],{"class":98,"line":1204},[96,4603,1229],{"class":250},[96,4605,168],{"class":138},[96,4607,4608,4611,4613,4616,4618,4620,4623,4625,4628,4630],{"class":98,"line":1226},[96,4609,4610],{"class":200},"    expect",[96,4612,204],{"class":250},[96,4614,4615],{"class":142},"isInsideDialog",[96,4617,260],{"class":250},[96,4619,244],{"class":138},[96,4621,4622],{"class":200},"toBe",[96,4624,204],{"class":250},[96,4626,3523],{"class":4627},"sTqCK",[96,4629,260],{"class":250},[96,4631,168],{"class":138},[96,4633,4634],{"class":98,"line":1234},[96,4635,2853],{"class":138},[96,4637,4638],{"class":98,"line":1240},[96,4639,194],{"emptyLinePlaceholder":193},[96,4641,4642],{"class":98,"line":2227},[96,4643,4644],{"class":1200},"  \u002F\u002F Escape should close the modal and it should no longer be visible\n",[96,4646,4647,4649,4651,4653,4655,4657,4659,4661,4663,4666,4668,4670],{"class":98,"line":2233},[96,4648,239],{"class":134},[96,4650,224],{"class":142},[96,4652,244],{"class":138},[96,4654,4522],{"class":142},[96,4656,244],{"class":138},[96,4658,4527],{"class":200},[96,4660,204],{"class":250},[96,4662,165],{"class":158},[96,4664,4665],{"class":106},"Escape",[96,4667,165],{"class":158},[96,4669,260],{"class":250},[96,4671,168],{"class":138},[96,4673,4674,4676,4678,4680,4682,4684,4686,4689,4691,4693,4695],{"class":98,"line":2254},[96,4675,239],{"class":134},[96,4677,149],{"class":200},[96,4679,204],{"class":250},[96,4681,4435],{"class":142},[96,4683,260],{"class":250},[96,4685,244],{"class":138},[96,4687,4688],{"class":142},"not",[96,4690,244],{"class":138},[96,4692,4458],{"class":200},[96,4694,304],{"class":250},[96,4696,168],{"class":138},[96,4698,4699,4701,4703],{"class":98,"line":2275},[96,4700,340],{"class":138},[96,4702,260],{"class":142},[96,4704,168],{"class":138},[96,4706,4707],{"class":98,"line":2296},[96,4708,194],{"emptyLinePlaceholder":193},[96,4710,4711,4713,4715,4717,4720,4722,4724,4726,4728,4730,4732,4734],{"class":98,"line":2309},[96,4712,201],{"class":200},[96,4714,204],{"class":142},[96,4716,165],{"class":158},[96,4718,4719],{"class":106},"modal returns focus to trigger on close",[96,4721,165],{"class":158},[96,4723,146],{"class":138},[96,4725,217],{"class":216},[96,4727,220],{"class":138},[96,4729,224],{"class":223},[96,4731,227],{"class":138},[96,4733,230],{"class":216},[96,4735,233],{"class":138},[96,4737,4738,4740,4742,4744,4746,4748,4750,4752,4754,4756],{"class":98,"line":2315},[96,4739,239],{"class":134},[96,4741,224],{"class":142},[96,4743,244],{"class":138},[96,4745,247],{"class":200},[96,4747,204],{"class":250},[96,4749,165],{"class":158},[96,4751,255],{"class":106},[96,4753,165],{"class":158},[96,4755,260],{"class":250},[96,4757,168],{"class":138},[96,4759,4760,4762,4765,4767,4769,4771,4773,4775,4777,4779,4781,4783,4785,4787,4789,4791,4793,4795,4797,4799],{"class":98,"line":2331},[96,4761,268],{"class":216},[96,4763,4764],{"class":271}," trigger",[96,4766,276],{"class":275},[96,4768,224],{"class":142},[96,4770,244],{"class":138},[96,4772,4376],{"class":200},[96,4774,204],{"class":250},[96,4776,165],{"class":158},[96,4778,3492],{"class":106},[96,4780,165],{"class":158},[96,4782,146],{"class":138},[96,4784,139],{"class":138},[96,4786,4391],{"class":250},[96,4788,728],{"class":138},[96,4790,159],{"class":158},[96,4792,4398],{"class":106},[96,4794,165],{"class":158},[96,4796,152],{"class":138},[96,4798,260],{"class":250},[96,4800,168],{"class":138},[96,4802,4803,4805,4807,4809,4811,4813],{"class":98,"line":2337},[96,4804,239],{"class":134},[96,4806,4764],{"class":142},[96,4808,244],{"class":138},[96,4810,4409],{"class":200},[96,4812,304],{"class":250},[96,4814,168],{"class":138},[96,4816,4817,4819,4821,4823,4825,4827,4829,4831,4833,4835,4837,4839],{"class":98,"line":2358},[96,4818,239],{"class":134},[96,4820,224],{"class":142},[96,4822,244],{"class":138},[96,4824,4522],{"class":142},[96,4826,244],{"class":138},[96,4828,4527],{"class":200},[96,4830,204],{"class":250},[96,4832,165],{"class":158},[96,4834,4665],{"class":106},[96,4836,165],{"class":158},[96,4838,260],{"class":250},[96,4840,168],{"class":138},[96,4842,4843,4845,4847,4849,4852,4854,4856,4859,4861],{"class":98,"line":2372},[96,4844,239],{"class":134},[96,4846,149],{"class":200},[96,4848,204],{"class":250},[96,4850,4851],{"class":142},"trigger",[96,4853,260],{"class":250},[96,4855,244],{"class":138},[96,4857,4858],{"class":200},"toBeFocused",[96,4860,304],{"class":250},[96,4862,168],{"class":138},[96,4864,4865,4867,4869],{"class":98,"line":2395},[96,4866,340],{"class":138},[96,4868,260],{"class":142},[96,4870,168],{"class":138},[96,4872,4873],{"class":98,"line":2416},[96,4874,194],{"emptyLinePlaceholder":193},[96,4876,4877,4879,4881,4883,4886,4888,4890,4892,4894,4896,4898,4900],{"class":98,"line":2433},[96,4878,201],{"class":200},[96,4880,204],{"class":142},[96,4882,165],{"class":158},[96,4884,4885],{"class":106},"expand\u002Fcollapse toggle is keyboard accessible",[96,4887,165],{"class":158},[96,4889,146],{"class":138},[96,4891,217],{"class":216},[96,4893,220],{"class":138},[96,4895,224],{"class":223},[96,4897,227],{"class":138},[96,4899,230],{"class":216},[96,4901,233],{"class":138},[96,4903,4904,4906,4908,4910,4912,4914,4916,4918,4920,4922],{"class":98,"line":2454},[96,4905,239],{"class":134},[96,4907,224],{"class":142},[96,4909,244],{"class":138},[96,4911,247],{"class":200},[96,4913,204],{"class":250},[96,4915,165],{"class":158},[96,4917,255],{"class":106},[96,4919,165],{"class":158},[96,4921,260],{"class":250},[96,4923,168],{"class":138},[96,4925,4926,4928,4931,4933,4935,4937,4939,4941,4943,4945,4947,4949,4951,4953,4955,4957,4960,4962,4964,4966],{"class":98,"line":2475},[96,4927,268],{"class":216},[96,4929,4930],{"class":271}," toggle",[96,4932,276],{"class":275},[96,4934,224],{"class":142},[96,4936,244],{"class":138},[96,4938,4376],{"class":200},[96,4940,204],{"class":250},[96,4942,165],{"class":158},[96,4944,3492],{"class":106},[96,4946,165],{"class":158},[96,4948,146],{"class":138},[96,4950,139],{"class":138},[96,4952,4391],{"class":250},[96,4954,728],{"class":138},[96,4956,159],{"class":158},[96,4958,4959],{"class":106},"Show details",[96,4961,165],{"class":158},[96,4963,152],{"class":138},[96,4965,260],{"class":250},[96,4967,168],{"class":138},[96,4969,4970,4972,4975,4977,4979,4981,4983,4985,4987,4989,4991,4993,4995,4997,4999,5001,5003,5005,5007,5009],{"class":98,"line":2491},[96,4971,268],{"class":216},[96,4973,4974],{"class":271}," content",[96,4976,276],{"class":275},[96,4978,224],{"class":142},[96,4980,244],{"class":138},[96,4982,4376],{"class":200},[96,4984,204],{"class":250},[96,4986,165],{"class":158},[96,4988,939],{"class":106},[96,4990,165],{"class":158},[96,4992,146],{"class":138},[96,4994,139],{"class":138},[96,4996,4391],{"class":250},[96,4998,728],{"class":138},[96,5000,159],{"class":158},[96,5002,4959],{"class":106},[96,5004,165],{"class":158},[96,5006,152],{"class":138},[96,5008,260],{"class":250},[96,5010,168],{"class":138},[96,5012,5013],{"class":98,"line":2510},[96,5014,194],{"emptyLinePlaceholder":193},[96,5016,5017,5019,5021,5023,5026,5028,5030,5033,5035,5037,5039,5041,5043,5045,5048,5050,5052],{"class":98,"line":2516},[96,5018,239],{"class":134},[96,5020,149],{"class":200},[96,5022,204],{"class":250},[96,5024,5025],{"class":142},"toggle",[96,5027,260],{"class":250},[96,5029,244],{"class":138},[96,5031,5032],{"class":200},"toHaveAttribute",[96,5034,204],{"class":250},[96,5036,165],{"class":158},[96,5038,4253],{"class":106},[96,5040,165],{"class":158},[96,5042,146],{"class":138},[96,5044,159],{"class":158},[96,5046,5047],{"class":106},"false",[96,5049,165],{"class":158},[96,5051,260],{"class":250},[96,5053,168],{"class":138},[96,5055,5056,5058,5060,5062,5065,5067,5069,5071,5073,5075,5077],{"class":98,"line":2530},[96,5057,239],{"class":134},[96,5059,149],{"class":200},[96,5061,204],{"class":250},[96,5063,5064],{"class":142},"content",[96,5066,260],{"class":250},[96,5068,244],{"class":138},[96,5070,4688],{"class":142},[96,5072,244],{"class":138},[96,5074,4458],{"class":200},[96,5076,304],{"class":250},[96,5078,168],{"class":138},[96,5080,5081],{"class":98,"line":2536},[96,5082,194],{"emptyLinePlaceholder":193},[96,5084,5085,5087,5089,5091,5093,5095,5097,5100,5102,5104],{"class":98,"line":2558},[96,5086,239],{"class":134},[96,5088,4930],{"class":142},[96,5090,244],{"class":138},[96,5092,4527],{"class":200},[96,5094,204],{"class":250},[96,5096,165],{"class":158},[96,5098,5099],{"class":106},"Enter",[96,5101,165],{"class":158},[96,5103,260],{"class":250},[96,5105,168],{"class":138},[96,5107,5108,5110,5112,5114,5116,5118,5120,5122,5124,5126,5128,5130,5132,5134,5136,5138,5140],{"class":98,"line":2571},[96,5109,239],{"class":134},[96,5111,149],{"class":200},[96,5113,204],{"class":250},[96,5115,5025],{"class":142},[96,5117,260],{"class":250},[96,5119,244],{"class":138},[96,5121,5032],{"class":200},[96,5123,204],{"class":250},[96,5125,165],{"class":158},[96,5127,4253],{"class":106},[96,5129,165],{"class":158},[96,5131,146],{"class":138},[96,5133,159],{"class":158},[96,5135,3523],{"class":106},[96,5137,165],{"class":158},[96,5139,260],{"class":250},[96,5141,168],{"class":138},[96,5143,5144,5146,5148,5150,5152,5154,5156,5158,5160],{"class":98,"line":2582},[96,5145,239],{"class":134},[96,5147,149],{"class":200},[96,5149,204],{"class":250},[96,5151,5064],{"class":142},[96,5153,260],{"class":250},[96,5155,244],{"class":138},[96,5157,4458],{"class":200},[96,5159,304],{"class":250},[96,5161,168],{"class":138},[96,5163,5164],{"class":98,"line":2588},[96,5165,194],{"emptyLinePlaceholder":193},[96,5167,5168,5170,5172,5174,5176,5178,5180,5183,5185,5187],{"class":98,"line":2594},[96,5169,239],{"class":134},[96,5171,4930],{"class":142},[96,5173,244],{"class":138},[96,5175,4527],{"class":200},[96,5177,204],{"class":250},[96,5179,165],{"class":158},[96,5181,5182],{"class":106},"Space",[96,5184,165],{"class":158},[96,5186,260],{"class":250},[96,5188,168],{"class":138},[96,5190,5191,5193,5195,5197,5199,5201,5203,5205,5207,5209,5211,5213,5215,5217,5219,5221,5223],{"class":98,"line":2600},[96,5192,239],{"class":134},[96,5194,149],{"class":200},[96,5196,204],{"class":250},[96,5198,5025],{"class":142},[96,5200,260],{"class":250},[96,5202,244],{"class":138},[96,5204,5032],{"class":200},[96,5206,204],{"class":250},[96,5208,165],{"class":158},[96,5210,4253],{"class":106},[96,5212,165],{"class":158},[96,5214,146],{"class":138},[96,5216,159],{"class":158},[96,5218,5047],{"class":106},[96,5220,165],{"class":158},[96,5222,260],{"class":250},[96,5224,168],{"class":138},[96,5226,5227,5229,5231,5233,5235,5237,5239,5241,5243,5245,5247],{"class":98,"line":2619},[96,5228,239],{"class":134},[96,5230,149],{"class":200},[96,5232,204],{"class":250},[96,5234,5064],{"class":142},[96,5236,260],{"class":250},[96,5238,244],{"class":138},[96,5240,4688],{"class":142},[96,5242,244],{"class":138},[96,5244,4458],{"class":200},[96,5246,304],{"class":250},[96,5248,168],{"class":138},[96,5250,5251,5253,5255],{"class":98,"line":2638},[96,5252,340],{"class":138},[96,5254,260],{"class":142},[96,5256,168],{"class":138},[3012,5258],{},[41,5260,5262],{"id":5261},"writing-smarter-playwright-tests-that-go-beyond-a-lighthouse-report","Writing Smarter Playwright Tests That Go Beyond a Lighthouse Report",[11,5264,5265,5266,5268],{},"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 ",[24,5267,30],{}," scan is the starting point, not the finish line.",[11,5270,5271,5272,5274],{},"If you're adding these checks to an existing site that already has violations, see ",[18,5273,5],{"href":2992}," for scoping the scan so it doesn't fail on everything at once.",[11,5276,5277],{},"These test patterns cover defects a default scan misses.",[701,5279,5281],{"id":5280},"default-axe-scan-your-baseline","Default axe scan (your baseline)",[86,5283,5285],{"className":4312,"code":5284,"filename":126,"language":4314,"meta":92,"style":92},"import { test, expect } from '@playwright\u002Ftest';\nimport AxeBuilder from '@axe-core\u002Fplaywright';\n\ntest('accessibility scan', async ({ page }) => {\n  await page.goto('\u002F');\n  const results = await new AxeBuilder({ page }).analyze();\n  \u002F\u002F Ensure the default page state has no violations\n  expect(results.violations).toEqual([]);\n});\n",[24,5286,5287,5311,5327,5331,5358,5380,5412,5417,5439],{"__ignoreMap":92},[96,5288,5289,5291,5293,5295,5297,5299,5301,5303,5305,5307,5309],{"class":98,"line":99},[96,5290,135],{"class":134},[96,5292,139],{"class":138},[96,5294,143],{"class":142},[96,5296,146],{"class":138},[96,5298,149],{"class":142},[96,5300,152],{"class":138},[96,5302,155],{"class":134},[96,5304,159],{"class":158},[96,5306,162],{"class":106},[96,5308,165],{"class":158},[96,5310,168],{"class":138},[96,5312,5313,5315,5317,5319,5321,5323,5325],{"class":98,"line":171},[96,5314,135],{"class":134},[96,5316,176],{"class":142},[96,5318,179],{"class":134},[96,5320,159],{"class":158},[96,5322,26],{"class":106},[96,5324,165],{"class":158},[96,5326,168],{"class":138},[96,5328,5329],{"class":98,"line":190},[96,5330,194],{"emptyLinePlaceholder":193},[96,5332,5333,5335,5337,5339,5342,5344,5346,5348,5350,5352,5354,5356],{"class":98,"line":197},[96,5334,201],{"class":200},[96,5336,204],{"class":142},[96,5338,165],{"class":158},[96,5340,5341],{"class":106},"accessibility scan",[96,5343,165],{"class":158},[96,5345,146],{"class":138},[96,5347,217],{"class":216},[96,5349,220],{"class":138},[96,5351,224],{"class":223},[96,5353,227],{"class":138},[96,5355,230],{"class":216},[96,5357,233],{"class":138},[96,5359,5360,5362,5364,5366,5368,5370,5372,5374,5376,5378],{"class":98,"line":236},[96,5361,239],{"class":134},[96,5363,224],{"class":142},[96,5365,244],{"class":138},[96,5367,247],{"class":200},[96,5369,204],{"class":250},[96,5371,165],{"class":158},[96,5373,255],{"class":106},[96,5375,165],{"class":158},[96,5377,260],{"class":250},[96,5379,168],{"class":138},[96,5381,5382,5384,5386,5388,5390,5392,5394,5396,5398,5400,5402,5404,5406,5408,5410],{"class":98,"line":265},[96,5383,268],{"class":216},[96,5385,272],{"class":271},[96,5387,276],{"class":275},[96,5389,279],{"class":134},[96,5391,282],{"class":275},[96,5393,285],{"class":200},[96,5395,204],{"class":250},[96,5397,290],{"class":138},[96,5399,224],{"class":142},[96,5401,152],{"class":138},[96,5403,260],{"class":250},[96,5405,244],{"class":138},[96,5407,301],{"class":200},[96,5409,304],{"class":250},[96,5411,168],{"class":138},[96,5413,5414],{"class":98,"line":309},[96,5415,5416],{"class":1200},"  \u002F\u002F Ensure the default page state has no violations\n",[96,5418,5419,5421,5423,5425,5427,5429,5431,5433,5435,5437],{"class":98,"line":337},[96,5420,312],{"class":200},[96,5422,204],{"class":250},[96,5424,317],{"class":142},[96,5426,244],{"class":138},[96,5428,322],{"class":142},[96,5430,260],{"class":250},[96,5432,244],{"class":138},[96,5434,329],{"class":200},[96,5436,332],{"class":250},[96,5438,168],{"class":138},[96,5440,5441,5443,5445],{"class":98,"line":1149},[96,5442,340],{"class":138},[96,5444,260],{"class":142},[96,5446,168],{"class":138},[701,5448,5450],{"id":5449},"hover-state-contrast","Hover state contrast",[11,5452,5453],{},"A standard scan misses contrast issues that only appear on hover. Trigger the state first, then scan.",[86,5455,5457],{"className":4312,"code":5456,"filename":126,"language":4314,"meta":92,"style":92},"test('accessibility scan on hovered button', async ({ page }) => {\n  await page.goto('\u002F');\n  \u002F\u002F Trigger hover state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).hover();\n  const results = await new AxeBuilder({ page }).analyze();\n  \u002F\u002F Verify with the button hovered we still have no violations present\n  expect(results.violations).toEqual([]);\n});\n",[24,5458,5459,5486,5508,5513,5559,5591,5596,5618],{"__ignoreMap":92},[96,5460,5461,5463,5465,5467,5470,5472,5474,5476,5478,5480,5482,5484],{"class":98,"line":99},[96,5462,201],{"class":200},[96,5464,204],{"class":142},[96,5466,165],{"class":158},[96,5468,5469],{"class":106},"accessibility scan on hovered button",[96,5471,165],{"class":158},[96,5473,146],{"class":138},[96,5475,217],{"class":216},[96,5477,220],{"class":138},[96,5479,224],{"class":223},[96,5481,227],{"class":138},[96,5483,230],{"class":216},[96,5485,233],{"class":138},[96,5487,5488,5490,5492,5494,5496,5498,5500,5502,5504,5506],{"class":98,"line":171},[96,5489,239],{"class":134},[96,5491,224],{"class":142},[96,5493,244],{"class":138},[96,5495,247],{"class":200},[96,5497,204],{"class":250},[96,5499,165],{"class":158},[96,5501,255],{"class":106},[96,5503,165],{"class":158},[96,5505,260],{"class":250},[96,5507,168],{"class":138},[96,5509,5510],{"class":98,"line":190},[96,5511,5512],{"class":1200},"  \u002F\u002F Trigger hover state on the button\n",[96,5514,5515,5517,5519,5521,5523,5525,5527,5529,5531,5533,5535,5537,5539,5541,5544,5546,5548,5550,5552,5555,5557],{"class":98,"line":197},[96,5516,239],{"class":134},[96,5518,224],{"class":142},[96,5520,244],{"class":138},[96,5522,4376],{"class":200},[96,5524,204],{"class":250},[96,5526,165],{"class":158},[96,5528,3492],{"class":106},[96,5530,165],{"class":158},[96,5532,146],{"class":138},[96,5534,139],{"class":138},[96,5536,4391],{"class":250},[96,5538,728],{"class":138},[96,5540,159],{"class":158},[96,5542,5543],{"class":106},"Subscribe",[96,5545,165],{"class":158},[96,5547,152],{"class":138},[96,5549,260],{"class":250},[96,5551,244],{"class":138},[96,5553,5554],{"class":200},"hover",[96,5556,304],{"class":250},[96,5558,168],{"class":138},[96,5560,5561,5563,5565,5567,5569,5571,5573,5575,5577,5579,5581,5583,5585,5587,5589],{"class":98,"line":236},[96,5562,268],{"class":216},[96,5564,272],{"class":271},[96,5566,276],{"class":275},[96,5568,279],{"class":134},[96,5570,282],{"class":275},[96,5572,285],{"class":200},[96,5574,204],{"class":250},[96,5576,290],{"class":138},[96,5578,224],{"class":142},[96,5580,152],{"class":138},[96,5582,260],{"class":250},[96,5584,244],{"class":138},[96,5586,301],{"class":200},[96,5588,304],{"class":250},[96,5590,168],{"class":138},[96,5592,5593],{"class":98,"line":265},[96,5594,5595],{"class":1200},"  \u002F\u002F Verify with the button hovered we still have no violations present\n",[96,5597,5598,5600,5602,5604,5606,5608,5610,5612,5614,5616],{"class":98,"line":309},[96,5599,312],{"class":200},[96,5601,204],{"class":250},[96,5603,317],{"class":142},[96,5605,244],{"class":138},[96,5607,322],{"class":142},[96,5609,260],{"class":250},[96,5611,244],{"class":138},[96,5613,329],{"class":200},[96,5615,332],{"class":250},[96,5617,168],{"class":138},[96,5619,5620,5622,5624],{"class":98,"line":337},[96,5621,340],{"class":138},[96,5623,260],{"class":142},[96,5625,168],{"class":138},[701,5627,5629],{"id":5628},"focus-state-contrast","Focus state contrast",[11,5631,5632],{},"Same principle — trigger focus before scanning so axe evaluates the focused styles.",[86,5634,5636],{"className":4312,"code":5635,"filename":126,"language":4314,"meta":92,"style":92},"test('accessibility scan on focused button', async ({ page }) => {\n  await page.goto('\u002F');\n  \u002F\u002F Trigger focus state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).focus();\n  const results = await new AxeBuilder({ page }).analyze();\n  \u002F\u002F Except no violations\n  expect(results.violations).toEqual([]);\n});\n",[24,5637,5638,5665,5687,5692,5737,5769,5774,5796],{"__ignoreMap":92},[96,5639,5640,5642,5644,5646,5649,5651,5653,5655,5657,5659,5661,5663],{"class":98,"line":99},[96,5641,201],{"class":200},[96,5643,204],{"class":142},[96,5645,165],{"class":158},[96,5647,5648],{"class":106},"accessibility scan on focused button",[96,5650,165],{"class":158},[96,5652,146],{"class":138},[96,5654,217],{"class":216},[96,5656,220],{"class":138},[96,5658,224],{"class":223},[96,5660,227],{"class":138},[96,5662,230],{"class":216},[96,5664,233],{"class":138},[96,5666,5667,5669,5671,5673,5675,5677,5679,5681,5683,5685],{"class":98,"line":171},[96,5668,239],{"class":134},[96,5670,224],{"class":142},[96,5672,244],{"class":138},[96,5674,247],{"class":200},[96,5676,204],{"class":250},[96,5678,165],{"class":158},[96,5680,255],{"class":106},[96,5682,165],{"class":158},[96,5684,260],{"class":250},[96,5686,168],{"class":138},[96,5688,5689],{"class":98,"line":190},[96,5690,5691],{"class":1200},"  \u002F\u002F Trigger focus state on the button\n",[96,5693,5694,5696,5698,5700,5702,5704,5706,5708,5710,5712,5714,5716,5718,5720,5722,5724,5726,5728,5730,5733,5735],{"class":98,"line":197},[96,5695,239],{"class":134},[96,5697,224],{"class":142},[96,5699,244],{"class":138},[96,5701,4376],{"class":200},[96,5703,204],{"class":250},[96,5705,165],{"class":158},[96,5707,3492],{"class":106},[96,5709,165],{"class":158},[96,5711,146],{"class":138},[96,5713,139],{"class":138},[96,5715,4391],{"class":250},[96,5717,728],{"class":138},[96,5719,159],{"class":158},[96,5721,5543],{"class":106},[96,5723,165],{"class":158},[96,5725,152],{"class":138},[96,5727,260],{"class":250},[96,5729,244],{"class":138},[96,5731,5732],{"class":200},"focus",[96,5734,304],{"class":250},[96,5736,168],{"class":138},[96,5738,5739,5741,5743,5745,5747,5749,5751,5753,5755,5757,5759,5761,5763,5765,5767],{"class":98,"line":236},[96,5740,268],{"class":216},[96,5742,272],{"class":271},[96,5744,276],{"class":275},[96,5746,279],{"class":134},[96,5748,282],{"class":275},[96,5750,285],{"class":200},[96,5752,204],{"class":250},[96,5754,290],{"class":138},[96,5756,224],{"class":142},[96,5758,152],{"class":138},[96,5760,260],{"class":250},[96,5762,244],{"class":138},[96,5764,301],{"class":200},[96,5766,304],{"class":250},[96,5768,168],{"class":138},[96,5770,5771],{"class":98,"line":265},[96,5772,5773],{"class":1200},"  \u002F\u002F Except no violations\n",[96,5775,5776,5778,5780,5782,5784,5786,5788,5790,5792,5794],{"class":98,"line":309},[96,5777,312],{"class":200},[96,5779,204],{"class":250},[96,5781,317],{"class":142},[96,5783,244],{"class":138},[96,5785,322],{"class":142},[96,5787,260],{"class":250},[96,5789,244],{"class":138},[96,5791,329],{"class":200},[96,5793,332],{"class":250},[96,5795,168],{"class":138},[96,5797,5798,5800,5802],{"class":98,"line":337},[96,5799,340],{"class":138},[96,5801,260],{"class":142},[96,5803,168],{"class":138},[701,5805,5807],{"id":5806},"dark-mode-contrast","Dark mode contrast",[11,5809,5810],{},"Contrast issues may appear on light mode vs dark mode or vice-versa.",[86,5812,5814],{"className":4312,"code":5813,"filename":126,"language":4314,"meta":92,"style":92},"test('accessibility scan in dark mode', async ({ page }) => {\n  await page.emulateMedia({ colorScheme: 'dark' });\n  await page.goto('\u002F');\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('\u002F');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n",[24,5815,5816,5843,5874,5896,5928,5950,5958,5962,5989,6019,6041,6073,6095],{"__ignoreMap":92},[96,5817,5818,5820,5822,5824,5827,5829,5831,5833,5835,5837,5839,5841],{"class":98,"line":99},[96,5819,201],{"class":200},[96,5821,204],{"class":142},[96,5823,165],{"class":158},[96,5825,5826],{"class":106},"accessibility scan in dark mode",[96,5828,165],{"class":158},[96,5830,146],{"class":138},[96,5832,217],{"class":216},[96,5834,220],{"class":138},[96,5836,224],{"class":223},[96,5838,227],{"class":138},[96,5840,230],{"class":216},[96,5842,233],{"class":138},[96,5844,5845,5847,5849,5851,5854,5856,5858,5860,5862,5864,5866,5868,5870,5872],{"class":98,"line":171},[96,5846,239],{"class":134},[96,5848,224],{"class":142},[96,5850,244],{"class":138},[96,5852,5853],{"class":200},"emulateMedia",[96,5855,204],{"class":250},[96,5857,290],{"class":138},[96,5859,1487],{"class":250},[96,5861,728],{"class":138},[96,5863,159],{"class":158},[96,5865,1720],{"class":106},[96,5867,165],{"class":158},[96,5869,152],{"class":138},[96,5871,260],{"class":250},[96,5873,168],{"class":138},[96,5875,5876,5878,5880,5882,5884,5886,5888,5890,5892,5894],{"class":98,"line":190},[96,5877,239],{"class":134},[96,5879,224],{"class":142},[96,5881,244],{"class":138},[96,5883,247],{"class":200},[96,5885,204],{"class":250},[96,5887,165],{"class":158},[96,5889,255],{"class":106},[96,5891,165],{"class":158},[96,5893,260],{"class":250},[96,5895,168],{"class":138},[96,5897,5898,5900,5902,5904,5906,5908,5910,5912,5914,5916,5918,5920,5922,5924,5926],{"class":98,"line":197},[96,5899,268],{"class":216},[96,5901,272],{"class":271},[96,5903,276],{"class":275},[96,5905,279],{"class":134},[96,5907,282],{"class":275},[96,5909,285],{"class":200},[96,5911,204],{"class":250},[96,5913,290],{"class":138},[96,5915,224],{"class":142},[96,5917,152],{"class":138},[96,5919,260],{"class":250},[96,5921,244],{"class":138},[96,5923,301],{"class":200},[96,5925,304],{"class":250},[96,5927,168],{"class":138},[96,5929,5930,5932,5934,5936,5938,5940,5942,5944,5946,5948],{"class":98,"line":236},[96,5931,312],{"class":200},[96,5933,204],{"class":250},[96,5935,317],{"class":142},[96,5937,244],{"class":138},[96,5939,322],{"class":142},[96,5941,260],{"class":250},[96,5943,244],{"class":138},[96,5945,329],{"class":200},[96,5947,332],{"class":250},[96,5949,168],{"class":138},[96,5951,5952,5954,5956],{"class":98,"line":265},[96,5953,340],{"class":138},[96,5955,260],{"class":142},[96,5957,168],{"class":138},[96,5959,5960],{"class":98,"line":309},[96,5961,194],{"emptyLinePlaceholder":193},[96,5963,5964,5966,5968,5970,5973,5975,5977,5979,5981,5983,5985,5987],{"class":98,"line":337},[96,5965,201],{"class":200},[96,5967,204],{"class":142},[96,5969,165],{"class":158},[96,5971,5972],{"class":106},"accessibility scan in light mode",[96,5974,165],{"class":158},[96,5976,146],{"class":138},[96,5978,217],{"class":216},[96,5980,220],{"class":138},[96,5982,224],{"class":223},[96,5984,227],{"class":138},[96,5986,230],{"class":216},[96,5988,233],{"class":138},[96,5990,5991,5993,5995,5997,5999,6001,6003,6005,6007,6009,6011,6013,6015,6017],{"class":98,"line":1149},[96,5992,239],{"class":134},[96,5994,224],{"class":142},[96,5996,244],{"class":138},[96,5998,5853],{"class":200},[96,6000,204],{"class":250},[96,6002,290],{"class":138},[96,6004,1487],{"class":250},[96,6006,728],{"class":138},[96,6008,159],{"class":158},[96,6010,1435],{"class":106},[96,6012,165],{"class":158},[96,6014,152],{"class":138},[96,6016,260],{"class":250},[96,6018,168],{"class":138},[96,6020,6021,6023,6025,6027,6029,6031,6033,6035,6037,6039],{"class":98,"line":1167},[96,6022,239],{"class":134},[96,6024,224],{"class":142},[96,6026,244],{"class":138},[96,6028,247],{"class":200},[96,6030,204],{"class":250},[96,6032,165],{"class":158},[96,6034,255],{"class":106},[96,6036,165],{"class":158},[96,6038,260],{"class":250},[96,6040,168],{"class":138},[96,6042,6043,6045,6047,6049,6051,6053,6055,6057,6059,6061,6063,6065,6067,6069,6071],{"class":98,"line":1182},[96,6044,268],{"class":216},[96,6046,272],{"class":271},[96,6048,276],{"class":275},[96,6050,279],{"class":134},[96,6052,282],{"class":275},[96,6054,285],{"class":200},[96,6056,204],{"class":250},[96,6058,290],{"class":138},[96,6060,224],{"class":142},[96,6062,152],{"class":138},[96,6064,260],{"class":250},[96,6066,244],{"class":138},[96,6068,301],{"class":200},[96,6070,304],{"class":250},[96,6072,168],{"class":138},[96,6074,6075,6077,6079,6081,6083,6085,6087,6089,6091,6093],{"class":98,"line":1204},[96,6076,312],{"class":200},[96,6078,204],{"class":250},[96,6080,317],{"class":142},[96,6082,244],{"class":138},[96,6084,322],{"class":142},[96,6086,260],{"class":250},[96,6088,244],{"class":138},[96,6090,329],{"class":200},[96,6092,332],{"class":250},[96,6094,168],{"class":138},[96,6096,6097,6099,6101],{"class":98,"line":1226},[96,6098,340],{"class":138},[96,6100,260],{"class":142},[96,6102,168],{"class":138},[701,6104,6106],{"id":6105},"skip-link-first-focusable-element","Skip link — first focusable element",[11,6108,6109],{},"Verify the skip link exists, is the first element reached by keyboard, and actually moves focus to the main content when activated.",[86,6111,6113],{"className":4312,"code":6112,"filename":126,"language":4314,"meta":92,"style":92},"test('first focusable element is a skip link that navigates to main content', async ({ page }) => {\n  await page.goto('\u002F');\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  \u002F\u002F Activate the skip link and verify it navigated to main content\n  await page.keyboard.press('Enter');\n  await expect(page).toHaveURL(\u002F#main-content$\u002F);\n});\n",[24,6114,6115,6142,6164,6190,6219,6257,6286,6290,6295,6321,6354],{"__ignoreMap":92},[96,6116,6117,6119,6121,6123,6126,6128,6130,6132,6134,6136,6138,6140],{"class":98,"line":99},[96,6118,201],{"class":200},[96,6120,204],{"class":142},[96,6122,165],{"class":158},[96,6124,6125],{"class":106},"first focusable element is a skip link that navigates to main content",[96,6127,165],{"class":158},[96,6129,146],{"class":138},[96,6131,217],{"class":216},[96,6133,220],{"class":138},[96,6135,224],{"class":223},[96,6137,227],{"class":138},[96,6139,230],{"class":216},[96,6141,233],{"class":138},[96,6143,6144,6146,6148,6150,6152,6154,6156,6158,6160,6162],{"class":98,"line":171},[96,6145,239],{"class":134},[96,6147,224],{"class":142},[96,6149,244],{"class":138},[96,6151,247],{"class":200},[96,6153,204],{"class":250},[96,6155,165],{"class":158},[96,6157,255],{"class":106},[96,6159,165],{"class":158},[96,6161,260],{"class":250},[96,6163,168],{"class":138},[96,6165,6166,6168,6170,6172,6174,6176,6178,6180,6182,6184,6186,6188],{"class":98,"line":190},[96,6167,239],{"class":134},[96,6169,224],{"class":142},[96,6171,244],{"class":138},[96,6173,4522],{"class":142},[96,6175,244],{"class":138},[96,6177,4527],{"class":200},[96,6179,204],{"class":250},[96,6181,165],{"class":158},[96,6183,4534],{"class":106},[96,6185,165],{"class":158},[96,6187,260],{"class":250},[96,6189,168],{"class":138},[96,6191,6192,6194,6197,6199,6201,6203,6206,6208,6210,6213,6215,6217],{"class":98,"line":197},[96,6193,268],{"class":216},[96,6195,6196],{"class":271}," focused",[96,6198,276],{"class":275},[96,6200,224],{"class":142},[96,6202,244],{"class":138},[96,6204,6205],{"class":200},"locator",[96,6207,204],{"class":250},[96,6209,165],{"class":158},[96,6211,6212],{"class":106},":focus",[96,6214,165],{"class":158},[96,6216,260],{"class":250},[96,6218,168],{"class":138},[96,6220,6221,6223,6225,6227,6230,6232,6234,6236,6238,6240,6243,6245,6247,6249,6251,6253,6255],{"class":98,"line":236},[96,6222,239],{"class":134},[96,6224,149],{"class":200},[96,6226,204],{"class":250},[96,6228,6229],{"class":142},"focused",[96,6231,260],{"class":250},[96,6233,244],{"class":138},[96,6235,5032],{"class":200},[96,6237,204],{"class":250},[96,6239,165],{"class":158},[96,6241,6242],{"class":106},"href",[96,6244,165],{"class":158},[96,6246,146],{"class":138},[96,6248,159],{"class":158},[96,6250,3252],{"class":106},[96,6252,165],{"class":158},[96,6254,260],{"class":250},[96,6256,168],{"class":138},[96,6258,6259,6261,6263,6265,6267,6269,6271,6274,6276,6278,6280,6282,6284],{"class":98,"line":265},[96,6260,239],{"class":134},[96,6262,149],{"class":200},[96,6264,204],{"class":250},[96,6266,6229],{"class":142},[96,6268,260],{"class":250},[96,6270,244],{"class":138},[96,6272,6273],{"class":200},"toHaveText",[96,6275,204],{"class":250},[96,6277,165],{"class":158},[96,6279,3319],{"class":106},[96,6281,165],{"class":158},[96,6283,260],{"class":250},[96,6285,168],{"class":138},[96,6287,6288],{"class":98,"line":309},[96,6289,194],{"emptyLinePlaceholder":193},[96,6291,6292],{"class":98,"line":337},[96,6293,6294],{"class":1200},"  \u002F\u002F Activate the skip link and verify it navigated to main content\n",[96,6296,6297,6299,6301,6303,6305,6307,6309,6311,6313,6315,6317,6319],{"class":98,"line":1149},[96,6298,239],{"class":134},[96,6300,224],{"class":142},[96,6302,244],{"class":138},[96,6304,4522],{"class":142},[96,6306,244],{"class":138},[96,6308,4527],{"class":200},[96,6310,204],{"class":250},[96,6312,165],{"class":158},[96,6314,5099],{"class":106},[96,6316,165],{"class":158},[96,6318,260],{"class":250},[96,6320,168],{"class":138},[96,6322,6323,6325,6327,6329,6332,6334,6336,6339,6341,6343,6345,6348,6350,6352],{"class":98,"line":1167},[96,6324,239],{"class":134},[96,6326,149],{"class":200},[96,6328,204],{"class":250},[96,6330,6331],{"class":142},"page",[96,6333,260],{"class":250},[96,6335,244],{"class":138},[96,6337,6338],{"class":200},"toHaveURL",[96,6340,204],{"class":250},[96,6342,255],{"class":158},[96,6344,3252],{"class":106},[96,6346,6347],{"class":134},"$",[96,6349,255],{"class":158},[96,6351,260],{"class":250},[96,6353,168],{"class":138},[96,6355,6356,6358,6360],{"class":98,"line":1182},[96,6357,340],{"class":138},[96,6359,260],{"class":142},[96,6361,168],{"class":138},[701,6363,6365],{"id":6364},"unique-landmark-labels","Unique landmark labels",[11,6367,6368],{},"Catch duplicate landmark names (best practive to have programmatically unique landmarks)",[86,6370,6372],{"className":4312,"code":6371,"filename":126,"language":4314,"meta":92,"style":92},"test('all nav landmarks have unique accessible names', async ({ page }) => {\n  await page.goto('\u002F');\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  \u002F\u002F 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  \u002F\u002F Verify all labels are unique\n  const uniqueLabels = new Set(labels);\n  expect(uniqueLabels.size).toBe(labels.length);\n});\n",[24,6373,6374,6401,6423,6458,6478,6518,6525,6529,6534,6562,6611,6619,6623,6628,6651,6684],{"__ignoreMap":92},[96,6375,6376,6378,6380,6382,6385,6387,6389,6391,6393,6395,6397,6399],{"class":98,"line":99},[96,6377,201],{"class":200},[96,6379,204],{"class":142},[96,6381,165],{"class":158},[96,6383,6384],{"class":106},"all nav landmarks have unique accessible names",[96,6386,165],{"class":158},[96,6388,146],{"class":138},[96,6390,217],{"class":216},[96,6392,220],{"class":138},[96,6394,224],{"class":223},[96,6396,227],{"class":138},[96,6398,230],{"class":216},[96,6400,233],{"class":138},[96,6402,6403,6405,6407,6409,6411,6413,6415,6417,6419,6421],{"class":98,"line":171},[96,6404,239],{"class":134},[96,6406,224],{"class":142},[96,6408,244],{"class":138},[96,6410,247],{"class":200},[96,6412,204],{"class":250},[96,6414,165],{"class":158},[96,6416,255],{"class":106},[96,6418,165],{"class":158},[96,6420,260],{"class":250},[96,6422,168],{"class":138},[96,6424,6425,6427,6430,6432,6434,6436,6438,6440,6442,6444,6446,6448,6450,6452,6454,6456],{"class":98,"line":190},[96,6426,268],{"class":216},[96,6428,6429],{"class":271}," navs",[96,6431,276],{"class":275},[96,6433,279],{"class":134},[96,6435,224],{"class":142},[96,6437,244],{"class":138},[96,6439,4376],{"class":200},[96,6441,204],{"class":250},[96,6443,165],{"class":158},[96,6445,4121],{"class":106},[96,6447,165],{"class":158},[96,6449,260],{"class":250},[96,6451,244],{"class":138},[96,6453,2655],{"class":200},[96,6455,304],{"class":250},[96,6457,168],{"class":138},[96,6459,6460,6462,6465,6467,6469,6472,6474,6476],{"class":98,"line":197},[96,6461,268],{"class":216},[96,6463,6464],{"class":271}," labels",[96,6466,276],{"class":275},[96,6468,279],{"class":134},[96,6470,6471],{"class":3963}," Promise",[96,6473,244],{"class":138},[96,6475,2655],{"class":200},[96,6477,1506],{"class":250},[96,6479,6480,6483,6485,6487,6489,6491,6494,6496,6498,6501,6503,6506,6508,6510,6512,6514,6516],{"class":98,"line":236},[96,6481,6482],{"class":142},"    navs",[96,6484,244],{"class":138},[96,6486,1516],{"class":200},[96,6488,204],{"class":250},[96,6490,204],{"class":138},[96,6492,6493],{"class":223},"nav",[96,6495,260],{"class":138},[96,6497,230],{"class":216},[96,6499,6500],{"class":142}," nav",[96,6502,244],{"class":138},[96,6504,6505],{"class":200},"getAttribute",[96,6507,204],{"class":250},[96,6509,165],{"class":158},[96,6511,3149],{"class":106},[96,6513,165],{"class":158},[96,6515,1615],{"class":250},[96,6517,1309],{"class":138},[96,6519,6520,6523],{"class":98,"line":265},[96,6521,6522],{"class":250},"  )",[96,6524,168],{"class":138},[96,6526,6527],{"class":98,"line":309},[96,6528,194],{"emptyLinePlaceholder":193},[96,6530,6531],{"class":98,"line":337},[96,6532,6533],{"class":1200},"  \u002F\u002F Verify every nav has a label\n",[96,6535,6536,6539,6541,6544,6546,6548,6551,6553,6556,6558,6560],{"class":98,"line":1149},[96,6537,6538],{"class":142},"  labels",[96,6540,244],{"class":138},[96,6542,6543],{"class":200},"forEach",[96,6545,204],{"class":250},[96,6547,204],{"class":138},[96,6549,6550],{"class":223},"label",[96,6552,146],{"class":138},[96,6554,6555],{"class":223}," index",[96,6557,260],{"class":138},[96,6559,230],{"class":216},[96,6561,233],{"class":138},[96,6563,6564,6566,6568,6570,6572,6575,6578,6580,6583,6586,6589,6591,6594,6596,6598,6600,6602,6604,6607,6609],{"class":98,"line":1167},[96,6565,4610],{"class":200},[96,6567,204],{"class":250},[96,6569,6550],{"class":142},[96,6571,146],{"class":138},[96,6573,6574],{"class":158}," `",[96,6576,6577],{"class":106},"Navigation element ",[96,6579,1757],{"class":158},[96,6581,6582],{"class":142},"index",[96,6584,6585],{"class":275}," +",[96,6587,6588],{"class":1969}," 1",[96,6590,340],{"class":158},[96,6592,6593],{"class":106}," is missing an aria-label",[96,6595,1751],{"class":158},[96,6597,260],{"class":250},[96,6599,244],{"class":138},[96,6601,4688],{"class":142},[96,6603,244],{"class":138},[96,6605,6606],{"class":200},"toBeNull",[96,6608,304],{"class":250},[96,6610,168],{"class":138},[96,6612,6613,6615,6617],{"class":98,"line":1182},[96,6614,1806],{"class":138},[96,6616,260],{"class":250},[96,6618,168],{"class":138},[96,6620,6621],{"class":98,"line":1204},[96,6622,194],{"emptyLinePlaceholder":193},[96,6624,6625],{"class":98,"line":1226},[96,6626,6627],{"class":1200},"  \u002F\u002F Verify all labels are unique\n",[96,6629,6630,6632,6635,6637,6639,6642,6644,6647,6649],{"class":98,"line":1234},[96,6631,268],{"class":216},[96,6633,6634],{"class":271}," uniqueLabels",[96,6636,276],{"class":275},[96,6638,282],{"class":275},[96,6640,6641],{"class":200}," Set",[96,6643,204],{"class":250},[96,6645,6646],{"class":142},"labels",[96,6648,260],{"class":250},[96,6650,168],{"class":138},[96,6652,6653,6655,6657,6660,6662,6665,6667,6669,6671,6673,6675,6677,6680,6682],{"class":98,"line":1240},[96,6654,312],{"class":200},[96,6656,204],{"class":250},[96,6658,6659],{"class":142},"uniqueLabels",[96,6661,244],{"class":138},[96,6663,6664],{"class":142},"size",[96,6666,260],{"class":250},[96,6668,244],{"class":138},[96,6670,4622],{"class":200},[96,6672,204],{"class":250},[96,6674,6646],{"class":142},[96,6676,244],{"class":138},[96,6678,6679],{"class":271},"length",[96,6681,260],{"class":250},[96,6683,168],{"class":138},[96,6685,6686,6688,6690],{"class":98,"line":2227},[96,6687,340],{"class":138},[96,6689,260],{"class":142},[96,6691,168],{"class":138},[701,6693,6695],{"id":6694},"ambiguous-link-text","Ambiguous link text",[11,6697,6698],{},"Check for known offenders that pass structural validation but fail in context.",[86,6700,6702],{"className":4312,"code":6701,"filename":126,"language":4314,"meta":92,"style":92},"test('no ambiguous link text', async ({ page }) => {\n  await page.goto('\u002F');\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",[24,6703,6704,6731,6753,6820,6856,6875,6915,6960,6964],{"__ignoreMap":92},[96,6705,6706,6708,6710,6712,6715,6717,6719,6721,6723,6725,6727,6729],{"class":98,"line":99},[96,6707,201],{"class":200},[96,6709,204],{"class":142},[96,6711,165],{"class":158},[96,6713,6714],{"class":106},"no ambiguous link text",[96,6716,165],{"class":158},[96,6718,146],{"class":138},[96,6720,217],{"class":216},[96,6722,220],{"class":138},[96,6724,224],{"class":223},[96,6726,227],{"class":138},[96,6728,230],{"class":216},[96,6730,233],{"class":138},[96,6732,6733,6735,6737,6739,6741,6743,6745,6747,6749,6751],{"class":98,"line":171},[96,6734,239],{"class":134},[96,6736,224],{"class":142},[96,6738,244],{"class":138},[96,6740,247],{"class":200},[96,6742,204],{"class":250},[96,6744,165],{"class":158},[96,6746,255],{"class":106},[96,6748,165],{"class":158},[96,6750,260],{"class":250},[96,6752,168],{"class":138},[96,6754,6755,6757,6760,6762,6764,6766,6769,6771,6773,6775,6778,6780,6782,6784,6787,6789,6791,6793,6796,6798,6800,6802,6805,6807,6809,6811,6814,6816,6818],{"class":98,"line":190},[96,6756,268],{"class":216},[96,6758,6759],{"class":271}," ambiguous",[96,6761,276],{"class":275},[96,6763,1021],{"class":250},[96,6765,165],{"class":158},[96,6767,6768],{"class":106},"read more",[96,6770,165],{"class":158},[96,6772,146],{"class":138},[96,6774,159],{"class":158},[96,6776,6777],{"class":106},"click here",[96,6779,165],{"class":158},[96,6781,146],{"class":138},[96,6783,159],{"class":158},[96,6785,6786],{"class":106},"browse all",[96,6788,165],{"class":158},[96,6790,146],{"class":138},[96,6792,159],{"class":158},[96,6794,6795],{"class":106},"learn more",[96,6797,165],{"class":158},[96,6799,146],{"class":138},[96,6801,159],{"class":158},[96,6803,6804],{"class":106},"here",[96,6806,165],{"class":158},[96,6808,146],{"class":138},[96,6810,159],{"class":158},[96,6812,6813],{"class":106},"more",[96,6815,165],{"class":158},[96,6817,1062],{"class":250},[96,6819,168],{"class":138},[96,6821,6822,6824,6827,6829,6831,6833,6835,6837,6839,6841,6844,6846,6848,6850,6852,6854],{"class":98,"line":197},[96,6823,268],{"class":216},[96,6825,6826],{"class":271}," links",[96,6828,276],{"class":275},[96,6830,279],{"class":134},[96,6832,224],{"class":142},[96,6834,244],{"class":138},[96,6836,4376],{"class":200},[96,6838,204],{"class":250},[96,6840,165],{"class":158},[96,6842,6843],{"class":106},"link",[96,6845,165],{"class":158},[96,6847,260],{"class":250},[96,6849,244],{"class":138},[96,6851,2655],{"class":200},[96,6853,304],{"class":250},[96,6855,168],{"class":138},[96,6857,6858,6860,6862,6864,6867,6869,6871,6873],{"class":98,"line":236},[96,6859,4476],{"class":134},[96,6861,1530],{"class":250},[96,6863,459],{"class":216},[96,6865,6866],{"class":271}," link",[96,6868,1705],{"class":275},[96,6870,6826],{"class":142},[96,6872,1734],{"class":250},[96,6874,1107],{"class":138},[96,6876,6877,6879,6882,6884,6886,6889,6891,6893,6896,6899,6901,6904,6906,6908,6911,6913],{"class":98,"line":265},[96,6878,4545],{"class":216},[96,6880,6881],{"class":271}," text",[96,6883,276],{"class":275},[96,6885,1530],{"class":250},[96,6887,6888],{"class":134},"await",[96,6890,6866],{"class":142},[96,6892,244],{"class":138},[96,6894,6895],{"class":200},"innerText",[96,6897,6898],{"class":250},"())",[96,6900,244],{"class":138},[96,6902,6903],{"class":200},"toLowerCase",[96,6905,304],{"class":250},[96,6907,244],{"class":138},[96,6909,6910],{"class":200},"trim",[96,6912,304],{"class":250},[96,6914,168],{"class":138},[96,6916,6917,6919,6921,6924,6926,6928,6931,6933,6935,6937,6939,6941,6943,6945,6947,6949,6952,6954,6956,6958],{"class":98,"line":309},[96,6918,4610],{"class":200},[96,6920,204],{"class":250},[96,6922,6923],{"class":142},"ambiguous",[96,6925,146],{"class":138},[96,6927,6574],{"class":158},[96,6929,6930],{"class":106},"Ambiguous link text found: \"",[96,6932,1757],{"class":158},[96,6934,1638],{"class":142},[96,6936,340],{"class":158},[96,6938,2072],{"class":106},[96,6940,1751],{"class":158},[96,6942,260],{"class":250},[96,6944,244],{"class":138},[96,6946,4688],{"class":142},[96,6948,244],{"class":138},[96,6950,6951],{"class":200},"toContain",[96,6953,204],{"class":250},[96,6955,1638],{"class":142},[96,6957,260],{"class":250},[96,6959,168],{"class":138},[96,6961,6962],{"class":98,"line":337},[96,6963,2853],{"class":138},[96,6965,6966,6968,6970],{"class":98,"line":1149},[96,6967,340],{"class":138},[96,6969,260],{"class":142},[96,6971,168],{"class":138},[701,6973,6975],{"id":6974},"dynamic-aria-label-on-stateful-controls","Dynamic aria-label on stateful controls",[11,6977,6978],{},"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\u002Fmisleading.",[86,6980,6983],{"className":4312,"code":6981,"filename":6982,"language":4314,"meta":92,"style":92},"test('color mode toggle label reflects current state', async ({ page }) => {\n  \u002F\u002F Start in light mode — toggle should offer to switch to dark\n  await page.emulateMedia({ colorScheme: 'light' });\n  await page.goto('\u002F');\n  const toggle = page.getByRole('button', { name: \u002Fswitch to\u002Fi });\n  await expect(toggle).toHaveAttribute('aria-label', 'Switch to dark mode');\n\n  \u002F\u002F 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",[24,6984,6985,7012,7017,7047,7069,7117,7154,7158,7163,7177,7214],{"__ignoreMap":92},[96,6986,6987,6989,6991,6993,6996,6998,7000,7002,7004,7006,7008,7010],{"class":98,"line":99},[96,6988,201],{"class":200},[96,6990,204],{"class":142},[96,6992,165],{"class":158},[96,6994,6995],{"class":106},"color mode toggle label reflects current state",[96,6997,165],{"class":158},[96,6999,146],{"class":138},[96,7001,217],{"class":216},[96,7003,220],{"class":138},[96,7005,224],{"class":223},[96,7007,227],{"class":138},[96,7009,230],{"class":216},[96,7011,233],{"class":138},[96,7013,7014],{"class":98,"line":171},[96,7015,7016],{"class":1200},"  \u002F\u002F Start in light mode — toggle should offer to switch to dark\n",[96,7018,7019,7021,7023,7025,7027,7029,7031,7033,7035,7037,7039,7041,7043,7045],{"class":98,"line":190},[96,7020,239],{"class":134},[96,7022,224],{"class":142},[96,7024,244],{"class":138},[96,7026,5853],{"class":200},[96,7028,204],{"class":250},[96,7030,290],{"class":138},[96,7032,1487],{"class":250},[96,7034,728],{"class":138},[96,7036,159],{"class":158},[96,7038,1435],{"class":106},[96,7040,165],{"class":158},[96,7042,152],{"class":138},[96,7044,260],{"class":250},[96,7046,168],{"class":138},[96,7048,7049,7051,7053,7055,7057,7059,7061,7063,7065,7067],{"class":98,"line":197},[96,7050,239],{"class":134},[96,7052,224],{"class":142},[96,7054,244],{"class":138},[96,7056,247],{"class":200},[96,7058,204],{"class":250},[96,7060,165],{"class":158},[96,7062,255],{"class":106},[96,7064,165],{"class":158},[96,7066,260],{"class":250},[96,7068,168],{"class":138},[96,7070,7071,7073,7075,7077,7079,7081,7083,7085,7087,7089,7091,7093,7095,7097,7099,7102,7105,7107,7111,7113,7115],{"class":98,"line":236},[96,7072,268],{"class":216},[96,7074,4930],{"class":271},[96,7076,276],{"class":275},[96,7078,224],{"class":142},[96,7080,244],{"class":138},[96,7082,4376],{"class":200},[96,7084,204],{"class":250},[96,7086,165],{"class":158},[96,7088,3492],{"class":106},[96,7090,165],{"class":158},[96,7092,146],{"class":138},[96,7094,139],{"class":138},[96,7096,4391],{"class":250},[96,7098,728],{"class":138},[96,7100,7101],{"class":158}," \u002F",[96,7103,7104],{"class":106},"switch to",[96,7106,255],{"class":158},[96,7108,7110],{"class":7109},"sPY_W","i",[96,7112,152],{"class":138},[96,7114,260],{"class":250},[96,7116,168],{"class":138},[96,7118,7119,7121,7123,7125,7127,7129,7131,7133,7135,7137,7139,7141,7143,7145,7148,7150,7152],{"class":98,"line":265},[96,7120,239],{"class":134},[96,7122,149],{"class":200},[96,7124,204],{"class":250},[96,7126,5025],{"class":142},[96,7128,260],{"class":250},[96,7130,244],{"class":138},[96,7132,5032],{"class":200},[96,7134,204],{"class":250},[96,7136,165],{"class":158},[96,7138,3149],{"class":106},[96,7140,165],{"class":158},[96,7142,146],{"class":138},[96,7144,159],{"class":158},[96,7146,7147],{"class":106},"Switch to dark mode",[96,7149,165],{"class":158},[96,7151,260],{"class":250},[96,7153,168],{"class":138},[96,7155,7156],{"class":98,"line":309},[96,7157,194],{"emptyLinePlaceholder":193},[96,7159,7160],{"class":98,"line":337},[96,7161,7162],{"class":1200},"  \u002F\u002F After clicking, page is in dark mode — toggle should offer to switch to light\n",[96,7164,7165,7167,7169,7171,7173,7175],{"class":98,"line":1149},[96,7166,239],{"class":134},[96,7168,4930],{"class":142},[96,7170,244],{"class":138},[96,7172,4409],{"class":200},[96,7174,304],{"class":250},[96,7176,168],{"class":138},[96,7178,7179,7181,7183,7185,7187,7189,7191,7193,7195,7197,7199,7201,7203,7205,7208,7210,7212],{"class":98,"line":1167},[96,7180,239],{"class":134},[96,7182,149],{"class":200},[96,7184,204],{"class":250},[96,7186,5025],{"class":142},[96,7188,260],{"class":250},[96,7190,244],{"class":138},[96,7192,5032],{"class":200},[96,7194,204],{"class":250},[96,7196,165],{"class":158},[96,7198,3149],{"class":106},[96,7200,165],{"class":158},[96,7202,146],{"class":138},[96,7204,159],{"class":158},[96,7206,7207],{"class":106},"Switch to light mode",[96,7209,165],{"class":158},[96,7211,260],{"class":250},[96,7213,168],{"class":138},[96,7215,7216,7218,7220],{"class":98,"line":1182},[96,7217,340],{"class":138},[96,7219,260],{"class":142},[96,7221,168],{"class":138},[701,7223,7225],{"id":7224},"generic-alt-text","Generic alt text",[11,7227,7228],{},"Catch alt text that was added to silence a linter but communicates nothing.",[86,7230,7232],{"className":4312,"code":7231,"filename":126,"language":4314,"meta":92,"style":92},"test('no generic alt text on images', async ({ page }) => {\n  await page.goto('\u002F');\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      \u002F\u002F 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",[24,7233,7234,7261,7283,7358,7394,7413,7463,7481,7486,7531,7536,7540],{"__ignoreMap":92},[96,7235,7236,7238,7240,7242,7245,7247,7249,7251,7253,7255,7257,7259],{"class":98,"line":99},[96,7237,201],{"class":200},[96,7239,204],{"class":142},[96,7241,165],{"class":158},[96,7243,7244],{"class":106},"no generic alt text on images",[96,7246,165],{"class":158},[96,7248,146],{"class":138},[96,7250,217],{"class":216},[96,7252,220],{"class":138},[96,7254,224],{"class":223},[96,7256,227],{"class":138},[96,7258,230],{"class":216},[96,7260,233],{"class":138},[96,7262,7263,7265,7267,7269,7271,7273,7275,7277,7279,7281],{"class":98,"line":171},[96,7264,239],{"class":134},[96,7266,224],{"class":142},[96,7268,244],{"class":138},[96,7270,247],{"class":200},[96,7272,204],{"class":250},[96,7274,165],{"class":158},[96,7276,255],{"class":106},[96,7278,165],{"class":158},[96,7280,260],{"class":250},[96,7282,168],{"class":138},[96,7284,7285,7287,7290,7292,7294,7296,7299,7301,7303,7305,7308,7310,7312,7314,7317,7319,7321,7323,7326,7328,7330,7332,7335,7337,7339,7341,7344,7346,7348,7350,7352,7354,7356],{"class":98,"line":190},[96,7286,268],{"class":216},[96,7288,7289],{"class":271}," genericAlts",[96,7291,276],{"class":275},[96,7293,1021],{"class":250},[96,7295,165],{"class":158},[96,7297,7298],{"class":106},"image",[96,7300,165],{"class":158},[96,7302,146],{"class":138},[96,7304,159],{"class":158},[96,7306,7307],{"class":106},"photo",[96,7309,165],{"class":158},[96,7311,146],{"class":138},[96,7313,159],{"class":158},[96,7315,7316],{"class":106},"picture",[96,7318,165],{"class":158},[96,7320,146],{"class":138},[96,7322,159],{"class":158},[96,7324,7325],{"class":106},"icon",[96,7327,165],{"class":158},[96,7329,146],{"class":138},[96,7331,159],{"class":158},[96,7333,7334],{"class":106},"decorative image",[96,7336,165],{"class":158},[96,7338,146],{"class":138},[96,7340,159],{"class":158},[96,7342,7343],{"class":106},"banner",[96,7345,165],{"class":158},[96,7347,146],{"class":138},[96,7349,159],{"class":158},[96,7351,2025],{"class":106},[96,7353,165],{"class":158},[96,7355,1062],{"class":250},[96,7357,168],{"class":138},[96,7359,7360,7362,7365,7367,7369,7371,7373,7375,7377,7379,7382,7384,7386,7388,7390,7392],{"class":98,"line":197},[96,7361,268],{"class":216},[96,7363,7364],{"class":271}," images",[96,7366,276],{"class":275},[96,7368,279],{"class":134},[96,7370,224],{"class":142},[96,7372,244],{"class":138},[96,7374,6205],{"class":200},[96,7376,204],{"class":250},[96,7378,165],{"class":158},[96,7380,7381],{"class":106},"img[alt]",[96,7383,165],{"class":158},[96,7385,260],{"class":250},[96,7387,244],{"class":138},[96,7389,2655],{"class":200},[96,7391,304],{"class":250},[96,7393,168],{"class":138},[96,7395,7396,7398,7400,7402,7405,7407,7409,7411],{"class":98,"line":236},[96,7397,4476],{"class":134},[96,7399,1530],{"class":250},[96,7401,459],{"class":216},[96,7403,7404],{"class":271}," img",[96,7406,1705],{"class":275},[96,7408,7364],{"class":142},[96,7410,1734],{"class":250},[96,7412,1107],{"class":138},[96,7414,7415,7417,7419,7421,7423,7425,7427,7429,7431,7433,7435,7437,7439,7441,7444,7447,7449,7451,7453,7455,7457,7459,7461],{"class":98,"line":265},[96,7416,4545],{"class":216},[96,7418,3674],{"class":271},[96,7420,276],{"class":275},[96,7422,1530],{"class":250},[96,7424,6888],{"class":134},[96,7426,7404],{"class":142},[96,7428,244],{"class":138},[96,7430,6505],{"class":200},[96,7432,204],{"class":250},[96,7434,165],{"class":158},[96,7436,3115],{"class":106},[96,7438,165],{"class":158},[96,7440,1734],{"class":250},[96,7442,7443],{"class":275},"??",[96,7445,7446],{"class":158}," ''",[96,7448,260],{"class":250},[96,7450,244],{"class":138},[96,7452,6903],{"class":200},[96,7454,304],{"class":250},[96,7456,244],{"class":138},[96,7458,6910],{"class":200},[96,7460,304],{"class":250},[96,7462,168],{"class":138},[96,7464,7465,7468,7470,7472,7475,7477,7479],{"class":98,"line":309},[96,7466,7467],{"class":134},"    if",[96,7469,1530],{"class":250},[96,7471,3115],{"class":142},[96,7473,7474],{"class":275}," !==",[96,7476,7446],{"class":158},[96,7478,1734],{"class":250},[96,7480,1107],{"class":138},[96,7482,7483],{"class":98,"line":337},[96,7484,7485],{"class":1200},"      \u002F\u002F Empty alt is valid and intentional for decorative images — don't flag it\n",[96,7487,7488,7491,7493,7496,7498,7500,7503,7505,7507,7509,7511,7513,7515,7517,7519,7521,7523,7525,7527,7529],{"class":98,"line":1149},[96,7489,7490],{"class":200},"      expect",[96,7492,204],{"class":250},[96,7494,7495],{"class":142},"genericAlts",[96,7497,146],{"class":138},[96,7499,6574],{"class":158},[96,7501,7502],{"class":106},"Generic alt text found: \"",[96,7504,1757],{"class":158},[96,7506,3115],{"class":142},[96,7508,340],{"class":158},[96,7510,2072],{"class":106},[96,7512,1751],{"class":158},[96,7514,260],{"class":250},[96,7516,244],{"class":138},[96,7518,4688],{"class":142},[96,7520,244],{"class":138},[96,7522,6951],{"class":200},[96,7524,204],{"class":250},[96,7526,3115],{"class":142},[96,7528,260],{"class":250},[96,7530,168],{"class":138},[96,7532,7533],{"class":98,"line":1167},[96,7534,7535],{"class":138},"    }\n",[96,7537,7538],{"class":98,"line":1182},[96,7539,2853],{"class":138},[96,7541,7542,7544,7546],{"class":98,"line":1204},[96,7543,340],{"class":138},[96,7545,260],{"class":142},[96,7547,168],{"class":138},[3012,7549],{},[41,7551,7553],{"id":7552},"manual-accessibility-tests-to-supplement-automated-tests","Manual Accessibility Tests to Supplement Automated Tests",[11,7555,7556,7557,7559],{},"Some accessibility defects cannot be detected through test automation alone. Whether a label is ",[606,7558,3194],{}," 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,7561,7562,7563,244],{},"For a structured place to start, see my ",[18,7564,7565],{"href":20},"minimum accessibility test plan",[11,7567,7568],{},"Use this checklist as part of your periodic accessibility audit — the checks automation structurally cannot perform:",[49,7570,7573,7582,7588,7594,7600,7606,7615,7621],{"className":7571},[7572],"contains-task-list",[52,7574,7577,7581],{"className":7575},[7576],"task-list-item",[7578,7579],"input",{"disabled":193,"type":7580},"checkbox"," Tab through the full page with no mouse — does focus order make sense?",[52,7583,7585,7587],{"className":7584},[7576],[7578,7586],{"disabled":193,"type":7580}," Is a skip link the first focusable element?",[52,7589,7591,7593],{"className":7590},[7576],[7578,7592],{"disabled":193,"type":7580}," Are all landmark regions labeled and distinct?",[52,7595,7597,7599],{"className":7596},[7576],[7578,7598],{"disabled":193,"type":7580}," Does every link make sense read aloud out of context?",[52,7601,7603,7605],{"className":7602},[7576],[7578,7604],{"disabled":193,"type":7580}," Do dynamic controls update their accessible name when state changes?",[52,7607,7609,7611,7612,7614],{"className":7608},[7576],[7578,7610],{"disabled":193,"type":7580}," Do ",[24,7613,3335],{}," references point to visible, accurate text?",[52,7616,7618,7620],{"className":7617},[7576],[7578,7619],{"disabled":193,"type":7580}," Test with a real screen reader — NVDA (free, Windows), JAWS, or VoiceOver (built into macOS and iOS)",[52,7622,7624,7626],{"className":7623},[7576],[7578,7625],{"disabled":193,"type":7580}," Navigate by landmarks, headings, and links — not just linearly",[11,7628,7629],{},"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.",[3012,7631],{},[41,7633,2935],{"id":2934},[11,7635,7636,7637,7639],{},"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 ",[24,7638,3149],{},", a dropped landmark, or a broken focus order leaves no visual trace and no failing test without an explicit check.",[11,7641,7642],{},"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,7644,7645],{},"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.",[2956,7647],{":items":7648},"[\"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Fdisable-exclude-phase-in-axe-rules-playwright\",\"\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fhow-to-handle-failing-tests-caused-by-known-bugs\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation\"]",[2960,7650,7651],{},"html pre.shiki code .sPJuK,html code.shiki .sPJuK{--shiki-light:#39ADB5;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}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 .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 .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 .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 .sPxkN,html code.shiki .sPxkN{--shiki-light:#39ADB5;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .slOWO,html code.shiki .slOWO{--shiki-light:#E2931D;--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}html pre.shiki code .sE6rD,html code.shiki .sE6rD{--shiki-light:#39ADB5;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .sQ79N,html code.shiki .sQ79N{--shiki-light:#90A4AE;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sb1SK,html code.shiki .sb1SK{--shiki-light:#6182B8;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}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 .s6g51,html code.shiki .s6g51{--shiki-light:#F76D47;--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 pre.shiki code .sPY_W,html code.shiki .sPY_W{--shiki-light:#F76D47;--shiki-default:#A0111F;--shiki-dark:#FF9492}",{"title":92,"searchDepth":171,"depth":171,"links":7653},[7654,7655,7656,7669,7680,7681],{"id":3016,"depth":171,"text":3017},{"id":3100,"depth":171,"text":3101},{"id":3164,"depth":171,"text":3165,"children":7657},[7658,7659,7660,7662,7663,7664,7665,7666,7667,7668],{"id":3168,"depth":190,"text":3169},{"id":3229,"depth":190,"text":3230},{"id":3331,"depth":190,"text":7661},"3. aria-labelledby vs aria-label — Both Pass, One Is Better",{"id":3559,"depth":190,"text":3560},{"id":3607,"depth":190,"text":3608},{"id":3763,"depth":190,"text":3764},{"id":3904,"depth":190,"text":3905},{"id":4052,"depth":190,"text":4053},{"id":4161,"depth":190,"text":4162},{"id":4242,"depth":190,"text":4243},{"id":5261,"depth":171,"text":5262,"children":7670},[7671,7672,7673,7674,7675,7676,7677,7678,7679],{"id":5280,"depth":190,"text":5281},{"id":5449,"depth":190,"text":5450},{"id":5628,"depth":190,"text":5629},{"id":5806,"depth":190,"text":5807},{"id":6105,"depth":190,"text":6106},{"id":6364,"depth":190,"text":6365},{"id":6694,"depth":190,"text":6695},{"id":6974,"depth":190,"text":6975},{"id":7224,"depth":190,"text":7225},{"id":7552,"depth":171,"text":7553},{"id":2934,"depth":171,"text":2935},"\u002Fimages\u002Fposts\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\u002Fwhat-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.",{},{"title":38,"description":7684},{"loc":37},"software-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations","2oj860u6SGHhkvekQAYY0JaWfYviz4pUcgeTbLuhgTg",{"id":7691,"title":21,"bmcUsername":6,"body":7692,"cover":8724,"date":8725,"description":8726,"draft":2989,"extension":2990,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":8727,"navigation":193,"npmPackage":6,"order":6,"path":20,"review":6,"seo":8728,"sitemap":8729,"stem":8730,"updated":6,"__hash__":8731},"content\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan.md",{"type":8,"value":7693,"toc":8710},[7694,7698,7701,7704,7711,7721,7724,7728,7732,7741,7744,7764,7767,7770,7774,7822,7825,7829,7832,7839,7842,7845,7848,7856,7859,7863,8037,8041,8158,8171,8175,8178,8181,8184,8187,8190,8282,8285,8623,8626,8630,8633,8636,8643,8647,8650,8653,8656,8659,8666,8669,8673,8676,8679,8684,8687,8695,8704,8707],[41,7695,7697],{"id":7696},"implementing-a-test-plan-for-accessibility-testing","Implementing a Test Plan for Accessibility Testing",[11,7699,7700],{},"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,7702,7703],{},"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,7705,7706,7707,7710],{},"I don't advocate for surprising developers with your \"gotcha\" test cases, the ones you bring out to find those ",[606,7708,7709],{},"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,7712,7713,7714,7720],{},"As of writing, there are 90 rules in ",[18,7715,7719],{"href":7716,"rel":7717},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1",[7718],"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,7722,7723],{},"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.",[7725,7726],"u-video",{"src":7727},"https:\u002F\u002Fwww.youtube.com\u002Fembed\u002Flsv_lwxu2tI",[41,7729,7731],{"id":7730},"creating-a-minimum-accessibility-test-plan","Creating a Minimum Accessibility Test Plan",[11,7733,7734,7735,7740],{},"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 ",[18,7736,7739],{"href":7737,"rel":7738},"https:\u002F\u002Fwww.w3.org\u002FTR\u002FWCAG21\u002F",[7718],"WCAG 2.1 AA"," compliance. The minimum accessibility test plan will be your first milestone along the way.",[11,7742,7743],{},"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",[49,7745,7746,7749,7752,7755,7758,7761],{},[52,7747,7748],{},"Who the user(s) is\u002Fare (e.g., someone reliant on a screen-reader)",[52,7750,7751],{},"How they would use the system",[52,7753,7754],{},"Scope of testing (entire site? or certain logical flows?)",[52,7756,7757],{},"How will the tests be run? (automated, manual, both)",[52,7759,7760],{},"How often will the tests be run?",[52,7762,7763],{},"Who will run them? (pipeline, STE, developer?)",[11,7765,7766],{},"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,7768,7769],{},"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.",[701,7771,7773],{"id":7772},"example-requirements-for-minimum-accessibility-test-plan","Example requirements for minimum accessibility test plan",[49,7775,7776,7784,7792,7800,7808,7816,7819],{},[52,7777,7778,7779,260],{},"Each page should have a relevant title (",[18,7780,7783],{"href":7781,"rel":7782},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1\u002Fdocument-title",[7718],"Axe rule 42",[52,7785,7786,7787,260],{},"Each page should have relevant, to the content, section headers (",[18,7788,7791],{"href":7789,"rel":7790},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1\u002Fpage-has-heading-one",[7718],"Axe rule 73",[52,7793,7794,7795,260],{},"Verify text has proper color contrast (",[18,7796,7799],{"href":7797,"rel":7798},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1\u002Fcolor-contrast",[7718],"Axe rule 83",[52,7801,7802,7803,260],{},"Verify site is navigable by keyboard alone (",[18,7804,7807],{"href":7805,"rel":7806},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1\u002Ftabindex",[7718],"Axe rule 46",[52,7809,7810,7811,260],{},"Verify images have descriptive alternative text (",[18,7812,7815],{"href":7813,"rel":7814},"https:\u002F\u002Fdequeuniversity.com\u002Frules\u002Faxe\u002F4.1\u002Fimage-alt",[7718],"Axe rule 64",[52,7817,7818],{},"Ensure forms can be filled in by keyboard alone",[52,7820,7821],{},"Ensure forms are not confusing when navigated with screen reading software",[11,7823,7824],{},"Once you've established an agreed upon minimum accessibility test plan you can turn them into automated accessibility test cases.",[41,7826,7828],{"id":7827},"automating-your-accessibility-test-plan","Automating your Accessibility Test Plan",[11,7830,7831],{},"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,7833,7834,7835,244],{},"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 ",[18,7836,7838],{"href":7837},".\u002Faccessibility-testing-with-nightwatchjs","accessibility testing with Nightwatch.js",[11,7840,7841],{},"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,7843,7844],{},"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,7846,7847],{},"You can accomplish running a subset of accessibility assertions using two different ways.",[49,7849,7850,7853],{},[52,7851,7852],{},"Disable specific tests from the axe ruleset",[52,7854,7855],{},"Run only specific rules from the ruleset",[11,7857,7858],{},"The former more aligns with the minimum accessibility test plan approach. Below are both examples.",[701,7860,7862],{"id":7861},"passing-rules-to-exclude-or-disable","Passing rules to exclude or disable",[86,7864,7868],{"className":7865,"code":7866,"language":7867,"meta":92,"style":92},"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",[24,7869,7870,7894,7899,7918,7928,7947,7956,7970,7980,7985,7997,8005,8010,8015,8022,8033],{"__ignoreMap":92},[96,7871,7872,7874,7877,7879,7882,7885,7887,7890,7892],{"class":98,"line":99},[96,7873,165],{"class":158},[96,7875,7876],{"class":106},"Run everything except contrast and region",[96,7878,165],{"class":158},[96,7880,7881],{"class":142},": ",[96,7883,7884],{"class":216},"function",[96,7886,1530],{"class":138},[96,7888,7889],{"class":223},"browser",[96,7891,260],{"class":138},[96,7893,233],{"class":138},[96,7895,7896],{"class":98,"line":171},[96,7897,7898],{"class":142},"        browser\n",[96,7900,7901,7904,7907,7909,7911,7914,7916],{"class":98,"line":190},[96,7902,7903],{"class":138},"            .",[96,7905,7906],{"class":200},"url",[96,7908,204],{"class":250},[96,7910,165],{"class":158},[96,7912,7913],{"class":106},"@homePage",[96,7915,165],{"class":158},[96,7917,481],{"class":250},[96,7919,7920,7922,7925],{"class":98,"line":197},[96,7921,7903],{"class":138},[96,7923,7924],{"class":200},"axeInject",[96,7926,7927],{"class":250},"()\n",[96,7929,7930,7932,7935,7937,7939,7941,7943,7945],{"class":98,"line":236},[96,7931,7903],{"class":138},[96,7933,7934],{"class":200},"axeRun",[96,7936,204],{"class":250},[96,7938,165],{"class":158},[96,7940,2577],{"class":106},[96,7942,165],{"class":158},[96,7944,146],{"class":138},[96,7946,233],{"class":138},[96,7948,7949,7952,7954],{"class":98,"line":265},[96,7950,7951],{"class":250},"                rules",[96,7953,728],{"class":138},[96,7955,233],{"class":138},[96,7957,7958,7961,7964,7966,7968],{"class":98,"line":309},[96,7959,7960],{"class":158},"                    '",[96,7962,394],{"class":7963},"sqmHM",[96,7965,165],{"class":158},[96,7967,728],{"class":138},[96,7969,233],{"class":138},[96,7971,7972,7975,7977],{"class":98,"line":337},[96,7973,7974],{"class":250},"                        enabled",[96,7976,728],{"class":138},[96,7978,7979],{"class":4627}," false\n",[96,7981,7982],{"class":98,"line":1149},[96,7983,7984],{"class":138},"                    },\n",[96,7986,7987,7989,7991,7993,7995],{"class":98,"line":1167},[96,7988,7960],{"class":158},[96,7990,939],{"class":7963},[96,7992,165],{"class":158},[96,7994,728],{"class":138},[96,7996,233],{"class":138},[96,7998,7999,8001,8003],{"class":98,"line":1182},[96,8000,7974],{"class":250},[96,8002,728],{"class":138},[96,8004,7979],{"class":4627},[96,8006,8007],{"class":98,"line":1204},[96,8008,8009],{"class":138},"                    }\n",[96,8011,8012],{"class":98,"line":1226},[96,8013,8014],{"class":138},"                }\n",[96,8016,8017,8020],{"class":98,"line":1234},[96,8018,8019],{"class":138},"            }",[96,8021,481],{"class":250},[96,8023,8024,8026,8029,8031],{"class":98,"line":1240},[96,8025,7903],{"class":138},[96,8027,8028],{"class":200},"end",[96,8030,304],{"class":250},[96,8032,168],{"class":138},[96,8034,8035],{"class":98,"line":2227},[96,8036,7535],{"class":138},[701,8038,8040],{"id":8039},"passing-specific-rules-to-include","Passing specific rules to include",[86,8042,8044],{"className":7865,"code":8043,"language":7867,"meta":92,"style":92},"'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",[24,8045,8046,8067,8071,8087,8095,8113,8138,8144,8154],{"__ignoreMap":92},[96,8047,8048,8050,8053,8055,8057,8059,8061,8063,8065],{"class":98,"line":99},[96,8049,165],{"class":158},[96,8051,8052],{"class":106},"Run these rules only",[96,8054,165],{"class":158},[96,8056,7881],{"class":142},[96,8058,7884],{"class":216},[96,8060,1530],{"class":138},[96,8062,7889],{"class":223},[96,8064,260],{"class":138},[96,8066,233],{"class":138},[96,8068,8069],{"class":98,"line":171},[96,8070,7898],{"class":142},[96,8072,8073,8075,8077,8079,8081,8083,8085],{"class":98,"line":190},[96,8074,7903],{"class":138},[96,8076,7906],{"class":200},[96,8078,204],{"class":250},[96,8080,165],{"class":158},[96,8082,7913],{"class":106},[96,8084,165],{"class":158},[96,8086,481],{"class":250},[96,8088,8089,8091,8093],{"class":98,"line":197},[96,8090,7903],{"class":138},[96,8092,7924],{"class":200},[96,8094,7927],{"class":250},[96,8096,8097,8099,8101,8103,8105,8107,8109,8111],{"class":98,"line":236},[96,8098,7903],{"class":138},[96,8100,7934],{"class":200},[96,8102,204],{"class":250},[96,8104,165],{"class":158},[96,8106,2577],{"class":106},[96,8108,165],{"class":158},[96,8110,146],{"class":138},[96,8112,233],{"class":138},[96,8114,8115,8118,8120,8122,8124,8126,8128,8130,8132,8134,8136],{"class":98,"line":265},[96,8116,8117],{"class":250},"                runOnly",[96,8119,728],{"class":138},[96,8121,1021],{"class":250},[96,8123,165],{"class":158},[96,8125,394],{"class":106},[96,8127,165],{"class":158},[96,8129,146],{"class":138},[96,8131,159],{"class":158},[96,8133,789],{"class":106},[96,8135,165],{"class":158},[96,8137,2859],{"class":250},[96,8139,8140,8142],{"class":98,"line":309},[96,8141,8019],{"class":138},[96,8143,481],{"class":250},[96,8145,8146,8148,8150,8152],{"class":98,"line":337},[96,8147,7903],{"class":138},[96,8149,8028],{"class":200},[96,8151,304],{"class":250},[96,8153,168],{"class":138},[96,8155,8156],{"class":98,"line":1149},[96,8157,7535],{"class":138},[11,8159,8160,8161,8165,8166,244],{},"The full list of rule names can be found in the ",[18,8162,8164],{"href":821,"rel":8163},[7718],"axe core rule descriptions",". To learn more about configuring your test project to run Nightwatch.js with nightwatch-axe-verbose watch this video on ",[18,8167,8170],{"href":8168,"rel":8169},"https:\u002F\u002Fyoutu.be\u002FnSodkqB-838",[7718],"Nightwatch.js accessibility testing",[701,8172,8174],{"id":8173},"test-suite-style-considerations","Test suite style considerations",[11,8176,8177],{},"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,8179,8180],{},"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,8182,8183],{},"This provides you with a lot of assertion coverage using fewer tests.",[11,8185,8186],{},"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,8188,8189],{},"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.",[86,8191,8195],{"className":8192,"code":8193,"language":8194,"meta":92,"style":92},"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",[24,8196,8197,8219,8264,8277],{"__ignoreMap":92},[96,8198,8199,8202,8205,8208,8211,8214,8217],{"class":98,"line":99},[96,8200,8201],{"class":102},"√",[96,8203,8204],{"class":106}," Passed",[96,8206,8207],{"class":142}," [ok]: aXe rule: region (",[96,8209,8210],{"class":102},"296",[96,8212,8213],{"class":106}," elements",[96,8215,8216],{"class":106}," checked",[96,8218,481],{"class":142},[96,8220,8221,8224,8227,8230,8233,8236,8239,8242,8245,8247,8249,8252,8255,8258,8261],{"class":98,"line":171},[96,8222,8223],{"class":102},"×",[96,8225,8226],{"class":106}," Failed",[96,8228,8229],{"class":142}," [fail]: (",[96,8231,8232],{"class":102},"aXe",[96,8234,8235],{"class":106}," rule:",[96,8237,8238],{"class":106}," region",[96,8240,8241],{"class":106}," -",[96,8243,8244],{"class":106}," All",[96,8246,224],{"class":106},[96,8248,4974],{"class":106},[96,8250,8251],{"class":106}," must",[96,8253,8254],{"class":106}," be",[96,8256,8257],{"class":106}," contained",[96,8259,8260],{"class":106}," by",[96,8262,8263],{"class":106}," landmarks\n",[96,8265,8266,8269,8272,8275],{"class":98,"line":190},[96,8267,8268],{"class":102},"        In",[96,8270,8271],{"class":106}," element:",[96,8273,8274],{"class":106}," h1",[96,8276,481],{"class":142},[96,8278,8279],{"class":98,"line":197},[96,8280,8281],{"class":3977},"...\n",[11,8283,8284],{},"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.",[86,8286,8288],{"className":7865,"code":8287,"language":7867,"meta":92,"style":92},"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",[24,8289,8290,8304,8322,8347,8352,8374,8379,8405,8409,8430,8434,8442,8461,8478,8485,8495,8499,8520,8524,8532,8550,8586,8598,8604,8614,8619],{"__ignoreMap":92},[96,8291,8292,8295,8297,8300,8302],{"class":98,"line":99},[96,8293,8294],{"class":1963},"module",[96,8296,244],{"class":138},[96,8298,8299],{"class":1963},"exports",[96,8301,276],{"class":275},[96,8303,233],{"class":138},[96,8305,8306,8309,8311,8314,8316,8318,8320],{"class":98,"line":171},[96,8307,8308],{"class":200},"        beforeEach",[96,8310,728],{"class":138},[96,8312,8313],{"class":216}," function",[96,8315,1530],{"class":138},[96,8317,7889],{"class":223},[96,8319,260],{"class":138},[96,8321,233],{"class":138},[96,8323,8324,8327,8329,8331,8333,8336,8338,8340,8343,8345],{"class":98,"line":190},[96,8325,8326],{"class":142},"            browser",[96,8328,244],{"class":138},[96,8330,6331],{"class":142},[96,8332,244],{"class":138},[96,8334,8335],{"class":200},"login",[96,8337,304],{"class":250},[96,8339,244],{"class":138},[96,8341,8342],{"class":200},"navigate",[96,8344,304],{"class":250},[96,8346,168],{"class":138},[96,8348,8349],{"class":98,"line":197},[96,8350,8351],{"class":138},"        },\n",[96,8353,8354,8357,8360,8362,8364,8366,8368,8370,8372],{"class":98,"line":236},[96,8355,8356],{"class":158},"        '",[96,8358,8359],{"class":7963},"Login page has descriptive title",[96,8361,165],{"class":158},[96,8363,728],{"class":138},[96,8365,8313],{"class":216},[96,8367,1530],{"class":138},[96,8369,7889],{"class":223},[96,8371,260],{"class":138},[96,8373,233],{"class":138},[96,8375,8376],{"class":98,"line":265},[96,8377,8378],{"class":142},"            browser\n",[96,8380,8381,8384,8387,8389,8392,8394,8396,8399,8401,8403],{"class":98,"line":309},[96,8382,8383],{"class":138},"                .",[96,8385,8386],{"class":142},"assert",[96,8388,244],{"class":138},[96,8390,8391],{"class":200},"title",[96,8393,204],{"class":250},[96,8395,165],{"class":158},[96,8397,8398],{"class":106},"Please login to BizCorp",[96,8400,165],{"class":158},[96,8402,260],{"class":250},[96,8404,168],{"class":138},[96,8406,8407],{"class":98,"line":337},[96,8408,8351],{"class":138},[96,8410,8411,8413,8416,8418,8420,8422,8424,8426,8428],{"class":98,"line":1149},[96,8412,8356],{"class":158},[96,8414,8415],{"class":7963},"Logo has alt text",[96,8417,165],{"class":158},[96,8419,728],{"class":138},[96,8421,8313],{"class":216},[96,8423,1530],{"class":138},[96,8425,7889],{"class":223},[96,8427,260],{"class":138},[96,8429,233],{"class":138},[96,8431,8432],{"class":98,"line":1167},[96,8433,8378],{"class":142},[96,8435,8436,8438,8440],{"class":98,"line":1182},[96,8437,8383],{"class":138},[96,8439,7924],{"class":200},[96,8441,7927],{"class":250},[96,8443,8444,8446,8448,8450,8452,8455,8457,8459],{"class":98,"line":1204},[96,8445,8383],{"class":138},[96,8447,7934],{"class":200},[96,8449,204],{"class":250},[96,8451,165],{"class":158},[96,8453,8454],{"class":106},"#mainLogo",[96,8456,165],{"class":158},[96,8458,146],{"class":138},[96,8460,233],{"class":138},[96,8462,8463,8466,8468,8470,8472,8474,8476],{"class":98,"line":1226},[96,8464,8465],{"class":250},"                    runOnly",[96,8467,728],{"class":138},[96,8469,1021],{"class":250},[96,8471,165],{"class":158},[96,8473,789],{"class":106},[96,8475,165],{"class":158},[96,8477,2859],{"class":250},[96,8479,8480,8483],{"class":98,"line":1234},[96,8481,8482],{"class":138},"                }",[96,8484,481],{"class":250},[96,8486,8487,8489,8491,8493],{"class":98,"line":1240},[96,8488,8383],{"class":138},[96,8490,8028],{"class":200},[96,8492,304],{"class":250},[96,8494,168],{"class":138},[96,8496,8497],{"class":98,"line":2227},[96,8498,8351],{"class":138},[96,8500,8501,8503,8506,8508,8510,8512,8514,8516,8518],{"class":98,"line":2233},[96,8502,8356],{"class":158},[96,8504,8505],{"class":7963},"Login page has accessible headers",[96,8507,165],{"class":158},[96,8509,728],{"class":138},[96,8511,8313],{"class":216},[96,8513,1530],{"class":138},[96,8515,7889],{"class":223},[96,8517,260],{"class":138},[96,8519,233],{"class":138},[96,8521,8522],{"class":98,"line":2254},[96,8523,8378],{"class":142},[96,8525,8526,8528,8530],{"class":98,"line":2275},[96,8527,8383],{"class":138},[96,8529,7924],{"class":200},[96,8531,7927],{"class":250},[96,8533,8534,8536,8538,8540,8542,8544,8546,8548],{"class":98,"line":2296},[96,8535,8383],{"class":138},[96,8537,7934],{"class":200},[96,8539,204],{"class":250},[96,8541,165],{"class":158},[96,8543,2577],{"class":106},[96,8545,165],{"class":158},[96,8547,146],{"class":138},[96,8549,233],{"class":138},[96,8551,8552,8554,8556,8558,8560,8563,8565,8567,8569,8571,8573,8575,8577,8579,8581,8583],{"class":98,"line":2309},[96,8553,8465],{"class":250},[96,8555,728],{"class":138},[96,8557,1021],{"class":250},[96,8559,165],{"class":158},[96,8561,8562],{"class":106},"empty-heading",[96,8564,165],{"class":158},[96,8566,146],{"class":138},[96,8568,159],{"class":158},[96,8570,936],{"class":106},[96,8572,165],{"class":158},[96,8574,146],{"class":138},[96,8576,159],{"class":158},[96,8578,780],{"class":106},[96,8580,165],{"class":158},[96,8582,146],{"class":138},[96,8584,8585],{"class":250}," \n",[96,8587,8588,8591,8594,8596],{"class":98,"line":2315},[96,8589,8590],{"class":158},"                             '",[96,8592,8593],{"class":106},"p-as-heading",[96,8595,165],{"class":158},[96,8597,2859],{"class":250},[96,8599,8600,8602],{"class":98,"line":2331},[96,8601,8482],{"class":138},[96,8603,481],{"class":250},[96,8605,8606,8608,8610,8612],{"class":98,"line":2337},[96,8607,8383],{"class":138},[96,8609,8028],{"class":200},[96,8611,304],{"class":250},[96,8613,168],{"class":138},[96,8615,8616],{"class":98,"line":2358},[96,8617,8618],{"class":138},"        }\n",[96,8620,8621],{"class":98,"line":2372},[96,8622,1815],{"class":138},[11,8624,8625],{},"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.",[701,8627,8629],{"id":8628},"dont-forget-state-changes","Don't forget state changes",[11,8631,8632],{},"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,8634,8635],{},"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,8637,8638,8639,8642],{},"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 ",[24,8640,8641],{},"this field is required"," without visual context may be confusing.",[41,8644,8646],{"id":8645},"workflow-considerations-do-i-file-a-defect","Workflow considerations - Do I file a defect?",[11,8648,8649],{},"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,8651,8652],{},"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,8654,8655],{},"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,8657,8658],{},"However, it should be considered a defect if the automated accessibility tests catch a regression after the requirements are in place.",[11,8660,8661,8662,8665],{},"Any ",[606,8663,8664],{},"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,8667,8668],{},"A11y violations on these new features not caught in development should be treated as defects.",[41,8670,8672],{"id":8671},"final-thoughts","Final Thoughts",[11,8674,8675],{},"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,8677,8678],{},"Use automated tests to efficiently scan your site for violations against your accessibility test plan and to prevent from backsliding with regression defects.",[11,8680,8681,8682,244],{},"For the Playwright version of this approach, including how to disable, exclude, and phase in axe rules on an existing site, see ",[18,8683,5],{"href":2992},[11,8685,8686],{},"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,8688,8689,8690],{},"I've posted the a working example of what I described in this article on my GitHub\n👉 ",[18,8691,8694],{"href":8692,"rel":8693},"https:\u002F\u002Fgithub.com\u002Freallymello\u002FnightwatchTutorials\u002Ftree\u002Fmaster\u002Fa11yTestPlanExample",[7718],"Implementing a minimum accessibility test plan using Nightwatch, aXe, and nightwatch-axe-verbose tutorial",[11,8696,8697,8698,8703],{},"To stay in touch, subscribe to the ",[18,8699,8702],{"href":8700,"rel":8701},"https:\u002F\u002Fyoutube.com\u002Fplaylist?list=PLLS_Ef55N6hmkt3-JlW40GAGpXSlp8t_D",[7718],"reallyMello channel on YouTube"," and check our my social links below. Thanks!",[2956,8705],{":items":8706},"[\"\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Faccessibility-testing-with-nightwatchjs\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\"]",[2960,8708,8709],{},"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":92,"searchDepth":171,"depth":171,"links":8711},[8712,8713,8716,8722,8723],{"id":7696,"depth":171,"text":7697},{"id":7730,"depth":171,"text":7731,"children":8714},[8715],{"id":7772,"depth":190,"text":7773},{"id":7827,"depth":171,"text":7828,"children":8717},[8718,8719,8720,8721],{"id":7861,"depth":190,"text":7862},{"id":8039,"depth":190,"text":8040},{"id":8173,"depth":190,"text":8174},{"id":8628,"depth":190,"text":8629},{"id":8645,"depth":171,"text":8646},{"id":8671,"depth":171,"text":8672},"\u002Fimages\u002Fposts\u002Fimplementing-a-minimum-accessibility-test-plan\u002Fa11y-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.",{},{"title":21,"description":8726},{"loc":20},"software-testing\u002Fframeworks\u002Fnightwatch\u002Fimplementing-a-minimum-accessibility-test-plan","6I9PDq22suewZ4MwM6v-g9nPSvgjlHduVyRfns5xySA",{"id":8733,"title":8734,"bmcUsername":6,"body":8735,"cover":12478,"date":12479,"description":12480,"draft":2989,"extension":2990,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":12481,"navigation":193,"npmPackage":6,"order":6,"path":360,"review":6,"seo":12482,"sitemap":12483,"stem":12484,"updated":6,"__hash__":12485},"content\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright.md","Run Parallel and Serial Tests Together in Playwright",{"type":8,"value":8736,"toc":12457},[8737,8740,8743,8750,8753,8756,8760,8792,8795,8803,8807,8818,8978,8989,9318,9321,9325,9328,9357,9371,9377,9386,9390,9397,9411,9657,9671,9686,9708,9711,9715,9723,9852,9950,9960,9973,9977,9990,10082,10089,10260,10264,10277,10282,10289,10292,10474,10481,10484,10488,10497,10645,10648,10773,10783,10786,10837,10840,10848,10852,10855,10858,10861,10875,11394,11408,11449,11462,11468,11668,11674,11695,11699,11702,11705,11708,11743,11746,11750,11867,11873,11877,11880,11892,11896,11903,12034,12038,12050,12159,12165,12175,12403,12406,12412,12416,12422,12444,12451,12454],[11,8738,8739],{},"Modern test frameworks like Playwright allow you to run your tests in parallel instead of serially, allowing for faster feedback loops in your automated test runs. Serial test execution is many times slower than parallel runs because you are running tests one at a time. Every test you add slows your test pipeline down by the duration of that test. Parallel execution of your test automation lets you concurrently execute tests, where, with enough workers, your test pipeline would only be as slow as the longest-running test in the test suite.",[11,8741,8742],{},"This article covers a common problem that keeps test suites from running fully in parallel: tests that change global state to set up preconditions, which breaks other tests running at the same time that expect different preconditions.",[11,8744,8745,8746,8749],{},"This is what I am currently dealing with. Our test framework before Playwright had one big limitation: everything ran serially, or across several isolated test environments. Some tests changed global settings in ",[24,8747,8748],{},"web.config",". Others alter global state by moving the server date to set up a precondition, like a billing rollover at month end. Run any of those next to another test and you break the other test. The entire legacy test suite was built around serial execution concepts that led to inefficient feedback loops that slowed down the Software Development Life Cycle (SDLC).",[11,8751,8752],{},"While modernizing and converting the legacy tests to Playwright, I was able to rework most scenarios to run in parallel without collisions or flakiness, but a few scenarios still required changing global state settings on the server, and they couldn't stay in the suite without making all the tests run serially.",[11,8754,8755],{},"Then I learned Playwright projects can separate parallel-safe tests from serial-only ones and run the serial ones on their own.",[41,8757,8759],{"id":8758},"three-ways-to-run-serial-and-parallel-tests-together","Three Ways to Run Serial and Parallel Tests Together",[49,8761,8762,8772,8782],{},[52,8763,8764,8771],{},[55,8765,8766,8770],{},[18,8767,8769],{"href":8768},"#option-a-playwright-project-dependencies-the-quick-win","Option A, project dependencies"," (the quick win):"," Split the tests into two projects and let Playwright run the serial one after the parallel one passes. It's a few lines of config and no CI changes, but there's a catch: one failing parallel test means the serial tests never run.",[52,8773,8774,8781],{},[55,8775,8776,8780],{},[18,8777,8779],{"href":8778},"#option-b-separate-ci-steps-for-playwright-projects-fixing-the-catch","Option B, separate CI steps"," (fixes the catch):"," The same split, with each project run as its own CI step. It takes more wiring, but you get results from both suites on every run.",[52,8783,8784,8791],{},[55,8785,8786,8790],{},[18,8787,8789],{"href":8788},"#option-c-preconfigured-test-environments-my-long-term-goal","Option C, preconfigured environments"," (my long-term goal):"," Stop changing settings from inside the tests. Stand up environments already configured for each precondition and point each test group at its own. It's the fastest and cleanest, but it isn't always feasible because of hosting cost and how easily you can create environments.",[11,8793,8794],{},"Options A and B split the tests the same way, by filename or by tag.",[11,8796,8797,8798,8802],{},"I built a small demo of every approach in this article. It's in the ",[391,8799],{"href":8800,"text":8801},"https:\u002F\u002Fgithub.com\u002Freallymello\u002Fplaywright-serial-parallel-dependencies-demo","playwright-serial-parallel-dependencies-demo"," repo, and the results below come from running it.",[41,8804,8806],{"id":8805},"demoing-the-problem-playwright-tests-that-change-shared-state","Demoing the Problem: Playwright Tests That Change Shared State",[11,8808,8809,8810,8813,8814,8817],{},"The demo uses one shared JSON file as a stand-in for global server state, holding a feature flag and a \"server date\". The ",[24,8811,8812],{},"parallel-safe"," tests in the demo project only read it and expect the defaults. The ",[24,8815,8816],{},"serial-only"," tests change its state to set up a precondition, then reset it afterward:",[86,8819,8822],{"className":124,"code":8820,"filename":8821,"language":127,"meta":92,"style":92},"test.afterEach(async () => {\n  await resetSettings();\n});\n\ntest('server date set to month end triggers billing rollover', async () => {\n  await writeSettings({ featureFlag: 'off', serverDate: '2026-01-31' });\n  expect((await readSettings()).serverDate).toBe('2026-01-31');\n});\n","tests\u002Fserver-settings.serial.spec.ts",[24,8823,8824,8844,8855,8863,8867,8890,8933,8970],{"__ignoreMap":92},[96,8825,8826,8828,8830,8833,8835,8838,8840,8842],{"class":98,"line":99},[96,8827,201],{"class":142},[96,8829,244],{"class":138},[96,8831,8832],{"class":200},"afterEach",[96,8834,204],{"class":142},[96,8836,8837],{"class":216},"async",[96,8839,1095],{"class":138},[96,8841,230],{"class":216},[96,8843,233],{"class":138},[96,8845,8846,8848,8851,8853],{"class":98,"line":171},[96,8847,239],{"class":134},[96,8849,8850],{"class":200}," resetSettings",[96,8852,304],{"class":250},[96,8854,168],{"class":138},[96,8856,8857,8859,8861],{"class":98,"line":190},[96,8858,340],{"class":138},[96,8860,260],{"class":142},[96,8862,168],{"class":138},[96,8864,8865],{"class":98,"line":197},[96,8866,194],{"emptyLinePlaceholder":193},[96,8868,8869,8871,8873,8875,8878,8880,8882,8884,8886,8888],{"class":98,"line":236},[96,8870,201],{"class":200},[96,8872,204],{"class":142},[96,8874,165],{"class":158},[96,8876,8877],{"class":106},"server date set to month end triggers billing rollover",[96,8879,165],{"class":158},[96,8881,146],{"class":138},[96,8883,217],{"class":216},[96,8885,1095],{"class":138},[96,8887,230],{"class":216},[96,8889,233],{"class":138},[96,8891,8892,8894,8897,8899,8901,8904,8906,8908,8911,8913,8915,8918,8920,8922,8925,8927,8929,8931],{"class":98,"line":265},[96,8893,239],{"class":134},[96,8895,8896],{"class":200}," writeSettings",[96,8898,204],{"class":250},[96,8900,290],{"class":138},[96,8902,8903],{"class":250}," featureFlag",[96,8905,728],{"class":138},[96,8907,159],{"class":158},[96,8909,8910],{"class":106},"off",[96,8912,165],{"class":158},[96,8914,146],{"class":138},[96,8916,8917],{"class":250}," serverDate",[96,8919,728],{"class":138},[96,8921,159],{"class":158},[96,8923,8924],{"class":106},"2026-01-31",[96,8926,165],{"class":158},[96,8928,152],{"class":138},[96,8930,260],{"class":250},[96,8932,168],{"class":138},[96,8934,8935,8937,8940,8942,8945,8947,8949,8952,8954,8956,8958,8960,8962,8964,8966,8968],{"class":98,"line":309},[96,8936,312],{"class":200},[96,8938,8939],{"class":250},"((",[96,8941,6888],{"class":134},[96,8943,8944],{"class":200}," readSettings",[96,8946,6898],{"class":250},[96,8948,244],{"class":138},[96,8950,8951],{"class":142},"serverDate",[96,8953,260],{"class":250},[96,8955,244],{"class":138},[96,8957,4622],{"class":200},[96,8959,204],{"class":250},[96,8961,165],{"class":158},[96,8963,8924],{"class":106},[96,8965,165],{"class":158},[96,8967,260],{"class":250},[96,8969,168],{"class":138},[96,8971,8972,8974,8976],{"class":98,"line":337},[96,8973,340],{"class":138},[96,8975,260],{"class":142},[96,8977,168],{"class":138},[11,8979,8980,8981,8984,8985,8988],{},"To see the shared state collision problem you can run ",[24,8982,8983],{},"npm run test:collision"," which evaluates to ",[24,8986,8987],{},"playwright test -c playwright.collision.config.ts",". The output below is trimmed to the first failure's details.",[86,8990,8994],{"className":8991,"code":8992,"filename":90,"language":8993,"meta":92,"style":92},"language-ansi shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","npm run test:collision\n\n> playwright-serial-parallel-dependencies-demo@1.0.0 test:collision\n> playwright test -c playwright.collision.config.ts\n\n\n\u001b[2mRunning \u001b[22m11\u001b[2m tests using \u001b[22m5\u001b[2m workers\u001b[22m\n\n  \u001b[32m✓\u001b[39m  \u001b[2m 1 \u001b[22mtests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 4 uses default settings\u001b[2m (319ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 2 \u001b[22mtests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 1 uses default settings\u001b[2m (319ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 3 \u001b[22mtests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 3 uses default settings\u001b[2m (318ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 4 \u001b[22mtests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 2 uses default settings\u001b[2m (312ms)\u001b[22m\n  \u001b[32m-\u001b[39m  \u001b[2m 9 \u001b[22m\u001b[36mtests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)\u001b[39m\n  \u001b[32m✓\u001b[39m  \u001b[2m 5 \u001b[22mtests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 1 renders with default settings\u001b[2m (311ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m 8 \u001b[22m\u001b[31mtests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings\u001b[39m\u001b[2m (305ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m 7 \u001b[22m\u001b[31mtests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings\u001b[39m\u001b[2m (305ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m 6 \u001b[22m\u001b[31mtests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings\u001b[39m\u001b[2m (307ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m10 \u001b[22mtests\\server-settings.serial.spec.ts:13:5 › feature flag on enables new checkout\u001b[2m (515ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m11 \u001b[22mtests\\server-settings.serial.spec.ts:19:5 › server date set to month end triggers billing rollover\u001b[2m (512ms)\u001b[22m\n\n\n\u001b[31m  1) tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings \u001b[2m──\u001b[22m\u001b[39m\n\n    Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) \u002F\u002F Object.is equality\u001b[22m\n\n    Expected: \u001b[32m\"o\u001b[7mff\u001b[27m\"\u001b[39m\n    Received: \u001b[31m\"o\u001b[7mn\u001b[27m\"\u001b[39m\n\n    ...\n\n\u001b[31m  3 failed\u001b[39m\n\u001b[31m    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings \u001b[2m───\u001b[22m\u001b[39m\n\u001b[31m    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings \u001b[2m───\u001b[22m\u001b[39m\n\u001b[31m    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings \u001b[2m───\u001b[22m\u001b[39m\n\u001b[33m  1 skipped\u001b[39m\n\u001b[32m  7 passed\u001b[39m\u001b[2m (1.7s)\u001b[22m\n","ansi",[24,8995,8996,9002,9006,9011,9016,9020,9024,9042,9046,9061,9073,9086,9099,9111,9124,9139,9151,9164,9177,9190,9194,9198,9207,9211,9234,9238,9252,9264,9268,9273,9277,9282,9290,9297,9304,9310],{"__ignoreMap":92},[96,8997,8998],{"class":98,"line":99},[96,8999,9001],{"class":9000},"sOsy2","npm run test:collision\n",[96,9003,9004],{"class":98,"line":171},[96,9005,194],{"emptyLinePlaceholder":193},[96,9007,9008],{"class":98,"line":190},[96,9009,9010],{"class":9000},"> playwright-serial-parallel-dependencies-demo@1.0.0 test:collision\n",[96,9012,9013],{"class":98,"line":197},[96,9014,9015],{"class":9000},"> playwright test -c playwright.collision.config.ts\n",[96,9017,9018],{"class":98,"line":236},[96,9019,194],{"emptyLinePlaceholder":193},[96,9021,9022],{"class":98,"line":265},[96,9023,194],{"emptyLinePlaceholder":193},[96,9025,9026,9030,9033,9036,9039],{"class":98,"line":309},[96,9027,9029],{"class":9028},"sFDEC","Running ",[96,9031,9032],{"class":9000},"11",[96,9034,9035],{"class":9028}," tests using ",[96,9037,9038],{"class":9000},"5",[96,9040,9041],{"class":9028}," workers\n",[96,9043,9044],{"class":98,"line":337},[96,9045,194],{"emptyLinePlaceholder":193},[96,9047,9048,9052,9055,9058],{"class":98,"line":1149},[96,9049,9051],{"class":9050},"ssIYW","  ✓",[96,9053,9054],{"class":9028},"   1 ",[96,9056,9057],{"class":9000},"tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 4 uses default settings",[96,9059,9060],{"class":9028}," (319ms)\n",[96,9062,9063,9065,9068,9071],{"class":98,"line":1167},[96,9064,9051],{"class":9050},[96,9066,9067],{"class":9028},"   2 ",[96,9069,9070],{"class":9000},"tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 1 uses default settings",[96,9072,9060],{"class":9028},[96,9074,9075,9077,9080,9083],{"class":98,"line":1182},[96,9076,9051],{"class":9050},[96,9078,9079],{"class":9028},"   3 ",[96,9081,9082],{"class":9000},"tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 3 uses default settings",[96,9084,9085],{"class":9028}," (318ms)\n",[96,9087,9088,9090,9093,9096],{"class":98,"line":1204},[96,9089,9051],{"class":9050},[96,9091,9092],{"class":9028},"   4 ",[96,9094,9095],{"class":9000},"tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 2 uses default settings",[96,9097,9098],{"class":9028}," (312ms)\n",[96,9100,9101,9104,9107],{"class":98,"line":1226},[96,9102,9103],{"class":9050},"  -",[96,9105,9106],{"class":9028},"   9 ",[96,9108,9110],{"class":9109},"sCdEk","tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)\n",[96,9112,9113,9115,9118,9121],{"class":98,"line":1234},[96,9114,9051],{"class":9050},[96,9116,9117],{"class":9028},"   5 ",[96,9119,9120],{"class":9000},"tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 1 renders with default settings",[96,9122,9123],{"class":9028}," (311ms)\n",[96,9125,9126,9130,9133,9136],{"class":98,"line":1240},[96,9127,9129],{"class":9128},"sDbPz","  ✘",[96,9131,9132],{"class":9028},"   8 ",[96,9134,9135],{"class":9128},"tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings",[96,9137,9138],{"class":9028}," (305ms)\n",[96,9140,9141,9143,9146,9149],{"class":98,"line":2227},[96,9142,9129],{"class":9128},[96,9144,9145],{"class":9028},"   7 ",[96,9147,9148],{"class":9128},"tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings",[96,9150,9138],{"class":9028},[96,9152,9153,9155,9158,9161],{"class":98,"line":2233},[96,9154,9129],{"class":9128},[96,9156,9157],{"class":9028},"   6 ",[96,9159,9160],{"class":9128},"tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings",[96,9162,9163],{"class":9028}," (307ms)\n",[96,9165,9166,9168,9171,9174],{"class":98,"line":2254},[96,9167,9051],{"class":9050},[96,9169,9170],{"class":9028},"  10 ",[96,9172,9173],{"class":9000},"tests\\server-settings.serial.spec.ts:13:5 › feature flag on enables new checkout",[96,9175,9176],{"class":9028}," (515ms)\n",[96,9178,9179,9181,9184,9187],{"class":98,"line":2275},[96,9180,9051],{"class":9050},[96,9182,9183],{"class":9028},"  11 ",[96,9185,9186],{"class":9000},"tests\\server-settings.serial.spec.ts:19:5 › server date set to month end triggers billing rollover",[96,9188,9189],{"class":9028}," (512ms)\n",[96,9191,9192],{"class":98,"line":2296},[96,9193,194],{"emptyLinePlaceholder":193},[96,9195,9196],{"class":98,"line":2309},[96,9197,194],{"emptyLinePlaceholder":193},[96,9199,9200,9203],{"class":98,"line":2315},[96,9201,9202],{"class":9128},"  1) tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings ",[96,9204,9206],{"class":9205},"smSMi","──\n",[96,9208,9209],{"class":98,"line":2331},[96,9210,194],{"emptyLinePlaceholder":193},[96,9212,9213,9216,9219,9222,9224,9226,9228,9231],{"class":98,"line":2337},[96,9214,9215],{"class":9000},"    Error: ",[96,9217,9218],{"class":9028},"expect(",[96,9220,9221],{"class":9128},"received",[96,9223,2885],{"class":9028},[96,9225,4622],{"class":9000},[96,9227,204],{"class":9028},[96,9229,9230],{"class":9050},"expected",[96,9232,9233],{"class":9028},") \u002F\u002F Object.is equality\n",[96,9235,9236],{"class":98,"line":2358},[96,9237,194],{"emptyLinePlaceholder":193},[96,9239,9240,9243,9246,9250],{"class":98,"line":2372},[96,9241,9242],{"class":9000},"    Expected: ",[96,9244,9245],{"class":9050},"\"o",[96,9247,9249],{"class":9248},"sBsy4","ff",[96,9251,2224],{"class":9050},[96,9253,9254,9257,9259,9262],{"class":98,"line":2395},[96,9255,9256],{"class":9000},"    Received: ",[96,9258,9245],{"class":9128},[96,9260,1584],{"class":9261},"sYBWh",[96,9263,2224],{"class":9128},[96,9265,9266],{"class":98,"line":2416},[96,9267,194],{"emptyLinePlaceholder":193},[96,9269,9270],{"class":98,"line":2433},[96,9271,9272],{"class":9000},"    ...\n",[96,9274,9275],{"class":98,"line":2454},[96,9276,194],{"emptyLinePlaceholder":193},[96,9278,9279],{"class":98,"line":2475},[96,9280,9281],{"class":9128},"  3 failed\n",[96,9283,9284,9287],{"class":98,"line":2491},[96,9285,9286],{"class":9128},"    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings ",[96,9288,9289],{"class":9205},"───\n",[96,9291,9292,9295],{"class":98,"line":2510},[96,9293,9294],{"class":9128},"    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings ",[96,9296,9289],{"class":9205},[96,9298,9299,9302],{"class":98,"line":2516},[96,9300,9301],{"class":9128},"    tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings ",[96,9303,9289],{"class":9205},[96,9305,9306],{"class":98,"line":2530},[96,9307,9309],{"class":9308},"spoJ1","  1 skipped\n",[96,9311,9312,9315],{"class":98,"line":2536},[96,9313,9314],{"class":9050},"  7 passed",[96,9316,9317],{"class":9028}," (1.7s)\n",[11,9319,9320],{},"As shown above, running them together in a parallel pool introduces failures when the tests that expect the default state evaluate when the serial tests change the state before the teardown runs. The demo holds the changed state for half a second to make the collision likely, but a real backend process reacting to a changed date, feature flag, or web.config setting fails the same way, just less predictably.",[41,9322,9324],{"id":9323},"serial-means-three-different-things-in-playwright","\"Serial\" Means Three Different Things in Playwright",[11,9326,9327],{},"Playwright has a few different ways of configuring serial runs depending on your use case and intention.",[49,9329,9330,9338,9351],{},[52,9331,9332,9337],{},[55,9333,9334],{},[24,9335,9336],{},"workers: 1"," limits how many tests run at once. It says nothing about order, and one failure doesn't affect the other tests.",[52,9339,9340,9345,9346,9350],{},[55,9341,9342],{},[24,9343,9344],{},"test.describe.configure({ mode: 'serial' })"," groups tests that depend on each other. ",[391,9347],{"href":9348,"text":9349},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-parallel","The Playwright docs"," say: \"If one of the serial tests fails, all subsequent tests are skipped.\" That's right for a chain of dependent steps, and wrong for independent tests that each change some state.",[52,9352,9353,9356],{},[55,9354,9355],{},"A serial project"," is a separate set of tests, selected by a matcher, that runs apart from the parallel-safe set. This is the one that solves the mixed-suite problem.",[11,9358,9359,9360,9362,9363,9366,9367,9370],{},"Even in parallel test runs many of my longer system testing user-journey type scenarios will be broken into individual tests in the same file that need to run serially, sequentially. For example, the first test may create an applicant and then the test after uses that applicant to apply for an account and so on. These test spec files use ",[24,9361,9344],{}," so that the tests in the spec run sequentially and skip tests that appear after the failing test ",[606,9364,9365],{},"in that spec file",". So for my example, if the applicant creation test fails there is no point running the next test where that applicant is used to apply for an account. Playwright will still parallelize the spec files in that project while running tests ",[606,9368,9369],{},"within"," the spec files serially with that setting applied.",[11,9372,9373,9374,9376],{},"Using ",[24,9375,9336],{}," on the project will prevent Playwright from running the spec files in parallel as well.",[11,9378,9379,9380,9382,9383,9385],{},"Options A and B that we will cover in the following article sections put the state-changing tests in a ",[24,9381,8816],{}," project, and ",[24,9384,9336],{}," inside that project makes them run one at a time, serially.",[41,9387,9389],{"id":9388},"split-tests-with-playwright-projects-and-matchers","Split Tests with Playwright Projects and Matchers",[11,9391,9392,9396],{},[391,9393],{"href":9394,"text":9395},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-projects","Playwright projects"," let you run different subsets of your tests with different settings. We will use this feature to determine which tests to run and when in the different solutions we will be showing to solve the parallel + serial test problem.",[11,9398,9399,9400,9403,9404,9407,9408,728],{},"First, name the state-changing files ",[24,9401,9402],{},"*.serial.spec.ts"," and match on that using the ",[24,9405,9406],{},"projects"," setting in ",[24,9409,9410],{},"playwright.config.ts",[86,9412,9414],{"className":124,"code":9413,"filename":9410,"language":127,"meta":92,"style":92},"import { defineConfig } from '@playwright\u002Ftest';\n\nexport default defineConfig({\n  testDir: '.\u002Ftests',\n  projects: [\n    {\n      name: 'parallel-safe',\n      testMatch: \u002F.*\\.spec\\.ts\u002F,\n      testIgnore: \u002F.*\\.serial\\.spec\\.ts\u002F,\n      fullyParallel: true,\n    },\n    {\n      name: 'serial-only',\n      testMatch: \u002F.*\\.serial\\.spec\\.ts\u002F,\n      workers: 1, \u002F\u002F per-project workers requires Playwright 1.52+\n    },\n  ],\n});\n",[24,9415,9416,9437,9441,9457,9473,9482,9487,9502,9531,9561,9573,9578,9582,9596,9624,9638,9642,9649],{"__ignoreMap":92},[96,9417,9418,9420,9422,9425,9427,9429,9431,9433,9435],{"class":98,"line":99},[96,9419,135],{"class":134},[96,9421,139],{"class":138},[96,9423,9424],{"class":142}," defineConfig",[96,9426,152],{"class":138},[96,9428,155],{"class":134},[96,9430,159],{"class":158},[96,9432,162],{"class":106},[96,9434,165],{"class":158},[96,9436,168],{"class":138},[96,9438,9439],{"class":98,"line":171},[96,9440,194],{"emptyLinePlaceholder":193},[96,9442,9443,9446,9449,9451,9454],{"class":98,"line":190},[96,9444,9445],{"class":134},"export",[96,9447,9448],{"class":134}," default",[96,9450,9424],{"class":200},[96,9452,204],{"class":9453},"sLHsM",[96,9455,1107],{"class":9456},"sCRTB",[96,9458,9459,9462,9464,9466,9469,9471],{"class":98,"line":197},[96,9460,9461],{"class":250},"  testDir",[96,9463,728],{"class":138},[96,9465,159],{"class":158},[96,9467,9468],{"class":106},".\u002Ftests",[96,9470,165],{"class":158},[96,9472,1309],{"class":9456},[96,9474,9475,9478,9480],{"class":98,"line":236},[96,9476,9477],{"class":250},"  projects",[96,9479,728],{"class":138},[96,9481,1287],{"class":142},[96,9483,9484],{"class":98,"line":265},[96,9485,9486],{"class":138},"    {\n",[96,9488,9489,9492,9494,9496,9498,9500],{"class":98,"line":309},[96,9490,9491],{"class":250},"      name",[96,9493,728],{"class":138},[96,9495,159],{"class":158},[96,9497,8812],{"class":106},[96,9499,165],{"class":158},[96,9501,1309],{"class":138},[96,9503,9504,9507,9509,9511,9513,9516,9520,9523,9525,9527,9529],{"class":98,"line":337},[96,9505,9506],{"class":250},"      testMatch",[96,9508,728],{"class":138},[96,9510,7101],{"class":158},[96,9512,244],{"class":110},[96,9514,9515],{"class":275},"*",[96,9517,9519],{"class":9518},"szoTG","\\.",[96,9521,9522],{"class":106},"spec",[96,9524,9519],{"class":9518},[96,9526,127],{"class":106},[96,9528,255],{"class":158},[96,9530,1309],{"class":138},[96,9532,9533,9536,9538,9540,9542,9544,9546,9549,9551,9553,9555,9557,9559],{"class":98,"line":1149},[96,9534,9535],{"class":250},"      testIgnore",[96,9537,728],{"class":138},[96,9539,7101],{"class":158},[96,9541,244],{"class":110},[96,9543,9515],{"class":275},[96,9545,9519],{"class":9518},[96,9547,9548],{"class":106},"serial",[96,9550,9519],{"class":9518},[96,9552,9522],{"class":106},[96,9554,9519],{"class":9518},[96,9556,127],{"class":106},[96,9558,255],{"class":158},[96,9560,1309],{"class":138},[96,9562,9563,9566,9568,9571],{"class":98,"line":1167},[96,9564,9565],{"class":250},"      fullyParallel",[96,9567,728],{"class":138},[96,9569,9570],{"class":4627}," true",[96,9572,1309],{"class":138},[96,9574,9575],{"class":98,"line":1182},[96,9576,9577],{"class":138},"    },\n",[96,9579,9580],{"class":98,"line":1204},[96,9581,9486],{"class":138},[96,9583,9584,9586,9588,9590,9592,9594],{"class":98,"line":1226},[96,9585,9491],{"class":250},[96,9587,728],{"class":138},[96,9589,159],{"class":158},[96,9591,8816],{"class":106},[96,9593,165],{"class":158},[96,9595,1309],{"class":138},[96,9597,9598,9600,9602,9604,9606,9608,9610,9612,9614,9616,9618,9620,9622],{"class":98,"line":1234},[96,9599,9506],{"class":250},[96,9601,728],{"class":138},[96,9603,7101],{"class":158},[96,9605,244],{"class":110},[96,9607,9515],{"class":275},[96,9609,9519],{"class":9518},[96,9611,9548],{"class":106},[96,9613,9519],{"class":9518},[96,9615,9522],{"class":106},[96,9617,9519],{"class":9518},[96,9619,127],{"class":106},[96,9621,255],{"class":158},[96,9623,1309],{"class":138},[96,9625,9626,9629,9631,9633,9635],{"class":98,"line":1240},[96,9627,9628],{"class":250},"      workers",[96,9630,728],{"class":138},[96,9632,6588],{"class":1969},[96,9634,146],{"class":138},[96,9636,9637],{"class":1200}," \u002F\u002F per-project workers requires Playwright 1.52+\n",[96,9639,9640],{"class":98,"line":2227},[96,9641,9577],{"class":138},[96,9643,9644,9647],{"class":98,"line":2233},[96,9645,9646],{"class":142},"  ]",[96,9648,1309],{"class":9456},[96,9650,9651,9653,9655],{"class":98,"line":2254},[96,9652,340],{"class":9456},[96,9654,260],{"class":9453},[96,9656,168],{"class":138},[11,9658,9659,9660,9663,9664,9666,9667,9670],{},"Note the use of ",[24,9661,9662],{},"testIgnore",". ",[24,9665,9402],{}," also matches ",[24,9668,9669],{},".*\\.spec\\.ts",", so without it the serial files would land in both projects and run in parallel too.",[11,9672,2862,9673,9675,9676,9679,9680,9683,9684,244],{},[24,9674,8812],{}," project will contain tests that ",[606,9677,9678],{},"play nicely with others",": they can run in parallel without colliding with other tests. There we set ",[24,9681,9682],{},"fullyParallel"," to ",[24,9685,3523],{},[11,9687,2862,9688,9690,9691,9683,9694,9696,9697,9699,9700,9703,9704,9707],{},[24,9689,8816],{}," project will hold the tests that change things that would create failures or flakiness in other tests if run at the same time. There we set the ",[24,9692,9693],{},"workers",[24,9695,426],{}," to ensure only one test runs at a time if they are in that project. These tests that ",[606,9698,3245],{}," play nicely with others will have the file suffix ",[24,9701,9702],{},".serial.spec.ts"," so we can use the ",[24,9705,9706],{},"testMatch"," parameter to find them.",[11,9709,9710],{},"The different solutions we will be going over use the project classifications in different ways to solve the parallel vs. serial testing problem.",[701,9712,9714],{"id":9713},"match-on-tags-instead-of-filenames","Match on Tags Instead of Filenames",[11,9716,9717,9718,9722],{},"A filename convention forces you to keep state-changing tests in their own files. If you'd rather keep related tests together, tag the serial ones (see ",[391,9719],{"href":9720,"text":9721},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-annotations","Playwright test tags",") and match on the tag:",[86,9724,9727],{"className":124,"code":9725,"filename":9726,"language":127,"meta":92,"style":92},"test.describe('checkout with mutated server state', { tag: '@serial' }, () => {\n  test.describe.configure({ mode: 'serial' });\n\n  test('feature flag on enables new checkout', async () => {\n    \u002F\u002F ...\n  });\n});\n","tests-tagged\u002Fcheckout.spec.ts",[24,9728,9729,9770,9804,9808,9831,9836,9844],{"__ignoreMap":92},[96,9730,9731,9733,9735,9737,9739,9741,9744,9746,9748,9750,9753,9755,9757,9760,9762,9764,9766,9768],{"class":98,"line":99},[96,9732,201],{"class":142},[96,9734,244],{"class":138},[96,9736,1746],{"class":200},[96,9738,204],{"class":142},[96,9740,165],{"class":158},[96,9742,9743],{"class":106},"checkout with mutated server state",[96,9745,165],{"class":158},[96,9747,146],{"class":138},[96,9749,139],{"class":138},[96,9751,9752],{"class":250}," tag",[96,9754,728],{"class":138},[96,9756,159],{"class":158},[96,9758,9759],{"class":106},"@serial",[96,9761,165],{"class":158},[96,9763,1123],{"class":138},[96,9765,1095],{"class":138},[96,9767,230],{"class":216},[96,9769,233],{"class":138},[96,9771,9772,9774,9776,9778,9780,9783,9785,9787,9790,9792,9794,9796,9798,9800,9802],{"class":98,"line":171},[96,9773,1741],{"class":142},[96,9775,244],{"class":138},[96,9777,1746],{"class":142},[96,9779,244],{"class":138},[96,9781,9782],{"class":200},"configure",[96,9784,204],{"class":250},[96,9786,290],{"class":138},[96,9788,9789],{"class":250}," mode",[96,9791,728],{"class":138},[96,9793,159],{"class":158},[96,9795,9548],{"class":106},[96,9797,165],{"class":158},[96,9799,152],{"class":138},[96,9801,260],{"class":250},[96,9803,168],{"class":138},[96,9805,9806],{"class":98,"line":190},[96,9807,194],{"emptyLinePlaceholder":193},[96,9809,9810,9812,9814,9816,9819,9821,9823,9825,9827,9829],{"class":98,"line":197},[96,9811,1741],{"class":200},[96,9813,204],{"class":250},[96,9815,165],{"class":158},[96,9817,9818],{"class":106},"feature flag on enables new checkout",[96,9820,165],{"class":158},[96,9822,146],{"class":138},[96,9824,217],{"class":216},[96,9826,1095],{"class":138},[96,9828,230],{"class":216},[96,9830,233],{"class":138},[96,9832,9833],{"class":98,"line":236},[96,9834,9835],{"class":1200},"    \u002F\u002F ...\n",[96,9837,9838,9840,9842],{"class":98,"line":265},[96,9839,1806],{"class":138},[96,9841,260],{"class":250},[96,9843,168],{"class":138},[96,9845,9846,9848,9850],{"class":98,"line":309},[96,9847,340],{"class":138},[96,9849,260],{"class":142},[96,9851,168],{"class":138},[86,9853,9856],{"className":124,"code":9854,"filename":9855,"language":127,"meta":92,"style":92},"projects: [\n  { name: 'parallel-safe', grepInvert: \u002F@serial\u002F, fullyParallel: true },\n  { name: 'serial-only', grep: \u002F@serial\u002F, workers: 1 },\n],\n","playwright.tagged.config.ts",[24,9857,9858,9866,9906,9944],{"__ignoreMap":92},[96,9859,9860,9862,9864],{"class":98,"line":99},[96,9861,9406],{"class":102},[96,9863,728],{"class":138},[96,9865,1287],{"class":142},[96,9867,9868,9871,9873,9875,9877,9879,9881,9883,9886,9888,9890,9892,9894,9896,9899,9901,9903],{"class":98,"line":171},[96,9869,9870],{"class":138},"  {",[96,9872,4391],{"class":250},[96,9874,728],{"class":138},[96,9876,159],{"class":158},[96,9878,8812],{"class":106},[96,9880,165],{"class":158},[96,9882,146],{"class":138},[96,9884,9885],{"class":250}," grepInvert",[96,9887,728],{"class":138},[96,9889,7101],{"class":158},[96,9891,9759],{"class":106},[96,9893,255],{"class":158},[96,9895,146],{"class":138},[96,9897,9898],{"class":250}," fullyParallel",[96,9900,728],{"class":138},[96,9902,9570],{"class":4627},[96,9904,9905],{"class":138}," },\n",[96,9907,9908,9910,9912,9914,9916,9918,9920,9922,9925,9927,9929,9931,9933,9935,9938,9940,9942],{"class":98,"line":190},[96,9909,9870],{"class":138},[96,9911,4391],{"class":250},[96,9913,728],{"class":138},[96,9915,159],{"class":158},[96,9917,8816],{"class":106},[96,9919,165],{"class":158},[96,9921,146],{"class":138},[96,9923,9924],{"class":250}," grep",[96,9926,728],{"class":138},[96,9928,7101],{"class":158},[96,9930,9759],{"class":106},[96,9932,255],{"class":158},[96,9934,146],{"class":138},[96,9936,9937],{"class":250}," workers",[96,9939,728],{"class":138},[96,9941,6588],{"class":1969},[96,9943,9905],{"class":138},[96,9945,9946,9948],{"class":98,"line":197},[96,9947,1062],{"class":142},[96,9949,1309],{"class":138},[11,9951,9952,9953,27,9956,9959],{},"You can skip projects entirely and filter on the command line with ",[24,9954,9955],{},"--grep @serial",[24,9957,9958],{},"--grep-invert @serial",". The demo has that version too.",[11,9961,9962,9963,9966,9967,9969,9970,9972],{},"One trap: the demo keeps its tagged tests in a separate ",[24,9964,9965],{},"tests-tagged\u002F"," folder. A tagged file inside the folder your filename-based projects already scan would be picked up by the ",[24,9968,8812],{}," project, and the ",[24,9971,9759],{}," tests would run in parallel anyway.",[41,9974,9976],{"id":9975},"option-a-playwright-project-dependencies-the-quick-win","Option A: Playwright Project Dependencies (The Quick Win)",[11,9978,9979,9980,9983,9984,9986,9987,9989],{},"Once the tests are split, you can use the Playwright ",[24,9981,9982],{},"dependencies"," project configuration setting to mark that the ",[24,9985,8816],{}," project depends on the ",[24,9988,8812],{}," project finishing first.",[86,9991,9994],{"className":124,"code":9992,"filename":9993,"language":127,"meta":92,"style":92},"{\n  name: 'serial-only',\n  testMatch: \u002F.*\\.serial\\.spec\\.ts\u002F,\n  dependencies: ['parallel-safe'], \u002F\u002F the magic happens here\n  workers: 1,\n},\n","playwright.dependencies.config.ts",[24,9995,9996,10000,10015,10044,10066,10077],{"__ignoreMap":92},[96,9997,9998],{"class":98,"line":99},[96,9999,1107],{"class":138},[96,10001,10002,10005,10007,10009,10011,10013],{"class":98,"line":171},[96,10003,10004],{"class":102},"  name",[96,10006,728],{"class":138},[96,10008,159],{"class":158},[96,10010,8816],{"class":106},[96,10012,165],{"class":158},[96,10014,1309],{"class":138},[96,10016,10017,10020,10022,10024,10026,10028,10030,10032,10034,10036,10038,10040,10042],{"class":98,"line":190},[96,10018,10019],{"class":102},"  testMatch",[96,10021,728],{"class":138},[96,10023,7101],{"class":158},[96,10025,244],{"class":110},[96,10027,9515],{"class":275},[96,10029,9519],{"class":9518},[96,10031,9548],{"class":106},[96,10033,9519],{"class":9518},[96,10035,9522],{"class":106},[96,10037,9519],{"class":9518},[96,10039,127],{"class":106},[96,10041,255],{"class":158},[96,10043,1309],{"class":138},[96,10045,10046,10049,10051,10053,10055,10057,10059,10061,10063],{"class":98,"line":197},[96,10047,10048],{"class":102},"  dependencies",[96,10050,728],{"class":138},[96,10052,1021],{"class":250},[96,10054,165],{"class":158},[96,10056,8812],{"class":106},[96,10058,165],{"class":158},[96,10060,1062],{"class":250},[96,10062,146],{"class":138},[96,10064,10065],{"class":1200}," \u002F\u002F the magic happens here\n",[96,10067,10068,10071,10073,10075],{"class":98,"line":236},[96,10069,10070],{"class":102},"  workers",[96,10072,728],{"class":138},[96,10074,6588],{"class":1969},[96,10076,1309],{"class":138},[96,10078,10079],{"class":98,"line":265},[96,10080,10081],{"class":138},"},\n",[11,10083,10084,10085,10088],{},"This is a quick and easy way to solve the problem. The parallel tests run first, and the serial ones run after they finish, so nothing collides. In the demo project, running ",[24,10086,10087],{},"npm run test:deps"," will run with this configuration-based solution.",[86,10090,10092],{"className":8991,"code":10091,"filename":90,"language":8993,"meta":92,"style":92},"npx playwright test -c playwright.dependencies.config.ts\n\n\u001b[2mRunning \u001b[22m11\u001b[2m tests using \u001b[22m9\u001b[2m workers\u001b[22m\n\n  \u001b[32m-\u001b[39m  \u001b[2m 9 \u001b[22m\u001b[36m[parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)\u001b[39m\n  \u001b[32m✓\u001b[39m  \u001b[2m 1 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 2 uses default settings\u001b[2m (309ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 2 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 4 uses default settings\u001b[2m (320ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 4 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 1 uses default settings\u001b[2m (307ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 3 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 3 uses default settings\u001b[2m (316ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 5 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 1 renders with default settings\u001b[2m (319ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 6 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings\u001b[2m (313ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 7 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings\u001b[2m (311ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 8 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings\u001b[2m (319ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m10 \u001b[22m[serial-only] › tests\\server-settings.serial.spec.ts:13:5 › feature flag on enables new checkout\u001b[2m (517ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m11 \u001b[22m[serial-only] › tests\\server-settings.serial.spec.ts:19:5 › server date set to month end triggers billing rollover\u001b[2m (508ms)\u001b[22m\n\n\u001b[33m  1 skipped\u001b[39m\n\u001b[32m  10 passed\u001b[39m\u001b[2m (1.9s)\u001b[22m\n",[24,10093,10094,10099,10103,10115,10119,10128,10140,10152,10163,10175,10186,10198,10209,10220,10232,10244,10248,10252],{"__ignoreMap":92},[96,10095,10096],{"class":98,"line":99},[96,10097,10098],{"class":9000},"npx playwright test -c playwright.dependencies.config.ts\n",[96,10100,10101],{"class":98,"line":171},[96,10102,194],{"emptyLinePlaceholder":193},[96,10104,10105,10107,10109,10111,10113],{"class":98,"line":190},[96,10106,9029],{"class":9028},[96,10108,9032],{"class":9000},[96,10110,9035],{"class":9028},[96,10112,412],{"class":9000},[96,10114,9041],{"class":9028},[96,10116,10117],{"class":98,"line":197},[96,10118,194],{"emptyLinePlaceholder":193},[96,10120,10121,10123,10125],{"class":98,"line":236},[96,10122,9103],{"class":9050},[96,10124,9106],{"class":9028},[96,10126,10127],{"class":9109},"[parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)\n",[96,10129,10130,10132,10134,10137],{"class":98,"line":265},[96,10131,9051],{"class":9050},[96,10133,9054],{"class":9028},[96,10135,10136],{"class":9000},"[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 2 uses default settings",[96,10138,10139],{"class":9028}," (309ms)\n",[96,10141,10142,10144,10146,10149],{"class":98,"line":309},[96,10143,9051],{"class":9050},[96,10145,9067],{"class":9028},[96,10147,10148],{"class":9000},"[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 4 uses default settings",[96,10150,10151],{"class":9028}," (320ms)\n",[96,10153,10154,10156,10158,10161],{"class":98,"line":337},[96,10155,9051],{"class":9050},[96,10157,9092],{"class":9028},[96,10159,10160],{"class":9000},"[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 1 uses default settings",[96,10162,9163],{"class":9028},[96,10164,10165,10167,10169,10172],{"class":98,"line":1149},[96,10166,9051],{"class":9050},[96,10168,9079],{"class":9028},[96,10170,10171],{"class":9000},"[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 3 uses default settings",[96,10173,10174],{"class":9028}," (316ms)\n",[96,10176,10177,10179,10181,10184],{"class":98,"line":1167},[96,10178,9051],{"class":9050},[96,10180,9117],{"class":9028},[96,10182,10183],{"class":9000},"[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 1 renders with default settings",[96,10185,9060],{"class":9028},[96,10187,10188,10190,10192,10195],{"class":98,"line":1182},[96,10189,9051],{"class":9050},[96,10191,9157],{"class":9028},[96,10193,10194],{"class":9000},"[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings",[96,10196,10197],{"class":9028}," (313ms)\n",[96,10199,10200,10202,10204,10207],{"class":98,"line":1204},[96,10201,9051],{"class":9050},[96,10203,9145],{"class":9028},[96,10205,10206],{"class":9000},"[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings",[96,10208,9123],{"class":9028},[96,10210,10211,10213,10215,10218],{"class":98,"line":1226},[96,10212,9051],{"class":9050},[96,10214,9132],{"class":9028},[96,10216,10217],{"class":9000},"[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings",[96,10219,9060],{"class":9028},[96,10221,10222,10224,10226,10229],{"class":98,"line":1234},[96,10223,9051],{"class":9050},[96,10225,9170],{"class":9028},[96,10227,10228],{"class":9000},"[serial-only] › tests\\server-settings.serial.spec.ts:13:5 › feature flag on enables new checkout",[96,10230,10231],{"class":9028}," (517ms)\n",[96,10233,10234,10236,10238,10241],{"class":98,"line":1240},[96,10235,9051],{"class":9050},[96,10237,9183],{"class":9028},[96,10239,10240],{"class":9000},"[serial-only] › tests\\server-settings.serial.spec.ts:19:5 › server date set to month end triggers billing rollover",[96,10242,10243],{"class":9028}," (508ms)\n",[96,10245,10246],{"class":98,"line":2227},[96,10247,194],{"emptyLinePlaceholder":193},[96,10249,10250],{"class":98,"line":2233},[96,10251,9309],{"class":9308},[96,10253,10254,10257],{"class":98,"line":2254},[96,10255,10256],{"class":9050},"  10 passed",[96,10258,10259],{"class":9028}," (1.9s)\n",[701,10261,10263],{"id":10262},"the-gotcha-a-failing-test-skips-the-serial-suite","The Gotcha: A Failing Test Skips the Serial Suite",[11,10265,10266,10267,10270,10271,10273,10274,728],{},"There is a ",[606,10268,10269],{},"gotcha"," or trade-off to leveraging the ",[24,10272,9982],{}," setting to separate and run the serial tests after the parallel tests finish. Per the ",[391,10275],{"href":9394,"text":10276},"Playwright documentation",[1648,10278,10279],{},[11,10280,10281],{},"\"If the tests from a dependency fails then the tests that rely on this project will not be run.\"",[11,10283,10284,10285,10288],{},"So if there is a test failure in the parallel tests the serial tests ",[606,10286,10287],{},"will not run"," so you'd lose visibility and coverage for that run for your serial tests since they are skipped.",[11,10290,10291],{},"The demo project uses this DEMO_FAIL test that has been getting skipped in the earlier runs to show this downside in action.",[86,10293,10295],{"className":8991,"code":10294,"filename":90,"language":8993,"meta":92,"style":92},"DEMO_FAIL=1 npx playwright test -c playwright.dependencies.config.ts\n\n\u001b[2mRunning \u001b[22m11\u001b[2m tests using \u001b[22m9\u001b[2m workers\u001b[22m\n\n  \u001b[31m✘\u001b[39m  \u001b[2m 9 \u001b[22m\u001b[31m[parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)\u001b[39m\u001b[2m (4ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 3 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 4 renders with default settings\u001b[2m (305ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 1 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 2 uses default settings\u001b[2m (312ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 5 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 3 uses default settings\u001b[2m (317ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 4 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 4 uses default settings\u001b[2m (316ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 2 \u001b[22m[parallel-safe] › tests\\pricing.spec.ts:8:9 › pricing (parallel-safe) › quote 1 uses default settings\u001b[2m (322ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 7 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 2 renders with default settings\u001b[2m (310ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 8 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 3 renders with default settings\u001b[2m (310ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 6 \u001b[22m[parallel-safe] › tests\\reports.spec.ts:6:9 › reports (parallel-safe) › report 1 renders with default settings\u001b[2m (316ms)\u001b[22m\n\n\n\u001b[31m  1) [parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1) \u001b[39m\n\n    Error: forced failure to demonstrate dependency blocking\n\n    ...\n\n\u001b[31m  1 failed\u001b[39m\n\u001b[31m    [parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1) \u001b[39m\n\u001b[33m  2 did not run\u001b[39m\n\u001b[32m  8 passed\u001b[39m\u001b[2m (656ms)\u001b[22m\n",[24,10296,10297,10302,10306,10318,10322,10334,10344,10354,10365,10375,10386,10397,10407,10417,10421,10425,10430,10434,10439,10443,10447,10451,10456,10461,10466],{"__ignoreMap":92},[96,10298,10299],{"class":98,"line":99},[96,10300,10301],{"class":9000},"DEMO_FAIL=1 npx playwright test -c playwright.dependencies.config.ts\n",[96,10303,10304],{"class":98,"line":171},[96,10305,194],{"emptyLinePlaceholder":193},[96,10307,10308,10310,10312,10314,10316],{"class":98,"line":190},[96,10309,9029],{"class":9028},[96,10311,9032],{"class":9000},[96,10313,9035],{"class":9028},[96,10315,412],{"class":9000},[96,10317,9041],{"class":9028},[96,10319,10320],{"class":98,"line":197},[96,10321,194],{"emptyLinePlaceholder":193},[96,10323,10324,10326,10328,10331],{"class":98,"line":236},[96,10325,9129],{"class":9128},[96,10327,9106],{"class":9028},[96,10329,10330],{"class":9128},"[parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1)",[96,10332,10333],{"class":9028}," (4ms)\n",[96,10335,10336,10338,10340,10342],{"class":98,"line":265},[96,10337,9051],{"class":9050},[96,10339,9079],{"class":9028},[96,10341,10206],{"class":9000},[96,10343,9138],{"class":9028},[96,10345,10346,10348,10350,10352],{"class":98,"line":309},[96,10347,9051],{"class":9050},[96,10349,9054],{"class":9028},[96,10351,10136],{"class":9000},[96,10353,9098],{"class":9028},[96,10355,10356,10358,10360,10362],{"class":98,"line":337},[96,10357,9051],{"class":9050},[96,10359,9117],{"class":9028},[96,10361,10171],{"class":9000},[96,10363,10364],{"class":9028}," (317ms)\n",[96,10366,10367,10369,10371,10373],{"class":98,"line":1149},[96,10368,9051],{"class":9050},[96,10370,9092],{"class":9028},[96,10372,10148],{"class":9000},[96,10374,10174],{"class":9028},[96,10376,10377,10379,10381,10383],{"class":98,"line":1167},[96,10378,9051],{"class":9050},[96,10380,9067],{"class":9028},[96,10382,10160],{"class":9000},[96,10384,10385],{"class":9028}," (322ms)\n",[96,10387,10388,10390,10392,10394],{"class":98,"line":1182},[96,10389,9051],{"class":9050},[96,10391,9145],{"class":9028},[96,10393,10194],{"class":9000},[96,10395,10396],{"class":9028}," (310ms)\n",[96,10398,10399,10401,10403,10405],{"class":98,"line":1204},[96,10400,9051],{"class":9050},[96,10402,9132],{"class":9028},[96,10404,10217],{"class":9000},[96,10406,10396],{"class":9028},[96,10408,10409,10411,10413,10415],{"class":98,"line":1226},[96,10410,9051],{"class":9050},[96,10412,9157],{"class":9028},[96,10414,10183],{"class":9000},[96,10416,10174],{"class":9028},[96,10418,10419],{"class":98,"line":1234},[96,10420,194],{"emptyLinePlaceholder":193},[96,10422,10423],{"class":98,"line":1240},[96,10424,194],{"emptyLinePlaceholder":193},[96,10426,10427],{"class":98,"line":2227},[96,10428,10429],{"class":9128},"  1) [parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1) \n",[96,10431,10432],{"class":98,"line":2233},[96,10433,194],{"emptyLinePlaceholder":193},[96,10435,10436],{"class":98,"line":2254},[96,10437,10438],{"class":9000},"    Error: forced failure to demonstrate dependency blocking\n",[96,10440,10441],{"class":98,"line":2275},[96,10442,194],{"emptyLinePlaceholder":193},[96,10444,10445],{"class":98,"line":2296},[96,10446,9272],{"class":9000},[96,10448,10449],{"class":98,"line":2309},[96,10450,194],{"emptyLinePlaceholder":193},[96,10452,10453],{"class":98,"line":2315},[96,10454,10455],{"class":9128},"  1 failed\n",[96,10457,10458],{"class":98,"line":2331},[96,10459,10460],{"class":9128},"    [parallel-safe] › tests\\reports.spec.ts:15:7 › reports (parallel-safe) › deliberately failing test (only when DEMO_FAIL=1) \n",[96,10462,10463],{"class":98,"line":2337},[96,10464,10465],{"class":9308},"  2 did not run\n",[96,10467,10468,10471],{"class":98,"line":2358},[96,10469,10470],{"class":9050},"  8 passed",[96,10472,10473],{"class":9028}," (656ms)\n",[11,10475,10476,10477,10480],{},"The two serial tests never ran as indicated by ",[24,10478,10479],{},"2 did not run"," in the terminal output. In a real suite, it means one failing test in a large parallel-safe project hides the results of the entire serial project. It also changes what \"the suite ran\" means when someone triages a red build. Every failure has to be fixed before you can see whether the second half is healthy at all. In other words, you don't know what other issues are hiding until you fix the early test failures which can make troubleshooting frustrating if you fix one test, rerun, and find another issue that was hiding behind it.",[11,10482,10483],{},"For my suite that's a dealbreaker. The serial tests cover some of the more important financial transactions in the systems that are part of the critical path. I want to know about a failure there on the same run where a parallel test fails, not one fix cycle later.",[41,10485,10487],{"id":10486},"option-b-separate-ci-steps-for-playwright-projects-fixing-the-catch","Option B: Separate CI Steps for Playwright Projects (Fixing the Catch)",[11,10489,10490,10491,10493,10494,10496],{},"As mentioned, Option A, leveraging Playwright ",[24,10492,9982],{},", is an easy solution to keep the serial and parallel tests separated, but has the trade-off where an upstream failure in the parallel test project causes your serial tests to be skipped. The fix in Option B is to keep leveraging Playwright projects to subset the parallel and serial tests, but drop ",[24,10495,9982],{}," and let CI do the ordering. The projects stay in the config, but each gets its own invocation:",[86,10498,10500],{"className":124,"code":10499,"filename":9410,"language":127,"meta":92,"style":92},"projects: [\n  { name: 'parallel-safe', testMatch: \u002F.*\\.spec\\.ts\u002F, testIgnore: \u002F.*\\.serial\\.spec\\.ts\u002F, fullyParallel: true },\n  { name: 'serial-only', testMatch: \u002F.*\\.serial\\.spec\\.ts\u002F, workers: 1 },\n  \u002F\u002F no `dependencies` between them\n],\n",[24,10501,10502,10510,10584,10634,10639],{"__ignoreMap":92},[96,10503,10504,10506,10508],{"class":98,"line":99},[96,10505,9406],{"class":102},[96,10507,728],{"class":138},[96,10509,1287],{"class":142},[96,10511,10512,10514,10516,10518,10520,10522,10524,10526,10529,10531,10533,10535,10537,10539,10541,10543,10545,10547,10549,10552,10554,10556,10558,10560,10562,10564,10566,10568,10570,10572,10574,10576,10578,10580,10582],{"class":98,"line":171},[96,10513,9870],{"class":138},[96,10515,4391],{"class":250},[96,10517,728],{"class":138},[96,10519,159],{"class":158},[96,10521,8812],{"class":106},[96,10523,165],{"class":158},[96,10525,146],{"class":138},[96,10527,10528],{"class":250}," testMatch",[96,10530,728],{"class":138},[96,10532,7101],{"class":158},[96,10534,244],{"class":110},[96,10536,9515],{"class":275},[96,10538,9519],{"class":9518},[96,10540,9522],{"class":106},[96,10542,9519],{"class":9518},[96,10544,127],{"class":106},[96,10546,255],{"class":158},[96,10548,146],{"class":138},[96,10550,10551],{"class":250}," testIgnore",[96,10553,728],{"class":138},[96,10555,7101],{"class":158},[96,10557,244],{"class":110},[96,10559,9515],{"class":275},[96,10561,9519],{"class":9518},[96,10563,9548],{"class":106},[96,10565,9519],{"class":9518},[96,10567,9522],{"class":106},[96,10569,9519],{"class":9518},[96,10571,127],{"class":106},[96,10573,255],{"class":158},[96,10575,146],{"class":138},[96,10577,9898],{"class":250},[96,10579,728],{"class":138},[96,10581,9570],{"class":4627},[96,10583,9905],{"class":138},[96,10585,10586,10588,10590,10592,10594,10596,10598,10600,10602,10604,10606,10608,10610,10612,10614,10616,10618,10620,10622,10624,10626,10628,10630,10632],{"class":98,"line":190},[96,10587,9870],{"class":138},[96,10589,4391],{"class":250},[96,10591,728],{"class":138},[96,10593,159],{"class":158},[96,10595,8816],{"class":106},[96,10597,165],{"class":158},[96,10599,146],{"class":138},[96,10601,10528],{"class":250},[96,10603,728],{"class":138},[96,10605,7101],{"class":158},[96,10607,244],{"class":110},[96,10609,9515],{"class":275},[96,10611,9519],{"class":9518},[96,10613,9548],{"class":106},[96,10615,9519],{"class":9518},[96,10617,9522],{"class":106},[96,10619,9519],{"class":9518},[96,10621,127],{"class":106},[96,10623,255],{"class":158},[96,10625,146],{"class":138},[96,10627,9937],{"class":250},[96,10629,728],{"class":138},[96,10631,6588],{"class":1969},[96,10633,9905],{"class":138},[96,10635,10636],{"class":98,"line":197},[96,10637,10638],{"class":1200},"  \u002F\u002F no `dependencies` between them\n",[96,10640,10641,10643],{"class":98,"line":236},[96,10642,1062],{"class":142},[96,10644,1309],{"class":138},[11,10646,10647],{},"In GitHub Actions, run the parallel-safe step first, let the serial step run whether or not the first one failed, then fail the job if either did:",[86,10649,10654],{"className":10650,"code":10651,"filename":10652,"language":10653,"meta":92,"style":92},"language-yaml shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","- name: Parallel-safe tests\n  id: parallel\n  run: npx playwright test --project=parallel-safe\n  continue-on-error: true\n\n- name: Serial-only tests\n  id: serial\n  run: npx playwright test --project=serial-only --workers=1\n  continue-on-error: true\n\n- name: Fail job if either suite failed\n  if: steps.parallel.outcome == 'failure' || steps.serial.outcome == 'failure'\n  run: exit 1\n",".github\u002Fworkflows\u002Fsplit-steps.yml","yaml",[24,10655,10656,10668,10678,10688,10698,10702,10713,10722,10731,10739,10743,10754,10764],{"__ignoreMap":92},[96,10657,10658,10661,10663,10665],{"class":98,"line":99},[96,10659,10660],{"class":138},"-",[96,10662,4391],{"class":2378},[96,10664,728],{"class":138},[96,10666,10667],{"class":106}," Parallel-safe tests\n",[96,10669,10670,10673,10675],{"class":98,"line":171},[96,10671,10672],{"class":2378},"  id",[96,10674,728],{"class":138},[96,10676,10677],{"class":106}," parallel\n",[96,10679,10680,10683,10685],{"class":98,"line":190},[96,10681,10682],{"class":2378},"  run",[96,10684,728],{"class":138},[96,10686,10687],{"class":106}," npx playwright test --project=parallel-safe\n",[96,10689,10690,10693,10695],{"class":98,"line":197},[96,10691,10692],{"class":2378},"  continue-on-error",[96,10694,728],{"class":138},[96,10696,10697],{"class":4627}," true\n",[96,10699,10700],{"class":98,"line":236},[96,10701,194],{"emptyLinePlaceholder":193},[96,10703,10704,10706,10708,10710],{"class":98,"line":265},[96,10705,10660],{"class":138},[96,10707,4391],{"class":2378},[96,10709,728],{"class":138},[96,10711,10712],{"class":106}," Serial-only tests\n",[96,10714,10715,10717,10719],{"class":98,"line":309},[96,10716,10672],{"class":2378},[96,10718,728],{"class":138},[96,10720,10721],{"class":106}," serial\n",[96,10723,10724,10726,10728],{"class":98,"line":337},[96,10725,10682],{"class":2378},[96,10727,728],{"class":138},[96,10729,10730],{"class":106}," npx playwright test --project=serial-only --workers=1\n",[96,10732,10733,10735,10737],{"class":98,"line":1149},[96,10734,10692],{"class":2378},[96,10736,728],{"class":138},[96,10738,10697],{"class":4627},[96,10740,10741],{"class":98,"line":1167},[96,10742,194],{"emptyLinePlaceholder":193},[96,10744,10745,10747,10749,10751],{"class":98,"line":1182},[96,10746,10660],{"class":138},[96,10748,4391],{"class":2378},[96,10750,728],{"class":138},[96,10752,10753],{"class":106}," Fail job if either suite failed\n",[96,10755,10756,10759,10761],{"class":98,"line":1204},[96,10757,10758],{"class":2378},"  if",[96,10760,728],{"class":138},[96,10762,10763],{"class":106}," steps.parallel.outcome == 'failure' || steps.serial.outcome == 'failure'\n",[96,10765,10766,10768,10770],{"class":98,"line":1226},[96,10767,10682],{"class":2378},[96,10769,728],{"class":138},[96,10771,10772],{"class":106}," exit 1\n",[11,10774,10775,10778,10779,10782],{},[24,10776,10777],{},"continue-on-error: true"," keeps a failed step from stopping the job, and the ",[24,10780,10781],{},"outcome"," check in the last step reads the result of each step before that setting is applied. The last step is what turns the build red.",[11,10784,10785],{},"Run the same forced failure locally as two separate commands and you get the result the dependency version couldn't:",[86,10787,10789],{"className":88,"code":10788,"filename":90,"language":91,"meta":92,"style":92},"DEMO_FAIL=1 npx playwright test --project=parallel-safe\n# 1 failed, 8 passed\n\nnpx playwright test --project=serial-only\n# 2 passed\n",[24,10790,10791,10811,10816,10820,10832],{"__ignoreMap":92},[96,10792,10793,10796,10798,10800,10803,10806,10808],{"class":98,"line":99},[96,10794,10795],{"class":142},"DEMO_FAIL",[96,10797,3278],{"class":275},[96,10799,426],{"class":106},[96,10801,10802],{"class":102}," npx",[96,10804,10805],{"class":106}," playwright",[96,10807,143],{"class":106},[96,10809,10810],{"class":110}," --project=parallel-safe\n",[96,10812,10813],{"class":98,"line":171},[96,10814,10815],{"class":1200},"# 1 failed, 8 passed\n",[96,10817,10818],{"class":98,"line":190},[96,10819,194],{"emptyLinePlaceholder":193},[96,10821,10822,10825,10827,10829],{"class":98,"line":197},[96,10823,10824],{"class":102},"npx",[96,10826,10805],{"class":106},[96,10828,143],{"class":106},[96,10830,10831],{"class":110}," --project=serial-only\n",[96,10833,10834],{"class":98,"line":236},[96,10835,10836],{"class":1200},"# 2 passed\n",[11,10838,10839],{},"The parallel step fails and the serial step still runs and passes. The order is still preserved, because the serial tests only start after the parallel step finishes. The two suites just no longer depend on each other's results with no coverage compromise this time.",[11,10841,10842,10843,27,10845,244],{},"The tag version works with the same two steps. Change the commands to ",[24,10844,9958],{},[24,10846,10847],{},"--grep @serial --workers=1",[41,10849,10851],{"id":10850},"option-c-preconfigured-test-environments-my-long-term-goal","Option C: Preconfigured Test Environments (My Long-Term Goal)",[11,10853,10854],{},"Both options so far keep the underlying problem. The tests still change global state on the fly, so they still need the environment to themselves. That covers things like the server date, web server settings, language settings, and feature flags.",[11,10856,10857],{},"What I'd rather do is stand up test environments that are already configured with those settings. It's less invasive and tends to be less of a flaky pattern than changing major server settings mid-test. It also lessens the amount of tests in the serial project grouping if groups of tests with similar state settings can run together on isolated environments.",[11,10859,10860],{},"One environment would have its server date set to a month end. One has its language set to Spanish. One has the new-checkout feature flag on. Tests that need a given precondition run against the environment that already has it, so nothing has to change a setting mid-run. That lets each group of tests run isolated from the rest and in parallel with them, with no ordering step in CI at all.",[11,10862,10863,10864,10866,10867,10870,10871,10874],{},"Playwright supports this with per-project ",[24,10865,1784],{}," options. ",[391,10868],{"href":10869,"text":9349},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-use-options"," say you can override options for a specific project, and ",[24,10872,10873],{},"baseURL"," is one of them. Name the test files after the environment they need, then match on that naming convention:",[86,10876,10878],{"className":124,"code":10877,"filename":9410,"language":127,"meta":92,"style":92},"import { defineConfig } from '@playwright\u002Ftest';\nimport dotenv from 'dotenv';\nimport path from 'path';\n\ndotenv.config({ path: path.resolve(__dirname, '.env') });\n\nexport default defineConfig({\n  testDir: '.\u002Ftests',\n  fullyParallel: true,\n  projects: [\n    {\n      name: 'parallel-safe',\n      testMatch: \u002F.*\\.spec\\.ts\u002F,\n      testIgnore: \u002F.*\\.(month-end|spanish|new-checkout)\\.spec\\.ts\u002F,\n      use: { baseURL: process.env.DEFAULT_BASE_URL },\n    },\n    {\n      name: 'month-end-date',\n      testMatch: \u002F.*\\.month-end\\.spec\\.ts\u002F,\n      use: { baseURL: process.env.MONTH_END_BASE_URL },\n    },\n    {\n      name: 'spanish-locale',\n      testMatch: \u002F.*\\.spanish\\.spec\\.ts\u002F,\n      use: { baseURL: process.env.SPANISH_BASE_URL },\n    },\n    {\n      name: 'new-checkout-flag',\n      testMatch: \u002F.*\\.new-checkout\\.spec\\.ts\u002F,\n      use: { baseURL: process.env.NEW_CHECKOUT_BASE_URL },\n    },\n  ],\n});\n",[24,10879,10880,10900,10918,10936,10940,10984,10988,11000,11014,11025,11033,11037,11051,11075,11119,11148,11152,11156,11171,11199,11224,11228,11232,11247,11275,11300,11304,11308,11323,11351,11376,11380,11386],{"__ignoreMap":92},[96,10881,10882,10884,10886,10888,10890,10892,10894,10896,10898],{"class":98,"line":99},[96,10883,135],{"class":134},[96,10885,139],{"class":138},[96,10887,9424],{"class":142},[96,10889,152],{"class":138},[96,10891,155],{"class":134},[96,10893,159],{"class":158},[96,10895,162],{"class":106},[96,10897,165],{"class":158},[96,10899,168],{"class":138},[96,10901,10902,10904,10907,10909,10911,10914,10916],{"class":98,"line":171},[96,10903,135],{"class":134},[96,10905,10906],{"class":142}," dotenv ",[96,10908,179],{"class":134},[96,10910,159],{"class":158},[96,10912,10913],{"class":106},"dotenv",[96,10915,165],{"class":158},[96,10917,168],{"class":138},[96,10919,10920,10922,10925,10927,10929,10932,10934],{"class":98,"line":190},[96,10921,135],{"class":134},[96,10923,10924],{"class":142}," path ",[96,10926,179],{"class":134},[96,10928,159],{"class":158},[96,10930,10931],{"class":106},"path",[96,10933,165],{"class":158},[96,10935,168],{"class":138},[96,10937,10938],{"class":98,"line":197},[96,10939,194],{"emptyLinePlaceholder":193},[96,10941,10942,10944,10946,10949,10951,10953,10955,10957,10959,10961,10964,10967,10969,10971,10974,10976,10978,10980,10982],{"class":98,"line":236},[96,10943,10913],{"class":142},[96,10945,244],{"class":138},[96,10947,10948],{"class":200},"config",[96,10950,204],{"class":142},[96,10952,290],{"class":138},[96,10954,1482],{"class":250},[96,10956,728],{"class":138},[96,10958,1482],{"class":142},[96,10960,244],{"class":138},[96,10962,10963],{"class":200},"resolve",[96,10965,10966],{"class":142},"(__dirname",[96,10968,146],{"class":138},[96,10970,159],{"class":158},[96,10972,10973],{"class":106},".env",[96,10975,165],{"class":158},[96,10977,1734],{"class":142},[96,10979,340],{"class":138},[96,10981,260],{"class":142},[96,10983,168],{"class":138},[96,10985,10986],{"class":98,"line":265},[96,10987,194],{"emptyLinePlaceholder":193},[96,10989,10990,10992,10994,10996,10998],{"class":98,"line":309},[96,10991,9445],{"class":134},[96,10993,9448],{"class":134},[96,10995,9424],{"class":200},[96,10997,204],{"class":9453},[96,10999,1107],{"class":9456},[96,11001,11002,11004,11006,11008,11010,11012],{"class":98,"line":337},[96,11003,9461],{"class":250},[96,11005,728],{"class":138},[96,11007,159],{"class":158},[96,11009,9468],{"class":106},[96,11011,165],{"class":158},[96,11013,1309],{"class":9456},[96,11015,11016,11019,11021,11023],{"class":98,"line":1149},[96,11017,11018],{"class":250},"  fullyParallel",[96,11020,728],{"class":138},[96,11022,9570],{"class":4627},[96,11024,1309],{"class":9456},[96,11026,11027,11029,11031],{"class":98,"line":1167},[96,11028,9477],{"class":250},[96,11030,728],{"class":138},[96,11032,1287],{"class":142},[96,11034,11035],{"class":98,"line":1182},[96,11036,9486],{"class":138},[96,11038,11039,11041,11043,11045,11047,11049],{"class":98,"line":1204},[96,11040,9491],{"class":250},[96,11042,728],{"class":138},[96,11044,159],{"class":158},[96,11046,8812],{"class":106},[96,11048,165],{"class":158},[96,11050,1309],{"class":138},[96,11052,11053,11055,11057,11059,11061,11063,11065,11067,11069,11071,11073],{"class":98,"line":1226},[96,11054,9506],{"class":250},[96,11056,728],{"class":138},[96,11058,7101],{"class":158},[96,11060,244],{"class":110},[96,11062,9515],{"class":275},[96,11064,9519],{"class":9518},[96,11066,9522],{"class":106},[96,11068,9519],{"class":9518},[96,11070,127],{"class":106},[96,11072,255],{"class":158},[96,11074,1309],{"class":138},[96,11076,11077,11079,11081,11083,11085,11087,11089,11091,11094,11097,11100,11102,11105,11107,11109,11111,11113,11115,11117],{"class":98,"line":1234},[96,11078,9535],{"class":250},[96,11080,728],{"class":138},[96,11082,7101],{"class":158},[96,11084,244],{"class":110},[96,11086,9515],{"class":275},[96,11088,9519],{"class":9518},[96,11090,204],{"class":158},[96,11092,11093],{"class":106},"month-end",[96,11095,11096],{"class":275},"|",[96,11098,11099],{"class":106},"spanish",[96,11101,11096],{"class":275},[96,11103,11104],{"class":106},"new-checkout",[96,11106,260],{"class":158},[96,11108,9519],{"class":9518},[96,11110,9522],{"class":106},[96,11112,9519],{"class":9518},[96,11114,127],{"class":106},[96,11116,255],{"class":158},[96,11118,1309],{"class":138},[96,11120,11121,11124,11126,11128,11131,11133,11136,11138,11141,11143,11146],{"class":98,"line":1240},[96,11122,11123],{"class":250},"      use",[96,11125,728],{"class":138},[96,11127,139],{"class":138},[96,11129,11130],{"class":250}," baseURL",[96,11132,728],{"class":138},[96,11134,11135],{"class":142}," process",[96,11137,244],{"class":138},[96,11139,11140],{"class":142},"env",[96,11142,244],{"class":138},[96,11144,11145],{"class":271},"DEFAULT_BASE_URL",[96,11147,9905],{"class":138},[96,11149,11150],{"class":98,"line":2227},[96,11151,9577],{"class":138},[96,11153,11154],{"class":98,"line":2233},[96,11155,9486],{"class":138},[96,11157,11158,11160,11162,11164,11167,11169],{"class":98,"line":2254},[96,11159,9491],{"class":250},[96,11161,728],{"class":138},[96,11163,159],{"class":158},[96,11165,11166],{"class":106},"month-end-date",[96,11168,165],{"class":158},[96,11170,1309],{"class":138},[96,11172,11173,11175,11177,11179,11181,11183,11185,11187,11189,11191,11193,11195,11197],{"class":98,"line":2275},[96,11174,9506],{"class":250},[96,11176,728],{"class":138},[96,11178,7101],{"class":158},[96,11180,244],{"class":110},[96,11182,9515],{"class":275},[96,11184,9519],{"class":9518},[96,11186,11093],{"class":106},[96,11188,9519],{"class":9518},[96,11190,9522],{"class":106},[96,11192,9519],{"class":9518},[96,11194,127],{"class":106},[96,11196,255],{"class":158},[96,11198,1309],{"class":138},[96,11200,11201,11203,11205,11207,11209,11211,11213,11215,11217,11219,11222],{"class":98,"line":2296},[96,11202,11123],{"class":250},[96,11204,728],{"class":138},[96,11206,139],{"class":138},[96,11208,11130],{"class":250},[96,11210,728],{"class":138},[96,11212,11135],{"class":142},[96,11214,244],{"class":138},[96,11216,11140],{"class":142},[96,11218,244],{"class":138},[96,11220,11221],{"class":271},"MONTH_END_BASE_URL",[96,11223,9905],{"class":138},[96,11225,11226],{"class":98,"line":2309},[96,11227,9577],{"class":138},[96,11229,11230],{"class":98,"line":2315},[96,11231,9486],{"class":138},[96,11233,11234,11236,11238,11240,11243,11245],{"class":98,"line":2331},[96,11235,9491],{"class":250},[96,11237,728],{"class":138},[96,11239,159],{"class":158},[96,11241,11242],{"class":106},"spanish-locale",[96,11244,165],{"class":158},[96,11246,1309],{"class":138},[96,11248,11249,11251,11253,11255,11257,11259,11261,11263,11265,11267,11269,11271,11273],{"class":98,"line":2337},[96,11250,9506],{"class":250},[96,11252,728],{"class":138},[96,11254,7101],{"class":158},[96,11256,244],{"class":110},[96,11258,9515],{"class":275},[96,11260,9519],{"class":9518},[96,11262,11099],{"class":106},[96,11264,9519],{"class":9518},[96,11266,9522],{"class":106},[96,11268,9519],{"class":9518},[96,11270,127],{"class":106},[96,11272,255],{"class":158},[96,11274,1309],{"class":138},[96,11276,11277,11279,11281,11283,11285,11287,11289,11291,11293,11295,11298],{"class":98,"line":2358},[96,11278,11123],{"class":250},[96,11280,728],{"class":138},[96,11282,139],{"class":138},[96,11284,11130],{"class":250},[96,11286,728],{"class":138},[96,11288,11135],{"class":142},[96,11290,244],{"class":138},[96,11292,11140],{"class":142},[96,11294,244],{"class":138},[96,11296,11297],{"class":271},"SPANISH_BASE_URL",[96,11299,9905],{"class":138},[96,11301,11302],{"class":98,"line":2372},[96,11303,9577],{"class":138},[96,11305,11306],{"class":98,"line":2395},[96,11307,9486],{"class":138},[96,11309,11310,11312,11314,11316,11319,11321],{"class":98,"line":2416},[96,11311,9491],{"class":250},[96,11313,728],{"class":138},[96,11315,159],{"class":158},[96,11317,11318],{"class":106},"new-checkout-flag",[96,11320,165],{"class":158},[96,11322,1309],{"class":138},[96,11324,11325,11327,11329,11331,11333,11335,11337,11339,11341,11343,11345,11347,11349],{"class":98,"line":2433},[96,11326,9506],{"class":250},[96,11328,728],{"class":138},[96,11330,7101],{"class":158},[96,11332,244],{"class":110},[96,11334,9515],{"class":275},[96,11336,9519],{"class":9518},[96,11338,11104],{"class":106},[96,11340,9519],{"class":9518},[96,11342,9522],{"class":106},[96,11344,9519],{"class":9518},[96,11346,127],{"class":106},[96,11348,255],{"class":158},[96,11350,1309],{"class":138},[96,11352,11353,11355,11357,11359,11361,11363,11365,11367,11369,11371,11374],{"class":98,"line":2454},[96,11354,11123],{"class":250},[96,11356,728],{"class":138},[96,11358,139],{"class":138},[96,11360,11130],{"class":250},[96,11362,728],{"class":138},[96,11364,11135],{"class":142},[96,11366,244],{"class":138},[96,11368,11140],{"class":142},[96,11370,244],{"class":138},[96,11372,11373],{"class":271},"NEW_CHECKOUT_BASE_URL",[96,11375,9905],{"class":138},[96,11377,11378],{"class":98,"line":2475},[96,11379,9577],{"class":138},[96,11381,11382,11384],{"class":98,"line":2491},[96,11383,9646],{"class":142},[96,11385,1309],{"class":9456},[96,11387,11388,11390,11392],{"class":98,"line":2510},[96,11389,340],{"class":9456},[96,11391,260],{"class":9453},[96,11393,168],{"class":138},[11,11395,11396,11397,11399,11400,11402,11403,11407],{},"Each environment flavor gets its own base URL in a ",[24,11398,10973],{}," file, which the config loads with ",[24,11401,10913],{}," as the ",[391,11404],{"href":11405,"text":11406},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-parameterize","Playwright docs"," suggest:",[86,11409,11411],{"className":88,"code":11410,"filename":10973,"language":91,"meta":92,"style":92},"DEFAULT_BASE_URL=https:\u002F\u002Ftest.example.com\nMONTH_END_BASE_URL=https:\u002F\u002Ftest-month-end.example.com\nSPANISH_BASE_URL=https:\u002F\u002Ftest-es.example.com\nNEW_CHECKOUT_BASE_URL=https:\u002F\u002Ftest-new-checkout.example.com\n",[24,11412,11413,11422,11431,11440],{"__ignoreMap":92},[96,11414,11415,11417,11419],{"class":98,"line":99},[96,11416,11145],{"class":142},[96,11418,3278],{"class":275},[96,11420,11421],{"class":106},"https:\u002F\u002Ftest.example.com\n",[96,11423,11424,11426,11428],{"class":98,"line":171},[96,11425,11221],{"class":142},[96,11427,3278],{"class":275},[96,11429,11430],{"class":106},"https:\u002F\u002Ftest-month-end.example.com\n",[96,11432,11433,11435,11437],{"class":98,"line":190},[96,11434,11297],{"class":142},[96,11436,3278],{"class":275},[96,11438,11439],{"class":106},"https:\u002F\u002Ftest-es.example.com\n",[96,11441,11442,11444,11446],{"class":98,"line":197},[96,11443,11373],{"class":142},[96,11445,3278],{"class":275},[96,11447,11448],{"class":106},"https:\u002F\u002Ftest-new-checkout.example.com\n",[11,11450,11451,11452,11455,11456,622,11459,11461],{},"A test file such as ",[24,11453,11454],{},"billing.month-end.spec.ts"," then uses relative navigation like ",[24,11457,11458],{},"page.goto('\u002Fbilling')",[24,11460,10873],{}," decides which environment it hits. In CI you'd set the same variables from your environment or secrets instead of a file.",[11,11463,11464,11465,11467],{},"The demo repo runs this configuration in one command with no ",[24,11466,9982],{}," and no separate steps:",[86,11469,11471],{"className":8991,"code":11470,"filename":90,"language":8993,"meta":92,"style":92},"npx playwright test -c playwright.environments.config.ts\n◇ injected env (4) from .env.example\n\n\u001b[2mRunning \u001b[22m10\u001b[2m tests using \u001b[22m10\u001b[2m workers\u001b[22m\n\n◇ injected env (0) from .env.example\n◇ injected env (0) from .env.example\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m 1 \u001b[22m[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 1 runs against the default environment\u001b[2m (310ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 2 \u001b[22m[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 4 runs against the default environment\u001b[2m (313ms)\u001b[22m\n◇ injected env (0) from .env.example\n◇ injected env (0) from .env.example\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m 3 \u001b[22m[month-end-date] › tests-environments\\billing.month-end.spec.ts:7:9 › billing rollover (month-end environment) › rollover scenario 1\u001b[2m (319ms)\u001b[22m\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m 4 \u001b[22m[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 3 runs against the default environment\u001b[2m (314ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 5 \u001b[22m[month-end-date] › tests-environments\\billing.month-end.spec.ts:7:9 › billing rollover (month-end environment) › rollover scenario 2\u001b[2m (314ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 6 \u001b[22m[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 2 runs against the default environment\u001b[2m (311ms)\u001b[22m\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m 7 \u001b[22m[spanish-locale] › tests-environments\\account.spanish.spec.ts:7:9 › account page (Spanish environment) › account scenario 2\u001b[2m (316ms)\u001b[22m\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m 8 \u001b[22m[new-checkout-flag] › tests-environments\\checkout.new-checkout.spec.ts:7:9 › checkout (new-checkout flag environment) › checkout scenario 1\u001b[2m (306ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m 9 \u001b[22m[new-checkout-flag] › tests-environments\\checkout.new-checkout.spec.ts:7:9 › checkout (new-checkout flag environment) › checkout scenario 2\u001b[2m (305ms)\u001b[22m\n◇ injected env (0) from .env.example\n  \u001b[32m✓\u001b[39m  \u001b[2m10 \u001b[22m[spanish-locale] › tests-environments\\account.spanish.spec.ts:7:9 › account page (Spanish environment) › account scenario 1\u001b[2m (306ms)\u001b[22m\n\n\u001b[32m  10 passed\u001b[39m\u001b[2m (726ms)\u001b[22m\n",[24,11472,11473,11478,11483,11487,11500,11504,11509,11513,11517,11528,11539,11543,11547,11551,11562,11566,11578,11589,11600,11604,11615,11619,11631,11642,11646,11657,11661],{"__ignoreMap":92},[96,11474,11475],{"class":98,"line":99},[96,11476,11477],{"class":9000},"npx playwright test -c playwright.environments.config.ts\n",[96,11479,11480],{"class":98,"line":171},[96,11481,11482],{"class":9000},"◇ injected env (4) from .env.example\n",[96,11484,11485],{"class":98,"line":190},[96,11486,194],{"emptyLinePlaceholder":193},[96,11488,11489,11491,11494,11496,11498],{"class":98,"line":197},[96,11490,9029],{"class":9028},[96,11492,11493],{"class":9000},"10",[96,11495,9035],{"class":9028},[96,11497,11493],{"class":9000},[96,11499,9041],{"class":9028},[96,11501,11502],{"class":98,"line":236},[96,11503,194],{"emptyLinePlaceholder":193},[96,11505,11506],{"class":98,"line":265},[96,11507,11508],{"class":9000},"◇ injected env (0) from .env.example\n",[96,11510,11511],{"class":98,"line":309},[96,11512,11508],{"class":9000},[96,11514,11515],{"class":98,"line":337},[96,11516,11508],{"class":9000},[96,11518,11519,11521,11523,11526],{"class":98,"line":1149},[96,11520,9051],{"class":9050},[96,11522,9054],{"class":9028},[96,11524,11525],{"class":9000},"[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 1 runs against the default environment",[96,11527,10396],{"class":9028},[96,11529,11530,11532,11534,11537],{"class":98,"line":1167},[96,11531,9051],{"class":9050},[96,11533,9067],{"class":9028},[96,11535,11536],{"class":9000},"[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 4 runs against the default environment",[96,11538,10197],{"class":9028},[96,11540,11541],{"class":98,"line":1182},[96,11542,11508],{"class":9000},[96,11544,11545],{"class":98,"line":1204},[96,11546,11508],{"class":9000},[96,11548,11549],{"class":98,"line":1226},[96,11550,11508],{"class":9000},[96,11552,11553,11555,11557,11560],{"class":98,"line":1234},[96,11554,9051],{"class":9050},[96,11556,9079],{"class":9028},[96,11558,11559],{"class":9000},"[month-end-date] › tests-environments\\billing.month-end.spec.ts:7:9 › billing rollover (month-end environment) › rollover scenario 1",[96,11561,9060],{"class":9028},[96,11563,11564],{"class":98,"line":1240},[96,11565,11508],{"class":9000},[96,11567,11568,11570,11572,11575],{"class":98,"line":2227},[96,11569,9051],{"class":9050},[96,11571,9092],{"class":9028},[96,11573,11574],{"class":9000},"[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 3 runs against the default environment",[96,11576,11577],{"class":9028}," (314ms)\n",[96,11579,11580,11582,11584,11587],{"class":98,"line":2233},[96,11581,9051],{"class":9050},[96,11583,9117],{"class":9028},[96,11585,11586],{"class":9000},"[month-end-date] › tests-environments\\billing.month-end.spec.ts:7:9 › billing rollover (month-end environment) › rollover scenario 2",[96,11588,11577],{"class":9028},[96,11590,11591,11593,11595,11598],{"class":98,"line":2254},[96,11592,9051],{"class":9050},[96,11594,9157],{"class":9028},[96,11596,11597],{"class":9000},"[parallel-safe] › tests-environments\\pricing.spec.ts:8:9 › pricing (default environment) › quote 2 runs against the default environment",[96,11599,9123],{"class":9028},[96,11601,11602],{"class":98,"line":2275},[96,11603,11508],{"class":9000},[96,11605,11606,11608,11610,11613],{"class":98,"line":2296},[96,11607,9051],{"class":9050},[96,11609,9145],{"class":9028},[96,11611,11612],{"class":9000},"[spanish-locale] › tests-environments\\account.spanish.spec.ts:7:9 › account page (Spanish environment) › account scenario 2",[96,11614,10174],{"class":9028},[96,11616,11617],{"class":98,"line":2309},[96,11618,11508],{"class":9000},[96,11620,11621,11623,11625,11628],{"class":98,"line":2315},[96,11622,9051],{"class":9050},[96,11624,9132],{"class":9028},[96,11626,11627],{"class":9000},"[new-checkout-flag] › tests-environments\\checkout.new-checkout.spec.ts:7:9 › checkout (new-checkout flag environment) › checkout scenario 1",[96,11629,11630],{"class":9028}," (306ms)\n",[96,11632,11633,11635,11637,11640],{"class":98,"line":2331},[96,11634,9051],{"class":9050},[96,11636,9106],{"class":9028},[96,11638,11639],{"class":9000},"[new-checkout-flag] › tests-environments\\checkout.new-checkout.spec.ts:7:9 › checkout (new-checkout flag environment) › checkout scenario 2",[96,11641,9138],{"class":9028},[96,11643,11644],{"class":98,"line":2337},[96,11645,11508],{"class":9000},[96,11647,11648,11650,11652,11655],{"class":98,"line":2358},[96,11649,9051],{"class":9050},[96,11651,9170],{"class":9028},[96,11653,11654],{"class":9000},"[spanish-locale] › tests-environments\\account.spanish.spec.ts:7:9 › account page (Spanish environment) › account scenario 1",[96,11656,11630],{"class":9028},[96,11658,11659],{"class":98,"line":2372},[96,11660,194],{"emptyLinePlaceholder":193},[96,11662,11663,11665],{"class":98,"line":2395},[96,11664,10256],{"class":9050},[96,11666,11667],{"class":9028}," (726ms)\n",[11,11669,11670,11671,11673],{},"All four projects ran together, each handing its own ",[24,11672,10873],{}," to its tests. The demo's tests check that each project provides a set, valid base URL that differs from every other environment's, but they don't prove an environment is actually configured, since the example URLs don't point at real servers.",[11,11675,2862,11676,11679,11680,11682,11683,11686,11687,11690,11691,11694],{},[24,11677,11678],{},"injected env"," lines are printed by ",[24,11681,10913],{},", not Playwright. The demo loads ",[24,11684,11685],{},".env.example"," so it runs out of the box. The ",[24,11688,11689],{},"(4)"," is the main process loading the four base URLs, and the ",[24,11692,11693],{},"(0)"," lines appear as each worker starts and loads the config.",[701,11696,11698],{"id":11697},"preconfigured-test-environments-the-cost","Preconfigured Test Environments: The Cost",[11,11700,11701],{},"Preconfigured test environments remove the need for serial tests since you can run those tests in parallel on isolated environments preconfigured with their preconditions where they won't pollute other tests running at the same time on other servers. This is the most performant of the solutions, but may not be possible for everyone.",[11,11703,11704],{},"For example, I want to use this approach, but I want these tests running on every pull request. We may have multiple pull requests that come in at a time and these preconfigured environments, which we provision from a base image with the PR applied, may need 5 or more flavors so that would mean creating 25 environments to support 5 pull requests. Since the system under test requires VMs instead of containers this is pretty expensive in provisioning time and quota.",[11,11706,11707],{},"Below are some things to evaluate:",[49,11709,11710,11716,11722,11728,11737],{},[52,11711,11712,11715],{},[55,11713,11714],{},"Hosting:"," Every flavor is another environment to pay for, and your hosting quota may not stretch to it.",[52,11717,11718,11721],{},[55,11719,11720],{},"Creating environments:"," It only works if you can stand up a preconfigured environment easily and repeatably. If a new environment takes days of manual setup, you won't keep several of them healthy.",[52,11723,11724,11727],{},[55,11725,11726],{},"Drift:"," Each flavor has to stay close to the baseline environment. A month-end environment that has quietly fallen behind the default one tests a different system.",[52,11729,11730,11733,11734,11736],{},[55,11731,11732],{},"Hardcoded URLs:"," A test that calls an absolute URL ignores ",[24,11735,10873],{},", and so does any test that reaches the server some other way, like a database connection.",[52,11738,11739,11742],{},[55,11740,11741],{},"Provisioning:"," Playwright only routes the tests. You still have to build and configure the environments yourself.",[11,11744,11745],{},"If you can't afford it, options A and B above are still solid. They're slower, because the serial tests wait for the parallel ones and run one at a time, but they need no extra environments.",[41,11747,11749],{"id":11748},"which-approach-to-use-for-serial-and-parallel-playwright-tests","Which Approach to Use for Serial and Parallel Playwright Tests",[364,11751,11752,11770],{},[367,11753,11754],{},[370,11755,11756,11759,11764,11767],{},[373,11757,11758],{},"Requirement",[373,11760,11761,11762],{},"Option A: Project ",[24,11763,9982],{},[373,11765,11766],{},"Option B: Separate CI steps",[373,11768,11769],{},"Option C: Preconfigured environments",[383,11771,11772,11785,11798,11810,11826,11839,11853],{},[370,11773,11774,11777,11780,11782],{},[388,11775,11776],{},"Tests change global state on the fly",[388,11778,11779],{},"Yes",[388,11781,11779],{},[388,11783,11784],{},"No, the environment is already set up",[370,11786,11787,11790,11792,11795],{},[388,11788,11789],{},"Runs state-changing tests after parallel ones",[388,11791,11779],{},[388,11793,11794],{},"Yes, by step order",[388,11796,11797],{},"No ordering needed, everything runs together",[370,11799,11800,11803,11806,11808],{},[388,11801,11802],{},"Runs state-changing tests when a parallel test fails",[388,11804,11805],{},"No",[388,11807,11779],{},[388,11809,11779],{},[370,11811,11812,11819,11821,11824],{},[388,11813,11814,11815,11818],{},"Single ",[24,11816,11817],{},"npx playwright test"," command",[388,11820,11779],{},[388,11822,11823],{},"No, one command per project",[388,11825,11779],{},[370,11827,11828,11831,11834,11836],{},[388,11829,11830],{},"Extra infrastructure",[388,11832,11833],{},"None",[388,11835,11833],{},[388,11837,11838],{},"One environment per flavor",[370,11840,11841,11844,11847,11850],{},[388,11842,11843],{},"Speed",[388,11845,11846],{},"Slowest",[388,11848,11849],{},"Slow",[388,11851,11852],{},"Fastest",[370,11854,11855,11858,11861,11864],{},[388,11856,11857],{},"Best when",[388,11859,11860],{},"The serial tests only make sense if the rest passed, like a smoke gate",[388,11862,11863],{},"The two suites are independent and you can't add environments",[388,11865,11866],{},"You can stand up preconfigured environments at a reasonable cost",[11,11868,11869,11870,11872],{},"If the serial tests build on the parallel ones, ",[24,11871,9982],{}," is the simpler option and skipping them on failure is the right behavior. For tests that only need the environment to themselves, use separate CI steps. If you can afford the environments, use those.",[41,11874,11876],{"id":11875},"reduce-how-many-tests-need-to-run-serially","Reduce How Many Tests Need to Run Serially",[11,11878,11879],{},"Splitting the suites fixes when the tests run. It doesn't make the state-changing tests any less fragile, and options A and B still need the environment to themselves. Fewer tests in the serial project means faster runs, so it's worth asking of each one whether it really needs to change global state or could be rewritten to leverage Playwright features that can emulate some of these things.",[11,11881,11882,11883,11886,11887,11891],{},"For example, if a test only depends on the browser's clock, Playwright has a ",[24,11884,11885],{},"page.clock"," API for controlling it (see the ",[391,11888],{"href":11889,"text":11890},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Fclock","Playwright Clock docs","). I haven't used it. It wouldn't help my case anyway, because the date I care about is on the server and triggers backend business processes, but it might solve some time travel test cases for others.",[701,11893,11895],{"id":11894},"testinfooutputpath-when-multiple-tests-save-the-same-file","testInfo.outputPath: When Multiple Tests Save the Same File",[11,11897,11898,11899,11902],{},"If you are running into situations where parallel files touch the same output file you can leverage Playwright ",[24,11900,11901],{},"testInfo.outputPath"," command which returns a path scoped to the current test to prevent parallel collisions.",[86,11904,11906],{"className":124,"code":11905,"filename":9410,"language":127,"meta":92,"style":92},"import { test } from '@playwright\u002Ftest';\n\ntest('exports rates PDF', async ({ page }, testInfo) => {\n  \u002F\u002F ...some sort of file download scenario\n  const file = testInfo.outputPath('insurance-rates.pdf'); \u002F\u002F Scoped, won't conflict with other tests downloading the same-named file with different contents\n  await download.saveAs(filePath);\n  \u002F\u002F ...verify the file contents\n});\n",[24,11907,11908,11928,11932,11963,11968,12000,12021,12026],{"__ignoreMap":92},[96,11909,11910,11912,11914,11916,11918,11920,11922,11924,11926],{"class":98,"line":99},[96,11911,135],{"class":134},[96,11913,139],{"class":138},[96,11915,143],{"class":142},[96,11917,152],{"class":138},[96,11919,155],{"class":134},[96,11921,159],{"class":158},[96,11923,162],{"class":106},[96,11925,165],{"class":158},[96,11927,168],{"class":138},[96,11929,11930],{"class":98,"line":171},[96,11931,194],{"emptyLinePlaceholder":193},[96,11933,11934,11936,11938,11940,11943,11945,11947,11949,11951,11953,11955,11957,11959,11961],{"class":98,"line":190},[96,11935,201],{"class":200},[96,11937,204],{"class":142},[96,11939,165],{"class":158},[96,11941,11942],{"class":106},"exports rates PDF",[96,11944,165],{"class":158},[96,11946,146],{"class":138},[96,11948,217],{"class":216},[96,11950,220],{"class":138},[96,11952,224],{"class":223},[96,11954,1123],{"class":138},[96,11956,1857],{"class":223},[96,11958,260],{"class":138},[96,11960,230],{"class":216},[96,11962,233],{"class":138},[96,11964,11965],{"class":98,"line":197},[96,11966,11967],{"class":1200},"  \u002F\u002F ...some sort of file download scenario\n",[96,11969,11970,11972,11975,11977,11979,11981,11984,11986,11988,11991,11993,11995,11997],{"class":98,"line":236},[96,11971,268],{"class":216},[96,11973,11974],{"class":271}," file",[96,11976,276],{"class":275},[96,11978,1857],{"class":142},[96,11980,244],{"class":138},[96,11982,11983],{"class":200},"outputPath",[96,11985,204],{"class":250},[96,11987,165],{"class":158},[96,11989,11990],{"class":106},"insurance-rates.pdf",[96,11992,165],{"class":158},[96,11994,260],{"class":250},[96,11996,4492],{"class":138},[96,11998,11999],{"class":1200}," \u002F\u002F Scoped, won't conflict with other tests downloading the same-named file with different contents\n",[96,12001,12002,12004,12007,12009,12012,12014,12017,12019],{"class":98,"line":265},[96,12003,239],{"class":134},[96,12005,12006],{"class":142}," download",[96,12008,244],{"class":138},[96,12010,12011],{"class":200},"saveAs",[96,12013,204],{"class":250},[96,12015,12016],{"class":142},"filePath",[96,12018,260],{"class":250},[96,12020,168],{"class":138},[96,12022,12023],{"class":98,"line":309},[96,12024,12025],{"class":1200},"  \u002F\u002F ...verify the file contents\n",[96,12027,12028,12030,12032],{"class":98,"line":337},[96,12029,340],{"class":138},[96,12031,260],{"class":142},[96,12033,168],{"class":138},[701,12035,12037],{"id":12036},"test-locks-when-only-a-few-tests-share-the-resource","Test Locks: When Only a Few Tests Share the Resource",[11,12039,12040,12041,12045,12046,12049],{},"Playwright 1.63 added ",[391,12042],{"href":12043,"text":12044},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftest-parallel#test-locks","test locks",". Give the tests that touch the same shared resource the same named ",[24,12047,12048],{},"lock",", and Playwright never runs them at the same time, across files, workers and projects, while every other test keeps running in parallel.",[86,12051,12054],{"className":124,"code":12052,"filename":12053,"language":127,"meta":92,"style":92},"test('feature flag on enables new checkout', { lock: 'server-settings' }, async () => {\n  \u002F\u002F ...\n});\n\ntest('server date set to month end triggers billing rollover', { lock: 'server-settings' }, async () => {\n  \u002F\u002F ...\n});\n","tests-locks\u002Fmutators.spec.ts",[24,12055,12056,12094,12099,12107,12111,12147,12151],{"__ignoreMap":92},[96,12057,12058,12060,12062,12064,12066,12068,12070,12072,12075,12077,12079,12082,12084,12086,12088,12090,12092],{"class":98,"line":99},[96,12059,201],{"class":200},[96,12061,204],{"class":142},[96,12063,165],{"class":158},[96,12065,9818],{"class":106},[96,12067,165],{"class":158},[96,12069,146],{"class":138},[96,12071,139],{"class":138},[96,12073,12074],{"class":250}," lock",[96,12076,728],{"class":138},[96,12078,159],{"class":158},[96,12080,12081],{"class":106},"server-settings",[96,12083,165],{"class":158},[96,12085,1123],{"class":138},[96,12087,217],{"class":216},[96,12089,1095],{"class":138},[96,12091,230],{"class":216},[96,12093,233],{"class":138},[96,12095,12096],{"class":98,"line":171},[96,12097,12098],{"class":1200},"  \u002F\u002F ...\n",[96,12100,12101,12103,12105],{"class":98,"line":190},[96,12102,340],{"class":138},[96,12104,260],{"class":142},[96,12106,168],{"class":138},[96,12108,12109],{"class":98,"line":197},[96,12110,194],{"emptyLinePlaceholder":193},[96,12112,12113,12115,12117,12119,12121,12123,12125,12127,12129,12131,12133,12135,12137,12139,12141,12143,12145],{"class":98,"line":236},[96,12114,201],{"class":200},[96,12116,204],{"class":142},[96,12118,165],{"class":158},[96,12120,8877],{"class":106},[96,12122,165],{"class":158},[96,12124,146],{"class":138},[96,12126,139],{"class":138},[96,12128,12074],{"class":250},[96,12130,728],{"class":138},[96,12132,159],{"class":158},[96,12134,12081],{"class":106},[96,12136,165],{"class":158},[96,12138,1123],{"class":138},[96,12140,217],{"class":216},[96,12142,1095],{"class":138},[96,12144,230],{"class":216},[96,12146,233],{"class":138},[96,12148,12149],{"class":98,"line":265},[96,12150,12098],{"class":1200},[96,12152,12153,12155,12157],{"class":98,"line":309},[96,12154,340],{"class":138},[96,12156,260],{"class":142},[96,12158,168],{"class":138},[11,12160,12161,12162,12164],{},"This is a good fit when a few tests only conflict with each other, like several tests that edit the same account setting. Those can stay in the main parallel suite instead of moving to the ",[24,12163,8816],{}," project. In the demo, the two tests above overwrote each other's state and failed in every one of 5 runs when I removed the lock, and passed in every one of 5 runs with it.",[11,12166,12167,12168,12170,12171,12174],{},"It doesn't solve my problem, though. A lock only keeps apart the tests that hold it. The ",[24,12169,8812],{}," tests that read the settings don't hold it, so they keep running while a locked test has the state changed. That's what ",[24,12172,12173],{},"npm run test:locks:partial"," shows in the demo:",[86,12176,12178],{"className":8991,"code":12177,"filename":90,"language":8993,"meta":92,"style":92},"npm run test:locks:partial\n\n> playwright-serial-parallel-dependencies-demo@1.0.0 test:locks:partial\n> playwright test -c playwright.locks.config.ts mutators readers-unlocked\n\n\n\u001b[2mRunning \u001b[22m6\u001b[2m tests using \u001b[22m5\u001b[2m workers\u001b[22m\n\n  \u001b[31m✘\u001b[39m  \u001b[2m3 \u001b[22m\u001b[31mtests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 3 renders with default settings\u001b[39m\u001b[2m (320ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m1 \u001b[22m\u001b[31mtests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 2 renders with default settings\u001b[39m\u001b[2m (322ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m5 \u001b[22m\u001b[31mtests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings\u001b[39m\u001b[2m (315ms)\u001b[22m\n  \u001b[31m✘\u001b[39m  \u001b[2m4 \u001b[22m\u001b[31mtests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 4 renders with default settings\u001b[39m\u001b[2m (322ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m2 \u001b[22mtests-locks\\mutators.spec.ts:13:5 › feature flag on enables new checkout\u001b[2m (518ms)\u001b[22m\n  \u001b[32m✓\u001b[39m  \u001b[2m6 \u001b[22mtests-locks\\mutators.spec.ts:19:5 › server date set to month end triggers billing rollover\u001b[2m (503ms)\u001b[22m\n\n\n\u001b[31m  1) tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings \u001b[39m\n\n    Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) \u002F\u002F Object.is equality\u001b[22m\n\n    Expected: \u001b[32m\"o\u001b[7mff\u001b[27m\"\u001b[39m\n    Received: \u001b[31m\"o\u001b[7mn\u001b[27m\"\u001b[39m\n\n    ...\n\n\u001b[31m  4 failed\u001b[39m\n\u001b[31m    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings \u001b[39m\n\u001b[31m    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 2 renders with default settings \u001b[39m\n\u001b[31m    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 3 renders with default settings \u001b[39m\n\u001b[31m    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 4 renders with default settings \u001b[39m\n\u001b[32m  2 passed\u001b[39m\u001b[2m (1.3s)\u001b[22m\n",[24,12179,12180,12185,12189,12194,12199,12203,12207,12220,12224,12236,12248,12261,12273,12286,12299,12303,12307,12312,12316,12334,12338,12348,12358,12362,12366,12370,12375,12380,12385,12390,12395],{"__ignoreMap":92},[96,12181,12182],{"class":98,"line":99},[96,12183,12184],{"class":9000},"npm run test:locks:partial\n",[96,12186,12187],{"class":98,"line":171},[96,12188,194],{"emptyLinePlaceholder":193},[96,12190,12191],{"class":98,"line":190},[96,12192,12193],{"class":9000},"> playwright-serial-parallel-dependencies-demo@1.0.0 test:locks:partial\n",[96,12195,12196],{"class":98,"line":197},[96,12197,12198],{"class":9000},"> playwright test -c playwright.locks.config.ts mutators readers-unlocked\n",[96,12200,12201],{"class":98,"line":236},[96,12202,194],{"emptyLinePlaceholder":193},[96,12204,12205],{"class":98,"line":265},[96,12206,194],{"emptyLinePlaceholder":193},[96,12208,12209,12211,12214,12216,12218],{"class":98,"line":309},[96,12210,9029],{"class":9028},[96,12212,12213],{"class":9000},"6",[96,12215,9035],{"class":9028},[96,12217,9038],{"class":9000},[96,12219,9041],{"class":9028},[96,12221,12222],{"class":98,"line":337},[96,12223,194],{"emptyLinePlaceholder":193},[96,12225,12226,12228,12231,12234],{"class":98,"line":1149},[96,12227,9129],{"class":9128},[96,12229,12230],{"class":9028},"  3 ",[96,12232,12233],{"class":9128},"tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 3 renders with default settings",[96,12235,10151],{"class":9028},[96,12237,12238,12240,12243,12246],{"class":98,"line":1167},[96,12239,9129],{"class":9128},[96,12241,12242],{"class":9028},"  1 ",[96,12244,12245],{"class":9128},"tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 2 renders with default settings",[96,12247,10385],{"class":9028},[96,12249,12250,12252,12255,12258],{"class":98,"line":1182},[96,12251,9129],{"class":9128},[96,12253,12254],{"class":9028},"  5 ",[96,12256,12257],{"class":9128},"tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings",[96,12259,12260],{"class":9028}," (315ms)\n",[96,12262,12263,12265,12268,12271],{"class":98,"line":1204},[96,12264,9129],{"class":9128},[96,12266,12267],{"class":9028},"  4 ",[96,12269,12270],{"class":9128},"tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 4 renders with default settings",[96,12272,10385],{"class":9028},[96,12274,12275,12277,12280,12283],{"class":98,"line":1226},[96,12276,9051],{"class":9050},[96,12278,12279],{"class":9028},"  2 ",[96,12281,12282],{"class":9000},"tests-locks\\mutators.spec.ts:13:5 › feature flag on enables new checkout",[96,12284,12285],{"class":9028}," (518ms)\n",[96,12287,12288,12290,12293,12296],{"class":98,"line":1234},[96,12289,9051],{"class":9050},[96,12291,12292],{"class":9028},"  6 ",[96,12294,12295],{"class":9000},"tests-locks\\mutators.spec.ts:19:5 › server date set to month end triggers billing rollover",[96,12297,12298],{"class":9028}," (503ms)\n",[96,12300,12301],{"class":98,"line":1240},[96,12302,194],{"emptyLinePlaceholder":193},[96,12304,12305],{"class":98,"line":2227},[96,12306,194],{"emptyLinePlaceholder":193},[96,12308,12309],{"class":98,"line":2233},[96,12310,12311],{"class":9128},"  1) tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings \n",[96,12313,12314],{"class":98,"line":2254},[96,12315,194],{"emptyLinePlaceholder":193},[96,12317,12318,12320,12322,12324,12326,12328,12330,12332],{"class":98,"line":2275},[96,12319,9215],{"class":9000},[96,12321,9218],{"class":9028},[96,12323,9221],{"class":9128},[96,12325,2885],{"class":9028},[96,12327,4622],{"class":9000},[96,12329,204],{"class":9028},[96,12331,9230],{"class":9050},[96,12333,9233],{"class":9028},[96,12335,12336],{"class":98,"line":2296},[96,12337,194],{"emptyLinePlaceholder":193},[96,12339,12340,12342,12344,12346],{"class":98,"line":2309},[96,12341,9242],{"class":9000},[96,12343,9245],{"class":9050},[96,12345,9249],{"class":9248},[96,12347,2224],{"class":9050},[96,12349,12350,12352,12354,12356],{"class":98,"line":2315},[96,12351,9256],{"class":9000},[96,12353,9245],{"class":9128},[96,12355,1584],{"class":9261},[96,12357,2224],{"class":9128},[96,12359,12360],{"class":98,"line":2331},[96,12361,194],{"emptyLinePlaceholder":193},[96,12363,12364],{"class":98,"line":2337},[96,12365,9272],{"class":9000},[96,12367,12368],{"class":98,"line":2358},[96,12369,194],{"emptyLinePlaceholder":193},[96,12371,12372],{"class":98,"line":2372},[96,12373,12374],{"class":9128},"  4 failed\n",[96,12376,12377],{"class":98,"line":2395},[96,12378,12379],{"class":9128},"    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 1 renders with default settings \n",[96,12381,12382],{"class":98,"line":2416},[96,12383,12384],{"class":9128},"    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 2 renders with default settings \n",[96,12386,12387],{"class":98,"line":2433},[96,12388,12389],{"class":9128},"    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 3 renders with default settings \n",[96,12391,12392],{"class":98,"line":2454},[96,12393,12394],{"class":9128},"    tests-locks\\readers-unlocked.spec.ts:9:9 › reports (no lock) › report 4 renders with default settings \n",[96,12396,12397,12400],{"class":98,"line":2475},[96,12398,12399],{"class":9050},"  2 passed",[96,12401,12402],{"class":9028}," (1.3s)\n",[11,12404,12405],{},"All four unlocked tests failed in each of 5 runs on my machine. You could give those tests the same lock and everything passes, but then they run one at a time too: the demo took about 2.6 seconds that way, against 1.3 seconds with the readers unlocked. When a change affects every test, like the server date, putting a lock on everything is a serial run again so locking should be used sparingly.",[11,12407,12408,12409,12411],{},"Also note that a lock on any test in a spec file is held for the duration of the whole file. That means if one test in a spec file needs the lock, the other tests in that file count as holding it too, and a locked test in another file has to wait until the whole spec file finishes. Keeping the tests that need a lock in their own file, or turning on ",[24,12410,9682],{},", avoids that.",[41,12413,12415],{"id":12414},"takeaway-run-serial-and-parallel-playwright-tests-together-without-failures","Takeaway: Run Serial and Parallel Playwright Tests Together Without Failures",[11,12417,12418,12419,12421],{},"Tests that change global state don't have to force your whole suite to run serially. Mark them, match them with a filename or a tag, and give them their own project with ",[24,12420,9336],{},". Then choose how the projects relate:",[49,12423,12424,12432,12438],{},[52,12425,12426,12431],{},[55,12427,12428,12429,146],{},"Option A, ",[24,12430,9982],{}," is the quick win when the serial tests should only run once everything else has passed.",[52,12433,12434,12437],{},[55,12435,12436],{},"Option B, separate CI steps,"," takes a little more wiring, but a red parallel run never hides your serial results.",[52,12439,12440,12443],{},[55,12441,12442],{},"Option C, preconfigured environments,"," is my long-term goal: if you can afford them, the tests never have to change global state at all.",[11,12445,12446,12447,244],{},"The demo repo has every variant if you want to try them. For more, see my ",[18,12448,12450],{"href":12449},"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright","Playwright articles",[2956,12452],{":items":12453},"[\"\u002Fsoftware-testing\u002Ftest-automation\u002Fwhat-would-you-stop-doing-when-ui-tests-are-flaky\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Frun-fewer-tests-catch-the-same-bugs\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fstareast-2026-playwright-ai-cost-efficient-testing\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation\"]",[2960,12455,12456],{},"html pre.shiki code .sZ-rw,html code.shiki .sZ-rw{--shiki-light:#90A4AE;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sPJuK,html code.shiki .sPJuK{--shiki-light:#39ADB5;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sb1SK,html code.shiki .sb1SK{--shiki-light:#6182B8;--shiki-default:#622CBC;--shiki-dark:#DBB7FF}html pre.shiki code .stWsX,html code.shiki .stWsX{--shiki-light:#9C3EDA;--shiki-default:#A0111F;--shiki-dark:#FF9492}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 .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 .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 .sOsy2,html code.shiki .sOsy2{--shiki-light:#90A4AE;--shiki-default:#0e1116;--shiki-dark:#f0f3f6}html pre.shiki code .sFDEC,html code.shiki .sFDEC{--shiki-light:#90A4AE80;--shiki-default:#0e111680;--shiki-dark:#f0f3f680}html pre.shiki code .ssIYW,html code.shiki .ssIYW{--shiki-light:#91B859;--shiki-default:#024c1a;--shiki-dark:#26cd4d}html pre.shiki code .sCdEk,html code.shiki .sCdEk{--shiki-light:#39ADB5;--shiki-default:#1b7c83;--shiki-dark:#39c5cf}html pre.shiki code .sDbPz,html code.shiki .sDbPz{--shiki-light:#E53935;--shiki-default:#a0111f;--shiki-dark:#ff9492}html pre.shiki code .smSMi,html code.shiki .smSMi{--shiki-light:#E5393580;--shiki-default:#a0111f80;--shiki-dark:#ff949280}html pre.shiki code .sBsy4,html code.shiki .sBsy4{--shiki-light:#FAFAFA;--shiki-light-bg:#91B859;--shiki-default:#ffffff;--shiki-default-bg:#024c1a;--shiki-dark:#0a0c10;--shiki-dark-bg:#26cd4d}html pre.shiki code .sYBWh,html code.shiki .sYBWh{--shiki-light:#FAFAFA;--shiki-light-bg:#E53935;--shiki-default:#ffffff;--shiki-default-bg:#a0111f;--shiki-dark:#0a0c10;--shiki-dark-bg:#ff9492}html pre.shiki code .spoJ1,html code.shiki .spoJ1{--shiki-light:#E2931D;--shiki-default:#3f2200;--shiki-dark:#f0b72f}html pre.shiki code .sLHsM,html code.shiki .sLHsM{--shiki-light:#90A4AE;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .sCRTB,html code.shiki .sCRTB{--shiki-light:#39ADB5;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .spMcu,html code.shiki .spMcu{--shiki-light:#91B859;--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 .szoTG,html code.shiki .szoTG{--shiki-light:#90A4AE;--shiki-light-font-weight:inherit;--shiki-default:#024C1A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .sTqCK,html code.shiki .sTqCK{--shiki-light:#FF5370;--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .s6g51,html code.shiki .s6g51{--shiki-light:#F76D47;--shiki-default:#023B95;--shiki-dark:#91CBFF}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 .sA8fK,html code.shiki .sA8fK{--shiki-light:#E2931D;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .saWzx,html code.shiki .saWzx{--shiki-light:#E53935;--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .sQ79N,html code.shiki .sQ79N{--shiki-light:#90A4AE;--shiki-default:#023B95;--shiki-dark:#91CBFF}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}",{"title":92,"searchDepth":171,"depth":171,"links":12458},[12459,12460,12461,12462,12465,12468,12469,12472,12473,12477],{"id":8758,"depth":171,"text":8759},{"id":8805,"depth":171,"text":8806},{"id":9323,"depth":171,"text":9324},{"id":9388,"depth":171,"text":9389,"children":12463},[12464],{"id":9713,"depth":190,"text":9714},{"id":9975,"depth":171,"text":9976,"children":12466},[12467],{"id":10262,"depth":190,"text":10263},{"id":10486,"depth":171,"text":10487},{"id":10850,"depth":171,"text":10851,"children":12470},[12471],{"id":11697,"depth":190,"text":11698},{"id":11748,"depth":171,"text":11749},{"id":11875,"depth":171,"text":11876,"children":12474},[12475,12476],{"id":11894,"depth":190,"text":11895},{"id":12036,"depth":190,"text":12037},{"id":12414,"depth":171,"text":12415},"\u002Fimages\u002Fposts\u002Frun-parallel-and-serial-tests-in-playwright\u002Frun-parallel-and-serial-tests-in-playwright-cover.webp","2026-09-20","Playwright tests that change global state break parallel test runs. Learn different ways to run serial and parallel tests together without failures.",{},{"title":8734,"description":12480},{"loc":360},"software-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright","ZLP4d0dCB2oVGoMCAazwf-dIixczF7EdjP00TkwyV-0",{"id":12487,"title":12488,"bmcUsername":6,"body":12489,"cover":13126,"date":13127,"description":13128,"draft":2989,"extension":2990,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":13129,"navigation":193,"npmPackage":6,"order":6,"path":13130,"review":6,"seo":13131,"sitemap":13132,"stem":13133,"updated":2987,"__hash__":13134},"content\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation.md","Best Websites for Practicing Test Automation and Playwright",{"type":8,"value":12490,"toc":13100},[12491,12494,12498,12713,12717,12720,12723,12743,12746,12750,12753,12850,12853,12857,12860,12863,12873,12876,12879,12883,12886,12889,12893,12896,12899,12903,12906,12909,12913,12916,12919,12923,12926,12929,12932,12935,12939,12943,12946,12950,12954,12957,12960,12963,12966,12977,12980,12984,12989,13003,13007,13013,13016,13020,13023,13026,13030,13033,13036,13040,13043,13046,13050,13053,13057,13065,13068,13076,13079,13087,13090,13094,13097],[11,12492,12493],{},"These are the demo websites I recommend for practicing Playwright test automation, and they work just as well for Selenium, Cypress, WebDriverIO, or Nightwatch. They cover the scenarios that are hard to practice on your day job: frames, shadow DOM, flaky load timing, multi-step app workflows, and APIs that need auth tokens.",[41,12495,12497],{"id":12496},"test-automation-practice-websites-at-a-glance","Test Automation Practice Websites at a Glance",[364,12499,12500,12513],{},[367,12501,12502],{},[370,12503,12504,12507,12510],{},[373,12505,12506],{},"Site",[373,12508,12509],{},"Best for",[373,12511,12512],{},"Type",[383,12514,12515,12529,12543,12556,12569,12582,12595,12608,12621,12634,12647,12660,12674,12687,12700],{},[370,12516,12517,12523,12526],{},[388,12518,12519],{},[18,12520,12522],{"href":12521},"#the-internet","The Internet",[388,12524,12525],{},"Hard-to-automate edge cases: frames, shadow DOM, new windows, file upload",[388,12527,12528],{},"UI",[370,12530,12531,12537,12540],{},[388,12532,12533],{},[18,12534,12536],{"href":12535},"#automation-exercise","Automation Exercise",[388,12538,12539],{},"Beginners: e-commerce store with written test cases to follow",[388,12541,12542],{},"UI + API",[370,12544,12545,12551,12554],{},[388,12546,12547],{},[18,12548,12550],{"href":12549},"#letcode","LetCode",[388,12552,12553],{},"Beginners: one web element at a time (inputs, tables, alerts, calendars)",[388,12555,12528],{},[370,12557,12558,12564,12567],{},[388,12559,12560],{},[18,12561,12563],{"href":12562},"#ui-testing-playground","UI Testing Playground",[388,12565,12566],{},"Timing and dynamic element edge cases",[388,12568,12528],{},[370,12570,12571,12577,12580],{},[388,12572,12573],{},[18,12574,12576],{"href":12575},"#saucedemo","SauceDemo",[388,12578,12579],{},"Login and cart flows with different user behaviors",[388,12581,12528],{},[370,12583,12584,12590,12593],{},[388,12585,12586],{},[18,12587,12589],{"href":12588},"#orangehrm","OrangeHRM",[388,12591,12592],{},"A complete, realistic business app",[388,12594,12528],{},[370,12596,12597,12603,12606],{},[388,12598,12599],{},[18,12600,12602],{"href":12601},"#parabank","ParaBank",[388,12604,12605],{},"Realistic banking workflows",[388,12607,12528],{},[370,12609,12610,12616,12619],{},[388,12611,12612],{},[18,12613,12615],{"href":12614},"#globalsqa-angular-banking-site","GlobalsQA Banking",[388,12617,12618],{},"Testing an Angular app",[388,12620,12528],{},[370,12622,12623,12629,12632],{},[388,12624,12625],{},[18,12626,12628],{"href":12627},"#automatenow-forms-sandbox","AutomateNow Sandbox",[388,12630,12631],{},"Form controls, pop-ups, and search",[388,12633,12528],{},[370,12635,12636,12642,12645],{},[388,12637,12638],{},[18,12639,12641],{"href":12640},"#qa-practice","QA-Practice",[388,12643,12644],{},"Mixed UI and API practice, plus a buggy form for bug hunting",[388,12646,12542],{},[370,12648,12649,12655,12658],{},[388,12650,12651],{},[18,12652,12654],{"href":12653},"#the-sweet-shop","The Sweet Shop",[388,12656,12657],{},"E-commerce with hidden defects",[388,12659,12528],{},[370,12661,12662,12668,12671],{},[388,12663,12664],{},[18,12665,12667],{"href":12666},"#swagger-petstore","Swagger Petstore",[388,12669,12670],{},"Swagger-documented REST API",[388,12672,12673],{},"API",[370,12675,12676,12682,12685],{},[388,12677,12678],{},[18,12679,12681],{"href":12680},"#restful-booker","Restful Booker",[388,12683,12684],{},"CRUD API testing with auth tokens",[388,12686,12673],{},[370,12688,12689,12695,12698],{},[388,12690,12691],{},[18,12692,12694],{"href":12693},"#fakerestapi","FakeRestAPI",[388,12696,12697],{},"Simple REST endpoints, no auth",[388,12699,12673],{},[370,12701,12702,12708,12711],{},[388,12703,12704],{},[18,12705,12707],{"href":12706},"#star-wars-api","Star Wars API",[388,12709,12710],{},"Read-only API for building a test suite or Postman collection",[388,12712,12673],{},[41,12714,12716],{"id":12715},"why-use-demo-websites-for-test-automation-practice","Why Use Demo Websites for Test Automation Practice",[11,12718,12719],{},"Before investing time in learning and building out a test suite against a new automation framework it is a good idea to try it out against different kinds of websites to ensure the framework performs reliably and is easy to work with. For example, if you are using Selenium currently, but are interested in trying Cypress, Playwright, WebDriverIO, or Nightwatch try doing more than just the basic example against these sites. This way you don't get too invested to find out your chosen automation framework doesn't work on some important area you need to write automation for.",[11,12721,12722],{},"You'll want to try out your test automation framework against things like:",[49,12724,12725,12728,12731,12734,12737,12740],{},[52,12726,12727],{},"iFrames",[52,12729,12730],{},"Modal dialogs",[52,12732,12733],{},"New tabs",[52,12735,12736],{},"Multiple windows",[52,12738,12739],{},"Asynchronous loading of content",[52,12741,12742],{},"File upload dialogs",[11,12744,12745],{},"You may not have one or all of these scenarios in your current automated software testing project. These demo sites become useful to provide somewhere to practice skills around these harder scenarios if you don't get exposure in your normal everyday feature testing.",[41,12747,12749],{"id":12748},"best-playwright-practice-websites-by-feature","Best Playwright Practice Websites by Feature",[11,12751,12752],{},"If you're learning Playwright specifically, here's where to practice the features you'll lean on most:",[49,12754,12755,12769,12779,12792,12805,12820,12829,12839],{},[52,12756,12757,12760,12761,12765,12766,244],{},[55,12758,12759],{},"Frames:"," The Internet's ",[391,12762],{"href":12763,"text":12764},"https:\u002F\u002Fthe-internet.herokuapp.com\u002Fnested_frames","nested frames"," page is good practice for ",[24,12767,12768],{},"frameLocator()",[52,12770,12771,12760,12774,12778],{},[55,12772,12773],{},"Shadow DOM:",[391,12775],{"href":12776,"text":12777},"https:\u002F\u002Fthe-internet.herokuapp.com\u002Fshadowdom","shadow DOM"," page. Playwright locators pierce open shadow roots by default, so this is a good way to see that in action.",[52,12780,12781,12760,12784,12788,12789,12791],{},[55,12782,12783],{},"New tabs and windows:",[391,12785],{"href":12786,"text":12787},"https:\u002F\u002Fthe-internet.herokuapp.com\u002Fwindows","multiple windows"," page, for practicing waiting on a new ",[24,12790,6331],{}," from the browser context.",[52,12793,12794,12760,12797,12801,12802,244],{},[55,12795,12796],{},"File upload:",[391,12798],{"href":12799,"text":12800},"https:\u002F\u002Fthe-internet.herokuapp.com\u002Fupload","file upload"," page, for ",[24,12803,12804],{},"setInputFiles()",[52,12806,12807,12810,12811,27,12815,12819],{},[55,12808,12809],{},"Auto-waiting and slow content:"," UI Testing Playground's ",[391,12812],{"href":12813,"text":12814},"http:\u002F\u002Fwww.uitestingplayground.com\u002Floaddelay","load delay",[391,12816],{"href":12817,"text":12818},"http:\u002F\u002Fwww.uitestingplayground.com\u002Fajax","AJAX data"," pages show how Playwright's auto-waiting handles delays without manual sleeps.",[52,12821,12822,12825,12826,244],{},[55,12823,12824],{},"Reusing login state:"," SauceDemo's multiple test users make it a good place to practice saving and reusing authentication with ",[24,12827,12828],{},"storageState",[52,12830,12831,12834,12835,12838],{},[55,12832,12833],{},"API testing:"," Restful Booker requires an auth token for updates and deletes, which is good practice for Playwright's ",[24,12836,12837],{},"request"," fixture.",[52,12840,12841,12844,12845,12849],{},[55,12842,12843],{},"Playwright's own demo:"," ",[391,12846],{"href":12847,"text":12848},"https:\u002F\u002Fdemo.playwright.dev\u002Ftodomvc","demo.playwright.dev\u002Ftodomvc"," is the TodoMVC app used throughout Playwright's documentation, so you can follow along with the docs against the same app.",[11,12851,12852],{},"For bigger end-to-end flows, OrangeHRM and ParaBank are full applications where you can practice page objects and test data setup across many pages.",[41,12854,12856],{"id":12855},"recommended-demo-websites-for-test-automation-practice","Recommended Demo Websites for Test Automation Practice",[11,12858,12859],{},"I've compiled a list of the best websites I've come across to practice or demo test automation frameworks against. They include the harder to automate scenarios just mentioned.",[701,12861,12522],{"id":12862},"the-internet",[11,12864,12865,4218,12869],{},[391,12866],{"href":12867,"text":12868},"http:\u002F\u002Fthe-internet.herokuapp.com\u002F","Live site",[391,12870],{"href":12871,"text":12872},"https:\u002F\u002Fgithub.com\u002Fsaucelabs\u002Fthe-internet","GitHub",[11,12874,12875],{},"This GitHub repository is a collection of common test automation scenarios including hard to automate situations; nested frames, shadow DOM, keypresses, and complicated DOMs. The hosting on this site can be unreliable so fall back to their GitHub if it happens to be down.",[701,12877,12536],{"id":12878},"automation-exercise",[391,12880],{"href":12881,"text":12882},"https:\u002F\u002Fwww.automationexercise.com\u002F","automationexercise.com",[11,12884,12885],{},"This example clothing storefront has both a web front end to test against and APIs. The site even includes test cases to guide you.",[701,12887,12550],{"id":12888},"letcode",[391,12890],{"href":12891,"text":12892},"https:\u002F\u002Fletcode.in\u002Ftest","letcode.in",[11,12894,12895],{},"This site isolates examples around common DOM elements letting you practice on specific examples of web elements like inputs, tables, alerts, slides, calendars, and more.",[701,12897,12563],{"id":12898},"ui-testing-playground",[391,12900],{"href":12901,"text":12902},"http:\u002F\u002Fwww.uitestingplayground.com\u002F","uitestingplayground.com",[11,12904,12905],{},"Smaller site, but it contains edge cases for load delays, mouse over behavior, dynamic IDs, and automation issues arising from hidden layers.",[701,12907,12576],{"id":12908},"saucedemo",[391,12910],{"href":12911,"text":12912},"https:\u002F\u002Fwww.saucedemo.com\u002F","saucedemo.com",[11,12914,12915],{},"Another demo web storefront (Swag Labs) useful for testing login and shopping cart flows. A key distinction on this site is that it has several different logins that give you different experiences on the same site: standard, locked out, problem user, performance glitch, error user, and visual user. Maintained by the folks at Sauce Labs.",[701,12917,12589],{"id":12918},"orangehrm",[391,12920],{"href":12921,"text":12922},"https:\u002F\u002Fopensource-demo.orangehrmlive.com\u002F","OrangeHRM demo",[11,12924,12925],{},"A live demo of the open source OrangeHRM human resources system. I've used it in the past and it's pretty complete, and a little more polished than ParaBank. It's a full business application with modules for admin, employee records (PIM), leave, time, recruitment, performance, claims, and more, so it's a realistic target for practicing page objects and multi-page workflows. The demo login credentials are shown on the login page.",[701,12927,12602],{"id":12928},"parabank",[391,12930],{"href":12931,"text":12602},"https:\u002F\u002Fparabank.parasoft.com\u002Fparabank\u002Findex.htm",[11,12933,12934],{},"A demo online bank maintained by Parasoft. Register an account and you can practice realistic banking workflows like transferring funds, bill pay, and checking account history.",[701,12936,12938],{"id":12937},"globalsqa-angular-banking-site","GlobalsQA Angular Banking Site",[391,12940],{"href":12941,"text":12942},"https:\u002F\u002Fwww.globalsqa.com\u002FangularJs-protractor\u002FBankingProject","GlobalsQA",[11,12944,12945],{},"Very small example bank website written in Angular for testing your automation framework against a website written in Angular. The site has login scenarios as well as transaction listing, deposits, and withdrawal behavior workflows.",[701,12947,12949],{"id":12948},"automatenow-forms-sandbox","AutomateNow Forms Sandbox",[391,12951],{"href":12952,"text":12953},"https:\u002F\u002Fautomatenow.io\u002Fsandbox\u002F","AutomateNow",[11,12955,12956],{},"Small, straight-forward single page site for practicing interactions with typical form controls, search, pop up dialogs, and a map.",[701,12958,12641],{"id":12959},"qa-practice",[391,12961],{"href":12962,"text":12641},"https:\u002F\u002Fqa-practice.razvanvancea.ro\u002F",[11,12964,12965],{},"Clean site that combines several different testing exercises in a small e-commerce site:",[49,12967,12968,12971,12974],{},[52,12969,12970],{},"Common web elements",[52,12972,12973],{},"API with Swagger documentation",[52,12975,12976],{},"Buggy web form you can use to challenge candidates to see how many bugs they can discover.",[701,12978,12654],{"id":12979},"the-sweet-shop",[391,12981],{"href":12982,"text":12983},"https:\u002F\u002Fsweetshop.netlify.app\u002F","sweetshop.netlify.app",[11,12985,12986,12988],{},[606,12987,12654],{}," is a fake e-commerce front end for a candy store with hidden defects and opportunity to automate:",[49,12990,12991,12994,12997,13000],{},[52,12992,12993],{},"Account login",[52,12995,12996],{},"Shopping cart",[52,12998,12999],{},"Product pages",[52,13001,13002],{},"Non-US currency",[41,13004,13006],{"id":13005},"recommended-api-testing-sites","Recommended API Testing Sites",[11,13008,13009,13010,13012],{},"If you are learning API test automation with Playwright's ",[24,13011,12837],{}," fixture, SuperTest + Mocha, or Postman, the following sites are handy REST APIs to practice tests against.",[701,13014,12667],{"id":13015},"swagger-petstore",[391,13017],{"href":13018,"text":13019},"https:\u002F\u002Fpetstore.swagger.io\u002F","petstore.swagger.io",[11,13021,13022],{},"Manage an example pet store inventory through this API. This site shows off the usefulness in Swagger when documenting REST APIs, but you can also practice API testing with it.",[701,13024,12681],{"id":13025},"restful-booker",[391,13027],{"href":13028,"text":13029},"https:\u002F\u002Frestful-booker.herokuapp.com\u002Fapidoc\u002Findex.html","restful-booker.herokuapp.com",[11,13031,13032],{},"Test all kinds of CRUD (Create, Read, Update, Delete) REST API scenarios using this example booking API, including creating an auth token for updates and deletes.",[701,13034,12694],{"id":13035},"fakerestapi",[391,13037],{"href":13038,"text":13039},"https:\u002F\u002Ffakerestapi.azurewebsites.net\u002Findex.html","fakerestapi.azurewebsites.net",[11,13041,13042],{},"More practice REST endpoints around a book store inventory API. This one lacks Auth operations so you can't practice token creation. However, Restful Booker (above), does support this.",[701,13044,12707],{"id":13045},"star-wars-api",[391,13047],{"href":13048,"text":13049},"https:\u002F\u002Fswapi.dev\u002F","SWAPI",[11,13051,13052],{},"A well-documented public API for Star Wars data that can be used for practicing building your own Postman collection or automated API test suite.",[41,13054,13056],{"id":13055},"other-lists-more-reading","Other Lists \u002F More Reading",[2876,13058,13059],{},[52,13060,13061],{},[391,13062],{"href":13063,"text":13064},"https:\u002F\u002Fgithub.com\u002FBMayhew\u002Fawesome-sites-to-test-on","Awesome Sites to Test On",[11,13066,13067],{},"Butch Mayhew maintains a list of more sites categorized by security, mobile, performance, and web.",[2876,13069,13070],{"start":171},[52,13071,13072],{},[391,13073],{"href":13074,"text":13075},"https:\u002F\u002Fautomationpanda.com\u002F2021\u002F12\u002F29\u002Fwant-to-practice-test-automation-try-these-demo-sites\u002F","Automation Panda",[11,13077,13078],{},"Recommended sites by Andrew Knight.",[2876,13080,13081],{"start":190},[52,13082,13083],{},[18,13084,13086],{"href":13085},"\u002Fsoftware-testing\u002Fframeworks\u002Fnightwatch\u002Fapi-testing-with-nightwatch-supertest","API Testing in Nightwatch.js",[2952,13088],{":items":13089},"[{\"label\":\"What is the best website to practice Playwright?\",\"content\":\"It depends on what you want to practice. The Internet covers hard-to-automate edge cases like frames, shadow DOM, new windows, and file upload. UI Testing Playground is good for learning how Playwright auto-waits on slow content. For realistic end-to-end flows, OrangeHRM is a complete business application, and demo.playwright.dev\u002Ftodomvc is the app used throughout the Playwright documentation.\"},{\"label\":\"What are good practice websites for beginners?\",\"content\":\"Automation Exercise is a good start because it includes written test cases you can automate, and LetCode isolates individual web elements like inputs, tables, alerts, and calendars so you can practice one at a time.\"},{\"label\":\"Which practice sites have APIs for testing?\",\"content\":\"Restful Booker is the most useful because it supports full CRUD operations and auth token creation. Swagger Petstore, FakeRestAPI, and the Star Wars API are also good REST targets, and Automation Exercise and QA-Practice include APIs alongside their web front ends.\"},{\"label\":\"Can I use these sites with Selenium or Cypress?\",\"content\":\"Yes. None of these sites are tied to a specific framework. They work for Playwright, Selenium, Cypress, WebDriverIO, and Nightwatch, which also makes them useful for comparing frameworks against the same scenarios before committing to one.\"}]",[41,13091,13093],{"id":13092},"know-of-a-good-one","Know of a good one?",[11,13095,13096],{},"If you know of any other good sites I should include please reach out through my social links below 👇",[2956,13098],{":items":13099},"[\"\u002Fsoftware-testing\u002Fframeworks\u002Fplaywright\u002Frun-parallel-and-serial-tests-in-playwright\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fplaywright-accessibility-testing-axe-lighthouse-limitations\",\"\u002Fsoftware-testing\u002Ftest-automation\u002Fautomated-api-testing-with-schemathesis\",\"\u002Fsoftware-testing\u002Fis-istqb-worth-it\"]",{"title":92,"searchDepth":171,"depth":171,"links":13101},[13102,13103,13104,13105,13118,13124,13125],{"id":12496,"depth":171,"text":12497},{"id":12715,"depth":171,"text":12716},{"id":12748,"depth":171,"text":12749},{"id":12855,"depth":171,"text":12856,"children":13106},[13107,13108,13109,13110,13111,13112,13113,13114,13115,13116,13117],{"id":12862,"depth":190,"text":12522},{"id":12878,"depth":190,"text":12536},{"id":12888,"depth":190,"text":12550},{"id":12898,"depth":190,"text":12563},{"id":12908,"depth":190,"text":12576},{"id":12918,"depth":190,"text":12589},{"id":12928,"depth":190,"text":12602},{"id":12937,"depth":190,"text":12938},{"id":12948,"depth":190,"text":12949},{"id":12959,"depth":190,"text":12641},{"id":12979,"depth":190,"text":12654},{"id":13005,"depth":171,"text":13006,"children":13119},[13120,13121,13122,13123],{"id":13015,"depth":190,"text":12667},{"id":13025,"depth":190,"text":12681},{"id":13035,"depth":190,"text":12694},{"id":13045,"depth":190,"text":12707},{"id":13055,"depth":171,"text":13056},{"id":13092,"depth":171,"text":13093},"\u002Fimages\u002Fposts\u002Fbest-websites-for-practicing-test-automation\u002Fbest-sites-for-test-practice.jpg","2026-03-12","The best demo websites to practice Playwright test automation, plus Selenium and Cypress: sites for frames, shadow DOM, real app workflows, and API testing.",{},"\u002Fsoftware-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation",{"title":12488,"description":13128},{"loc":13130},"software-testing\u002Ftest-automation\u002Fbest-websites-for-practicing-test-automation","WyHp6gAH6ABqVG5HQwPjOnsfFCqmM8eEMg6q-aMEfes",1791065638199]