aria-ease 6.9.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/dist/index.cjs +14 -14
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- 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/package.json +7 -6
package/dist/index.cjs
CHANGED
|
@@ -487,14 +487,14 @@ function validateConfig(config) {
|
|
|
487
487
|
if (!Array.isArray(cfg.contracts)) {
|
|
488
488
|
errors.push("contracts must be an array");
|
|
489
489
|
} else {
|
|
490
|
-
cfg.contracts.forEach((
|
|
491
|
-
if (typeof
|
|
490
|
+
cfg.contracts.forEach((contract, idx) => {
|
|
491
|
+
if (typeof contract !== "object" || contract === null) {
|
|
492
492
|
errors.push(`contracts[${idx}] must be an object`);
|
|
493
493
|
} else {
|
|
494
|
-
if (typeof
|
|
494
|
+
if (typeof contract.src !== "string") {
|
|
495
495
|
errors.push(`contracts[${idx}].src is required and must be a string`);
|
|
496
496
|
}
|
|
497
|
-
if (
|
|
497
|
+
if (contract.out !== void 0 && typeof contract.out !== "string") {
|
|
498
498
|
errors.push(`contracts[${idx}].out must be a string`);
|
|
499
499
|
}
|
|
500
500
|
}
|
|
@@ -2060,7 +2060,7 @@ var init_badgeHelper = __esm({
|
|
|
2060
2060
|
var index_exports = {};
|
|
2061
2061
|
__export(index_exports, {
|
|
2062
2062
|
cleanupTests: () => cleanupTests,
|
|
2063
|
-
|
|
2063
|
+
createContract: () => createContract,
|
|
2064
2064
|
makeAccordionAccessible: () => makeAccordionAccessible,
|
|
2065
2065
|
makeBlockAccessible: () => makeBlockAccessible,
|
|
2066
2066
|
makeCheckboxAccessible: () => makeCheckboxAccessible,
|
|
@@ -3870,7 +3870,7 @@ var ContractBuilder = class {
|
|
|
3870
3870
|
};
|
|
3871
3871
|
}
|
|
3872
3872
|
};
|
|
3873
|
-
function
|
|
3873
|
+
function createContract(componentName, define) {
|
|
3874
3874
|
const builder = new ContractBuilder(componentName);
|
|
3875
3875
|
define(builder);
|
|
3876
3876
|
return new FluentContract(builder.build());
|
|
@@ -4112,14 +4112,14 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
4112
4112
|
}
|
|
4113
4113
|
}
|
|
4114
4114
|
}
|
|
4115
|
-
let
|
|
4115
|
+
let contract;
|
|
4116
4116
|
try {
|
|
4117
4117
|
if (url) {
|
|
4118
4118
|
const devServerUrl = await checkDevServer(url);
|
|
4119
4119
|
if (devServerUrl) {
|
|
4120
4120
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
4121
4121
|
const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
|
|
4122
|
-
|
|
4122
|
+
contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness, config, configBaseDir);
|
|
4123
4123
|
} else {
|
|
4124
4124
|
throw new Error(
|
|
4125
4125
|
`\u274C Dev server not running at ${url}
|
|
@@ -4128,7 +4128,7 @@ Please start your dev server and try again.`
|
|
|
4128
4128
|
}
|
|
4129
4129
|
} else if (component) {
|
|
4130
4130
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
4131
|
-
|
|
4131
|
+
contract = await runContractTests(componentName, component, strictness);
|
|
4132
4132
|
} else {
|
|
4133
4133
|
throw new Error("\u274C Either component or URL must be provided");
|
|
4134
4134
|
}
|
|
@@ -4141,13 +4141,13 @@ Please start your dev server and try again.`
|
|
|
4141
4141
|
const result = {
|
|
4142
4142
|
violations: results.violations,
|
|
4143
4143
|
raw: results,
|
|
4144
|
-
contract
|
|
4144
|
+
contract
|
|
4145
4145
|
};
|
|
4146
|
-
if (
|
|
4146
|
+
if (contract.failures.length > 0 && url === "Playwright") {
|
|
4147
4147
|
throw new Error(
|
|
4148
4148
|
`
|
|
4149
|
-
\u274C ${
|
|
4150
|
-
\u2705 ${
|
|
4149
|
+
\u274C ${contract.failures.length} accessibility contract test${contract.failures.length > 1 ? "s" : ""} failed (Playwright mode)
|
|
4150
|
+
\u2705 ${contract.passes.length} test${contract.passes.length > 1 ? "s" : ""} passed
|
|
4151
4151
|
|
|
4152
4152
|
\u{1F4CB} Review the detailed test report above for specific failures.
|
|
4153
4153
|
\u{1F4A1} Contract tests validate ARIA attributes and keyboard interactions per W3C APG guidelines.`
|
|
@@ -4223,7 +4223,7 @@ async function cleanupTests() {
|
|
|
4223
4223
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4224
4224
|
0 && (module.exports = {
|
|
4225
4225
|
cleanupTests,
|
|
4226
|
-
|
|
4226
|
+
createContract,
|
|
4227
4227
|
makeAccordionAccessible,
|
|
4228
4228
|
makeBlockAccessible,
|
|
4229
4229
|
makeCheckboxAccessible,
|
package/dist/index.d.cts
CHANGED
|
@@ -327,7 +327,7 @@ declare class ContractBuilder {
|
|
|
327
327
|
private validateDynamicTargets;
|
|
328
328
|
build(): JsonContract;
|
|
329
329
|
}
|
|
330
|
-
declare function
|
|
330
|
+
declare function createContract(componentName: string, define: (c: ContractBuilder) => void): FluentContract;
|
|
331
331
|
|
|
332
332
|
type StrictnessMode = 'minimal' | 'balanced' | 'strict' | 'paranoid';
|
|
333
333
|
|
|
@@ -348,4 +348,4 @@ declare function testUiComponent(componentName: string, component: HTMLElement |
|
|
|
348
348
|
*/
|
|
349
349
|
declare function cleanupTests(): Promise<void>;
|
|
350
350
|
|
|
351
|
-
export { ContractBuilder, type JsonContract, type RelationshipInvariant, cleanupTests,
|
|
351
|
+
export { ContractBuilder, type JsonContract, type RelationshipInvariant, cleanupTests, createContract, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeTabsAccessible, makeToggleAccessible, testUiComponent };
|
package/dist/index.d.ts
CHANGED
|
@@ -327,7 +327,7 @@ declare class ContractBuilder {
|
|
|
327
327
|
private validateDynamicTargets;
|
|
328
328
|
build(): JsonContract;
|
|
329
329
|
}
|
|
330
|
-
declare function
|
|
330
|
+
declare function createContract(componentName: string, define: (c: ContractBuilder) => void): FluentContract;
|
|
331
331
|
|
|
332
332
|
type StrictnessMode = 'minimal' | 'balanced' | 'strict' | 'paranoid';
|
|
333
333
|
|
|
@@ -348,4 +348,4 @@ declare function testUiComponent(componentName: string, component: HTMLElement |
|
|
|
348
348
|
*/
|
|
349
349
|
declare function cleanupTests(): Promise<void>;
|
|
350
350
|
|
|
351
|
-
export { ContractBuilder, type JsonContract, type RelationshipInvariant, cleanupTests,
|
|
351
|
+
export { ContractBuilder, type JsonContract, type RelationshipInvariant, cleanupTests, createContract, makeAccordionAccessible, makeBlockAccessible, makeCheckboxAccessible, makeComboboxAccessible, makeMenuAccessible, makeRadioAccessible, makeTabsAccessible, makeToggleAccessible, testUiComponent };
|
package/dist/index.js
CHANGED
|
@@ -1805,7 +1805,7 @@ var ContractBuilder = class {
|
|
|
1805
1805
|
};
|
|
1806
1806
|
}
|
|
1807
1807
|
};
|
|
1808
|
-
function
|
|
1808
|
+
function createContract(componentName, define) {
|
|
1809
1809
|
const builder = new ContractBuilder(componentName);
|
|
1810
1810
|
define(builder);
|
|
1811
1811
|
return new FluentContract(builder.build());
|
|
@@ -2041,14 +2041,14 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
2041
2041
|
}
|
|
2042
2042
|
}
|
|
2043
2043
|
}
|
|
2044
|
-
let
|
|
2044
|
+
let contract;
|
|
2045
2045
|
try {
|
|
2046
2046
|
if (url) {
|
|
2047
2047
|
const devServerUrl = await checkDevServer(url);
|
|
2048
2048
|
if (devServerUrl) {
|
|
2049
2049
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
2050
2050
|
const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-XBWJZMR3.js");
|
|
2051
|
-
|
|
2051
|
+
contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
|
|
2052
2052
|
} else {
|
|
2053
2053
|
throw new Error(
|
|
2054
2054
|
`\u274C Dev server not running at ${url}
|
|
@@ -2057,7 +2057,7 @@ Please start your dev server and try again.`
|
|
|
2057
2057
|
}
|
|
2058
2058
|
} else if (component) {
|
|
2059
2059
|
console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
|
|
2060
|
-
|
|
2060
|
+
contract = await runContractTests(componentName, component, strictness);
|
|
2061
2061
|
} else {
|
|
2062
2062
|
throw new Error("\u274C Either component or URL must be provided");
|
|
2063
2063
|
}
|
|
@@ -2070,13 +2070,13 @@ Please start your dev server and try again.`
|
|
|
2070
2070
|
const result = {
|
|
2071
2071
|
violations: results.violations,
|
|
2072
2072
|
raw: results,
|
|
2073
|
-
contract
|
|
2073
|
+
contract
|
|
2074
2074
|
};
|
|
2075
|
-
if (
|
|
2075
|
+
if (contract.failures.length > 0 && url === "Playwright") {
|
|
2076
2076
|
throw new Error(
|
|
2077
2077
|
`
|
|
2078
|
-
\u274C ${
|
|
2079
|
-
\u2705 ${
|
|
2078
|
+
\u274C ${contract.failures.length} accessibility contract test${contract.failures.length > 1 ? "s" : ""} failed (Playwright mode)
|
|
2079
|
+
\u2705 ${contract.passes.length} test${contract.passes.length > 1 ? "s" : ""} passed
|
|
2080
2080
|
|
|
2081
2081
|
\u{1F4CB} Review the detailed test report above for specific failures.
|
|
2082
2082
|
\u{1F4A1} Contract tests validate ARIA attributes and keyboard interactions per W3C APG guidelines.`
|
|
@@ -2151,7 +2151,7 @@ async function cleanupTests() {
|
|
|
2151
2151
|
}
|
|
2152
2152
|
export {
|
|
2153
2153
|
cleanupTests,
|
|
2154
|
-
|
|
2154
|
+
createContract,
|
|
2155
2155
|
makeAccordionAccessible,
|
|
2156
2156
|
makeBlockAccessible,
|
|
2157
2157
|
makeCheckboxAccessible,
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/test/dsl/index.ts
|
|
4
|
+
var FluentContract = class {
|
|
5
|
+
constructor(jsonContract) {
|
|
6
|
+
this.jsonContract = jsonContract;
|
|
7
|
+
}
|
|
8
|
+
toJSON() {
|
|
9
|
+
return this.jsonContract;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var StaticTargetBuilder = class {
|
|
13
|
+
constructor(targetName, sink) {
|
|
14
|
+
this.targetName = targetName;
|
|
15
|
+
this.sink = sink;
|
|
16
|
+
}
|
|
17
|
+
has(attribute, expectedValue) {
|
|
18
|
+
const create = (level) => {
|
|
19
|
+
this.sink.push({
|
|
20
|
+
target: this.targetName,
|
|
21
|
+
attribute,
|
|
22
|
+
expectedValue,
|
|
23
|
+
failureMessage: `Expected ${this.targetName} to have ${attribute}${expectedValue !== void 0 ? `=${expectedValue}` : ""}.`,
|
|
24
|
+
level
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
required: () => create("required"),
|
|
29
|
+
recommended: () => create("recommended"),
|
|
30
|
+
optional: () => create("optional")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var StaticBuilder = class {
|
|
35
|
+
constructor(sink) {
|
|
36
|
+
this.sink = sink;
|
|
37
|
+
}
|
|
38
|
+
target(targetName) {
|
|
39
|
+
return new StaticTargetBuilder(targetName, this.sink);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var DynamicChain = class {
|
|
43
|
+
constructor(key, testsSink, selectors) {
|
|
44
|
+
this.key = key;
|
|
45
|
+
this.testsSink = testsSink;
|
|
46
|
+
this.selectors = selectors;
|
|
47
|
+
}
|
|
48
|
+
selectorTarget = "";
|
|
49
|
+
actions = [];
|
|
50
|
+
assertions = [];
|
|
51
|
+
explicitDescription = "";
|
|
52
|
+
on(target) {
|
|
53
|
+
this.selectorTarget = target;
|
|
54
|
+
this.actions.push({ type: "keypress", target, key: this.key });
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
describe(description) {
|
|
58
|
+
this.explicitDescription = description;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
focus(targetExpression) {
|
|
62
|
+
const parsed = this.parseRelativeExpression(targetExpression);
|
|
63
|
+
if (parsed) {
|
|
64
|
+
if (!this.selectors[parsed.selectorKey]) {
|
|
65
|
+
const availableSelectors = Object.keys(this.selectors).sort().join(", ") || "(none)";
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Invalid focus target expression "${targetExpression}": selector "${parsed.selectorKey}" is not defined. Available selectors: ${availableSelectors}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!this.selectors.relative && this.selectors[parsed.selectorKey]) {
|
|
71
|
+
this.selectors.relative = this.selectors[parsed.selectorKey];
|
|
72
|
+
}
|
|
73
|
+
this.assertions.push({
|
|
74
|
+
target: "relative",
|
|
75
|
+
assertion: "toHaveFocus",
|
|
76
|
+
relativeTarget: parsed.relativeTarget
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
this.assertions.push({
|
|
80
|
+
target: targetExpression,
|
|
81
|
+
assertion: "toHaveFocus"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
visible(target) {
|
|
87
|
+
this.assertions.push({ target, assertion: "toBeVisible" });
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
hidden(target) {
|
|
91
|
+
this.assertions.push({ target, assertion: "notToBeVisible" });
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
has(target, attribute, expectedValue) {
|
|
95
|
+
this.assertions.push({
|
|
96
|
+
target,
|
|
97
|
+
assertion: "toHaveAttribute",
|
|
98
|
+
attribute,
|
|
99
|
+
expectedValue
|
|
100
|
+
});
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
required() {
|
|
104
|
+
this.finalize("required");
|
|
105
|
+
}
|
|
106
|
+
recommended() {
|
|
107
|
+
this.finalize("recommended");
|
|
108
|
+
}
|
|
109
|
+
optional() {
|
|
110
|
+
this.finalize("optional");
|
|
111
|
+
}
|
|
112
|
+
finalize(level) {
|
|
113
|
+
if (!this.selectorTarget) {
|
|
114
|
+
throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
|
|
115
|
+
}
|
|
116
|
+
const description = this.explicitDescription || `Pressing ${this.key} on ${this.selectorTarget} satisfies expected behavior.`;
|
|
117
|
+
this.testsSink.push({
|
|
118
|
+
description,
|
|
119
|
+
level,
|
|
120
|
+
action: this.actions,
|
|
121
|
+
assertions: this.assertions.map((a) => ({ ...a, level }))
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
parseRelativeExpression(input) {
|
|
125
|
+
const match = input.match(/^(next|previous|first|last)\(([^)]+)\)$/);
|
|
126
|
+
if (!match) return null;
|
|
127
|
+
const relativeTarget = match[1];
|
|
128
|
+
const selectorKey = match[2].trim();
|
|
129
|
+
return { relativeTarget, selectorKey };
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var ContractBuilder = class {
|
|
133
|
+
constructor(componentName) {
|
|
134
|
+
this.componentName = componentName;
|
|
135
|
+
}
|
|
136
|
+
metaValue = {};
|
|
137
|
+
selectorsValue = {};
|
|
138
|
+
relationshipInvariants = [];
|
|
139
|
+
staticAssertions = [];
|
|
140
|
+
dynamicTests = [];
|
|
141
|
+
meta(meta) {
|
|
142
|
+
this.metaValue = { ...this.metaValue, ...meta };
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
selectors(selectors) {
|
|
146
|
+
this.selectorsValue = { ...this.selectorsValue, ...selectors };
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
relationship(invariant) {
|
|
150
|
+
this.relationshipInvariants.push(invariant);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
relationships(builderFn) {
|
|
154
|
+
builderFn({
|
|
155
|
+
ariaReference: (from, attribute, to) => {
|
|
156
|
+
const create = (level) => {
|
|
157
|
+
this.relationshipInvariants.push({
|
|
158
|
+
type: "aria-reference",
|
|
159
|
+
from,
|
|
160
|
+
attribute,
|
|
161
|
+
to,
|
|
162
|
+
level
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
required: () => create("required"),
|
|
167
|
+
recommended: () => create("recommended"),
|
|
168
|
+
optional: () => create("optional")
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
contains: (parent, child) => {
|
|
172
|
+
const create = (level) => {
|
|
173
|
+
this.relationshipInvariants.push({
|
|
174
|
+
type: "contains",
|
|
175
|
+
parent,
|
|
176
|
+
child,
|
|
177
|
+
level
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
required: () => create("required"),
|
|
182
|
+
recommended: () => create("recommended"),
|
|
183
|
+
optional: () => create("optional")
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
static(builderFn) {
|
|
190
|
+
builderFn(new StaticBuilder(this.staticAssertions));
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
when(key) {
|
|
194
|
+
return new DynamicChain(key, this.dynamicTests, this.selectorsValue);
|
|
195
|
+
}
|
|
196
|
+
validateRelationshipInvariants() {
|
|
197
|
+
if (this.relationshipInvariants.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
201
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ");
|
|
202
|
+
const errors = [];
|
|
203
|
+
this.relationshipInvariants.forEach((invariant, index) => {
|
|
204
|
+
const prefix = `relationships[${index}] (${invariant.type})`;
|
|
205
|
+
if (invariant.type === "aria-reference") {
|
|
206
|
+
if (!selectorKeys.has(invariant.from)) {
|
|
207
|
+
errors.push(`${prefix}: "from" references unknown selector "${invariant.from}"`);
|
|
208
|
+
}
|
|
209
|
+
if (!selectorKeys.has(invariant.to)) {
|
|
210
|
+
errors.push(`${prefix}: "to" references unknown selector "${invariant.to}"`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (invariant.type === "contains") {
|
|
214
|
+
if (!selectorKeys.has(invariant.parent)) {
|
|
215
|
+
errors.push(`${prefix}: "parent" references unknown selector "${invariant.parent}"`);
|
|
216
|
+
}
|
|
217
|
+
if (!selectorKeys.has(invariant.child)) {
|
|
218
|
+
errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
if (errors.length > 0) {
|
|
223
|
+
const availableSelectorsMessage = available.length > 0 ? available : "(none)";
|
|
224
|
+
throw new Error(
|
|
225
|
+
[
|
|
226
|
+
`Contract invariant validation failed for component "${this.componentName}".`,
|
|
227
|
+
...errors.map((error) => `- ${error}`),
|
|
228
|
+
`Available selectors: ${availableSelectorsMessage}`
|
|
229
|
+
].join("\n")
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
validateStaticTargets() {
|
|
234
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
235
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
236
|
+
const errors = [];
|
|
237
|
+
this.staticAssertions.forEach((assertion, index) => {
|
|
238
|
+
if (!selectorKeys.has(assertion.target)) {
|
|
239
|
+
errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
if (errors.length > 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
[
|
|
245
|
+
`Contract static target validation failed for component "${this.componentName}".`,
|
|
246
|
+
...errors.map((error) => `- ${error}`),
|
|
247
|
+
`Available selectors: ${available}`
|
|
248
|
+
].join("\n")
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
validateDynamicTargets() {
|
|
253
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
254
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
255
|
+
const errors = [];
|
|
256
|
+
const isValidActionTarget = (target) => {
|
|
257
|
+
return selectorKeys.has(target) || target === "document" || target === "relative";
|
|
258
|
+
};
|
|
259
|
+
const isValidAssertionTarget = (target) => {
|
|
260
|
+
return selectorKeys.has(target) || target === "relative";
|
|
261
|
+
};
|
|
262
|
+
this.dynamicTests.forEach((test, testIndex) => {
|
|
263
|
+
test.action.forEach((action, actionIndex) => {
|
|
264
|
+
if (!isValidActionTarget(action.target)) {
|
|
265
|
+
errors.push(
|
|
266
|
+
`dynamic[${testIndex}].action[${actionIndex}]: target "${action.target}" is not defined in selectors`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
test.assertions.forEach((assertion, assertionIndex) => {
|
|
271
|
+
if (!isValidAssertionTarget(assertion.target)) {
|
|
272
|
+
errors.push(
|
|
273
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (assertion.target === "relative" && !this.selectorsValue.relative) {
|
|
277
|
+
errors.push(
|
|
278
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "relative" requires selectors.relative to be defined`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
if (errors.length > 0) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
[
|
|
286
|
+
`Contract dynamic target validation failed for component "${this.componentName}".`,
|
|
287
|
+
...errors.map((error) => `- ${error}`),
|
|
288
|
+
`Available selectors: ${available}`,
|
|
289
|
+
`Allowed special targets: document, relative`
|
|
290
|
+
].join("\n")
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
build() {
|
|
295
|
+
this.validateRelationshipInvariants();
|
|
296
|
+
this.validateStaticTargets();
|
|
297
|
+
this.validateDynamicTargets();
|
|
298
|
+
const fallbackId = this.metaValue.id || `aria-ease.contract.${this.componentName}`;
|
|
299
|
+
return {
|
|
300
|
+
meta: {
|
|
301
|
+
id: fallbackId,
|
|
302
|
+
version: this.metaValue.version || "1.0.0",
|
|
303
|
+
description: this.metaValue.description || `Fluent contract for ${this.componentName}`,
|
|
304
|
+
source: this.metaValue.source,
|
|
305
|
+
W3CName: this.metaValue.W3CName
|
|
306
|
+
},
|
|
307
|
+
selectors: this.selectorsValue,
|
|
308
|
+
relationships: this.relationshipInvariants,
|
|
309
|
+
static: [{ assertions: this.staticAssertions }],
|
|
310
|
+
dynamic: this.dynamicTests
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
function createContract(componentName, define) {
|
|
315
|
+
const builder = new ContractBuilder(componentName);
|
|
316
|
+
define(builder);
|
|
317
|
+
return new FluentContract(builder.build());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
exports.createContract = createContract;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
type Level = "required" | "recommended" | "optional";
|
|
2
|
+
type ContractMeta = {
|
|
3
|
+
id?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
source?: {
|
|
7
|
+
apg?: string;
|
|
8
|
+
wcag?: string[];
|
|
9
|
+
};
|
|
10
|
+
W3CName?: string;
|
|
11
|
+
};
|
|
12
|
+
type SelectorsMap = Record<string, string>;
|
|
13
|
+
type RelationshipInvariant = {
|
|
14
|
+
type: "aria-reference";
|
|
15
|
+
from: string;
|
|
16
|
+
attribute: string;
|
|
17
|
+
to: string;
|
|
18
|
+
level?: Level;
|
|
19
|
+
} | {
|
|
20
|
+
type: "contains";
|
|
21
|
+
parent: string;
|
|
22
|
+
child: string;
|
|
23
|
+
level?: Level;
|
|
24
|
+
};
|
|
25
|
+
type StaticAssertion = {
|
|
26
|
+
target: string;
|
|
27
|
+
attribute: string;
|
|
28
|
+
expectedValue?: string;
|
|
29
|
+
failureMessage: string;
|
|
30
|
+
level: Level;
|
|
31
|
+
};
|
|
32
|
+
type DynamicAction = {
|
|
33
|
+
type: "focus" | "type" | "click" | "keypress" | "hover";
|
|
34
|
+
target: string;
|
|
35
|
+
key?: string;
|
|
36
|
+
value?: string;
|
|
37
|
+
relativeTarget?: string;
|
|
38
|
+
};
|
|
39
|
+
type DynamicAssertion = {
|
|
40
|
+
target: string;
|
|
41
|
+
assertion: "toBeVisible" | "notToBeVisible" | "toHaveAttribute" | "toHaveValue" | "toHaveFocus" | "toHaveRole";
|
|
42
|
+
attribute?: string;
|
|
43
|
+
expectedValue?: string;
|
|
44
|
+
failureMessage?: string;
|
|
45
|
+
relativeTarget?: string;
|
|
46
|
+
level?: Level;
|
|
47
|
+
};
|
|
48
|
+
type DynamicTest = {
|
|
49
|
+
description: string;
|
|
50
|
+
level?: Level;
|
|
51
|
+
action: DynamicAction[];
|
|
52
|
+
assertions: DynamicAssertion[];
|
|
53
|
+
};
|
|
54
|
+
type JsonContract = {
|
|
55
|
+
meta?: ContractMeta;
|
|
56
|
+
selectors: SelectorsMap;
|
|
57
|
+
relationships?: RelationshipInvariant[];
|
|
58
|
+
static: Array<{
|
|
59
|
+
assertions: StaticAssertion[];
|
|
60
|
+
}>;
|
|
61
|
+
dynamic: DynamicTest[];
|
|
62
|
+
};
|
|
63
|
+
declare class FluentContract {
|
|
64
|
+
private readonly jsonContract;
|
|
65
|
+
constructor(jsonContract: JsonContract);
|
|
66
|
+
toJSON(): JsonContract;
|
|
67
|
+
}
|
|
68
|
+
declare class StaticTargetBuilder {
|
|
69
|
+
private readonly targetName;
|
|
70
|
+
private readonly sink;
|
|
71
|
+
constructor(targetName: string, sink: StaticAssertion[]);
|
|
72
|
+
has(attribute: string, expectedValue?: string): {
|
|
73
|
+
required: () => void;
|
|
74
|
+
recommended: () => void;
|
|
75
|
+
optional: () => void;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare class StaticBuilder {
|
|
79
|
+
private readonly sink;
|
|
80
|
+
constructor(sink: StaticAssertion[]);
|
|
81
|
+
target(targetName: string): StaticTargetBuilder;
|
|
82
|
+
}
|
|
83
|
+
declare class DynamicChain {
|
|
84
|
+
private readonly key;
|
|
85
|
+
private readonly testsSink;
|
|
86
|
+
private readonly selectors;
|
|
87
|
+
private selectorTarget;
|
|
88
|
+
private readonly actions;
|
|
89
|
+
private readonly assertions;
|
|
90
|
+
private explicitDescription;
|
|
91
|
+
constructor(key: string, testsSink: DynamicTest[], selectors: SelectorsMap);
|
|
92
|
+
on(target: string): this;
|
|
93
|
+
describe(description: string): this;
|
|
94
|
+
focus(targetExpression: string): this;
|
|
95
|
+
visible(target: string): this;
|
|
96
|
+
hidden(target: string): this;
|
|
97
|
+
has(target: string, attribute: string, expectedValue?: string): this;
|
|
98
|
+
required(): void;
|
|
99
|
+
recommended(): void;
|
|
100
|
+
optional(): void;
|
|
101
|
+
private finalize;
|
|
102
|
+
private parseRelativeExpression;
|
|
103
|
+
}
|
|
104
|
+
declare class ContractBuilder {
|
|
105
|
+
private readonly componentName;
|
|
106
|
+
private metaValue;
|
|
107
|
+
private selectorsValue;
|
|
108
|
+
private readonly relationshipInvariants;
|
|
109
|
+
private readonly staticAssertions;
|
|
110
|
+
private readonly dynamicTests;
|
|
111
|
+
constructor(componentName: string);
|
|
112
|
+
meta(meta: ContractMeta): this;
|
|
113
|
+
selectors(selectors: SelectorsMap): this;
|
|
114
|
+
relationship(invariant: RelationshipInvariant): this;
|
|
115
|
+
relationships(builderFn: (r: {
|
|
116
|
+
ariaReference: (from: string, attribute: string, to: string) => {
|
|
117
|
+
required: () => void;
|
|
118
|
+
recommended: () => void;
|
|
119
|
+
optional: () => void;
|
|
120
|
+
};
|
|
121
|
+
contains: (parent: string, child: string) => {
|
|
122
|
+
required: () => void;
|
|
123
|
+
recommended: () => void;
|
|
124
|
+
optional: () => void;
|
|
125
|
+
};
|
|
126
|
+
}) => void): this;
|
|
127
|
+
static(builderFn: (s: StaticBuilder) => void): this;
|
|
128
|
+
when(key: string): DynamicChain;
|
|
129
|
+
private validateRelationshipInvariants;
|
|
130
|
+
private validateStaticTargets;
|
|
131
|
+
private validateDynamicTargets;
|
|
132
|
+
build(): JsonContract;
|
|
133
|
+
}
|
|
134
|
+
declare function createContract(componentName: string, define: (c: ContractBuilder) => void): FluentContract;
|
|
135
|
+
|
|
136
|
+
export { ContractBuilder, type JsonContract, type RelationshipInvariant, createContract };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
type Level = "required" | "recommended" | "optional";
|
|
2
|
+
type ContractMeta = {
|
|
3
|
+
id?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
source?: {
|
|
7
|
+
apg?: string;
|
|
8
|
+
wcag?: string[];
|
|
9
|
+
};
|
|
10
|
+
W3CName?: string;
|
|
11
|
+
};
|
|
12
|
+
type SelectorsMap = Record<string, string>;
|
|
13
|
+
type RelationshipInvariant = {
|
|
14
|
+
type: "aria-reference";
|
|
15
|
+
from: string;
|
|
16
|
+
attribute: string;
|
|
17
|
+
to: string;
|
|
18
|
+
level?: Level;
|
|
19
|
+
} | {
|
|
20
|
+
type: "contains";
|
|
21
|
+
parent: string;
|
|
22
|
+
child: string;
|
|
23
|
+
level?: Level;
|
|
24
|
+
};
|
|
25
|
+
type StaticAssertion = {
|
|
26
|
+
target: string;
|
|
27
|
+
attribute: string;
|
|
28
|
+
expectedValue?: string;
|
|
29
|
+
failureMessage: string;
|
|
30
|
+
level: Level;
|
|
31
|
+
};
|
|
32
|
+
type DynamicAction = {
|
|
33
|
+
type: "focus" | "type" | "click" | "keypress" | "hover";
|
|
34
|
+
target: string;
|
|
35
|
+
key?: string;
|
|
36
|
+
value?: string;
|
|
37
|
+
relativeTarget?: string;
|
|
38
|
+
};
|
|
39
|
+
type DynamicAssertion = {
|
|
40
|
+
target: string;
|
|
41
|
+
assertion: "toBeVisible" | "notToBeVisible" | "toHaveAttribute" | "toHaveValue" | "toHaveFocus" | "toHaveRole";
|
|
42
|
+
attribute?: string;
|
|
43
|
+
expectedValue?: string;
|
|
44
|
+
failureMessage?: string;
|
|
45
|
+
relativeTarget?: string;
|
|
46
|
+
level?: Level;
|
|
47
|
+
};
|
|
48
|
+
type DynamicTest = {
|
|
49
|
+
description: string;
|
|
50
|
+
level?: Level;
|
|
51
|
+
action: DynamicAction[];
|
|
52
|
+
assertions: DynamicAssertion[];
|
|
53
|
+
};
|
|
54
|
+
type JsonContract = {
|
|
55
|
+
meta?: ContractMeta;
|
|
56
|
+
selectors: SelectorsMap;
|
|
57
|
+
relationships?: RelationshipInvariant[];
|
|
58
|
+
static: Array<{
|
|
59
|
+
assertions: StaticAssertion[];
|
|
60
|
+
}>;
|
|
61
|
+
dynamic: DynamicTest[];
|
|
62
|
+
};
|
|
63
|
+
declare class FluentContract {
|
|
64
|
+
private readonly jsonContract;
|
|
65
|
+
constructor(jsonContract: JsonContract);
|
|
66
|
+
toJSON(): JsonContract;
|
|
67
|
+
}
|
|
68
|
+
declare class StaticTargetBuilder {
|
|
69
|
+
private readonly targetName;
|
|
70
|
+
private readonly sink;
|
|
71
|
+
constructor(targetName: string, sink: StaticAssertion[]);
|
|
72
|
+
has(attribute: string, expectedValue?: string): {
|
|
73
|
+
required: () => void;
|
|
74
|
+
recommended: () => void;
|
|
75
|
+
optional: () => void;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
declare class StaticBuilder {
|
|
79
|
+
private readonly sink;
|
|
80
|
+
constructor(sink: StaticAssertion[]);
|
|
81
|
+
target(targetName: string): StaticTargetBuilder;
|
|
82
|
+
}
|
|
83
|
+
declare class DynamicChain {
|
|
84
|
+
private readonly key;
|
|
85
|
+
private readonly testsSink;
|
|
86
|
+
private readonly selectors;
|
|
87
|
+
private selectorTarget;
|
|
88
|
+
private readonly actions;
|
|
89
|
+
private readonly assertions;
|
|
90
|
+
private explicitDescription;
|
|
91
|
+
constructor(key: string, testsSink: DynamicTest[], selectors: SelectorsMap);
|
|
92
|
+
on(target: string): this;
|
|
93
|
+
describe(description: string): this;
|
|
94
|
+
focus(targetExpression: string): this;
|
|
95
|
+
visible(target: string): this;
|
|
96
|
+
hidden(target: string): this;
|
|
97
|
+
has(target: string, attribute: string, expectedValue?: string): this;
|
|
98
|
+
required(): void;
|
|
99
|
+
recommended(): void;
|
|
100
|
+
optional(): void;
|
|
101
|
+
private finalize;
|
|
102
|
+
private parseRelativeExpression;
|
|
103
|
+
}
|
|
104
|
+
declare class ContractBuilder {
|
|
105
|
+
private readonly componentName;
|
|
106
|
+
private metaValue;
|
|
107
|
+
private selectorsValue;
|
|
108
|
+
private readonly relationshipInvariants;
|
|
109
|
+
private readonly staticAssertions;
|
|
110
|
+
private readonly dynamicTests;
|
|
111
|
+
constructor(componentName: string);
|
|
112
|
+
meta(meta: ContractMeta): this;
|
|
113
|
+
selectors(selectors: SelectorsMap): this;
|
|
114
|
+
relationship(invariant: RelationshipInvariant): this;
|
|
115
|
+
relationships(builderFn: (r: {
|
|
116
|
+
ariaReference: (from: string, attribute: string, to: string) => {
|
|
117
|
+
required: () => void;
|
|
118
|
+
recommended: () => void;
|
|
119
|
+
optional: () => void;
|
|
120
|
+
};
|
|
121
|
+
contains: (parent: string, child: string) => {
|
|
122
|
+
required: () => void;
|
|
123
|
+
recommended: () => void;
|
|
124
|
+
optional: () => void;
|
|
125
|
+
};
|
|
126
|
+
}) => void): this;
|
|
127
|
+
static(builderFn: (s: StaticBuilder) => void): this;
|
|
128
|
+
when(key: string): DynamicChain;
|
|
129
|
+
private validateRelationshipInvariants;
|
|
130
|
+
private validateStaticTargets;
|
|
131
|
+
private validateDynamicTargets;
|
|
132
|
+
build(): JsonContract;
|
|
133
|
+
}
|
|
134
|
+
declare function createContract(componentName: string, define: (c: ContractBuilder) => void): FluentContract;
|
|
135
|
+
|
|
136
|
+
export { ContractBuilder, type JsonContract, type RelationshipInvariant, createContract };
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// src/utils/test/dsl/index.ts
|
|
2
|
+
var FluentContract = class {
|
|
3
|
+
constructor(jsonContract) {
|
|
4
|
+
this.jsonContract = jsonContract;
|
|
5
|
+
}
|
|
6
|
+
toJSON() {
|
|
7
|
+
return this.jsonContract;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var StaticTargetBuilder = class {
|
|
11
|
+
constructor(targetName, sink) {
|
|
12
|
+
this.targetName = targetName;
|
|
13
|
+
this.sink = sink;
|
|
14
|
+
}
|
|
15
|
+
has(attribute, expectedValue) {
|
|
16
|
+
const create = (level) => {
|
|
17
|
+
this.sink.push({
|
|
18
|
+
target: this.targetName,
|
|
19
|
+
attribute,
|
|
20
|
+
expectedValue,
|
|
21
|
+
failureMessage: `Expected ${this.targetName} to have ${attribute}${expectedValue !== void 0 ? `=${expectedValue}` : ""}.`,
|
|
22
|
+
level
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
required: () => create("required"),
|
|
27
|
+
recommended: () => create("recommended"),
|
|
28
|
+
optional: () => create("optional")
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var StaticBuilder = class {
|
|
33
|
+
constructor(sink) {
|
|
34
|
+
this.sink = sink;
|
|
35
|
+
}
|
|
36
|
+
target(targetName) {
|
|
37
|
+
return new StaticTargetBuilder(targetName, this.sink);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var DynamicChain = class {
|
|
41
|
+
constructor(key, testsSink, selectors) {
|
|
42
|
+
this.key = key;
|
|
43
|
+
this.testsSink = testsSink;
|
|
44
|
+
this.selectors = selectors;
|
|
45
|
+
}
|
|
46
|
+
selectorTarget = "";
|
|
47
|
+
actions = [];
|
|
48
|
+
assertions = [];
|
|
49
|
+
explicitDescription = "";
|
|
50
|
+
on(target) {
|
|
51
|
+
this.selectorTarget = target;
|
|
52
|
+
this.actions.push({ type: "keypress", target, key: this.key });
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
describe(description) {
|
|
56
|
+
this.explicitDescription = description;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
focus(targetExpression) {
|
|
60
|
+
const parsed = this.parseRelativeExpression(targetExpression);
|
|
61
|
+
if (parsed) {
|
|
62
|
+
if (!this.selectors[parsed.selectorKey]) {
|
|
63
|
+
const availableSelectors = Object.keys(this.selectors).sort().join(", ") || "(none)";
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Invalid focus target expression "${targetExpression}": selector "${parsed.selectorKey}" is not defined. Available selectors: ${availableSelectors}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!this.selectors.relative && this.selectors[parsed.selectorKey]) {
|
|
69
|
+
this.selectors.relative = this.selectors[parsed.selectorKey];
|
|
70
|
+
}
|
|
71
|
+
this.assertions.push({
|
|
72
|
+
target: "relative",
|
|
73
|
+
assertion: "toHaveFocus",
|
|
74
|
+
relativeTarget: parsed.relativeTarget
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
this.assertions.push({
|
|
78
|
+
target: targetExpression,
|
|
79
|
+
assertion: "toHaveFocus"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
visible(target) {
|
|
85
|
+
this.assertions.push({ target, assertion: "toBeVisible" });
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
hidden(target) {
|
|
89
|
+
this.assertions.push({ target, assertion: "notToBeVisible" });
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
has(target, attribute, expectedValue) {
|
|
93
|
+
this.assertions.push({
|
|
94
|
+
target,
|
|
95
|
+
assertion: "toHaveAttribute",
|
|
96
|
+
attribute,
|
|
97
|
+
expectedValue
|
|
98
|
+
});
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
required() {
|
|
102
|
+
this.finalize("required");
|
|
103
|
+
}
|
|
104
|
+
recommended() {
|
|
105
|
+
this.finalize("recommended");
|
|
106
|
+
}
|
|
107
|
+
optional() {
|
|
108
|
+
this.finalize("optional");
|
|
109
|
+
}
|
|
110
|
+
finalize(level) {
|
|
111
|
+
if (!this.selectorTarget) {
|
|
112
|
+
throw new Error("Dynamic contract chain requires .on(<selectorKey>) before level terminator.");
|
|
113
|
+
}
|
|
114
|
+
const description = this.explicitDescription || `Pressing ${this.key} on ${this.selectorTarget} satisfies expected behavior.`;
|
|
115
|
+
this.testsSink.push({
|
|
116
|
+
description,
|
|
117
|
+
level,
|
|
118
|
+
action: this.actions,
|
|
119
|
+
assertions: this.assertions.map((a) => ({ ...a, level }))
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
parseRelativeExpression(input) {
|
|
123
|
+
const match = input.match(/^(next|previous|first|last)\(([^)]+)\)$/);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
const relativeTarget = match[1];
|
|
126
|
+
const selectorKey = match[2].trim();
|
|
127
|
+
return { relativeTarget, selectorKey };
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var ContractBuilder = class {
|
|
131
|
+
constructor(componentName) {
|
|
132
|
+
this.componentName = componentName;
|
|
133
|
+
}
|
|
134
|
+
metaValue = {};
|
|
135
|
+
selectorsValue = {};
|
|
136
|
+
relationshipInvariants = [];
|
|
137
|
+
staticAssertions = [];
|
|
138
|
+
dynamicTests = [];
|
|
139
|
+
meta(meta) {
|
|
140
|
+
this.metaValue = { ...this.metaValue, ...meta };
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
selectors(selectors) {
|
|
144
|
+
this.selectorsValue = { ...this.selectorsValue, ...selectors };
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
relationship(invariant) {
|
|
148
|
+
this.relationshipInvariants.push(invariant);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
relationships(builderFn) {
|
|
152
|
+
builderFn({
|
|
153
|
+
ariaReference: (from, attribute, to) => {
|
|
154
|
+
const create = (level) => {
|
|
155
|
+
this.relationshipInvariants.push({
|
|
156
|
+
type: "aria-reference",
|
|
157
|
+
from,
|
|
158
|
+
attribute,
|
|
159
|
+
to,
|
|
160
|
+
level
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
required: () => create("required"),
|
|
165
|
+
recommended: () => create("recommended"),
|
|
166
|
+
optional: () => create("optional")
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
contains: (parent, child) => {
|
|
170
|
+
const create = (level) => {
|
|
171
|
+
this.relationshipInvariants.push({
|
|
172
|
+
type: "contains",
|
|
173
|
+
parent,
|
|
174
|
+
child,
|
|
175
|
+
level
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
required: () => create("required"),
|
|
180
|
+
recommended: () => create("recommended"),
|
|
181
|
+
optional: () => create("optional")
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return this;
|
|
186
|
+
}
|
|
187
|
+
static(builderFn) {
|
|
188
|
+
builderFn(new StaticBuilder(this.staticAssertions));
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
when(key) {
|
|
192
|
+
return new DynamicChain(key, this.dynamicTests, this.selectorsValue);
|
|
193
|
+
}
|
|
194
|
+
validateRelationshipInvariants() {
|
|
195
|
+
if (this.relationshipInvariants.length === 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
199
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ");
|
|
200
|
+
const errors = [];
|
|
201
|
+
this.relationshipInvariants.forEach((invariant, index) => {
|
|
202
|
+
const prefix = `relationships[${index}] (${invariant.type})`;
|
|
203
|
+
if (invariant.type === "aria-reference") {
|
|
204
|
+
if (!selectorKeys.has(invariant.from)) {
|
|
205
|
+
errors.push(`${prefix}: "from" references unknown selector "${invariant.from}"`);
|
|
206
|
+
}
|
|
207
|
+
if (!selectorKeys.has(invariant.to)) {
|
|
208
|
+
errors.push(`${prefix}: "to" references unknown selector "${invariant.to}"`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (invariant.type === "contains") {
|
|
212
|
+
if (!selectorKeys.has(invariant.parent)) {
|
|
213
|
+
errors.push(`${prefix}: "parent" references unknown selector "${invariant.parent}"`);
|
|
214
|
+
}
|
|
215
|
+
if (!selectorKeys.has(invariant.child)) {
|
|
216
|
+
errors.push(`${prefix}: "child" references unknown selector "${invariant.child}"`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
if (errors.length > 0) {
|
|
221
|
+
const availableSelectorsMessage = available.length > 0 ? available : "(none)";
|
|
222
|
+
throw new Error(
|
|
223
|
+
[
|
|
224
|
+
`Contract invariant validation failed for component "${this.componentName}".`,
|
|
225
|
+
...errors.map((error) => `- ${error}`),
|
|
226
|
+
`Available selectors: ${availableSelectorsMessage}`
|
|
227
|
+
].join("\n")
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
validateStaticTargets() {
|
|
232
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
233
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
234
|
+
const errors = [];
|
|
235
|
+
this.staticAssertions.forEach((assertion, index) => {
|
|
236
|
+
if (!selectorKeys.has(assertion.target)) {
|
|
237
|
+
errors.push(`static.assertions[${index}]: target "${assertion.target}" is not defined in selectors`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
if (errors.length > 0) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
[
|
|
243
|
+
`Contract static target validation failed for component "${this.componentName}".`,
|
|
244
|
+
...errors.map((error) => `- ${error}`),
|
|
245
|
+
`Available selectors: ${available}`
|
|
246
|
+
].join("\n")
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
validateDynamicTargets() {
|
|
251
|
+
const selectorKeys = new Set(Object.keys(this.selectorsValue));
|
|
252
|
+
const available = Object.keys(this.selectorsValue).sort().join(", ") || "(none)";
|
|
253
|
+
const errors = [];
|
|
254
|
+
const isValidActionTarget = (target) => {
|
|
255
|
+
return selectorKeys.has(target) || target === "document" || target === "relative";
|
|
256
|
+
};
|
|
257
|
+
const isValidAssertionTarget = (target) => {
|
|
258
|
+
return selectorKeys.has(target) || target === "relative";
|
|
259
|
+
};
|
|
260
|
+
this.dynamicTests.forEach((test, testIndex) => {
|
|
261
|
+
test.action.forEach((action, actionIndex) => {
|
|
262
|
+
if (!isValidActionTarget(action.target)) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`dynamic[${testIndex}].action[${actionIndex}]: target "${action.target}" is not defined in selectors`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
test.assertions.forEach((assertion, assertionIndex) => {
|
|
269
|
+
if (!isValidAssertionTarget(assertion.target)) {
|
|
270
|
+
errors.push(
|
|
271
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "${assertion.target}" is not defined in selectors`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (assertion.target === "relative" && !this.selectorsValue.relative) {
|
|
275
|
+
errors.push(
|
|
276
|
+
`dynamic[${testIndex}].assertions[${assertionIndex}]: target "relative" requires selectors.relative to be defined`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
if (errors.length > 0) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
[
|
|
284
|
+
`Contract dynamic target validation failed for component "${this.componentName}".`,
|
|
285
|
+
...errors.map((error) => `- ${error}`),
|
|
286
|
+
`Available selectors: ${available}`,
|
|
287
|
+
`Allowed special targets: document, relative`
|
|
288
|
+
].join("\n")
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
build() {
|
|
293
|
+
this.validateRelationshipInvariants();
|
|
294
|
+
this.validateStaticTargets();
|
|
295
|
+
this.validateDynamicTargets();
|
|
296
|
+
const fallbackId = this.metaValue.id || `aria-ease.contract.${this.componentName}`;
|
|
297
|
+
return {
|
|
298
|
+
meta: {
|
|
299
|
+
id: fallbackId,
|
|
300
|
+
version: this.metaValue.version || "1.0.0",
|
|
301
|
+
description: this.metaValue.description || `Fluent contract for ${this.componentName}`,
|
|
302
|
+
source: this.metaValue.source,
|
|
303
|
+
W3CName: this.metaValue.W3CName
|
|
304
|
+
},
|
|
305
|
+
selectors: this.selectorsValue,
|
|
306
|
+
relationships: this.relationshipInvariants,
|
|
307
|
+
static: [{ assertions: this.staticAssertions }],
|
|
308
|
+
dynamic: this.dynamicTests
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
function createContract(componentName, define) {
|
|
313
|
+
const builder = new ContractBuilder(componentName);
|
|
314
|
+
define(builder);
|
|
315
|
+
return new FluentContract(builder.build());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { createContract };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aria-ease",
|
|
3
|
-
"version": "6.9.
|
|
3
|
+
"version": "6.9.1",
|
|
4
4
|
"description": "Accessibility infrastructure for the entire frontend engineering lifecycle. Build accessible patterns, run automated audits, verify component interactions, and gate deployments — all in one system.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"build:core": "tsup ./index.ts --format esm,cjs --dts --outDir dist --external jest-axe --external @testing-library/react --external @axe-core/playwright --external playwright",
|
|
21
21
|
"build:modules": "tsup ./src/combobox/index.ts ./src/accordion/index.ts ./src/block/index.ts ./src/checkbox/index.ts ./src/menu/index.ts ./src/tabs/index.ts ./src/radio/index.ts ./src/toggle/index.ts ./src/contracts/index.ts --format esm,cjs --dts --treeshake --outDir dist/src",
|
|
22
22
|
"build:test": "tsup ./src/utils/test/index.ts --format esm,cjs --dts --treeshake --external jest-axe --external @testing-library/react --external playwright --external @playwright/test --outDir dist/src/utils/test",
|
|
23
|
+
"build:dsl": "tsup ./src/utils/test/dsl/index.ts --format esm,cjs --dts --treeshake --outDir dist/src/utils/test/dsl",
|
|
23
24
|
"build:cli": "tsup ./src/utils/cli/cli.ts --format esm,cjs --dts --outDir bin --external commander --external chalk --external jest-axe --external @testing-library/react --external @axe-core/playwright --external playwright",
|
|
24
25
|
"build:contracts": "mkdir -p ./dist/src/utils/test && cp -r ./src/utils/test/contract/aria-contracts ./dist/src/utils/test/",
|
|
25
|
-
"build": "npm run clean && npm run build:core && npm run build:modules && npm run build:test && npm run build:cli && npm run build:contracts"
|
|
26
|
+
"build": "npm run clean && npm run build:core && npm run build:modules && npm run build:test && npm run build:dsl && npm run build:cli && npm run build:contracts"
|
|
26
27
|
},
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
@@ -118,10 +119,10 @@
|
|
|
118
119
|
"import": "./dist/src/tabs/index.js",
|
|
119
120
|
"require": "./dist/src/tabs/index.cjs"
|
|
120
121
|
},
|
|
121
|
-
"./
|
|
122
|
-
"types": "./dist/src/
|
|
123
|
-
"import": "./dist/src/
|
|
124
|
-
"require": "./dist/src/
|
|
122
|
+
"./contract": {
|
|
123
|
+
"types": "./dist/src/utils/test/dsl/index.d.ts",
|
|
124
|
+
"import": "./dist/src/utils/test/dsl/index.js",
|
|
125
|
+
"require": "./dist/src/utils/test/dsl/index.cjs"
|
|
125
126
|
}
|
|
126
127
|
},
|
|
127
128
|
"files": [
|