@stackoverflow/stacks 2.0.9 → 2.1.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/LICENSE.MD +1 -1
- package/README.md +7 -9
- package/dist/css/stacks.css +193 -206
- package/dist/css/stacks.min.css +1 -1
- package/dist/js/stacks.js +1 -1
- package/lib/components/activity-indicator/activity-indicator.a11y.test.ts +2 -3
- package/lib/components/activity-indicator/activity-indicator.visual.test.ts +2 -3
- package/lib/components/anchor/anchor.a11y.test.ts +2 -4
- package/lib/components/anchor/anchor.visual.test.ts +2 -4
- package/lib/components/avatar/avatar.a11y.test.ts +2 -3
- package/lib/components/avatar/avatar.visual.test.ts +2 -3
- package/lib/components/award-bling/award-bling.a11y.test.ts +2 -4
- package/lib/components/award-bling/award-bling.visual.test.ts +2 -4
- package/lib/components/badge/badge.a11y.test.ts +7 -16
- package/lib/components/badge/badge.visual.test.ts +8 -21
- package/lib/components/banner/banner.a11y.test.ts +2 -3
- package/lib/components/banner/banner.visual.test.ts +2 -3
- package/lib/components/block-link/block-link.a11y.test.ts +4 -9
- package/lib/components/block-link/block-link.less +7 -10
- package/lib/components/block-link/block-link.visual.test.ts +4 -9
- package/lib/components/breadcrumbs/breadcrumbs.a11y.test.ts +2 -3
- package/lib/components/breadcrumbs/breadcrumbs.visual.test.ts +2 -3
- package/lib/components/button/button.a11y.test.ts +2 -3
- package/lib/components/button/button.less +70 -35
- package/lib/components/button/button.visual.test.ts +2 -3
- package/lib/components/card/card.a11y.test.ts +2 -3
- package/lib/components/card/card.visual.test.ts +3 -6
- package/lib/components/check-control/check-control.a11y.test.ts +2 -4
- package/lib/components/check-control/check-control.visual.test.ts +2 -4
- package/lib/components/check-group/check-group.a11y.test.ts +2 -4
- package/lib/components/check-group/check-group.visual.test.ts +2 -4
- package/lib/components/checkbox_radio/checkbox_radio.a11y.test.ts +2 -4
- package/lib/components/checkbox_radio/checkbox_radio.less +1 -13
- package/lib/components/checkbox_radio/checkbox_radio.visual.test.ts +2 -4
- package/lib/components/code-block/code-block.a11y.test.ts +2 -4
- package/lib/components/code-block/code-block.visual.test.ts +2 -4
- package/lib/components/description/description.a11y.test.ts +2 -4
- package/lib/components/description/description.visual.test.ts +2 -4
- package/lib/components/empty-state/empty-state.a11y.test.ts +2 -3
- package/lib/components/empty-state/empty-state.visual.test.ts +2 -3
- package/lib/components/expandable/expandable.a11y.test.ts +2 -3
- package/lib/components/expandable/expandable.visual.test.ts +2 -3
- package/lib/components/input-fill/input-fill.a11y.test.ts +2 -3
- package/lib/components/input-fill/input-fill.visual.test.ts +2 -3
- package/lib/components/input-message/input-message.a11y.test.ts +2 -3
- package/lib/components/input-message/input-message.visual.test.ts +2 -3
- package/lib/components/input_textarea/input_textarea.a11y.test.ts +4 -7
- package/lib/components/input_textarea/input_textarea.less +2 -20
- package/lib/components/input_textarea/input_textarea.visual.test.ts +4 -7
- package/lib/components/label/label.a11y.test.ts +2 -3
- package/lib/components/label/label.visual.test.ts +2 -3
- package/lib/components/link/link.a11y.test.ts +2 -3
- package/lib/components/link/link.visual.test.ts +2 -3
- package/lib/components/link-preview/link-preview.a11y.test.ts +2 -3
- package/lib/components/link-preview/link-preview.visual.test.ts +3 -3
- package/lib/components/menu/menu.a11y.test.ts +2 -3
- package/lib/components/menu/menu.visual.test.ts +2 -3
- package/lib/components/modal/modal.a11y.test.ts +2 -3
- package/lib/components/modal/modal.visual.test.ts +2 -3
- package/lib/components/navigation/navigation.a11y.test.ts +2 -3
- package/lib/components/navigation/navigation.less +3 -1
- package/lib/components/navigation/navigation.visual.test.ts +3 -6
- package/lib/components/notice/notice.a11y.test.ts +2 -3
- package/lib/components/notice/notice.visual.test.ts +2 -3
- package/lib/components/page-title/page-title.a11y.test.ts +2 -3
- package/lib/components/page-title/page-title.visual.test.ts +2 -3
- package/lib/components/pagination/pagination.a11y.test.ts +2 -3
- package/lib/components/pagination/pagination.less +9 -0
- package/lib/components/pagination/pagination.visual.test.ts +2 -3
- package/lib/components/progress-bar/progress-bar.a11y.test.ts +7 -18
- package/lib/components/progress-bar/progress-bar.visual.test.ts +7 -18
- package/lib/components/select/select.less +1 -15
- package/lib/components/spinner/spinner.a11y.test.ts +2 -3
- package/lib/components/spinner/spinner.visual.test.ts +4 -7
- package/lib/components/table/table.a11y.test.ts +3 -4
- package/lib/components/table/table.visual.test.ts +2 -3
- package/lib/components/tag/tag.a11y.test.ts +2 -3
- package/lib/components/tag/tag.less +27 -21
- package/lib/components/tag/tag.visual.test.ts +3 -6
- package/lib/components/toast/toast.a11y.test.ts +2 -3
- package/lib/components/toast/toast.visual.test.ts +2 -3
- package/lib/components/toggle-switch/toggle-switch.a11y.test.ts +3 -6
- package/lib/components/toggle-switch/toggle-switch.less +5 -16
- package/lib/components/toggle-switch/toggle-switch.visual.test.ts +3 -7
- package/lib/components/topbar/topbar.less +61 -39
- package/lib/components/topbar/topbar.visual.test.ts +188 -0
- package/lib/components/uploader/uploader.less +1 -1
- package/lib/exports/__snapshots__/color-mixins.less.test.ts.snap +2 -0
- package/lib/exports/__snapshots__/color.less.test.ts.snap +12 -0
- package/lib/exports/color-sets.less +17 -7
- package/lib/exports/mixins.less +33 -0
- package/lib/input-utils.less +0 -3
- package/lib/test/a11y-test-utils.ts +94 -0
- package/lib/test/assertions.ts +10 -3
- package/lib/test/test-utils.ts +152 -300
- package/lib/test/visual-test-utils.ts +58 -0
- package/package.json +7 -8
- package/lib/components/popover/tooltip.visual.test.ts +0 -31
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { html, fixture, expect } from "@open-wc/testing";
|
|
2
|
+
import { screen } from "@testing-library/dom";
|
|
3
|
+
import axe from "axe-core";
|
|
4
|
+
import registerAPCACheck from "apca-check";
|
|
5
|
+
import { generateTestVariations, type TestVariationArgs } from "./test-utils";
|
|
6
|
+
import type { AdditionalAssertion } from "./assertions";
|
|
7
|
+
|
|
8
|
+
type A11yTestArgs = TestVariationArgs & {
|
|
9
|
+
/**
|
|
10
|
+
* Additional assertions to run against the test element
|
|
11
|
+
*/
|
|
12
|
+
additionalAssertions?: AdditionalAssertion[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// register Stack APCA conformance threshold function
|
|
16
|
+
// see also https://stackoverflow.design/product/base/color-fundamentals/#accessibility-standards
|
|
17
|
+
const customConformanceThresholdFn = (fontSize: string): number | null => {
|
|
18
|
+
// if the font size is 32px or larger, we use a 45Lc threshold
|
|
19
|
+
// otherwise, we use a 60Lc threshold
|
|
20
|
+
return parseFloat(fontSize) >= 32 ? 45 : 60;
|
|
21
|
+
};
|
|
22
|
+
registerAPCACheck("custom", customConformanceThresholdFn);
|
|
23
|
+
|
|
24
|
+
const scheduleA11yTest = ({
|
|
25
|
+
element,
|
|
26
|
+
testid,
|
|
27
|
+
theme,
|
|
28
|
+
additionalAssertions = [],
|
|
29
|
+
}: {
|
|
30
|
+
element: ReturnType<typeof html>;
|
|
31
|
+
testid: string;
|
|
32
|
+
theme: string[];
|
|
33
|
+
additionalAssertions?: AdditionalAssertion[];
|
|
34
|
+
}) => {
|
|
35
|
+
it(`a11y: ${testid} should be accessible`, async () => {
|
|
36
|
+
await fixture(element);
|
|
37
|
+
const el = screen.getByTestId(testid);
|
|
38
|
+
|
|
39
|
+
document.body.className = "";
|
|
40
|
+
|
|
41
|
+
if (theme?.length) {
|
|
42
|
+
const prefixedThemes = theme.map((t) => `theme-${t}`);
|
|
43
|
+
document.body.classList.add(...prefixedThemes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const highcontrast = theme?.includes("highcontrast");
|
|
47
|
+
|
|
48
|
+
axe.configure({
|
|
49
|
+
rules: [
|
|
50
|
+
// for non-high contrast, we disable WCAG 2.1 AA (4.5:1)
|
|
51
|
+
// and use a Stacks-specific APCA custom level instead
|
|
52
|
+
{ id: "color-contrast", enabled: false },
|
|
53
|
+
{
|
|
54
|
+
id: "color-contrast-apca-custom",
|
|
55
|
+
enabled: !highcontrast,
|
|
56
|
+
},
|
|
57
|
+
// for high contrast, we check against WCAG 2.1 AAA (7:1)
|
|
58
|
+
{ id: "color-contrast-enhanced", enabled: highcontrast },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await expect(el).to.be.accessible();
|
|
63
|
+
el.remove();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
additionalAssertions.forEach((assertion) => {
|
|
67
|
+
it(`a11y: ${testid} ${assertion.description}`, async () => {
|
|
68
|
+
await fixture(element);
|
|
69
|
+
const el = screen.getByTestId(testid);
|
|
70
|
+
await assertion.assertion(el);
|
|
71
|
+
el.remove();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const runA11yTests = (args: A11yTestArgs) => {
|
|
77
|
+
const testVariations = generateTestVariations(args);
|
|
78
|
+
testVariations.forEach((variation) => {
|
|
79
|
+
if (variation.skipped) {
|
|
80
|
+
it.skip(`a11y: ${variation.testid} (skipped)`, () => {
|
|
81
|
+
return;
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
scheduleA11yTest({
|
|
87
|
+
...variation,
|
|
88
|
+
additionalAssertions: args.additionalAssertions,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type { AdditionalAssertion };
|
|
94
|
+
export { runA11yTests };
|
package/lib/test/assertions.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { expect } from "@open-wc/testing";
|
|
2
2
|
import Color from "colorjs.io";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
type AdditionalAssertion = {
|
|
5
|
+
description: string;
|
|
6
|
+
assertion: (node: HTMLElement) => Promise<void> | void;
|
|
7
|
+
};
|
|
4
8
|
|
|
5
9
|
// TODO: evaluate if we can do this check against all the components
|
|
6
10
|
// automatically instead of having to add the assertion manually
|
|
7
|
-
|
|
11
|
+
const WCAGNonTextContrast: AdditionalAssertion = {
|
|
8
12
|
description:
|
|
9
13
|
"should pass WCAG22 1.4.11 non-text-contrast success criterion (https://www.w3.org/TR/WCAG22/#non-text-contrast)",
|
|
10
14
|
assertion: (node) => {
|
|
@@ -18,7 +22,7 @@ export const WCAGNonTextContrast: AdditionalAssertion = {
|
|
|
18
22
|
selectedNodeStyles.getPropertyValue("background-color")
|
|
19
23
|
);
|
|
20
24
|
|
|
21
|
-
// we are
|
|
25
|
+
// we are specifing WCAG21 because of colorjs.io API
|
|
22
26
|
// WCAG21 and WCAG22 algoirthms are the same
|
|
23
27
|
const WCAGcontrast = bgSelectedNodeColor.contrast(
|
|
24
28
|
bgBodyColor,
|
|
@@ -27,3 +31,6 @@ export const WCAGNonTextContrast: AdditionalAssertion = {
|
|
|
27
31
|
expect(WCAGcontrast).to.be.at.least(3);
|
|
28
32
|
},
|
|
29
33
|
};
|
|
34
|
+
|
|
35
|
+
export type { AdditionalAssertion };
|
|
36
|
+
export { WCAGNonTextContrast };
|
package/lib/test/test-utils.ts
CHANGED
|
@@ -1,108 +1,11 @@
|
|
|
1
|
-
import { html,
|
|
2
|
-
import { screen } from "@testing-library/dom";
|
|
3
|
-
import { visualDiff } from "@web/test-runner-visual-regression";
|
|
4
|
-
import type { TemplateResult } from "lit-html";
|
|
5
|
-
import axe from "axe-core";
|
|
6
|
-
import registerAPCACheck from "apca-check";
|
|
1
|
+
import { html, unsafeStatic } from "@open-wc/testing";
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
return parseFloat(fontSize) >= 32 ? 45 : 60;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
registerAPCACheck("custom", customConformanceThresholdFn);
|
|
13
|
-
|
|
14
|
-
const colorThemes = ["dark", "light"];
|
|
15
|
-
const baseThemes = ["", "highcontrast"];
|
|
16
|
-
export const defaultOptions = {
|
|
17
|
-
testColorThemes: true,
|
|
18
|
-
testHighContrast: true,
|
|
19
|
-
includeNullVariant: true,
|
|
20
|
-
includeNullModifier: true,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type Themes = ["light" | "dark" | "highcontrast" | ""];
|
|
24
|
-
type TestTypes = "visual" | "a11y";
|
|
25
|
-
export type AdditionalAssertion = {
|
|
26
|
-
description: string;
|
|
27
|
-
assertion: (node: HTMLElement) => Promise<void> | void;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type TestOptions = {
|
|
31
|
-
/**
|
|
32
|
-
* Enable tests for all color themes
|
|
33
|
-
* default: true
|
|
34
|
-
*/
|
|
35
|
-
testColorThemes: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Enable tests for high contrast
|
|
38
|
-
* default: true
|
|
39
|
-
*/
|
|
40
|
-
testHighContrast: boolean;
|
|
41
|
-
/**
|
|
42
|
-
* Provide a custom testid suffix
|
|
43
|
-
* default: undefined
|
|
44
|
-
*/
|
|
45
|
-
testidSuffix?: string;
|
|
46
|
-
/**
|
|
47
|
-
* Include tests for the component without any variants applied
|
|
48
|
-
* default: true
|
|
49
|
-
*/
|
|
50
|
-
includeNullVariant: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Include tests for the component without any modifiers applied
|
|
53
|
-
* default: true
|
|
54
|
-
*/
|
|
55
|
-
includeNullModifier: boolean;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
interface ComponentTestVariationArgs {
|
|
3
|
+
type TestVariationArgs = {
|
|
59
4
|
/**
|
|
60
5
|
* Base class of the component
|
|
61
6
|
* (e.g. "s-component")
|
|
62
7
|
*/
|
|
63
8
|
baseClass: string;
|
|
64
|
-
/**
|
|
65
|
-
* Variants of the component
|
|
66
|
-
* (e.g. ["primary", "secondary"])
|
|
67
|
-
*/
|
|
68
|
-
variants?: string[];
|
|
69
|
-
/**
|
|
70
|
-
* Modifiers of the component
|
|
71
|
-
* (e.g. { primary: ["filled", "outlined"], secondary: ["xs", "sm", "md"] })
|
|
72
|
-
*/
|
|
73
|
-
modifiers?: ComponentTestModifiers;
|
|
74
|
-
/**
|
|
75
|
-
* Options for the test
|
|
76
|
-
*/
|
|
77
|
-
options?: TestOptions;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type ComponentTestArgs = {
|
|
81
|
-
/**
|
|
82
|
-
* The element to test
|
|
83
|
-
* use the `html` template tag to render the element
|
|
84
|
-
*/
|
|
85
|
-
element: TemplateResult;
|
|
86
|
-
/**
|
|
87
|
-
* testid of the test
|
|
88
|
-
* (e.g. "s-component-primary-important")
|
|
89
|
-
*/
|
|
90
|
-
testid: string;
|
|
91
|
-
/**
|
|
92
|
-
* Theme to apply to the test element
|
|
93
|
-
*/
|
|
94
|
-
theme?: Themes;
|
|
95
|
-
/**
|
|
96
|
-
* Type of test to run
|
|
97
|
-
*/
|
|
98
|
-
type: TestTypes;
|
|
99
|
-
/**
|
|
100
|
-
* Additional assertions to run against the test element
|
|
101
|
-
*/
|
|
102
|
-
additionalAssertions: AdditionalAssertion[];
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
interface ComponentTestsArgs extends ComponentTestVariationArgs {
|
|
106
9
|
/**
|
|
107
10
|
* Additional html attributes applied to the test element
|
|
108
11
|
* (e.g. { role: "button", id: "id" } -> <element role="button" id="id"> )
|
|
@@ -137,16 +40,22 @@ interface ComponentTestsArgs extends ComponentTestVariationArgs {
|
|
|
137
40
|
testid: string;
|
|
138
41
|
}) => ReturnType<typeof html>;
|
|
139
42
|
/**
|
|
140
|
-
*
|
|
43
|
+
* Variants of the component
|
|
44
|
+
* (e.g. ["primary", "secondary"])
|
|
141
45
|
*/
|
|
142
|
-
|
|
46
|
+
variants?: string[];
|
|
143
47
|
/**
|
|
144
|
-
*
|
|
48
|
+
* Modifiers of the component
|
|
49
|
+
* (e.g. { primary: ["filled", "outlined"], secondary: ["xs", "sm", "md"] })
|
|
145
50
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
51
|
+
modifiers?: Modifiers;
|
|
52
|
+
/**
|
|
53
|
+
* Options for the test
|
|
54
|
+
*/
|
|
55
|
+
options?: Partial<TestOptions>;
|
|
56
|
+
};
|
|
148
57
|
|
|
149
|
-
type
|
|
58
|
+
type Modifiers = {
|
|
150
59
|
/**
|
|
151
60
|
* Primary grouping of modifiers to test
|
|
152
61
|
* The base class will be used as a prefix for these modifiers
|
|
@@ -168,12 +77,46 @@ type ComponentTestModifiers = {
|
|
|
168
77
|
standalone?: string[];
|
|
169
78
|
};
|
|
170
79
|
|
|
171
|
-
type
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
80
|
+
type TestOptions = {
|
|
81
|
+
/**
|
|
82
|
+
* Enable tests for all color themes
|
|
83
|
+
* default: true
|
|
84
|
+
*/
|
|
85
|
+
testColorThemes: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Enable tests for high contrast
|
|
88
|
+
* default: true
|
|
89
|
+
*/
|
|
90
|
+
testHighContrast: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Provide a custom testid suffix
|
|
93
|
+
* default: undefined
|
|
94
|
+
*/
|
|
95
|
+
testidSuffix?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Include tests for the component without any variants applied
|
|
98
|
+
* default: true
|
|
99
|
+
*/
|
|
100
|
+
includeNullVariant: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Include tests for the component without any modifiers applied
|
|
103
|
+
* default: true
|
|
104
|
+
*/
|
|
105
|
+
includeNullModifier: boolean;
|
|
175
106
|
};
|
|
176
107
|
|
|
108
|
+
export const DEFAULT_OPTIONS = {
|
|
109
|
+
testColorThemes: true,
|
|
110
|
+
testHighContrast: true,
|
|
111
|
+
includeNullVariant: true,
|
|
112
|
+
includeNullModifier: true,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// TODO: refactor using MODES as opposed to THEMES
|
|
116
|
+
const COLOR_THEMES = ["dark", "light"];
|
|
117
|
+
const BASE_THEMES = ["", "highcontrast"];
|
|
118
|
+
type Themes = ["light" | "dark" | "highcontrast" | ""];
|
|
119
|
+
|
|
177
120
|
const attrObjToString = (attrs: Record<string, string>): string => {
|
|
178
121
|
const attrString = Object.keys(attrs).map((key) => {
|
|
179
122
|
return `${key}="${attrs[key]}"` || "";
|
|
@@ -181,6 +124,22 @@ const attrObjToString = (attrs: Record<string, string>): string => {
|
|
|
181
124
|
return attrString.join(" ") || "";
|
|
182
125
|
};
|
|
183
126
|
|
|
127
|
+
const matchTestidByPattern = ({
|
|
128
|
+
testid,
|
|
129
|
+
pattern,
|
|
130
|
+
}: {
|
|
131
|
+
testid: string;
|
|
132
|
+
pattern: string | RegExp;
|
|
133
|
+
}): boolean => {
|
|
134
|
+
if (pattern instanceof RegExp) {
|
|
135
|
+
return pattern.test(testid);
|
|
136
|
+
} else {
|
|
137
|
+
return pattern === testid;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const buildTestid = (arr: string[]) => arr.filter(Boolean).join("-");
|
|
142
|
+
|
|
184
143
|
const buildClasses = ({
|
|
185
144
|
baseClass,
|
|
186
145
|
prefixed = [],
|
|
@@ -214,34 +173,44 @@ const buildTestElement = ({
|
|
|
214
173
|
};
|
|
215
174
|
|
|
216
175
|
return html`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
176
|
+
<${unsafe.tag}
|
|
177
|
+
${unsafe.attributes}
|
|
178
|
+
data-testid="${testid}"
|
|
179
|
+
>${unsafe.children}</${unsafe.tag}>
|
|
180
|
+
`;
|
|
222
181
|
};
|
|
223
182
|
|
|
224
|
-
|
|
183
|
+
type PrimitiveVariation = {
|
|
184
|
+
classes: string;
|
|
185
|
+
testid: string;
|
|
186
|
+
theme: Themes;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
type PrimitiveVariationArgs = Pick<
|
|
190
|
+
TestVariationArgs,
|
|
191
|
+
"baseClass" | "variants" | "modifiers" | "options"
|
|
192
|
+
>;
|
|
225
193
|
|
|
226
|
-
const
|
|
194
|
+
const generatePrimitiveVariations = ({
|
|
227
195
|
baseClass,
|
|
228
196
|
variants = [],
|
|
229
197
|
modifiers,
|
|
230
|
-
options =
|
|
231
|
-
}:
|
|
232
|
-
const
|
|
198
|
+
options = {},
|
|
199
|
+
}: PrimitiveVariationArgs): PrimitiveVariation[] => {
|
|
200
|
+
const primitiveVariations: PrimitiveVariation[] = [];
|
|
201
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
233
202
|
// Test default, high contrast themes
|
|
234
|
-
[...(
|
|
203
|
+
[...(opts.testHighContrast ? BASE_THEMES : [""])].forEach((baseTheme) => {
|
|
235
204
|
// Test light, dark theme
|
|
236
|
-
[...(
|
|
205
|
+
[...(opts.testColorThemes ? COLOR_THEMES : [""])].forEach(
|
|
237
206
|
(colorTheme) => {
|
|
238
207
|
const theme = [baseTheme, colorTheme].filter(Boolean) as Themes;
|
|
239
208
|
const testidBase = buildTestid([baseClass, ...theme]);
|
|
240
|
-
const allVariants =
|
|
209
|
+
const allVariants = opts.includeNullVariant
|
|
241
210
|
? ["", ...variants]
|
|
242
211
|
: variants;
|
|
243
212
|
const primaryModifiers = modifiers?.primary
|
|
244
|
-
?
|
|
213
|
+
? opts.includeNullModifier
|
|
245
214
|
? ["", ...(<[]>modifiers.primary)]
|
|
246
215
|
: modifiers.primary
|
|
247
216
|
: [""];
|
|
@@ -256,7 +225,7 @@ const getComponentTestVariations = ({
|
|
|
256
225
|
secondaryModifiers.forEach((secondaryModifier) => {
|
|
257
226
|
globalModifiers.forEach((globalModifier) => {
|
|
258
227
|
allVariants.forEach((variant) => {
|
|
259
|
-
|
|
228
|
+
primitiveVariations.push({
|
|
260
229
|
classes: buildClasses({
|
|
261
230
|
baseClass,
|
|
262
231
|
prefixed: [
|
|
@@ -286,7 +255,7 @@ const getComponentTestVariations = ({
|
|
|
286
255
|
|
|
287
256
|
// create standalone modifiers test props
|
|
288
257
|
modifiers?.standalone?.forEach((standaloneModifier) => {
|
|
289
|
-
|
|
258
|
+
primitiveVariations.push({
|
|
290
259
|
testid: buildTestid([testidBase, standaloneModifier]),
|
|
291
260
|
classes: buildClasses({
|
|
292
261
|
baseClass,
|
|
@@ -300,209 +269,92 @@ const getComponentTestVariations = ({
|
|
|
300
269
|
});
|
|
301
270
|
|
|
302
271
|
// Sorting for readability
|
|
303
|
-
return
|
|
272
|
+
return primitiveVariations.sort((a, b) => a.testid.localeCompare(b.testid));
|
|
304
273
|
};
|
|
305
274
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
testid,
|
|
312
|
-
theme,
|
|
313
|
-
type,
|
|
314
|
-
additionalAssertions,
|
|
315
|
-
}: ComponentTestArgs) => {
|
|
316
|
-
const getDescription = (type: TestTypes) => {
|
|
317
|
-
switch (type) {
|
|
318
|
-
case "a11y":
|
|
319
|
-
return "should be accessible";
|
|
320
|
-
case "visual":
|
|
321
|
-
return "should not introduce visual regressions";
|
|
322
|
-
default:
|
|
323
|
-
return "";
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
it(`${type}: ${testid} ${getDescription(type)}`, async () => {
|
|
328
|
-
await fixture(element);
|
|
329
|
-
const el = screen.getByTestId(testid);
|
|
330
|
-
|
|
331
|
-
document.body.className = "";
|
|
332
|
-
|
|
333
|
-
if (theme?.length) {
|
|
334
|
-
const prefixedThemes = theme.map((t) => `theme-${t}`);
|
|
335
|
-
document.body.classList.add(...prefixedThemes);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (type === "a11y") {
|
|
339
|
-
const highcontrast = theme?.includes("highcontrast");
|
|
340
|
-
axe.configure({
|
|
341
|
-
rules: [
|
|
342
|
-
// for non-high contrast, we disable WCAG 2.1 AA (4.5:1)
|
|
343
|
-
// and use a Stacks-specific APCA custom level instead
|
|
344
|
-
{ id: "color-contrast", enabled: false },
|
|
345
|
-
{
|
|
346
|
-
id: "color-contrast-apca-custom",
|
|
347
|
-
enabled: !highcontrast,
|
|
348
|
-
},
|
|
349
|
-
// for high contrast, we check against WCAG 2.1 AAA (7:1)
|
|
350
|
-
{ id: "color-contrast-enhanced", enabled: highcontrast },
|
|
351
|
-
],
|
|
352
|
-
});
|
|
353
|
-
await expect(el).to.be.accessible();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (type === "visual") {
|
|
357
|
-
await visualDiff(el, testid);
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
additionalAssertions.forEach((assertion) => {
|
|
362
|
-
it(`${type}: ${testid} ${assertion.description}`, async () => {
|
|
363
|
-
await fixture(element);
|
|
364
|
-
const el = screen.getByTestId(testid);
|
|
365
|
-
await assertion.assertion(el);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
275
|
+
type TestVariation = {
|
|
276
|
+
testid: string;
|
|
277
|
+
element: ReturnType<typeof html>;
|
|
278
|
+
skipped: boolean;
|
|
279
|
+
theme: Themes;
|
|
368
280
|
};
|
|
369
281
|
|
|
370
|
-
|
|
371
|
-
* Constructs and runs tests for a component with a each provided combination
|
|
372
|
-
*/
|
|
373
|
-
const runComponentTests = ({
|
|
282
|
+
const generateTestVariations = ({
|
|
374
283
|
baseClass,
|
|
375
284
|
variants = [],
|
|
376
285
|
modifiers,
|
|
377
|
-
options =
|
|
286
|
+
options = DEFAULT_OPTIONS,
|
|
378
287
|
attributes,
|
|
379
288
|
children,
|
|
380
289
|
excludedTestids = [],
|
|
381
290
|
skippedTestids = [],
|
|
382
291
|
tag,
|
|
383
292
|
template,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}: ComponentTestsArgs) => {
|
|
387
|
-
getComponentTestVariations({
|
|
293
|
+
}: TestVariationArgs): TestVariation[] => {
|
|
294
|
+
return generatePrimitiveVariations({
|
|
388
295
|
baseClass,
|
|
389
296
|
variants,
|
|
390
297
|
modifiers,
|
|
391
298
|
options,
|
|
392
|
-
}).
|
|
299
|
+
}).flatMap(({ testid, classes, theme }) => {
|
|
393
300
|
const allChildren: {
|
|
394
301
|
[key: string]: string;
|
|
395
302
|
} = children ? { ...children } : { default: "" };
|
|
396
303
|
const { testidSuffix } = options;
|
|
397
304
|
|
|
398
|
-
Object.keys(allChildren)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const element = template
|
|
426
|
-
? html`${template({
|
|
427
|
-
testid: testidModified,
|
|
428
|
-
component: buildTestElement({
|
|
305
|
+
return Object.keys(allChildren)
|
|
306
|
+
.map((key) => {
|
|
307
|
+
let testidModified = (
|
|
308
|
+
key !== "default" ? `${testid}-${key}` : testid
|
|
309
|
+
).replace(" ", "-");
|
|
310
|
+
testidModified = testidSuffix
|
|
311
|
+
? `${testidModified}-${testidSuffix}`
|
|
312
|
+
: testidModified;
|
|
313
|
+
|
|
314
|
+
const children = allChildren[key];
|
|
315
|
+
|
|
316
|
+
const element = template
|
|
317
|
+
? html`${template({
|
|
318
|
+
testid: testidModified,
|
|
319
|
+
component: buildTestElement({
|
|
320
|
+
attributes: {
|
|
321
|
+
...attributes,
|
|
322
|
+
class: `${classes} ${attributes?.class || ""}`,
|
|
323
|
+
},
|
|
324
|
+
children,
|
|
325
|
+
testid: `${testidModified}-nested`,
|
|
326
|
+
tag,
|
|
327
|
+
}),
|
|
328
|
+
})}`
|
|
329
|
+
: buildTestElement({
|
|
429
330
|
attributes: {
|
|
430
331
|
...attributes,
|
|
431
332
|
class: `${classes} ${attributes?.class || ""}`,
|
|
432
333
|
},
|
|
433
334
|
children,
|
|
434
|
-
testid:
|
|
335
|
+
testid: testidModified,
|
|
435
336
|
tag,
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
testid
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const skipped = skippedTestids.some((pattern) =>
|
|
340
|
+
matchTestidByPattern({ testid: testidModified, pattern })
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
element,
|
|
345
|
+
testid: testidModified,
|
|
346
|
+
theme,
|
|
347
|
+
skipped,
|
|
348
|
+
};
|
|
349
|
+
})
|
|
350
|
+
.filter(
|
|
351
|
+
({ testid }) =>
|
|
352
|
+
!excludedTestids.some((pattern) =>
|
|
353
|
+
matchTestidByPattern({ testid, pattern })
|
|
354
|
+
)
|
|
355
|
+
);
|
|
456
356
|
});
|
|
457
357
|
};
|
|
458
358
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
pattern,
|
|
462
|
-
}: {
|
|
463
|
-
testid: string;
|
|
464
|
-
pattern: string | RegExp;
|
|
465
|
-
}): boolean => {
|
|
466
|
-
if (pattern instanceof RegExp) {
|
|
467
|
-
return pattern.test(testid);
|
|
468
|
-
} else {
|
|
469
|
-
return pattern === testid;
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const excludeOrSkipTest = ({
|
|
474
|
-
patterns,
|
|
475
|
-
skip = false,
|
|
476
|
-
testid,
|
|
477
|
-
type,
|
|
478
|
-
}: {
|
|
479
|
-
patterns: (string | RegExp)[];
|
|
480
|
-
skip?: boolean;
|
|
481
|
-
testid: string;
|
|
482
|
-
type: TestTypes;
|
|
483
|
-
}): boolean => {
|
|
484
|
-
const matchesTest = patterns.some((pattern) => {
|
|
485
|
-
return matchTestidByPattern({ testid, pattern });
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
if (matchesTest && skip) {
|
|
489
|
-
it.skip(`${type}: ${testid} (skipped)`, () => {
|
|
490
|
-
return;
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return matchesTest;
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
export { runComponentTest, runComponentTests };
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Convert a const array of strings into a union type of the array's values.
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```
|
|
504
|
-
* const arrayOfStrings = ['Stacky', 'Ben', 'Dan', 'Giamir'] as const;
|
|
505
|
-
* type StringLiterals = AsLiterals<typeof arrayOfStrings>; // 'Stacky' | 'Ben' | 'Dan' | 'Giamir'
|
|
506
|
-
* ```
|
|
507
|
-
*/
|
|
508
|
-
export type AsLiterals<T extends Readonly<string[]>> = T[number];
|
|
359
|
+
export type { TestVariationArgs, TestVariation };
|
|
360
|
+
export { generateTestVariations };
|