@zappie/verify 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +7 -3
- package/dist/index.d.ts +14 -1
- package/dist/index.js +480 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/schema/verify-report.v1.json +5 -0
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
requestWithRetry,
|
|
10
10
|
resolveSourceMenu,
|
|
11
11
|
stableErrorCode,
|
|
12
|
+
sumMoney,
|
|
12
13
|
validateConfig,
|
|
13
14
|
validateLine
|
|
14
15
|
} from "./chunk-CW3HI5TY.js";
|
|
@@ -279,6 +280,14 @@ function signal(child, value) {
|
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// ../sdk/src/site-config/types.ts
|
|
284
|
+
var DEFAULT_BUDGETS = {
|
|
285
|
+
jsKbGz: 200,
|
|
286
|
+
cssKbGz: 50,
|
|
287
|
+
lcpMs: 2500,
|
|
288
|
+
tappableMs: 3e3
|
|
289
|
+
};
|
|
290
|
+
|
|
282
291
|
// src/project-config.ts
|
|
283
292
|
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
284
293
|
import path2 from "path";
|
|
@@ -303,10 +312,15 @@ async function readProjectSettings(cwd, { required }) {
|
|
|
303
312
|
}
|
|
304
313
|
const config = record(value) ? value : {};
|
|
305
314
|
const verify2 = record(config.verify) ? config.verify : {};
|
|
315
|
+
const budgets = record(config.budgets) ? config.budgets : {};
|
|
316
|
+
const override = record(budgets.override) ? budgets.override : {};
|
|
317
|
+
const tappableMs = budgets.tappableMs;
|
|
318
|
+
const explainedRaisedTappableBudget = typeof override.reason === "string" && override.reason.trim().length > 0;
|
|
306
319
|
const settings = {
|
|
307
320
|
...typeof config.store === "string" ? { store: config.store } : {},
|
|
308
321
|
...verify2.startPath !== void 0 ? { startPath: verify2.startPath } : {},
|
|
309
|
-
...verify2.cataloguePath !== void 0 ? { cataloguePath: verify2.cataloguePath } : {}
|
|
322
|
+
...verify2.cataloguePath !== void 0 ? { cataloguePath: verify2.cataloguePath } : {},
|
|
323
|
+
...typeof tappableMs === "number" && (tappableMs <= DEFAULT_BUDGETS.tappableMs || explainedRaisedTappableBudget) ? { tappableMs } : {}
|
|
310
324
|
};
|
|
311
325
|
if (required && settings.store === void 0) {
|
|
312
326
|
throw new VerifyError(
|
|
@@ -360,13 +374,16 @@ function isRoute(path4) {
|
|
|
360
374
|
function startPath(options) {
|
|
361
375
|
return options.startPath ?? DEFAULT_START_PATH;
|
|
362
376
|
}
|
|
377
|
+
function hasPlaceholders(path4) {
|
|
378
|
+
return path4.includes("{menu}") || path4.includes("{branch}");
|
|
379
|
+
}
|
|
363
380
|
function checkPaths(options) {
|
|
364
381
|
const { startPath: start, cataloguePath } = options;
|
|
365
382
|
if (start !== void 0 && !isRoute(start)) {
|
|
366
383
|
throw new VerifyError(
|
|
367
384
|
"invalid_start_path",
|
|
368
385
|
`The start path ${JSON.stringify(start)} is not a route on the site`,
|
|
369
|
-
"Set verify.startPath or --start to a route that starts with one /, such as /menu."
|
|
386
|
+
"Set verify.startPath or --start to a route that starts with one /, such as /menu or /menu?menu={menu}."
|
|
370
387
|
);
|
|
371
388
|
}
|
|
372
389
|
if (cataloguePath !== void 0 && (!isRoute(cataloguePath) || !cataloguePath.includes("{menu}"))) {
|
|
@@ -898,7 +915,7 @@ async function installFault(page, fault) {
|
|
|
898
915
|
return;
|
|
899
916
|
}
|
|
900
917
|
const group = original.closest('[role="dialog"]')?.querySelector(
|
|
901
|
-
'[data-zappie-option-group][aria-required="true"]:not(:has([data-state="checked"]))'
|
|
918
|
+
'[data-zappie-option-group]:is([data-zappie-required="true"], [aria-required="true"]):has([data-zappie-option]):not(:has([data-state="checked"]))'
|
|
902
919
|
);
|
|
903
920
|
if (flags && group) {
|
|
904
921
|
group.setAttribute("aria-invalid", "true");
|
|
@@ -1240,6 +1257,23 @@ ${await response.text()}`
|
|
|
1240
1257
|
});
|
|
1241
1258
|
});
|
|
1242
1259
|
}
|
|
1260
|
+
if (fault === "from-price") {
|
|
1261
|
+
await page.addInitScript(() => {
|
|
1262
|
+
new MutationObserver(() => {
|
|
1263
|
+
for (const node of document.querySelectorAll(
|
|
1264
|
+
'[data-zappie-item="item_jollof"] .z-kit-money'
|
|
1265
|
+
)) {
|
|
1266
|
+
if (node.textContent !== "From \u20A65,000") {
|
|
1267
|
+
node.textContent = "From \u20A65,000";
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}).observe(document, {
|
|
1271
|
+
childList: true,
|
|
1272
|
+
subtree: true,
|
|
1273
|
+
characterData: true
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1243
1277
|
if (fault === "add-stuck") {
|
|
1244
1278
|
await page.addInitScript(() => {
|
|
1245
1279
|
document.addEventListener(
|
|
@@ -1277,14 +1311,237 @@ ${await response.text()}`
|
|
|
1277
1311
|
}).observe(document, { childList: true, subtree: true });
|
|
1278
1312
|
});
|
|
1279
1313
|
}
|
|
1314
|
+
if (isMarkupFault(fault)) {
|
|
1315
|
+
await page.addInitScript(keepMarkupFault, fault);
|
|
1316
|
+
}
|
|
1280
1317
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1318
|
+
var markupFaults = [
|
|
1319
|
+
"from-price",
|
|
1320
|
+
"quick-add-via-item",
|
|
1321
|
+
"missing-marker",
|
|
1322
|
+
"hidden-marker-attribute",
|
|
1323
|
+
"hidden-marker-display",
|
|
1324
|
+
"hidden-marker-visibility",
|
|
1325
|
+
"hidden-marker-aria",
|
|
1326
|
+
"zero-size-marker",
|
|
1327
|
+
"tiny-marker",
|
|
1328
|
+
"clipped-marker",
|
|
1329
|
+
"clip-path-marker",
|
|
1330
|
+
"negative-coordinate-marker",
|
|
1331
|
+
"popular-name-only-markers",
|
|
1332
|
+
"marker-without-name",
|
|
1333
|
+
"uppercase-marker-name",
|
|
1334
|
+
"scrollable-markers",
|
|
1335
|
+
"content-visibility-auto-markers",
|
|
1336
|
+
"nfkc-marker-name",
|
|
1337
|
+
"entity-marker-name",
|
|
1338
|
+
"entity-apos-marker-name",
|
|
1339
|
+
"entity-gt-marker-name",
|
|
1340
|
+
"entity-lt-marker-name",
|
|
1341
|
+
"entity-quot-marker-name",
|
|
1342
|
+
"soft-hyphen-marker-name",
|
|
1343
|
+
"zero-width-marker-name",
|
|
1344
|
+
"axe"
|
|
1345
|
+
];
|
|
1346
|
+
function isMarkupFault(fault) {
|
|
1347
|
+
return markupFaults.includes(fault);
|
|
1348
|
+
}
|
|
1349
|
+
function keepMarkupFault(fault) {
|
|
1350
|
+
const outermost = "[data-zappie-item]:not([data-zappie-item] [data-zappie-item])";
|
|
1351
|
+
const markers = () => document.querySelectorAll("[data-zappie-item]");
|
|
1352
|
+
const setText = (node, text) => {
|
|
1353
|
+
if (node && node.textContent !== text) {
|
|
1354
|
+
node.textContent = text;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const styles = {
|
|
1358
|
+
"hidden-marker-display": [["display", "none"]],
|
|
1359
|
+
"hidden-marker-visibility": [["visibility", "hidden"]],
|
|
1360
|
+
"zero-size-marker": [
|
|
1361
|
+
["border", "0"],
|
|
1362
|
+
["height", "0"],
|
|
1363
|
+
["margin", "0"],
|
|
1364
|
+
["min-height", "0"],
|
|
1365
|
+
["min-width", "0"],
|
|
1366
|
+
["overflow", "hidden"],
|
|
1367
|
+
["padding", "0"],
|
|
1368
|
+
["width", "0"]
|
|
1369
|
+
],
|
|
1370
|
+
"tiny-marker": [
|
|
1371
|
+
["border", "0"],
|
|
1372
|
+
["height", "1px"],
|
|
1373
|
+
["min-height", "0"],
|
|
1374
|
+
["min-width", "0"],
|
|
1375
|
+
["overflow", "hidden"],
|
|
1376
|
+
["padding", "0"],
|
|
1377
|
+
["width", "1px"]
|
|
1378
|
+
],
|
|
1379
|
+
"clipped-marker": [
|
|
1380
|
+
["clip", "rect(0 0 0 0)"],
|
|
1381
|
+
["position", "absolute"]
|
|
1382
|
+
],
|
|
1383
|
+
"clip-path-marker": [["clip-path", "inset(50%)"]],
|
|
1384
|
+
"negative-coordinate-marker": [
|
|
1385
|
+
["left", "-10000px"],
|
|
1386
|
+
["position", "absolute"],
|
|
1387
|
+
["top", "-10000px"]
|
|
1388
|
+
],
|
|
1389
|
+
"uppercase-marker-name": [["text-transform", "uppercase"]]
|
|
1390
|
+
};
|
|
1391
|
+
const names = {
|
|
1392
|
+
"nfkc-marker-name": "Jollof rice",
|
|
1393
|
+
"entity-marker-name": "Jollof & rice",
|
|
1394
|
+
"entity-apos-marker-name": "Jollof ' rice",
|
|
1395
|
+
"entity-gt-marker-name": "Jollof > rice",
|
|
1396
|
+
"entity-lt-marker-name": "Jollof < rice",
|
|
1397
|
+
"entity-quot-marker-name": 'Jollof " rice',
|
|
1398
|
+
"soft-hyphen-marker-name": "Jollof rice",
|
|
1399
|
+
"zero-width-marker-name": "Jollof rice"
|
|
1400
|
+
};
|
|
1401
|
+
const apply = () => {
|
|
1402
|
+
const style = styles[fault];
|
|
1403
|
+
if (style) {
|
|
1404
|
+
for (const node of markers()) {
|
|
1405
|
+
for (const [property, value] of style) {
|
|
1406
|
+
node.style.setProperty(property, value, "important");
|
|
1407
|
+
}
|
|
1286
1408
|
}
|
|
1287
|
-
}
|
|
1409
|
+
}
|
|
1410
|
+
const name = names[fault];
|
|
1411
|
+
if (name !== void 0) {
|
|
1412
|
+
for (const node of document.querySelectorAll(
|
|
1413
|
+
'[data-zappie-item="item_jollof"]'
|
|
1414
|
+
)) {
|
|
1415
|
+
setText(node.querySelector("h1, h2, h3"), name);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (fault === "from-price") {
|
|
1419
|
+
for (const node of document.querySelectorAll(
|
|
1420
|
+
'[data-zappie-item="item_jollof"] .z-kit-money'
|
|
1421
|
+
)) {
|
|
1422
|
+
setText(node, "From \u20A65,000");
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (fault === "quick-add-via-item") {
|
|
1426
|
+
for (const node of document.querySelectorAll(
|
|
1427
|
+
'[data-zappie-item] [data-zappie-action="add-to-cart"]'
|
|
1428
|
+
)) {
|
|
1429
|
+
if (!node.closest('[data-zappie-overlay="item"], [role="dialog"]')) {
|
|
1430
|
+
node.remove();
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (fault === "missing-marker") {
|
|
1435
|
+
for (const node of markers()) {
|
|
1436
|
+
node.removeAttribute("data-zappie-item");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (fault === "hidden-marker-attribute") {
|
|
1440
|
+
for (const node of markers()) {
|
|
1441
|
+
if (!node.hasAttribute("hidden")) {
|
|
1442
|
+
node.setAttribute("hidden", "");
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (fault === "hidden-marker-aria") {
|
|
1447
|
+
for (const node of markers()) {
|
|
1448
|
+
if (node.getAttribute("aria-hidden") !== "true") {
|
|
1449
|
+
node.setAttribute("aria-hidden", "true");
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (fault === "marker-without-name") {
|
|
1454
|
+
for (const heading of document.querySelectorAll(
|
|
1455
|
+
"[data-zappie-item] h1, [data-zappie-item] h2, [data-zappie-item] h3"
|
|
1456
|
+
)) {
|
|
1457
|
+
heading.closest("[data-zappie-item]")?.before(heading);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (fault === "popular-name-only-markers") {
|
|
1461
|
+
const main = document.querySelector("main");
|
|
1462
|
+
const cards = [
|
|
1463
|
+
...document.querySelectorAll("article[data-zappie-item]")
|
|
1464
|
+
].map((card) => ({
|
|
1465
|
+
id: card.getAttribute("data-zappie-item") ?? "",
|
|
1466
|
+
href: card.querySelector('a[href*="/item/"]')?.href ?? "#",
|
|
1467
|
+
name: card.querySelector("h1, h2, h3")?.textContent ?? "Item"
|
|
1468
|
+
}));
|
|
1469
|
+
const key = JSON.stringify(cards);
|
|
1470
|
+
const current = document.querySelector("[data-verify-popular]");
|
|
1471
|
+
if (main && cards.length > 0 && (current?.getAttribute("data-verify-popular") !== key || main.firstElementChild !== current)) {
|
|
1472
|
+
current?.remove();
|
|
1473
|
+
const popular = document.createElement("section");
|
|
1474
|
+
popular.setAttribute("data-verify-popular", key);
|
|
1475
|
+
const heading = document.createElement("h2");
|
|
1476
|
+
heading.textContent = "Popular";
|
|
1477
|
+
popular.append(heading);
|
|
1478
|
+
for (const card of cards) {
|
|
1479
|
+
const link = document.createElement("a");
|
|
1480
|
+
link.href = card.href;
|
|
1481
|
+
link.setAttribute("data-zappie-item", card.id);
|
|
1482
|
+
link.textContent = card.name;
|
|
1483
|
+
popular.append(link);
|
|
1484
|
+
}
|
|
1485
|
+
main.prepend(popular);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (fault === "scrollable-markers") {
|
|
1489
|
+
for (const node of document.querySelectorAll(outermost)) {
|
|
1490
|
+
if (node.parentElement?.hasAttribute("data-verify-scroll-track")) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const scroller = document.createElement("div");
|
|
1494
|
+
scroller.style.cssText = "height: 1px; overflow: auto; width: 1px";
|
|
1495
|
+
const track = document.createElement("div");
|
|
1496
|
+
track.setAttribute("data-verify-scroll-track", "");
|
|
1497
|
+
track.style.cssText = "padding-left: 1000px; width: 2000px";
|
|
1498
|
+
node.before(scroller);
|
|
1499
|
+
scroller.append(track);
|
|
1500
|
+
track.append(node);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (fault === "content-visibility-auto-markers") {
|
|
1504
|
+
const nodes = [...document.querySelectorAll(outermost)].filter(
|
|
1505
|
+
(node) => !node.parentElement?.hasAttribute("data-verify-deferred")
|
|
1506
|
+
);
|
|
1507
|
+
if (nodes.length > 0) {
|
|
1508
|
+
let section = document.querySelector("[data-verify-deferred]");
|
|
1509
|
+
if (!section) {
|
|
1510
|
+
section = document.createElement("section");
|
|
1511
|
+
section.setAttribute("data-verify-deferred", "");
|
|
1512
|
+
section.style.cssText = "content-visibility:auto;contain-intrinsic-size:auto 1000px;margin-top:10000px";
|
|
1513
|
+
nodes[0]?.before(section);
|
|
1514
|
+
}
|
|
1515
|
+
section.append(...nodes);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (fault === "axe" && document.body && !document.querySelector("[data-verify-axe]")) {
|
|
1519
|
+
const button = document.createElement("button");
|
|
1520
|
+
button.setAttribute("data-verify-axe", "");
|
|
1521
|
+
button.style.color = "#fff";
|
|
1522
|
+
button.style.background = "#fff";
|
|
1523
|
+
document.body.append(button);
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
let started = false;
|
|
1527
|
+
window.__verifyMarkupFault = () => {
|
|
1528
|
+
started = true;
|
|
1529
|
+
apply();
|
|
1530
|
+
};
|
|
1531
|
+
new MutationObserver(() => {
|
|
1532
|
+
if (started) {
|
|
1533
|
+
apply();
|
|
1534
|
+
}
|
|
1535
|
+
}).observe(document, { childList: true, subtree: true });
|
|
1536
|
+
}
|
|
1537
|
+
function pageHydrated() {
|
|
1538
|
+
const hydrated2 = (node) => node !== null && Object.keys(node).some((key) => key.startsWith("__reactProps$"));
|
|
1539
|
+
return hydrated2(document.body) && [...document.querySelectorAll("[data-zappie-item]")].every(hydrated2);
|
|
1540
|
+
}
|
|
1541
|
+
async function applyPageFault(page, fault) {
|
|
1542
|
+
if (fault && isMarkupFault(fault)) {
|
|
1543
|
+
await page.waitForFunction(pageHydrated);
|
|
1544
|
+
await page.evaluate(() => window.__verifyMarkupFault?.());
|
|
1288
1545
|
}
|
|
1289
1546
|
if (fault === "card-link") {
|
|
1290
1547
|
await page.waitForFunction(() => {
|
|
@@ -1302,21 +1559,6 @@ async function applyPageFault(page, fault) {
|
|
|
1302
1559
|
}
|
|
1303
1560
|
});
|
|
1304
1561
|
}
|
|
1305
|
-
if (fault === "missing-marker") {
|
|
1306
|
-
await page.locator("[data-zappie-item]").evaluateAll((nodes) => {
|
|
1307
|
-
for (const node of nodes) {
|
|
1308
|
-
node.removeAttribute("data-zappie-item");
|
|
1309
|
-
}
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
if (fault === "axe") {
|
|
1313
|
-
await page.evaluate(() => {
|
|
1314
|
-
const button = document.createElement("button");
|
|
1315
|
-
button.style.color = "#fff";
|
|
1316
|
-
button.style.background = "#fff";
|
|
1317
|
-
document.body.append(button);
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
1562
|
}
|
|
1321
1563
|
function randomScript(name, length) {
|
|
1322
1564
|
let state = 1;
|
|
@@ -1376,8 +1618,43 @@ function catalogues(menu) {
|
|
|
1376
1618
|
};
|
|
1377
1619
|
}).filter((catalogue) => catalogue.items.length > 0);
|
|
1378
1620
|
}
|
|
1621
|
+
function fillStartPath(template, menu) {
|
|
1622
|
+
const first = catalogues(menu)[0]?.id ?? menu.menus?.[0]?.id ?? menu.id;
|
|
1623
|
+
return { path: cataloguePage(template, first, menu.branchId), menuId: first };
|
|
1624
|
+
}
|
|
1625
|
+
var startMenus = /* @__PURE__ */ new WeakMap();
|
|
1626
|
+
async function fillStartOptions(options, read = readMenu) {
|
|
1627
|
+
const template = options.startPath;
|
|
1628
|
+
if (template === void 0 || !hasPlaceholders(template)) {
|
|
1629
|
+
return options;
|
|
1630
|
+
}
|
|
1631
|
+
let filled;
|
|
1632
|
+
try {
|
|
1633
|
+
filled = fillStartPath(template, await read(options));
|
|
1634
|
+
} catch (failure) {
|
|
1635
|
+
if (failure instanceof VerifyError) {
|
|
1636
|
+
throw failure;
|
|
1637
|
+
}
|
|
1638
|
+
throw new VerifyError(
|
|
1639
|
+
"start_path_unfilled",
|
|
1640
|
+
`Verify could not read the menu to fill the start path ${template}: ${failure instanceof Error ? failure.message : String(failure)}`,
|
|
1641
|
+
"Check the store and key, or set a start path without {menu} or {branch}."
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
checkPaths({ startPath: filled.path });
|
|
1645
|
+
const result = { ...options, startPath: filled.path };
|
|
1646
|
+
if (template.includes("{menu}")) {
|
|
1647
|
+
startMenus.set(result, filled.menuId);
|
|
1648
|
+
}
|
|
1649
|
+
return result;
|
|
1650
|
+
}
|
|
1379
1651
|
function cataloguePages(options, menu) {
|
|
1380
1652
|
const { cataloguePath } = options;
|
|
1653
|
+
const startMenu = startMenus.get(options);
|
|
1654
|
+
const shown = !cataloguePath && startMenu !== void 0 ? catalogues(menu).find((catalogue) => catalogue.id === startMenu) : void 0;
|
|
1655
|
+
if (shown) {
|
|
1656
|
+
return [{ path: startPath(options), items: shown.items, name: shown.name }];
|
|
1657
|
+
}
|
|
1381
1658
|
const pages = cataloguePath ? catalogues(menu) : [];
|
|
1382
1659
|
if (!cataloguePath || pages.length === 0) {
|
|
1383
1660
|
return [{ path: startPath(options), items: sortedItems(menu) }];
|
|
@@ -1395,6 +1672,31 @@ function itemPath(options, menu, itemId) {
|
|
|
1395
1672
|
return holder && options.cataloguePath ? cataloguePage(options.cataloguePath, holder.id, menu.branchId) : startPath(options);
|
|
1396
1673
|
}
|
|
1397
1674
|
|
|
1675
|
+
// ../sdk/src/pricing/index.ts
|
|
1676
|
+
var cheapest = (prices) => prices.filter((price) => price.amountMinor >= 0).sort((a, b) => a.amountMinor - b.amountMinor)[0];
|
|
1677
|
+
function startingPrice(item) {
|
|
1678
|
+
const available = item.variants.filter((variant) => variant.isAvailable);
|
|
1679
|
+
const base = cheapest(
|
|
1680
|
+
(available.length ? available : item.variants).map(
|
|
1681
|
+
(variant) => variant.price
|
|
1682
|
+
)
|
|
1683
|
+
) ?? item.price;
|
|
1684
|
+
return sumMoney(
|
|
1685
|
+
item.optionGroups.flatMap(
|
|
1686
|
+
(group) => !group.isAttribute && group.required && group.kind === "select" ? cheapest(group.choices.map((choice) => choice.price)) ?? [] : []
|
|
1687
|
+
),
|
|
1688
|
+
base
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
function showsFromPrice(item) {
|
|
1692
|
+
const prices = [
|
|
1693
|
+
...new Set(
|
|
1694
|
+
item.variants.filter((variant) => variant.isAvailable).map((variant) => variant.price.amountMinor)
|
|
1695
|
+
)
|
|
1696
|
+
];
|
|
1697
|
+
return prices.length > 1 || prices.length === 1 && startingPrice(item).amountMinor > prices[0];
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1398
1700
|
// src/checks/menu-match.ts
|
|
1399
1701
|
function menuCoverage(items, shownIds) {
|
|
1400
1702
|
const available = items.filter((item) => item.isAvailable);
|
|
@@ -1408,6 +1710,16 @@ function menuCoverage(items, shownIds) {
|
|
|
1408
1710
|
function priceMatches(text, formatted) {
|
|
1409
1711
|
return text?.includes(formatted) ?? false;
|
|
1410
1712
|
}
|
|
1713
|
+
function shownPrices(item) {
|
|
1714
|
+
const prices = [format.money(item.price)];
|
|
1715
|
+
if (showsFromPrice(item)) {
|
|
1716
|
+
const from = format.money(startingPrice(item));
|
|
1717
|
+
if (from !== prices[0]) {
|
|
1718
|
+
prices.push(from);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return prices;
|
|
1722
|
+
}
|
|
1411
1723
|
|
|
1412
1724
|
// src/checks/v03-menu.ts
|
|
1413
1725
|
var v03 = {
|
|
@@ -1417,8 +1729,9 @@ var v03 = {
|
|
|
1417
1729
|
async run({ page, options, fault }, viewport) {
|
|
1418
1730
|
const menu = await readMenu(options);
|
|
1419
1731
|
await page.setViewportSize(viewport);
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1732
|
+
const pages = cataloguePages(options, menu);
|
|
1733
|
+
const severalMenus = !options.cataloguePath && pages[0]?.name === void 0 && catalogues(menu).length > 1;
|
|
1734
|
+
for (const catalogue of pages) {
|
|
1422
1735
|
await open(page, catalogue.path);
|
|
1423
1736
|
await applyPageFault(page, fault);
|
|
1424
1737
|
await judgeCatalogue(
|
|
@@ -1433,24 +1746,46 @@ var v03 = {
|
|
|
1433
1746
|
async function judgeCatalogue(page, items, menuName, severalMenus) {
|
|
1434
1747
|
const available = items.filter((item) => item.isAvailable);
|
|
1435
1748
|
const shownIds = /* @__PURE__ */ new Set();
|
|
1749
|
+
const markerText = /* @__PURE__ */ new Map();
|
|
1436
1750
|
for (const item of available) {
|
|
1437
1751
|
const marker = page.locator(`[data-zappie-item="${item.id}"]`);
|
|
1438
|
-
|
|
1752
|
+
const text = await visibleMarkerText(marker);
|
|
1753
|
+
markerText.set(item.id, text);
|
|
1754
|
+
const prices = shownPrices(item);
|
|
1755
|
+
if (text.some(
|
|
1756
|
+
(value) => nameMatches(value, item.name) && prices.some((price) => priceMatches(value, normalizeText(price)))
|
|
1757
|
+
) || await marker.count() === 0 && await page.getByText(item.name, { exact: true }).filter({ visible: true }).count() > 0) {
|
|
1439
1758
|
shownIds.add(item.id);
|
|
1440
1759
|
}
|
|
1441
1760
|
}
|
|
1761
|
+
for (const item of available) {
|
|
1762
|
+
if (shownIds.has(item.id)) {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const text = markerText.get(item.id)?.find((value) => nameMatches(value, item.name));
|
|
1766
|
+
if (text === void 0) {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
const prices = shownPrices(item);
|
|
1770
|
+
assertion(
|
|
1771
|
+
prices.some((price) => priceMatches(text, normalizeText(price))),
|
|
1772
|
+
"price_mismatch",
|
|
1773
|
+
`${item.name} does not show ${prices.join(" or From ")}`,
|
|
1774
|
+
"Render prices with format.money from @zappie/sdk, and a From price with startingPrice from @zappie/sdk/pricing."
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1442
1777
|
const coverage = menuCoverage(items, shownIds);
|
|
1443
1778
|
assertion(
|
|
1444
1779
|
coverage.passes || severalMenus,
|
|
1445
1780
|
"menu_mismatch",
|
|
1446
1781
|
`The ${menuName} shows ${coverage.shown} of ${coverage.available} available items`,
|
|
1447
|
-
"Render at least 95% of available menu items with data-zappie-item markers or visible names."
|
|
1782
|
+
"Render at least 95% of available menu items with visible data-zappie-item markers that contain their name and price, or visible names when no marker exists."
|
|
1448
1783
|
);
|
|
1449
1784
|
assertion(
|
|
1450
1785
|
coverage.passes,
|
|
1451
1786
|
"menu_mismatch",
|
|
1452
1787
|
`The ${menuName} shows ${coverage.shown} of ${coverage.available} available items`,
|
|
1453
|
-
"Render at least 95% of available menu items with data-zappie-item markers or visible names. If each menu has its own page, set verify.cataloguePath, such as /menu?menu={menu}."
|
|
1788
|
+
"Render at least 95% of available menu items with visible data-zappie-item markers that contain their name and price, or visible names when no marker exists. If each menu has its own page, set verify.cataloguePath, such as /menu?menu={menu}."
|
|
1454
1789
|
);
|
|
1455
1790
|
assertion(
|
|
1456
1791
|
await page.locator("[data-zappie-item]").count(),
|
|
@@ -1471,15 +1806,95 @@ async function judgeCatalogue(page, items, menuName, severalMenus) {
|
|
|
1471
1806
|
);
|
|
1472
1807
|
}
|
|
1473
1808
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1809
|
+
}
|
|
1810
|
+
function nameMatches(text, name) {
|
|
1811
|
+
return text.includes(normalizeText(decodeBasicEntities(name)));
|
|
1812
|
+
}
|
|
1813
|
+
function normalizeText(value) {
|
|
1814
|
+
return value.normalize("NFKC").replace(/[\u00ad\u200b-\u200d\u2060\ufeff]/g, "").replace(/\s+/g, " ").trim().toLocaleLowerCase();
|
|
1815
|
+
}
|
|
1816
|
+
function decodeBasicEntities(value) {
|
|
1817
|
+
const entities = {
|
|
1818
|
+
amp: "&",
|
|
1819
|
+
apos: "'",
|
|
1820
|
+
gt: ">",
|
|
1821
|
+
lt: "<",
|
|
1822
|
+
quot: '"'
|
|
1823
|
+
};
|
|
1824
|
+
return value.replace(
|
|
1825
|
+
/&(amp|apos|gt|lt|quot);/gi,
|
|
1826
|
+
(entity) => entities[entity.slice(1, -1).toLowerCase()] ?? entity
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
async function visibleMarkerText(marker) {
|
|
1830
|
+
const text = await marker.evaluateAll(
|
|
1831
|
+
(nodes) => nodes.flatMap((node) => {
|
|
1832
|
+
for (let current = node; current; current = current.parentElement) {
|
|
1833
|
+
const ariaHidden = current.getAttribute("aria-hidden")?.trim().toLowerCase();
|
|
1834
|
+
const style = getComputedStyle(current);
|
|
1835
|
+
if (current.hasAttribute("hidden") || ariaHidden === "true" || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.contentVisibility === "hidden" || fullyClipped(current, style)) {
|
|
1836
|
+
return [];
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const hasBox = [...node.getClientRects()].some((box) => {
|
|
1840
|
+
const documentRight = box.right + window.scrollX;
|
|
1841
|
+
const documentBottom = box.bottom + window.scrollY;
|
|
1842
|
+
return box.width >= 2 && box.height >= 2 && documentRight > 0 && documentBottom > 0;
|
|
1843
|
+
});
|
|
1844
|
+
if (!hasBox) {
|
|
1845
|
+
return [];
|
|
1846
|
+
}
|
|
1847
|
+
const text2 = node.textContent;
|
|
1848
|
+
return text2 === null ? [] : [text2];
|
|
1849
|
+
function fullyClipped(element, style) {
|
|
1850
|
+
const box = element.getBoundingClientRect();
|
|
1851
|
+
const clip = style.clip.match(
|
|
1852
|
+
/^rect\((auto|-?[\d.]+px)[, ]+(auto|-?[\d.]+px)[, ]+(auto|-?[\d.]+px)[, ]+(auto|-?[\d.]+px)\)$/
|
|
1853
|
+
);
|
|
1854
|
+
if (clip) {
|
|
1855
|
+
const top2 = length(clip[1], 0);
|
|
1856
|
+
const right2 = length(clip[2], box.width);
|
|
1857
|
+
const bottom2 = length(clip[3], box.height);
|
|
1858
|
+
const left2 = length(clip[4], 0);
|
|
1859
|
+
if (right2 <= left2 || bottom2 <= top2) {
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
const clipPath = style.clipPath;
|
|
1864
|
+
if (/^(circle|ellipse)\(0(?:px|%)?(?:\s|\))/i.test(clipPath)) {
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
const inset = clipPath.match(/^inset\(([^)]+)\)/i);
|
|
1868
|
+
if (!inset) {
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
const values = inset[1].split(/\s+/).slice(0, 4).map(
|
|
1872
|
+
(value, index) => insetLength(value, index % 2 ? box.width : box.height)
|
|
1873
|
+
);
|
|
1874
|
+
if (values.some((value) => value === void 0)) {
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
const top = values[0];
|
|
1878
|
+
const right = values.length === 1 ? top : values[1];
|
|
1879
|
+
const bottom = values.length < 3 ? top : values[2];
|
|
1880
|
+
const left = values.length < 4 ? right : values[3];
|
|
1881
|
+
return top + bottom >= box.height || right + left >= box.width;
|
|
1882
|
+
}
|
|
1883
|
+
function length(value, fallback) {
|
|
1884
|
+
return value === "auto" || value === void 0 ? fallback : Number.parseFloat(value);
|
|
1885
|
+
}
|
|
1886
|
+
function insetLength(value, dimension) {
|
|
1887
|
+
if (value.endsWith("%")) {
|
|
1888
|
+
return Number.parseFloat(value) / 100 * dimension;
|
|
1889
|
+
}
|
|
1890
|
+
if (value.endsWith("px") || value === "0") {
|
|
1891
|
+
return Number.parseFloat(value);
|
|
1892
|
+
}
|
|
1893
|
+
return void 0;
|
|
1894
|
+
}
|
|
1895
|
+
})
|
|
1896
|
+
);
|
|
1897
|
+
return text.map(normalizeText);
|
|
1483
1898
|
}
|
|
1484
1899
|
|
|
1485
1900
|
// src/checks/v04-add-simple.ts
|
|
@@ -1571,6 +1986,7 @@ var v04 = {
|
|
|
1571
1986
|
};
|
|
1572
1987
|
|
|
1573
1988
|
// src/checks/v05-add-options.ts
|
|
1989
|
+
var REQUIRED_GROUP = '[data-zappie-option-group]:is([data-zappie-required="true"], [aria-required="true"])';
|
|
1574
1990
|
var v05 = {
|
|
1575
1991
|
id: "V05",
|
|
1576
1992
|
title: "Add an item with required options",
|
|
@@ -1589,22 +2005,22 @@ var v05 = {
|
|
|
1589
2005
|
await openItem(page, selected.id);
|
|
1590
2006
|
const dialog = page.getByRole("dialog");
|
|
1591
2007
|
await dialog.waitFor();
|
|
1592
|
-
const required = dialog.locator(
|
|
2008
|
+
const required = dialog.locator(REQUIRED_GROUP).first();
|
|
1593
2009
|
await required.waitFor({ timeout: 5e3 }).catch(() => void 0);
|
|
1594
2010
|
assertion(
|
|
1595
2011
|
await required.count(),
|
|
1596
2012
|
"required_group_missing",
|
|
1597
2013
|
"The item dialog has no marked required option group",
|
|
1598
|
-
"
|
|
2014
|
+
"Mark each option group with data-zappie-option-group and each required one with data-zappie-required, as OptionGroup from @zappie/foundations does."
|
|
1599
2015
|
);
|
|
1600
2016
|
const before = await debugValue(page, () => window.__zappie.cart().count);
|
|
1601
2017
|
const add = dialog.locator('[data-zappie-action="add-to-cart"]');
|
|
1602
2018
|
const unsatisfied = dialog.locator(
|
|
1603
|
-
|
|
2019
|
+
`${REQUIRED_GROUP}:has([data-zappie-option]):not(:has([data-state="checked"]))`
|
|
1604
2020
|
);
|
|
1605
2021
|
const blocked2 = await add.isDisabled();
|
|
1606
|
-
|
|
1607
|
-
|
|
2022
|
+
const group = unsatisfied.first();
|
|
2023
|
+
if (!blocked2 && await group.count() > 0) {
|
|
1608
2024
|
const textBefore = (await group.innerText().catch(() => "")).trim();
|
|
1609
2025
|
await add.click();
|
|
1610
2026
|
await page.waitForTimeout(500);
|
|
@@ -2936,13 +3352,20 @@ var v14 = {
|
|
|
2936
3352
|
};
|
|
2937
3353
|
|
|
2938
3354
|
// src/checks/v15-tappable-product-cards.ts
|
|
2939
|
-
var TAPPABLE_BUDGET_MS = 3e3;
|
|
2940
3355
|
var OPEN_BUDGET_MS = 300;
|
|
3356
|
+
function tappableBudgetMs(configured) {
|
|
3357
|
+
if (configured === void 0 || !Number.isFinite(configured)) {
|
|
3358
|
+
return DEFAULT_BUDGETS.tappableMs;
|
|
3359
|
+
}
|
|
3360
|
+
return Math.min(5e3, Math.max(DEFAULT_BUDGETS.tappableMs, configured));
|
|
3361
|
+
}
|
|
2941
3362
|
var v15 = {
|
|
2942
3363
|
id: "V15",
|
|
2943
3364
|
title: "Tappable product cards",
|
|
2944
3365
|
viewports: "mobile",
|
|
2945
3366
|
async run({ page, options, metrics, cpuThrottlingRate: cpuThrottlingRate2 }, viewport) {
|
|
3367
|
+
const limitMs = tappableBudgetMs(options.tappableMs);
|
|
3368
|
+
metrics.productCardTappableLimitMs = limitMs;
|
|
2946
3369
|
await page.setViewportSize(viewport);
|
|
2947
3370
|
const { cdp } = await applyPhoneProfile(page, cpuThrottlingRate2);
|
|
2948
3371
|
try {
|
|
@@ -2987,10 +3410,10 @@ var v15 = {
|
|
|
2987
3410
|
const tappableMs = await page.evaluate(() => performance.now());
|
|
2988
3411
|
metrics.productCardTappableMs = Math.round(tappableMs);
|
|
2989
3412
|
assertion(
|
|
2990
|
-
tappableMs <=
|
|
3413
|
+
tappableMs <= limitMs,
|
|
2991
3414
|
"product_card_tappable_too_late",
|
|
2992
|
-
`The first product card became tappable in ${Math.round(tappableMs)} ms`,
|
|
2993
|
-
"Reduce critical scripts, fonts and high-priority images until product cards
|
|
3415
|
+
`The first product card became tappable in ${Math.round(tappableMs)} ms; the site limit is ${limitMs} ms`,
|
|
3416
|
+
"Reduce critical scripts, fonts and high-priority images until product cards meet the site's tappable limit."
|
|
2994
3417
|
);
|
|
2995
3418
|
assertion(
|
|
2996
3419
|
await prefetched,
|
|
@@ -3547,7 +3970,7 @@ async function reset(options) {
|
|
|
3547
3970
|
}
|
|
3548
3971
|
|
|
3549
3972
|
// src/version.ts
|
|
3550
|
-
var PACKAGE_VERSION = true ? "0.3.
|
|
3973
|
+
var PACKAGE_VERSION = true ? "0.3.2" : "0.0.0-development";
|
|
3551
3974
|
|
|
3552
3975
|
// src/runner.ts
|
|
3553
3976
|
var VERSION = PACKAGE_VERSION;
|
|
@@ -3672,15 +4095,16 @@ async function preparePage(context, options, fault) {
|
|
|
3672
4095
|
await installFault(page, fault);
|
|
3673
4096
|
return page;
|
|
3674
4097
|
}
|
|
3675
|
-
async function runVerify(
|
|
3676
|
-
if (!/^pk_test_.+/.test(
|
|
4098
|
+
async function runVerify(input, fault) {
|
|
4099
|
+
if (!/^pk_test_.+/.test(input.key)) {
|
|
3677
4100
|
throw new VerifyError(
|
|
3678
4101
|
"live_key_refused",
|
|
3679
4102
|
"Verify accepts only pk_test_ keys",
|
|
3680
4103
|
"Create a test publishable key and rerun verify."
|
|
3681
4104
|
);
|
|
3682
4105
|
}
|
|
3683
|
-
checkPaths(
|
|
4106
|
+
checkPaths(input);
|
|
4107
|
+
const options = await fillStartOptions(input);
|
|
3684
4108
|
const wanted = options.only ? expandOnly(options.only, knownCheckIds()) : void 0;
|
|
3685
4109
|
const started = Date.now();
|
|
3686
4110
|
const executablePath = browserExecutable(options);
|
|
@@ -3747,6 +4171,7 @@ async function runVerify(options, fault) {
|
|
|
3747
4171
|
...cpuBenchmark === void 0 ? {} : { cpuBenchmark },
|
|
3748
4172
|
cpuThrottlingRate: throttlingRate,
|
|
3749
4173
|
startPath: startPath(options),
|
|
4174
|
+
...options.startPath !== input.startPath ? { startPathTemplate: input.startPath } : {},
|
|
3750
4175
|
...options.cataloguePath ? { cataloguePath: options.cataloguePath } : {}
|
|
3751
4176
|
},
|
|
3752
4177
|
startedAt: new Date(started).toISOString(),
|
|
@@ -3774,16 +4199,17 @@ async function runVerify(options, fault) {
|
|
|
3774
4199
|
|
|
3775
4200
|
// src/project.ts
|
|
3776
4201
|
async function verifyProject(options) {
|
|
3777
|
-
const settings = options.store && options.startPath && options.cataloguePath ? {} : await readProjectSettings(options.cwd, { required: !options.store });
|
|
4202
|
+
const settings = options.store && options.startPath && options.cataloguePath && options.tappableMs !== void 0 ? {} : await readProjectSettings(options.cwd, { required: !options.store });
|
|
3778
4203
|
const store = options.store ?? settings.store;
|
|
3779
4204
|
const startPath2 = options.startPath ?? settings.startPath;
|
|
3780
4205
|
const warnings = "ignored" in settings && settings.ignored ? [
|
|
3781
4206
|
{
|
|
3782
4207
|
code: "config_settings_ignored",
|
|
3783
|
-
message: `${settings.ignored}, so verify ran without its
|
|
4208
|
+
message: `${settings.ignored}, so verify ran without its route settings or tappable budget. Fix zappie.config.ts before rerunning verify.`
|
|
3784
4209
|
}
|
|
3785
4210
|
] : [];
|
|
3786
4211
|
const cataloguePath = options.cataloguePath ?? settings.cataloguePath;
|
|
4212
|
+
const tappableMs = options.tappableMs ?? settings.tappableMs;
|
|
3787
4213
|
let preview;
|
|
3788
4214
|
try {
|
|
3789
4215
|
preview = options.url ? void 0 : await startLocalPreview({
|
|
@@ -3804,6 +4230,7 @@ async function verifyProject(options) {
|
|
|
3804
4230
|
...options.only ? { only: options.only } : {},
|
|
3805
4231
|
...startPath2 !== void 0 ? { startPath: startPath2 } : {},
|
|
3806
4232
|
...cataloguePath !== void 0 ? { cataloguePath } : {},
|
|
4233
|
+
...tappableMs !== void 0 ? { tappableMs } : {},
|
|
3807
4234
|
...warnings.length ? { warnings } : {},
|
|
3808
4235
|
...options.viewports ? { viewports: options.viewports } : {},
|
|
3809
4236
|
...options.artifactsDir ? { artifactsDir: options.artifactsDir } : {},
|