@stackoverflow/stacks 1.9.0 → 1.9.2

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.
Files changed (132) hide show
  1. package/README.md +161 -153
  2. package/dist/components/table/table.d.ts +26 -4
  3. package/dist/css/stacks.css +21 -6
  4. package/dist/css/stacks.min.css +1 -1
  5. package/dist/js/stacks.js +93 -64
  6. package/dist/js/stacks.min.js +1 -1
  7. package/lib/atomic/border.less +397 -397
  8. package/lib/atomic/color.less +210 -210
  9. package/lib/atomic/flex.less +426 -426
  10. package/lib/atomic/gap.less +44 -44
  11. package/lib/atomic/grid.less +139 -139
  12. package/lib/atomic/misc.less +343 -343
  13. package/lib/atomic/spacing.less +342 -342
  14. package/lib/atomic/typography.less +267 -267
  15. package/lib/atomic/width-height.less +194 -194
  16. package/lib/base/body.less +44 -44
  17. package/lib/base/configuration-static.less +61 -61
  18. package/lib/base/fieldset.less +5 -5
  19. package/lib/base/icon.less +11 -11
  20. package/lib/base/internal.less +220 -220
  21. package/lib/base/reset-meyer.less +64 -64
  22. package/lib/base/reset-normalize.less +449 -449
  23. package/lib/base/reset.less +20 -20
  24. package/lib/components/activity-indicator/activity-indicator.a11y.test.ts +21 -21
  25. package/lib/components/activity-indicator/activity-indicator.less +40 -40
  26. package/lib/components/activity-indicator/activity-indicator.visual.test.ts +23 -23
  27. package/lib/components/anchor/anchor.less +61 -61
  28. package/lib/components/avatar/avatar.a11y.test.ts +36 -36
  29. package/lib/components/avatar/avatar.less +108 -108
  30. package/lib/components/avatar/avatar.visual.test.ts +54 -54
  31. package/lib/components/award-bling/award-bling.a11y.test.ts +17 -0
  32. package/lib/components/award-bling/award-bling.less +31 -31
  33. package/lib/components/award-bling/award-bling.visual.test.ts +26 -0
  34. package/lib/components/badge/badge.less +251 -251
  35. package/lib/components/banner/banner.a11y.test.ts +37 -0
  36. package/lib/components/banner/banner.less +51 -51
  37. package/lib/components/banner/banner.test.ts +73 -77
  38. package/lib/components/banner/banner.ts +149 -149
  39. package/lib/components/banner/banner.visual.test.ts +37 -36
  40. package/lib/components/block-link/block-link.a11y.test.ts +68 -0
  41. package/lib/components/block-link/block-link.less +80 -80
  42. package/lib/components/block-link/block-link.visual.test.ts +61 -0
  43. package/lib/components/breadcrumbs/breadcrumbs.a11y.test.ts +37 -0
  44. package/lib/components/breadcrumbs/breadcrumbs.less +41 -41
  45. package/lib/components/breadcrumbs/breadcrumbs.visual.test.ts +37 -0
  46. package/lib/components/button/button.a11y.test.ts +32 -32
  47. package/lib/components/button/button.less +502 -502
  48. package/lib/components/button/button.visual.test.ts +52 -52
  49. package/lib/components/button-group/button-group.less +83 -83
  50. package/lib/components/card/card.a11y.test.ts +13 -0
  51. package/lib/components/card/card.less +29 -29
  52. package/lib/components/card/card.visual.test.ts +54 -0
  53. package/lib/components/check-control/check-control.less +17 -17
  54. package/lib/components/check-group/check-group.less +19 -19
  55. package/lib/components/checkbox_radio/checkbox_radio.less +158 -158
  56. package/lib/components/code-block/code-block.less +116 -116
  57. package/lib/components/description/description.less +9 -9
  58. package/lib/components/empty-state/empty-state.less +16 -16
  59. package/lib/components/expandable/expandable.less +118 -118
  60. package/lib/components/expandable/expandable.test.ts +51 -53
  61. package/lib/components/expandable/expandable.ts +238 -238
  62. package/lib/components/input-fill/input-fill.less +35 -35
  63. package/lib/components/input-icon/input-icon.less +45 -45
  64. package/lib/components/input-message/input-message.less +48 -48
  65. package/lib/components/input_textarea/input_textarea.less +166 -166
  66. package/lib/components/label/label.less +111 -111
  67. package/lib/components/link/link.less +119 -119
  68. package/lib/components/link-preview/link-preview.less +139 -139
  69. package/lib/components/menu/menu.less +41 -41
  70. package/lib/components/modal/modal.less +113 -113
  71. package/lib/components/modal/modal.ts +379 -379
  72. package/lib/components/navigation/navigation.less +134 -134
  73. package/lib/components/navigation/navigation.ts +128 -128
  74. package/lib/components/notice/notice.less +203 -203
  75. package/lib/components/page-title/page-title.less +51 -51
  76. package/lib/components/pagination/pagination.less +52 -52
  77. package/lib/components/popover/popover.less +148 -148
  78. package/lib/components/popover/popover.ts +651 -651
  79. package/lib/components/popover/tooltip.test.ts +62 -66
  80. package/lib/components/popover/tooltip.ts +343 -343
  81. package/lib/components/popover/tooltip.visual.test.ts +31 -31
  82. package/lib/components/post-summary/post-summary.less +415 -415
  83. package/lib/components/progress-bar/progress-bar.less +291 -291
  84. package/lib/components/prose/prose.less +452 -452
  85. package/lib/components/select/select.less +148 -148
  86. package/lib/components/sidebar-widget/sidebar-widget.less +257 -258
  87. package/lib/components/spinner/spinner.less +103 -103
  88. package/lib/components/table/table.less +307 -292
  89. package/lib/components/table/table.test.ts +366 -0
  90. package/lib/components/table/table.ts +296 -263
  91. package/lib/components/table-container/table-container.less +4 -4
  92. package/lib/components/tag/tag.less +213 -213
  93. package/lib/components/toast/toast.less +35 -35
  94. package/lib/components/toast/toast.test.ts +63 -67
  95. package/lib/components/toast/toast.ts +357 -357
  96. package/lib/components/toast/toast.visual.test.ts +27 -27
  97. package/lib/components/toggle-switch/toggle-switch.less +110 -110
  98. package/lib/components/topbar/topbar.less +436 -435
  99. package/lib/components/uploader/uploader.less +195 -195
  100. package/lib/components/uploader/uploader.ts +205 -205
  101. package/lib/components/user-card/user-card.less +129 -129
  102. package/lib/controllers.ts +33 -33
  103. package/lib/exports/constants-colors.less +1112 -1111
  104. package/lib/exports/constants-helpers.less +108 -108
  105. package/lib/exports/constants-type.less +153 -153
  106. package/lib/exports/exports.less +15 -15
  107. package/lib/exports/mixins.less +299 -299
  108. package/lib/index.ts +32 -32
  109. package/lib/input-utils.less +44 -44
  110. package/lib/stacks-dynamic.less +24 -24
  111. package/lib/stacks-static.less +93 -93
  112. package/lib/stacks.less +13 -13
  113. package/lib/stacks.ts +113 -113
  114. package/lib/test/open-wc-testing-patch.d.ts +26 -0
  115. package/lib/test/test-utils.ts +466 -444
  116. package/lib/tsconfig.build.json +4 -0
  117. package/lib/tsconfig.json +16 -13
  118. package/package.json +106 -105
  119. package/dist/components/activity-indicator/activity-indicator.a11y.test.d.ts +0 -1
  120. package/dist/components/activity-indicator/activity-indicator.visual.test.d.ts +0 -1
  121. package/dist/components/avatar/avatar.a11y.test.d.ts +0 -1
  122. package/dist/components/avatar/avatar.visual.test.d.ts +0 -1
  123. package/dist/components/banner/banner.test.d.ts +0 -1
  124. package/dist/components/banner/banner.visual.test.d.ts +0 -1
  125. package/dist/components/button/button.a11y.test.d.ts +0 -1
  126. package/dist/components/button/button.visual.test.d.ts +0 -1
  127. package/dist/components/expandable/expandable.test.d.ts +0 -1
  128. package/dist/components/popover/tooltip.test.d.ts +0 -1
  129. package/dist/components/popover/tooltip.visual.test.d.ts +0 -1
  130. package/dist/components/toast/toast.test.d.ts +0 -1
  131. package/dist/components/toast/toast.visual.test.d.ts +0 -1
  132. package/dist/test/test-utils.d.ts +0 -136
