@accesslint/storybook-addon 0.6.9 → 0.8.0

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/README.md CHANGED
@@ -7,7 +7,6 @@ Catch accessibility violations in your Storybook stories as you develop. Powered
7
7
 
8
8
  <img width="637" height="414" alt="Storybook screenshot with alt text violation in the details of the AccessLint tab" src="https://github.com/user-attachments/assets/01d2de92-0769-4564-8971-f6edc1986010" />
9
9
 
10
-
11
10
  ## Getting Started
12
11
 
13
12
  ```sh
@@ -18,52 +17,138 @@ Add the addon to your `.storybook/main.ts` (or `.storybook/main.js`):
18
17
 
19
18
  ```ts
20
19
  const config = {
21
- addons: ["@storybook/addon-vitest", "@accesslint/storybook-addon"],
20
+ addons: ["@accesslint/storybook-addon"],
22
21
  };
23
22
 
24
23
  export default config;
25
24
  ```
26
25
 
27
- Add the vitest plugin to your `vite.config.ts`:
26
+ Restart Storybook and an **AccessLint** panel will appear in the addon bar. Every story is audited automatically after it renders.
27
+
28
+ ## Vitest integration
29
+
30
+ If you use [`@storybook/addon-vitest`](https://storybook.js.org/docs/writing-tests/vitest-plugin), add the AccessLint plugin next to `storybookTest()` in your Vite config:
28
31
 
29
32
  ```ts
30
- import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
31
33
  import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
32
34
 
