Skip to content

Commit 5f2df75

Browse files
committed
feat(plugin-axe): add setup wizard binding
1 parent b3fcaad commit 5f2df75

11 files changed

Lines changed: 329 additions & 19 deletions

File tree

‎packages/create-cli/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
7272
|**`--lighthouse.urls`**|`string \| string[]`|`http://localhost:4200`| Target URL(s) (comma-separated) |
7373
|**`--lighthouse.categories`**|`('performance'`\|`'a11y'`\|`'best-practices'`\|`'seo')[]`| all | Lighthouse categories |
7474

75+
#### Axe
76+
77+
| Option | Type | Default | Description |
78+
| ----------------------- | ------------------------------------------------------------ | ----------------------- | ------------------------------------------ |
79+
|**`--axe.urls`**|`string \| string[]`|`http://localhost:4200`| Target URL(s) (comma-separated) |
80+
|**`--axe.preset`**|`'wcag21aa'`\|`'wcag22aa'`\|`'best-practice'`\|`'all'`|`wcag21aa`| Accessibility preset |
81+
|**`--axe.setupScript`**|`boolean`|`false`| Create setup script for auth-protected app |
82+
|**`--axe.categories`**|`boolean`|`true`| Add Axe categories |
83+
7584
### Examples
7685

7786
Run interactively (default):