@@ -1,444 +1,466 @@
1
- import { html, fixture, expect, unsafeStatic } from "@open-wc/testing";
2
- import { screen } from "@testing-library/dom";
3
- import { visualDiff } from "@web/test-runner-visual-regression";
4
- import type { TemplateResult } from "lit-html";
5
-
6
- const colorThemes = ["dark", "light"];
7
- const baseThemes = ["", "highcontrast"];
8
- type Themes = ["light" | "dark" | "highcontrast" | ""];
9
- type TestTypes = "visual" | "a11y";
10
-
11
- type TestOptions = {
12
- /**
13
- * Enable tests for all color themes
14
- * default: true
15
- */
16
- testColorThemes: boolean;
17
- /**
18
- * Enable tests for high contrast
19
- * default: true
20
- */
21
- testHighContrast: boolean;
22
- /**
23
- * Include tests for the component without any variants applied
24
- * default: true
25
- */
26
- includeNullVariant: boolean;
27
- /**
28
- * Include tests for the component without any modifiers applied
29
- * default: true
30
- */
31
- includeNullModifier: boolean;
32
- };
33
-
34
- interface ComponentTestVariationArgs {
35
- /**
36
- * Base class of the component
37
- * (e.g. "s-component")
38
- */
39
- baseClass: string;
40
- /**
41
- * Variants of the component
42
- * (e.g. ["primary", "secondary"])
43
- */
44
- variants?: string[];
45
- /**
46
- * Modifiers of the component
47
- * (e.g. { primary: ["filled", "outlined"], secondary: ["xs", "sm", "md"] })
48
- */
49
- modifiers?: ComponentTestModifiers;
50
- /**
51
- * Options for the test
52
- */
53
- options?: TestOptions;
54
- }
55
-
56
- type ComponentTestArgs = {
57
- /**
58
- * The element to test
59
- * use the `html` template tag to render the element
60
- */
61
- element: TemplateResult;
62
- /**
63
- * testid of the test
64
- * (e.g. "s-component-primary-important")
65
- */
66
- testid: string;
67
- /**
68
- * Theme to apply to the test element
69
- */
70
- theme?: Themes;
71
- /**
72
- * Type of test to run
73
- */
74
- type: TestTypes;
75
- };
76
-
77
- interface ComponentTestsArgs extends ComponentTestVariationArgs {
78
- /**
79
- * Additional html attributes applied to the test element
80
- * (e.g. { role: "button", id: "id" } -> <element role="button" id="id"> )
81
- */
82
- attributes?: Record<string, string>;
83
- /**
84
- * Child elements to render inside the test element
85
- * (if key `default` is used, the testid will not include the child name)
86
- */
87
- children?: {
88
- [key: string]: string;
89
- };
90
- /**
91
- * testids of tests to exclude from testing
92
- */
93
- excludedTestids?: (string | RegExp)[];
94
- /**
95
- * testids of tests to skip
96
- */
97
- skippedTestids?: (string | RegExp)[];
98
- /**
99
- * HTML tag name of the test element
100
- */
101
- tag?: string;
102
- /**
103
- * Function that returns a template for the test element
104
- * used to wrap the component test element in a container
105
- */
106
- template?: (args: {
107
- component: unknown;
108
- tag?: string;
109
- testid: string;
110
- }) => ReturnType<typeof html>;
111
- /**
112
- * Type of test to run
113
- */
114
- type: TestTypes;
115
- }
116
-
117
- type ComponentTestModifiers = {
118
- /**
119
- * Primary grouping of modifiers to test
120
- * The base class will be used as a prefix for these modifiers
121
- */
122
- primary?: string[];
123
- /**
124
- * Secondary grouping of modifiers to test
125
- * The base class will be used as a prefix for these modifiers
126
- */
127
- secondary?: string[];
128
- /**
129
- * Grouping of modifers to test that will not be prefixed with the base class
130
- */
131
- global?: string[];
132
- /**
133
- * Modifiers to test individually
134
- * The base class will be used as a prefix for these modifiers
135
- */
136
- standalone?: string[];
137
- };
138
-
139
- type ComponentTestProps = {
140
- classes: string;
141
- testid: string;
142
- theme?: Themes;
143
- };
144
-
145
- const attrObjToString = (attrs: Record<string, string>): string => {
146
- const attrString = Object.keys(attrs).map((key) => {
147
- return `${key}="${attrs[key]}"` || "";
148
- });
149
- return attrString.join(" ") || "";
150
- };
151
-
152
- const buildClasses = ({
153
- baseClass,
154
- prefixed = [],
155
- unprefixed = [],
156
- }: {
157
- baseClass: string;
158
- prefixed?: string[];
159
- unprefixed?: string[];
160
- }) =>
161
- [
162
- baseClass,
163
- ...prefixed.filter((x) => x).map((suffix) => `${baseClass}__${suffix}`),
164
- ...unprefixed.filter((x) => x),
165
- ].join(" ");
166
-
167
- const buildTestElement = ({
168
- attributes = {},
169
- children = "",
170
- tag = "div",
171
- testid,
172
- }: {
173
- attributes?: Record<string, string>;
174
- children?: string;
175
- tag?: string;
176
- testid: string;
177
- }) => {
178
- const unsafe = {
179
- tag: unsafeStatic(tag),
180
- attributes: unsafeStatic(attrObjToString(attributes).toString()),
181
- children: unsafeStatic(children),
182
- };
183
-
184
- return html`
185
- <${unsafe.tag}
186
- ${unsafe.attributes}
187
- data-testid="${testid}"
188
- >
189
- ${unsafe.children}
190
- </${unsafe.tag}>
191
- `;
192
- };
193
-
194
- const buildTestid = (arr: string[]) => arr.filter(Boolean).join("-");
195
-
196
- const getComponentTestVariations = ({
197
- baseClass,
198
- variants = [],
199
- modifiers,
200
- options = {
201
- testColorThemes: true,
202
- testHighContrast: true,
203
- includeNullVariant: false,
204
- includeNullModifier: true,
205
- },
206
- }: ComponentTestVariationArgs): ComponentTestProps[] => {
207
- const testVariations: ComponentTestProps[] = [];
208
- // Test default, high contrast themes
209
- [...(options.testHighContrast ? baseThemes : [""])].forEach((baseTheme) => {
210
- // Test light, dark theme
211
- [...(options.testColorThemes ? colorThemes : [""])].forEach(
212
- (colorTheme) => {
213
- const theme = [baseTheme, colorTheme].filter(Boolean) as Themes;
214
- const testidBase = buildTestid([baseClass, ...theme]);
215
- const primaryModifiers = modifiers?.primary
216
- ? ["", ...(<[]>modifiers.primary)]
217
- : [""];
218
- const secondaryModifiers = modifiers?.secondary
219
- ? ["", ...(<[]>modifiers.secondary)]
220
- : [""];
221
- const globalModifiers = modifiers?.global
222
- ? ["", ...(<[]>modifiers.global)]
223
- : [""];
224
-
225
- primaryModifiers.forEach((primaryModifier) => {
226
- secondaryModifiers.forEach((secondaryModifier) => {
227
- globalModifiers.forEach((globalModifier) => {
228
- ["", ...variants].forEach((variant) => {
229
- testVariations.push({
230
- classes: buildClasses({
231
- baseClass,
232
- prefixed: [
233
- variant,
234
- primaryModifier,
235
- secondaryModifier,
236
- ],
237
- unprefixed: [globalModifier],
238
- }),
239
- testid: buildTestid([
240
- testidBase,
241
- variant,
242
- [
243
- primaryModifier,
244
- secondaryModifier,
245
- globalModifier,
246
- ]
247
- .filter(Boolean)
248
- .join("-"),
249
- ]),
250
- theme,
251
- });
252
- });
253
- });
254
- });
255
- });
256
-
257
- // create standalone modifiers test props
258
- modifiers?.standalone?.forEach((standaloneModifier) => {
259
- testVariations.push({
260
- testid: buildTestid([testidBase, standaloneModifier]),
261
- classes: buildClasses({
262
- baseClass,
263
- prefixed: [standaloneModifier],
264
- }),
265
- theme,
266
- });
267
- });
268
- }
269
- );
270
- });
271
-
272
- // Sorting for readability
273
- return testVariations.sort((a, b) => a.testid.localeCompare(b.testid));
274
- };
275
-
276
- /**
277
- * Constructs and runs an individual test for a component
278
- */
279
- const runComponentTest = ({
280
- element,
281
- testid,
282
- theme,
283
- type,
284
- }: ComponentTestArgs) => {
285
- const getDescription = (type: TestTypes) => {
286
- switch (type) {
287
- case "a11y":
288
- return "should be accessible";
289
- case "visual":
290
- return "should not introduce visual regressions";
291
- default:
292
- return "";
293
- }
294
- };
295
-
296
- it(`${type}: ${testid} ${getDescription(type)}`, async () => {
297
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
298
- await fixture(element);
299
- const el = screen.getByTestId(testid);
300
-
301
- document.body.className = "";
302
-
303
- if (theme?.length) {
304
- const prefixedThemes = theme.map((t) => `theme-${t}`);
305
- document.body.classList.add(...prefixedThemes);
306
- }
307
-
308
- if (type === "a11y") {
309
- // TODO add conditional option for high contrast mode to test against AAA
310
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
311
- await expect(el).to.be.accessible();
312
- }
313
-
314
- if (type === "visual") {
315
- await visualDiff(el, testid);
316
- }
317
- });
318
- };
319
-
320
- /**
321
- * Constructs and runs tests for a component with a each provided combination
322
- */
323
- const runComponentTests = ({
324
- baseClass,
325
- variants = [],
326
- modifiers,
327
- options = {
328
- testColorThemes: true,
329
- testHighContrast: true,
330
- includeNullVariant: false,
331
- includeNullModifier: true,
332
- },
333
- attributes,
334
- children,
335
- excludedTestids = [],
336
- skippedTestids = [],
337
- tag,
338
- template,
339
- type,
340
- }: ComponentTestsArgs) => {
341
- getComponentTestVariations({
342
- baseClass,
343
- variants,
344
- modifiers,
345
- options,
346
- }).forEach(({ testid, classes, theme }) => {
347
- const allChildren: {
348
- [key: string]: string;
349
- } = children ? { ...children } : { default: "" };
350
-
351
- Object.keys(allChildren).forEach((key) => {
352
- const testidModified =
353
- key !== "default" ? `${testid}-${key}` : testid;
354
- const children = allChildren[key];
355
-
356
- const shouldSkipTest = excludeOrSkipTest({
357
- patterns: skippedTestids,
358
- skip: true,
359
- testid: testidModified,
360
- type,
361
- });
362
-
363
- const shouldExcludeTest = excludeOrSkipTest({
364
- patterns: excludedTestids,
365
- testid: testidModified,
366
- type,
367
- });
368
-
369
- if (shouldSkipTest || shouldExcludeTest) {
370
- return;
371
- }
372
-
373
- const element = template
374
- ? html`${template({
375
- testid: testidModified,
376
- component: buildTestElement({
377
- attributes: {
378
- class: classes,
379
- ...attributes,
380
- },
381
- children,
382
- testid: `${testidModified}-nested`,
383
- tag,
384
- }),
385
- })}`
386
- : buildTestElement({
387
- attributes: {
388
- class: classes,
389
- ...attributes,
390
- },
391
- children,
392
- testid: testidModified,
393
- tag,
394
- });
395
-
396
- runComponentTest({
397
- element,
398
- testid: testidModified,
399
- theme,
400
- type,
401
- });
402
- });
403
- });
404
- };
405
-
406
- const matchTestidByPattern = ({
407
- testid,
408
- pattern,
409
- }: {
410
- testid: string;
411
- pattern: string | RegExp;
412
- }): boolean => {
413
- if (pattern instanceof RegExp) {
414
- return pattern.test(testid);
415
- } else {
416
- return pattern === testid;
417
- }
418
- };
419
-
420
- const excludeOrSkipTest = ({
421
- patterns,
422
- skip = false,
423
- testid,
424
- type,
425
- }: {
426
- patterns: (string | RegExp)[];
427
- skip?: boolean;
428
- testid: string;
429
- type: TestTypes;
430
- }): boolean => {
431
- const matchesTest = patterns.some((pattern) => {
432
- return matchTestidByPattern({ testid, pattern });
433
- });
434
-
435
- if (matchesTest && skip) {
436
- it.skip(`${type}: ${testid} (skipped)`, () => {
437
- return;
438
- });
439
- }
440
-
441
- return matchesTest;
442
- };
443
-
444
- export { runComponentTest, runComponentTests };
1
+ import { html, fixture, expect, unsafeStatic } from "@open-wc/testing";
2
+ import { screen } from "@testing-library/dom";
3
+ import { visualDiff } from "@web/test-runner-visual-regression";
4
+ import type { TemplateResult } from "lit-html";
5
+
6
+ const colorThemes = ["dark", "light"];
7
+ const baseThemes = ["", "highcontrast"];
8
+ export const defaultOptions = {
9
+ testColorThemes: true,
10
+ testHighContrast: true,
11
+ includeNullVariant: true,
12
+ includeNullModifier: true,
13
+ };
14
+
15
+ type Themes = ["light" | "dark" | "highcontrast" | ""];
16
+ type TestTypes = "visual" | "a11y";
17
+
18
+ type TestOptions = {
19
+ /**
20
+ * Enable tests for all color themes
21
+ * default: true
22
+ */
23
+ testColorThemes: boolean;
24
+ /**
25
+ * Enable tests for high contrast
26
+ * default: true
27
+ */
28
+ testHighContrast: boolean;
29
+ /**
30
+ * Provide a custom testid suffix
31
+ * default: undefined
32
+ */
33
+ testidSuffix?: string;
34
+ /**
35
+ * Include tests for the component without any variants applied
36
+ * default: true
37
+ */
38
+ includeNullVariant: boolean;
39
+ /**
40
+ * Include tests for the component without any modifiers applied
41
+ * default: true
42
+ */
43
+ includeNullModifier: boolean;
44
+ };
45
+
46
+ interface ComponentTestVariationArgs {
47
+ /**
48
+ * Base class of the component
49
+ * (e.g. "s-component")
50
+ */
51
+ baseClass: string;
52
+ /**
53
+ * Variants of the component
54
+ * (e.g. ["primary", "secondary"])
55
+ */
56
+ variants?: string[];
57
+ /**
58
+ * Modifiers of the component
59
+ * (e.g. { primary: ["filled", "outlined"], secondary: ["xs", "sm", "md"] })
60
+ */
61
+ modifiers?: ComponentTestModifiers;
62
+ /**
63
+ * Options for the test
64
+ */
65
+ options?: TestOptions;
66
+ }
67
+
68
+ type ComponentTestArgs = {
69
+ /**
70
+ * The element to test
71
+ * use the `html` template tag to render the element
72
+ */
73
+ element: TemplateResult;
74
+ /**
75
+ * testid of the test
76
+ * (e.g. "s-component-primary-important")
77
+ */
78
+ testid: string;
79
+ /**
80
+ * Theme to apply to the test element
81
+ */
82
+ theme?: Themes;
83
+ /**
84
+ * Type of test to run
85
+ */
86
+ type: TestTypes;
87
+ };
88
+
89
+ interface ComponentTestsArgs extends ComponentTestVariationArgs {
90
+ /**
91
+ * Additional html attributes applied to the test element
92
+ * (e.g. { role: "button", id: "id" } -> <element role="button" id="id"> )
93
+ */
94
+ attributes?: Record<string, string>;
95
+ /**
96
+ * Child elements to render inside the test element
97
+ * (if key `default` is used, the testid will not include the child name)
98
+ */
99
+ children?: {
100
+ [key: string]: string;
101
+ };
102
+ /**
103
+ * testids of tests to exclude from testing
104
+ */
105
+ excludedTestids?: (string | RegExp)[];
106
+ /**
107
+ * testids of tests to skip
108
+ */
109
+ skippedTestids?: (string | RegExp)[];
110
+ /**
111
+ * HTML tag name of the test element
112
+ */
113
+ tag?: string;
114
+ /**
115
+ * Function that returns a template for the test element
116
+ * used to wrap the component test element in a container
117
+ */
118
+ template?: (args: {
119
+ component: unknown;
120
+ tag?: string;
121
+ testid: string;
122
+ }) => ReturnType<typeof html>;
123
+ /**
124
+ * Type of test to run
125
+ */
126
+ type: TestTypes;
127
+ }
128
+
129
+ type ComponentTestModifiers = {
130
+ /**
131
+ * Primary grouping of modifiers to test
132
+ * The base class will be used as a prefix for these modifiers
133
+ */
134
+ primary?: string[];
135
+ /**
136
+ * Secondary grouping of modifiers to test
137
+ * The base class will be used as a prefix for these modifiers
138
+ */
139
+ secondary?: string[];
140
+ /**
141
+ * Grouping of modifers to test that will not be prefixed with the base class
142
+ */
143
+ global?: string[];
144
+ /**
145
+ * Modifiers to test individually
146
+ * The base class will be used as a prefix for these modifiers
147
+ */
148
+ standalone?: string[];
149
+ };
150
+
151
+ type ComponentTestProps = {
152
+ classes: string;
153
+ testid: string;
154
+ theme?: Themes;
155
+ };
156
+
157
+ const attrObjToString = (attrs: Record<string, string>): string => {
158
+ const attrString = Object.keys(attrs).map((key) => {
159
+ return `${key}="${attrs[key]}"` || "";
160
+ });
161
+ return attrString.join(" ") || "";
162
+ };
163
+
164
+ const buildClasses = ({
165
+ baseClass,
166
+ prefixed = [],
167
+ unprefixed = [],
168
+ }: {
169
+ baseClass: string;
170
+ prefixed?: string[];
171
+ unprefixed?: string[];
172
+ }) =>
173
+ [
174
+ baseClass,
175
+ ...prefixed.filter((x) => x).map((suffix) => `${baseClass}__${suffix}`),
176
+ ...unprefixed.filter((x) => x),
177
+ ].join(" ");
178
+
179
+ const buildTestElement = ({
180
+ attributes = {},
181
+ children = "",
182
+ tag = "div",
183
+ testid,
184
+ }: {
185
+ attributes?: Record<string, string>;
186
+ children?: string;
187
+ tag?: string;
188
+ testid: string;
189
+ }) => {
190
+ const unsafe = {
191
+ tag: unsafeStatic(tag),
192
+ attributes: unsafeStatic(attrObjToString(attributes).toString()),
193
+ children: unsafeStatic(children),
194
+ };
195
+
196
+ return html`
197
+ <${unsafe.tag}
198
+ ${unsafe.attributes}
199
+ data-testid="${testid}"
200
+ >
201
+ ${unsafe.children}
202
+ </${unsafe.tag}>
203
+ `;
204
+ };
205
+
206
+ const buildTestid = (arr: string[]) => arr.filter(Boolean).join("-");
207
+
208
+ const getComponentTestVariations = ({
209
+ baseClass,
210
+ variants = [],
211
+ modifiers,
212
+ options = defaultOptions,
213
+ }: ComponentTestVariationArgs): ComponentTestProps[] => {
214
+ const testVariations: ComponentTestProps[] = [];
215
+ // Test default, high contrast themes
216
+ [...(options.testHighContrast ? baseThemes : [""])].forEach((baseTheme) => {
217
+ // Test light, dark theme
218
+ [...(options.testColorThemes ? colorThemes : [""])].forEach(
219
+ (colorTheme) => {
220
+ const theme = [baseTheme, colorTheme].filter(Boolean) as Themes;
221
+ const testidBase = buildTestid([baseClass, ...theme]);
222
+ const allVariants = options.includeNullVariant
223
+ ? ["", ...variants]
224
+ : variants;
225
+ const primaryModifiers = modifiers?.primary
226
+ ? options.includeNullModifier
227
+ ? ["", ...(<[]>modifiers.primary)]
228
+ : modifiers.primary
229
+ : [""];
230
+ const secondaryModifiers = modifiers?.secondary
231
+ ? ["", ...(<[]>modifiers.secondary)]
232
+ : [""];
233
+ const globalModifiers = modifiers?.global
234
+ ? ["", ...(<[]>modifiers.global)]
235
+ : [""];
236
+
237
+ primaryModifiers.forEach((primaryModifier) => {
238
+ secondaryModifiers.forEach((secondaryModifier) => {
239
+ globalModifiers.forEach((globalModifier) => {
240
+ allVariants.forEach((variant) => {
241
+ testVariations.push({
242
+ classes: buildClasses({
243
+ baseClass,
244
+ prefixed: [
245
+ variant,
246
+ primaryModifier,
247
+ secondaryModifier,
248
+ ],
249
+ unprefixed: [globalModifier],
250
+ }),
251
+ testid: buildTestid([
252
+ testidBase,
253
+ variant,
254
+ [
255
+ primaryModifier,
256
+ secondaryModifier,
257
+ globalModifier,
258
+ ]
259
+ .filter(Boolean)
260
+ .join("-"),
261
+ ]),
262
+ theme,
263
+ });
264
+ });
265
+ });
266
+ });
267
+ });
268
+
269
+ // create standalone modifiers test props
270
+ modifiers?.standalone?.forEach((standaloneModifier) => {
271
+ testVariations.push({
272
+ testid: buildTestid([testidBase, standaloneModifier]),
273
+ classes: buildClasses({
274
+ baseClass,
275
+ prefixed: [standaloneModifier],
276
+ }),
277
+ theme,
278
+ });
279
+ });
280
+ }
281
+ );
282
+ });
283
+
284
+ // Sorting for readability
285
+ return testVariations.sort((a, b) => a.testid.localeCompare(b.testid));
286
+ };
287
+
288
+ /**
289
+ * Constructs and runs an individual test for a component
290
+ */
291
+ const runComponentTest = ({
292
+ element,
293
+ testid,
294
+ theme,
295
+ type,
296
+ }: ComponentTestArgs) => {
297
+ const getDescription = (type: TestTypes) => {
298
+ switch (type) {
299
+ case "a11y":
300
+ return "should be accessible";
301
+ case "visual":
302
+ return "should not introduce visual regressions";
303
+ default:
304
+ return "";
305
+ }
306
+ };
307
+
308
+ it(`${type}: ${testid} ${getDescription(type)}`, async () => {
309
+ await fixture(element);
310
+ const el = screen.getByTestId(testid);
311
+
312
+ document.body.className = "";
313
+
314
+ if (theme?.length) {
315
+ const prefixedThemes = theme.map((t) => `theme-${t}`);
316
+ document.body.classList.add(...prefixedThemes);
317
+ }
318
+
319
+ if (type === "a11y") {
320
+ // TODO add conditional option for high contrast mode to test against AAA
321
+ await expect(el).to.be.accessible();
322
+ }
323
+
324
+ if (type === "visual") {
325
+ await visualDiff(el, testid);
326
+ }
327
+ });
328
+ };
329
+
330
+ /**
331
+ * Constructs and runs tests for a component with a each provided combination
332
+ */
333
+ const runComponentTests = ({
334
+ baseClass,
335
+ variants = [],
336
+ modifiers,
337
+ options = defaultOptions,
338
+ attributes,
339
+ children,
340
+ excludedTestids = [],
341
+ skippedTestids = [],
342
+ tag,
343
+ template,
344
+ type,
345
+ }: ComponentTestsArgs) => {
346
+ getComponentTestVariations({
347
+ baseClass,
348
+ variants,
349
+ modifiers,
350
+ options,
351
+ }).forEach(({ testid, classes, theme }) => {
352
+ const allChildren: {
353
+ [key: string]: string;
354
+ } = children ? { ...children } : { default: "" };
355
+ const { testidSuffix } = options;
356
+
357
+ Object.keys(allChildren).forEach((key) => {
358
+ let testidModified = (
359
+ key !== "default" ? `${testid}-${key}` : testid
360
+ ).replace(" ", "-");
361
+ testidModified = testidSuffix
362
+ ? `${testidModified}-${testidSuffix}`
363
+ : testidModified;
364
+
365
+ const children = allChildren[key];
366
+
367
+ const shouldSkipTest = excludeOrSkipTest({
368
+ patterns: skippedTestids,
369
+ skip: true,
370
+ testid: testidModified,
371
+ type,
372
+ });
373
+
374
+ const shouldExcludeTest = excludeOrSkipTest({
375
+ patterns: excludedTestids,
376
+ testid: testidModified,
377
+ type,
378
+ });
379
+
380
+ if (shouldSkipTest || shouldExcludeTest) {
381
+ return;
382
+ }
383
+
384
+ const element = template
385
+ ? html`${template({
386
+ testid: testidModified,
387
+ component: buildTestElement({
388
+ attributes: {
389
+ ...attributes,
390
+ class: `${classes} ${attributes?.class || ""}`,
391
+ },
392
+ children,
393
+ testid: `${testidModified}-nested`,
394
+ tag,
395
+ }),
396
+ })}`
397
+ : buildTestElement({
398
+ attributes: {
399
+ ...attributes,
400
+ class: `${classes} ${attributes?.class || ""}`,
401
+ },
402
+ children,
403
+ testid: testidModified,
404
+ tag,
405
+ });
406
+
407
+ runComponentTest({
408
+ element,
409
+ testid: testidModified,
410
+ theme,
411
+ type,
412
+ });
413
+ });
414
+ });
415
+ };
416
+
417
+ const matchTestidByPattern = ({
418
+ testid,
419
+ pattern,
420
+ }: {
421
+ testid: string;
422
+ pattern: string | RegExp;
423
+ }): boolean => {
424
+ if (pattern instanceof RegExp) {
425
+ return pattern.test(testid);
426
+ } else {
427
+ return pattern === testid;
428
+ }
429
+ };
430
+
431
+ const excludeOrSkipTest = ({
432
+ patterns,
433
+ skip = false,
434
+ testid,
435
+ type,
436
+ }: {
437
+ patterns: (string | RegExp)[];
438
+ skip?: boolean;
439
+ testid: string;
440
+ type: TestTypes;
441
+ }): boolean => {
442
+ const matchesTest = patterns.some((pattern) => {
443
+ return matchTestidByPattern({ testid, pattern });
444
+ });
445
+
446
+ if (matchesTest && skip) {
447
+ it.skip(`${type}: ${testid} (skipped)`, () => {
448
+ return;
449
+ });
450
+ }
451
+
452
+ return matchesTest;
453
+ };
454
+
455
+ export { runComponentTest, runComponentTests };
456
+
457
+ /**
458
+ * Convert a const array of strings into a union type of the array's values.
459
+ *
460
+ * @example
461
+ * ```
462
+ * const arrayOfStrings = ['Stacky', 'Ben', 'Dan', 'Giamir'] as const;
463
+ * type StringLiterals = AsLiterals<typeof arrayOfStrings>; // 'Stacky' | 'Ben' | 'Dan' | 'Giamir'
464
+ * ```
465
+ */
466
+ export type AsLiterals<T extends Readonly<string[]>> = T[number];