@zappie/verify 0.2.2 → 0.2.3

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,9 @@ 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
+ const benchmark = await benchmarkPage(page).catch(() => void 0);
353
+ const rate = benchmark === void 0 ? fallbackRate : cpuThrottlingRate(benchmark);
293
354
  const cdp = await page.context().newCDPSession(page);
294
355
  await cdp.send("Network.enable");
295
356
  await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
@@ -301,7 +362,143 @@ async function applyPhoneProfile(page, rate) {
301
362
  connectionType: "cellular3g"
302
363
  });
303
364
  await cdp.send("Emulation.setCPUThrottlingRate", { rate });
304
- return cdp;
365
+ return { cdp, rate };
366
+ }
367
+ var ITEM_DIALOG = '[data-zappie-overlay="item"], [role="dialog"]';
368
+ async function itemOpener(page, itemId) {
369
+ const marked = `[data-zappie-item="${itemId}"]`;
370
+ await page.locator(marked).first().waitFor();
371
+ const own = page.locator(
372
+ [
373
+ `a[href]${marked}`,
374
+ `button${marked}`,
375
+ `[role="link"]${marked}`,
376
+ `[role="button"]${marked}`
377
+ ].join(", ")
378
+ ).filter({ visible: true }).first();
379
+ if (await own.count()) {
380
+ return { opener: own, focusTarget: own };
381
+ }
382
+ const card = page.locator(marked).filter({ visible: true }).first();
383
+ const inner = card.locator('a[href], [role="link"]').first();
384
+ if (await inner.count()) {
385
+ return { opener: inner, focusTarget: card };
386
+ }
387
+ const cell = card.locator("xpath=..");
388
+ return {
389
+ opener: cell.locator('a[href], [role="link"]').first(),
390
+ focusTarget: cell
391
+ };
392
+ }
393
+ async function openItem(page, itemId) {
394
+ const found = await itemOpener(page, itemId);
395
+ await found.opener.click();
396
+ await itemShown(page);
397
+ return found;
398
+ }
399
+ async function itemShown(page) {
400
+ await Promise.race([
401
+ page.waitForURL((url) => /\/item\//.test(url.pathname)),
402
+ page.locator(ITEM_DIALOG).filter({ visible: true }).first().waitFor()
403
+ ]);
404
+ }
405
+ async function closeItem(page) {
406
+ const dialog = page.locator(ITEM_DIALOG).filter({ visible: true }).last();
407
+ if (!await dialog.count()) {
408
+ await page.goBack();
409
+ return;
410
+ }
411
+ const close = dialog.getByRole("button", { name: /close/i }).filter({ visible: true }).first();
412
+ if (await close.count()) {
413
+ await close.click();
414
+ return;
415
+ }
416
+ const outside = page.getByRole("button", { name: /close/i }).filter({ visible: true }).last();
417
+ if (await outside.count()) {
418
+ await outside.click();
419
+ return;
420
+ }
421
+ await page.keyboard.press("Escape");
422
+ }
423
+ function itemOpenerSelector(itemId) {
424
+ const marked = itemId ? `[data-zappie-item="${itemId}"]` : "[data-zappie-item]";
425
+ return [
426
+ `a[href]${marked}`,
427
+ `button${marked}`,
428
+ `[role="link"]${marked}`,
429
+ `[role="button"]${marked}`,
430
+ `${marked} a[href]`
431
+ ].join(", ");
432
+ }
433
+ async function cartTriggerSelector(page) {
434
+ return await page.locator("[data-zappie-cart-trigger]").count() ? "[data-zappie-cart-trigger]" : '[data-zappie-overlay-trigger="bag"]';
435
+ }
436
+ async function cartTrigger(page) {
437
+ return page.locator(await cartTriggerSelector(page)).filter({ visible: true }).first();
438
+ }
439
+ async function tabTo(page, selector, presses = 60) {
440
+ for (let index = 0; index < presses; index++) {
441
+ await page.keyboard.press("Tab");
442
+ const inside = await page.evaluate(
443
+ (value) => Boolean(document.activeElement?.closest(value)),
444
+ selector
445
+ );
446
+ if (inside) {
447
+ return true;
448
+ }
449
+ }
450
+ return false;
451
+ }
452
+ async function signIn(page, site = page.url()) {
453
+ await page.context().addCookies([
454
+ {
455
+ name: "zappie_session",
456
+ value: "signed",
457
+ domain: new URL(site).hostname,
458
+ path: "/"
459
+ }
460
+ ]);
461
+ }
462
+ async function revealField(page, field, input, reach = async () => {
463
+ }) {
464
+ const site = new URL(page.url());
465
+ const opener = page.locator(`[data-zappie-field-open="${field}"]`).filter({ visible: true }).first();
466
+ const shown = input.filter({ visible: true }).first();
467
+ await shown.or(opener).first().waitFor();
468
+ if (await shown.isVisible()) {
469
+ return shown;
470
+ }
471
+ await opener.click();
472
+ if (await sentToSignIn(page, site, shown)) {
473
+ await signIn(page, site.href);
474
+ await open(page, site.pathname);
475
+ await reach();
476
+ await opener.click();
477
+ }
478
+ await shown.waitFor();
479
+ return shown;
480
+ }
481
+ async function sentToSignIn(page, site, shown) {
482
+ const deadline = Date.now() + 1e4;
483
+ while (Date.now() < deadline) {
484
+ const url = new URL(page.url());
485
+ if (url.origin !== site.origin || url.pathname === "/_zappie/login") {
486
+ return true;
487
+ }
488
+ if (await shown.isVisible().catch(() => false)) {
489
+ return false;
490
+ }
491
+ await page.waitForTimeout(100);
492
+ }
493
+ return false;
494
+ }
495
+ async function dialogConfirm(page, confirm) {
496
+ const marked = page.locator(confirm).filter({ visible: true }).first();
497
+ if (await marked.count()) {
498
+ return marked;
499
+ }
500
+ const button = page.locator('[role="dialog"]').filter({ visible: true }).last().locator('footer button:enabled, button[type="submit"]:enabled').filter({ visible: true }).last();
501
+ return await button.count() ? button : void 0;
305
502
  }
306
503
 
307
504
  // src/checks/v01-routes.ts
@@ -373,6 +570,7 @@ var v02 = {
373
570
  };
374
571
 
375
572
  // src/fixtures.ts
573
+ var preparedPages = /* @__PURE__ */ new WeakMap();
376
574
  async function installFault(page, fault) {
377
575
  if (!fault) {
378
576
  return;
@@ -408,6 +606,179 @@ async function installFault(page, fault) {
408
606
  }).observe(document, { childList: true, subtree: true });
409
607
  });
