@forsakringskassan/cypress-config 1.2.0 → 1.4.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/README.md +61 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.mjs +71 -6
- package/dist/support.d.ts +1 -0
- package/dist/support.mjs +463 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ If you previously had these packages installed, they can be uninstalled:
|
|
|
14
14
|
npm rm mocha-multi-reporter
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Axe](#axe)
|
|
19
|
+
- [docs-generator](#docs-generator)
|
|
20
|
+
- [HTML-Validate](#html-validate)
|
|
21
|
+
|
|
17
22
|
## Usage
|
|
18
23
|
|
|
19
24
|
In `cypress.config.ts`, replace the vanilla `defineConfig()` helper from `cypress` with `@forsakringskassan/cypress-config` and pass in `import.meta.dirname` as the first parameter:
|
|
@@ -59,6 +64,61 @@ configure({
|
|
|
59
64
|
|
|
60
65
|
See the various components for details.
|
|
61
66
|
|
|
67
|
+
### Axe
|
|
68
|
+
|
|
69
|
+
A preconfigured [Axe](https://github.com/dequelabs/axe-core) plugin ([@forsakringskassan/cypress-axe](https://github.com/Forsakringskassan/cypress-axe/)) is exported as `axePlugin`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { defineConfig, axePlugin } from "@forsakringskassan/cypress-config";
|
|
73
|
+
|
|
74
|
+
export default defineConfig(import.meta.dirname, {
|
|
75
|
+
component: {
|
|
76
|
+
async setupNodeEvents(on, config) {
|
|
77
|
+
config = await axePlugin(on, config);
|
|
78
|
+
return config;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Automatic validation after each test cannot currently be disabled.
|
|
85
|
+
|
|
86
|
+
### docs-generator
|
|
87
|
+
|
|
88
|
+
A plugin to get all pages and examples generated by `@forsakringskassan/docs-generator`.
|
|
89
|
+
This is typically only useful in E2E testing.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import {
|
|
93
|
+
defineConfig,
|
|
94
|
+
docsGeneratorPlugin,
|
|
95
|
+
} from "@forsakringskassan/cypress-config";
|
|
96
|
+
/
|
|
97
|
+
const docs = new Generator(import.meta.url, {});
|
|
98
|
+
const sourceFiles = [];
|
|
99
|
+
|
|
100
|
+
export default defineConfig(import.meta.dirname, {
|
|
101
|
+
e2e: {
|
|
102
|
+
async setupNodeEvents(on, config) {
|
|
103
|
+
config = await docsGeneratorPlugin(on, config, docs, sourceFiles);
|
|
104
|
+
return config;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The pages are retrieved using `Cypress.expose("pages")` which contains additional information:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const pages = Cypress.expose("pages") ?? [];
|
|
114
|
+
for (const page of pages) {
|
|
115
|
+
const { path, examples } = page;
|
|
116
|
+
/* ... */
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
See [manifest documentation](https://forsakringskassan.github.io/docs-generator/latest/processors/manifest.html) for details about what each page entry contains.
|
|
121
|
+
|
|
62
122
|
### HTML-Validate
|
|
63
123
|
|
|
64
124
|
A preconfigured [HTML-validate](https://html-validate.org/) plugin ([cypress-html-validate](https://gitlab.com/html-validate/cypress-html-validate/)) is exported as `htmlValidatePlugin`:
|
|
@@ -73,6 +133,7 @@ export default defineConfig(import.meta.dirname, {
|
|
|
73
133
|
component: {
|
|
74
134
|
async setupNodeEvents(on, config) {
|
|
75
135
|
config = await htmlValidatePlugin(on, config);
|
|
136
|
+
return config;
|
|
76
137
|
},
|
|
77
138
|
},
|
|
78
139
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,18 @@ import { ConfigData } from 'cypress-html-validate';
|
|
|
2
2
|
import { CypressHtmlValidateOptions } from 'cypress-html-validate';
|
|
3
3
|
import { defineConfig as defineConfig_2 } from 'cypress';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Preconfigured `@forsakringskassan/cypress-axe` plugin.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
* @since v1.4.0
|
|
10
|
+
* @param on - Cypress `on` callback from `setupNodeEvents`.
|
|
11
|
+
* @param config - Cypress `config` parameter from `setupNodeEvents`.
|
|
12
|
+
* @param userOptions - Custom configuration to pass to `@forsakringskassan/cypress-axe`.
|
|
13
|
+
* @returns A promise resolved with the updated cypress configuration.
|
|
14
|
+
*/
|
|
15
|
+
export declare function axePlugin(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, userOptions?: object): Promise<Cypress.PluginConfigOptions>;
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* @public
|
|
7
19
|
*/
|
|
@@ -42,6 +54,22 @@ export declare function defineConfig(_rootDir: string, userConfig?: Omit<ConfigO
|
|
|
42
54
|
component?: Partial<ConfigOptions["component"]>;
|
|
43
55
|
}): Readonly<ConfigOptions>;
|
|
44
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Fetch document pages and examples for documentation generated by
|
|
59
|
+
* `@forsakringskassan/docs-generator`.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
* @since v1.3.0
|
|
63
|
+
* @param on - Cypress `on` callback from `setupNodeEvents`.
|
|
64
|
+
* @param config - Cypress `config` parameter from `setupNodeEvents`.
|
|
65
|
+
* @param docs - A `Generator` instance.
|
|
66
|
+
* @param sourceFiles - A set of source files generate manifest for.
|
|
67
|
+
* @returns A promise resolved with the updated cypress configuration.
|
|
68
|
+
*/
|
|
69
|
+
export declare function docsGeneratorPlugin<T extends unknown[]>(_on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, docs: {
|
|
70
|
+
manifest(sourceFiles: T): Promise<unknown>;
|
|
71
|
+
}, sourceFiles: T): Promise<Cypress.PluginConfigOptions>;
|
|
72
|
+
|
|
45
73
|
/**
|
|
46
74
|
* Preconfigured `cypress-html-validate` plugin.
|
|
47
75
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -131,10 +131,42 @@ var require_cjs = __commonJS({
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
// src/plugins/
|
|
135
|
-
import { createRequire } from "node:module";
|
|
134
|
+
// src/plugins/axe.ts
|
|
136
135
|
import "cypress";
|
|
137
|
-
|
|
136
|
+
|
|
137
|
+
// node_modules/@forsakringskassan/cypress-axe/dist/esm/plugins.js
|
|
138
|
+
import fs from "node:fs";
|
|
139
|
+
import { createRequire } from "node:module";
|
|
140
|
+
var localConfig = null;
|
|
141
|
+
var tasks = {
|
|
142
|
+
// Allows access to regular log output
|
|
143
|
+
axeReportLog(message) {
|
|
144
|
+
console.log(message);
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
axeGetInjectorSource() {
|
|
148
|
+
const require4 = createRequire(import.meta.url);
|
|
149
|
+
const scriptPath = require4.resolve("axe-core/axe.min.js");
|
|
150
|
+
return fs.readFileSync(scriptPath, "utf8");
|
|
151
|
+
},
|
|
152
|
+
getLocalConfig() {
|
|
153
|
+
return localConfig;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function init(on, config, userOptions = void 0) {
|
|
157
|
+
on("task", tasks);
|
|
158
|
+
if (userOptions !== void 0) {
|
|
159
|
+
localConfig = userOptions;
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
localConfig = JSON.parse(fs.readFileSync("fk-cypress-axe.json", {
|
|
164
|
+
encoding: "utf8"
|
|
165
|
+
}));
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
return config;
|
|
169
|
+
}
|
|
138
170
|
|
|
139
171
|
// src/utils/merge-config.ts
|
|
140
172
|
var import_deepmerge = __toESM(require_cjs(), 1);
|
|
@@ -145,8 +177,39 @@ function mergeConfig(a, b) {
|
|
|
145
177
|
return (0, import_deepmerge.default)(a, b ?? {}, { arrayMerge: overwriteMerge });
|
|
146
178
|
}
|
|
147
179
|
|
|
180
|
+
// src/plugins/axe.ts
|
|
181
|
+
var defaultOptions = {
|
|
182
|
+
context: {
|
|
183
|
+
include: [[".code-preview"], ["[data-preview]"], ["[data-cy-root]"]],
|
|
184
|
+
exclude: [
|
|
185
|
+
[".calendar__item--selected"],
|
|
186
|
+
[".file-selector input"],
|
|
187
|
+
[".wizard-step__header__title"],
|
|
188
|
+
[".live-example__code"]
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function axePlugin(on, config, userOptions) {
|
|
193
|
+
const options = mergeConfig(defaultOptions, userOptions);
|
|
194
|
+
config = init(on, config, options);
|
|
195
|
+
return Promise.resolve(config);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/plugins/docs-generator.ts
|
|
199
|
+
async function docsGeneratorPlugin(_on, config, docs, sourceFiles) {
|
|
200
|
+
const manifest = await docs.manifest(sourceFiles);
|
|
201
|
+
const pages = manifest.pages.filter((it) => {
|
|
202
|
+
return it.path.endsWith(".html");
|
|
203
|
+
});
|
|
204
|
+
config.expose["pages"] = pages;
|
|
205
|
+
return config;
|
|
206
|
+
}
|
|
207
|
+
|
|
148
208
|
// src/plugins/html-validate.ts
|
|
149
|
-
|
|
209
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
210
|
+
import "cypress";
|
|
211
|
+
import { install } from "cypress-html-validate/plugin";
|
|
212
|
+
var require2 = createRequire2(import.meta.url);
|
|
150
213
|
var exclude = [];
|
|
151
214
|
try {
|
|
152
215
|
exclude = require2("@fkui/vue/htmlvalidate/cypress");
|
|
@@ -195,8 +258,8 @@ function htmlValidatePlugin(on, cypressConfig, userConfig, userOptions) {
|
|
|
195
258
|
}
|
|
196
259
|
|
|
197
260
|
// src/default-config.ts
|
|
198
|
-
import { createRequire as
|
|
199
|
-
var require3 =
|
|
261
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
262
|
+
var require3 = createRequire3(import.meta.url);
|
|
200
263
|
var isGithub = Boolean(process.env["GITHUB_ACTION"]);
|
|
201
264
|
var defaultConfig = Object.freeze({
|
|
202
265
|
allowCypressEnv: false,
|
|
@@ -230,7 +293,9 @@ function defineConfig(_rootDir, userConfig) {
|
|
|
230
293
|
);
|
|
231
294
|
}
|
|
232
295
|
export {
|
|
296
|
+
axePlugin,
|
|
233
297
|
defaultConfig,
|
|
234
298
|
defineConfig,
|
|
299
|
+
docsGeneratorPlugin,
|
|
235
300
|
htmlValidatePlugin
|
|
236
301
|
};
|
package/dist/support.d.ts
CHANGED
package/dist/support.mjs
CHANGED
|
@@ -1,3 +1,466 @@
|
|
|
1
|
+
// node_modules/@forsakringskassan/cypress-axe/dist/esm/support.js
|
|
2
|
+
var defaultConfig = {
|
|
3
|
+
axe: {
|
|
4
|
+
rules: [
|
|
5
|
+
//Disable color contrast rule
|
|
6
|
+
{
|
|
7
|
+
id: "color-contrast",
|
|
8
|
+
enabled: false
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* Context object to pass to axe. If not set `window.document` is passed.
|
|
14
|
+
*
|
|
15
|
+
* Can be set to:
|
|
16
|
+
*
|
|
17
|
+
* - CSS selector with root element.
|
|
18
|
+
* - An object `{include: string[][], exclude: string[][]}`
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
*
|
|
22
|
+
* "context": {
|
|
23
|
+
* "include": [["#app"]],
|
|
24
|
+
* "exclude": [[".selector1"], [".selector2"]]
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* See
|
|
28
|
+
* https://www.deque.com/axe/axe-for-web/documentation/api-documentation/#context-parameter
|
|
29
|
+
* for a longer description.
|
|
30
|
+
*/
|
|
31
|
+
context: null,
|
|
32
|
+
assertOnViolation: true,
|
|
33
|
+
// Stoppar resterande tester så fort ett misslyckas. (Annars rapporteras även felaktiga testfall som lyckade).
|
|
34
|
+
displayHelpUrl: true,
|
|
35
|
+
// Visar aXe help URL tillsammans med felet. Default true.
|
|
36
|
+
displayContext: true,
|
|
37
|
+
// Visar sammanhang (CSS selector, HTML, Förslag till rättelse, osv.)
|
|
38
|
+
displayPasses: false,
|
|
39
|
+
// Visar även passes. Default false
|
|
40
|
+
displayViolations: true,
|
|
41
|
+
// Visar felen. Default true.
|
|
42
|
+
standardsToReport: ["wcag2a", "wcag2aa"],
|
|
43
|
+
// Lista med standarder som kontrolleras. Om listan är tom visas valideras samtliga standarder.
|
|
44
|
+
ignoreAxeFailures: false,
|
|
45
|
+
// Ignorera Axe-valideringsfel. Default false
|
|
46
|
+
displayWhen: "both",
|
|
47
|
+
// När felen ska visas ("immediate", "end", "both")
|
|
48
|
+
excludeSelectorsList: []
|
|
49
|
+
// Lista med eventuella CSS selectorer som ska undantas från tillgänglighetskontrollerna
|
|
50
|
+
};
|
|
51
|
+
var Attribute = {
|
|
52
|
+
ColorCodes: {
|
|
53
|
+
red: "\x1B[31m",
|
|
54
|
+
grey: "\x1B[37m",
|
|
55
|
+
white: "\x1B[37;1m",
|
|
56
|
+
normalColor: "\x1B[39m",
|
|
57
|
+
indent: "",
|
|
58
|
+
green: "\x1B[32m",
|
|
59
|
+
yellow: "\x1B[33m",
|
|
60
|
+
magenta: "\x1B[35m",
|
|
61
|
+
cyan: "\x1B[36m",
|
|
62
|
+
resetColor: "\x1B[0m",
|
|
63
|
+
bold: "\x1B[1m",
|
|
64
|
+
underline: "\x1B[4m",
|
|
65
|
+
reversed: "\x1B[7m"
|
|
66
|
+
},
|
|
67
|
+
red(message) {
|
|
68
|
+
return `${this.ColorCodes.red}${message}${this.ColorCodes.resetColor}`;
|
|
69
|
+
},
|
|
70
|
+
grey(message) {
|
|
71
|
+
return `${this.ColorCodes.grey}${message}${this.ColorCodes.resetColor}`;
|
|
72
|
+
},
|
|
73
|
+
white(message) {
|
|
74
|
+
return `${this.ColorCodes.white}${message}${this.ColorCodes.resetColor}`;
|
|
75
|
+
},
|
|
76
|
+
green(message) {
|
|
77
|
+
return `${this.ColorCodes.green}${message}${this.ColorCodes.resetColor}`;
|
|
78
|
+
},
|
|
79
|
+
yellow(message) {
|
|
80
|
+
return `${this.ColorCodes.yellow}${message}${this.ColorCodes.resetColor}`;
|
|
81
|
+
},
|
|
82
|
+
magenta(message) {
|
|
83
|
+
return `${this.ColorCodes.magenta}${message}${this.ColorCodes.resetColor}`;
|
|
84
|
+
},
|
|
85
|
+
cyan(message) {
|
|
86
|
+
return `${this.ColorCodes.cyan}${message}${this.ColorCodes.resetColor}`;
|
|
87
|
+
},
|
|
88
|
+
bold(message) {
|
|
89
|
+
return `${this.ColorCodes.bold}${message}${this.ColorCodes.resetColor}`;
|
|
90
|
+
},
|
|
91
|
+
underline(message) {
|
|
92
|
+
return `${this.ColorCodes.underline}${message}${this.ColorCodes.resetColor}`;
|
|
93
|
+
},
|
|
94
|
+
reversed(message) {
|
|
95
|
+
return `${this.ColorCodes.reversed}${message}${this.ColorCodes.resetColor}`;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var cachedLocalConfig = null;
|
|
99
|
+
var globalResults = {
|
|
100
|
+
passes: { enabled: [], disabled: [] },
|
|
101
|
+
violations: { enabled: [], disabled: [] }
|
|
102
|
+
};
|
|
103
|
+
function nodeLog(message) {
|
|
104
|
+
cy.task("axeReportLog", message, { log: false });
|
|
105
|
+
}
|
|
106
|
+
function getInjectorSource() {
|
|
107
|
+
return cy.task("axeGetInjectorSource", null, { log: false });
|
|
108
|
+
}
|
|
109
|
+
function validateConfig(config) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
for (const key of Object.keys(config)) {
|
|
112
|
+
if (defaultConfig[key] === void 0) {
|
|
113
|
+
errors.push(`- The field "${key}" is not a valid configuration option.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (config.displayWhen !== "immediate" && config.displayWhen !== "end" && config.displayWhen !== "both") {
|
|
117
|
+
errors.push(`- "displayWhen" can only be set to "immediate", "end", or "both" (was: ${config.displayWhen}).`);
|
|
118
|
+
}
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
nodeLog(Attribute.reversed(Attribute.red(`Invalid fk-cypress-axe configuration!`)));
|
|
121
|
+
for (const error of errors) {
|
|
122
|
+
nodeLog(Attribute.red(error));
|
|
123
|
+
}
|
|
124
|
+
nodeLog(Attribute.red(`As a result, some tests may be omitted or displayed when they shouldn't be!`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function getConfig() {
|
|
128
|
+
const localConfig = cachedLocalConfig;
|
|
129
|
+
if (!localConfig) {
|
|
130
|
+
return defaultConfig;
|
|
131
|
+
}
|
|
132
|
+
const finalConfig = { ...defaultConfig, ...localConfig };
|
|
133
|
+
if (localConfig.axe) {
|
|
134
|
+
finalConfig.axe = {
|
|
135
|
+
...defaultConfig.axe,
|
|
136
|
+
...localConfig.axe
|
|
137
|
+
};
|
|
138
|
+
if (localConfig.axe.rules) {
|
|
139
|
+
finalConfig.axe.rules = [
|
|
140
|
+
...defaultConfig.axe.rules.filter((defaultRule) => {
|
|
141
|
+
return localConfig.axe.rules.every((parsedRule) => {
|
|
142
|
+
return parsedRule.id !== defaultRule.id;
|
|
143
|
+
});
|
|
144
|
+
}),
|
|
145
|
+
...localConfig.axe.rules
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
validateConfig(finalConfig);
|
|
150
|
+
return finalConfig;
|
|
151
|
+
}
|
|
152
|
+
function prepareAxe() {
|
|
153
|
+
getInjectorSource().then((source) => cy.window({ log: false }).then((window) => {
|
|
154
|
+
window.eval(source);
|
|
155
|
+
if (!window.axe) {
|
|
156
|
+
nodeLog(Attribute.red(Attribute.reversed("AXE COULD NOT BE LOADED")) + Attribute.red("\nAxe was likely injected before cy.visit or incorrectly processed"));
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function filterResultsByStandardTags(results, standardTags) {
|
|
161
|
+
const filteredResults = {
|
|
162
|
+
enabled: [],
|
|
163
|
+
disabled: []
|
|
164
|
+
};
|
|
165
|
+
if (standardTags.length === 0) {
|
|
166
|
+
filteredResults.enabled = results;
|
|
167
|
+
return filteredResults;
|
|
168
|
+
}
|
|
169
|
+
const isStandardTag2 = (tag) => {
|
|
170
|
+
return standardTags.includes(tag);
|
|
171
|
+
};
|
|
172
|
+
filteredResults.enabled = results.filter((result) => {
|
|
173
|
+
return result.tags.some(isStandardTag2);
|
|
174
|
+
});
|
|
175
|
+
filteredResults.disabled = results.filter((result) => {
|
|
176
|
+
return !result.tags.some(isStandardTag2);
|
|
177
|
+
});
|
|
178
|
+
return filteredResults;
|
|
179
|
+
}
|
|
180
|
+
function filterResultsByExcludedSelectors(results, excludedSelectors) {
|
|
181
|
+
const filteredResults = {
|
|
182
|
+
enabled: [],
|
|
183
|
+
disabled: []
|
|
184
|
+
};
|
|
185
|
+
const isExcluded = (selector) => {
|
|
186
|
+
return excludedSelectors.includes(selector);
|
|
187
|
+
};
|
|
188
|
+
filteredResults.enabled = results.filter((result) => {
|
|
189
|
+
return result.nodes.some((node) => {
|
|
190
|
+
return !node.target.some(isExcluded);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
filteredResults.disabled = results.filter((result) => {
|
|
194
|
+
return result.nodes.some((node) => {
|
|
195
|
+
return node.target.some(isExcluded);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
return filteredResults;
|
|
199
|
+
}
|
|
200
|
+
function impactToPriority(impact) {
|
|
201
|
+
switch (impact) {
|
|
202
|
+
case "minor":
|
|
203
|
+
return 3;
|
|
204
|
+
case "moderate":
|
|
205
|
+
return 2;
|
|
206
|
+
case "serious":
|
|
207
|
+
return 1;
|
|
208
|
+
case "critical":
|
|
209
|
+
return 0;
|
|
210
|
+
default:
|
|
211
|
+
return 4;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function formatSuggestions(suggestions) {
|
|
215
|
+
return suggestions.reduce((msg, line) => {
|
|
216
|
+
return `${msg}
|
|
217
|
+
* ${line.replace("\n", "").trim()}`;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function formatHTML(html) {
|
|
221
|
+
return html.reduce((msg, line) => {
|
|
222
|
+
return `${msg}
|
|
223
|
+
${line.replace("\n", "")}`;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function formatTarget(target) {
|
|
227
|
+
return target.reduce((msg, target2) => {
|
|
228
|
+
return `${msg} ${target2}`;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
var isStandardTag = (tag) => {
|
|
232
|
+
const config = getConfig();
|
|
233
|
+
return config.standardsToReport.includes(tag);
|
|
234
|
+
};
|
|
235
|
+
function formatTags(tags) {
|
|
236
|
+
return tags.reduce((msg, tag) => {
|
|
237
|
+
if (isStandardTag(tag)) {
|
|
238
|
+
return `${msg}, ${Attribute.green(tag)}`;
|
|
239
|
+
}
|
|
240
|
+
return `${msg}, ${tag}`;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function printContextField(name, value, withBullet) {
|
|
244
|
+
name = `${name}:`.padEnd(14);
|
|
245
|
+
let bullet = ``;
|
|
246
|
+
if (withBullet) {
|
|
247
|
+
bullet = `*`;
|
|
248
|
+
}
|
|
249
|
+
nodeLog(` ${bullet} ${name}${value}`);
|
|
250
|
+
}
|
|
251
|
+
function displayContext(violation, config) {
|
|
252
|
+
cy.wrap(violation.nodes, { log: false }).each((node) => {
|
|
253
|
+
const context = {
|
|
254
|
+
["CSS Selector"]: formatTarget(node.target),
|
|
255
|
+
["HTML"]: formatHTML(node.html.split("\n")),
|
|
256
|
+
["Suggestions"]: formatSuggestions(node.failureSummary.split("\n")),
|
|
257
|
+
["Standards"]: formatTags(violation.tags)
|
|
258
|
+
};
|
|
259
|
+
let withBullet = true;
|
|
260
|
+
for (const [key, value] of Object.entries(context)) {
|
|
261
|
+
if (config.displayContext !== true) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
printContextField(key, value, withBullet);
|
|
265
|
+
withBullet = false;
|
|
266
|
+
}
|
|
267
|
+
const log = Cypress.log({
|
|
268
|
+
name: `(${violation.impact}) ACCESSIBILITY VIOLATION`,
|
|
269
|
+
consoleProps: () => context,
|
|
270
|
+
message: violation.help
|
|
271
|
+
});
|
|
272
|
+
cy.get(context["CSS Selector"], { log: false }).then(($el) => {
|
|
273
|
+
log.set({ $el }).snapshot().end();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function printPass(pass, config) {
|
|
278
|
+
let messageTemplate = ` ${pass.help}`;
|
|
279
|
+
if (config.displayHelpUrl) {
|
|
280
|
+
messageTemplate += `
|
|
281
|
+
${pass.helpUrl}`;
|
|
282
|
+
}
|
|
283
|
+
nodeLog(` ${Attribute.green(Attribute.reversed(" PASS "))}${Attribute.green(messageTemplate)}: [${pass.id}]`);
|
|
284
|
+
}
|
|
285
|
+
function printViolation(violation, config) {
|
|
286
|
+
const impactPriority = impactToPriority(violation.impact);
|
|
287
|
+
let messageTemplate = ` (${violation.impact}) ${violation.help}`;
|
|
288
|
+
if (config.displayHelpUrl) {
|
|
289
|
+
messageTemplate += `
|
|
290
|
+
${violation.helpUrl}`;
|
|
291
|
+
}
|
|
292
|
+
if (impactPriority <= 1) {
|
|
293
|
+
nodeLog(` ${Attribute.red(Attribute.reversed(" FAIL "))}${Attribute.red(messageTemplate)}: [${violation.id}]`);
|
|
294
|
+
} else if (impactPriority <= 2) {
|
|
295
|
+
nodeLog(` ${Attribute.yellow(Attribute.reversed(` FAIL `))}${Attribute.yellow(messageTemplate)}: [${violation.id}]`);
|
|
296
|
+
} else {
|
|
297
|
+
nodeLog(` ${Attribute.magenta(Attribute.reversed(` FAIL `))}${Attribute.magenta(messageTemplate)}: [${violation.id}]`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
var impactComparator = (a, b) => {
|
|
301
|
+
const aImpact = impactToPriority(a.impact);
|
|
302
|
+
const bImpact = impactToPriority(b.impact);
|
|
303
|
+
return Math.sign(aImpact - bImpact);
|
|
304
|
+
};
|
|
305
|
+
function processResults(results, config) {
|
|
306
|
+
results.sort(impactComparator);
|
|
307
|
+
const postPass1 = filterResultsByStandardTags(results, config.standardsToReport);
|
|
308
|
+
const postPass2 = filterResultsByExcludedSelectors(postPass1.enabled, config.excludeSelectorsList);
|
|
309
|
+
return {
|
|
310
|
+
enabled: postPass2.enabled,
|
|
311
|
+
disabled: [...postPass1.disabled, ...postPass2.disabled]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function displayPasses(passes, config) {
|
|
315
|
+
if (!config.displayPasses || passes.enabled.length === 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
for (const pass of passes.enabled) {
|
|
319
|
+
printPass(pass, config);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function displayViolations(violations, config, withTitle) {
|
|
323
|
+
if (!config.displayViolations || violations.enabled.length === 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (violations.enabled.length > 0) {
|
|
327
|
+
if (withTitle) {
|
|
328
|
+
const testName = Cypress.mocha.getRunner()?.suite?.ctx?.currentTest?.title ?? "";
|
|
329
|
+
nodeLog(Attribute.red(` [${violations.enabled.length} Accessibility violation(s) were detected after `) + Attribute.red(Attribute.underline(`"${testName}"`)) + Attribute.red(`]`));
|
|
330
|
+
}
|
|
331
|
+
for (const violation of violations.enabled) {
|
|
332
|
+
printViolation(violation, config);
|
|
333
|
+
if (config.displayContext) {
|
|
334
|
+
displayContext(violation, config);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
nodeLog(Attribute.green(` All enabled tests passed!`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function anythingToDisplay(config, passes, violations) {
|
|
342
|
+
const { displayPasses: displayPasses2, displayViolations: displayViolations2 } = config;
|
|
343
|
+
const numPasses = passes.enabled.length;
|
|
344
|
+
const numViolations = violations.enabled.length;
|
|
345
|
+
const anyPasses = displayPasses2 && numPasses > 0;
|
|
346
|
+
const anyViolations = displayViolations2 && numViolations > 0;
|
|
347
|
+
return anyPasses || anyViolations;
|
|
348
|
+
}
|
|
349
|
+
function runAxe() {
|
|
350
|
+
Cypress.log({
|
|
351
|
+
name: "fk-cypress-axe",
|
|
352
|
+
displayName: "Axe",
|
|
353
|
+
message: ["Running accessibility tests"]
|
|
354
|
+
});
|
|
355
|
+
const config = getConfig();
|
|
356
|
+
if (config.ignoreAxeFailures === true) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
cy.window({ log: false }).then((window) => {
|
|
360
|
+
window.axe.configure(config.axe);
|
|
361
|
+
return window.axe.run(config.context || window.document);
|
|
362
|
+
}).then(({ passes, violations }) => {
|
|
363
|
+
passes = processResults(passes, config);
|
|
364
|
+
violations = processResults(violations, config);
|
|
365
|
+
const displayNow = config.displayWhen === "immediate" || config.displayWhen === "both";
|
|
366
|
+
globalResults.passes.enabled = globalResults.passes.enabled.concat(passes.enabled);
|
|
367
|
+
globalResults.passes.disabled = globalResults.passes.disabled.concat(passes.disabled);
|
|
368
|
+
globalResults.violations.enabled = globalResults.violations.enabled.concat(violations.enabled);
|
|
369
|
+
globalResults.violations.disabled = globalResults.violations.disabled.concat(violations.disabled);
|
|
370
|
+
if (displayNow && anythingToDisplay(config, passes, violations)) {
|
|
371
|
+
nodeLog(" Accessibility results:");
|
|
372
|
+
displayPasses(passes, config);
|
|
373
|
+
displayViolations(violations, config, true);
|
|
374
|
+
nodeLog("");
|
|
375
|
+
}
|
|
376
|
+
return cy.wrap(violations, { log: false });
|
|
377
|
+
}).then((violations) => {
|
|
378
|
+
if (config.assertOnViolation) {
|
|
379
|
+
assert.equal(violations.enabled.length, 0, `${violations.enabled.length} A11y problems were detected`, { log: false });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function removeDuplicates(results) {
|
|
384
|
+
let finalResults = JSON.parse(JSON.stringify(results));
|
|
385
|
+
for (const [index, result] of finalResults.entries()) {
|
|
386
|
+
const foundIndex = finalResults.findIndex((v) => {
|
|
387
|
+
return v.help === result.help;
|
|
388
|
+
});
|
|
389
|
+
if (foundIndex !== index) {
|
|
390
|
+
finalResults[foundIndex].nodes = finalResults[foundIndex].nodes.concat(result.nodes);
|
|
391
|
+
finalResults[index].toBeRemoved = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
finalResults = finalResults.filter((result) => {
|
|
395
|
+
return !result.toBeRemoved;
|
|
396
|
+
});
|
|
397
|
+
for (const result of finalResults) {
|
|
398
|
+
for (const [index, node] of result.nodes.entries()) {
|
|
399
|
+
const foundIndex = result.nodes.findIndex((n) => {
|
|
400
|
+
return n.html === node.html;
|
|
401
|
+
});
|
|
402
|
+
if (foundIndex !== index) {
|
|
403
|
+
result.nodes[index].toBeRemoved = true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
result.nodes = result.nodes.filter((n) => {
|
|
407
|
+
return !n.toBeRemoved;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return finalResults;
|
|
411
|
+
}
|
|
412
|
+
Cypress.Commands.add("getAxeConfigThisFile", () => {
|
|
413
|
+
const config = getConfig();
|
|
414
|
+
return cy.wrap(config);
|
|
415
|
+
});
|
|
416
|
+
Cypress.Commands.add("setAxeConfigThisFile", (userConfig) => {
|
|
417
|
+
cachedLocalConfig = userConfig;
|
|
418
|
+
return cy.wrap(userConfig);
|
|
419
|
+
});
|
|
420
|
+
Cypress.Commands.add("enableAxeFailures", () => {
|
|
421
|
+
const config = getConfig();
|
|
422
|
+
config.ignoreAxeFailures = false;
|
|
423
|
+
cachedLocalConfig = config;
|
|
424
|
+
});
|
|
425
|
+
Cypress.Commands.add("disableAxeFailures", () => {
|
|
426
|
+
const config = getConfig();
|
|
427
|
+
config.ignoreAxeFailures = true;
|
|
428
|
+
cachedLocalConfig = config;
|
|
429
|
+
});
|
|
430
|
+
Cypress.Commands.add("axe", () => {
|
|
431
|
+
prepareAxe();
|
|
432
|
+
runAxe();
|
|
433
|
+
});
|
|
434
|
+
before(() => {
|
|
435
|
+
cy.task("getLocalConfig", null, { log: false }).then((config) => {
|
|
436
|
+
cachedLocalConfig = config;
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
afterEach(() => {
|
|
440
|
+
cy.axe();
|
|
441
|
+
});
|
|
442
|
+
after(() => {
|
|
443
|
+
const config = getConfig();
|
|
444
|
+
const displayNow = config.displayWhen === "end" || config.displayWhen === "both";
|
|
445
|
+
if (config.ignoreAxeFailures === false && displayNow) {
|
|
446
|
+
globalResults.passes.enabled = removeDuplicates(globalResults.passes.enabled);
|
|
447
|
+
globalResults.passes.disabled = removeDuplicates(globalResults.passes.disabled);
|
|
448
|
+
globalResults.violations.enabled = removeDuplicates(globalResults.violations.enabled);
|
|
449
|
+
globalResults.violations.disabled = removeDuplicates(globalResults.violations.disabled);
|
|
450
|
+
globalResults.passes.enabled = globalResults.passes.enabled.filter((pass) => {
|
|
451
|
+
return globalResults.violations.enabled.every((violation) => {
|
|
452
|
+
return pass.id !== violation.id;
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
const { passes, violations } = globalResults;
|
|
456
|
+
if (anythingToDisplay(config, passes, violations)) {
|
|
457
|
+
nodeLog(Attribute.reversed(Attribute.yellow("---------------Accessibility Summary----------------")));
|
|
458
|
+
displayPasses(passes, config);
|
|
459
|
+
displayViolations(violations, config, false);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
1
464
|
// src/support/html-validate.ts
|
|
2
465
|
import "cypress-html-validate/commands";
|
|
3
466
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forsakringskassan/cypress-config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Sharable Cypress configuration building blocks for Försäkringskassan",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cypress"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"author": "Försäkringskassan",
|
|
18
18
|
"sideEffects": [
|
|
19
19
|
"./dist/support.mjs",
|
|
20
|
+
"./src/support/axe.ts",
|
|
20
21
|
"./src/support/html-validate.ts"
|
|
21
22
|
],
|
|
22
23
|
"type": "module",
|
|
@@ -44,9 +45,15 @@
|
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
47
|
"@fkui/vue": ">= 6.0.0",
|
|
48
|
+
"@forsakringskassan/docs-generator": "^3.0.0",
|
|
47
49
|
"cypress": "^15.17.0",
|
|
48
50
|
"html-validate": ">= 8.0.0"
|
|
49
51
|
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@forsakringskassan/docs-generator": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
50
57
|
"engines": {
|
|
51
58
|
"node": "^22.16 || >= 24",
|
|
52
59
|
"npm": ">= 7"
|