diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 132bc31b..0a75766e 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -96,6 +96,7 @@

+opens in a new tab
@@ -281,7 +282,8 @@

Dashboard

function openDetail(f){ lastFocus = document.activeElement; const s = String(f.severity||'INFO').toUpperCase(); - const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
'); + const extIcon = ``; + const refs = (f.references||[]).map(r=>`${esc(r)}${extIcon}`).join('
'); const owasp = (f.owasp||[]).join(', '); const cwe = (f.cwe||[]).join(', '); const d = document.getElementById('detail'); diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 75a5809f..f6e9bac7 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -302,3 +302,48 @@ def test_dashboard_search_escape_clears_input(): assert "e.key === 'Escape'" in html assert "query = '';" in html assert "render();" in html + +def test_dashboard_external_links_have_accessible_visual_indicator(): + """External links opening in new tabs must have both an assistive warning and a visual indicator (WCAG G201).""" + from html.parser import HTMLParser + import re + + html = dashboard_index_path().read_text(encoding="utf-8") + + # Verify the global description element exists + assert 'opens in a new tab' in html + + refs_markup = re.search( + r"const refs\s*=\s*\(f\.references\|\|\[\]\)\.map\(r=>`(?P.*?)`\)\.join\('
'\);", + html, + flags=re.DOTALL, + ) + assert refs_markup is not None + + class _LinkAttributeParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.links = [] + self.svgs = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag == "a": + self.links.append(dict(attrs)) + if tag == "svg": + self.svgs.append(dict(attrs)) + + parser = _LinkAttributeParser() + # Inject a dummy href to make the template string parseable HTML + test_html = refs_markup.group("markup").replace('${esc(safeUrl(r))}', 'https://example.com').replace('${esc(r)}', 'Link').replace('${extIcon}', '') + parser.feed(test_html) + + assert len(parser.links) == 1 + link = parser.links[0] + assert link.get("target") == "_blank" + assert link.get("rel") == "noopener" + assert link.get("aria-describedby") == "ext-link-desc" + + assert len(parser.svgs) >= 1 + svg = parser.svgs[0] + assert svg.get("aria-hidden") == "true" + assert svg.get("focusable") == "false"