410
608
  }
609
+ if (fault === "card-link") {
610
+ await page.addInitScript(() => {
611
+ new MutationObserver(() => {
612
+ for (const node of document.querySelectorAll(
613
+ '[role="dialog"] [aria-label="Close item"]'
614
+ )) {
615
+ node.setAttribute("aria-label", "Close");
616
+ }
617
+ }).observe(document, {
618
+ childList: true,
619
+ subtree: true,
620
+ attributes: true,
621
+ attributeFilter: ["aria-label"]
622
+ });
623
+ });
624
+ }
625
+ if (fault === "no-add-marker") {
626
+ await page.addInitScript(() => {
627
+ new MutationObserver(() => {
628
+ for (const node of document.querySelectorAll(
629
+ '[data-zappie-action="add-to-cart"]'
630
+ )) {
631
+ node.removeAttribute("data-zappie-action");
632
+ }
633
+ }).observe(document, { childList: true, subtree: true });
634
+ });
635
+ }
636
+ if (fault === "add-enabled-invalid" || fault === "add-enabled-silent") {
637
+ await page.addInitScript((flags) => {
638
+ new MutationObserver(() => {
639
+ for (const original of document.querySelectorAll(
640
+ '[role="dialog"] button[data-zappie-action="add-to-cart"]:not([data-verify-proxied])'
641
+ )) {
642
+ original.setAttribute("data-verify-proxied", "");
643
+ original.removeAttribute("data-zappie-action");
644
+ original.style.display = "none";
645
+ const proxy = document.createElement("button");
646
+ proxy.type = "button";
647
+ proxy.textContent = "Add";
648
+ proxy.setAttribute("data-zappie-action", "add-to-cart");
649
+ proxy.setAttribute("data-verify-proxied", "");
650
+ proxy.addEventListener("click", () => {
651
+ if (!original.disabled) {
652
+ original.click();
653
+ return;
654
+ }
655
+ const group = original.closest('[role="dialog"]')?.querySelector(
656
+ '[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
657
+ );
658
+ if (flags && group) {
659
+ group.setAttribute("aria-invalid", "true");
660
+ group.setAttribute("data-invalid", "");
661
+ const message = document.createElement("p");
662
+ message.textContent = "Choose one to continue";
663
+ group.append(message);
664
+ group.focus();
665
+ }
666
+ });
667
+ original.after(proxy);
668
+ }
669
+ }).observe(document, { childList: true, subtree: true });
670
+ }, fault === "add-enabled-invalid");
671
+ }
672
+ if (fault === "total-on-checkout" || fault === "total-missing") {
673
+ await page.addInitScript((onCheckout) => {
674
+ window.addEventListener(
675
+ "click",
676
+ (event) => {
677
+ const link = event.target?.closest("a[data-zappie-cart-checkout]");
678
+ if (!link) {
679
+ return;
680
+ }
681
+ event.preventDefault();
682
+ event.stopImmediatePropagation();
683
+ history.pushState(history.state, "", link.href);
684
+ window.dispatchEvent(
685
+ new PopStateEvent("popstate", { state: history.state })
686
+ );
687
+ },
688
+ { capture: true }
689
+ );
690
+ new MutationObserver(() => {
691
+ for (const node of document.querySelectorAll(
692
+ '[data-zappie-overlay="bag"] [data-zappie-total]'
693
+ )) {
694
+ sessionStorage.setItem("verify-total", node.textContent ?? "");
695
+ node.removeAttribute("data-zappie-total");
696
+ }
697
+ const text = sessionStorage.getItem("verify-total");
698
+ if (onCheckout && text && location.pathname === "/checkout" && document.querySelector("main") && !document.querySelector("[data-verify-total]")) {
699
+ const total = document.createElement("p");
700
+ total.setAttribute("data-verify-total", "");
701
+ total.setAttribute("data-zappie-total", "");
702
+ total.textContent = text;
703
+ document.querySelector("main").prepend(total);
704
+ }
705
+ }).observe(document, { childList: true, subtree: true });
706
+ }, fault === "total-on-checkout");
707
+ }
708
+ if (fault === "address-confirm" || fault === "address-not-stored") {
709
+ await page.addInitScript((confirms) => {
710
+ let release = false;
711
+ window.addEventListener(
712
+ "click",
713
+ (event) => {
714
+ const option = event.target?.closest(
715
+ '[role="option"]'
716
+ );
717
+ if (!option || release) {
718
+ return;
719
+ }
720
+ event.stopImmediatePropagation();
721
+ event.preventDefault();
722
+ if (!confirms) {
723
+ return;
724
+ }
725
+ const dialog = document.createElement("div");
726
+ dialog.setAttribute("role", "dialog");
727
+ dialog.setAttribute("aria-label", "Confirm address");
728
+ const footer = document.createElement("footer");
729
+ const button = document.createElement("button");
730
+ button.type = "button";
731
+ button.textContent = "Deliver here";
732
+ button.addEventListener("click", () => {
733
+ dialog.remove();
734
+ release = true;
735
+ option.click();
736
+ release = false;
737
+ });
738
+ footer.append(button);
739
+ dialog.append(footer);
740
+ document.body.append(dialog);
741
+ },
742
+ { capture: true }
743
+ );
744
+ }, fault === "address-confirm");
745
+ }
746
+ if (fault === "field-rows") {
747
+ await page.addInitScript(() => {
748
+ const fields = [
749
+ ["address", "[data-zappie-address-input]"],
750
+ ["phone", 'input[type="tel"]']
751
+ ];
752
+ new MutationObserver(() => {
753
+ for (const [field, selector] of fields) {
754
+ for (const input of document.querySelectorAll(
755
+ `${selector}:not([data-verify-row])`
756
+ )) {
757
+ input.setAttribute("data-verify-row", "");
758
+ input.hidden = true;
759
+ const row = document.createElement("button");
760
+ row.type = "button";
761
+ row.textContent = `Add your ${field}`;
762
+ row.setAttribute("data-zappie-field-open", field);
763
+ row.addEventListener("click", () => {
764
+ if (!document.cookie.includes("zappie_session=signed")) {
765
+ window.location.assign(
766
+ new URL(
767
+ `/_zappie/login?return=${encodeURIComponent(location.pathname)}`,
768
+ location.href
769
+ ).href
770
+ );
771
+ return;
772
+ }
773
+ row.remove();
774
+ input.hidden = false;
775
+ });
776
+ input.before(row);
777
+ }
778
+ }
779
+ }).observe(document, { childList: true, subtree: true });
780
+ });
781
+ }
411
782
  if (fault === "overlay-markers") {
412
783
  await page.addInitScript(() => {
413
784
  new MutationObserver(() => {
@@ -426,7 +797,32 @@ async function installFault(page, fault) {
426
797
  }).observe(document, { childList: true, subtree: true });
427
798
  });
428
799
  }
429
- if (fault === "bag-on-tap") {
800
+ if (fault === "bag-after-add" || fault === "bag-after-add-on-tap") {
801
+ await page.addInitScript(() => {
802
+ const sync = () => {
803
+ let count = 0;
804
+ try {
805
+ count = window.__zappie?.cart().count ?? 0;
806
+ } catch {
807
+ }
808
+ for (const node of document.querySelectorAll(
809
+ '[data-zappie-overlay-trigger="bag"]'
810
+ )) {
811
+ if (count === 0) {
812
+ node.style.setProperty("display", "none", "important");
813
+ } else {
814
+ node.style.removeProperty("display");
815
+ }
816
+ }
817
+ };
818
+ new MutationObserver(sync).observe(document, {
819
+ childList: true,
820
+ subtree: true
821
+ });
822
+ window.setInterval(sync, 50);
823
+ });
824
+ }
825
+ if (fault === "bag-on-tap" || fault === "bag-after-add-on-tap") {
430
826
  let release = () => {
431
827
  };
432
828
  let tapped = Promise.resolve();
@@ -462,24 +858,26 @@ async function installFault(page, fault) {
462
858
  );
463
859
  }
464
860
  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) {
861
+ const browser = page.context().browser();
862
+ const index = browser ? preparedPages.get(browser) ?? 0 : 0;
863
+ if (browser) {
864
+ preparedPages.set(browser, index + 1);
865
+ }
866
+ if (index > 0 && (index - 1) % 3 === 0) {
867
+ await page.addInitScript(() => {
868
+ document.addEventListener(
869
+ "click",
870
+ (event) => {
871
+ if (event.target instanceof Element && event.target.closest("[data-zappie-overlay-trigger]")) {
872
+ const until = performance.now() + 250;
873
+ while (performance.now() < until) {
874
+ }
477
875
  }
478
- }
479
- },
480
- { capture: true }
481
- );
482
- });
876
+ },
877
+ { capture: true }
878
+ );
879
+ });
880
+ }
483
881
  }
