@accesslint/storybook-addon 0.6.7 → 0.6.9
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 +40 -6
- package/dist/manager.js +50 -53
- package/dist/preview.cjs +48 -50
- package/dist/preview.d.cts +8 -2
- package/dist/preview.d.ts +8 -2
- package/dist/preview.js +48 -50
- package/dist/vitest-plugin.cjs +24 -0
- package/dist/vitest-plugin.d.cts +19 -0
- package/dist/vitest-plugin.d.ts +19 -0
- package/dist/vitest-plugin.js +21 -0
- package/dist/vitest-setup.cjs +74 -0
- package/dist/vitest-setup.js +72 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -14,31 +14,65 @@ Catch accessibility violations in your Storybook stories as you develop. Powered
|
|
|
14
14
|
npm install @accesslint/storybook-addon
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Add the addon to your `.storybook/main.ts` (or `.storybook/main.js`):
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
const config = {
|
|
21
|
-
addons: ["@accesslint/storybook-addon"],
|
|
21
|
+
addons: ["@storybook/addon-vitest", "@accesslint/storybook-addon"],
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export default config;
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Add the vitest plugin to your `vite.config.ts`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
31
|
+
import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
plugins: [
|
|
35
|
+
storybookTest({ configDir: ".storybook" }),
|
|
36
|
+
accesslintTest(),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Restart Storybook and an **AccessLint** panel will appear in the addon bar.
|
|
28
42
|
|
|
29
43
|
## Usage
|
|
30
44
|
|
|
31
|
-
The addon
|
|
45
|
+
The addon audits each story after it renders and displays violations sorted by severity. Expand any violation to see:
|
|
32
46
|
|
|
33
47
|
- **Impact level** — critical, serious, moderate, or minor
|
|
34
48
|
- **WCAG criteria** and conformance level (A, AA, AAA)
|
|
35
49
|
- **How to fix** guidance for each rule
|
|
36
50
|
- **Element HTML** snippet of the failing element
|
|
37
51
|
|
|
38
|
-
Selecting a violation highlights the affected element in the story preview.
|
|
39
|
-
|
|
40
52
|
## Configuration
|
|
41
53
|
|
|
54
|
+
### Parameters
|
|
55
|
+
|
|
56
|
+
Control AccessLint behavior per-story or globally via `parameters.accesslint`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// .storybook/preview.ts
|
|
60
|
+
const preview = {
|
|
61
|
+
parameters: {
|
|
62
|
+
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",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default preview;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Disabling rules
|
|
75
|
+
|
|
42
76
|
Disable specific rules in your preview file:
|
|
43
77
|
|
|
44
78
|
```ts
|
package/dist/manager.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import React, { useMemo, useState, useRef,
|
|
2
|
-
import { addons, types,
|
|
3
|
-
import { ActionList,
|
|
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';
|
|
4
4
|
import { styled, useTheme } from 'storybook/internal/theming';
|
|
5
|
+
import { STORY_CHANGED, STORY_FINISHED } from 'storybook/internal/core-events';
|
|
5
6
|
|
|
6
7
|
// src/manager.tsx
|
|
7
8
|
|
|
8
9
|
// src/constants.ts
|
|
9
10
|
var ADDON_ID = "accesslint/a11y";
|
|
10
|
-
var
|
|
11
|
-
var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
|
|
11
|
+
var PARAM_KEY = "accesslint";
|
|
12
12
|
var IMPACT_COLOR = {
|
|
13
13
|
critical: "#d32f2f",
|
|
14
14
|
serious: "#d32f2f",
|
|
@@ -32,8 +32,7 @@ var LEVEL_COLOR = {
|
|
|
32
32
|
AA: "#1565c0",
|
|
33
33
|
AAA: "#6a1b9a"
|
|
34
34
|
};
|
|
35
|
-
var
|
|
36
|
-
var Panel = ({ active, ...rest }) => {
|
|
35
|
+
var Panel = ({ active }) => {
|
|
37
36
|
const theme = useTheme();
|
|
38
37
|
const isDark = theme.base === "dark";
|
|
39
38
|
const colors = useMemo(() => ({
|
|
@@ -49,41 +48,29 @@ var Panel = ({ active, ...rest }) => {
|
|
|
49
48
|
tagText: isDark ? "#ccc" : "#616161",
|
|
50
49
|
ruleId: isDark ? "#64b5f6" : "#1565c0"
|
|
51
50
|
}), [isDark, theme]);
|
|
52
|
-
const [violations, setViolations] =
|
|
53
|
-
|
|
54
|
-
[]
|
|
55
|
-
);
|
|
56
|
-
const [meta, setMeta] = useState(null);
|
|
51
|
+
const [violations, setViolations] = useState([]);
|
|
52
|
+
const [ruleCount, setRuleCount] = useState(0);
|
|
57
53
|
const [expandedIndex, setExpandedIndex] = useState(null);
|
|
58
54
|
const buttonRefs = useRef([]);
|
|
59
|
-
|
|
60
|
-
[
|
|
61
|
-
|
|
55
|
+
useChannel({
|
|
56
|
+
[STORY_FINISHED]: ({ reporters }) => {
|
|
57
|
+
const report = reporters.find((r) => r.type === "accesslint");
|
|
58
|
+
if (!report) return;
|
|
59
|
+
const result = report.result;
|
|
60
|
+
setViolations(result.violations ?? []);
|
|
61
|
+
setRuleCount(result.ruleCount ?? 0);
|
|
62
62
|
setExpandedIndex(null);
|
|
63
63
|
},
|
|
64
|
-
[
|
|
65
|
-
|
|
64
|
+
[STORY_CHANGED]: () => {
|
|
65
|
+
setViolations([]);
|
|
66
|
+
setRuleCount(0);
|
|
67
|
+
setExpandedIndex(null);
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
|
-
const sorted =
|
|
69
|
-
(a, b) => (IMPACT_ORDER[a.impact] ?? 4) - (IMPACT_ORDER[b.impact] ?? 4)
|
|
70
|
+
const sorted = useMemo(
|
|
71
|
+
() => [...violations].sort((a, b) => (IMPACT_ORDER[a.impact] ?? 4) - (IMPACT_ORDER[b.impact] ?? 4)),
|
|
72
|
+
[violations]
|
|
70
73
|
);
|
|
71
|
-
const expanded = expandedIndex !== null ? sorted[expandedIndex] : null;
|
|
72
|
-
useEffect(() => {
|
|
73
|
-
if (expanded?.selector) {
|
|
74
|
-
const local = expanded.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
75
|
-
emit("storybook/highlight/add", {
|
|
76
|
-
id: HIGHLIGHT_ID,
|
|
77
|
-
selectors: [local],
|
|
78
|
-
styles: {
|
|
79
|
-
outline: `2px solid ${IMPACT_COLOR[expanded.impact] || "#1565c0"}`,
|
|
80
|
-
outlineOffset: "2px"
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
} else {
|
|
84
|
-
emit("storybook/highlight/remove", { id: HIGHLIGHT_ID });
|
|
85
|
-
}
|
|
86
|
-
}, [expandedIndex]);
|
|
87
74
|
const handleKeyDown = useCallback((e, index) => {
|
|
88
75
|
let next = null;
|
|
89
76
|
switch (e.key) {
|
|
@@ -106,7 +93,8 @@ var Panel = ({ active, ...rest }) => {
|
|
|
106
93
|
buttonRefs.current[next]?.focus();
|
|
107
94
|
}, [sorted.length]);
|
|
108
95
|
if (!active) return null;
|
|
109
|
-
|
|
96
|
+
const passed = ruleCount - new Set(violations.map((v) => v.ruleId)).size;
|
|
97
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", height: "100%", fontFamily: "system-ui, sans-serif" } }, ruleCount > 0 && /* @__PURE__ */ React.createElement("div", { style: {
|
|
110
98
|
display: "flex",
|
|
111
99
|
gap: "12px",
|
|
112
100
|
padding: "8px 12px",
|
|
@@ -114,7 +102,7 @@ var Panel = ({ active, ...rest }) => {
|
|
|
114
102
|
color: colors.textMuted,
|
|
115
103
|
borderBottom: `1px solid ${colors.border}`,
|
|
116
104
|
flexShrink: 0
|
|
117
|
-
} }, /* @__PURE__ */ React.createElement("span", null,
|
|
105
|
+
} }, /* @__PURE__ */ React.createElement("span", null, ruleCount, " rules"), /* @__PURE__ */ React.createElement("span", { style: { color: "#2e7d32" } }, passed, " passed"), /* @__PURE__ */ React.createElement("span", { style: { color: violations.length > 0 ? "#d32f2f" : colors.textMuted } }, new Set(violations.map((v) => v.ruleId)).size, " failed")), violations.length === 0 ? /* @__PURE__ */ React.createElement("p", { style: { padding: "12px", margin: 0, fontSize: "13px", color: colors.textMuted } }, "No accessibility violations found.") : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", minHeight: 0 } }, /* @__PURE__ */ React.createElement(
|
|
118
106
|
"ul",
|
|
119
107
|
{
|
|
120
108
|
style: { listStyle: "none", padding: 0, margin: 0 },
|
|
@@ -201,13 +189,21 @@ var Panel = ({ active, ...rest }) => {
|
|
|
201
189
|
v.html
|
|
202
190
|
))));
|
|
203
191
|
})
|
|
204
|
-
)))
|
|
192
|
+
)));
|
|
205
193
|
};
|
|
206
194
|
|
|
207
195
|
// src/manager.tsx
|
|
196
|
+
var PANEL_ID = `${ADDON_ID}/panel`;
|
|
197
|
+
var TEST_PROVIDER_ID = `${ADDON_ID}/test-provider`;
|
|
208
198
|
var Title = () => {
|
|
209
|
-
const [
|
|
210
|
-
|
|
199
|
+
const [count, setCount] = React.useState(0);
|
|
200
|
+
useChannel({
|
|
201
|
+
[STORY_FINISHED]: ({ reporters }) => {
|
|
202
|
+
const report = reporters.find((r) => r.type === "accesslint");
|
|
203
|
+
const violations = report?.result?.violations;
|
|
204
|
+
setCount(violations?.length ?? 0);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
211
207
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, "AccessLint", count > 0 && /* @__PURE__ */ React.createElement("span", { style: {
|
|
212
208
|
display: "inline-block",
|
|
213
209
|
marginLeft: "8px",
|
|
@@ -236,9 +232,6 @@ var StatusDot = styled.div(
|
|
|
236
232
|
({ status, theme }) => status === "positive" && {
|
|
237
233
|
"--status-color": theme.color.positive
|
|
238
234
|
},
|
|
239
|
-
({ status, theme }) => status === "warning" && {
|
|
240
|
-
"--status-color": theme.color.gold
|
|
241
|
-
},
|
|
242
235
|
({ status, theme }) => status === "negative" && {
|
|
243
236
|
"--status-color": theme.color.negative
|
|
244
237
|
},
|
|
@@ -247,15 +240,18 @@ var StatusDot = styled.div(
|
|
|
247
240
|
}
|
|
248
241
|
);
|
|
249
242
|
var TestProviderWidget = () => {
|
|
250
|
-
const [
|
|
243
|
+
const [violationCount, setViolationCount] = React.useState(null);
|
|
251
244
|
const api = useStorybookApi();
|
|
252
245
|
useChannel({
|
|
253
|
-
[
|
|
254
|
-
|
|
246
|
+
[STORY_FINISHED]: ({ reporters }) => {
|
|
247
|
+
const report = reporters.find((r) => r.type === "accesslint");
|
|
248
|
+
if (!report) return;
|
|
249
|
+
const violations = report.result?.violations;
|
|
250
|
+
setViolationCount(violations?.length ?? 0);
|
|
255
251
|
}
|
|
256
252
|
});
|
|
257
|
-
const hasViolations =
|
|
258
|
-
const status =
|
|
253
|
+
const hasViolations = violationCount !== null && violationCount > 0;
|
|
254
|
+
const status = violationCount === null ? "unknown" : hasViolations ? "negative" : "positive";
|
|
259
255
|
const openPanel = () => {
|
|
260
256
|
api.setSelectedPanel(PANEL_ID);
|
|
261
257
|
api.togglePanel(true);
|
|
@@ -263,19 +259,20 @@ var TestProviderWidget = () => {
|
|
|
263
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(
|
|
264
260
|
ActionList.Button,
|
|
265
261
|
{
|
|
266
|
-
ariaLabel:
|
|
267
|
-
disabled:
|
|
262
|
+
ariaLabel: violationCount === null ? "AccessLint: not run yet" : hasViolations ? `AccessLint: ${violationCount} violation${violationCount === 1 ? "" : "s"}` : "AccessLint: no violations",
|
|
263
|
+
disabled: violationCount === null,
|
|
268
264
|
onClick: openPanel
|
|
269
265
|
},
|
|
270
|
-
hasViolations ?
|
|
266
|
+
hasViolations ? violationCount : null,
|
|
271
267
|
/* @__PURE__ */ React.createElement(StatusDot, { status })
|
|
272
268
|
)));
|
|
273
269
|
};
|
|
274
270
|
addons.register(ADDON_ID, () => {
|
|
275
271
|
addons.add(PANEL_ID, {
|
|
276
|
-
type: types.PANEL,
|
|
277
272
|
title: Title,
|
|
278
|
-
|
|
273
|
+
type: types.PANEL,
|
|
274
|
+
render: Panel,
|
|
275
|
+
paramKey: PARAM_KEY
|
|
279
276
|
});
|
|
280
277
|
addons.add(TEST_PROVIDER_ID, {
|
|
281
278
|
type: types.experimental_TEST_PROVIDER,
|
package/dist/preview.cjs
CHANGED
|
@@ -1,62 +1,60 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var previewApi = require('storybook/internal/preview-api');
|
|
4
3
|
var core = require('@accesslint/core');
|
|
5
4
|
|
|
6
|
-
// src/preview.ts
|
|
7
|
-
|
|
8
|
-
// src/constants.ts
|
|
9
|
-
var ADDON_ID = "accesslint/a11y";
|
|
10
|
-
|
|
11
5
|
// src/preview.ts
|
|
12
6
|
core.configureRules({
|
|
13
7
|
disabledRules: ["accesslint-045"]
|
|
14
8
|
});
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
9
|
+
function scopeViolations(violations) {
|
|
10
|
+
const root = document.getElementById("storybook-root");
|
|
11
|
+
if (!root) return violations;
|
|
12
|
+
return violations.filter((v) => {
|
|
13
|
+
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
18
14
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const scoped = root ? results.violations.filter((v) => {
|
|
24
|
-
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
25
|
-
try {
|
|
26
|
-
const el = document.querySelector(local);
|
|
27
|
-
return el && root.contains(el);
|
|
28
|
-
} catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}) : results.violations;
|
|
32
|
-
const enriched = scoped.map((v) => {
|
|
33
|
-
const rule = core.getRuleById(v.ruleId);
|
|
34
|
-
return {
|
|
35
|
-
...v,
|
|
36
|
-
element: void 0,
|
|
37
|
-
// not serializable
|
|
38
|
-
description: rule?.description,
|
|
39
|
-
wcag: rule?.wcag,
|
|
40
|
-
level: rule?.level,
|
|
41
|
-
guidance: rule?.guidance
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
const failedRuleIds = new Set(scoped.map((v) => v.ruleId));
|
|
45
|
-
const channel = previewApi.addons.getChannel();
|
|
46
|
-
channel.emit(`${ADDON_ID}/results`, enriched);
|
|
47
|
-
channel.emit(`${ADDON_ID}/meta`, {
|
|
48
|
-
duration,
|
|
49
|
-
ruleCount: results.ruleCount,
|
|
50
|
-
failed: failedRuleIds.size,
|
|
51
|
-
passed: results.ruleCount - failedRuleIds.size,
|
|
52
|
-
violations: scoped.length
|
|
53
|
-
});
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error("[AccessLint] decorator error:", err);
|
|
15
|
+
const el = document.querySelector(local);
|
|
16
|
+
return el && root.contains(el);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
56
19
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function enrichViolations(violations) {
|
|
23
|
+
return violations.map((v) => {
|
|
24
|
+
const rule = core.getRuleById(v.ruleId);
|
|
25
|
+
return {
|
|
26
|
+
...v,
|
|
27
|
+
element: void 0,
|
|
28
|
+
// not serializable
|
|
29
|
+
description: rule?.description,
|
|
30
|
+
wcag: rule?.wcag,
|
|
31
|
+
level: rule?.level,
|
|
32
|
+
guidance: rule?.guidance
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
var afterEach = async ({
|
|
37
|
+
reporting,
|
|
38
|
+
parameters,
|
|
39
|
+
viewMode
|
|
40
|
+
}) => {
|
|
41
|
+
const accesslintParam = parameters?.accesslint;
|
|
42
|
+
if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
|
|
43
|
+
if (viewMode !== "story") return;
|
|
44
|
+
const result = core.runAudit(document);
|
|
45
|
+
const scoped = scopeViolations(result.violations);
|
|
46
|
+
const enriched = enrichViolations(scoped);
|
|
47
|
+
const hasViolations = enriched.length > 0;
|
|
48
|
+
const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
|
|
49
|
+
reporting.addReport({
|
|
50
|
+
type: "accesslint",
|
|
51
|
+
version: 1,
|
|
52
|
+
result: {
|
|
53
|
+
violations: enriched,
|
|
54
|
+
ruleCount: result.ruleCount
|
|
55
|
+
},
|
|
56
|
+
status: hasViolations ? mode : "passed"
|
|
57
|
+
});
|
|
59
58
|
};
|
|
60
|
-
var decorators = [decorator];
|
|
61
59
|
|
|
62
|
-
exports.
|
|
60
|
+
exports.afterEach = afterEach;
|
package/dist/preview.d.cts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
declare const
|
|
1
|
+
declare const afterEach: ({ reporting, parameters, viewMode, }: {
|
|
2
|
+
reporting: {
|
|
3
|
+
addReport: (report: Record<string, unknown>) => void;
|
|
4
|
+
};
|
|
5
|
+
parameters: Record<string, unknown>;
|
|
6
|
+
viewMode: string;
|
|
7
|
+
}) => Promise<void>;
|
|
2
8
|
|
|
3
|
-
export {
|
|
9
|
+
export { afterEach };
|
package/dist/preview.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
declare const
|
|
1
|
+
declare const afterEach: ({ reporting, parameters, viewMode, }: {
|
|
2
|
+
reporting: {
|
|
3
|
+
addReport: (report: Record<string, unknown>) => void;
|
|
4
|
+
};
|
|
5
|
+
parameters: Record<string, unknown>;
|
|
6
|
+
viewMode: string;
|
|
7
|
+
}) => Promise<void>;
|
|
2
8
|
|
|
3
|
-
export {
|
|
9
|
+
export { afterEach };
|
package/dist/preview.js
CHANGED
|
@@ -1,60 +1,58 @@
|
|
|
1
|
-
import { addons } from 'storybook/internal/preview-api';
|
|
2
1
|
import { configureRules, runAudit, getRuleById } from '@accesslint/core';
|
|
3
2
|
|
|
4
|
-
// src/preview.ts
|
|
5
|
-
|
|
6
|
-
// src/constants.ts
|
|
7
|
-
var ADDON_ID = "accesslint/a11y";
|
|
8
|
-
|
|
9
3
|
// src/preview.ts
|
|
10
4
|
configureRules({
|
|
11
5
|
disabledRules: ["accesslint-045"]
|
|
12
6
|
});
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
7
|
+
function scopeViolations(violations) {
|
|
8
|
+
const root = document.getElementById("storybook-root");
|
|
9
|
+
if (!root) return violations;
|
|
10
|
+
return violations.filter((v) => {
|
|
11
|
+
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
16
12
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const scoped = root ? results.violations.filter((v) => {
|
|
22
|
-
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
23
|
-
try {
|
|
24
|
-
const el = document.querySelector(local);
|
|
25
|
-
return el && root.contains(el);
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}) : results.violations;
|
|
30
|
-
const enriched = scoped.map((v) => {
|
|
31
|
-
const rule = getRuleById(v.ruleId);
|
|
32
|
-
return {
|
|
33
|
-
...v,
|
|
34
|
-
element: void 0,
|
|
35
|
-
// not serializable
|
|
36
|
-
description: rule?.description,
|
|
37
|
-
wcag: rule?.wcag,
|
|
38
|
-
level: rule?.level,
|
|
39
|
-
guidance: rule?.guidance
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
const failedRuleIds = new Set(scoped.map((v) => v.ruleId));
|
|
43
|
-
const channel = addons.getChannel();
|
|
44
|
-
channel.emit(`${ADDON_ID}/results`, enriched);
|
|
45
|
-
channel.emit(`${ADDON_ID}/meta`, {
|
|
46
|
-
duration,
|
|
47
|
-
ruleCount: results.ruleCount,
|
|
48
|
-
failed: failedRuleIds.size,
|
|
49
|
-
passed: results.ruleCount - failedRuleIds.size,
|
|
50
|
-
violations: scoped.length
|
|
51
|
-
});
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.error("[AccessLint] decorator error:", err);
|
|
13
|
+
const el = document.querySelector(local);
|
|
14
|
+
return el && root.contains(el);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
54
17
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function enrichViolations(violations) {
|
|
21
|
+
return violations.map((v) => {
|
|
22
|
+
const rule = getRuleById(v.ruleId);
|
|
23
|
+
return {
|
|
24
|
+
...v,
|
|
25
|
+
element: void 0,
|
|
26
|
+
// not serializable
|
|
27
|
+
description: rule?.description,
|
|
28
|
+
wcag: rule?.wcag,
|
|
29
|
+
level: rule?.level,
|
|
30
|
+
guidance: rule?.guidance
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
var afterEach = async ({
|
|
35
|
+
reporting,
|
|
36
|
+
parameters,
|
|
37
|
+
viewMode
|
|
38
|
+
}) => {
|
|
39
|
+
const accesslintParam = parameters?.accesslint;
|
|
40
|
+
if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
|
|
41
|
+
if (viewMode !== "story") return;
|
|
42
|
+
const result = runAudit(document);
|
|
43
|
+
const scoped = scopeViolations(result.violations);
|
|
44
|
+
const enriched = enrichViolations(scoped);
|
|
45
|
+
const hasViolations = enriched.length > 0;
|
|
46
|
+
const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
|
|
47
|
+
reporting.addReport({
|
|
48
|
+
type: "accesslint",
|
|
49
|
+
version: 1,
|
|
50
|
+
result: {
|
|
51
|
+
violations: enriched,
|
|
52
|
+
ruleCount: result.ruleCount
|
|
53
|
+
},
|
|
54
|
+
status: hasViolations ? mode : "passed"
|
|
55
|
+
});
|
|
57
56
|
};
|
|
58
|
-
var decorators = [decorator];
|
|
59
57
|
|
|
60
|
-
export {
|
|
58
|
+
export { afterEach };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
4
|
+
// src/vitest-plugin.ts
|
|
5
|
+
function accesslintTest() {
|
|
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
|
+
return {
|
|
8
|
+
name: "@accesslint/storybook-addon",
|
|
9
|
+
config() {
|
|
10
|
+
return {
|
|
11
|
+
server: {
|
|
12
|
+
fs: {
|
|
13
|
+
allow: [distDir]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
test: {
|
|
17
|
+
setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.accesslintTest = accesslintTest;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest plugin that automatically registers AccessLint's afterEach annotation
|
|
3
|
+
* so that running component tests produces per-story accessibility badges.
|
|
4
|
+
*
|
|
5
|
+
* Usage in vitest.config.ts (or the storybook vitest workspace):
|
|
6
|
+
*
|
|
7
|
+
* import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
8
|
+
* import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* plugins: [storybookTest(), accesslintTest()],
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
declare function accesslintTest(): {
|
|
15
|
+
name: string;
|
|
16
|
+
config: () => Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { accesslintTest };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest plugin that automatically registers AccessLint's afterEach annotation
|
|
3
|
+
* so that running component tests produces per-story accessibility badges.
|
|
4
|
+
*
|
|
5
|
+
* Usage in vitest.config.ts (or the storybook vitest workspace):
|
|
6
|
+
*
|
|
7
|
+
* import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
8
|
+
* import { accesslintTest } from "@accesslint/storybook-addon/vitest-plugin";
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* plugins: [storybookTest(), accesslintTest()],
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
declare function accesslintTest(): {
|
|
15
|
+
name: string;
|
|
16
|
+
config: () => Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { accesslintTest };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/vitest-plugin.ts
|
|
2
|
+
function accesslintTest() {
|
|
3
|
+
const distDir = new URL(".", import.meta.url).pathname;
|
|
4
|
+
return {
|
|
5
|
+
name: "@accesslint/storybook-addon",
|
|
6
|
+
config() {
|
|
7
|
+
return {
|
|
8
|
+
server: {
|
|
9
|
+
fs: {
|
|
10
|
+
allow: [distDir]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
test: {
|
|
14
|
+
setupFiles: ["@accesslint/storybook-addon/vitest-setup"]
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { accesslintTest };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var previewApi = require('storybook/preview-api');
|
|
4
|
+
var core = require('@accesslint/core');
|
|
5
|
+
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/preview.ts
|
|
13
|
+
var preview_exports = {};
|
|
14
|
+
__export(preview_exports, {
|
|
15
|
+
afterEach: () => afterEach
|
|
16
|
+
});
|
|
17
|
+
core.configureRules({
|
|
18
|
+
disabledRules: ["accesslint-045"]
|
|
19
|
+
});
|
|
20
|
+
function scopeViolations(violations) {
|
|
21
|
+
const root = document.getElementById("storybook-root");
|
|
22
|
+
if (!root) return violations;
|
|
23
|
+
return violations.filter((v) => {
|
|
24
|
+
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
25
|
+
try {
|
|
26
|
+
const el = document.querySelector(local);
|
|
27
|
+
return el && root.contains(el);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function enrichViolations(violations) {
|
|
34
|
+
return violations.map((v) => {
|
|
35
|
+
const rule = core.getRuleById(v.ruleId);
|
|
36
|
+
return {
|
|
37
|
+
...v,
|
|
38
|
+
element: void 0,
|
|
39
|
+
// not serializable
|
|
40
|
+
description: rule?.description,
|
|
41
|
+
wcag: rule?.wcag,
|
|
42
|
+
level: rule?.level,
|
|
43
|
+
guidance: rule?.guidance
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
var afterEach = async ({
|
|
48
|
+
reporting,
|
|
49
|
+
parameters,
|
|
50
|
+
viewMode
|
|
51
|
+
}) => {
|
|
52
|
+
const accesslintParam = parameters?.accesslint;
|
|
53
|
+
if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
|
|
54
|
+
if (viewMode !== "story") return;
|
|
55
|
+
const result = core.runAudit(document);
|
|
56
|
+
const scoped = scopeViolations(result.violations);
|
|
57
|
+
const enriched = enrichViolations(scoped);
|
|
58
|
+
const hasViolations = enriched.length > 0;
|
|
59
|
+
const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
|
|
60
|
+
reporting.addReport({
|
|
61
|
+
type: "accesslint",
|
|
62
|
+
version: 1,
|
|
63
|
+
result: {
|
|
64
|
+
violations: enriched,
|
|
65
|
+
ruleCount: result.ruleCount
|
|
66
|
+
},
|
|
67
|
+
status: hasViolations ? mode : "passed"
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/vitest-setup.ts
|
|
72
|
+
var g = globalThis;
|
|
73
|
+
var existing = g.globalProjectAnnotations;
|
|
74
|
+
g.globalProjectAnnotations = existing ? previewApi.composeConfigs([existing, preview_exports]) : previewApi.composeConfigs([preview_exports]);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { composeConfigs } from 'storybook/preview-api';
|
|
2
|
+
import { configureRules, runAudit, getRuleById } from '@accesslint/core';
|
|
3
|
+
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/preview.ts
|
|
11
|
+
var preview_exports = {};
|
|
12
|
+
__export(preview_exports, {
|
|
13
|
+
afterEach: () => afterEach
|
|
14
|
+
});
|
|
15
|
+
configureRules({
|
|
16
|
+
disabledRules: ["accesslint-045"]
|
|
17
|
+
});
|
|
18
|
+
function scopeViolations(violations) {
|
|
19
|
+
const root = document.getElementById("storybook-root");
|
|
20
|
+
if (!root) return violations;
|
|
21
|
+
return violations.filter((v) => {
|
|
22
|
+
const local = v.selector.replace(/^.*>>>\s*iframe>\s*/, "");
|
|
23
|
+
try {
|
|
24
|
+
const el = document.querySelector(local);
|
|
25
|
+
return el && root.contains(el);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function enrichViolations(violations) {
|
|
32
|
+
return violations.map((v) => {
|
|
33
|
+
const rule = getRuleById(v.ruleId);
|
|
34
|
+
return {
|
|
35
|
+
...v,
|
|
36
|
+
element: void 0,
|
|
37
|
+
// not serializable
|
|
38
|
+
description: rule?.description,
|
|
39
|
+
wcag: rule?.wcag,
|
|
40
|
+
level: rule?.level,
|
|
41
|
+
guidance: rule?.guidance
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
var afterEach = async ({
|
|
46
|
+
reporting,
|
|
47
|
+
parameters,
|
|
48
|
+
viewMode
|
|
49
|
+
}) => {
|
|
50
|
+
const accesslintParam = parameters?.accesslint;
|
|
51
|
+
if (accesslintParam?.disable === true || accesslintParam?.test === "off") return;
|
|
52
|
+
if (viewMode !== "story") return;
|
|
53
|
+
const result = runAudit(document);
|
|
54
|
+
const scoped = scopeViolations(result.violations);
|
|
55
|
+
const enriched = enrichViolations(scoped);
|
|
56
|
+
const hasViolations = enriched.length > 0;
|
|
57
|
+
const mode = accesslintParam?.test === "todo" ? "warning" : "failed";
|
|
58
|
+
reporting.addReport({
|
|
59
|
+
type: "accesslint",
|
|
60
|
+
version: 1,
|
|
61
|
+
result: {
|
|
62
|
+
violations: enriched,
|
|
63
|
+
ruleCount: result.ruleCount
|
|
64
|
+
},
|
|
65
|
+
status: hasViolations ? mode : "passed"
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/vitest-setup.ts
|
|
70
|
+
var g = globalThis;
|
|
71
|
+
var existing = g.globalProjectAnnotations;
|
|
72
|
+
g.globalProjectAnnotations = existing ? composeConfigs([existing, preview_exports]) : composeConfigs([preview_exports]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@accesslint/storybook-addon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "Catch accessibility violations in your Storybook stories as you develop",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"./preview": {
|
|
25
25
|
"import": "./dist/preview.js",
|
|
26
26
|
"require": "./dist/preview.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./vitest-plugin": {
|
|
29
|
+
"import": "./dist/vitest-plugin.js",
|
|
30
|
+
"require": "./dist/vitest-plugin.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./vitest-setup": {
|
|
33
|
+
"import": "./dist/vitest-setup.js",
|
|
34
|
+
"require": "./dist/vitest-setup.cjs"
|
|
27
35
|
}
|
|
28
36
|
},
|
|
29
37
|
"main": "dist/index.cjs",
|