@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.
Files changed (32) hide show
  1. package/dist/assets/{ErrorBoundary-BH1wbHb3.js → ErrorBoundary-Ck0XMUiQ.js} +22 -22
  2. package/dist/assets/{copy-ai-fixer-prompt-FO0N3IMf.js → copy-ai-fixer-prompt-CKp52Td1.js} +2 -2
  3. package/dist/assets/{devtools-panel-BAHfgRsc.js → devtools-panel-Dw0aFhIc.js} +1 -1
  4. package/dist/assets/{parallel-tab-flow-C4bjrDqL.js → parallel-tab-flow-BuZZIdRb.js} +1 -1
  5. package/dist/assets/{scheduled-audit-runner-XCDyFhY6.js → scheduled-audit-runner-9R-aGzwO.js} +137 -137
  6. package/dist/assets/{service-worker.ts-57FwiS_-.js → service-worker.ts-CyL2aQvI.js} +2 -2
  7. package/dist/assets/{side-panel-Dl247dJv.js → side-panel-CuEUwRBD.js} +3 -3
  8. package/dist/assets/{site-report-renderer-CNWZjbOd.js → site-report-renderer-46_JyAkB.js} +1 -1
  9. package/dist/devtools/panel.html +3 -3
  10. package/dist/manifest.json +3 -4
  11. package/dist/service-worker-loader.js +1 -1
  12. package/dist/side-panel/side-panel.html +4 -4
  13. package/package.json +1 -1
  14. package/dist/side-panel/App.tsx +0 -308
  15. package/dist/side-panel/README.md +0 -57
  16. package/dist/side-panel/audit-launcher.test.ts +0 -56
  17. package/dist/side-panel/audit-launcher.ts +0 -98
  18. package/dist/side-panel/azure-devops-issue.test.ts +0 -68
  19. package/dist/side-panel/azure-devops-issue.ts +0 -89
  20. package/dist/side-panel/format-component-id.test.ts +0 -89
  21. package/dist/side-panel/format-component-id.ts +0 -40
  22. package/dist/side-panel/github-issue.test.ts +0 -102
  23. package/dist/side-panel/github-issue.ts +0 -66
  24. package/dist/side-panel/gitlab-issue.test.ts +0 -53
  25. package/dist/side-panel/gitlab-issue.ts +0 -78
  26. package/dist/side-panel/jira-issue.ts +0 -64
  27. package/dist/side-panel/main.tsx +0 -86
  28. package/dist/side-panel/store.ts +0 -435
  29. package/dist/side-panel/styles.css +0 -55
  30. package/dist/side-panel/use-audit-view-model.ts +0 -116
  31. package/dist/side-panel/wire-messaging.test.ts +0 -234
  32. 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-Dl247dJv.js"></script>
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-XCDyFhY6.js">
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-BH1wbHb3.js">
18
- <link rel="modulepreload" crossorigin href="/assets/copy-ai-fixer-prompt-FO0N3IMf.js">
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.366",
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",
@@ -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
- });