484
882
  if (fault === "late-hydration") {
485
883
  await page.route(
@@ -643,6 +1041,22 @@ async function applyPageFault(page, fault) {
643
1041
  }
644
1042
  });
645
1043
  }
1044
+ if (fault === "card-link") {
1045
+ await page.waitForFunction(() => {
1046
+ const link = document.querySelector("a[data-zappie-item]");
1047
+ return link !== null && Object.keys(link).some((key) => key.startsWith("__reactProps$"));
1048
+ });
1049
+ await page.locator("a[data-zappie-item]").evaluateAll((links) => {
1050
+ for (const link of links) {
1051
+ const id = link.getAttribute("data-zappie-item");
1052
+ const card = link.parentElement?.querySelector(
1053
+ `article[data-zappie-item="${id}"]`
1054
+ );
1055
+ link.textContent = card?.querySelector("h3")?.textContent ?? "Item";
1056
+ card?.removeAttribute("data-zappie-item");
1057
+ }
1058
+ });
1059
+ }
646
1060
  if (fault === "missing-marker") {
647
1061
  await page.locator("[data-zappie-item]").evaluateAll((nodes) => {
648
1062
  for (const node of nodes) {
@@ -878,11 +1292,11 @@ var v05 = {
878
1292
  "The test menu has no available item with required options",
879
1293
  "Keep one available configurable item in the test menu."
880
1294
  );
881
- const item = page.locator(`[data-zappie-item="${selected.id}"]`).first();
882
- await item.locator("xpath=..").getByRole("link", { name: "View details" }).click();
1295
+ await openItem(page, selected.id);
883
1296
  const dialog = page.getByRole("dialog");
884
1297
  await dialog.waitFor();
885
1298
  const required = dialog.locator('[data-zappie-option-group][aria-required="true"]').first();
1299
+ await required.waitFor({ timeout: 5e3 }).catch(() => void 0);
886
1300
  assertion(
887
1301
  await required.count(),
888
1302
  "required_group_missing",
@@ -891,23 +1305,41 @@ var v05 = {
891
1305
  );
892
1306
  const before = await debugValue(page, () => window.__zappie.cart().count);
893
1307
  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();
1308
+ const unsatisfied = dialog.locator(
1309
+ '[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
1310
+ );
1311
+ const blocked2 = await add.isDisabled();
1312
+ if (!blocked2) {
1313
+ const group = unsatisfied.first();
1314
+ const textBefore = (await group.innerText().catch(() => "")).trim();
1315
+ await add.click();
1316
+ await page.waitForTimeout(500);
1317
+ const added = await page.evaluate(
1318
+ (count) => window.__zappie.cart().count > count,
1319
+ before
1320
+ );
1321
+ assertion(
1322
+ !added && await dialog.isVisible(),
1323
+ "required_option_bypassed",
1324
+ "Add put the item in the cart before the shopper chose a required option",
1325
+ "Disable Add until the shopper completes every required option group, or keep the item open and flag the group when Add is tapped."
1326
+ );
1327
+ assertion(
1328
+ await group.evaluate(flagged, textBefore),
1329
+ "required_option_unflagged",
1330
+ "Tapping Add before a required choice did not flag the group",
1331
+ "Mark the group that needs a choice with aria-invalid or data-invalid and show a message saying why."
1332
+ );
1333
+ }
1334
+ for (let index = 0; index < 5 && (blocked2 ? await add.isDisabled() : await unsatisfied.count() > 0); index++) {
1335
+ const next = unsatisfied.first();
904
1336
  assertion(
905
- await unsatisfied.count(),
1337
+ await next.count(),
906
1338
  "required_option_unselectable",
907
1339
  "A required option group has no selectable choice",
908
1340
  "Keep an available choice in every required option group."
909
1341
  );
910
- await unsatisfied.locator('[data-zappie-option]:not([aria-disabled="true"])').first().click();
1342
+ await next.locator('[data-zappie-option]:not([aria-disabled="true"])').first().click();
911
1343
  }
912
1344
  assertion(
913
1345
  await add.isEnabled(),
@@ -935,6 +1367,22 @@ var v05 = {
935
1367
  );
936
1368
  }
937
1369
  };
1370
+ function flagged(group, textBefore) {
1371
+ const invalid = group.getAttribute("aria-invalid") === "true" || group.hasAttribute("data-invalid") || group.querySelector('[aria-invalid="true"], [data-invalid]') !== null;
1372
+ if (!invalid) {
1373
+ return false;
1374
+ }
1375
+ const shown = (element) => element instanceof HTMLElement && element.getClientRects().length > 0 && Boolean(element.innerText.trim());
1376
+ if (group instanceof HTMLElement && group.innerText.trim().length > textBefore.length) {
1377
+ return true;
1378
+ }
1379
+ const referenced = ["aria-describedby", "aria-errormessage"].flatMap((name) => group.getAttribute(name)?.split(/\s+/) ?? []).some((id) => shown(document.getElementById(id)));
1380
+ if (referenced) {
1381
+ return true;
1382
+ }
1383
+ const dialog = group.closest('[role="dialog"]') ?? document;
1384
+ return [...dialog.querySelectorAll('[role="alert"]')].some(shown);
1385
+ }
938
1386
 
939
1387
  // src/checks/v06-quote.ts
940
1388
  var v06 = {
@@ -946,9 +1394,20 @@ var v06 = {
946
1394
  if (new URL(page.url()).pathname !== "/") {
947
1395
  await open(page);
948
1396
  }
949
- const total = page.locator("[data-zappie-total]");
950
- if (!await total.isVisible()) {
951
- await page.locator("[data-zappie-cart-trigger]").first().click();
1397
+ const total = page.locator("[data-zappie-total]").filter({ visible: true });
1398
+ const bagOpen = page.locator('[data-zappie-overlay="bag"]').filter({ visible: true });
1399
+ if (!await total.count()) {
1400
+ if (!await bagOpen.count()) {
1401
+ await (await cartTrigger(page)).click();
1402
+ }
1403
+ await total.first().waitFor({ timeout: 2e3 }).catch(() => void 0);
1404
+ }
1405
+ if (!await total.count()) {
1406
+ 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();
1407
+ if (await handover.count()) {
1408
+ await handover.click();
1409
+ await page.waitForURL((url) => url.pathname.endsWith("/checkout"));
1410
+ }
952
1411
  }
953
1412
  await page.waitForFunction(
954
1413
  () => window.__zappie.events.some((entry) => entry.name === "quote"),
@@ -962,11 +1421,21 @@ var v06 = {
962
1421
  "The cart did not produce a quote within 2 seconds",
963
1422
  "Open the cart through the SDK and render Totals so it requests a quote."
964
1423
  );
965
- const rendered = await total.textContent();
1424
+ let expected = format.money(quote.total);
1425
+ let rendered = null;
1426
+ for (let attempt = 0; attempt < 20; attempt++) {
1427
+ const latest = await page.evaluate(() => window.__zappie.quote());
1428
+ expected = format.money((latest ?? quote).total);
1429
+ rendered = await total.first().textContent({ timeout: 5e3 }).catch(() => null);
1430
+ if (rendered === null || rendered.includes(expected)) {
1431
+ break;
1432
+ }
1433
+ await page.waitForTimeout(250);
1434
+ }
966
1435
  assertion(
967
- rendered?.includes(format.money(quote.total)),
1436
+ rendered?.includes(expected),
968
1437
  "total_mismatch",
969
- `Rendered total does not match ${format.money(quote.total)}`,
1438
+ `Rendered total does not match ${expected}`,
970
1439
  "Render quote.total with the SDK Money formatter."
971
1440
  );
972
1441
  }
@@ -1101,7 +1570,7 @@ var v07 = {
1101
1570
  await window.__zappie.cart.inject({ lines: lines2 });
1102
1571
  }, lines);
1103
1572
  await page.reload({ waitUntil: "domcontentloaded" });
1104
- await page.locator("[data-zappie-cart-trigger]").first().click();
1573
+ await (await cartTrigger(page)).click();
1105
1574
  const codes = unavailable ? ["price_changed", "item_unavailable"] : ["price_changed"];
1106
1575
  for (const code of codes) {
1107
1576
  const issue = page.locator(`[data-zappie-issue="${code}"]`).first();
@@ -1424,14 +1893,28 @@ var v10 = {
1424
1893
  "Render a delivery control with data-zappie-fulfilment=delivery."
1425
1894
  );
1426
1895
  await delivery.click();
1427
- const input = page.locator("[data-zappie-address-input]");
1428
- await input.waitFor();
1896
+ const input = await revealField(
1897
+ page,
1898
+ "address",
1899
+ page.locator("[data-zappie-address-input]"),
1900
+ () => delivery.click()
1901
+ );
1429
1902
  await input.fill(ADDRESS);
1430
1903
  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
- });
1904
+ if (!await addressStored(page, 3e3)) {
1905
+ const confirm = await dialogConfirm(
1906
+ page,
1907
+ "[data-zappie-address-confirm]"
1908
+ );
1909
+ assertion(
1910
+ confirm,
1911
+ "delivery_address_missing",
1912
+ "The selected address was not stored",
1913
+ "Store the resolved address on delivery fulfilment when the shopper picks it, or mark the control that confirms it with data-zappie-address-confirm."
1914
+ );
1915
+ await confirm.click();
1916
+ }
1917
+ await addressStored(page);
1435
1918
  const fulfilment = await debugValue(
1436
1919
  page,
1437
1920
  () => window.__zappie.cart().fulfilment
@@ -1475,12 +1958,30 @@ var v10 = {
1475
1958
  );
1476
1959
  }
1477
1960
  };
1961
+ async function addressStored(page, timeout) {
1962
+ return page.waitForFunction(
1963
+ () => {
1964
+ const fulfilment = window.__zappie.cart().fulfilment;
1965
+ return fulfilment?.type === "delivery" && Boolean(fulfilment.address.placeId);
1966
+ },
1967
+ void 0,
1968
+ timeout === void 0 ? {} : { timeout }
1969
+ ).then(
1970
+ () => true,
1971
+ (error) => {
1972
+ if (timeout === void 0) {
1973
+ throw error;
1974
+ }
1975
+ return false;
1976
+ }
1977
+ );
1978
+ }
1478
1979
 
1479
1980
  // src/checks/v13-accessibility.ts
1480
1981
  import AxeBuilder from "@axe-core/playwright";
1481
1982
 
1482
1983
  // src/checks/semantics.ts
1483
- var TOUCH_TARGET_PX = 44;
1984
+ var TOUCH_TARGET_PX = 24;
1484
1985
  async function semanticFindings(page, scope) {
1485
1986
  return page.evaluate(
1486
1987
  ({ scope: scope2, minimum }) => {
@@ -1583,11 +2084,28 @@ var semanticHints = {
1583
2084
  h1_count: "Give each page exactly one h1 that names it.",
1584
2085
  dialog_name_missing: "Name every dialog with aria-labelledby pointing at its title, or with aria-label.",
1585
2086
  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."
2087
+ touch_target_too_small: "Make every menu control at least 24\xD724 px on a phone, with padding or a larger hit area."
1587
2088
  };
1588
2089
 
1589
2090
  // src/checks/v13-accessibility.ts
1590
2091
  async function severeViolations(page) {
2092
+ await page.evaluate(
2093
+ () => new Promise(
2094
+ (resolve) => requestAnimationFrame(
2095
+ () => requestAnimationFrame(() => requestAnimationFrame(resolve))
2096
+ )
2097
+ )
2098
+ );
2099
+ await page.evaluate(
2100
+ () => Promise.race([
2101
+ Promise.all(
2102
+ document.getAnimations().filter(
2103
+ (animation) => animation.effect?.getComputedTiming().iterations !== Infinity
2104
+ ).map((animation) => animation.finished.catch(() => void 0))
2105
+ ),
2106
+ new Promise((resolve) => setTimeout(resolve, 2e3))
2107
+ ])
2108
+ );
1591
2109
  const analysis = await new AxeBuilder({ page }).analyze();
1592
2110
  return analysis.violations.filter(
1593
2111
  (entry) => entry.impact === "critical" || entry.impact === "serious"
@@ -1621,11 +2139,29 @@ async function holdsFocus(target2) {
1621
2139
  }
1622
2140
  return false;
1623
2141
  }
2142
+ var ADD = '[data-zappie-action="add-to-cart"]';
2143
+ async function keyboardReachesAddAndCart(page, itemId, cartInColumn) {
2144
+ const cart = cartInColumn ? [] : [await cartTriggerSelector(page)];
2145
+ if (await page.locator(ADD).count() || !itemId) {
2146
+ return controlsAreReachable(page, [ADD, ...cart]);
2147
+ }
2148
+ const opener = itemOpenerSelector(itemId);
2149
+ if (cart.length && !await controlsAreReachable(page, cart)) {
2150
+ return false;
2151
+ }
2152
+ await open(page);
2153
+ if (!await tabTo(page, opener, 200)) {
2154
+ return false;
2155
+ }
2156
+ await page.keyboard.press("Enter");
2157
+ await itemShown(page);
2158
+ return tabTo(page, ADD);
2159
+ }
1624
2160
  var v13 = {
1625
2161
  id: "V13",
1626
2162
  title: "Accessibility",
1627
2163
  viewports: "both",
1628
- async run({ page, context, options, fault }, viewport) {
2164
+ async run({ page, options, fault }, viewport) {
1629
2165
  await page.setViewportSize(viewport);
1630
2166
  await open(page);
1631
2167
  await applyPageFault(page, fault);
@@ -1643,21 +2179,23 @@ var v13 = {
1643
2179
  "No available item can be opened for accessibility checks",
1644
2180
  "Keep one available item in the test menu."
1645
2181
  );
1646
- const card = page.locator(`[data-zappie-item="${item.id}"]`).first().locator("xpath=..");
1647
- await card.getByRole("link", { name: "View details" }).click();
2182
+ const { focusTarget: card } = await openItem(page, item.id);
1648
2183
  violations.push(...await severeViolations(page));
1649
2184
  semantics.push(
1650
2185
  ...await semanticFindings(page, { page: false, phone: false })
1651
2186
  );
1652
- await page.getByRole("button", { name: "Close item" }).click();
2187
+ await closeItem(page);
1653
2188
  if (!await holdsFocus(card)) {
1654
2189
  semantics.push({
1655
2190
  code: "focus_not_returned",
1656
2191
  message: "Focus did not return to the item card after the item closed"
1657
2192
  });
1658
2193
  }
1659
- const cartTrigger = page.locator("[data-zappie-cart-trigger]").first();
1660
- await cartTrigger.click();
2194
+ const cart = await cartTrigger(page);
2195
+ const cartInColumn = viewport.width >= 768 && !await cart.isVisible();
2196
+ if (!cartInColumn) {
2197
+ await cart.click();
2198
+ }
1661
2199
  violations.push(...await severeViolations(page));
1662
2200
  semantics.push(
1663
2201
  ...await semanticFindings(page, { page: false, phone: false })
@@ -1671,8 +2209,10 @@ var v13 = {
1671
2209
  message || "Accessibility violation",
1672
2210
  "Fix every serious or critical axe violation in the menu, item dialog, cart and order page."
1673
2211
  );
1674
- await page.keyboard.press("Escape");
1675
- if (!await holdsFocus(cartTrigger)) {
2212
+ if (!cartInColumn) {
2213
+ await page.keyboard.press("Escape");
2214
+ }
2215
+ if (!cartInColumn && !await holdsFocus(cart)) {
1676
2216
  semantics.push({
1677
2217
  code: "focus_not_returned",
1678
2218
  message: "Focus did not return to the cart control after the cart closed"
@@ -1691,14 +2231,7 @@ var v13 = {
1691
2231
  )
1692
2232
  ].join(" ")
1693
2233
  );
1694
- await context.addCookies([
1695
- {
1696
- name: "zappie_session",
1697
- value: "signed",
1698
- domain: new URL(page.url()).hostname,
1699
- path: "/"
1700
- }
1701
- ]);
2234
+ await signIn(page);
1702
2235
  await open(page, "/orders/order_fixture");
1703
2236
  const order = await severeViolations(page);
1704
2237
  assertion(
@@ -1709,16 +2242,25 @@ var v13 = {
1709
2242
  );
1710
2243
  await open(page);
1711
2244
  assertion(
1712
- await controlsAreReachable(page, [
1713
- '[data-zappie-action="add-to-cart"]',
1714
- "[data-zappie-cart-trigger]"
1715
- ]),
2245
+ await keyboardReachesAddAndCart(
2246
+ page,
2247
+ simpleItem(await readMenu(options))?.id,
2248
+ cartInColumn
2249
+ ),
1716
2250
  "keyboard_marker_unreachable",
1717
2251
  "Keyboard navigation did not reach the Add and cart controls",
1718
2252
  "Keep the marked Add and cart controls in the tab order."
1719
2253
  );
1720
2254
  await open(page, "/checkout");
1721
- await page.locator('input[type="tel"]').fill("+2348000000001");
2255
+ const phone = await revealField(
2256
+ page,
2257
+ "phone",
2258
+ page.locator('input[type="tel"]')
2259
+ );
2260
+ await phone.fill("+2348000000001");
2261
+ if (await page.locator('[role="dialog"]').filter({ has: phone }).isVisible()) {
2262
+ await (await dialogConfirm(page, "[data-zappie-field-confirm]"))?.click();
2263
+ }
1722
2264
  await page.waitForFunction(
1723
2265
  () => !document.querySelector("[data-zappie-checkout]")?.disabled
1724
2266
  );
@@ -1738,7 +2280,7 @@ var v14 = {
1738
2280
  viewports: "mobile",
1739
2281
  async run({ page, metrics, fault, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
1740
2282
  await page.setViewportSize(viewport);
1741
- const cdp = await applyPhoneProfile(page, cpuThrottlingRate2);
2283
+ const { cdp } = await applyPhoneProfile(page, cpuThrottlingRate2);
1742
2284
  await page.addInitScript(() => {
1743
2285
  const state = { lcp: 0, cls: 0 };
1744
2286
  Object.assign(window, { __zappiePerformance: state });
@@ -1820,7 +2362,7 @@ var v15 = {
1820
2362
  viewports: "mobile",
1821
2363
  async run({ page, metrics, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
1822
2364
  await page.setViewportSize(viewport);
1823
- const cdp = await applyPhoneProfile(page, cpuThrottlingRate2);
2365
+ const { cdp } = await applyPhoneProfile(page, cpuThrottlingRate2);
1824
2366
  try {
1825
2367
  await page.goto("/", { waitUntil: "domcontentloaded" });
1826
2368
  await cdp.send("Network.setCacheDisabled", { cacheDisabled: false });
@@ -2101,6 +2643,41 @@ async function openSettledMenu(page) {
2101
2643
  network.stop();
2102
2644
  }
2103
2645
  }
2646
+ async function revealBagTrigger(page, itemId) {
2647
+ const bag = page.locator(`${triggerSelector("bag")}:visible`).first();
2648
+ if (!itemId || await bag.count()) {
2649
+ return;
2650
+ }
2651
+ const before = await page.evaluate(() => window.__zappie?.cart().count ?? 0).catch(() => 0);
2652
+ const add = '[data-zappie-action="add-to-cart"]';
2653
+ const quick = page.locator(`[data-zappie-item="${itemId}"] ${add}`).filter({ visible: true }).first();
2654
+ if (await quick.count()) {
2655
+ await quick.click();
2656
+ } else {
2657
+ const opener = page.locator(itemOpenerSelector(itemId)).filter({ visible: true }).first();
2658
+ if (!await opener.count()) {
2659
+ return;
2660
+ }
2661
+ await opener.click();
2662
+ const inItem = page.locator(
2663
+ `[role="dialog"] [data-zappie-item] ${add}:visible, main [data-zappie-item] ${add}:visible`
2664
+ ).first();
2665
+ if (!await inItem.waitFor({ timeout: 5e3 }).then(
2666
+ () => true,
2667
+ () => false
2668
+ )) {
2669
+ return;
2670
+ }
2671
+ await inItem.click();
2672
+ }
2673
+ await page.waitForFunction(
2674
+ (count) => (window.__zappie?.cart().count ?? 0) > count,
2675
+ before,
2676
+ { timeout: 5e3 }
2677
+ ).catch(() => void 0);
2678
+ await openSettledMenu(page);
2679
+ await bag.waitFor({ timeout: 5e3 }).catch(() => void 0);
2680
+ }
2104
2681
 
2105
2682
  // src/checks/v16-overlays-open-quickly.ts
2106
2683
  var OVERLAY_OPEN_BUDGET_MS = 100;
@@ -2110,64 +2687,89 @@ var median = (values) => {
2110
2687
  const sorted = [...values].sort((a, b) => a - b);
2111
2688
  return sorted[Math.floor(sorted.length / 2)];
2112
2689
  };
2690
+ async function firstOpen(fresh, viewport, name, cpuThrottlingRate2, itemId) {
2691
+ const { page } = fresh;
2692
+ try {
2693
+ await page.setViewportSize(viewport);
2694
+ const { cdp, rate } = await applyPhoneProfile(page, cpuThrottlingRate2);
2695
+ try {
2696
+ await openSettledMenu(page);
2697
+ if (name === "bag") {
2698
+ await revealBagTrigger(page, itemId);
2699
+ }
2700
+ const trigger = page.locator(`${triggerSelector(name)}:visible`).first();
2701
+ await trigger.evaluate(
2702
+ (node) => node.scrollIntoView({ block: "center", inline: "nearest" })
2703
+ );
2704
+ await armOverlayTimer(page, name, OPEN_TIMEOUT_MS);
2705
+ await trigger.click({ noWaitAfter: true });
2706
+ const ms = await readOverlayTimer(page);
2707
+ return ms === void 0 ? void 0 : { ms, rate };
2708
+ } finally {
2709
+ await cdp.detach().catch(() => void 0);
2710
+ }
2711
+ } finally {
2712
+ await fresh.close();
2713
+ }
2714
+ }
2113
2715
  var v16 = {
2114
2716
  id: "V16",
2115
2717
  title: "Overlays open quickly",
2116
2718
  viewports: "mobile",
2117
- async run({ page, metrics, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
2719
+ async run({ page, options, metrics, freshPage, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
2118
2720
  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);
2721
+ const itemId = simpleItem(await readMenu(options))?.id;
2722
+ await openSettledMenu(page);
2723
+ await revealBagTrigger(page, itemId);
2724
+ const names = (await overlayTriggers(page)).map(({ name }) => name);
2725
+ skipWithoutTriggers(names);
2726
+ const timings = {};
2727
+ const samples = {};
2728
+ const rates = {};
2729
+ const unopened = [];
2730
+ for (const name of names) {
2731
+ const opens = [];
2732
+ const opensRates = [];
2733
+ for (let sample = 0; sample < OVERLAY_OPEN_SAMPLES; sample++) {
2734
+ const open2 = await firstOpen(
2735
+ await freshPage(),
2736
+ viewport,
2737
+ name,
2738
+ cpuThrottlingRate2,
2739
+ itemId
2740
+ );
2741
+ if (open2 === void 0) {
2742
+ break;
2149
2743
  }
2744
+ opens.push(Math.round(open2.ms));
2745
+ opensRates.push(open2.rate);
2746
+ }
2747
+ if (opens.length < OVERLAY_OPEN_SAMPLES) {
2748
+ unopened.push(name);
2749
+ } else {
2750
+ samples[name] = opens;
2751
+ rates[name] = opensRates;
2752
+ timings[name] = median(opens);
2150
2753
  }
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
2754
  }
2755
+ metrics.overlayOpenMs = timings;
2756
+ metrics.overlayOpenSamplesMs = samples;
2757
+ metrics.overlayOpenRates = rates;
2758
+ assertion(
2759
+ unopened.length === 0,
2760
+ "overlay_not_opened",
2761
+ `No data-zappie-overlay painted after tapping ${unopened.map((name) => `the ${name} trigger`).join(", ")}`,
2762
+ `Put data-zappie-overlay with the trigger's name on the role="dialog" element that the trigger opens.`
2763
+ );
2764
+ const slow = Object.entries(timings).filter(
2765
+ ([, ms]) => ms > OVERLAY_OPEN_BUDGET_MS
2766
+ );
2767
+ assertion(
2768
+ slow.length === 0,
2769
+ "overlay_open_too_slow",
2770
+ `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(", ")}`,
2771
+ "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."
2772
+ );
2171
2773
  }
2172
2774
  };
2173
2775
 
@@ -2201,8 +2803,11 @@ var scrollLocked = (page) => page.evaluate(() => {
2201
2803
  };
2202
2804
  return held(document.documentElement) || held(document.body);
2203
2805
  });
2204
- async function checkOverlay(page, name) {
2806
+ async function checkOverlay(page, name, itemId) {
2205
2807
  await openSettledMenu(page);
2808
+ if (name === "bag") {
2809
+ await revealBagTrigger(page, itemId);
2810
+ }
2206
2811
  const trigger = page.locator(`${triggerSelector(name)}:visible`).first();
2207
2812
  await trigger.focus();
2208
2813
  await page.keyboard.press("Enter");
@@ -2280,15 +2885,26 @@ var v17 = {
2280
2885
  id: "V17",
2281
2886
  title: "Overlays manage focus",
2282
2887
  viewports: "mobile",
2283
- async run({ page }, viewport) {
2888
+ async run({ page, options, freshPage }, viewport) {
2284
2889
  await page.setViewportSize(viewport);
2890
+ const itemId = simpleItem(await readMenu(options))?.id;
2285
2891
  await page.goto("/", { waitUntil: "domcontentloaded" });
2286
2892
  await page.locator("[data-zappie-item]").first().waitFor();
2893
+ if (!await page.locator(`${triggerSelector("bag")}:visible`).count()) {
2894
+ await openSettledMenu(page);
2895
+ await revealBagTrigger(page, itemId);
2896
+ }
2287
2897
  const names = (await overlayTriggers(page)).map(({ name }) => name);
2288
2898
  skipWithoutTriggers(names);
2289
2899
  const findings = [];
2290
2900
  for (const name of names) {
2291
- findings.push(...await checkOverlay(page, name));
2901
+ const fresh = await freshPage();
2902
+ try {
2903
+ await fresh.page.setViewportSize(viewport);
2904
+ findings.push(...await checkOverlay(fresh.page, name, itemId));
2905
+ } finally {
2906
+ await fresh.close();
2907
+ }
2292
2908
  }
2293
2909
  const [first] = findings;
2294
2910
  if (first) {
@@ -2362,60 +2978,6 @@ function skippedChecks(suite) {
2362
2978
  ];
2363
2979
  }
2364
2980
 
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
2981
  // src/report.ts
2420
2982
  import { createPrivateKey, randomUUID, sign } from "crypto";
2421
2983
  var REPORT_SCHEMA = "https://registry.zappie.ai/v1/schema/verify-report.json";
@@ -2502,7 +3064,7 @@ async function reset(options) {
2502
3064
  }
2503
3065
 
2504
3066
  // src/version.ts
2505
- var PACKAGE_VERSION = true ? "0.2.2" : "0.0.0-development";
3067
+ var PACKAGE_VERSION = true ? "0.2.3" : "0.0.0-development";
2506
3068
 
2507
3069
  // src/runner.ts
2508
3070
  var VERSION = PACKAGE_VERSION;
@@ -2583,18 +3145,7 @@ async function execute(check, run2) {
2583
3145
  }
2584
3146
  }
2585
3147
  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);
3148
+ async function openContext(browser, options) {
2598
3149
  const context = await browser.newContext(browserOptions(options));
2599
3150
  if (options.standInForPlatform) {
2600
3151
  await context.route(
@@ -2617,10 +3168,29 @@ async function runVerify(options, fault) {
2617
3168
  }
2618
3169
  );
2619
3170
  }
2620
- await context.tracing.start({ screenshots: true, snapshots: true });
3171
+ return context;
3172
+ }
3173
+ async function preparePage(context, options, fault) {
2621
3174
  const page = await context.newPage();
2622
3175
  page.setDefaultTimeout(Math.min(options.timeoutMs ?? 2e4, 2e4));
2623
3176
  await installFault(page, fault);
3177
+ return page;
3178
+ }
3179
+ async function runVerify(options, fault) {
3180
+ if (!/^pk_test_.+/.test(options.key)) {
3181
+ throw new VerifyError(
3182
+ "live_key_refused",
3183
+ "Verify accepts only pk_test_ keys",
3184
+ "Create a test publishable key and rerun verify."
3185
+ );
3186
+ }
3187
+ const started = Date.now();
3188
+ const browser = await chromium.launch();
3189
+ const cpuBenchmark = await benchmarkCpu(browser).catch(() => void 0);
3190
+ const throttlingRate = cpuBenchmark === void 0 ? REFERENCE_CPU_THROTTLING_RATE : cpuThrottlingRate(cpuBenchmark);
3191
+ const context = await openContext(browser, options);
3192
+ await context.tracing.start({ screenshots: true, snapshots: true });
3193
+ const page = await preparePage(context, options, fault);
2624
3194
  const metrics = {};
2625
3195
  const run2 = {
2626
3196
  options,
@@ -2629,7 +3199,14 @@ async function runVerify(options, fault) {
2629
3199
  page,
2630
3200
  metrics,
2631
3201
  cpuThrottlingRate: throttlingRate,
2632
- ...fault ? { fault } : {}
3202
+ ...fault ? { fault } : {},
3203
+ async freshPage() {
3204
+ const fresh = await openContext(browser, options);
3205
+ return {
3206
+ page: await preparePage(fresh, options, fault),
3207
+ close: () => fresh.close()
3208
+ };
3209
+ }
2633
3210
  };
2634
3211
  const results = [await reset(options)];
2635
3212
  try {