@webhands/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +20 -4
  2. package/dist/errors.d.ts +92 -1
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +100 -0
  5. package/dist/errors.js.map +1 -1
  6. package/dist/hand-host.d.ts +198 -5
  7. package/dist/hand-host.d.ts.map +1 -1
  8. package/dist/hand-host.js +664 -21
  9. package/dist/hand-host.js.map +1 -1
  10. package/dist/index.d.ts +4 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/playwright-attach-transport.d.ts +8 -1
  15. package/dist/playwright-attach-transport.d.ts.map +1 -1
  16. package/dist/playwright-attach-transport.js +19 -4
  17. package/dist/playwright-attach-transport.js.map +1 -1
  18. package/dist/playwright-launch-transport.d.ts.map +1 -1
  19. package/dist/playwright-launch-transport.js +13 -4
  20. package/dist/playwright-launch-transport.js.map +1 -1
  21. package/dist/profile-location.d.ts +19 -0
  22. package/dist/profile-location.d.ts.map +1 -1
  23. package/dist/profile-location.js +21 -0
  24. package/dist/profile-location.js.map +1 -1
  25. package/dist/seam.d.ts +501 -7
  26. package/dist/seam.d.ts.map +1 -1
  27. package/dist/seam.js +31 -0
  28. package/dist/seam.js.map +1 -1
  29. package/dist/session-rpc.d.ts +63 -1
  30. package/dist/session-rpc.d.ts.map +1 -1
  31. package/dist/session-rpc.js +174 -11
  32. package/dist/session-rpc.js.map +1 -1
  33. package/dist/stub-transport.d.ts.map +1 -1
  34. package/dist/stub-transport.js +74 -6
  35. package/dist/stub-transport.js.map +1 -1
  36. package/dist/test-fixtures/fixture-pages.d.ts.map +1 -1
  37. package/dist/test-fixtures/fixture-pages.js +994 -0
  38. package/dist/test-fixtures/fixture-pages.js.map +1 -1
  39. package/dist/test-fixtures/fixture-server.d.ts.map +1 -1
  40. package/dist/test-fixtures/fixture-server.js +33 -3
  41. package/dist/test-fixtures/fixture-server.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/errors.ts +134 -1
  44. package/src/hand-host.ts +797 -21
  45. package/src/index.ts +20 -1
  46. package/src/playwright-attach-transport.ts +25 -3
  47. package/src/playwright-launch-transport.ts +13 -2
  48. package/src/profile-location.ts +25 -0
  49. package/src/seam.ts +535 -7
  50. package/src/session-rpc.ts +276 -14
  51. package/src/stub-transport.ts +83 -6
  52. package/src/test-fixtures/fixture-pages.ts +1010 -0
  53. package/src/test-fixtures/fixture-server.ts +32 -3
@@ -97,6 +97,35 @@ const CLICK_TYPE = `<!doctype html>
97
97
  </body>
98
98
  </html>
99
99
  `;
