diff --git a/.jules/palette.md b/.jules/palette.md index feccb6c3..d742dcbd 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -57,7 +57,3 @@ ## 2026-08-04 - Native File Input Iteration Friction **Learning:** Browsers suppress `change` events when a file input still holds the same selected path. Clearing the value in an inline `onclick` handler fixes repetition but also discards the previous selection when the user cancels the picker and couples behavior to markup. **Action:** Capture the selected `File` in the input's `change` listener, clear the input value immediately afterward, and then process the captured object. Use native buttons with explicit event listeners to proxy the picker from an empty-state CTA, preserving keyboard access and CSP-compatible separation of markup and behavior. - -## 2026-08-05 - External Link Accessibility -**Learning:** External links (`target="_blank"`) that open new tabs are disorienting to screen reader users without explicit textual and visual cues. -**Action:** For external links, always append a visual indicator (`↗`) and an `aria-label="... (opens in a new tab)"` to inform users of the behavior before activation. diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 49744ab9..baca3a38 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -114,8 +114,11 @@ const ctx = String(f.context||'app-code'); return BLOCKING_SEV.has(sev) && !NON_BLOCKING.has(ctx); } -function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));} +function esc(s){return String(s==null?'':s).replace(/[&<>"'`]/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":''','`':'`'}[c]));} function safeUrl(u){ + if (typeof u === 'string' && u.startsWith('//')) { + return '#'; + } try { const parsed = new URL(u, window.location.href); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return u; @@ -248,7 +251,7 @@
${esc(f.file)}:${esc(f.line)} · ${esc(f.category)} · context: ${esc(f.context||'app-code')}${isDeployBlocking(f)?' · deploy-blocking':''}
diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 77def601..ec9bde11 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -2,10 +2,12 @@ import json import json as _json +import re import threading import urllib.error import urllib.request from contextlib import closing +from html.parser import HTMLParser import pytest @@ -14,6 +16,20 @@ make_dashboard_server, render_tokens_css) +class _ButtonAttributeParser(HTMLParser): + """Collect attributes from every dashboard button element.""" + + def __init__(self): + """Initialize an empty button-attribute collection.""" + super().__init__() + self.buttons = [] + + def handle_starttag(self, tag, attrs): + """Record one button's attributes while ignoring other elements.""" + if tag == "button": + self.buttons.append(dict(attrs)) + + def _serve(server): thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() @@ -223,6 +239,7 @@ def test_server_404s_missing_findings(tmp_path): server.shutdown() server.server_close() + def test_dashboard_empty_state_clear_filters(): """Empty state CTA must expose Clear filters control that resets state.""" html = dashboard_index_path().read_text(encoding="utf-8") @@ -231,3 +248,23 @@ def test_dashboard_empty_state_clear_filters(): assert "aria-label=\"Clear filters\"" in html assert "onclick=\"query=''; filterSev=''; render(); document.getElementById('q')?.focus();\"" in html assert "Clear filters" in html + + +def test_dashboard_dialog_close_button_has_tooltip(): + """The dynamically rendered close button exposes its label and Esc tooltip.""" + html = dashboard_index_path().read_text(encoding="utf-8") + detail_markup = re.search( + r"d\.innerHTML\s*=\s*`(?P