@emulsify/core 3.1.1 → 3.3.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/.storybook/main.js +4 -4
- package/config/webpack/loaders.js +29 -15
- package/config/webpack/optimizers.js +0 -1
- package/config/webpack/plugins.js +188 -41
- package/config/webpack/resolves.js +110 -44
- package/config/webpack/webpack.common.js +197 -156
- package/package.json +28 -29
- package/scripts/a11y.js +83 -23
- package/scripts/a11y.test.js +51 -38
package/scripts/a11y.js
CHANGED
|
@@ -1,61 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* @fileoverview a11y.js
|
|
4
|
+
* Runs accessibility linting (pa11y/axe) against a Storybook build
|
|
5
|
+
* and reports issues.
|
|
6
6
|
*/
|
|
7
|
-
const chalk = import("chalk").then(m => m.default);
|
|
8
7
|
|
|
9
8
|
const R = require('ramda');
|
|
10
9
|
const path = require('path');
|
|
11
10
|
const pa11y = require('pa11y');
|
|
11
|
+
|
|
12
12
|
const {
|
|
13
13
|
storybookBuildDir,
|
|
14
14
|
pa11y: pa11yConfig,
|
|
15
15
|
} = require('../config/a11y.config.js');
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
// Project-specific configuration.
|
|
17
18
|
const {
|
|
18
19
|
ignore,
|
|
19
20
|
components,
|
|
20
21
|
} = require('../../../config/emulsify-core/a11y.config.js');
|
|
21
22
|
|
|
23
|
+
/** Absolute path to Storybook build directory. */
|
|
22
24
|
const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
|
|
25
|
+
/** Absolute path to Storybook iframe file used for per-story rendering. */
|
|
23
26
|
const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Map pa11y/axe severity to a label (historically a color name).
|
|
30
|
+
* Retained for backward compatibility, but not used for styling anymore.
|
|
31
|
+
* @deprecated Colors are no longer used; this function returns a label only.
|
|
32
|
+
* @param {'error'|'warning'|'notice'} severity
|
|
33
|
+
* @returns {'red'|'yellow'|'blue'|undefined}
|
|
34
|
+
*/
|
|
25
35
|
const severityToColor = R.cond([
|
|
26
36
|
[R.equals('error'), R.always('red')],
|
|
27
37
|
[R.equals('warning'), R.always('yellow')],
|
|
28
38
|
[R.equals('notice'), R.always('blue')],
|
|
29
39
|
]);
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} Pa11yIssue
|
|
43
|
+
* @property {string} code - Rule identifier.
|
|
44
|
+
* @property {'error'|'warning'|'notice'} type - Severity level.
|
|
45
|
+
* @property {string} message - Human-readable description.
|
|
46
|
+
* @property {string} context - HTML context snippet.
|
|
47
|
+
* @property {string} selector - CSS selector for the node.
|
|
48
|
+
* @property {{ description?: string }} [runnerExtras] - Extra data from the runner.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determine whether an issue should be reported (not ignored).
|
|
53
|
+
* @param {Pa11yIssue} issue
|
|
54
|
+
* @returns {boolean} True if the issue is NOT ignored and should be logged.
|
|
55
|
+
*/
|
|
56
|
+
const issueIsValid = (issue) => {
|
|
57
|
+
const code = issue?.code;
|
|
58
|
+
const description = issue?.runnerExtras?.description;
|
|
59
|
+
const codeIgnored = Array.isArray(ignore?.codes) && ignore.codes.includes(code);
|
|
60
|
+
const descIgnored =
|
|
61
|
+
description &&
|
|
62
|
+
Array.isArray(ignore?.descriptions) &&
|
|
63
|
+
ignore.descriptions.includes(description);
|
|
64
|
+
return !(codeIgnored || descIgnored);
|
|
65
|
+
};
|
|
35
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Log a single accessibility issue in a readable, colorless block.
|
|
69
|
+
* @param {Pa11yIssue} issue
|
|
70
|
+
* @returns {void}
|
|
71
|
+
*/
|
|
36
72
|
const logIssue = ({ type: severity, message, context, selector }) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
const lines = [
|
|
74
|
+
'', // leading blank for readability
|
|
75
|
+
`severity: ${severity}`,
|
|
76
|
+
`message: ${message}`,
|
|
77
|
+
`context: ${context}`,
|
|
78
|
+
`selector: ${selector}`,
|
|
79
|
+
'',
|
|
80
|
+
];
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.log(lines.join('\n'));
|
|
43
83
|
};
|
|
44
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Log a report for a single component/page and return whether it had issues.
|
|
87
|
+
* @param {{ issues: Pa11yIssue[], pageUrl: string }} report
|
|
88
|
+
* @returns {boolean} True if the component has at least one non-ignored issue.
|
|
89
|
+
*/
|
|
45
90
|
const logReport = ({ issues, pageUrl }) => {
|
|
46
|
-
const validIssues = issues.filter(issueIsValid);
|
|
91
|
+
const validIssues = (issues || []).filter(issueIsValid);
|
|
47
92
|
const hasIssues = validIssues.length > 0;
|
|
48
93
|
|
|
49
94
|
if (hasIssues) {
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log(`Issues found in component: ${pageUrl}`);
|
|
97
|
+
validIssues.forEach(logIssue);
|
|
52
98
|
} else {
|
|
53
|
-
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(`No issues found in component: ${pageUrl}`);
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
return hasIssues;
|
|
57
104
|
};
|
|
58
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Run pa11y on a single Storybook story by its ID.
|
|
108
|
+
* @param {string} name - Story ID (e.g., "components-button--primary").
|
|
109
|
+
* @returns {Promise<{ issues: Pa11yIssue[], pageUrl: string }>} Pa11y result.
|
|
110
|
+
*/
|
|
59
111
|
const lintComponent = async (name) =>
|
|
60
112
|
pa11y(`${STORYBOOK_IFRAME}?id=${name}`, {
|
|
61
113
|
includeNotices: true,
|
|
@@ -64,21 +116,29 @@ const lintComponent = async (name) =>
|
|
|
64
116
|
...pa11yConfig,
|
|
65
117
|
});
|
|
66
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Lint a list of components, log reports, and exit(1) if any have issues.
|
|
121
|
+
* @param {string[]} names - List of Storybook story IDs.
|
|
122
|
+
* @returns {Promise<void>}
|
|
123
|
+
*/
|
|
67
124
|
const lintReportAndExit = R.pipe(
|
|
68
|
-
|
|
69
|
-
(
|
|
125
|
+
/** @param {string[]} list */
|
|
126
|
+
(list) => list.map(lintComponent),
|
|
127
|
+
(promises) => Promise.all(promises),
|
|
70
128
|
R.andThen(
|
|
71
129
|
R.pipe(
|
|
72
|
-
|
|
130
|
+
/** @param {Array<{issues: Pa11yIssue[], pageUrl: string}>} results */
|
|
131
|
+
(results) => results.map(logReport),
|
|
73
132
|
R.reject(R.equals(false)),
|
|
74
133
|
R.unless(R.isEmpty, () => process.exit(1)),
|
|
75
134
|
),
|
|
76
135
|
),
|
|
77
136
|
);
|
|
78
137
|
|
|
79
|
-
// Only perform linting/reporting when instructed.
|
|
138
|
+
// Only perform linting/reporting when instructed via "-r".
|
|
80
139
|
/* istanbul ignore next */
|
|
81
140
|
if (R.pathEq(['argv', 2], '-r')(process)) {
|
|
141
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
82
142
|
lintReportAndExit(components);
|
|
83
143
|
}
|
|
84
144
|
|
package/scripts/a11y.test.js
CHANGED
|
@@ -31,7 +31,9 @@ describe('a11y', () => {
|
|
|
31
31
|
global.console.log.mockClear();
|
|
32
32
|
global.process.exit.mockClear();
|
|
33
33
|
});
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
it('maps axe issue severity to a label', () => {
|
|
36
|
+
// (Name no longer mentions "chalk")
|
|
35
37
|
expect.assertions(3);
|
|
36
38
|
expect(severityToColor('error')).toBe('red');
|
|
37
39
|
expect(severityToColor('warning')).toBe('yellow');
|
|
@@ -56,7 +58,7 @@ describe('a11y', () => {
|
|
|
56
58
|
expect(issueIsValid({ code: 'chicken', runnerExtras: {} })).toBe(true);
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
it('
|
|
61
|
+
it('logs a single issue without color codes', () => {
|
|
60
62
|
expect.assertions(1);
|
|
61
63
|
logIssue({
|
|
62
64
|
type: 'error',
|
|
@@ -65,16 +67,16 @@ describe('a11y', () => {
|
|
|
65
67
|
selector: 'kfc > popeyes > .chicken',
|
|
66
68
|
});
|
|
67
69
|
expect(global.console.log.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
"
|
|
71
|
+
severity: error
|
|
72
|
+
message: this chicken is not fried enough.
|
|
73
|
+
context: https://example.com
|
|
74
|
+
selector: kfc > popeyes > .chicken
|
|
75
|
+
"
|
|
76
|
+
`);
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
it('
|
|
79
|
+
it('logs a whole report without color codes', () => {
|
|
78
80
|
const report = {
|
|
79
81
|
issues: [
|
|
80
82
|
{
|
|
@@ -96,45 +98,56 @@ describe('a11y', () => {
|
|
|
96
98
|
};
|
|
97
99
|
expect(logReport(report)).toBe(true);
|
|
98
100
|
expect(global.console.log.mock.calls).toMatchInlineSnapshot(`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
Array [
|
|
102
|
+
Array [
|
|
103
|
+
"Issues found in component: https://example/component.html",
|
|
104
|
+
],
|
|
105
|
+
Array [
|
|
106
|
+
"
|
|
107
|
+
severity: error
|
|
108
|
+
message: this pizza is too soggy
|
|
109
|
+
context: https://example.com
|
|
110
|
+
selector: pizza > .hut
|
|
111
|
+
",
|
|
112
|
+
],
|
|
113
|
+
Array [
|
|
114
|
+
"
|
|
115
|
+
severity: error
|
|
116
|
+
message: this pasta is undercooked
|
|
117
|
+
context: https://example.com
|
|
118
|
+
selector: olive > .garden
|
|
119
|
+
",
|
|
120
|
+
],
|
|
121
|
+
]
|
|
122
|
+
`);
|
|
121
123
|
});
|
|
122
124
|
|
|
123
|
-
it('logs
|
|
125
|
+
it('logs that a component has no issues when a report is empty', () => {
|
|
124
126
|
expect(logReport({ issues: [], pageUrl: 'papa-johns' })).toBe(false);
|
|
125
127
|
expect(global.console.log.mock.calls[0][0]).toMatchInlineSnapshot(
|
|
126
|
-
`"
|
|
128
|
+
`"No issues found in component: papa-johns"`,
|
|
127
129
|
);
|
|
128
130
|
});
|
|
129
131
|
|
|
130
|
-
it('
|
|
131
|
-
expect.assertions(
|
|
132
|
+
it('calls pa11y with the full path to a component', async () => {
|
|
133
|
+
expect.assertions(3);
|
|
132
134
|
await expect(lintComponent('chicken-strips')).resolves.toBe(
|
|
133
135
|
'very official report',
|
|
134
136
|
);
|
|
135
|
-
|
|
137
|
+
|
|
138
|
+
// First arg: URL
|
|
139
|
+
expect(pa11y.mock.calls[0][0]).toBe(
|
|
136
140
|
`${STORYBOOK_IFRAME}?id=chicken-strips`,
|
|
137
|
-
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Second arg: options merged with defaults in a11y.js
|
|
144
|
+
expect(pa11y.mock.calls[0][1]).toEqual(
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
includeNotices: true,
|
|
147
|
+
includeWarnings: true,
|
|
148
|
+
runners: ['axe'],
|
|
149
|
+
...pa11yConfig,
|
|
150
|
+
}),
|
|
138
151
|
);
|
|
139
152
|
});
|
|
140
153
|
|