@zappie/verify 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -269,10 +269,69 @@ async function readStore(cwd) {
269
269
  }
270
270
 
271
271
  // src/runner.ts
272
- import { chromium, errors } from "@playwright/test";
272
+ import {
273
+ chromium,
274
+ errors
275
+ } from "@playwright/test";
273
276
  import { mkdir } from "fs/promises";
274
277
  import path3 from "path";
275
278
 
279
+ // src/cpu.ts
280
+ var REFERENCE_CPU_BENCHMARK = 26e3;
281
+ var REFERENCE_CPU_THROTTLING_RATE = 4;
282
+ var MIN_RATE = 1;
283
+ var MAX_RATE = 8;
284
+ var ROUNDS = 5;
285
+ var ROUND_MS = 100;
286
+ function cpuThrottlingRate(benchmark) {
287
+ if (!Number.isFinite(benchmark) || benchmark <= 0) {
288
+ return REFERENCE_CPU_THROTTLING_RATE;
289
+ }
290
+ const rate = REFERENCE_CPU_THROTTLING_RATE * benchmark / REFERENCE_CPU_BENCHMARK;
291
+ return Math.round(Math.min(Math.max(rate, MIN_RATE), MAX_RATE) * 10) / 10;
292
+ }
293
+ function benchmarkPage(page) {
294
+ return page.evaluate(
295
+ ({ rounds, roundMs }) => {
296
+ const work = () => {
297
+ let text = "";
298
+ for (let index = 0; index < 500; index++) {
299
+ text += String.fromCharCode(97 + index % 26);
300
+ }
301
+ const values = Array.from(
302
+ { length: 500 },
303
+ (_, index) => index * 7919 % 500
304
+ );
305
+ values.sort((a, b) => a - b);
306
+ return text.length + values[0];
307
+ };
308
+ const scores = [];
309
+ let sink = 0;
310
+ for (let round = 0; round < rounds; round++) {
311
+ const started = performance.now();
312
+ let iterations = 0;
313
+ while (performance.now() - started < roundMs) {
314
+ sink += work();
315
+ iterations++;
316
+ }
317
+ scores.push(iterations * 1e3 / (performance.now() - started));
318
+ }
319
+ scores.sort((a, b) => a - b);
320
+ window.__verifySink = sink;
321
+ return Math.round(scores[Math.floor(scores.length / 2)]);
322
+ },
323
+ { rounds: ROUNDS, roundMs: ROUND_MS }
324
+ );
325
+ }
326
+ async function benchmarkCpu(browser) {
327
+ const context = await browser.newContext();
328
+ try {
329
+ return await benchmarkPage(await context.newPage());
330
+ } finally {
331
+ await context.close();
332
+ }
333
+ }
334
+
276
335
  // src/checks/common.ts
277
336
  async function open(page, path4 = "/") {
278
337
  const response = await page.goto(path4, { waitUntil: "domcontentloaded" });
@@ -289,7 +348,10 @@ async function debugValue(page, expression) {
289
348
  }
290
349
  var mobile = { width: 390, height: 844 };
291
350
  var desktop = { width: 1440, height: 900 };
292
- async function applyPhoneProfile(page, rate) {
351
+ async function applyPhoneProfile(page, fallbackRate) {
352
+ await page.goto("about:blank");
353
+ const benchmark = await benchmarkPage(page).catch(() => void 0);
354
+ const rate = benchmark === void 0 ? fallbackRate : cpuThrottlingRate(benchmark);
293
355
  const cdp = await page.context().newCDPSession(page);
294
356
  await cdp.send("Network.enable");
295
357
  await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
@@ -301,7 +363,143 @@ async function applyPhoneProfile(page, rate) {
301
363
  connectionType: "cellular3g"
302
364
  });
303
365
  await cdp.send("Emulation.setCPUThrottlingRate", { rate });
304
- return cdp;
366
+ return { cdp, rate };
367
+ }
368
+ var ITEM_DIALOG = '[data-zappie-overlay="item"], [role="dialog"]';
369
+ async function itemOpener(page, itemId) {
370
+ const marked = `[data-zappie-item="${itemId}"]`;
371
+ await page.locator(marked).first().waitFor();
372
+ const own = page.locator(
373
+ [
374
+ `a[href]${marked}`,
375
+ `button${marked}`,
376
+ `[role="link"]${marked}`,
377
+ `[role="button"]${marked}`
378
+ ].join(", ")
379
+ ).filter({ visible: true }).first();
380
+ if (await own.count()) {
381
+ return { opener: own, focusTarget: own };
382
+ }
383
+ const card = page.locator(marked).filter({ visible: true }).first();
384
+ const inner = card.locator('a[href], [role="link"]').first();
385
+ if (await inner.count()) {
386
+ return { opener: inner, focusTarget: card };
387
+ }
388
+ const cell = card.locator("xpath=..");
389
+ return {
390
+ opener: cell.locator('a[href], [role="link"]').first(),
391
+ focusTarget: cell
392
+ };
393
+ }
394
+ async function openItem(page, itemId) {
395
+ const found = await itemOpener(page, itemId);
396
+ await found.opener.click();
397
+ await itemShown(page);
398
+ return found;
399
+ }
400
+ async function itemShown(page) {
401
+ await Promise.race([
402
+ page.waitForURL((url) => /\/item\//.test(url.pathname)),
403
+ page.locator(ITEM_DIALOG).filter({ visible: true }).first().waitFor()
404
+ ]);
405
+ }
406
+ async function closeItem(page) {
407
+ const dialog = page.locator(ITEM_DIALOG).filter({ visible: true }).last();
408
+ if (!await dialog.count()) {
409
+ await page.goBack();
410
+ return;
411
+ }
412
+ const close = dialog.getByRole("button", { name: /close/i }).filter({ visible: true }).first();
413
+ if (await close.count()) {
414
+ await close.click();
415
+ return;
416
+ }
417
+ const outside = page.getByRole("button", { name: /close/i }).filter({ visible: true }).last();
418
+ if (await outside.count()) {
419
+ await outside.click();
420
+ return;
421
+ }
422
+ await page.keyboard.press("Escape");
423
+ }
424
+ function itemOpenerSelector(itemId) {
425
+ const marked = itemId ? `[data-zappie-item="${itemId}"]` : "[data-zappie-item]";
426
+ return [
427
+ `a[href]${marked}`,
428
+ `button${marked}`,
429
+ `[role="link"]${marked}`,
430
+ `[role="button"]${marked}`,
431
+ `${marked} a[href]`
432
+ ].join(", ");
433
+ }
434
+ async function cartTriggerSelector(page) {
435
+ return await page.locator("[data-zappie-cart-trigger]").count() ? "[data-zappie-cart-trigger]" : '[data-zappie-overlay-trigger="bag"]';
436
+ }
437
+ async function cartTrigger(page) {
438
+ return page.locator(await cartTriggerSelector(page)).filter({ visible: true }).first();
439
+ }
440
+ async function tabTo(page, selector, presses = 60) {
441
+ for (let index = 0; index < presses; index++) {
442
+ await page.keyboard.press("Tab");
443
+ const inside = await page.evaluate(
444
+ (value) => Boolean(document.activeElement?.closest(value)),
445
+ selector
446
+ );
447
+ if (inside) {
448
+ return true;
449
+ }
450
+ }
451
+ return false;
452
+ }
453
+ async function signIn(page, site = page.url()) {
454
+ await page.context().addCookies([
455
+ {
456
+ name: "zappie_session",
457
+ value: "signed",
458
+ domain: new URL(site).hostname,
459
+ path: "/"
460
+ }
461
+ ]);
462
+ }
463
+ async function revealField(page, field, input, reach = async () => {
464
+ }) {
465
+ const site = new URL(page.url());
466
+ const opener = page.locator(`[data-zappie-field-open="${field}"]`).filter({ visible: true }).first();
467
+ const shown = input.filter({ visible: true }).first();
468
+ await shown.or(opener).first().waitFor();
469
+ if (await shown.isVisible()) {
470
+ return shown;
471
+ }
472
+ await opener.click();
473
+ if (await sentToSignIn(page, site, shown)) {
474
+ await signIn(page, site.href);
475
+ await open(page, site.pathname);
476
+ await reach();
477
+ await opener.click();
478
+ }
479
+ await shown.waitFor();
480
+ return shown;
481
+ }
482
+ async function sentToSignIn(page, site, shown) {
483
+ const deadline = Date.now() + 1e4;
484
+ while (Date.now() < deadline) {
485
+ const url = new URL(page.url());
486
+ if (url.origin !== site.origin || url.pathname === "/_zappie/login") {
487
+ return true;
488
+ }
489
+ if (await shown.isVisible().catch(() => false)) {
490
+ return false;
491
+ }
492
+ await page.waitForTimeout(100);
493
+ }
494
+ return false;
495
+ }
496
+ async function dialogConfirm(page, confirm) {
497
+ const marked = page.locator(confirm).filter({ visible: true }).first();
498
+ if (await marked.count()) {
499
+ return marked;
500
+ }
501
+ const button = page.locator('[role="dialog"]').filter({ visible: true }).last().locator('footer button:enabled, button[type="submit"]:enabled').filter({ visible: true }).last();
502
+ return await button.count() ? button : void 0;
305
503
  }
306
504
 
307
505
  // src/checks/v01-routes.ts
@@ -373,6 +571,7 @@ var v02 = {
373
571
  };
374
572
 
375
573
  // src/fixtures.ts
574
+ var preparedPages = /* @__PURE__ */ new WeakMap();
376
575
  async function installFault(page, fault) {
377
576
  if (!fault) {
378
577
  return;
@@ -408,6 +607,179 @@ async function installFault(page, fault) {
408
607
  }).observe(document, { childList: true, subtree: true });
409
608
  });
410
609
  }
