@molecule/app-e2e-fixtures-default 1.0.1 → 1.0.2

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.
@@ -0,0 +1,1501 @@
1
+ /**
2
+ * The in-page half of the Playwright-shaped page.
3
+ *
4
+ * `String(installE2ERuntime)` is sent through a bond's `evaluate` and run once
5
+ * per document; it installs `globalThis.__molE2E`, a dispatcher the page
6
+ * driver calls with `{ fn, args }`. Everything Playwright-shaped — locator
7
+ * chains, `getByRole` semantics, actionability (visible, enabled, not covered
8
+ * by another element), auto-waiting, the expect matchers' probes, `page.request`,
9
+ * mouse/keyboard synthesis — lives here so the wire carries one call per action.
10
+ *
11
+ * Self-contained by construction: no imports, no closures over module scope,
12
+ * plain ES2022 (the COMPILED text is what runs in the page). Keep it that way.
13
+ *
14
+ * @module
15
+ */
16
+ /** Bumped when the in-page runtime's protocol changes; a page holding an older one is re-installed. */
17
+ export const E2E_RUNTIME_VERSION = 1;
18
+ /**
19
+ * Install the runtime. Runs INSIDE the page — see the module doc; the body
20
+ * must stay self-contained.
21
+ */
22
+ export function installE2ERuntime() {
23
+ const VERSION = 1;
24
+ const g = globalThis;
25
+ if (g.__molE2E && g.__molE2E.version === VERSION)
26
+ return { version: VERSION };
27
+ const norm = (s) => (s ?? '').replace(/\s+/g, ' ').trim();
28
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
29
+ const matchText = (text, m) => {
30
+ if (!m)
31
+ return true;
32
+ if (m.re)
33
+ return new RegExp(m.re.source, m.re.flags).test(text);
34
+ const a = norm(text);
35
+ const b = norm(m.s ?? '');
36
+ if (m.exact)
37
+ return m.ignoreCase ? a.toLowerCase() === b.toLowerCase() : a === b;
38
+ return m.ignoreCase === false ? a.includes(b) : a.toLowerCase().includes(b.toLowerCase());
39
+ };
40
+ const matchFull = (text, m) => {
41
+ if (m.re)
42
+ return new RegExp(m.re.source, m.re.flags).test(text);
43
+ const a = norm(text);
44
+ const b = norm(m.s ?? '');
45
+ return m.ignoreCase ? a.toLowerCase() === b.toLowerCase() : a === b;
46
+ };
47
+ const matchSub = (text, m) => {
48
+ if (m.re)
49
+ return new RegExp(m.re.source, m.re.flags).test(text);
50
+ const a = norm(text);
51
+ const b = norm(m.s ?? '');
52
+ return m.ignoreCase ? a.toLowerCase().includes(b.toLowerCase()) : a.includes(b);
53
+ };
54
+ const isVisible = (el) => {
55
+ if (el.tagName === 'OPTION') {
56
+ const sel = el.closest('select, datalist');
57
+ return sel ? isVisible(sel) : false;
58
+ }
59
+ const r = el.getBoundingClientRect();
60
+ if (!(r.width || r.height))
61
+ return false;
62
+ if (getComputedStyle(el).visibility === 'hidden')
63
+ return false;
64
+ return true;
65
+ };
66
+ const isDisabled = (el) => {
67
+ if (el.matches(':disabled'))
68
+ return true;
69
+ if (el.getAttribute('aria-disabled') === 'true')
70
+ return true;
71
+ const fs = el.closest('fieldset:disabled');
72
+ if (fs && !el.closest('legend'))
73
+ return true;
74
+ return false;
75
+ };
76
+ const isEditable = (el) => {
77
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
78
+ return !el.readOnly && !isDisabled(el);
79
+ if (el instanceof HTMLSelectElement)
80
+ return !isDisabled(el);
81
+ return el.isContentEditable === true;
82
+ };
83
+ const isCheckable = (el) => (el instanceof HTMLInputElement && (el.type === 'checkbox' || el.type === 'radio')) ||
84
+ ['checkbox', 'radio', 'switch', 'menuitemcheckbox', 'menuitemradio'].includes(el.getAttribute('role') ?? '');
85
+ const isChecked = (el) => {
86
+ if (el instanceof HTMLInputElement)
87
+ return el.checked;
88
+ return el.getAttribute('aria-checked') === 'true';
89
+ };
90
+ const isFocusable = (el) => {
91
+ if (isDisabled(el))
92
+ return false;
93
+ if (el instanceof HTMLElement && el.tabIndex >= 0)
94
+ return true;
95
+ return el.matches('a[href], button, input, select, textarea, summary, [contenteditable="true"]');
96
+ };
97
+ // ---- accessibility: role + name (the subset getByRole needs) ----
98
+ const roleOf = (el) => {
99
+ const explicit = (el.getAttribute('role') ?? '').trim().split(/\s+/)[0];
100
+ if (explicit)
101
+ return explicit;
102
+ const tag = el.tagName.toLowerCase();
103
+ switch (tag) {
104
+ case 'a':
105
+ case 'area':
106
+ return el.hasAttribute('href') ? 'link' : null;
107
+ case 'article':
108
+ return 'article';
109
+ case 'aside':
110
+ return 'complementary';
111
+ case 'blockquote':
112
+ return 'blockquote';
113
+ case 'button':
114
+ case 'summary':
115
+ return 'button';
116
+ case 'caption':
117
+ return 'caption';
118
+ case 'code':
119
+ return 'code';
120
+ case 'datalist':
121
+ return 'listbox';
122
+ case 'dd':
123
+ return 'definition';
124
+ case 'del':
125
+ return 'deletion';
126
+ case 'details':
127
+ case 'fieldset':
128
+ case 'optgroup':
129
+ case 'hgroup':
130
+ return 'group';
131
+ case 'dialog':
132
+ return 'dialog';
133
+ case 'dt':
134
+ return 'term';
135
+ case 'em':
136
+ return 'emphasis';
137
+ case 'figure':
138
+ return 'figure';
139
+ case 'footer':
140
+ return el.closest('article, aside, main, nav, section') ? 'generic' : 'contentinfo';
141
+ case 'form':
142
+ return 'form';
143
+ case 'h1':
144
+ case 'h2':
145
+ case 'h3':
146
+ case 'h4':
147
+ case 'h5':
148
+ case 'h6':
149
+ return 'heading';
150
+ case 'header':
151
+ return el.closest('article, aside, main, nav, section') ? 'generic' : 'banner';
152
+ case 'hr':
153
+ return 'separator';
154
+ case 'html':
155
+ return 'document';
156
+ case 'img':
157
+ return el.getAttribute('alt') === '' ? 'presentation' : 'img';
158
+ case 'input': {
159
+ const type = el.type;
160
+ const list = el.hasAttribute('list');
161
+ if (['button', 'submit', 'reset', 'image'].includes(type))
162
+ return 'button';
163
+ if (type === 'checkbox')
164
+ return 'checkbox';
165
+ if (type === 'radio')
166
+ return 'radio';
167
+ if (type === 'range')
168
+ return 'slider';
169
+ if (type === 'number')
170
+ return 'spinbutton';
171
+ if (type === 'search')
172
+ return list ? 'combobox' : 'searchbox';
173
+ if (['email', 'tel', 'text', 'url'].includes(type))
174
+ return list ? 'combobox' : 'textbox';
175
+ return null;
176
+ }
177
+ case 'ins':
178
+ return 'insertion';
179
+ case 'li':
180
+ return 'listitem';
181
+ case 'main':
182
+ return 'main';
183
+ case 'math':
184
+ return 'math';
185
+ case 'menu':
186
+ case 'ol':
187
+ case 'ul':
188
+ return 'list';
189
+ case 'meter':
190
+ return 'meter';
191
+ case 'nav':
192
+ return 'navigation';
193
+ case 'option':
194
+ return 'option';
195
+ case 'output':
196
+ return 'status';
197
+ case 'p':
198
+ return 'paragraph';
199
+ case 'progress':
200
+ return 'progressbar';
201
+ case 'search':
202
+ return 'search';
203
+ case 'section':
204
+ return el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby') ? 'region' : null;
205
+ case 'select': {
206
+ const s = el;
207
+ return s.multiple || s.size > 1 ? 'listbox' : 'combobox';
208
+ }
209
+ case 'strong':
210
+ return 'strong';
211
+ case 'sub':
212
+ return 'subscript';
213
+ case 'sup':
214
+ return 'superscript';
215
+ case 'table':
216
+ return 'table';
217
+ case 'tbody':
218
+ case 'thead':
219
+ case 'tfoot':
220
+ return 'rowgroup';
221
+ case 'td':
222
+ return 'cell';
223
+ case 'textarea':
224
+ return 'textbox';
225
+ case 'th':
226
+ return el.getAttribute('scope') === 'row' ? 'rowheader' : 'columnheader';
227
+ case 'time':
228
+ return 'time';
229
+ case 'tr':
230
+ return 'row';
231
+ default:
232
+ return null;
233
+ }
234
+ };
235
+ const textFromContent = (el) => {
236
+ let out = '';
237
+ for (const node of Array.from(el.childNodes)) {
238
+ if (node.nodeType === 3)
239
+ out += node.textContent ?? '';
240
+ else if (node.nodeType === 1) {
241
+ const child = node;
242
+ const label = child.getAttribute('aria-label');
243
+ if (label)
244
+ out += ' ' + label + ' ';
245
+ else if (child.tagName === 'IMG')
246
+ out += ' ' + (child.getAttribute('alt') ?? '') + ' ';
247
+ else if (child.tagName === 'SVG' && child.querySelector('title'))
248
+ out += ' ' + (child.querySelector('title')?.textContent ?? '') + ' ';
249
+ else if (getComputedStyle(child).display !== 'none')
250
+ out += textFromContent(child);
251
+ }
252
+ }
253
+ return out;
254
+ };
255
+ const accessibleName = (el) => {
256
+ const labelledby = el.getAttribute('aria-labelledby');
257
+ if (labelledby) {
258
+ const parts = labelledby
259
+ .split(/\s+/)
260
+ .map((id) => document.getElementById(id))
261
+ .filter((n) => !!n)
262
+ .map((n) => textFromContent(n));
263
+ if (parts.length)
264
+ return norm(parts.join(' '));
265
+ }
266
+ const ariaLabel = el.getAttribute('aria-label');
267
+ if (ariaLabel && ariaLabel.trim())
268
+ return norm(ariaLabel);
269
+ if (el instanceof HTMLInputElement ||
270
+ el instanceof HTMLTextAreaElement ||
271
+ el instanceof HTMLSelectElement) {
272
+ const labels = el.labels;
273
+ if (labels && labels.length)
274
+ return norm(Array.from(labels)
275
+ .map((l) => textFromContent(l))
276
+ .join(' '));
277
+ if (el instanceof HTMLInputElement) {
278
+ if (['button', 'submit', 'reset'].includes(el.type))
279
+ return norm(el.value || (el.type === 'submit' ? 'Submit' : el.type === 'reset' ? 'Reset' : ''));
280
+ if (el.type === 'image')
281
+ return norm(el.getAttribute('alt') ?? el.getAttribute('title') ?? '');
282
+ }
283
+ const ph = el.getAttribute('placeholder');
284
+ if (ph)
285
+ return norm(ph);
286
+ return norm(el.getAttribute('title') ?? '');
287
+ }
288
+ if (el.tagName === 'IMG' || el.tagName === 'AREA')
289
+ return norm(el.getAttribute('alt') ?? el.getAttribute('title') ?? '');
290
+ if (el.tagName.toLowerCase() === 'svg')
291
+ return norm(el.querySelector('title')?.textContent ?? '');
292
+ const fromContent = norm(textFromContent(el));
293
+ if (fromContent)
294
+ return fromContent;
295
+ return norm(el.getAttribute('title') ?? '');
296
+ };
297
+ const headingLevel = (el) => {
298
+ const m = /^H([1-6])$/.exec(el.tagName);
299
+ if (m)
300
+ return Number(m[1]);
301
+ const aria = el.getAttribute('aria-level');
302
+ return aria ? Number(aria) : null;
303
+ };
304
+ // ---- locator resolution ----
305
+ const parseSelector = (selector) => {
306
+ // Playwright selector string → steps. Supports `>>` chaining, `text=`, `xpath=`,
307
+ // `nth=`, `css=`, `id=`, `data-testid=`, and CSS with :has-text()/:text()/:text-is()/:visible.
308
+ const steps = [];
309
+ const parts = selector.split(/\s*>>\s*/);
310
+ for (const raw of parts) {
311
+ const part = raw.trim();
312
+ if (!part)
313
+ continue;
314
+ let m;
315
+ if ((m = /^text=(.*)$/s.exec(part))) {
316
+ const v = m[1];
317
+ const quoted = /^"(.*)"$/s.exec(v) ?? /^'(.*)'$/s.exec(v);
318
+ steps.push(quoted ? { k: 'text', m: { s: quoted[1], exact: true } } : { k: 'text', m: { s: v } });
319
+ }
320
+ else if ((m = /^xpath=(.*)$/s.exec(part)) || (m = /^(\/\/.*)$/s.exec(part))) {
321
+ steps.push({ k: 'xpath', expr: m[1] });
322
+ }
323
+ else if ((m = /^nth=(-?\d+)$/.exec(part))) {
324
+ steps.push({ k: 'nth', i: Number(m[1]) });
325
+ }
326
+ else if ((m = /^id=(.*)$/.exec(part))) {
327
+ const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
328
+ ? CSS.escape(m[1])
329
+ : m[1].replace(/([^\w-])/g, '\\$1');
330
+ steps.push({ k: 'css', sel: '#' + escaped });
331
+ }
332
+ else if ((m = /^data-testid=(.*)$/.exec(part))) {
333
+ steps.push({ k: 'testid', attr: 'data-testid', m: { s: m[1], exact: true } });
334
+ }
335
+ else if ((m = /^css=(.*)$/s.exec(part))) {
336
+ steps.push(...cssWithPseudo(m[1]));
337
+ }
338
+ else {
339
+ steps.push(...cssWithPseudo(part));
340
+ }
341
+ }
342
+ return steps;
343
+ };
344
+ const cssWithPseudo = (sel) => {
345
+ const out = [];
346
+ let css = sel;
347
+ let visible = false;
348
+ const filters = [];
349
+ let leafText = null;
350
+ css = css.replace(/:has-text\((["'])(.*?)\1\)/g, (_a, _q, t) => {
351
+ filters.push({ s: t });
352
+ return '';
353
+ });
354
+ css = css.replace(/:text-is\((["'])(.*?)\1\)/g, (_a, _q, t) => {
355
+ leafText = { s: t, exact: true };
356
+ return '';
357
+ });
358
+ css = css.replace(/:text\((["'])(.*?)\1\)/g, (_a, _q, t) => {
359
+ leafText = { s: t };
360
+ return '';
361
+ });
362
+ if (/:visible\b/.test(css)) {
363
+ visible = true;
364
+ css = css.replace(/:visible\b/g, '');
365
+ }
366
+ out.push({ k: 'css', sel: css.trim() || '*' });
367
+ for (const f of filters)
368
+ out.push({ k: 'filter', hasText: f });
369
+ if (leafText)
370
+ out.push({ k: 'text', m: leafText });
371
+ if (visible)
372
+ out.push({ k: 'visible' });
373
+ return out;
374
+ };
375
+ const textCandidates = (root, m) => {
376
+ const all = Array.from(root.querySelectorAll('*')).filter((el) => {
377
+ if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE'].includes(el.tagName))
378
+ return false;
379
+ return matchText(el.textContent ?? '', m);
380
+ });
381
+ // The smallest elements that still match: drop any candidate containing another candidate.
382
+ const set = new Set(all);
383
+ return all.filter((el) => !Array.from(set).some((other) => other !== el && el.contains(other)));
384
+ };
385
+ const labelTargets = (root, m) => {
386
+ const out = new Set();
387
+ for (const label of Array.from(root.querySelectorAll('label'))) {
388
+ if (!matchText(textFromContent(label), m))
389
+ continue;
390
+ const control = label.control ??
391
+ label.querySelector('input, select, textarea, button');
392
+ if (control)
393
+ out.add(control);
394
+ }
395
+ for (const el of Array.from(root.querySelectorAll('[aria-label], [aria-labelledby]'))) {
396
+ if (matchText(accessibleName(el), m))
397
+ out.add(el);
398
+ }
399
+ return Array.from(out);
400
+ };
401
+ const resolveStep = (step, roots, docRoot) => {
402
+ const scopes = roots ?? [docRoot];
403
+ const within = (fn) => {
404
+ const out = [];
405
+ const seen = new Set();
406
+ for (const r of scopes)
407
+ for (const el of fn(r))
408
+ if (!seen.has(el)) {
409
+ seen.add(el);
410
+ out.push(el);
411
+ }
412
+ return out;
413
+ };
414
+ switch (step.k) {
415
+ case 'css':
416
+ return within((r) => Array.from(r.querySelectorAll(step.sel)));
417
+ case 'text':
418
+ return within((r) => textCandidates(r, step.m));
419
+ case 'xpath':
420
+ return within((r) => {
421
+ const res = document.evaluate(step.expr, r, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
422
+ const out = [];
423
+ for (let i = 0; i < res.snapshotLength; i++) {
424
+ const n = res.snapshotItem(i);
425
+ if (n && n.nodeType === 1)
426
+ out.push(n);
427
+ }
428
+ return out;
429
+ });
430
+ case 'role':
431
+ return within((r) => Array.from(r.querySelectorAll('*')).filter((el) => {
432
+ if (roleOf(el) !== step.role)
433
+ return false;
434
+ if (!step.includeHidden && !isVisible(el))
435
+ return false;
436
+ if (step.name !== undefined && !matchText(accessibleName(el), step.name))
437
+ return false;
438
+ if (step.level !== undefined && headingLevel(el) !== step.level)
439
+ return false;
440
+ if (step.checked !== undefined && isChecked(el) !== step.checked)
441
+ return false;
442
+ if (step.pressed !== undefined &&
443
+ (el.getAttribute('aria-pressed') === 'true') !== step.pressed)
444
+ return false;
445
+ if (step.expanded !== undefined &&
446
+ (el.getAttribute('aria-expanded') === 'true') !== step.expanded)
447
+ return false;
448
+ if (step.selected !== undefined) {
449
+ const sel = el instanceof HTMLOptionElement
450
+ ? el.selected
451
+ : el.getAttribute('aria-selected') === 'true';
452
+ if (sel !== step.selected)
453
+ return false;
454
+ }
455
+ if (step.disabled !== undefined && isDisabled(el) !== step.disabled)
456
+ return false;
457
+ return true;
458
+ }));
459
+ case 'label':
460
+ return within((r) => labelTargets(r, step.m));
461
+ case 'placeholder':
462
+ return within((r) => Array.from(r.querySelectorAll('[placeholder]')).filter((el) => matchText(el.getAttribute('placeholder') ?? '', step.m)));
463
+ case 'testid':
464
+ return within((r) => Array.from(r.querySelectorAll('[' + step.attr + ']')).filter((el) => matchFull(el.getAttribute(step.attr) ?? '', step.m)));
465
+ case 'title':
466
+ return within((r) => Array.from(r.querySelectorAll('[title]')).filter((el) => matchText(el.getAttribute('title') ?? '', step.m)));
467
+ case 'alt':
468
+ return within((r) => Array.from(r.querySelectorAll('[alt]')).filter((el) => matchText(el.getAttribute('alt') ?? '', step.m)));
469
+ case 'nth': {
470
+ const list = roots ?? [];
471
+ const i = step.i;
472
+ const el = i < 0 ? list[list.length + i] : list[i];
473
+ return el ? [el] : [];
474
+ }
475
+ case 'visible':
476
+ return (roots ?? []).filter(isVisible);
477
+ case 'filter': {
478
+ const list = roots ?? [];
479
+ return list.filter((el) => {
480
+ if (step.hasText && !matchText(el.textContent ?? '', step.hasText))
481
+ return false;
482
+ if (step.hasNotText && matchText(el.textContent ?? '', step.hasNotText))
483
+ return false;
484
+ if (step.has && resolveChain(step.has, el).length === 0)
485
+ return false;
486
+ if (step.hasNot && resolveChain(step.hasNot, el).length > 0)
487
+ return false;
488
+ return true;
489
+ });
490
+ }
491
+ default:
492
+ throw new Error('unknown locator step: ' + step.k);
493
+ }
494
+ };
495
+ const resolveChain = (steps, root = document) => {
496
+ let current = null;
497
+ const expanded = [];
498
+ for (const step of steps) {
499
+ if (step.k === 'raw')
500
+ expanded.push(...parseSelector(String(step.sel)));
501
+ else
502
+ expanded.push(step);
503
+ }
504
+ for (const step of expanded) {
505
+ if (['nth', 'visible', 'filter'].includes(step.k))
506
+ current = resolveStep(step, current ?? resolveStep({ k: 'css', sel: '*' }, null, root), root);
507
+ else
508
+ current = resolveStep(step, current, root);
509
+ }
510
+ return current ?? [];
511
+ };
512
+ const describe = (steps) => steps
513
+ .map((s) => {
514
+ const t = (m) => m ? (m.re ? '/' + m.re.source + '/' + m.re.flags : JSON.stringify(m.s)) : '';
515
+ switch (s.k) {
516
+ case 'css':
517
+ case 'raw':
518
+ return 'locator(' + JSON.stringify(s.sel) + ')';
519
+ case 'text':
520
+ return 'getByText(' + t(s.m) + ')';
521
+ case 'xpath':
522
+ return 'locator(' + JSON.stringify('xpath=' + s.expr) + ')';
523
+ case 'role':
524
+ return ('getByRole(' +
525
+ JSON.stringify(s.role) +
526
+ (s.name !== undefined ? ', { name: ' + t(s.name) + ' }' : '') +
527
+ ')');
528
+ case 'label':
529
+ return 'getByLabel(' + t(s.m) + ')';
530
+ case 'placeholder':
531
+ return 'getByPlaceholder(' + t(s.m) + ')';
532
+ case 'testid':
533
+ return 'getByTestId(' + t(s.m) + ')';
534
+ case 'title':
535
+ return 'getByTitle(' + t(s.m) + ')';
536
+ case 'alt':
537
+ return 'getByAltText(' + t(s.m) + ')';
538
+ case 'nth':
539
+ return s.i === 0 ? 'first()' : s.i === -1 ? 'last()' : 'nth(' + s.i + ')';
540
+ case 'visible':
541
+ return 'filter({ visible: true })';
542
+ case 'filter':
543
+ return ('filter(' +
544
+ JSON.stringify({
545
+ hasText: s.hasText?.s,
546
+ hasNotText: s.hasNotText?.s,
547
+ }) +
548
+ ')');
549
+ default:
550
+ return s.k;
551
+ }
552
+ })
553
+ .join('.');
554
+ // ---- events ----
555
+ const fire = (el, type, init = {}) => {
556
+ const base = { bubbles: true, cancelable: true, composed: true };
557
+ let ev;
558
+ if (/^(pointer)/.test(type))
559
+ ev =
560
+ typeof PointerEvent === 'function'
561
+ ? new PointerEvent(type, {
562
+ ...base,
563
+ pointerId: 1,
564
+ pointerType: 'mouse',
565
+ isPrimary: true,
566
+ ...init,
567
+ })
568
+ : new MouseEvent(type, { ...base, ...init });
569
+ else if (/^(mouse|click|dblclick|contextmenu)/.test(type))
570
+ ev = new MouseEvent(type, { ...base, ...init });
571
+ else if (/^key/.test(type))
572
+ ev = new KeyboardEvent(type, { ...base, ...init });
573
+ else if (/^(input|beforeinput)$/.test(type))
574
+ ev = new InputEvent(type, { ...base, ...init });
575
+ else if (/^(focus|blur)/.test(type))
576
+ ev = new FocusEvent(type, { bubbles: type.endsWith('in') || type.endsWith('out'), ...init });
577
+ else if (/^touch/.test(type))
578
+ ev = new Event(type, base);
579
+ else
580
+ ev = new Event(type, base);
581
+ return el.dispatchEvent(ev);
582
+ };
583
+ const nativeSetValue = (el, value) => {
584
+ const proto = el instanceof HTMLTextAreaElement
585
+ ? HTMLTextAreaElement.prototype
586
+ : el instanceof HTMLSelectElement
587
+ ? HTMLSelectElement.prototype
588
+ : HTMLInputElement.prototype;
589
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
590
+ if (desc && desc.set)
591
+ desc.set.call(el, value);
592
+ else
593
+ el.value = value;
594
+ };
595
+ const insertText = (el, text) => {
596
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
597
+ const start = el.selectionStart ?? el.value.length;
598
+ const end = el.selectionEnd ?? el.value.length;
599
+ const next = el.value.slice(0, start) + text + el.value.slice(end);
600
+ fire(el, 'beforeinput', { inputType: 'insertText', data: text });
601
+ nativeSetValue(el, next);
602
+ try {
603
+ el.setSelectionRange(start + text.length, start + text.length);
604
+ }
605
+ catch (_error) {
606
+ /* type=number etc. */
607
+ }
608
+ fire(el, 'input', { inputType: 'insertText', data: text });
609
+ }
610
+ else if (el.isContentEditable) {
611
+ let done;
612
+ try {
613
+ done = document.execCommand('insertText', false, text);
614
+ }
615
+ catch (_error) {
616
+ done = false;
617
+ }
618
+ if (!done) {
619
+ el.textContent = (el.textContent ?? '') + text;
620
+ fire(el, 'input', { inputType: 'insertText', data: text });
621
+ }
622
+ }
623
+ };
624
+ const pointAt = (el, position) => {
625
+ const r = el.getBoundingClientRect();
626
+ return {
627
+ x: r.left + (position ? position.x : r.width / 2),
628
+ y: r.top + (position ? position.y : r.height / 2),
629
+ };
630
+ };
631
+ const hitOk = (el, hit) => {
632
+ if (!hit)
633
+ return false;
634
+ if (hit === el || el.contains(hit) || hit.contains(el))
635
+ return true;
636
+ // A <label> covering its control, or a control's label: Playwright retargets both ways.
637
+ const lbl = hit.closest('label');
638
+ if (lbl && lbl.control === el)
639
+ return true;
640
+ const elLbl = el.closest('label');
641
+ if (elLbl && elLbl.control === hit)
642
+ return true;
643
+ return false;
644
+ };
645
+ const clickAt = (el, x, y, opts) => {
646
+ const button = opts.button === 'right' ? 2 : opts.button === 'middle' ? 1 : 0;
647
+ const mods = opts.modifiers ?? [];
648
+ const init = {
649
+ clientX: x,
650
+ clientY: y,
651
+ button,
652
+ buttons: button === 0 ? 1 : button === 2 ? 2 : 4,
653
+ detail: 1,
654
+ ctrlKey: mods.includes('Control') || mods.includes('ControlOrMeta'),
655
+ shiftKey: mods.includes('Shift'),
656
+ altKey: mods.includes('Alt'),
657
+ metaKey: mods.includes('Meta'),
658
+ };
659
+ fire(el, 'pointermove', { ...init, buttons: 0 });
660
+ fire(el, 'mousemove', { ...init, buttons: 0 });
661
+ fire(el, 'pointerdown', init);
662
+ const proceed = fire(el, 'mousedown', init);
663
+ if (proceed) {
664
+ const focusTarget = isFocusable(el)
665
+ ? el
666
+ : (el.closest('a[href], button, input, select, textarea, [tabindex]') ?? null);
667
+ if (focusTarget && focusTarget !== document.activeElement)
668
+ focusTarget.focus();
669
+ }
670
+ fire(el, 'pointerup', { ...init, buttons: 0 });
671
+ fire(el, 'mouseup', { ...init, buttons: 0 });
672
+ const count = opts.clickCount ?? 1;
673
+ for (let i = 1; i <= count; i++) {
674
+ if (button === 2)
675
+ fire(el, 'contextmenu', { ...init, detail: i });
676
+ else
677
+ fire(el, 'click', { ...init, buttons: 0, detail: i });
678
+ }
679
+ if (count >= 2 && button === 0)
680
+ fire(el, 'dblclick', { ...init, buttons: 0, detail: 2 });
681
+ };
682
+ const hoverAt = (el, x, y) => {
683
+ const init = { clientX: x, clientY: y, buttons: 0 };
684
+ fire(el, 'pointerover', init);
685
+ fire(el, 'pointerenter', { ...init, bubbles: false });
686
+ fire(el, 'mouseover', init);
687
+ fire(el, 'mouseenter', { ...init, bubbles: false });
688
+ fire(el, 'pointermove', init);
689
+ fire(el, 'mousemove', init);
690
+ };
691
+ const KEY_CODES = {
692
+ Enter: 'Enter',
693
+ Tab: 'Tab',
694
+ Escape: 'Escape',
695
+ Backspace: 'Backspace',
696
+ Delete: 'Delete',
697
+ ArrowUp: 'ArrowUp',
698
+ ArrowDown: 'ArrowDown',
699
+ ArrowLeft: 'ArrowLeft',
700
+ ArrowRight: 'ArrowRight',
701
+ Home: 'Home',
702
+ End: 'End',
703
+ PageUp: 'PageUp',
704
+ PageDown: 'PageDown',
705
+ ' ': 'Space',
706
+ Space: 'Space',
707
+ };
708
+ const focusables = () => Array.from(document.querySelectorAll('a[href], button, input, select, textarea, summary, [tabindex], [contenteditable="true"]')).filter((el) => isFocusable(el) && isVisible(el) && el.tabIndex !== -1);
709
+ const pressKey = (target, combo) => {
710
+ const parts = combo.split('+');
711
+ let key = parts.pop() ?? '';
712
+ if (key === 'Space')
713
+ key = ' ';
714
+ const mods = parts;
715
+ const init = {
716
+ key,
717
+ code: KEY_CODES[key] ?? (key.length === 1 ? 'Key' + key.toUpperCase() : key),
718
+ ctrlKey: mods.includes('Control') || mods.includes('ControlOrMeta'),
719
+ shiftKey: mods.includes('Shift'),
720
+ altKey: mods.includes('Alt'),
721
+ metaKey: mods.includes('Meta'),
722
+ };
723
+ const downOk = fire(target, 'keydown', init);
724
+ let pressOk = true;
725
+ if (key.length === 1 || key === 'Enter')
726
+ pressOk = fire(target, 'keypress', {
727
+ ...init,
728
+ charCode: key.length === 1 ? key.charCodeAt(0) : 13,
729
+ });
730
+ if (downOk && pressOk) {
731
+ if (key.length === 1 && !init.ctrlKey && !init.metaKey && isEditable(target))
732
+ insertText(target, key);
733
+ else if (key === 'Enter') {
734
+ if (target instanceof HTMLTextAreaElement)
735
+ insertText(target, '\n');
736
+ else if (target instanceof HTMLInputElement && target.form) {
737
+ const form = target.form;
738
+ const submitter = form.querySelector('button:not([type]), button[type="submit"], input[type="submit"]');
739
+ if (typeof form.requestSubmit === 'function')
740
+ form.requestSubmit(submitter ?? undefined);
741
+ else
742
+ form.submit();
743
+ }
744
+ else if (target instanceof HTMLElement &&
745
+ (target.tagName === 'BUTTON' ||
746
+ target.tagName === 'A' ||
747
+ target.getAttribute('role') === 'button'))
748
+ target.click();
749
+ }
750
+ else if (key === ' ' &&
751
+ target instanceof HTMLElement &&
752
+ (target.tagName === 'BUTTON' ||
753
+ isCheckable(target) ||
754
+ target.getAttribute('role') === 'button'))
755
+ target.click();
756
+ else if (key === 'Backspace' &&
757
+ isEditable(target) &&
758
+ (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
759
+ const s = target.selectionStart ?? target.value.length;
760
+ const e = target.selectionEnd ?? target.value.length;
761
+ const from = s === e ? Math.max(0, s - 1) : s;
762
+ nativeSetValue(target, target.value.slice(0, from) + target.value.slice(e));
763
+ try {
764
+ target.setSelectionRange(from, from);
765
+ }
766
+ catch (_error) {
767
+ /* ignore */
768
+ }
769
+ fire(target, 'input', { inputType: 'deleteContentBackward' });
770
+ }
771
+ else if (key === 'Tab') {
772
+ const list = focusables();
773
+ const idx = list.indexOf(document.activeElement);
774
+ const next = init.shiftKey
775
+ ? (list[idx - 1] ?? list[list.length - 1])
776
+ : (list[idx + 1] ?? list[0]);
777
+ if (next)
778
+ next.focus();
779
+ }
780
+ }
781
+ fire(target, 'keyup', init);
782
+ };
783
+ // ---- single-target actions with actionability + auto-wait ----
784
+ const ACTIONS_NEEDING_VISIBLE = new Set([
785
+ 'click',
786
+ 'dblclick',
787
+ 'hover',
788
+ 'tap',
789
+ 'fill',
790
+ 'type',
791
+ 'press',
792
+ 'check',
793
+ 'uncheck',
794
+ 'setChecked',
795
+ 'selectOption',
796
+ 'focus',
797
+ 'scrollIntoViewIfNeeded',
798
+ 'clear',
799
+ ]);
800
+ const ACTIONS_NEEDING_ENABLED = new Set([
801
+ 'click',
802
+ 'dblclick',
803
+ 'tap',
804
+ 'fill',
805
+ 'type',
806
+ 'press',
807
+ 'check',
808
+ 'uncheck',
809
+ 'setChecked',
810
+ 'selectOption',
811
+ 'clear',
812
+ ]);
813
+ const ACTIONS_NEEDING_POINTER = new Set(['click', 'dblclick', 'hover', 'tap']);
814
+ const act = async (steps, action, opts, deadline) => {
815
+ let reason;
816
+ for (;;) {
817
+ const els = resolveChain(steps);
818
+ if (els.length > 1)
819
+ return { ok: false, strict: true, count: els.length };
820
+ const el = els[0];
821
+ if (!el)
822
+ reason = describe(steps) + ' — element not found';
823
+ else if (ACTIONS_NEEDING_VISIBLE.has(action) && !opts.force && !isVisible(el))
824
+ reason = describe(steps) + ' — element is not visible';
825
+ else if (ACTIONS_NEEDING_ENABLED.has(action) && !opts.force && isDisabled(el))
826
+ reason = describe(steps) + ' — element is disabled';
827
+ else if (['fill', 'type', 'clear'].includes(action) && !opts.force && !isEditable(el))
828
+ return {
829
+ ok: false,
830
+ error: describe(steps) +
831
+ ' — element is not an <input>, <textarea>, <select> or [contenteditable] element',
832
+ };
833
+ else {
834
+ if (ACTIONS_NEEDING_POINTER.has(action) || action === 'scrollIntoViewIfNeeded') {
835
+ if (!opts.noScroll) {
836
+ try {
837
+ el.scrollIntoView({ block: 'center', inline: 'center' });
838
+ }
839
+ catch (_error) {
840
+ /* ignore */
841
+ }
842
+ }
843
+ if (action === 'scrollIntoViewIfNeeded')
844
+ return { ok: true };
845
+ const p = pointAt(el, opts.position);
846
+ if (!opts.force) {
847
+ const hit = typeof document.elementFromPoint === 'function'
848
+ ? document.elementFromPoint(p.x, p.y)
849
+ : null;
850
+ // A null hit with the point inside the viewport is a document that cannot hit-test
851
+ // (jsdom, a detached rendering); treat it as uncovered rather than unreachable.
852
+ const inView = p.x >= 0 && p.y >= 0 && p.x <= innerWidth && p.y <= innerHeight;
853
+ if (!(hit === null && inView) && !hitOk(el, hit)) {
854
+ if (!hit || p.x < 0 || p.y < 0 || p.x > innerWidth || p.y > innerHeight)
855
+ reason = describe(steps) + ' — element is outside of the viewport';
856
+ else {
857
+ const h = hit;
858
+ reason =
859
+ describe(steps) +
860
+ ' — <' +
861
+ h.tagName.toLowerCase() +
862
+ (h.id ? '#' + h.id : '') +
863
+ (h.className && typeof h.className === 'string'
864
+ ? '.' + h.className.trim().split(/\s+/).slice(0, 2).join('.')
865
+ : '') +
866
+ '> intercepts pointer events';
867
+ }
868
+ if (Date.now() > deadline)
869
+ return {
870
+ ok: false,
871
+ error: reason ?? 'waiting for ' + describe(steps),
872
+ timeout: true,
873
+ };
874
+ await sleep(50);
875
+ continue;
876
+ }
877
+ }
878
+ if (action === 'hover')
879
+ hoverAt(el, p.x, p.y);
880
+ else if (action === 'tap') {
881
+ fire(el, 'touchstart');
882
+ fire(el, 'touchend');
883
+ clickAt(el, p.x, p.y, opts);
884
+ }
885
+ else
886
+ clickAt(el, p.x, p.y, {
887
+ ...opts,
888
+ clickCount: action === 'dblclick' ? 2 : (opts.clickCount ?? 1),
889
+ });
890
+ return { ok: true };
891
+ }
892
+ if (action === 'focus') {
893
+ ;
894
+ el.focus();
895
+ return { ok: true };
896
+ }
897
+ if (action === 'blur') {
898
+ ;
899
+ el.blur();
900
+ return { ok: true };
901
+ }
902
+ if (action === 'fill' || action === 'clear') {
903
+ const value = action === 'clear' ? '' : String(opts.value ?? '');
904
+ el.focus();
905
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
906
+ const simple = !(el instanceof HTMLInputElement) ||
907
+ !['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'image'].includes(el.type);
908
+ if (!simple)
909
+ return {
910
+ ok: false,
911
+ error: describe(steps) + ' — cannot fill an input of type ' + el.type,
912
+ };
913
+ if (el instanceof HTMLInputElement &&
914
+ [
915
+ 'date',
916
+ 'time',
917
+ 'datetime-local',
918
+ 'month',
919
+ 'week',
920
+ 'color',
921
+ 'range',
922
+ 'number',
923
+ ].includes(el.type)) {
924
+ nativeSetValue(el, value);
925
+ fire(el, 'input', { inputType: 'insertText', data: value });
926
+ }
927
+ else {
928
+ try {
929
+ el.select();
930
+ }
931
+ catch (_error) {
932
+ /* ignore */
933
+ }
934
+ fire(el, 'beforeinput', { inputType: 'insertText', data: value });
935
+ nativeSetValue(el, value);
936
+ try {
937
+ el.setSelectionRange(value.length, value.length);
938
+ }
939
+ catch (_error) {
940
+ /* ignore */
941
+ }
942
+ fire(el, 'input', { inputType: 'insertText', data: value });
943
+ }
944
+ fire(el, 'change');
945
+ }
946
+ else if (el instanceof HTMLSelectElement) {
947
+ nativeSetValue(el, value);
948
+ fire(el, 'input');
949
+ fire(el, 'change');
950
+ }
951
+ else {
952
+ const sel = window.getSelection();
953
+ if (sel) {
954
+ sel.selectAllChildren(el);
955
+ }
956
+ let done;
957
+ try {
958
+ done = document.execCommand('insertText', false, value);
959
+ }
960
+ catch (_error) {
961
+ done = false;
962
+ }
963
+ if (!done) {
964
+ el.textContent = value;
965
+ fire(el, 'input', { inputType: 'insertText', data: value });
966
+ }
967
+ }
968
+ return { ok: true };
969
+ }
970
+ if (action === 'type') {
971
+ ;
972
+ el.focus();
973
+ for (const ch of String(opts.text ?? ''))
974
+ pressKey(el, ch === '\n' ? 'Enter' : ch);
975
+ return { ok: true };
976
+ }
977
+ if (action === 'press') {
978
+ ;
979
+ el.focus();
980
+ pressKey(el, String(opts.key ?? ''));
981
+ return { ok: true };
982
+ }
983
+ if (action === 'check' || action === 'uncheck' || action === 'setChecked') {
984
+ if (!isCheckable(el))
985
+ return { ok: false, error: describe(steps) + ' — not a checkbox, radio or switch' };
986
+ const want = action === 'check' ? true : action === 'uncheck' ? false : Boolean(opts.checked);
987
+ if (isChecked(el) !== want) {
988
+ const p = pointAt(el);
989
+ clickAt(el, p.x, p.y, {});
990
+ if (isChecked(el) !== want)
991
+ return {
992
+ ok: false,
993
+ error: describe(steps) + ' — clicking did not change its checked state',
994
+ };
995
+ }
996
+ return { ok: true };
997
+ }
998
+ if (action === 'selectOption') {
999
+ if (!(el instanceof HTMLSelectElement))
1000
+ return { ok: false, error: describe(steps) + ' — not a <select>' };
1001
+ const wanted = (Array.isArray(opts.values) ? opts.values : [opts.values]);
1002
+ const chosen = [];
1003
+ for (const option of Array.from(el.options)) {
1004
+ const match = wanted.some((w) => {
1005
+ if (w === null || w === undefined)
1006
+ return false;
1007
+ if (typeof w === 'string')
1008
+ return (option.value === w ||
1009
+ norm(option.label) === norm(w) ||
1010
+ norm(option.textContent) === norm(w));
1011
+ if (w.value !== undefined)
1012
+ return option.value === w.value;
1013
+ if (w.label !== undefined)
1014
+ return norm(option.label) === norm(w.label);
1015
+ if (w.index !== undefined)
1016
+ return option.index === w.index;
1017
+ return false;
1018
+ });
1019
+ option.selected = match;
1020
+ if (match)
1021
+ chosen.push(option.value);
1022
+ }
1023
+ if (chosen.length === 0 && wanted.length)
1024
+ return {
1025
+ ok: false,
1026
+ error: describe(steps) + ' — no option matched ' + JSON.stringify(wanted),
1027
+ };
1028
+ fire(el, 'input');
1029
+ fire(el, 'change');
1030
+ return { ok: true, values: chosen };
1031
+ }
1032
+ if (action === 'dispatchEvent') {
1033
+ const type = String(opts.type ?? '');
1034
+ const init = opts.init ?? {};
1035
+ fire(el, type, init);
1036
+ return { ok: true };
1037
+ }
1038
+ if (action === 'highlight')
1039
+ return { ok: true };
1040
+ return { ok: false, error: 'unknown action: ' + action };
1041
+ }
1042
+ if (Date.now() > deadline)
1043
+ return { ok: false, error: reason ?? 'waiting for ' + describe(steps), timeout: true };
1044
+ await sleep(50);
1045
+ }
1046
+ };
1047
+ // ---- reads (strict) ----
1048
+ const single = async (steps, deadline, waitAttached) => {
1049
+ for (;;) {
1050
+ const els = resolveChain(steps);
1051
+ if (els.length > 1)
1052
+ return { res: { ok: false, strict: true, count: els.length } };
1053
+ if (els.length === 1)
1054
+ return { el: els[0] };
1055
+ if (!waitAttached || Date.now() > deadline)
1056
+ return {
1057
+ res: {
1058
+ ok: false,
1059
+ error: describe(steps) + ' — element not found',
1060
+ timeout: waitAttached,
1061
+ },
1062
+ };
1063
+ await sleep(50);
1064
+ }
1065
+ };
1066
+ const boundingBox = (el) => {
1067
+ if (!isVisible(el))
1068
+ return null;
1069
+ const r = el.getBoundingClientRect();
1070
+ return { x: r.x, y: r.y, width: r.width, height: r.height };
1071
+ };
1072
+ const read = async (steps, what, args, deadline) => {
1073
+ if (what === 'isVisible' || what === 'isHidden') {
1074
+ const els = resolveChain(steps);
1075
+ if (els.length > 1)
1076
+ return { ok: false, strict: true, count: els.length };
1077
+ const vis = els.length === 1 && isVisible(els[0]);
1078
+ return { ok: true, value: what === 'isVisible' ? vis : !vis };
1079
+ }
1080
+ if (what === 'count')
1081
+ return { ok: true, value: resolveChain(steps).length };
1082
+ const got = await single(steps, deadline, true);
1083
+ if (got.res)
1084
+ return got.res;
1085
+ const el = got.el;
1086
+ switch (what) {
1087
+ case 'textContent':
1088
+ return { ok: true, value: el.textContent };
1089
+ case 'innerText':
1090
+ return { ok: true, value: el.innerText };
1091
+ case 'innerHTML':
1092
+ return { ok: true, value: el.innerHTML };
1093
+ case 'inputValue':
1094
+ if (el instanceof HTMLInputElement ||
1095
+ el instanceof HTMLTextAreaElement ||
1096
+ el instanceof HTMLSelectElement)
1097
+ return { ok: true, value: el.value };
1098
+ return {
1099
+ ok: false,
1100
+ error: describe(steps) + ' — inputValue: not an input, textarea or select',
1101
+ };
1102
+ case 'getAttribute':
1103
+ return { ok: true, value: el.getAttribute(String(args[0])) };
1104
+ case 'isEnabled':
1105
+ return { ok: true, value: !isDisabled(el) };
1106
+ case 'isDisabled':
1107
+ return { ok: true, value: isDisabled(el) };
1108
+ case 'isEditable':
1109
+ return { ok: true, value: isEditable(el) };
1110
+ case 'isChecked':
1111
+ if (!isCheckable(el))
1112
+ return { ok: false, error: describe(steps) + ' — not a checkbox, radio or switch' };
1113
+ return { ok: true, value: isChecked(el) };
1114
+ case 'boundingBox':
1115
+ return { ok: true, value: boundingBox(el) };
1116
+ case 'ariaSnapshot':
1117
+ return {
1118
+ ok: true,
1119
+ value: '- ' +
1120
+ (roleOf(el) ?? el.tagName.toLowerCase()) +
1121
+ (accessibleName(el) ? ' "' + accessibleName(el) + '"' : ''),
1122
+ };
1123
+ default:
1124
+ return { ok: false, error: 'unknown read: ' + what };
1125
+ }
1126
+ };
1127
+ const readAll = (steps, what) => {
1128
+ const els = resolveChain(steps);
1129
+ if (what === 'allTextContents')
1130
+ return { ok: true, value: els.map((el) => el.textContent ?? '') };
1131
+ if (what === 'allInnerTexts')
1132
+ return { ok: true, value: els.map((el) => el.innerText) };
1133
+ if (what === 'count')
1134
+ return { ok: true, value: els.length };
1135
+ return { ok: false, error: 'unknown readAll: ' + what };
1136
+ };
1137
+ const runFn = (source) => new Function('return (' + source + ')')();
1138
+ const evalOn = async (steps, source, arg, deadline) => {
1139
+ const got = await single(steps, deadline, true);
1140
+ if (got.res)
1141
+ return got.res;
1142
+ return { ok: true, value: await runFn(source)(got.el, arg) };
1143
+ };
1144
+ const evalAll = async (steps, source, arg) => ({
1145
+ ok: true,
1146
+ value: await runFn(source)(resolveChain(steps), arg),
1147
+ });
1148
+ const waitFor = async (steps, state, deadline) => {
1149
+ for (;;) {
1150
+ const els = resolveChain(steps);
1151
+ if (els.length > 1 && state !== 'detached' && state !== 'hidden')
1152
+ return { ok: false, strict: true, count: els.length };
1153
+ const el = els[0];
1154
+ const met = state === 'attached'
1155
+ ? !!el
1156
+ : state === 'detached'
1157
+ ? !el
1158
+ : state === 'visible'
1159
+ ? !!el && isVisible(el)
1160
+ : state === 'hidden'
1161
+ ? !el || !isVisible(el)
1162
+ : false;
1163
+ if (met)
1164
+ return { ok: true };
1165
+ if (Date.now() > deadline)
1166
+ return { ok: false, error: describe(steps) + ' — waiting for ' + state, timeout: true };
1167
+ await sleep(50);
1168
+ }
1169
+ };
1170
+ // ---- expect probes: one evaluation, the driver polls ----
1171
+ const probe = (steps, matcher, args) => {
1172
+ const els = resolveChain(steps);
1173
+ const strictOk = els.length <= 1;
1174
+ const el = els[0];
1175
+ const text = (e) => args.useInnerText ? e.innerText : (e.textContent ?? '');
1176
+ switch (matcher) {
1177
+ case 'toHaveCount':
1178
+ return { pass: els.length === args.n, received: els.length };
1179
+ case 'toBeVisible':
1180
+ if (!strictOk)
1181
+ return { strict: true, count: els.length };
1182
+ return {
1183
+ pass: !!el && isVisible(el),
1184
+ received: el ? (isVisible(el) ? 'visible' : 'hidden') : 'not found',
1185
+ };
1186
+ case 'toBeHidden':
1187
+ if (!strictOk)
1188
+ return { strict: true, count: els.length };
1189
+ return {
1190
+ pass: !el || !isVisible(el),
1191
+ received: el ? (isVisible(el) ? 'visible' : 'hidden') : 'not found',
1192
+ };
1193
+ case 'toBeAttached':
1194
+ if (!strictOk)
1195
+ return { strict: true, count: els.length };
1196
+ return { pass: !!el, received: el ? 'attached' : 'detached' };
1197
+ case 'toHaveText': {
1198
+ const expected = args.expected;
1199
+ if (Array.isArray(expected)) {
1200
+ const texts = els.map(text);
1201
+ const pass = texts.length === expected.length && expected.every((m, i) => matchFull(texts[i], m));
1202
+ return { pass, received: texts.map(norm) };
1203
+ }
1204
+ if (!strictOk)
1205
+ return { strict: true, count: els.length };
1206
+ return {
1207
+ pass: !!el && matchFull(text(el), expected),
1208
+ received: el ? norm(text(el)) : 'not found',
1209
+ };
1210
+ }
1211
+ case 'toContainText': {
1212
+ const expected = args.expected;
1213
+ if (Array.isArray(expected)) {
1214
+ const texts = els.map(text);
1215
+ const pass = expected.every((m) => texts.some((t) => matchSub(t, m)));
1216
+ return { pass, received: texts.map(norm) };
1217
+ }
1218
+ if (!strictOk)
1219
+ return { strict: true, count: els.length };
1220
+ return {
1221
+ pass: !!el && matchSub(text(el), expected),
1222
+ received: el ? norm(text(el)) : 'not found',
1223
+ };
1224
+ }
1225
+ default:
1226
+ break;
1227
+ }
1228
+ if (!strictOk)
1229
+ return { strict: true, count: els.length };
1230
+ if (!el)
1231
+ return { pass: false, received: 'not found' };
1232
+ switch (matcher) {
1233
+ case 'toHaveAttribute': {
1234
+ const name = String(args.name);
1235
+ const has = el.hasAttribute(name);
1236
+ const value = el.getAttribute(name);
1237
+ if (args.expected === undefined)
1238
+ return { pass: has, received: has ? value : 'no attribute' };
1239
+ return {
1240
+ pass: has && matchFull(value ?? '', args.expected),
1241
+ received: has ? value : 'no attribute',
1242
+ };
1243
+ }
1244
+ case 'toHaveClass': {
1245
+ const cls = norm(el.getAttribute('class') ?? '');
1246
+ const expected = args.expected;
1247
+ if (Array.isArray(expected))
1248
+ return { pass: expected.every((m) => matchFull(cls, m)), received: cls };
1249
+ return { pass: matchFull(cls, expected), received: cls };
1250
+ }
1251
+ case 'toContainClass': {
1252
+ const have = new Set(norm(el.getAttribute('class') ?? '')
1253
+ .split(' ')
1254
+ .filter(Boolean));
1255
+ const wanted = String(args.expected).split(/\s+/).filter(Boolean);
1256
+ return { pass: wanted.every((c) => have.has(c)), received: Array.from(have).join(' ') };
1257
+ }
1258
+ case 'toHaveCSS': {
1259
+ const value = getComputedStyle(el).getPropertyValue(String(args.name)).trim();
1260
+ return { pass: matchFull(value, args.expected), received: value };
1261
+ }
1262
+ case 'toHaveValue': {
1263
+ const value = el instanceof HTMLInputElement ||
1264
+ el instanceof HTMLTextAreaElement ||
1265
+ el instanceof HTMLSelectElement
1266
+ ? el.value
1267
+ : '';
1268
+ return { pass: matchFull(value, args.expected), received: value };
1269
+ }
1270
+ case 'toHaveValues': {
1271
+ const values = el instanceof HTMLSelectElement ? Array.from(el.selectedOptions).map((o) => o.value) : [];
1272
+ const expected = args.expected;
1273
+ return {
1274
+ pass: values.length === expected.length && expected.every((m, i) => matchFull(values[i], m)),
1275
+ received: values,
1276
+ };
1277
+ }
1278
+ case 'toHaveId':
1279
+ return { pass: matchFull(el.id, args.expected), received: el.id };
1280
+ case 'toHaveJSProperty': {
1281
+ const value = el[String(args.name)];
1282
+ return { pass: JSON.stringify(value) === JSON.stringify(args.expected), received: value };
1283
+ }
1284
+ case 'toBeChecked': {
1285
+ if (!isCheckable(el))
1286
+ return { pass: false, received: 'not a checkbox, radio or switch' };
1287
+ const want = args.checked === undefined ? true : Boolean(args.checked);
1288
+ return { pass: isChecked(el) === want, received: isChecked(el) ? 'checked' : 'unchecked' };
1289
+ }
1290
+ case 'toBeEnabled':
1291
+ return { pass: !isDisabled(el), received: isDisabled(el) ? 'disabled' : 'enabled' };
1292
+ case 'toBeDisabled':
1293
+ return { pass: isDisabled(el), received: isDisabled(el) ? 'disabled' : 'enabled' };
1294
+ case 'toBeEditable':
1295
+ return { pass: isEditable(el), received: isEditable(el) ? 'editable' : 'not editable' };
1296
+ case 'toBeEmpty': {
1297
+ const empty = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement
1298
+ ? el.value === ''
1299
+ : norm(el.textContent).length === 0 && el.children.length === 0;
1300
+ return { pass: empty, received: empty ? 'empty' : 'not empty' };
1301
+ }
1302
+ case 'toBeFocused':
1303
+ return {
1304
+ pass: document.activeElement === el,
1305
+ received: document.activeElement
1306
+ ? '<' + document.activeElement.tagName.toLowerCase() + '>'
1307
+ : 'nothing focused',
1308
+ };
1309
+ case 'toBeInViewport': {
1310
+ const r = el.getBoundingClientRect();
1311
+ const ix = Math.max(0, Math.min(r.right, innerWidth) - Math.max(r.left, 0));
1312
+ const iy = Math.max(0, Math.min(r.bottom, innerHeight) - Math.max(r.top, 0));
1313
+ const area = r.width * r.height;
1314
+ const ratio = area > 0 ? (ix * iy) / area : 0;
1315
+ const min = args.ratio === undefined ? 0 : Number(args.ratio);
1316
+ return {
1317
+ pass: area > 0 && (min > 0 ? ratio >= min : ratio > 0),
1318
+ received: 'viewport ratio ' + ratio.toFixed(2),
1319
+ };
1320
+ }
1321
+ case 'toHaveAccessibleName':
1322
+ return {
1323
+ pass: matchFull(accessibleName(el), args.expected),
1324
+ received: accessibleName(el),
1325
+ };
1326
+ case 'toHaveRole':
1327
+ return { pass: roleOf(el) === args.expected, received: roleOf(el) };
1328
+ default:
1329
+ return { error: 'unknown matcher: ' + matcher };
1330
+ }
1331
+ };
1332
+ // ---- page-level helpers ----
1333
+ const info = () => ({
1334
+ url: location.href,
1335
+ title: document.title,
1336
+ readyState: document.readyState,
1337
+ innerWidth,
1338
+ innerHeight,
1339
+ scrollX,
1340
+ scrollY,
1341
+ scrollHeight: document.documentElement.scrollHeight,
1342
+ });
1343
+ const mouse = (action, x, y, opts) => {
1344
+ if (action === 'wheel') {
1345
+ window.scrollBy(x, y);
1346
+ const target = document.elementFromPoint(innerWidth / 2, innerHeight / 2) ?? document.documentElement;
1347
+ target.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaX: x, deltaY: y }));
1348
+ return { ok: true };
1349
+ }
1350
+ const el = typeof document.elementFromPoint === 'function' ? document.elementFromPoint(x, y) : null;
1351
+ if (!el)
1352
+ return { ok: false, error: 'no element at (' + x + ', ' + y + ')' };
1353
+ if (action === 'move')
1354
+ hoverAt(el, x, y);
1355
+ else if (action === 'click' || action === 'dblclick')
1356
+ clickAt(el, x, y, {
1357
+ ...opts,
1358
+ clickCount: action === 'dblclick' ? 2 : (opts.clickCount ?? 1),
1359
+ });
1360
+ else if (action === 'down') {
1361
+ fire(el, 'pointerdown', { clientX: x, clientY: y, button: 0, buttons: 1 });
1362
+ fire(el, 'mousedown', { clientX: x, clientY: y, button: 0, buttons: 1 });
1363
+ }
1364
+ else if (action === 'up') {
1365
+ fire(el, 'pointerup', { clientX: x, clientY: y, button: 0 });
1366
+ fire(el, 'mouseup', { clientX: x, clientY: y, button: 0 });
1367
+ fire(el, 'click', { clientX: x, clientY: y, button: 0, detail: 1 });
1368
+ }
1369
+ return { ok: true };
1370
+ };
1371
+ const keyboard = (action, arg) => {
1372
+ const target = document.activeElement ?? document.body;
1373
+ if (action === 'press')
1374
+ for (const combo of String(arg).split(/(?<!\+)\s+/))
1375
+ pressKey(target, combo);
1376
+ else if (action === 'type')
1377
+ for (const ch of String(arg))
1378
+ pressKey(target, ch === '\n' ? 'Enter' : ch);
1379
+ else if (action === 'insertText')
1380
+ insertText(target, String(arg));
1381
+ else if (action === 'down')
1382
+ fire(target, 'keydown', { key: arg, code: KEY_CODES[arg] ?? arg });
1383
+ else if (action === 'up')
1384
+ fire(target, 'keyup', { key: arg, code: KEY_CODES[arg] ?? arg });
1385
+ return { ok: true };
1386
+ };
1387
+ const resolveUrl = (url) => {
1388
+ const u = new URL(url, location.href);
1389
+ if ((u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '0.0.0.0') &&
1390
+ u.host !== location.host)
1391
+ return location.origin + u.pathname + u.search + u.hash;
1392
+ return u.href;
1393
+ };
1394
+ const doFetch = async (url, init) => {
1395
+ const headers = init.headers ?? {};
1396
+ const body = init.data !== undefined
1397
+ ? typeof init.data === 'string'
1398
+ ? init.data
1399
+ : JSON.stringify(init.data)
1400
+ : init.body;
1401
+ if (init.data !== undefined &&
1402
+ typeof init.data !== 'string' &&
1403
+ !Object.keys(headers).some((h) => h.toLowerCase() === 'content-type'))
1404
+ headers['content-type'] = 'application/json';
1405
+ const res = await fetch(resolveUrl(url), {
1406
+ method: String(init.method ?? 'GET'),
1407
+ headers,
1408
+ body,
1409
+ credentials: 'include',
1410
+ redirect: init.maxRedirects === 0 ? 'manual' : 'follow',
1411
+ });
1412
+ const text = await res.text();
1413
+ return {
1414
+ ok: true,
1415
+ value: {
1416
+ url: res.url,
1417
+ status: res.status,
1418
+ statusText: res.statusText,
1419
+ ok: res.ok,
1420
+ headers: Array.from(res.headers.entries()),
1421
+ text,
1422
+ },
1423
+ };
1424
+ };
1425
+ const addTag = (kind, opts) => {
1426
+ if (kind === 'style') {
1427
+ const style = document.createElement('style');
1428
+ style.textContent = String(opts.content ?? '');
1429
+ document.head.appendChild(style);
1430
+ return { ok: true };
1431
+ }
1432
+ const script = document.createElement('script');
1433
+ if (opts.url)
1434
+ script.src = String(opts.url);
1435
+ else
1436
+ script.textContent = String(opts.content ?? '');
1437
+ if (opts.type)
1438
+ script.type = String(opts.type);
1439
+ document.head.appendChild(script);
1440
+ return { ok: true };
1441
+ };
1442
+ const scroll = (opts) => {
1443
+ if (opts.selector) {
1444
+ const el = resolveChain(parseSelector(String(opts.selector)))[0];
1445
+ if (!el)
1446
+ return { ok: false, error: 'scroll target not found' };
1447
+ el.scrollIntoView({ block: opts.block ?? 'center' });
1448
+ }
1449
+ else if (opts.to === 'bottom')
1450
+ window.scrollTo(0, document.documentElement.scrollHeight);
1451
+ else if (opts.to === 'top')
1452
+ window.scrollTo(0, 0);
1453
+ else
1454
+ window.scrollTo(Number(opts.x ?? scrollX), Number(opts.y ?? scrollY));
1455
+ return { ok: true, scrollY, scrollHeight: document.documentElement.scrollHeight, innerHeight };
1456
+ };
1457
+ const call = async (req) => {
1458
+ const fn = String(req.fn);
1459
+ const a = req.args ?? [];
1460
+ const deadline = Date.now() + Number(req.timeout ?? 10000);
1461
+ switch (fn) {
1462
+ case 'parse':
1463
+ return parseSelector(String(a[0]));
1464
+ case 'act':
1465
+ return act(a[0], String(a[1]), a[2] ?? {}, deadline);
1466
+ case 'read':
1467
+ return read(a[0], String(a[1]), a[2] ?? [], deadline);
1468
+ case 'readAll':
1469
+ return readAll(a[0], String(a[1]));
1470
+ case 'evalOn':
1471
+ return evalOn(a[0], String(a[1]), a[2], deadline);
1472
+ case 'evalAll':
1473
+ return evalAll(a[0], String(a[1]), a[2]);
1474
+ case 'waitFor':
1475
+ return waitFor(a[0], String(a[1]), deadline);
1476
+ case 'probe':
1477
+ return probe(a[0], String(a[1]), a[2] ?? {});
1478
+ case 'describe':
1479
+ return describe(a[0]);
1480
+ case 'info':
1481
+ return info();
1482
+ case 'mouse':
1483
+ return mouse(String(a[0]), Number(a[1]), Number(a[2]), a[3] ?? {});
1484
+ case 'keyboard':
1485
+ return keyboard(String(a[0]), String(a[1] ?? ''));
1486
+ case 'fetch':
1487
+ return doFetch(String(a[0]), a[1] ?? {});
1488
+ case 'addTag':
1489
+ return addTag(String(a[0]), a[1] ?? {});
1490
+ case 'scroll':
1491
+ return scroll(a[0] ?? {});
1492
+ case 'eval':
1493
+ return { ok: true, value: await runFn(String(a[0]))(a[1]) };
1494
+ default:
1495
+ return { ok: false, error: 'unknown runtime call: ' + fn };
1496
+ }
1497
+ };
1498
+ g.__molE2E = { version: VERSION, call };
1499
+ return { version: VERSION };
1500
+ }
1501
+ //# sourceMappingURL=runtime.js.map