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.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(testing_exports, {
|
|
|
45
45
|
createTimerMock: () => createTimerMock,
|
|
46
46
|
createUniversalAdapter: () => createUniversalAdapter,
|
|
47
47
|
createVisualSuite: () => createVisualSuite,
|
|
48
|
+
enableListenerTracking: () => enableListenerTracking,
|
|
48
49
|
findByRole: () => findByRole,
|
|
49
50
|
findByTestId: () => findByTestId,
|
|
50
51
|
findByText: () => findByText,
|
|
@@ -52,6 +53,8 @@ __export(testing_exports, {
|
|
|
52
53
|
matchSnapshot: () => matchSnapshot,
|
|
53
54
|
mockRouter: () => mockRouter,
|
|
54
55
|
mockStore: () => mockStore,
|
|
56
|
+
queryAllByAttribute: () => queryAllByAttribute,
|
|
57
|
+
queryByAttribute: () => queryByAttribute,
|
|
55
58
|
queryByLabel: () => queryByLabel,
|
|
56
59
|
queryByRole: () => queryByRole,
|
|
57
60
|
queryByTestId: () => queryByTestId,
|
|
@@ -181,6 +184,23 @@ function reportDrainRunaway(label, executed, remaining) {
|
|
|
181
184
|
);
|
|
182
185
|
}
|
|
183
186
|
var activeBindingCount = 0;
|
|
187
|
+
function resolveCapture(capture) {
|
|
188
|
+
let frame = capture;
|
|
189
|
+
while (frame?.closed) frame = frame.forwardTo;
|
|
190
|
+
return frame ?? null;
|
|
191
|
+
}
|
|
192
|
+
var registrationCaptures = [];
|
|
193
|
+
function currentDisposerCapture() {
|
|
194
|
+
return resolveCapture(registrationCaptures[registrationCaptures.length - 1]);
|
|
195
|
+
}
|
|
196
|
+
function beginDisposerCapture(capture) {
|
|
197
|
+
if (capture === null && registrationCaptures.length === 0) return false;
|
|
198
|
+
registrationCaptures.push(resolveCapture(capture));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
function endDisposerCapture() {
|
|
202
|
+
registrationCaptures.pop();
|
|
203
|
+
}
|
|
184
204
|
function dispose(node) {
|
|
185
205
|
const stack = [node];
|
|
186
206
|
const order = [];
|
|
@@ -193,33 +213,34 @@ function dispose(node) {
|
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
215
|
for (let i = order.length - 1; i >= 0; i--) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
disposeNodeOwn(order[i]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function disposeNodeOwn(node) {
|
|
220
|
+
if (!elementDisposers.has(node)) return;
|
|
221
|
+
let executed = 0;
|
|
222
|
+
let runaway = false;
|
|
223
|
+
while (!runaway) {
|
|
224
|
+
const pending = elementDisposers.get(node);
|
|
225
|
+
if (!pending || pending.length === 0) break;
|
|
226
|
+
const snapshot = pending.slice();
|
|
227
|
+
elementDisposers.delete(node);
|
|
228
|
+
if (DEV) activeBindingCount -= snapshot.length;
|
|
229
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
230
|
+
if (executed >= MAX_DRAIN_TEARDOWNS) {
|
|
231
|
+
const rest = snapshot.slice(i);
|
|
232
|
+
const added = elementDisposers.get(node);
|
|
233
|
+
elementDisposers.set(node, added ? rest.concat(added) : rest);
|
|
234
|
+
if (DEV) activeBindingCount += rest.length;
|
|
235
|
+
reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
|
|
236
|
+
runaway = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
executed++;
|
|
240
|
+
try {
|
|
241
|
+
snapshot[i]();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
reportError(err, { phase: "cleanup", name: "disposer" });
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
246
|
}
|
|
@@ -242,2291 +263,2537 @@ function globalSingleton(key, create) {
|
|
|
242
263
|
return g[key] ?? (g[key] = create());
|
|
243
264
|
}
|
|
244
265
|
|
|
245
|
-
// src/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"complementary",
|
|
261
|
-
"contentinfo",
|
|
262
|
-
"definition",
|
|
263
|
-
"dialog",
|
|
264
|
-
"directory",
|
|
265
|
-
"document",
|
|
266
|
-
"feed",
|
|
267
|
-
"figure",
|
|
268
|
-
"form",
|
|
269
|
-
"grid",
|
|
270
|
-
"gridcell",
|
|
271
|
-
"group",
|
|
272
|
-
"heading",
|
|
273
|
-
"img",
|
|
274
|
-
"link",
|
|
275
|
-
"list",
|
|
276
|
-
"listbox",
|
|
277
|
-
"listitem",
|
|
278
|
-
"log",
|
|
279
|
-
"main",
|
|
280
|
-
"marquee",
|
|
281
|
-
"math",
|
|
282
|
-
"menu",
|
|
283
|
-
"menubar",
|
|
284
|
-
"menuitem",
|
|
285
|
-
"menuitemcheckbox",
|
|
286
|
-
"menuitemradio",
|
|
287
|
-
"meter",
|
|
288
|
-
"navigation",
|
|
289
|
-
"none",
|
|
290
|
-
"note",
|
|
291
|
-
"option",
|
|
292
|
-
"presentation",
|
|
293
|
-
"progressbar",
|
|
294
|
-
"radio",
|
|
295
|
-
"radiogroup",
|
|
296
|
-
"region",
|
|
297
|
-
"row",
|
|
298
|
-
"rowgroup",
|
|
299
|
-
"rowheader",
|
|
300
|
-
"scrollbar",
|
|
301
|
-
"search",
|
|
302
|
-
"searchbox",
|
|
303
|
-
"separator",
|
|
304
|
-
"slider",
|
|
305
|
-
"spinbutton",
|
|
306
|
-
"status",
|
|
307
|
-
"switch",
|
|
308
|
-
"tab",
|
|
309
|
-
"table",
|
|
310
|
-
"tablist",
|
|
311
|
-
"tabpanel",
|
|
312
|
-
"term",
|
|
313
|
-
"textbox",
|
|
314
|
-
"timer",
|
|
315
|
-
"toolbar",
|
|
316
|
-
"tooltip",
|
|
317
|
-
"tree",
|
|
318
|
-
"treegrid",
|
|
319
|
-
"treeitem"
|
|
320
|
-
]);
|
|
321
|
-
var LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
322
|
-
"banner",
|
|
323
|
-
"complementary",
|
|
324
|
-
"contentinfo",
|
|
325
|
-
"form",
|
|
326
|
-
"main",
|
|
327
|
-
"navigation",
|
|
328
|
-
"region",
|
|
329
|
-
"search"
|
|
330
|
-
]);
|
|
331
|
-
var LANDMARK_ELEMENTS = {
|
|
332
|
-
header: "banner",
|
|
333
|
-
footer: "contentinfo",
|
|
334
|
-
main: "main",
|
|
335
|
-
nav: "navigation",
|
|
336
|
-
aside: "complementary",
|
|
337
|
-
form: "form",
|
|
338
|
-
section: "region"
|
|
339
|
-
};
|
|
340
|
-
var REQUIRED_ARIA_PROPS = {
|
|
341
|
-
checkbox: ["aria-checked"],
|
|
342
|
-
combobox: ["aria-expanded"],
|
|
343
|
-
heading: ["aria-level"],
|
|
344
|
-
meter: ["aria-valuenow"],
|
|
345
|
-
option: ["aria-selected"],
|
|
346
|
-
radio: ["aria-checked"],
|
|
347
|
-
scrollbar: ["aria-controls", "aria-valuenow"],
|
|
348
|
-
separator: [],
|
|
349
|
-
// only required when focusable
|
|
350
|
-
slider: ["aria-valuenow"],
|
|
351
|
-
spinbutton: ["aria-valuenow"],
|
|
352
|
-
switch: ["aria-checked"]
|
|
353
|
-
};
|
|
354
|
-
var VALID_ARIA_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
355
|
-
"aria-activedescendant",
|
|
356
|
-
"aria-atomic",
|
|
357
|
-
"aria-autocomplete",
|
|
358
|
-
"aria-busy",
|
|
359
|
-
"aria-checked",
|
|
360
|
-
"aria-colcount",
|
|
361
|
-
"aria-colindex",
|
|
362
|
-
"aria-colspan",
|
|
363
|
-
"aria-controls",
|
|
364
|
-
"aria-current",
|
|
365
|
-
"aria-describedby",
|
|
366
|
-
"aria-details",
|
|
367
|
-
"aria-disabled",
|
|
368
|
-
"aria-dropeffect",
|
|
369
|
-
"aria-errormessage",
|
|
370
|
-
"aria-expanded",
|
|
371
|
-
"aria-flowto",
|
|
372
|
-
"aria-grabbed",
|
|
373
|
-
"aria-haspopup",
|
|
374
|
-
"aria-hidden",
|
|
375
|
-
"aria-invalid",
|
|
376
|
-
"aria-keyshortcuts",
|
|
377
|
-
"aria-label",
|
|
378
|
-
"aria-labelledby",
|
|
379
|
-
"aria-level",
|
|
380
|
-
"aria-live",
|
|
381
|
-
"aria-modal",
|
|
382
|
-
"aria-multiline",
|
|
383
|
-
"aria-multiselectable",
|
|
384
|
-
"aria-orientation",
|
|
385
|
-
"aria-owns",
|
|
386
|
-
"aria-placeholder",
|
|
387
|
-
"aria-posinset",
|
|
388
|
-
"aria-pressed",
|
|
389
|
-
"aria-readonly",
|
|
390
|
-
"aria-relevant",
|
|
391
|
-
"aria-required",
|
|
392
|
-
"aria-roledescription",
|
|
393
|
-
"aria-rowcount",
|
|
394
|
-
"aria-rowindex",
|
|
395
|
-
"aria-rowspan",
|
|
396
|
-
"aria-selected",
|
|
397
|
-
"aria-setsize",
|
|
398
|
-
"aria-sort",
|
|
399
|
-
"aria-valuemax",
|
|
400
|
-
"aria-valuemin",
|
|
401
|
-
"aria-valuenow",
|
|
402
|
-
"aria-valuetext"
|
|
403
|
-
]);
|
|
404
|
-
function getSelector(el) {
|
|
405
|
-
if (el.id) return `#${el.id}`;
|
|
406
|
-
const tag = el.tagName.toLowerCase();
|
|
407
|
-
const classes = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).join(".")}` : "";
|
|
408
|
-
const role = el.getAttribute("role");
|
|
409
|
-
const roleStr = role ? `[role="${role}"]` : "";
|
|
410
|
-
return `${tag}${classes}${roleStr}`;
|
|
266
|
+
// src/reactivity/track-core.ts
|
|
267
|
+
var POOL_MAX = 4096;
|
|
268
|
+
var nodePool = [];
|
|
269
|
+
function createNode() {
|
|
270
|
+
return {
|
|
271
|
+
sig: null,
|
|
272
|
+
sub: null,
|
|
273
|
+
epoch: 0,
|
|
274
|
+
depVersion: 0,
|
|
275
|
+
sigPrev: null,
|
|
276
|
+
sigNext: null,
|
|
277
|
+
subPrev: null,
|
|
278
|
+
subNext: null,
|
|
279
|
+
prevActive: null
|
|
280
|
+
};
|
|
411
281
|
}
|
|
412
|
-
function
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const ref = doc.getElementById(id);
|
|
420
|
-
if (ref?.textContent?.trim()) return true;
|
|
421
|
-
}
|
|
282
|
+
function allocNode(sig, sub, epoch) {
|
|
283
|
+
const n = nodePool.pop();
|
|
284
|
+
if (n) {
|
|
285
|
+
n.sig = sig;
|
|
286
|
+
n.sub = sub;
|
|
287
|
+
n.epoch = epoch;
|
|
288
|
+
return n;
|
|
422
289
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
290
|
+
const fresh = createNode();
|
|
291
|
+
fresh.sig = sig;
|
|
292
|
+
fresh.sub = sub;
|
|
293
|
+
fresh.epoch = epoch;
|
|
294
|
+
return fresh;
|
|
426
295
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
296
|
+
function freeNode(node) {
|
|
297
|
+
node.sig = null;
|
|
298
|
+
node.sub = null;
|
|
299
|
+
node.sigPrev = null;
|
|
300
|
+
node.sigNext = null;
|
|
301
|
+
node.subPrev = null;
|
|
302
|
+
node.subNext = null;
|
|
303
|
+
node.prevActive = null;
|
|
304
|
+
if (nodePool.length < POOL_MAX) nodePool.push(node);
|
|
434
305
|
}
|
|
435
|
-
function
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
for (const el of Array.from(roleImgs)) {
|
|
459
|
-
if (!hasAccessibleName(el)) {
|
|
460
|
-
violations.push({
|
|
461
|
-
rule: "image-alt",
|
|
462
|
-
level: "error",
|
|
463
|
-
message: 'Element with role="img" must have an accessible name (aria-label or aria-labelledby).',
|
|
464
|
-
element: el,
|
|
465
|
-
selector: getSelector(el)
|
|
466
|
-
});
|
|
467
|
-
}
|
|
306
|
+
function linkSignal(sig, node) {
|
|
307
|
+
const oldHead = sig.subsHead ?? null;
|
|
308
|
+
node.sigPrev = null;
|
|
309
|
+
node.sigNext = oldHead;
|
|
310
|
+
if (oldHead) oldHead.sigPrev = node;
|
|
311
|
+
else sig.subsTail = node;
|
|
312
|
+
sig.subsHead = node;
|
|
313
|
+
sig.__sc = (sig.__sc ?? 0) + 1;
|
|
314
|
+
}
|
|
315
|
+
function unlinkSignal(node) {
|
|
316
|
+
const sig = node.sig;
|
|
317
|
+
if (!sig) return;
|
|
318
|
+
const prev = node.sigPrev;
|
|
319
|
+
const next = node.sigNext;
|
|
320
|
+
if (prev) prev.sigNext = next;
|
|
321
|
+
else sig.subsHead = next;
|
|
322
|
+
if (next) next.sigPrev = prev;
|
|
323
|
+
else sig.subsTail = prev;
|
|
324
|
+
sig.__sc = (sig.__sc ?? 1) - 1;
|
|
325
|
+
if (sig.__activeNode === node) sig.__activeNode = node.prevActive;
|
|
326
|
+
if (sig.__sc === 0) {
|
|
327
|
+
sig.subsHead = null;
|
|
328
|
+
sig.subsTail = null;
|
|
468
329
|
}
|
|
469
|
-
return violations;
|
|
470
330
|
}
|
|
471
|
-
function
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
331
|
+
function linkSub(sub, node) {
|
|
332
|
+
const oldTail = sub.depsTail ?? null;
|
|
333
|
+
node.subPrev = oldTail;
|
|
334
|
+
node.subNext = null;
|
|
335
|
+
if (oldTail) oldTail.subNext = node;
|
|
336
|
+
else sub.depsHead = node;
|
|
337
|
+
sub.depsTail = node;
|
|
338
|
+
}
|
|
339
|
+
function unlinkSub(node) {
|
|
340
|
+
const sub = node.sub;
|
|
341
|
+
if (!sub) return;
|
|
342
|
+
const prev = node.subPrev;
|
|
343
|
+
const next = node.subNext;
|
|
344
|
+
if (prev) prev.subNext = next;
|
|
345
|
+
else sub.depsHead = next;
|
|
346
|
+
if (next) next.subPrev = prev;
|
|
347
|
+
else sub.depsTail = prev;
|
|
348
|
+
}
|
|
349
|
+
var currentSubscriber = null;
|
|
350
|
+
var suspendSavedSub = null;
|
|
351
|
+
var notifyDepth = 0;
|
|
352
|
+
var pendingQueue = [];
|
|
353
|
+
var pendingSet = /* @__PURE__ */ new Set();
|
|
354
|
+
var propagateStack = [];
|
|
355
|
+
function safeInvoke(sub) {
|
|
356
|
+
try {
|
|
357
|
+
sub();
|
|
358
|
+
} catch (err) {
|
|
359
|
+
const s = sub;
|
|
360
|
+
reportError(err, { phase: s._errorPhase ?? "effect", name: s.__name, node: s._errorNode });
|
|
488
361
|
}
|
|
489
|
-
return violations;
|
|
490
362
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
363
|
+
function depsChanged(sub) {
|
|
364
|
+
let node = sub.depsHead ?? null;
|
|
365
|
+
if (node === null) return true;
|
|
366
|
+
while (node !== null) {
|
|
367
|
+
const sig = node.sig;
|
|
368
|
+
if (sig === null) return true;
|
|
369
|
+
if (sig._d === true && sig._validate !== void 0) {
|
|
370
|
+
try {
|
|
371
|
+
sig._validate();
|
|
372
|
+
} catch {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
500
375
|
}
|
|
376
|
+
if (node.depVersion !== sig.__v) return true;
|
|
377
|
+
node = node.subNext;
|
|
501
378
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
var suspendDepth = 0;
|
|
382
|
+
var trackingSuspended = false;
|
|
383
|
+
function suspendTracking() {
|
|
384
|
+
if (suspendDepth === 0) {
|
|
385
|
+
suspendSavedSub = currentSubscriber;
|
|
386
|
+
currentSubscriber = null;
|
|
387
|
+
trackingSuspended = true;
|
|
507
388
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
389
|
+
suspendDepth++;
|
|
390
|
+
}
|
|
391
|
+
function resumeTracking() {
|
|
392
|
+
suspendDepth--;
|
|
393
|
+
if (suspendDepth === 0) {
|
|
394
|
+
currentSubscriber = suspendSavedSub;
|
|
395
|
+
suspendSavedSub = null;
|
|
396
|
+
trackingSuspended = false;
|
|
512
397
|
}
|
|
513
|
-
return false;
|
|
514
398
|
}
|
|
515
|
-
function
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
399
|
+
function isTrackingSuspended() {
|
|
400
|
+
return trackingSuspended;
|
|
401
|
+
}
|
|
402
|
+
function untracked(fn) {
|
|
403
|
+
suspendTracking();
|
|
404
|
+
try {
|
|
405
|
+
return fn();
|
|
406
|
+
} finally {
|
|
407
|
+
resumeTracking();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
var subscriberEpochCounter = 0;
|
|
411
|
+
function retrack(effectFn, subscriber) {
|
|
412
|
+
const prev = currentSubscriber;
|
|
413
|
+
let savedDepth = 0;
|
|
414
|
+
let savedSuspendSub = null;
|
|
415
|
+
if (trackingSuspended) {
|
|
416
|
+
savedDepth = suspendDepth;
|
|
417
|
+
savedSuspendSub = suspendSavedSub;
|
|
418
|
+
suspendDepth = 0;
|
|
419
|
+
suspendSavedSub = null;
|
|
420
|
+
trackingSuspended = false;
|
|
421
|
+
}
|
|
422
|
+
currentSubscriber = subscriber;
|
|
423
|
+
const sub = subscriber;
|
|
424
|
+
if (sub._cap === void 0) sub._cap = currentDisposerCapture();
|
|
425
|
+
const epoch = ++subscriberEpochCounter;
|
|
426
|
+
sub._epoch = epoch;
|
|
427
|
+
sub._structDirty = false;
|
|
428
|
+
for (let n = sub.depsHead ?? null; n !== null; n = n.subNext) {
|
|
429
|
+
const sig = n.sig;
|
|
430
|
+
n.prevActive = sig.__activeNode ?? null;
|
|
431
|
+
sig.__activeNode = n;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
effectFn();
|
|
435
|
+
} finally {
|
|
436
|
+
currentSubscriber = prev;
|
|
437
|
+
if (savedDepth !== 0) {
|
|
438
|
+
suspendDepth = savedDepth;
|
|
439
|
+
suspendSavedSub = savedSuspendSub;
|
|
440
|
+
trackingSuspended = true;
|
|
530
441
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
442
|
+
let node = sub.depsHead ?? null;
|
|
443
|
+
while (node !== null) {
|
|
444
|
+
const next = node.subNext;
|
|
445
|
+
const sig = node.sig;
|
|
446
|
+
sig.__activeNode = node.prevActive;
|
|
447
|
+
node.prevActive = null;
|
|
448
|
+
if (node.epoch !== epoch) {
|
|
449
|
+
unlinkSub(node);
|
|
450
|
+
unlinkSignal(node);
|
|
451
|
+
freeNode(node);
|
|
452
|
+
}
|
|
453
|
+
node = next;
|
|
539
454
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function track(effectFn, subscriber) {
|
|
458
|
+
if (!subscriber) return reactiveBinding(effectFn);
|
|
459
|
+
cleanup(subscriber);
|
|
460
|
+
const prev = currentSubscriber;
|
|
461
|
+
let savedDepth = 0;
|
|
462
|
+
let savedSuspendSub = null;
|
|
463
|
+
if (trackingSuspended) {
|
|
464
|
+
savedDepth = suspendDepth;
|
|
465
|
+
savedSuspendSub = suspendSavedSub;
|
|
466
|
+
suspendDepth = 0;
|
|
467
|
+
suspendSavedSub = null;
|
|
468
|
+
trackingSuspended = false;
|
|
469
|
+
}
|
|
470
|
+
currentSubscriber = subscriber;
|
|
471
|
+
try {
|
|
472
|
+
effectFn();
|
|
473
|
+
} finally {
|
|
474
|
+
currentSubscriber = prev;
|
|
475
|
+
if (savedDepth !== 0) {
|
|
476
|
+
suspendDepth = savedDepth;
|
|
477
|
+
suspendSavedSub = savedSuspendSub;
|
|
478
|
+
trackingSuspended = true;
|
|
479
|
+
}
|
|
480
|
+
const sub2 = subscriber;
|
|
481
|
+
for (let n = sub2.depsHead ?? null; n !== null; n = n.subNext) {
|
|
482
|
+
const sig = n.sig;
|
|
483
|
+
sig.__activeNode = n.prevActive;
|
|
484
|
+
n.prevActive = null;
|
|
548
485
|
}
|
|
549
|
-
previousLevel = currentLevel;
|
|
550
486
|
}
|
|
551
|
-
|
|
487
|
+
const sub = subscriber;
|
|
488
|
+
return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
|
|
552
489
|
}
|
|
553
|
-
function
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
"alertdialog",
|
|
563
|
-
"button",
|
|
564
|
-
"checkbox",
|
|
565
|
-
"combobox",
|
|
566
|
-
"dialog",
|
|
567
|
-
"link",
|
|
568
|
-
"menuitem",
|
|
569
|
-
"menuitemcheckbox",
|
|
570
|
-
"menuitemradio",
|
|
571
|
-
"option",
|
|
572
|
-
"progressbar",
|
|
573
|
-
"radio",
|
|
574
|
-
"slider",
|
|
575
|
-
"spinbutton",
|
|
576
|
-
"switch",
|
|
577
|
-
"tab",
|
|
578
|
-
"textbox",
|
|
579
|
-
"treeitem"
|
|
580
|
-
]);
|
|
581
|
-
if (nameRequiredRoles.has(role) && !hasAccessibleName(el)) {
|
|
582
|
-
violations.push({
|
|
583
|
-
rule: "accessible-name",
|
|
584
|
-
level: "error",
|
|
585
|
-
message: `Element with role="${role}" must have an accessible name (via aria-label, aria-labelledby, or text content).`,
|
|
586
|
-
element: el,
|
|
587
|
-
selector: getSelector(el)
|
|
588
|
-
});
|
|
490
|
+
function reactiveBinding(commit, ownerNode) {
|
|
491
|
+
const run = () => {
|
|
492
|
+
const s = subscriber;
|
|
493
|
+
if (s._disposed || s._reentrant) return;
|
|
494
|
+
s._reentrant = true;
|
|
495
|
+
try {
|
|
496
|
+
retrack(commit, subscriber);
|
|
497
|
+
} finally {
|
|
498
|
+
s._reentrant = false;
|
|
589
499
|
}
|
|
500
|
+
};
|
|
501
|
+
const subscriber = run;
|
|
502
|
+
subscriber.depsHead = null;
|
|
503
|
+
subscriber.depsTail = null;
|
|
504
|
+
subscriber._epoch = 0;
|
|
505
|
+
subscriber._structDirty = false;
|
|
506
|
+
subscriber._hasComputedDep = false;
|
|
507
|
+
subscriber._runEpoch = 0;
|
|
508
|
+
subscriber._runs = 0;
|
|
509
|
+
subscriber._reentrant = false;
|
|
510
|
+
subscriber._disposed = false;
|
|
511
|
+
subscriber._errorPhase = "binding";
|
|
512
|
+
subscriber._errorNode = ownerNode;
|
|
513
|
+
try {
|
|
514
|
+
run();
|
|
515
|
+
} catch (err) {
|
|
516
|
+
subscriber._disposed = true;
|
|
517
|
+
subscriber._errorNode = void 0;
|
|
518
|
+
cleanup(subscriber);
|
|
519
|
+
throw err;
|
|
590
520
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
rule: "color-contrast",
|
|
608
|
-
level: "warning",
|
|
609
|
-
message: "Element sets background color but not text color. Ensure sufficient contrast with the inherited text color.",
|
|
610
|
-
element: el,
|
|
611
|
-
selector: getSelector(el)
|
|
612
|
-
});
|
|
613
|
-
}
|
|
521
|
+
return subscriber._dispose ?? (subscriber._dispose = () => {
|
|
522
|
+
subscriber._disposed = true;
|
|
523
|
+
subscriber._errorNode = void 0;
|
|
524
|
+
cleanup(subscriber);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function recordDependency(signal) {
|
|
528
|
+
if (!currentSubscriber) return;
|
|
529
|
+
const sub = currentSubscriber;
|
|
530
|
+
const sig = signal;
|
|
531
|
+
const epoch = sub._epoch ?? 0;
|
|
532
|
+
const active = sig.__activeNode ?? null;
|
|
533
|
+
if (active !== null && active.sub === sub) {
|
|
534
|
+
active.epoch = epoch;
|
|
535
|
+
active.depVersion = sig.__v ?? Number.NaN;
|
|
536
|
+
return;
|
|
614
537
|
}
|
|
615
|
-
|
|
538
|
+
if (sig._validate !== void 0) sub._hasComputedDep = true;
|
|
539
|
+
const node = allocNode(signal, sub, epoch);
|
|
540
|
+
node.depVersion = sig.__v ?? Number.NaN;
|
|
541
|
+
node.prevActive = active;
|
|
542
|
+
sig.__activeNode = node;
|
|
543
|
+
linkSub(sub, node);
|
|
544
|
+
linkSignal(sig, node);
|
|
545
|
+
sub._structDirty = true;
|
|
616
546
|
}
|
|
617
|
-
function
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
rule: "keyboard-access",
|
|
628
|
-
level: "error",
|
|
629
|
-
message: "Interactive element with onclick must have tabindex to be keyboard accessible.",
|
|
630
|
-
element: el,
|
|
631
|
-
selector: getSelector(el)
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
if (!hasRole) {
|
|
635
|
-
violations.push({
|
|
636
|
-
rule: "keyboard-access",
|
|
637
|
-
level: "warning",
|
|
638
|
-
message: 'Interactive element with onclick should have an explicit role attribute (e.g., role="button").',
|
|
639
|
-
element: el,
|
|
640
|
-
selector: getSelector(el)
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
if (!hasKeyHandler) {
|
|
644
|
-
violations.push({
|
|
645
|
-
rule: "keyboard-access",
|
|
646
|
-
level: "error",
|
|
647
|
-
message: "Interactive element with onclick must also have a keyboard event handler (onkeydown or onkeyup) for keyboard accessibility.",
|
|
648
|
-
element: el,
|
|
649
|
-
selector: getSelector(el)
|
|
650
|
-
});
|
|
651
|
-
}
|
|
547
|
+
function cleanup(subscriber) {
|
|
548
|
+
const sub = subscriber;
|
|
549
|
+
let node = sub.depsHead ?? null;
|
|
550
|
+
sub.depsHead = null;
|
|
551
|
+
sub.depsTail = null;
|
|
552
|
+
while (node) {
|
|
553
|
+
const next = node.subNext;
|
|
554
|
+
unlinkSignal(node);
|
|
555
|
+
freeNode(node);
|
|
556
|
+
node = next;
|
|
652
557
|
}
|
|
653
|
-
|
|
654
|
-
|
|
558
|
+
}
|
|
559
|
+
var maxSubscriberRepeats = 1e3;
|
|
560
|
+
var maxDrainIterations = 1e6;
|
|
561
|
+
var drainEpoch = 0;
|
|
562
|
+
var quarantined = /* @__PURE__ */ new Set();
|
|
563
|
+
function setMaxSubscriberRepeats(n) {
|
|
564
|
+
const prev = maxSubscriberRepeats;
|
|
565
|
+
if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
|
|
566
|
+
return prev;
|
|
567
|
+
}
|
|
568
|
+
function setMaxDrainIterations(n) {
|
|
569
|
+
const prev = maxDrainIterations;
|
|
570
|
+
if (Number.isFinite(n) && n > 0) maxDrainIterations = Math.floor(n);
|
|
571
|
+
return prev;
|
|
572
|
+
}
|
|
573
|
+
function tickRepeat(sub) {
|
|
574
|
+
const s = sub;
|
|
575
|
+
if (s._runEpoch !== drainEpoch) {
|
|
576
|
+
s._runEpoch = drainEpoch;
|
|
577
|
+
s._runs = 1;
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
s._runs = (s._runs ?? 0) + 1;
|
|
581
|
+
return s._runs > maxSubscriberRepeats;
|
|
582
|
+
}
|
|
583
|
+
function cycleError(sub) {
|
|
584
|
+
const name = sub.__name ?? "<unnamed>";
|
|
585
|
+
reportError(
|
|
586
|
+
new Error(
|
|
587
|
+
`subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. This subscriber is quarantined for the rest of this update; other pending work still runs.`
|
|
588
|
+
),
|
|
589
|
+
{ phase: "scheduler", name }
|
|
655
590
|
);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
591
|
+
}
|
|
592
|
+
function absoluteDrainError() {
|
|
593
|
+
reportError(
|
|
594
|
+
new Error(
|
|
595
|
+
`Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
|
|
596
|
+
),
|
|
597
|
+
{ phase: "scheduler" }
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
function drainQueue() {
|
|
601
|
+
let i = 0;
|
|
602
|
+
while (i < pendingQueue.length) {
|
|
603
|
+
if (i >= maxDrainIterations) {
|
|
604
|
+
absoluteDrainError();
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
const sub = pendingQueue[i++];
|
|
608
|
+
pendingSet.delete(sub);
|
|
609
|
+
if (quarantined.size > 0 && quarantined.has(sub)) continue;
|
|
610
|
+
if (sub._hasComputedDep === true && !depsChanged(sub)) continue;
|
|
611
|
+
if (tickRepeat(sub)) {
|
|
612
|
+
cycleError(sub);
|
|
613
|
+
quarantined.add(sub);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const pushed = beginDisposerCapture(sub._cap ?? null);
|
|
617
|
+
try {
|
|
618
|
+
safeInvoke(sub);
|
|
619
|
+
} finally {
|
|
620
|
+
if (pushed) endDisposerCapture();
|
|
666
621
|
}
|
|
667
622
|
}
|
|
668
|
-
return violations;
|
|
669
623
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
selector: getSelector(el)
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
if (REQUIRED_ARIA_PROPS[role]) {
|
|
686
|
-
for (const requiredProp of REQUIRED_ARIA_PROPS[role]) {
|
|
687
|
-
if (!el.hasAttribute(requiredProp)) {
|
|
688
|
-
violations.push({
|
|
689
|
-
rule: "aria-required-attr",
|
|
690
|
-
level: "error",
|
|
691
|
-
message: `Element with role="${role}" requires the ${requiredProp} attribute.`,
|
|
692
|
-
element: el,
|
|
693
|
-
selector: getSelector(el)
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}
|
|
624
|
+
function drainNotificationQueue() {
|
|
625
|
+
if (notifyDepth > 0) return;
|
|
626
|
+
notifyDepth++;
|
|
627
|
+
drainEpoch++;
|
|
628
|
+
try {
|
|
629
|
+
drainQueue();
|
|
630
|
+
} finally {
|
|
631
|
+
notifyDepth--;
|
|
632
|
+
if (notifyDepth === 0) {
|
|
633
|
+
pendingQueue.length = 0;
|
|
634
|
+
pendingSet.clear();
|
|
635
|
+
if (quarantined.size > 0) quarantined.clear();
|
|
697
636
|
}
|
|
698
637
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
});
|
|
638
|
+
}
|
|
639
|
+
function propagateDirty(sub) {
|
|
640
|
+
sub();
|
|
641
|
+
const rootSig = sub._sig;
|
|
642
|
+
if (!rootSig) return;
|
|
643
|
+
const stack = propagateStack;
|
|
644
|
+
const baseLen = stack.length;
|
|
645
|
+
stack.push(rootSig);
|
|
646
|
+
while (stack.length > baseLen) {
|
|
647
|
+
const sig = stack.pop();
|
|
648
|
+
let node = sig.subsHead ?? null;
|
|
649
|
+
while (node) {
|
|
650
|
+
const s = node.sub;
|
|
651
|
+
if (s) {
|
|
652
|
+
if (s._c) {
|
|
653
|
+
const nSig = s._sig;
|
|
654
|
+
if (nSig) {
|
|
655
|
+
if (!nSig._d || nSig._f === true) {
|
|
656
|
+
nSig._d = true;
|
|
657
|
+
nSig._f = false;
|
|
658
|
+
stack.push(nSig);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
s();
|
|
724
662
|
}
|
|
663
|
+
} else if (!pendingSet.has(s)) {
|
|
664
|
+
pendingSet.add(s);
|
|
665
|
+
pendingQueue.push(s);
|
|
725
666
|
}
|
|
726
667
|
}
|
|
727
|
-
|
|
728
|
-
"aria-checked",
|
|
729
|
-
"aria-disabled",
|
|
730
|
-
"aria-expanded",
|
|
731
|
-
"aria-hidden",
|
|
732
|
-
"aria-pressed",
|
|
733
|
-
"aria-required",
|
|
734
|
-
"aria-selected",
|
|
735
|
-
"aria-busy",
|
|
736
|
-
"aria-modal",
|
|
737
|
-
"aria-multiline",
|
|
738
|
-
"aria-multiselectable",
|
|
739
|
-
"aria-readonly"
|
|
740
|
-
]);
|
|
741
|
-
if (booleanAttrs.has(attr.name)) {
|
|
742
|
-
const validValues = ["true", "false"];
|
|
743
|
-
if (attr.name === "aria-checked" || attr.name === "aria-pressed") {
|
|
744
|
-
validValues.push("mixed");
|
|
745
|
-
}
|
|
746
|
-
if (!validValues.includes(attr.value)) {
|
|
747
|
-
violations.push({
|
|
748
|
-
rule: "aria-valid-attr-value",
|
|
749
|
-
level: "error",
|
|
750
|
-
message: `${attr.name} has invalid value "${attr.value}". Expected one of: ${validValues.join(", ")}.`,
|
|
751
|
-
element: el,
|
|
752
|
-
selector: getSelector(el)
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
for (const attr of Array.from(root.attributes)) {
|
|
759
|
-
if (!attr.name.startsWith("aria-")) continue;
|
|
760
|
-
if (!VALID_ARIA_ATTRIBUTES.has(attr.name)) {
|
|
761
|
-
violations.push({
|
|
762
|
-
rule: "aria-valid-attr",
|
|
763
|
-
level: "error",
|
|
764
|
-
message: `Invalid ARIA attribute "${attr.name}" on root element. Must be a valid WAI-ARIA attribute.`,
|
|
765
|
-
element: root,
|
|
766
|
-
selector: getSelector(root)
|
|
767
|
-
});
|
|
668
|
+
node = node.sigNext;
|
|
768
669
|
}
|
|
769
670
|
}
|
|
770
|
-
return violations;
|
|
771
671
|
}
|
|
772
|
-
function
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (elements.length > 0) {
|
|
784
|
-
foundLandmarks.add(role);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (!foundLandmarks.has("main")) {
|
|
788
|
-
violations.push({
|
|
789
|
-
rule: "landmark-main",
|
|
790
|
-
level: "warning",
|
|
791
|
-
message: 'Page should have a main landmark (<main> element or role="main").',
|
|
792
|
-
selector: getSelector(root)
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
const mainElements = root.querySelectorAll('main, [role="main"]');
|
|
796
|
-
if (mainElements.length > 1) {
|
|
797
|
-
violations.push({
|
|
798
|
-
rule: "landmark-unique",
|
|
799
|
-
level: "error",
|
|
800
|
-
message: `Multiple main landmarks found (${mainElements.length}). A page should have at most one main landmark.`,
|
|
801
|
-
selector: getSelector(root)
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
const bannerElements = root.querySelectorAll('header, [role="banner"]');
|
|
805
|
-
if (bannerElements.length > 1) {
|
|
806
|
-
violations.push({
|
|
807
|
-
rule: "landmark-unique",
|
|
808
|
-
level: "warning",
|
|
809
|
-
message: `Multiple banner landmarks found (${bannerElements.length}). Consider using a single banner landmark.`,
|
|
810
|
-
selector: getSelector(root)
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
const contentinfoElements = root.querySelectorAll('footer, [role="contentinfo"]');
|
|
814
|
-
if (contentinfoElements.length > 1) {
|
|
815
|
-
violations.push({
|
|
816
|
-
rule: "landmark-unique",
|
|
817
|
-
level: "warning",
|
|
818
|
-
message: `Multiple contentinfo landmarks found (${contentinfoElements.length}). Consider using a single contentinfo landmark.`,
|
|
819
|
-
selector: getSelector(root)
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
const navElements = root.querySelectorAll('nav, [role="navigation"]');
|
|
823
|
-
if (navElements.length > 1) {
|
|
824
|
-
for (const nav of Array.from(navElements)) {
|
|
825
|
-
if (!nav.getAttribute("aria-label") && !nav.getAttribute("aria-labelledby")) {
|
|
826
|
-
violations.push({
|
|
827
|
-
rule: "landmark-label",
|
|
828
|
-
level: "warning",
|
|
829
|
-
message: "When multiple navigation landmarks exist, each should have a unique label (aria-label or aria-labelledby).",
|
|
830
|
-
element: nav,
|
|
831
|
-
selector: getSelector(nav)
|
|
832
|
-
});
|
|
672
|
+
function queueSignalNotification(signal) {
|
|
673
|
+
const sig = signal;
|
|
674
|
+
let node = sig.subsHead ?? null;
|
|
675
|
+
while (node) {
|
|
676
|
+
const s = node.sub;
|
|
677
|
+
if (s) {
|
|
678
|
+
if (s._c) {
|
|
679
|
+
propagateDirty(s);
|
|
680
|
+
} else if (!pendingSet.has(s)) {
|
|
681
|
+
pendingSet.add(s);
|
|
682
|
+
pendingQueue.push(s);
|
|
833
683
|
}
|
|
834
684
|
}
|
|
685
|
+
node = node.sigNext;
|
|
835
686
|
}
|
|
836
|
-
return violations;
|
|
837
687
|
}
|
|
838
|
-
function
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
rule: "link-new-window",
|
|
854
|
-
level: "warning",
|
|
855
|
-
message: 'Link opens in a new window/tab (target="_blank"). Consider warning users or adding an icon to indicate this.',
|
|
856
|
-
element: link,
|
|
857
|
-
selector: getSelector(link)
|
|
858
|
-
});
|
|
859
|
-
const rel = link.getAttribute("rel") || "";
|
|
860
|
-
if (!rel.includes("noopener") && !rel.includes("noreferrer")) {
|
|
861
|
-
violations.push({
|
|
862
|
-
rule: "link-noopener",
|
|
863
|
-
level: "warning",
|
|
864
|
-
message: 'Link with target="_blank" should include rel="noopener" or rel="noreferrer" for security.',
|
|
865
|
-
element: link,
|
|
866
|
-
selector: getSelector(link)
|
|
867
|
-
});
|
|
688
|
+
function notifySubscribers(signal) {
|
|
689
|
+
const sig = signal;
|
|
690
|
+
const head = sig.subsHead;
|
|
691
|
+
if (!head) return;
|
|
692
|
+
if (notifyDepth > 0) {
|
|
693
|
+
let node = head;
|
|
694
|
+
while (node) {
|
|
695
|
+
const s = node.sub;
|
|
696
|
+
if (s) {
|
|
697
|
+
if (s._c) {
|
|
698
|
+
propagateDirty(s);
|
|
699
|
+
} else if (!pendingSet.has(s)) {
|
|
700
|
+
pendingSet.add(s);
|
|
701
|
+
pendingQueue.push(s);
|
|
702
|
+
}
|
|
868
703
|
}
|
|
704
|
+
node = node.sigNext;
|
|
869
705
|
}
|
|
870
|
-
|
|
871
|
-
const genericTexts = ["click here", "here", "read more", "more", "learn more", "link"];
|
|
872
|
-
if (genericTexts.includes(linkText)) {
|
|
873
|
-
violations.push({
|
|
874
|
-
rule: "link-purpose",
|
|
875
|
-
level: "warning",
|
|
876
|
-
message: `Link text "${link.textContent?.trim()}" is generic. Use descriptive text that explains the link's purpose.`,
|
|
877
|
-
element: link,
|
|
878
|
-
selector: getSelector(link)
|
|
879
|
-
});
|
|
880
|
-
}
|
|
706
|
+
return;
|
|
881
707
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
708
|
+
notifyDepth++;
|
|
709
|
+
drainEpoch++;
|
|
710
|
+
try {
|
|
711
|
+
let node = head;
|
|
712
|
+
while (node) {
|
|
713
|
+
const s = node.sub;
|
|
714
|
+
if (s) {
|
|
715
|
+
if (s._c) {
|
|
716
|
+
propagateDirty(s);
|
|
717
|
+
} else if (!pendingSet.has(s)) {
|
|
718
|
+
pendingSet.add(s);
|
|
719
|
+
pendingQueue.push(s);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
node = node.sigNext;
|
|
892
723
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
level: "error",
|
|
901
|
-
message: `Input button (type="${input.getAttribute("type")}") must have a value, aria-label, or aria-labelledby.`,
|
|
902
|
-
element: input,
|
|
903
|
-
selector: getSelector(input)
|
|
904
|
-
});
|
|
724
|
+
drainQueue();
|
|
725
|
+
} finally {
|
|
726
|
+
notifyDepth--;
|
|
727
|
+
if (notifyDepth === 0) {
|
|
728
|
+
pendingQueue.length = 0;
|
|
729
|
+
pendingSet.clear();
|
|
730
|
+
if (quarantined.size > 0) quarantined.clear();
|
|
905
731
|
}
|
|
906
732
|
}
|
|
907
|
-
return violations;
|
|
908
733
|
}
|
|
909
|
-
function
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
level: "error",
|
|
920
|
-
message: `Direct children of <${list.tagName.toLowerCase()}> must be <li> elements. Found <${tag}>.`,
|
|
921
|
-
element: child,
|
|
922
|
-
selector: getSelector(child)
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
if (children.length === 0) {
|
|
927
|
-
violations.push({
|
|
928
|
-
rule: "list-empty",
|
|
929
|
-
level: "info",
|
|
930
|
-
message: `Empty <${list.tagName.toLowerCase()}> element found. Consider removing or adding items.`,
|
|
931
|
-
element: list,
|
|
932
|
-
selector: getSelector(list)
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
const dlLists = root.querySelectorAll("dl");
|
|
937
|
-
for (const dl of Array.from(dlLists)) {
|
|
938
|
-
const children = Array.from(dl.children);
|
|
939
|
-
for (const child of children) {
|
|
940
|
-
const tag = child.tagName.toLowerCase();
|
|
941
|
-
if (tag !== "dt" && tag !== "dd" && tag !== "div" && tag !== "script" && tag !== "template") {
|
|
942
|
-
violations.push({
|
|
943
|
-
rule: "list-semantics",
|
|
944
|
-
level: "error",
|
|
945
|
-
message: `Direct children of <dl> must be <dt>, <dd>, or <div> elements. Found <${tag}>.`,
|
|
946
|
-
element: child,
|
|
947
|
-
selector: getSelector(child)
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
const roleLists = root.querySelectorAll('[role="list"]');
|
|
953
|
-
for (const list of Array.from(roleLists)) {
|
|
954
|
-
const children = Array.from(list.children);
|
|
955
|
-
const hasListItems = children.some(
|
|
956
|
-
(child) => child.getAttribute("role") === "listitem" || child.tagName.toLowerCase() === "li"
|
|
957
|
-
);
|
|
958
|
-
if (children.length > 0 && !hasListItems) {
|
|
959
|
-
violations.push({
|
|
960
|
-
rule: "list-semantics",
|
|
961
|
-
level: "warning",
|
|
962
|
-
message: 'Element with role="list" should contain children with role="listitem".',
|
|
963
|
-
element: list,
|
|
964
|
-
selector: getSelector(list)
|
|
965
|
-
});
|
|
966
|
-
}
|
|
734
|
+
function getSubscriberCount(signal) {
|
|
735
|
+
return signal.__sc ?? 0;
|
|
736
|
+
}
|
|
737
|
+
function getSubscriberDeps(subscriber) {
|
|
738
|
+
const sub = subscriber;
|
|
739
|
+
const out = [];
|
|
740
|
+
let node = sub.depsHead ?? null;
|
|
741
|
+
while (node) {
|
|
742
|
+
if (node.sig) out.push(node.sig);
|
|
743
|
+
node = node.subNext;
|
|
967
744
|
}
|
|
968
|
-
return
|
|
745
|
+
return out;
|
|
969
746
|
}
|
|
970
|
-
function
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
violations.push({
|
|
977
|
-
rule: "tabindex-positive",
|
|
978
|
-
level: "warning",
|
|
979
|
-
message: `Element has tabindex="${tabindex}". Positive tabindex values create a confusing tab order. Use tabindex="0" instead and rely on DOM order.`,
|
|
980
|
-
element: el,
|
|
981
|
-
selector: getSelector(el)
|
|
982
|
-
});
|
|
983
|
-
}
|
|
747
|
+
function forEachSubscriber(signal, visit) {
|
|
748
|
+
let node = signal.subsHead ?? null;
|
|
749
|
+
while (node) {
|
|
750
|
+
const s = node.sub;
|
|
751
|
+
if (s) visit(s);
|
|
752
|
+
node = node.sigNext;
|
|
984
753
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/reactivity/track.ts
|
|
757
|
+
var _runtimeVersion = true ? "4.7.0" : "dev";
|
|
758
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
|
|
759
|
+
function resolveReactiveApi() {
|
|
760
|
+
const g = globalThis;
|
|
761
|
+
const existing = g[REGISTRY_KEY];
|
|
762
|
+
if (existing) {
|
|
763
|
+
if (DEV && !existing.__dupWarned) {
|
|
764
|
+
existing.__dupWarned = true;
|
|
765
|
+
devWarn(
|
|
766
|
+
`Multiple instances of the reactive runtime detected on this page (active: ${existing.version}, duplicate: ${_runtimeVersion}). Reactivity is NOT broken and no bundler configuration is required: every copy routes through the first one via a global registry, so signal writes still reach subscribers registered by any copy. This is only a SIZE issue \u2014 the duplicate copy's bytes are downloaded and parsed for nothing. If you want them back, de-duplicate sibujs in your bundler (Vite: resolve.dedupe: ['sibujs']). Mismatched VERSIONS above are worth fixing regardless, since only the first one loaded is used.`
|
|
991
767
|
);
|
|
992
|
-
if (!hasCloseButton) {
|
|
993
|
-
violations.push({
|
|
994
|
-
rule: "focus-trap-escape",
|
|
995
|
-
level: "warning",
|
|
996
|
-
message: "Modal/dialog with focus trap should have a visible close button. Users must be able to dismiss the dialog.",
|
|
997
|
-
element: trap,
|
|
998
|
-
selector: getSelector(trap)
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
const focusable = trap.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
1003
|
-
if (focusable.length === 0) {
|
|
1004
|
-
violations.push({
|
|
1005
|
-
rule: "focus-trap-empty",
|
|
1006
|
-
level: "error",
|
|
1007
|
-
message: "Focus trap contains no focusable elements. Users will be trapped with no way to interact.",
|
|
1008
|
-
element: trap,
|
|
1009
|
-
selector: getSelector(trap)
|
|
1010
|
-
});
|
|
1011
768
|
}
|
|
769
|
+
return existing;
|
|
1012
770
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
"keyboard",
|
|
1033
|
-
"aria",
|
|
1034
|
-
"landmarks",
|
|
1035
|
-
"links",
|
|
1036
|
-
"lists",
|
|
1037
|
-
"tabOrder"
|
|
1038
|
-
];
|
|
1039
|
-
function checkA11y(root, options) {
|
|
1040
|
-
const checksToRun = options?.checks || ALL_CHECKS;
|
|
1041
|
-
const minLevel = options?.level || "info";
|
|
1042
|
-
const levelPriority = {
|
|
1043
|
-
error: 2,
|
|
1044
|
-
warning: 1,
|
|
1045
|
-
info: 0
|
|
771
|
+
const local = {
|
|
772
|
+
suspendTracking,
|
|
773
|
+
resumeTracking,
|
|
774
|
+
isTrackingSuspended,
|
|
775
|
+
untracked,
|
|
776
|
+
retrack,
|
|
777
|
+
track,
|
|
778
|
+
reactiveBinding,
|
|
779
|
+
recordDependency,
|
|
780
|
+
cleanup,
|
|
781
|
+
setMaxSubscriberRepeats,
|
|
782
|
+
setMaxDrainIterations,
|
|
783
|
+
drainNotificationQueue,
|
|
784
|
+
queueSignalNotification,
|
|
785
|
+
notifySubscribers,
|
|
786
|
+
getSubscriberCount,
|
|
787
|
+
getSubscriberDeps,
|
|
788
|
+
forEachSubscriber,
|
|
789
|
+
version: _runtimeVersion
|
|
1046
790
|
};
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
791
|
+
g[REGISTRY_KEY] = local;
|
|
792
|
+
return local;
|
|
793
|
+
}
|
|
794
|
+
var API = resolveReactiveApi();
|
|
795
|
+
var suspendTracking2 = API.suspendTracking;
|
|
796
|
+
var resumeTracking2 = API.resumeTracking;
|
|
797
|
+
var isTrackingSuspended2 = API.isTrackingSuspended;
|
|
798
|
+
var untracked2 = API.untracked;
|
|
799
|
+
var retrack2 = API.retrack;
|
|
800
|
+
var track2 = API.track;
|
|
801
|
+
var reactiveBinding2 = API.reactiveBinding;
|
|
802
|
+
var recordDependency2 = API.recordDependency;
|
|
803
|
+
var cleanup2 = API.cleanup;
|
|
804
|
+
var setMaxSubscriberRepeats2 = API.setMaxSubscriberRepeats;
|
|
805
|
+
var setMaxDrainIterations2 = API.setMaxDrainIterations;
|
|
806
|
+
var drainNotificationQueue2 = API.drainNotificationQueue;
|
|
807
|
+
var queueSignalNotification2 = API.queueSignalNotification;
|
|
808
|
+
var notifySubscribers2 = API.notifySubscribers;
|
|
809
|
+
var getSubscriberCount2 = API.getSubscriberCount;
|
|
810
|
+
var getSubscriberDeps2 = API.getSubscriberDeps;
|
|
811
|
+
var forEachSubscriber2 = API.forEachSubscriber;
|
|
812
|
+
|
|
813
|
+
// src/core/devtoolsHook.ts
|
|
814
|
+
function emitDevtools(hook, event, payload) {
|
|
815
|
+
try {
|
|
816
|
+
hook.emit(event, payload);
|
|
817
|
+
} catch {
|
|
1055
818
|
}
|
|
1056
|
-
const filtered = allViolations.filter((v) => levelPriority[v.level] >= minPriority);
|
|
1057
|
-
const violations = filtered.filter((v) => v.level === "error");
|
|
1058
|
-
const warnings = filtered.filter((v) => v.level === "warning");
|
|
1059
|
-
const info = filtered.filter((v) => v.level === "info");
|
|
1060
|
-
const parts = [];
|
|
1061
|
-
if (violations.length > 0) parts.push(`${violations.length} error(s)`);
|
|
1062
|
-
if (warnings.length > 0) parts.push(`${warnings.length} warning(s)`);
|
|
1063
|
-
if (info.length > 0) parts.push(`${info.length} info`);
|
|
1064
|
-
const passed = violations.length === 0;
|
|
1065
|
-
const summary = passed ? parts.length > 0 ? `Passed with ${parts.join(", ")}.` : "Passed with no issues." : `Failed with ${parts.join(", ")}.`;
|
|
1066
|
-
return { passed, violations, warnings, info, summary };
|
|
1067
819
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
for (const w of result.warnings) {
|
|
1082
|
-
lines.push(` [${w.rule}] ${w.message}${w.selector ? ` (${w.selector})` : ""}`);
|
|
820
|
+
|
|
821
|
+
// src/core/ssr-context.ts
|
|
822
|
+
var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
|
|
823
|
+
function detectSSRShared() {
|
|
824
|
+
let detected = null;
|
|
825
|
+
try {
|
|
826
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
827
|
+
let mod = null;
|
|
828
|
+
const getBuiltin = process.getBuiltinModule;
|
|
829
|
+
if (typeof getBuiltin === "function") {
|
|
830
|
+
mod = getBuiltin("node:async_hooks");
|
|
831
|
+
} else if (typeof require === "function") {
|
|
832
|
+
mod = require("async_hooks");
|
|
1083
833
|
}
|
|
1084
|
-
|
|
834
|
+
if (mod) detected = new mod.AsyncLocalStorage();
|
|
1085
835
|
}
|
|
1086
|
-
|
|
836
|
+
} catch {
|
|
837
|
+
detected = null;
|
|
1087
838
|
}
|
|
839
|
+
return {
|
|
840
|
+
als: detected,
|
|
841
|
+
fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
|
|
842
|
+
fallbackDepth: 0
|
|
843
|
+
};
|
|
1088
844
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
845
|
+
var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
|
|
846
|
+
var als = _shared.als;
|
|
847
|
+
var fallbackStore = _shared.fallbackStore;
|
|
848
|
+
_shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
|
|
849
|
+
function getSSRStore() {
|
|
850
|
+
if (als) {
|
|
851
|
+
const s = als.getStore();
|
|
852
|
+
if (s) return s;
|
|
853
|
+
}
|
|
854
|
+
return fallbackStore;
|
|
1093
855
|
}
|
|
1094
|
-
function
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
856
|
+
function isSSR() {
|
|
857
|
+
return getSSRStore().ssr;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/core/signals/effect.ts
|
|
861
|
+
var _g = globalThis;
|
|
862
|
+
var MAX_RERUNS = 100;
|
|
863
|
+
function flushUserCleanups(ctx) {
|
|
864
|
+
const list = ctx.userCleanups;
|
|
865
|
+
if (list.length === 0) return;
|
|
866
|
+
ctx.userCleanups = [];
|
|
867
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
868
|
+
try {
|
|
869
|
+
list[i]();
|
|
870
|
+
} catch (err) {
|
|
871
|
+
reportError(err, { phase: "cleanup" });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function drainReruns(ctx) {
|
|
876
|
+
let reruns = 1;
|
|
877
|
+
do {
|
|
878
|
+
ctx.rerunPending = false;
|
|
879
|
+
if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
|
|
880
|
+
retrack2(ctx.bodyFn, ctx.subscriber);
|
|
881
|
+
} while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
|
|
882
|
+
if (ctx.rerunPending) {
|
|
883
|
+
ctx.rerunPending = false;
|
|
884
|
+
reportError(
|
|
885
|
+
new Error(
|
|
886
|
+
`effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
|
|
887
|
+
),
|
|
888
|
+
{ phase: "effect" }
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function disposeEffect(ctx) {
|
|
893
|
+
if (ctx.disposed) return;
|
|
894
|
+
ctx.disposed = true;
|
|
895
|
+
const h = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
|
|
896
|
+
if (h) {
|
|
897
|
+
try {
|
|
898
|
+
h.emit("effect:destroy", { effectFn: ctx.fn });
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
if (typeof console !== "undefined") {
|
|
906
|
+
console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
cleanup2(ctx.subscriber);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
if (typeof console !== "undefined") {
|
|
913
|
+
console.warn("[SibuJS effect] dispose threw:", err);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function effect(effectFn, options) {
|
|
918
|
+
devAssert(typeof effectFn === "function", "effect: argument must be a function.");
|
|
919
|
+
if (isSSR()) return () => {
|
|
920
|
+
};
|
|
921
|
+
const ctx = {
|
|
922
|
+
fn: effectFn,
|
|
923
|
+
onError: options?.onError,
|
|
924
|
+
userCleanups: [],
|
|
925
|
+
running: false,
|
|
926
|
+
rerunPending: false,
|
|
927
|
+
disposed: false,
|
|
928
|
+
onCleanup: null,
|
|
929
|
+
subscriber: null,
|
|
930
|
+
bodyFn: null
|
|
931
|
+
};
|
|
932
|
+
ctx.onCleanup = (fn) => {
|
|
933
|
+
ctx.userCleanups.push(fn);
|
|
934
|
+
};
|
|
935
|
+
const onErrorCaptured = ctx.onError;
|
|
936
|
+
ctx.bodyFn = onErrorCaptured ? () => {
|
|
937
|
+
try {
|
|
938
|
+
ctx.fn(ctx.onCleanup);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
onErrorCaptured(err);
|
|
941
|
+
}
|
|
942
|
+
} : () => {
|
|
943
|
+
ctx.fn(ctx.onCleanup);
|
|
944
|
+
};
|
|
945
|
+
const sub = (() => {
|
|
946
|
+
if (ctx.disposed) return;
|
|
947
|
+
if (ctx.running) {
|
|
948
|
+
ctx.rerunPending = true;
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
ctx.running = true;
|
|
952
|
+
try {
|
|
953
|
+
ctx.rerunPending = false;
|
|
954
|
+
if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
|
|
955
|
+
retrack2(ctx.bodyFn, sub);
|
|
956
|
+
if (ctx.rerunPending) drainReruns(ctx);
|
|
957
|
+
} finally {
|
|
958
|
+
ctx.running = false;
|
|
959
|
+
ctx.rerunPending = false;
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
sub.depsHead = null;
|
|
963
|
+
sub.depsTail = null;
|
|
964
|
+
sub._epoch = 0;
|
|
965
|
+
sub._structDirty = false;
|
|
966
|
+
sub._hasComputedDep = false;
|
|
967
|
+
sub._errorPhase = "effect";
|
|
968
|
+
sub._runEpoch = 0;
|
|
969
|
+
sub._runs = 0;
|
|
970
|
+
ctx.subscriber = sub;
|
|
971
|
+
ctx.running = true;
|
|
972
|
+
try {
|
|
973
|
+
retrack2(ctx.bodyFn, ctx.subscriber);
|
|
974
|
+
if (ctx.rerunPending) drainReruns(ctx);
|
|
975
|
+
} catch (err) {
|
|
976
|
+
ctx.disposed = true;
|
|
977
|
+
if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
|
|
978
|
+
cleanup2(ctx.subscriber);
|
|
979
|
+
throw err;
|
|
980
|
+
} finally {
|
|
981
|
+
ctx.running = false;
|
|
982
|
+
ctx.rerunPending = false;
|
|
983
|
+
}
|
|
984
|
+
const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
|
|
985
|
+
if (hook) emitDevtools(hook, "effect:create", { effectFn });
|
|
986
|
+
return () => disposeEffect(ctx);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/testing/queries.ts
|
|
990
|
+
function queryByText(container, text) {
|
|
991
|
+
const walk = (node) => {
|
|
992
|
+
if (node.childNodes.length === 1 && node.childNodes[0].nodeType === 3) {
|
|
993
|
+
if (node.textContent?.includes(text)) return node;
|
|
994
|
+
}
|
|
995
|
+
for (const child of Array.from(node.children)) {
|
|
996
|
+
const found = walk(child);
|
|
997
|
+
if (found) return found;
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
};
|
|
1001
|
+
return walk(container);
|
|
1002
|
+
}
|
|
1003
|
+
function queryByAttribute(container, attribute, value) {
|
|
1004
|
+
for (const el of Array.from(container.querySelectorAll(`[${attribute}]`))) {
|
|
1005
|
+
if (el.getAttribute(attribute) === value) return el;
|
|
1006
|
+
}
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
function queryAllByAttribute(container, attribute, value) {
|
|
1010
|
+
return Array.from(container.querySelectorAll(`[${attribute}]`)).filter(
|
|
1011
|
+
(el) => el.getAttribute(attribute) === value
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
function queryByTestId(container, testId) {
|
|
1015
|
+
return queryByAttribute(container, "data-testid", testId);
|
|
1016
|
+
}
|
|
1017
|
+
function queryByRole(container, role) {
|
|
1018
|
+
return queryByAttribute(container, "role", role);
|
|
1019
|
+
}
|
|
1020
|
+
function queryByLabel(container, labelText) {
|
|
1021
|
+
const labels = Array.from(container.querySelectorAll("label"));
|
|
1022
|
+
for (const label of labels) {
|
|
1023
|
+
if (label.textContent?.trim() === labelText) {
|
|
1024
|
+
const forId = label.getAttribute("for");
|
|
1025
|
+
if (forId) {
|
|
1026
|
+
const target = queryByAttribute(container, "id", forId);
|
|
1027
|
+
if (target) return target;
|
|
1112
1028
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1029
|
+
const child = label.querySelector("input, select, textarea, button");
|
|
1030
|
+
if (child) return child;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return queryByAttribute(container, "aria-label", labelText);
|
|
1034
|
+
}
|
|
1035
|
+
async function pollUntil(fn, timeout, interval, errorIfTimeout) {
|
|
1036
|
+
const start = Date.now();
|
|
1037
|
+
return new Promise((resolve, reject) => {
|
|
1038
|
+
let timer;
|
|
1039
|
+
const check = () => {
|
|
1040
|
+
timer = void 0;
|
|
1041
|
+
let result;
|
|
1042
|
+
try {
|
|
1043
|
+
result = fn();
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
reject(err);
|
|
1046
|
+
return;
|
|
1120
1047
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
toHaveTextContent(element, expected) {
|
|
1129
|
-
const actual = element.textContent || "";
|
|
1130
|
-
const pass = actual.includes(expected);
|
|
1131
|
-
return {
|
|
1132
|
-
pass,
|
|
1133
|
-
message: () => pass ? `Expected element not to have text content "${expected}", but found "${actual}"` : `Expected element to have text content "${expected}", but found "${actual}"`
|
|
1134
|
-
};
|
|
1135
|
-
},
|
|
1136
|
-
/** Check if element has specific attribute */
|
|
1137
|
-
toHaveAttribute(element, attr, value) {
|
|
1138
|
-
const hasAttr = element.hasAttribute(attr);
|
|
1139
|
-
const attrValue = element.getAttribute(attr);
|
|
1140
|
-
const pass = value !== void 0 ? hasAttr && attrValue === value : hasAttr;
|
|
1141
|
-
return {
|
|
1142
|
-
pass,
|
|
1143
|
-
message: () => {
|
|
1144
|
-
if (value !== void 0) {
|
|
1145
|
-
return pass ? `Expected element not to have attribute "${attr}" with value "${value}", but it does` : `Expected element to have attribute "${attr}" with value "${value}", but got "${attrValue}"`;
|
|
1146
|
-
}
|
|
1147
|
-
return pass ? `Expected element not to have attribute "${attr}", but it does` : `Expected element to have attribute "${attr}", but it does not`;
|
|
1148
|
-
}
|
|
1149
|
-
};
|
|
1150
|
-
},
|
|
1151
|
-
/** Check if element has specific class */
|
|
1152
|
-
toHaveClass(element, className) {
|
|
1153
|
-
const pass = element.classList.contains(className);
|
|
1154
|
-
return {
|
|
1155
|
-
pass,
|
|
1156
|
-
message: () => pass ? `Expected element not to have class "${className}", but it does` : `Expected element to have class "${className}", but it has "${element.className}"`
|
|
1157
|
-
};
|
|
1158
|
-
},
|
|
1159
|
-
/** Check if element is visible (no display:none, visibility:hidden, hidden attr) */
|
|
1160
|
-
toBeVisible(element) {
|
|
1161
|
-
const htmlEl = element;
|
|
1162
|
-
const isHiddenAttr = element.hasAttribute("hidden");
|
|
1163
|
-
const style = htmlEl.style;
|
|
1164
|
-
const isDisplayNone = style?.display === "none";
|
|
1165
|
-
const isVisibilityHidden = style?.visibility === "hidden";
|
|
1166
|
-
const isOpacityZero = style?.opacity === "0";
|
|
1167
|
-
const pass = !isHiddenAttr && !isDisplayNone && !isVisibilityHidden && !isOpacityZero;
|
|
1168
|
-
return {
|
|
1169
|
-
pass,
|
|
1170
|
-
message: () => {
|
|
1171
|
-
if (pass) {
|
|
1172
|
-
return "Expected element to be hidden, but it is visible";
|
|
1173
|
-
}
|
|
1174
|
-
const reasons = [];
|
|
1175
|
-
if (isHiddenAttr) reasons.push('has "hidden" attribute');
|
|
1176
|
-
if (isDisplayNone) reasons.push("has display:none");
|
|
1177
|
-
if (isVisibilityHidden) reasons.push("has visibility:hidden");
|
|
1178
|
-
if (isOpacityZero) reasons.push("has opacity:0");
|
|
1179
|
-
return `Expected element to be visible, but it ${reasons.join(" and ")}`;
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
},
|
|
1183
|
-
/** Check if element is disabled */
|
|
1184
|
-
toBeDisabled(element) {
|
|
1185
|
-
const isDisabledProp = element.disabled === true;
|
|
1186
|
-
const isDisabledAttr = element.hasAttribute("disabled");
|
|
1187
|
-
const isAriaDisabled = element.getAttribute("aria-disabled") === "true";
|
|
1188
|
-
const pass = isDisabledProp || isDisabledAttr || isAriaDisabled;
|
|
1189
|
-
return {
|
|
1190
|
-
pass,
|
|
1191
|
-
message: () => pass ? "Expected element not to be disabled, but it is" : "Expected element to be disabled, but it is not"
|
|
1192
|
-
};
|
|
1193
|
-
},
|
|
1194
|
-
/** Check if element has focus */
|
|
1195
|
-
toHaveFocus(element) {
|
|
1196
|
-
const pass = document.activeElement === element;
|
|
1197
|
-
return {
|
|
1198
|
-
pass,
|
|
1199
|
-
message: () => pass ? "Expected element not to have focus, but it does" : `Expected element to have focus, but active element is <${document.activeElement?.tagName?.toLowerCase() || "none"}>`
|
|
1200
|
-
};
|
|
1201
|
-
},
|
|
1202
|
-
/** Check if element has specific style */
|
|
1203
|
-
toHaveStyle(element, style) {
|
|
1204
|
-
const mismatches = [];
|
|
1205
|
-
for (const [prop, expected] of Object.entries(style)) {
|
|
1206
|
-
const kebabProp = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
1207
|
-
const actual = element.style.getPropertyValue(kebabProp);
|
|
1208
|
-
if (actual !== expected) {
|
|
1209
|
-
mismatches.push(`${prop}: expected "${expected}" but got "${actual}"`);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const pass = mismatches.length === 0;
|
|
1213
|
-
return {
|
|
1214
|
-
pass,
|
|
1215
|
-
message: () => pass ? "Expected element not to have the specified styles, but it does" : `Style mismatches: ${mismatches.join("; ")}`
|
|
1216
|
-
};
|
|
1048
|
+
if (result !== null) {
|
|
1049
|
+
resolve(result);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (Date.now() - start >= timeout) {
|
|
1053
|
+
reject(new Error(errorIfTimeout));
|
|
1054
|
+
return;
|
|
1217
1055
|
}
|
|
1056
|
+
timer = setTimeout(check, interval);
|
|
1057
|
+
};
|
|
1058
|
+
check();
|
|
1059
|
+
void timer;
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function findByText(container, text, options = {}) {
|
|
1063
|
+
return pollUntil(
|
|
1064
|
+
() => queryByText(container, text),
|
|
1065
|
+
options.timeout ?? 1e3,
|
|
1066
|
+
options.interval ?? 50,
|
|
1067
|
+
`findByText: no element with text "${text}" after ${options.timeout ?? 1e3}ms`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
function findByTestId(container, testId, options = {}) {
|
|
1071
|
+
return pollUntil(
|
|
1072
|
+
() => queryByTestId(container, testId),
|
|
1073
|
+
options.timeout ?? 1e3,
|
|
1074
|
+
options.interval ?? 50,
|
|
1075
|
+
`findByTestId: no element with data-testid="${testId}" after ${options.timeout ?? 1e3}ms`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
function findByRole(container, role, options = {}) {
|
|
1079
|
+
return pollUntil(
|
|
1080
|
+
() => queryByRole(container, role),
|
|
1081
|
+
options.timeout ?? 1e3,
|
|
1082
|
+
options.interval ?? 50,
|
|
1083
|
+
`findByRole: no element with role="${role}" after ${options.timeout ?? 1e3}ms`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
function waitForSignal(getter, predicate, options = {}) {
|
|
1087
|
+
const timeoutMs = options.timeout ?? 1e3;
|
|
1088
|
+
return new Promise((resolve, reject) => {
|
|
1089
|
+
let resolved = false;
|
|
1090
|
+
let timer;
|
|
1091
|
+
const finish = (fn) => {
|
|
1092
|
+
if (resolved) return;
|
|
1093
|
+
resolved = true;
|
|
1094
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1095
|
+
fn();
|
|
1096
|
+
};
|
|
1097
|
+
timer = setTimeout(() => {
|
|
1098
|
+
finish(() => {
|
|
1099
|
+
teardown();
|
|
1100
|
+
reject(new Error(`waitForSignal: predicate did not match within ${timeoutMs}ms`));
|
|
1101
|
+
});
|
|
1102
|
+
}, timeoutMs);
|
|
1103
|
+
const teardown = effect(() => {
|
|
1104
|
+
if (resolved) return;
|
|
1105
|
+
const value = getter();
|
|
1106
|
+
if (predicate(value)) {
|
|
1107
|
+
finish(() => {
|
|
1108
|
+
queueMicrotask(() => teardown());
|
|
1109
|
+
resolve(value);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function type(element, text) {
|
|
1116
|
+
for (const char of text) {
|
|
1117
|
+
element.value += char;
|
|
1118
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: char }));
|
|
1119
|
+
}
|
|
1120
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/testing/listenerTracking.ts
|
|
1124
|
+
var KEY = /* @__PURE__ */ Symbol.for("sibujs.listenerTracking.v2");
|
|
1125
|
+
var host = globalThis;
|
|
1126
|
+
function captureOf(options) {
|
|
1127
|
+
if (typeof options === "boolean") return options;
|
|
1128
|
+
return typeof options === "object" && options !== null && !!options.capture;
|
|
1129
|
+
}
|
|
1130
|
+
function enableListenerTracking() {
|
|
1131
|
+
if (host[KEY] || typeof EventTarget === "undefined") return;
|
|
1132
|
+
const tracker = /* @__PURE__ */ new WeakMap();
|
|
1133
|
+
host[KEY] = tracker;
|
|
1134
|
+
const proto = EventTarget.prototype;
|
|
1135
|
+
const add = proto.addEventListener;
|
|
1136
|
+
const remove = proto.removeEventListener;
|
|
1137
|
+
const find = (target, type2, listener, capture) => tracker.get(target)?.get(type2)?.find((e) => e.listener === listener && e.capture === capture);
|
|
1138
|
+
const drop = (target, type2, entry) => {
|
|
1139
|
+
const types = tracker.get(target);
|
|
1140
|
+
const entries = types?.get(type2);
|
|
1141
|
+
if (!types || !entries) return;
|
|
1142
|
+
const index = entries.indexOf(entry);
|
|
1143
|
+
if (index === -1) return;
|
|
1144
|
+
entries.splice(index, 1);
|
|
1145
|
+
if (entries.length === 0) types.delete(type2);
|
|
1146
|
+
entry.release?.();
|
|
1147
|
+
};
|
|
1148
|
+
proto.addEventListener = function(type2, listener, options) {
|
|
1149
|
+
const tracked = listener != null && typeof Element !== "undefined" && this instanceof Element;
|
|
1150
|
+
const capture = captureOf(options);
|
|
1151
|
+
const opts = typeof options === "object" && options !== null ? options : void 0;
|
|
1152
|
+
if (!tracked || opts?.signal?.aborted || find(this, type2, listener, capture)) {
|
|
1153
|
+
add.call(this, type2, listener, options);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const entry = { listener, capture };
|
|
1157
|
+
const releases = [];
|
|
1158
|
+
if (opts?.once) {
|
|
1159
|
+
const onFire = () => drop(this, type2, entry);
|
|
1160
|
+
add.call(this, type2, onFire, { capture, once: true });
|
|
1161
|
+
releases.push(() => remove.call(this, type2, onFire, { capture }));
|
|
1162
|
+
}
|
|
1163
|
+
if (opts?.signal) {
|
|
1164
|
+
const signal = opts.signal;
|
|
1165
|
+
const onAbort = () => drop(this, type2, entry);
|
|
1166
|
+
add.call(signal, "abort", onAbort, { once: true });
|
|
1167
|
+
releases.push(() => remove.call(signal, "abort", onAbort));
|
|
1168
|
+
}
|
|
1169
|
+
if (releases.length > 0) {
|
|
1170
|
+
entry.release = () => {
|
|
1171
|
+
for (const release of releases) release();
|
|
1172
|
+
};
|
|
1218
1173
|
}
|
|
1174
|
+
let types = tracker.get(this);
|
|
1175
|
+
if (!types) tracker.set(this, types = /* @__PURE__ */ new Map());
|
|
1176
|
+
let entries = types.get(type2);
|
|
1177
|
+
if (!entries) types.set(type2, entries = []);
|
|
1178
|
+
entries.push(entry);
|
|
1179
|
+
add.call(this, type2, listener, options);
|
|
1180
|
+
};
|
|
1181
|
+
proto.removeEventListener = function(type2, listener, options) {
|
|
1182
|
+
remove.call(this, type2, listener, options);
|
|
1183
|
+
const entry = find(this, type2, listener, captureOf(options));
|
|
1184
|
+
if (entry) drop(this, type2, entry);
|
|
1219
1185
|
};
|
|
1220
1186
|
}
|
|
1221
|
-
function
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1187
|
+
function listenerTypes(el) {
|
|
1188
|
+
const types = host[KEY]?.get(el);
|
|
1189
|
+
return new Set(types ? types.keys() : []);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/testing/a11y.ts
|
|
1193
|
+
enableListenerTracking();
|
|
1194
|
+
var VALID_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
1195
|
+
"alert",
|
|
1196
|
+
"alertdialog",
|
|
1197
|
+
"application",
|
|
1198
|
+
"article",
|
|
1199
|
+
"banner",
|
|
1200
|
+
"button",
|
|
1201
|
+
"cell",
|
|
1202
|
+
"checkbox",
|
|
1203
|
+
"columnheader",
|
|
1204
|
+
"combobox",
|
|
1205
|
+
"complementary",
|
|
1206
|
+
"contentinfo",
|
|
1207
|
+
"definition",
|
|
1208
|
+
"dialog",
|
|
1209
|
+
"directory",
|
|
1210
|
+
"document",
|
|
1211
|
+
"feed",
|
|
1212
|
+
"figure",
|
|
1213
|
+
"form",
|
|
1214
|
+
"grid",
|
|
1215
|
+
"gridcell",
|
|
1216
|
+
"group",
|
|
1217
|
+
"heading",
|
|
1218
|
+
"img",
|
|
1219
|
+
"link",
|
|
1220
|
+
"list",
|
|
1221
|
+
"listbox",
|
|
1222
|
+
"listitem",
|
|
1223
|
+
"log",
|
|
1224
|
+
"main",
|
|
1225
|
+
"marquee",
|
|
1226
|
+
"math",
|
|
1227
|
+
"menu",
|
|
1228
|
+
"menubar",
|
|
1229
|
+
"menuitem",
|
|
1230
|
+
"menuitemcheckbox",
|
|
1231
|
+
"menuitemradio",
|
|
1232
|
+
"meter",
|
|
1233
|
+
"navigation",
|
|
1234
|
+
"none",
|
|
1235
|
+
"note",
|
|
1236
|
+
"option",
|
|
1237
|
+
"presentation",
|
|
1238
|
+
"progressbar",
|
|
1239
|
+
"radio",
|
|
1240
|
+
"radiogroup",
|
|
1241
|
+
"region",
|
|
1242
|
+
"row",
|
|
1243
|
+
"rowgroup",
|
|
1244
|
+
"rowheader",
|
|
1245
|
+
"scrollbar",
|
|
1246
|
+
"search",
|
|
1247
|
+
"searchbox",
|
|
1248
|
+
"separator",
|
|
1249
|
+
"slider",
|
|
1250
|
+
"spinbutton",
|
|
1251
|
+
"status",
|
|
1252
|
+
"switch",
|
|
1253
|
+
"tab",
|
|
1254
|
+
"table",
|
|
1255
|
+
"tablist",
|
|
1256
|
+
"tabpanel",
|
|
1257
|
+
"term",
|
|
1258
|
+
"textbox",
|
|
1259
|
+
"timer",
|
|
1260
|
+
"toolbar",
|
|
1261
|
+
"tooltip",
|
|
1262
|
+
"tree",
|
|
1263
|
+
"treegrid",
|
|
1264
|
+
"treeitem"
|
|
1265
|
+
]);
|
|
1266
|
+
var LANDMARK_ROLES = /* @__PURE__ */ new Set([
|
|
1267
|
+
"banner",
|
|
1268
|
+
"complementary",
|
|
1269
|
+
"contentinfo",
|
|
1270
|
+
"form",
|
|
1271
|
+
"main",
|
|
1272
|
+
"navigation",
|
|
1273
|
+
"region",
|
|
1274
|
+
"search"
|
|
1275
|
+
]);
|
|
1276
|
+
var LANDMARK_ELEMENTS = {
|
|
1277
|
+
header: "banner",
|
|
1278
|
+
footer: "contentinfo",
|
|
1279
|
+
main: "main",
|
|
1280
|
+
nav: "navigation",
|
|
1281
|
+
aside: "complementary",
|
|
1282
|
+
form: "form",
|
|
1283
|
+
section: "region"
|
|
1284
|
+
};
|
|
1285
|
+
var REQUIRED_ARIA_PROPS = {
|
|
1286
|
+
checkbox: ["aria-checked"],
|
|
1287
|
+
combobox: ["aria-expanded"],
|
|
1288
|
+
heading: ["aria-level"],
|
|
1289
|
+
meter: ["aria-valuenow"],
|
|
1290
|
+
option: ["aria-selected"],
|
|
1291
|
+
radio: ["aria-checked"],
|
|
1292
|
+
scrollbar: ["aria-controls", "aria-valuenow"],
|
|
1293
|
+
separator: [],
|
|
1294
|
+
// only required when focusable
|
|
1295
|
+
slider: ["aria-valuenow"],
|
|
1296
|
+
spinbutton: ["aria-valuenow"],
|
|
1297
|
+
switch: ["aria-checked"]
|
|
1298
|
+
};
|
|
1299
|
+
var VALID_ARIA_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1300
|
+
"aria-activedescendant",
|
|
1301
|
+
"aria-atomic",
|
|
1302
|
+
"aria-autocomplete",
|
|
1303
|
+
"aria-busy",
|
|
1304
|
+
"aria-checked",
|
|
1305
|
+
"aria-colcount",
|
|
1306
|
+
"aria-colindex",
|
|
1307
|
+
"aria-colspan",
|
|
1308
|
+
"aria-controls",
|
|
1309
|
+
"aria-current",
|
|
1310
|
+
"aria-describedby",
|
|
1311
|
+
"aria-details",
|
|
1312
|
+
"aria-disabled",
|
|
1313
|
+
"aria-dropeffect",
|
|
1314
|
+
"aria-errormessage",
|
|
1315
|
+
"aria-expanded",
|
|
1316
|
+
"aria-flowto",
|
|
1317
|
+
"aria-grabbed",
|
|
1318
|
+
"aria-haspopup",
|
|
1319
|
+
"aria-hidden",
|
|
1320
|
+
"aria-invalid",
|
|
1321
|
+
"aria-keyshortcuts",
|
|
1322
|
+
"aria-label",
|
|
1323
|
+
"aria-labelledby",
|
|
1324
|
+
"aria-level",
|
|
1325
|
+
"aria-live",
|
|
1326
|
+
"aria-modal",
|
|
1327
|
+
"aria-multiline",
|
|
1328
|
+
"aria-multiselectable",
|
|
1329
|
+
"aria-orientation",
|
|
1330
|
+
"aria-owns",
|
|
1331
|
+
"aria-placeholder",
|
|
1332
|
+
"aria-posinset",
|
|
1333
|
+
"aria-pressed",
|
|
1334
|
+
"aria-readonly",
|
|
1335
|
+
"aria-relevant",
|
|
1336
|
+
"aria-required",
|
|
1337
|
+
"aria-roledescription",
|
|
1338
|
+
"aria-rowcount",
|
|
1339
|
+
"aria-rowindex",
|
|
1340
|
+
"aria-rowspan",
|
|
1341
|
+
"aria-selected",
|
|
1342
|
+
"aria-setsize",
|
|
1343
|
+
"aria-sort",
|
|
1344
|
+
"aria-valuemax",
|
|
1345
|
+
"aria-valuemin",
|
|
1346
|
+
"aria-valuenow",
|
|
1347
|
+
"aria-valuetext"
|
|
1348
|
+
]);
|
|
1349
|
+
function getSelector(el) {
|
|
1350
|
+
if (el.id) return `#${el.id}`;
|
|
1351
|
+
const tag = el.tagName.toLowerCase();
|
|
1352
|
+
const classes = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).join(".")}` : "";
|
|
1353
|
+
const role = el.getAttribute("role");
|
|
1354
|
+
const roleStr = role ? `[role="${role}"]` : "";
|
|
1355
|
+
return `${tag}${classes}${roleStr}`;
|
|
1356
|
+
}
|
|
1357
|
+
function selectAll(root, selector) {
|
|
1358
|
+
const matches = Array.from(root.querySelectorAll(selector));
|
|
1359
|
+
if (root.matches(selector)) matches.unshift(root);
|
|
1360
|
+
return matches;
|
|
1361
|
+
}
|
|
1362
|
+
function hasActivationHandler(el) {
|
|
1363
|
+
if (el.hasAttribute("onclick")) return true;
|
|
1364
|
+
const types = listenerTypes(el);
|
|
1365
|
+
return ["click", "pointerdown", "pointerup", "mousedown", "mouseup"].some((t) => types.has(t));
|
|
1366
|
+
}
|
|
1367
|
+
function hasKeyboardHandler(el) {
|
|
1368
|
+
if (el.hasAttribute("onkeydown") || el.hasAttribute("onkeyup") || el.hasAttribute("onkeypress")) return true;
|
|
1369
|
+
const types = listenerTypes(el);
|
|
1370
|
+
return types.has("keydown") || types.has("keyup") || types.has("keypress");
|
|
1371
|
+
}
|
|
1372
|
+
function hasAccessibleName(el) {
|
|
1373
|
+
if (el.getAttribute("aria-label")?.trim()) return true;
|
|
1374
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
1375
|
+
if (labelledBy) {
|
|
1376
|
+
const doc = el.ownerDocument;
|
|
1377
|
+
const ids = labelledBy.split(/\s+/);
|
|
1378
|
+
for (const id of ids) {
|
|
1379
|
+
const ref = doc.getElementById(id);
|
|
1380
|
+
if (ref?.textContent?.trim()) return true;
|
|
1242
1381
|
}
|
|
1243
|
-
}
|
|
1382
|
+
}
|
|
1383
|
+
if (el.getAttribute("title")?.trim()) return true;
|
|
1384
|
+
if (el.textContent?.trim()) return true;
|
|
1385
|
+
return false;
|
|
1244
1386
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1387
|
+
function isNativeInteractive(el) {
|
|
1388
|
+
const tag = el.tagName.toLowerCase();
|
|
1389
|
+
if (["button", "select", "textarea", "summary"].includes(tag)) return true;
|
|
1390
|
+
if (tag === "input" && el.getAttribute("type") !== "hidden") return true;
|
|
1391
|
+
if (tag === "a" && el.hasAttribute("href")) return true;
|
|
1392
|
+
if (tag === "area" && el.hasAttribute("href")) return true;
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
var DELEGATES_ATTRIBUTE = "data-a11y-delegates";
|
|
1396
|
+
function checkImageAlt(root) {
|
|
1397
|
+
const violations = [];
|
|
1398
|
+
const images = selectAll(root, "img");
|
|
1399
|
+
for (const img of Array.from(images)) {
|
|
1400
|
+
if (!img.hasAttribute("alt")) {
|
|
1401
|
+
violations.push({
|
|
1402
|
+
rule: "image-alt",
|
|
1403
|
+
level: "error",
|
|
1404
|
+
message: 'Image element must have an alt attribute. Use alt="" for decorative images.',
|
|
1405
|
+
element: img,
|
|
1406
|
+
selector: getSelector(img)
|
|
1407
|
+
});
|
|
1408
|
+
} else if (img.getAttribute("alt") === "" && !img.getAttribute("role")) {
|
|
1409
|
+
violations.push({
|
|
1410
|
+
rule: "image-alt-empty",
|
|
1411
|
+
level: "info",
|
|
1412
|
+
message: "Image has empty alt text. Ensure this is a decorative image; otherwise provide meaningful alt text.",
|
|
1413
|
+
element: img,
|
|
1414
|
+
selector: getSelector(img)
|
|
1415
|
+
});
|
|
1272
1416
|
}
|
|
1273
|
-
}
|
|
1417
|
+
}
|
|
1418
|
+
const roleImgs = selectAll(root, '[role="img"]');
|
|
1419
|
+
for (const el of Array.from(roleImgs)) {
|
|
1420
|
+
if (!hasAccessibleName(el)) {
|
|
1421
|
+
violations.push({
|
|
1422
|
+
rule: "image-alt",
|
|
1423
|
+
level: "error",
|
|
1424
|
+
message: 'Element with role="img" must have an accessible name (aria-label or aria-labelledby).',
|
|
1425
|
+
element: el,
|
|
1426
|
+
selector: getSelector(el)
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return violations;
|
|
1274
1431
|
}
|
|
1275
|
-
function
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
container = null;
|
|
1293
|
-
}
|
|
1294
|
-
},
|
|
1295
|
-
/** Render component */
|
|
1296
|
-
render(component) {
|
|
1297
|
-
if (!container) {
|
|
1298
|
-
container = document.createElement("div");
|
|
1299
|
-
container.setAttribute("data-testenv", "universal");
|
|
1300
|
-
document.body.appendChild(container);
|
|
1301
|
-
}
|
|
1302
|
-
const element = typeof component === "function" ? component() : component;
|
|
1303
|
-
container.appendChild(element);
|
|
1304
|
-
return { element, container };
|
|
1305
|
-
},
|
|
1306
|
-
/** Query helpers */
|
|
1307
|
-
queries: {
|
|
1308
|
-
byTestId(container2, id) {
|
|
1309
|
-
return container2.querySelector(`[data-testid="${escSel2(id)}"]`);
|
|
1310
|
-
},
|
|
1311
|
-
byRole(container2, role) {
|
|
1312
|
-
return container2.querySelector(`[role="${escSel2(role)}"]`);
|
|
1313
|
-
},
|
|
1314
|
-
byText(container2, text) {
|
|
1315
|
-
const walker = document.createTreeWalker(container2, NodeFilter.SHOW_TEXT);
|
|
1316
|
-
while (walker.nextNode()) {
|
|
1317
|
-
if (walker.currentNode.textContent?.includes(text)) {
|
|
1318
|
-
return walker.currentNode.parentElement;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
return null;
|
|
1322
|
-
},
|
|
1323
|
-
byLabelText(container2, label) {
|
|
1324
|
-
const labels = container2.querySelectorAll("label");
|
|
1325
|
-
for (const labelEl of Array.from(labels)) {
|
|
1326
|
-
if (labelEl.textContent?.includes(label)) {
|
|
1327
|
-
const forAttr = labelEl.getAttribute("for");
|
|
1328
|
-
if (forAttr) {
|
|
1329
|
-
return container2.querySelector(`[id="${escSel2(forAttr)}"]`);
|
|
1330
|
-
}
|
|
1331
|
-
const nested = labelEl.querySelector("input, select, textarea");
|
|
1332
|
-
if (nested) return nested;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
return container2.querySelector(`[aria-label="${escSel2(label)}"]`);
|
|
1336
|
-
},
|
|
1337
|
-
allByRole(container2, role) {
|
|
1338
|
-
return Array.from(container2.querySelectorAll(`[role="${escSel2(role)}"]`));
|
|
1339
|
-
}
|
|
1340
|
-
},
|
|
1341
|
-
/** Assertion helpers */
|
|
1342
|
-
assert: {
|
|
1343
|
-
textContent(el, expected) {
|
|
1344
|
-
const actual = el.textContent || "";
|
|
1345
|
-
if (!actual.includes(expected)) {
|
|
1346
|
-
throw new Error(`Expected text content to include "${expected}", but got "${actual}"`);
|
|
1347
|
-
}
|
|
1348
|
-
},
|
|
1349
|
-
attribute(el, attr, value) {
|
|
1350
|
-
if (!el.hasAttribute(attr)) {
|
|
1351
|
-
throw new Error(`Expected element to have attribute "${attr}", but it does not`);
|
|
1352
|
-
}
|
|
1353
|
-
if (value !== void 0) {
|
|
1354
|
-
const actual = el.getAttribute(attr);
|
|
1355
|
-
if (actual !== value) {
|
|
1356
|
-
throw new Error(`Expected attribute "${attr}" to be "${value}", but got "${actual}"`);
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
},
|
|
1360
|
-
visible(el) {
|
|
1361
|
-
const htmlEl = el;
|
|
1362
|
-
const reasons = [];
|
|
1363
|
-
if (el.hasAttribute("hidden")) reasons.push('has "hidden" attribute');
|
|
1364
|
-
if (htmlEl.style?.display === "none") reasons.push("has display:none");
|
|
1365
|
-
if (htmlEl.style?.visibility === "hidden") reasons.push("has visibility:hidden");
|
|
1366
|
-
if (reasons.length > 0) {
|
|
1367
|
-
throw new Error(`Expected element to be visible, but it ${reasons.join(" and ")}`);
|
|
1368
|
-
}
|
|
1369
|
-
},
|
|
1370
|
-
disabled(el) {
|
|
1371
|
-
const isDisabled = el.disabled === true || el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
1372
|
-
if (!isDisabled) {
|
|
1373
|
-
throw new Error("Expected element to be disabled, but it is not");
|
|
1374
|
-
}
|
|
1375
|
-
},
|
|
1376
|
-
className(el, name) {
|
|
1377
|
-
if (!el.classList.contains(name)) {
|
|
1378
|
-
throw new Error(`Expected element to have class "${name}", but it has "${el.className}"`);
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1432
|
+
function checkFormLabels(root) {
|
|
1433
|
+
const violations = [];
|
|
1434
|
+
const inputs = selectAll(root, "input, select, textarea");
|
|
1435
|
+
for (const input of Array.from(inputs)) {
|
|
1436
|
+
if (input.getAttribute("type") === "hidden") continue;
|
|
1437
|
+
const inputType = input.getAttribute("type");
|
|
1438
|
+
if (inputType === "submit" || inputType === "reset" || inputType === "button") continue;
|
|
1439
|
+
const hasLabel = checkInputHasLabel(input, root);
|
|
1440
|
+
if (!hasLabel) {
|
|
1441
|
+
violations.push({
|
|
1442
|
+
rule: "form-label",
|
|
1443
|
+
level: "error",
|
|
1444
|
+
message: `Form ${input.tagName.toLowerCase()} element must have an associated label. Use <label for="id">, aria-label, aria-labelledby, or wrap in a <label>.`,
|
|
1445
|
+
element: input,
|
|
1446
|
+
selector: getSelector(input)
|
|
1447
|
+
});
|
|
1381
1448
|
}
|
|
1382
|
-
}
|
|
1449
|
+
}
|
|
1450
|
+
return violations;
|
|
1383
1451
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (!methodMatch) return false;
|
|
1395
|
-
if (typeof route.url === "string") return url === route.url || url.endsWith(route.url);
|
|
1396
|
-
return route.url.test(url);
|
|
1397
|
-
});
|
|
1452
|
+
function checkInputHasLabel(input, root) {
|
|
1453
|
+
if (input.getAttribute("aria-label")?.trim()) return true;
|
|
1454
|
+
const labelledBy = input.getAttribute("aria-labelledby");
|
|
1455
|
+
if (labelledBy) {
|
|
1456
|
+
const doc = input.ownerDocument;
|
|
1457
|
+
const ids = labelledBy.split(/\s+/);
|
|
1458
|
+
for (const id2 of ids) {
|
|
1459
|
+
const ref = doc.getElementById(id2);
|
|
1460
|
+
if (ref?.textContent?.trim()) return true;
|
|
1461
|
+
}
|
|
1398
1462
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1463
|
+
if (input.getAttribute("title")?.trim()) return true;
|
|
1464
|
+
const id = input.getAttribute("id");
|
|
1465
|
+
if (id) {
|
|
1466
|
+
const scope = input.getRootNode();
|
|
1467
|
+
const searchIn = typeof scope.querySelectorAll === "function" ? scope : root;
|
|
1468
|
+
const label = queryAllByAttribute(searchIn, "for", id).find((el) => el.tagName === "LABEL");
|
|
1469
|
+
if (label?.textContent?.trim()) return true;
|
|
1470
|
+
}
|
|
1471
|
+
let parent = input.parentElement;
|
|
1472
|
+
while (parent && parent !== root) {
|
|
1473
|
+
if (parent.tagName.toLowerCase() === "label") return true;
|
|
1474
|
+
parent = parent.parentElement;
|
|
1475
|
+
}
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
function checkHeadingHierarchy(root) {
|
|
1479
|
+
const violations = [];
|
|
1480
|
+
const headings = selectAll(root, "h1, h2, h3, h4, h5, h6");
|
|
1481
|
+
if (headings.length === 0) return violations;
|
|
1482
|
+
let previousLevel = 0;
|
|
1483
|
+
for (const heading of Array.from(headings)) {
|
|
1484
|
+
const currentLevel = parseInt(heading.tagName[1], 10);
|
|
1485
|
+
if (previousLevel === 0 && currentLevel !== 1) {
|
|
1486
|
+
violations.push({
|
|
1487
|
+
rule: "heading-order",
|
|
1488
|
+
level: "warning",
|
|
1489
|
+
message: `Heading hierarchy should start with h1. Found ${heading.tagName.toLowerCase()} as the first heading.`,
|
|
1490
|
+
element: heading,
|
|
1491
|
+
selector: getSelector(heading)
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (previousLevel > 0 && currentLevel > previousLevel + 1) {
|
|
1495
|
+
violations.push({
|
|
1496
|
+
rule: "heading-order",
|
|
1497
|
+
level: "error",
|
|
1498
|
+
message: `Heading level skipped: ${heading.tagName.toLowerCase()} follows h${previousLevel}. Headings should not skip levels.`,
|
|
1499
|
+
element: heading,
|
|
1500
|
+
selector: getSelector(heading)
|
|
1501
|
+
});
|
|
1410
1502
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1503
|
+
if (!heading.textContent?.trim()) {
|
|
1504
|
+
violations.push({
|
|
1505
|
+
rule: "heading-empty",
|
|
1506
|
+
level: "error",
|
|
1507
|
+
message: `Heading ${heading.tagName.toLowerCase()} is empty. Headings must have discernible text.`,
|
|
1508
|
+
element: heading,
|
|
1509
|
+
selector: getSelector(heading)
|
|
1510
|
+
});
|
|
1415
1511
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1512
|
+
previousLevel = currentLevel;
|
|
1513
|
+
}
|
|
1514
|
+
return violations;
|
|
1515
|
+
}
|
|
1516
|
+
function checkColorContrast(root) {
|
|
1517
|
+
const violations = [];
|
|
1518
|
+
const roledElements = selectAll(root, "[role]");
|
|
1519
|
+
for (const el of Array.from(roledElements)) {
|
|
1520
|
+
const role = el.getAttribute("role");
|
|
1521
|
+
if (!role) continue;
|
|
1522
|
+
if (role === "presentation" || role === "none") continue;
|
|
1523
|
+
const nameRequiredRoles = /* @__PURE__ */ new Set([
|
|
1524
|
+
"alert",
|
|
1525
|
+
"alertdialog",
|
|
1526
|
+
"button",
|
|
1527
|
+
"checkbox",
|
|
1528
|
+
"combobox",
|
|
1529
|
+
"dialog",
|
|
1530
|
+
"link",
|
|
1531
|
+
"menuitem",
|
|
1532
|
+
"menuitemcheckbox",
|
|
1533
|
+
"menuitemradio",
|
|
1534
|
+
"option",
|
|
1535
|
+
"progressbar",
|
|
1536
|
+
"radio",
|
|
1537
|
+
"slider",
|
|
1538
|
+
"spinbutton",
|
|
1539
|
+
"switch",
|
|
1540
|
+
"tab",
|
|
1541
|
+
"textbox",
|
|
1542
|
+
"treeitem"
|
|
1543
|
+
]);
|
|
1544
|
+
if (nameRequiredRoles.has(role) && !hasAccessibleName(el)) {
|
|
1545
|
+
violations.push({
|
|
1546
|
+
rule: "accessible-name",
|
|
1547
|
+
level: "error",
|
|
1548
|
+
message: `Element with role="${role}" must have an accessible name (via aria-label, aria-labelledby, or text content).`,
|
|
1549
|
+
element: el,
|
|
1550
|
+
selector: getSelector(el)
|
|
1551
|
+
});
|
|
1436
1552
|
}
|
|
1437
|
-
}
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1553
|
+
}
|
|
1554
|
+
const inlineStyled = selectAll(root, "[style]");
|
|
1555
|
+
for (const el of Array.from(inlineStyled)) {
|
|
1556
|
+
const style = el.getAttribute("style") || "";
|
|
1557
|
+
const hasColor = /(?:^|;)\s*color\s*:/i.test(style);
|
|
1558
|
+
const hasBg = /(?:^|;)\s*background(?:-color)?\s*:/i.test(style);
|
|
1559
|
+
if (hasColor && !hasBg && el.textContent?.trim()) {
|
|
1560
|
+
violations.push({
|
|
1561
|
+
rule: "color-contrast",
|
|
1562
|
+
level: "warning",
|
|
1563
|
+
message: "Element sets text color but not background color. Ensure sufficient contrast with the inherited background.",
|
|
1564
|
+
element: el,
|
|
1565
|
+
selector: getSelector(el)
|
|
1566
|
+
});
|
|
1443
1567
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
/** Add a mock route */
|
|
1453
|
-
addRoute(route) {
|
|
1454
|
-
mockRoutes.push(route);
|
|
1455
|
-
},
|
|
1456
|
-
/** Remove all mock routes */
|
|
1457
|
-
clearRoutes() {
|
|
1458
|
-
mockRoutes.length = 0;
|
|
1459
|
-
},
|
|
1460
|
-
/** Get the request log */
|
|
1461
|
-
getRequests() {
|
|
1462
|
-
return [...requestLog];
|
|
1463
|
-
},
|
|
1464
|
-
/** Clear the request log */
|
|
1465
|
-
clearLog() {
|
|
1466
|
-
requestLog.length = 0;
|
|
1467
|
-
},
|
|
1468
|
-
/** Assert that a URL was called */
|
|
1469
|
-
assertCalled(url, method = "GET") {
|
|
1470
|
-
const found = requestLog.some((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase());
|
|
1471
|
-
if (!found) throw new Error(`Expected ${method} ${url} to have been called`);
|
|
1472
|
-
},
|
|
1473
|
-
/** Assert that a URL was NOT called */
|
|
1474
|
-
assertNotCalled(url, method = "GET") {
|
|
1475
|
-
const found = requestLog.some((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase());
|
|
1476
|
-
if (found) throw new Error(`Expected ${method} ${url} to NOT have been called`);
|
|
1477
|
-
},
|
|
1478
|
-
/** Get number of times a URL was called */
|
|
1479
|
-
callCount(url, method = "GET") {
|
|
1480
|
-
return requestLog.filter((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase()).length;
|
|
1568
|
+
if (hasBg && !hasColor && el.textContent?.trim()) {
|
|
1569
|
+
violations.push({
|
|
1570
|
+
rule: "color-contrast",
|
|
1571
|
+
level: "warning",
|
|
1572
|
+
message: "Element sets background color but not text color. Ensure sufficient contrast with the inherited text color.",
|
|
1573
|
+
element: el,
|
|
1574
|
+
selector: getSelector(el)
|
|
1575
|
+
});
|
|
1481
1576
|
}
|
|
1482
|
-
};
|
|
1483
|
-
if (typeof options.afterEach === "function") {
|
|
1484
|
-
options.afterEach(() => api.restore());
|
|
1485
1577
|
}
|
|
1486
|
-
return
|
|
1578
|
+
return violations;
|
|
1487
1579
|
}
|
|
1488
|
-
function
|
|
1489
|
-
const
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const idx = timers.findIndex((t) => t.id === id);
|
|
1524
|
-
if (idx !== -1) timers.splice(idx, 1);
|
|
1525
|
-
};
|
|
1526
|
-
globalThis.requestAnimationFrame = (cb) => {
|
|
1527
|
-
const id = nextId++;
|
|
1528
|
-
timers.push({ id, callback: () => cb(currentTime), time: currentTime + 16 });
|
|
1529
|
-
return id;
|
|
1530
|
-
};
|
|
1531
|
-
globalThis.cancelAnimationFrame = (id) => {
|
|
1532
|
-
const idx = timers.findIndex((t) => t.id === id);
|
|
1533
|
-
if (idx !== -1) timers.splice(idx, 1);
|
|
1534
|
-
};
|
|
1535
|
-
},
|
|
1536
|
-
restore() {
|
|
1537
|
-
for (const [key, snap] of Object.entries(saved)) {
|
|
1538
|
-
if (snap.had) {
|
|
1539
|
-
g[key] = snap.value;
|
|
1540
|
-
} else {
|
|
1541
|
-
delete g[key];
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
timers.length = 0;
|
|
1545
|
-
},
|
|
1546
|
-
/** Advance time by a given number of ms, running any timers that fire */
|
|
1547
|
-
advance(ms) {
|
|
1548
|
-
const targetTime = currentTime + ms;
|
|
1549
|
-
while (true) {
|
|
1550
|
-
timers.sort((a, b) => a.time - b.time);
|
|
1551
|
-
const next = timers.find((t) => t.time <= targetTime);
|
|
1552
|
-
if (!next) break;
|
|
1553
|
-
currentTime = next.time;
|
|
1554
|
-
const idx = timers.indexOf(next);
|
|
1555
|
-
if (next.interval) {
|
|
1556
|
-
next.time += next.interval;
|
|
1557
|
-
} else {
|
|
1558
|
-
timers.splice(idx, 1);
|
|
1559
|
-
}
|
|
1560
|
-
next.callback();
|
|
1561
|
-
}
|
|
1562
|
-
currentTime = targetTime;
|
|
1563
|
-
},
|
|
1564
|
-
/** Run all pending timers immediately */
|
|
1565
|
-
flush() {
|
|
1566
|
-
const maxIterations = 1e3;
|
|
1567
|
-
let i = 0;
|
|
1568
|
-
while (timers.length > 0 && i++ < maxIterations) {
|
|
1569
|
-
timers.sort((a, b) => a.time - b.time);
|
|
1570
|
-
const next = timers[0];
|
|
1571
|
-
currentTime = next.time;
|
|
1572
|
-
if (next.interval) {
|
|
1573
|
-
next.time += next.interval;
|
|
1574
|
-
} else {
|
|
1575
|
-
timers.shift();
|
|
1576
|
-
}
|
|
1577
|
-
next.callback();
|
|
1578
|
-
}
|
|
1579
|
-
},
|
|
1580
|
-
/** Get current fake time */
|
|
1581
|
-
now() {
|
|
1582
|
-
return currentTime;
|
|
1583
|
-
},
|
|
1584
|
-
/** Get number of pending timers */
|
|
1585
|
-
pendingCount() {
|
|
1586
|
-
return timers.length;
|
|
1580
|
+
function checkKeyboardAccess(root, options) {
|
|
1581
|
+
const violations = [];
|
|
1582
|
+
const clickElements = selectAll(root, "*").filter(hasActivationHandler);
|
|
1583
|
+
for (const el of clickElements) {
|
|
1584
|
+
if (isNativeInteractive(el)) continue;
|
|
1585
|
+
if (el.hasAttribute(DELEGATES_ATTRIBUTE) || options?.delegatesActivation?.(el)) continue;
|
|
1586
|
+
const hasTabindex = el.hasAttribute("tabindex");
|
|
1587
|
+
const hasRole = el.hasAttribute("role");
|
|
1588
|
+
const hasKeyHandler = hasKeyboardHandler(el);
|
|
1589
|
+
if (!hasTabindex) {
|
|
1590
|
+
violations.push({
|
|
1591
|
+
rule: "keyboard-access",
|
|
1592
|
+
level: "error",
|
|
1593
|
+
message: "Interactive element with onclick must have tabindex to be keyboard accessible.",
|
|
1594
|
+
element: el,
|
|
1595
|
+
selector: getSelector(el)
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
if (!hasRole) {
|
|
1599
|
+
violations.push({
|
|
1600
|
+
rule: "keyboard-access",
|
|
1601
|
+
level: "warning",
|
|
1602
|
+
message: 'Interactive element with onclick should have an explicit role attribute (e.g., role="button").',
|
|
1603
|
+
element: el,
|
|
1604
|
+
selector: getSelector(el)
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (!hasKeyHandler) {
|
|
1608
|
+
violations.push({
|
|
1609
|
+
rule: "keyboard-access",
|
|
1610
|
+
level: "error",
|
|
1611
|
+
message: "Interactive element with onclick must also have a keyboard event handler (onkeydown or onkeyup) for keyboard accessibility.",
|
|
1612
|
+
element: el,
|
|
1613
|
+
selector: getSelector(el)
|
|
1614
|
+
});
|
|
1587
1615
|
}
|
|
1588
|
-
};
|
|
1589
|
-
if (typeof options.afterEach === "function") {
|
|
1590
|
-
options.afterEach(() => api.restore());
|
|
1591
|
-
}
|
|
1592
|
-
return api;
|
|
1593
|
-
}
|
|
1594
|
-
function createDOMSnapshot(element) {
|
|
1595
|
-
return serializeElement(element, 0);
|
|
1596
|
-
}
|
|
1597
|
-
function serializeElement(el, indent) {
|
|
1598
|
-
const pad = " ".repeat(indent);
|
|
1599
|
-
const tag = el.tagName.toLowerCase();
|
|
1600
|
-
const attrs = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}="${a.value}"`).join(" ");
|
|
1601
|
-
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
1602
|
-
const children = Array.from(el.childNodes);
|
|
1603
|
-
if (children.length === 0) {
|
|
1604
|
-
return `${open}</${tag}>`;
|
|
1605
|
-
}
|
|
1606
|
-
if (children.length === 1 && children[0].nodeType === 3) {
|
|
1607
|
-
const text = children[0].textContent?.trim() || "";
|
|
1608
|
-
return `${open}${text}</${tag}>`;
|
|
1609
1616
|
}
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
if (
|
|
1616
|
-
|
|
1617
|
+
const interactiveRoles = selectAll(
|
|
1618
|
+
root,
|
|
1619
|
+
'[role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"]'
|
|
1620
|
+
);
|
|
1621
|
+
for (const el of Array.from(interactiveRoles)) {
|
|
1622
|
+
if (isNativeInteractive(el)) continue;
|
|
1623
|
+
if (!el.hasAttribute("tabindex")) {
|
|
1624
|
+
violations.push({
|
|
1625
|
+
rule: "keyboard-access",
|
|
1626
|
+
level: "error",
|
|
1627
|
+
message: `Element with role="${el.getAttribute("role")}" must have tabindex to be keyboard accessible.`,
|
|
1628
|
+
element: el,
|
|
1629
|
+
selector: getSelector(el)
|
|
1630
|
+
});
|
|
1617
1631
|
}
|
|
1618
|
-
return "";
|
|
1619
|
-
}).filter(Boolean).join("\n");
|
|
1620
|
-
return `${open}
|
|
1621
|
-
${childStr}
|
|
1622
|
-
${pad}</${tag}>`;
|
|
1623
|
-
}
|
|
1624
|
-
function assertDOMEquals(actual, expected) {
|
|
1625
|
-
const actualSnapshot = createDOMSnapshot(actual);
|
|
1626
|
-
const expectedSnapshot = createDOMSnapshot(expected);
|
|
1627
|
-
if (actualSnapshot !== expectedSnapshot) {
|
|
1628
|
-
throw new Error(`DOM mismatch:
|
|
1629
|
-
|
|
1630
|
-
Actual:
|
|
1631
|
-
${actualSnapshot}
|
|
1632
|
-
|
|
1633
|
-
Expected:
|
|
1634
|
-
${expectedSnapshot}`);
|
|
1635
1632
|
}
|
|
1633
|
+
return violations;
|
|
1636
1634
|
}
|
|
1637
|
-
function
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1635
|
+
function checkAriaAttributes(root) {
|
|
1636
|
+
const violations = [];
|
|
1637
|
+
const roledElements = selectAll(root, "[role]");
|
|
1638
|
+
for (const el of Array.from(roledElements)) {
|
|
1639
|
+
const role = el.getAttribute("role");
|
|
1640
|
+
if (!role) continue;
|
|
1641
|
+
if (!VALID_ARIA_ROLES.has(role)) {
|
|
1642
|
+
violations.push({
|
|
1643
|
+
rule: "aria-valid-role",
|
|
1644
|
+
level: "error",
|
|
1645
|
+
message: `Invalid ARIA role "${role}". Must be a valid WAI-ARIA role.`,
|
|
1646
|
+
element: el,
|
|
1647
|
+
selector: getSelector(el)
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
if (REQUIRED_ARIA_PROPS[role]) {
|
|
1651
|
+
for (const requiredProp of REQUIRED_ARIA_PROPS[role]) {
|
|
1652
|
+
if (!el.hasAttribute(requiredProp)) {
|
|
1653
|
+
violations.push({
|
|
1654
|
+
rule: "aria-required-attr",
|
|
1655
|
+
level: "error",
|
|
1656
|
+
message: `Element with role="${role}" requires the ${requiredProp} attribute.`,
|
|
1657
|
+
element: el,
|
|
1658
|
+
selector: getSelector(el)
|
|
1659
|
+
});
|
|
1656
1660
|
}
|
|
1657
1661
|
}
|
|
1658
|
-
return null;
|
|
1659
|
-
},
|
|
1660
|
-
click(el) {
|
|
1661
|
-
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
1662
|
-
},
|
|
1663
|
-
type(el, value) {
|
|
1664
|
-
el.value = value;
|
|
1665
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1666
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1667
|
-
},
|
|
1668
|
-
async waitForUpdate() {
|
|
1669
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
1670
|
-
},
|
|
1671
|
-
destroy() {
|
|
1672
|
-
if (container.parentNode) container.parentNode.removeChild(container);
|
|
1673
1662
|
}
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// src/reactivity/track-core.ts
|
|
1678
|
-
var POOL_MAX = 4096;
|
|
1679
|
-
var nodePool = [];
|
|
1680
|
-
function createNode() {
|
|
1681
|
-
return {
|
|
1682
|
-
sig: null,
|
|
1683
|
-
sub: null,
|
|
1684
|
-
epoch: 0,
|
|
1685
|
-
depVersion: 0,
|
|
1686
|
-
sigPrev: null,
|
|
1687
|
-
sigNext: null,
|
|
1688
|
-
subPrev: null,
|
|
1689
|
-
subNext: null,
|
|
1690
|
-
prevActive: null
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
function allocNode(sig, sub, epoch) {
|
|
1694
|
-
const n = nodePool.pop();
|
|
1695
|
-
if (n) {
|
|
1696
|
-
n.sig = sig;
|
|
1697
|
-
n.sub = sub;
|
|
1698
|
-
n.epoch = epoch;
|
|
1699
|
-
return n;
|
|
1700
|
-
}
|
|
1701
|
-
const fresh = createNode();
|
|
1702
|
-
fresh.sig = sig;
|
|
1703
|
-
fresh.sub = sub;
|
|
1704
|
-
fresh.epoch = epoch;
|
|
1705
|
-
return fresh;
|
|
1706
|
-
}
|
|
1707
|
-
function freeNode(node) {
|
|
1708
|
-
node.sig = null;
|
|
1709
|
-
node.sub = null;
|
|
1710
|
-
node.sigPrev = null;
|
|
1711
|
-
node.sigNext = null;
|
|
1712
|
-
node.subPrev = null;
|
|
1713
|
-
node.subNext = null;
|
|
1714
|
-
node.prevActive = null;
|
|
1715
|
-
if (nodePool.length < POOL_MAX) nodePool.push(node);
|
|
1716
|
-
}
|
|
1717
|
-
function linkSignal(sig, node) {
|
|
1718
|
-
const oldHead = sig.subsHead ?? null;
|
|
1719
|
-
node.sigPrev = null;
|
|
1720
|
-
node.sigNext = oldHead;
|
|
1721
|
-
if (oldHead) oldHead.sigPrev = node;
|
|
1722
|
-
else sig.subsTail = node;
|
|
1723
|
-
sig.subsHead = node;
|
|
1724
|
-
sig.__sc = (sig.__sc ?? 0) + 1;
|
|
1725
|
-
}
|
|
1726
|
-
function unlinkSignal(node) {
|
|
1727
|
-
const sig = node.sig;
|
|
1728
|
-
if (!sig) return;
|
|
1729
|
-
const prev = node.sigPrev;
|
|
1730
|
-
const next = node.sigNext;
|
|
1731
|
-
if (prev) prev.sigNext = next;
|
|
1732
|
-
else sig.subsHead = next;
|
|
1733
|
-
if (next) next.sigPrev = prev;
|
|
1734
|
-
else sig.subsTail = prev;
|
|
1735
|
-
sig.__sc = (sig.__sc ?? 1) - 1;
|
|
1736
|
-
if (sig.__activeNode === node) sig.__activeNode = node.prevActive;
|
|
1737
|
-
if (sig.__sc === 0) {
|
|
1738
|
-
sig.subsHead = null;
|
|
1739
|
-
sig.subsTail = null;
|
|
1740
1663
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1664
|
+
const allElements = selectAll(root, "*");
|
|
1665
|
+
for (const el of Array.from(allElements)) {
|
|
1666
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1667
|
+
if (!attr.name.startsWith("aria-")) continue;
|
|
1668
|
+
if (!VALID_ARIA_ATTRIBUTES.has(attr.name)) {
|
|
1669
|
+
violations.push({
|
|
1670
|
+
rule: "aria-valid-attr",
|
|
1671
|
+
level: "error",
|
|
1672
|
+
message: `Invalid ARIA attribute "${attr.name}". Must be a valid WAI-ARIA attribute.`,
|
|
1673
|
+
element: el,
|
|
1674
|
+
selector: getSelector(el)
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
if (attr.name === "aria-labelledby" || attr.name === "aria-describedby" || attr.name === "aria-controls" || attr.name === "aria-owns") {
|
|
1678
|
+
const ids = attr.value.split(/\s+/).filter(Boolean);
|
|
1679
|
+
const doc = el.ownerDocument;
|
|
1680
|
+
for (const id of ids) {
|
|
1681
|
+
if (!doc.getElementById(id)) {
|
|
1682
|
+
violations.push({
|
|
1683
|
+
rule: "aria-valid-attr-value",
|
|
1684
|
+
level: "error",
|
|
1685
|
+
message: `${attr.name} references id "${id}" which does not exist in the document.`,
|
|
1686
|
+
element: el,
|
|
1687
|
+
selector: getSelector(el)
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const booleanAttrs = /* @__PURE__ */ new Set([
|
|
1693
|
+
"aria-checked",
|
|
1694
|
+
"aria-disabled",
|
|
1695
|
+
"aria-expanded",
|
|
1696
|
+
"aria-hidden",
|
|
1697
|
+
"aria-pressed",
|
|
1698
|
+
"aria-required",
|
|
1699
|
+
"aria-selected",
|
|
1700
|
+
"aria-busy",
|
|
1701
|
+
"aria-modal",
|
|
1702
|
+
"aria-multiline",
|
|
1703
|
+
"aria-multiselectable",
|
|
1704
|
+
"aria-readonly"
|
|
1705
|
+
]);
|
|
1706
|
+
if (booleanAttrs.has(attr.name)) {
|
|
1707
|
+
const validValues = ["true", "false"];
|
|
1708
|
+
if (attr.name === "aria-checked" || attr.name === "aria-pressed") {
|
|
1709
|
+
validValues.push("mixed");
|
|
1710
|
+
}
|
|
1711
|
+
if (!validValues.includes(attr.value)) {
|
|
1712
|
+
violations.push({
|
|
1713
|
+
rule: "aria-valid-attr-value",
|
|
1714
|
+
level: "error",
|
|
1715
|
+
message: `${attr.name} has invalid value "${attr.value}". Expected one of: ${validValues.join(", ")}.`,
|
|
1716
|
+
element: el,
|
|
1717
|
+
selector: getSelector(el)
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1772
1722
|
}
|
|
1723
|
+
return violations;
|
|
1773
1724
|
}
|
|
1774
|
-
function
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const
|
|
1779
|
-
if (
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1725
|
+
function checkLandmarks(root) {
|
|
1726
|
+
const violations = [];
|
|
1727
|
+
const foundLandmarks = /* @__PURE__ */ new Set();
|
|
1728
|
+
for (const role of LANDMARK_ROLES) {
|
|
1729
|
+
const elements = selectAll(root, `[role="${role}"]`);
|
|
1730
|
+
if (elements.length > 0) {
|
|
1731
|
+
foundLandmarks.add(role);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
for (const [tag, role] of Object.entries(LANDMARK_ELEMENTS)) {
|
|
1735
|
+
const elements = selectAll(root, tag);
|
|
1736
|
+
if (elements.length > 0) {
|
|
1737
|
+
foundLandmarks.add(role);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (!foundLandmarks.has("main")) {
|
|
1741
|
+
violations.push({
|
|
1742
|
+
rule: "landmark-main",
|
|
1743
|
+
level: "warning",
|
|
1744
|
+
message: 'Page should have a main landmark (<main> element or role="main").',
|
|
1745
|
+
selector: getSelector(root)
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
const mainElements = selectAll(root, 'main, [role="main"]');
|
|
1749
|
+
if (mainElements.length > 1) {
|
|
1750
|
+
violations.push({
|
|
1751
|
+
rule: "landmark-unique",
|
|
1752
|
+
level: "error",
|
|
1753
|
+
message: `Multiple main landmarks found (${mainElements.length}). A page should have at most one main landmark.`,
|
|
1754
|
+
selector: getSelector(root)
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
const bannerElements = selectAll(root, 'header, [role="banner"]');
|
|
1758
|
+
if (bannerElements.length > 1) {
|
|
1759
|
+
violations.push({
|
|
1760
|
+
rule: "landmark-unique",
|
|
1761
|
+
level: "warning",
|
|
1762
|
+
message: `Multiple banner landmarks found (${bannerElements.length}). Consider using a single banner landmark.`,
|
|
1763
|
+
selector: getSelector(root)
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const contentinfoElements = selectAll(root, 'footer, [role="contentinfo"]');
|
|
1767
|
+
if (contentinfoElements.length > 1) {
|
|
1768
|
+
violations.push({
|
|
1769
|
+
rule: "landmark-unique",
|
|
1770
|
+
level: "warning",
|
|
1771
|
+
message: `Multiple contentinfo landmarks found (${contentinfoElements.length}). Consider using a single contentinfo landmark.`,
|
|
1772
|
+
selector: getSelector(root)
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
const navElements = selectAll(root, 'nav, [role="navigation"]');
|
|
1776
|
+
if (navElements.length > 1) {
|
|
1777
|
+
for (const nav of Array.from(navElements)) {
|
|
1778
|
+
if (!nav.getAttribute("aria-label") && !nav.getAttribute("aria-labelledby")) {
|
|
1779
|
+
violations.push({
|
|
1780
|
+
rule: "landmark-label",
|
|
1781
|
+
level: "warning",
|
|
1782
|
+
message: "When multiple navigation landmarks exist, each should have a unique label (aria-label or aria-labelledby).",
|
|
1783
|
+
element: nav,
|
|
1784
|
+
selector: getSelector(nav)
|
|
1785
|
+
});
|
|
1785
1786
|
}
|
|
1786
1787
|
}
|
|
1787
|
-
if (node.depVersion !== sig.__v) return true;
|
|
1788
|
-
node = node.subNext;
|
|
1789
1788
|
}
|
|
1790
|
-
return
|
|
1789
|
+
return violations;
|
|
1791
1790
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1791
|
+
function checkLinksAndButtons(root) {
|
|
1792
|
+
const violations = [];
|
|
1793
|
+
const links = selectAll(root, "a[href]");
|
|
1794
|
+
for (const link of Array.from(links)) {
|
|
1795
|
+
if (!hasAccessibleName(link)) {
|
|
1796
|
+
violations.push({
|
|
1797
|
+
rule: "link-name",
|
|
1798
|
+
level: "error",
|
|
1799
|
+
message: "Link must have discernible text. Add text content, aria-label, or aria-labelledby.",
|
|
1800
|
+
element: link,
|
|
1801
|
+
selector: getSelector(link)
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
if (link.getAttribute("target") === "_blank") {
|
|
1805
|
+
violations.push({
|
|
1806
|
+
rule: "link-new-window",
|
|
1807
|
+
level: "warning",
|
|
1808
|
+
message: 'Link opens in a new window/tab (target="_blank"). Consider warning users or adding an icon to indicate this.',
|
|
1809
|
+
element: link,
|
|
1810
|
+
selector: getSelector(link)
|
|
1811
|
+
});
|
|
1812
|
+
const rel = link.getAttribute("rel") || "";
|
|
1813
|
+
if (!rel.includes("noopener") && !rel.includes("noreferrer")) {
|
|
1814
|
+
violations.push({
|
|
1815
|
+
rule: "link-noopener",
|
|
1816
|
+
level: "warning",
|
|
1817
|
+
message: 'Link with target="_blank" should include rel="noopener" or rel="noreferrer" for security.',
|
|
1818
|
+
element: link,
|
|
1819
|
+
selector: getSelector(link)
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const linkText = link.textContent?.trim().toLowerCase() || "";
|
|
1824
|
+
const genericTexts = ["click here", "here", "read more", "more", "learn more", "link"];
|
|
1825
|
+
if (genericTexts.includes(linkText)) {
|
|
1826
|
+
violations.push({
|
|
1827
|
+
rule: "link-purpose",
|
|
1828
|
+
level: "warning",
|
|
1829
|
+
message: `Link text "${link.textContent?.trim()}" is generic. Use descriptive text that explains the link's purpose.`,
|
|
1830
|
+
element: link,
|
|
1831
|
+
selector: getSelector(link)
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1799
1834
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1835
|
+
const buttons = selectAll(root, 'button, [role="button"]');
|
|
1836
|
+
for (const button of Array.from(buttons)) {
|
|
1837
|
+
if (!hasAccessibleName(button)) {
|
|
1838
|
+
violations.push({
|
|
1839
|
+
rule: "button-name",
|
|
1840
|
+
level: "error",
|
|
1841
|
+
message: "Button must have an accessible name. Add text content, aria-label, or aria-labelledby.",
|
|
1842
|
+
element: button,
|
|
1843
|
+
selector: getSelector(button)
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1808
1846
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1847
|
+
const inputButtons = selectAll(root, 'input[type="submit"], input[type="reset"], input[type="button"]');
|
|
1848
|
+
for (const input of Array.from(inputButtons)) {
|
|
1849
|
+
const value = input.getAttribute("value");
|
|
1850
|
+
if (!value?.trim() && !input.getAttribute("aria-label") && !input.getAttribute("aria-labelledby")) {
|
|
1851
|
+
violations.push({
|
|
1852
|
+
rule: "button-name",
|
|
1853
|
+
level: "error",
|
|
1854
|
+
message: `Input button (type="${input.getAttribute("type")}") must have a value, aria-label, or aria-labelledby.`,
|
|
1855
|
+
element: input,
|
|
1856
|
+
selector: getSelector(input)
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1819
1859
|
}
|
|
1860
|
+
return violations;
|
|
1820
1861
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1862
|
+
function checkListSemantics(root) {
|
|
1863
|
+
const violations = [];
|
|
1864
|
+
const lists = selectAll(root, "ul, ol");
|
|
1865
|
+
for (const list of Array.from(lists)) {
|
|
1866
|
+
const children = Array.from(list.children);
|
|
1867
|
+
for (const child of children) {
|
|
1868
|
+
const tag = child.tagName.toLowerCase();
|
|
1869
|
+
if (tag !== "li" && tag !== "script" && tag !== "template") {
|
|
1870
|
+
violations.push({
|
|
1871
|
+
rule: "list-semantics",
|
|
1872
|
+
level: "error",
|
|
1873
|
+
message: `Direct children of <${list.tagName.toLowerCase()}> must be <li> elements. Found <${tag}>.`,
|
|
1874
|
+
element: child,
|
|
1875
|
+
selector: getSelector(child)
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (children.length === 0) {
|
|
1880
|
+
violations.push({
|
|
1881
|
+
rule: "list-empty",
|
|
1882
|
+
level: "info",
|
|
1883
|
+
message: `Empty <${list.tagName.toLowerCase()}> element found. Consider removing or adding items.`,
|
|
1884
|
+
element: list,
|
|
1885
|
+
selector: getSelector(list)
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1832
1888
|
}
|
|
1833
|
-
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1889
|
+
const dlLists = selectAll(root, "dl");
|
|
1890
|
+
for (const dl of Array.from(dlLists)) {
|
|
1891
|
+
const children = Array.from(dl.children);
|
|
1892
|
+
for (const child of children) {
|
|
1893
|
+
const tag = child.tagName.toLowerCase();
|
|
1894
|
+
if (tag !== "dt" && tag !== "dd" && tag !== "div" && tag !== "script" && tag !== "template") {
|
|
1895
|
+
violations.push({
|
|
1896
|
+
rule: "list-semantics",
|
|
1897
|
+
level: "error",
|
|
1898
|
+
message: `Direct children of <dl> must be <dt>, <dd>, or <div> elements. Found <${tag}>.`,
|
|
1899
|
+
element: child,
|
|
1900
|
+
selector: getSelector(child)
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1842
1904
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1905
|
+
const roleLists = selectAll(root, '[role="list"]');
|
|
1906
|
+
for (const list of Array.from(roleLists)) {
|
|
1907
|
+
const children = Array.from(list.children);
|
|
1908
|
+
const hasListItems = children.some(
|
|
1909
|
+
(child) => child.getAttribute("role") === "listitem" || child.tagName.toLowerCase() === "li"
|
|
1910
|
+
);
|
|
1911
|
+
if (children.length > 0 && !hasListItems) {
|
|
1912
|
+
violations.push({
|
|
1913
|
+
rule: "list-semantics",
|
|
1914
|
+
level: "warning",
|
|
1915
|
+
message: 'Element with role="list" should contain children with role="listitem".',
|
|
1916
|
+
element: list,
|
|
1917
|
+
selector: getSelector(list)
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return violations;
|
|
1922
|
+
}
|
|
1923
|
+
function checkTabOrder(root) {
|
|
1924
|
+
const violations = [];
|
|
1925
|
+
const tabbable = selectAll(root, "[tabindex]");
|
|
1926
|
+
for (const el of Array.from(tabbable)) {
|
|
1927
|
+
const tabindex = parseInt(el.getAttribute("tabindex") ?? "", 10);
|
|
1928
|
+
if (!Number.isNaN(tabindex) && tabindex > 0) {
|
|
1929
|
+
violations.push({
|
|
1930
|
+
rule: "tabindex-positive",
|
|
1931
|
+
level: "warning",
|
|
1932
|
+
message: `Element has tabindex="${tabindex}". Positive tabindex values create a confusing tab order. Use tabindex="0" instead and rely on DOM order.`,
|
|
1933
|
+
element: el,
|
|
1934
|
+
selector: getSelector(el)
|
|
1935
|
+
});
|
|
1851
1936
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1937
|
+
}
|
|
1938
|
+
const focusTraps = selectAll(root, '[data-sibu-focus-trap], [aria-modal="true"]');
|
|
1939
|
+
for (const trap of Array.from(focusTraps)) {
|
|
1940
|
+
const role = trap.getAttribute("role");
|
|
1941
|
+
if (role === "dialog" || role === "alertdialog" || trap.hasAttribute("aria-modal")) {
|
|
1942
|
+
const hasCloseButton = trap.querySelector(
|
|
1943
|
+
'button[aria-label*="close" i], button[aria-label*="dismiss" i], [data-dismiss], [data-close]'
|
|
1944
|
+
);
|
|
1945
|
+
if (!hasCloseButton) {
|
|
1946
|
+
violations.push({
|
|
1947
|
+
rule: "focus-trap-escape",
|
|
1948
|
+
level: "warning",
|
|
1949
|
+
message: "Modal/dialog with focus trap should have a visible close button. Users must be able to dismiss the dialog.",
|
|
1950
|
+
element: trap,
|
|
1951
|
+
selector: getSelector(trap)
|
|
1952
|
+
});
|
|
1862
1953
|
}
|
|
1863
|
-
|
|
1954
|
+
}
|
|
1955
|
+
const focusable = trap.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
1956
|
+
if (focusable.length === 0) {
|
|
1957
|
+
violations.push({
|
|
1958
|
+
rule: "focus-trap-empty",
|
|
1959
|
+
level: "error",
|
|
1960
|
+
message: "Focus trap contains no focusable elements. Users will be trapped with no way to interact.",
|
|
1961
|
+
element: trap,
|
|
1962
|
+
selector: getSelector(trap)
|
|
1963
|
+
});
|
|
1864
1964
|
}
|
|
1865
1965
|
}
|
|
1966
|
+
return violations;
|
|
1866
1967
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1968
|
+
var CHECK_FUNCTIONS = {
|
|
1969
|
+
images: checkImageAlt,
|
|
1970
|
+
forms: checkFormLabels,
|
|
1971
|
+
headings: checkHeadingHierarchy,
|
|
1972
|
+
contrast: checkColorContrast,
|
|
1973
|
+
keyboard: checkKeyboardAccess,
|
|
1974
|
+
aria: checkAriaAttributes,
|
|
1975
|
+
landmarks: checkLandmarks,
|
|
1976
|
+
links: checkLinksAndButtons,
|
|
1977
|
+
lists: checkListSemantics,
|
|
1978
|
+
tabOrder: checkTabOrder
|
|
1979
|
+
};
|
|
1980
|
+
var ALL_CHECKS = [
|
|
1981
|
+
"images",
|
|
1982
|
+
"forms",
|
|
1983
|
+
"headings",
|
|
1984
|
+
"contrast",
|
|
1985
|
+
"keyboard",
|
|
1986
|
+
"aria",
|
|
1987
|
+
"landmarks",
|
|
1988
|
+
"links",
|
|
1989
|
+
"lists",
|
|
1990
|
+
"tabOrder"
|
|
1991
|
+
];
|
|
1992
|
+
function checkA11y(root, options) {
|
|
1993
|
+
const checksToRun = options?.checks || ALL_CHECKS;
|
|
1994
|
+
const minLevel = options?.level || "info";
|
|
1995
|
+
const levelPriority = {
|
|
1996
|
+
error: 2,
|
|
1997
|
+
warning: 1,
|
|
1998
|
+
info: 0
|
|
1999
|
+
};
|
|
2000
|
+
const minPriority = levelPriority[minLevel];
|
|
2001
|
+
const allViolations = [];
|
|
2002
|
+
for (const check of checksToRun) {
|
|
2003
|
+
const fn = CHECK_FUNCTIONS[check];
|
|
2004
|
+
if (fn) {
|
|
2005
|
+
const results = fn(root);
|
|
2006
|
+
allViolations.push(...results);
|
|
2007
|
+
}
|
|
1879
2008
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2009
|
+
const filtered = allViolations.filter((v) => levelPriority[v.level] >= minPriority);
|
|
2010
|
+
const violations = filtered.filter((v) => v.level === "error");
|
|
2011
|
+
const warnings = filtered.filter((v) => v.level === "warning");
|
|
2012
|
+
const info = filtered.filter((v) => v.level === "info");
|
|
2013
|
+
const parts = [];
|
|
2014
|
+
if (violations.length > 0) parts.push(`${violations.length} error(s)`);
|
|
2015
|
+
if (warnings.length > 0) parts.push(`${warnings.length} warning(s)`);
|
|
2016
|
+
if (info.length > 0) parts.push(`${info.length} info`);
|
|
2017
|
+
const passed = violations.length === 0;
|
|
2018
|
+
const summary = passed ? parts.length > 0 ? `Passed with ${parts.join(", ")}.` : "Passed with no issues." : `Failed with ${parts.join(", ")}.`;
|
|
2019
|
+
return { passed, violations, warnings, info, summary };
|
|
2020
|
+
}
|
|
2021
|
+
function assertA11y(root, options) {
|
|
2022
|
+
const result = checkA11y(root, options);
|
|
2023
|
+
if (!result.passed) {
|
|
2024
|
+
const lines = [`Accessibility check failed: ${result.summary}`, ""];
|
|
2025
|
+
if (result.violations.length > 0) {
|
|
2026
|
+
lines.push("ERRORS:");
|
|
2027
|
+
for (const v of result.violations) {
|
|
2028
|
+
lines.push(` [${v.rule}] ${v.message}${v.selector ? ` (${v.selector})` : ""}`);
|
|
2029
|
+
}
|
|
2030
|
+
lines.push("");
|
|
1889
2031
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const
|
|
1893
|
-
|
|
1894
|
-
|
|
2032
|
+
if (result.warnings.length > 0) {
|
|
2033
|
+
lines.push("WARNINGS:");
|
|
2034
|
+
for (const w of result.warnings) {
|
|
2035
|
+
lines.push(` [${w.rule}] ${w.message}${w.selector ? ` (${w.selector})` : ""}`);
|
|
2036
|
+
}
|
|
2037
|
+
lines.push("");
|
|
1895
2038
|
}
|
|
2039
|
+
throw new Error(lines.join("\n"));
|
|
1896
2040
|
}
|
|
1897
|
-
const sub = subscriber;
|
|
1898
|
-
return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
|
|
1899
2041
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2042
|
+
|
|
2043
|
+
// src/testing/adapters.ts
|
|
2044
|
+
function escapeAttrValue(value) {
|
|
2045
|
+
return value.replace(/[\\"]/g, "\\$&").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ").replace(/\0/g, "\\fffd ");
|
|
2046
|
+
}
|
|
2047
|
+
function createJestAdapter() {
|
|
2048
|
+
let container = null;
|
|
2049
|
+
return {
|
|
2050
|
+
/** Call in beforeEach to set up DOM container */
|
|
2051
|
+
setup() {
|
|
2052
|
+
container = document.createElement("div");
|
|
2053
|
+
container.setAttribute("data-testenv", "jest");
|
|
2054
|
+
document.body.appendChild(container);
|
|
2055
|
+
return container;
|
|
2056
|
+
},
|
|
2057
|
+
/** Call in afterEach to clean up */
|
|
2058
|
+
teardown() {
|
|
2059
|
+
if (container) {
|
|
2060
|
+
replaceChildrenSafely(container);
|
|
2061
|
+
if (container.parentNode) {
|
|
2062
|
+
container.parentNode.removeChild(container);
|
|
2063
|
+
}
|
|
2064
|
+
container = null;
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
/** Render a component into the test container */
|
|
2068
|
+
render(component) {
|
|
2069
|
+
if (!container) {
|
|
2070
|
+
container = document.createElement("div");
|
|
2071
|
+
container.setAttribute("data-testenv", "jest");
|
|
2072
|
+
document.body.appendChild(container);
|
|
2073
|
+
}
|
|
2074
|
+
const element = typeof component === "function" ? component() : component;
|
|
2075
|
+
container.appendChild(element);
|
|
2076
|
+
return { element, container };
|
|
2077
|
+
},
|
|
2078
|
+
/** Custom Jest matchers */
|
|
2079
|
+
matchers: {
|
|
2080
|
+
/** Check if element has specific text content */
|
|
2081
|
+
toHaveTextContent(element, expected) {
|
|
2082
|
+
const actual = element.textContent || "";
|
|
2083
|
+
const pass = actual.includes(expected);
|
|
2084
|
+
return {
|
|
2085
|
+
pass,
|
|
2086
|
+
message: () => pass ? `Expected element not to have text content "${expected}", but found "${actual}"` : `Expected element to have text content "${expected}", but found "${actual}"`
|
|
2087
|
+
};
|
|
2088
|
+
},
|
|
2089
|
+
/** Check if element has specific attribute */
|
|
2090
|
+
toHaveAttribute(element, attr, value) {
|
|
2091
|
+
const hasAttr = element.hasAttribute(attr);
|
|
2092
|
+
const attrValue = element.getAttribute(attr);
|
|
2093
|
+
const pass = value !== void 0 ? hasAttr && attrValue === value : hasAttr;
|
|
2094
|
+
return {
|
|
2095
|
+
pass,
|
|
2096
|
+
message: () => {
|
|
2097
|
+
if (value !== void 0) {
|
|
2098
|
+
return pass ? `Expected element not to have attribute "${attr}" with value "${value}", but it does` : `Expected element to have attribute "${attr}" with value "${value}", but got "${attrValue}"`;
|
|
2099
|
+
}
|
|
2100
|
+
return pass ? `Expected element not to have attribute "${attr}", but it does` : `Expected element to have attribute "${attr}", but it does not`;
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
},
|
|
2104
|
+
/** Check if element has specific class */
|
|
2105
|
+
toHaveClass(element, className) {
|
|
2106
|
+
const pass = element.classList.contains(className);
|
|
2107
|
+
return {
|
|
2108
|
+
pass,
|
|
2109
|
+
message: () => pass ? `Expected element not to have class "${className}", but it does` : `Expected element to have class "${className}", but it has "${element.className}"`
|
|
2110
|
+
};
|
|
2111
|
+
},
|
|
2112
|
+
/** Check if element is visible (no display:none, visibility:hidden, hidden attr) */
|
|
2113
|
+
toBeVisible(element) {
|
|
2114
|
+
const htmlEl = element;
|
|
2115
|
+
const isHiddenAttr = element.hasAttribute("hidden");
|
|
2116
|
+
const style = htmlEl.style;
|
|
2117
|
+
const isDisplayNone = style?.display === "none";
|
|
2118
|
+
const isVisibilityHidden = style?.visibility === "hidden";
|
|
2119
|
+
const isOpacityZero = style?.opacity === "0";
|
|
2120
|
+
const pass = !isHiddenAttr && !isDisplayNone && !isVisibilityHidden && !isOpacityZero;
|
|
2121
|
+
return {
|
|
2122
|
+
pass,
|
|
2123
|
+
message: () => {
|
|
2124
|
+
if (pass) {
|
|
2125
|
+
return "Expected element to be hidden, but it is visible";
|
|
2126
|
+
}
|
|
2127
|
+
const reasons = [];
|
|
2128
|
+
if (isHiddenAttr) reasons.push('has "hidden" attribute');
|
|
2129
|
+
if (isDisplayNone) reasons.push("has display:none");
|
|
2130
|
+
if (isVisibilityHidden) reasons.push("has visibility:hidden");
|
|
2131
|
+
if (isOpacityZero) reasons.push("has opacity:0");
|
|
2132
|
+
return `Expected element to be visible, but it ${reasons.join(" and ")}`;
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
},
|
|
2136
|
+
/** Check if element is disabled */
|
|
2137
|
+
toBeDisabled(element) {
|
|
2138
|
+
const isDisabledProp = element.disabled === true;
|
|
2139
|
+
const isDisabledAttr = element.hasAttribute("disabled");
|
|
2140
|
+
const isAriaDisabled = element.getAttribute("aria-disabled") === "true";
|
|
2141
|
+
const pass = isDisabledProp || isDisabledAttr || isAriaDisabled;
|
|
2142
|
+
return {
|
|
2143
|
+
pass,
|
|
2144
|
+
message: () => pass ? "Expected element not to be disabled, but it is" : "Expected element to be disabled, but it is not"
|
|
2145
|
+
};
|
|
2146
|
+
},
|
|
2147
|
+
/** Check if element has focus */
|
|
2148
|
+
toHaveFocus(element) {
|
|
2149
|
+
const pass = document.activeElement === element;
|
|
2150
|
+
return {
|
|
2151
|
+
pass,
|
|
2152
|
+
message: () => pass ? "Expected element not to have focus, but it does" : `Expected element to have focus, but active element is <${document.activeElement?.tagName?.toLowerCase() || "none"}>`
|
|
2153
|
+
};
|
|
2154
|
+
},
|
|
2155
|
+
/** Check if element has specific style */
|
|
2156
|
+
toHaveStyle(element, style) {
|
|
2157
|
+
const mismatches = [];
|
|
2158
|
+
for (const [prop, expected] of Object.entries(style)) {
|
|
2159
|
+
const kebabProp = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
2160
|
+
const actual = element.style.getPropertyValue(kebabProp);
|
|
2161
|
+
if (actual !== expected) {
|
|
2162
|
+
mismatches.push(`${prop}: expected "${expected}" but got "${actual}"`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const pass = mismatches.length === 0;
|
|
2166
|
+
return {
|
|
2167
|
+
pass,
|
|
2168
|
+
message: () => pass ? "Expected element not to have the specified styles, but it does" : `Style mismatches: ${mismatches.join("; ")}`
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
1909
2171
|
}
|
|
1910
2172
|
};
|
|
1911
|
-
const subscriber = run;
|
|
1912
|
-
subscriber.depsHead = null;
|
|
1913
|
-
subscriber.depsTail = null;
|
|
1914
|
-
subscriber._epoch = 0;
|
|
1915
|
-
subscriber._structDirty = false;
|
|
1916
|
-
subscriber._hasComputedDep = false;
|
|
1917
|
-
subscriber._runEpoch = 0;
|
|
1918
|
-
subscriber._runs = 0;
|
|
1919
|
-
subscriber._reentrant = false;
|
|
1920
|
-
subscriber._disposed = false;
|
|
1921
|
-
subscriber._errorPhase = "binding";
|
|
1922
|
-
subscriber._errorNode = ownerNode;
|
|
1923
|
-
try {
|
|
1924
|
-
run();
|
|
1925
|
-
} catch (err) {
|
|
1926
|
-
subscriber._disposed = true;
|
|
1927
|
-
subscriber._errorNode = void 0;
|
|
1928
|
-
cleanup(subscriber);
|
|
1929
|
-
throw err;
|
|
1930
|
-
}
|
|
1931
|
-
return subscriber._dispose ?? (subscriber._dispose = () => {
|
|
1932
|
-
subscriber._disposed = true;
|
|
1933
|
-
subscriber._errorNode = void 0;
|
|
1934
|
-
cleanup(subscriber);
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
function recordDependency(signal) {
|
|
1938
|
-
if (!currentSubscriber) return;
|
|
1939
|
-
const sub = currentSubscriber;
|
|
1940
|
-
const sig = signal;
|
|
1941
|
-
const epoch = sub._epoch ?? 0;
|
|
1942
|
-
const active = sig.__activeNode ?? null;
|
|
1943
|
-
if (active !== null && active.sub === sub) {
|
|
1944
|
-
active.epoch = epoch;
|
|
1945
|
-
active.depVersion = sig.__v ?? Number.NaN;
|
|
1946
|
-
return;
|
|
1947
|
-
}
|
|
1948
|
-
if (sig._validate !== void 0) sub._hasComputedDep = true;
|
|
1949
|
-
const node = allocNode(signal, sub, epoch);
|
|
1950
|
-
node.depVersion = sig.__v ?? Number.NaN;
|
|
1951
|
-
node.prevActive = active;
|
|
1952
|
-
sig.__activeNode = node;
|
|
1953
|
-
linkSub(sub, node);
|
|
1954
|
-
linkSignal(sig, node);
|
|
1955
|
-
sub._structDirty = true;
|
|
1956
|
-
}
|
|
1957
|
-
function cleanup(subscriber) {
|
|
1958
|
-
const sub = subscriber;
|
|
1959
|
-
let node = sub.depsHead ?? null;
|
|
1960
|
-
sub.depsHead = null;
|
|
1961
|
-
sub.depsTail = null;
|
|
1962
|
-
while (node) {
|
|
1963
|
-
const next = node.subNext;
|
|
1964
|
-
unlinkSignal(node);
|
|
1965
|
-
freeNode(node);
|
|
1966
|
-
node = next;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
var maxSubscriberRepeats = 1e3;
|
|
1970
|
-
var maxDrainIterations = 1e6;
|
|
1971
|
-
var drainEpoch = 0;
|
|
1972
|
-
var quarantined = /* @__PURE__ */ new Set();
|
|
1973
|
-
function setMaxSubscriberRepeats(n) {
|
|
1974
|
-
const prev = maxSubscriberRepeats;
|
|
1975
|
-
if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
|
|
1976
|
-
return prev;
|
|
1977
|
-
}
|
|
1978
|
-
function setMaxDrainIterations(n) {
|
|
1979
|
-
const prev = maxDrainIterations;
|
|
1980
|
-
if (Number.isFinite(n) && n > 0) maxDrainIterations = Math.floor(n);
|
|
1981
|
-
return prev;
|
|
1982
|
-
}
|
|
1983
|
-
function tickRepeat(sub) {
|
|
1984
|
-
const s = sub;
|
|
1985
|
-
if (s._runEpoch !== drainEpoch) {
|
|
1986
|
-
s._runEpoch = drainEpoch;
|
|
1987
|
-
s._runs = 1;
|
|
1988
|
-
return false;
|
|
1989
|
-
}
|
|
1990
|
-
s._runs = (s._runs ?? 0) + 1;
|
|
1991
|
-
return s._runs > maxSubscriberRepeats;
|
|
1992
|
-
}
|
|
1993
|
-
function cycleError(sub) {
|
|
1994
|
-
const name = sub.__name ?? "<unnamed>";
|
|
1995
|
-
reportError(
|
|
1996
|
-
new Error(
|
|
1997
|
-
`subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. This subscriber is quarantined for the rest of this update; other pending work still runs.`
|
|
1998
|
-
),
|
|
1999
|
-
{ phase: "scheduler", name }
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
function absoluteDrainError() {
|
|
2003
|
-
reportError(
|
|
2004
|
-
new Error(
|
|
2005
|
-
`Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
|
|
2006
|
-
),
|
|
2007
|
-
{ phase: "scheduler" }
|
|
2008
|
-
);
|
|
2009
2173
|
}
|
|
2010
|
-
function
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2174
|
+
function createCypressAdapter() {
|
|
2175
|
+
return {
|
|
2176
|
+
/** Mount a component for Cypress component testing */
|
|
2177
|
+
mount(component, options) {
|
|
2178
|
+
const ownsContainer = !options?.container;
|
|
2179
|
+
const container = options?.container || document.createElement("div");
|
|
2180
|
+
if (ownsContainer) {
|
|
2181
|
+
container.setAttribute("data-testenv", "cypress");
|
|
2182
|
+
document.body.appendChild(container);
|
|
2183
|
+
}
|
|
2184
|
+
const element = typeof component === "function" ? component() : component;
|
|
2185
|
+
container.appendChild(element);
|
|
2186
|
+
let unmounted = false;
|
|
2187
|
+
const unmount = () => {
|
|
2188
|
+
if (unmounted) return;
|
|
2189
|
+
unmounted = true;
|
|
2190
|
+
dispose(element);
|
|
2191
|
+
element.parentNode?.removeChild(element);
|
|
2192
|
+
if (ownsContainer) container.parentNode?.removeChild(container);
|
|
2193
|
+
};
|
|
2194
|
+
return { element, container, unmount };
|
|
2195
|
+
},
|
|
2196
|
+
/** Generate Cypress custom commands for SibuJS */
|
|
2197
|
+
commands: {
|
|
2198
|
+
/** Find by data-testid */
|
|
2199
|
+
getByTestId: (id) => `[data-testid="${escapeAttrValue(id)}"]`,
|
|
2200
|
+
/** Find by text */
|
|
2201
|
+
getByText: (text) => `:contains("${escapeAttrValue(text)}")`,
|
|
2202
|
+
/** Find by role */
|
|
2203
|
+
getByRole: (role) => `[role="${escapeAttrValue(role)}"]`
|
|
2025
2204
|
}
|
|
2026
|
-
|
|
2027
|
-
}
|
|
2205
|
+
};
|
|
2028
2206
|
}
|
|
2029
|
-
function
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2207
|
+
function createPlaywrightAdapter() {
|
|
2208
|
+
return {
|
|
2209
|
+
/** Selectors for common SibuJS patterns */
|
|
2210
|
+
selectors: {
|
|
2211
|
+
byTestId: (id) => `[data-testid="${escapeAttrValue(id)}"]`,
|
|
2212
|
+
byRole: (role) => `[role="${escapeAttrValue(role)}"]`,
|
|
2213
|
+
byAriaLabel: (label) => `[aria-label="${escapeAttrValue(label)}"]`,
|
|
2214
|
+
byDataAttr: (attr, value) => value ? `[data-${attr}="${escapeAttrValue(value)}"]` : `[data-${attr}]`
|
|
2215
|
+
},
|
|
2216
|
+
/** Generate a page object for a SibuJS component */
|
|
2217
|
+
createPageObject(name, selectors) {
|
|
2218
|
+
const selectorMap = { ...selectors };
|
|
2219
|
+
return {
|
|
2220
|
+
name,
|
|
2221
|
+
getSelector(key) {
|
|
2222
|
+
const selector = selectorMap[key];
|
|
2223
|
+
if (!selector) {
|
|
2224
|
+
throw new Error(
|
|
2225
|
+
`Page object "${name}" has no selector for key "${key}". Available keys: ${Object.keys(selectorMap).join(", ")}`
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
return selector;
|
|
2229
|
+
},
|
|
2230
|
+
allSelectors() {
|
|
2231
|
+
return { ...selectorMap };
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2041
2234
|
}
|
|
2042
|
-
}
|
|
2235
|
+
};
|
|
2043
2236
|
}
|
|
2044
|
-
function
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
if (
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2237
|
+
function createUniversalAdapter() {
|
|
2238
|
+
let container = null;
|
|
2239
|
+
return {
|
|
2240
|
+
/** Setup test environment */
|
|
2241
|
+
setup() {
|
|
2242
|
+
container = document.createElement("div");
|
|
2243
|
+
container.setAttribute("data-testenv", "universal");
|
|
2244
|
+
document.body.appendChild(container);
|
|
2245
|
+
return container;
|
|
2246
|
+
},
|
|
2247
|
+
/** Teardown test environment */
|
|
2248
|
+
teardown() {
|
|
2249
|
+
if (container) {
|
|
2250
|
+
replaceChildrenSafely(container);
|
|
2251
|
+
if (container.parentNode) {
|
|
2252
|
+
container.parentNode.removeChild(container);
|
|
2253
|
+
}
|
|
2254
|
+
container = null;
|
|
2255
|
+
}
|
|
2256
|
+
},
|
|
2257
|
+
/** Render component */
|
|
2258
|
+
render(component) {
|
|
2259
|
+
if (!container) {
|
|
2260
|
+
container = document.createElement("div");
|
|
2261
|
+
container.setAttribute("data-testenv", "universal");
|
|
2262
|
+
document.body.appendChild(container);
|
|
2263
|
+
}
|
|
2264
|
+
const element = typeof component === "function" ? component() : component;
|
|
2265
|
+
container.appendChild(element);
|
|
2266
|
+
return { element, container };
|
|
2267
|
+
},
|
|
2268
|
+
/** Query helpers */
|
|
2269
|
+
queries: {
|
|
2270
|
+
byTestId(container2, id) {
|
|
2271
|
+
return queryByAttribute(container2, "data-testid", id);
|
|
2272
|
+
},
|
|
2273
|
+
byRole(container2, role) {
|
|
2274
|
+
return queryByAttribute(container2, "role", role);
|
|
2275
|
+
},
|
|
2276
|
+
byText(container2, text) {
|
|
2277
|
+
const walker = document.createTreeWalker(container2, NodeFilter.SHOW_TEXT);
|
|
2278
|
+
while (walker.nextNode()) {
|
|
2279
|
+
if (walker.currentNode.textContent?.includes(text)) {
|
|
2280
|
+
return walker.currentNode.parentElement;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
return null;
|
|
2284
|
+
},
|
|
2285
|
+
byLabelText(container2, label) {
|
|
2286
|
+
const labels = container2.querySelectorAll("label");
|
|
2287
|
+
for (const labelEl of Array.from(labels)) {
|
|
2288
|
+
if (labelEl.textContent?.includes(label)) {
|
|
2289
|
+
const forAttr = labelEl.getAttribute("for");
|
|
2290
|
+
if (forAttr) {
|
|
2291
|
+
return queryByAttribute(container2, "id", forAttr);
|
|
2064
2292
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2293
|
+
const nested = labelEl.querySelector("input, select, textarea");
|
|
2294
|
+
if (nested) return nested;
|
|
2067
2295
|
}
|
|
2068
|
-
} else if (!pendingSet.has(s)) {
|
|
2069
|
-
pendingSet.add(s);
|
|
2070
|
-
pendingQueue.push(s);
|
|
2071
2296
|
}
|
|
2297
|
+
return queryByAttribute(container2, "aria-label", label);
|
|
2298
|
+
},
|
|
2299
|
+
allByRole(container2, role) {
|
|
2300
|
+
return queryAllByAttribute(container2, "role", role);
|
|
2072
2301
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
while (node) {
|
|
2081
|
-
const s = node.sub;
|
|
2082
|
-
if (s) {
|
|
2083
|
-
if (s._c) {
|
|
2084
|
-
propagateDirty(s);
|
|
2085
|
-
} else if (!pendingSet.has(s)) {
|
|
2086
|
-
pendingSet.add(s);
|
|
2087
|
-
pendingQueue.push(s);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
node = node.sigNext;
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
function notifySubscribers(signal) {
|
|
2094
|
-
const sig = signal;
|
|
2095
|
-
const head = sig.subsHead;
|
|
2096
|
-
if (!head) return;
|
|
2097
|
-
if (notifyDepth > 0) {
|
|
2098
|
-
let node = head;
|
|
2099
|
-
while (node) {
|
|
2100
|
-
const s = node.sub;
|
|
2101
|
-
if (s) {
|
|
2102
|
-
if (s._c) {
|
|
2103
|
-
propagateDirty(s);
|
|
2104
|
-
} else if (!pendingSet.has(s)) {
|
|
2105
|
-
pendingSet.add(s);
|
|
2106
|
-
pendingQueue.push(s);
|
|
2302
|
+
},
|
|
2303
|
+
/** Assertion helpers */
|
|
2304
|
+
assert: {
|
|
2305
|
+
textContent(el, expected) {
|
|
2306
|
+
const actual = el.textContent || "";
|
|
2307
|
+
if (!actual.includes(expected)) {
|
|
2308
|
+
throw new Error(`Expected text content to include "${expected}", but got "${actual}"`);
|
|
2107
2309
|
}
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2310
|
+
},
|
|
2311
|
+
attribute(el, attr, value) {
|
|
2312
|
+
if (!el.hasAttribute(attr)) {
|
|
2313
|
+
throw new Error(`Expected element to have attribute "${attr}", but it does not`);
|
|
2314
|
+
}
|
|
2315
|
+
if (value !== void 0) {
|
|
2316
|
+
const actual = el.getAttribute(attr);
|
|
2317
|
+
if (actual !== value) {
|
|
2318
|
+
throw new Error(`Expected attribute "${attr}" to be "${value}", but got "${actual}"`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
visible(el) {
|
|
2323
|
+
const htmlEl = el;
|
|
2324
|
+
const reasons = [];
|
|
2325
|
+
if (el.hasAttribute("hidden")) reasons.push('has "hidden" attribute');
|
|
2326
|
+
if (htmlEl.style?.display === "none") reasons.push("has display:none");
|
|
2327
|
+
if (htmlEl.style?.visibility === "hidden") reasons.push("has visibility:hidden");
|
|
2328
|
+
if (reasons.length > 0) {
|
|
2329
|
+
throw new Error(`Expected element to be visible, but it ${reasons.join(" and ")}`);
|
|
2330
|
+
}
|
|
2331
|
+
},
|
|
2332
|
+
disabled(el) {
|
|
2333
|
+
const isDisabled = el.disabled === true || el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
2334
|
+
if (!isDisabled) {
|
|
2335
|
+
throw new Error("Expected element to be disabled, but it is not");
|
|
2336
|
+
}
|
|
2337
|
+
},
|
|
2338
|
+
className(el, name) {
|
|
2339
|
+
if (!el.classList.contains(name)) {
|
|
2340
|
+
throw new Error(`Expected element to have class "${name}", but it has "${el.className}"`);
|
|
2125
2341
|
}
|
|
2126
2342
|
}
|
|
2127
|
-
node = node.sigNext;
|
|
2128
|
-
}
|
|
2129
|
-
drainQueue();
|
|
2130
|
-
} finally {
|
|
2131
|
-
notifyDepth--;
|
|
2132
|
-
if (notifyDepth === 0) {
|
|
2133
|
-
pendingQueue.length = 0;
|
|
2134
|
-
pendingSet.clear();
|
|
2135
|
-
if (quarantined.size > 0) quarantined.clear();
|
|
2136
2343
|
}
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
function getSubscriberCount(signal) {
|
|
2140
|
-
return signal.__sc ?? 0;
|
|
2344
|
+
};
|
|
2141
2345
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
while (node) {
|
|
2147
|
-
if (node.sig) out.push(node.sig);
|
|
2148
|
-
node = node.subNext;
|
|
2149
|
-
}
|
|
2150
|
-
return out;
|
|
2346
|
+
|
|
2347
|
+
// src/testing/serializeDom.ts
|
|
2348
|
+
function escapeSnapshotText(value) {
|
|
2349
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2151
2350
|
}
|
|
2152
|
-
function
|
|
2153
|
-
|
|
2154
|
-
while (node) {
|
|
2155
|
-
const s = node.sub;
|
|
2156
|
-
if (s) visit(s);
|
|
2157
|
-
node = node.sigNext;
|
|
2158
|
-
}
|
|
2351
|
+
function escapeSnapshotAttribute(value) {
|
|
2352
|
+
return escapeSnapshotText(value).replace(/"/g, """);
|
|
2159
2353
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
var _runtimeVersion = true ? "4.5.0" : "dev";
|
|
2163
|
-
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
|
|
2164
|
-
function resolveReactiveApi() {
|
|
2165
|
-
const g = globalThis;
|
|
2166
|
-
const existing = g[REGISTRY_KEY];
|
|
2167
|
-
if (existing) {
|
|
2168
|
-
if (DEV && !existing.__dupWarned) {
|
|
2169
|
-
existing.__dupWarned = true;
|
|
2170
|
-
devWarn(
|
|
2171
|
-
`Multiple instances of the reactive runtime detected on this page (active: ${existing.version}, duplicate: ${_runtimeVersion}). Reactivity is NOT broken and no bundler configuration is required: every copy routes through the first one via a global registry, so signal writes still reach subscribers registered by any copy. This is only a SIZE issue \u2014 the duplicate copy's bytes are downloaded and parsed for nothing. If you want them back, de-duplicate sibujs in your bundler (Vite: resolve.dedupe: ['sibujs']). Mismatched VERSIONS above are worth fixing regardless, since only the first one loaded is used.`
|
|
2172
|
-
);
|
|
2173
|
-
}
|
|
2174
|
-
return existing;
|
|
2175
|
-
}
|
|
2176
|
-
const local = {
|
|
2177
|
-
suspendTracking,
|
|
2178
|
-
resumeTracking,
|
|
2179
|
-
isTrackingSuspended,
|
|
2180
|
-
untracked,
|
|
2181
|
-
retrack,
|
|
2182
|
-
track,
|
|
2183
|
-
reactiveBinding,
|
|
2184
|
-
recordDependency,
|
|
2185
|
-
cleanup,
|
|
2186
|
-
setMaxSubscriberRepeats,
|
|
2187
|
-
setMaxDrainIterations,
|
|
2188
|
-
drainNotificationQueue,
|
|
2189
|
-
queueSignalNotification,
|
|
2190
|
-
notifySubscribers,
|
|
2191
|
-
getSubscriberCount,
|
|
2192
|
-
getSubscriberDeps,
|
|
2193
|
-
forEachSubscriber,
|
|
2194
|
-
version: _runtimeVersion
|
|
2195
|
-
};
|
|
2196
|
-
g[REGISTRY_KEY] = local;
|
|
2197
|
-
return local;
|
|
2354
|
+
function escapeSnapshotComment(value) {
|
|
2355
|
+
return escapeSnapshotText(value).replace(/-/g, "-");
|
|
2198
2356
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
var recordDependency2 = API.recordDependency;
|
|
2208
|
-
var cleanup2 = API.cleanup;
|
|
2209
|
-
var setMaxSubscriberRepeats2 = API.setMaxSubscriberRepeats;
|
|
2210
|
-
var setMaxDrainIterations2 = API.setMaxDrainIterations;
|
|
2211
|
-
var drainNotificationQueue2 = API.drainNotificationQueue;
|
|
2212
|
-
var queueSignalNotification2 = API.queueSignalNotification;
|
|
2213
|
-
var notifySubscribers2 = API.notifySubscribers;
|
|
2214
|
-
var getSubscriberCount2 = API.getSubscriberCount;
|
|
2215
|
-
var getSubscriberDeps2 = API.getSubscriberDeps;
|
|
2216
|
-
var forEachSubscriber2 = API.forEachSubscriber;
|
|
2217
|
-
|
|
2218
|
-
// src/core/ssr-context.ts
|
|
2219
|
-
var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
|
|
2220
|
-
function detectSSRShared() {
|
|
2221
|
-
let detected = null;
|
|
2222
|
-
try {
|
|
2223
|
-
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
2224
|
-
let mod = null;
|
|
2225
|
-
const getBuiltin = process.getBuiltinModule;
|
|
2226
|
-
if (typeof getBuiltin === "function") {
|
|
2227
|
-
mod = getBuiltin("node:async_hooks");
|
|
2228
|
-
} else if (typeof require === "function") {
|
|
2229
|
-
mod = require("async_hooks");
|
|
2230
|
-
}
|
|
2231
|
-
if (mod) detected = new mod.AsyncLocalStorage();
|
|
2232
|
-
}
|
|
2233
|
-
} catch {
|
|
2234
|
-
detected = null;
|
|
2357
|
+
function serializeDom(el, indent = 0, options = {}) {
|
|
2358
|
+
const pad = " ".repeat(indent);
|
|
2359
|
+
const tag = el.tagName.toLowerCase();
|
|
2360
|
+
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(" ");
|
|
2361
|
+
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
2362
|
+
const children = Array.from(el.childNodes);
|
|
2363
|
+
if (children.length === 0) {
|
|
2364
|
+
return `${open}</${tag}>`;
|
|
2235
2365
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
|
|
2239
|
-
fallbackDepth: 0
|
|
2240
|
-
};
|
|
2241
|
-
}
|
|
2242
|
-
var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
|
|
2243
|
-
var als = _shared.als;
|
|
2244
|
-
var fallbackStore = _shared.fallbackStore;
|
|
2245
|
-
_shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
|
|
2246
|
-
function getSSRStore() {
|
|
2247
|
-
if (als) {
|
|
2248
|
-
const s = als.getStore();
|
|
2249
|
-
if (s) return s;
|
|
2366
|
+
if (children.length === 1 && children[0].nodeType === 3) {
|
|
2367
|
+
return `${open}${escapeSnapshotText(children[0].textContent?.trim() || "")}</${tag}>`;
|
|
2250
2368
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2369
|
+
const childPad = " ".repeat(indent + 1);
|
|
2370
|
+
const childStr = children.map((child) => {
|
|
2371
|
+
if (child.nodeType === 3) {
|
|
2372
|
+
const text = child.textContent?.trim();
|
|
2373
|
+
return text ? `${childPad}${escapeSnapshotText(text)}` : "";
|
|
2374
|
+
}
|
|
2375
|
+
if (child.nodeType === 1) {
|
|
2376
|
+
return serializeDom(child, indent + 1, options);
|
|
2377
|
+
}
|
|
2378
|
+
if (child.nodeType === 8 && options.comments) {
|
|
2379
|
+
return `${childPad}<!-- ${escapeSnapshotComment(child.textContent?.trim() || "")} -->`;
|
|
2380
|
+
}
|
|
2381
|
+
return "";
|
|
2382
|
+
}).filter(Boolean).join("\n");
|
|
2383
|
+
return `${open}
|
|
2384
|
+
${childStr}
|
|
2385
|
+
${pad}</${tag}>`;
|
|
2255
2386
|
}
|
|
2256
2387
|
|
|
2257
|
-
// src/
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
const
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2388
|
+
// src/testing/e2e.ts
|
|
2389
|
+
function createHttpMock(routes = [], options = {}) {
|
|
2390
|
+
const originalFetch = globalThis.fetch;
|
|
2391
|
+
const hadOriginalFetch = Object.hasOwn(globalThis, "fetch");
|
|
2392
|
+
const requestLog = [];
|
|
2393
|
+
const mockRoutes = [...routes];
|
|
2394
|
+
function matchesStringRoute(route, url) {
|
|
2395
|
+
if (url === route) return true;
|
|
2396
|
+
let target;
|
|
2265
2397
|
try {
|
|
2266
|
-
|
|
2267
|
-
} catch
|
|
2268
|
-
|
|
2398
|
+
target = new URL(url, "http://localhost");
|
|
2399
|
+
} catch {
|
|
2400
|
+
return false;
|
|
2269
2401
|
}
|
|
2402
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(route)) {
|
|
2403
|
+
return `${target.origin}${target.pathname}${target.search}` === route || target.href === route;
|
|
2404
|
+
}
|
|
2405
|
+
const hashIndex = route.indexOf("#");
|
|
2406
|
+
const bare = hashIndex === -1 ? route : route.slice(0, hashIndex);
|
|
2407
|
+
return bare.includes("?") ? `${target.pathname}${target.search}` === bare : target.pathname === bare;
|
|
2270
2408
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
if (ctx.rerunPending) {
|
|
2280
|
-
ctx.rerunPending = false;
|
|
2281
|
-
reportError(
|
|
2282
|
-
new Error(
|
|
2283
|
-
`effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
|
|
2284
|
-
),
|
|
2285
|
-
{ phase: "effect" }
|
|
2286
|
-
);
|
|
2409
|
+
function matchRoute(url, method) {
|
|
2410
|
+
return mockRoutes.find((route) => {
|
|
2411
|
+
const methodMatch = !route.method || route.method.toUpperCase() === method.toUpperCase();
|
|
2412
|
+
if (!methodMatch) return false;
|
|
2413
|
+
if (typeof route.url === "string") return matchesStringRoute(route.url, url);
|
|
2414
|
+
route.url.lastIndex = 0;
|
|
2415
|
+
return route.url.test(url);
|
|
2416
|
+
});
|
|
2287
2417
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
if (h) {
|
|
2418
|
+
const abortError = (signal) => signal.reason !== void 0 ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
2419
|
+
const isFormEncoded = (type2) => /^application\/x-www-form-urlencoded\b/i.test(type2);
|
|
2420
|
+
const isTextLike = (type2) => /^text\/|[/+](?:json|xml|javascript)\b/i.test(type2);
|
|
2421
|
+
const decodeText = (text, type2) => {
|
|
2422
|
+
if (isFormEncoded(type2)) return new URLSearchParams(text);
|
|
2294
2423
|
try {
|
|
2295
|
-
|
|
2424
|
+
return JSON.parse(text);
|
|
2296
2425
|
} catch {
|
|
2426
|
+
return text;
|
|
2297
2427
|
}
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2428
|
+
};
|
|
2429
|
+
const httpBase = () => {
|
|
2430
|
+
const href = globalThis.location?.href;
|
|
2431
|
+
if (href) {
|
|
2432
|
+
try {
|
|
2433
|
+
if (/^https?:$/.test(new URL(href).protocol)) return href;
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2304
2436
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2437
|
+
return "http://localhost";
|
|
2438
|
+
};
|
|
2439
|
+
const STRUCTURED_CONTENT_TYPE = {
|
|
2440
|
+
FormData: () => "multipart/form-data",
|
|
2441
|
+
URLSearchParams: () => "application/x-www-form-urlencoded;charset=UTF-8",
|
|
2442
|
+
Blob: (raw) => raw.type || null,
|
|
2443
|
+
File: (raw) => raw.type || null
|
|
2444
|
+
};
|
|
2445
|
+
const AUTO_TEXT_TYPE = /^text\/plain;charset=utf-8$/i;
|
|
2446
|
+
const structuredTag = (raw) => {
|
|
2447
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
2448
|
+
const tag = Object.prototype.toString.call(raw).slice(8, -1);
|
|
2449
|
+
return Object.hasOwn(STRUCTURED_CONTENT_TYPE, tag) ? tag : null;
|
|
2450
|
+
};
|
|
2451
|
+
const unusableBody = async (raw, tag) => {
|
|
2452
|
+
let probe;
|
|
2453
|
+
try {
|
|
2454
|
+
probe = new Request("http://localhost/", { method: "POST", body: raw });
|
|
2455
|
+
} catch {
|
|
2456
|
+
return true;
|
|
2311
2457
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
function effect(effectFn, options) {
|
|
2315
|
-
devAssert(typeof effectFn === "function", "effect: argument must be a function.");
|
|
2316
|
-
if (isSSR()) return () => {
|
|
2458
|
+
if (!AUTO_TEXT_TYPE.test(probe.headers.get("content-type") ?? "")) return false;
|
|
2459
|
+
return tag === "URLSearchParams" || await probe.text() === String(raw);
|
|
2317
2460
|
};
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2461
|
+
const toEffectiveRequest = (input, request, init, method, rawBody, omitBody) => {
|
|
2462
|
+
const overrides = {};
|
|
2463
|
+
const headers = init?.headers;
|
|
2464
|
+
if (method !== void 0) overrides.method = method;
|
|
2465
|
+
if (headers !== void 0) overrides.headers = headers;
|
|
2466
|
+
if (rawBody != null && !omitBody) {
|
|
2467
|
+
overrides.body = rawBody;
|
|
2468
|
+
if (typeof ReadableStream !== "undefined" && rawBody instanceof ReadableStream) overrides.duplex = "half";
|
|
2469
|
+
}
|
|
2470
|
+
if (request) return new Request(request.clone(), overrides);
|
|
2471
|
+
const raw = input instanceof URL ? input.href : input;
|
|
2472
|
+
const href = new URL(raw, httpBase()).href;
|
|
2473
|
+
return new Request(href, overrides);
|
|
2328
2474
|
};
|
|
2329
|
-
|
|
2330
|
-
|
|
2475
|
+
const decodeBody = async (effective) => {
|
|
2476
|
+
if (effective.body === null) return void 0;
|
|
2477
|
+
const type2 = effective.headers.get("content-type") ?? "";
|
|
2478
|
+
if (/^multipart\/form-data\b/i.test(type2)) return effective.formData();
|
|
2479
|
+
if (isTextLike(type2) || isFormEncoded(type2)) return decodeText(await effective.text(), type2);
|
|
2480
|
+
return effective.blob();
|
|
2331
2481
|
};
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2482
|
+
const mockFetch = async (input, init) => {
|
|
2483
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
2484
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2485
|
+
const initSignal = init?.signal;
|
|
2486
|
+
const signal = initSignal === null ? void 0 : initSignal ?? request?.signal;
|
|
2487
|
+
if (signal?.aborted) throw abortError(signal);
|
|
2488
|
+
const rawBody = init?.body;
|
|
2489
|
+
const initMethod = init?.method;
|
|
2490
|
+
const tag = structuredTag(rawBody);
|
|
2491
|
+
const passThrough = tag !== null && await unusableBody(rawBody, tag);
|
|
2492
|
+
if (passThrough) {
|
|
2493
|
+
const intended = (initMethod ?? request?.method ?? "GET").toUpperCase();
|
|
2494
|
+
if (intended === "GET" || intended === "HEAD") {
|
|
2495
|
+
throw new TypeError(`Request with ${intended} method cannot have body.`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
const effective = toEffectiveRequest(input, request, init, initMethod, rawBody, passThrough);
|
|
2499
|
+
const method = effective.method.toUpperCase();
|
|
2500
|
+
let headers = effective.headers;
|
|
2501
|
+
let body;
|
|
2502
|
+
if (passThrough && tag) {
|
|
2503
|
+
body = rawBody;
|
|
2504
|
+
if (!headers.has("content-type")) {
|
|
2505
|
+
const type2 = STRUCTURED_CONTENT_TYPE[tag](rawBody);
|
|
2506
|
+
if (type2) {
|
|
2507
|
+
headers = new Headers(headers);
|
|
2508
|
+
headers.set("content-type", type2);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
} else {
|
|
2512
|
+
body = await decodeBody(effective);
|
|
2513
|
+
}
|
|
2514
|
+
if (signal?.aborted) throw abortError(signal);
|
|
2515
|
+
requestLog.push({ url, method, body, timestamp: Date.now() });
|
|
2516
|
+
const route = matchRoute(url, method);
|
|
2517
|
+
if (!route) {
|
|
2518
|
+
return new Response(JSON.stringify({ error: "Not mocked" }), { status: 404 });
|
|
2519
|
+
}
|
|
2520
|
+
const abortable = (work) => {
|
|
2521
|
+
if (!signal) return work;
|
|
2522
|
+
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
2523
|
+
return new Promise((resolve, reject) => {
|
|
2524
|
+
const onAbort = () => reject(abortError(signal));
|
|
2525
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2526
|
+
work.then(
|
|
2527
|
+
(value) => {
|
|
2528
|
+
signal.removeEventListener("abort", onAbort);
|
|
2529
|
+
resolve(value);
|
|
2530
|
+
},
|
|
2531
|
+
(err) => {
|
|
2532
|
+
signal.removeEventListener("abort", onAbort);
|
|
2533
|
+
reject(err);
|
|
2534
|
+
}
|
|
2535
|
+
);
|
|
2536
|
+
});
|
|
2537
|
+
};
|
|
2538
|
+
let mockResponse;
|
|
2334
2539
|
try {
|
|
2335
|
-
|
|
2540
|
+
if (typeof route.response === "function") {
|
|
2541
|
+
const handler = route.response;
|
|
2542
|
+
mockResponse = await abortable(Promise.resolve().then(() => handler({ url, method, body, headers })));
|
|
2543
|
+
} else {
|
|
2544
|
+
mockResponse = route.response;
|
|
2545
|
+
}
|
|
2336
2546
|
} catch (err) {
|
|
2337
|
-
|
|
2547
|
+
if (signal?.aborted) throw abortError(signal);
|
|
2548
|
+
return new Response(JSON.stringify({ error: String(err) }), { status: 500 });
|
|
2549
|
+
}
|
|
2550
|
+
if (mockResponse.delay) {
|
|
2551
|
+
let timer;
|
|
2552
|
+
try {
|
|
2553
|
+
await abortable(
|
|
2554
|
+
new Promise((r) => {
|
|
2555
|
+
timer = setTimeout(r, mockResponse.delay);
|
|
2556
|
+
})
|
|
2557
|
+
);
|
|
2558
|
+
} finally {
|
|
2559
|
+
clearTimeout(timer);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return new Response(typeof mockResponse.body === "string" ? mockResponse.body : JSON.stringify(mockResponse.body), {
|
|
2563
|
+
status: mockResponse.status || 200,
|
|
2564
|
+
statusText: mockResponse.statusText || "OK",
|
|
2565
|
+
headers: mockResponse.headers
|
|
2566
|
+
});
|
|
2567
|
+
};
|
|
2568
|
+
const restore = () => {
|
|
2569
|
+
if (hadOriginalFetch) {
|
|
2570
|
+
globalThis.fetch = originalFetch;
|
|
2571
|
+
} else {
|
|
2572
|
+
delete globalThis.fetch;
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
const api = {
|
|
2576
|
+
/** Install the mock (replace global fetch) */
|
|
2577
|
+
install() {
|
|
2578
|
+
globalThis.fetch = mockFetch;
|
|
2579
|
+
},
|
|
2580
|
+
/** Restore original fetch */
|
|
2581
|
+
restore,
|
|
2582
|
+
/** Add a mock route */
|
|
2583
|
+
addRoute(route) {
|
|
2584
|
+
mockRoutes.push(route);
|
|
2585
|
+
},
|
|
2586
|
+
/** Remove all mock routes */
|
|
2587
|
+
clearRoutes() {
|
|
2588
|
+
mockRoutes.length = 0;
|
|
2589
|
+
},
|
|
2590
|
+
/** Get the request log */
|
|
2591
|
+
getRequests() {
|
|
2592
|
+
return [...requestLog];
|
|
2593
|
+
},
|
|
2594
|
+
/** Clear the request log */
|
|
2595
|
+
clearLog() {
|
|
2596
|
+
requestLog.length = 0;
|
|
2597
|
+
},
|
|
2598
|
+
/** Assert that a URL was called */
|
|
2599
|
+
assertCalled(url, method = "GET") {
|
|
2600
|
+
const found = requestLog.some((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase());
|
|
2601
|
+
if (!found) throw new Error(`Expected ${method} ${url} to have been called`);
|
|
2602
|
+
},
|
|
2603
|
+
/** Assert that a URL was NOT called */
|
|
2604
|
+
assertNotCalled(url, method = "GET") {
|
|
2605
|
+
const found = requestLog.some((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase());
|
|
2606
|
+
if (found) throw new Error(`Expected ${method} ${url} to NOT have been called`);
|
|
2607
|
+
},
|
|
2608
|
+
/** Get number of times a URL was called */
|
|
2609
|
+
callCount(url, method = "GET") {
|
|
2610
|
+
return requestLog.filter((r) => r.url.includes(url) && r.method.toUpperCase() === method.toUpperCase()).length;
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
if (typeof options.afterEach === "function") {
|
|
2614
|
+
options.afterEach(() => api.restore());
|
|
2615
|
+
}
|
|
2616
|
+
return api;
|
|
2617
|
+
}
|
|
2618
|
+
var MIN_INTERVAL_MS = 1;
|
|
2619
|
+
function normalizeIntervalDelay(delay) {
|
|
2620
|
+
return Number.isFinite(delay) && delay >= MIN_INTERVAL_MS ? delay : MIN_INTERVAL_MS;
|
|
2621
|
+
}
|
|
2622
|
+
function createTimerMock(options = {}) {
|
|
2623
|
+
const g = globalThis;
|
|
2624
|
+
const snapshot = (key) => ({
|
|
2625
|
+
had: Object.hasOwn(globalThis, key),
|
|
2626
|
+
value: g[key]
|
|
2627
|
+
});
|
|
2628
|
+
const saved = {
|
|
2629
|
+
setTimeout: snapshot("setTimeout"),
|
|
2630
|
+
setInterval: snapshot("setInterval"),
|
|
2631
|
+
clearTimeout: snapshot("clearTimeout"),
|
|
2632
|
+
clearInterval: snapshot("clearInterval"),
|
|
2633
|
+
requestAnimationFrame: snapshot("requestAnimationFrame"),
|
|
2634
|
+
cancelAnimationFrame: snapshot("cancelAnimationFrame")
|
|
2635
|
+
};
|
|
2636
|
+
let currentTime = 0;
|
|
2637
|
+
let nextId = 1;
|
|
2638
|
+
const timers = [];
|
|
2639
|
+
const api = {
|
|
2640
|
+
install() {
|
|
2641
|
+
currentTime = 0;
|
|
2642
|
+
globalThis.setTimeout = (cb, delay = 0) => {
|
|
2643
|
+
const id = nextId++;
|
|
2644
|
+
timers.push({ id, callback: cb, time: currentTime + delay });
|
|
2645
|
+
return id;
|
|
2646
|
+
};
|
|
2647
|
+
globalThis.setInterval = (cb, interval) => {
|
|
2648
|
+
const id = nextId++;
|
|
2649
|
+
const period = normalizeIntervalDelay(interval);
|
|
2650
|
+
timers.push({ id, callback: cb, time: currentTime + period, interval: period });
|
|
2651
|
+
return id;
|
|
2652
|
+
};
|
|
2653
|
+
globalThis.clearTimeout = (id) => {
|
|
2654
|
+
const idx = timers.findIndex((t) => t.id === id);
|
|
2655
|
+
if (idx !== -1) timers.splice(idx, 1);
|
|
2656
|
+
};
|
|
2657
|
+
globalThis.clearInterval = (id) => {
|
|
2658
|
+
const idx = timers.findIndex((t) => t.id === id);
|
|
2659
|
+
if (idx !== -1) timers.splice(idx, 1);
|
|
2660
|
+
};
|
|
2661
|
+
globalThis.requestAnimationFrame = (cb) => {
|
|
2662
|
+
const id = nextId++;
|
|
2663
|
+
timers.push({ id, callback: () => cb(currentTime), time: currentTime + 16 });
|
|
2664
|
+
return id;
|
|
2665
|
+
};
|
|
2666
|
+
globalThis.cancelAnimationFrame = (id) => {
|
|
2667
|
+
const idx = timers.findIndex((t) => t.id === id);
|
|
2668
|
+
if (idx !== -1) timers.splice(idx, 1);
|
|
2669
|
+
};
|
|
2670
|
+
},
|
|
2671
|
+
restore() {
|
|
2672
|
+
for (const [key, snap] of Object.entries(saved)) {
|
|
2673
|
+
if (snap.had) {
|
|
2674
|
+
g[key] = snap.value;
|
|
2675
|
+
} else {
|
|
2676
|
+
delete g[key];
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
timers.length = 0;
|
|
2680
|
+
},
|
|
2681
|
+
/** Advance time by a given number of ms, running any timers that fire */
|
|
2682
|
+
advance(ms) {
|
|
2683
|
+
const targetTime = currentTime + ms;
|
|
2684
|
+
while (true) {
|
|
2685
|
+
timers.sort((a, b) => a.time - b.time);
|
|
2686
|
+
const next = timers.find((t) => t.time <= targetTime);
|
|
2687
|
+
if (!next) break;
|
|
2688
|
+
currentTime = next.time;
|
|
2689
|
+
const idx = timers.indexOf(next);
|
|
2690
|
+
if (next.interval !== void 0) {
|
|
2691
|
+
next.time += next.interval;
|
|
2692
|
+
} else {
|
|
2693
|
+
timers.splice(idx, 1);
|
|
2694
|
+
}
|
|
2695
|
+
next.callback();
|
|
2696
|
+
}
|
|
2697
|
+
currentTime = targetTime;
|
|
2698
|
+
},
|
|
2699
|
+
/** Run all pending timers immediately */
|
|
2700
|
+
flush() {
|
|
2701
|
+
const maxIterations = 1e3;
|
|
2702
|
+
let i = 0;
|
|
2703
|
+
while (timers.length > 0 && i++ < maxIterations) {
|
|
2704
|
+
timers.sort((a, b) => a.time - b.time);
|
|
2705
|
+
const next = timers[0];
|
|
2706
|
+
currentTime = next.time;
|
|
2707
|
+
if (next.interval !== void 0) {
|
|
2708
|
+
next.time += next.interval;
|
|
2709
|
+
} else {
|
|
2710
|
+
timers.shift();
|
|
2711
|
+
}
|
|
2712
|
+
next.callback();
|
|
2713
|
+
}
|
|
2714
|
+
if (timers.length > 0 && i > maxIterations) {
|
|
2715
|
+
reportError(
|
|
2716
|
+
new Error(
|
|
2717
|
+
`[createTimerMock] flush() stopped after ${maxIterations} timer runs with ${timers.length} still pending \u2014 likely a recurring interval.`
|
|
2718
|
+
),
|
|
2719
|
+
{ phase: "scheduler", name: "createTimerMock.flush" }
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
},
|
|
2723
|
+
/** Get current fake time */
|
|
2724
|
+
now() {
|
|
2725
|
+
return currentTime;
|
|
2726
|
+
},
|
|
2727
|
+
/** Get number of pending timers */
|
|
2728
|
+
pendingCount() {
|
|
2729
|
+
return timers.length;
|
|
2338
2730
|
}
|
|
2339
|
-
} : () => {
|
|
2340
|
-
ctx.fn(ctx.onCleanup);
|
|
2341
2731
|
};
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
if (ctx.running) {
|
|
2345
|
-
ctx.rerunPending = true;
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
ctx.running = true;
|
|
2349
|
-
try {
|
|
2350
|
-
ctx.rerunPending = false;
|
|
2351
|
-
if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
|
|
2352
|
-
retrack2(ctx.bodyFn, sub);
|
|
2353
|
-
if (ctx.rerunPending) drainReruns(ctx);
|
|
2354
|
-
} finally {
|
|
2355
|
-
ctx.running = false;
|
|
2356
|
-
ctx.rerunPending = false;
|
|
2357
|
-
}
|
|
2358
|
-
});
|
|
2359
|
-
sub.depsHead = null;
|
|
2360
|
-
sub.depsTail = null;
|
|
2361
|
-
sub._epoch = 0;
|
|
2362
|
-
sub._structDirty = false;
|
|
2363
|
-
sub._hasComputedDep = false;
|
|
2364
|
-
sub._errorPhase = "effect";
|
|
2365
|
-
sub._runEpoch = 0;
|
|
2366
|
-
sub._runs = 0;
|
|
2367
|
-
ctx.subscriber = sub;
|
|
2368
|
-
ctx.running = true;
|
|
2369
|
-
try {
|
|
2370
|
-
retrack2(ctx.bodyFn, ctx.subscriber);
|
|
2371
|
-
if (ctx.rerunPending) drainReruns(ctx);
|
|
2372
|
-
} finally {
|
|
2373
|
-
ctx.running = false;
|
|
2374
|
-
ctx.rerunPending = false;
|
|
2732
|
+
if (typeof options.afterEach === "function") {
|
|
2733
|
+
options.afterEach(() => api.restore());
|
|
2375
2734
|
}
|
|
2376
|
-
|
|
2377
|
-
if (hook) hook.emit("effect:create", { effectFn });
|
|
2378
|
-
return () => disposeEffect(ctx);
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
// src/testing/queries.ts
|
|
2382
|
-
function queryByText(container, text) {
|
|
2383
|
-
const walk = (node) => {
|
|
2384
|
-
if (node.childNodes.length === 1 && node.childNodes[0].nodeType === 3) {
|
|
2385
|
-
if (node.textContent?.includes(text)) return node;
|
|
2386
|
-
}
|
|
2387
|
-
for (const child of Array.from(node.children)) {
|
|
2388
|
-
const found = walk(child);
|
|
2389
|
-
if (found) return found;
|
|
2390
|
-
}
|
|
2391
|
-
return null;
|
|
2392
|
-
};
|
|
2393
|
-
return walk(container);
|
|
2394
|
-
}
|
|
2395
|
-
function queryByTestId(container, testId) {
|
|
2396
|
-
return container.querySelector(`[data-testid="${testId}"]`);
|
|
2397
|
-
}
|
|
2398
|
-
function queryByRole(container, role) {
|
|
2399
|
-
return container.querySelector(`[role="${role}"]`);
|
|
2735
|
+
return api;
|
|
2400
2736
|
}
|
|
2401
|
-
function
|
|
2402
|
-
|
|
2403
|
-
if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
|
|
2404
|
-
return value.replace(/[^\w-]/g, (m) => `\\${m.charCodeAt(0).toString(16)} `);
|
|
2737
|
+
function createDOMSnapshot(element) {
|
|
2738
|
+
return serializeDom(element);
|
|
2405
2739
|
}
|
|
2406
|
-
function
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
}
|
|
2740
|
+
function assertDOMEquals(actual, expected) {
|
|
2741
|
+
const actualSnapshot = createDOMSnapshot(actual);
|
|
2742
|
+
const expectedSnapshot = createDOMSnapshot(expected);
|
|
2743
|
+
if (actualSnapshot !== expectedSnapshot) {
|
|
2744
|
+
throw new Error(`DOM mismatch:
|
|
2745
|
+
|
|
2746
|
+
Actual:
|
|
2747
|
+
${actualSnapshot}
|
|
2748
|
+
|
|
2749
|
+
Expected:
|
|
2750
|
+
${expectedSnapshot}`);
|
|
2418
2751
|
}
|
|
2419
|
-
return container.querySelector(`[aria-label="${labelText}"]`);
|
|
2420
|
-
}
|
|
2421
|
-
async function pollUntil(fn, timeout, interval, errorIfTimeout) {
|
|
2422
|
-
const start = Date.now();
|
|
2423
|
-
return new Promise((resolve, reject) => {
|
|
2424
|
-
const check = () => {
|
|
2425
|
-
const result = fn();
|
|
2426
|
-
if (result !== null) {
|
|
2427
|
-
resolve(result);
|
|
2428
|
-
return;
|
|
2429
|
-
}
|
|
2430
|
-
if (Date.now() - start >= timeout) {
|
|
2431
|
-
reject(new Error(errorIfTimeout));
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
setTimeout(check, interval);
|
|
2435
|
-
};
|
|
2436
|
-
check();
|
|
2437
|
-
});
|
|
2438
|
-
}
|
|
2439
|
-
function findByText(container, text, options = {}) {
|
|
2440
|
-
return pollUntil(
|
|
2441
|
-
() => queryByText(container, text),
|
|
2442
|
-
options.timeout ?? 1e3,
|
|
2443
|
-
options.interval ?? 50,
|
|
2444
|
-
`findByText: no element with text "${text}" after ${options.timeout ?? 1e3}ms`
|
|
2445
|
-
);
|
|
2446
|
-
}
|
|
2447
|
-
function findByTestId(container, testId, options = {}) {
|
|
2448
|
-
return pollUntil(
|
|
2449
|
-
() => queryByTestId(container, testId),
|
|
2450
|
-
options.timeout ?? 1e3,
|
|
2451
|
-
options.interval ?? 50,
|
|
2452
|
-
`findByTestId: no element with data-testid="${testId}" after ${options.timeout ?? 1e3}ms`
|
|
2453
|
-
);
|
|
2454
|
-
}
|
|
2455
|
-
function findByRole(container, role, options = {}) {
|
|
2456
|
-
return pollUntil(
|
|
2457
|
-
() => queryByRole(container, role),
|
|
2458
|
-
options.timeout ?? 1e3,
|
|
2459
|
-
options.interval ?? 50,
|
|
2460
|
-
`findByRole: no element with role="${role}" after ${options.timeout ?? 1e3}ms`
|
|
2461
|
-
);
|
|
2462
2752
|
}
|
|
2463
|
-
function
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
if (predicate(value)) {
|
|
2484
|
-
finish(() => {
|
|
2485
|
-
queueMicrotask(() => teardown());
|
|
2486
|
-
resolve(value);
|
|
2487
|
-
});
|
|
2753
|
+
function testComponent(component, options = {}) {
|
|
2754
|
+
const container = options.container || document.createElement("div");
|
|
2755
|
+
if (!options.container) document.body.appendChild(container);
|
|
2756
|
+
const element = typeof component === "function" ? component() : component;
|
|
2757
|
+
container.appendChild(element);
|
|
2758
|
+
return {
|
|
2759
|
+
element,
|
|
2760
|
+
container,
|
|
2761
|
+
getByTestId(id) {
|
|
2762
|
+
return queryByAttribute(container, "data-testid", id);
|
|
2763
|
+
},
|
|
2764
|
+
getAllByTestId(id) {
|
|
2765
|
+
return queryAllByAttribute(container, "data-testid", id);
|
|
2766
|
+
},
|
|
2767
|
+
getByText(text) {
|
|
2768
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
2769
|
+
while (walker.nextNode()) {
|
|
2770
|
+
if (walker.currentNode.textContent?.includes(text)) {
|
|
2771
|
+
return walker.currentNode.parentElement;
|
|
2772
|
+
}
|
|
2488
2773
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2774
|
+
return null;
|
|
2775
|
+
},
|
|
2776
|
+
click(el) {
|
|
2777
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
2778
|
+
},
|
|
2779
|
+
type(el, value) {
|
|
2780
|
+
el.value = value;
|
|
2781
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2782
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2783
|
+
},
|
|
2784
|
+
async waitForUpdate() {
|
|
2785
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
2786
|
+
},
|
|
2787
|
+
destroy() {
|
|
2788
|
+
replaceChildrenSafely(container);
|
|
2789
|
+
if (container.parentNode) container.parentNode.removeChild(container);
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2498
2792
|
}
|
|
2499
2793
|
|
|
2500
2794
|
// src/testing/snapshot.ts
|
|
2501
|
-
function
|
|
2502
|
-
|
|
2503
|
-
const tag = el.tagName.toLowerCase();
|
|
2504
|
-
const attrs = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}="${a.value}"`).join(" ");
|
|
2505
|
-
const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
|
|
2506
|
-
const children = Array.from(el.childNodes);
|
|
2507
|
-
if (children.length === 0) {
|
|
2508
|
-
return `${open}</${tag}>`;
|
|
2509
|
-
}
|
|
2510
|
-
if (children.length === 1 && children[0].nodeType === 3) {
|
|
2511
|
-
const text = children[0].textContent?.trim() || "";
|
|
2512
|
-
return `${open}${text}</${tag}>`;
|
|
2513
|
-
}
|
|
2514
|
-
const childStr = children.map((child) => {
|
|
2515
|
-
if (child.nodeType === 3) {
|
|
2516
|
-
const text = child.textContent?.trim();
|
|
2517
|
-
return text ? `${" ".repeat(indent + 1)}${text}` : "";
|
|
2518
|
-
}
|
|
2519
|
-
if (child.nodeType === 1) {
|
|
2520
|
-
return serializeElement2(child, indent + 1);
|
|
2521
|
-
}
|
|
2522
|
-
if (child.nodeType === 8) {
|
|
2523
|
-
return `${" ".repeat(indent + 1)}<!-- ${child.textContent?.trim() || ""} -->`;
|
|
2524
|
-
}
|
|
2525
|
-
return "";
|
|
2526
|
-
}).filter(Boolean).join("\n");
|
|
2527
|
-
return `${open}
|
|
2528
|
-
${childStr}
|
|
2529
|
-
${pad}</${tag}>`;
|
|
2795
|
+
function serializeElement(el, indent) {
|
|
2796
|
+
return serializeDom(el, indent, { comments: true });
|
|
2530
2797
|
}
|
|
2531
2798
|
function simpleDiff(a, b) {
|
|
2532
2799
|
const aLines = a.split("\n");
|
|
@@ -2588,7 +2855,11 @@ function snapshotComponent(component) {
|
|
|
2588
2855
|
const container = document.createElement("div");
|
|
2589
2856
|
const element = component();
|
|
2590
2857
|
container.appendChild(element);
|
|
2591
|
-
|
|
2858
|
+
try {
|
|
2859
|
+
return serializeElement(element, 0);
|
|
2860
|
+
} finally {
|
|
2861
|
+
replaceChildrenSafely(container);
|
|
2862
|
+
}
|
|
2592
2863
|
}
|
|
2593
2864
|
function matchSnapshot(store, name, component, options) {
|
|
2594
2865
|
const snapshot = snapshotComponent(component);
|
|
@@ -2644,31 +2915,57 @@ function walkElements(root) {
|
|
|
2644
2915
|
return elements;
|
|
2645
2916
|
}
|
|
2646
2917
|
function serializeStructure(el, indent) {
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2918
|
+
return serializeDom(el, indent);
|
|
2919
|
+
}
|
|
2920
|
+
var APPEARANCE_PROPERTIES = [
|
|
2921
|
+
"display",
|
|
2922
|
+
"visibility",
|
|
2923
|
+
"opacity",
|
|
2924
|
+
"position",
|
|
2925
|
+
"color",
|
|
2926
|
+
"background-color",
|
|
2927
|
+
"background-image",
|
|
2928
|
+
"font-family",
|
|
2929
|
+
"font-size",
|
|
2930
|
+
"font-weight",
|
|
2931
|
+
"font-style",
|
|
2932
|
+
"line-height",
|
|
2933
|
+
"text-align",
|
|
2934
|
+
"text-decoration",
|
|
2935
|
+
"text-transform",
|
|
2936
|
+
"width",
|
|
2937
|
+
"height",
|
|
2938
|
+
"margin",
|
|
2939
|
+
"padding",
|
|
2940
|
+
"border",
|
|
2941
|
+
"border-radius",
|
|
2942
|
+
"box-shadow",
|
|
2943
|
+
"transform",
|
|
2944
|
+
"z-index",
|
|
2945
|
+
"overflow",
|
|
2946
|
+
"flex-direction",
|
|
2947
|
+
"justify-content",
|
|
2948
|
+
"align-items",
|
|
2949
|
+
"gap"
|
|
2950
|
+
];
|
|
2951
|
+
function captureComputedStyles(elements) {
|
|
2952
|
+
const view = elements[0]?.ownerDocument?.defaultView;
|
|
2953
|
+
if (!view || typeof view.getComputedStyle !== "function") return [];
|
|
2954
|
+
const counters = {};
|
|
2955
|
+
return elements.map((el) => {
|
|
2956
|
+
const tag = el.tagName.toLowerCase();
|
|
2957
|
+
counters[tag] = (counters[tag] || 0) + 1;
|
|
2958
|
+
let declarations = "";
|
|
2959
|
+
try {
|
|
2960
|
+
const style = view.getComputedStyle(el);
|
|
2961
|
+
for (const property of APPEARANCE_PROPERTIES) {
|
|
2962
|
+
const value = style.getPropertyValue(property);
|
|
2963
|
+
if (value) declarations += `${property}:${value};`;
|
|
2964
|
+
}
|
|
2965
|
+
} catch {
|
|
2666
2966
|
}
|
|
2667
|
-
return
|
|
2668
|
-
})
|
|
2669
|
-
return `${open}
|
|
2670
|
-
${childStr}
|
|
2671
|
-
${pad}</${tag}>`;
|
|
2967
|
+
return `${tag}[${counters[tag]}]: ${declarations}`;
|
|
2968
|
+
});
|
|
2672
2969
|
}
|
|
2673
2970
|
function captureFingerprint(element) {
|
|
2674
2971
|
const allElements = walkElements(element);
|
|
@@ -2706,14 +3003,16 @@ function captureFingerprint(element) {
|
|
|
2706
3003
|
}
|
|
2707
3004
|
}
|
|
2708
3005
|
}
|
|
2709
|
-
const
|
|
3006
|
+
const computedStyles = captureComputedStyles(allElements);
|
|
3007
|
+
const composite = JSON.stringify([
|
|
2710
3008
|
structure,
|
|
2711
3009
|
textContent,
|
|
2712
|
-
|
|
2713
|
-
classNames
|
|
2714
|
-
inlineStyles
|
|
2715
|
-
|
|
2716
|
-
|
|
3010
|
+
elementCounts,
|
|
3011
|
+
classNames,
|
|
3012
|
+
inlineStyles,
|
|
3013
|
+
dataAttributes,
|
|
3014
|
+
computedStyles
|
|
3015
|
+
]);
|
|
2717
3016
|
const hash = djb2Hash(composite);
|
|
2718
3017
|
return {
|
|
2719
3018
|
structure,
|
|
@@ -2722,6 +3021,7 @@ function captureFingerprint(element) {
|
|
|
2722
3021
|
classNames,
|
|
2723
3022
|
inlineStyles,
|
|
2724
3023
|
dataAttributes,
|
|
3024
|
+
computedStyles,
|
|
2725
3025
|
hash
|
|
2726
3026
|
};
|
|
2727
3027
|
}
|
|
@@ -2796,6 +3096,20 @@ function compareFingerprints(baseline, current) {
|
|
|
2796
3096
|
description: `Data attributes changed: ${dataChanges.join("; ")}`
|
|
2797
3097
|
});
|
|
2798
3098
|
}
|
|
3099
|
+
const baselineComputed = baseline.computedStyles ?? [];
|
|
3100
|
+
const currentComputed = current.computedStyles ?? [];
|
|
3101
|
+
const computedChanges = [];
|
|
3102
|
+
for (let i = 0; i < Math.max(baselineComputed.length, currentComputed.length); i++) {
|
|
3103
|
+
if (baselineComputed[i] !== currentComputed[i]) {
|
|
3104
|
+
computedChanges.push(currentComputed[i] ?? baselineComputed[i]);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
if (computedChanges.length > 0) {
|
|
3108
|
+
changes.push({
|
|
3109
|
+
type: "computed",
|
|
3110
|
+
description: `Computed styles changed on ${computedChanges.length} element(s): ${truncate(computedChanges.join(" | "), 200)}`
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
2799
3113
|
return {
|
|
2800
3114
|
match: changes.length === 0,
|
|
2801
3115
|
changes
|
|
@@ -2835,11 +3149,6 @@ function truncate(str, maxLen) {
|
|
|
2835
3149
|
}
|
|
2836
3150
|
|
|
2837
3151
|
// src/testing/index.ts
|
|
2838
|
-
function escapeSelector(value) {
|
|
2839
|
-
const g = globalThis;
|
|
2840
|
-
if (g.CSS && typeof g.CSS.escape === "function") return g.CSS.escape(value);
|
|
2841
|
-
return value.replace(/[^\w-]/g, (m) => `\\${m.charCodeAt(0).toString(16)} `);
|
|
2842
|
-
}
|
|
2843
3152
|
var _renderedContainers = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.testing.containers.v1"), () => /* @__PURE__ */ new Set());
|
|
2844
3153
|
function unmountAll() {
|
|
2845
3154
|
for (const container of _renderedContainers) {
|
|
@@ -2868,10 +3177,10 @@ function render(component) {
|
|
|
2868
3177
|
return walk(container);
|
|
2869
3178
|
}
|
|
2870
3179
|
function getByTestId(testId) {
|
|
2871
|
-
return container
|
|
3180
|
+
return queryByAttribute(container, "data-testid", testId);
|
|
2872
3181
|
}
|
|
2873
3182
|
function getByRole(role) {
|
|
2874
|
-
return container
|
|
3183
|
+
return queryByAttribute(container, "role", role);
|
|
2875
3184
|
}
|
|
2876
3185
|
function queryAll(selector) {
|
|
2877
3186
|
return Array.from(container.querySelectorAll(selector));
|
|
@@ -2975,6 +3284,7 @@ function mockStore(initialState) {
|
|
|
2975
3284
|
createTimerMock,
|
|
2976
3285
|
createUniversalAdapter,
|
|
2977
3286
|
createVisualSuite,
|
|
3287
|
+
enableListenerTracking,
|
|
2978
3288
|
findByRole,
|
|
2979
3289
|
findByTestId,
|
|
2980
3290
|
findByText,
|
|
@@ -2982,6 +3292,8 @@ function mockStore(initialState) {
|
|
|
2982
3292
|
matchSnapshot,
|
|
2983
3293
|
mockRouter,
|
|
2984
3294
|
mockStore,
|
|
3295
|
+
queryAllByAttribute,
|
|
3296
|
+
queryByAttribute,
|
|
2985
3297
|
queryByLabel,
|
|
2986
3298
|
queryByRole,
|
|
2987
3299
|
queryByTestId,
|