aria-ease 6.8.0 → 6.9.1
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 +68 -6
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/bin/buildContracts-GBOY7UXG.js +437 -0
- package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
- package/bin/chunk-PK5L2SAF.js +17 -0
- package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/bin/cli.cjs +991 -128
- package/bin/cli.js +33 -2
- package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
- package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
- package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
- package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/dist/chunk-PK5L2SAF.js +17 -0
- package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
- package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
- package/dist/index.cjs +794 -90
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +415 -10
- package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
- package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
- package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
- package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
- package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
- package/dist/src/utils/test/dsl/index.cjs +320 -0
- package/dist/src/utils/test/dsl/index.d.cts +136 -0
- package/dist/src/utils/test/dsl/index.d.ts +136 -0
- package/dist/src/utils/test/dsl/index.js +318 -0
- package/dist/src/utils/test/index.cjs +472 -88
- package/dist/src/utils/test/index.js +97 -12
- package/package.json +9 -3
|
@@ -32,11 +32,13 @@ var ContractReporter = class {
|
|
|
32
32
|
skipped = 0;
|
|
33
33
|
warnings = 0;
|
|
34
34
|
isPlaywright = false;
|
|
35
|
+
isCustomContract = false;
|
|
35
36
|
apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
|
|
36
37
|
hasPrintedStaticSection = false;
|
|
37
38
|
hasPrintedDynamicSection = false;
|
|
38
|
-
constructor(isPlaywright = false) {
|
|
39
|
+
constructor(isPlaywright = false, isCustomContract = false) {
|
|
39
40
|
this.isPlaywright = isPlaywright;
|
|
41
|
+
this.isCustomContract = isCustomContract;
|
|
40
42
|
}
|
|
41
43
|
log(message) {
|
|
42
44
|
process.stderr.write(message + "\n");
|
|
@@ -197,6 +199,13 @@ ${"\u2500".repeat(60)}`);
|
|
|
197
199
|
const totalPasses = this.staticPasses + dynamicPasses;
|
|
198
200
|
const totalFailures = this.staticFailures + dynamicFailures;
|
|
199
201
|
const totalRun = totalPasses + totalFailures + this.warnings;
|
|
202
|
+
const getComponentMessage = () => {
|
|
203
|
+
const componentDisplayName = `${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)}`;
|
|
204
|
+
if (this.isCustomContract) {
|
|
205
|
+
return `${componentDisplayName} component validates against your custom accessibility policy \u2713`;
|
|
206
|
+
}
|
|
207
|
+
return `${componentDisplayName} component meets Aria-Ease baseline WAI-ARIA expectations \u2713`;
|
|
208
|
+
};
|
|
200
209
|
if (failures.length > 0) {
|
|
201
210
|
this.reportFailures(failures);
|
|
202
211
|
}
|
|
@@ -208,7 +217,7 @@ ${"\u2550".repeat(60)}`);
|
|
|
208
217
|
`);
|
|
209
218
|
if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
|
|
210
219
|
this.log(`\u2705 All ${totalRun} tests passed!`);
|
|
211
|
-
this.log(` ${
|
|
220
|
+
this.log(` ${getComponentMessage()}`);
|
|
212
221
|
} else if (totalFailures === 0) {
|
|
213
222
|
this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
|
|
214
223
|
if (this.skipped > 0) {
|
|
@@ -217,7 +226,7 @@ ${"\u2550".repeat(60)}`);
|
|
|
217
226
|
if (this.warnings > 0) {
|
|
218
227
|
this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
|
|
219
228
|
}
|
|
220
|
-
this.log(` ${
|
|
229
|
+
this.log(` ${getComponentMessage()}`);
|
|
221
230
|
} else {
|
|
222
231
|
this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
|
|
223
232
|
this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
|
|
@@ -55,6 +55,9 @@ function validateConfig(config) {
|
|
|
55
55
|
if (comp.path !== void 0 && typeof comp.path !== "string") {
|
|
56
56
|
errors.push(`test.components[${idx}].path must be a string when provided`);
|
|
57
57
|
}
|
|
58
|
+
if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
|
|
59
|
+
errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
|
|
60
|
+
}
|
|
58
61
|
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
59
62
|
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
60
63
|
}
|
|
@@ -69,6 +72,24 @@ function validateConfig(config) {
|
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
if (cfg.contracts !== void 0) {
|
|
76
|
+
if (!Array.isArray(cfg.contracts)) {
|
|
77
|
+
errors.push("contracts must be an array");
|
|
78
|
+
} else {
|
|
79
|
+
cfg.contracts.forEach((contract, idx) => {
|
|
80
|
+
if (typeof contract !== "object" || contract === null) {
|
|
81
|
+
errors.push(`contracts[${idx}] must be an object`);
|
|
82
|
+
} else {
|
|
83
|
+
if (typeof contract.src !== "string") {
|
|
84
|
+
errors.push(`contracts[${idx}].src is required and must be a string`);
|
|
85
|
+
}
|
|
86
|
+
if (contract.out !== void 0 && typeof contract.out !== "string") {
|
|
87
|
+
errors.push(`contracts[${idx}].out must be a string`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
return { valid: errors.length === 0, errors };
|
|
73
94
|
}
|
|
74
95
|
async function loadConfigFile(filePath) {
|
|
@@ -1,220 +1,158 @@
|
|
|
1
|
-
import { ContractReporter, normalizeStrictness,
|
|
1
|
+
import { contract_default, ContractReporter, normalizeStrictness, createTestPage, normalizeLevel, resolveEnforcement } from './chunk-XERMSYEH.js';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import path2 from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
3
5
|
import { expect } from '@playwright/test';
|
|
4
6
|
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this.
|
|
9
|
-
this.actionTimeoutMs = actionTimeoutMs;
|
|
10
|
-
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
11
|
-
}
|
|
12
|
-
async resetState(page) {
|
|
13
|
-
if (!this.selectors.popup) return;
|
|
14
|
-
const popupSelector = this.selectors.popup;
|
|
15
|
-
const popupElement = page.locator(popupSelector).first();
|
|
16
|
-
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
17
|
-
if (!isPopupVisible) return;
|
|
18
|
-
let menuClosed = false;
|
|
19
|
-
let closeSelector = this.selectors.input;
|
|
20
|
-
if (!closeSelector && this.selectors.focusable) {
|
|
21
|
-
closeSelector = this.selectors.focusable;
|
|
22
|
-
} else if (!closeSelector) {
|
|
23
|
-
closeSelector = this.selectors.trigger;
|
|
24
|
-
}
|
|
25
|
-
if (closeSelector) {
|
|
26
|
-
const closeElement = page.locator(closeSelector).first();
|
|
27
|
-
await closeElement.focus();
|
|
28
|
-
await page.keyboard.press("Escape");
|
|
29
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
30
|
-
}
|
|
31
|
-
if (!menuClosed && this.selectors.trigger) {
|
|
32
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
33
|
-
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
34
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
35
|
-
}
|
|
36
|
-
if (!menuClosed) {
|
|
37
|
-
await page.mouse.click(10, 10);
|
|
38
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
39
|
-
}
|
|
40
|
-
if (!menuClosed) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
43
|
-
1. Escape key
|
|
44
|
-
2. Clicking trigger
|
|
45
|
-
3. Clicking outside
|
|
46
|
-
This indicates a problem with the combobox component's close functionality.`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
if (this.selectors.input) {
|
|
50
|
-
await page.locator(this.selectors.input).first().clear();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async shouldSkipTest() {
|
|
54
|
-
return false;
|
|
7
|
+
var StrategyRegistry = class {
|
|
8
|
+
builtInStrategies = /* @__PURE__ */ new Map();
|
|
9
|
+
constructor() {
|
|
10
|
+
this.registerBuiltInStrategies();
|
|
55
11
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Register built-in strategies
|
|
14
|
+
*/
|
|
15
|
+
registerBuiltInStrategies() {
|
|
16
|
+
this.builtInStrategies.set(
|
|
17
|
+
"menu",
|
|
18
|
+
() => import('./MenuComponentStrategy-VKZQYLBE.js').then(
|
|
19
|
+
(m) => m.MenuComponentStrategy
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
this.builtInStrategies.set(
|
|
23
|
+
"accordion",
|
|
24
|
+
() => import('./AccordionComponentStrategy-WRHZOEN6.js').then(
|
|
25
|
+
(m) => m.AccordionComponentStrategy
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
this.builtInStrategies.set(
|
|
29
|
+
"combobox",
|
|
30
|
+
() => import('./ComboboxComponentStrategy-5AECQSRN.js').then(
|
|
31
|
+
(m) => m.ComboboxComponentStrategy
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
this.builtInStrategies.set(
|
|
35
|
+
"tabs",
|
|
36
|
+
() => import('./TabsComponentStrategy-BKG53SEV.js').then(
|
|
37
|
+
(m) => m.TabsComponentStrategy
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
this.builtInStrategies.set(
|
|
41
|
+
"combobox.listbox",
|
|
42
|
+
() => import('./ComboboxComponentStrategy-5AECQSRN.js').then(
|
|
43
|
+
(m) => m.ComboboxComponentStrategy
|
|
44
|
+
)
|
|
45
|
+
);
|
|
66
46
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Load a strategy - either from custom path or built-in registry
|
|
49
|
+
* @param componentName - Component name (e.g., "menu", "accordion")
|
|
50
|
+
* @param customStrategyPath - Optional custom strategy file path
|
|
51
|
+
* @returns Strategy constructor function or null if not found
|
|
52
|
+
*/
|
|
53
|
+
async loadStrategy(componentName, customStrategyPath, configBaseDir) {
|
|
54
|
+
try {
|
|
55
|
+
if (customStrategyPath) {
|
|
56
|
+
try {
|
|
57
|
+
const resolvedCustomPath = path2.isAbsolute(customStrategyPath) ? customStrategyPath : path2.resolve(configBaseDir || process.cwd(), customStrategyPath);
|
|
58
|
+
const customModule = await import(pathToFileURL(resolvedCustomPath).href);
|
|
59
|
+
const strategy = customModule.default || customModule;
|
|
60
|
+
if (!strategy) {
|
|
61
|
+
throw new Error(`No default export found in ${customStrategyPath}`);
|
|
62
|
+
}
|
|
63
|
+
return strategy;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
83
69
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return this.mainSelector;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
var MenuComponentStrategy = class {
|
|
94
|
-
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
95
|
-
this.mainSelector = mainSelector;
|
|
96
|
-
this.selectors = selectors;
|
|
97
|
-
this.actionTimeoutMs = actionTimeoutMs;
|
|
98
|
-
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
99
|
-
}
|
|
100
|
-
async resetState(page) {
|
|
101
|
-
if (!this.selectors.popup) return;
|
|
102
|
-
const popupSelector = this.selectors.popup;
|
|
103
|
-
const popupElement = page.locator(popupSelector).first();
|
|
104
|
-
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
105
|
-
if (!isPopupVisible) return;
|
|
106
|
-
let menuClosed = false;
|
|
107
|
-
let closeSelector = this.selectors.input;
|
|
108
|
-
if (!closeSelector && this.selectors.focusable) {
|
|
109
|
-
closeSelector = this.selectors.focusable;
|
|
110
|
-
} else if (!closeSelector) {
|
|
111
|
-
closeSelector = this.selectors.trigger;
|
|
112
|
-
}
|
|
113
|
-
if (closeSelector) {
|
|
114
|
-
const closeElement = page.locator(closeSelector).first();
|
|
115
|
-
await closeElement.focus();
|
|
116
|
-
await page.keyboard.press("Escape");
|
|
117
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
118
|
-
}
|
|
119
|
-
if (!menuClosed && this.selectors.trigger) {
|
|
120
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
121
|
-
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
122
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
123
|
-
}
|
|
124
|
-
if (!menuClosed) {
|
|
125
|
-
await page.mouse.click(10, 10);
|
|
126
|
-
menuClosed = await expect(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
127
|
-
}
|
|
128
|
-
if (!menuClosed) {
|
|
70
|
+
const builtInLoader = this.builtInStrategies.get(componentName);
|
|
71
|
+
if (!builtInLoader) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return builtInLoader();
|
|
75
|
+
} catch (error) {
|
|
129
76
|
throw new Error(
|
|
130
|
-
|
|
131
|
-
1. Escape key
|
|
132
|
-
2. Clicking trigger
|
|
133
|
-
3. Clicking outside
|
|
134
|
-
This indicates a problem with the menu component's close functionality.`
|
|
77
|
+
`Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
|
|
135
78
|
);
|
|
136
79
|
}
|
|
137
|
-
if (this.selectors.input) {
|
|
138
|
-
await page.locator(this.selectors.input).first().clear();
|
|
139
|
-
}
|
|
140
|
-
if (this.selectors.trigger) {
|
|
141
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
142
|
-
await triggerElement.focus();
|
|
143
|
-
}
|
|
144
80
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
if (!requiresSubmenu) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
const submenuTriggerSelector = this.selectors.submenuTrigger;
|
|
155
|
-
if (!submenuTriggerSelector) {
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
|
|
159
|
-
return submenuTriggerCount === 0;
|
|
160
|
-
}
|
|
161
|
-
getMainSelector() {
|
|
162
|
-
return this.mainSelector;
|
|
81
|
+
/**
|
|
82
|
+
* Check if a strategy exists (either built-in or custom path provided)
|
|
83
|
+
*/
|
|
84
|
+
has(componentName, customStrategyPath) {
|
|
85
|
+
return !!customStrategyPath || this.builtInStrategies.has(componentName);
|
|
163
86
|
}
|
|
164
87
|
};
|
|
165
88
|
|
|
166
|
-
// src/utils/test/src/
|
|
167
|
-
var
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
async resetState() {
|
|
89
|
+
// src/utils/test/src/ComponentDetector.ts
|
|
90
|
+
var ComponentDetector = class {
|
|
91
|
+
static strategyRegistry = new StrategyRegistry();
|
|
92
|
+
static isComponentConfig(value) {
|
|
93
|
+
return typeof value === "object" && value !== null;
|
|
173
94
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Detect and instantiate a component strategy
|
|
97
|
+
* Supports:
|
|
98
|
+
* - Built-in strategies (menu, accordion, combobox, tabs)
|
|
99
|
+
* - Custom strategies via config (strategyPath)
|
|
100
|
+
* - Custom contract paths via config (path)
|
|
101
|
+
* @param componentName - Component name
|
|
102
|
+
* @param componentConfig - Component config from ariaease.config.js
|
|
103
|
+
* @param actionTimeoutMs - Action timeout in milliseconds
|
|
104
|
+
* @param assertionTimeoutMs - Assertion timeout in milliseconds
|
|
105
|
+
* @returns Instantiated ComponentStrategy or null
|
|
106
|
+
*/
|
|
107
|
+
static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
|
|
108
|
+
const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
|
|
109
|
+
let contractPath = typedComponentConfig?.path;
|
|
110
|
+
if (!contractPath) {
|
|
111
|
+
const contractTyped = contract_default;
|
|
112
|
+
contractPath = contractTyped[componentName]?.path;
|
|
183
113
|
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
getMainSelector() {
|
|
187
|
-
return this.mainSelector;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
var ComponentDetector = class {
|
|
191
|
-
static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
192
|
-
const contractTyped = contract_default;
|
|
193
|
-
const contractPath = contractTyped[componentName]?.path;
|
|
194
114
|
if (!contractPath) {
|
|
195
115
|
throw new Error(`Contract path not found for component: ${componentName}`);
|
|
196
116
|
}
|
|
197
|
-
const resolvedPath =
|
|
117
|
+
const resolvedPath = (() => {
|
|
118
|
+
if (path2.isAbsolute(contractPath)) return contractPath;
|
|
119
|
+
if (configBaseDir) {
|
|
120
|
+
const configResolved = path2.resolve(configBaseDir, contractPath);
|
|
121
|
+
try {
|
|
122
|
+
readFileSync(configResolved, "utf-8");
|
|
123
|
+
return configResolved;
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const cwdResolved = path2.resolve(process.cwd(), contractPath);
|
|
128
|
+
try {
|
|
129
|
+
readFileSync(cwdResolved, "utf-8");
|
|
130
|
+
return cwdResolved;
|
|
131
|
+
} catch {
|
|
132
|
+
return new URL(contractPath, import.meta.url).pathname;
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
198
135
|
const contractData = readFileSync(resolvedPath, "utf-8");
|
|
199
136
|
const componentContract = JSON.parse(contractData);
|
|
200
137
|
const selectors = componentContract.selectors;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
if (componentName === "menu") {
|
|
210
|
-
const mainSelector = selectors.trigger || selectors.container;
|
|
211
|
-
return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
|
|
138
|
+
const strategyClass = await this.strategyRegistry.loadStrategy(
|
|
139
|
+
componentName,
|
|
140
|
+
typedComponentConfig?.strategyPath,
|
|
141
|
+
configBaseDir
|
|
142
|
+
);
|
|
143
|
+
if (!strategyClass) {
|
|
144
|
+
return null;
|
|
212
145
|
}
|
|
146
|
+
const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
|
|
213
147
|
if (componentName === "tabs") {
|
|
214
|
-
|
|
215
|
-
return new TabsComponentStrategy(mainSelector, selectors);
|
|
148
|
+
return new strategyClass(mainSelector, selectors);
|
|
216
149
|
}
|
|
217
|
-
return
|
|
150
|
+
return new strategyClass(
|
|
151
|
+
mainSelector,
|
|
152
|
+
selectors,
|
|
153
|
+
actionTimeoutMs,
|
|
154
|
+
assertionTimeoutMs
|
|
155
|
+
);
|
|
218
156
|
}
|
|
219
157
|
};
|
|
220
158
|
|
|
@@ -696,17 +634,42 @@ var AssertionRunner = class {
|
|
|
696
634
|
};
|
|
697
635
|
|
|
698
636
|
// src/utils/test/src/contractTestRunnerPlaywright.ts
|
|
699
|
-
async function runContractTestsPlaywright(componentName, url, strictness) {
|
|
700
|
-
const
|
|
637
|
+
async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
|
|
638
|
+
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
639
|
+
const isCustomContract = !!componentConfig?.path;
|
|
640
|
+
const reporter = new ContractReporter(true, isCustomContract);
|
|
701
641
|
const actionTimeoutMs = 400;
|
|
702
642
|
const assertionTimeoutMs = 400;
|
|
703
643
|
const strictnessMode = normalizeStrictness(strictness);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
644
|
+
let contractPath = componentConfig?.path;
|
|
645
|
+
if (!contractPath) {
|
|
646
|
+
const contractTyped = contract_default;
|
|
647
|
+
contractPath = contractTyped[componentName]?.path;
|
|
648
|
+
}
|
|
649
|
+
if (!contractPath) {
|
|
650
|
+
throw new Error(`Contract path not found for component: ${componentName}`);
|
|
651
|
+
}
|
|
652
|
+
const resolvedPath = (() => {
|
|
653
|
+
if (path2.isAbsolute(contractPath)) return contractPath;
|
|
654
|
+
if (configBaseDir) {
|
|
655
|
+
const configResolved = path2.resolve(configBaseDir, contractPath);
|
|
656
|
+
try {
|
|
657
|
+
readFileSync(configResolved, "utf-8");
|
|
658
|
+
return configResolved;
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const cwdResolved = path2.resolve(process.cwd(), contractPath);
|
|
663
|
+
try {
|
|
664
|
+
readFileSync(cwdResolved, "utf-8");
|
|
665
|
+
return cwdResolved;
|
|
666
|
+
} catch {
|
|
667
|
+
return new URL(contractPath, import.meta.url).pathname;
|
|
668
|
+
}
|
|
669
|
+
})();
|
|
707
670
|
const contractData = readFileSync(resolvedPath, "utf-8");
|
|
708
671
|
const componentContract = JSON.parse(contractData);
|
|
709
|
-
const totalTests = componentContract.static[0]
|
|
672
|
+
const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
|
|
710
673
|
const apgUrl = componentContract.meta?.source?.apg;
|
|
711
674
|
const failures = [];
|
|
712
675
|
const warnings = [];
|
|
@@ -743,7 +706,7 @@ async function runContractTestsPlaywright(componentName, url, strictness) {
|
|
|
743
706
|
}
|
|
744
707
|
await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
|
|
745
708
|
}
|
|
746
|
-
const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
|
|
709
|
+
const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
|
|
747
710
|
if (!strategy) {
|
|
748
711
|
throw new Error(`Unsupported component: ${componentName}`);
|
|
749
712
|
}
|
|
@@ -776,6 +739,105 @@ This usually means:
|
|
|
776
739
|
let staticPassed = 0;
|
|
777
740
|
let staticFailed = 0;
|
|
778
741
|
let staticWarnings = 0;
|
|
742
|
+
for (const rel of componentContract.relationships || []) {
|
|
743
|
+
const relationshipLevel = normalizeLevel(rel.level);
|
|
744
|
+
if (rel.type === "aria-reference") {
|
|
745
|
+
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
746
|
+
const fromSelector = componentContract.selectors[rel.from];
|
|
747
|
+
const toSelector = componentContract.selectors[rel.to];
|
|
748
|
+
if (!fromSelector || !toSelector) {
|
|
749
|
+
const outcome = classifyFailure(
|
|
750
|
+
`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
|
|
751
|
+
rel.level
|
|
752
|
+
);
|
|
753
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
754
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
755
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const fromTarget = page.locator(fromSelector).first();
|
|
759
|
+
const toTarget = page.locator(toSelector).first();
|
|
760
|
+
const fromExists = await fromTarget.count() > 0;
|
|
761
|
+
const toExists = await toTarget.count() > 0;
|
|
762
|
+
if (!fromExists || !toExists) {
|
|
763
|
+
const outcome = classifyFailure(
|
|
764
|
+
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
765
|
+
rel.level
|
|
766
|
+
);
|
|
767
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
768
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
769
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const attrValue = await fromTarget.getAttribute(rel.attribute);
|
|
773
|
+
const toId = await toTarget.getAttribute("id");
|
|
774
|
+
if (!toId) {
|
|
775
|
+
const outcome = classifyFailure(
|
|
776
|
+
`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
|
|
777
|
+
rel.level
|
|
778
|
+
);
|
|
779
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
780
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
781
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const references = (attrValue || "").split(/\s+/).filter(Boolean);
|
|
785
|
+
const matches = references.includes(toId);
|
|
786
|
+
if (!matches) {
|
|
787
|
+
const outcome = classifyFailure(
|
|
788
|
+
`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
|
|
789
|
+
rel.level
|
|
790
|
+
);
|
|
791
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
792
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
793
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
|
|
797
|
+
staticPassed += 1;
|
|
798
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (rel.type === "contains") {
|
|
802
|
+
const relDescription = `${rel.parent} contains ${rel.child}`;
|
|
803
|
+
const parentSelector = componentContract.selectors[rel.parent];
|
|
804
|
+
const childSelector = componentContract.selectors[rel.child];
|
|
805
|
+
if (!parentSelector || !childSelector) {
|
|
806
|
+
const outcome = classifyFailure(
|
|
807
|
+
`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
|
|
808
|
+
rel.level
|
|
809
|
+
);
|
|
810
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
811
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
812
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const parent = page.locator(parentSelector).first();
|
|
816
|
+
const parentExists = await parent.count() > 0;
|
|
817
|
+
if (!parentExists) {
|
|
818
|
+
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
819
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
820
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
821
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const descendants = parent.locator(childSelector);
|
|
825
|
+
const descendantCount = await descendants.count();
|
|
826
|
+
if (descendantCount < 1) {
|
|
827
|
+
const outcome = classifyFailure(
|
|
828
|
+
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
829
|
+
rel.level
|
|
830
|
+
);
|
|
831
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
832
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
833
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
|
|
837
|
+
staticPassed += 1;
|
|
838
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
779
841
|
const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
780
842
|
for (const test of componentContract.static[0]?.assertions || []) {
|
|
781
843
|
if (test.target === "relative") continue;
|