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