sibujs 4.5.0 → 4.7.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 +1 -1
- package/dist/browser.cjs +326 -133
- package/dist/browser.d.cts +46 -14
- package/dist/browser.d.ts +46 -14
- package/dist/browser.js +8 -6
- package/dist/build.cjs +2094 -1033
- package/dist/build.d.cts +133 -44
- package/dist/build.d.ts +133 -44
- package/dist/build.js +1372 -574
- package/dist/cdn.dev.global.js +10 -10
- package/dist/cdn.full.dev.global.js +11 -11
- package/dist/cdn.full.global.js +10 -10
- package/dist/cdn.global.js +10 -10
- package/dist/{chunk-2DCGACUU.js → chunk-2BPG2XDA.js} +251 -66
- package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
- package/dist/{chunk-4PMLNECI.js → chunk-3QWBSL5R.js} +98 -29
- package/dist/{chunk-KKLW7YWL.js → chunk-4AWA2PVD.js} +275 -144
- package/dist/chunk-5HZXGZ6T.js +24 -0
- package/dist/{chunk-PCT43HW3.js → chunk-7LUJQAOJ.js} +1 -1
- package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
- package/dist/chunk-BC2SECJD.js +44 -0
- package/dist/{chunk-TBYTO6BS.js → chunk-BSY63EM6.js} +136 -80
- package/dist/{chunk-FTIR4QW2.js → chunk-DKXRACVN.js} +6 -8
- package/dist/{chunk-ONOHFDLG.js → chunk-FNJXNGYZ.js} +3 -3
- package/dist/{chunk-XZZOBQAY.js → chunk-FQRUXCEE.js} +6 -5
- package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
- package/dist/chunk-J6FW5TV6.js +233 -0
- package/dist/{chunk-UGRX3S57.js → chunk-JWKYU5GV.js} +22 -4
- package/dist/chunk-NYNYSPK7.js +318 -0
- package/dist/{chunk-RBTPLM32.js → chunk-RUSSKG6G.js} +13 -12
- package/dist/{chunk-7LN645I6.js → chunk-SLM3IA34.js} +3 -3
- package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
- package/dist/{chunk-B3WHI2QA.js → chunk-UNWRJRKC.js} +55 -31
- package/dist/{chunk-KEISJXBU.js → chunk-UOL2ECCS.js} +44 -17
- package/dist/{chunk-RJE2BNI4.js → chunk-VZSG24LS.js} +108 -45
- package/dist/{chunk-RIXRAYIU.js → chunk-WEQ3DMVL.js} +10 -4
- package/dist/{chunk-3JZ4L5TJ.js → chunk-WOLJZUFQ.js} +354 -90
- package/dist/{chunk-S373NSMK.js → chunk-XYV3EDB7.js} +283 -123
- package/dist/{chunk-VZKNK2V7.js → chunk-XZR4PXRE.js} +296 -38
- package/dist/{chunk-GW3SCCZG.js → chunk-Z3OHK6QT.js} +153 -110
- package/dist/{chunk-OMJJM3KM.js → chunk-ZVL7TY4K.js} +287 -130
- package/dist/{contracts-DBdg9J_a.d.ts → contracts-CLqzJnOV.d.ts} +36 -17
- package/dist/{contracts-DBdg9J_a.d.cts → contracts-CTOJXu-x.d.cts} +36 -17
- package/dist/{customElement-OB9CIsc5.d.cts → customElement-MmInOW1U.d.cts} +21 -0
- package/dist/{customElement-OB9CIsc5.d.ts → customElement-MmInOW1U.d.ts} +21 -0
- package/dist/data.cjs +410 -155
- package/dist/data.d.cts +166 -12
- package/dist/data.d.ts +166 -12
- package/dist/data.js +12 -9
- package/dist/devtools.cjs +98 -61
- package/dist/devtools.js +7 -8
- package/dist/dispose-GEIG2KOF.js +28 -0
- package/dist/ecosystem.cjs +372 -126
- package/dist/ecosystem.d.cts +20 -3
- package/dist/ecosystem.d.ts +20 -3
- package/dist/ecosystem.js +12 -12
- package/dist/extras.cjs +2366 -909
- package/dist/extras.d.cts +11 -9
- package/dist/extras.d.ts +11 -9
- package/dist/extras.js +39 -28
- package/dist/index.cjs +421 -158
- package/dist/index.d.cts +213 -171
- package/dist/index.d.ts +213 -171
- package/dist/index.js +24 -27
- package/dist/motion.cjs +118 -44
- package/dist/motion.js +5 -5
- package/dist/patterns.cjs +344 -53
- package/dist/patterns.d.cts +28 -9
- package/dist/patterns.d.ts +28 -9
- package/dist/patterns.js +8 -8
- package/dist/performance.cjs +325 -220
- package/dist/performance.d.cts +2 -2
- package/dist/performance.d.ts +2 -2
- package/dist/performance.js +8 -9
- package/dist/plugin-DVgSnTfK.d.cts +112 -0
- package/dist/plugin-DVgSnTfK.d.ts +112 -0
- package/dist/plugins.cjs +664 -233
- package/dist/plugins.d.cts +127 -14
- package/dist/plugins.d.ts +127 -14
- package/dist/plugins.js +96 -42
- package/dist/signal-EotCj4hS.d.cts +110 -0
- package/dist/signal-EotCj4hS.d.ts +110 -0
- package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
- package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
- package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
- package/dist/ssr.cjs +229 -84
- package/dist/ssr.d.cts +9 -3
- package/dist/ssr.d.ts +9 -3
- package/dist/ssr.js +11 -12
- package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
- package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
- package/dist/tagFactory-8qL9LCIx.d.cts +156 -0
- package/dist/tagFactory-BL2fymez.d.ts +156 -0
- package/dist/testing.cjs +2503 -2191
- package/dist/testing.d.cts +56 -5
- package/dist/testing.d.ts +56 -5
- package/dist/testing.js +580 -307
- package/dist/types-CJFViL6Q.d.cts +26 -0
- package/dist/types-CJFViL6Q.d.ts +26 -0
- package/dist/ui.cjs +732 -329
- package/dist/ui.d.cts +41 -7
- package/dist/ui.d.ts +41 -7
- package/dist/ui.js +151 -56
- package/dist/widgets.cjs +267 -291
- package/dist/widgets.js +9 -10
- package/package.json +4 -2
- package/dist/chunk-2WLZ6757.js +0 -149
- package/dist/chunk-CCSJMTRN.js +0 -15
- package/dist/chunk-QKRPLZ2V.js +0 -108
- package/dist/chunk-VUF4ALSW.js +0 -60
- package/dist/chunk-WWV3SJ3L.js +0 -131
- package/dist/dispose-46BOMMQJ.js +0 -19
- package/dist/plugin-D30wlGW5.d.cts +0 -71
- package/dist/plugin-D30wlGW5.d.ts +0 -71
- package/dist/tagFactory-DVoDpHye.d.cts +0 -215
- package/dist/tagFactory-DVoDpHye.d.ts +0 -215
package/dist/testing.js
CHANGED
|
@@ -3,18 +3,220 @@ import {
|
|
|
3
3
|
} from "./chunk-7ZHH77QA.js";
|
|
4
4
|
import {
|
|
5
5
|
effect
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-WEQ3DMVL.js";
|
|
7
|
+
import "./chunk-7XHATCIH.js";
|
|
8
|
+
import "./chunk-JWKYU5GV.js";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
dispose,
|
|
11
|
+
replaceChildrenSafely,
|
|
12
|
+
reportError
|
|
13
|
+
} from "./chunk-NYNYSPK7.js";
|
|
13
14
|
|
|
14
|
-
// src/testing/
|
|
15
|
-
function
|
|
16
|
-
|
|
15
|
+
// src/testing/queries.ts
|
|
16
|
+
function queryByText(container, text) {
|
|
17
|
+
const walk = (node) => {
|
|
18
|
+
if (node.childNodes.length === 1 && node.childNodes[0].nodeType === 3) {
|
|
19
|
+
if (node.textContent?.includes(text)) return node;
|
|
20
|
+
}
|
|
21
|
+
for (const child of Array.from(node.children)) {
|
|
22
|
+
const found = walk(child);
|
|
23
|
+
if (found) return found;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
return walk(container);
|
|
28
|
+
}
|
|
29
|
+
function queryByAttribute(container, attribute, value) {
|
|
30
|
+
for (const el of Array.from(container.querySelectorAll(`[${attribute}]`))) {
|
|
31
|
+
if (el.getAttribute(attribute) === value) return el;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function queryAllByAttribute(container, attribute, value) {
|
|
36
|
+
return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(
|
|
37
|
+
(el) => el.getAttribute(attribute) === value
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function queryByTestId(container, testId) {
|
|
41
|
+
return queryByAttribute(container, "data-testid", testId);
|
|
42
|
+
}
|
|
43
|
+
function queryByRole(container, role) {
|
|
44
|
+
return queryByAttribute(container, "role", role);
|
|
45
|
+
}
|
|
46
|
+
function queryByLabel(container, labelText) {
|
|
47
|
+
const labels = Array.from(container.querySelectorAll("label"));
|
|
48
|
+
for (const label of labels) {
|
|
49
|
+
if (label.textContent?.trim() === labelText) {
|
|
50
|
+
const forId = label.getAttribute("for");
|
|
51
|
+
if (forId) {
|
|
52
|
+
const target = queryByAttribute(container, "id", forId);
|
|
53
|
+
if (target) return target;
|
|
54
|
+
}
|
|
55
|
+
const child = label.querySelector("input, select, textarea, button");
|
|
56
|
+
if (child) return child;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return queryByAttribute(container, "aria-label", labelText);
|
|
60
|
+
}
|
|
61
|
+
async function pollUntil(fn, timeout, interval, errorIfTimeout) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
let timer;
|
|
65
|
+
const check = () => {
|
|
66
|
+
timer = void 0;
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = fn();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
reject(err);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (result !== null) {
|
|
75
|
+
resolve(result);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (Date.now() - start >= timeout) {
|
|
79
|
+
reject(new Error(errorIfTimeout));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
timer = setTimeout(check, interval);
|
|
83
|
+
};
|
|
84
|
+
check();
|
|
85
|
+
void timer;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function findByText(container, text, options = {}) {
|
|
89
|
+
return pollUntil(
|
|
90
|
+
() => queryByText(container, text),
|
|
91
|
+
options.timeout ?? 1e3,
|
|
92
|
+
options.interval ?? 50,
|
|
93
|
+
`findByText: no element with text "${text}" after ${options.timeout ?? 1e3}ms`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function findByTestId(container, testId, options = {}) {
|
|
97
|
+
return pollUntil(
|
|
98
|
+
() => queryByTestId(container, testId),
|
|
99
|
+
options.timeout ?? 1e3,
|
|
100
|
+
options.interval ?? 50,
|
|
101
|
+
`findByTestId: no element with data-testid="${testId}" after ${options.timeout ?? 1e3}ms`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function findByRole(container, role, options = {}) {
|
|
105
|
+
return pollUntil(
|
|
106
|
+
() => queryByRole(container, role),
|
|
107
|
+
options.timeout ?? 1e3,
|
|
108
|
+
options.interval ?? 50,
|
|
109
|
+
`findByRole: no element with role="${role}" after ${options.timeout ?? 1e3}ms`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function waitForSignal(getter, predicate, options = {}) {
|
|
113
|
+
const timeoutMs = options.timeout ?? 1e3;
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let resolved = false;
|
|
116
|
+
let timer;
|
|
117
|
+
const finish = (fn) => {
|
|
118
|
+
if (resolved) return;
|
|
119
|
+
resolved = true;
|
|
120
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
121
|
+
fn();
|
|
122
|
+
};
|
|
123
|
+
timer = setTimeout(() => {
|
|
124
|
+
finish(() => {
|
|
125
|
+
teardown();
|
|
126
|
+
reject(new Error(`waitForSignal: predicate did not match within ${timeoutMs}ms`));
|
|
127
|
+
});
|
|
128
|
+
}, timeoutMs);
|
|
129
|
+
const teardown = effect(() => {
|
|
130
|
+
if (resolved) return;
|
|
131
|
+
const value = getter();
|
|
132
|
+
if (predicate(value)) {
|
|
133
|
+
finish(() => {
|
|
134
|
+
queueMicrotask(() => teardown());
|
|
135
|
+
resolve(value);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function type(element, text) {
|
|
142
|
+
for (const char of text) {
|
|
143
|
+
element.value += char;
|
|
144
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: char }));
|
|
145
|
+
}
|
|
146
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/testing/listenerTracking.ts
|
|
150
|
+
var KEY = /* @__PURE__ */ Symbol.for("sibujs.listenerTracking.v2");
|
|
151
|
+
var host = globalThis;
|
|
152
|
+
function captureOf(options) {
|
|
153
|
+
if (typeof options === "boolean") return options;
|
|
154
|
+
return typeof options === "object" && options !== null && !!options.capture;
|
|
155
|
+
}
|
|
156
|
+
function enableListenerTracking() {
|
|
157
|
+
if (host[KEY] || typeof EventTarget === "undefined") return;
|
|
158
|
+
const tracker = /* @__PURE__ */ new WeakMap();
|
|
159
|
+
host[KEY] = tracker;
|
|
160
|
+
const proto = EventTarget.prototype;
|
|
161
|
+
const add = proto.addEventListener;
|
|
162
|
+
const remove = proto.removeEventListener;
|
|
163
|
+
const find = (target, type2, listener, capture) => tracker.get(target)?.get(type2)?.find((e) => e.listener === listener && e.capture === capture);
|
|
164
|
+
const drop = (target, type2, entry) => {
|
|
165
|
+
const types = tracker.get(target);
|
|
166
|
+
const entries = types?.get(type2);
|
|
167
|
+
if (!types || !entries) return;
|
|
168
|
+
const index = entries.indexOf(entry);
|
|
169
|
+
if (index === -1) return;
|
|
170
|
+
entries.splice(index, 1);
|
|
171
|
+
if (entries.length === 0) types.delete(type2);
|
|
172
|
+
entry.release?.();
|
|
173
|
+
};
|
|
174
|
+
proto.addEventListener = function(type2, listener, options) {
|
|
175
|
+
const tracked = listener != null && typeof Element !== "undefined" && this instanceof Element;
|
|
176
|
+
const capture = captureOf(options);
|
|
177
|
+
const opts = typeof options === "object" && options !== null ? options : void 0;
|
|
178
|
+
if (!tracked || opts?.signal?.aborted || find(this, type2, listener, capture)) {
|
|
179
|
+
add.call(this, type2, listener, options);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const entry = { listener, capture };
|
|
183
|
+
const releases = [];
|
|
184
|
+
if (opts?.once) {
|
|
185
|
+
const onFire = () => drop(this, type2, entry);
|
|
186
|
+
add.call(this, type2, onFire, { capture, once: true });
|
|
187
|
+
releases.push(() => remove.call(this, type2, onFire, { capture }));
|
|
188
|
+
}
|
|
189
|
+
if (opts?.signal) {
|
|
190
|
+
const signal = opts.signal;
|
|
191
|
+
const onAbort = () => drop(this, type2, entry);
|
|
192
|
+
add.call(signal, "abort", onAbort, { once: true });
|
|
193
|
+
releases.push(() => remove.call(signal, "abort", onAbort));
|
|
194
|
+
}
|
|
195
|
+
if (releases.length > 0) {
|
|
196
|
+
entry.release = () => {
|
|
197
|
+
for (const release of releases) release();
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
let types = tracker.get(this);
|
|
201
|
+
if (!types) tracker.set(this, types = /* @__PURE__ */ new Map());
|
|
202
|
+
let entries = types.get(type2);
|
|
203
|
+
if (!entries) types.set(type2, entries = []);
|
|
204
|
+
entries.push(entry);
|
|
205
|
+
add.call(this, type2, listener, options);
|
|
206
|
+
};
|
|
207
|
+
proto.removeEventListener = function(type2, listener, options) {
|
|
208
|
+
remove.call(this, type2, listener, options);
|
|
209
|
+
const entry = find(this, type2, listener, captureOf(options));
|
|
210
|
+
if (entry) drop(this, type2, entry);
|
|
211
|
+
};
|
|
17
212
|
}
|
|
213
|
+
function listenerTypes(el) {
|
|
214
|
+
const types = host[KEY]?.get(el);
|
|
215
|
+
return new Set(types ? types.keys() : []);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/testing/a11y.ts
|
|
219
|
+
enableListenerTracking();
|
|
18
220
|
var VALID_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
19
221
|
"alert",
|
|
20
222
|
"alertdialog",
|
|
@@ -178,6 +380,21 @@ function getSelector(el) {
|
|
|
178
380
|
const roleStr = role ? `[role="${role}"]` : "";
|
|
179
381
|
return `${tag}${classes}${roleStr}`;
|
|
180
382
|
}
|
|
383
|
+
function selectAll(root, selector) {
|
|
384
|
+
const matches = Array.from(root.querySelectorAll(selector));
|
|
385
|
+
if (root.matches(selector)) matches.unshift(root);
|
|
386
|
+
return matches;
|
|
387
|
+
}
|
|
388
|
+
function hasActivationHandler(el) {
|
|
389
|
+
if (el.hasAttribute("onclick")) return true;
|
|
390
|
+
const types = listenerTypes(el);
|
|
391
|
+
return ["click", "pointerdown", "pointerup", "mousedown", "mouseup"].some((t) => types.has(t));
|
|
392
|
+
}
|
|
393
|
+
function hasKeyboardHandler(el) {
|
|
394
|
+
if (el.hasAttribute("onkeydown") || el.hasAttribute("onkeyup") || el.hasAttribute("onkeypress")) return true;
|
|
395
|
+
const types = listenerTypes(el);
|
|
396
|
+
return types.has("keydown") || types.has("keyup") || types.has("keypress");
|
|
397
|
+
}
|
|
181
398
|
function hasAccessibleName(el) {
|
|
182
399
|
if (el.getAttribute("aria-label")?.trim()) return true;
|
|
183
400
|
const labelledBy = el.getAttribute("aria-labelledby");
|
|
@@ -195,15 +412,16 @@ function hasAccessibleName(el) {
|
|
|
195
412
|
}
|
|
196
413
|
function isNativeInteractive(el) {
|
|
197
414
|
const tag = el.tagName.toLowerCase();
|
|
198
|
-
if (["button", "select", "textarea"].includes(tag)) return true;
|
|
415
|
+
if (["button", "select", "textarea", "summary"].includes(tag)) return true;
|
|
199
416
|
if (tag === "input" && el.getAttribute("type") !== "hidden") return true;
|
|
200
417
|
if (tag === "a" && el.hasAttribute("href")) return true;
|
|
201
418
|
if (tag === "area" && el.hasAttribute("href")) return true;
|
|
202
419
|
return false;
|
|
203
420
|
}
|
|
421
|
+
var DELEGATES_ATTRIBUTE = "data-a11y-delegates";
|
|
204
422
|
function checkImageAlt(root) {
|
|
205
423
|
const violations = [];
|
|
206
|
-
const images = root
|
|
424
|
+
const images = selectAll(root, "img");
|
|
207
425
|
for (const img of Array.from(images)) {
|
|
208
426
|
if (!img.hasAttribute("alt")) {
|
|
209
427
|
violations.push({
|
|
@@ -223,7 +441,7 @@ function checkImageAlt(root) {
|
|
|
223
441
|
});
|
|
224
442
|
}
|
|
225
443
|
}
|
|
226
|
-
const roleImgs = root
|
|
444
|
+
const roleImgs = selectAll(root, '[role="img"]');
|
|
227
445
|
for (const el of Array.from(roleImgs)) {
|
|
228
446
|
if (!hasAccessibleName(el)) {
|
|
229
447
|
violations.push({
|
|
@@ -239,7 +457,7 @@ function checkImageAlt(root) {
|
|
|
239
457
|
}
|
|
240
458
|
function checkFormLabels(root) {
|
|
241
459
|
const violations = [];
|
|
242
|
-
const inputs = root
|
|
460
|
+
const inputs = selectAll(root, "input, select, textarea");
|
|
243
461
|
for (const input of Array.from(inputs)) {
|
|
244
462
|
if (input.getAttribute("type") === "hidden") continue;
|
|
245
463
|
const inputType = input.getAttribute("type");
|
|
@@ -271,7 +489,9 @@ function checkInputHasLabel(input, root) {
|
|
|
271
489
|
if (input.getAttribute("title")?.trim()) return true;
|
|
272
490
|
const id = input.getAttribute("id");
|
|
273
491
|
if (id) {
|
|
274
|
-
const
|
|
492
|
+
const scope = input.getRootNode();
|
|
493
|
+
const searchIn = typeof scope.querySelectorAll === "function" ? scope : root;
|
|
494
|
+
const label = queryAllByAttribute(searchIn, "for", id).find((el) => el.tagName === "LABEL");
|
|
275
495
|
if (label?.textContent?.trim()) return true;
|
|
276
496
|
}
|
|
277
497
|
let parent = input.parentElement;
|
|
@@ -283,7 +503,7 @@ function checkInputHasLabel(input, root) {
|
|
|
283
503
|
}
|
|
284
504
|
function checkHeadingHierarchy(root) {
|
|
285
505
|
const violations = [];
|
|
286
|
-
const headings = root
|
|
506
|
+
const headings = selectAll(root, "h1, h2, h3, h4, h5, h6");
|
|
287
507
|
if (headings.length === 0) return violations;
|
|
288
508
|
let previousLevel = 0;
|
|
289
509
|
for (const heading of Array.from(headings)) {
|
|
@@ -321,7 +541,7 @@ function checkHeadingHierarchy(root) {
|
|
|
321
541
|
}
|
|
322
542
|
function checkColorContrast(root) {
|
|
323
543
|
const violations = [];
|
|
324
|
-
const roledElements = root
|
|
544
|
+
const roledElements = selectAll(root, "[role]");
|
|
325
545
|
for (const el of Array.from(roledElements)) {
|
|
326
546
|
const role = el.getAttribute("role");
|
|
327
547
|
if (!role) continue;
|
|
@@ -357,7 +577,7 @@ function checkColorContrast(root) {
|
|
|
357
577
|
});
|
|
358
578
|
}
|
|
359
579
|
}
|
|
360
|
-
const inlineStyled = root
|
|
580
|
+
const inlineStyled = selectAll(root, "[style]");
|
|
361
581
|
for (const el of Array.from(inlineStyled)) {
|
|
362
582
|
const style = el.getAttribute("style") || "";
|
|
363
583
|
const hasColor = /(?:^|;)\s*color\s*:/i.test(style);
|
|
@@ -383,14 +603,15 @@ function checkColorContrast(root) {
|
|
|
383
603
|
}
|
|
384
604
|
return violations;
|
|
385
605
|
}
|
|
386
|
-
function checkKeyboardAccess(root) {
|
|
606
|
+
function checkKeyboardAccess(root, options) {
|
|
387
607
|
const violations = [];
|
|
388
|
-
const clickElements = root
|
|
389
|
-
for (const el of
|
|
608
|
+
const clickElements = selectAll(root, "*").filter(hasActivationHandler);
|
|
609
|
+
for (const el of clickElements) {
|
|
390
610
|
if (isNativeInteractive(el)) continue;
|
|
611
|
+
if (el.hasAttribute(DELEGATES_ATTRIBUTE) || options?.delegatesActivation?.(el)) continue;
|
|
391
612
|
const hasTabindex = el.hasAttribute("tabindex");
|
|
392
613
|
const hasRole = el.hasAttribute("role");
|
|
393
|
-
const hasKeyHandler =
|
|
614
|
+
const hasKeyHandler = hasKeyboardHandler(el);
|
|
394
615
|
if (!hasTabindex) {
|
|
395
616
|
violations.push({
|
|
396
617
|
rule: "keyboard-access",
|
|
@@ -419,7 +640,8 @@ function checkKeyboardAccess(root) {
|
|
|
419
640
|
});
|
|
420
641
|
}
|
|
421
642
|
}
|
|
422
|
-
const interactiveRoles =
|
|
643
|
+
const interactiveRoles = selectAll(
|
|
644
|
+
root,
|
|
423
645
|
'[role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"]'
|
|
424
646
|
);
|
|
425
647
|
for (const el of Array.from(interactiveRoles)) {
|
|
@@ -438,7 +660,7 @@ function checkKeyboardAccess(root) {
|
|
|
438
660
|
}
|
|
439
661
|
function checkAriaAttributes(root) {
|
|
440
662
|
const violations = [];
|
|
441
|
-
const roledElements = root
|
|
663
|
+
const roledElements = selectAll(root, "[role]");
|
|
442
664
|
for (const el of Array.from(roledElements)) {
|
|
443
665
|
const role = el.getAttribute("role");
|
|
444
666
|
if (!role) continue;
|
|
@@ -465,7 +687,7 @@ function checkAriaAttributes(root) {
|
|
|
465
687
|
}
|
|
466
688
|
}
|
|
467
689
|
}
|
|
468
|
-
const allElements = root
|
|
690
|
+
const allElements = selectAll(root, "*");
|
|
469
691
|
for (const el of Array.from(allElements)) {
|
|
470
692
|
for (const attr of Array.from(el.attributes)) {
|
|
471
693
|
if (!attr.name.startsWith("aria-")) continue;
|
|
@@ -524,31 +746,19 @@ function checkAriaAttributes(root) {
|
|
|
524
746
|
}
|
|
525
747
|
}
|
|
526
748
|
}
|
|
527
|
-
for (const attr of Array.from(root.attributes)) {
|
|
528
|
-
if (!attr.name.startsWith("aria-")) continue;
|
|
529
|
-
if (!VALID_ARIA_ATTRIBUTES.has(attr.name)) {
|
|
530
|
-
violations.push({
|
|
531
|
-
rule: "aria-valid-attr",
|
|
532
|
-
level: "error",
|
|
533
|
-
message: `Invalid ARIA attribute "${attr.name}" on root element. Must be a valid WAI-ARIA attribute.`,
|
|
534
|
-
element: root,
|
|
535
|
-
selector: getSelector(root)
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
749
|
return violations;
|
|
540
750
|
}
|
|
541
751
|
function checkLandmarks(root) {
|
|
542
752
|
const violations = [];
|
|
543
753
|
const foundLandmarks = /* @__PURE__ */ new Set();
|
|
544
754
|
for (const role of LANDMARK_ROLES) {
|
|
545
|
-
const elements = root
|
|
755
|
+
const elements = selectAll(root, `[role="${role}"]`);
|
|
546
756
|
if (elements.length > 0) {
|
|
547
757
|
foundLandmarks.add(role);
|
|
548
758
|
}
|
|
549
759
|
}
|
|
550
760
|
for (const [tag, role] of Object.entries(LANDMARK_ELEMENTS)) {
|
|
551
|
-
const elements = root
|
|
761
|
+
const elements = selectAll(root, tag);
|
|
552
762
|
if (elements.length > 0) {
|
|
553
763
|
foundLandmarks.add(role);
|
|
554
764
|
}
|
|
@@ -561,7 +771,7 @@ function checkLandmarks(root) {
|
|
|
561
771
|
selector: getSelector(root)
|
|
562
772
|
});
|
|
563
773
|
}
|
|
564
|
-
const mainElements = root
|
|
774
|
+
const mainElements = selectAll(root, 'main, [role="main"]');
|
|
565
775
|
if (mainElements.length > 1) {
|
|
566
776
|
violations.push({
|
|
567
777
|
rule: "landmark-unique",
|
|
@@ -570,7 +780,7 @@ function checkLandmarks(root) {
|
|
|
570
780
|
selector: getSelector(root)
|
|
571
781
|
});
|
|
572
782
|
}
|
|
573
|
-
const bannerElements = root
|
|
783
|
+
const bannerElements = selectAll(root, 'header, [role="banner"]');
|
|
574
784
|
if (bannerElements.length > 1) {
|
|
575
785
|
violations.push({
|
|
576
786
|
rule: "landmark-unique",
|
|
@@ -579,7 +789,7 @@ function checkLandmarks(root) {
|
|
|
579
789
|
selector: getSelector(root)
|
|
580
790
|
});
|
|
581
791
|
}
|
|
582
|
-
const contentinfoElements = root
|
|
792
|
+
const contentinfoElements = selectAll(root, 'footer, [role="contentinfo"]');
|
|
583
793
|
if (contentinfoElements.length > 1) {
|
|
584
794
|
violations.push({
|
|
585
795
|
rule: "landmark-unique",
|
|
@@ -588,7 +798,7 @@ function checkLandmarks(root) {
|
|
|
588
798
|
selector: getSelector(root)
|
|
589
799
|
});
|
|
590
800
|
}
|
|
591
|
-
const navElements = root
|
|
801
|
+
const navElements = selectAll(root, 'nav, [role="navigation"]');
|
|
592
802
|
if (navElements.length > 1) {
|
|
593
803
|
for (const nav of Array.from(navElements)) {
|
|
594
804
|
if (!nav.getAttribute("aria-label") && !nav.getAttribute("aria-labelledby")) {
|
|
@@ -606,7 +816,7 @@ function checkLandmarks(root) {
|
|
|
606
816
|
}
|
|
607
817
|
function checkLinksAndButtons(root) {
|
|
608
818
|
const violations = [];
|
|
609
|
-
const links = root
|
|
819
|
+
const links = selectAll(root, "a[href]");
|
|
610
820
|
for (const link of Array.from(links)) {
|
|
611
821
|
if (!hasAccessibleName(link)) {
|
|
612
822
|
violations.push({
|
|
@@ -648,7 +858,7 @@ function checkLinksAndButtons(root) {
|
|
|
648
858
|
});
|
|
649
859
|
}
|
|
650
860
|
}
|
|
651
|
-
const buttons = root
|
|
861
|
+
const buttons = selectAll(root, 'button, [role="button"]');
|
|
652
862
|
for (const button of Array.from(buttons)) {
|
|
653
863
|
if (!hasAccessibleName(button)) {
|
|
654
864
|
violations.push({
|
|
@@ -660,7 +870,7 @@ function checkLinksAndButtons(root) {
|
|
|
660
870
|
});
|
|
661
871
|
}
|
|
662
872
|
}
|
|
663
|
-
const inputButtons = root
|
|
873
|
+
const inputButtons = selectAll(root, 'input[type="submit"], input[type="reset"], input[type="button"]');
|
|
664
874
|
for (const input of Array.from(inputButtons)) {
|
|
665
875
|
const value = input.getAttribute("value");
|
|
666
876
|
if (!value?.trim() && !input.getAttribute("aria-label") && !input.getAttribute("aria-labelledby")) {
|
|
@@ -677,7 +887,7 @@ function checkLinksAndButtons(root) {
|
|
|
677
887
|
}
|
|
678
888
|
function checkListSemantics(root) {
|
|
679
889
|
const violations = [];
|
|
680
|
-
const lists = root
|
|
890
|
+
const lists = selectAll(root, "ul, ol");
|
|
681
891
|
for (const list of Array.from(lists)) {
|
|
682
892
|
const children = Array.from(list.children);
|
|
683
893
|
for (const child of children) {
|
|
@@ -702,7 +912,7 @@ function checkListSemantics(root) {
|
|
|
702
912
|
});
|
|
703
913
|
}
|
|
704
914
|
}
|
|
705
|
-
const dlLists = root
|
|
915
|
+
const dlLists = selectAll(root, "dl");
|
|
706
916
|
for (const dl of Array.from(dlLists)) {
|
|
707
917
|
const children = Array.from(dl.children);
|
|
708
918
|
for (const child of children) {
|
|
@@ -718,7 +928,7 @@ function checkListSemantics(root) {
|
|
|
718
928
|
}
|
|
719
929
|
}
|
|
720
930
|
}
|
|
721
|
-
const roleLists = root
|
|
931
|
+
const roleLists = selectAll(root, '[role="list"]');
|
|
722
932
|
for (const list of Array.from(roleLists)) {
|
|
723
933
|
const children = Array.from(list.children);
|
|
724
934
|
const hasListItems = children.some(
|
|
@@ -738,7 +948,7 @@ function checkListSemantics(root) {
|
|
|
738
948
|
}
|
|
739
949
|
function checkTabOrder(root) {
|
|
740
950
|
const violations = [];
|
|
741
|
-
const tabbable = root
|
|
951
|
+
const tabbable = selectAll(root, "[tabindex]");
|
|
742
952
|
for (const el of Array.from(tabbable)) {
|
|
743
953
|
const tabindex = parseInt(el.getAttribute("tabindex") ?? "", 10);
|
|
744
954
|
if (!Number.isNaN(tabindex) && tabindex > 0) {
|
|
@@ -751,7 +961,7 @@ function checkTabOrder(root) {
|
|
|
751
961
|
});
|
|
752
962
|
}
|
|
753
963
|
}
|
|
754
|
-
const focusTraps = root
|
|
964
|
+
const focusTraps = selectAll(root, '[data-sibu-focus-trap], [aria-modal="true"]');
|
|
755
965
|
for (const trap of Array.from(focusTraps)) {
|
|
756
966
|
const role = trap.getAttribute("role");
|
|
757
967
|
if (role === "dialog" || role === "alertdialog" || trap.hasAttribute("aria-modal")) {
|
|
@@ -857,8 +1067,8 @@ function assertA11y(root, options) {
|
|
|
857
1067
|
}
|
|
858
1068
|
|
|
859
1069
|
// src/testing/adapters.ts
|
|
860
|
-
function
|
|
861
|
-
return value.replace(/["
|
|
1070
|
+
function escapeAttrValue(value) {
|
|
1071
|
+
return value.replace(/[\\"]/g, "\\$&").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ").replace(/\0/g, "\\fffd ");
|
|
862
1072
|
}
|
|
863
1073
|
function createJestAdapter() {
|
|
864
1074
|
let container = null;
|
|
@@ -873,7 +1083,7 @@ function createJestAdapter() {
|
|
|
873
1083
|
/** Call in afterEach to clean up */
|
|
874
1084
|
teardown() {
|
|
875
1085
|
if (container) {
|
|
876
|
-
container
|
|
1086
|
+
replaceChildrenSafely(container);
|
|
877
1087
|
if (container.parentNode) {
|
|
878
1088
|
container.parentNode.removeChild(container);
|
|
879
1089
|
}
|
|
@@ -991,23 +1201,32 @@ function createCypressAdapter() {
|
|
|
991
1201
|
return {
|
|
992
1202
|
/** Mount a component for Cypress component testing */
|
|
993
1203
|
mount(component, options) {
|
|
1204
|
+
const ownsContainer = !options?.container;
|
|
994
1205
|
const container = options?.container || document.createElement("div");
|
|
995
|
-
if (
|
|
1206
|
+
if (ownsContainer) {
|
|
996
1207
|
container.setAttribute("data-testenv", "cypress");
|
|
997
1208
|
document.body.appendChild(container);
|
|
998
1209
|
}
|
|
999
1210
|
const element = typeof component === "function" ? component() : component;
|
|
1000
1211
|
container.appendChild(element);
|
|
1001
|
-
|
|
1212
|
+
let unmounted = false;
|
|
1213
|
+
const unmount = () => {
|
|
1214
|
+
if (unmounted) return;
|
|
1215
|
+
unmounted = true;
|
|
1216
|
+
dispose(element);
|
|
1217
|
+
element.parentNode?.removeChild(element);
|
|
1218
|
+
if (ownsContainer) container.parentNode?.removeChild(container);
|
|
1219
|
+
};
|
|
1220
|
+
return { element, container, unmount };
|
|
1002
1221
|
},
|
|
1003
1222
|
/** Generate Cypress custom commands for SibuJS */
|
|
1004
1223
|
commands: {
|
|
1005
1224
|
/** Find by data-testid */
|
|
1006
|
-
getByTestId: (id) => `[data-testid="${id}"]`,
|
|
1225
|
+
getByTestId: (id) => `[data-testid="${escapeAttrValue(id)}"]`,
|
|
1007
1226
|
/** Find by text */
|
|
1008
|
-
getByText: (text) => `:contains("${text}")`,
|
|
1227
|
+
getByText: (text) => `:contains("${escapeAttrValue(text)}")`,
|
|
1009
1228
|
/** Find by role */
|
|
1010
|
-
getByRole: (role) => `[role="${role}"]`
|
|
1229
|
+
getByRole: (role) => `[role="${escapeAttrValue(role)}"]`
|
|
1011
1230
|
}
|
|
1012
1231
|
};
|
|
1013
1232
|
}
|
|
@@ -1015,10 +1234,10 @@ function createPlaywrightAdapter() {
|
|
|
1015
1234
|
return {
|
|
1016
1235
|
/** Selectors for common SibuJS patterns */
|
|
1017
1236
|
selectors: {
|
|
1018
|
-
byTestId: (id) => `[data-testid="${id}"]`,
|
|
1019
|
-
byRole: (role) => `[role="${role}"]`,
|
|
1020
|
-
byAriaLabel: (label) => `[aria-label="${label}"]`,
|
|
1021
|
-
byDataAttr: (attr, value) => value ? `[data-${attr}="${value}"]` : `[data-${attr}]`
|
|
1237
|
+
byTestId: (id) => `[data-testid="${escapeAttrValue(id)}"]`,
|
|
1238
|
+
byRole: (role) => `[role="${escapeAttrValue(role)}"]`,
|
|
1239
|
+
byAriaLabel: (label) => `[aria-label="${escapeAttrValue(label)}"]`,
|
|
1240
|
+
byDataAttr: (attr, value) => value ? `[data-${attr}="${escapeAttrValue(value)}"]` : `[data-${attr}]`
|
|
1022
1241
|
},
|
|
1023
1242
|
/** Generate a page object for a SibuJS component */
|
|
1024
1243
|
createPageObject(name, selectors) {
|
|
@@ -1054,7 +1273,7 @@ function createUniversalAdapter() {
|
|
|
1054
1273
|
/** Teardown test environment */
|
|
1055
1274
|
teardown() {
|
|
1056
1275
|
if (container) {
|
|
1057
|
-
container
|
|
1276
|
+
replaceChildrenSafely(container);
|
|
1058
1277
|
if (container.parentNode) {
|
|
1059
1278
|
container.parentNode.removeChild(container);
|
|
1060
1279
|
}
|
|
@@ -1075,10 +1294,10 @@ function createUniversalAdapter() {
|
|
|
1075
1294
|
/** Query helpers */
|
|
1076
1295
|
queries: {
|
|
1077
1296
|
byTestId(container2, id) {
|
|
1078
|
-
return container2
|
|
1297
|
+
return queryByAttribute(container2, "data-testid", id);
|
|
1079
1298
|
},
|
|
1080
1299
|
byRole(container2, role) {
|
|
1081
|
-
return container2
|
|
1300
|
+
return queryByAttribute(container2, "role", role);
|
|
1082
1301
|
},
|
|
1083
1302
|
byText(container2, text) {
|
|
1084
1303
|
const walker = document.createTreeWalker(container2, NodeFilter.SHOW_TEXT);
|
|
@@ -1095,16 +1314,16 @@ function createUniversalAdapter() {
|
|
|
1095
1314
|
if (labelEl.textContent?.includes(label)) {
|
|
1096
1315
|
const forAttr = labelEl.getAttribute("for");
|
|
1097
1316
|
if (forAttr) {
|
|
1098
|
-
return container2
|
|
1317
|
+
return queryByAttribute(container2, "id", forAttr);
|
|
1099
1318
|
}
|
|
1100
1319
|
const nested = labelEl.querySelector("input, select, textarea");
|
|
1101
1320
|
if (nested) return nested;
|
|
1102
1321
|
}
|
|
1103
1322
|
}
|
|
1104
|
-
return container2
|
|
1323
|
+
return queryByAttribute(container2, "aria-label", label);
|
|
1105
1324
|
},
|
|
1106
1325
|
allByRole(container2, role) {
|
|
1107
|
-
return
|
|
1326
|
+
return queryAllByAttribute(container2, "role", role);
|
|
1108
1327
|
}
|
|
1109
1328
|
},
|
|
1110
1329
|
/** Assertion helpers */
|
|
@@ -1151,58 +1370,226 @@ function createUniversalAdapter() {
|
|
|
1151
1370
|
};
|
|
1152
1371
|
}
|
|
1153
1372
|
|
|
1373
|
+
// src/testing/serializeDom.ts
|
|
1374
|
+
function escapeSnapshotText(value) {
|
|
1375
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1376
|
+
}
|
|
1377
|
+
function escapeSnapshotAttribute(value) {
|
|
1378
|
+
return escapeSnapshotText(value).replace(/"/g, """);
|
|
1379
|
+
}
|
|
1380
|
+
function escapeSnapshotComment(value) {
|
|
1381
|
+
return escapeSnapshotText(value).replace(/-/g, "-");
|
|
1382
|
+
}
|
|
1383
|
+
function serializeDom(el, indent = 0, options = {}) {
|
|
1384
|
+
const pad = " ".repeat(indent);
|
|
1385
|
+
const tag = el.tagName.toLowerCase();
|
|
1386
|
+
const attrs = Array.from(el.attributes).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((a) => `${a.name}="${escapeSnapshotAttribute(a.value)}"`).join(" ");
|
|
1387
|
+
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
1388
|
+
const children = Array.from(el.childNodes);
|
|
1389
|
+
if (children.length === 0) {
|
|
1390
|
+
return `${open}</${tag}>`;
|
|
1391
|
+
}
|
|
1392
|
+
if (children.length === 1 && children[0].nodeType === 3) {
|
|
1393
|
+
return `${open}${escapeSnapshotText(children[0].textContent?.trim() || "")}</${tag}>`;
|
|
1394
|
+
}
|
|
1395
|
+
const childPad = " ".repeat(indent + 1);
|
|
1396
|
+
const childStr = children.map((child) => {
|
|
1397
|
+
if (child.nodeType === 3) {
|
|
1398
|
+
const text = child.textContent?.trim();
|
|
1399
|
+
return text ? `${childPad}${escapeSnapshotText(text)}` : "";
|
|
1400
|
+
}
|
|
1401
|
+
if (child.nodeType === 1) {
|
|
1402
|
+
return serializeDom(child, indent + 1, options);
|
|
1403
|
+
}
|
|
1404
|
+
if (child.nodeType === 8 && options.comments) {
|
|
1405
|
+
return `${childPad}<!-- ${escapeSnapshotComment(child.textContent?.trim() || "")} -->`;
|
|
1406
|
+
}
|
|
1407
|
+
return "";
|
|
1408
|
+
}).filter(Boolean).join("\n");
|
|
1409
|
+
return `${open}
|
|
1410
|
+
${childStr}
|
|
1411
|
+
${pad}</${tag}>`;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1154
1414
|
// src/testing/e2e.ts
|
|
1155
1415
|
function createHttpMock(routes = [], options = {}) {
|
|
1156
1416
|
const originalFetch = globalThis.fetch;
|
|
1157
1417
|
const hadOriginalFetch = Object.hasOwn(globalThis, "fetch");
|
|
1158
1418
|
const requestLog = [];
|
|
1159
1419
|
const mockRoutes = [...routes];
|
|
1420
|
+
function matchesStringRoute(route, url) {
|
|
1421
|
+
if (url === route) return true;
|
|
1422
|
+
let target;
|
|
1423
|
+
try {
|
|
1424
|
+
target = new URL(url, "http://localhost");
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(route)) {
|
|
1429
|
+
return `${target.origin}${target.pathname}${target.search}` === route || target.href === route;
|
|
1430
|
+
}
|
|
1431
|
+
const hashIndex = route.indexOf("#");
|
|
1432
|
+
const bare = hashIndex === -1 ? route : route.slice(0, hashIndex);
|
|
1433
|
+
return bare.includes("?") ? `${target.pathname}${target.search}` === bare : target.pathname === bare;
|
|
1434
|
+
}
|
|
1160
1435
|
function matchRoute(url, method) {
|
|
1161
1436
|
return mockRoutes.find((route) => {
|
|
1162
1437
|
const methodMatch = !route.method || route.method.toUpperCase() === method.toUpperCase();
|
|
1163
1438
|
if (!methodMatch) return false;
|
|
1164
|
-
if (typeof route.url === "string") return
|
|
1439
|
+
if (typeof route.url === "string") return matchesStringRoute(route.url, url);
|
|
1440
|
+
route.url.lastIndex = 0;
|
|
1165
1441
|
return route.url.test(url);
|
|
1166
1442
|
});
|
|
1167
1443
|
}
|
|
1444
|
+
const abortError = (signal) => signal.reason !== void 0 ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
1445
|
+
const isFormEncoded = (type2) => /^application\/x-www-form-urlencoded\b/i.test(type2);
|
|
1446
|
+
const isTextLike = (type2) => /^text\/|[/+](?:json|xml|javascript)\b/i.test(type2);
|
|
1447
|
+
const decodeText = (text, type2) => {
|
|
1448
|
+
if (isFormEncoded(type2)) return new URLSearchParams(text);
|
|
1449
|
+
try {
|
|
1450
|
+
return JSON.parse(text);
|
|
1451
|
+
} catch {
|
|
1452
|
+
return text;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const httpBase = () => {
|
|
1456
|
+
const href = globalThis.location?.href;
|
|
1457
|
+
if (href) {
|
|
1458
|
+
try {
|
|
1459
|
+
if (/^https?:$/.test(new URL(href).protocol)) return href;
|
|
1460
|
+
} catch {
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return "http://localhost";
|
|
1464
|
+
};
|
|
1465
|
+
const STRUCTURED_CONTENT_TYPE = {
|
|
1466
|
+
FormData: () => "multipart/form-data",
|
|
1467
|
+
URLSearchParams: () => "application/x-www-form-urlencoded;charset=UTF-8",
|
|
1468
|
+
Blob: (raw) => raw.type || null,
|
|
1469
|
+
File: (raw) => raw.type || null
|
|
1470
|
+
};
|
|
1471
|
+
const AUTO_TEXT_TYPE = /^text\/plain;charset=utf-8$/i;
|
|
1472
|
+
const structuredTag = (raw) => {
|
|
1473
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
1474
|
+
const tag = Object.prototype.toString.call(raw).slice(8, -1);
|
|
1475
|
+
return Object.hasOwn(STRUCTURED_CONTENT_TYPE, tag) ? tag : null;
|
|
1476
|
+
};
|
|
1477
|
+
const unusableBody = async (raw, tag) => {
|
|
1478
|
+
let probe;
|
|
1479
|
+
try {
|
|
1480
|
+
probe = new Request("http://localhost/", { method: "POST", body: raw });
|
|
1481
|
+
} catch {
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
if (!AUTO_TEXT_TYPE.test(probe.headers.get("content-type") ?? "")) return false;
|
|
1485
|
+
return tag === "URLSearchParams" || await probe.text() === String(raw);
|
|
1486
|
+
};
|
|
1487
|
+
const toEffectiveRequest = (input, request, init, method, rawBody, omitBody) => {
|
|
1488
|
+
const overrides = {};
|
|
1489
|
+
const headers = init?.headers;
|
|
1490
|
+
if (method !== void 0) overrides.method = method;
|
|
1491
|
+
if (headers !== void 0) overrides.headers = headers;
|
|
1492
|
+
if (rawBody != null && !omitBody) {
|
|
1493
|
+
overrides.body = rawBody;
|
|
1494
|
+
if (typeof ReadableStream !== "undefined" && rawBody instanceof ReadableStream) overrides.duplex = "half";
|
|
1495
|
+
}
|
|
1496
|
+
if (request) return new Request(request.clone(), overrides);
|
|
1497
|
+
const raw = input instanceof URL ? input.href : input;
|
|
1498
|
+
const href = new URL(raw, httpBase()).href;
|
|
1499
|
+
return new Request(href, overrides);
|
|
1500
|
+
};
|
|
1501
|
+
const decodeBody = async (effective) => {
|
|
1502
|
+
if (effective.body === null) return void 0;
|
|
1503
|
+
const type2 = effective.headers.get("content-type") ?? "";
|
|
1504
|
+
if (/^multipart\/form-data\b/i.test(type2)) return effective.formData();
|
|
1505
|
+
if (isTextLike(type2) || isFormEncoded(type2)) return decodeText(await effective.text(), type2);
|
|
1506
|
+
return effective.blob();
|
|
1507
|
+
};
|
|
1168
1508
|
const mockFetch = async (input, init) => {
|
|
1509
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
1169
1510
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
1170
|
-
const
|
|
1511
|
+
const initSignal = init?.signal;
|
|
1512
|
+
const signal = initSignal === null ? void 0 : initSignal ?? request?.signal;
|
|
1513
|
+
if (signal?.aborted) throw abortError(signal);
|
|
1514
|
+
const rawBody = init?.body;
|
|
1515
|
+
const initMethod = init?.method;
|
|
1516
|
+
const tag = structuredTag(rawBody);
|
|
1517
|
+
const passThrough = tag !== null && await unusableBody(rawBody, tag);
|
|
1518
|
+
if (passThrough) {
|
|
1519
|
+
const intended = (initMethod ?? request?.method ?? "GET").toUpperCase();
|
|
1520
|
+
if (intended === "GET" || intended === "HEAD") {
|
|
1521
|
+
throw new TypeError(`Request with ${intended} method cannot have body.`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const effective = toEffectiveRequest(input, request, init, initMethod, rawBody, passThrough);
|
|
1525
|
+
const method = effective.method.toUpperCase();
|
|
1526
|
+
let headers = effective.headers;
|
|
1171
1527
|
let body;
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1528
|
+
if (passThrough && tag) {
|
|
1529
|
+
body = rawBody;
|
|
1530
|
+
if (!headers.has("content-type")) {
|
|
1531
|
+
const type2 = STRUCTURED_CONTENT_TYPE[tag](rawBody);
|
|
1532
|
+
if (type2) {
|
|
1533
|
+
headers = new Headers(headers);
|
|
1534
|
+
headers.set("content-type", type2);
|
|
1535
|
+
}
|
|
1178
1536
|
}
|
|
1537
|
+
} else {
|
|
1538
|
+
body = await decodeBody(effective);
|
|
1179
1539
|
}
|
|
1540
|
+
if (signal?.aborted) throw abortError(signal);
|
|
1180
1541
|
requestLog.push({ url, method, body, timestamp: Date.now() });
|
|
1181
1542
|
const route = matchRoute(url, method);
|
|
1182
1543
|
if (!route) {
|
|
1183
1544
|
return new Response(JSON.stringify({ error: "Not mocked" }), { status: 404 });
|
|
1184
1545
|
}
|
|
1546
|
+
const abortable = (work) => {
|
|
1547
|
+
if (!signal) return work;
|
|
1548
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
1549
|
+
return new Promise((resolve, reject) => {
|
|
1550
|
+
const onAbort = () => reject(abortError(signal));
|
|
1551
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1552
|
+
work.then(
|
|
1553
|
+
(value) => {
|
|
1554
|
+
signal.removeEventListener("abort", onAbort);
|
|
1555
|
+
resolve(value);
|
|
1556
|
+
},
|
|
1557
|
+
(err) => {
|
|
1558
|
+
signal.removeEventListener("abort", onAbort);
|
|
1559
|
+
reject(err);
|
|
1560
|
+
}
|
|
1561
|
+
);
|
|
1562
|
+
});
|
|
1563
|
+
};
|
|
1185
1564
|
let mockResponse;
|
|
1186
1565
|
try {
|
|
1187
1566
|
if (typeof route.response === "function") {
|
|
1188
|
-
|
|
1567
|
+
const handler = route.response;
|
|
1568
|
+
mockResponse = await abortable(Promise.resolve().then(() => handler({ url, method, body, headers })));
|
|
1189
1569
|
} else {
|
|
1190
1570
|
mockResponse = route.response;
|
|
1191
1571
|
}
|
|
1192
|
-
if (mockResponse.delay) {
|
|
1193
|
-
await new Promise((r) => setTimeout(r, mockResponse.delay));
|
|
1194
|
-
}
|
|
1195
|
-
return new Response(
|
|
1196
|
-
typeof mockResponse.body === "string" ? mockResponse.body : JSON.stringify(mockResponse.body),
|
|
1197
|
-
{
|
|
1198
|
-
status: mockResponse.status || 200,
|
|
1199
|
-
statusText: mockResponse.statusText || "OK",
|
|
1200
|
-
headers: mockResponse.headers
|
|
1201
|
-
}
|
|
1202
|
-
);
|
|
1203
1572
|
} catch (err) {
|
|
1573
|
+
if (signal?.aborted) throw abortError(signal);
|
|
1204
1574
|
return new Response(JSON.stringify({ error: String(err) }), { status: 500 });
|
|
1205
1575
|
}
|
|
1576
|
+
if (mockResponse.delay) {
|
|
1577
|
+
let timer;
|
|
1578
|
+
try {
|
|
1579
|
+
await abortable(
|
|
1580
|
+
new Promise((r) => {
|
|
1581
|
+
timer = setTimeout(r, mockResponse.delay);
|
|
1582
|
+
})
|
|
1583
|
+
);
|
|
1584
|
+
} finally {
|
|
1585
|
+
clearTimeout(timer);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return new Response(typeof mockResponse.body === "string" ? mockResponse.body : JSON.stringify(mockResponse.body), {
|
|
1589
|
+
status: mockResponse.status || 200,
|
|
1590
|
+
statusText: mockResponse.statusText || "OK",
|
|
1591
|
+
headers: mockResponse.headers
|
|
1592
|
+
});
|
|
1206
1593
|
};
|
|
1207
1594
|
const restore = () => {
|
|
1208
1595
|
if (hadOriginalFetch) {
|
|
@@ -1254,6 +1641,10 @@ function createHttpMock(routes = [], options = {}) {
|
|
|
1254
1641
|
}
|
|
1255
1642
|
return api;
|
|
1256
1643
|
}
|
|
1644
|
+
var MIN_INTERVAL_MS = 1;
|
|
1645
|
+
function normalizeIntervalDelay(delay) {
|
|
1646
|
+
return Number.isFinite(delay) && delay >= MIN_INTERVAL_MS ? delay : MIN_INTERVAL_MS;
|
|
1647
|
+
}
|
|
1257
1648
|
function createTimerMock(options = {}) {
|
|
1258
1649
|
const g = globalThis;
|
|
1259
1650
|
const snapshot = (key) => ({
|
|
@@ -1281,7 +1672,8 @@ function createTimerMock(options = {}) {
|
|
|
1281
1672
|
};
|
|
1282
1673
|
globalThis.setInterval = (cb, interval) => {
|
|
1283
1674
|
const id = nextId++;
|
|
1284
|
-
|
|
1675
|
+
const period = normalizeIntervalDelay(interval);
|
|
1676
|
+
timers.push({ id, callback: cb, time: currentTime + period, interval: period });
|
|
1285
1677
|
return id;
|
|
1286
1678
|
};
|
|
1287
1679
|
globalThis.clearTimeout = (id) => {
|
|
@@ -1321,7 +1713,7 @@ function createTimerMock(options = {}) {
|
|
|
1321
1713
|
if (!next) break;
|
|
1322
1714
|
currentTime = next.time;
|
|
1323
1715
|
const idx = timers.indexOf(next);
|
|
1324
|
-
if (next.interval) {
|
|
1716
|
+
if (next.interval !== void 0) {
|
|
1325
1717
|
next.time += next.interval;
|
|
1326
1718
|
} else {
|
|
1327
1719
|
timers.splice(idx, 1);
|
|
@@ -1338,13 +1730,21 @@ function createTimerMock(options = {}) {
|
|
|
1338
1730
|
timers.sort((a, b) => a.time - b.time);
|
|
1339
1731
|
const next = timers[0];
|
|
1340
1732
|
currentTime = next.time;
|
|
1341
|
-
if (next.interval) {
|
|
1733
|
+
if (next.interval !== void 0) {
|
|
1342
1734
|
next.time += next.interval;
|
|
1343
1735
|
} else {
|
|
1344
1736
|
timers.shift();
|
|
1345
1737
|
}
|
|
1346
1738
|
next.callback();
|
|
1347
1739
|
}
|
|
1740
|
+
if (timers.length > 0 && i > maxIterations) {
|
|
1741
|
+
reportError(
|
|
1742
|
+
new Error(
|
|
1743
|
+
`[createTimerMock] flush() stopped after ${maxIterations} timer runs with ${timers.length} still pending \u2014 likely a recurring interval.`
|
|
1744
|
+
),
|
|
1745
|
+
{ phase: "scheduler", name: "createTimerMock.flush" }
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1348
1748
|
},
|
|
1349
1749
|
/** Get current fake time */
|
|
1350
1750
|
now() {
|
|
@@ -1361,34 +1761,7 @@ function createTimerMock(options = {}) {
|
|
|
1361
1761
|
return api;
|
|
1362
1762
|
}
|
|
1363
1763
|
function createDOMSnapshot(element) {
|
|
1364
|
-
return
|
|
1365
|
-
}
|
|
1366
|
-
function serializeElement(el, indent) {
|
|
1367
|
-
const pad = " ".repeat(indent);
|
|
1368
|
-
const tag = el.tagName.toLowerCase();
|
|
1369
|
-
const attrs = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}="${a.value}"`).join(" ");
|
|
1370
|
-
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
1371
|
-
const children = Array.from(el.childNodes);
|
|
1372
|
-
if (children.length === 0) {
|
|
1373
|
-
return `${open}</${tag}>`;
|
|
1374
|
-
}
|
|
1375
|
-
if (children.length === 1 && children[0].nodeType === 3) {
|
|
1376
|
-
const text = children[0].textContent?.trim() || "";
|
|
1377
|
-
return `${open}${text}</${tag}>`;
|
|
1378
|
-
}
|
|
1379
|
-
const childStr = children.map((child) => {
|
|
1380
|
-
if (child.nodeType === 3) {
|
|
1381
|
-
const text = child.textContent?.trim();
|
|
1382
|
-
return text ? `${" ".repeat(indent + 1)}${text}` : "";
|
|
1383
|
-
}
|
|
1384
|
-
if (child.nodeType === 1) {
|
|
1385
|
-
return serializeElement(child, indent + 1);
|
|
1386
|
-
}
|
|
1387
|
-
return "";
|
|
1388
|
-
}).filter(Boolean).join("\n");
|
|
1389
|
-
return `${open}
|
|
1390
|
-
${childStr}
|
|
1391
|
-
${pad}</${tag}>`;
|
|
1764
|
+
return serializeDom(element);
|
|
1392
1765
|
}
|
|
1393
1766
|
function assertDOMEquals(actual, expected) {
|
|
1394
1767
|
const actualSnapshot = createDOMSnapshot(actual);
|
|
@@ -1412,10 +1785,10 @@ function testComponent(component, options = {}) {
|
|
|
1412
1785
|
element,
|
|
1413
1786
|
container,
|
|
1414
1787
|
getByTestId(id) {
|
|
1415
|
-
return container
|
|
1788
|
+
return queryByAttribute(container, "data-testid", id);
|
|
1416
1789
|
},
|
|
1417
1790
|
getAllByTestId(id) {
|
|
1418
|
-
return
|
|
1791
|
+
return queryAllByAttribute(container, "data-testid", id);
|
|
1419
1792
|
},
|
|
1420
1793
|
getByText(text) {
|
|
1421
1794
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
@@ -1438,160 +1811,15 @@ function testComponent(component, options = {}) {
|
|
|
1438
1811
|
await new Promise((r) => setTimeout(r, 0));
|
|
1439
1812
|
},
|
|
1440
1813
|
destroy() {
|
|
1814
|
+
replaceChildrenSafely(container);
|
|
1441
1815
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
1442
1816
|
}
|
|
1443
1817
|
};
|
|
1444
1818
|
}
|
|
1445
1819
|
|
|
1446
|
-
// src/testing/queries.ts
|
|
1447
|
-
function queryByText(container, text) {
|
|
1448
|
-
const walk = (node) => {
|
|
1449
|
-
if (node.childNodes.length === 1 && node.childNodes[0].nodeType === 3) {
|
|
1450
|
-
if (node.textContent?.includes(text)) return node;
|
|
1451
|
-
}
|
|
1452
|
-
for (const child of Array.from(node.children)) {
|
|
1453
|
-
const found = walk(child);
|
|
1454
|
-
if (found) return found;
|
|
1455
|
-
}
|
|
1456
|
-
return null;
|
|
1457
|
-
};
|
|
1458
|
-
return walk(container);
|
|
1459
|
-
}
|
|
1460
|
-
function queryByTestId(container, testId) {
|
|
1461
|
-
return container.querySelector(`[data-testid="${testId}"]`);
|
|
1462
|
-
}
|
|
1463
|
-
function queryByRole(container, role) {
|
|
1464
|
-
return container.querySelector(`[role="${role}"]`);
|
|
1465
|
-
}
|
|
1466
|
-
function cssEscape(value) {
|
|
1467
|
-
const g = globalThis;
|
|
1468
|
-
if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
|
|
1469
|
-
return value.replace(/[^\w-]/g, (m) => `\\${m.charCodeAt(0).toString(16)} `);
|
|
1470
|
-
}
|
|
1471
|
-
function queryByLabel(container, labelText) {
|
|
1472
|
-
const labels = Array.from(container.querySelectorAll("label"));
|
|
1473
|
-
for (const label of labels) {
|
|
1474
|
-
if (label.textContent?.trim() === labelText) {
|
|
1475
|
-
const forId = label.getAttribute("for");
|
|
1476
|
-
if (forId) {
|
|
1477
|
-
const target = container.querySelector(`#${cssEscape(forId)}`);
|
|
1478
|
-
if (target) return target;
|
|
1479
|
-
}
|
|
1480
|
-
const child = label.querySelector("input, select, textarea, button");
|
|
1481
|
-
if (child) return child;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return container.querySelector(`[aria-label="${labelText}"]`);
|
|
1485
|
-
}
|
|
1486
|
-
async function pollUntil(fn, timeout, interval, errorIfTimeout) {
|
|
1487
|
-
const start = Date.now();
|
|
1488
|
-
return new Promise((resolve, reject) => {
|
|
1489
|
-
const check = () => {
|
|
1490
|
-
const result = fn();
|
|
1491
|
-
if (result !== null) {
|
|
1492
|
-
resolve(result);
|
|
1493
|
-
return;
|
|
1494
|
-
}
|
|
1495
|
-
if (Date.now() - start >= timeout) {
|
|
1496
|
-
reject(new Error(errorIfTimeout));
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
setTimeout(check, interval);
|
|
1500
|
-
};
|
|
1501
|
-
check();
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
function findByText(container, text, options = {}) {
|
|
1505
|
-
return pollUntil(
|
|
1506
|
-
() => queryByText(container, text),
|
|
1507
|
-
options.timeout ?? 1e3,
|
|
1508
|
-
options.interval ?? 50,
|
|
1509
|
-
`findByText: no element with text "${text}" after ${options.timeout ?? 1e3}ms`
|
|
1510
|
-
);
|
|
1511
|
-
}
|
|
1512
|
-
function findByTestId(container, testId, options = {}) {
|
|
1513
|
-
return pollUntil(
|
|
1514
|
-
() => queryByTestId(container, testId),
|
|
1515
|
-
options.timeout ?? 1e3,
|
|
1516
|
-
options.interval ?? 50,
|
|
1517
|
-
`findByTestId: no element with data-testid="${testId}" after ${options.timeout ?? 1e3}ms`
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
function findByRole(container, role, options = {}) {
|
|
1521
|
-
return pollUntil(
|
|
1522
|
-
() => queryByRole(container, role),
|
|
1523
|
-
options.timeout ?? 1e3,
|
|
1524
|
-
options.interval ?? 50,
|
|
1525
|
-
`findByRole: no element with role="${role}" after ${options.timeout ?? 1e3}ms`
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
1528
|
-
function waitForSignal(getter, predicate, options = {}) {
|
|
1529
|
-
const timeoutMs = options.timeout ?? 1e3;
|
|
1530
|
-
return new Promise((resolve, reject) => {
|
|
1531
|
-
let resolved = false;
|
|
1532
|
-
let timer;
|
|
1533
|
-
const finish = (fn) => {
|
|
1534
|
-
if (resolved) return;
|
|
1535
|
-
resolved = true;
|
|
1536
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
1537
|
-
fn();
|
|
1538
|
-
};
|
|
1539
|
-
timer = setTimeout(() => {
|
|
1540
|
-
finish(() => {
|
|
1541
|
-
teardown();
|
|
1542
|
-
reject(new Error(`waitForSignal: predicate did not match within ${timeoutMs}ms`));
|
|
1543
|
-
});
|
|
1544
|
-
}, timeoutMs);
|
|
1545
|
-
const teardown = effect(() => {
|
|
1546
|
-
if (resolved) return;
|
|
1547
|
-
const value = getter();
|
|
1548
|
-
if (predicate(value)) {
|
|
1549
|
-
finish(() => {
|
|
1550
|
-
queueMicrotask(() => teardown());
|
|
1551
|
-
resolve(value);
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
});
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
1557
|
-
function type(element, text) {
|
|
1558
|
-
for (const char of text) {
|
|
1559
|
-
element.value += char;
|
|
1560
|
-
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: char }));
|
|
1561
|
-
}
|
|
1562
|
-
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
1820
|
// src/testing/snapshot.ts
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
const tag = el.tagName.toLowerCase();
|
|
1569
|
-
const attrs = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}="${a.value}"`).join(" ");
|
|
1570
|
-
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
1571
|
-
const children = Array.from(el.childNodes);
|
|
1572
|
-
if (children.length === 0) {
|
|
1573
|
-
return `${open}</${tag}>`;
|
|
1574
|
-
}
|
|
1575
|
-
if (children.length === 1 && children[0].nodeType === 3) {
|
|
1576
|
-
const text = children[0].textContent?.trim() || "";
|
|
1577
|
-
return `${open}${text}</${tag}>`;
|
|
1578
|
-
}
|
|
1579
|
-
const childStr = children.map((child) => {
|
|
1580
|
-
if (child.nodeType === 3) {
|
|
1581
|
-
const text = child.textContent?.trim();
|
|
1582
|
-
return text ? `${" ".repeat(indent + 1)}${text}` : "";
|
|
1583
|
-
}
|
|
1584
|
-
if (child.nodeType === 1) {
|
|
1585
|
-
return serializeElement2(child, indent + 1);
|
|
1586
|
-
}
|
|
1587
|
-
if (child.nodeType === 8) {
|
|
1588
|
-
return `${" ".repeat(indent + 1)}<!-- ${child.textContent?.trim() || ""} -->`;
|
|
1589
|
-
}
|
|
1590
|
-
return "";
|
|
1591
|
-
}).filter(Boolean).join("\n");
|
|
1592
|
-
return `${open}
|
|
1593
|
-
${childStr}
|
|
1594
|
-
${pad}</${tag}>`;
|
|
1821
|
+
function serializeElement(el, indent) {
|
|
1822
|
+
return serializeDom(el, indent, { comments: true });
|
|
1595
1823
|
}
|
|
1596
1824
|
function simpleDiff(a, b) {
|
|
1597
1825
|
const aLines = a.split("\n");
|
|
@@ -1653,7 +1881,11 @@ function snapshotComponent(component) {
|
|
|
1653
1881
|
const container = document.createElement("div");
|
|
1654
1882
|
const element = component();
|
|
1655
1883
|
container.appendChild(element);
|
|
1656
|
-
|
|
1884
|
+
try {
|
|
1885
|
+
return serializeElement(element, 0);
|
|
1886
|
+
} finally {
|
|
1887
|
+
replaceChildrenSafely(container);
|
|
1888
|
+
}
|
|
1657
1889
|
}
|
|
1658
1890
|
function matchSnapshot(store, name, component, options) {
|
|
1659
1891
|
const snapshot = snapshotComponent(component);
|
|
@@ -1709,31 +1941,57 @@ function walkElements(root) {
|
|
|
1709
1941
|
return elements;
|
|
1710
1942
|
}
|
|
1711
1943
|
function serializeStructure(el, indent) {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1944
|
+
return serializeDom(el, indent);
|
|
1945
|
+
}
|
|
1946
|
+
var APPEARANCE_PROPERTIES = [
|
|
1947
|
+
"display",
|
|
1948
|
+
"visibility",
|
|
1949
|
+
"opacity",
|
|
1950
|
+
"position",
|
|
1951
|
+
"color",
|
|
1952
|
+
"background-color",
|
|
1953
|
+
"background-image",
|
|
1954
|
+
"font-family",
|
|
1955
|
+
"font-size",
|
|
1956
|
+
"font-weight",
|
|
1957
|
+
"font-style",
|
|
1958
|
+
"line-height",
|
|
1959
|
+
"text-align",
|
|
1960
|
+
"text-decoration",
|
|
1961
|
+
"text-transform",
|
|
1962
|
+
"width",
|
|
1963
|
+
"height",
|
|
1964
|
+
"margin",
|
|
1965
|
+
"padding",
|
|
1966
|
+
"border",
|
|
1967
|
+
"border-radius",
|
|
1968
|
+
"box-shadow",
|
|
1969
|
+
"transform",
|
|
1970
|
+
"z-index",
|
|
1971
|
+
"overflow",
|
|
1972
|
+
"flex-direction",
|
|
1973
|
+
"justify-content",
|
|
1974
|
+
"align-items",
|
|
1975
|
+
"gap"
|
|
1976
|
+
];
|
|
1977
|
+
function captureComputedStyles(elements) {
|
|
1978
|
+
const view = elements[0]?.ownerDocument?.defaultView;
|
|
1979
|
+
if (!view || typeof view.getComputedStyle !== "function") return [];
|
|
1980
|
+
const counters = {};
|
|
1981
|
+
return elements.map((el) => {
|
|
1982
|
+
const tag = el.tagName.toLowerCase();
|
|
1983
|
+
counters[tag] = (counters[tag] || 0) + 1;
|
|
1984
|
+
let declarations = "";
|
|
1985
|
+
try {
|
|
1986
|
+
const style = view.getComputedStyle(el);
|
|
1987
|
+
for (const property of APPEARANCE_PROPERTIES) {
|
|
1988
|
+
const value = style.getPropertyValue(property);
|
|
1989
|
+
if (value) declarations += `${property}:${value};`;
|
|
1990
|
+
}
|
|
1991
|
+
} catch {
|
|
1731
1992
|
}
|
|
1732
|
-
return
|
|
1733
|
-
})
|
|
1734
|
-
return `${open}
|
|
1735
|
-
${childStr}
|
|
1736
|
-
${pad}</${tag}>`;
|
|
1993
|
+
return `${tag}[${counters[tag]}]: ${declarations}`;
|
|
1994
|
+
});
|
|
1737
1995
|
}
|
|
1738
1996
|
function captureFingerprint(element) {
|
|
1739
1997
|
const allElements = walkElements(element);
|
|
@@ -1771,14 +2029,16 @@ function captureFingerprint(element) {
|
|
|
1771
2029
|
}
|
|
1772
2030
|
}
|
|
1773
2031
|
}
|
|
1774
|
-
const
|
|
2032
|
+
const computedStyles = captureComputedStyles(allElements);
|
|
2033
|
+
const composite = JSON.stringify([
|
|
1775
2034
|
structure,
|
|
1776
2035
|
textContent,
|
|
1777
|
-
|
|
1778
|
-
classNames
|
|
1779
|
-
inlineStyles
|
|
1780
|
-
|
|
1781
|
-
|
|
2036
|
+
elementCounts,
|
|
2037
|
+
classNames,
|
|
2038
|
+
inlineStyles,
|
|
2039
|
+
dataAttributes,
|
|
2040
|
+
computedStyles
|
|
2041
|
+
]);
|
|
1782
2042
|
const hash = djb2Hash(composite);
|
|
1783
2043
|
return {
|
|
1784
2044
|
structure,
|
|
@@ -1787,6 +2047,7 @@ function captureFingerprint(element) {
|
|
|
1787
2047
|
classNames,
|
|
1788
2048
|
inlineStyles,
|
|
1789
2049
|
dataAttributes,
|
|
2050
|
+
computedStyles,
|
|
1790
2051
|
hash
|
|
1791
2052
|
};
|
|
1792
2053
|
}
|
|
@@ -1861,6 +2122,20 @@ function compareFingerprints(baseline, current) {
|
|
|
1861
2122
|
description: `Data attributes changed: ${dataChanges.join("; ")}`
|
|
1862
2123
|
});
|
|
1863
2124
|
}
|
|
2125
|
+
const baselineComputed = baseline.computedStyles ?? [];
|
|
2126
|
+
const currentComputed = current.computedStyles ?? [];
|
|
2127
|
+
const computedChanges = [];
|
|
2128
|
+
for (let i = 0; i < Math.max(baselineComputed.length, currentComputed.length); i++) {
|
|
2129
|
+
if (baselineComputed[i] !== currentComputed[i]) {
|
|
2130
|
+
computedChanges.push(currentComputed[i] ?? baselineComputed[i]);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
if (computedChanges.length > 0) {
|
|
2134
|
+
changes.push({
|
|
2135
|
+
type: "computed",
|
|
2136
|
+
description: `Computed styles changed on ${computedChanges.length} element(s): ${truncate(computedChanges.join(" | "), 200)}`
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
1864
2139
|
return {
|
|
1865
2140
|
match: changes.length === 0,
|
|
1866
2141
|
changes
|
|
@@ -1900,11 +2175,6 @@ function truncate(str, maxLen) {
|
|
|
1900
2175
|
}
|
|
1901
2176
|
|
|
1902
2177
|
// src/testing/index.ts
|
|
1903
|
-
function escapeSelector(value) {
|
|
1904
|
-
const g = globalThis;
|
|
1905
|
-
if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
|
|
1906
|
-
return value.replace(/[^\w-]/g, (m) => `\\${m.charCodeAt(0).toString(16)} `);
|
|
1907
|
-
}
|
|
1908
2178
|
var _renderedContainers = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.testing.containers.v1"), () => /* @__PURE__ */ new Set());
|
|
1909
2179
|
function unmountAll() {
|
|
1910
2180
|
for (const container of _renderedContainers) {
|
|
@@ -1933,10 +2203,10 @@ function render(component) {
|
|
|
1933
2203
|
return walk(container);
|
|
1934
2204
|
}
|
|
1935
2205
|
function getByTestId(testId) {
|
|
1936
|
-
return container
|
|
2206
|
+
return queryByAttribute(container, "data-testid", testId);
|
|
1937
2207
|
}
|
|
1938
2208
|
function getByRole(role) {
|
|
1939
|
-
return container
|
|
2209
|
+
return queryByAttribute(container, "role", role);
|
|
1940
2210
|
}
|
|
1941
2211
|
function queryAll(selector) {
|
|
1942
2212
|
return Array.from(container.querySelectorAll(selector));
|
|
@@ -2039,6 +2309,7 @@ export {
|
|
|
2039
2309
|
createTimerMock,
|
|
2040
2310
|
createUniversalAdapter,
|
|
2041
2311
|
createVisualSuite,
|
|
2312
|
+
enableListenerTracking,
|
|
2042
2313
|
findByRole,
|
|
2043
2314
|
findByTestId,
|
|
2044
2315
|
findByText,
|
|
@@ -2046,6 +2317,8 @@ export {
|
|
|
2046
2317
|
matchSnapshot,
|
|
2047
2318
|
mockRouter,
|
|
2048
2319
|
mockStore,
|
|
2320
|
+
queryAllByAttribute,
|
|
2321
|
+
queryByAttribute,
|
|
2049
2322
|
queryByLabel,
|
|
2050
2323
|
queryByRole,
|
|
2051
2324
|
queryByTestId,
|