610
+ if (fault === "card-link") {
611
+ await page.addInitScript(() => {
612
+ new MutationObserver(() => {
613
+ for (const node of document.querySelectorAll(
614
+ '[role="dialog"] [aria-label="Close item"]'
615
+ )) {
616
+ node.setAttribute("aria-label", "Close");
617
+ }
618
+ }).observe(document, {
619
+ childList: true,
620
+ subtree: true,
621
+ attributes: true,
622
+ attributeFilter: ["aria-label"]
623
+ });
624
+ });
625
+ }
626
+ if (fault === "no-add-marker") {
627
+ await page.addInitScript(() => {
628
+ new MutationObserver(() => {
629
+ for (const node of document.querySelectorAll(
630
+ '[data-zappie-action="add-to-cart"]'
631
+ )) {
632
+ node.removeAttribute("data-zappie-action");
633
+ }
634
+ }).observe(document, { childList: true, subtree: true });
635
+ });
636
+ }
637
+ if (fault === "add-enabled-invalid" || fault === "add-enabled-silent") {
638
+ await page.addInitScript((flags) => {
639
+ new MutationObserver(() => {
640
+ for (const original of document.querySelectorAll(
641
+ '[role="dialog"] button[data-zappie-action="add-to-cart"]:not([data-verify-proxied])'
642
+ )) {
643
+ original.setAttribute("data-verify-proxied", "");
644
+ original.removeAttribute("data-zappie-action");
645
+ original.style.display = "none";
646
+ const proxy = document.createElement("button");
647
+ proxy.type = "button";
648
+ proxy.textContent = "Add";
649
+ proxy.setAttribute("data-zappie-action", "add-to-cart");
650
+ proxy.setAttribute("data-verify-proxied", "");
651
+ proxy.addEventListener("click", () => {
652
+ if (!original.disabled) {
653
+ original.click();
654
+ return;
655
+ }
656
+ const group = original.closest('[role="dialog"]')?.querySelector(
657
+ '[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
658
+ );
659
+ if (flags && group) {
660
+ group.setAttribute("aria-invalid", "true");
661
+ group.setAttribute("data-invalid", "");
662
+ const message = document.createElement("p");
663
+ message.textContent = "Choose one to continue";
664
+ group.append(message);
665
+ group.focus();
666
+ }
667
+ });
668
+ original.after(proxy);
669
+ }
670
+ }).observe(document, { childList: true, subtree: true });
671
+ }, fault === "add-enabled-invalid");
672
+ }
673
+ if (fault === "total-on-checkout" || fault === "total-missing") {
674
+ await page.addInitScript((onCheckout) => {
675
+ window.addEventListener(
676
+ "click",
677
+ (event) => {
678
+ const link = event.target?.closest("a[data-zappie-cart-checkout]");
679
+ if (!link) {
680
+ return;
681
+ }
682
+ event.preventDefault();
683
+ event.stopImmediatePropagation();
684
+ history.pushState(history.state, "", link.href);
685
+ window.dispatchEvent(
686
+ new PopStateEvent("popstate", { state: history.state })
687
+ );
688
+ },
689
+ { capture: true }
690
+ );
691
+ new MutationObserver(() => {
692
+ for (const node of document.querySelectorAll(
693
+ '[data-zappie-overlay="bag"] [data-zappie-total]'
694
+ )) {
695
+ sessionStorage.setItem("verify-total", node.textContent ?? "");
696
+ node.removeAttribute("data-zappie-total");
697
+ }
698
+ const text = sessionStorage.getItem("verify-total");
699
+ if (onCheckout && text && location.pathname === "/checkout" && document.querySelector("main") && !document.querySelector("[data-verify-total]")) {
700
+ const total = document.createElement("p");
701
+ total.setAttribute("data-verify-total", "");
702
+ total.setAttribute("data-zappie-total", "");
703
+ total.textContent = text;
704
+ document.querySelector("main").prepend(total);
705
+ }
706
+ }).observe(document, { childList: true, subtree: true });
707
+ }, fault === "total-on-checkout");
708
+ }
709
+ if (fault === "address-confirm" || fault === "address-not-stored") {
710
+ await page.addInitScript((confirms) => {
711
+ let release = false;
712
+ window.addEventListener(
713
+ "click",
714
+ (event) => {
715
+ const option = event.target?.closest(
716
+ '[role="option"]'
717
+ );
718
+ if (!option || release) {
719
+ return;
720
+ }
721
+ event.stopImmediatePropagation();
722
+ event.preventDefault();
723
+ if (!confirms) {
724
+ return;
725
+ }
726
+ const dialog = document.createElement("div");
727
+ dialog.setAttribute("role", "dialog");
728
+ dialog.setAttribute("aria-label", "Confirm address");
729
+ const footer = document.createElement("footer");
730
+ const button = document.createElement("button");
731
+ button.type = "button";
732
+ button.textContent = "Deliver here";
733
+ button.addEventListener("click", () => {
734
+ dialog.remove();
735
+ release = true;
736
+ option.click();
737
+ release = false;
738
+ });
739
+ footer.append(button);
740
+ dialog.append(footer);
741
+ document.body.append(dialog);
742
+ },
743
+ { capture: true }
744
+ );
745
+ }, fault === "address-confirm");
746
+ }
747
+ if (fault === "field-rows") {
748
+ await page.addInitScript(() => {
749
+ const fields = [
750
+ ["address", "[data-zappie-address-input]"],
751
+ ["phone", 'input[type="tel"]']
752
+ ];
753
+ new MutationObserver(() => {
754
+ for (const [field, selector] of fields) {
755
+ for (const input of document.querySelectorAll(
756
+ `${selector}:not([data-verify-row])`
757
+ )) {
758
+ input.setAttribute("data-verify-row", "");
759
+ input.hidden = true;
760
+ const row = document.createElement("button");
761
+ row.type = "button";
762
+ row.textContent = `Add your ${field}`;
763
+ row.setAttribute("data-zappie-field-open", field);
764
+ row.addEventListener("click", () => {
765
+ if (!document.cookie.includes("zappie_session=signed")) {
766
+ window.location.assign(
767
+ new URL(
768
+ `/_zappie/login?return=${encodeURIComponent(location.pathname)}`,
769
+ location.href
770
+ ).href
771
+ );
772
+ return;
773
+ }
774
+ row.remove();
775
+ input.hidden = false;
776
+ });
777
+ input.before(row);
778
+ }
779
+ }
780
+ }).observe(document, { childList: true, subtree: true });
781
+ });
782
+ }
411
783
  if (fault === "overlay-markers") {
412
784
  await page.addInitScript(() => {
413
785
  new MutationObserver(() => {
@@ -426,7 +798,32 @@ async function installFault(page, fault) {
426
798
  }).observe(document, { childList: true, subtree: true });
427
799
  });
428
800
  }
429
- if (fault === "bag-on-tap") {
801
+ if (fault === "bag-after-add" || fault === "bag-after-add-on-tap") {
802
+ await page.addInitScript(() => {
803
+ const sync = () => {
804
+ let count = 0;
805
+ try {
806
+ count = window.__zappie?.cart().count ?? 0;
807
+ } catch {
808
+ }
809
+ for (const node of document.querySelectorAll(
810
+ '[data-zappie-overlay-trigger="bag"]'
811
+ )) {
812
+ if (count === 0) {
813
+ node.style.setProperty("display", "none", "important");
814
+ } else {
815
+ node.style.removeProperty("display");
816
+ }
817
+ }
818
+ };
819
+ new MutationObserver(sync).observe(document, {
820
+ childList: true,
821
+ subtree: true
822
+ });
823
+ window.setInterval(sync, 50);
824
+ });
825
+ }
826
+ if (fault === "bag-on-tap" || fault === "bag-after-add-on-tap") {
430
827
  let release = () => {
431
828
  };
432
829
  let tapped = Promise.resolve();
@@ -462,24 +859,26 @@ async function installFault(page, fault) {
462
859
  );
463
860
  }
464
861
  if (fault === "one-slow-open") {
465
- await page.addInitScript(() => {
466
- document.addEventListener(
467
- "click",
468
- (event) => {
469
- if (!(event.target instanceof Element) || !event.target.closest("[data-zappie-overlay-trigger]")) {
470
- return;
471
- }
472
- const taps = Number(sessionStorage.getItem("__verifyTaps") ?? 0);
473
- sessionStorage.setItem("__verifyTaps", String(taps + 1));
474
- if (taps % 3 === 0) {
475
- const until = performance.now() + 250;
476
- while (performance.now() < until) {
862
+ const browser = page.context().browser();
863
+ const index = browser ? preparedPages.get(browser) ?? 0 : 0;
864
+ if (browser) {
865
+ preparedPages.set(browser, index + 1);
866
+ }
867
+ if (index > 0 && (index - 1) % 3 === 0) {
868
+ await page.addInitScript(() => {
869
+ document.addEventListener(
870
+ "click",
871
+ (event) => {
872
+ if (event.target instanceof Element && event.target.closest("[data-zappie-overlay-trigger]")) {
873
+ const until = performance.now() + 250;
874
+ while (performance.now() < until) {
875
+ }
477
876
  }
478
- }
479
- },
480
- { capture: true }
481
- );
482
- });
877
+ },
878
+ { capture: true }
879
+ );
880
+ });
881
+ }
483
882
  }
484
883
  if (fault === "late-hydration") {
485
884
  await page.route(
@@ -643,6 +1042,22 @@ async function applyPageFault(page, fault) {
643
1042
  }
644
1043
  });
645
1044
  }
1045
+ if (fault === "card-link") {
1046
+ await page.waitForFunction(() => {
1047
+ const link = document.querySelector("a[data-zappie-item]");
1048
+ return link !== null && Object.keys(link).some((key) => key.startsWith("__reactProps$"));
1049
+ });
1050
+ await page.locator("a[data-zappie-item]").evaluateAll((links) => {
1051
+ for (const link of links) {
1052
+ const id = link.getAttribute("data-zappie-item");
1053
+ const card = link.parentElement?.querySelector(
1054
+ `article[data-zappie-item="${id}"]`
1055
+ );
1056
+ link.textContent = card?.querySelector("h3")?.textContent ?? "Item";
1057
+ card?.removeAttribute("data-zappie-item");
1058
+ }
1059
+ });
1060
+ }
646
1061
  if (fault === "missing-marker") {
647
1062
  await page.locator("[data-zappie-item]").evaluateAll((nodes) => {
648
1063
  for (const node of nodes) {
@@ -878,11 +1293,11 @@ var v05 = {
878
1293
  "The test menu has no available item with required options",
879
1294
  "Keep one available configurable item in the test menu."
880
1295
  );
881
- const item = page.locator(`[data-zappie-item="${selected.id}"]`).first();
882
- await item.locator("xpath=..").getByRole("link", { name: "View details" }).click();
1296
+ await openItem(page, selected.id);
883
1297
  const dialog = page.getByRole("dialog");
884
1298
  await dialog.waitFor();
885
1299
  const required = dialog.locator('[data-zappie-option-group][aria-required="true"]').first();
1300
+ await required.waitFor({ timeout: 5e3 }).catch(() => void 0);
886
1301
  assertion(
887
1302
  await required.count(),
888
1303
  "required_group_missing",
@@ -891,23 +1306,41 @@ var v05 = {
891
1306
  );
892
1307
  const before = await debugValue(page, () => window.__zappie.cart().count);
893
1308
  const add = dialog.locator('[data-zappie-action="add-to-cart"]');
894
- assertion(
895
- await add.isDisabled(),
896
- "required_option_bypassed",
897
- "Add is enabled before the shopper chooses a required option",
898
- "Disable Add until the shopper completes every required option group."
899
- );
900
- for (let index = 0; index < 5 && await add.isDisabled(); index++) {
901
- const unsatisfied = dialog.locator(
902
- '[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
903
- ).first();
1309
+ const unsatisfied = dialog.locator(
1310
+ '[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
1311
+ );
1312
+ const blocked2 = await add.isDisabled();
1313
+ if (!blocked2) {
1314
+ const group = unsatisfied.first();
1315
+ const textBefore = (await group.innerText().catch(() => "")).trim();
1316
+ await add.click();
1317
+ await page.waitForTimeout(500);
1318
+ const added = await page.evaluate(
1319
+ (count) => window.__zappie.cart().count > count,
1320
+ before
1321
+ );
904
1322
  assertion(
905
- await unsatisfied.count(),
1323
+ !added && await dialog.isVisible(),
1324
+ "required_option_bypassed",
1325
+ "Add put the item in the cart before the shopper chose a required option",
1326
+ "Disable Add until the shopper completes every required option group, or keep the item open and flag the group when Add is tapped."
1327
+ );
1328
+ assertion(
1329
+ await group.evaluate(flagged, textBefore),
1330
+ "required_option_unflagged",
1331
+ "Tapping Add before a required choice did not flag the group",
1332
+ "Mark the group that needs a choice with aria-invalid or data-invalid and show a message saying why."
1333
+ );
1334
+ }
1335
+ for (let index = 0; index < 5 && (blocked2 ? await add.isDisabled() : await unsatisfied.count() > 0); index++) {
1336
+ const next = unsatisfied.first();
1337
+ assertion(
1338
+ await next.count(),
906
1339
  "required_option_unselectable",
907
1340
  "A required option group has no selectable choice",
908
1341
  "Keep an available choice in every required option group."
909
1342
  );
910
- await unsatisfied.locator('[data-zappie-option]:not([aria-disabled="true"])').first().click();
1343
+ await next.locator('[data-zappie-option]:not([aria-disabled="true"])').first().click();
911
1344
  }
912
1345
  assertion(
913
1346
  await add.isEnabled(),
@@ -935,6 +1368,22 @@ var v05 = {
935
1368
  );
936
1369
  }
937
1370
  };
1371
+ function flagged(group, textBefore) {
1372
+ const invalid = group.getAttribute("aria-invalid") === "true" || group.hasAttribute("data-invalid") || group.querySelector('[aria-invalid="true"], [data-invalid]') !== null;
1373
+ if (!invalid) {
1374
+ return false;
1375
+ }
1376
+ const shown = (element) => element instanceof HTMLElement && element.getClientRects().length > 0 && Boolean(element.innerText.trim());
1377
+ if (group instanceof HTMLElement && group.innerText.trim().length > textBefore.length) {
1378
+ return true;
1379
+ }
1380
+ const referenced = ["aria-describedby", "aria-errormessage"].flatMap((name) => group.getAttribute(name)?.split(/\s+/) ?? []).some((id) => shown(document.getElementById(id)));
1381
+ if (referenced) {
1382
+ return true;
1383
+ }
1384
+ const dialog = group.closest('[role="dialog"]') ?? document;
1385
+ return [...dialog.querySelectorAll('[role="alert"]')].some(shown);
1386
+ }
938
1387
 
939
1388
  // src/checks/v06-quote.ts
940
1389
  var v06 = {
@@ -946,9 +1395,20 @@ var v06 = {
946
1395
  if (new URL(page.url()).pathname !== "/") {
947
1396
  await open(page);
948
1397
  }
949
- const total = page.locator("[data-zappie-total]");
950
- if (!await total.isVisible()) {
951
- await page.locator("[data-zappie-cart-trigger]").first().click();
1398
+ const total = page.locator("[data-zappie-total]").filter({ visible: true });
1399
+ const bagOpen = page.locator('[data-zappie-overlay="bag"]').filter({ visible: true });
1400
+ if (!await total.count()) {
1401
+ if (!await bagOpen.count()) {
1402
+ await (await cartTrigger(page)).click();
1403
+ }
1404
+ await total.first().waitFor({ timeout: 2e3 }).catch(() => void 0);
1405
+ }
1406
+ if (!await total.count()) {
1407
+ const handover = page.locator('[data-zappie-overlay="bag"], [role="dialog"]').filter({ visible: true }).locator('[data-zappie-cart-checkout], a[href*="/checkout"]').filter({ visible: true }).first();
1408
+ if (await handover.count()) {
1409
+ await handover.click();
1410
+ await page.waitForURL((url) => url.pathname.endsWith("/checkout"));
1411
+ }
952
1412
  }
953
1413
  await page.waitForFunction(
954
1414
  () => window.__zappie.events.some((entry) => entry.name === "quote"),
@@ -962,11 +1422,21 @@ var v06 = {
962
1422
  "The cart did not produce a quote within 2 seconds",
963
1423
  "Open the cart through the SDK and render Totals so it requests a quote."
964
1424
  );
965
- const rendered = await total.textContent();
1425
+ let expected = format.money(quote.total);
1426
+ let rendered = null;
1427
+ for (let attempt = 0; attempt < 20; attempt++) {
1428
+ const latest = await page.evaluate(() => window.__zappie.quote());
1429
+ expected = format.money((latest ?? quote).total);
1430
+ rendered = await total.first().textContent({ timeout: 5e3 }).catch(() => null);
1431
+ if (rendered === null || rendered.includes(expected)) {
1432
+ break;
1433
+ }
1434
+ await page.waitForTimeout(250);
1435
+ }
966
1436
  assertion(
967
- rendered?.includes(format.money(quote.total)),
1437
+ rendered?.includes(expected),
968
1438
  "total_mismatch",
969
- `Rendered total does not match ${format.money(quote.total)}`,
1439
+ `Rendered total does not match ${expected}`,
970
1440
  "Render quote.total with the SDK Money formatter."
971
1441
  );
972
1442
  }
@@ -1101,7 +1571,7 @@ var v07 = {
1101
1571
  await window.__zappie.cart.inject({ lines: lines2 });
1102
1572
  }, lines);
1103
1573
  await page.reload({ waitUntil: "domcontentloaded" });
1104
- await page.locator("[data-zappie-cart-trigger]").first().click();
1574
+ await (await cartTrigger(page)).click();
1105
1575
  const codes = unavailable ? ["price_changed", "item_unavailable"] : ["price_changed"];
1106
1576
  for (const code of codes) {
1107
1577
  const issue = page.locator(`[data-zappie-issue="${code}"]`).first();
@@ -1424,14 +1894,28 @@ var v10 = {
1424
1894
  "Render a delivery control with data-zappie-fulfilment=delivery."
1425
1895
  );
1426
1896
  await delivery.click();
1427
- const input = page.locator("[data-zappie-address-input]");
1428
- await input.waitFor();
1897
+ const input = await revealField(
1898
+ page,
1899
+ "address",
1900
+ page.locator("[data-zappie-address-input]"),
1901
+ () => delivery.click()
1902
+ );
1429
1903
  await input.fill(ADDRESS);
1430
1904
  await page.getByText(ADDRESS, { exact: true }).last().click();
1431
- await page.waitForFunction(() => {
1432
- const fulfilment2 = window.__zappie.cart().fulfilment;
1433
- return fulfilment2?.type === "delivery" && Boolean(fulfilment2.address.placeId);
1434
- });
1905
+ if (!await addressStored(page, 3e3)) {
1906
+ const confirm = await dialogConfirm(
1907
+ page,
1908
+ "[data-zappie-address-confirm]"
1909
+ );
1910
+ assertion(
1911
+ confirm,
1912
+ "delivery_address_missing",
1913
+ "The selected address was not stored",
1914
+ "Store the resolved address on delivery fulfilment when the shopper picks it, or mark the control that confirms it with data-zappie-address-confirm."
1915
+ );
1916
+ await confirm.click();
1917
+ }
1918
+ await addressStored(page);
1435
1919
  const fulfilment = await debugValue(
1436
1920
  page,
1437
1921
  () => window.__zappie.cart().fulfilment
@@ -1475,12 +1959,30 @@ var v10 = {
1475
1959
  );
1476
1960
  }
1477
1961
  };
1962
+ async function addressStored(page, timeout) {
1963
+ return page.waitForFunction(
1964
+ () => {
1965
+ const fulfilment = window.__zappie.cart().fulfilment;
1966
+ return fulfilment?.type === "delivery" && Boolean(fulfilment.address.placeId);
1967
+ },
1968
+ void 0,
1969
+ timeout === void 0 ? {} : { timeout }
1970
+ ).then(
1971
+ () => true,
1972
+ (error) => {
1973
+ if (timeout === void 0) {
1974
+ throw error;
1975
+ }
1976
+ return false;
1977
+ }
1978
+ );
1979
+ }
1478
1980
 
1479
1981
  // src/checks/v13-accessibility.ts
1480
1982
  import AxeBuilder from "@axe-core/playwright";
1481
1983
 
1482
1984
  // src/checks/semantics.ts
1483
- var TOUCH_TARGET_PX = 44;
1985
+ var TOUCH_TARGET_PX = 24;
1484
1986
  async function semanticFindings(page, scope) {
1485
1987
  return page.evaluate(
1486
1988
  ({ scope: scope2, minimum }) => {
@@ -1583,11 +2085,29 @@ var semanticHints = {
1583
2085
  h1_count: "Give each page exactly one h1 that names it.",
1584
2086
  dialog_name_missing: "Name every dialog with aria-labelledby pointing at its title, or with aria-label.",
1585
2087
  positive_tabindex: "Remove positive tabindex values. Use the DOM order, or tabindex 0 and -1.",
1586
- touch_target_too_small: "Make every menu control at least 44\xD744 px on a phone, with padding or a larger hit area."
2088
+ touch_target_too_small: "Make every menu control at least 24\xD724 px on a phone, with padding or a larger hit area."
1587
2089
  };
1588
2090
 
1589
2091
  // src/checks/v13-accessibility.ts
1590
2092
  async function severeViolations(page) {
2093
+ await page.evaluate(
2094
+ () => new Promise((resolve) => {
2095
+ const deadline = performance.now() + 2e3;
2096
+ let quiet = 0;
2097
+ const frame = () => {
2098
+ const running = document.getAnimations().some(
2099
+ (animation) => animation.playState === "running" && animation.effect?.getComputedTiming().iterations !== Infinity
2100
+ );
2101
+ quiet = running ? 0 : quiet + 1;
2102
+ if (quiet >= 3 || performance.now() > deadline) {
2103
+ resolve();
2104
+ return;
2105
+ }
2106
+ requestAnimationFrame(frame);
2107
+ };
2108
+ requestAnimationFrame(frame);
2109
+ })
2110
+ );
1591
2111
  const analysis = await new AxeBuilder({ page }).analyze();
1592
2112
  return analysis.violations.filter(
1593
2113
  (entry) => entry.impact === "critical" || entry.impact === "serious"
@@ -1621,11 +2141,48 @@ async function holdsFocus(target2) {
1621
2141
  }
1622
2142
  return false;
1623
2143
  }
2144
+ var ADD = '[data-zappie-action="add-to-cart"]';
2145
+ async function keyboardReachesAddAndCart(page, itemId, cartInColumn) {
2146
+ const cart = cartInColumn ? [] : [await cartTriggerSelector(page)];
2147
+ if (await page.locator(ADD).count() || !itemId) {
2148
+ return controlsAreReachable(page, [ADD, ...cart]);
2149
+ }
2150
+ const opener = itemOpenerSelector(itemId);
2151
+ if (cart.length && !await controlsAreReachable(page, cart)) {
2152
+ return false;
2153
+ }
2154
+ await open(page);
2155
+ if (!await tabTo(page, opener, 200)) {
2156
+ return false;
2157
+ }
2158
+ await page.keyboard.press("Enter");
2159
+ await itemShown(page);
2160
+ return tabTo(page, ADD);
2161
+ }
2162
+ async function checkoutEnabled(page, timeout) {
2163
+ return page.waitForFunction(
2164
+ () => document.querySelector("[data-zappie-checkout]")?.disabled === false,
2165
+ void 0,
2166
+ { timeout }
2167
+ ).then(
2168
+ () => true,
2169
+ () => false
2170
+ );
2171
+ }
2172
+ async function fillSteadily(input, value) {
2173
+ for (let attempt = 0; attempt < 3; attempt++) {
2174
+ await input.fill(value);
2175
+ await input.page().waitForTimeout(150);
2176
+ if (await input.inputValue() === value) {
2177
+ return;
2178
+ }
2179
+ }
2180
+ }
1624
2181
  var v13 = {
1625
2182
  id: "V13",
1626
2183
  title: "Accessibility",
1627
2184
  viewports: "both",
1628
- async run({ page, context, options, fault }, viewport) {
2185
+ async run({ page, options, fault }, viewport) {
1629
2186
  await page.setViewportSize(viewport);
1630
2187
  await open(page);
1631
2188
  await applyPageFault(page, fault);
@@ -1643,21 +2200,23 @@ var v13 = {
1643
2200
  "No available item can be opened for accessibility checks",
1644
2201
  "Keep one available item in the test menu."
1645
2202
  );
1646
- const card = page.locator(`[data-zappie-item="${item.id}"]`).first().locator("xpath=..");
1647
- await card.getByRole("link", { name: "View details" }).click();
2203
+ const { focusTarget: card } = await openItem(page, item.id);
1648
2204
  violations.push(...await severeViolations(page));
1649
2205
  semantics.push(
1650
2206
  ...await semanticFindings(page, { page: false, phone: false })
1651
2207
  );
1652
- await page.getByRole("button", { name: "Close item" }).click();
2208
+ await closeItem(page);
1653
2209
  if (!await holdsFocus(card)) {
1654
2210
  semantics.push({
1655
2211
  code: "focus_not_returned",
1656
2212
  message: "Focus did not return to the item card after the item closed"
1657
2213
  });
1658
2214
  }
1659
- const cartTrigger = page.locator("[data-zappie-cart-trigger]").first();
1660
- await cartTrigger.click();
2215
+ const cart = await cartTrigger(page);
2216
+ const cartInColumn = viewport.width >= 768 && !await cart.isVisible();
2217
+ if (!cartInColumn) {
2218
+ await cart.click();
2219
+ }
1661
2220
  violations.push(...await severeViolations(page));
1662
2221
  semantics.push(
1663
2222
  ...await semanticFindings(page, { page: false, phone: false })
@@ -1671,8 +2230,10 @@ var v13 = {
1671
2230
  message || "Accessibility violation",
1672
2231
  "Fix every serious or critical axe violation in the menu, item dialog, cart and order page."
1673
2232
  );
1674
- await page.keyboard.press("Escape");
1675
- if (!await holdsFocus(cartTrigger)) {
2233
+ if (!cartInColumn) {
2234
+ await page.keyboard.press("Escape");
2235
+ }
2236
+ if (!cartInColumn && !await holdsFocus(cart)) {
1676
2237
  semantics.push({
1677
2238
  code: "focus_not_returned",
1678
2239
  message: "Focus did not return to the cart control after the cart closed"
@@ -1691,14 +2252,7 @@ var v13 = {
1691
2252
  )
1692
2253
  ].join(" ")
1693
2254
  );
1694
- await context.addCookies([
1695
- {
1696
- name: "zappie_session",
1697
- value: "signed",
1698
- domain: new URL(page.url()).hostname,
1699
- path: "/"
1700
- }
1701
- ]);
2255
+ await signIn(page);
1702
2256
  await open(page, "/orders/order_fixture");
1703
2257
  const order = await severeViolations(page);
1704
2258
  assertion(
@@ -1709,16 +2263,32 @@ var v13 = {
1709
2263
  );
1710
2264
  await open(page);
1711
2265
  assertion(
1712
- await controlsAreReachable(page, [
1713
- '[data-zappie-action="add-to-cart"]',
1714
- "[data-zappie-cart-trigger]"
1715
- ]),
2266
+ await keyboardReachesAddAndCart(
2267
+ page,
2268
+ simpleItem(await readMenu(options))?.id,
2269
+ cartInColumn
2270
+ ),
1716
2271
  "keyboard_marker_unreachable",
1717
2272
  "Keyboard navigation did not reach the Add and cart controls",
1718
2273
  "Keep the marked Add and cart controls in the tab order."
1719
2274
  );
1720
2275
  await open(page, "/checkout");
1721
- await page.locator('input[type="tel"]').fill("+2348000000001");
2276
+ const tel = page.locator('input[type="tel"]').filter({ visible: true }).first();
2277
+ const phoneRow = page.locator('[data-zappie-field-open="phone"]').filter({ visible: true }).first();
2278
+ await tel.or(phoneRow).first().waitFor();
2279
+ if (!await tel.isVisible() && await checkoutEnabled(page, 3e3)) {
2280
+ await phoneRow.focus();
2281
+ } else {
2282
+ const phone = await revealField(
2283
+ page,
2284
+ "phone",
2285
+ page.locator('input[type="tel"]')
2286
+ );
2287
+ await fillSteadily(phone, "+2348000000001");
2288
+ if (await page.locator('[role="dialog"]').filter({ has: phone }).isVisible()) {
2289
+ await (await dialogConfirm(page, "[data-zappie-field-confirm]"))?.click();
2290
+ }
2291
+ }
1722
2292
  await page.waitForFunction(
1723
2293
  () => !document.querySelector("[data-zappie-checkout]")?.disabled
1724
2294
  );
@@ -1738,7 +2308,7 @@ var v14 = {
1738
2308
  viewports: "mobile",
1739
2309
  async run({ page, metrics, fault, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
1740
2310
  await page.setViewportSize(viewport);
1741
- const cdp = await applyPhoneProfile(page, cpuThrottlingRate2);
2311
+ const { cdp } = await applyPhoneProfile(page, cpuThrottlingRate2);
1742
2312
  await page.addInitScript(() => {
1743
2313
  const state = { lcp: 0, cls: 0 };
1744
2314
  Object.assign(window, { __zappiePerformance: state });
@@ -1820,7 +2390,7 @@ var v15 = {
1820
2390
  viewports: "mobile",
1821
2391
  async run({ page, metrics, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
1822
2392
  await page.setViewportSize(viewport);
1823
- const cdp = await applyPhoneProfile(page, cpuThrottlingRate2);
2393
+ const { cdp } = await applyPhoneProfile(page, cpuThrottlingRate2);
1824
2394
  try {
1825
2395
  await page.goto("/", { waitUntil: "domcontentloaded" });
1826
2396
  await cdp.send("Network.setCacheDisabled", { cacheDisabled: false });
@@ -2101,6 +2671,41 @@ async function openSettledMenu(page) {
2101
2671
  network.stop();
2102
2672
  }
2103
2673
  }
2674
+ async function revealBagTrigger(page, itemId) {
2675
+ const bag = page.locator(`${triggerSelector("bag")}:visible`).first();
2676
+ if (!itemId || await bag.count()) {
2677
+ return;
2678
+ }
2679
+ const before = await page.evaluate(() => window.__zappie?.cart().count ?? 0).catch(() => 0);
2680
+ const add = '[data-zappie-action="add-to-cart"]';
2681
+ const quick = page.locator(`[data-zappie-item="${itemId}"] ${add}`).filter({ visible: true }).first();
2682
+ if (await quick.count()) {
2683
+ await quick.click();
2684
+ } else {
2685
+ const opener = page.locator(itemOpenerSelector(itemId)).filter({ visible: true }).first();
2686
+ if (!await opener.count()) {
2687
+ return;
2688
+ }
2689
+ await opener.click();
2690
+ const inItem = page.locator(
2691
+ `[role="dialog"] [data-zappie-item] ${add}:visible, main [data-zappie-item] ${add}:visible`
2692
+ ).first();
2693
+ if (!await inItem.waitFor({ timeout: 5e3 }).then(
2694
+ () => true,
2695
+ () => false
2696
+ )) {
2697
+ return;
2698
+ }
2699
+ await inItem.click();
2700
+ }
2701
+ await page.waitForFunction(
2702
+ (count) => (window.__zappie?.cart().count ?? 0) > count,
2703
+ before,
2704
+ { timeout: 5e3 }
2705
+ ).catch(() => void 0);
2706
+ await openSettledMenu(page);
2707
+ await bag.waitFor({ timeout: 5e3 }).catch(() => void 0);
2708
+ }
2104
2709
 
2105
2710
  // src/checks/v16-overlays-open-quickly.ts
2106
2711
  var OVERLAY_OPEN_BUDGET_MS = 100;
@@ -2110,64 +2715,89 @@ var median = (values) => {
2110
2715
  const sorted = [...values].sort((a, b) => a - b);
2111
2716
  return sorted[Math.floor(sorted.length / 2)];
2112
2717
  };
2718
+ async function firstOpen(fresh, viewport, name, cpuThrottlingRate2, itemId) {
2719
+ const { page } = fresh;
2720
+ try {
2721
+ await page.setViewportSize(viewport);
2722
+ const { cdp, rate } = await applyPhoneProfile(page, cpuThrottlingRate2);
2723
+ try {
2724
+ await openSettledMenu(page);
2725
+ if (name === "bag") {
2726
+ await revealBagTrigger(page, itemId);
2727
+ }
2728
+ const trigger = page.locator(`${triggerSelector(name)}:visible`).first();
2729
+ await trigger.evaluate(
2730
+ (node) => node.scrollIntoView({ block: "center", inline: "nearest" })
2731
+ );
2732
+ await armOverlayTimer(page, name, OPEN_TIMEOUT_MS);
2733
+ await trigger.click({ noWaitAfter: true });
2734
+ const ms = await readOverlayTimer(page);
2735
+ return ms === void 0 ? void 0 : { ms, rate };
2736
+ } finally {
2737
+ await cdp.detach().catch(() => void 0);
2738
+ }
2739
+ } finally {
2740
+ await fresh.close();
2741
+ }
2742
+ }
2113
2743
  var v16 = {
2114
2744
  id: "V16",
2115
2745
  title: "Overlays open quickly",
2116
2746
  viewports: "mobile",
2117
- async run({ page, metrics, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
2747
+ async run({ page, options, metrics, freshPage, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
2118
2748
  await page.setViewportSize(viewport);
2119
- const cdp = await applyPhoneProfile(page, cpuThrottlingRate2);
2120
- try {
2121
- await openSettledMenu(page);
2122
- const names = (await overlayTriggers(page)).map(({ name }) => name);
2123
- skipWithoutTriggers(names);
2124
- await cdp.send("Network.setCacheDisabled", { cacheDisabled: false });
2125
- const timings = {};
2126
- const samples = {};
2127
- const unopened = [];
2128
- for (const name of names) {
2129
- const opens = [];
2130
- for (let sample = 0; sample < OVERLAY_OPEN_SAMPLES; sample++) {
2131
- await openSettledMenu(page);
2132
- const trigger = page.locator(`${triggerSelector(name)}:visible`).first();
2133
- await trigger.evaluate(
2134
- (node) => node.scrollIntoView({ block: "center", inline: "nearest" })
2135
- );
2136
- await armOverlayTimer(page, name, OPEN_TIMEOUT_MS);
2137
- await trigger.click({ noWaitAfter: true });
2138
- const ms = await readOverlayTimer(page);
2139
- if (ms === void 0) {
2140
- break;
2141
- }
2142
- opens.push(Math.round(ms));
2143
- }
2144
- if (opens.length < OVERLAY_OPEN_SAMPLES) {
2145
- unopened.push(name);
2146
- } else {
2147
- samples[name] = opens;
2148
- timings[name] = median(opens);
2749
+ const itemId = simpleItem(await readMenu(options))?.id;
2750
+ await openSettledMenu(page);
2751
+ await revealBagTrigger(page, itemId);
2752
+ const names = (await overlayTriggers(page)).map(({ name }) => name);
2753
+ skipWithoutTriggers(names);
2754
+ const timings = {};
2755
+ const samples = {};
2756
+ const rates = {};
2757
+ const unopened = [];
2758
+ for (const name of names) {
2759
+ const opens = [];
2760
+ const opensRates = [];
2761
+ for (let sample = 0; sample < OVERLAY_OPEN_SAMPLES; sample++) {
2762
+ const open2 = await firstOpen(
2763
+ await freshPage(),
2764
+ viewport,
2765
+ name,
2766
+ cpuThrottlingRate2,
2767
+ itemId
2768
+ );
2769
+ if (open2 === void 0) {
2770
+ break;
2149
2771
  }
2772
+ opens.push(Math.round(open2.ms));
2773
+ opensRates.push(open2.rate);
2774
+ }
2775
+ if (opens.length < OVERLAY_OPEN_SAMPLES) {
2776
+ unopened.push(name);
2777
+ } else {
2778
+ samples[name] = opens;
2779
+ rates[name] = opensRates;
2780
+ timings[name] = median(opens);
2150
2781
  }
2151
- metrics.overlayOpenMs = timings;
2152
- metrics.overlayOpenSamplesMs = samples;
2153
- assertion(
2154
- unopened.length === 0,
2155
- "overlay_not_opened",
2156
- `No data-zappie-overlay painted after tapping ${unopened.map((name) => `the ${name} trigger`).join(", ")}`,
2157
- `Put data-zappie-overlay with the trigger's name on the role="dialog" element that the trigger opens.`
2158
- );
2159
- const slow = Object.entries(timings).filter(
2160
- ([, ms]) => ms > OVERLAY_OPEN_BUDGET_MS
2161
- );
2162
- assertion(
2163
- slow.length === 0,
2164
- "overlay_open_too_slow",
2165
- `Overlays took longer than ${OVERLAY_OPEN_BUDGET_MS} ms to paint, as the median of ${OVERLAY_OPEN_SAMPLES} opens: ${slow.map(([name, ms]) => `${name} ${ms} ms (${samples[name].join(", ")} ms)`).join(", ")}`,
2166
- "Load overlay code with deferred() from @zappie/foundations, spread useIntentPreload on the trigger, and keep work out of the first render so the overlay paints within 100 ms of the tap."
2167
- );
2168
- } finally {
2169
- await cdp.detach();
2170
2782
  }
2783
+ metrics.overlayOpenMs = timings;
2784
+ metrics.overlayOpenSamplesMs = samples;
2785
+ metrics.overlayOpenRates = rates;
2786
+ assertion(
2787
+ unopened.length === 0,
2788
+ "overlay_not_opened",
2789
+ `No data-zappie-overlay painted after tapping ${unopened.map((name) => `the ${name} trigger`).join(", ")}`,
2790
+ `Put data-zappie-overlay with the trigger's name on the role="dialog" element that the trigger opens.`
2791
+ );
2792
+ const slow = Object.entries(timings).filter(
2793
+ ([, ms]) => ms > OVERLAY_OPEN_BUDGET_MS
2794
+ );
2795
+ assertion(
2796
+ slow.length === 0,
2797
+ "overlay_open_too_slow",
2798
+ `Overlays took longer than ${OVERLAY_OPEN_BUDGET_MS} ms to paint, as the median of ${OVERLAY_OPEN_SAMPLES} first opens: ${slow.map(([name, ms]) => `${name} ${ms} ms (${samples[name].join(", ")} ms)`).join(", ")}`,
2799
+ "Load overlay code with deferred() from @zappie/foundations, spread useIntentPreload on the trigger, and keep work out of the first render so the overlay paints within 100 ms of the tap."
2800
+ );
2171
2801
  }
2172
2802
  };
2173
2803
 
@@ -2201,8 +2831,11 @@ var scrollLocked = (page) => page.evaluate(() => {
2201
2831
  };
2202
2832
  return held(document.documentElement) || held(document.body);
2203
2833
  });
2204
- async function checkOverlay(page, name) {
2834
+ async function checkOverlay(page, name, itemId) {
2205
2835
  await openSettledMenu(page);
2836
+ if (name === "bag") {
2837
+ await revealBagTrigger(page, itemId);
2838
+ }
2206
2839
  const trigger = page.locator(`${triggerSelector(name)}:visible`).first();
2207
2840
  await trigger.focus();
2208
2841
  await page.keyboard.press("Enter");
@@ -2280,15 +2913,26 @@ var v17 = {
2280
2913
  id: "V17",
2281
2914
  title: "Overlays manage focus",
2282
2915
  viewports: "mobile",
2283
- async run({ page }, viewport) {
2916
+ async run({ page, options, freshPage }, viewport) {
2284
2917
  await page.setViewportSize(viewport);
2918
+ const itemId = simpleItem(await readMenu(options))?.id;
2285
2919
  await page.goto("/", { waitUntil: "domcontentloaded" });
2286
2920
  await page.locator("[data-zappie-item]").first().waitFor();
2921
+ if (!await page.locator(`${triggerSelector("bag")}:visible`).count()) {
2922
+ await openSettledMenu(page);
2923
+ await revealBagTrigger(page, itemId);
2924
+ }
2287
2925
  const names = (await overlayTriggers(page)).map(({ name }) => name);
2288
2926
  skipWithoutTriggers(names);
2289
2927
  const findings = [];
2290
2928
  for (const name of names) {
2291
- findings.push(...await checkOverlay(page, name));
2929
+ const fresh = await freshPage();
2930
+ try {
2931
+ await fresh.page.setViewportSize(viewport);
2932
+ findings.push(...await checkOverlay(fresh.page, name, itemId));
2933
+ } finally {
2934
+ await fresh.close();
2935
+ }
2292
2936
  }
2293
2937
  const [first] = findings;
2294
2938
  if (first) {
@@ -2362,60 +3006,6 @@ function skippedChecks(suite) {
2362
3006
  ];
2363
3007
  }
2364
3008
 
2365
- // src/cpu.ts
2366
- var REFERENCE_CPU_BENCHMARK = 26e3;
2367
- var REFERENCE_CPU_THROTTLING_RATE = 4;
2368
- var MIN_RATE = 1;
2369
- var MAX_RATE = 8;
2370
- var ROUNDS = 5;
2371
- var ROUND_MS = 100;
2372
- function cpuThrottlingRate(benchmark) {
2373
- if (!Number.isFinite(benchmark) || benchmark <= 0) {
2374
- return REFERENCE_CPU_THROTTLING_RATE;
2375
- }
2376
- const rate = REFERENCE_CPU_THROTTLING_RATE * benchmark / REFERENCE_CPU_BENCHMARK;
2377
- return Math.round(Math.min(Math.max(rate, MIN_RATE), MAX_RATE) * 10) / 10;
2378
- }
2379
- async function benchmarkCpu(browser) {
2380
- const context = await browser.newContext();
2381
- try {
2382
- const page = await context.newPage();
2383
- return await page.evaluate(
2384
- ({ rounds, roundMs }) => {
2385
- const work = () => {
2386
- let text = "";
2387
- for (let index = 0; index < 500; index++) {
2388
- text += String.fromCharCode(97 + index % 26);
2389
- }
2390
- const values = Array.from(
2391
- { length: 500 },
2392
- (_, index) => index * 7919 % 500
2393
- );
2394
- values.sort((a, b) => a - b);
2395
- return text.length + values[0];
2396
- };
2397
- const scores = [];
2398
- let sink = 0;
2399
- for (let round = 0; round < rounds; round++) {
2400
- const started = performance.now();
2401
- let iterations = 0;
2402
- while (performance.now() - started < roundMs) {
2403
- sink += work();
2404
- iterations++;
2405
- }
2406
- scores.push(iterations * 1e3 / (performance.now() - started));
2407
- }
2408
- scores.sort((a, b) => a - b);
2409
- window.__verifySink = sink;
2410
- return Math.round(scores[Math.floor(scores.length / 2)]);
2411
- },
2412
- { rounds: ROUNDS, roundMs: ROUND_MS }
2413
- );
2414
- } finally {
2415
- await context.close();
2416
- }
2417
- }
2418
-
2419
3009
  // src/report.ts
2420
3010
  import { createPrivateKey, randomUUID, sign } from "crypto";
2421
3011
  var REPORT_SCHEMA = "https://registry.zappie.ai/v1/schema/verify-report.json";
@@ -2502,7 +3092,7 @@ async function reset(options) {
2502
3092
  }
2503
3093
 
2504
3094
  // src/version.ts
2505
- var PACKAGE_VERSION = true ? "0.2.2" : "0.0.0-development";
3095
+ var PACKAGE_VERSION = true ? "0.2.4" : "0.0.0-development";
2506
3096
 
2507
3097
  // src/runner.ts
2508
3098
  var VERSION = PACKAGE_VERSION;
@@ -2583,18 +3173,7 @@ async function execute(check, run2) {
2583
3173
  }
2584
3174
  }
2585
3175
  var label = (viewport) => `${viewport.width}x${viewport.height}`;
2586
- async function runVerify(options, fault) {
2587
- if (!/^pk_test_.+/.test(options.key)) {
2588
- throw new VerifyError(
2589
- "live_key_refused",
2590
- "Verify accepts only pk_test_ keys",
2591
- "Create a test publishable key and rerun verify."
2592
- );
2593
- }
2594
- const started = Date.now();
2595
- const browser = await chromium.launch();
2596
- const cpuBenchmark = await benchmarkCpu(browser).catch(() => void 0);
2597
- const throttlingRate = cpuBenchmark === void 0 ? REFERENCE_CPU_THROTTLING_RATE : cpuThrottlingRate(cpuBenchmark);
3176
+ async function openContext(browser, options) {
2598
3177
  const context = await browser.newContext(browserOptions(options));
2599
3178
  if (options.standInForPlatform) {
2600
3179
  await context.route(
@@ -2617,10 +3196,29 @@ async function runVerify(options, fault) {
2617
3196
  }
2618
3197
  );
2619
3198
  }
2620
- await context.tracing.start({ screenshots: true, snapshots: true });
3199
+ return context;
3200
+ }
3201
+ async function preparePage(context, options, fault) {
2621
3202
  const page = await context.newPage();
2622
3203
  page.setDefaultTimeout(Math.min(options.timeoutMs ?? 2e4, 2e4));
2623
3204
  await installFault(page, fault);
3205
+ return page;
3206
+ }
3207
+ async function runVerify(options, fault) {
3208
+ if (!/^pk_test_.+/.test(options.key)) {
3209
+ throw new VerifyError(
3210
+ "live_key_refused",
3211
+ "Verify accepts only pk_test_ keys",
3212
+ "Create a test publishable key and rerun verify."
3213
+ );
3214
+ }
3215
+ const started = Date.now();
3216
+ const browser = await chromium.launch();
3217
+ const cpuBenchmark = await benchmarkCpu(browser).catch(() => void 0);
3218
+ const throttlingRate = cpuBenchmark === void 0 ? REFERENCE_CPU_THROTTLING_RATE : cpuThrottlingRate(cpuBenchmark);
3219
+ const context = await openContext(browser, options);
3220
+ await context.tracing.start({ screenshots: true, snapshots: true });
3221
+ const page = await preparePage(context, options, fault);
2624
3222
  const metrics = {};
2625
3223
  const run2 = {
2626
3224
  options,
@@ -2629,7 +3227,14 @@ async function runVerify(options, fault) {
2629
3227
  page,
2630
3228
  metrics,
2631
3229
  cpuThrottlingRate: throttlingRate,
2632
- ...fault ? { fault } : {}
3230
+ ...fault ? { fault } : {},
3231
+ async freshPage() {
3232
+ const fresh = await openContext(browser, options);
3233
+ return {
3234
+ page: await preparePage(fresh, options, fault),
3235
+ close: () => fresh.close()
3236
+ };
3237
+ }
2633
3238
  };
2634
3239
  const results = [await reset(options)];
2635
3240
  try {