100
+ /**
101
+ * A page whose submit button triggers a SLOW navigation (PRD story 8, the
102
+ * "real submit button" path). Clicking `#slow-submit` navigates to
103
+ * `index.html?delayMs=1500`; the fixture server holds that response back ~1.5s,
104
+ * so the navigation the click schedules takes far longer than the verb's short
105
+ * actionability budget. The element is a perfectly normal, visible, actionable
106
+ * button, so `click` must perform the click and NOT mistake the slow post-click
107
+ * navigation for a non-actionable element (which would wrongly route it to the
108
+ * dispatch escape and re-click a page already navigating away). The test
109
+ * asserts the effect: after the click, the reader ends up on `index.html`.
110
+ */
111
+ const SLOW_SUBMIT = `<!doctype html>
112
+ <html lang="en">
113
+ <head>
114
+ <meta charset="utf-8" />
115
+ <title>slow submit fixture</title>
116
+ </head>
117
+ <body>
118
+ <h1 id="heading">Slow Submit Fixture</h1>
119
+ <!-- GET form: the action's query is dropped and rebuilt from the fields,
120
+ so delayMs is carried as a hidden field (a bare action query would be
121
+ lost on submit). -->
122
+ <form action="/index.html" method="get">
123
+ <input type="hidden" name="delayMs" value="1500" />
124
+ <input id="slow-submit" type="submit" value="Continue" />
125
+ </form>
126
+ </body>
127
+ </html>
128
+ `;
100
129
  /**
101
130
  * A page that NAVIGATES itself to `index.html` ~150ms after load, the way a
102
131
  * landing/redirect page bounces to the real destination. `goto` here settles on
@@ -192,13 +221,978 @@ const COOKIES = `<!doctype html>
192
221
  </body>
193
222
  </html>
194
223
  `;
224
+ /**
225
+ * A structured-LIST page for the Tier-1 `query` extraction verb plus the state
226
+ * verbs `exists`/`count`/`isVisible`/`getAttribute` (prd
227
+ * `broaden-agent-verb-surface`, R2). Controlled, deterministic markup the seam
228
+ * tests assert one ROW PER MATCH against, never third-party DOM:
229
+ *
230
+ * - `.result` is a LIST of three rows (a mini shopping result set), each with a
231
+ * distinct `data-asin` attribute, a `.title` text, a `.price` text, and an
232
+ * anchor with an `href` — so a multi-match `query` returns three rows and
233
+ * `--limit` can bound them. The title/price live in CHILD elements so a row's
234
+ * `innerText` property carries the whole composed text (proving `props` reads
235
+ * live runtime state, not markup).
236
+ * - `#optin` is a checkbox whose `checked` ATTRIBUTE is absent in the markup but
237
+ * whose live `checked` PROPERTY is set `true` by script after load — the
238
+ * controlled attrs-vs-props DIVERGENCE: `attrs:['checked']` reads `null`
239
+ * (no markup attribute) while `props:['checked']` reads `true` (live state).
240
+ * It also carries `value="on"` (a present markup attribute) and a runtime
241
+ * `type` property, so an attribute and a property that genuinely differ are
242
+ * both observable on one element.
243
+ * - `#hidden-row` is a present-but-HIDDEN element (`display:none`) carrying a
244
+ * `data-sitekey`, so `pw:['visible']` reads `false` for it (actionability-
245
+ * grade visibility) while a `getAttribute('data-sitekey')` still reads its
246
+ * value, and a VISIBLE `#shown-row` reads `pw:['visible'] === true`.
247
+ * - There is deliberately NO `.absent` element, so a `query`/`count`/`exists`
248
+ * against `.absent` exercises the empty match-set (`[]` / `0` / `false`).
249
+ */
250
+ const QUERY_LIST = `<!doctype html>
251
+ <html lang="en">
252
+ <head>
253
+ <meta charset="utf-8" />
254
+ <title>query list fixture</title>
255
+ </head>
256
+ <body>
257
+ <h1 id="heading">Query List Fixture</h1>
258
+
259
+ <ul id="results">
260
+ <li class="result" data-asin="A001">
261
+ <a class="link" href="/item/A001"
262
+ ><span class="title">Alpha Widget</span></a
263
+ >
264
+ <span class="price">$10.00</span>
265
+ </li>
266
+ <li class="result" data-asin="B002">
267
+ <a class="link" href="/item/B002"
268
+ ><span class="title">Bravo Widget</span></a
269
+ >
270
+ <span class="price">$20.00</span>
271
+ </li>
272
+ <li class="result" data-asin="C003">
273
+ <a class="link" href="/item/C003"
274
+ ><span class="title">Charlie Widget</span></a
275
+ >
276
+ <span class="price">$30.00</span>
277
+ </li>
278
+ </ul>
279
+
280
+ <!-- attrs-vs-props divergence: no \`checked\` attribute in markup, but the
281
+ live \`checked\` property is set true after load. -->
282
+ <input id="optin" type="checkbox" value="on" />
283
+
284
+ <!-- a present-but-hidden element carrying a readable attribute -->
285
+ <div
286
+ id="hidden-row"
287
+ class="sitekey"
288
+ data-sitekey="sk-hidden-123"
289
+ style="display: none"
290
+ ></div>
291
+ <div id="shown-row" class="sitekey" data-sitekey="sk-shown-456">visible</div>
292
+
293
+ <script>
294
+ // Toggle the live property WITHOUT touching the markup attribute, so
295
+ // attrs:['checked'] (null) and props:['checked'] (true) genuinely differ.
296
+ document.getElementById('optin').checked = true;
297
+ </script>
298
+ </body>
299
+ </html>
300
+ `;
301
+ /**
302
+ * A page exercising the Tier-2 `press` verb (prd `broaden-agent-verb-surface`,
303
+ * story 8). It RECORDS keyboard events deterministically so a test asserts the
304
+ * verb fired the right key, not merely that it did not throw:
305
+ *
306
+ * - `#focus-input` is a text input. A `keydown` listener appends each event's
307
+ * `key` (and a `+` for each held modifier) to `#keylog`, so a single key, a
308
+ * named key (Enter/ArrowLeft), and a chord (`Control+a`) are all observable.
309
+ * - `#counter` is driven by ArrowUp/ArrowDown on a SECOND input (`#game`),
310
+ * modelling a game's keyboard control: ArrowUp increments, ArrowDown
311
+ * decrements, so `press('ArrowUp')` at that locator moves the counter.
312
+ * - The page focuses `#focus-input` on load, so a `press(key)` with NO locator
313
+ * (the focused-element form) lands there — the test asserts the focused-element
314
+ * path AND the at-a-locator path against the SAME recorder.
315
+ */
316
+ const KEYBOARD = `<!doctype html>
317
+ <html lang="en">
318
+ <head>
319
+ <meta charset="utf-8" />
320
+ <title>keyboard fixture</title>
321
+ </head>
322
+ <body>
323
+ <h1 id="heading">Keyboard Fixture</h1>
324
+ <input id="focus-input" type="text" aria-label="Focus Input" />
325
+ <pre id="keylog"></pre>
326
+
327
+ <input id="game" type="text" aria-label="Game" />
328
+ <p id="counter">0</p>
329
+
330
+ <script>
331
+ function describe(e) {
332
+ var mods = '';
333
+ if (e.ctrlKey) mods += 'Control+';
334
+ if (e.altKey) mods += 'Alt+';
335
+ if (e.shiftKey) mods += 'Shift+';
336
+ if (e.metaKey) mods += 'Meta+';
337
+ return mods + e.key;
338
+ }
339
+ var MODIFIER_KEYS = {Control: 1, Alt: 1, Shift: 1, Meta: 1};
340
+ var log = document.getElementById('keylog');
341
+ document
342
+ .getElementById('focus-input')
343
+ .addEventListener('keydown', function (e) {
344
+ // A chord (Control+a) fires a bare-modifier keydown (key === 'Control')
345
+ // before the real key; ignore those so the log records one entry per
346
+ // logical press.
347
+ if (MODIFIER_KEYS[e.key]) return;
348
+ log.textContent += (log.textContent ? ',' : '') + describe(e);
349
+ });
350
+
351
+ var counter = document.getElementById('counter');
352
+ document.getElementById('game').addEventListener('keydown', function (e) {
353
+ if (e.key === 'ArrowUp')
354
+ counter.textContent = String(Number(counter.textContent) + 1);
355
+ if (e.key === 'ArrowDown')
356
+ counter.textContent = String(Number(counter.textContent) - 1);
357
+ });
358
+
359
+ // Focus the recorder input so a press() with NO locator lands here.
360
+ document.getElementById('focus-input').focus();
361
+ </script>
362
+ </body>
363
+ </html>
364
+ `;
365
+ /**
366
+ * A page exercising the Tier-2 `hover` verb (prd `broaden-agent-verb-surface`,
367
+ * story 9). `#menu` reveals a `#menu-item` ONLY while `#menu` is hovered (CSS
368
+ * `:hover`), AND a `mouseenter` listener flips `#hover-state` to `entered`, so
369
+ * the test asserts the hover affordance fired (the item became visible / the
370
+ * state changed), not merely that `hover` did not throw — something `click`
371
+ * could not surface.
372
+ */
373
+ const HOVER = `<!doctype html>
374
+ <html lang="en">
375
+ <head>
376
+ <meta charset="utf-8" />
377
+ <title>hover fixture</title>
378
+ <style>
379
+ #menu-item {
380
+ display: none;
381
+ }
382
+ #menu:hover #menu-item {
383
+ display: block;
384
+ }
385
+ </style>
386
+ </head>
387
+ <body>
388
+ <h1 id="heading">Hover Fixture</h1>
389
+ <div id="menu">
390
+ Menu
391
+ <div id="menu-item">Reveal-on-hover item</div>
392
+ </div>
393
+ <p id="hover-state">idle</p>
394
+ <script>
395
+ document.getElementById('menu').addEventListener('mouseenter', function () {
396
+ document.getElementById('hover-state').textContent = 'entered';
397
+ });
398
+ </script>
399
+ </body>
400
+ </html>
401
+ `;
402
+ /**
403
+ * A page exercising the Tier-2 `select` verb (prd `broaden-agent-verb-surface`,
404
+ * story 10). `#color` is a native `<select>` with three options whose VALUE and
405
+ * LABEL deliberately DIFFER (value `r` / label `Red`, etc.), so a select-by-value
406
+ * and a select-by-label are distinguishable. A `change` listener mirrors the
407
+ * chosen value into `#chosen`, and the test also reads the live `value`
408
+ * property, so the choice is asserted as reflected in the element's STATE.
409
+ */
410
+ const SELECT = `<!doctype html>
411
+ <html lang="en">
412
+ <head>
413
+ <meta charset="utf-8" />
414
+ <title>select fixture</title>
415
+ </head>
416
+ <body>
417
+ <h1 id="heading">Select Fixture</h1>
418
+ <select id="color" aria-label="Color">
419
+ <option value="r">Red</option>
420
+ <option value="g">Green</option>
421
+ <option value="b">Blue</option>
422
+ </select>
423
+ <p id="chosen">r</p>
424
+ <script>
425
+ var sel = document.getElementById('color');
426
+ sel.addEventListener('change', function () {
427
+ document.getElementById('chosen').textContent = sel.value;
428
+ });
429
+ </script>
430
+ </body>
431
+ </html>
432
+ `;
433
+ /**
434
+ * A page exercising the Tier-2 `scroll` verb (prd `broaden-agent-verb-surface`,
435
+ * story 11). The body is much taller than the viewport, with `#far-target` near
436
+ * the BOTTOM (off-viewport at load), so:
437
+ *
438
+ * - `scroll --to (#far-target)` brings it into view (its `pw:['visible']` /
439
+ * `scrollY` change is observable).
440
+ * - `scroll --by 0,400` scrolls the page DOWN by 400px, so `window.scrollY`
441
+ * moves by the given amount.
442
+ *
443
+ * A tall `#spacer` provides the scroll distance; `#far-target` sits after it.
444
+ */
445
+ const SCROLL = `<!doctype html>
446
+ <html lang="en">
447
+ <head>
448
+ <meta charset="utf-8" />
449
+ <title>scroll fixture</title>
450
+ </head>
451
+ <body>
452
+ <h1 id="heading">Scroll Fixture</h1>
453
+ <div id="spacer" style="height: 4000px">spacer</div>
454
+ <div id="far-target">Far target at the bottom</div>
455
+ </body>
456
+ </html>
457
+ `;
458
+ /**
459
+ * A page exercising the Tier-2 `drag` verb (prd `broaden-agent-verb-surface`,
460
+ * story 12). `#drag-source` is a draggable element and `#drop-target` is a drop
461
+ * zone wired with the HTML5 drag-and-drop events: on `drop`, the handler moves
462
+ * the source INTO the target and flips `#drop-state` to `dropped`, so the test
463
+ * asserts the drop handler RAN (the DOM order / state changed), not merely that
464
+ * `drag` did not throw.
465
+ */
466
+ const DRAG = `<!doctype html>
467
+ <html lang="en">
468
+ <head>
469
+ <meta charset="utf-8" />
470
+ <title>drag fixture</title>
471
+ <style>
472
+ #drag-source,
473
+ #drop-target {
474
+ width: 120px;
475
+ height: 120px;
476
+ margin: 10px;
477
+ }
478
+ #drag-source {
479
+ background: #cde;
480
+ }
481
+ #drop-target {
482
+ background: #edc;
483
+ }
484
+ </style>
485
+ </head>
486
+ <body>
487
+ <h1 id="heading">Drag Fixture</h1>
488
+ <div id="drag-source" draggable="true">Drag me</div>
489
+ <div id="drop-target">Drop here</div>
490
+ <p id="drop-state">idle</p>
491
+ <script>
492
+ var source = document.getElementById('drag-source');
493
+ var target = document.getElementById('drop-target');
494
+ source.addEventListener('dragstart', function (e) {
495
+ e.dataTransfer.setData('text/plain', 'drag-source');
496
+ });
497
+ target.addEventListener('dragover', function (e) {
498
+ e.preventDefault();
499
+ });
500
+ target.addEventListener('drop', function (e) {
501
+ e.preventDefault();
502
+ target.appendChild(source);
503
+ document.getElementById('drop-state').textContent = 'dropped';
504
+ });
505
+ </script>
506
+ </body>
507
+ </html>
508
+ `;
509
+ /**
510
+ * The SAME-ORIGIN child frame embedded by {@link FRAME_PARENT} (Tier-3
511
+ * frame-scoped `eval`, prd `broaden-agent-verb-surface`, story 13). It carries
512
+ * controlled, deterministic state the parent's top document CANNOT see, so a
513
+ * frame-scoped `eval` is proved to actually land IN the child:
514
+ *
515
+ * - `#child-marker` holds a text only present in the child document, so
516
+ * `eval --frame` reading it proves the expression ran in the child (the top
517
+ * document's `document.getElementById('child-marker')` is `null`).
518
+ * - `window.__childValue` is a runtime-only JS value the top frame's page world
519
+ * cannot reach, the "read a runtime-only value" case.
520
+ * - `window.fireCallback()` flips `#callback-state` to `fired` and sets
521
+ * `window.__callbackFired`, modelling a captcha `data-callback`: a frame-scoped
522
+ * `eval` firing it has an OBSERVABLE effect inside the child (the
523
+ * backward-compatible top-frame `eval` cannot reach it).
524
+ */
525
+ const FRAME_CHILD = `<!doctype html>
526
+ <html lang="en">
527
+ <head>
528
+ <meta charset="utf-8" />
529
+ <title>frame child fixture</title>
530
+ </head>
531
+ <body>
532
+ <h1 id="child-heading">Frame Child</h1>
533
+ <p id="child-marker">child-only-value</p>
534
+ <p id="callback-state">idle</p>
535
+ <script>
536
+ window.__childValue = 'runtime-only-child-value';
537
+ window.__callbackFired = false;
538
+ window.fireCallback = function () {
539
+ window.__callbackFired = true;
540
+ document.getElementById('callback-state').textContent = 'fired';
541
+ return 'callback-result';
542
+ };
543
+ </script>
544
+ </body>
545
+ </html>
546
+ `;
547
+ /**
548
+ * The PARENT page for the Tier-3 frame-scoped `eval` (prd
549
+ * `broaden-agent-verb-surface`, story 13). It embeds {@link FRAME_CHILD} as a
550
+ * SAME-ORIGIN child frame (`#main-iframe`, relative `src`), mimicking the
551
+ * Imperva `#main-iframe` structure the idea names. The top document carries a
552
+ * DIFFERENT `#child-marker`-less state so a test can tell the top frame from the
553
+ * child frame:
554
+ *
555
+ * - `#top-marker` holds a top-document-only text; there is deliberately no
556
+ * `#child-marker` in the top document, so `eval` with no frame reading
557
+ * `document.getElementById('child-marker')` is `null` (backward-compatible
558
+ * top-frame default) while `eval --frame '#main-iframe'` reading it is the
559
+ * child value.
560
+ * - `#cross-iframe` is an iframe whose `src` is set by the TEST to a SECOND
561
+ * fixture server (a different port == a different origin), so a frame-scoped
562
+ * `eval` against it must fail LOUD with the cross-origin typed error. It is
563
+ * left blank here and pointed cross-origin by the test (the fixture server
564
+ * serves one origin, so the cross-origin half is wired in the test).
565
+ */
566
+ const FRAME_PARENT = `<!doctype html>
567
+ <html lang="en">
568
+ <head>
569
+ <meta charset="utf-8" />
570
+ <title>frame parent fixture</title>
571
+ </head>
572
+ <body>
573
+ <h1 id="heading">Frame Parent</h1>
574
+ <p id="top-marker">top-only-value</p>
575
+ <iframe
576
+ id="main-iframe"
577
+ name="main-iframe"
578
+ src="/frame-child.html"
579
+ width="320"
580
+ height="200"
581
+ ></iframe>
582
+ </body>
583
+ </html>
584
+ `;
585
+ /**
586
+ * A page exercising the Tier-4 coordinate `mouse` verb (prd
587
+ * `broaden-agent-verb-surface`, R3, stories 17-18). It records WHERE a
588
+ * coordinate click/press landed so a test asserts the verb acted at the
589
+ * intended VIEWPORT pixel, and proves the VIEWPORT-screenshot <-> `mouse`
590
+ * coordinate contract end to end:
591
+ *
592
+ * - `#hit-target` is an absolutely-positioned box at a KNOWN viewport position
593
+ * and size. Its `click` handler flips `#hit-state` to `hit` and records the
594
+ * event's `clientX,clientY` into `#hit-coords`, so a `mouse({action:'click',
595
+ * x, y})` at a coordinate OVER the box runs the box's handler (assert the
596
+ * effect), and a coordinate OUTSIDE it does NOT (the look-then-click loop is
597
+ * only meaningful if the click lands where the agent aimed).
598
+ * - `#move-target` flips `#move-state` to `moved` on `mouseenter`, so a bare
599
+ * `mouse({action:'move', x, y})` (no button) is observable too.
600
+ * - `#down-up-target` records `mousedown`/`mouseup` so the press/release halves
601
+ * are exercised.
602
+ *
603
+ * The page pins `margin:0` and a deterministic layout so the box's viewport
604
+ * coordinates are stable; the test reads the box's real `getBoundingClientRect`
605
+ * (via `eval`) and clicks its centre, so it never hard-codes a pixel that a
606
+ * platform default could shift.
607
+ */
608
+ const COORDINATE = `<!doctype html>
609
+ <html lang="en">
610
+ <head>
611
+ <meta charset="utf-8" />
612
+ <title>coordinate fixture</title>
613
+ <style>
614
+ html,
615
+ body {
616
+ margin: 0;
617
+ padding: 0;
618
+ }
619
+ #hit-target {
620
+ position: absolute;
621
+ left: 100px;
622
+ top: 80px;
623
+ width: 120px;
624
+ height: 90px;
625
+ background: #4a7;
626
+ }
627
+ #move-target {
628
+ position: absolute;
629
+ left: 300px;
630
+ top: 80px;
631
+ width: 80px;
632
+ height: 80px;
633
+ background: #74a;
634
+ }
635
+ #down-up-target {
636
+ position: absolute;
637
+ left: 100px;
638
+ top: 220px;
639
+ width: 120px;
640
+ height: 90px;
641
+ background: #a74;
642
+ }
643
+ </style>
644
+ </head>
645
+ <body>
646
+ <div id="hit-target"></div>
647
+ <div id="move-target"></div>
648
+ <div id="down-up-target"></div>
649
+ <p id="hit-state">untouched</p>
650
+ <p id="hit-coords"></p>
651
+ <p id="move-state">idle</p>
652
+ <p id="down-up-state">idle</p>
653
+ <script>
654
+ var hit = document.getElementById('hit-target');
655
+ hit.addEventListener('click', function (e) {
656
+ document.getElementById('hit-state').textContent = 'hit';
657
+ document.getElementById('hit-coords').textContent =
658
+ Math.round(e.clientX) + ',' + Math.round(e.clientY);
659
+ });
660
+ document
661
+ .getElementById('move-target')
662
+ .addEventListener('mouseenter', function () {
663
+ document.getElementById('move-state').textContent = 'moved';
664
+ });
665
+ var du = document.getElementById('down-up-target');
666
+ du.addEventListener('mousedown', function () {
667
+ document.getElementById('down-up-state').textContent = 'down';
668
+ });
669
+ du.addEventListener('mouseup', function () {
670
+ var el = document.getElementById('down-up-state');
671
+ el.textContent = el.textContent === 'down' ? 'down-up' : 'up-only';
672
+ });
673
+ </script>
674
+ </body>
675
+ </html>
676
+ `;
677
+ /**
678
+ * A page exercising the Tier-4 `screenshot` verb's ELEMENT scope (prd
679
+ * `broaden-agent-verb-surface`, R3, story 17). `#widget` is a fixed-size,
680
+ * solid-colour box (a stand-in for a captcha widget) at a known size, so an
681
+ * element-clipped screenshot of it produces a PNG whose dimensions match the
682
+ * widget, not the viewport. A taller body makes a FULL-PAGE shot strictly
683
+ * taller than a VIEWPORT shot, so the three scopes are distinguishable by the
684
+ * returned dimensions.
685
+ */
686
+ const SCREENSHOT = `<!doctype html>
687
+ <html lang="en">
688
+ <head>
689
+ <meta charset="utf-8" />
690
+ <title>screenshot fixture</title>
691
+ <style>
692
+ html,
693
+ body {
694
+ margin: 0;
695
+ padding: 0;
696
+ }
697
+ #widget {
698
+ width: 200px;
699
+ height: 150px;
700
+ background: #2a6;
701
+ }
702
+ #tall {
703
+ height: 3000px;
704
+ background: linear-gradient(#fff, #036);
705
+ }
706
+ </style>
707
+ </head>
708
+ <body>
709
+ <div id="widget">widget</div>
710
+ <div id="tall">tall body for full-page</div>
711
+ </body>
712
+ </html>
713
+ `;
714
+ /**
715
+ * The SAME-ORIGIN TOKEN-HARVEST captcha child frame (prd
716
+ * `broaden-agent-verb-surface`, stories 6-7; the
717
+ * `frame-aware-query-token-harvest-captcha-proof` task). It is the child
718
+ * embedded by {@link TOKEN_CAPTCHA_PARENT} as `#main-iframe`, and it carries the
719
+ * whole token-harvest widget the way a real hCaptcha/Imperva `#main-iframe`
720
+ * does (per `work/notes/findings/click-and-type-already-frame-scoped-via-
721
+ * framelocator.md` and `…playwright-cross-origin-frame-captcha-mechanics.md`):
722
+ *
723
+ * - `div.h-captcha[data-sitekey]` is the PAGE-READABLE sitekey an agent reads
724
+ * through a `frameLocator('#main-iframe').locator('.h-captcha')` +
725
+ * `attrs:['data-sitekey']` (the one frame-aware READ the spike found missing).
726
+ * `data-callback="onCaptchaFinished"` names the page callback, exactly as the
727
+ * real widget wires it.
728
+ * - `textarea#h-captcha-response` is the same-origin RESPONSE SINK an agent
729
+ * `type`s the provider token into (the delivery half the spike proved already
730
+ * works through the `frameLocator` hop).
731
+ * - `window.onCaptchaFinished(token)` is the page callback: it ACCEPTS the token
732
+ * ONLY when it matches what was written into the sink (so a real token must
733
+ * travel sink -> callback, not be conjured), flips `#captcha-state` to
734
+ * `verified`, sets `window.__captchaSolved`, and reveals `#protected-content`
735
+ * (the page ADVANCES). A mismatched/empty token flips it to `rejected` and the
736
+ * page does NOT advance, so the proof asserts a real transition the verbs
737
+ * drove, not a no-op.
738
+ *
739
+ * webhands ships NO solver and NO key: the token is whatever the agent brings
740
+ * (in the proof, a TEST FAKE provider mints one: no real network, no real key).
741
+ */
742
+ const TOKEN_CAPTCHA_CHILD = `<!doctype html>
743
+ <html lang="en">
744
+ <head>
745
+ <meta charset="utf-8" />
746
+ <title>token-harvest captcha frame</title>
747
+ </head>
748
+ <body>
749
+ <h1 id="captcha-heading">Verify you are human</h1>
750
+ <!-- The page-readable sitekey + the callback name, the real widget shape. -->
751
+ <div
752
+ class="h-captcha"
753
+ data-sitekey="sk-token-harvest-abc123"
754
+ data-callback="onCaptchaFinished"
755
+ ></div>
756
+ <!-- The same-origin response sink the token is typed into. -->
757
+ <textarea id="h-captcha-response" name="h-captcha-response"></textarea>
758
+ <p id="captcha-state">pending</p>
759
+ <div id="protected-content" style="display: none">protected resource</div>
760
+ <script>
761
+ window.__captchaSolved = false;
762
+ // The page callback the agent fires AFTER writing the token into the sink.
763
+ // It accepts the token only if it matches the sink value (the token must
764
+ // genuinely travel sink -> callback), modelling the real widget's
765
+ // inject-token-then-fire-callback flow. The page ADVANCES on success.
766
+ window.onCaptchaFinished = function (token) {
767
+ var sink = document.getElementById('h-captcha-response');
768
+ var state = document.getElementById('captcha-state');
769
+ if (token && token === sink.value) {
770
+ window.__captchaSolved = true;
771
+ state.textContent = 'verified';
772
+ document.getElementById('protected-content').style.display = '';
773
+ return 'verified';
774
+ }
775
+ state.textContent = 'rejected';
776
+ return 'rejected';
777
+ };
778
+ </script>
779
+ </body>
780
+ </html>
781
+ `;
782
+ /**
783
+ * The PARENT page for the SAME-ORIGIN TOKEN-HARVEST captcha proof (prd
784
+ * `broaden-agent-verb-surface`, stories 6-7). It embeds {@link
785
+ * TOKEN_CAPTCHA_CHILD} as a SAME-ORIGIN child frame (`#main-iframe`, relative
786
+ * `src`), mirroring the Imperva `#main-iframe` structure the findings give for
787
+ * the reachable token-harvest path: the sitekey, the response sink, and the
788
+ * callback ALL live one same-origin frame down, addressed via a
789
+ * `frameLocator('#main-iframe')` hop in the locator string (no `--frame` flag,
790
+ * R1). The top document carries only a heading, so the captcha widget is
791
+ * genuinely IN the child frame (a top-document `.h-captcha` query is empty).
792
+ */
793
+ const TOKEN_CAPTCHA_PARENT = `<!doctype html>
794
+ <html lang="en">
795
+ <head>
796
+ <meta charset="utf-8" />
797
+ <title>token-harvest captcha host</title>
798
+ </head>
799
+ <body>
800
+ <h1 id="host-heading">Protected page</h1>
801
+ <iframe
802
+ id="main-iframe"
803
+ name="main-iframe"
804
+ src="/token-captcha-child.html"
805
+ width="320"
806
+ height="240"
807
+ ></iframe>
808
+ </body>
809
+ </html>
810
+ `;
811
+ /**
812
+ * The Tier-4 CROSS-ORIGIN nested-frame fixture (prd
813
+ * `broaden-agent-verb-surface`, R3, stories 17-19), mirroring the synthetic
814
+ * doubly-nested cross-origin tree the finding
815
+ * `playwright-cross-origin-frame-captcha-mechanics.md` spike-verified:
816
+ *
817
+ * ```
818
+ * top (host)
819
+ * └─ iframe#child-frame ← CROSS-ORIGIN (a second fixture-server origin)
820
+ * └─ iframe#child-frame ← CROSS-ORIGIN AGAIN (a third origin)
821
+ * └─ the tile grid + token sink
822
+ * ```
823
+ *
824
+ * Because the fixture server serves IDENTICAL pages on every port, the test
825
+ * composes the three origins by passing each child frame's absolute URL in a
826
+ * `?child=<url>` query param; this page reads its OWN query string and points
827
+ * `#child-frame` at the given child URL. Every level uses the SAME iframe id
828
+ * (`#child-frame`), so a `frameLocator('#child-frame').frameLocator('#child-frame')`
829
+ * chain reads two cross-origin boundaries deep. With no `child` param the frame
830
+ * is removed, so the SAME page is the host, the WAF level, and the deepest
831
+ * captcha level depending on the URL the test embeds. The deepest level (no
832
+ * `child`) carries the controlled tile/token state the READ asserts, and a
833
+ * `#tile-1` widget an element-clipped screenshot targets.
834
+ */
835
+ const NESTED_FRAME = `<!doctype html>
836
+ <html lang="en">
837
+ <head>
838
+ <meta charset="utf-8" />
839
+ <title>nested frame fixture</title>
840
+ <style>
841
+ #tile-1 {
842
+ width: 90px;
843
+ height: 90px;
844
+ background: #c33;
845
+ }
846
+ </style>
847
+ </head>
848
+ <body>
849
+ <h1 id="level-heading">nested frame level</h1>
850
+ <!-- The tile grid + token sink, present at EVERY level but meaningful at
851
+ the deepest (captcha) one the test reads into. -->
852
+ <div id="challenge" class="challenge">
853
+ <div id="tile-1" class="tile" data-tile="1">tile 1</div>
854
+ <div class="tile" data-tile="2">tile 2</div>
855
+ <div class="tile" data-tile="3">tile 3</div>
856
+ <textarea id="h-captcha-response" name="h-captcha-response">deep-token-123</textarea>
857
+ </div>
858
+ <iframe id="child-frame" name="child-frame" width="360" height="300"></iframe>
859
+ <script>
860
+ // Read this level's OWN ?child=<url> and point the nested frame at it, so
861
+ // the test composes the cross-origin tree across distinct origins. Every
862
+ // level uses the SAME iframe id (#child-frame) so a
863
+ // frameLocator('#child-frame').frameLocator('#child-frame') chain reads two
864
+ // cross-origin boundaries deep.
865
+ var params = new URLSearchParams(window.location.search);
866
+ var child = params.get('child');
867
+ var frame = document.getElementById('child-frame');
868
+ if (child) {
869
+ frame.src = child;
870
+ } else if (frame && frame.parentNode) {
871
+ // Deepest level: no child, remove the empty trailing iframe so the tree
872
+ // ends cleanly at the tile grid.
873
+ frame.parentNode.removeChild(frame);
874
+ }
875
+ </script>
876
+ </body>
877
+ </html>
878
+ `;
879
+ /**
880
+ * The Tier-4 VISION/TILE captcha fixture (prd `broaden-agent-verb-surface`, R3,
881
+ * story 17; the `vision-tile-captcha-end-to-end-proof` task). It mirrors the
882
+ * doubly-nested CROSS-ORIGIN tree the finding
883
+ * `playwright-cross-origin-frame-captcha-mechanics.md` spike-verified, but unlike
884
+ * the read-only {@link NESTED_FRAME} fixture its deepest level is an INTERACTIVE
885
+ * tile challenge that ADVANCES when the right tiles are clicked:
886
+ *
887
+ * ```
888
+ * top (host)
889
+ * └─ iframe#child-frame ← CROSS-ORIGIN (a second fixture-server origin, the WAF level)
890
+ * └─ iframe#child-frame ← CROSS-ORIGIN AGAIN (a third origin, the captcha level)
891
+ * └─ the interactive 3x3 tile grid + challenge state
892
+ * ```
893
+ *
894
+ * Cross-origin composition is the SAME `?child=<url>` mechanism
895
+ * {@link NESTED_FRAME} uses (the fixture server serves identical pages on every
896
+ * port, so the test threads three distinct origins by passing each child's
897
+ * absolute URL). Every level shares the iframe id `#child-frame`, so a
898
+ * `frameLocator('#child-frame').frameLocator('#child-frame')` chain reaches the
899
+ * deepest (captcha) level two cross-origin boundaries deep.
900
+ *
901
+ * The deepest level (no `?child`) is the challenge. It is the part that makes
902
+ * this a vision/tile PROOF rather than a static read:
903
+ *
904
+ * - A 3x3 grid of `.tile` cells, each absolutely positioned at a KNOWN, distinct
905
+ * spot (so an element-clipped/viewport screenshot of the widget shows them at
906
+ * stable coordinates, and a `bbox` read gives each tile's VIEWPORT-relative
907
+ * centre — the coordinate<->screenshot bridge). Each tile carries `data-tile`
908
+ * (its index `0..8`) and `data-target` (`"1"` for the tiles the challenge wants
909
+ * selected, `"0"` otherwise). The TARGET set is fixed in the markup, so the
910
+ * test's selection is DETERMINISTIC (it stands in for a vision model's
911
+ * decision; webhands ships no solver).
912
+ * - Clicking a tile (a real coordinate `mouse` click runs the tile's own click
913
+ * handler) toggles its `selected` state and appends its index to `#selection`.
914
+ * Clicking a tile that is NOT a target marks the attempt `wrong` (so a sloppy
915
+ * coordinate that hit the neighbouring tile is OBSERVABLE, not silently
916
+ * tolerated — the coordinate contract is load-bearing).
917
+ * - `#submit` checks the selection against the target set; when EXACTLY the
918
+ * target tiles are selected it flips `#challenge-state` to `solved` and sets
919
+ * `window.__solved = true` (the challenge ADVANCES). Until then it reads
920
+ * `pending` (or `wrong` after a mis-click), so the proof asserts a real state
921
+ * transition the verbs drove, not a no-op.
922
+ *
923
+ * All challenge state lives in the deepest frame's DOM, so the proof reads it
924
+ * through the SAME cross-origin `frameLocator` chain the clicks act through.
925
+ */
926
+ const TILE_CAPTCHA = `<!doctype html>
927
+ <html lang="en">
928
+ <head>
929
+ <meta charset="utf-8" />
930
+ <title>tile captcha fixture</title>
931
+ <style>
932
+ html,
933
+ body {
934
+ margin: 0;
935
+ padding: 0;
936
+ }
937
+ /* #grid is the positioned WIDGET (a known size), so an element-clipped
938
+ screenshot of it clips the tile grid, and the absolutely-positioned
939
+ tiles lay out relative to IT at stable, distinct coordinates. */
940
+ #grid {
941
+ position: relative;
942
+ width: 300px;
943
+ height: 300px;
944
+ }
945
+ .tile {
946
+ position: absolute;
947
+ width: 90px;
948
+ height: 90px;
949
+ background: #cdd;
950
+ box-sizing: border-box;
951
+ border: 1px solid #899;
952
+ }
953
+ .tile.selected {
954
+ background: #3a7;
955
+ }
956
+ </style>
957
+ </head>
958
+ <body>
959
+ <!-- The child iframe comes FIRST and is pinned to the top-left, so on a
960
+ PARENT level (one with ?child) the nested tree starts at the viewport
961
+ origin and the deepest grid stays IN the viewport — the coordinate<->
962
+ screenshot contract only holds for on-screen tiles. The challenge UI is
963
+ hidden on parent levels (it is meaningful only at the deepest level). -->
964
+ <iframe id="child-frame" name="child-frame" width="380" height="560"></iframe>
965
+ <!-- The interactive challenge, present at EVERY level (so the chain is
966
+ uniform) but meaningful only at the deepest (no-child) one the proof
967
+ reads + clicks into. -->
968
+ <div id="challenge" class="challenge">
969
+ <p id="prompt">Select the marked tiles</p>
970
+ <p id="challenge-state">pending</p>
971
+ <p id="selection"></p>
972
+ <div id="grid"></div>
973
+ <button id="submit" type="button">Verify</button>
974
+ </div>
975
+ <script>
976
+ // The fixed TARGET set (the tiles the challenge wants). Deterministic, so
977
+ // the proof's selection stands in for a vision model with zero solver code.
978
+ var TARGETS = [0, 4, 8];
979
+ var GRID = document.getElementById('grid');
980
+ var selection = [];
981
+ for (var i = 0; i < 9; i++) {
982
+ (function (index) {
983
+ var tile = document.createElement('div');
984
+ tile.className = 'tile';
985
+ tile.id = 'tile-' + index;
986
+ tile.setAttribute('data-tile', String(index));
987
+ tile.setAttribute(
988
+ 'data-target',
989
+ TARGETS.indexOf(index) >= 0 ? '1' : '0',
990
+ );
991
+ tile.style.left = (index % 3) * 100 + 'px';
992
+ tile.style.top = Math.floor(index / 3) * 100 + 'px';
993
+ tile.textContent = String(index);
994
+ tile.addEventListener('click', function () {
995
+ var at = selection.indexOf(index);
996
+ if (at >= 0) {
997
+ selection.splice(at, 1);
998
+ tile.classList.remove('selected');
999
+ } else {
1000
+ selection.push(index);
1001
+ tile.classList.add('selected');
1002
+ }
1003
+ document.getElementById('selection').textContent = selection
1004
+ .slice()
1005
+ .sort(function (a, b) {
1006
+ return a - b;
1007
+ })
1008
+ .join(',');
1009
+ });
1010
+ GRID.appendChild(tile);
1011
+ })(i);
1012
+ }
1013
+
1014
+ window.__solved = false;
1015
+ document.getElementById('submit').addEventListener('click', function () {
1016
+ var chosen = selection.slice().sort(function (a, b) {
1017
+ return a - b;
1018
+ });
1019
+ var want = TARGETS.slice().sort(function (a, b) {
1020
+ return a - b;
1021
+ });
1022
+ var ok =
1023
+ chosen.length === want.length &&
1024
+ chosen.every(function (v, i) {
1025
+ return v === want[i];
1026
+ });
1027
+ var state = document.getElementById('challenge-state');
1028
+ if (ok) {
1029
+ window.__solved = true;
1030
+ state.textContent = 'solved';
1031
+ } else {
1032
+ state.textContent = 'wrong';
1033
+ }
1034
+ });
1035
+
1036
+ // Cross-origin composition, mirroring NESTED_FRAME: read this level's
1037
+ // ?child=<url> and point #child-frame at it. On a PARENT level we HIDE this
1038
+ // level's own challenge UI and pin the iframe to the top, so the only thing
1039
+ // occupying the viewport is the nested tree — keeping the DEEPEST grid on
1040
+ // screen (its tiles' viewport coordinates are what the mouse verb clicks).
1041
+ // The
1042
+ // deepest level (no child) removes the empty trailing iframe and shows the
1043
+ // challenge.
1044
+ var params = new URLSearchParams(window.location.search);
1045
+ var child = params.get('child');
1046
+ var frame = document.getElementById('child-frame');
1047
+ if (child) {
1048
+ frame.src = child;
1049
+ document.getElementById('challenge').style.display = 'none';
1050
+ } else if (frame && frame.parentNode) {
1051
+ frame.parentNode.removeChild(frame);
1052
+ }
1053
+ </script>
1054
+ </body>
1055
+ </html>
1056
+ `;
1057
+ /**
1058
+ * A page for the durable `query` `ref` (prd `broaden-agent-verb-surface`, R4;
1059
+ * task `query-durable-ref-handle`). A results list whose rows exercise the
1060
+ * REF PREFERENCE LADDER and the loud-stale contract WITHOUT any framework, by
1061
+ * driving the exact reconciliation shapes the React/Svelte spike measured via
1062
+ * plain DOM mutations:
1063
+ *
1064
+ * - Each `.result` has a per-row buy button. Charlie's button carries a STABLE
1065
+ * UNIQUE id (`#buy-charlie`) and a `data-testid` (ladder step 1 reuse). Other
1066
+ * buy buttons are ANONYMOUS (no id/testid/name), so a ref for them must MINT
1067
+ * (ladder step 2).
1068
+ * - `#dupe-a` / `#dupe-b` rows both carry `data-testid="dupe"` (NON-unique), so a
1069
+ * ref must VERIFY uniqueness and fall through (mint) rather than reuse it.
1070
+ * - `window.__prepend()` inserts a NEW row at the TOP (index drift): a positional
1071
+ * `.nth(i)` now points at the wrong row, but a ref rides with its element.
1072
+ * - `window.__replaceCharlie()` REMOVES Charlie's row and inserts a fresh node in
1073
+ * its place (the keyed NODE-REPLACEMENT case): a MINTED ref on it goes stale
1074
+ * (resolve-to-zero), a reused `#buy-charlie` ref also goes stale if the new
1075
+ * node lacks it — both must fail LOUD, never act on the wrong element.
1076
+ * - `window.__cloneAnon(id)` deep-clones an anonymous row carrying a minted attr,
1077
+ * so its ref resolves to MORE THAN ONE element (ambiguous) — also loud-stale.
1078
+ */
1079
+ const REF_LIST = `<!doctype html>
1080
+ <html lang="en">
1081
+ <head>
1082
+ <meta charset="utf-8" />
1083
+ <title>ref list fixture</title>
1084
+ </head>
1085
+ <body>
1086
+ <h1 id="heading">Ref List Fixture</h1>
1087
+ <ul id="results">
1088
+ <li class="result" data-name="Alpha">
1089
+ <span class="title">Alpha Widget</span>
1090
+ <button class="buy">Buy</button>
1091
+ </li>
1092
+ <li class="result" data-name="Bravo">
1093
+ <span class="title">Bravo Widget</span>
1094
+ <button class="buy">Buy</button>
1095
+ </li>
1096
+ <li class="result" data-name="Charlie" id="row-charlie">
1097
+ <span class="title">Charlie Widget</span>
1098
+ <button class="buy" id="buy-charlie" data-testid="buy-charlie">
1099
+ Buy
1100
+ </button>
1101
+ </li>
1102
+ <li class="result" data-name="Delta">
1103
+ <span class="title">Delta Widget</span>
1104
+ <button class="buy">Buy</button>
1105
+ </li>
1106
+ </ul>
1107
+
1108
+ <!-- two rows sharing a NON-unique data-testid, to prove uniqueness is
1109
+ VERIFIED and a duplicate falls through the ladder to a mint. -->
1110
+ <ul id="dupes">
1111
+ <li class="dupe-row" data-testid="dupe"><button class="buy">A</button></li>
1112
+ <li class="dupe-row" data-testid="dupe"><button class="buy">B</button></li>
1113
+ </ul>
1114
+
1115
+ <!-- a recorder so a test can prove WHICH buy button was clicked, not merely
1116
+ that the click did not throw. -->
1117
+ <pre id="clicklog"></pre>
1118
+
1119
+ <script>
1120
+ document.addEventListener('click', function (e) {
1121
+ var btn = e.target.closest('button.buy');
1122
+ if (!btn) return;
1123
+ var row = btn.closest('.result, .dupe-row');
1124
+ var label = row ? row.getAttribute('data-name') || row.textContent.trim() : '?';
1125
+ document.getElementById('clicklog').textContent += label + ';';
1126
+ });
1127
+
1128
+ function makeRow(name) {
1129
+ var li = document.createElement('li');
1130
+ li.className = 'result';
1131
+ li.setAttribute('data-name', name);
1132
+ var title = document.createElement('span');
1133
+ title.className = 'title';
1134
+ title.textContent = name + ' Widget';
1135
+ var buy = document.createElement('button');
1136
+ buy.className = 'buy';
1137
+ buy.textContent = 'Buy';
1138
+ li.appendChild(title);
1139
+ li.appendChild(buy);
1140
+ return li;
1141
+ }
1142
+
1143
+ // Index drift: a NEW row at the TOP. Existing element nodes are KEPT (a
1144
+ // reused stable attr AND a minted attr ride with them); only positions
1145
+ // shift, so a positional .nth(i) now picks the wrong row.
1146
+ window.__prepend = function () {
1147
+ var ul = document.getElementById('results');
1148
+ ul.insertBefore(makeRow('Zeta'), ul.firstElementChild);
1149
+ };
1150
+
1151
+ // NODE REPLACEMENT: remove Charlie's row entirely and insert a FRESH node
1152
+ // in its place (no minted attr, no #buy-charlie). A ref minted on the old
1153
+ // Charlie button resolves to ZERO; a reused #buy-charlie ref also to ZERO.
1154
+ window.__replaceCharlie = function () {
1155
+ var old = document.getElementById('row-charlie');
1156
+ var fresh = makeRow('Charlie');
1157
+ old.parentNode.replaceChild(fresh, old);
1158
+ };
1159
+
1160
+ // AMBIGUITY: deep-clone a node carrying a minted attribute so the ref now
1161
+ // resolves to MORE THAN ONE element.
1162
+ window.__cloneByAttr = function (attr, value) {
1163
+ var el = document.querySelector('[' + attr + '="' + value + '"]');
1164
+ if (!el) return false;
1165
+ var host = document.getElementById('results');
1166
+ host.appendChild(el.closest('.result, .dupe-row, button').cloneNode(true));
1167
+ return true;
1168
+ };
1169
+ </script>
1170
+ </body>
1171
+ </html>
1172
+ `;
195
1173
  /** Map of request path (relative to root, no leading slash) to page markup. */
