aria-ease 6.7.0 → 6.9.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 +77 -10
- 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 +800 -96
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +421 -16
- 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/aria-contracts/accordion/accordion.contract.json +5 -11
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +1 -1
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +1 -1
- 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/index.cjs +472 -88
- package/dist/src/utils/test/index.js +97 -12
- package/package.json +7 -2
|
@@ -5,245 +5,172 @@ import {
|
|
|
5
5
|
normalizeLevel,
|
|
6
6
|
normalizeStrictness,
|
|
7
7
|
resolveEnforcement
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-XERMSYEH.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
test_exports
|
|
11
|
+
} from "./chunk-PK5L2SAF.js";
|
|
12
|
+
import "./chunk-I2KLQ2HA.js";
|
|
13
13
|
|
|
14
14
|
// src/utils/test/src/contractTestRunnerPlaywright.ts
|
|
15
15
|
import { readFileSync as readFileSync2 } from "fs";
|
|
16
|
+
import path3 from "path";
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
default: () => default2
|
|
21
|
-
});
|
|
22
|
-
__reExport(test_exports, test_star);
|
|
23
|
-
import * as test_star from "playwright/test";
|
|
24
|
-
import { default as default2 } from "playwright/test";
|
|
18
|
+
// src/utils/test/src/ComponentDetector.ts
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
import path2 from "path";
|
|
25
21
|
|
|
26
|
-
// src/utils/test/src/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
}
|
|
34
|
-
async resetState(page) {
|
|
35
|
-
if (!this.selectors.popup) return;
|
|
36
|
-
const popupSelector = this.selectors.popup;
|
|
37
|
-
const popupElement = page.locator(popupSelector).first();
|
|
38
|
-
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
39
|
-
if (!isPopupVisible) return;
|
|
40
|
-
let menuClosed = false;
|
|
41
|
-
let closeSelector = this.selectors.input;
|
|
42
|
-
if (!closeSelector && this.selectors.focusable) {
|
|
43
|
-
closeSelector = this.selectors.focusable;
|
|
44
|
-
} else if (!closeSelector) {
|
|
45
|
-
closeSelector = this.selectors.trigger;
|
|
46
|
-
}
|
|
47
|
-
if (closeSelector) {
|
|
48
|
-
const closeElement = page.locator(closeSelector).first();
|
|
49
|
-
await closeElement.focus();
|
|
50
|
-
await page.keyboard.press("Escape");
|
|
51
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
52
|
-
}
|
|
53
|
-
if (!menuClosed && this.selectors.trigger) {
|
|
54
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
55
|
-
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
56
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
57
|
-
}
|
|
58
|
-
if (!menuClosed) {
|
|
59
|
-
await page.mouse.click(10, 10);
|
|
60
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
61
|
-
}
|
|
62
|
-
if (!menuClosed) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
65
|
-
1. Escape key
|
|
66
|
-
2. Clicking trigger
|
|
67
|
-
3. Clicking outside
|
|
68
|
-
This indicates a problem with the combobox component's close functionality.`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
if (this.selectors.input) {
|
|
72
|
-
await page.locator(this.selectors.input).first().clear();
|
|
73
|
-
}
|
|
22
|
+
// src/utils/test/src/StrategyRegistry.ts
|
|
23
|
+
import path from "path";
|
|
24
|
+
import { pathToFileURL } from "url";
|
|
25
|
+
var StrategyRegistry = class {
|
|
26
|
+
builtInStrategies = /* @__PURE__ */ new Map();
|
|
27
|
+
constructor() {
|
|
28
|
+
this.registerBuiltInStrategies();
|
|
74
29
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Register built-in strategies
|
|
32
|
+
*/
|
|
33
|
+
registerBuiltInStrategies() {
|
|
34
|
+
this.builtInStrategies.set(
|
|
35
|
+
"menu",
|
|
36
|
+
() => import("./MenuComponentStrategy-JAMTCSNF.js").then(
|
|
37
|
+
(m) => m.MenuComponentStrategy
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
this.builtInStrategies.set(
|
|
41
|
+
"accordion",
|
|
42
|
+
() => import("./AccordionComponentStrategy-4ZEIQ2V6.js").then(
|
|
43
|
+
(m) => m.AccordionComponentStrategy
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
this.builtInStrategies.set(
|
|
47
|
+
"combobox",
|
|
48
|
+
() => import("./ComboboxComponentStrategy-OGRVZXAF.js").then(
|
|
49
|
+
(m) => m.ComboboxComponentStrategy
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
this.builtInStrategies.set(
|
|
53
|
+
"tabs",
|
|
54
|
+
() => import("./TabsComponentStrategy-3SQURPMX.js").then(
|
|
55
|
+
(m) => m.TabsComponentStrategy
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
this.builtInStrategies.set(
|
|
59
|
+
"combobox.listbox",
|
|
60
|
+
() => import("./ComboboxComponentStrategy-OGRVZXAF.js").then(
|
|
61
|
+
(m) => m.ComboboxComponentStrategy
|
|
62
|
+
)
|
|
63
|
+
);
|
|
90
64
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Load a strategy - either from custom path or built-in registry
|
|
67
|
+
* @param componentName - Component name (e.g., "menu", "accordion")
|
|
68
|
+
* @param customStrategyPath - Optional custom strategy file path
|
|
69
|
+
* @returns Strategy constructor function or null if not found
|
|
70
|
+
*/
|
|
71
|
+
async loadStrategy(componentName, customStrategyPath, configBaseDir) {
|
|
72
|
+
try {
|
|
73
|
+
if (customStrategyPath) {
|
|
74
|
+
try {
|
|
75
|
+
const resolvedCustomPath = path.isAbsolute(customStrategyPath) ? customStrategyPath : path.resolve(configBaseDir || process.cwd(), customStrategyPath);
|
|
76
|
+
const customModule = await import(pathToFileURL(resolvedCustomPath).href);
|
|
77
|
+
const strategy = customModule.default || customModule;
|
|
78
|
+
if (!strategy) {
|
|
79
|
+
throw new Error(`No default export found in ${customStrategyPath}`);
|
|
80
|
+
}
|
|
81
|
+
return strategy;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
107
87
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return this.mainSelector;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// src/utils/test/src/component-strategies/MenuComponentStrategy.ts
|
|
119
|
-
var MenuComponentStrategy = class {
|
|
120
|
-
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
121
|
-
this.mainSelector = mainSelector;
|
|
122
|
-
this.selectors = selectors;
|
|
123
|
-
this.actionTimeoutMs = actionTimeoutMs;
|
|
124
|
-
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
125
|
-
}
|
|
126
|
-
async resetState(page) {
|
|
127
|
-
if (!this.selectors.popup) return;
|
|
128
|
-
const popupSelector = this.selectors.popup;
|
|
129
|
-
const popupElement = page.locator(popupSelector).first();
|
|
130
|
-
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
131
|
-
if (!isPopupVisible) return;
|
|
132
|
-
let menuClosed = false;
|
|
133
|
-
let closeSelector = this.selectors.input;
|
|
134
|
-
if (!closeSelector && this.selectors.focusable) {
|
|
135
|
-
closeSelector = this.selectors.focusable;
|
|
136
|
-
} else if (!closeSelector) {
|
|
137
|
-
closeSelector = this.selectors.trigger;
|
|
138
|
-
}
|
|
139
|
-
if (closeSelector) {
|
|
140
|
-
const closeElement = page.locator(closeSelector).first();
|
|
141
|
-
await closeElement.focus();
|
|
142
|
-
await page.keyboard.press("Escape");
|
|
143
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
144
|
-
}
|
|
145
|
-
if (!menuClosed && this.selectors.trigger) {
|
|
146
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
147
|
-
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
148
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
149
|
-
}
|
|
150
|
-
if (!menuClosed) {
|
|
151
|
-
await page.mouse.click(10, 10);
|
|
152
|
-
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
153
|
-
}
|
|
154
|
-
if (!menuClosed) {
|
|
88
|
+
const builtInLoader = this.builtInStrategies.get(componentName);
|
|
89
|
+
if (!builtInLoader) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return builtInLoader();
|
|
93
|
+
} catch (error) {
|
|
155
94
|
throw new Error(
|
|
156
|
-
|
|
157
|
-
1. Escape key
|
|
158
|
-
2. Clicking trigger
|
|
159
|
-
3. Clicking outside
|
|
160
|
-
This indicates a problem with the menu component's close functionality.`
|
|
95
|
+
`Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
|
|
161
96
|
);
|
|
162
97
|
}
|
|
163
|
-
if (this.selectors.input) {
|
|
164
|
-
await page.locator(this.selectors.input).first().clear();
|
|
165
|
-
}
|
|
166
|
-
if (this.selectors.trigger) {
|
|
167
|
-
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
168
|
-
await triggerElement.focus();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async shouldSkipTest(test, page) {
|
|
172
|
-
const requiresSubmenu = test.action.some(
|
|
173
|
-
(act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
|
|
174
|
-
) || test.assertions.some(
|
|
175
|
-
(assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
|
|
176
|
-
);
|
|
177
|
-
if (!requiresSubmenu) {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
const submenuTriggerSelector = this.selectors.submenuTrigger;
|
|
181
|
-
if (!submenuTriggerSelector) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
|
|
185
|
-
return submenuTriggerCount === 0;
|
|
186
|
-
}
|
|
187
|
-
getMainSelector() {
|
|
188
|
-
return this.mainSelector;
|
|
189
98
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.mainSelector = mainSelector;
|
|
196
|
-
this.selectors = selectors;
|
|
197
|
-
}
|
|
198
|
-
async resetState() {
|
|
199
|
-
}
|
|
200
|
-
async shouldSkipTest(test, page) {
|
|
201
|
-
if (test.isVertical !== void 0 && this.selectors.tablist) {
|
|
202
|
-
const tablistSelector = this.selectors.tablist;
|
|
203
|
-
const tablist = page.locator(tablistSelector).first();
|
|
204
|
-
const orientation = await tablist.getAttribute("aria-orientation");
|
|
205
|
-
const isVertical = orientation === "vertical";
|
|
206
|
-
if (test.isVertical !== isVertical) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
getMainSelector() {
|
|
213
|
-
return this.mainSelector;
|
|
99
|
+
/**
|
|
100
|
+
* Check if a strategy exists (either built-in or custom path provided)
|
|
101
|
+
*/
|
|
102
|
+
has(componentName, customStrategyPath) {
|
|
103
|
+
return !!customStrategyPath || this.builtInStrategies.has(componentName);
|
|
214
104
|
}
|
|
215
105
|
};
|
|
216
106
|
|
|
217
107
|
// src/utils/test/src/ComponentDetector.ts
|
|
218
|
-
import { readFileSync } from "fs";
|
|
219
108
|
var ComponentDetector = class {
|
|
220
|
-
static
|
|
221
|
-
|
|
222
|
-
|
|
109
|
+
static strategyRegistry = new StrategyRegistry();
|
|
110
|
+
static isComponentConfig(value) {
|
|
111
|
+
return typeof value === "object" && value !== null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Detect and instantiate a component strategy
|
|
115
|
+
* Supports:
|
|
116
|
+
* - Built-in strategies (menu, accordion, combobox, tabs)
|
|
117
|
+
* - Custom strategies via config (strategyPath)
|
|
118
|
+
* - Custom contract paths via config (path)
|
|
119
|
+
* @param componentName - Component name
|
|
120
|
+
* @param componentConfig - Component config from ariaease.config.js
|
|
121
|
+
* @param actionTimeoutMs - Action timeout in milliseconds
|
|
122
|
+
* @param assertionTimeoutMs - Assertion timeout in milliseconds
|
|
123
|
+
* @returns Instantiated ComponentStrategy or null
|
|
124
|
+
*/
|
|
125
|
+
static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
|
|
126
|
+
const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
|
|
127
|
+
let contractPath = typedComponentConfig?.path;
|
|
128
|
+
if (!contractPath) {
|
|
129
|
+
const contractTyped = contract_default;
|
|
130
|
+
contractPath = contractTyped[componentName]?.path;
|
|
131
|
+
}
|
|
223
132
|
if (!contractPath) {
|
|
224
133
|
throw new Error(`Contract path not found for component: ${componentName}`);
|
|
225
134
|
}
|
|
226
|
-
const resolvedPath =
|
|
135
|
+
const resolvedPath = (() => {
|
|
136
|
+
if (path2.isAbsolute(contractPath)) return contractPath;
|
|
137
|
+
if (configBaseDir) {
|
|
138
|
+
const configResolved = path2.resolve(configBaseDir, contractPath);
|
|
139
|
+
try {
|
|
140
|
+
readFileSync(configResolved, "utf-8");
|
|
141
|
+
return configResolved;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const cwdResolved = path2.resolve(process.cwd(), contractPath);
|
|
146
|
+
try {
|
|
147
|
+
readFileSync(cwdResolved, "utf-8");
|
|
148
|
+
return cwdResolved;
|
|
149
|
+
} catch {
|
|
150
|
+
return new URL(contractPath, import.meta.url).pathname;
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
227
153
|
const contractData = readFileSync(resolvedPath, "utf-8");
|
|
228
154
|
const componentContract = JSON.parse(contractData);
|
|
229
155
|
const selectors = componentContract.selectors;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
if (componentName === "menu") {
|
|
239
|
-
const mainSelector = selectors.trigger || selectors.container;
|
|
240
|
-
return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
|
|
156
|
+
const strategyClass = await this.strategyRegistry.loadStrategy(
|
|
157
|
+
componentName,
|
|
158
|
+
typedComponentConfig?.strategyPath,
|
|
159
|
+
configBaseDir
|
|
160
|
+
);
|
|
161
|
+
if (!strategyClass) {
|
|
162
|
+
return null;
|
|
241
163
|
}
|
|
164
|
+
const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
|
|
242
165
|
if (componentName === "tabs") {
|
|
243
|
-
|
|
244
|
-
return new TabsComponentStrategy(mainSelector, selectors);
|
|
166
|
+
return new strategyClass(mainSelector, selectors);
|
|
245
167
|
}
|
|
246
|
-
return
|
|
168
|
+
return new strategyClass(
|
|
169
|
+
mainSelector,
|
|
170
|
+
selectors,
|
|
171
|
+
actionTimeoutMs,
|
|
172
|
+
assertionTimeoutMs
|
|
173
|
+
);
|
|
247
174
|
}
|
|
248
175
|
};
|
|
249
176
|
|
|
@@ -727,17 +654,42 @@ var AssertionRunner = class {
|
|
|
727
654
|
};
|
|
728
655
|
|
|
729
656
|
// src/utils/test/src/contractTestRunnerPlaywright.ts
|
|
730
|
-
async function runContractTestsPlaywright(componentName, url, strictness) {
|
|
731
|
-
const
|
|
657
|
+
async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
|
|
658
|
+
const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
|
|
659
|
+
const isCustomContract = !!componentConfig?.path;
|
|
660
|
+
const reporter = new ContractReporter(true, isCustomContract);
|
|
732
661
|
const actionTimeoutMs = 400;
|
|
733
662
|
const assertionTimeoutMs = 400;
|
|
734
663
|
const strictnessMode = normalizeStrictness(strictness);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
664
|
+
let contractPath = componentConfig?.path;
|
|
665
|
+
if (!contractPath) {
|
|
666
|
+
const contractTyped = contract_default;
|
|
667
|
+
contractPath = contractTyped[componentName]?.path;
|
|
668
|
+
}
|
|
669
|
+
if (!contractPath) {
|
|
670
|
+
throw new Error(`Contract path not found for component: ${componentName}`);
|
|
671
|
+
}
|
|
672
|
+
const resolvedPath = (() => {
|
|
673
|
+
if (path3.isAbsolute(contractPath)) return contractPath;
|
|
674
|
+
if (configBaseDir) {
|
|
675
|
+
const configResolved = path3.resolve(configBaseDir, contractPath);
|
|
676
|
+
try {
|
|
677
|
+
readFileSync2(configResolved, "utf-8");
|
|
678
|
+
return configResolved;
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const cwdResolved = path3.resolve(process.cwd(), contractPath);
|
|
683
|
+
try {
|
|
684
|
+
readFileSync2(cwdResolved, "utf-8");
|
|
685
|
+
return cwdResolved;
|
|
686
|
+
} catch {
|
|
687
|
+
return new URL(contractPath, import.meta.url).pathname;
|
|
688
|
+
}
|
|
689
|
+
})();
|
|
738
690
|
const contractData = readFileSync2(resolvedPath, "utf-8");
|
|
739
691
|
const componentContract = JSON.parse(contractData);
|
|
740
|
-
const totalTests = componentContract.static[0]
|
|
692
|
+
const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
|
|
741
693
|
const apgUrl = componentContract.meta?.source?.apg;
|
|
742
694
|
const failures = [];
|
|
743
695
|
const warnings = [];
|
|
@@ -774,7 +726,7 @@ async function runContractTestsPlaywright(componentName, url, strictness) {
|
|
|
774
726
|
}
|
|
775
727
|
await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
|
|
776
728
|
}
|
|
777
|
-
const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
|
|
729
|
+
const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
|
|
778
730
|
if (!strategy) {
|
|
779
731
|
throw new Error(`Unsupported component: ${componentName}`);
|
|
780
732
|
}
|
|
@@ -807,6 +759,105 @@ This usually means:
|
|
|
807
759
|
let staticPassed = 0;
|
|
808
760
|
let staticFailed = 0;
|
|
809
761
|
let staticWarnings = 0;
|
|
762
|
+
for (const rel of componentContract.relationships || []) {
|
|
763
|
+
const relationshipLevel = normalizeLevel(rel.level);
|
|
764
|
+
if (rel.type === "aria-reference") {
|
|
765
|
+
const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
|
|
766
|
+
const fromSelector = componentContract.selectors[rel.from];
|
|
767
|
+
const toSelector = componentContract.selectors[rel.to];
|
|
768
|
+
if (!fromSelector || !toSelector) {
|
|
769
|
+
const outcome = classifyFailure(
|
|
770
|
+
`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
|
|
771
|
+
rel.level
|
|
772
|
+
);
|
|
773
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
774
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
775
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const fromTarget = page.locator(fromSelector).first();
|
|
779
|
+
const toTarget = page.locator(toSelector).first();
|
|
780
|
+
const fromExists = await fromTarget.count() > 0;
|
|
781
|
+
const toExists = await toTarget.count() > 0;
|
|
782
|
+
if (!fromExists || !toExists) {
|
|
783
|
+
const outcome = classifyFailure(
|
|
784
|
+
`Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
|
|
785
|
+
rel.level
|
|
786
|
+
);
|
|
787
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
788
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
789
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const attrValue = await fromTarget.getAttribute(rel.attribute);
|
|
793
|
+
const toId = await toTarget.getAttribute("id");
|
|
794
|
+
if (!toId) {
|
|
795
|
+
const outcome = classifyFailure(
|
|
796
|
+
`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
|
|
797
|
+
rel.level
|
|
798
|
+
);
|
|
799
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
800
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
801
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const references = (attrValue || "").split(/\s+/).filter(Boolean);
|
|
805
|
+
const matches = references.includes(toId);
|
|
806
|
+
if (!matches) {
|
|
807
|
+
const outcome = classifyFailure(
|
|
808
|
+
`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
|
|
809
|
+
rel.level
|
|
810
|
+
);
|
|
811
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
812
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
813
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
|
|
817
|
+
staticPassed += 1;
|
|
818
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (rel.type === "contains") {
|
|
822
|
+
const relDescription = `${rel.parent} contains ${rel.child}`;
|
|
823
|
+
const parentSelector = componentContract.selectors[rel.parent];
|
|
824
|
+
const childSelector = componentContract.selectors[rel.child];
|
|
825
|
+
if (!parentSelector || !childSelector) {
|
|
826
|
+
const outcome = classifyFailure(
|
|
827
|
+
`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
|
|
828
|
+
rel.level
|
|
829
|
+
);
|
|
830
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
831
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
832
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const parent = page.locator(parentSelector).first();
|
|
836
|
+
const parentExists = await parent.count() > 0;
|
|
837
|
+
if (!parentExists) {
|
|
838
|
+
const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
|
|
839
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
840
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
841
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const descendants = parent.locator(childSelector);
|
|
845
|
+
const descendantCount = await descendants.count();
|
|
846
|
+
if (descendantCount < 1) {
|
|
847
|
+
const outcome = classifyFailure(
|
|
848
|
+
`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
|
|
849
|
+
rel.level
|
|
850
|
+
);
|
|
851
|
+
if (outcome.status === "fail") staticFailed += 1;
|
|
852
|
+
if (outcome.status === "warn") staticWarnings += 1;
|
|
853
|
+
reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
|
|
857
|
+
staticPassed += 1;
|
|
858
|
+
reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
810
861
|
const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
|
|
811
862
|
for (const test of componentContract.static[0]?.assertions || []) {
|
|
812
863
|
if (test.target === "relative") continue;
|