[{"data":1,"prerenderedAt":6732},["ShallowReactive",2],{"content:/software-testing/test-automation/how-to-handle-failing-tests-caused-by-known-bugs":3,"category:/software-testing/test-automation/how-to-handle-failing-tests-caused-by-known-bugs":6,"read-next:/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations,/software-testing/test-automation/best-websites-for-practicing-test-automation":1663},{"id":4,"title":5,"bmcUsername":6,"body":7,"cover":1653,"date":1654,"description":1655,"draft":1656,"extension":1657,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":1658,"navigation":183,"npmPackage":6,"order":6,"path":1659,"seo":1660,"stem":1661,"__hash__":1662},"content/software-testing/test-automation/how-to-handle-failing-tests-caused-by-known-bugs.md","How to Handle Failing Tests Caused by a Known Bug",null,{"type":8,"value":9,"toc":1626},"minimark",[10,14,20,23,26,29,34,42,45,50,55,58,71,74,77,84,88,91,102,105,109,112,120,123,126,128,132,135,153,255,260,263,271,274,278,315,319,322,333,336,338,342,345,348,352,355,358,465,476,479,490,492,496,500,653,664,677,681,883,892,898,902,985,991,994,1081,1087,1093,1097,1175,1181,1185,1220,1226,1230,1256,1262,1266,1315,1321,1325,1416,1564,1570,1572,1576,1606,1608,1612,1615,1618,1622],[11,12,13],"p",{},"A question came up on a developer forum recently for a solution to a problem that occurs in almost every engineering team eventually:",[15,16,17],"blockquote",{},[11,18,19],{},"\"If a test has already found a bug, one option is to comment the test out until the issue is fixed. However, this has to be done manually, and it becomes time-consuming and hard to manage when there are many tests. How do you handle this in your workflow?\"",[11,21,22],{},"The fact that commenting it out is the assumed default is why I wanted to write this article. Commenting out often feels like the obvious move: the test is noisy, you can't fix the bug right now, so you silence it and move on. However, those with experience know that decision has consequences that only become visible weeks or months later when you've forgotten the test ever existed.",[11,24,25],{},"There's a better pattern to temporarily skip or disable your tests, and every major test framework already supports it.",[27,28],"hr",{},[30,31,33],"h2",{"id":32},"the-three-wrong-answers","The Three Wrong Answers",[11,35,36,37,41],{},"As a test engineer, I want all my bugs fixed as soon as I find them, but in a practical sense that isn't always possible. In Kanban iterations and Scrum team sprints there may not be enough capacity in the maintenance or bug fix bucket to address bugs triaged ",[38,39,40],"em",{},"below the line",".",[11,43,44],{},"So when a test is failing due to a confirmed bug that won't be fixed this sprint, there are four options: leave it failing, comment it out, delete it, or skip (disable) it. The first three are wrong. Let's explore why.",[46,47,49],"h3",{"id":48},"why-leaving-a-failing-test-in-ci-breaks-your-build-signal","Why Leaving a Failing Test in CI Breaks Your Build Signal",[15,51,52],{},[11,53,54],{},"The test documents a failing test so leave the build failing until its resolved since it reflects reality",[11,56,57],{},"While one could argue it makes sense to keep the test failing until the bug its detecting is resolved, in practice, its a terrible idea to check in a known failing test.",[59,60,61,65,68],"ul",{},[62,63,64],"li",{},"It breaks your build pipeline",[62,66,67],{},"The defect may not be fixable for a long time due to priorities or complexity",[62,69,70],{},"An always red, broken, build gets ignored and let's more bugs sneak in",[11,72,73],{},"A red build that everyone knows is \"just that known bug\" trains the team to ignore red builds. It's like your house alarm going off because someone smashed a window. If you leave the alarm going without fixing anything, you won't notice when someone kicks in the backdoor and robs you again. A failing test everyone ignores is a disabled alarm and a low severity defect in a complex area, for example, may sit unresolved for months given real sprint priorities. The build can't stay red that entire time.",[11,75,76],{},"With the skip pattern, that we'll discuss, it silences the noise deliberately and intentionally, with a paper trail, so the alarm means something again.",[11,78,79,80,83],{},"With that said, there are exceptions. For example, if ",[38,81,82],{},"existing tests"," fail due to a code change, breaking functionality the tests are covering, the build should stay red until the change is reverted or bug that was introduced is fixed. This is different than adding a known failing test to an otherwise green build.",[46,85,87],{"id":86},"delete-the-test","Delete the Test",[11,89,90],{},"Another approach would be to delete the failing test, but I've almost never seen this done in practice.",[59,92,93,96,99],{},[62,94,95],{},"You lose coverage",[62,97,98],{},"Someone has to write or put back the test again later, wasteful and error prone",[62,100,101],{},"Easy to forget about",[11,103,104],{},"Again, the skip pattern is the better approach to disable the test.",[46,106,108],{"id":107},"why-commenting-out-a-failing-test-is-worse-than-it-seems","Why Commenting Out a Failing Test Is Worse Than It Seems",[11,110,111],{},"Commenting out the test seems like a natural way of handling this. Teams do it all the time when temporarily disabling code for debugging. It seems natural to do it for the tests as well. You can just uncomment it later, but those who've worked in legacy code bases know how they are graveyards of forgotten code comments. Tests can have the same fate.",[11,113,114,115,119],{},"Commented-out test code is invisible to your tooling, silently rots, and is almost guaranteed to be forgotten. Outside of maybe, ",[116,117,118],"code",{},"TODO:"," patterns, there is no reminder in your codebase to reenable them nor how many have accumulated.",[11,121,122],{},"I've seen this play out directly: a test was commented out when a bug was discovered, and it stayed that way until a major cleanup initiative was launched specifically to find dead code and commented-out blocks. When the team went to re-enable it, the codebase had drifted so far that the test was no longer compatible. It had to be rewritten from scratch, not simply re-enabled. The original time investment in writing it produced zero long-term value, and there was no way to know how long that coverage gap had existed or what had shipped during it.",[11,124,125],{},"Now, let's discuss the correct way of handling failing tests for bugs that can't be fixed quickly.",[27,127],{},[30,129,131],{"id":130},"how-to-skip-a-failing-test-the-right-way","How to Skip a Failing Test the Right Way",[11,133,134],{},"Every major test framework has a built-in skip mechanism for this very scenario. Use it.",[136,137,138,141,144,147,150],"ol",{},[62,139,140],{},"Create a bug ticket for the issue in your team's bug tracking system.",[62,142,143],{},"Note the defect number.",[62,145,146],{},"Use the test.skip syntax for your test framework to disable/skip the test programmatically",[62,148,149],{},"Include a TODO comment to unskip or reenable the test once the bug is resolved.",[62,151,152],{},"Note the location of the test in the bug ticket with instructions to enable and run the test to verify the defect is resolved and to check in the test update with the bug fix.",[154,155,161],"pre",{"className":156,"code":157,"filename":158,"language":159,"meta":160,"style":160},"language-typescript shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","// Don't do this: invisible, rots silently, easy to forget\n// test('user can reset password', async () => { ... })\n\n// Do this: explicit, visible in reports, linked to the bug\n\n// TODO: Test finding BUG#4521 - password reset endpoint returns 500, re-enable when fixed\ntest.skip('user can reset password', async () => { ... })\n","skipped-test-example.ts","typescript","",[116,162,163,172,178,185,191,196,202],{"__ignoreMap":160},[164,165,168],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"s_gjE","// Don't do this: invisible, rots silently, easy to forget\n",[164,173,175],{"class":166,"line":174},2,[164,176,177],{"class":170},"// test('user can reset password', async () => { ... })\n",[164,179,181],{"class":166,"line":180},3,[164,182,184],{"emptyLinePlaceholder":183},true,"\n",[164,186,188],{"class":166,"line":187},4,[164,189,190],{"class":170},"// Do this: explicit, visible in reports, linked to the bug\n",[164,192,194],{"class":166,"line":193},5,[164,195,184],{"emptyLinePlaceholder":183},[164,197,199],{"class":166,"line":198},6,[164,200,201],{"class":170},"// TODO: Test finding BUG#4521 - password reset endpoint returns 500, re-enable when fixed\n",[164,203,205,209,212,216,219,223,227,229,232,236,239,242,245,249,252],{"class":166,"line":204},7,[164,206,208],{"class":207},"sZ-rw","test",[164,210,41],{"class":211},"sPJuK",[164,213,215],{"class":214},"sb1SK","skip",[164,217,218],{"class":207},"(",[164,220,222],{"class":221},"sZi47","'",[164,224,226],{"class":225},"srGNg","user can reset password",[164,228,222],{"class":221},[164,230,231],{"class":211},",",[164,233,235],{"class":234},"stWsX"," async",[164,237,238],{"class":211}," ()",[164,240,241],{"class":234}," =>",[164,243,244],{"class":211}," {",[164,246,248],{"class":247},"sE6rD"," ...",[164,250,251],{"class":211}," }",[164,253,254],{"class":207},")\n",[11,256,257],{},[38,258,259],{},"Some test frameworks also allow inline comments as a test.skip or test disable parameter alleviating the need for a seperate TODO comment line",[11,261,262],{},"Unlike commented-out code, a skipped test still surfaces in your run reports:",[154,264,269],{"className":265,"code":267,"language":268},[266],"language-text","12 passed, 0 failed, 1 skipped\n","text",[116,270,267],{"__ignoreMap":160},[11,272,273],{},"That count is a standing reminder that something needs to come back. It shows up on every run, in every CI report, without anyone having to go looking for it.",[46,275,277],{"id":276},"why-skip-beats-commenting-out","Why Skip Beats Commenting Out",[59,279,280,287,297,303,309],{},[62,281,282,286],{},[283,284,285],"strong",{},"Commented-out tests are completely invisible."," No skipped count, no reason string, no indication in test output that anything is missing. The gap is hidden from anyone reviewing CI results.",[62,288,289,292,293,296],{},[283,290,291],{},"Comments don't surface in TODO tracking."," IDEs and code review tools can surface ",[116,294,295],{},"// TODO"," comments as actionable items. A commented-out test block is dead code. It won't appear in any report or task list prompting someone to revisit it.",[62,298,299,302],{},[283,300,301],{},"Commented-out code goes stale silently."," As the codebase evolves, commented-out tests develop broken syntax, outdated method calls, and references to renamed or removed APIs. Nobody notices because the code never has to compile. When someone eventually tries to re-enable it, they're restoring broken code.",[62,304,305,308],{},[283,306,307],{},"Skipped tests still compile."," A skipped test is live code. In typed languages, if a method is renamed or a parameter type changes, the skipped test will surface a compile error immediately. The breakage is caught, not hidden.",[62,310,311,314],{},[283,312,313],{},"Skip reasons are searchable."," Searching the codebase for a ticket number instantly finds every test gated on that bug.",[46,316,318],{"id":317},"linking-skipped-tests-to-bug-tickets","Linking Skipped Tests to Bug Tickets",[11,320,321],{},"The skip pattern only closes the loop if both sides reference each other:",[136,323,324,327,330],{},[62,325,326],{},"The skip reason includes the bug ticket number or URL",[62,328,329],{},"The bug ticket description references the test file and test name",[62,331,332],{},"Re-enabling the test is an explicit step in the bug fix, not an afterthought",[11,334,335],{},"When the bug is fixed, the developer checks the ticket, finds the test reference, re-enables it, and verifies it passes before closing. This makes test restoration a first-class step in the fix workflow rather than something that gets remembered, or more often, forgotten.",[27,337],{},[30,339,341],{"id":340},"a-fair-counterpoint","A Fair Counterpoint",[11,343,344],{},"A commenter responding to the forum thread made a fair point: the skip pattern is technically the right answer, but it still requires discipline. Skipped tests are easy to ignore. It takes active effort to monitor the skipped count, prioritize the underlying bugs, and actually re-enable tests when fixes land. Otherwise, skipped tests accumulate and become their own form of technical debt.",[11,346,347],{},"That's true. But the same discipline argument applies even more strongly to commented-out tests. A skipped count is visible in every CI run: it's a number that can be tracked, trended, and reviewed in sprint planning. A commented-out test shows up nowhere. If discipline is the concern, the approach that provides the most visibility is the better starting point.",[46,349,351],{"id":350},"using-a-ci-gate-to-enforce-a-skipped-test-threshold","Using a CI Gate to Enforce a Skipped Test Threshold",[11,353,354],{},"If skipped count drift is a real concern for your team, you can turn discipline into policy with a CI gate that fails the build if the skipped count exceeds a defined threshold.",[11,356,357],{},"To my knowledge neither Jest nor JUnit have a built-in threshold option for this, but there is a practical, framework-agnostic, approach using a two-step GitHub Actions pattern: parse your JUnit XML test output to extract the skipped count, then fail the step if it exceeds your threshold.",[154,359,364],{"className":360,"code":361,"filename":362,"language":363,"meta":160,"style":160},"language-yaml shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","- uses: mikepenz/action-junit-report@v4\n  id: junit\n  with:\n    report_paths: '**/test-results/*.xml'\n\n- name: Fail if skipped tests exceed threshold\n  if: fromJson(steps.junit.outputs.skipped) > 5\n  run: |\n    echo \"Skipped test count (${{ steps.junit.outputs.skipped }}) exceeds threshold of 5\"\n    exit 1\n",".github/workflows/test.yml","yaml",[116,365,366,381,391,399,415,419,431,441,453,459],{"__ignoreMap":160},[164,367,368,371,375,378],{"class":166,"line":167},[164,369,370],{"class":211},"-",[164,372,374],{"class":373},"saWzx"," uses",[164,376,377],{"class":211},":",[164,379,380],{"class":225}," mikepenz/action-junit-report@v4\n",[164,382,383,386,388],{"class":166,"line":174},[164,384,385],{"class":373},"  id",[164,387,377],{"class":211},[164,389,390],{"class":225}," junit\n",[164,392,393,396],{"class":166,"line":180},[164,394,395],{"class":373},"  with",[164,397,398],{"class":211},":\n",[164,400,401,404,406,409,412],{"class":166,"line":187},[164,402,403],{"class":373},"    report_paths",[164,405,377],{"class":211},[164,407,408],{"class":221}," '",[164,410,411],{"class":225},"**/test-results/*.xml",[164,413,414],{"class":221},"'\n",[164,416,417],{"class":166,"line":193},[164,418,184],{"emptyLinePlaceholder":183},[164,420,421,423,426,428],{"class":166,"line":198},[164,422,370],{"class":211},[164,424,425],{"class":373}," name",[164,427,377],{"class":211},[164,429,430],{"class":225}," Fail if skipped tests exceed threshold\n",[164,432,433,436,438],{"class":166,"line":204},[164,434,435],{"class":373},"  if",[164,437,377],{"class":211},[164,439,440],{"class":225}," fromJson(steps.junit.outputs.skipped) > 5\n",[164,442,444,447,449],{"class":166,"line":443},8,[164,445,446],{"class":373},"  run",[164,448,377],{"class":211},[164,450,452],{"class":451},"sZTni"," |\n",[164,454,456],{"class":166,"line":455},9,[164,457,458],{"class":225},"    echo \"Skipped test count (${{ steps.junit.outputs.skipped }}) exceeds threshold of 5\"\n",[164,460,462],{"class":166,"line":461},10,[164,463,464],{"class":225},"    exit 1\n",[11,466,467,468,471,472,475],{},"This works for any framework that outputs JUnit XML: Jest via ",[116,469,470],{},"jest-junit",", Playwright via its built-in JUnit reporter, pytest via ",[116,473,474],{},"pytest-junit",", and JUnit 5 natively. The threshold should reflect what's acceptable for your team. Even setting it generously and trending the number over sprints is more actionable than having no visibility at all.",[11,477,478],{},"Critically, this kind of gate is only possible with skips. You cannot gate on commented-out tests because your tooling has no visibility into them.",[11,480,481,482,485,486,489],{},"For teams using Jest, the ",[116,483,484],{},"eslint-plugin-jest/no-disabled-tests"," linting rule is a useful complement. It catches ",[116,487,488],{},"test.skip()"," at code review time, before it reaches CI.",[27,491],{},[30,493,495],{"id":494},"test-skip-syntax-by-framework","Test Skip Syntax by Framework",[46,497,499],{"id":498},"jest-and-vitest","Jest and Vitest",[154,501,504],{"className":156,"code":502,"filename":503,"language":159,"meta":160,"style":160},"// Skip a single test\ntest.skip('user can reset password', () => {\n  // Bug #4521: password reset endpoint returns 500\n})\n\n// Skip a suite\ndescribe.skip('Password Reset', () => { ... })\n\n// Older alias syntax, both work\nxit('user can reset password', () => { ... })\nxdescribe('Password Reset', () => { ... })\n","jest-vitest-test-skip-example.ts",[116,505,506,511,536,541,548,552,557,589,593,598,625],{"__ignoreMap":160},[164,507,508],{"class":166,"line":167},[164,509,510],{"class":170},"// Skip a single test\n",[164,512,513,515,517,519,521,523,525,527,529,531,533],{"class":166,"line":174},[164,514,208],{"class":207},[164,516,41],{"class":211},[164,518,215],{"class":214},[164,520,218],{"class":207},[164,522,222],{"class":221},[164,524,226],{"class":225},[164,526,222],{"class":221},[164,528,231],{"class":211},[164,530,238],{"class":211},[164,532,241],{"class":234},[164,534,535],{"class":211}," {\n",[164,537,538],{"class":166,"line":180},[164,539,540],{"class":170},"  // Bug #4521: password reset endpoint returns 500\n",[164,542,543,546],{"class":166,"line":187},[164,544,545],{"class":211},"}",[164,547,254],{"class":207},[164,549,550],{"class":166,"line":193},[164,551,184],{"emptyLinePlaceholder":183},[164,553,554],{"class":166,"line":198},[164,555,556],{"class":170},"// Skip a suite\n",[164,558,559,562,564,566,568,570,573,575,577,579,581,583,585,587],{"class":166,"line":204},[164,560,561],{"class":207},"describe",[164,563,41],{"class":211},[164,565,215],{"class":214},[164,567,218],{"class":207},[164,569,222],{"class":221},[164,571,572],{"class":225},"Password Reset",[164,574,222],{"class":221},[164,576,231],{"class":211},[164,578,238],{"class":211},[164,580,241],{"class":234},[164,582,244],{"class":211},[164,584,248],{"class":247},[164,586,251],{"class":211},[164,588,254],{"class":207},[164,590,591],{"class":166,"line":443},[164,592,184],{"emptyLinePlaceholder":183},[164,594,595],{"class":166,"line":455},[164,596,597],{"class":170},"// Older alias syntax, both work\n",[164,599,600,603,605,607,609,611,613,615,617,619,621,623],{"class":166,"line":461},[164,601,602],{"class":214},"xit",[164,604,218],{"class":207},[164,606,222],{"class":221},[164,608,226],{"class":225},[164,610,222],{"class":221},[164,612,231],{"class":211},[164,614,238],{"class":211},[164,616,241],{"class":234},[164,618,244],{"class":211},[164,620,248],{"class":247},[164,622,251],{"class":211},[164,624,254],{"class":207},[164,626,628,631,633,635,637,639,641,643,645,647,649,651],{"class":166,"line":627},11,[164,629,630],{"class":214},"xdescribe",[164,632,218],{"class":207},[164,634,222],{"class":221},[164,636,572],{"class":225},[164,638,222],{"class":221},[164,640,231],{"class":211},[164,642,238],{"class":211},[164,644,241],{"class":234},[164,646,244],{"class":211},[164,648,248],{"class":247},[164,650,251],{"class":211},[164,652,254],{"class":207},[11,654,655,656,659,660,663],{},"Vitest uses identical syntax to Jest. ",[116,657,658],{},"test.skip"," and ",[116,661,662],{},"describe.skip"," work the same way.",[11,665,666,667,672,673],{},"Docs: ",[668,669],"external-link",{"href":670,"text":671},"https://jestjs.io/docs/api#describeskipname-fn","Jest skip"," · ",[668,674],{"href":675,"text":676},"https://vitest.dev/api/test.html#test-skip","Vitest skip",[46,678,680],{"id":679},"playwright","Playwright",[154,682,685],{"className":156,"code":683,"filename":684,"language":159,"meta":160,"style":160},"// Skip unconditionally\ntest.skip('user can reset password', async ({ page }) => {\n  // Bug #4521: password reset endpoint returns 500\n})\n\n// Skip conditionally, useful for browser-specific bugs\ntest('user can reset password', async ({ page, browserName }) => {\n  test.skip(browserName === 'webkit', 'Bug #4521: fails on Safari only')\n  // ...\n})\n\n// test.fixme: skips the test but signals it urgently needs attention\n// Shows up differently in the Playwright HTML report\ntest.fixme('user can reset password', async ({ page }) => {\n  // Bug #4521: password reset endpoint returns 500\n})\n","playwright-test-skip-disable-example.ts",[116,686,687,692,726,730,736,740,745,776,812,817,823,827,833,839,871,876],{"__ignoreMap":160},[164,688,689],{"class":166,"line":167},[164,690,691],{"class":170},"// Skip unconditionally\n",[164,693,694,696,698,700,702,704,706,708,710,712,715,719,722,724],{"class":166,"line":174},[164,695,208],{"class":207},[164,697,41],{"class":211},[164,699,215],{"class":214},[164,701,218],{"class":207},[164,703,222],{"class":221},[164,705,226],{"class":225},[164,707,222],{"class":221},[164,709,231],{"class":211},[164,711,235],{"class":234},[164,713,714],{"class":211}," ({",[164,716,718],{"class":717},"s2xgV"," page",[164,720,721],{"class":211}," })",[164,723,241],{"class":234},[164,725,535],{"class":211},[164,727,728],{"class":166,"line":180},[164,729,540],{"class":170},[164,731,732,734],{"class":166,"line":187},[164,733,545],{"class":211},[164,735,254],{"class":207},[164,737,738],{"class":166,"line":193},[164,739,184],{"emptyLinePlaceholder":183},[164,741,742],{"class":166,"line":198},[164,743,744],{"class":170},"// Skip conditionally, useful for browser-specific bugs\n",[164,746,747,749,751,753,755,757,759,761,763,765,767,770,772,774],{"class":166,"line":204},[164,748,208],{"class":214},[164,750,218],{"class":207},[164,752,222],{"class":221},[164,754,226],{"class":225},[164,756,222],{"class":221},[164,758,231],{"class":211},[164,760,235],{"class":234},[164,762,714],{"class":211},[164,764,718],{"class":717},[164,766,231],{"class":211},[164,768,769],{"class":717}," browserName",[164,771,721],{"class":211},[164,773,241],{"class":234},[164,775,535],{"class":211},[164,777,778,781,783,785,788,791,794,796,799,801,803,805,808,810],{"class":166,"line":443},[164,779,780],{"class":207},"  test",[164,782,41],{"class":211},[164,784,215],{"class":214},[164,786,218],{"class":787},"sq0XF",[164,789,790],{"class":207},"browserName",[164,792,793],{"class":247}," ===",[164,795,408],{"class":221},[164,797,798],{"class":225},"webkit",[164,800,222],{"class":221},[164,802,231],{"class":211},[164,804,408],{"class":221},[164,806,807],{"class":225},"Bug #4521: fails on Safari only",[164,809,222],{"class":221},[164,811,254],{"class":787},[164,813,814],{"class":166,"line":455},[164,815,816],{"class":170},"  // ...\n",[164,818,819,821],{"class":166,"line":461},[164,820,545],{"class":211},[164,822,254],{"class":207},[164,824,825],{"class":166,"line":627},[164,826,184],{"emptyLinePlaceholder":183},[164,828,830],{"class":166,"line":829},12,[164,831,832],{"class":170},"// test.fixme: skips the test but signals it urgently needs attention\n",[164,834,836],{"class":166,"line":835},13,[164,837,838],{"class":170},"// Shows up differently in the Playwright HTML report\n",[164,840,842,844,846,849,851,853,855,857,859,861,863,865,867,869],{"class":166,"line":841},14,[164,843,208],{"class":207},[164,845,41],{"class":211},[164,847,848],{"class":214},"fixme",[164,850,218],{"class":207},[164,852,222],{"class":221},[164,854,226],{"class":225},[164,856,222],{"class":221},[164,858,231],{"class":211},[164,860,235],{"class":234},[164,862,714],{"class":211},[164,864,718],{"class":717},[164,866,721],{"class":211},[164,868,241],{"class":234},[164,870,535],{"class":211},[164,872,874],{"class":166,"line":873},15,[164,875,540],{"class":170},[164,877,879,881],{"class":166,"line":878},16,[164,880,545],{"class":211},[164,882,254],{"class":207},[11,884,885,888,889,891],{},[116,886,887],{},"test.fixme"," behaves like ",[116,890,658],{}," but communicates more urgency. Use it when the test needs to come back soon rather than being parked indefinitely.",[11,893,666,894],{},[668,895],{"href":896,"text":897},"https://playwright.dev/docs/test-annotations#skip-a-test","Playwright test annotations",[46,899,901],{"id":900},"cypress","Cypress",[154,903,906],{"className":156,"code":904,"filename":905,"language":159,"meta":160,"style":160},"// Skip a single test\nit.skip('user can reset password', () => {\n  // Bug #4521: password reset endpoint returns 500\n})\n\n// Skip a suite\ndescribe.skip('Password Reset', () => { ... })\n","cypress-test-skip-example.ts",[116,907,908,912,937,941,947,951,955],{"__ignoreMap":160},[164,909,910],{"class":166,"line":167},[164,911,510],{"class":170},[164,913,914,917,919,921,923,925,927,929,931,933,935],{"class":166,"line":174},[164,915,916],{"class":207},"it",[164,918,41],{"class":211},[164,920,215],{"class":214},[164,922,218],{"class":207},[164,924,222],{"class":221},[164,926,226],{"class":225},[164,928,222],{"class":221},[164,930,231],{"class":211},[164,932,238],{"class":211},[164,934,241],{"class":234},[164,936,535],{"class":211},[164,938,939],{"class":166,"line":180},[164,940,540],{"class":170},[164,942,943,945],{"class":166,"line":187},[164,944,545],{"class":211},[164,946,254],{"class":207},[164,948,949],{"class":166,"line":193},[164,950,184],{"emptyLinePlaceholder":183},[164,952,953],{"class":166,"line":198},[164,954,556],{"class":170},[164,956,957,959,961,963,965,967,969,971,973,975,977,979,981,983],{"class":166,"line":204},[164,958,561],{"class":207},[164,960,41],{"class":211},[164,962,215],{"class":214},[164,964,218],{"class":207},[164,966,222],{"class":221},[164,968,572],{"class":225},[164,970,222],{"class":221},[164,972,231],{"class":211},[164,974,238],{"class":211},[164,976,241],{"class":234},[164,978,244],{"class":211},[164,980,248],{"class":247},[164,982,251],{"class":211},[164,984,254],{"class":207},[11,986,666,987],{},[668,988],{"href":989,"text":990},"https://docs.cypress.io/app/guides/migration/playwright-to-cypress#Test-structure-and-syntax-migration","Cypress test structure",[46,992,993],{"id":993},"pytest",[154,995,1000],{"className":996,"code":997,"filename":998,"language":999,"meta":160,"style":160},"language-python shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","import pytest\n\n# Skip unconditionally with reason\n@pytest.mark.skip(reason=\"Bug #4521: password reset endpoint returns 500\")\ndef test_user_can_reset_password():\n    ...\n\n# Skip conditionally, useful for environment-specific bugs\n@pytest.mark.skipif(os.getenv(\"ENV\") == \"staging\", reason=\"Bug #4521: only affects staging\")\ndef test_user_can_reset_password():\n    ...\n\n# xfail: marks as expected failure, test still runs\n# Use when you want the test to run but not break the build\n@pytest.mark.xfail(reason=\"Bug #4521: known failure, fix in progress\")\ndef test_user_can_reset_password():\n    ...\n","pytest-skip-test-example.py","python",[116,1001,1002,1007,1011,1016,1021,1026,1031,1035,1040,1045,1049,1053,1057,1062,1067,1072,1076],{"__ignoreMap":160},[164,1003,1004],{"class":166,"line":167},[164,1005,1006],{},"import pytest\n",[164,1008,1009],{"class":166,"line":174},[164,1010,184],{"emptyLinePlaceholder":183},[164,1012,1013],{"class":166,"line":180},[164,1014,1015],{},"# Skip unconditionally with reason\n",[164,1017,1018],{"class":166,"line":187},[164,1019,1020],{},"@pytest.mark.skip(reason=\"Bug #4521: password reset endpoint returns 500\")\n",[164,1022,1023],{"class":166,"line":193},[164,1024,1025],{},"def test_user_can_reset_password():\n",[164,1027,1028],{"class":166,"line":198},[164,1029,1030],{},"    ...\n",[164,1032,1033],{"class":166,"line":204},[164,1034,184],{"emptyLinePlaceholder":183},[164,1036,1037],{"class":166,"line":443},[164,1038,1039],{},"# Skip conditionally, useful for environment-specific bugs\n",[164,1041,1042],{"class":166,"line":455},[164,1043,1044],{},"@pytest.mark.skipif(os.getenv(\"ENV\") == \"staging\", reason=\"Bug #4521: only affects staging\")\n",[164,1046,1047],{"class":166,"line":461},[164,1048,1025],{},[164,1050,1051],{"class":166,"line":627},[164,1052,1030],{},[164,1054,1055],{"class":166,"line":829},[164,1056,184],{"emptyLinePlaceholder":183},[164,1058,1059],{"class":166,"line":835},[164,1060,1061],{},"# xfail: marks as expected failure, test still runs\n",[164,1063,1064],{"class":166,"line":841},[164,1065,1066],{},"# Use when you want the test to run but not break the build\n",[164,1068,1069],{"class":166,"line":873},[164,1070,1071],{},"@pytest.mark.xfail(reason=\"Bug #4521: known failure, fix in progress\")\n",[164,1073,1074],{"class":166,"line":878},[164,1075,1025],{},[164,1077,1079],{"class":166,"line":1078},17,[164,1080,1030],{},[11,1082,1083,1086],{},[116,1084,1085],{},"pytest.mark.xfail"," is a useful middle ground. The test still runs, but a failure is expected and won't break the build. Use it when you want visibility that the test is currently broken without silencing it entirely.",[11,1088,666,1089],{},[668,1090],{"href":1091,"text":1092},"https://docs.pytest.org/en/stable/reference/reference.html#pytest.skip","pytest skip reference",[46,1094,1096],{"id":1095},"junit-5","JUnit 5",[154,1098,1103],{"className":1099,"code":1100,"filename":1101,"language":1102,"meta":160,"style":160},"language-java shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","import org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\n// Skip a single test\n@Disabled(\"Bug #4521: password reset endpoint returns 500, fix pending\")\n@Test\nvoid userCanResetPassword() {\n    // ...\n}\n\n// Skip an entire test class\n@Disabled(\"Bug #4521: all password reset tests affected\")\nclass PasswordResetTests {\n    // ...\n}\n","junit-test-disable.spec.java","java",[116,1104,1105,1110,1115,1119,1123,1128,1133,1138,1143,1148,1152,1157,1162,1167,1171],{"__ignoreMap":160},[164,1106,1107],{"class":166,"line":167},[164,1108,1109],{},"import org.junit.jupiter.api.Disabled;\n",[164,1111,1112],{"class":166,"line":174},[164,1113,1114],{},"import org.junit.jupiter.api.Test;\n",[164,1116,1117],{"class":166,"line":180},[164,1118,184],{"emptyLinePlaceholder":183},[164,1120,1121],{"class":166,"line":187},[164,1122,510],{},[164,1124,1125],{"class":166,"line":193},[164,1126,1127],{},"@Disabled(\"Bug #4521: password reset endpoint returns 500, fix pending\")\n",[164,1129,1130],{"class":166,"line":198},[164,1131,1132],{},"@Test\n",[164,1134,1135],{"class":166,"line":204},[164,1136,1137],{},"void userCanResetPassword() {\n",[164,1139,1140],{"class":166,"line":443},[164,1141,1142],{},"    // ...\n",[164,1144,1145],{"class":166,"line":455},[164,1146,1147],{},"}\n",[164,1149,1150],{"class":166,"line":461},[164,1151,184],{"emptyLinePlaceholder":183},[164,1153,1154],{"class":166,"line":627},[164,1155,1156],{},"// Skip an entire test class\n",[164,1158,1159],{"class":166,"line":829},[164,1160,1161],{},"@Disabled(\"Bug #4521: all password reset tests affected\")\n",[164,1163,1164],{"class":166,"line":835},[164,1165,1166],{},"class PasswordResetTests {\n",[164,1168,1169],{"class":166,"line":841},[164,1170,1142],{},[164,1172,1173],{"class":166,"line":873},[164,1174,1147],{},[11,1176,666,1177],{},[668,1178],{"href":1179,"text":1180},"https://docs.junit.org/6.0.3/writing-tests/disabling-tests.html","JUnit disabling tests",[46,1182,1184],{"id":1183},"nunit-net","NUnit (.NET)",[154,1186,1190],{"className":1187,"code":1188,"language":1189,"meta":160,"style":160},"language-csharp shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","[Test]\n[Ignore(\"Bug #4521: password reset endpoint returns 500, fix pending\")]\npublic void UserCanResetPassword()\n{\n    // ...\n}\n","csharp",[116,1191,1192,1197,1202,1207,1212,1216],{"__ignoreMap":160},[164,1193,1194],{"class":166,"line":167},[164,1195,1196],{},"[Test]\n",[164,1198,1199],{"class":166,"line":174},[164,1200,1201],{},"[Ignore(\"Bug #4521: password reset endpoint returns 500, fix pending\")]\n",[164,1203,1204],{"class":166,"line":180},[164,1205,1206],{},"public void UserCanResetPassword()\n",[164,1208,1209],{"class":166,"line":187},[164,1210,1211],{},"{\n",[164,1213,1214],{"class":166,"line":193},[164,1215,1142],{},[164,1217,1218],{"class":166,"line":198},[164,1219,1147],{},[11,1221,666,1222],{},[668,1223],{"href":1224,"text":1225},"https://docs.nunit.org/articles/nunit/writing-tests/attributes/ignore.html","NUnit Ignore attribute",[46,1227,1229],{"id":1228},"xunit-net","xUnit (.NET)",[154,1231,1233],{"className":1187,"code":1232,"language":1189,"meta":160,"style":160},"[Fact(Skip = \"Bug #4521: password reset endpoint returns 500, fix pending\")]\npublic void UserCanResetPassword()\n{\n    // ...\n}\n",[116,1234,1235,1240,1244,1248,1252],{"__ignoreMap":160},[164,1236,1237],{"class":166,"line":167},[164,1238,1239],{},"[Fact(Skip = \"Bug #4521: password reset endpoint returns 500, fix pending\")]\n",[164,1241,1242],{"class":166,"line":174},[164,1243,1206],{},[164,1245,1246],{"class":166,"line":180},[164,1247,1211],{},[164,1249,1250],{"class":166,"line":187},[164,1251,1142],{},[164,1253,1254],{"class":166,"line":193},[164,1255,1147],{},[11,1257,666,1258],{},[668,1259],{"href":1260,"text":1261},"https://api.xunit.net/v3/3.2.2/v3.3.2.2-Xunit.Assert.Skip.html","xUnit Skip",[46,1263,1265],{"id":1264},"rspec-ruby","RSpec (Ruby)",[154,1267,1271],{"className":1268,"code":1269,"language":1270,"meta":160,"style":160},"language-ruby shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","# Skip with reason\nit 'allows user to reset password', :skip => 'Bug #4521: password reset returns 500' do\n  # ...\nend\n\n# pending: similar to xfail, marks as pending, body is not executed\npending 'Bug #4521: password reset returns 500' do\n  # ...\nend\n","ruby",[116,1272,1273,1278,1283,1288,1293,1297,1302,1307,1311],{"__ignoreMap":160},[164,1274,1275],{"class":166,"line":167},[164,1276,1277],{},"# Skip with reason\n",[164,1279,1280],{"class":166,"line":174},[164,1281,1282],{},"it 'allows user to reset password', :skip => 'Bug #4521: password reset returns 500' do\n",[164,1284,1285],{"class":166,"line":180},[164,1286,1287],{},"  # ...\n",[164,1289,1290],{"class":166,"line":187},[164,1291,1292],{},"end\n",[164,1294,1295],{"class":166,"line":193},[164,1296,184],{"emptyLinePlaceholder":183},[164,1298,1299],{"class":166,"line":198},[164,1300,1301],{},"# pending: similar to xfail, marks as pending, body is not executed\n",[164,1303,1304],{"class":166,"line":204},[164,1305,1306],{},"pending 'Bug #4521: password reset returns 500' do\n",[164,1308,1309],{"class":166,"line":443},[164,1310,1287],{},[164,1312,1313],{"class":166,"line":455},[164,1314,1292],{},[11,1316,666,1317],{},[668,1318],{"href":1319,"text":1320},"https://rspec.info/features/3-12/rspec-core/pending-and-skipped-examples/","RSpec pending and skipped examples",[46,1322,1324],{"id":1323},"nightwatch","Nightwatch",[154,1326,1331],{"className":1327,"code":1328,"filename":1329,"language":1330,"meta":160,"style":160},"language-javascript shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","module.exports = {\n  '@disabled': true, // This will prevent the test module from running.\n  \n  'sample test': function (browser) {\n    // test code\n  }\n};\n","nightwatch-skip-pattern.js","javascript",[116,1332,1333,1349,1371,1376,1401,1406,1411],{"__ignoreMap":160},[164,1334,1335,1339,1341,1344,1347],{"class":166,"line":167},[164,1336,1338],{"class":1337},"sPxkN","module",[164,1340,41],{"class":211},[164,1342,1343],{"class":1337},"exports",[164,1345,1346],{"class":247}," =",[164,1348,535],{"class":211},[164,1350,1351,1354,1358,1360,1362,1366,1368],{"class":166,"line":174},[164,1352,1353],{"class":221},"  '",[164,1355,1357],{"class":1356},"sqmHM","@disabled",[164,1359,222],{"class":221},[164,1361,377],{"class":211},[164,1363,1365],{"class":1364},"sTqCK"," true",[164,1367,231],{"class":211},[164,1369,1370],{"class":170}," // This will prevent the test module from running.\n",[164,1372,1373],{"class":166,"line":180},[164,1374,1375],{"class":207},"  \n",[164,1377,1378,1380,1383,1385,1387,1390,1393,1396,1399],{"class":166,"line":187},[164,1379,1353],{"class":221},[164,1381,1382],{"class":1356},"sample test",[164,1384,222],{"class":221},[164,1386,377],{"class":211},[164,1388,1389],{"class":234}," function",[164,1391,1392],{"class":211}," (",[164,1394,1395],{"class":717},"browser",[164,1397,1398],{"class":211},")",[164,1400,535],{"class":211},[164,1402,1403],{"class":166,"line":193},[164,1404,1405],{"class":170},"    // test code\n",[164,1407,1408],{"class":166,"line":198},[164,1409,1410],{"class":211},"  }\n",[164,1412,1413],{"class":166,"line":204},[164,1414,1415],{"class":211},"};\n",[154,1417,1420],{"className":1327,"code":1418,"filename":1419,"language":1330,"meta":160,"style":160},"describe('homepage test with describe', function() {\n  \n  // skipped testcase: equivalent to: test.skip(), it.skip(), and xit()\n  it.skip('async testcase', async browser => {\n    const result = await browser.getText('#navigation');\n    console.log('result', result.value)\n  });\n});\n","nightwatch-skip-describe-style.js",[116,1421,1422,1444,1448,1453,1482,1517,1547,1556],{"__ignoreMap":160},[164,1423,1424,1426,1428,1430,1433,1435,1437,1439,1442],{"class":166,"line":167},[164,1425,561],{"class":214},[164,1427,218],{"class":207},[164,1429,222],{"class":221},[164,1431,1432],{"class":225},"homepage test with describe",[164,1434,222],{"class":221},[164,1436,231],{"class":211},[164,1438,1389],{"class":234},[164,1440,1441],{"class":211},"()",[164,1443,535],{"class":211},[164,1445,1446],{"class":166,"line":174},[164,1447,1375],{"class":787},[164,1449,1450],{"class":166,"line":180},[164,1451,1452],{"class":170},"  // skipped testcase: equivalent to: test.skip(), it.skip(), and xit()\n",[164,1454,1455,1458,1460,1462,1464,1466,1469,1471,1473,1475,1478,1480],{"class":166,"line":187},[164,1456,1457],{"class":207},"  it",[164,1459,41],{"class":211},[164,1461,215],{"class":214},[164,1463,218],{"class":787},[164,1465,222],{"class":221},[164,1467,1468],{"class":225},"async testcase",[164,1470,222],{"class":221},[164,1472,231],{"class":211},[164,1474,235],{"class":234},[164,1476,1477],{"class":717}," browser",[164,1479,241],{"class":234},[164,1481,535],{"class":211},[164,1483,1484,1487,1491,1493,1496,1498,1500,1503,1505,1507,1510,1512,1514],{"class":166,"line":193},[164,1485,1486],{"class":234},"    const",[164,1488,1490],{"class":1489},"sQ79N"," result",[164,1492,1346],{"class":247},[164,1494,1495],{"class":451}," await",[164,1497,1477],{"class":207},[164,1499,41],{"class":211},[164,1501,1502],{"class":214},"getText",[164,1504,218],{"class":787},[164,1506,222],{"class":221},[164,1508,1509],{"class":225},"#navigation",[164,1511,222],{"class":221},[164,1513,1398],{"class":787},[164,1515,1516],{"class":211},";\n",[164,1518,1519,1522,1524,1527,1529,1531,1534,1536,1538,1540,1542,1545],{"class":166,"line":198},[164,1520,1521],{"class":207},"    console",[164,1523,41],{"class":211},[164,1525,1526],{"class":214},"log",[164,1528,218],{"class":787},[164,1530,222],{"class":221},[164,1532,1533],{"class":225},"result",[164,1535,222],{"class":221},[164,1537,231],{"class":211},[164,1539,1490],{"class":207},[164,1541,41],{"class":211},[164,1543,1544],{"class":207},"value",[164,1546,254],{"class":787},[164,1548,1549,1552,1554],{"class":166,"line":204},[164,1550,1551],{"class":211},"  }",[164,1553,1398],{"class":787},[164,1555,1516],{"class":211},[164,1557,1558,1560,1562],{"class":166,"line":443},[164,1559,545],{"class":211},[164,1561,1398],{"class":207},[164,1563,1516],{"class":211},[11,1565,666,1566],{},[668,1567],{"href":1568,"text":1569},"https://nightwatchjs.org/guide/running-tests/skipping-disabling-tests.html","Nightwatch skipping and disabling tests",[27,1571],{},[30,1573,1575],{"id":1574},"common-mistakes-when-disabling-tests-for-known-bugs","Common Mistakes When Disabling Tests for Known Bugs",[59,1577,1578,1584,1590,1596],{},[62,1579,1580,1583],{},[283,1581,1582],{},"Don't comment out."," Invisible in reports, won't surface in any tracking system, and goes stale silently as the codebase changes around it.",[62,1585,1586,1589],{},[283,1587,1588],{},"Don't delete."," The coverage is gone permanently. Someone has to rewrite the test from scratch when the bug is fixed, assuming anyone remembers it existed.",[62,1591,1592,1595],{},[283,1593,1594],{},"Don't leave it failing."," A red build everyone ignores is a disabled alarm. When a real regression slips through, nobody notices.",[62,1597,1598,1601,1602,1605],{},[283,1599,1600],{},"Don't skip without a reason."," A bare ",[116,1603,1604],{},"test.skip('user can reset password')"," with no context is almost as bad as a comment. There's no ticket reference, no way to know why it was skipped, and no path back to re-enabling it.",[27,1607],{},[30,1609,1611],{"id":1610},"conclusion","Conclusion",[11,1613,1614],{},"The skip pattern costs almost nothing to apply. It takes maybe thirty seconds longer than commenting out. What it buys you is a test that stays in the codebase, stays visible in reports, stays linked to the bug that caused it, and is right there waiting to be re-enabled when the fix lands.",[11,1616,1617],{},"The ticket reference is what closes the loop. Without it, skipped tests are marginally better than commented-out ones, still visible but still forgotten. With it, restoring the test becomes a natural last step in fixing the bug rather than something that has to be remembered.",[1619,1620],"read-next",{":items":1621},"[\"/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations\",\"/software-testing/test-automation/best-websites-for-practicing-test-automation\"]",[1623,1624,1625],"style",{},"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 .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 .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 .stWsX, html code.shiki .stWsX{--shiki-light:#9C3EDA;--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .sE6rD, html code.shiki .sE6rD{--shiki-light:#39ADB5;--shiki-default:#A0111F;--shiki-dark:#FF9492}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 .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 .sPxkN, html code.shiki .sPxkN{--shiki-light:#39ADB5;--shiki-default:#023B95;--shiki-dark:#91CBFF}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 pre.shiki code .sQ79N, html code.shiki .sQ79N{--shiki-light:#90A4AE;--shiki-default:#023B95;--shiki-dark:#91CBFF}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 .saWzx, html code.shiki .saWzx{--shiki-light:#E53935;--shiki-default:#024C1A;--shiki-dark:#72F088}",{"title":160,"searchDepth":174,"depth":174,"links":1627},[1628,1633,1637,1640,1651,1652],{"id":32,"depth":174,"text":33,"children":1629},[1630,1631,1632],{"id":48,"depth":180,"text":49},{"id":86,"depth":180,"text":87},{"id":107,"depth":180,"text":108},{"id":130,"depth":174,"text":131,"children":1634},[1635,1636],{"id":276,"depth":180,"text":277},{"id":317,"depth":180,"text":318},{"id":340,"depth":174,"text":341,"children":1638},[1639],{"id":350,"depth":180,"text":351},{"id":494,"depth":174,"text":495,"children":1641},[1642,1643,1644,1645,1646,1647,1648,1649,1650],{"id":498,"depth":180,"text":499},{"id":679,"depth":180,"text":680},{"id":900,"depth":180,"text":901},{"id":993,"depth":180,"text":993},{"id":1095,"depth":180,"text":1096},{"id":1183,"depth":180,"text":1184},{"id":1228,"depth":180,"text":1229},{"id":1264,"depth":180,"text":1265},{"id":1323,"depth":180,"text":1324},{"id":1574,"depth":174,"text":1575},{"id":1610,"depth":174,"text":1611},"/images/posts/how-to-handle-failing-tests-caused-by-known-bugs/how-to-handle-failing-tests-caused-by-known-bugs-cover.webp","2026-04-16","When a test fails due to a known bug that can't be fixed immediately, commenting it out is the wrong move. Here's the right pattern, with skip syntax for every major test framework.",false,"md",{},"/software-testing/test-automation/how-to-handle-failing-tests-caused-by-known-bugs",{"title":5,"description":1655},"software-testing/test-automation/how-to-handle-failing-tests-caused-by-known-bugs","IJMbyLVkM-296RYnBQXsC4cH_RdGhwDsXMMNJtdQDfs",[1664,6429],{"id":1665,"title":1666,"bmcUsername":6,"body":1667,"cover":6421,"date":6422,"description":6423,"draft":1656,"extension":1657,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":6424,"navigation":183,"npmPackage":6,"order":6,"path":6425,"seo":6426,"stem":6427,"__hash__":6428},"content/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations.md","Playwright Accessibility Testing: What axe and Lighthouse Miss",{"type":8,"value":1668,"toc":6391},[1669,1676,1679,1681,1685,1688,1763,1766,1769,1771,1775,1778,1781,1811,1814,1824,1830,1833,1835,1839,1843,1861,1864,1871,1883,1897,1900,1902,1906,1916,1923,1930,1935,1993,2000,2007,2009,2020,2028,2033,2044,2050,2156,2225,2236,2238,2242,2249,2259,2267,2278,2284,2286,2290,2293,2313,2319,2326,2434,2437,2442,2444,2448,2451,2454,2466,2478,2481,2491,2583,2585,2589,2592,2595,2621,2624,2632,2635,2708,2726,2734,2736,2740,2750,2753,2759,2831,2834,2843,2845,2849,2856,2867,2870,2919,2924,2926,2930,2941,2944,2947,2988,2993,2996,3978,3980,3984,3991,3994,3998,4179,4183,4186,4358,4362,4365,4536,4540,4543,4838,4842,4845,5097,5101,5104,5433,5437,5440,5717,5721,5724,5967,5971,5974,6293,6295,6299,6305,6308,6366,6369,6371,6373,6379,6382,6385,6388],[11,1670,1671,1672,1675],{},"Automated accessibility tools like axe, Lighthouse, and Playwright's ",[116,1673,1674],{},"@axe-core/playwright"," integration only catch an estimated 30–40% of real WCAG violations. That gap is dangerous in an environment where developers are already suffering tool fatigue managing a queue of linters, security scanners, and CI checks competing for attention. When an AI assistant suggests a fix that turns a failing accessibility check green, it's tempting to accept it and move on — the fix looks reasonable, the build passes, and the queue gets shorter.",[11,1677,1678],{},"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.",[27,1680],{},[30,1682,1684],{"id":1683},"automated-accessibility-testing-coverage-what-the-research-shows","Automated Accessibility Testing Coverage: What the Research Shows",[11,1686,1687],{},"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:",[1689,1690,1691,1704],"table",{},[1692,1693,1694],"thead",{},[1695,1696,1697,1701],"tr",{},[1698,1699,1700],"th",{},"Source",[1698,1702,1703],{},"Finding",[1705,1706,1707,1719,1730,1741,1752],"tbody",{},[1695,1708,1709,1716],{},[1710,1711,1712],"td",{},[668,1713],{"href":1714,"text":1715},"https://webaim.org/projects/million/","WebAIM",[1710,1717,1718],{},"~30% of real WCAG failures are detectable by automation",[1695,1720,1721,1727],{},[1710,1722,1723],{},[668,1724],{"href":1725,"text":1726},"https://www.w3.org/WAI/test-evaluate/","W3C / WAI",[1710,1728,1729],{},"~20–30% of WCAG Success Criteria are fully automatable",[1695,1731,1732,1738],{},[1710,1733,1734],{},[668,1735],{"href":1736,"text":1737},"https://www.section508.gov/test/","U.S. GSA – Section 508 Program",[1710,1739,1740],{},"Some materials state 1 in 3 issues are caught by automated accessibility tests",[1695,1742,1743,1749],{},[1710,1744,1745],{},[668,1746],{"href":1747,"text":1748},"https://www.deque.com/automated-accessibility-coverage-report/","Deque (axe)",[1710,1750,1751],{},"57.38% of known violations detected when axe-core was run against a large sample of real-world audited pages",[1695,1753,1754,1760],{},[1710,1755,1756],{},[668,1757],{"href":1758,"text":1759},"https://accessible.org/automated-scans-wcag/","Accessible.org",[1710,1761,1762],{},"13% fully automatable, 45% partially, 42% not automatable (WCAG 2.2 AA)",[11,1764,1765],{},"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,1767,1768],{},"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.",[27,1770],{},[30,1772,1774],{"id":1773},"what-automated-accessibility-tools-reliably-detect","What Automated Accessibility Tools Reliably Detect",[11,1776,1777],{},"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,1779,1780],{},"Automated tools reliably catch mechanical rule violations:",[59,1782,1783,1790,1793,1796,1805,1808],{},[62,1784,1785,1786,1789],{},"Missing ",[116,1787,1788],{},"alt"," attributes on images",[62,1791,1792],{},"Missing form labels",[62,1794,1795],{},"Insufficient color contrast ratios on static text",[62,1797,1785,1798,1801,1802],{},[116,1799,1800],{},"lang"," attribute on ",[116,1803,1804],{},"\u003Chtml>",[62,1806,1807],{},"Missing document title",[62,1809,1810],{},"Invalid ARIA roles and attribute values",[11,1812,1813],{},"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,1815,1816,1819,1820,1823],{},[283,1817,1818],{},"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 ",[116,1821,1822],{},"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,1825,1826,1829],{},[283,1827,1828],{},"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,1831,1832],{},"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.",[27,1834],{},[30,1836,1838],{"id":1837},"what-automated-accessibility-checks-cant-evaluate","What Automated Accessibility Checks Can't Evaluate",[46,1840,1842],{"id":1841},"_1-ambiguous-link-text","1. Ambiguous Link Text",[11,1844,1845,1846,1849,1850,1849,1853,1856,1857,1860],{},"Links like ",[38,1847,1848],{},"Read more",", ",[38,1851,1852],{},"Click here",[38,1854,1855],{},"Browse all",", and ",[38,1858,1859],{},"Learn more"," all pass automated checks without complaint. There's nothing structurally wrong with them — they're valid anchor elements with text content.",[11,1862,1863],{},"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,1865,1866,1867,1870],{},"Automated tools can't evaluate whether link text is ",[38,1868,1869],{},"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,1872,1873,1876,1877,1879,1880,41],{},[283,1874,1875],{},"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 ",[116,1878,1822],{}," to provide the full context: ",[116,1881,1882],{},"aria-label=\"Read more about Playwright accessibility testing\"",[11,1884,1885,1888,1889,1892,1893,1896],{},[283,1886,1887],{},"Fix (static analysis):"," For React projects, ",[116,1890,1891],{},"eslint-plugin-jsx-a11y"," includes a built-in ",[116,1894,1895],{},"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,1898,1899],{},"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.",[27,1901],{},[46,1903,1905],{"id":1904},"_2-missing-skip-navigation-links","2. Missing Skip Navigation Links",[11,1907,1908,1909,659,1912,1915],{},"Landmark elements like ",[116,1910,1911],{},"\u003Cmain>",[116,1913,1914],{},"\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,1917,1918,1919,1922],{},"But keyboard-only users who ",[38,1920,1921],{},"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,1924,1925,1926,1929],{},"A skip link — a visually hidden anchor as the first focusable element that jumps to ",[116,1927,1928],{},"#main-content"," — serves both groups. Landmarks alone don't.",[11,1931,1932,1934],{},[283,1933,1875],{}," Add a skip link as the first focusable element on every page, even if your landmark structure is correct.",[154,1936,1940],{"className":1937,"code":1938,"language":1939,"meta":160,"style":160},"language-html shiki shiki-themes material-theme-lighter github-light-high-contrast github-dark-high-contrast","\u003Ca href=\"#main-content\" class=\"sr-only focus:not-sr-only\">\n  Skip to main content\n\u003C/a>\n","html",[116,1941,1942,1979,1984],{"__ignoreMap":160},[164,1943,1944,1947,1950,1954,1957,1960,1962,1964,1967,1969,1971,1974,1976],{"class":166,"line":167},[164,1945,1946],{"class":211},"\u003C",[164,1948,1949],{"class":373},"a",[164,1951,1953],{"class":1952},"sM74w"," href",[164,1955,1956],{"class":211},"=",[164,1958,1959],{"class":221},"\"",[164,1961,1928],{"class":225},[164,1963,1959],{"class":221},[164,1965,1966],{"class":1952}," class",[164,1968,1956],{"class":211},[164,1970,1959],{"class":221},[164,1972,1973],{"class":225},"sr-only focus:not-sr-only",[164,1975,1959],{"class":221},[164,1977,1978],{"class":211},">\n",[164,1980,1981],{"class":166,"line":174},[164,1982,1983],{"class":207},"  Skip to main content\n",[164,1985,1986,1989,1991],{"class":166,"line":180},[164,1987,1988],{"class":211},"\u003C/",[164,1990,1949],{"class":373},[164,1992,1978],{"class":211},[11,1994,1995,1996,1999],{},"Below is a screenshot showing how it works on this site. When a keyboard-only user visits, the first focusable element is a ",[38,1997,1998],{},"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,2001,2002],{},[2003,2004],"img",{"alt":2005,"src":2006},"Screenshot of www.davidmello.com showing the skip link focused at the top of the page, header navigation elements marked in red to bypass, and the main content area highlighted in green as the destination","images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/skip-link-example.webp",[27,2008],{},[46,2010,2012,2013,2016,2017,2019],{"id":2011},"_3-aria-labelledby-vs-aria-label-both-pass-one-is-better","3. ",[116,2014,2015],{},"aria-labelledby"," vs ",[116,2018,1822],{}," — Both Pass, One Is Better",[11,2021,2022,2023,659,2025,2027],{},"Both ",[116,2024,2015],{},[116,2026,1822],{}," 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,2029,2030,2032],{},[116,2031,1822],{}," strings are invisible — they exist only in the accessibility tree. This creates two problems:",[59,2034,2035,2038],{},[62,2036,2037],{},"For sighted users who also use assistive technology, there can be a mismatch between what they see and what their screen reader announces",[62,2039,2040,2041,2043],{},"Translation tools frequently miss ",[116,2042,1822],{}," attributes, leaving non-English users with untranslated accessible names",[11,2045,2046,2047,2049],{},"When a visible heading or label already exists on the page, ",[116,2048,2015],{}," pointing to that element is almost always the better choice — it reuses the visible text so both representations stay in sync automatically.",[154,2051,2054],{"className":1937,"code":2052,"filename":2053,"language":1939,"meta":160,"style":160},"\u003Cbody>\n  \u003C!-- A visible header exists with text to label the section with, so aria-labelledby is correct here -->\n  \u003Csection aria-labelledby=\"aria-header\">\n    \u003Ch2 id=\"aria-header\">Using aria attributes correctly\u003C/h2>\n    \u003Cp>...\u003C/p>\n  \u003C/section>\n\u003C/body>\n","aria-labelledby-example.html",[116,2055,2056,2065,2070,2092,2122,2139,2148],{"__ignoreMap":160},[164,2057,2058,2060,2063],{"class":166,"line":167},[164,2059,1946],{"class":211},[164,2061,2062],{"class":373},"body",[164,2064,1978],{"class":211},[164,2066,2067],{"class":166,"line":174},[164,2068,2069],{"class":170},"  \u003C!-- A visible header exists with text to label the section with, so aria-labelledby is correct here -->\n",[164,2071,2072,2075,2078,2081,2083,2085,2088,2090],{"class":166,"line":180},[164,2073,2074],{"class":211},"  \u003C",[164,2076,2077],{"class":373},"section",[164,2079,2080],{"class":1952}," aria-labelledby",[164,2082,1956],{"class":211},[164,2084,1959],{"class":221},[164,2086,2087],{"class":225},"aria-header",[164,2089,1959],{"class":221},[164,2091,1978],{"class":211},[164,2093,2094,2097,2099,2102,2104,2106,2108,2110,2113,2116,2118,2120],{"class":166,"line":187},[164,2095,2096],{"class":211},"    \u003C",[164,2098,30],{"class":373},[164,2100,2101],{"class":1952}," id",[164,2103,1956],{"class":211},[164,2105,1959],{"class":221},[164,2107,2087],{"class":225},[164,2109,1959],{"class":221},[164,2111,2112],{"class":211},">",[164,2114,2115],{"class":207},"Using aria attributes correctly",[164,2117,1988],{"class":211},[164,2119,30],{"class":373},[164,2121,1978],{"class":211},[164,2123,2124,2126,2128,2130,2133,2135,2137],{"class":166,"line":193},[164,2125,2096],{"class":211},[164,2127,11],{"class":373},[164,2129,2112],{"class":211},[164,2131,2132],{"class":207},"...",[164,2134,1988],{"class":211},[164,2136,11],{"class":373},[164,2138,1978],{"class":211},[164,2140,2141,2144,2146],{"class":166,"line":198},[164,2142,2143],{"class":211},"  \u003C/",[164,2145,2077],{"class":373},[164,2147,1978],{"class":211},[164,2149,2150,2152,2154],{"class":166,"line":204},[164,2151,1988],{"class":211},[164,2153,2062],{"class":373},[164,2155,1978],{"class":211},[154,2157,2160],{"className":1937,"code":2158,"filename":2159,"language":1939,"meta":160,"style":160},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n\u003Cbutton aria-label=\"Close dialog\">\n  \u003Csvg aria-hidden=\"true\">...\u003C/svg>\n\u003C/button>\n","aria-label-example.html",[116,2161,2162,2167,2188,2217],{"__ignoreMap":160},[164,2163,2164],{"class":166,"line":167},[164,2165,2166],{"class":170},"\u003C!-- No visible label text exists, so aria-label is appropriate -->\n",[164,2168,2169,2171,2174,2177,2179,2181,2184,2186],{"class":166,"line":174},[164,2170,1946],{"class":211},[164,2172,2173],{"class":373},"button",[164,2175,2176],{"class":1952}," aria-label",[164,2178,1956],{"class":211},[164,2180,1959],{"class":221},[164,2182,2183],{"class":225},"Close dialog",[164,2185,1959],{"class":221},[164,2187,1978],{"class":211},[164,2189,2190,2192,2195,2198,2200,2202,2205,2207,2209,2211,2213,2215],{"class":166,"line":180},[164,2191,2074],{"class":211},[164,2193,2194],{"class":373},"svg",[164,2196,2197],{"class":1952}," aria-hidden",[164,2199,1956],{"class":211},[164,2201,1959],{"class":221},[164,2203,2204],{"class":225},"true",[164,2206,1959],{"class":221},[164,2208,2112],{"class":211},[164,2210,2132],{"class":207},[164,2212,1988],{"class":211},[164,2214,2194],{"class":373},[164,2216,1978],{"class":211},[164,2218,2219,2221,2223],{"class":166,"line":187},[164,2220,1988],{"class":211},[164,2222,2173],{"class":373},[164,2224,1978],{"class":211},[11,2226,2227,2229,2230,2232,2233,2235],{},[283,2228,1875],{}," Prefer ",[116,2231,2015],{}," when a visible text element can serve as the label. Reserve ",[116,2234,1822],{}," for cases where no visible text exists.",[27,2237],{},[46,2239,2241],{"id":2240},"_4-static-aria-labels-on-dynamic-controls","4. Static ARIA Labels on Dynamic Controls",[11,2243,2244,2245,2248],{},"A toggle button with ",[116,2246,2247],{},"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,2250,2251,2252,659,2255,2258],{},"Automated tools check ",[38,2253,2254],{},"presence",[38,2256,2257],{},"format",", not accuracy or correctness over time. A static label on a stateful control is invisible to a scanner.",[11,2260,2261,2263,2264,2266],{},[283,2262,1875],{}," Bind ",[116,2265,1822],{}," dynamically to reflect the current state:",[154,2268,2272],{"className":2269,"code":2270,"language":2271,"meta":160,"style":160},"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",[116,2273,2274],{"__ignoreMap":160},[164,2275,2276],{"class":166,"line":167},[164,2277,2270],{"class":207},[11,2279,2280,2281,2283],{},"Later in this article, I'll show a Playwright test that verifies a toggle's ",[116,2282,1822],{}," updates after interaction — catching stale labels before they ship.",[27,2285],{},[46,2287,2289],{"id":2288},"_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,2291,2292],{},"axe-core validates presence and format. It does not validate quality.",[11,2294,2295,1849,2298,1856,2301,2304,2305,2308,2309,2312],{},[116,2296,2297],{},"aria-label=\"nav\"",[116,2299,2300],{},"aria-label=\"section 1\"",[116,2302,2303],{},"alt=\"image\""," all pass automated checks. So does ",[116,2306,2307],{},"role=\"button\""," on a ",[116,2310,2311],{},"\u003Cdiv>"," that isn't keyboard focusable. A screen reader user hearing \"image\" or \"section 1\" is no better off than they were before the attribute was added — in some cases they're worse off because the presence of the attribute signals to the developer that the defect has been addressed.",[11,2314,2315,2316,2318],{},"The same trap applies to landmarks: adding ",[116,2317,1822],{}," 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,2320,2321,2322,2325],{},"The same principle extends to image alt text. ",[116,2323,2324],{},"alt=\"logo\""," on a company logo passes axe — but says nothing useful. The correct alt text depends on context that automation can't evaluate:",[154,2327,2329],{"className":1937,"code":2328,"language":1939,"meta":160,"style":160},"\u003C!-- Standalone logo — identify the brand -->\n\u003Cimg src=\"logo.svg\" alt=\"Acme Corp\" />\n\n\u003C!-- Logo as a link — describe the destination, not the image -->\n\u003Ca href=\"/\">\n  \u003Cimg src=\"logo.svg\" alt=\"Acme Corp — Home\" />\n\u003C/a>\n",[116,2330,2331,2336,2369,2373,2378,2397,2426],{"__ignoreMap":160},[164,2332,2333],{"class":166,"line":167},[164,2334,2335],{"class":170},"\u003C!-- Standalone logo — identify the brand -->\n",[164,2337,2338,2340,2342,2345,2347,2349,2352,2354,2357,2359,2361,2364,2366],{"class":166,"line":174},[164,2339,1946],{"class":211},[164,2341,2003],{"class":373},[164,2343,2344],{"class":1952}," src",[164,2346,1956],{"class":211},[164,2348,1959],{"class":221},[164,2350,2351],{"class":225},"logo.svg",[164,2353,1959],{"class":221},[164,2355,2356],{"class":1952}," alt",[164,2358,1956],{"class":211},[164,2360,1959],{"class":221},[164,2362,2363],{"class":225},"Acme Corp",[164,2365,1959],{"class":221},[164,2367,2368],{"class":211}," />\n",[164,2370,2371],{"class":166,"line":180},[164,2372,184],{"emptyLinePlaceholder":183},[164,2374,2375],{"class":166,"line":187},[164,2376,2377],{"class":170},"\u003C!-- Logo as a link — describe the destination, not the image -->\n",[164,2379,2380,2382,2384,2386,2388,2390,2393,2395],{"class":166,"line":193},[164,2381,1946],{"class":211},[164,2383,1949],{"class":373},[164,2385,1953],{"class":1952},[164,2387,1956],{"class":211},[164,2389,1959],{"class":221},[164,2391,2392],{"class":225},"/",[164,2394,1959],{"class":221},[164,2396,1978],{"class":211},[164,2398,2399,2401,2403,2405,2407,2409,2411,2413,2415,2417,2419,2422,2424],{"class":166,"line":198},[164,2400,2074],{"class":211},[164,2402,2003],{"class":373},[164,2404,2344],{"class":1952},[164,2406,1956],{"class":211},[164,2408,1959],{"class":221},[164,2410,2351],{"class":225},[164,2412,1959],{"class":221},[164,2414,2356],{"class":1952},[164,2416,1956],{"class":211},[164,2418,1959],{"class":221},[164,2420,2421],{"class":225},"Acme Corp — Home",[164,2423,1959],{"class":221},[164,2425,2368],{"class":211},[164,2427,2428,2430,2432],{"class":166,"line":204},[164,2429,1988],{"class":211},[164,2431,1949],{"class":373},[164,2433,1978],{"class":211},[11,2435,2436],{},"Both pass axe equally. Only one communicates the right thing in each context.",[11,2438,2439,2441],{},[283,2440,1875],{}," 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.",[27,2443],{},[46,2445,2447],{"id":2446},"_6-accessibility-regressions-automated-checks-wont-catch","6. Accessibility Regressions Automated Checks Won't Catch",[11,2449,2450],{},"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,2452,2453],{},"Two examples that come up regularly:",[11,2455,2456,2461,2462,2465],{},[283,2457,2458],{},[116,2459,2460],{},"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 ",[116,2463,2464],{},"alt=\"decorative image\"",". Now screen readers announce \"decorative image\" on every decorative element in the page — noise that wasn't there before.",[11,2467,2468,2473,2474,2477],{},[283,2469,2470],{},[116,2471,2472],{},"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 ",[116,2475,2476],{},"aria-label=\"icon\""," passes tooling, but pollutes the accessible name computation and potentially overrides the button's actual label.",[11,2479,2480],{},"In both cases, a passing automated check actively masks a regression. The original state was more accessible than the \"fixed\" state.",[11,2482,2483,2485,2486,659,2488,2490],{},[283,2484,1875],{}," Treat ",[116,2487,2460],{},[116,2489,2472],{}," as intentional signals, not oversights. Add a comment explaining the intent so your team doesn't correct them:",[154,2492,2494],{"className":1937,"code":2493,"language":1939,"meta":160,"style":160},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n\u003Cimg src=\"divider.svg\" alt=\"\" />\n\n\u003C!-- aria-hidden intentional: button label provided by visible text -->\n\u003Cbutton>\n  \u003Csvg aria-hidden=\"true\">...\u003C/svg>\n  Submit\n\u003C/button>\n",[116,2495,2496,2501,2527,2531,2536,2544,2570,2575],{"__ignoreMap":160},[164,2497,2498],{"class":166,"line":167},[164,2499,2500],{"class":170},"\u003C!-- Empty alt intentional: decorative image, screen readers should skip -->\n",[164,2502,2503,2505,2507,2509,2511,2513,2516,2518,2520,2522,2525],{"class":166,"line":174},[164,2504,1946],{"class":211},[164,2506,2003],{"class":373},[164,2508,2344],{"class":1952},[164,2510,1956],{"class":211},[164,2512,1959],{"class":221},[164,2514,2515],{"class":225},"divider.svg",[164,2517,1959],{"class":221},[164,2519,2356],{"class":1952},[164,2521,1956],{"class":211},[164,2523,2524],{"class":221},"\"\"",[164,2526,2368],{"class":211},[164,2528,2529],{"class":166,"line":180},[164,2530,184],{"emptyLinePlaceholder":183},[164,2532,2533],{"class":166,"line":187},[164,2534,2535],{"class":170},"\u003C!-- aria-hidden intentional: button label provided by visible text -->\n",[164,2537,2538,2540,2542],{"class":166,"line":193},[164,2539,1946],{"class":211},[164,2541,2173],{"class":373},[164,2543,1978],{"class":211},[164,2545,2546,2548,2550,2552,2554,2556,2558,2560,2562,2564,2566,2568],{"class":166,"line":198},[164,2547,2074],{"class":211},[164,2549,2194],{"class":373},[164,2551,2197],{"class":1952},[164,2553,1956],{"class":211},[164,2555,1959],{"class":221},[164,2557,2204],{"class":225},[164,2559,1959],{"class":221},[164,2561,2112],{"class":211},[164,2563,2132],{"class":207},[164,2565,1988],{"class":211},[164,2567,2194],{"class":373},[164,2569,1978],{"class":211},[164,2571,2572],{"class":166,"line":204},[164,2573,2574],{"class":207},"  Submit\n",[164,2576,2577,2579,2581],{"class":166,"line":443},[164,2578,1988],{"class":211},[164,2580,2173],{"class":373},[164,2582,1978],{"class":211},[27,2584],{},[46,2586,2588],{"id":2587},"_7-automated-wcag-color-contrast-testing-limitations","7. Automated WCAG Color Contrast Testing Limitations",[11,2590,2591],{},"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,2593,2594],{},"But gaps remain:",[59,2596,2597,2603,2609,2615],{},[62,2598,2599,2602],{},[283,2600,2601],{},"Text over images or gradients"," — axe-core may not be able to reliably report contrast issues when working with gradient or image backgrounds",[62,2604,2605,2608],{},[283,2606,2607],{},"Semi-transparent backgrounds"," — the computed color may not be accurate",[62,2610,2611,2614],{},[283,2612,2613],{},"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",[62,2616,2617,2620],{},[283,2618,2619],{},"Dark mode"," — a scan in light mode won't catch dark mode contrast failures, and vice versa",[11,2622,2623],{},"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,2625,2626,2627,2631],{},"For the contrast issues automated tools do catch, a dedicated checker gives you more control than waiting for a CI scan. The ",[1949,2628,2630],{"href":2629},"/tools/color-contrast-checker","WCAG Color Contrast Checker"," I built lets you test specific color pairs against AA and AAA thresholds, simulate how the combination appears across four types of colorblindness, and generate a ready-to-paste bug report including recommended closest-matching color fixes for your issue tracker.",[11,2633,2634],{},"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:",[154,2636,2640],{"className":2637,"code":2638,"language":2639,"meta":160,"style":160},"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",[116,2641,2642,2652,2690,2704],{"__ignoreMap":160},[164,2643,2644,2646,2650],{"class":166,"line":167},[164,2645,41],{"class":1337},[164,2647,2649],{"class":2648},"slOWO","hero-text",[164,2651,535],{"class":211},[164,2653,2654,2658,2660,2664,2666,2669,2672,2674,2677,2680,2682,2684,2687],{"class":166,"line":174},[164,2655,2657],{"class":2656},"shxsR","  background",[164,2659,377],{"class":211},[164,2661,2663],{"class":2662},"slPND"," linear-gradient",[164,2665,218],{"class":211},[164,2667,2668],{"class":247},"to",[164,2670,2671],{"class":1489}," bottom",[164,2673,231],{"class":211},[164,2675,2676],{"class":1337}," #",[164,2678,2679],{"class":1489},"ffffff",[164,2681,231],{"class":211},[164,2683,2676],{"class":1337},[164,2685,2686],{"class":1489},"000000",[164,2688,2689],{"class":211},");\n",[164,2691,2692,2695,2697,2699,2702],{"class":166,"line":180},[164,2693,2694],{"class":2656},"  color",[164,2696,377],{"class":211},[164,2698,2676],{"class":1337},[164,2700,2701],{"class":1489},"544f4f",[164,2703,1516],{"class":211},[164,2705,2706],{"class":166,"line":187},[164,2707,1147],{"class":211},[11,2709,2710,2711,2714,2715,2718,2719,2721,2722,2725],{},"Enter ",[116,2712,2713],{},"#544f4f"," as the foreground color and ",[116,2716,2717],{},"#ffffff"," as the background in the ",[1949,2720,2630],{"href":2629},", verify it passes, then repeat with ",[116,2723,2724],{},"#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,2727,2728],{},[2003,2729],{"alt":2730,"src":2731,"className":2732},"Screenshot of color contrast interface with #544f4f and #000000 combination showing a contrast violation","images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/wcag-contrast-color-checker-example.webp",[2733],"portrait",[27,2735],{},[46,2737,2739],{"id":2738},"_8-when-lighthouse-landmark-recommendations-break-legacy-html","8. When Lighthouse Landmark Recommendations Break Legacy HTML",[11,2741,2742,2743,1849,2745,1849,2747,41],{},"Lighthouse flags missing landmark regions as accessibility violations. In a modern HTML5 codebase that recommendation is straightforward — add ",[116,2744,1911],{},[116,2746,1914],{},[116,2748,2749],{},"\u003Cheader>",[11,2751,2752],{},"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,2754,2755,2756,2758],{},"The correct fix in legacy markup contexts is ARIA landmark roles on ",[116,2757,2311],{}," elements:",[154,2760,2762],{"className":1937,"code":2761,"language":1939,"meta":160,"style":160},"\u003Cdiv role=\"main\">...\u003C/div>\n\u003Cdiv role=\"navigation\" aria-label=\"Main navigation\">...\u003C/div>\n",[116,2763,2764,2793],{"__ignoreMap":160},[164,2765,2766,2768,2771,2774,2776,2778,2781,2783,2785,2787,2789,2791],{"class":166,"line":167},[164,2767,1946],{"class":211},[164,2769,2770],{"class":373},"div",[164,2772,2773],{"class":1952}," role",[164,2775,1956],{"class":211},[164,2777,1959],{"class":221},[164,2779,2780],{"class":225},"main",[164,2782,1959],{"class":221},[164,2784,2112],{"class":211},[164,2786,2132],{"class":207},[164,2788,1988],{"class":211},[164,2790,2770],{"class":373},[164,2792,1978],{"class":211},[164,2794,2795,2797,2799,2801,2803,2805,2808,2810,2812,2814,2816,2819,2821,2823,2825,2827,2829],{"class":166,"line":174},[164,2796,1946],{"class":211},[164,2798,2770],{"class":373},[164,2800,2773],{"class":1952},[164,2802,1956],{"class":211},[164,2804,1959],{"class":221},[164,2806,2807],{"class":225},"navigation",[164,2809,1959],{"class":221},[164,2811,2176],{"class":1952},[164,2813,1956],{"class":211},[164,2815,1959],{"class":221},[164,2817,2818],{"class":225},"Main navigation",[164,2820,1959],{"class":221},[164,2822,2112],{"class":211},[164,2824,2132],{"class":207},[164,2826,1988],{"class":211},[164,2828,2770],{"class":373},[164,2830,1978],{"class":211},[11,2832,2833],{},"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,2835,2836,2838,2839,2842],{},[283,2837,1875],{}," 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 ",[116,2840,2841],{},"role"," attribute.",[27,2844],{},[46,2846,2848],{"id":2847},"_9-form-accessibility-labels-instructions-and-error-messages-automated-tools-miss","9. Form Accessibility: Labels, Instructions, and Error Messages Automated Tools Miss",[11,2850,2851,2852,2855],{},"Automated tools reliably catch a missing ",[116,2853,2854],{},"\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,2857,2858,2859,2862,2863,2866],{},"A ",[116,2860,2861],{},"\u003Clabel>First name\u003C/label>"," associated with a text input passes every automated check. So does ",[116,2864,2865],{},"\u003Clabel>Field 1\u003C/label>",". The tool sees a label — it doesn't read it.",[11,2868,2869],{},"The same gap extends across the full form experience:",[59,2871,2872,2878,2889,2895,2909],{},[62,2873,2874,2877],{},[283,2875,2876],{},"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.",[62,2879,2880,2883,2884,2888],{},[283,2881,2882],{},"Vague error messages"," — \"Invalid input\" and \"Your email address must be in the format ",[1949,2885,2887],{"href":2886},"mailto:name@example.com","name@example.com","\" both pass automated checks. Only one helps the user recover.",[62,2890,2891,2894],{},[283,2892,2893],{},"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.",[62,2896,2897,2900,2901,2904,2905,2908],{},[283,2898,2899],{},"Error announcement timing"," — whether an error is surfaced via ",[116,2902,2903],{},"aria-live"," or ",[116,2906,2907],{},"aria-describedby"," in a way screen readers actually pick up requires testing with real assistive technology, not a DOM scan.",[62,2910,2911,2914,2915,2918],{},[283,2912,2913],{},"Required field indicators"," — a red asterisk is a visual convention. Without a text equivalent or ",[116,2916,2917],{},"aria-required=\"true\"",", screen reader users have no indication a field is mandatory until a failed submission tells them so.",[11,2920,2921,2923],{},[283,2922,1875],{}," 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.",[27,2925],{},[46,2927,2929],{"id":2928},"_10-keyboard-accessibility-dynamic-content-modals-and-interactive-components","10. Keyboard Accessibility: Dynamic Content, Modals, and Interactive Components",[11,2931,2932,2933,2936,2937,2940],{},"Automated tools can verify that a modal has ",[116,2934,2935],{},"role=\"dialog\""," and an accessible name, or that an accordion button has ",[116,2938,2939],{},"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,2942,2943],{},"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,2945,2946],{},"The most common failures in this area:",[59,2948,2949,2955,2961,2967,2973,2979],{},[62,2950,2951,2954],{},[283,2952,2953],{},"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",[62,2956,2957,2960],{},[283,2958,2959],{},"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",[62,2962,2963,2966],{},[283,2964,2965],{},"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",[62,2968,2969,2972],{},[283,2970,2971],{},"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",[62,2974,2975,2978],{},[283,2976,2977],{},"Expand/collapse keyboard activation"," — an accordion that only responds to mouse clicks isn't keyboard accessible; Enter and Space should both activate toggle controls",[62,2980,2981,2984,2985,2987],{},[283,2982,2983],{},"Dynamically added content not announced"," — content that appears after an interaction (search results, inline validation, notifications) needs ",[116,2986,2903],{}," regions or explicit focus management to reach screen reader users",[11,2989,2990,2992],{},[283,2991,1875],{}," Test every interactive component with keyboard only — no mouse. Open modals with Enter, navigate within them with Tab and Shift+Tab, close with Escape, and verify focus returns to the trigger. Activate expand/collapse controls with both Enter and Space and confirm the revealed content is reachable by Tab.",[11,2994,2995],{},"These keyboard paths can then be automated in Playwright:",[154,2997,3000],{"className":156,"code":2998,"filename":2999,"language":159,"meta":160,"style":160},"test('modal traps focus and closes on Escape', async ({ page }) => {\n  await page.goto('/');\n  await page.getByRole('button', { name: 'Open dialog' }).click();\n  const dialog = page.getByRole('dialog');\n  await expect(dialog).toBeVisible();\n\n  // Tab multiple times — focus should remain within the dialog\n  for (let i = 0; i \u003C 10; i++) {\n    await page.keyboard.press('Tab');\n    const isInsideDialog = await page.evaluate(() =>\n      document.activeElement?.closest('[role=\"dialog\"]') !== null,\n    );\n    expect(isInsideDialog).toBe(true);\n  }\n\n  // Escape should close the modal and it should no longer be visible\n  await page.keyboard.press('Escape');\n  await expect(dialog).not.toBeVisible();\n});\n\ntest('modal returns focus to trigger on close', async ({ page }) => {\n  await page.goto('/');\n  const trigger = page.getByRole('button', { name: 'Open dialog' });\n  await trigger.click();\n  await page.keyboard.press('Escape');\n  await expect(trigger).toBeFocused();\n});\n\ntest('expand/collapse toggle is keyboard accessible', async ({ page }) => {\n  await page.goto('/');\n  const toggle = page.getByRole('button', { name: 'Show details' });\n  const content = page.getByRole('region', { name: 'Show details' });\n\n  await expect(toggle).toHaveAttribute('aria-expanded', 'false');\n  await expect(content).not.toBeVisible();\n\n  await toggle.press('Enter');\n  await expect(toggle).toHaveAttribute('aria-expanded', 'true');\n  await expect(content).toBeVisible();\n\n  await toggle.press('Space');\n  await expect(toggle).toHaveAttribute('aria-expanded', 'false');\n  await expect(content).not.toBeVisible();\n});\n","accessibility.spec.ts",[116,3001,3002,3029,3053,3100,3129,3151,3155,3160,3202,3232,3257,3293,3300,3325,3329,3333,3338,3365,3391,3400,3405,3433,3456,3500,3515,3542,3565,3574,3579,3607,3630,3675,3720,3725,3765,3791,3796,3820,3857,3878,3883,3907,3944,3969],{"__ignoreMap":160},[164,3003,3004,3006,3008,3010,3013,3015,3017,3019,3021,3023,3025,3027],{"class":166,"line":167},[164,3005,208],{"class":214},[164,3007,218],{"class":207},[164,3009,222],{"class":221},[164,3011,3012],{"class":225},"modal traps focus and closes on Escape",[164,3014,222],{"class":221},[164,3016,231],{"class":211},[164,3018,235],{"class":234},[164,3020,714],{"class":211},[164,3022,718],{"class":717},[164,3024,721],{"class":211},[164,3026,241],{"class":234},[164,3028,535],{"class":211},[164,3030,3031,3034,3036,3038,3041,3043,3045,3047,3049,3051],{"class":166,"line":174},[164,3032,3033],{"class":451},"  await",[164,3035,718],{"class":207},[164,3037,41],{"class":211},[164,3039,3040],{"class":214},"goto",[164,3042,218],{"class":787},[164,3044,222],{"class":221},[164,3046,2392],{"class":225},[164,3048,222],{"class":221},[164,3050,1398],{"class":787},[164,3052,1516],{"class":211},[164,3054,3055,3057,3059,3061,3064,3066,3068,3070,3072,3074,3076,3078,3080,3082,3085,3087,3089,3091,3093,3096,3098],{"class":166,"line":180},[164,3056,3033],{"class":451},[164,3058,718],{"class":207},[164,3060,41],{"class":211},[164,3062,3063],{"class":214},"getByRole",[164,3065,218],{"class":787},[164,3067,222],{"class":221},[164,3069,2173],{"class":225},[164,3071,222],{"class":221},[164,3073,231],{"class":211},[164,3075,244],{"class":211},[164,3077,425],{"class":787},[164,3079,377],{"class":211},[164,3081,408],{"class":221},[164,3083,3084],{"class":225},"Open dialog",[164,3086,222],{"class":221},[164,3088,251],{"class":211},[164,3090,1398],{"class":787},[164,3092,41],{"class":211},[164,3094,3095],{"class":214},"click",[164,3097,1441],{"class":787},[164,3099,1516],{"class":211},[164,3101,3102,3105,3108,3110,3112,3114,3116,3118,3120,3123,3125,3127],{"class":166,"line":187},[164,3103,3104],{"class":234},"  const",[164,3106,3107],{"class":1489}," dialog",[164,3109,1346],{"class":247},[164,3111,718],{"class":207},[164,3113,41],{"class":211},[164,3115,3063],{"class":214},[164,3117,218],{"class":787},[164,3119,222],{"class":221},[164,3121,3122],{"class":225},"dialog",[164,3124,222],{"class":221},[164,3126,1398],{"class":787},[164,3128,1516],{"class":211},[164,3130,3131,3133,3136,3138,3140,3142,3144,3147,3149],{"class":166,"line":193},[164,3132,3033],{"class":451},[164,3134,3135],{"class":214}," expect",[164,3137,218],{"class":787},[164,3139,3122],{"class":207},[164,3141,1398],{"class":787},[164,3143,41],{"class":211},[164,3145,3146],{"class":214},"toBeVisible",[164,3148,1441],{"class":787},[164,3150,1516],{"class":211},[164,3152,3153],{"class":166,"line":198},[164,3154,184],{"emptyLinePlaceholder":183},[164,3156,3157],{"class":166,"line":204},[164,3158,3159],{"class":170},"  // Tab multiple times — focus should remain within the dialog\n",[164,3161,3162,3165,3167,3170,3173,3175,3179,3182,3184,3187,3190,3192,3194,3197,3200],{"class":166,"line":443},[164,3163,3164],{"class":451},"  for",[164,3166,1392],{"class":787},[164,3168,3169],{"class":234},"let",[164,3171,3172],{"class":207}," i",[164,3174,1346],{"class":247},[164,3176,3178],{"class":3177},"s6g51"," 0",[164,3180,3181],{"class":211},";",[164,3183,3172],{"class":207},[164,3185,3186],{"class":247}," \u003C",[164,3188,3189],{"class":3177}," 10",[164,3191,3181],{"class":211},[164,3193,3172],{"class":207},[164,3195,3196],{"class":247},"++",[164,3198,3199],{"class":787},") ",[164,3201,1211],{"class":211},[164,3203,3204,3207,3209,3211,3214,3216,3219,3221,3223,3226,3228,3230],{"class":166,"line":455},[164,3205,3206],{"class":451},"    await",[164,3208,718],{"class":207},[164,3210,41],{"class":211},[164,3212,3213],{"class":207},"keyboard",[164,3215,41],{"class":211},[164,3217,3218],{"class":214},"press",[164,3220,218],{"class":787},[164,3222,222],{"class":221},[164,3224,3225],{"class":225},"Tab",[164,3227,222],{"class":221},[164,3229,1398],{"class":787},[164,3231,1516],{"class":211},[164,3233,3234,3236,3239,3241,3243,3245,3247,3250,3252,3254],{"class":166,"line":461},[164,3235,1486],{"class":234},[164,3237,3238],{"class":1489}," isInsideDialog",[164,3240,1346],{"class":247},[164,3242,1495],{"class":451},[164,3244,718],{"class":207},[164,3246,41],{"class":211},[164,3248,3249],{"class":214},"evaluate",[164,3251,218],{"class":787},[164,3253,1441],{"class":211},[164,3255,3256],{"class":234}," =>\n",[164,3258,3259,3262,3264,3267,3270,3273,3275,3277,3280,3282,3284,3287,3290],{"class":166,"line":627},[164,3260,3261],{"class":207},"      document",[164,3263,41],{"class":211},[164,3265,3266],{"class":207},"activeElement",[164,3268,3269],{"class":211},"?.",[164,3271,3272],{"class":214},"closest",[164,3274,218],{"class":787},[164,3276,222],{"class":221},[164,3278,3279],{"class":225},"[role=\"dialog\"]",[164,3281,222],{"class":221},[164,3283,3199],{"class":787},[164,3285,3286],{"class":247},"!==",[164,3288,3289],{"class":1337}," null",[164,3291,3292],{"class":211},",\n",[164,3294,3295,3298],{"class":166,"line":829},[164,3296,3297],{"class":787},"    )",[164,3299,1516],{"class":211},[164,3301,3302,3305,3307,3310,3312,3314,3317,3319,3321,3323],{"class":166,"line":835},[164,3303,3304],{"class":214},"    expect",[164,3306,218],{"class":787},[164,3308,3309],{"class":207},"isInsideDialog",[164,3311,1398],{"class":787},[164,3313,41],{"class":211},[164,3315,3316],{"class":214},"toBe",[164,3318,218],{"class":787},[164,3320,2204],{"class":1364},[164,3322,1398],{"class":787},[164,3324,1516],{"class":211},[164,3326,3327],{"class":166,"line":841},[164,3328,1410],{"class":211},[164,3330,3331],{"class":166,"line":873},[164,3332,184],{"emptyLinePlaceholder":183},[164,3334,3335],{"class":166,"line":878},[164,3336,3337],{"class":170},"  // Escape should close the modal and it should no longer be visible\n",[164,3339,3340,3342,3344,3346,3348,3350,3352,3354,3356,3359,3361,3363],{"class":166,"line":1078},[164,3341,3033],{"class":451},[164,3343,718],{"class":207},[164,3345,41],{"class":211},[164,3347,3213],{"class":207},[164,3349,41],{"class":211},[164,3351,3218],{"class":214},[164,3353,218],{"class":787},[164,3355,222],{"class":221},[164,3357,3358],{"class":225},"Escape",[164,3360,222],{"class":221},[164,3362,1398],{"class":787},[164,3364,1516],{"class":211},[164,3366,3368,3370,3372,3374,3376,3378,3380,3383,3385,3387,3389],{"class":166,"line":3367},18,[164,3369,3033],{"class":451},[164,3371,3135],{"class":214},[164,3373,218],{"class":787},[164,3375,3122],{"class":207},[164,3377,1398],{"class":787},[164,3379,41],{"class":211},[164,3381,3382],{"class":207},"not",[164,3384,41],{"class":211},[164,3386,3146],{"class":214},[164,3388,1441],{"class":787},[164,3390,1516],{"class":211},[164,3392,3394,3396,3398],{"class":166,"line":3393},19,[164,3395,545],{"class":211},[164,3397,1398],{"class":207},[164,3399,1516],{"class":211},[164,3401,3403],{"class":166,"line":3402},20,[164,3404,184],{"emptyLinePlaceholder":183},[164,3406,3408,3410,3412,3414,3417,3419,3421,3423,3425,3427,3429,3431],{"class":166,"line":3407},21,[164,3409,208],{"class":214},[164,3411,218],{"class":207},[164,3413,222],{"class":221},[164,3415,3416],{"class":225},"modal returns focus to trigger on close",[164,3418,222],{"class":221},[164,3420,231],{"class":211},[164,3422,235],{"class":234},[164,3424,714],{"class":211},[164,3426,718],{"class":717},[164,3428,721],{"class":211},[164,3430,241],{"class":234},[164,3432,535],{"class":211},[164,3434,3436,3438,3440,3442,3444,3446,3448,3450,3452,3454],{"class":166,"line":3435},22,[164,3437,3033],{"class":451},[164,3439,718],{"class":207},[164,3441,41],{"class":211},[164,3443,3040],{"class":214},[164,3445,218],{"class":787},[164,3447,222],{"class":221},[164,3449,2392],{"class":225},[164,3451,222],{"class":221},[164,3453,1398],{"class":787},[164,3455,1516],{"class":211},[164,3457,3459,3461,3464,3466,3468,3470,3472,3474,3476,3478,3480,3482,3484,3486,3488,3490,3492,3494,3496,3498],{"class":166,"line":3458},23,[164,3460,3104],{"class":234},[164,3462,3463],{"class":1489}," trigger",[164,3465,1346],{"class":247},[164,3467,718],{"class":207},[164,3469,41],{"class":211},[164,3471,3063],{"class":214},[164,3473,218],{"class":787},[164,3475,222],{"class":221},[164,3477,2173],{"class":225},[164,3479,222],{"class":221},[164,3481,231],{"class":211},[164,3483,244],{"class":211},[164,3485,425],{"class":787},[164,3487,377],{"class":211},[164,3489,408],{"class":221},[164,3491,3084],{"class":225},[164,3493,222],{"class":221},[164,3495,251],{"class":211},[164,3497,1398],{"class":787},[164,3499,1516],{"class":211},[164,3501,3503,3505,3507,3509,3511,3513],{"class":166,"line":3502},24,[164,3504,3033],{"class":451},[164,3506,3463],{"class":207},[164,3508,41],{"class":211},[164,3510,3095],{"class":214},[164,3512,1441],{"class":787},[164,3514,1516],{"class":211},[164,3516,3518,3520,3522,3524,3526,3528,3530,3532,3534,3536,3538,3540],{"class":166,"line":3517},25,[164,3519,3033],{"class":451},[164,3521,718],{"class":207},[164,3523,41],{"class":211},[164,3525,3213],{"class":207},[164,3527,41],{"class":211},[164,3529,3218],{"class":214},[164,3531,218],{"class":787},[164,3533,222],{"class":221},[164,3535,3358],{"class":225},[164,3537,222],{"class":221},[164,3539,1398],{"class":787},[164,3541,1516],{"class":211},[164,3543,3545,3547,3549,3551,3554,3556,3558,3561,3563],{"class":166,"line":3544},26,[164,3546,3033],{"class":451},[164,3548,3135],{"class":214},[164,3550,218],{"class":787},[164,3552,3553],{"class":207},"trigger",[164,3555,1398],{"class":787},[164,3557,41],{"class":211},[164,3559,3560],{"class":214},"toBeFocused",[164,3562,1441],{"class":787},[164,3564,1516],{"class":211},[164,3566,3568,3570,3572],{"class":166,"line":3567},27,[164,3569,545],{"class":211},[164,3571,1398],{"class":207},[164,3573,1516],{"class":211},[164,3575,3577],{"class":166,"line":3576},28,[164,3578,184],{"emptyLinePlaceholder":183},[164,3580,3582,3584,3586,3588,3591,3593,3595,3597,3599,3601,3603,3605],{"class":166,"line":3581},29,[164,3583,208],{"class":214},[164,3585,218],{"class":207},[164,3587,222],{"class":221},[164,3589,3590],{"class":225},"expand/collapse toggle is keyboard accessible",[164,3592,222],{"class":221},[164,3594,231],{"class":211},[164,3596,235],{"class":234},[164,3598,714],{"class":211},[164,3600,718],{"class":717},[164,3602,721],{"class":211},[164,3604,241],{"class":234},[164,3606,535],{"class":211},[164,3608,3610,3612,3614,3616,3618,3620,3622,3624,3626,3628],{"class":166,"line":3609},30,[164,3611,3033],{"class":451},[164,3613,718],{"class":207},[164,3615,41],{"class":211},[164,3617,3040],{"class":214},[164,3619,218],{"class":787},[164,3621,222],{"class":221},[164,3623,2392],{"class":225},[164,3625,222],{"class":221},[164,3627,1398],{"class":787},[164,3629,1516],{"class":211},[164,3631,3633,3635,3638,3640,3642,3644,3646,3648,3650,3652,3654,3656,3658,3660,3662,3664,3667,3669,3671,3673],{"class":166,"line":3632},31,[164,3634,3104],{"class":234},[164,3636,3637],{"class":1489}," toggle",[164,3639,1346],{"class":247},[164,3641,718],{"class":207},[164,3643,41],{"class":211},[164,3645,3063],{"class":214},[164,3647,218],{"class":787},[164,3649,222],{"class":221},[164,3651,2173],{"class":225},[164,3653,222],{"class":221},[164,3655,231],{"class":211},[164,3657,244],{"class":211},[164,3659,425],{"class":787},[164,3661,377],{"class":211},[164,3663,408],{"class":221},[164,3665,3666],{"class":225},"Show details",[164,3668,222],{"class":221},[164,3670,251],{"class":211},[164,3672,1398],{"class":787},[164,3674,1516],{"class":211},[164,3676,3678,3680,3683,3685,3687,3689,3691,3693,3695,3698,3700,3702,3704,3706,3708,3710,3712,3714,3716,3718],{"class":166,"line":3677},32,[164,3679,3104],{"class":234},[164,3681,3682],{"class":1489}," content",[164,3684,1346],{"class":247},[164,3686,718],{"class":207},[164,3688,41],{"class":211},[164,3690,3063],{"class":214},[164,3692,218],{"class":787},[164,3694,222],{"class":221},[164,3696,3697],{"class":225},"region",[164,3699,222],{"class":221},[164,3701,231],{"class":211},[164,3703,244],{"class":211},[164,3705,425],{"class":787},[164,3707,377],{"class":211},[164,3709,408],{"class":221},[164,3711,3666],{"class":225},[164,3713,222],{"class":221},[164,3715,251],{"class":211},[164,3717,1398],{"class":787},[164,3719,1516],{"class":211},[164,3721,3723],{"class":166,"line":3722},33,[164,3724,184],{"emptyLinePlaceholder":183},[164,3726,3728,3730,3732,3734,3737,3739,3741,3744,3746,3748,3750,3752,3754,3756,3759,3761,3763],{"class":166,"line":3727},34,[164,3729,3033],{"class":451},[164,3731,3135],{"class":214},[164,3733,218],{"class":787},[164,3735,3736],{"class":207},"toggle",[164,3738,1398],{"class":787},[164,3740,41],{"class":211},[164,3742,3743],{"class":214},"toHaveAttribute",[164,3745,218],{"class":787},[164,3747,222],{"class":221},[164,3749,2939],{"class":225},[164,3751,222],{"class":221},[164,3753,231],{"class":211},[164,3755,408],{"class":221},[164,3757,3758],{"class":225},"false",[164,3760,222],{"class":221},[164,3762,1398],{"class":787},[164,3764,1516],{"class":211},[164,3766,3768,3770,3772,3774,3777,3779,3781,3783,3785,3787,3789],{"class":166,"line":3767},35,[164,3769,3033],{"class":451},[164,3771,3135],{"class":214},[164,3773,218],{"class":787},[164,3775,3776],{"class":207},"content",[164,3778,1398],{"class":787},[164,3780,41],{"class":211},[164,3782,3382],{"class":207},[164,3784,41],{"class":211},[164,3786,3146],{"class":214},[164,3788,1441],{"class":787},[164,3790,1516],{"class":211},[164,3792,3794],{"class":166,"line":3793},36,[164,3795,184],{"emptyLinePlaceholder":183},[164,3797,3799,3801,3803,3805,3807,3809,3811,3814,3816,3818],{"class":166,"line":3798},37,[164,3800,3033],{"class":451},[164,3802,3637],{"class":207},[164,3804,41],{"class":211},[164,3806,3218],{"class":214},[164,3808,218],{"class":787},[164,3810,222],{"class":221},[164,3812,3813],{"class":225},"Enter",[164,3815,222],{"class":221},[164,3817,1398],{"class":787},[164,3819,1516],{"class":211},[164,3821,3823,3825,3827,3829,3831,3833,3835,3837,3839,3841,3843,3845,3847,3849,3851,3853,3855],{"class":166,"line":3822},38,[164,3824,3033],{"class":451},[164,3826,3135],{"class":214},[164,3828,218],{"class":787},[164,3830,3736],{"class":207},[164,3832,1398],{"class":787},[164,3834,41],{"class":211},[164,3836,3743],{"class":214},[164,3838,218],{"class":787},[164,3840,222],{"class":221},[164,3842,2939],{"class":225},[164,3844,222],{"class":221},[164,3846,231],{"class":211},[164,3848,408],{"class":221},[164,3850,2204],{"class":225},[164,3852,222],{"class":221},[164,3854,1398],{"class":787},[164,3856,1516],{"class":211},[164,3858,3860,3862,3864,3866,3868,3870,3872,3874,3876],{"class":166,"line":3859},39,[164,3861,3033],{"class":451},[164,3863,3135],{"class":214},[164,3865,218],{"class":787},[164,3867,3776],{"class":207},[164,3869,1398],{"class":787},[164,3871,41],{"class":211},[164,3873,3146],{"class":214},[164,3875,1441],{"class":787},[164,3877,1516],{"class":211},[164,3879,3881],{"class":166,"line":3880},40,[164,3882,184],{"emptyLinePlaceholder":183},[164,3884,3886,3888,3890,3892,3894,3896,3898,3901,3903,3905],{"class":166,"line":3885},41,[164,3887,3033],{"class":451},[164,3889,3637],{"class":207},[164,3891,41],{"class":211},[164,3893,3218],{"class":214},[164,3895,218],{"class":787},[164,3897,222],{"class":221},[164,3899,3900],{"class":225},"Space",[164,3902,222],{"class":221},[164,3904,1398],{"class":787},[164,3906,1516],{"class":211},[164,3908,3910,3912,3914,3916,3918,3920,3922,3924,3926,3928,3930,3932,3934,3936,3938,3940,3942],{"class":166,"line":3909},42,[164,3911,3033],{"class":451},[164,3913,3135],{"class":214},[164,3915,218],{"class":787},[164,3917,3736],{"class":207},[164,3919,1398],{"class":787},[164,3921,41],{"class":211},[164,3923,3743],{"class":214},[164,3925,218],{"class":787},[164,3927,222],{"class":221},[164,3929,2939],{"class":225},[164,3931,222],{"class":221},[164,3933,231],{"class":211},[164,3935,408],{"class":221},[164,3937,3758],{"class":225},[164,3939,222],{"class":221},[164,3941,1398],{"class":787},[164,3943,1516],{"class":211},[164,3945,3947,3949,3951,3953,3955,3957,3959,3961,3963,3965,3967],{"class":166,"line":3946},43,[164,3948,3033],{"class":451},[164,3950,3135],{"class":214},[164,3952,218],{"class":787},[164,3954,3776],{"class":207},[164,3956,1398],{"class":787},[164,3958,41],{"class":211},[164,3960,3382],{"class":207},[164,3962,41],{"class":211},[164,3964,3146],{"class":214},[164,3966,1441],{"class":787},[164,3968,1516],{"class":211},[164,3970,3972,3974,3976],{"class":166,"line":3971},44,[164,3973,545],{"class":211},[164,3975,1398],{"class":207},[164,3977,1516],{"class":211},[27,3979],{},[30,3981,3983],{"id":3982},"writing-smarter-playwright-tests-that-go-beyond-a-lighthouse-report","Writing Smarter Playwright Tests That Go Beyond a Lighthouse Report",[11,3985,3986,3987,3990],{},"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 ",[116,3988,3989],{},"AxeBuilder"," scan is the starting point, not the finish line.",[11,3992,3993],{},"These test patterns cover defects a default scan misses.",[46,3995,3997],{"id":3996},"default-axe-scan-your-baseline","Default axe scan (your baseline)",[154,3999,4001],{"className":156,"code":4000,"filename":2999,"language":159,"meta":160,"style":160},"import { test, expect } from '@playwright/test';\nimport AxeBuilder from '@axe-core/playwright';\n\ntest('accessibility scan', async ({ page }) => {\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  // Ensure the default page state has no violations\n  expect(results.violations).toEqual([]);\n});\n",[116,4002,4003,4031,4049,4053,4080,4102,4139,4144,4171],{"__ignoreMap":160},[164,4004,4005,4008,4010,4013,4015,4017,4019,4022,4024,4027,4029],{"class":166,"line":167},[164,4006,4007],{"class":451},"import",[164,4009,244],{"class":211},[164,4011,4012],{"class":207}," test",[164,4014,231],{"class":211},[164,4016,3135],{"class":207},[164,4018,251],{"class":211},[164,4020,4021],{"class":451}," from",[164,4023,408],{"class":221},[164,4025,4026],{"class":225},"@playwright/test",[164,4028,222],{"class":221},[164,4030,1516],{"class":211},[164,4032,4033,4035,4038,4041,4043,4045,4047],{"class":166,"line":174},[164,4034,4007],{"class":451},[164,4036,4037],{"class":207}," AxeBuilder ",[164,4039,4040],{"class":451},"from",[164,4042,408],{"class":221},[164,4044,1674],{"class":225},[164,4046,222],{"class":221},[164,4048,1516],{"class":211},[164,4050,4051],{"class":166,"line":180},[164,4052,184],{"emptyLinePlaceholder":183},[164,4054,4055,4057,4059,4061,4064,4066,4068,4070,4072,4074,4076,4078],{"class":166,"line":187},[164,4056,208],{"class":214},[164,4058,218],{"class":207},[164,4060,222],{"class":221},[164,4062,4063],{"class":225},"accessibility scan",[164,4065,222],{"class":221},[164,4067,231],{"class":211},[164,4069,235],{"class":234},[164,4071,714],{"class":211},[164,4073,718],{"class":717},[164,4075,721],{"class":211},[164,4077,241],{"class":234},[164,4079,535],{"class":211},[164,4081,4082,4084,4086,4088,4090,4092,4094,4096,4098,4100],{"class":166,"line":193},[164,4083,3033],{"class":451},[164,4085,718],{"class":207},[164,4087,41],{"class":211},[164,4089,3040],{"class":214},[164,4091,218],{"class":787},[164,4093,222],{"class":221},[164,4095,2392],{"class":225},[164,4097,222],{"class":221},[164,4099,1398],{"class":787},[164,4101,1516],{"class":211},[164,4103,4104,4106,4109,4111,4113,4116,4119,4121,4124,4126,4128,4130,4132,4135,4137],{"class":166,"line":198},[164,4105,3104],{"class":234},[164,4107,4108],{"class":1489}," results",[164,4110,1346],{"class":247},[164,4112,1495],{"class":451},[164,4114,4115],{"class":247}," new",[164,4117,4118],{"class":214}," AxeBuilder",[164,4120,218],{"class":787},[164,4122,4123],{"class":211},"{",[164,4125,718],{"class":207},[164,4127,251],{"class":211},[164,4129,1398],{"class":787},[164,4131,41],{"class":211},[164,4133,4134],{"class":214},"analyze",[164,4136,1441],{"class":787},[164,4138,1516],{"class":211},[164,4140,4141],{"class":166,"line":204},[164,4142,4143],{"class":170},"  // Ensure the default page state has no violations\n",[164,4145,4146,4149,4151,4154,4156,4159,4161,4163,4166,4169],{"class":166,"line":443},[164,4147,4148],{"class":214},"  expect",[164,4150,218],{"class":787},[164,4152,4153],{"class":207},"results",[164,4155,41],{"class":211},[164,4157,4158],{"class":207},"violations",[164,4160,1398],{"class":787},[164,4162,41],{"class":211},[164,4164,4165],{"class":214},"toEqual",[164,4167,4168],{"class":787},"([])",[164,4170,1516],{"class":211},[164,4172,4173,4175,4177],{"class":166,"line":455},[164,4174,545],{"class":211},[164,4176,1398],{"class":207},[164,4178,1516],{"class":211},[46,4180,4182],{"id":4181},"hover-state-contrast","Hover state contrast",[11,4184,4185],{},"A standard scan misses contrast issues that only appear on hover. Trigger the state first, then scan.",[154,4187,4189],{"className":156,"code":4188,"filename":2999,"language":159,"meta":160,"style":160},"test('accessibility scan on hovered button', async ({ page }) => {\n  await page.goto('/');\n  // Trigger hover state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).hover();\n  const results = await new AxeBuilder({ page }).analyze();\n  // Verify with the button hovered we still have no violations present\n  expect(results.violations).toEqual([]);\n});\n",[116,4190,4191,4218,4240,4245,4291,4323,4328,4350],{"__ignoreMap":160},[164,4192,4193,4195,4197,4199,4202,4204,4206,4208,4210,4212,4214,4216],{"class":166,"line":167},[164,4194,208],{"class":214},[164,4196,218],{"class":207},[164,4198,222],{"class":221},[164,4200,4201],{"class":225},"accessibility scan on hovered button",[164,4203,222],{"class":221},[164,4205,231],{"class":211},[164,4207,235],{"class":234},[164,4209,714],{"class":211},[164,4211,718],{"class":717},[164,4213,721],{"class":211},[164,4215,241],{"class":234},[164,4217,535],{"class":211},[164,4219,4220,4222,4224,4226,4228,4230,4232,4234,4236,4238],{"class":166,"line":174},[164,4221,3033],{"class":451},[164,4223,718],{"class":207},[164,4225,41],{"class":211},[164,4227,3040],{"class":214},[164,4229,218],{"class":787},[164,4231,222],{"class":221},[164,4233,2392],{"class":225},[164,4235,222],{"class":221},[164,4237,1398],{"class":787},[164,4239,1516],{"class":211},[164,4241,4242],{"class":166,"line":180},[164,4243,4244],{"class":170},"  // Trigger hover state on the button\n",[164,4246,4247,4249,4251,4253,4255,4257,4259,4261,4263,4265,4267,4269,4271,4273,4276,4278,4280,4282,4284,4287,4289],{"class":166,"line":187},[164,4248,3033],{"class":451},[164,4250,718],{"class":207},[164,4252,41],{"class":211},[164,4254,3063],{"class":214},[164,4256,218],{"class":787},[164,4258,222],{"class":221},[164,4260,2173],{"class":225},[164,4262,222],{"class":221},[164,4264,231],{"class":211},[164,4266,244],{"class":211},[164,4268,425],{"class":787},[164,4270,377],{"class":211},[164,4272,408],{"class":221},[164,4274,4275],{"class":225},"Subscribe",[164,4277,222],{"class":221},[164,4279,251],{"class":211},[164,4281,1398],{"class":787},[164,4283,41],{"class":211},[164,4285,4286],{"class":214},"hover",[164,4288,1441],{"class":787},[164,4290,1516],{"class":211},[164,4292,4293,4295,4297,4299,4301,4303,4305,4307,4309,4311,4313,4315,4317,4319,4321],{"class":166,"line":193},[164,4294,3104],{"class":234},[164,4296,4108],{"class":1489},[164,4298,1346],{"class":247},[164,4300,1495],{"class":451},[164,4302,4115],{"class":247},[164,4304,4118],{"class":214},[164,4306,218],{"class":787},[164,4308,4123],{"class":211},[164,4310,718],{"class":207},[164,4312,251],{"class":211},[164,4314,1398],{"class":787},[164,4316,41],{"class":211},[164,4318,4134],{"class":214},[164,4320,1441],{"class":787},[164,4322,1516],{"class":211},[164,4324,4325],{"class":166,"line":198},[164,4326,4327],{"class":170},"  // Verify with the button hovered we still have no violations present\n",[164,4329,4330,4332,4334,4336,4338,4340,4342,4344,4346,4348],{"class":166,"line":204},[164,4331,4148],{"class":214},[164,4333,218],{"class":787},[164,4335,4153],{"class":207},[164,4337,41],{"class":211},[164,4339,4158],{"class":207},[164,4341,1398],{"class":787},[164,4343,41],{"class":211},[164,4345,4165],{"class":214},[164,4347,4168],{"class":787},[164,4349,1516],{"class":211},[164,4351,4352,4354,4356],{"class":166,"line":443},[164,4353,545],{"class":211},[164,4355,1398],{"class":207},[164,4357,1516],{"class":211},[46,4359,4361],{"id":4360},"focus-state-contrast","Focus state contrast",[11,4363,4364],{},"Same principle — trigger focus before scanning so axe evaluates the focused styles.",[154,4366,4368],{"className":156,"code":4367,"filename":2999,"language":159,"meta":160,"style":160},"test('accessibility scan on focused button', async ({ page }) => {\n  await page.goto('/');\n  // Trigger focus state on the button\n  await page.getByRole('button', { name: 'Subscribe' }).focus();\n  const results = await new AxeBuilder({ page }).analyze();\n  // Except no violations\n  expect(results.violations).toEqual([]);\n});\n",[116,4369,4370,4397,4419,4424,4469,4501,4506,4528],{"__ignoreMap":160},[164,4371,4372,4374,4376,4378,4381,4383,4385,4387,4389,4391,4393,4395],{"class":166,"line":167},[164,4373,208],{"class":214},[164,4375,218],{"class":207},[164,4377,222],{"class":221},[164,4379,4380],{"class":225},"accessibility scan on focused button",[164,4382,222],{"class":221},[164,4384,231],{"class":211},[164,4386,235],{"class":234},[164,4388,714],{"class":211},[164,4390,718],{"class":717},[164,4392,721],{"class":211},[164,4394,241],{"class":234},[164,4396,535],{"class":211},[164,4398,4399,4401,4403,4405,4407,4409,4411,4413,4415,4417],{"class":166,"line":174},[164,4400,3033],{"class":451},[164,4402,718],{"class":207},[164,4404,41],{"class":211},[164,4406,3040],{"class":214},[164,4408,218],{"class":787},[164,4410,222],{"class":221},[164,4412,2392],{"class":225},[164,4414,222],{"class":221},[164,4416,1398],{"class":787},[164,4418,1516],{"class":211},[164,4420,4421],{"class":166,"line":180},[164,4422,4423],{"class":170},"  // Trigger focus state on the button\n",[164,4425,4426,4428,4430,4432,4434,4436,4438,4440,4442,4444,4446,4448,4450,4452,4454,4456,4458,4460,4462,4465,4467],{"class":166,"line":187},[164,4427,3033],{"class":451},[164,4429,718],{"class":207},[164,4431,41],{"class":211},[164,4433,3063],{"class":214},[164,4435,218],{"class":787},[164,4437,222],{"class":221},[164,4439,2173],{"class":225},[164,4441,222],{"class":221},[164,4443,231],{"class":211},[164,4445,244],{"class":211},[164,4447,425],{"class":787},[164,4449,377],{"class":211},[164,4451,408],{"class":221},[164,4453,4275],{"class":225},[164,4455,222],{"class":221},[164,4457,251],{"class":211},[164,4459,1398],{"class":787},[164,4461,41],{"class":211},[164,4463,4464],{"class":214},"focus",[164,4466,1441],{"class":787},[164,4468,1516],{"class":211},[164,4470,4471,4473,4475,4477,4479,4481,4483,4485,4487,4489,4491,4493,4495,4497,4499],{"class":166,"line":193},[164,4472,3104],{"class":234},[164,4474,4108],{"class":1489},[164,4476,1346],{"class":247},[164,4478,1495],{"class":451},[164,4480,4115],{"class":247},[164,4482,4118],{"class":214},[164,4484,218],{"class":787},[164,4486,4123],{"class":211},[164,4488,718],{"class":207},[164,4490,251],{"class":211},[164,4492,1398],{"class":787},[164,4494,41],{"class":211},[164,4496,4134],{"class":214},[164,4498,1441],{"class":787},[164,4500,1516],{"class":211},[164,4502,4503],{"class":166,"line":198},[164,4504,4505],{"class":170},"  // Except no violations\n",[164,4507,4508,4510,4512,4514,4516,4518,4520,4522,4524,4526],{"class":166,"line":204},[164,4509,4148],{"class":214},[164,4511,218],{"class":787},[164,4513,4153],{"class":207},[164,4515,41],{"class":211},[164,4517,4158],{"class":207},[164,4519,1398],{"class":787},[164,4521,41],{"class":211},[164,4523,4165],{"class":214},[164,4525,4168],{"class":787},[164,4527,1516],{"class":211},[164,4529,4530,4532,4534],{"class":166,"line":443},[164,4531,545],{"class":211},[164,4533,1398],{"class":207},[164,4535,1516],{"class":211},[46,4537,4539],{"id":4538},"dark-mode-contrast","Dark mode contrast",[11,4541,4542],{},"Contrast issues may appear on light mode vs dark mode or vice-versa.",[154,4544,4546],{"className":156,"code":4545,"filename":2999,"language":159,"meta":160,"style":160},"test('accessibility scan in dark mode', async ({ page }) => {\n  await page.emulateMedia({ colorScheme: 'dark' });\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n\ntest('accessibility scan in light mode', async ({ page }) => {\n  await page.emulateMedia({ colorScheme: 'light' });\n  await page.goto('/');\n  const results = await new AxeBuilder({ page }).analyze();\n  expect(results.violations).toEqual([]);\n});\n",[116,4547,4548,4575,4608,4630,4662,4684,4692,4696,4723,4754,4776,4808,4830],{"__ignoreMap":160},[164,4549,4550,4552,4554,4556,4559,4561,4563,4565,4567,4569,4571,4573],{"class":166,"line":167},[164,4551,208],{"class":214},[164,4553,218],{"class":207},[164,4555,222],{"class":221},[164,4557,4558],{"class":225},"accessibility scan in dark mode",[164,4560,222],{"class":221},[164,4562,231],{"class":211},[164,4564,235],{"class":234},[164,4566,714],{"class":211},[164,4568,718],{"class":717},[164,4570,721],{"class":211},[164,4572,241],{"class":234},[164,4574,535],{"class":211},[164,4576,4577,4579,4581,4583,4586,4588,4590,4593,4595,4597,4600,4602,4604,4606],{"class":166,"line":174},[164,4578,3033],{"class":451},[164,4580,718],{"class":207},[164,4582,41],{"class":211},[164,4584,4585],{"class":214},"emulateMedia",[164,4587,218],{"class":787},[164,4589,4123],{"class":211},[164,4591,4592],{"class":787}," colorScheme",[164,4594,377],{"class":211},[164,4596,408],{"class":221},[164,4598,4599],{"class":225},"dark",[164,4601,222],{"class":221},[164,4603,251],{"class":211},[164,4605,1398],{"class":787},[164,4607,1516],{"class":211},[164,4609,4610,4612,4614,4616,4618,4620,4622,4624,4626,4628],{"class":166,"line":180},[164,4611,3033],{"class":451},[164,4613,718],{"class":207},[164,4615,41],{"class":211},[164,4617,3040],{"class":214},[164,4619,218],{"class":787},[164,4621,222],{"class":221},[164,4623,2392],{"class":225},[164,4625,222],{"class":221},[164,4627,1398],{"class":787},[164,4629,1516],{"class":211},[164,4631,4632,4634,4636,4638,4640,4642,4644,4646,4648,4650,4652,4654,4656,4658,4660],{"class":166,"line":187},[164,4633,3104],{"class":234},[164,4635,4108],{"class":1489},[164,4637,1346],{"class":247},[164,4639,1495],{"class":451},[164,4641,4115],{"class":247},[164,4643,4118],{"class":214},[164,4645,218],{"class":787},[164,4647,4123],{"class":211},[164,4649,718],{"class":207},[164,4651,251],{"class":211},[164,4653,1398],{"class":787},[164,4655,41],{"class":211},[164,4657,4134],{"class":214},[164,4659,1441],{"class":787},[164,4661,1516],{"class":211},[164,4663,4664,4666,4668,4670,4672,4674,4676,4678,4680,4682],{"class":166,"line":193},[164,4665,4148],{"class":214},[164,4667,218],{"class":787},[164,4669,4153],{"class":207},[164,4671,41],{"class":211},[164,4673,4158],{"class":207},[164,4675,1398],{"class":787},[164,4677,41],{"class":211},[164,4679,4165],{"class":214},[164,4681,4168],{"class":787},[164,4683,1516],{"class":211},[164,4685,4686,4688,4690],{"class":166,"line":198},[164,4687,545],{"class":211},[164,4689,1398],{"class":207},[164,4691,1516],{"class":211},[164,4693,4694],{"class":166,"line":204},[164,4695,184],{"emptyLinePlaceholder":183},[164,4697,4698,4700,4702,4704,4707,4709,4711,4713,4715,4717,4719,4721],{"class":166,"line":443},[164,4699,208],{"class":214},[164,4701,218],{"class":207},[164,4703,222],{"class":221},[164,4705,4706],{"class":225},"accessibility scan in light mode",[164,4708,222],{"class":221},[164,4710,231],{"class":211},[164,4712,235],{"class":234},[164,4714,714],{"class":211},[164,4716,718],{"class":717},[164,4718,721],{"class":211},[164,4720,241],{"class":234},[164,4722,535],{"class":211},[164,4724,4725,4727,4729,4731,4733,4735,4737,4739,4741,4743,4746,4748,4750,4752],{"class":166,"line":455},[164,4726,3033],{"class":451},[164,4728,718],{"class":207},[164,4730,41],{"class":211},[164,4732,4585],{"class":214},[164,4734,218],{"class":787},[164,4736,4123],{"class":211},[164,4738,4592],{"class":787},[164,4740,377],{"class":211},[164,4742,408],{"class":221},[164,4744,4745],{"class":225},"light",[164,4747,222],{"class":221},[164,4749,251],{"class":211},[164,4751,1398],{"class":787},[164,4753,1516],{"class":211},[164,4755,4756,4758,4760,4762,4764,4766,4768,4770,4772,4774],{"class":166,"line":461},[164,4757,3033],{"class":451},[164,4759,718],{"class":207},[164,4761,41],{"class":211},[164,4763,3040],{"class":214},[164,4765,218],{"class":787},[164,4767,222],{"class":221},[164,4769,2392],{"class":225},[164,4771,222],{"class":221},[164,4773,1398],{"class":787},[164,4775,1516],{"class":211},[164,4777,4778,4780,4782,4784,4786,4788,4790,4792,4794,4796,4798,4800,4802,4804,4806],{"class":166,"line":627},[164,4779,3104],{"class":234},[164,4781,4108],{"class":1489},[164,4783,1346],{"class":247},[164,4785,1495],{"class":451},[164,4787,4115],{"class":247},[164,4789,4118],{"class":214},[164,4791,218],{"class":787},[164,4793,4123],{"class":211},[164,4795,718],{"class":207},[164,4797,251],{"class":211},[164,4799,1398],{"class":787},[164,4801,41],{"class":211},[164,4803,4134],{"class":214},[164,4805,1441],{"class":787},[164,4807,1516],{"class":211},[164,4809,4810,4812,4814,4816,4818,4820,4822,4824,4826,4828],{"class":166,"line":829},[164,4811,4148],{"class":214},[164,4813,218],{"class":787},[164,4815,4153],{"class":207},[164,4817,41],{"class":211},[164,4819,4158],{"class":207},[164,4821,1398],{"class":787},[164,4823,41],{"class":211},[164,4825,4165],{"class":214},[164,4827,4168],{"class":787},[164,4829,1516],{"class":211},[164,4831,4832,4834,4836],{"class":166,"line":835},[164,4833,545],{"class":211},[164,4835,1398],{"class":207},[164,4837,1516],{"class":211},[46,4839,4841],{"id":4840},"skip-link-first-focusable-element","Skip link — first focusable element",[11,4843,4844],{},"Verify the skip link exists, is the first element reached by keyboard, and actually moves focus to the main content when activated.",[154,4846,4848],{"className":156,"code":4847,"filename":2999,"language":159,"meta":160,"style":160},"test('first focusable element is a skip link that navigates to main content', async ({ page }) => {\n  await page.goto('/');\n  await page.keyboard.press('Tab');\n  const focused = page.locator(':focus');\n  await expect(focused).toHaveAttribute('href', '#main-content');\n  await expect(focused).toHaveText('Skip to main content');\n\n  // Activate the skip link and verify it navigated to main content\n  await page.keyboard.press('Enter');\n  await expect(page).toHaveURL(/#main-content$/);\n});\n",[116,4849,4850,4877,4899,4925,4954,4992,5021,5025,5030,5056,5089],{"__ignoreMap":160},[164,4851,4852,4854,4856,4858,4861,4863,4865,4867,4869,4871,4873,4875],{"class":166,"line":167},[164,4853,208],{"class":214},[164,4855,218],{"class":207},[164,4857,222],{"class":221},[164,4859,4860],{"class":225},"first focusable element is a skip link that navigates to main content",[164,4862,222],{"class":221},[164,4864,231],{"class":211},[164,4866,235],{"class":234},[164,4868,714],{"class":211},[164,4870,718],{"class":717},[164,4872,721],{"class":211},[164,4874,241],{"class":234},[164,4876,535],{"class":211},[164,4878,4879,4881,4883,4885,4887,4889,4891,4893,4895,4897],{"class":166,"line":174},[164,4880,3033],{"class":451},[164,4882,718],{"class":207},[164,4884,41],{"class":211},[164,4886,3040],{"class":214},[164,4888,218],{"class":787},[164,4890,222],{"class":221},[164,4892,2392],{"class":225},[164,4894,222],{"class":221},[164,4896,1398],{"class":787},[164,4898,1516],{"class":211},[164,4900,4901,4903,4905,4907,4909,4911,4913,4915,4917,4919,4921,4923],{"class":166,"line":180},[164,4902,3033],{"class":451},[164,4904,718],{"class":207},[164,4906,41],{"class":211},[164,4908,3213],{"class":207},[164,4910,41],{"class":211},[164,4912,3218],{"class":214},[164,4914,218],{"class":787},[164,4916,222],{"class":221},[164,4918,3225],{"class":225},[164,4920,222],{"class":221},[164,4922,1398],{"class":787},[164,4924,1516],{"class":211},[164,4926,4927,4929,4932,4934,4936,4938,4941,4943,4945,4948,4950,4952],{"class":166,"line":187},[164,4928,3104],{"class":234},[164,4930,4931],{"class":1489}," focused",[164,4933,1346],{"class":247},[164,4935,718],{"class":207},[164,4937,41],{"class":211},[164,4939,4940],{"class":214},"locator",[164,4942,218],{"class":787},[164,4944,222],{"class":221},[164,4946,4947],{"class":225},":focus",[164,4949,222],{"class":221},[164,4951,1398],{"class":787},[164,4953,1516],{"class":211},[164,4955,4956,4958,4960,4962,4965,4967,4969,4971,4973,4975,4978,4980,4982,4984,4986,4988,4990],{"class":166,"line":193},[164,4957,3033],{"class":451},[164,4959,3135],{"class":214},[164,4961,218],{"class":787},[164,4963,4964],{"class":207},"focused",[164,4966,1398],{"class":787},[164,4968,41],{"class":211},[164,4970,3743],{"class":214},[164,4972,218],{"class":787},[164,4974,222],{"class":221},[164,4976,4977],{"class":225},"href",[164,4979,222],{"class":221},[164,4981,231],{"class":211},[164,4983,408],{"class":221},[164,4985,1928],{"class":225},[164,4987,222],{"class":221},[164,4989,1398],{"class":787},[164,4991,1516],{"class":211},[164,4993,4994,4996,4998,5000,5002,5004,5006,5009,5011,5013,5015,5017,5019],{"class":166,"line":198},[164,4995,3033],{"class":451},[164,4997,3135],{"class":214},[164,4999,218],{"class":787},[164,5001,4964],{"class":207},[164,5003,1398],{"class":787},[164,5005,41],{"class":211},[164,5007,5008],{"class":214},"toHaveText",[164,5010,218],{"class":787},[164,5012,222],{"class":221},[164,5014,1998],{"class":225},[164,5016,222],{"class":221},[164,5018,1398],{"class":787},[164,5020,1516],{"class":211},[164,5022,5023],{"class":166,"line":204},[164,5024,184],{"emptyLinePlaceholder":183},[164,5026,5027],{"class":166,"line":443},[164,5028,5029],{"class":170},"  // Activate the skip link and verify it navigated to main content\n",[164,5031,5032,5034,5036,5038,5040,5042,5044,5046,5048,5050,5052,5054],{"class":166,"line":455},[164,5033,3033],{"class":451},[164,5035,718],{"class":207},[164,5037,41],{"class":211},[164,5039,3213],{"class":207},[164,5041,41],{"class":211},[164,5043,3218],{"class":214},[164,5045,218],{"class":787},[164,5047,222],{"class":221},[164,5049,3813],{"class":225},[164,5051,222],{"class":221},[164,5053,1398],{"class":787},[164,5055,1516],{"class":211},[164,5057,5058,5060,5062,5064,5067,5069,5071,5074,5076,5078,5080,5083,5085,5087],{"class":166,"line":461},[164,5059,3033],{"class":451},[164,5061,3135],{"class":214},[164,5063,218],{"class":787},[164,5065,5066],{"class":207},"page",[164,5068,1398],{"class":787},[164,5070,41],{"class":211},[164,5072,5073],{"class":214},"toHaveURL",[164,5075,218],{"class":787},[164,5077,2392],{"class":221},[164,5079,1928],{"class":225},[164,5081,5082],{"class":451},"$",[164,5084,2392],{"class":221},[164,5086,1398],{"class":787},[164,5088,1516],{"class":211},[164,5090,5091,5093,5095],{"class":166,"line":627},[164,5092,545],{"class":211},[164,5094,1398],{"class":207},[164,5096,1516],{"class":211},[46,5098,5100],{"id":5099},"unique-landmark-labels","Unique landmark labels",[11,5102,5103],{},"Catch duplicate landmark names (best practive to have programmatically unique landmarks)",[154,5105,5107],{"className":156,"code":5106,"filename":2999,"language":159,"meta":160,"style":160},"test('all nav landmarks have unique accessible names', async ({ page }) => {\n  await page.goto('/');\n  const navs = await page.getByRole('navigation').all();\n  const labels = await Promise.all(\n    navs.map((nav) => nav.getAttribute('aria-label')),\n  );\n\n  // Verify every nav has a label\n  labels.forEach((label, index) => {\n    expect(label, `Navigation element ${index + 1} is missing an aria-label`).not.toBeNull();\n  });\n\n  // Verify all labels are unique\n  const uniqueLabels = new Set(labels);\n  expect(uniqueLabels.size).toBe(labels.length);\n});\n",[116,5108,5109,5136,5158,5194,5215,5257,5264,5268,5273,5301,5352,5360,5364,5369,5392,5425],{"__ignoreMap":160},[164,5110,5111,5113,5115,5117,5120,5122,5124,5126,5128,5130,5132,5134],{"class":166,"line":167},[164,5112,208],{"class":214},[164,5114,218],{"class":207},[164,5116,222],{"class":221},[164,5118,5119],{"class":225},"all nav landmarks have unique accessible names",[164,5121,222],{"class":221},[164,5123,231],{"class":211},[164,5125,235],{"class":234},[164,5127,714],{"class":211},[164,5129,718],{"class":717},[164,5131,721],{"class":211},[164,5133,241],{"class":234},[164,5135,535],{"class":211},[164,5137,5138,5140,5142,5144,5146,5148,5150,5152,5154,5156],{"class":166,"line":174},[164,5139,3033],{"class":451},[164,5141,718],{"class":207},[164,5143,41],{"class":211},[164,5145,3040],{"class":214},[164,5147,218],{"class":787},[164,5149,222],{"class":221},[164,5151,2392],{"class":225},[164,5153,222],{"class":221},[164,5155,1398],{"class":787},[164,5157,1516],{"class":211},[164,5159,5160,5162,5165,5167,5169,5171,5173,5175,5177,5179,5181,5183,5185,5187,5190,5192],{"class":166,"line":180},[164,5161,3104],{"class":234},[164,5163,5164],{"class":1489}," navs",[164,5166,1346],{"class":247},[164,5168,1495],{"class":451},[164,5170,718],{"class":207},[164,5172,41],{"class":211},[164,5174,3063],{"class":214},[164,5176,218],{"class":787},[164,5178,222],{"class":221},[164,5180,2807],{"class":225},[164,5182,222],{"class":221},[164,5184,1398],{"class":787},[164,5186,41],{"class":211},[164,5188,5189],{"class":214},"all",[164,5191,1441],{"class":787},[164,5193,1516],{"class":211},[164,5195,5196,5198,5201,5203,5205,5208,5210,5212],{"class":166,"line":187},[164,5197,3104],{"class":234},[164,5199,5200],{"class":1489}," labels",[164,5202,1346],{"class":247},[164,5204,1495],{"class":451},[164,5206,5207],{"class":2648}," Promise",[164,5209,41],{"class":211},[164,5211,5189],{"class":214},[164,5213,5214],{"class":787},"(\n",[164,5216,5217,5220,5222,5225,5227,5229,5232,5234,5236,5239,5241,5244,5246,5248,5250,5252,5255],{"class":166,"line":193},[164,5218,5219],{"class":207},"    navs",[164,5221,41],{"class":211},[164,5223,5224],{"class":214},"map",[164,5226,218],{"class":787},[164,5228,218],{"class":211},[164,5230,5231],{"class":717},"nav",[164,5233,1398],{"class":211},[164,5235,241],{"class":234},[164,5237,5238],{"class":207}," nav",[164,5240,41],{"class":211},[164,5242,5243],{"class":214},"getAttribute",[164,5245,218],{"class":787},[164,5247,222],{"class":221},[164,5249,1822],{"class":225},[164,5251,222],{"class":221},[164,5253,5254],{"class":787},"))",[164,5256,3292],{"class":211},[164,5258,5259,5262],{"class":166,"line":198},[164,5260,5261],{"class":787},"  )",[164,5263,1516],{"class":211},[164,5265,5266],{"class":166,"line":204},[164,5267,184],{"emptyLinePlaceholder":183},[164,5269,5270],{"class":166,"line":443},[164,5271,5272],{"class":170},"  // Verify every nav has a label\n",[164,5274,5275,5278,5280,5283,5285,5287,5290,5292,5295,5297,5299],{"class":166,"line":455},[164,5276,5277],{"class":207},"  labels",[164,5279,41],{"class":211},[164,5281,5282],{"class":214},"forEach",[164,5284,218],{"class":787},[164,5286,218],{"class":211},[164,5288,5289],{"class":717},"label",[164,5291,231],{"class":211},[164,5293,5294],{"class":717}," index",[164,5296,1398],{"class":211},[164,5298,241],{"class":234},[164,5300,535],{"class":211},[164,5302,5303,5305,5307,5309,5311,5314,5317,5320,5323,5326,5329,5331,5334,5337,5339,5341,5343,5345,5348,5350],{"class":166,"line":461},[164,5304,3304],{"class":214},[164,5306,218],{"class":787},[164,5308,5289],{"class":207},[164,5310,231],{"class":211},[164,5312,5313],{"class":221}," `",[164,5315,5316],{"class":225},"Navigation element ",[164,5318,5319],{"class":221},"${",[164,5321,5322],{"class":207},"index",[164,5324,5325],{"class":247}," +",[164,5327,5328],{"class":3177}," 1",[164,5330,545],{"class":221},[164,5332,5333],{"class":225}," is missing an aria-label",[164,5335,5336],{"class":221},"`",[164,5338,1398],{"class":787},[164,5340,41],{"class":211},[164,5342,3382],{"class":207},[164,5344,41],{"class":211},[164,5346,5347],{"class":214},"toBeNull",[164,5349,1441],{"class":787},[164,5351,1516],{"class":211},[164,5353,5354,5356,5358],{"class":166,"line":627},[164,5355,1551],{"class":211},[164,5357,1398],{"class":787},[164,5359,1516],{"class":211},[164,5361,5362],{"class":166,"line":829},[164,5363,184],{"emptyLinePlaceholder":183},[164,5365,5366],{"class":166,"line":835},[164,5367,5368],{"class":170},"  // Verify all labels are unique\n",[164,5370,5371,5373,5376,5378,5380,5383,5385,5388,5390],{"class":166,"line":841},[164,5372,3104],{"class":234},[164,5374,5375],{"class":1489}," uniqueLabels",[164,5377,1346],{"class":247},[164,5379,4115],{"class":247},[164,5381,5382],{"class":214}," Set",[164,5384,218],{"class":787},[164,5386,5387],{"class":207},"labels",[164,5389,1398],{"class":787},[164,5391,1516],{"class":211},[164,5393,5394,5396,5398,5401,5403,5406,5408,5410,5412,5414,5416,5418,5421,5423],{"class":166,"line":873},[164,5395,4148],{"class":214},[164,5397,218],{"class":787},[164,5399,5400],{"class":207},"uniqueLabels",[164,5402,41],{"class":211},[164,5404,5405],{"class":207},"size",[164,5407,1398],{"class":787},[164,5409,41],{"class":211},[164,5411,3316],{"class":214},[164,5413,218],{"class":787},[164,5415,5387],{"class":207},[164,5417,41],{"class":211},[164,5419,5420],{"class":1489},"length",[164,5422,1398],{"class":787},[164,5424,1516],{"class":211},[164,5426,5427,5429,5431],{"class":166,"line":878},[164,5428,545],{"class":211},[164,5430,1398],{"class":207},[164,5432,1516],{"class":211},[46,5434,5436],{"id":5435},"ambiguous-link-text","Ambiguous link text",[11,5438,5439],{},"Check for known offenders that pass structural validation but fail in context.",[154,5441,5443],{"className":156,"code":5442,"filename":2999,"language":159,"meta":160,"style":160},"test('no ambiguous link text', async ({ page }) => {\n  await page.goto('/');\n  const ambiguous = ['read more', 'click here', 'browse all', 'learn more', 'here', 'more'];\n  const links = await page.getByRole('link').all();\n  for (const link of links) {\n    const text = (await link.innerText()).toLowerCase().trim();\n    expect(ambiguous, `Ambiguous link text found: \"${text}\"`).not.toContain(text);\n  }\n});\n",[116,5444,5445,5472,5494,5563,5599,5620,5660,5705,5709],{"__ignoreMap":160},[164,5446,5447,5449,5451,5453,5456,5458,5460,5462,5464,5466,5468,5470],{"class":166,"line":167},[164,5448,208],{"class":214},[164,5450,218],{"class":207},[164,5452,222],{"class":221},[164,5454,5455],{"class":225},"no ambiguous link text",[164,5457,222],{"class":221},[164,5459,231],{"class":211},[164,5461,235],{"class":234},[164,5463,714],{"class":211},[164,5465,718],{"class":717},[164,5467,721],{"class":211},[164,5469,241],{"class":234},[164,5471,535],{"class":211},[164,5473,5474,5476,5478,5480,5482,5484,5486,5488,5490,5492],{"class":166,"line":174},[164,5475,3033],{"class":451},[164,5477,718],{"class":207},[164,5479,41],{"class":211},[164,5481,3040],{"class":214},[164,5483,218],{"class":787},[164,5485,222],{"class":221},[164,5487,2392],{"class":225},[164,5489,222],{"class":221},[164,5491,1398],{"class":787},[164,5493,1516],{"class":211},[164,5495,5496,5498,5501,5503,5506,5508,5511,5513,5515,5517,5520,5522,5524,5526,5529,5531,5533,5535,5538,5540,5542,5544,5547,5549,5551,5553,5556,5558,5561],{"class":166,"line":180},[164,5497,3104],{"class":234},[164,5499,5500],{"class":1489}," ambiguous",[164,5502,1346],{"class":247},[164,5504,5505],{"class":787}," [",[164,5507,222],{"class":221},[164,5509,5510],{"class":225},"read more",[164,5512,222],{"class":221},[164,5514,231],{"class":211},[164,5516,408],{"class":221},[164,5518,5519],{"class":225},"click here",[164,5521,222],{"class":221},[164,5523,231],{"class":211},[164,5525,408],{"class":221},[164,5527,5528],{"class":225},"browse all",[164,5530,222],{"class":221},[164,5532,231],{"class":211},[164,5534,408],{"class":221},[164,5536,5537],{"class":225},"learn more",[164,5539,222],{"class":221},[164,5541,231],{"class":211},[164,5543,408],{"class":221},[164,5545,5546],{"class":225},"here",[164,5548,222],{"class":221},[164,5550,231],{"class":211},[164,5552,408],{"class":221},[164,5554,5555],{"class":225},"more",[164,5557,222],{"class":221},[164,5559,5560],{"class":787},"]",[164,5562,1516],{"class":211},[164,5564,5565,5567,5570,5572,5574,5576,5578,5580,5582,5584,5587,5589,5591,5593,5595,5597],{"class":166,"line":187},[164,5566,3104],{"class":234},[164,5568,5569],{"class":1489}," links",[164,5571,1346],{"class":247},[164,5573,1495],{"class":451},[164,5575,718],{"class":207},[164,5577,41],{"class":211},[164,5579,3063],{"class":214},[164,5581,218],{"class":787},[164,5583,222],{"class":221},[164,5585,5586],{"class":225},"link",[164,5588,222],{"class":221},[164,5590,1398],{"class":787},[164,5592,41],{"class":211},[164,5594,5189],{"class":214},[164,5596,1441],{"class":787},[164,5598,1516],{"class":211},[164,5600,5601,5603,5605,5608,5611,5614,5616,5618],{"class":166,"line":193},[164,5602,3164],{"class":451},[164,5604,1392],{"class":787},[164,5606,5607],{"class":234},"const",[164,5609,5610],{"class":1489}," link",[164,5612,5613],{"class":247}," of",[164,5615,5569],{"class":207},[164,5617,3199],{"class":787},[164,5619,1211],{"class":211},[164,5621,5622,5624,5627,5629,5631,5634,5636,5638,5641,5644,5646,5649,5651,5653,5656,5658],{"class":166,"line":198},[164,5623,1486],{"class":234},[164,5625,5626],{"class":1489}," text",[164,5628,1346],{"class":247},[164,5630,1392],{"class":787},[164,5632,5633],{"class":451},"await",[164,5635,5610],{"class":207},[164,5637,41],{"class":211},[164,5639,5640],{"class":214},"innerText",[164,5642,5643],{"class":787},"())",[164,5645,41],{"class":211},[164,5647,5648],{"class":214},"toLowerCase",[164,5650,1441],{"class":787},[164,5652,41],{"class":211},[164,5654,5655],{"class":214},"trim",[164,5657,1441],{"class":787},[164,5659,1516],{"class":211},[164,5661,5662,5664,5666,5669,5671,5673,5676,5678,5680,5682,5684,5686,5688,5690,5692,5694,5697,5699,5701,5703],{"class":166,"line":204},[164,5663,3304],{"class":214},[164,5665,218],{"class":787},[164,5667,5668],{"class":207},"ambiguous",[164,5670,231],{"class":211},[164,5672,5313],{"class":221},[164,5674,5675],{"class":225},"Ambiguous link text found: \"",[164,5677,5319],{"class":221},[164,5679,268],{"class":207},[164,5681,545],{"class":221},[164,5683,1959],{"class":225},[164,5685,5336],{"class":221},[164,5687,1398],{"class":787},[164,5689,41],{"class":211},[164,5691,3382],{"class":207},[164,5693,41],{"class":211},[164,5695,5696],{"class":214},"toContain",[164,5698,218],{"class":787},[164,5700,268],{"class":207},[164,5702,1398],{"class":787},[164,5704,1516],{"class":211},[164,5706,5707],{"class":166,"line":443},[164,5708,1410],{"class":211},[164,5710,5711,5713,5715],{"class":166,"line":455},[164,5712,545],{"class":211},[164,5714,1398],{"class":207},[164,5716,1516],{"class":211},[46,5718,5720],{"id":5719},"dynamic-aria-label-on-stateful-controls","Dynamic aria-label on stateful controls",[11,5722,5723],{},"Verify that a toggle's accessible name reflects the action it will perform, not just that it changes after interaction. A color mode toggle in light mode should read \"Switch to dark mode\" — and once clicked, \"Switch to light mode.\" A label that says \"Switch to light mode\" when the page is already in light mode is wrong/misleading.",[154,5725,5728],{"className":156,"code":5726,"filename":5727,"language":159,"meta":160,"style":160},"test('color mode toggle label reflects current state', async ({ page }) => {\n  // Start in light mode — toggle should offer to switch to dark\n  await page.emulateMedia({ colorScheme: 'light' });\n  await page.goto('/');\n  const toggle = page.getByRole('button', { name: /switch to/i });\n  await expect(toggle).toHaveAttribute('aria-label', 'Switch to dark mode');\n\n  // After clicking, page is in dark mode — toggle should offer to switch to light\n  await toggle.click();\n  await expect(toggle).toHaveAttribute('aria-label', 'Switch to light mode');\n});\n","color-mode-toggle-label-test.spec.ts",[116,5729,5730,5757,5762,5792,5814,5862,5899,5903,5908,5922,5959],{"__ignoreMap":160},[164,5731,5732,5734,5736,5738,5741,5743,5745,5747,5749,5751,5753,5755],{"class":166,"line":167},[164,5733,208],{"class":214},[164,5735,218],{"class":207},[164,5737,222],{"class":221},[164,5739,5740],{"class":225},"color mode toggle label reflects current state",[164,5742,222],{"class":221},[164,5744,231],{"class":211},[164,5746,235],{"class":234},[164,5748,714],{"class":211},[164,5750,718],{"class":717},[164,5752,721],{"class":211},[164,5754,241],{"class":234},[164,5756,535],{"class":211},[164,5758,5759],{"class":166,"line":174},[164,5760,5761],{"class":170},"  // Start in light mode — toggle should offer to switch to dark\n",[164,5763,5764,5766,5768,5770,5772,5774,5776,5778,5780,5782,5784,5786,5788,5790],{"class":166,"line":180},[164,5765,3033],{"class":451},[164,5767,718],{"class":207},[164,5769,41],{"class":211},[164,5771,4585],{"class":214},[164,5773,218],{"class":787},[164,5775,4123],{"class":211},[164,5777,4592],{"class":787},[164,5779,377],{"class":211},[164,5781,408],{"class":221},[164,5783,4745],{"class":225},[164,5785,222],{"class":221},[164,5787,251],{"class":211},[164,5789,1398],{"class":787},[164,5791,1516],{"class":211},[164,5793,5794,5796,5798,5800,5802,5804,5806,5808,5810,5812],{"class":166,"line":187},[164,5795,3033],{"class":451},[164,5797,718],{"class":207},[164,5799,41],{"class":211},[164,5801,3040],{"class":214},[164,5803,218],{"class":787},[164,5805,222],{"class":221},[164,5807,2392],{"class":225},[164,5809,222],{"class":221},[164,5811,1398],{"class":787},[164,5813,1516],{"class":211},[164,5815,5816,5818,5820,5822,5824,5826,5828,5830,5832,5834,5836,5838,5840,5842,5844,5847,5850,5852,5856,5858,5860],{"class":166,"line":193},[164,5817,3104],{"class":234},[164,5819,3637],{"class":1489},[164,5821,1346],{"class":247},[164,5823,718],{"class":207},[164,5825,41],{"class":211},[164,5827,3063],{"class":214},[164,5829,218],{"class":787},[164,5831,222],{"class":221},[164,5833,2173],{"class":225},[164,5835,222],{"class":221},[164,5837,231],{"class":211},[164,5839,244],{"class":211},[164,5841,425],{"class":787},[164,5843,377],{"class":211},[164,5845,5846],{"class":221}," /",[164,5848,5849],{"class":225},"switch to",[164,5851,2392],{"class":221},[164,5853,5855],{"class":5854},"sPY_W","i",[164,5857,251],{"class":211},[164,5859,1398],{"class":787},[164,5861,1516],{"class":211},[164,5863,5864,5866,5868,5870,5872,5874,5876,5878,5880,5882,5884,5886,5888,5890,5893,5895,5897],{"class":166,"line":198},[164,5865,3033],{"class":451},[164,5867,3135],{"class":214},[164,5869,218],{"class":787},[164,5871,3736],{"class":207},[164,5873,1398],{"class":787},[164,5875,41],{"class":211},[164,5877,3743],{"class":214},[164,5879,218],{"class":787},[164,5881,222],{"class":221},[164,5883,1822],{"class":225},[164,5885,222],{"class":221},[164,5887,231],{"class":211},[164,5889,408],{"class":221},[164,5891,5892],{"class":225},"Switch to dark mode",[164,5894,222],{"class":221},[164,5896,1398],{"class":787},[164,5898,1516],{"class":211},[164,5900,5901],{"class":166,"line":204},[164,5902,184],{"emptyLinePlaceholder":183},[164,5904,5905],{"class":166,"line":443},[164,5906,5907],{"class":170},"  // After clicking, page is in dark mode — toggle should offer to switch to light\n",[164,5909,5910,5912,5914,5916,5918,5920],{"class":166,"line":455},[164,5911,3033],{"class":451},[164,5913,3637],{"class":207},[164,5915,41],{"class":211},[164,5917,3095],{"class":214},[164,5919,1441],{"class":787},[164,5921,1516],{"class":211},[164,5923,5924,5926,5928,5930,5932,5934,5936,5938,5940,5942,5944,5946,5948,5950,5953,5955,5957],{"class":166,"line":461},[164,5925,3033],{"class":451},[164,5927,3135],{"class":214},[164,5929,218],{"class":787},[164,5931,3736],{"class":207},[164,5933,1398],{"class":787},[164,5935,41],{"class":211},[164,5937,3743],{"class":214},[164,5939,218],{"class":787},[164,5941,222],{"class":221},[164,5943,1822],{"class":225},[164,5945,222],{"class":221},[164,5947,231],{"class":211},[164,5949,408],{"class":221},[164,5951,5952],{"class":225},"Switch to light mode",[164,5954,222],{"class":221},[164,5956,1398],{"class":787},[164,5958,1516],{"class":211},[164,5960,5961,5963,5965],{"class":166,"line":627},[164,5962,545],{"class":211},[164,5964,1398],{"class":207},[164,5966,1516],{"class":211},[46,5968,5970],{"id":5969},"generic-alt-text","Generic alt text",[11,5972,5973],{},"Catch alt text that was added to silence a linter but communicates nothing.",[154,5975,5977],{"className":156,"code":5976,"filename":2999,"language":159,"meta":160,"style":160},"test('no generic alt text on images', async ({ page }) => {\n  await page.goto('/');\n  const genericAlts = ['image', 'photo', 'picture', 'icon', 'decorative image', 'banner', 'img'];\n  const images = await page.locator('img[alt]').all();\n  for (const img of images) {\n    const alt = (await img.getAttribute('alt') ?? '').toLowerCase().trim();\n    if (alt !== '') {\n      // Empty alt is valid and intentional for decorative images — don't flag it\n      expect(genericAlts, `Generic alt text found: \"${alt}\"`).not.toContain(alt);\n    }\n  }\n});\n",[116,5978,5979,6006,6028,6103,6139,6158,6208,6226,6231,6276,6281,6285],{"__ignoreMap":160},[164,5980,5981,5983,5985,5987,5990,5992,5994,5996,5998,6000,6002,6004],{"class":166,"line":167},[164,5982,208],{"class":214},[164,5984,218],{"class":207},[164,5986,222],{"class":221},[164,5988,5989],{"class":225},"no generic alt text on images",[164,5991,222],{"class":221},[164,5993,231],{"class":211},[164,5995,235],{"class":234},[164,5997,714],{"class":211},[164,5999,718],{"class":717},[164,6001,721],{"class":211},[164,6003,241],{"class":234},[164,6005,535],{"class":211},[164,6007,6008,6010,6012,6014,6016,6018,6020,6022,6024,6026],{"class":166,"line":174},[164,6009,3033],{"class":451},[164,6011,718],{"class":207},[164,6013,41],{"class":211},[164,6015,3040],{"class":214},[164,6017,218],{"class":787},[164,6019,222],{"class":221},[164,6021,2392],{"class":225},[164,6023,222],{"class":221},[164,6025,1398],{"class":787},[164,6027,1516],{"class":211},[164,6029,6030,6032,6035,6037,6039,6041,6044,6046,6048,6050,6053,6055,6057,6059,6062,6064,6066,6068,6071,6073,6075,6077,6080,6082,6084,6086,6089,6091,6093,6095,6097,6099,6101],{"class":166,"line":180},[164,6031,3104],{"class":234},[164,6033,6034],{"class":1489}," genericAlts",[164,6036,1346],{"class":247},[164,6038,5505],{"class":787},[164,6040,222],{"class":221},[164,6042,6043],{"class":225},"image",[164,6045,222],{"class":221},[164,6047,231],{"class":211},[164,6049,408],{"class":221},[164,6051,6052],{"class":225},"photo",[164,6054,222],{"class":221},[164,6056,231],{"class":211},[164,6058,408],{"class":221},[164,6060,6061],{"class":225},"picture",[164,6063,222],{"class":221},[164,6065,231],{"class":211},[164,6067,408],{"class":221},[164,6069,6070],{"class":225},"icon",[164,6072,222],{"class":221},[164,6074,231],{"class":211},[164,6076,408],{"class":221},[164,6078,6079],{"class":225},"decorative image",[164,6081,222],{"class":221},[164,6083,231],{"class":211},[164,6085,408],{"class":221},[164,6087,6088],{"class":225},"banner",[164,6090,222],{"class":221},[164,6092,231],{"class":211},[164,6094,408],{"class":221},[164,6096,2003],{"class":225},[164,6098,222],{"class":221},[164,6100,5560],{"class":787},[164,6102,1516],{"class":211},[164,6104,6105,6107,6110,6112,6114,6116,6118,6120,6122,6124,6127,6129,6131,6133,6135,6137],{"class":166,"line":187},[164,6106,3104],{"class":234},[164,6108,6109],{"class":1489}," images",[164,6111,1346],{"class":247},[164,6113,1495],{"class":451},[164,6115,718],{"class":207},[164,6117,41],{"class":211},[164,6119,4940],{"class":214},[164,6121,218],{"class":787},[164,6123,222],{"class":221},[164,6125,6126],{"class":225},"img[alt]",[164,6128,222],{"class":221},[164,6130,1398],{"class":787},[164,6132,41],{"class":211},[164,6134,5189],{"class":214},[164,6136,1441],{"class":787},[164,6138,1516],{"class":211},[164,6140,6141,6143,6145,6147,6150,6152,6154,6156],{"class":166,"line":193},[164,6142,3164],{"class":451},[164,6144,1392],{"class":787},[164,6146,5607],{"class":234},[164,6148,6149],{"class":1489}," img",[164,6151,5613],{"class":247},[164,6153,6109],{"class":207},[164,6155,3199],{"class":787},[164,6157,1211],{"class":211},[164,6159,6160,6162,6164,6166,6168,6170,6172,6174,6176,6178,6180,6182,6184,6186,6189,6192,6194,6196,6198,6200,6202,6204,6206],{"class":166,"line":198},[164,6161,1486],{"class":234},[164,6163,2356],{"class":1489},[164,6165,1346],{"class":247},[164,6167,1392],{"class":787},[164,6169,5633],{"class":451},[164,6171,6149],{"class":207},[164,6173,41],{"class":211},[164,6175,5243],{"class":214},[164,6177,218],{"class":787},[164,6179,222],{"class":221},[164,6181,1788],{"class":225},[164,6183,222],{"class":221},[164,6185,3199],{"class":787},[164,6187,6188],{"class":247},"??",[164,6190,6191],{"class":221}," ''",[164,6193,1398],{"class":787},[164,6195,41],{"class":211},[164,6197,5648],{"class":214},[164,6199,1441],{"class":787},[164,6201,41],{"class":211},[164,6203,5655],{"class":214},[164,6205,1441],{"class":787},[164,6207,1516],{"class":211},[164,6209,6210,6213,6215,6217,6220,6222,6224],{"class":166,"line":204},[164,6211,6212],{"class":451},"    if",[164,6214,1392],{"class":787},[164,6216,1788],{"class":207},[164,6218,6219],{"class":247}," !==",[164,6221,6191],{"class":221},[164,6223,3199],{"class":787},[164,6225,1211],{"class":211},[164,6227,6228],{"class":166,"line":443},[164,6229,6230],{"class":170},"      // Empty alt is valid and intentional for decorative images — don't flag it\n",[164,6232,6233,6236,6238,6241,6243,6245,6248,6250,6252,6254,6256,6258,6260,6262,6264,6266,6268,6270,6272,6274],{"class":166,"line":455},[164,6234,6235],{"class":214},"      expect",[164,6237,218],{"class":787},[164,6239,6240],{"class":207},"genericAlts",[164,6242,231],{"class":211},[164,6244,5313],{"class":221},[164,6246,6247],{"class":225},"Generic alt text found: \"",[164,6249,5319],{"class":221},[164,6251,1788],{"class":207},[164,6253,545],{"class":221},[164,6255,1959],{"class":225},[164,6257,5336],{"class":221},[164,6259,1398],{"class":787},[164,6261,41],{"class":211},[164,6263,3382],{"class":207},[164,6265,41],{"class":211},[164,6267,5696],{"class":214},[164,6269,218],{"class":787},[164,6271,1788],{"class":207},[164,6273,1398],{"class":787},[164,6275,1516],{"class":211},[164,6277,6278],{"class":166,"line":461},[164,6279,6280],{"class":211},"    }\n",[164,6282,6283],{"class":166,"line":627},[164,6284,1410],{"class":211},[164,6286,6287,6289,6291],{"class":166,"line":829},[164,6288,545],{"class":211},[164,6290,1398],{"class":207},[164,6292,1516],{"class":211},[27,6294],{},[30,6296,6298],{"id":6297},"manual-accessibility-tests-to-supplement-automated-tests","Manual Accessibility Tests to Supplement Automated Tests",[11,6300,6301,6302,6304],{},"Some accessibility defects cannot be detected through test automation alone. Whether a label is ",[38,6303,1869],{}," 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,6306,6307],{},"Use this checklist as part of your periodic accessibility audit — the checks automation structurally cannot perform:",[59,6309,6312,6321,6327,6333,6339,6345,6354,6360],{"className":6310},[6311],"contains-task-list",[62,6313,6316,6320],{"className":6314},[6315],"task-list-item",[6317,6318],"input",{"disabled":183,"type":6319},"checkbox"," Tab through the full page with no mouse — does focus order make sense?",[62,6322,6324,6326],{"className":6323},[6315],[6317,6325],{"disabled":183,"type":6319}," Is a skip link the first focusable element?",[62,6328,6330,6332],{"className":6329},[6315],[6317,6331],{"disabled":183,"type":6319}," Are all landmark regions labeled and distinct?",[62,6334,6336,6338],{"className":6335},[6315],[6317,6337],{"disabled":183,"type":6319}," Does every link make sense read aloud out of context?",[62,6340,6342,6344],{"className":6341},[6315],[6317,6343],{"disabled":183,"type":6319}," Do dynamic controls update their accessible name when state changes?",[62,6346,6348,6350,6351,6353],{"className":6347},[6315],[6317,6349],{"disabled":183,"type":6319}," Do ",[116,6352,2015],{}," references point to visible, accurate text?",[62,6355,6357,6359],{"className":6356},[6315],[6317,6358],{"disabled":183,"type":6319}," Test with a real screen reader — NVDA (free, Windows), JAWS, or VoiceOver (built into macOS and iOS)",[62,6361,6363,6365],{"className":6362},[6315],[6317,6364],{"disabled":183,"type":6319}," Navigate by landmarks, headings, and links — not just linearly",[11,6367,6368],{},"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.",[27,6370],{},[30,6372,1611],{"id":1610},[11,6374,6375,6376,6378],{},"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 ",[116,6377,1822],{},", a dropped landmark, or a broken focus order leaves no visual trace and no failing test without an explicit check.",[11,6380,6381],{},"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,6383,6384],{},"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.",[1619,6386],{":items":6387},"[\"/software-testing/frameworks/nightwatch/implementing-a-minimum-accessibility-test-plan\"]",[1623,6389,6390],{},"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":160,"searchDepth":174,"depth":174,"links":6392},[6393,6394,6395,6408,6419,6420],{"id":1683,"depth":174,"text":1684},{"id":1773,"depth":174,"text":1774},{"id":1837,"depth":174,"text":1838,"children":6396},[6397,6398,6399,6401,6402,6403,6404,6405,6406,6407],{"id":1841,"depth":180,"text":1842},{"id":1904,"depth":180,"text":1905},{"id":2011,"depth":180,"text":6400},"3. aria-labelledby vs aria-label — Both Pass, One Is Better",{"id":2240,"depth":180,"text":2241},{"id":2288,"depth":180,"text":2289},{"id":2446,"depth":180,"text":2447},{"id":2587,"depth":180,"text":2588},{"id":2738,"depth":180,"text":2739},{"id":2847,"depth":180,"text":2848},{"id":2928,"depth":180,"text":2929},{"id":3982,"depth":174,"text":3983,"children":6409},[6410,6411,6412,6413,6414,6415,6416,6417,6418],{"id":3996,"depth":180,"text":3997},{"id":4181,"depth":180,"text":4182},{"id":4360,"depth":180,"text":4361},{"id":4538,"depth":180,"text":4539},{"id":4840,"depth":180,"text":4841},{"id":5099,"depth":180,"text":5100},{"id":5435,"depth":180,"text":5436},{"id":5719,"depth":180,"text":5720},{"id":5969,"depth":180,"text":5970},{"id":6297,"depth":174,"text":6298},{"id":1610,"depth":174,"text":1611},"/images/posts/playwright-accessibility-testing-axe-lighthouse-limitations/what-automated-accessibility-tests-miss-cover.webp","2026-04-10","axe and Lighthouse miss 60–70% of WCAG violations. Learn the limitations of automated accessibility testing and how to write smarter Playwright tests.",{},"/software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations",{"title":1666,"description":6423},"software-testing/test-automation/playwright-accessibility-testing-axe-lighthouse-limitations","lo_u9eP8852yVW5Jb3GEAfsnJBS9qEVw1EaX2_dzRLM",{"id":6430,"title":6431,"bmcUsername":6,"body":6432,"cover":6724,"date":6725,"description":6726,"draft":1656,"extension":1657,"features":6,"githubRepo":6,"headline":6,"highlight":6,"icon":6,"meta":6727,"navigation":183,"npmPackage":6,"order":6,"path":6728,"seo":6729,"stem":6730,"__hash__":6731},"content/software-testing/test-automation/best-websites-for-practicing-test-automation.md","Best Websites for Practicing Test Automation",{"type":8,"value":6433,"toc":6717},[6434,6438,6441,6444,6464,6467,6470,6474,6487,6490,6503,6506,6515,6518,6527,6530,6539,6542,6551,6554,6563,6566,6575,6578,6589,6598,6604,6618,6622,6625,6634,6637,6646,6649,6658,6661,6670,6673,6677,6685,6688,6696,6699,6707,6711,6714],[30,6435,6437],{"id":6436},"why-use-demo-websites-for-test-automation-practice","Why use demo websites for test automation practice",[11,6439,6440],{},"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,6442,6443],{},"You'll want to try out your test automation framework against things like:",[59,6445,6446,6449,6452,6455,6458,6461],{},[62,6447,6448],{},"iFrames",[62,6450,6451],{},"Modal dialogs",[62,6453,6454],{},"New tabs",[62,6456,6457],{},"Multiple windows",[62,6459,6460],{},"Asynchronous loading of content",[62,6462,6463],{},"File upload dialogs",[11,6465,6466],{},"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.",[11,6468,6469],{},"I've compiled a list of the best websites I've come across to practice or demo test automation frameworks against in the list below. They include the harder to automate scenarios just mentioned.",[30,6471,6473],{"id":6472},"recommended-demo-websites-for-test-automation-practice","Recommended demo websites for test automation practice",[136,6475,6476],{},[62,6477,6478,6479,2904,6483],{},"\"The Internet\" ",[668,6480],{"href":6481,"text":6482},"https://github.com/saucelabs/the-internet","github",[668,6484],{"href":6485,"text":6486},"http://the-internet.herokuapp.com/","live site",[11,6488,6489],{},"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.",[136,6491,6492],{"start":174},[62,6493,6494,6495,6498,6499],{},"The ",[38,6496,6497],{},"Automation Exercise"," clothing store ",[668,6500],{"href":6501,"text":6502},"https://www.automationexercise.com/","automationexercise.com",[11,6504,6505],{},"This example clothing storefront has both a web front end to test against and APIs. The site even includes test cases to guide you.",[136,6507,6508],{"start":180},[62,6509,6510,6511],{},"LetCode ",[668,6512],{"href":6513,"text":6514},"https://letcode.in/test","letcode.in",[11,6516,6517],{},"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.",[136,6519,6520],{"start":187},[62,6521,6522,6523],{},"UI Test Automation Playground ",[668,6524],{"href":6525,"text":6526},"http://www.uitestingplayground.com/","uitestingplayground.com",[11,6528,6529],{},"Smaller site, but it contains edge cases for load delays, mouse over behavior, dynamic IDs, and automation issues arising from hidden layers.",[136,6531,6532],{"start":193},[62,6533,6534,6535],{},"SwagLabs ",[668,6536],{"href":6537,"text":6538},"https://www.saucedemo.com/","saucedemo.com",[11,6540,6541],{},"Another demo web storefront useful for testing login and shopping cart flows. A key distinction on this site is that it has 4 different logins you can use for different experiences for the same site; normal, locked out, problem user, and performance glitch user. Maintained by the folks at SauceLabs.",[136,6543,6544],{"start":198},[62,6545,6546,6547],{},"Angular Banking Site ",[668,6548],{"href":6549,"text":6550},"https://www.globalsqa.com/angularJs-protractor/BankingProject","GlobalsQA",[11,6552,6553],{},"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.",[136,6555,6556],{"start":204},[62,6557,6558,6559],{},"Forms sandbox ",[668,6560],{"href":6561,"text":6562},"https://automatenow.io/sandbox/","AutomateNow",[11,6564,6565],{},"Small, straight-forward single page site for practicing interactions with typical form controls, search, pop up dialogs, and a map.",[136,6567,6568],{"start":443},[62,6569,6570,6571],{},"Automation Practice ",[668,6572],{"href":6573,"text":6574},"https://qa-practice.razvanvancea.ro/","QA-Practice",[11,6576,6577],{},"Clean site that combines several different testing exercises in a small e-commerce site:",[59,6579,6580,6583,6586],{},[62,6581,6582],{},"Common web elements",[62,6584,6585],{},"API with Swagger documentation",[62,6587,6588],{},"Buggy web form you can use to challenge candidates to see how many bugs they can discover.",[136,6590,6591],{"start":455},[62,6592,6593,6594],{},"The Sweet Shop ",[668,6595],{"href":6596,"text":6597},"https://sweetshop.netlify.app/","sweetshop.netlify.app",[11,6599,6600,6603],{},[38,6601,6602],{},"The Sweet Shop"," is a fake e-commerce front end for a candy store with hidden defects and opportunity to automate:",[59,6605,6606,6609,6612,6615],{},[62,6607,6608],{},"Account login",[62,6610,6611],{},"Shopping cart",[62,6613,6614],{},"Product pages",[62,6616,6617],{},"Non-US currency",[30,6619,6621],{"id":6620},"recommended-api-testing-sites","Recommended API testing sites",[11,6623,6624],{},"If you are learning SuperTest + Mocha for API Test automation or learning Postman the following sites are handy REST APIs to practice tests against.",[136,6626,6627],{},[62,6628,6629,6630],{},"Swagger Petstore ",[668,6631],{"href":6632,"text":6633},"https://petstore.swagger.io/","petstore.swagger.io",[11,6635,6636],{},"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.",[136,6638,6639],{"start":174},[62,6640,6641,6642],{},"Restful Booker ",[668,6643],{"href":6644,"text":6645},"https://restful-booker.herokuapp.com/apidoc/index.html","restful-booker.herokuapp.com",[11,6647,6648],{},"Test all kinds of CRUD (Create, Update, Delete) REST API scenarios using this example booking API.",[136,6650,6651],{"start":180},[62,6652,6653,6654],{},"FakeRestAPI ",[668,6655],{"href":6656,"text":6657},"https://fakerestapi.azurewebsites.net/index.html","fakerestapi.azurewebsites.net",[11,6659,6660],{},"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.",[136,6662,6663],{"start":187},[62,6664,6665,6666],{},"Star Wars API ",[668,6667],{"href":6668,"text":6669},"https://swapi.dev/","SWAPI",[11,6671,6672],{},"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.",[30,6674,6676],{"id":6675},"other-lists-more-reading","Other Lists / More Reading",[136,6678,6679],{},[62,6680,6681],{},[668,6682],{"href":6683,"text":6684},"https://github.com/BMayhew/awesome-sites-to-test-on","Awesome Sites to Test On",[11,6686,6687],{},"Butch Mayhew maintains a list of more sites categorized by security, mobile, performance, and web.",[136,6689,6690],{"start":174},[62,6691,6692],{},[668,6693],{"href":6694,"text":6695},"https://automationpanda.com/2021/12/29/want-to-practice-test-automation-try-these-demo-sites/","Automation Panda",[11,6697,6698],{},"Recommended sites by Andrew Knight.",[136,6700,6701],{"start":180},[62,6702,6703],{},[1949,6704,6706],{"href":6705},"/software-testing/frameworks/nightwatch/api-testing-with-nightwatch-supertest","API Testing in Nightwatch.js",[30,6708,6710],{"id":6709},"know-of-a-good-one","Know of a good one?",[11,6712,6713],{},"If you know of any other good sites I should include please reach out through my social links below 👇",[1619,6715],{"to":6716},"/software-testing/test-automation",{"title":160,"searchDepth":174,"depth":174,"links":6718},[6719,6720,6721,6722,6723],{"id":6436,"depth":174,"text":6437},{"id":6472,"depth":174,"text":6473},{"id":6620,"depth":174,"text":6621},{"id":6675,"depth":174,"text":6676},{"id":6709,"depth":174,"text":6710},"/images/posts/best-websites-for-practicing-test-automation/best-sites-for-test-practice.jpg","2026-03-12","Looking for example demo sites to practice your test automation against with Selenium, Playwright, or Cypress? Here is a list of the top demo websites for practicing your test automation skills against.",{},"/software-testing/test-automation/best-websites-for-practicing-test-automation",{"title":6431,"description":6726},"software-testing/test-automation/best-websites-for-practicing-test-automation","BPGWHU_1LTRwQJQp50G1zM3QKu7eqfqP2aNRmJp3puc",1776371530967]