196
1174
  export const FIXTURE_PAGES = {
197
1175
  'index.html': INDEX,
198
1176
  'click-type.html': CLICK_TYPE,
199
1177
  'delayed.html': DELAYED_CONTENT,
1178
+ 'slow-submit.html': SLOW_SUBMIT,
200
1179
  'redirecting.html': REDIRECTING,
201
1180
  'eval.html': EVAL,
202
1181
  'cookies.html': COOKIES,
1182
+ 'query-list.html': QUERY_LIST,
1183
+ 'ref-list.html': REF_LIST,
1184
+ 'keyboard.html': KEYBOARD,
1185
+ 'hover.html': HOVER,
1186
+ 'select.html': SELECT,
1187
+ 'scroll.html': SCROLL,
1188
+ 'drag.html': DRAG,
1189
+ 'frame-parent.html': FRAME_PARENT,
1190
+ 'frame-child.html': FRAME_CHILD,
1191
+ 'coordinate.html': COORDINATE,
1192
+ 'screenshot.html': SCREENSHOT,
1193
+ 'token-captcha-parent.html': TOKEN_CAPTCHA_PARENT,
1194
+ 'token-captcha-child.html': TOKEN_CAPTCHA_CHILD,
1195
+ 'nested-frame.html': NESTED_FRAME,
1196
+ 'tile-captcha.html': TILE_CAPTCHA,
203
1197
  };
204
1198
  //# sourceMappingURL=fixture-pages.js.map