‎packages/create-cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"type": "module",
2828
"dependencies": {
29+
"@code-pushup/axe-plugin": "0.124.0",
2930
"@code-pushup/coverage-plugin": "0.124.0",
3031
"@code-pushup/eslint-plugin": "0.124.0",
3132
"@code-pushup/js-packages-plugin": "0.124.0",

‎packages/create-cli/src/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#! /usr/bin/env node
22
importyargsfrom'yargs';
33
import{hideBin}from'yargs/helpers';
4+
import{axeSetupBinding}from'@code-pushup/axe-plugin';
45
import{coverageSetupBinding}from'@code-pushup/coverage-plugin';
56
import{eslintSetupBinding}from'@code-pushup/eslint-plugin';
67
import{jsPackagesSetupBinding}from'@code-pushup/js-packages-plugin';
@@ -15,13 +16,14 @@ import {
1516
}from'./lib/setup/types.js';
1617
import{runSetupWizard}from'./lib/setup/wizard.js';
1718

18-
// TODO: create, import and pass remaining plugin bindings (jsdocs, axe)
19+
// TODO: create, import and pass remaining plugin bindings (jsdocs)
1920
constbindings: PluginSetupBinding[]=[
2021
eslintSetupBinding,
2122
coverageSetupBinding,
2223
jsPackagesSetupBinding,
2324
typescriptSetupBinding,
2425
lighthouseSetupBinding,
26+
axeSetupBinding,
2527
];
2628

2729
constargv=awaityargs(hideBin(process.argv))

‎packages/create-cli/src/lib/setup/codegen-categories.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ function addCategoryRefs(
8585
refsExpressions: MergedCategory['refsExpressions'],
8686
depth: number,
8787
): void{
88+
if(refsExpressions.length===1&&refs.length===0){
89+
builder.addLine(`refs: ${refsExpressions[0]},`,depth);
90+
return;
91+
}
8892
builder.addLine('refs: [',depth);
8993
builder.addLines(
9094
refsExpressions.map(expr=>`...${expr},`),

‎packages/create-cli/src/lib/setup/codegen.unit.test.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,7 @@ describe('generateConfigSource', () => {
490490
{
491491
slug: 'performance',
492492
title: 'Performance',
493-
refs: [
494-
...lighthouseGroupRefs(lhPlugin, 'performance'),
495-
],
493+
refs: lighthouseGroupRefs(lhPlugin, 'performance'),
496494
},
497495
],
498496
} satisfies CoreConfig;

‎packages/plugin-axe/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export {
1212
axeGroupRefs,
1313
}from'./lib/utils.js';
1414
export{axeCategories}from'./lib/categories.js';
15+
export{axeSetupBinding}from'./lib/binding.js';
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import{createRequire}from'node:module';
2+
importtype{
3+
CategoryCodegenConfig,
4+
PluginAnswer,
5+
PluginSetupBinding,
6+
PluginSetupTree,
7+
}from'@code-pushup/models';
8+
import{
9+
answerBoolean,
10+
answerNonEmptyArray,
11+
answerString,
12+
singleQuote,
13+
}from'@code-pushup/utils';
14+
import{
15+
AXE_DEFAULT_PRESET,
16+
AXE_PLUGIN_SLUG,
17+
AXE_PLUGIN_TITLE,
18+
AXE_PRESET_NAMES,
19+
}from'./constants.js';
20+
21+
const{name: PACKAGE_NAME}=createRequire(import.meta.url)(
22+
'../../package.json',
23+
)astypeofimport('../../package.json');
24+
25+
constDEFAULT_URL='http://localhost:4200';
26+
constPLUGIN_VAR='axe';
27+
constSETUP_SCRIPT_PATH='./axe-setup.ts';
28+
29+
constCATEGORIES: CategoryCodegenConfig[]=[
30+
{
31+
slug: 'a11y',
32+
title: 'Accessibility',
33+
description: 'Tests website **accessibility** in accordance with WCAG',
34+
refsExpression: `axeGroupRefs(${PLUGIN_VAR})`,
35+
},
36+
];
37+
38+
constPRESET_CHOICES=Object.entries(AXE_PRESET_NAMES).map(
39+
([value,name])=>({ name, value }),
40+
);
41+
42+
constSETUP_SCRIPT_CONTENT=`import type { Page } from 'playwright-core';
43+
44+
export default async function (page: Page): Promise<void> {
45+
// ... add your custom logic here ...
46+
}
47+
`;
48+
49+
typeAxeOptions={
50+
urls: [string, ...string[]];
51+
preset: string;
52+
setupScript: boolean;
53+
categories: boolean;
54+
};
55+
56+
exportconstaxeSetupBinding={
57+
slug: AXE_PLUGIN_SLUG,
58+
title: AXE_PLUGIN_TITLE,
59+
packageName: PACKAGE_NAME,
60+
prompts: async()=>[
61+
{
62+
key: 'axe.urls',
63+
message: 'Target URL(s) (comma-separated)',
64+
type: 'input',
65+
default: DEFAULT_URL,
66+
},
67+
{
68+
key: 'axe.preset',
69+
message: 'Accessibility preset',
70+
type: 'select',
71+
choices: [...PRESET_CHOICES],
72+
default: AXE_DEFAULT_PRESET,
73+
},
74+
{
75+
key: 'axe.setupScript',
76+
message: 'Create setup script for auth-protected app?',
77+
type: 'confirm',
78+
default: false,
79+
},
80+
{
81+
key: 'axe.categories',
82+
message: 'Add Axe categories?',
83+
type: 'confirm',
84+
default: true,
85+
},
86+
],
87+
generateConfig: async(
88+
answers: Record<string,PluginAnswer>,
89+
tree: PluginSetupTree,
90+
)=>{
91+
constoptions=parseAnswers(answers);
92+
if(options.setupScript){
93+
awaittree.write(SETUP_SCRIPT_PATH,SETUP_SCRIPT_CONTENT);
94+
}
95+
consthasCategories=options.categories;
96+
constimports=[
97+
{
98+
moduleSpecifier: PACKAGE_NAME,
99+
defaultImport: 'axePlugin',
100+
...(hasCategories ? {namedImports: ['axeGroupRefs']} : {}),
101+
},
102+
];
103+
constpluginCall=formatPluginCall(options);
104+
105+
if(!hasCategories){
106+
return{
107+
imports,
108+
pluginInit: [`${pluginCall},`],
109+
};
110+
}
111+
return{
112+
imports,
113+
pluginDeclaration: {
114+
identifier: PLUGIN_VAR,
115+
expression: pluginCall,
116+
},
117+
pluginInit: [`${PLUGIN_VAR},`],
118+
categories: CATEGORIES,
119+
};
120+
},
121+
}satisfiesPluginSetupBinding;
122+
123+
functionparseAnswers(answers: Record<string,PluginAnswer>): AxeOptions{
124+
return{
125+
urls: answerNonEmptyArray(answers,'axe.urls',DEFAULT_URL),
126+
preset: answerString(answers,'axe.preset')||AXE_DEFAULT_PRESET,
127+
setupScript: answerBoolean(answers,'axe.setupScript'),
128+
categories: answerBoolean(answers,'axe.categories'),
129+
};
130+
}
131+
132+
functionformatPluginCall({ urls, preset, setupScript }: AxeOptions): string{
133+
constformattedUrls=formatUrls(urls);
134+
constoptions=[
135+
preset!==AXE_DEFAULT_PRESET&&`preset: ${singleQuote(preset)}`,
136+
setupScript&&`setupScript: ${singleQuote(SETUP_SCRIPT_PATH)}`,
137+
].filter(Boolean);
138+
139+
if(options.length===0){
140+
return`axePlugin(${formattedUrls})`;
141+
}
142+
return`axePlugin(${formattedUrls}, { ${options.join(', ')} })`;
143+
}
144+
145+
functionformatUrls([first, ...rest]: [string, ...string[]]): string{
146+
if(rest.length===0){
147+
returnsingleQuote(first);
148+
}
149+
return`[${[first, ...rest].map(singleQuote).join(', ')}]`;
150+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
importtype{PluginAnswer}from'@code-pushup/models';
2+
import{createMockTree}from'@code-pushup/test-utils';
3+
import{axeSetupBindingasbinding}from'./binding.js';
4+
5+
constdefaultAnswers: Record<string,PluginAnswer>={
6+
'axe.urls': 'http://localhost:4200',
7+
'axe.preset': 'wcag21aa',
8+
'axe.setupScript': false,
9+
'axe.categories': true,
10+
};
11+
12+
constnoCategoryAnswers: Record<string,PluginAnswer>={
13+
...defaultAnswers,
14+
'axe.categories': false,
15+
};
16+
17+
describe('axeSetupBinding',()=>{
18+
describe('prompts',()=>{
19+
it('should offer preset choices with wcag21aa as default',async()=>{
20+
awaitexpect(binding.prompts!()).resolves.toIncludeAllPartialMembers([
21+
{key: 'axe.preset',type: 'select',default: 'wcag21aa'},
22+
]);
23+
});
24+
25+
it('should default setupScript to false',async()=>{
26+
awaitexpect(binding.prompts!()).resolves.toIncludeAllPartialMembers([
27+
{key: 'axe.setupScript',type: 'confirm',default: false},
28+
]);
29+
});
30+
});
31+
32+
describe('generateConfig with categories selected',()=>{
33+
it('should declare plugin as a variable for use in category refs',async()=>{
34+
const{ pluginDeclaration }=awaitbinding.generateConfig(
35+
defaultAnswers,
36+
createMockTree(),
37+
);
38+
expect(pluginDeclaration).toStrictEqual({
39+
identifier: 'axe',
40+
expression: "axePlugin('http://localhost:4200')",
41+
});
42+
});
43+
44+
it('should import axeGroupRefs helper',async()=>{
45+
const{ imports }=awaitbinding.generateConfig(
46+
defaultAnswers,
47+
createMockTree(),
48+
);
49+
expect(imports).toStrictEqual([
50+
expect.objectContaining({namedImports: ['axeGroupRefs']}),
51+
]);
52+
});
53+
54+
it('should produce accessibility category with refs expression',async()=>{
55+
const{ categories }=awaitbinding.generateConfig(
56+
defaultAnswers,
57+
createMockTree(),
58+
);
59+
expect(categories).toStrictEqual([
60+
expect.objectContaining({
61+
slug: 'a11y',
62+
refsExpression: 'axeGroupRefs(axe)',
63+
}),
64+
]);
65+
});
66+
});
67+
68+
describe('generateConfig without categories selected',()=>{
69+
it('should not declare plugin as a variable',async()=>{
70+
const{ pluginDeclaration }=awaitbinding.generateConfig(
71+
noCategoryAnswers,
72+
createMockTree(),
73+
);
74+
expect(pluginDeclaration).toBeUndefined();
75+
});
76+
77+
it('should not import axeGroupRefs helper',async()=>{
78+
const{ imports }=awaitbinding.generateConfig(
79+
noCategoryAnswers,
80+
createMockTree(),
81+
);
82+
expect(imports[0]).not.toHaveProperty('namedImports');
83+
});
84+
85+
it('should not produce categories',async()=>{
86+
const{ categories }=awaitbinding.generateConfig(
87+
noCategoryAnswers,
88+
createMockTree(),
89+
);
90+
expect(categories).toBeUndefined();
91+
});
92+
});
93+
94+
describe('setup script',()=>{
95+
it('should write setup script file when confirmed',async()=>{
96+
consttree=createMockTree();
97+
awaitbinding.generateConfig(
98+
{ ...defaultAnswers,'axe.setupScript': true},
99+
tree,
100+
);
101+
expect(tree.written.get('./axe-setup.ts')).toContain(
102+
"import type { Page } from 'playwright-core'",
103+
);
104+
});
105+
106+
it('should include setupScript in plugin call when confirmed',async()=>{
107+
const{ pluginDeclaration }=awaitbinding.generateConfig(
108+
{ ...defaultAnswers,'axe.setupScript': true},
109+
createMockTree(),
110+
);
111+
expect(pluginDeclaration!.expression).toContain(
112+
"setupScript: './axe-setup.ts'",
113+
);
114+
});
115+
116+
it('should not write setup script file when declined',async()=>{
117+
consttree=createMockTree();
118+
awaitbinding.generateConfig(defaultAnswers,tree);
119+
expect(tree.written.size).toBe(0);
120+
});
121+
});
122+
123+
it('should include non-default preset in plugin call',async()=>{
124+
const{ pluginDeclaration }=awaitbinding.generateConfig(
125+
{ ...defaultAnswers,'axe.preset': 'wcag22aa'},
126+
createMockTree(),
127+
);
128+
expect(pluginDeclaration!.expression).toContain("preset: 'wcag22aa'");
129+
});
130+
131+
it('should format multiple URLs as array',async()=>{
132+
const{ pluginDeclaration }=awaitbinding.generateConfig(
133+
{
134+
...defaultAnswers,
135+
'axe.urls': 'http://localhost:4200/login, http://localhost:4200/home',
136+
},
137+
createMockTree(),
138+
);
139+
expect(pluginDeclaration!.expression).toContain(
140+
"axePlugin(['http://localhost:4200/login', 'http://localhost:4200/home']",
141+
);
142+
});
143+
});

‎packages/plugin-coverage/src/lib/binding.unit.test.ts‎

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import{vol}from'memfs';
2-
importtype{PluginAnswer,PluginSetupTree}from'@code-pushup/models';
3-
import{MEMFS_VOLUME}from'@code-pushup/test-utils';
2+
importtype{PluginAnswer}from'@code-pushup/models';
3+
import{MEMFS_VOLUME,createMockTree}from'@code-pushup/test-utils';
44
import{readJsonFile}from'@code-pushup/utils';
55
import{coverageSetupBindingasbinding}from'./binding.js';
66

@@ -22,19 +22,6 @@ const defaultAnswers: Record<string, PluginAnswer> = {
2222
'coverage.categories': true,
2323
};
2424

25-
functioncreateMockTree(
26-
files: Record<string,string>={},
27-
): PluginSetupTree&{written: Map<string,string>}{
28-
constwritten=newMap<string,string>();
29-
return{
30-
written,
31-
read: async(filePath: string)=>files[filePath]??null,
32-
write: async(filePath: string,content: string)=>{
33-
written.set(filePath,content);
34-
},
35-
};
36-
}
37-
3825
describe('coverageSetupBinding',()=>{
3926
beforeEach(()=>{
4027
vol.fromJSON({'.gitkeep': ''},MEMFS_VOLUME);

0 commit comments

Comments
 (0)