@accesslint/storybook-addon 0.7.0 → 0.8.2

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
@@ -17,153 +17,111 @@ Add the addon to your `.storybook/main.ts` (or `.storybook/main.js`):
17
17
 
18
18
  ```ts
19
19
  const config = {
20
- addons: ["@storybook/addon-vitest", "@accesslint/storybook-addon"],
20
+ addons: ["@accesslint/storybook-addon"],
21
21
  };
22
22
 
23
23
  export default config;
24
24
  ```
25
25
 
26
- 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:
27
31
 
28
32
  ```ts
29
- import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
30
33
  import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
31
34
 
32
- export default defineConfig({
33
- test: {
34
- projects: [
35
- {
36
- plugins: [
37
- storybookTest({ configDir: ".storybook" }),
38
- accesslintTest(),
39
- ],
40
- test: {
41
- name: "storybook",
42
- browser: {
43
- enabled: true,
44
- headless: true,
45
- provider: playwright({}),
46
- instances: [{ browser: "chromium" }],
47
- },
48
- setupFiles: [".storybook/vitest.setup.ts"],
49
- },
50
- },
51
- ],
52
- },
53
- });
35
+ // Inside your Storybook test project:
36
+ plugins: [
37
+ storybookTest({ configDir: ".storybook" }),
38
+ accesslintTest(),
39
+ ],
54
40
  ```
55
41
 
56
- Restart Storybook and an **AccessLint** panel will appear in the addon bar.
57
-
58
- ## Usage
42
+ This gives you:
59
43
 
60
- The addon audits each story after it renders and displays violations sorted by severity. Expand any violation to see:
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
61
48
 
62
- - **Impact level** — critical, serious, moderate, or minor
63
- - **WCAG criteria** and conformance level (A, AA, AAA)
64
- - **How to fix** guidance for each rule
65
- - **Element HTML** snippet of the failing element
49
+ ## Accessibility assertions
66
50
 
67
- ### Sidebar status indicators
51
+ Use `toBeAccessible()` to make accessibility a first-class assertion in your tests and play functions.
68
52
 
69
- Each story gets a colored dot in the sidebar tree showing its accessibility status:
53
+ ### With the Vitest plugin
70
54
 
71
- - Green no violations
72
- - Yellow — violations present, but running in `"todo"` mode (warnings)
73
- - Red — violations present in `"error"` mode (failures)
74
-
75
- Click a status dot to jump to the AccessLint panel for that story. Right-click any story in the sidebar to access "View AccessLint results".
76
-
77
- ### Test widget
78
-
79
- The AccessLint test provider widget appears in the sidebar's testing module alongside Storybook's built-in component tests. It shows the current story's violation count and responds to the global "Run all" and "Clear all" buttons.
80
-
81
- ## Configuration
82
-
83
- ### Parameters
84
-
85
- Control AccessLint behavior per-story or globally via `parameters.accesslint`:
55
+ If you added `accesslintTest()` above, the matcher is already registered. Use it directly in play functions:
86
56
 
87
57
  ```ts
88
- // .storybook/preview.ts
89
- const preview = {
90
- parameters: {
91
- accesslint: {
92
- // 'todo' - show violations as warnings in the test UI (non-blocking)
93
- // 'error' - fail CI on violations
94
- // 'off' - skip checks entirely
95
- test: "todo",
96
- },
97
- },
98
- };
99
-
100
- export default preview;
101
- ```
102
-
103
- Override per-story:
58
+ import { expect } from "storybook/test";
104
59
 
105
- ```ts
106
- export const Experimental = {
107
- parameters: {
108
- accesslint: { test: "off" },
60
+ export const Default = {
61
+ play: async ({ canvasElement }) => {
62
+ await expect(canvasElement).toBeAccessible();
109
63
  },
110
64
  };
111
65
  ```
112
66
 
113
- ### Disabling rules
67
+ ### Without the Vitest plugin
114
68
 
115
- Disable specific rules in your preview file:
69
+ For play functions or standalone tests without the plugin, import the matchers entry point to register `toBeAccessible()`:
116
70
 
117
71
  ```ts
118
- // .storybook/preview.ts
119
- import { configureRules } from "@accesslint/core";
120
-
121
- configureRules({
122
- disabledRules: ["accesslint-045"], // e.g. disable landmark region rule
123
- });
72
+ import "@accesslint/storybook-addon/matchers";
124
73
  ```
125
74
 
126
- ### Skipping stories with tags
127
-
128
- Tag stories with `"no-a11y"` to skip AccessLint auditing:
75
+ Then use it in a play function:
129
76
 
