@wcag-checkr/ci 1.0.0-rc.366 → 1.0.0-rc.367
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{ErrorBoundary-BH1wbHb3.js → ErrorBoundary-Ck0XMUiQ.js} +22 -22
- package/dist/assets/{copy-ai-fixer-prompt-FO0N3IMf.js → copy-ai-fixer-prompt-CKp52Td1.js} +2 -2
- package/dist/assets/{devtools-panel-BAHfgRsc.js → devtools-panel-Dw0aFhIc.js} +1 -1
- package/dist/assets/{parallel-tab-flow-C4bjrDqL.js → parallel-tab-flow-BuZZIdRb.js} +1 -1
- package/dist/assets/{scheduled-audit-runner-XCDyFhY6.js → scheduled-audit-runner-9R-aGzwO.js} +137 -137
- package/dist/assets/{service-worker.ts-57FwiS_-.js → service-worker.ts-CyL2aQvI.js} +2 -2
- package/dist/assets/{side-panel-Dl247dJv.js → side-panel-CuEUwRBD.js} +3 -3
- package/dist/assets/{site-report-renderer-CNWZjbOd.js → site-report-renderer-46_JyAkB.js} +1 -1
- package/dist/devtools/panel.html +3 -3
- package/dist/manifest.json +3 -4
- package/dist/service-worker-loader.js +1 -1
- package/dist/side-panel/side-panel.html +4 -4
- package/package.json +1 -1
- package/dist/side-panel/App.tsx +0 -308
- package/dist/side-panel/README.md +0 -57
- package/dist/side-panel/audit-launcher.test.ts +0 -56
- package/dist/side-panel/audit-launcher.ts +0 -98
- package/dist/side-panel/azure-devops-issue.test.ts +0 -68
- package/dist/side-panel/azure-devops-issue.ts +0 -89
- package/dist/side-panel/format-component-id.test.ts +0 -89
- package/dist/side-panel/format-component-id.ts +0 -40
- package/dist/side-panel/github-issue.test.ts +0 -102
- package/dist/side-panel/github-issue.ts +0 -66
- package/dist/side-panel/gitlab-issue.test.ts +0 -53
- package/dist/side-panel/gitlab-issue.ts +0 -78
- package/dist/side-panel/jira-issue.ts +0 -64
- package/dist/side-panel/main.tsx +0 -86
- package/dist/side-panel/store.ts +0 -435
- package/dist/side-panel/styles.css +0 -55
- package/dist/side-panel/use-audit-view-model.ts +0 -116
- package/dist/side-panel/wire-messaging.test.ts +0 -234
- package/dist/side-panel/wire-messaging.ts +0 -521
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>WCAG Component Auditor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/side-panel-
|
|
7
|
+
<script type="module" crossorigin src="/assets/side-panel-CuEUwRBD.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-Cpj98o6Y.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/crash-reporter-7_6X-wSd.js">
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-D7HrI6pR.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/diff-NSOR-wHP.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/ai-usage-log-CvwMbWGw.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/scheduled-audit-runner-
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/scheduled-audit-runner-9R-aGzwO.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/design-system-audit-DpxJrxnb.js">
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/ErrorBoundary-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/copy-ai-fixer-prompt-
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/ErrorBoundary-Ck0XMUiQ.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/copy-ai-fixer-prompt-CKp52Td1.js">
|
|
19
19
|
<link rel="stylesheet" crossorigin href="/assets/styles-DZ8gnBYK.css">
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/side-panel-Ctm2yXeo.css">
|
|
21
21
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcag-checkr/ci",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.367",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Headless wcagcheckr accessibility audit runner for CI/CD pipelines. Drives the wcagcheckr Chrome extension via Playwright, runs full-page audits across the state matrix (108 combinations: hover, focus, dark mode, RTL, breakpoints), outputs JSON / SARIF / JUnit, exits with severity-aware codes.",
|
|
6
6
|
"license": "UNLICENSED",
|
package/dist/side-panel/App.tsx
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { useStore } from './store';
|
|
3
|
-
import {
|
|
4
|
-
HEADLESS_BUILD_LOCKDOWN,
|
|
5
|
-
isLockoutInEffectForTier,
|
|
6
|
-
} from '../shared/headless-build-lockdown';
|
|
7
|
-
// rc.79 — Reverted the rc.72 lazy-load NormalApp routing. Owner mode
|
|
8
|
-
// goes back to using OwnerView (lawfare-risk grade, Quick/Thorough
|
|
9
|
-
// scan depth, USER_RECIPES, full export set, no Suspense boundary).
|
|
10
|
-
// rc.72's NormalApp stripped those features and added a chunk-load
|
|
11
|
-
// step that wasn't there before; the regression wasn't worth the
|
|
12
|
-
// "two code paths" structural symmetry it claimed.
|
|
13
|
-
import {
|
|
14
|
-
wireMessaging,
|
|
15
|
-
refreshBaselineList,
|
|
16
|
-
refreshLicenseTier,
|
|
17
|
-
refreshUserMode,
|
|
18
|
-
startKeepalive,
|
|
19
|
-
loadLastAudit,
|
|
20
|
-
warmAndWatchRubrics,
|
|
21
|
-
} from './wire-messaging';
|
|
22
|
-
import { primeAuditLauncher } from './audit-launcher';
|
|
23
|
-
import { sendToTab } from '../shared/messaging';
|
|
24
|
-
import { getAuditTargetTabId } from '../shared/active-tab';
|
|
25
|
-
import { loadAcknowledged } from '../shared/acknowledged-findings';
|
|
26
|
-
import { getDismissalsForUrl } from '../shared/dismissals';
|
|
27
|
-
import { loadSiteCrawlReport } from '../shared/site-crawl-storage';
|
|
28
|
-
import { Header } from './components/Header';
|
|
29
|
-
import { AuditControls } from './components/AuditControls';
|
|
30
|
-
import { VisualizerTools } from './components/VisualizerTools';
|
|
31
|
-
import { GatedView } from './components/GatedView';
|
|
32
|
-
import { ProgressBar } from './components/ProgressBar';
|
|
33
|
-
import { LastRunBanner } from './components/LastRunBanner';
|
|
34
|
-
import { MatrixView } from './views/MatrixView';
|
|
35
|
-
import { GeneralReportView } from './views/GeneralReportView';
|
|
36
|
-
import { DeltaView } from './views/DeltaView';
|
|
37
|
-
import { ActivityView } from './views/ActivityView';
|
|
38
|
-
import { GuidedView } from './views/GuidedView';
|
|
39
|
-
import { FlowsView } from './views/FlowsView';
|
|
40
|
-
import { ScorecardView } from './views/ScorecardView';
|
|
41
|
-
import { SiteCrawlPanel } from './components/SiteCrawlPanel';
|
|
42
|
-
import { OnboardingModal } from './components/OnboardingModal';
|
|
43
|
-
import { ErrorBanner } from './components/ErrorBanner';
|
|
44
|
-
import { AiFailureBanner } from './components/AiFailureBanner';
|
|
45
|
-
import { AiOffBanner } from './components/AiOffBanner';
|
|
46
|
-
import { DebuggerBusyModal } from './components/DebuggerBusyModal';
|
|
47
|
-
import { ResumeAuditBanner } from './components/ResumeAuditBanner';
|
|
48
|
-
import { StorybookHint } from './components/StorybookHint';
|
|
49
|
-
import { AuditFooter } from './components/AuditFooter';
|
|
50
|
-
import { ComplianceDisclaimer } from './components/ComplianceDisclaimer';
|
|
51
|
-
import { Announcer } from '../shared/announcer';
|
|
52
|
-
import { OwnerView } from './views/OwnerView';
|
|
53
|
-
import { ForensicLogView } from './views/ForensicLogView';
|
|
54
|
-
import { WcagEmView } from './views/WcagEmView';
|
|
55
|
-
import { SchedulesView } from './views/SchedulesView';
|
|
56
|
-
import { RiskView } from './views/RiskView';
|
|
57
|
-
import { CopilotView } from './views/CopilotView';
|
|
58
|
-
import { AxTreeView } from './views/AxTreeView';
|
|
59
|
-
import { Wcag3View } from './views/Wcag3View';
|
|
60
|
-
import { DesignSystemView } from './views/DesignSystemView';
|
|
61
|
-
import { OnboardingChecklist } from './components/OnboardingChecklist';
|
|
62
|
-
import { UpgradeCard } from './components/UpgradeCard';
|
|
63
|
-
import { CriticalBanner } from './components/MessagesBell';
|
|
64
|
-
import { LicenseRequiredView } from './views/LicenseRequiredView';
|
|
65
|
-
|
|
66
|
-
export function App() {
|
|
67
|
-
const view = useStore((s) => s.view);
|
|
68
|
-
const userMode = useStore((s) => s.userMode);
|
|
69
|
-
const tier = useStore((s) => s.tier);
|
|
70
|
-
// rc.124 — Headless-build lockdown. When active, only team-license holders
|
|
71
|
-
// get the full UI; everyone else sees the license-entry surface. The check
|
|
72
|
-
// is rendered EARLY so no other UI mounts for locked-out users (no audit
|
|
73
|
-
// controls, no visualizers, no exports). The audit dispatchers + export
|
|
74
|
-
// handlers also gate on the same helper for defense-in-depth.
|
|
75
|
-
const [tierResolved, setTierResolved] = useState(false);
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
// Wait one tick for the initial refreshLicenseTier() call below to land
|
|
78
|
-
// before deciding lockout — otherwise the store's default 'trial' would
|
|
79
|
-
// render LicenseRequiredView for a real team license that's still being
|
|
80
|
-
// validated.
|
|
81
|
-
const t = setTimeout(() => setTierResolved(true), 600);
|
|
82
|
-
return () => clearTimeout(t);
|
|
83
|
-
}, []);
|
|
84
|
-
const lockoutActive = HEADLESS_BUILD_LOCKDOWN && tierResolved && isLockoutInEffectForTier(tier);
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const port = startKeepalive();
|
|
88
|
-
// Detect runtime context: iframe-overlay vs chrome.sidePanel.
|
|
89
|
-
// ?context=overlay → loaded by modules/sidebar-handle.ts inside the page
|
|
90
|
-
// no query param → loaded by chrome.sidePanel (the fallback during audits)
|
|
91
|
-
// Only the chrome.sidePanel instance opens the tracker port — its disconnect
|
|
92
|
-
// is the SW's signal that the fallback UI just closed, time to restore the
|
|
93
|
-
// in-page overlay.
|
|
94
|
-
const isOverlayContext =
|
|
95
|
-
new URLSearchParams(window.location.search).get('context') === 'overlay';
|
|
96
|
-
const trackerPort = isOverlayContext
|
|
97
|
-
? null
|
|
98
|
-
: chrome.runtime.connect({ name: 'sidepanel-tracker' });
|
|
99
|
-
const off = wireMessaging();
|
|
100
|
-
// Defensive .catch — these are bootstrapping side effects; failures should not
|
|
101
|
-
// surface as uncaught promise rejections in the console.
|
|
102
|
-
// First load: bypass the 24h license cache so the UpgradeCard reads
|
|
103
|
-
// any newly-added server fields (seats counts, cancel-at-period-end,
|
|
104
|
-
// single-month plan code, etc.). Subsequent LICENSE_CHANGED_EVENT
|
|
105
|
-
// refreshes use the cache normally — the SW already invalidates it
|
|
106
|
-
// on every LICENSE_SET_REQUEST.
|
|
107
|
-
refreshLicenseTier({ forceRefresh: true }).catch(() => {});
|
|
108
|
-
refreshBaselineList().catch(() => {});
|
|
109
|
-
refreshUserMode().catch(() => {});
|
|
110
|
-
warmAndWatchRubrics().catch(() => {});
|
|
111
|
-
// Pre-cache windowId so audit-start click handlers can open
|
|
112
|
-
// chrome.sidePanel synchronously (preserves user-gesture context).
|
|
113
|
-
primeAuditLauncher().catch(() => {});
|
|
114
|
-
// Load any persisted audit. Results stay until the user clicks Clear —
|
|
115
|
-
// switching tabs or navigating the audited page no longer wipes them.
|
|
116
|
-
// The Header shows the audited domain so the user always knows which
|
|
117
|
-
// page the displayed results belong to.
|
|
118
|
-
loadLastAudit().catch(() => {});
|
|
119
|
-
// rc.37 — load the user's "acknowledged" matchKeys so every
|
|
120
|
-
// ViolationCard can render the acknowledged pill immediately on
|
|
121
|
-
// mount (in every view, not just Delta).
|
|
122
|
-
loadAcknowledged()
|
|
123
|
-
.then((all) => {
|
|
124
|
-
useStore.getState().setAcknowledgedKeys(new Set(all.map((a) => a.matchKey)));
|
|
125
|
-
})
|
|
126
|
-
.catch(() => {});
|
|
127
|
-
|
|
128
|
-
// rc.200 — Subscribe to results changes and reload dismissals for
|
|
129
|
-
// the audited page URL. Dismissals are URL-scoped (not matchKey-
|
|
130
|
-
// scoped like acks), so we re-fetch every time a new audit
|
|
131
|
-
// completes against a different page.
|
|
132
|
-
const unsubResults = useStore.subscribe((state, prev) => {
|
|
133
|
-
const url = state.results[0]?.pageUrl;
|
|
134
|
-
const prevUrl = prev.results[0]?.pageUrl;
|
|
135
|
-
if (!url || url === prevUrl) return;
|
|
136
|
-
void getDismissalsForUrl(url)
|
|
137
|
-
.then((d) => {
|
|
138
|
-
useStore.getState().setDismissedKeys(new Set(Object.keys(d)));
|
|
139
|
-
})
|
|
140
|
-
.catch(() => {});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// rc.111 — rehydrate the most-recent site-crawl report so the AI fixer
|
|
144
|
-
// prompt's "## Site-wide context" section survives panel close / SW
|
|
145
|
-
// restart. setState (not setSiteCrawlReport) so we don't re-write the
|
|
146
|
-
// same data back to storage on hydration.
|
|
147
|
-
// rc.263 — Phase 2 of screen consolidation: when a crawl is in
|
|
148
|
-
// storage on side-panel open AND the user hasn't navigated
|
|
149
|
-
// somewhere else yet, default the view to 'crawl' so they land
|
|
150
|
-
// on the new inline-expandable master view instead of the empty
|
|
151
|
-
// Dashboard. Cliff: "we have so many screens for the user that
|
|
152
|
-
// it is fully confusing." If results from a single-page audit
|
|
153
|
-
// also exist, prefer 'report' (the user just did an audit and
|
|
154
|
-
// wants to see those results). View stays at whatever the user
|
|
155
|
-
// explicitly switches to after first paint.
|
|
156
|
-
loadSiteCrawlReport()
|
|
157
|
-
.then((report) => {
|
|
158
|
-
if (!report) return;
|
|
159
|
-
useStore.setState((prev) => {
|
|
160
|
-
const next: Partial<typeof prev> = { siteCrawlReport: report };
|
|
161
|
-
if (prev.view === 'matrix' && prev.results.length === 0) {
|
|
162
|
-
next.view = 'crawl';
|
|
163
|
-
}
|
|
164
|
-
return next;
|
|
165
|
-
});
|
|
166
|
-
})
|
|
167
|
-
.catch(() => {});
|
|
168
|
-
|
|
169
|
-
// Esc inside the side panel clears the highlight pin. The page's own keydown
|
|
170
|
-
// handler (in element-highlighter) only fires when the page has focus.
|
|
171
|
-
const onEsc = (e: KeyboardEvent) => {
|
|
172
|
-
if (e.key !== 'Escape') return;
|
|
173
|
-
const { pinnedMatchKey, setPinned, results } = useStore.getState();
|
|
174
|
-
if (!pinnedMatchKey) return;
|
|
175
|
-
setPinned(null);
|
|
176
|
-
void getAuditTargetTabId().then((tabId) => {
|
|
177
|
-
if (!tabId) return;
|
|
178
|
-
sendToTab(
|
|
179
|
-
tabId,
|
|
180
|
-
{ type: 'HIGHLIGHT_CLEAR_REQUEST', tabId },
|
|
181
|
-
results[0]?.frameId
|
|
182
|
-
).catch(() => {});
|
|
183
|
-
});
|
|
184
|
-
};
|
|
185
|
-
window.addEventListener('keydown', onEsc);
|
|
186
|
-
|
|
187
|
-
return () => {
|
|
188
|
-
off();
|
|
189
|
-
port.disconnect();
|
|
190
|
-
trackerPort?.disconnect();
|
|
191
|
-
window.removeEventListener('keydown', onEsc);
|
|
192
|
-
unsubResults();
|
|
193
|
-
};
|
|
194
|
-
}, []);
|
|
195
|
-
|
|
196
|
-
// rc.124 — Headless-build lockdown takes precedence over EVERY other view.
|
|
197
|
-
// No persona wizard, no owner view, no dev view. Just the license entry.
|
|
198
|
-
if (lockoutActive) {
|
|
199
|
-
return (
|
|
200
|
-
<>
|
|
201
|
-
<LicenseRequiredView />
|
|
202
|
-
<Announcer />
|
|
203
|
-
</>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// rc.333 — Wizard removed. main.tsx Root resolves missing userMode
|
|
208
|
-
// to 'owner' (the simpler default) before this component ever
|
|
209
|
-
// mounts. The PersonaSwitcher at the top of every UI lets users
|
|
210
|
-
// flip to power-user or dev whenever they want; the settings page
|
|
211
|
-
// sets the persisted default. If anything still passes null
|
|
212
|
-
// through (programmatic store reset, defensive recovery), fall
|
|
213
|
-
// through to owner mode below.
|
|
214
|
-
|
|
215
|
-
// Owner mode — simplified surface. No view tabs, no visualizers, no IGT.
|
|
216
|
-
// OwnerView holds its own header + footer for the simplified flow.
|
|
217
|
-
// (rc.79 reverted the rc.72 NormalApp routing; OwnerView is the right
|
|
218
|
-
// surface for Owner mode and was working before I broke it.)
|
|
219
|
-
if (userMode === 'owner' || userMode === null) {
|
|
220
|
-
return (
|
|
221
|
-
<>
|
|
222
|
-
<OwnerView />
|
|
223
|
-
<DebuggerBusyModal />
|
|
224
|
-
<Announcer />
|
|
225
|
-
</>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Dev mode — full feature surface (everything that existed before).
|
|
230
|
-
// Power-user mode is rendered by V2App via main.tsx's Root router —
|
|
231
|
-
// we should never reach here with userMode === 'power'.
|
|
232
|
-
return (
|
|
233
|
-
<div className="flex flex-col h-screen bg-slate-50 text-slate-900 text-sm">
|
|
234
|
-
{/* Skip link — visually hidden by default; revealed on focus so keyboard
|
|
235
|
-
users can jump past the header/UpgradeCard chrome straight to the
|
|
236
|
-
audit results. Required for WCAG 2.4.1 (Bypass Blocks) when there
|
|
237
|
-
are many tab stops before the main content. */}
|
|
238
|
-
<a
|
|
239
|
-
href="#main-content"
|
|
240
|
-
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-slate-900 focus:px-3 focus:py-1.5 focus:rounded focus:shadow focus:outline focus:outline-2 focus:outline-brand-500"
|
|
241
|
-
>
|
|
242
|
-
Skip to main content
|
|
243
|
-
</a>
|
|
244
|
-
<UpgradeCard />
|
|
245
|
-
<CriticalBanner />
|
|
246
|
-
<Header />
|
|
247
|
-
<OnboardingChecklist />
|
|
248
|
-
<main
|
|
249
|
-
id="main-content"
|
|
250
|
-
className="flex-1 flex flex-col overflow-hidden"
|
|
251
|
-
aria-label="wcagcheckr"
|
|
252
|
-
>
|
|
253
|
-
{/* rc.163 — Audit-launching surface (mode picker, throttle, target
|
|
254
|
-
chooser, visualizer toggles, storybook hint, resume banner) is
|
|
255
|
-
only relevant on the Dashboard, where the user is configuring
|
|
256
|
-
and starting an audit. On Findings and the other specialized
|
|
257
|
-
views these widgets just clutter the screen — Cliff: "remove
|
|
258
|
-
the form fields from the findings tab — distracting and
|
|
259
|
-
serves no real purpose being there — the tab will be better
|
|
260
|
-
fully focused on the findings."
|
|
261
|
-
Progress / error / AI-failure banners stay GLOBAL because
|
|
262
|
-
they're audit-state alerts the user needs to see regardless
|
|
263
|
-
of which view they happened to navigate to. */}
|
|
264
|
-
{view === 'matrix' && (
|
|
265
|
-
<>
|
|
266
|
-
<AuditControls />
|
|
267
|
-
<VisualizerTools />
|
|
268
|
-
<StorybookHint />
|
|
269
|
-
<ResumeAuditBanner />
|
|
270
|
-
</>
|
|
271
|
-
)}
|
|
272
|
-
<ProgressBar />
|
|
273
|
-
<ErrorBanner />
|
|
274
|
-
<AiFailureBanner />
|
|
275
|
-
<AiOffBanner />
|
|
276
|
-
{/* rc.351 — Surface an existing run in every mode (Phase 1 of the
|
|
277
|
-
single-data-pool work). Hidden when already on the run's view. */}
|
|
278
|
-
<LastRunBanner
|
|
279
|
-
viewingRun={view === 'crawl' || view === 'report'}
|
|
280
|
-
onView={() => useStore.getState().setView(useStore.getState().siteCrawlReport ? 'crawl' : 'report')}
|
|
281
|
-
/>
|
|
282
|
-
<div className="flex-1 overflow-y-auto" role="region" aria-label={`${view} view`}>
|
|
283
|
-
{view === 'matrix' && <MatrixView />}
|
|
284
|
-
{view === 'report' && <GeneralReportView />}
|
|
285
|
-
{view === 'delta' && <DeltaView />}
|
|
286
|
-
{view === 'activity' && <ActivityView />}
|
|
287
|
-
{view === 'guided' && <GatedView feature="igtWorkflows" label="Intelligent Guided Tests"><GuidedView /></GatedView>}
|
|
288
|
-
{view === 'flows' && <GatedView feature="flowRecorder" label="Flow recorder"><FlowsView /></GatedView>}
|
|
289
|
-
{view === 'scorecard' && <ScorecardView />}
|
|
290
|
-
{view === 'crawl' && <GatedView feature="siteCrawl" label="Site crawl"><SiteCrawlPanel /></GatedView>}
|
|
291
|
-
{view === 'forensic' && <GatedView feature="forensicLog" label="Forensic log"><ForensicLogView /></GatedView>}
|
|
292
|
-
{view === 'compliance' && <GatedView feature="wcagEmReport" label="WCAG-EM conformance report"><WcagEmView /></GatedView>}
|
|
293
|
-
{view === 'schedules' && <GatedView feature="scheduledAudits" label="Scheduled audits"><SchedulesView /></GatedView>}
|
|
294
|
-
{view === 'risk' && <GatedView feature="riskDashboard" label="Risk dashboard"><RiskView /></GatedView>}
|
|
295
|
-
{view === 'copilot' && <GatedView feature="aiCopilot" label="AI Copilot"><CopilotView /></GatedView>}
|
|
296
|
-
{view === 'ax-tree' && <GatedView feature="axTreeViewer" label="AX-tree viewer"><AxTreeView /></GatedView>}
|
|
297
|
-
{view === 'wcag3' && <GatedView feature="wcag3Preview" label="WCAG 3 preview"><Wcag3View /></GatedView>}
|
|
298
|
-
{view === 'tokens' && <GatedView feature="designSystemAudit" label="Design-system audit"><DesignSystemView /></GatedView>}
|
|
299
|
-
</div>
|
|
300
|
-
</main>
|
|
301
|
-
<ComplianceDisclaimer />
|
|
302
|
-
<AuditFooter />
|
|
303
|
-
<OnboardingModal />
|
|
304
|
-
<DebuggerBusyModal />
|
|
305
|
-
<Announcer />
|
|
306
|
-
</div>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# side-panel/
|
|
2
|
-
|
|
3
|
-
The user-facing UI surface. Spec: `phase-1-specs.md` §9 (Zustand) + §11 (panel structure).
|
|
4
|
-
|
|
5
|
-
## Component map
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
App.tsx
|
|
9
|
-
├── Header (title + version + tier badge + Upgrade/License/Settings/Support buttons + view tabs)
|
|
10
|
-
├── AuditControls (mode select, Pick/Audit-all button, Stop button, upgrade-modal trigger)
|
|
11
|
-
├── StorybookHint (banner: shown if active tab is a Storybook page on first open)
|
|
12
|
-
├── ResumeAuditBanner (banner: shown if a previous audit was interrupted)
|
|
13
|
-
├── ProgressBar (shown only while status === 'running')
|
|
14
|
-
├── ErrorBanner (shown only while status === 'failed')
|
|
15
|
-
├── <view> (matrix | delta | scorecard, picked by store.view)
|
|
16
|
-
├── AuditFooter (always-visible: tracked count + last-audit relative time)
|
|
17
|
-
├── OnboardingModal (first-run, explains chrome.debugger banner)
|
|
18
|
-
└── DebuggerBusyModal (DEBUGGER_BUSY events open this)
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## State
|
|
22
|
-
|
|
23
|
-
`store.ts` defines a single Zustand store. Slices: `auditState`, `progress`, `results`, `delta`, `componentId`, `errorMessage`, `baselineList`, `tier`, `view`. Setters are flat methods on the store.
|
|
24
|
-
|
|
25
|
-
`wire-messaging.ts` subscribes the store to messaging events:
|
|
26
|
-
- `AUDIT_PROGRESS_EVENT` → `setProgress` + `setStatus('running')`
|
|
27
|
-
- `AUDIT_COMPLETE_EVENT` → `setResults` + persist to `chrome.storage.local` for next-open hydrate
|
|
28
|
-
- `AUDIT_FAILED_EVENT` → `setError`
|
|
29
|
-
- `LICENSE_CHANGED_EVENT` → re-fetch tier
|
|
30
|
-
- `SCORECARD_UPDATED_EVENT` → re-fetch baseline list
|
|
31
|
-
|
|
32
|
-
`loadLastAudit()` runs on App mount to restore previous results.
|
|
33
|
-
|
|
34
|
-
## ErrorBoundary
|
|
35
|
-
|
|
36
|
-
Wraps the App root in `main.tsx`. Catches React render errors so a single broken view doesn't blank the whole panel. Shows a Reload button that calls `location.reload()`.
|
|
37
|
-
|
|
38
|
-
## Keepalive
|
|
39
|
-
|
|
40
|
-
`startKeepalive()` opens a `chrome.runtime.connect({ name: 'audit-keepalive' })` port on App mount. While this port is open, Chrome won't suspend the service worker — keeps in-flight audits alive.
|
|
41
|
-
|
|
42
|
-
## Reusable bits
|
|
43
|
-
|
|
44
|
-
- `IconButton` — consistent text-button styling for icon-only triggers (settings ⚙, support ?). Required `ariaLabel`.
|
|
45
|
-
- `Modal` — Radix Dialog wrapper with consistent overlay + sizing.
|
|
46
|
-
- `ViolationCard` — used by both Matrix and Delta views.
|
|
47
|
-
- `UpgradePrompt` — used by Header upgrade modal + AuditControls upgrade modal.
|
|
48
|
-
- `format-component-id.ts` — pretty-display of canonical IDs (e.g., `::story:atoms-button--primary` → "Atoms Button / Primary").
|
|
49
|
-
|
|
50
|
-
## Tier gating in the UI
|
|
51
|
-
|
|
52
|
-
Performed locally against `TIER_RULES` from `shared/tier-rules` once `tier` is in the store. No round-trip per gate check.
|
|
53
|
-
|
|
54
|
-
- `AuditControls` blocks "All stories" mode when `!isFeatureAllowed(tier, 'storybookAutoIterate')`
|
|
55
|
-
- `DeltaView` blocks "Accept as baseline" when free + already has 1 baseline; blocks Export when below paid tier
|
|
56
|
-
- `ScorecardView` shows a banner when free user is at the baseline limit
|
|
57
|
-
- `Header` shows the Upgrade CTA when tier is `trial` or `free`
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { openFallbackPanel, primeAuditLauncher } from './audit-launcher';
|
|
3
|
-
|
|
4
|
-
// chrome.sidePanel.open requires a live user-gesture and silently no-ops
|
|
5
|
-
// without one. The contract this test guards: openFallbackPanel runs
|
|
6
|
-
// SYNCHRONOUSLY relative to its caller — it does not await — so when
|
|
7
|
-
// invoked from a click handler the user-gesture is still alive at call
|
|
8
|
-
// time. Any future refactor that awaits inside this function silently
|
|
9
|
-
// breaks the bug fix it backs (Load state on narrow-breakpoint violations
|
|
10
|
-
// must open chrome.sidePanel as fallback before the viewport shrinks the
|
|
11
|
-
// overlay out of existence).
|
|
12
|
-
|
|
13
|
-
describe('openFallbackPanel', () => {
|
|
14
|
-
const openSpy = vi.fn();
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
openSpy.mockReset();
|
|
17
|
-
(globalThis as unknown as { chrome: unknown }).chrome = {
|
|
18
|
-
sidePanel: { open: openSpy.mockResolvedValue(undefined) },
|
|
19
|
-
windows: { getCurrent: vi.fn().mockResolvedValue({ id: 42 }) },
|
|
20
|
-
};
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('does nothing when not primed (no cached windowId)', () => {
|
|
24
|
-
// primeAuditLauncher hasn't run yet → no cached windowId → fallback
|
|
25
|
-
// open should silently no-op rather than throw.
|
|
26
|
-
openFallbackPanel();
|
|
27
|
-
expect(openSpy).not.toHaveBeenCalled();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('opens chrome.sidePanel with the cached windowId after priming', async () => {
|
|
31
|
-
await primeAuditLauncher();
|
|
32
|
-
openFallbackPanel();
|
|
33
|
-
expect(openSpy).toHaveBeenCalledOnce();
|
|
34
|
-
expect(openSpy).toHaveBeenCalledWith({ windowId: 42 });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('does not throw when chrome.sidePanel is missing (ui-preview / test contexts)', async () => {
|
|
38
|
-
(globalThis as unknown as { chrome: unknown }).chrome = {
|
|
39
|
-
windows: { getCurrent: vi.fn().mockResolvedValue({ id: 7 }) },
|
|
40
|
-
};
|
|
41
|
-
await primeAuditLauncher();
|
|
42
|
-
expect(() => openFallbackPanel()).not.toThrow();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('the call is synchronous — returns before chrome.sidePanel.open resolves', async () => {
|
|
46
|
-
// Promise that never resolves, simulating Chrome holding the open()
|
|
47
|
-
// promise pending. If openFallbackPanel awaited it, the caller would
|
|
48
|
-
// hang forever — the test would time out.
|
|
49
|
-
const neverResolves = new Promise<void>(() => {});
|
|
50
|
-
openSpy.mockReturnValue(neverResolves);
|
|
51
|
-
await primeAuditLauncher();
|
|
52
|
-
// No await — must return immediately even though open() never settles.
|
|
53
|
-
openFallbackPanel();
|
|
54
|
-
expect(openSpy).toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Audit launcher — wraps every START_AUDIT / START_SITE_CRAWL dispatch with
|
|
2
|
-
// an opening of chrome.sidePanel as the fallback UI.
|
|
3
|
-
//
|
|
4
|
-
// Why this exists: chrome.debugger viewport emulation during an audit affects
|
|
5
|
-
// everything in the audited tab — including our in-page sidebar overlay
|
|
6
|
-
// (which then gets squished into the emulated viewport and visually trapped).
|
|
7
|
-
// The in-page overlay therefore HIDES during audits (service-worker broadcasts
|
|
8
|
-
// SIDEBAR_HIDE_REQUEST). chrome.sidePanel is the only UI surface that renders
|
|
9
|
-
// OUTSIDE the tab's render tree, so we open it as the fallback so users still
|
|
10
|
-
// see progress + stop controls.
|
|
11
|
-
//
|
|
12
|
-
// Why this can't live in the SW: chrome.sidePanel.open() requires a user-
|
|
13
|
-
// gesture context. User gestures DO propagate through chrome.runtime.send-
|
|
14
|
-
// Message into the SW, but Chrome consumes the gesture as soon as the SW
|
|
15
|
-
// awaits anything (chrome.tabs.query, etc.). Calling chrome.sidePanel.open
|
|
16
|
-
// synchronously from the original click handler — before any await — is the
|
|
17
|
-
// only reliable path. To do that we need the windowId synchronously, so we
|
|
18
|
-
// pre-cache it on side-panel mount.
|
|
19
|
-
|
|
20
|
-
import { send } from '../shared/messaging';
|
|
21
|
-
import type { StartAuditAction, StartSiteCrawl, StartCrawlDiscovery } from '../shared/messages';
|
|
22
|
-
import { saveSiteCrawlReport } from '../shared/site-crawl-storage';
|
|
23
|
-
import { useStore } from './store';
|
|
24
|
-
|
|
25
|
-
let cachedWindowId: number | null = null;
|
|
26
|
-
|
|
27
|
-
/** Pre-fetch the side-panel's window id so audit launches can open
|
|
28
|
-
* chrome.sidePanel synchronously without burning user-gesture context on
|
|
29
|
-
* an await. Call once on side-panel mount. Idempotent. */
|
|
30
|
-
export async function primeAuditLauncher(): Promise<void> {
|
|
31
|
-
if (cachedWindowId !== null) return;
|
|
32
|
-
try {
|
|
33
|
-
const w = await chrome.windows.getCurrent();
|
|
34
|
-
if (typeof w.id === 'number') cachedWindowId = w.id;
|
|
35
|
-
} catch {
|
|
36
|
-
// chrome.windows is unavailable in some contexts (ui-preview shim) —
|
|
37
|
-
// launcher gracefully degrades; user just loses the fallback panel.
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Open chrome.sidePanel synchronously while the click-handler user-gesture
|
|
42
|
-
* is still live. Returns immediately; the open() promise resolves later but
|
|
43
|
-
* the gesture is consumed at call time, which is what matters.
|
|
44
|
-
*
|
|
45
|
-
* Exported so call sites OTHER than audit-launch (e.g. ViolationCard's
|
|
46
|
-
* Load state link, which drives the page through a narrow-breakpoint
|
|
47
|
-
* state) can pre-open the fallback before issuing their async work. */
|
|
48
|
-
export function openFallbackPanel(): void {
|
|
49
|
-
if (cachedWindowId === null) return;
|
|
50
|
-
// Guard chrome.sidePanel access — the API isn't present in non-extension
|
|
51
|
-
// contexts (tests, ui-preview).
|
|
52
|
-
const api = chrome.sidePanel;
|
|
53
|
-
if (!api || typeof api.open !== 'function') return;
|
|
54
|
-
api.open({ windowId: cachedWindowId }).catch(() => {
|
|
55
|
-
// best-effort — failures here just mean the user won't see the fallback
|
|
56
|
-
// panel, which is acceptable. The audit still runs.
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Drop-in replacement for `send(startAuditMessage)` at audit-start sites.
|
|
61
|
-
* Opens chrome.sidePanel first (preserving user-gesture), then dispatches
|
|
62
|
-
* the audit start to the service worker. Use for both START_AUDIT and
|
|
63
|
-
* START_SITE_CRAWL — both run the state-matrix audit pipeline.
|
|
64
|
-
*
|
|
65
|
-
* rc.165 — Clear the cached site-crawl report at every audit start.
|
|
66
|
-
* Cliff's mental model: "remember when I click baseline, else not."
|
|
67
|
-
* Pre-rc.165 the crawl report persisted in chrome.storage.local
|
|
68
|
-
* forever (rc.111 added the persistence to survive panel close mid-
|
|
69
|
-
* crawl), so a single-page audit run days after a crawl would pull
|
|
70
|
-
* the stale crawl into the AI fix prompt's "Site-wide context"
|
|
71
|
-
* section. Now the crawl is session-ephemeral: any new audit start
|
|
72
|
-
* (single-page OR a fresh crawl) wipes the cached report. If the
|
|
73
|
-
* audit is itself a crawl, the new crawl will write a fresh report
|
|
74
|
-
* at completion. */
|
|
75
|
-
export function launchAudit(msg: StartAuditAction | StartSiteCrawl): void {
|
|
76
|
-
openFallbackPanel();
|
|
77
|
-
// Clear in-memory store first so any UI reads the empty state
|
|
78
|
-
// immediately, then clear persisted storage (best-effort).
|
|
79
|
-
useStore.getState().setSiteCrawlReport(null);
|
|
80
|
-
void saveSiteCrawlReport(null).catch(() => {
|
|
81
|
-
// Storage clears are best-effort; the in-memory clear above is the
|
|
82
|
-
// user-visible source of truth for the current session.
|
|
83
|
-
});
|
|
84
|
-
void send(msg);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** rc.344 — Launch the discovery-only pass for the review-before-audit
|
|
88
|
-
* flow. Same gesture-preserving fallback-panel open as launchAudit (the
|
|
89
|
-
* discovery pass navigates the target tab, so the in-page overlay hides
|
|
90
|
-
* and the side panel is the user's progress surface), then dispatches
|
|
91
|
-
* START_CRAWL_DISCOVERY. Clears the stale crawl report too — same
|
|
92
|
-
* "new crawl start wipes the old report" mental model as launchAudit. */
|
|
93
|
-
export function launchDiscovery(msg: StartCrawlDiscovery): void {
|
|
94
|
-
openFallbackPanel();
|
|
95
|
-
useStore.getState().setSiteCrawlReport(null);
|
|
96
|
-
void saveSiteCrawlReport(null).catch(() => {});
|
|
97
|
-
void send(msg);
|
|
98
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// rc.139 — G9 — Unit tests for the Azure DevOps issue URL builder.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect } from 'vitest';
|
|
4
|
-
import { buildAzureDevOpsIssueUrl } from './azure-devops-issue';
|
|
5
|
-
import type { Violation } from '../types/audit';
|
|
6
|
-
|
|
7
|
-
function v(over: Partial<Violation> = {}): Violation {
|
|
8
|
-
return {
|
|
9
|
-
ruleId: 'image-alt', wcagCriterion: '1.1.1', wcagLevel: 'A', impact: 'serious',
|
|
10
|
-
description: 'Images must have alt', helpUrl: 'https://x',
|
|
11
|
-
target: { selector: 'img.hero', outerHTML: '<img>', failureSummary: '', tagName: 'IMG', role: null, accessibleName: null, textSnippet: null, attrId: null, attrTestid: null },
|
|
12
|
-
componentId: '::c', currentState: { pseudoState: 'default', theme: 'light', direction: 'ltr', viewport: 'desktop' } as unknown as Violation['currentState'],
|
|
13
|
-
axeVersion: '4.11.4', detectedAt: '2026-05-22T00:00:00Z', matchKey: 'mk',
|
|
14
|
-
...over,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('buildAzureDevOpsIssueUrl (rc.139)', () => {
|
|
19
|
-
it('builds a URL targeting the Bug type by default', () => {
|
|
20
|
-
const r = buildAzureDevOpsIssueUrl('https://dev.azure.com/myorg/myproject', '::c', [v()]);
|
|
21
|
-
expect(r.url.startsWith('https://dev.azure.com/myorg/myproject/_workitems/create/Bug?')).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('honors a custom work-item type', () => {
|
|
25
|
-
const r = buildAzureDevOpsIssueUrl(
|
|
26
|
-
'https://dev.azure.com/myorg/myproject',
|
|
27
|
-
'::c',
|
|
28
|
-
[v()],
|
|
29
|
-
'Accessibility Defect',
|
|
30
|
-
);
|
|
31
|
-
expect(r.url).toContain('/_workitems/create/Accessibility%20Defect?');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('embeds title + description as bracketed params', () => {
|
|
35
|
-
const r = buildAzureDevOpsIssueUrl('https://dev.azure.com/myorg/myproject', '::c', [v()]);
|
|
36
|
-
// URLSearchParams encodes the bracket bytes; verify presence in the URL.
|
|
37
|
-
expect(r.url).toMatch(/%5BTitle%5D=/);
|
|
38
|
-
expect(r.url).toMatch(/%5BDescription%5D=/);
|
|
39
|
-
expect(r.title).toContain('1 new violation');
|
|
40
|
-
expect(r.description).toContain('image-alt');
|
|
41
|
-
expect(r.description).toContain('1.1.1');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('caps URL length at 7800', () => {
|
|
45
|
-
// Build a huge violation set to overflow.
|
|
46
|
-
const many = Array.from({ length: 200 }, (_, i) =>
|
|
47
|
-
v({
|
|
48
|
-
ruleId: `rule-${i}`,
|
|
49
|
-
target: { ...v().target, selector: `.really.long.selector.with.lots.of.classes.number-${i}` },
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
const r = buildAzureDevOpsIssueUrl('https://dev.azure.com/org/proj', '::c', many);
|
|
53
|
-
expect(r.url.length).toBeLessThanOrEqual(7800);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('dedupes by rule + selector + aggregates state labels', () => {
|
|
57
|
-
const viols = [
|
|
58
|
-
v({ matchKey: 'a', currentState: { pseudoState: 'default', theme: 'light', direction: 'ltr', viewport: 'desktop' } as unknown as Violation['currentState'] }),
|
|
59
|
-
v({ matchKey: 'b', currentState: { pseudoState: 'hover', theme: 'light', direction: 'ltr', viewport: 'desktop' } as unknown as Violation['currentState'] }),
|
|
60
|
-
];
|
|
61
|
-
const r = buildAzureDevOpsIssueUrl('https://dev.azure.com/org/proj', '::c', viols);
|
|
62
|
-
// Single ticket section since the rule + selector matched.
|
|
63
|
-
expect(r.description.match(/### `image-alt`/g)?.length).toBe(1);
|
|
64
|
-
// But both states aggregated.
|
|
65
|
-
expect(r.description).toContain(':default');
|
|
66
|
-
expect(r.description).toContain(':hover');
|
|
67
|
-
});
|
|
68
|
-
});
|