33
- export default defineConfig({
34
- plugins: [
35
- storybookTest({ configDir: ".storybook" }),
36
- accesslintTest(),
37
- ],
35
+ // Inside your Storybook test project:
36
+ plugins: [
37
+ storybookTest({ configDir: ".storybook" }),
38
+ accesslintTest(),
39
+ ],
40
+ ```
41
+
42
+ This gives you:
43
+
44
+ - Per-story status dots in the sidebar (green/yellow/red)
45
+ - A test widget in the sidebar's testing module
46
+ - The `toBeAccessible()` matcher registered automatically
47
+ - Accessibility results in CI alongside your component tests
48
+
49
+ ## Accessibility assertions
50
+
51
+ Use `toBeAccessible()` to make accessibility a first-class assertion in your tests and play functions.
52
+
53
+ ### With the Vitest plugin
54
+
55
+ If you added `accesslintTest()` above, the matcher is already registered. Use it directly in play functions:
56
+
57
+ ```ts
58
+ import { expect } from "storybook/test";
59
+
60
+ export const Default = {
61
+ play: async ({ canvasElement }) => {
62
+ await expect(canvasElement).toBeAccessible();
63
+ },
64
+ };
65
+ ```
66
+
67
+ ### Without the Vitest plugin
68
+
69
+ For play functions or standalone tests without the plugin, import the matchers entry point to register `toBeAccessible()`:
70
+
71
+ ```ts
72
+ import "@accesslint/storybook-addon/matchers";
73
+ ```
74
+
75
+ Then use it in a play function:
76
+
77
+ ```ts
78
+ import { expect } from "storybook/test";
79
+ import "@accesslint/storybook-addon/matchers";
80
+
81
+ export const Default = {
82
+ play: async ({ canvasElement }) => {
83
+ await expect(canvasElement).toBeAccessible();
84
+ },
85
+ };
86
+ ```
87
+
88
+ Or in a standalone Vitest/Jest test:
89
+
90
+ ```ts
91
+ import "@accesslint/storybook-addon/matchers";
92
+ import { render } from "@testing-library/react";
93
+
94
+ test("LoginForm is accessible", () => {
95
+ const { container } = render(<LoginForm />);
96
+ expect(container).toBeAccessible();
97
+ });
98
+ ```
99
+
100
+ ### Disabling rules per assertion
101
+
102
+ ```ts
103
+ await expect(canvasElement).toBeAccessible({
104
+ disabledRules: ["accesslint-045"],
38
105
  });
39
106
  ```
40
107
 
41
- Restart Storybook and an **AccessLint** panel will appear in the addon bar.
108
+ ### Failure output
42
109
 
43
- ## Usage
110
+ When the assertion fails, the error message lists each violation with its rule ID, WCAG criteria, conformance level, message, and the CSS selector of the failing element:
44
111
 
45
- The addon audits each story after it renders and displays violations sorted by severity. Expand any violation to see:
112
+ ```
113
+ Expected element to have no accessibility violations, but found 2:
114
+
115
+ accesslint-001 [A] (1.1.1): Image is missing alt text
116
+ img[src="hero.png"]
117
+
118
+ accesslint-012 [A] (1.3.1): Form input is missing a label
119
+ input[type="email"]
120
+ ```
121
+
122
+ ### TypeScript support
123
+
124
+ Add the type reference to your `tsconfig.json`:
46
125
 
47
- - **Impact level** — critical, serious, moderate, or minor
48
- - **WCAG criteria** and conformance level (A, AA, AAA)
49
- - **How to fix** guidance for each rule
50
- - **Element HTML** snippet of the failing element
126
+ ```json
127
+ {
128
+ "compilerOptions": {
129
+ "types": ["@accesslint/storybook-addon/matchers"]
130
+ }
131
+ }
132
+ ```
133
+
134
+ Or add a triple-slash reference in a `.d.ts` file:
135
+
136
+ ```ts
137
+ /// <reference types="@accesslint/storybook-addon/matchers" />
138
+ ```
51
139
 
52
140
  ## Configuration
53
141
 
54
- ### Parameters
142
+ ### Test mode
55
143
 
56
- Control AccessLint behavior per-story or globally via `parameters.accesslint`:
144
+ Control how violations are reported via `parameters.accesslint`:
57
145
 
58
146
  ```ts
59
- // .storybook/preview.ts
147
+ // .storybook/preview.ts — applies to all stories
60
148
  const preview = {
61
149
  parameters: {
62
150
  accesslint: {
63
- // 'todo' - show violations as warnings in the test UI
64
- // 'error' - fail CI on violations
65
- // 'off' - skip checks entirely
66
- test: "todo",
151
+ test: "todo", // "error" (default) | "todo" | "off"
67
152
  },
68
153
  },
69
154
  };
@@ -71,9 +156,25 @@ const preview = {
71
156
  export default preview;
72
157
  ```
73
158
 
159
+ | Mode | Behavior |
160
+ | --- | --- |
161
+ | `"error"` | Violations fail the test (default) |
162
+ | `"todo"` | Violations show as warnings — yellow sidebar dots, non-blocking in CI |
163
+ | `"off"` | Skip auditing entirely |
164
+
165
+ Override per-story:
166
+
167
+ ```ts
168
+ export const Experimental = {
169
+ parameters: {
170
+ accesslint: { test: "off" },
171
+ },
172
+ };
173
+ ```
174
+
74
175
  ### Disabling rules
75
176
 
76
- Disable specific rules in your preview file:
177
+ Disable specific rules globally in your preview file:
77
178
 
78
179
  ```ts
79
180
  // .storybook/preview.ts
@@ -84,10 +185,110 @@ configureRules({
84
185
  });
85
186
  ```
86
187
 
188
+ ### Skipping stories with tags
189
+
190
+ Tag individual stories or entire components with `"no-a11y"` to skip auditing:
191
+
192
+ ```ts
193
+ // Skip a single story
194
+ export const Prototype = {
195
+ tags: ["no-a11y"],
196
+ };
197
+
198
+ // Skip all stories for a component
199
+ export default {
200
+ component: ExperimentalWidget,
201
+ tags: ["no-a11y"],
202
+ };
203
+ ```
204
+
205
+ With the Vitest plugin, you can also define custom skip tags:
206
+
207
+ ```ts
208
+ accesslintTest({
209
+ tags: { skip: ["no-a11y", "wip"] },
210
+ });
211
+ ```
212
+
213
+ ## Portable stories
214
+
215
+ Use AccessLint with [`composeStories`](https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest) outside of Storybook (plain Vitest, Jest, or Playwright CT).
216
+
217
+ In your test setup file, pass the AccessLint annotations to `setProjectAnnotations`:
218
+
219
+ ```ts
220
+ // vitest.setup.ts
221
+ import { setProjectAnnotations } from "@storybook/react";
222
+ import { enableAccessLint } from "@accesslint/storybook-addon/portable";
223
+ import * as previewAnnotations from "./.storybook/preview";
224
+
225
+ const project = setProjectAnnotations([
226
+ previewAnnotations,
227
+ enableAccessLint(),
228
+ ]);
229
+
230
+ beforeAll(project.beforeAll);
231
+ ```
232
+
233
+ Then in your tests:
234
+
235
+ ```ts
236
+ import { composeStories } from "@storybook/react";
237
+ import * as stories from "./Button.stories";
238
+
239
+ const { Primary } = composeStories(stories);
240
+
241
+ test("Primary button is accessible", async () => {
242
+ await Primary.run();
243
+ // AccessLint afterEach runs automatically via the annotations
244
+ });
245
+ ```
246
+
247
+ ## API reference
248
+
249
+ ### Exports
250
+
251
+ | Entry point | Description |
252
+ | --- | --- |
253
+ | `@accesslint/storybook-addon` | Main addon registration (manager + preview) |
254
+ | `@accesslint/storybook-addon/vitest-plugin` | `accesslintTest()` Vite plugin for Vitest integration |
255
+ | `@accesslint/storybook-addon/vitest-setup` | Setup file registered by the Vite plugin |
256
+ | `@accesslint/storybook-addon/matchers` | `toBeAccessible()` custom matcher |
257
+ | `@accesslint/storybook-addon/portable` | `enableAccessLint()` for portable stories |
258
+ | `@accesslint/storybook-addon/preview` | Preview annotations (afterEach hook) |
259
+
260
+ ### `accesslintTest(options?)`
261
+
262
+ Vite plugin that registers AccessLint's `afterEach` annotation and the `toBeAccessible()` matcher for Vitest story tests.
263
+
264
+ | Option | Type | Description |
265
+ | --- | --- | --- |
266
+ | `tags.skip` | `string[]` | Stories with any of these tags will not be audited |
267
+
268
+ ### `parameters.accesslint`
269
+
270
+ | Parameter | Type | Default | Description |
271
+ | --- | --- | --- | --- |
272
+ | `test` | `"todo" \| "error" \| "off"` | `"error"` | Controls how violations are reported |
273
+ | `disable` | `boolean` | `false` | Set to `true` to skip auditing (same as `test: "off"`) |
274
+
275
+ ### `toBeAccessible(options?)`
276
+
277
+ Custom matcher for asserting an element has no accessibility violations.
278
+
279
+ | Option | Type | Description |
280
+ | --- | --- | --- |
281
+ | `disabledRules` | `string[]` | Rule IDs to skip for this assertion |
282
+
283
+ ### `enableAccessLint()`
284
+
285
+ Returns AccessLint's preview annotations for use with `setProjectAnnotations` in portable stories setups.
286
+
87
287
  ## Compatibility
88
288
 
89
289
  | Addon version | Storybook version |
90
290
  | ------------- | ----------------- |
291
+ | 0.7.x | 10.x |
91
292
  | 0.6.x | 10.x |
92
293
 
93
294
  ## Issues
package/dist/manager.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import React, { useMemo, useState, useRef, useCallback } from 'react';
2
- import { addons, types, useChannel, useStorybookApi } from 'storybook/internal/manager-api';
3
- import { ActionList, Form } from 'storybook/internal/components';
2
+ import * as managerApi from 'storybook/internal/manager-api';
3
+ import { useChannel } from 'storybook/internal/manager-api';
4
4
  import { styled, useTheme } from 'storybook/internal/theming';
5
5
  import { STORY_CHANGED, STORY_FINISHED } from 'storybook/internal/core-events';
6
6
 
7
- // src/manager.tsx
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
8
13
 
9
14
  // src/constants.ts
10
15
  var ADDON_ID = "accesslint/a11y";
11
16
  var PARAM_KEY = "accesslint";
17
+ var STATUS_TYPE_ID = "accesslint/a11y";
12
18
  var IMPACT_COLOR = {
13
19
  critical: "#d32f2f",
14
20
  serious: "#d32f2f",
@@ -193,11 +199,31 @@ var Panel = ({ active }) => {
193
199
  };
194
200
 
195
201
  // src/manager.tsx
202
+ var { addons, types, useChannel: useChannel2, useStorybookApi } = managerApi;
196
203
  var PANEL_ID = `${ADDON_ID}/panel`;
197
204
  var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
205
+ var _getStatusStore = managerApi.experimental_getStatusStore;
206
+ var _getTestProviderStore = managerApi.experimental_getTestProviderStore;
207
+ var _useTestProviderStore = managerApi.experimental_useTestProviderStore ?? null;
208
+ var hasTestProvider = !!(_getStatusStore && _getTestProviderStore);
209
+ var statusStore = hasTestProvider ? _getStatusStore(STATUS_TYPE_ID) : null;
210
+ var testProviderStore = hasTestProvider ? _getTestProviderStore(TEST_PROVIDER_ID) : null;
211
+ if (testProviderStore && statusStore) {
212
+ testProviderStore.onClearAll(() => {
213
+ statusStore.unset();
214
+ });
215
+ }
216
+ var ActionList = null;
217
+ var Form = null;
218
+ try {
219
+ const components = __require("storybook/internal/components");
220
+ ActionList = components.ActionList ?? null;
221
+ Form = components.Form ?? null;
222
+ } catch {
223
+ }
198
224
  var Title = () => {
199
225
  const [count, setCount] = React.useState(0);
200
- useChannel({
226
+ useChannel2({
201
227
  [STORY_FINISHED]: ({ reporters }) => {
202
228
  const report = reporters.find((r) => r.type === "accesslint");
203
229
  const violations = report?.result?.violations;
@@ -218,9 +244,7 @@ var Title = () => {
218
244
  color: "inherit"
219
245
  } }, /* @__PURE__ */ React.createElement("span", { style: { color: "#fff" } }, count)));
220
246
  };
221
- var StyledActionList = styled(ActionList)({
222
- padding: 0
223
- });
247
+ var StyledActionList = ActionList ? styled(ActionList)({ padding: 0 }) : styled.div({ padding: 0 });
224
248
  var StatusDot = styled.div(
225
249
  {
226
250
  width: 6,
@@ -242,12 +266,36 @@ var StatusDot = styled.div(
242
266
  var TestProviderWidget = () => {
243
267
  const [violationCount, setViolationCount] = React.useState(null);
244
268
  const api = useStorybookApi();
245
- useChannel({
246
- [STORY_FINISHED]: ({ reporters }) => {
269
+ const providerState = _useTestProviderStore ? _useTestProviderStore((state) => state[TEST_PROVIDER_ID]) : null;
270
+ React.useEffect(() => {
271
+ if (!statusStore) return;
272
+ const unsub = statusStore.onSelect(() => {
273
+ api.setSelectedPanel(PANEL_ID);
274
+ api.togglePanel(true);
275
+ });
276
+ return unsub;
277
+ }, [api]);
278
+ useChannel2({
279
+ [STORY_FINISHED]: ({ storyId, reporters }) => {
247
280
  const report = reporters.find((r) => r.type === "accesslint");
248
281
  if (!report) return;
249
- const violations = report.result?.violations;
250
- setViolationCount(violations?.length ?? 0);
282
+ const violations = report.result?.violations ?? [];
283
+ setViolationCount(violations.length);
284
+ if (statusStore) {
285
+ const hasViolations2 = violations.length > 0;
286
+ const isWarning = report.status === "warning";
287
+ statusStore.set([{
288
+ value: hasViolations2 ? isWarning ? "status-value:warning" : "status-value:error" : "status-value:success",
289
+ typeId: STATUS_TYPE_ID,
290
+ storyId,
291
+ title: "AccessLint",
292
+ description: hasViolations2 ? `${violations.length} violation${violations.length === 1 ? "" : "s"}` : "No violations",
293
+ sidebarContextMenu: true
294
+ }]);
295
+ }
296
+ if (testProviderStore && providerState === "test-provider-state:running") {
297
+ testProviderStore.setState("test-provider-state:succeeded");
298
+ }
251
299
  }
252
300
  });
253
301
  const hasViolations = violationCount !== null && violationCount > 0;
@@ -256,16 +304,43 @@ var TestProviderWidget = () => {
256
304
  api.setSelectedPanel(PANEL_ID);
257
305
  api.togglePanel(true);
258
306
  };
259
- return /* @__PURE__ */ React.createElement(StyledActionList, null, /* @__PURE__ */ React.createElement(ActionList.Item, null, /* @__PURE__ */ React.createElement(ActionList.Action, { as: "label", readOnly: true }, /* @__PURE__ */ React.createElement(ActionList.Icon, null, /* @__PURE__ */ React.createElement(Form.Checkbox, { name: "AccessLint", checked: true, disabled: true })), /* @__PURE__ */ React.createElement(ActionList.Text, null, "AccessLint")), /* @__PURE__ */ React.createElement(
260
- ActionList.Button,
307
+ if (ActionList && Form) {
308
+ return /* @__PURE__ */ React.createElement(StyledActionList, null, /* @__PURE__ */ React.createElement(ActionList.Item, null, /* @__PURE__ */ React.createElement(ActionList.Action, { as: "label", readOnly: true }, /* @__PURE__ */ React.createElement(ActionList.Icon, null, /* @__PURE__ */ React.createElement(Form.Checkbox, { name: "AccessLint", checked: true, disabled: true })), /* @__PURE__ */ React.createElement(ActionList.Text, null, "AccessLint")), /* @__PURE__ */ React.createElement(
309
+ ActionList.Button,
310
+ {
311
+ ariaLabel: violationCount === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${violationCount} violation${violationCount === 1 ? "" : "s"}` : "AccessLint: no violations",
312
+ disabled: violationCount === null,
313
+ onClick: openPanel
314
+ },
315
+ hasViolations ? violationCount : null,
316
+ /* @__PURE__ */ React.createElement(StatusDot, { status })
317
+ )));
318
+ }
319
+ return /* @__PURE__ */ React.createElement(
320
+ StyledActionList,
261
321
  {
262
- ariaLabel: violationCount === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${violationCount} violation${violationCount === 1 ? "" : "s"}` : "AccessLint: no violations",
263
- disabled: violationCount === null,
322
+ style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", cursor: "pointer" },
264
323
  onClick: openPanel
265
324
  },
266
- hasViolations ? violationCount : null,
267
- /* @__PURE__ */ React.createElement(StatusDot, { status })
268
- )));
325
+ /* @__PURE__ */ React.createElement(StatusDot, { status }),
326
+ /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12 } }, "AccessLint"),
327
+ hasViolations && /* @__PURE__ */ React.createElement("span", { style: { fontSize: 11, fontWeight: "bold" } }, violationCount)
328
+ );
329
+ };
330
+ var SidebarContextMenu = ({ context }) => {
331
+ const api = useStorybookApi();
332
+ if (context.type !== "story" || !ActionList) return null;
333
+ return /* @__PURE__ */ React.createElement(
334
+ ActionList.Item,
335
+ {
336
+ onClick: () => {
337
+ api.selectStory(context.id);
338
+ api.setSelectedPanel(PANEL_ID);
339
+ api.togglePanel(true);
340
+ }
341
+ },
342
+ /* @__PURE__ */ React.createElement(ActionList.Text, null, "View AccessLint results")
343
+ );
269
344
  };
270
345
  addons.register(ADDON_ID, () => {
271
346
  addons.add(PANEL_ID, {
@@ -274,8 +349,11 @@ addons.register(ADDON_ID, () => {
274
349
  render: Panel,
275
350
  paramKey: PARAM_KEY
276
351
  });
277
- addons.add(TEST_PROVIDER_ID, {
278
- type: types.experimental_TEST_PROVIDER,
279
- render: () => /* @__PURE__ */ React.createElement(TestProviderWidget, null)
280
- });
352
+ if (hasTestProvider && types.experimental_TEST_PROVIDER) {
353
+ addons.add(TEST_PROVIDER_ID, {
354
+ type: types.experimental_TEST_PROVIDER,
355
+ render: () => /* @__PURE__ */ React.createElement(TestProviderWidget, null),
356
+ sidebarContextMenu: ({ context }) => /* @__PURE__ */ React.createElement(SidebarContextMenu, { context })
357
+ });
358
+ }
281
359
  });
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var matchers = require('@accesslint/vitest/matchers');
4
+
5
+
6
+
7
+ Object.defineProperty(exports, "accesslintMatchers", {
8
+ enumerable: true,
9
+ get: function () { return matchers.accesslintMatchers; }
10
+ });
11
+ Object.defineProperty(exports, "toBeAccessible", {
12
+ enumerable: true,
13
+ get: function () { return matchers.toBeAccessible; }
14
+ });
@@ -0,0 +1 @@
1
+ export { AccessibleMatcherOptions, accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
@@ -0,0 +1 @@
1
+ export { AccessibleMatcherOptions, accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
@@ -0,0 +1 @@
1
+ export { accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ var core = require('@accesslint/core');
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/preview.ts
12
+ var preview_exports = {};
13
+ __export(preview_exports, {
14
+ afterEach: () => afterEach
15
+ });
16
+ core.configureRules({
17
+ disabledRules: ["accesslint-045"]
18
+ });
19
+ function scopeViolations(violations) {
20
+ const root = document.getElementById("storybook-root");
21
+ if (!root) return violations;
22
+ return violations.filter((v) => {
23
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
24
+ try {
25
+ const el = document.querySelector(local);
26
+ return el && root.contains(el);
27
+ } catch {
28
+ return false;
29
+ }
30
+ });
31
+ }
32
+ function enrichViolations(violations) {
33
+ return violations.map((v) => {
34
+ const rule = core.getRuleById(v.ruleId);
35
+ return {
36
+ ...v,
37
+ element: void 0,
38
+ // not serializable
39
+ description: rule?.description,
40
+ wcag: rule?.wcag,
41
+ level: rule?.level,
42
+ guidance: rule?.guidance
43
+ };
44
+ });
45
+ }
46
+ var afterEach = async ({
47
+ reporting,
48
+ parameters,
49
+ viewMode,
50
+ tags
51
+ }) => {
52
+ const accesslintParam = parameters?.accesslint;
53
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
54
+ if (viewMode !== "story") return;
55
+ if (tags?.includes("no-a11y")) return;
56
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
57
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
58
+ const result = core.runAudit(document);
59
+ const scoped = scopeViolations(result.violations);
60
+ const enriched = enrichViolations(scoped);
61
+ const hasViolations = enriched.length > 0;
62
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
63
+ reporting.addReport({
64
+ type: "accesslint",
65
+ version: 1,
66
+ result: {
67
+ violations: enriched,
68
+ ruleCount: result.ruleCount
69
+ },
70
+ status: hasViolations ? mode : "passed"
71
+ });
72
+ };
73
+
74
+ // src/portable.ts
75
+ function enableAccessLint() {
76
+ return preview_exports;
77
+ }
78
+
79
+ exports.accesslintAnnotations = preview_exports;
80
+ exports.enableAccessLint = enableAccessLint;
@@ -0,0 +1,46 @@
1
+ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
2
+ reporting: {
3
+ addReport: (report: Record<string, unknown>) => void;
4
+ };
5
+ parameters: Record<string, unknown>;
6
+ viewMode: string;
7
+ tags?: string[];
8
+ }) => Promise<void>;
9
+
10
+ declare const accesslintAnnotations_afterEach: typeof afterEach;
11
+ declare namespace accesslintAnnotations {
12
+ export { accesslintAnnotations_afterEach as afterEach };
13
+ }
14
+
15
+ /**
16
+ * Portable stories helper for using AccessLint with composeStories
17
+ * outside of Storybook (plain Vitest, Jest, Playwright CT).
18
+ *
19
+ * Usage in your test setup file:
20
+ *
21
+ * import { enableAccessLint } from "@accesslint/storybook-addon/portable";
22
+ * import { setProjectAnnotations } from "@storybook/react";
23
+ * import * as previewAnnotations from "./.storybook/preview";
24
+ *
25
+ * const project = setProjectAnnotations([
26
+ * previewAnnotations,
27
+ * enableAccessLint(),
28
+ * ]);
29
+ * beforeAll(project.beforeAll);
30
+ *
31
+ * Then in tests:
32
+ *
33
+ * import { composeStories } from "@storybook/react";
34
+ * import * as stories from "./Button.stories";
35
+ *
36
+ * const { Primary } = composeStories(stories);
37
+ *
38
+ * test("is accessible", async () => {
39
+ * await Primary.run();
40
+ * // AccessLint afterEach runs automatically and reports violations
41
+ * });
42
+ */
43
+
44
+ declare function enableAccessLint(): typeof accesslintAnnotations;
45
+
46
+ export { accesslintAnnotations, enableAccessLint };
@@ -0,0 +1,46 @@
1
+ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
2
+ reporting: {
3
+ addReport: (report: Record<string, unknown>) => void;
4
+ };
5
+ parameters: Record<string, unknown>;
6
+ viewMode: string;
7
+ tags?: string[];
8
+ }) => Promise<void>;
9
+
10
+ declare const accesslintAnnotations_afterEach: typeof afterEach;
11
+ declare namespace accesslintAnnotations {
12
+ export { accesslintAnnotations_afterEach as afterEach };
13
+ }
14
+
15
+ /**
16
+ * Portable stories helper for using AccessLint with composeStories
17
+ * outside of Storybook (plain Vitest, Jest, Playwright CT).
18
+ *
19
+ * Usage in your test setup file:
20
+ *
21
+ * import { enableAccessLint } from "@accesslint/storybook-addon/portable";
22
+ * import { setProjectAnnotations } from "@storybook/react";
23
+ * import * as previewAnnotations from "./.storybook/preview";
24
+ *
25
+ * const project = setProjectAnnotations([
26
+ * previewAnnotations,
27
+ * enableAccessLint(),
28
+ * ]);
29
+ * beforeAll(project.beforeAll);
30
+ *
31
+ * Then in tests:
32
+ *
33
+ * import { composeStories } from "@storybook/react";
34
+ * import * as stories from "./Button.stories";
35
+ *
36
+ * const { Primary } = composeStories(stories);
37
+ *
38
+ * test("is accessible", async () => {
39
+ * await Primary.run();
40
+ * // AccessLint afterEach runs automatically and reports violations
41
+ * });
42
+ */
43
+
44
+ declare function enableAccessLint(): typeof accesslintAnnotations;
45
+
46
+ export { accesslintAnnotations, enableAccessLint };
@@ -0,0 +1,77 @@
1
+ import { configureRules, runAudit, getRuleById } from '@accesslint/core';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/preview.ts
10
+ var preview_exports = {};
11
+ __export(preview_exports, {
12
+ afterEach: () => afterEach
13
+ });
14
+ configureRules({
15
+ disabledRules: ["accesslint-045"]
16
+ });
17
+ function scopeViolations(violations) {
18
+ const root = document.getElementById("storybook-root");
19
+ if (!root) return violations;
20
+ return violations.filter((v) => {
21
+ const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
22
+ try {
23
+ const el = document.querySelector(local);
24
+ return el && root.contains(el);
25
+ } catch {
26
+ return false;
27
+ }
28
+ });
29
+ }
30
+ function enrichViolations(violations) {
31
+ return violations.map((v) => {
32
+ const rule = getRuleById(v.ruleId);
33
+ return {
34
+ ...v,
35
+ element: void 0,
36
+ // not serializable
37
+ description: rule?.description,
38
+ wcag: rule?.wcag,
39
+ level: rule?.level,
40
+ guidance: rule?.guidance
41
+ };
42
+ });
43
+ }
44
+ var afterEach = async ({
45
+ reporting,
46
+ parameters,
47
+ viewMode,
48
+ tags
49
+ }) => {
50
+ const accesslintParam = parameters?.accesslint;
51
+ if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
52
+ if (viewMode !== "story") return;
53
+ if (tags?.includes("no-a11y")) return;
54
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
55
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
56
+ const result = runAudit(document);
57
+ const scoped = scopeViolations(result.violations);
58
+ const enriched = enrichViolations(scoped);
59
+ const hasViolations = enriched.length > 0;
60
+ const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
61
+ reporting.addReport({
62
+ type: "accesslint",
63
+ version: 1,
64
+ result: {
65
+ violations: enriched,
66
+ ruleCount: result.ruleCount
67
+ },
68
+ status: hasViolations ? mode : "passed"
69
+ });
70
+ };
71
+
72
+ // src/portable.ts
73
+ function enableAccessLint() {
74
+ return preview_exports;
75
+ }
76
+
77
+ export { preview_exports as accesslintAnnotations, enableAccessLint };
package/dist/preview.cjs CHANGED
@@ -36,11 +36,15 @@ function enrichViolations(violations) {
36
36
  var afterEach = async ({
37
37
  reporting,
38
38
  parameters,
39
- viewMode
39
+ viewMode,
40
+ tags
40
41
  }) => {
41
42
  const accesslintParam = parameters?.accesslint;
42
43
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
43
44
  if (viewMode !== "story") return;
45
+ if (tags?.includes("no-a11y")) return;
46
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
47
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
44
48
  const result = core.runAudit(document);
45
49
  const scoped = scopeViolations(result.violations);
46
50
  const enriched = enrichViolations(scoped);
@@ -1,9 +1,10 @@
1
- declare const afterEach: ({ reporting, parameters, viewMode, }: {
1
+ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
2
2
  reporting: {
3
3
  addReport: (report: Record<string, unknown>) => void;
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
+ tags?: string[];
7
8
  }) => Promise<void>;
8
9
 
9
10
  export { afterEach };
package/dist/preview.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- declare const afterEach: ({ reporting, parameters, viewMode, }: {
1
+ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
2
2
  reporting: {
3
3
  addReport: (report: Record<string, unknown>) => void;
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
+ tags?: string[];
7
8
  }) => Promise<void>;
8
9
 
9
10
  export { afterEach };
package/dist/preview.js CHANGED
@@ -34,11 +34,15 @@ function enrichViolations(violations) {
34
34
  var afterEach = async ({
35
35
  reporting,
36
36
  parameters,
37
- viewMode
37
+ viewMode,
38
+ tags
38
39
  }) => {
39
40
  const accesslintParam = parameters?.accesslint;
40
41
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
41
42
  if (viewMode !== "story") return;
43
+ if (tags?.includes("no-a11y")) return;
44
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
45
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
42
46
  const result = runAudit(document);
43
47
  const scoped = scopeViolations(result.violations);
44
48
  const enriched = enrichViolations(scoped);
@@ -2,12 +2,12 @@
2
2
 
3
3
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
4
  // src/vitest-plugin.ts
5
- function accesslintTest() {
5
+ function accesslintTest(options) {
6
6
  const distDir = new URL(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vitest-plugin.cjs', document.baseURI).href))).pathname;
7
7
  return {
8
8
  name: "@accesslint/storybook-addon",
9
9
  config() {
10
- return {
10
+ const config = {
11
11
  server: {
12
12
  fs: {
13
13
  allow: [distDir]
@@ -17,6 +17,12 @@ function accesslintTest() {
17
17
  setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
18
18
  }
19
19
  };
20
+ if (options?.tags) {
21
+ config.define = {
22
+ "__ACCESSLINT_SKIP_TAGS__": JSON.stringify(options.tags.skip ?? [])
23
+ };
24
+ }
25
+ return config;
20
26
  }
21
27
  };
22
28
  }
@@ -11,9 +11,21 @@
11
11
  * plugins: [storybookTest(), accesslintTest()],
12
12
  * });
13
13
  */
14
- declare function accesslintTest(): {
14
+ interface AccessLintTestOptions {
15
+ /**
16
+ * Tags-based filtering for which stories to audit.
17
+ *
18
+ * accesslintTest({ tags: { skip: ["no-a11y"] } })
19
+ *
20
+ * Stories with any of the `skip` tags will not be audited.
21
+ */
22
+ tags?: {
23
+ skip?: string[];
24
+ };
25
+ }
26
+ declare function accesslintTest(options?: AccessLintTestOptions): {
15
27
  name: string;
16
28
  config: () => Record<string, unknown>;
17
29
  };
18
30
 
19
- export { accesslintTest };
31
+ export { type AccessLintTestOptions, accesslintTest };
@@ -11,9 +11,21 @@
11
11
  * plugins: [storybookTest(), accesslintTest()],
12
12
  * });
13
13
  */
14
- declare function accesslintTest(): {
14
+ interface AccessLintTestOptions {
15
+ /**
16
+ * Tags-based filtering for which stories to audit.
17
+ *
18
+ * accesslintTest({ tags: { skip: ["no-a11y"] } })
19
+ *
20
+ * Stories with any of the `skip` tags will not be audited.
21
+ */
22
+ tags?: {
23
+ skip?: string[];
24
+ };
25
+ }
26
+ declare function accesslintTest(options?: AccessLintTestOptions): {
15
27
  name: string;
16
28
  config: () => Record<string, unknown>;
17
29
  };
18
30
 
19
- export { accesslintTest };
31
+ export { type AccessLintTestOptions, accesslintTest };
@@ -1,10 +1,10 @@
1
1
  // src/vitest-plugin.ts
2
- function accesslintTest() {
2
+ function accesslintTest(options) {
3
3
  const distDir = new URL(".", import.meta.url).pathname;
4
4
  return {
5
5
  name: "@accesslint/storybook-addon",
6
6
  config() {
7
- return {
7
+ const config = {
8
8
  server: {
9
9
  fs: {
10
10
  allow: [distDir]
@@ -14,6 +14,12 @@ function accesslintTest() {
14
14
  setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
15
15
  }
16
16
  };
17
+ if (options?.tags) {
18
+ config.define = {
19
+ "__ACCESSLINT_SKIP_TAGS__": JSON.stringify(options.tags.skip ?? [])
20
+ };
21
+ }
22
+ return config;
17
23
  }
18
24
  };
19
25
  }
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ var test = require('storybook/test');
4
+ var vitest = require('vitest');
3
5
  var previewApi = require('storybook/preview-api');
4
6
  var core = require('@accesslint/core');
7
+ var matchers = require('@accesslint/vitest/matchers');
5
8
 
6
9
  var __defProp = Object.defineProperty;
7
10
  var __export = (target, all) => {
@@ -47,11 +50,15 @@ function enrichViolations(violations) {
47
50
  var afterEach = async ({
48
51
  reporting,
49
52
  parameters,
50
- viewMode
53
+ viewMode,
54
+ tags
51
55
  }) => {
52
56
  const accesslintParam = parameters?.accesslint;
53
57
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
54
58
  if (viewMode !== "story") return;
59
+ if (tags?.includes("no-a11y")) return;
60
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
61
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
55
62
  const result = core.runAudit(document);
56
63
  const scoped = scopeViolations(result.violations);
57
64
  const enriched = enrichViolations(scoped);
@@ -67,8 +74,8 @@ var afterEach = async ({
67
74
  status: hasViolations ? mode : "passed"
68
75
  });
69
76
  };
70
-
71
- // src/vitest-setup.ts
77
+ test.expect.extend(matchers.accesslintMatchers);
78
+ vitest.expect.extend(matchers.accesslintMatchers);
72
79
  var g = globalThis;
73
80
  var existing = g.globalProjectAnnotations;
74
81
  g.globalProjectAnnotations = existing ? previewApi.composeConfigs([existing, preview_exports]) : previewApi.composeConfigs([preview_exports]);
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -1,5 +1,8 @@
1
+ import { expect } from 'storybook/test';
2
+ import { expect as expect$1 } from 'vitest';
1
3
  import { composeConfigs } from 'storybook/preview-api';
2
4
  import { configureRules, runAudit, getRuleById } from '@accesslint/core';
5
+ import { accesslintMatchers } from '@accesslint/vitest/matchers';
3
6
 
4
7
  var __defProp = Object.defineProperty;
5
8
  var __export = (target, all) => {
@@ -45,11 +48,15 @@ function enrichViolations(violations) {
45
48
  var afterEach = async ({
46
49
  reporting,
47
50
  parameters,
48
- viewMode
51
+ viewMode,
52
+ tags
49
53
  }) => {
50
54
  const accesslintParam = parameters?.accesslint;
51
55
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
52
56
  if (viewMode !== "story") return;
57
+ if (tags?.includes("no-a11y")) return;
58
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
59
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
53
60
  const result = runAudit(document);
54
61
  const scoped = scopeViolations(result.violations);
55
62
  const enriched = enrichViolations(scoped);
@@ -65,8 +72,8 @@ var afterEach = async ({
65
72
  status: hasViolations ? mode : "passed"
66
73
  });
67
74
  };
68
-
69
- // src/vitest-setup.ts
75
+ expect.extend(accesslintMatchers);
76
+ expect$1.extend(accesslintMatchers);
70
77
  var g = globalThis;
71
78
  var existing = g.globalProjectAnnotations;
72
79
  g.globalProjectAnnotations = existing ? composeConfigs([existing, preview_exports]) : composeConfigs([preview_exports]);
package/matchers.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AccessibleMatcherOptions } from "@accesslint/vitest/matchers";
2
+
3
+ declare module "vitest" {
4
+ interface Assertion<T> {
5
+ toBeAccessible(options?: AccessibleMatcherOptions): void;
6
+ }
7
+ interface AsymmetricMatchersContaining {
8
+ toBeAccessible(options?: AccessibleMatcherOptions): void;
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accesslint/storybook-addon",
3
- "version": "0.6.9",
3
+ "version": "0.8.0",
4
4
  "description": "Catch accessibility violations in your Storybook stories as you develop",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -30,8 +30,17 @@
30
30
  "require": "./dist/vitest-plugin.cjs"
31
31
  },
32
32
  "./vitest-setup": {
33
+ "types": "./dist/vitest-setup.d.ts",
33
34
  "import": "./dist/vitest-setup.js",
34
35
  "require": "./dist/vitest-setup.cjs"
36
+ },
37
+ "./matchers": {
38
+ "import": "./dist/matchers.js",
39
+ "require": "./dist/matchers.cjs"
40
+ },
41
+ "./portable": {
42
+ "import": "./dist/portable.js",
43
+ "require": "./dist/portable.cjs"
35
44
  }
36
45
  },
37
46
  "main": "dist/index.cjs",
@@ -49,17 +58,19 @@
49
58
  "typecheck": "tsc --noEmit"
50
59
  },
51
60
  "dependencies": {
52
- "@accesslint/core": "^0.6.5"
61
+ "@accesslint/core": "^0.6.5",
62
+ "@accesslint/vitest": "^0.1.3"
53
63
  },
54
64
  "devDependencies": {
55
65
  "react": "^18.2.0",
56
66
  "react-dom": "^18.2.0",
57
67
  "storybook": "^10.2.0",
58
68
  "tsup": "^8.4.0",
59
- "typescript": "^5.7.0"
69
+ "typescript": "^5.7.0",
70
+ "vitest": "^3.0.0"
60
71
  },
61
72
  "peerDependencies": {
62
- "storybook": "^10.0.0"
73
+ "storybook": "^9.0.0 || ^10.0.0"
63
74
  },
64
75
  "keywords": [
65
76
  "storybook-addon",