130
77
  ```ts
131
- export const Prototype = {
132
- tags: ["no-a11y"],
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
+ },
133
85
  };
134
86
  ```
135
87
 
136
- The tag can also be set at the component level to skip all stories for a component:
88
+ Or in a standalone Vitest/Jest test:
137
89
 
138
90
  ```ts
139
- export default {
140
- title: "Prototypes/ExperimentalWidget",
141
- component: ExperimentalWidget,
142
- tags: ["no-a11y"],
143
- };
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
+ });
144
98
  ```
145
99
 
146
- You can also configure the `accesslintTest()` plugin with custom skip tags:
100
+ ### Disabling rules per assertion
147
101
 
148
102
  ```ts
149
- accesslintTest({
150
- tags: { skip: ["no-a11y", "wip"] },
103
+ await expect(canvasElement).toBeAccessible({
104
+ disabledRules: ["accesslint-045"],
151
105
  });
152
106
  ```
153
107
 
154
- ## Accessibility assertions in play functions
108
+ ### Failure output
155
109
 
156
- The `toBeAccessible()` matcher lets you make accessibility a first-class assertion in interaction tests and play functions.
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:
157
111
 
158
- ### Setup
112
+ ```
113
+ Expected element to have no accessibility violations, but found 2:
159
114
 
160
- Import the matchers entry point in your test setup or directly in a story file:
115
+ accesslint-001 [A] (1.1.1): Image is missing alt text
116
+ img[src="hero.png"]
161
117
 
162
- ```ts
163
- import "@accesslint/storybook-addon/matchers";
118
+ accesslint-012 [A] (1.3.1): Form input is missing a label
119
+ input[type="email"]
164
120
  ```
165
121
 
166
- For TypeScript support, add the type reference to your `tsconfig.json`:
122
+ ### TypeScript support
123
+
124
+ Add the type reference to your `tsconfig.json`:
167
125
 
168
126
  ```json
