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.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.cjs +326 -133
  3. package/dist/browser.d.cts +46 -14
  4. package/dist/browser.d.ts +46 -14
  5. package/dist/browser.js +8 -6
  6. package/dist/build.cjs +2094 -1033
  7. package/dist/build.d.cts +133 -44
  8. package/dist/build.d.ts +133 -44
  9. package/dist/build.js +1372 -574
  10. package/dist/cdn.dev.global.js +10 -10
  11. package/dist/cdn.full.dev.global.js +11 -11
  12. package/dist/cdn.full.global.js +10 -10
  13. package/dist/cdn.global.js +10 -10
  14. package/dist/{chunk-2DCGACUU.js → chunk-2BPG2XDA.js} +251 -66
  15. package/dist/{chunk-IXKSNWV5.js → chunk-2INLLLMZ.js} +1 -1
  16. package/dist/{chunk-4PMLNECI.js → chunk-3QWBSL5R.js} +98 -29
  17. package/dist/{chunk-KKLW7YWL.js → chunk-4AWA2PVD.js} +275 -144
  18. package/dist/chunk-5HZXGZ6T.js +24 -0
  19. package/dist/{chunk-PCT43HW3.js → chunk-7LUJQAOJ.js} +1 -1
  20. package/dist/{chunk-5DXA2J44.js → chunk-7XHATCIH.js} +5 -2
  21. package/dist/chunk-BC2SECJD.js +44 -0
  22. package/dist/{chunk-TBYTO6BS.js → chunk-BSY63EM6.js} +136 -80
  23. package/dist/{chunk-FTIR4QW2.js → chunk-DKXRACVN.js} +6 -8
  24. package/dist/{chunk-ONOHFDLG.js → chunk-FNJXNGYZ.js} +3 -3
  25. package/dist/{chunk-XZZOBQAY.js → chunk-FQRUXCEE.js} +6 -5
  26. package/dist/{chunk-NIOYEGBQ.js → chunk-HURREPU2.js} +27 -13
  27. package/dist/chunk-J6FW5TV6.js +233 -0
  28. package/dist/{chunk-UGRX3S57.js → chunk-JWKYU5GV.js} +22 -4
  29. package/dist/chunk-NYNYSPK7.js +318 -0
  30. package/dist/{chunk-RBTPLM32.js → chunk-RUSSKG6G.js} +13 -12
  31. package/dist/{chunk-7LN645I6.js → chunk-SLM3IA34.js} +3 -3
  32. package/dist/{chunk-R25EFXXC.js → chunk-TUCPL2HB.js} +3 -3
  33. package/dist/{chunk-B3WHI2QA.js → chunk-UNWRJRKC.js} +55 -31
  34. package/dist/{chunk-KEISJXBU.js → chunk-UOL2ECCS.js} +44 -17
  35. package/dist/{chunk-RJE2BNI4.js → chunk-VZSG24LS.js} +108 -45
  36. package/dist/{chunk-RIXRAYIU.js → chunk-WEQ3DMVL.js} +10 -4
  37. package/dist/{chunk-3JZ4L5TJ.js → chunk-WOLJZUFQ.js} +354 -90
  38. package/dist/{chunk-S373NSMK.js → chunk-XYV3EDB7.js} +283 -123
  39. package/dist/{chunk-VZKNK2V7.js → chunk-XZR4PXRE.js} +296 -38
  40. package/dist/{chunk-GW3SCCZG.js → chunk-Z3OHK6QT.js} +153 -110
  41. package/dist/{chunk-OMJJM3KM.js → chunk-ZVL7TY4K.js} +287 -130
  42. package/dist/{contracts-DBdg9J_a.d.ts → contracts-CLqzJnOV.d.ts} +36 -17
  43. package/dist/{contracts-DBdg9J_a.d.cts → contracts-CTOJXu-x.d.cts} +36 -17
  44. package/dist/{customElement-OB9CIsc5.d.cts → customElement-MmInOW1U.d.cts} +21 -0
  45. package/dist/{customElement-OB9CIsc5.d.ts → customElement-MmInOW1U.d.ts} +21 -0
  46. package/dist/data.cjs +410 -155
  47. package/dist/data.d.cts +166 -12
  48. package/dist/data.d.ts +166 -12
  49. package/dist/data.js +12 -9
  50. package/dist/devtools.cjs +98 -61
  51. package/dist/devtools.js +7 -8
  52. package/dist/dispose-GEIG2KOF.js +28 -0
  53. package/dist/ecosystem.cjs +372 -126
  54. package/dist/ecosystem.d.cts +20 -3
  55. package/dist/ecosystem.d.ts +20 -3
  56. package/dist/ecosystem.js +12 -12
  57. package/dist/extras.cjs +2366 -909
  58. package/dist/extras.d.cts +11 -9
  59. package/dist/extras.d.ts +11 -9
  60. package/dist/extras.js +39 -28
  61. package/dist/index.cjs +421 -158
  62. package/dist/index.d.cts +213 -171
  63. package/dist/index.d.ts +213 -171
  64. package/dist/index.js +24 -27
  65. package/dist/motion.cjs +118 -44
  66. package/dist/motion.js +5 -5
  67. package/dist/patterns.cjs +344 -53
  68. package/dist/patterns.d.cts +28 -9
  69. package/dist/patterns.d.ts +28 -9
  70. package/dist/patterns.js +8 -8
  71. package/dist/performance.cjs +325 -220
  72. package/dist/performance.d.cts +2 -2
  73. package/dist/performance.d.ts +2 -2
  74. package/dist/performance.js +8 -9
  75. package/dist/plugin-DVgSnTfK.d.cts +112 -0
  76. package/dist/plugin-DVgSnTfK.d.ts +112 -0
  77. package/dist/plugins.cjs +664 -233
  78. package/dist/plugins.d.cts +127 -14
  79. package/dist/plugins.d.ts +127 -14
  80. package/dist/plugins.js +96 -42
  81. package/dist/signal-EotCj4hS.d.cts +110 -0
  82. package/dist/signal-EotCj4hS.d.ts +110 -0
  83. package/dist/{ssr-BiPRdZ6n.d.cts → ssr-Bli9XRW5.d.cts} +5 -0
  84. package/dist/{ssr-BiPRdZ6n.d.ts → ssr-Bli9XRW5.d.ts} +5 -0
  85. package/dist/{ssr-Y7XOEPEN.js → ssr-XOTUASDO.js} +4 -5
  86. package/dist/ssr.cjs +229 -84
  87. package/dist/ssr.d.cts +9 -3
  88. package/dist/ssr.d.ts +9 -3
  89. package/dist/ssr.js +11 -12
  90. package/dist/{startup-BMpaiMhP.d.ts → startup-BLfSeL15.d.cts} +73 -22
  91. package/dist/{startup-BMpaiMhP.d.cts → startup-BLfSeL15.d.ts} +73 -22
  92. package/dist/tagFactory-8qL9LCIx.d.cts +156 -0
  93. package/dist/tagFactory-BL2fymez.d.ts +156 -0
  94. package/dist/testing.cjs +2503 -2191
  95. package/dist/testing.d.cts +56 -5
  96. package/dist/testing.d.ts +56 -5
  97. package/dist/testing.js +580 -307
  98. package/dist/types-CJFViL6Q.d.cts +26 -0
  99. package/dist/types-CJFViL6Q.d.ts +26 -0
  100. package/dist/ui.cjs +732 -329
  101. package/dist/ui.d.cts +41 -7
  102. package/dist/ui.d.ts +41 -7
  103. package/dist/ui.js +151 -56
  104. package/dist/widgets.cjs +267 -291
  105. package/dist/widgets.js +9 -10
  106. package/package.json +4 -2
  107. package/dist/chunk-2WLZ6757.js +0 -149
  108. package/dist/chunk-CCSJMTRN.js +0 -15
  109. package/dist/chunk-QKRPLZ2V.js +0 -108
  110. package/dist/chunk-VUF4ALSW.js +0 -60
  111. package/dist/chunk-WWV3SJ3L.js +0 -131
  112. package/dist/dispose-46BOMMQJ.js +0 -19
  113. package/dist/plugin-D30wlGW5.d.cts +0 -71
  114. package/dist/plugin-D30wlGW5.d.ts +0 -71
  115. package/dist/tagFactory-DVoDpHye.d.cts +0 -215
  116. 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
