@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/scripts/a11y.js CHANGED
@@ -1,61 +1,113 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @file a11y.js
4
- * Contains a script that, when executed, will execute a11y linting tools
5
- * against the storybook build.
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
- // project specific configuration.
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
- const issueIsValid = ({ code, runnerExtras: { description } }) =>
32
- ignore.codes.includes(code) || ignore.descriptions.includes(description)
33
- ? false
34
- : true;
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
- console.log(`
38
- severity: ${chalk[severityToColor(severity)](severity)}
39
- message: ${message}
40
- context: ${context}
41
- selector: ${selector}
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
- console.log(chalk.red(`Issues found in component: ${pageUrl}`));
51
- validIssues.map(logIssue);
95
+ // eslint-disable-next-line no-console
96
+ console.log(`Issues found in component: ${pageUrl}`);
97
+ validIssues.forEach(logIssue);
52
98
  } else {
53
- console.log(chalk.green(`No issues found in component: ${pageUrl}`));
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
- R.map(lintComponent),
69
- (p) => Promise.all(p),
125
+ /** @param {string[]} list */
126
+ (list) => list.map(lintComponent),
127
+ (promises) => Promise.all(promises),
70
128
  R.andThen(
71
129
  R.pipe(
72
- R.map(logReport),
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
 
@@ -31,7 +31,9 @@ describe('a11y', () => {
31
31
  global.console.log.mockClear();
32
32
  global.process.exit.mockClear();
33
33
  });
34
- it('can map axe issue severity to the correct chalk color', () => {
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('can use an axe issue to generate a single log message about the issue', () => {
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
- severity: error
70
- message: this chicken is not fried enough.
71
- context: https://example.com
72
- selector: kfc > popeyes > .chicken
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('can log a whole axe report', () => {
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
- Array [
100
- Array [
101
- "Issues found in component: https://example/component.html",
102
- ],
103
- Array [
104
- "
105
- severity: error
106
- message: this pizza is too soggy
107
- context: https://example.com
108
- selector: pizza > .hut
109
- ",
110
- ],
111
- Array [
112
- "
113
- severity: error
114
- message: this pasta is undercooked
115
- context: https://example.com
116
- selector: olive > .garden
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 about a component having no issue if a report comes back empty', () => {
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
- `"No issues found in component: papa-johns"`,
128
+ `"No issues found in component: papa-johns"`,
127
129
  );
128
130
  });
129
131
 
130
- it('can call pa11y with the full path to a component', async () => {
131
- expect.assertions(2);
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
- expect(pa11y).toHaveBeenCalledWith(
137
+
138
+ // First arg: URL
139
+ expect(pa11y.mock.calls[0][0]).toBe(
136
140
  `${STORYBOOK_IFRAME}?id=chicken-strips`,
137
- pa11yConfig,
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