169
127
  {
@@ -179,60 +137,82 @@ Or add a triple-slash reference in a `.d.ts` file:
179
137
  /// <reference types="@accesslint/storybook-addon/matchers" />
180
138
  ```
181
139
 
182
- ### Usage in a play function
140
+ ## Configuration
183
141
 
184
- ```ts
185
- import { expect } from "storybook/test";
186
- import "@accesslint/storybook-addon/matchers";
142
+ ### Test mode
187
143
 
188
- export const Default = {
189
- play: async ({ canvasElement }) => {
190
- await expect(canvasElement).toBeAccessible();
144
+ Control how violations are reported via `parameters.accesslint`:
145
+
146
+ ```ts
147
+ // .storybook/preview.ts — applies to all stories
148
+ const preview = {
149
+ parameters: {
150
+ accesslint: {
151
+ test: "todo", // "error" (default) | "todo" | "off"
152
+ },
191
153
  },
192
154
  };
155
+
156
+ export default preview;
193
157
  ```
194
158
 
195
- ### Usage in a standalone Vitest test
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 |
196
164
 
197
- ```ts
198
- import "@accesslint/storybook-addon/matchers";
199
- import { render } from "@testing-library/react";
165
+ Override per-story:
200
166
 
201
- test("LoginForm is accessible", () => {
202
- const { container } = render(<LoginForm />);
203
- expect(container).toBeAccessible();
204
- });
167
+ ```ts
168
+ export const Experimental = {
169
+ parameters: {
170
+ accesslint: { test: "off" },
171
+ },
172
+ };
205
173
  ```
206
174
 
207
- ### Options
175
+ ### Disabling rules
208
176
 
209
- Pass options to disable specific rules for a single assertion:
177
+ Disable specific rules globally in your preview file:
210
178
 
211
179
  ```ts
212
- await expect(canvasElement).toBeAccessible({
213
- disabledRules: ["accesslint-045"],
180
+ // .storybook/preview.ts
181
+ import { configureRules } from "@accesslint/core";
182
+
183
+ configureRules({
184
+ disabledRules: ["accesslint-045"], // e.g. disable landmark region rule
214
185
  });
215
186
  ```
216
187
 
217
- ### Failure output
188
+ ### Skipping stories with tags
218
189
 
219
- 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:
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
+ };
220
197
 
198
+ // Skip all stories for a component
199
+ export default {
200
+ component: ExperimentalWidget,
201
+ tags: ["no-a11y"],
202
+ };
221
203
  ```
222
- Expected element to have no accessibility violations, but found 2:
223
204
 
224
- accesslint-001 [A] (1.1.1): Image is missing alt text
225
- img[src="hero.png"]
205
+ With the Vitest plugin, you can also define custom skip tags:
226
206
 
227
- accesslint-012 [A] (1.3.1): Form input is missing a label
228
- input[type="email"]
207
+ ```ts
208
+ accesslintTest({
209
+ tags: { skip: ["no-a11y", "wip"] },
210
+ });
229
211
  ```
230
212
 
231
213
  ## Portable stories
232
214
 
233
- Use AccessLint with `composeStories` outside of Storybook (plain Vitest, Jest, or Playwright CT).
234
-
235
- ### Setup
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).
236
216
 
237
217
  In your test setup file, pass the AccessLint annotations to `setProjectAnnotations`:
238
218
 
@@ -250,7 +230,7 @@ const project = setProjectAnnotations([
250
230
  beforeAll(project.beforeAll);
251
231
  ```
252
232
 
253
- ### Usage
233
+ Then in your tests:
254
234
 
255
235
  ```ts
256
236
  import { composeStories } from "@storybook/react";
@@ -273,22 +253,13 @@ test("Primary button is accessible", async () => {
273
253
  | `@accesslint/storybook-addon` | Main addon registration (manager + preview) |
274
254
  | `@accesslint/storybook-addon/vitest-plugin` | `accesslintTest()` Vite plugin for Vitest integration |
275
255
  | `@accesslint/storybook-addon/vitest-setup` | Setup file registered by the Vite plugin |
276
- | `@accesslint/storybook-addon/matchers` | `toBeAccessible()` custom Vitest/Jest matcher |
256
+ | `@accesslint/storybook-addon/matchers` | `toBeAccessible()` custom matcher |
277
257
  | `@accesslint/storybook-addon/portable` | `enableAccessLint()` for portable stories |
278
258
  | `@accesslint/storybook-addon/preview` | Preview annotations (afterEach hook) |
279
259
 
280
260
  ### `accesslintTest(options?)`
281
261
 
282
- Vite plugin that registers AccessLint's `afterEach` annotation for Vitest story tests.
283
-
284
- ```ts
285
- import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
286
-
287
- accesslintTest();
288
- accesslintTest({ tags: { skip: ["no-a11y"] } });
289
- ```
290
-
291
- **Options:**
262
+ Vite plugin that registers AccessLint's `afterEach` annotation and the `toBeAccessible()` matcher for Vitest story tests.
292
263
 
293
264
  | Option | Type | Description |
294
265
  | --- | --- | --- |
@@ -298,20 +269,13 @@ accesslintTest({ tags: { skip: ["no-a11y"] } });
298
269
 
299
270
  | Parameter | Type | Default | Description |
300
271
  | --- | --- | --- | --- |
301
- | `test` | `"todo" \| "error" \| "off"` | `"error"` | `"todo"` reports violations as warnings. `"error"` fails the test. `"off"` skips auditing. |
272
+ | `test` | `"todo" \| "error" \| "off"` | `"error"` | Controls how violations are reported |
302
273
  | `disable` | `boolean` | `false` | Set to `true` to skip auditing (same as `test: "off"`) |
303
274
 
304
275
  ### `toBeAccessible(options?)`
305
276
 
306
277
  Custom matcher for asserting an element has no accessibility violations.
307
278
 
308
- ```ts
309
- expect(element).toBeAccessible();
310
- expect(element).toBeAccessible({ disabledRules: ["accesslint-045"] });
311
- ```
312
-
313
- **Options:**
314
-
315
279
  | Option | Type | Description |
316
280
  | --- | --- | --- |
317
281
  | `disabledRules` | `string[]` | Rule IDs to skip for this assertion |
@@ -320,14 +284,11 @@ expect(element).toBeAccessible({ disabledRules: ["accesslint-045"] });
320
284
 
321
285
  Returns AccessLint's preview annotations for use with `setProjectAnnotations` in portable stories setups.
322
286
 
323
- ```ts
324
- import { enableAccessLint } from "@accesslint/storybook-addon/portable";
325
- ```
326
-
327
287
  ## Compatibility
328
288
 
329
289
  | Addon version | Storybook version |
330
290
  | ------------- | ----------------- |
291
+ | 0.7.x | 10.x |
331
292
  | 0.6.x | 10.x |
332
293
 
333
294
  ## Issues
package/dist/manager.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import React, { useMemo, useState, useRef, useCallback } from 'react';
2
- import { experimental_getStatusStore, experimental_getTestProviderStore, addons, types, useChannel, useStorybookApi, experimental_useTestProviderStore } 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";
@@ -194,16 +199,31 @@ var Panel = ({ active }) => {
194
199
  };
195
200
 
196
201
  // src/manager.tsx
202
+ var { addons, types, useChannel: useChannel2, useStorybookApi } = managerApi;
197
203
  var PANEL_ID = `${ADDON_ID}/panel`;
198
204
  var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
199
- var statusStore = experimental_getStatusStore(STATUS_TYPE_ID);
200
- var testProviderStore = experimental_getTestProviderStore(TEST_PROVIDER_ID);
201
- testProviderStore.onClearAll(() => {
202
- statusStore.unset();
203
- });
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
+ }
204
224
  var Title = () => {
205
225
  const [count, setCount] = React.useState(0);
206
- useChannel({
226
+ useChannel2({
207
227
  [STORY_FINISHED]: ({ reporters }) => {
208
228
  const report = reporters.find((r) => r.type === "accesslint");
209
229
  const violations = report?.result?.violations;
@@ -224,9 +244,7 @@ var Title = () => {
224
244
  color: "inherit"
225
245
  } }, /* @__PURE__ */ React.createElement("span", { style: { color: "#fff" } }, count)));
226
246
  };
227
- var StyledActionList = styled(ActionList)({
228
- padding: 0
229
- });
247
+ var StyledActionList = ActionList ? styled(ActionList)({ padding: 0 }) : styled.div({ padding: 0 });
230
248
  var StatusDot = styled.div(
231
249
  {
232
250
  width: 6,
@@ -248,33 +266,34 @@ var StatusDot = styled.div(
248
266
  var TestProviderWidget = () => {
249
267
  const [violationCount, setViolationCount] = React.useState(null);
250
268
  const api = useStorybookApi();
251
- const providerState = experimental_useTestProviderStore(
252
- (state) => state[TEST_PROVIDER_ID]
253
- );
269
+ const providerState = _useTestProviderStore ? _useTestProviderStore((state) => state[TEST_PROVIDER_ID]) : null;
254
270
  React.useEffect(() => {
271
+ if (!statusStore) return;
255
272
  const unsub = statusStore.onSelect(() => {
256
273
  api.setSelectedPanel(PANEL_ID);
257
274
  api.togglePanel(true);
258
275
  });
259
276
  return unsub;
260
277
  }, [api]);
261
- useChannel({
278
+ useChannel2({
262
279
  [STORY_FINISHED]: ({ storyId, reporters }) => {
263
280
  const report = reporters.find((r) => r.type === "accesslint");
264
281
  if (!report) return;
265
282
  const violations = report.result?.violations ?? [];
266
283
  setViolationCount(violations.length);
267
- const hasViolations2 = violations.length > 0;
268
- const isWarning = report.status === "warning";
269
- statusStore.set([{
270
- value: hasViolations2 ? isWarning ? "status-value:warning" : "status-value:error" : "status-value:success",
271
- typeId: STATUS_TYPE_ID,
272
- storyId,
273
- title: "AccessLint",
274
- description: hasViolations2 ? `${violations.length} violation${violations.length === 1 ? "" : "s"}` : "No violations",
275
- sidebarContextMenu: true
276
- }]);
277
- if (providerState === "test-provider-state:running") {
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") {
278
297
  testProviderStore.setState("test-provider-state:succeeded");
279
298
  }
280
299
  }
@@ -285,20 +304,32 @@ var TestProviderWidget = () => {
285
304
  api.setSelectedPanel(PANEL_ID);
286
305
  api.togglePanel(true);
287
306
  };
288
- 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(
289
- 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,
290
321
  {
291
- ariaLabel: violationCount === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${violationCount} violation${violationCount === 1 ? "" : "s"}` : "AccessLint: no violations",
292
- disabled: violationCount === null,
322
+ style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", cursor: "pointer" },
293
323
  onClick: openPanel
294
324
  },
295
- hasViolations ? violationCount : null,
296
- /* @__PURE__ */ React.createElement(StatusDot, { status })
297
- )));
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
+ );
298
329
  };
299
330
  var SidebarContextMenu = ({ context }) => {
300
331
  const api = useStorybookApi();
301
- if (context.type !== "story") return null;
332
+ if (context.type !== "story" || !ActionList) return null;
302
333
  return /* @__PURE__ */ React.createElement(
303
334
  ActionList.Item,
304
335
  {
@@ -318,9 +349,11 @@ addons.register(ADDON_ID, () => {
318
349
  render: Panel,
319
350
  paramKey: PARAM_KEY
320
351
  });
321
- addons.add(TEST_PROVIDER_ID, {
322
- type: types.experimental_TEST_PROVIDER,
323
- render: () => /* @__PURE__ */ React.createElement(TestProviderWidget, null),
324
- sidebarContextMenu: ({ context }) => /* @__PURE__ */ React.createElement(SidebarContextMenu, { context })
325
- });
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
+ }
326
359
  });
package/dist/matchers.cjs CHANGED
@@ -1,57 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var core = require('@accesslint/core');
3
+ var matchers = require('@accesslint/vitest/matchers');
4
4
 
5
- // src/matchers.ts
6
- function scopeViolationsToElement(violations, root) {
7
- return violations.filter((v) => {
8
- try {
9
- const el = root.ownerDocument.querySelector(v.selector);
10
- return el && root.contains(el);
11
- } catch {
12
- return false;
13
- }
14
- });
15
- }
16
- function formatViolation(v) {
17
- const rule = core.getRuleById(v.ruleId);
18
- const wcag = rule?.wcag?.length ? ` (${rule.wcag.join(", ")})` : "";
19
- const level = rule?.level ? ` [${rule.level}]` : "";
20
- return ` ${v.ruleId}${level}${wcag}: ${v.message}
21
- ${v.selector}`;
22
- }
23
- var toBeAccessible = function(received, options) {
24
- if (!(received instanceof Element)) {
25
- return {
26
- pass: false,
27
- message: () => `toBeAccessible() expects an Element (e.g. canvasElement), but received ${typeof received}`
28
- };
29
- }
30
- if (options?.disabledRules) {
31
- core.configureRules({ disabledRules: options.disabledRules });
32
- }
33
- const result = core.runAudit(received.ownerDocument);
34
- const scoped = scopeViolationsToElement(result.violations, received);
35
- const pass = scoped.length === 0;
36
- return {
37
- pass,
38
- message: () => {
39
- if (pass) {
40
- return "Expected element to have accessibility violations, but none were found";
41
- }
42
- const summary = scoped.map(formatViolation).join("\n\n");
43
- return `Expected element to have no accessibility violations, but found ${scoped.length}:
44
5
 
45
- ${summary}`;
46
- }
47
- };
48
- };
49
- if (typeof globalThis !== "undefined") {
50
- const g = globalThis;
51
- const expectFn = g.expect;
52
- if (expectFn?.extend) {
53
- expectFn.extend({ toBeAccessible });
54
- }
55
- }
56
6
 
57
- exports.toBeAccessible = toBeAccessible;
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
+ });
@@ -1,11 +1 @@
1
- interface AccessibleMatcherOptions {
2
- disabledRules?: string[];
3
- }
4
- declare const toBeAccessible: (this: {
5
- isNot: boolean;
6
- }, received: Element, options?: AccessibleMatcherOptions) => {
7
- pass: boolean;
8
- message: () => string;
9
- };
10
-
11
- export { type AccessibleMatcherOptions, toBeAccessible };
1
+ export { AccessibleMatcherOptions, accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
@@ -1,11 +1 @@
1
- interface AccessibleMatcherOptions {
2
- disabledRules?: string[];
3
- }
4
- declare const toBeAccessible: (this: {
5
- isNot: boolean;
6
- }, received: Element, options?: AccessibleMatcherOptions) => {
7
- pass: boolean;
8
- message: () => string;
9
- };
10
-
11
- export { type AccessibleMatcherOptions, toBeAccessible };
1
+ export { AccessibleMatcherOptions, accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
package/dist/matchers.js CHANGED
@@ -1,55 +1 @@
1
- import { configureRules, runAudit, getRuleById } from '@accesslint/core';
2
-
3
- // src/matchers.ts
4
- function scopeViolationsToElement(violations, root) {
5
- return violations.filter((v) => {
6
- try {
7
- const el = root.ownerDocument.querySelector(v.selector);
8
- return el && root.contains(el);
9
- } catch {
10
- return false;
11
- }
12
- });
13
- }
14
- function formatViolation(v) {
15
- const rule = getRuleById(v.ruleId);
16
- const wcag = rule?.wcag?.length ? ` (${rule.wcag.join(", ")})` : "";
17
- const level = rule?.level ? ` [${rule.level}]` : "";
18
- return ` ${v.ruleId}${level}${wcag}: ${v.message}
19
- ${v.selector}`;
20
- }
21
- var toBeAccessible = function(received, options) {
22
- if (!(received instanceof Element)) {
23
- return {
24
- pass: false,
25
- message: () => `toBeAccessible() expects an Element (e.g. canvasElement), but received ${typeof received}`
26
- };
27
- }
28
- if (options?.disabledRules) {
29
- configureRules({ disabledRules: options.disabledRules });
30
- }
31
- const result = runAudit(received.ownerDocument);
32
- const scoped = scopeViolationsToElement(result.violations, received);
33
- const pass = scoped.length === 0;
34
- return {
35
- pass,
36
- message: () => {
37
- if (pass) {
38
- return "Expected element to have accessibility violations, but none were found";
39
- }
40
- const summary = scoped.map(formatViolation).join("\n\n");
41
- return `Expected element to have no accessibility violations, but found ${scoped.length}:
42
-
43
- ${summary}`;
44
- }
45
- };
46
- };
47
- if (typeof globalThis !== "undefined") {
48
- const g = globalThis;
49
- const expectFn = g.expect;
50
- if (expectFn?.extend) {
51
- expectFn.extend({ toBeAccessible });
52
- }
53
- }
54
-
55
- export { toBeAccessible };
1
+ export { accesslintMatchers, toBeAccessible } from '@accesslint/vitest/matchers';
package/dist/portable.cjs CHANGED
@@ -16,6 +16,10 @@ __export(preview_exports, {
16
16
  core.configureRules({
17
17
  disabledRules: ["accesslint-045"]
18
18
  });
19
+ var BUDGET_MS = 12;
20
+ function yieldToMain() {
21
+ return new Promise((resolve) => setTimeout(resolve, 0));
22
+ }
19
23
  function scopeViolations(violations) {
20
24
  const root = document.getElementById("storybook-root");
21
25
  if (!root) return violations;
@@ -53,8 +57,14 @@ var afterEach = async ({
53
57
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
54
58
  if (viewMode !== "story") return;
55
59
  if (tags?.includes("no-a11y")) return;
56
- const result = core.runAudit(document);
57
- const scoped = scopeViolations(result.violations);
60
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
61
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
62
+ const audit = core.createChunkedAudit(document);
63
+ while (audit.processChunk(BUDGET_MS)) {
64
+ await yieldToMain();
65
+ }
66
+ const violations = audit.getViolations();
67
+ const scoped = scopeViolations(violations);
58
68
  const enriched = enrichViolations(scoped);
59
69
  const hasViolations = enriched.length > 0;
60
70
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -63,7 +73,7 @@ var afterEach = async ({
63
73
  version: 1,
64
74
  result: {
65
75
  violations: enriched,
66
- ruleCount: result.ruleCount
76
+ ruleCount: core.getActiveRules().length
67
77
  },
68
78
  status: hasViolations ? mode : "passed"
69
79
  });
@@ -4,7 +4,7 @@ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
- tags: string[];
7
+ tags?: string[];
8
8
  }) => Promise<void>;
9
9
 
10
10
  declare const accesslintAnnotations_afterEach: typeof afterEach;
@@ -4,7 +4,7 @@ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
- tags: string[];
7
+ tags?: string[];
8
8
  }) => Promise<void>;
9
9
 
10
10
  declare const accesslintAnnotations_afterEach: typeof afterEach;
package/dist/portable.js CHANGED
@@ -1,4 +1,4 @@
1
- import { configureRules, runAudit, getRuleById } from '@accesslint/core';
1
+ import { configureRules, createChunkedAudit, getActiveRules, getRuleById } from '@accesslint/core';
2
2
 
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -14,6 +14,10 @@ __export(preview_exports, {
14
14
  configureRules({
15
15
  disabledRules: ["accesslint-045"]
16
16
  });
17
+ var BUDGET_MS = 12;
18
+ function yieldToMain() {
19
+ return new Promise((resolve) => setTimeout(resolve, 0));
20
+ }
17
21
  function scopeViolations(violations) {
18
22
  const root = document.getElementById("storybook-root");
19
23
  if (!root) return violations;
@@ -51,8 +55,14 @@ var afterEach = async ({
51
55
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
52
56
  if (viewMode !== "story") return;
53
57
  if (tags?.includes("no-a11y")) return;
54
- const result = runAudit(document);
55
- const scoped = scopeViolations(result.violations);
58
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
59
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
60
+ const audit = createChunkedAudit(document);
61
+ while (audit.processChunk(BUDGET_MS)) {
62
+ await yieldToMain();
63
+ }
64
+ const violations = audit.getViolations();
65
+ const scoped = scopeViolations(violations);
56
66
  const enriched = enrichViolations(scoped);
57
67
  const hasViolations = enriched.length > 0;
58
68
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -61,7 +71,7 @@ var afterEach = async ({
61
71
  version: 1,
62
72
  result: {
63
73
  violations: enriched,
64
- ruleCount: result.ruleCount
74
+ ruleCount: getActiveRules().length
65
75
  },
66
76
  status: hasViolations ? mode : "passed"
67
77
  });
package/dist/preview.cjs CHANGED
@@ -6,6 +6,10 @@ var core = require('@accesslint/core');
6
6
  core.configureRules({
7
7
  disabledRules: ["accesslint-045"]
8
8
  });
9
+ var BUDGET_MS = 12;
10
+ function yieldToMain() {
11
+ return new Promise((resolve) => setTimeout(resolve, 0));
12
+ }
9
13
  function scopeViolations(violations) {
10
14
  const root = document.getElementById("storybook-root");
11
15
  if (!root) return violations;
@@ -43,8 +47,14 @@ var afterEach = async ({
43
47
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
44
48
  if (viewMode !== "story") return;
45
49
  if (tags?.includes("no-a11y")) return;
46
- const result = core.runAudit(document);
47
- const scoped = scopeViolations(result.violations);
50
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
51
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
52
+ const audit = core.createChunkedAudit(document);
53
+ while (audit.processChunk(BUDGET_MS)) {
54
+ await yieldToMain();
55
+ }
56
+ const violations = audit.getViolations();
57
+ const scoped = scopeViolations(violations);
48
58
  const enriched = enrichViolations(scoped);
49
59
  const hasViolations = enriched.length > 0;
50
60
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -53,7 +63,7 @@ var afterEach = async ({
53
63
  version: 1,
54
64
  result: {
55
65
  violations: enriched,
56
- ruleCount: result.ruleCount
66
+ ruleCount: core.getActiveRules().length
57
67
  },
58
68
  status: hasViolations ? mode : "passed"
59
69
  });
@@ -4,7 +4,7 @@ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
- tags: string[];
7
+ tags?: string[];
8
8
  }) => Promise<void>;
9
9
 
10
10
  export { afterEach };
package/dist/preview.d.ts CHANGED
@@ -4,7 +4,7 @@ declare const afterEach: ({ reporting, parameters, viewMode, tags, }: {
4
4
  };
5
5
  parameters: Record<string, unknown>;
6
6
  viewMode: string;
7
- tags: string[];
7
+ tags?: string[];
8
8
  }) => Promise<void>;
9
9
 
10
10
  export { afterEach };
package/dist/preview.js CHANGED
@@ -1,9 +1,13 @@
1
- import { configureRules, runAudit, getRuleById } from '@accesslint/core';
1
+ import { configureRules, createChunkedAudit, getActiveRules, getRuleById } from '@accesslint/core';
2
2
 
3
3
  // src/preview.ts
4
4
  configureRules({
5
5
  disabledRules: ["accesslint-045"]
6
6
  });
7
+ var BUDGET_MS = 12;
8
+ function yieldToMain() {
9
+ return new Promise((resolve) => setTimeout(resolve, 0));
10
+ }
7
11
  function scopeViolations(violations) {
8
12
  const root = document.getElementById("storybook-root");
9
13
  if (!root) return violations;
@@ -41,8 +45,14 @@ var afterEach = async ({
41
45
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
42
46
  if (viewMode !== "story") return;
43
47
  if (tags?.includes("no-a11y")) return;
44
- const result = runAudit(document);
45
- const scoped = scopeViolations(result.violations);
48
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
49
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
50
+ const audit = createChunkedAudit(document);
51
+ while (audit.processChunk(BUDGET_MS)) {
52
+ await yieldToMain();
53
+ }
54
+ const violations = audit.getViolations();
55
+ const scoped = scopeViolations(violations);
46
56
  const enriched = enrichViolations(scoped);
47
57
  const hasViolations = enriched.length > 0;
48
58
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -51,7 +61,7 @@ var afterEach = async ({
51
61
  version: 1,
52
62
  result: {
53
63
  violations: enriched,
54
- ruleCount: result.ruleCount
64
+ ruleCount: getActiveRules().length
55
65
  },
56
66
  status: hasViolations ? mode : "passed"
57
67
  });
@@ -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) => {
@@ -17,6 +20,10 @@ __export(preview_exports, {
17
20
  core.configureRules({
18
21
  disabledRules: ["accesslint-045"]
19
22
  });
23
+ var BUDGET_MS = 12;
24
+ function yieldToMain() {
25
+ return new Promise((resolve) => setTimeout(resolve, 0));
26
+ }
20
27
  function scopeViolations(violations) {
21
28
  const root = document.getElementById("storybook-root");
22
29
  if (!root) return violations;
@@ -54,8 +61,14 @@ var afterEach = async ({
54
61
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
55
62
  if (viewMode !== "story") return;
56
63
  if (tags?.includes("no-a11y")) return;
57
- const result = core.runAudit(document);
58
- const scoped = scopeViolations(result.violations);
64
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
65
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
66
+ const audit = core.createChunkedAudit(document);
67
+ while (audit.processChunk(BUDGET_MS)) {
68
+ await yieldToMain();
69
+ }
70
+ const violations = audit.getViolations();
71
+ const scoped = scopeViolations(violations);
59
72
  const enriched = enrichViolations(scoped);
60
73
  const hasViolations = enriched.length > 0;
61
74
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -64,13 +77,13 @@ var afterEach = async ({
64
77
  version: 1,
65
78
  result: {
66
79
  violations: enriched,
67
- ruleCount: result.ruleCount
80
+ ruleCount: core.getActiveRules().length
68
81
  },
69
82
  status: hasViolations ? mode : "passed"
70
83
  });
71
84
  };
72
-
73
- // src/vitest-setup.ts
85
+ test.expect.extend(matchers.accesslintMatchers);
86
+ vitest.expect.extend(matchers.accesslintMatchers);
74
87
  var g = globalThis;
75
88
  var existing = g.globalProjectAnnotations;
76
89
  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
- import { configureRules, runAudit, getRuleById } from '@accesslint/core';
4
+ import { configureRules, createChunkedAudit, getActiveRules, 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) => {
@@ -15,6 +18,10 @@ __export(preview_exports, {
15
18
  configureRules({
16
19
  disabledRules: ["accesslint-045"]
17
20
  });
21
+ var BUDGET_MS = 12;
22
+ function yieldToMain() {
23
+ return new Promise((resolve) => setTimeout(resolve, 0));
24
+ }
18
25
  function scopeViolations(violations) {
19
26
  const root = document.getElementById("storybook-root");
20
27
  if (!root) return violations;
@@ -52,8 +59,14 @@ var afterEach = async ({
52
59
  if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
53
60
  if (viewMode !== "story") return;
54
61
  if (tags?.includes("no-a11y")) return;
55
- const result = runAudit(document);
56
- const scoped = scopeViolations(result.violations);
62
+ const skipTags = typeof __ACCESSLINT_SKIP_TAGS__ !== "undefined" ? __ACCESSLINT_SKIP_TAGS__ : [];
63
+ if (skipTags.length > 0 && tags?.some((t) => skipTags.includes(t))) return;
64
+ const audit = createChunkedAudit(document);
65
+ while (audit.processChunk(BUDGET_MS)) {
66
+ await yieldToMain();
67
+ }
68
+ const violations = audit.getViolations();
69
+ const scoped = scopeViolations(violations);
57
70
  const enriched = enrichViolations(scoped);
58
71
  const hasViolations = enriched.length > 0;
59
72
  const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
@@ -62,13 +75,13 @@ var afterEach = async ({
62
75
  version: 1,
63
76
  result: {
64
77
  violations: enriched,
65
- ruleCount: result.ruleCount
78
+ ruleCount: getActiveRules().length
66
79
  },
67
80
  status: hasViolations ? mode : "passed"
68
81
  });
69
82
  };
70
-
71
- // src/vitest-setup.ts
83
+ expect.extend(accesslintMatchers);
84
+ expect$1.extend(accesslintMatchers);
72
85
  var g = globalThis;
73
86
  var existing = g.globalProjectAnnotations;
74
87
  g.globalProjectAnnotations = existing ? composeConfigs([existing, preview_exports]) : composeConfigs([preview_exports]);
package/matchers.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccessibleMatcherOptions } from "./dist/matchers";
1
+ import type { AccessibleMatcherOptions } from "@accesslint/vitest/matchers";
2
2
 
3
3
  declare module "vitest" {
4
4
  interface Assertion<T> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accesslint/storybook-addon",
3
- "version": "0.7.0",
3
+ "version": "0.8.2",
4
4
  "description": "Catch accessibility violations in your Storybook stories as you develop",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -30,6 +30,7 @@
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"
35
36
  },
@@ -57,17 +58,20 @@
57
58
  "typecheck": "tsc --noEmit"
58
59
  },
59
60
  "dependencies": {
60
- "@accesslint/core": "^0.6.5"
61
+ "@accesslint/core": "^0.6.5",
62
+ "@accesslint/vitest": "^0.1.3"
61
63
  },
62
64
  "devDependencies": {
63
65
  "react": "^18.2.0",
64
66
  "react-dom": "^18.2.0",
65
67
  "storybook": "^10.2.0",
66
68
  "tsup": "^8.4.0",
67
- "typescript": "^5.7.0"
69
+ "typescript": "^5.7.0",
70
+ "vitest": "^4.0.18"
68
71
  },
69
72
  "peerDependencies": {
70
- "storybook": "^10.0.0"
73
+ "storybook": "^9.0.0 || ^10.0.0",
74
+ "vitest": ">=3.0.0"
71
75
  },
72
76
  "keywords": [
73
77
  "storybook-addon",