- const current = order[i];
197
- if (elementDisposers.has(current)) {
198
- let executed = 0;
199
- let runaway = false;
200
- while (!runaway) {
201
- const pending = elementDisposers.get(current);
202
- if (!pending || pending.length === 0) break;
203
- const snapshot = pending.slice();
204
- elementDisposers.delete(current);
205
- if (DEV) activeBindingCount -= snapshot.length;
206
- for (let i2 = 0; i2 < snapshot.length; i2++) {
207
- if (executed >= MAX_DRAIN_TEARDOWNS) {
208
- const rest = snapshot.slice(i2);
209
- const added = elementDisposers.get(current);
210
- elementDisposers.set(current, added ? rest.concat(added) : rest);
211
- if (DEV) activeBindingCount += rest.length;
212
- reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
213
- runaway = true;
214
- break;
215
- }
216
- executed++;
217
- try {
218
- snapshot[i2]();
219
- } catch (err) {
220
- reportError(err, { phase: "cleanup", name: "disposer" });
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/testing/a11y.ts
246
- function escSel(value) {
247
- return value.replace(/["\\]/g, "\\$&");
248
- }
249
- var VALID_ARIA_ROLES = /* @__PURE__ */ new Set([
250
- "alert",
251
- "alertdialog",
252
- "application",
253
- "article",
254
- "banner",
255
- "button",
256
- "cell",
257
- "checkbox",
258
- "columnheader",
259
- "combobox",
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 hasAccessibleName(el) {
413
- if (el.getAttribute("aria-label")?.trim()) return true;
414
- const labelledBy = el.getAttribute("aria-labelledby");
415
- if (labelledBy) {
416
- const doc = el.ownerDocument;
417
- const ids = labelledBy.split(/\s+/);
418
- for (const id of ids) {
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
- if (el.getAttribute("title")?.trim()) return true;
424
- if (el.textContent?.trim()) return true;
425
- return false;
290
+ const fresh = createNode();
291
+ fresh.sig = sig;
292
+ fresh.sub = sub;
293
+ fresh.epoch = epoch;
294
+ return fresh;
426
295
  }
427
- function isNativeInteractive(el) {
428
- const tag = el.tagName.toLowerCase();
429
- if (["button", "select", "textarea"].includes(tag)) return true;
430
- if (tag === "input" && el.getAttribute("type") !== "hidden") return true;
431
- if (tag === "a" && el.hasAttribute("href")) return true;
432
- if (tag === "area" && el.hasAttribute("href")) return true;
433
- return false;
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 checkImageAlt(root) {
436
- const violations = [];
437
- const images = root.querySelectorAll("img");
438
- for (const img of Array.from(images)) {
439
- if (!img.hasAttribute("alt")) {
440
- violations.push({
441
- rule: "image-alt",
442
- level: "error",
443
- message: 'Image element must have an alt attribute. Use alt="" for decorative images.',
444
- element: img,
445
- selector: getSelector(img)
446
- });
447
- } else if (img.getAttribute("alt") === "" && !img.getAttribute("role")) {
448
- violations.push({
449
- rule: "image-alt-empty",
450
- level: "info",
451
- message: "Image has empty alt text. Ensure this is a decorative image; otherwise provide meaningful alt text.",
452
- element: img,
453
- selector: getSelector(img)
454
- });
455
- }
456
- }
457
- const roleImgs = root.querySelectorAll('[role="img"]');
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 checkFormLabels(root) {
472
- const violations = [];
473
- const inputs = root.querySelectorAll("input, select, textarea");
474
- for (const input of Array.from(inputs)) {
475
- if (input.getAttribute("type") === "hidden") continue;
476
- const inputType = input.getAttribute("type");
477
- if (inputType === "submit" || inputType === "reset" || inputType === "button") continue;
478
- const hasLabel = checkInputHasLabel(input, root);
479
- if (!hasLabel) {
480
- violations.push({
481
- rule: "form-label",
482
- level: "error",
483
- message: `Form ${input.tagName.toLowerCase()} element must have an associated label. Use <label for="id">, aria-label, aria-labelledby, or wrap in a <label>.`,
484
- element: input,
485
- selector: getSelector(input)
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 checkInputHasLabel(input, root) {
492
- if (input.getAttribute("aria-label")?.trim()) return true;
493
- const labelledBy = input.getAttribute("aria-labelledby");
494
- if (labelledBy) {
495
- const doc = input.ownerDocument;
496
- const ids = labelledBy.split(/\s+/);
497
- for (const id2 of ids) {
498
- const ref = doc.getElementById(id2);
499
- if (ref?.textContent?.trim()) return true;
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
- if (input.getAttribute("title")?.trim()) return true;
503
- const id = input.getAttribute("id");
504
- if (id) {
505
- const label = root.querySelector(`label[for="${escSel(id)}"]`);
506
- if (label?.textContent?.trim()) return true;
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
- let parent = input.parentElement;
509
- while (parent && parent !== root) {
510
- if (parent.tagName.toLowerCase() === "label") return true;
511
- parent = parent.parentElement;
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 checkHeadingHierarchy(root) {
516
- const violations = [];
517
- const headings = root.querySelectorAll("h1, h2, h3, h4, h5, h6");
518
- if (headings.length === 0) return violations;
519
- let previousLevel = 0;
520
- for (const heading of Array.from(headings)) {
521
- const currentLevel = parseInt(heading.tagName[1], 10);
522
- if (previousLevel === 0 && currentLevel !== 1) {
523
- violations.push({
524
- rule: "heading-order",
525
- level: "warning",
526
- message: `Heading hierarchy should start with h1. Found ${heading.tagName.toLowerCase()} as the first heading.`,
527
- element: heading,
528
- selector: getSelector(heading)
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
- if (previousLevel > 0 && currentLevel > previousLevel + 1) {
532
- violations.push({
533
- rule: "heading-order",
534
- level: "error",
535
- message: `Heading level skipped: ${heading.tagName.toLowerCase()} follows h${previousLevel}. Headings should not skip levels.`,
536
- element: heading,
537
- selector: getSelector(heading)
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
- if (!heading.textContent?.trim()) {
541
- violations.push({
542
- rule: "heading-empty",
543
- level: "error",
544
- message: `Heading ${heading.tagName.toLowerCase()} is empty. Headings must have discernible text.`,
545
- element: heading,
546
- selector: getSelector(heading)
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
- return violations;
487
+ const sub = subscriber;
488
+ return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
552
489
  }
553
- function checkColorContrast(root) {
554
- const violations = [];
555
- const roledElements = root.querySelectorAll("[role]");
556
- for (const el of Array.from(roledElements)) {
557
- const role = el.getAttribute("role");
558
- if (!role) continue;
559
- if (role === "presentation" || role === "none") continue;
560
- const nameRequiredRoles = /* @__PURE__ */ new Set([
561
- "alert",
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
- const inlineStyled = root.querySelectorAll("[style]");
592
- for (const el of Array.from(inlineStyled)) {
593
- const style = el.getAttribute("style") || "";
594
- const hasColor = /(?:^|;)\s*color\s*:/i.test(style);
595
- const hasBg = /(?:^|;)\s*background(?:-color)?\s*:/i.test(style);
596
- if (hasColor && !hasBg && el.textContent?.trim()) {
597
- violations.push({
598
- rule: "color-contrast",
599
- level: "warning",
600
- message: "Element sets text color but not background color. Ensure sufficient contrast with the inherited background.",
601
- element: el,
602
- selector: getSelector(el)
603
- });
604
- }
605
- if (hasBg && !hasColor && el.textContent?.trim()) {
606
- violations.push({
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
- return violations;
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 checkKeyboardAccess(root) {
618
- const violations = [];
619
- const clickElements = root.querySelectorAll("[onclick]");
620
- for (const el of Array.from(clickElements)) {
621
- if (isNativeInteractive(el)) continue;
622
- const hasTabindex = el.hasAttribute("tabindex");
623
- const hasRole = el.hasAttribute("role");
624
- const hasKeyHandler = el.hasAttribute("onkeydown") || el.hasAttribute("onkeyup") || el.hasAttribute("onkeypress");
625
- if (!hasTabindex) {
626
- violations.push({
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
- const interactiveRoles = root.querySelectorAll(
654
- '[role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"]'
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
- for (const el of Array.from(interactiveRoles)) {
657
- if (isNativeInteractive(el)) continue;
658
- if (!el.hasAttribute("tabindex")) {
659
- violations.push({
660
- rule: "keyboard-access",
661
- level: "error",
662
- message: `Element with role="${el.getAttribute("role")}" must have tabindex to be keyboard accessible.`,
663
- element: el,
664
- selector: getSelector(el)
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 checkAriaAttributes(root) {
671
- const violations = [];
672
- const roledElements = root.querySelectorAll("[role]");
673
- for (const el of Array.from(roledElements)) {
674
- const role = el.getAttribute("role");
675
- if (!role) continue;
676
- if (!VALID_ARIA_ROLES.has(role)) {
677
- violations.push({
678
- rule: "aria-valid-role",
679
- level: "error",
680
- message: `Invalid ARIA role "${role}". Must be a valid WAI-ARIA role.`,
681
- element: el,
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
- const allElements = root.querySelectorAll("*");
700
- for (const el of Array.from(allElements)) {
701
- for (const attr of Array.from(el.attributes)) {
702
- if (!attr.name.startsWith("aria-")) continue;
703
- if (!VALID_ARIA_ATTRIBUTES.has(attr.name)) {
704
- violations.push({
705
- rule: "aria-valid-attr",
706
- level: "error",
707
- message: `Invalid ARIA attribute "${attr.name}". Must be a valid WAI-ARIA attribute.`,
708
- element: el,
709
- selector: getSelector(el)
710
- });
711
- }
712
- if (attr.name === "aria-labelledby" || attr.name === "aria-describedby" || attr.name === "aria-controls" || attr.name === "aria-owns") {
713
- const ids = attr.value.split(/\s+/).filter(Boolean);
714
- const doc = el.ownerDocument;
715
- for (const id of ids) {
716
- if (!doc.getElementById(id)) {
717
- violations.push({
718
- rule: "aria-valid-attr-value",
719
- level: "error",
720
- message: `${attr.name} references id "${id}" which does not exist in the document.`,
721
- element: el,
722
- selector: getSelector(el)
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
- const booleanAttrs = /* @__PURE__ */ new Set([
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 checkLandmarks(root) {
773
- const violations = [];
774
- const foundLandmarks = /* @__PURE__ */ new Set();
775
- for (const role of LANDMARK_ROLES) {
776
- const elements = root.querySelectorAll(`[role="${role}"]`);
777
- if (elements.length > 0) {
778
- foundLandmarks.add(role);
779
- }
780
- }
781
- for (const [tag, role] of Object.entries(LANDMARK_ELEMENTS)) {
782
- const elements = root.querySelectorAll(tag);
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 checkLinksAndButtons(root) {
839
- const violations = [];
840
- const links = root.querySelectorAll("a[href]");
841
- for (const link of Array.from(links)) {
842
- if (!hasAccessibleName(link)) {
843
- violations.push({
844
- rule: "link-name",
845
- level: "error",
846
- message: "Link must have discernible text. Add text content, aria-label, or aria-labelledby.",
847
- element: link,
848
- selector: getSelector(link)
849
- });
850
- }
851
- if (link.getAttribute("target") === "_blank") {
852
- violations.push({
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
- const linkText = link.textContent?.trim().toLowerCase() || "";
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
- const buttons = root.querySelectorAll('button, [role="button"]');
883
- for (const button of Array.from(buttons)) {
884
- if (!hasAccessibleName(button)) {
885
- violations.push({
886
- rule: "button-name",
887
- level: "error",
888
- message: "Button must have an accessible name. Add text content, aria-label, or aria-labelledby.",
889
- element: button,
890
- selector: getSelector(button)
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
- const inputButtons = root.querySelectorAll('input[type="submit"], input[type="reset"], input[type="button"]');
895
- for (const input of Array.from(inputButtons)) {
896
- const value = input.getAttribute("value");
897
- if (!value?.trim() && !input.getAttribute("aria-label") && !input.getAttribute("aria-labelledby")) {
898
- violations.push({
899
- rule: "button-name",
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 checkListSemantics(root) {
910
- const violations = [];
911
- const lists = root.querySelectorAll("ul, ol");
912
- for (const list of Array.from(lists)) {
913
- const children = Array.from(list.children);
914
- for (const child of children) {
915
- const tag = child.tagName.toLowerCase();
916
- if (tag !== "li" && tag !== "script" && tag !== "template") {
917
- violations.push({
918
- rule: "list-semantics",
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 violations;
745
+ return out;
969
746
  }
970
- function checkTabOrder(root) {
971
- const violations = [];
972
- const tabbable = root.querySelectorAll("[tabindex]");
973
- for (const el of Array.from(tabbable)) {
974
- const tabindex = parseInt(el.getAttribute("tabindex") ?? "", 10);
975
- if (!Number.isNaN(tabindex) && tabindex > 0) {
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
- const focusTraps = root.querySelectorAll('[data-sibu-focus-trap], [aria-modal="true"]');
986
- for (const trap of Array.from(focusTraps)) {
987
- const role = trap.getAttribute("role");
988
- if (role === "dialog" || role === "alertdialog" || trap.hasAttribute("aria-modal")) {
989
- const hasCloseButton = trap.querySelector(
990
- 'button[aria-label*="close" i], button[aria-label*="dismiss" i], [data-dismiss], [data-close]'
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
- return violations;
1014
- }
1015
- var CHECK_FUNCTIONS = {
1016
- images: checkImageAlt,
1017
- forms: checkFormLabels,
1018
- headings: checkHeadingHierarchy,
1019
- contrast: checkColorContrast,
1020
- keyboard: checkKeyboardAccess,
1021
- aria: checkAriaAttributes,
1022
- landmarks: checkLandmarks,
1023
- links: checkLinksAndButtons,
1024
- lists: checkListSemantics,
1025
- tabOrder: checkTabOrder
1026
- };
1027
- var ALL_CHECKS = [
1028
- "images",
1029
- "forms",
1030
- "headings",
1031
- "contrast",
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
- const minPriority = levelPriority[minLevel];
1048
- const allViolations = [];
1049
- for (const check of checksToRun) {
1050
- const fn = CHECK_FUNCTIONS[check];
1051
- if (fn) {
1052
- const results = fn(root);
1053
- allViolations.push(...results);
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
- function assertA11y(root, options) {
1069
- const result = checkA11y(root, options);
1070
- if (!result.passed) {
1071
- const lines = [`Accessibility check failed: ${result.summary}`, ""];
1072
- if (result.violations.length > 0) {
1073
- lines.push("ERRORS:");
1074
- for (const v of result.violations) {
1075
- lines.push(` [${v.rule}] ${v.message}${v.selector ? ` (${v.selector})` : ""}`);
1076
- }
1077
- lines.push("");
1078
- }
1079
- if (result.warnings.length > 0) {
1080
- lines.push("WARNINGS:");
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
- lines.push("");
834
+ if (mod) detected = new mod.AsyncLocalStorage();
1085
835
  }
1086
- throw new Error(lines.join("\n"));
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
- // src/testing/adapters.ts
1091
- function escSel2(value) {
1092
- return value.replace(/["\\]/g, "\\$&");
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 createJestAdapter() {
1095
- let container = null;
1096
- return {
1097
- /** Call in beforeEach to set up DOM container */
1098
- setup() {
1099
- container = document.createElement("div");
1100
- container.setAttribute("data-testenv", "jest");
1101
- document.body.appendChild(container);
1102
- return container;
1103
- },
1104
- /** Call in afterEach to clean up */
1105
- teardown() {
1106
- if (container) {
1107
- container.innerHTML = "";
1108
- if (container.parentNode) {
1109
- container.parentNode.removeChild(container);
1110
- }
1111
- container = null;
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
- /** Render a component into the test container */
1115
- render(component) {
1116
- if (!container) {
1117
- container = document.createElement("div");
1118
- container.setAttribute("data-testenv", "jest");
1119
- document.body.appendChild(container);
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
- const element = typeof component === "function" ? component() : component;
1122
- container.appendChild(element);
1123
- return { element, container };
1124
- },
1125
- /** Custom Jest matchers */
1126
- matchers: {
1127
- /** Check if element has specific text content */
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 createCypressAdapter() {
1222
- return {
1223
- /** Mount a component for Cypress component testing */
1224
- mount(component, options) {
1225
- const container = options?.container || document.createElement("div");
1226
- if (!options?.container) {
1227
- container.setAttribute("data-testenv", "cypress");
1228
- document.body.appendChild(container);
1229
- }
1230
- const element = typeof component === "function" ? component() : component;
1231
- container.appendChild(element);
1232
- return { element, container };
1233
- },
1234
- /** Generate Cypress custom commands for SibuJS */
1235
- commands: {
1236
- /** Find by data-testid */
1237
- getByTestId: (id) => `[data-testid="${id}"]`,
1238
- /** Find by text */
1239
- getByText: (text) => `:contains("${text}")`,
1240
- /** Find by role */
1241
- getByRole: (role) => `[role="${role}"]`
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 createPlaywrightAdapter() {
1246
- return {
1247
- /** Selectors for common SibuJS patterns */
1248
- selectors: {
1249
- byTestId: (id) => `[data-testid="${id}"]`,
1250
- byRole: (role) => `[role="${role}"]`,
1251
- byAriaLabel: (label) => `[aria-label="${label}"]`,
1252
- byDataAttr: (attr, value) => value ? `[data-${attr}="${value}"]` : `[data-${attr}]`
1253
- },
1254
- /** Generate a page object for a SibuJS component */
1255
- createPageObject(name, selectors) {
1256
- const selectorMap = { ...selectors };
1257
- return {
1258
- name,
1259
- getSelector(key) {
1260
- const selector = selectorMap[key];
1261
- if (!selector) {
1262
- throw new Error(
1263
- `Page object "${name}" has no selector for key "${key}". Available keys: ${Object.keys(selectorMap).join(", ")}`
1264
- );
1265
- }
1266
- return selector;
1267
- },
1268
- allSelectors() {
1269
- return { ...selectorMap };
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 createUniversalAdapter() {
1276
- let container = null;
1277
- return {
1278
- /** Setup test environment */
1279
- setup() {
1280
- container = document.createElement("div");
1281
- container.setAttribute("data-testenv", "universal");
1282
- document.body.appendChild(container);
1283
- return container;
1284
- },
1285
- /** Teardown test environment */
1286
- teardown() {
1287
- if (container) {
1288
- container.innerHTML = "";
1289
- if (container.parentNode) {
1290
- container.parentNode.removeChild(container);
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
- // src/testing/e2e.ts
1386
- function createHttpMock(routes = [], options = {}) {
1387
- const originalFetch = globalThis.fetch;
1388
- const hadOriginalFetch = Object.hasOwn(globalThis, "fetch");
1389
- const requestLog = [];
1390
- const mockRoutes = [...routes];
1391
- function matchRoute(url, method) {
1392
- return mockRoutes.find((route) => {
1393
- const methodMatch = !route.method || route.method.toUpperCase() === method.toUpperCase();
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
- const mockFetch = async (input, init) => {
1400
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1401
- const method = init?.method || "GET";
1402
- let body;
1403
- if (init?.body != null) {
1404
- const raw = String(init.body);
1405
- try {
1406
- body = JSON.parse(raw);
1407
- } catch {
1408
- body = raw;
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
- requestLog.push({ url, method, body, timestamp: Date.now() });
1412
- const route = matchRoute(url, method);
1413
- if (!route) {
1414
- return new Response(JSON.stringify({ error: "Not mocked" }), { status: 404 });
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
- let mockResponse;
1417
- try {
1418
- if (typeof route.response === "function") {
1419
- mockResponse = await route.response({ url, method, body, headers: new Headers(init?.headers) });
1420
- } else {
1421
- mockResponse = route.response;
1422
- }
1423
- if (mockResponse.delay) {
1424
- await new Promise((r) => setTimeout(r, mockResponse.delay));
1425
- }
1426
- return new Response(
1427
- typeof mockResponse.body === "string" ? mockResponse.body : JSON.stringify(mockResponse.body),
1428
- {
1429
- status: mockResponse.status || 200,
1430
- statusText: mockResponse.statusText || "OK",
1431
- headers: mockResponse.headers
1432
- }
1433
- );
1434
- } catch (err) {
1435
- return new Response(JSON.stringify({ error: String(err) }), { status: 500 });
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 restore = () => {
1439
- if (hadOriginalFetch) {
1440
- globalThis.fetch = originalFetch;
1441
- } else {
1442
- delete globalThis.fetch;
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
- const api = {
1446
- /** Install the mock (replace global fetch) */
1447
- install() {
1448
- globalThis.fetch = mockFetch;
1449
- },
1450
- /** Restore original fetch */
1451
- restore,
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 api;
1578
+ return violations;
1487
1579
  }
1488
- function createTimerMock(options = {}) {
1489
- const g = globalThis;
1490
- const snapshot = (key) => ({
1491
- had: Object.hasOwn(globalThis, key),
1492
- value: g[key]
1493
- });
1494
- const saved = {
1495
- setTimeout: snapshot("setTimeout"),
1496
- setInterval: snapshot("setInterval"),
1497
- clearTimeout: snapshot("clearTimeout"),
1498
- clearInterval: snapshot("clearInterval"),
1499
- requestAnimationFrame: snapshot("requestAnimationFrame"),
1500
- cancelAnimationFrame: snapshot("cancelAnimationFrame")
1501
- };
1502
- let currentTime = 0;
1503
- let nextId = 1;
1504
- const timers = [];
1505
- const api = {
1506
- install() {
1507
- currentTime = 0;
1508
- globalThis.setTimeout = (cb, delay = 0) => {
1509
- const id = nextId++;
1510
- timers.push({ id, callback: cb, time: currentTime + delay });
1511
- return id;
1512
- };
1513
- globalThis.setInterval = (cb, interval) => {
1514
- const id = nextId++;
1515
- timers.push({ id, callback: cb, time: currentTime + interval, interval });
1516
- return id;
1517
- };
1518
- globalThis.clearTimeout = (id) => {
1519
- const idx = timers.findIndex((t) => t.id === id);
1520
- if (idx !== -1) timers.splice(idx, 1);
1521
- };
1522
- globalThis.clearInterval = (id) => {
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 childStr = children.map((child) => {
1611
- if (child.nodeType === 3) {
1612
- const text = child.textContent?.trim();
1613
- return text ? `${" ".repeat(indent + 1)}${text}` : "";
1614
- }
1615
- if (child.nodeType === 1) {
1616
- return serializeElement(child, indent + 1);
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 testComponent(component, options = {}) {
1638
- const container = options.container || document.createElement("div");
1639
- if (!options.container) document.body.appendChild(container);
1640
- const element = typeof component === "function" ? component() : component;
1641
- container.appendChild(element);
1642
- return {
1643
- element,
1644
- container,
1645
- getByTestId(id) {
1646
- return container.querySelector(`[data-testid="${id}"]`);
1647
- },
1648
- getAllByTestId(id) {
1649
- return Array.from(container.querySelectorAll(`[data-testid="${id}"]`));
1650
- },
1651
- getByText(text) {
1652
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
1653
- while (walker.nextNode()) {
1654
- if (walker.currentNode.textContent?.includes(text)) {
1655
- return walker.currentNode.parentElement;
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
- function linkSub(sub, node) {
1743
- const oldTail = sub.depsTail ?? null;
1744
- node.subPrev = oldTail;
1745
- node.subNext = null;
1746
- if (oldTail) oldTail.subNext = node;
1747
- else sub.depsHead = node;
1748
- sub.depsTail = node;
1749
- }
1750
- function unlinkSub(node) {
1751
- const sub = node.sub;
1752
- if (!sub) return;
1753
- const prev = node.subPrev;
1754
- const next = node.subNext;
1755
- if (prev) prev.subNext = next;
1756
- else sub.depsHead = next;
1757
- if (next) next.subPrev = prev;
1758
- else sub.depsTail = prev;
1759
- }
1760
- var currentSubscriber = null;
1761
- var suspendSavedSub = null;
1762
- var notifyDepth = 0;
1763
- var pendingQueue = [];
1764
- var pendingSet = /* @__PURE__ */ new Set();
1765
- var propagateStack = [];
1766
- function safeInvoke(sub) {
1767
- try {
1768
- sub();
1769
- } catch (err) {
1770
- const s = sub;
1771
- reportError(err, { phase: s._errorPhase ?? "effect", name: s.__name, node: s._errorNode });
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 depsChanged(sub) {
1775
- let node = sub.depsHead ?? null;
1776
- if (node === null) return true;
1777
- while (node !== null) {
1778
- const sig = node.sig;
1779
- if (sig === null) return true;
1780
- if (sig._d === true && sig._validate !== void 0) {
1781
- try {
1782
- sig._validate();
1783
- } catch {
1784
- return true;
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 false;
1789
+ return violations;
1791
1790
  }
1792
- var suspendDepth = 0;
1793
- var trackingSuspended = false;
1794
- function suspendTracking() {
1795
- if (suspendDepth === 0) {
1796
- suspendSavedSub = currentSubscriber;
1797
- currentSubscriber = null;
1798
- trackingSuspended = true;
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
- suspendDepth++;
1801
- }
1802
- function resumeTracking() {
1803
- suspendDepth--;
1804
- if (suspendDepth === 0) {
1805
- currentSubscriber = suspendSavedSub;
1806
- suspendSavedSub = null;
1807
- trackingSuspended = false;
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
- function isTrackingSuspended() {
1811
- return trackingSuspended;
1812
- }
1813
- function untracked(fn) {
1814
- suspendTracking();
1815
- try {
1816
- return fn();
1817
- } finally {
1818
- resumeTracking();
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
- var subscriberEpochCounter = 0;
1822
- function retrack(effectFn, subscriber) {
1823
- const prev = currentSubscriber;
1824
- let savedDepth = 0;
1825
- let savedSuspendSub = null;
1826
- if (trackingSuspended) {
1827
- savedDepth = suspendDepth;
1828
- savedSuspendSub = suspendSavedSub;
1829
- suspendDepth = 0;
1830
- suspendSavedSub = null;
1831
- trackingSuspended = false;
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
- currentSubscriber = subscriber;
1834
- const sub = subscriber;
1835
- const epoch = ++subscriberEpochCounter;
1836
- sub._epoch = epoch;
1837
- sub._structDirty = false;
1838
- for (let n = sub.depsHead ?? null; n !== null; n = n.subNext) {
1839
- const sig = n.sig;
1840
- n.prevActive = sig.__activeNode ?? null;
1841
- sig.__activeNode = n;
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
- try {
1844
- effectFn();
1845
- } finally {
1846
- currentSubscriber = prev;
1847
- if (savedDepth !== 0) {
1848
- suspendDepth = savedDepth;
1849
- suspendSavedSub = savedSuspendSub;
1850
- trackingSuspended = true;
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
- let node = sub.depsHead ?? null;
1853
- while (node !== null) {
1854
- const next = node.subNext;
1855
- const sig = node.sig;
1856
- sig.__activeNode = node.prevActive;
1857
- node.prevActive = null;
1858
- if (node.epoch !== epoch) {
1859
- unlinkSub(node);
1860
- unlinkSignal(node);
1861
- freeNode(node);
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
- node = next;
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
- function track(effectFn, subscriber) {
1868
- if (!subscriber) return reactiveBinding(effectFn);
1869
- cleanup(subscriber);
1870
- const prev = currentSubscriber;
1871
- let savedDepth = 0;
1872
- let savedSuspendSub = null;
1873
- if (trackingSuspended) {
1874
- savedDepth = suspendDepth;
1875
- savedSuspendSub = suspendSavedSub;
1876
- suspendDepth = 0;
1877
- suspendSavedSub = null;
1878
- trackingSuspended = false;
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
- currentSubscriber = subscriber;
1881
- try {
1882
- effectFn();
1883
- } finally {
1884
- currentSubscriber = prev;
1885
- if (savedDepth !== 0) {
1886
- suspendDepth = savedDepth;
1887
- suspendSavedSub = savedSuspendSub;
1888
- trackingSuspended = true;
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
- const sub2 = subscriber;
1891
- for (let n = sub2.depsHead ?? null; n !== null; n = n.subNext) {
1892
- const sig = n.sig;
1893
- sig.__activeNode = n.prevActive;
1894
- n.prevActive = null;
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
- function reactiveBinding(commit, ownerNode) {
1901
- const run = () => {
1902
- const s = subscriber;
1903
- if (s._disposed || s._reentrant) return;
1904
- s._reentrant = true;
1905
- try {
1906
- retrack(commit, subscriber);
1907
- } finally {
1908
- s._reentrant = false;
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 drainQueue() {
2011
- let i = 0;
2012
- while (i < pendingQueue.length) {
2013
- if (i >= maxDrainIterations) {
2014
- absoluteDrainError();
2015
- break;
2016
- }
2017
- const sub = pendingQueue[i++];
2018
- pendingSet.delete(sub);
2019
- if (quarantined.size > 0 && quarantined.has(sub)) continue;
2020
- if (sub._hasComputedDep === true && !depsChanged(sub)) continue;
2021
- if (tickRepeat(sub)) {
2022
- cycleError(sub);
2023
- quarantined.add(sub);
2024
- continue;
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
- safeInvoke(sub);
2027
- }
2205
+ };
2028
2206
  }
2029
- function drainNotificationQueue() {
2030
- if (notifyDepth > 0) return;
2031
- notifyDepth++;
2032
- drainEpoch++;
2033
- try {
2034
- drainQueue();
2035
- } finally {
2036
- notifyDepth--;
2037
- if (notifyDepth === 0) {
2038
- pendingQueue.length = 0;
2039
- pendingSet.clear();
2040
- if (quarantined.size > 0) quarantined.clear();
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 propagateDirty(sub) {
2045
- sub();
2046
- const rootSig = sub._sig;
2047
- if (!rootSig) return;
2048
- const stack = propagateStack;
2049
- const baseLen = stack.length;
2050
- stack.push(rootSig);
2051
- while (stack.length > baseLen) {
2052
- const sig = stack.pop();
2053
- let node = sig.subsHead ?? null;
2054
- while (node) {
2055
- const s = node.sub;
2056
- if (s) {
2057
- if (s._c) {
2058
- const nSig = s._sig;
2059
- if (nSig) {
2060
- if (!nSig._d || nSig._f === true) {
2061
- nSig._d = true;
2062
- nSig._f = false;
2063
- stack.push(nSig);
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
- } else {
2066
- s();
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
- node = node.sigNext;
2074
- }
2075
- }
2076
- }
2077
- function queueSignalNotification(signal) {
2078
- const sig = signal;
2079
- let node = sig.subsHead ?? null;
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
- node = node.sigNext;
2110
- }
2111
- return;
2112
- }
2113
- notifyDepth++;
2114
- drainEpoch++;
2115
- try {
2116
- let node = head;
2117
- while (node) {
2118
- const s = node.sub;
2119
- if (s) {
2120
- if (s._c) {
2121
- propagateDirty(s);
2122
- } else if (!pendingSet.has(s)) {
2123
- pendingSet.add(s);
2124
- pendingQueue.push(s);
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
- function getSubscriberDeps(subscriber) {
2143
- const sub = subscriber;
2144
- const out = [];
2145
- let node = sub.depsHead ?? null;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2151
2350
  }
2152
- function forEachSubscriber(signal, visit) {
2153
- let node = signal.subsHead ?? null;
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, "&quot;");
2159
2353
  }
2160
-
2161
- // src/reactivity/track.ts
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, "&#45;");
2198
2356
  }
2199
- var API = resolveReactiveApi();
2200
- var suspendTracking2 = API.suspendTracking;
2201
- var resumeTracking2 = API.resumeTracking;
2202
- var isTrackingSuspended2 = API.isTrackingSuspended;
2203
- var untracked2 = API.untracked;
2204
- var retrack2 = API.retrack;
2205
- var track2 = API.track;
2206
- var reactiveBinding2 = API.reactiveBinding;
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
- return {
2237
- als: detected,
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
- return fallbackStore;
2252
- }
2253
- function isSSR() {
2254
- return getSSRStore().ssr;
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/core/signals/effect.ts
2258
- var _g = globalThis;
2259
- var MAX_RERUNS = 100;
2260
- function flushUserCleanups(ctx) {
2261
- const list = ctx.userCleanups;
2262
- if (list.length === 0) return;
2263
- ctx.userCleanups = [];
2264
- for (let i = list.length - 1; i >= 0; i--) {
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
- list[i]();
2267
- } catch (err) {
2268
- reportError(err, { phase: "cleanup" });
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
- function drainReruns(ctx) {
2273
- let reruns = 1;
2274
- do {
2275
- ctx.rerunPending = false;
2276
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
2277
- retrack2(ctx.bodyFn, ctx.subscriber);
2278
- } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
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
- function disposeEffect(ctx) {
2290
- if (ctx.disposed) return;
2291
- ctx.disposed = true;
2292
- const h = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
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
- h.emit("effect:destroy", { effectFn: ctx.fn });
2424
+ return JSON.parse(text);
2296
2425
  } catch {
2426
+ return text;
2297
2427
  }
2298
- }
2299
- try {
2300
- if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
2301
- } catch (err) {
2302
- if (typeof console !== "undefined") {
2303
- console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
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
- try {
2307
- cleanup2(ctx.subscriber);
2308
- } catch (err) {
2309
- if (typeof console !== "undefined") {
2310
- console.warn("[SibuJS effect] dispose threw:", err);
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 ctx = {
2319
- fn: effectFn,
2320
- onError: options?.onError,
2321
- userCleanups: [],
2322
- running: false,
2323
- rerunPending: false,
2324
- disposed: false,
2325
- onCleanup: null,
2326
- subscriber: null,
2327
- bodyFn: null
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
- ctx.onCleanup = (fn) => {
2330
- ctx.userCleanups.push(fn);
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 onErrorCaptured = ctx.onError;
2333
- ctx.bodyFn = onErrorCaptured ? () => {
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
- ctx.fn(ctx.onCleanup);
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
- onErrorCaptured(err);
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
- const sub = (() => {
2343
- if (ctx.disposed) return;
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
- const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
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 cssEscape(value) {
2402
- const g = globalThis;
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 queryByLabel(container, labelText) {
2407
- const labels = Array.from(container.querySelectorAll("label"));
2408
- for (const label of labels) {
2409
- if (label.textContent?.trim() === labelText) {
2410
- const forId = label.getAttribute("for");
2411
- if (forId) {
2412
- const target = container.querySelector(`#${cssEscape(forId)}`);
2413
- if (target) return target;
2414
- }
2415
- const child = label.querySelector("input, select, textarea, button");
2416
- if (child) return child;
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 waitForSignal(getter, predicate, options = {}) {
2464
- const timeoutMs = options.timeout ?? 1e3;
2465
- return new Promise((resolve, reject) => {
2466
- let resolved = false;
2467
- let timer;
2468
- const finish = (fn) => {
2469
- if (resolved) return;
2470
- resolved = true;
2471
- if (timer !== void 0) clearTimeout(timer);
2472
- fn();
2473
- };
2474
- timer = setTimeout(() => {
2475
- finish(() => {
2476
- teardown();
2477
- reject(new Error(`waitForSignal: predicate did not match within ${timeoutMs}ms`));
2478
- });
2479
- }, timeoutMs);
2480
- const teardown = effect(() => {
2481
- if (resolved) return;
2482
- const value = getter();
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
- function type(element, text) {
2493
- for (const char of text) {
2494
- element.value += char;
2495
- element.dispatchEvent(new InputEvent("input", { bubbles: true, data: char }));
2496
- }
2497
- element.dispatchEvent(new Event("change", { bubbles: true }));
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 serializeElement2(el, indent) {
2502
- const pad = " ".repeat(indent);
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
- return serializeElement2(element, 0);
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
- const pad = " ".repeat(indent);
2648
- const tag = el.tagName.toLowerCase();
2649
- const attrs = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}="${a.value}"`).join(" ");
2650
- const open = attrs ? `${pad}<${tag} ${attrs}>` : `${pad}<${tag}>`;
2651
- const children = Array.from(el.childNodes);
2652
- if (children.length === 0) {
2653
- return `${open}</${tag}>`;
2654
- }
2655
- if (children.length === 1 && children[0].nodeType === 3) {
2656
- const text = children[0].textContent?.trim() || "";
2657
- return `${open}${text}</${tag}>`;
2658
- }
2659
- const childStr = children.map((child) => {
2660
- if (child.nodeType === 3) {
2661
- const text = child.textContent?.trim();
2662
- return text ? `${" ".repeat(indent + 1)}${text}` : "";
2663
- }
2664
- if (child.nodeType === 1) {
2665
- return serializeStructure(child, indent + 1);
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
- }).filter(Boolean).join("\n");
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 composite = [
3006
+ const computedStyles = captureComputedStyles(allElements);
3007
+ const composite = JSON.stringify([
2710
3008
  structure,
2711
3009
  textContent,
2712
- JSON.stringify(elementCounts),
2713
- classNames.join(","),
2714
- inlineStyles.join(";"),
2715
- JSON.stringify(dataAttributes)
2716
- ].join("|");
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.querySelector(`[data-testid="${escapeSelector(testId)}"]`);
3180
+ return queryByAttribute(container, "data-testid", testId);
2872
3181
  }
2873
3182
  function getByRole(role) {
2874
- return container.querySelector(`[role="${role}"]`);
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,