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