test(05-05): add five route capture legs racing ARAHARAH-ROUTE (RED)
- SIGNALS + seen-map + diagnostics gain ARAHARAH-ROUTE; waitMode 'route' races the state token, asserts expectState + expectText DOM text, and applies the PX-02 theme-slot honesty check on themed legs - captureLeg gains an interact(page) pre-shot hook (the alt leg's chip tap) - five legs data-driven from the committed corpus (demoPair picked at runtime — never a hardcoded pair literal): fixture / alt / pending / dark / route-gallery-compare (composer refactored into composeCompare) - expected list grows 8 → 13 - RED by construction: the ?fixpair knob does not exist yet, so the default fix sits outside the demo pair's 200m tolerance → the drawn legs must fail with state=pending
This commit is contained in:
+183
-12
@@ -27,7 +27,12 @@
|
|||||||
* shell-denied.png denied card + ASK VIA CHAT (GEO-02 entry point)
|
* shell-denied.png denied card + ASK VIA CHAT (GEO-02 entry point)
|
||||||
* shell-chat-pin.png LIVE via the chat-pin relay (?pin=1, stubbed /api/location)
|
* shell-chat-pin.png LIVE via the chat-pin relay (?pin=1, stubbed /api/location)
|
||||||
* shell-dest-preload.png the Mini App booted WITH a preloaded destination (?dest=<id>)
|
* shell-dest-preload.png the Mini App booted WITH a preloaded destination (?dest=<id>)
|
||||||
|
* shell-route-fixture.png route drawn from the demo pair's pinned origin (?fixpair=, 05-05)
|
||||||
|
* shell-route-alt.png alternative selected via chip tap (ROUTE {n}/{N} re-render, 05-05)
|
||||||
|
* shell-route-pending.png the honest ROUTE // ROUTER pending card (D-03, 05-05)
|
||||||
|
* shell-route-dark.png route drawn, Telegram dark theme (PX-02 route half, 05-05)
|
||||||
* gallery-compare.png DSGN-02 side-by-side vs navigate_map_overview/screen.png
|
* gallery-compare.png DSGN-02 side-by-side vs navigate_map_overview/screen.png
|
||||||
|
* route-gallery-compare.png DSGN-02 route fidelity artifact (05-05, same composer)
|
||||||
*
|
*
|
||||||
* Also runs the GEO-03 runtime probe in-page after boot-to-live and fails
|
* Also runs the GEO-03 runtime probe in-page after boot-to-live and fails
|
||||||
* the run on any non-empty storage surface.
|
* the run on any non-empty storage surface.
|
||||||
@@ -163,7 +168,24 @@ log.push(`serving web/dist-harness/ → ${base}/`);
|
|||||||
|
|
||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST'];
|
const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST', 'ARAHARAH-ROUTE'];
|
||||||
|
|
||||||
|
/* The five route legs (05-05) are DATA-DRIVEN from the committed corpus:
|
||||||
|
* the demo pair is picked AT RUNTIME — never a hardcoded pair literal.
|
||||||
|
* RT10 guarantees >= 4 multi-route pairs, so the find succeeds; the
|
||||||
|
* fallback keeps the legs runnable in the degenerate single-route case. */
|
||||||
|
const routePairs = JSON.parse(
|
||||||
|
readFileSync(resolve(REPOROOT, 'routing/src/data/route-fixtures.json'), 'utf8'),
|
||||||
|
).pairs;
|
||||||
|
const demoPair = routePairs.find((p) => p.routes.length >= 2) ?? routePairs[0];
|
||||||
|
const routeGazetteer = JSON.parse(
|
||||||
|
readFileSync(resolve(REPOROOT, 'search/src/data/kl-gazetteer.json'), 'utf8'),
|
||||||
|
);
|
||||||
|
const routeDestEntry = routeGazetteer.entries.find((e) => e.id === demoPair.destinationId);
|
||||||
|
if (routeDestEntry === undefined) {
|
||||||
|
console.error(`[capture] FAILED demo pair destination "${demoPair.destinationId}" is not a gazetteer id`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* One capture leg = one context + one screenshot. The Phase-1 READY-race
|
* One capture leg = one context + one screenshot. The Phase-1 READY-race
|
||||||
@@ -174,8 +196,15 @@ const SIGNALS = ['RAMS-BASEMAP-READY', 'ARAHARAH-LIVE', 'ARAHARAH-DEST'];
|
|||||||
* 'settle400' → shot after 400ms (boot card 2)
|
* 'settle400' → shot after 400ms (boot card 2)
|
||||||
* 'live' → ARAHARAH-LIVE race + 2000ms tile settle
|
* 'live' → ARAHARAH-LIVE race + 2000ms tile settle
|
||||||
* 'denied' → .lamp[data-state="denied"] race (LINK 3's 60s poll first)
|
* 'denied' → .lamp[data-state="denied"] race (LINK 3's 60s poll first)
|
||||||
|
* 'dest' → ARAHARAH-DEST race + state=resolved + card-text poll
|
||||||
|
* 'route' → ARAHARAH-ROUTE race + expectState + expectText DOM poll
|
||||||
|
* (05-05; expectState = 'drawn'|'pending', expectText =
|
||||||
|
* body-text substrings polled up to 15s; a themed leg
|
||||||
|
* gets the same PX-02 honesty check as 'live';
|
||||||
|
* `interact(page)` runs after the waits, pre-shot —
|
||||||
|
* the alt leg's chip tap)
|
||||||
*/
|
*/
|
||||||
async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
|
async function captureLeg({ name, query, colorScheme, grantGeo, waitMode, expectState, expectText, interact }) {
|
||||||
const target = join(SHOTS, name);
|
const target = join(SHOTS, name);
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
viewport: { width: 390, height: 844 },
|
viewport: { width: 390, height: 844 },
|
||||||
@@ -187,7 +216,7 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
|
|||||||
await context.setGeolocation({ latitude: 3.1513, longitude: 101.6958, accuracy: 12 });
|
await context.setGeolocation({ latitude: 3.1513, longitude: 101.6958, accuracy: 12 });
|
||||||
}
|
}
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const seen = { 'RAMS-BASEMAP-READY': false, 'ARAHARAH-LIVE': false, 'ARAHARAH-DEST': false, live: 'no' };
|
const seen = { 'RAMS-BASEMAP-READY': false, 'ARAHARAH-LIVE': false, 'ARAHARAH-DEST': false, 'ARAHARAH-ROUTE': false, live: 'no' };
|
||||||
const consoleLines = [];
|
const consoleLines = [];
|
||||||
page.on('console', (m) => {
|
page.on('console', (m) => {
|
||||||
for (const s of SIGNALS) if (m.text().startsWith(s)) seen[s] = true;
|
for (const s of SIGNALS) if (m.text().startsWith(s)) seen[s] = true;
|
||||||
@@ -195,7 +224,7 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
|
|||||||
if (consoleLines.length > 20) consoleLines.shift();
|
if (consoleLines.length > 20) consoleLines.shift();
|
||||||
});
|
});
|
||||||
const signalDiagnostics = () =>
|
const signalDiagnostics = () =>
|
||||||
`${seen['RAMS-BASEMAP-READY'] ? 'RAMS-BASEMAP-READY yes' : 'RAMS-BASEMAP-READY no'}; ${seen['ARAHARAH-LIVE'] ? 'ARAHARAH-LIVE yes' : 'ARAHARAH-LIVE no'}; ${seen['ARAHARAH-DEST'] ? 'ARAHARAH-DEST yes' : 'ARAHARAH-DEST no'}; last console: ${JSON.stringify(consoleLines.slice(-5))}`;
|
`${seen['RAMS-BASEMAP-READY'] ? 'RAMS-BASEMAP-READY yes' : 'RAMS-BASEMAP-READY no'}; ${seen['ARAHARAH-LIVE'] ? 'ARAHARAH-LIVE yes' : 'ARAHARAH-LIVE no'}; ${seen['ARAHARAH-DEST'] ? 'ARAHARAH-DEST yes' : 'ARAHARAH-DEST no'}; ${seen['ARAHARAH-ROUTE'] ? 'ARAHARAH-ROUTE yes' : 'ARAHARAH-ROUTE no'}; last console: ${JSON.stringify(consoleLines.slice(-5))}`;
|
||||||
try {
|
try {
|
||||||
await page.goto(`${base}/?${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
await page.goto(`${base}/?${query}`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
|
|
||||||
@@ -244,6 +273,40 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
|
|||||||
{ timeout: 15000, polling: 250 },
|
{ timeout: 15000, polling: 250 },
|
||||||
);
|
);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
} else if (waitMode === 'route') {
|
||||||
|
/* 05-05: race the ARAHARAH-ROUTE console event — a STATE TOKEN only
|
||||||
|
* (state=drawn|pending, never coordinates; D-15/T-05-13: RS8 pins the
|
||||||
|
* source, this race pins the runtime). The leg then asserts its
|
||||||
|
* expected state and DOM text, and themed legs get the same PX-02
|
||||||
|
* honesty check as the live branch. */
|
||||||
|
const signal = await Promise.race([
|
||||||
|
page.waitForEvent('console', {
|
||||||
|
predicate: (m) => m.text().startsWith('ARAHARAH-ROUTE'),
|
||||||
|
timeout: SIGNAL_TIMEOUT_MS,
|
||||||
|
}).then(() => 'signal'),
|
||||||
|
new Promise((r) => setTimeout(() => r('timeout'), SIGNAL_TIMEOUT_MS)),
|
||||||
|
]);
|
||||||
|
if (signal === 'timeout') {
|
||||||
|
throw new Error(`ARAHARAH-ROUTE race timed out after ${SIGNAL_TIMEOUT_MS}ms — ${signalDiagnostics()}`);
|
||||||
|
}
|
||||||
|
const routeLine = consoleLines.filter((l) => l.startsWith('ARAHARAH-ROUTE')).pop() ?? '';
|
||||||
|
if (!routeLine.includes(`state=${expectState}`)) {
|
||||||
|
throw new Error(`route state mismatch: "${routeLine}" — expected state=${expectState}`);
|
||||||
|
}
|
||||||
|
for (const text of expectText ?? []) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
(t) => (document.body.textContent ?? '').includes(t),
|
||||||
|
text,
|
||||||
|
{ timeout: 15000, polling: 250 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* PX-02 honesty check (anti-fake, T-03-30): the theme slot ACTUALLY
|
||||||
|
* flipped to the requested scheme before shooting. */
|
||||||
|
const themed = await page.evaluate(() => document.documentElement.dataset.tgTheme ?? null);
|
||||||
|
if (themed !== colorScheme) {
|
||||||
|
throw new Error(`theme slot mismatch: expected ${colorScheme}, documentElement[data-tg-theme]=${String(themed)}`);
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(2000); // tile render settle (route + camera refit at 400ms)
|
||||||
} else if (waitMode === 'denied') {
|
} else if (waitMode === 'denied') {
|
||||||
// Wait on the denied strip lamp specifically — the awaiting-pin variant of
|
// Wait on the denied strip lamp specifically — the awaiting-pin variant of
|
||||||
// the card also carries a CTA during LINK 3's 60s poll, so the CARD alone
|
// the card also carries a CTA during LINK 3's 60s poll, so the CARD alone
|
||||||
@@ -262,6 +325,8 @@ async function captureLeg({ name, query, colorScheme, grantGeo, waitMode }) {
|
|||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
} // 'immediate' → no wait beyond goto
|
} // 'immediate' → no wait beyond goto
|
||||||
|
|
||||||
|
if (interact) await interact(page);
|
||||||
|
|
||||||
await page.screenshot({ path: target });
|
await page.screenshot({ path: target });
|
||||||
log.push(`captured ${name} (${sizeOf(target)}B, ?${query || '—'} | wait=${waitMode})`);
|
log.push(`captured ${name} (${sizeOf(target)}B, ?${query || '—'} | wait=${waitMode})`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -349,13 +414,11 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 8 — DSGN-02 fidelity artifact: the live-light capture beside the committed
|
/* ————— DSGN-02 composer (shared by legs 8 and 13) ————— */
|
||||||
* navigate_map_overview mockup, each column labeled with its source path. */
|
async function composeCompare(target, implementedFile, implementedCaption) {
|
||||||
{
|
|
||||||
const target = join(SHOTS, 'gallery-compare.png');
|
|
||||||
try {
|
|
||||||
if (!existsSync(MOCKUP_COMPARE)) throw new Error(`committed mockup missing: ${MOCKUP_COMPARE}`);
|
if (!existsSync(MOCKUP_COMPARE)) throw new Error(`committed mockup missing: ${MOCKUP_COMPARE}`);
|
||||||
const liveB64 = readFileSync(join(SHOTS, 'shell-live-light.png')).toString('base64');
|
if (!existsSync(implementedFile)) throw new Error(`implemented capture missing: ${implementedFile}`);
|
||||||
|
const liveB64 = readFileSync(implementedFile).toString('base64');
|
||||||
const mockB64 = readFileSync(MOCKUP_COMPARE).toString('base64');
|
const mockB64 = readFileSync(MOCKUP_COMPARE).toString('base64');
|
||||||
const context = await browser.newContext({ viewport: { width: 880, height: 960 }, deviceScaleFactor: 1 });
|
const context = await browser.newContext({ viewport: { width: 880, height: 960 }, deviceScaleFactor: 1 });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
@@ -367,7 +430,7 @@ try {
|
|||||||
.cell { width: 100%; height: 800px; object-fit: contain; display: block; background: #d9d2c6; }
|
.cell { width: 100%; height: 800px; object-fit: contain; display: block; background: #d9d2c6; }
|
||||||
.mockimg { background: #ffffff; }
|
.mockimg { background: #ffffff; }
|
||||||
</style></head><body><div class="row">
|
</style></head><body><div class="row">
|
||||||
<figure><figcaption>IMPLEMENTED — web/screenshots/shell-live-light.png (?mock=android, harness build)</figcaption>
|
<figure><figcaption>IMPLEMENTED — ${implementedCaption}</figcaption>
|
||||||
<img class="cell" src="data:image/png;base64,${liveB64}"></figure>
|
<img class="cell" src="data:image/png;base64,${liveB64}"></figure>
|
||||||
<figure><figcaption>MOCKUP REFERENCE — stitch_minimalist_route_navigator/navigate_map_overview/screen.png</figcaption>
|
<figure><figcaption>MOCKUP REFERENCE — stitch_minimalist_route_navigator/navigate_map_overview/screen.png</figcaption>
|
||||||
<img class="cell mockimg" src="data:image/png;base64,${mockB64}"></figure>
|
<img class="cell mockimg" src="data:image/png;base64,${mockB64}"></figure>
|
||||||
@@ -378,11 +441,114 @@ try {
|
|||||||
await page.screenshot({ path: target, fullPage: true });
|
await page.screenshot({ path: target, fullPage: true });
|
||||||
await context.close();
|
await context.close();
|
||||||
if (sizeOf(target) < MIN_BYTES) throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B)`);
|
if (sizeOf(target) < MIN_BYTES) throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8 — DSGN-02 fidelity artifact: the live-light capture beside the committed
|
||||||
|
* navigate_map_overview mockup, each column labeled with its source path. */
|
||||||
|
{
|
||||||
|
const target = join(SHOTS, 'gallery-compare.png');
|
||||||
|
try {
|
||||||
|
await composeCompare(
|
||||||
|
target,
|
||||||
|
join(SHOTS, 'shell-live-light.png'),
|
||||||
|
'web/screenshots/shell-live-light.png (?mock=android, harness build)',
|
||||||
|
);
|
||||||
log.push(`captured gallery-compare.png (${sizeOf(target)}B, DSGN-02 side-by-side)`);
|
log.push(`captured gallery-compare.png (${sizeOf(target)}B, DSGN-02 side-by-side)`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failures.push(`gallery-compare.png: ${err.message}`);
|
failures.push(`gallery-compare.png: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 9–12 — the route experience (05-05). Every leg rides the ?fixpair knob:
|
||||||
|
* the mock's Telegram fix is the demo pair's PINNED PUBLIC origin resolved
|
||||||
|
* from the committed fixture corpus (D-12 — the harness introduces no user
|
||||||
|
* coordinate), paired with the ?dest carrier. All raced on the
|
||||||
|
* ARAHARAH-ROUTE state token + DOM text assertions. */
|
||||||
|
const routeQuery = `mock=android&dest=${demoPair.destinationId}&fixpair=${demoPair.pairId}`;
|
||||||
|
|
||||||
|
/* 9 — route drawn: orange casing stack + bottom card + endpoints, light. */
|
||||||
|
await captureLeg({
|
||||||
|
name: 'shell-route-fixture.png',
|
||||||
|
query: routeQuery,
|
||||||
|
colorScheme: 'light',
|
||||||
|
grantGeo: true,
|
||||||
|
waitMode: 'route',
|
||||||
|
expectState: 'drawn',
|
||||||
|
expectText: [routeDestEntry.name, 'MIN'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 10 — alternative selection: tap the first chip → instant geometry swap +
|
||||||
|
* card re-render + camera refit (D-10 ALWAYS-refit). The demo pair is
|
||||||
|
* multi-route by selection above (RT10 floor 4); the degenerate arm proves
|
||||||
|
* the single-route law instead (single-route draws render NO chip row). */
|
||||||
|
await captureLeg({
|
||||||
|
name: 'shell-route-alt.png',
|
||||||
|
query: routeQuery,
|
||||||
|
colorScheme: 'light',
|
||||||
|
grantGeo: true,
|
||||||
|
waitMode: 'route',
|
||||||
|
expectState: 'drawn',
|
||||||
|
expectText: [routeDestEntry.name, 'MIN'],
|
||||||
|
interact: async (page) => {
|
||||||
|
const chip = page.locator('.route-chip').first();
|
||||||
|
if ((await chip.count()) > 0) {
|
||||||
|
await chip.click();
|
||||||
|
await page.waitForTimeout(800); // instant swap + 400ms refit + margin
|
||||||
|
const bodyText = await page.evaluate(() => document.body.textContent ?? '');
|
||||||
|
if (!/ROUTE \d\/\d/.test(bodyText)) {
|
||||||
|
throw new Error(`chip tap did not re-render the ROUTE {n}/{N} register — body text: ${JSON.stringify(bodyText.slice(0, 300))}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const chipCount = await page.locator('.route-chip').count();
|
||||||
|
if (chipCount !== 0) {
|
||||||
|
throw new Error(`single-route draw rendered ${chipCount} chips — the single-route law says none`);
|
||||||
|
}
|
||||||
|
log.push('shell-route-alt.png: degenerate single-route pair — no-chip law asserted');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 11 — router pending (D-03): an out-of-corpus destination renders the
|
||||||
|
* honest card — never a blank map, never a fabricated geometry (T-05-14).
|
||||||
|
* national-museum is RT4-guaranteed absent from corpus destinationIds
|
||||||
|
* (05-02's pinned probe carrier). */
|
||||||
|
await captureLeg({
|
||||||
|
name: 'shell-route-pending.png',
|
||||||
|
query: `mock=android&dest=national-museum&fixpair=${demoPair.pairId}`,
|
||||||
|
colorScheme: 'light',
|
||||||
|
grantGeo: true,
|
||||||
|
waitMode: 'route',
|
||||||
|
expectState: 'pending',
|
||||||
|
expectText: ['not wired up yet'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 12 — dark theme (PX-02 cold-boot half for the route surface): chrome
|
||||||
|
* re-slots, route + basemap stay Rams-constant. */
|
||||||
|
await captureLeg({
|
||||||
|
name: 'shell-route-dark.png',
|
||||||
|
query: `${routeQuery}&scheme=dark`,
|
||||||
|
colorScheme: 'dark',
|
||||||
|
grantGeo: true,
|
||||||
|
waitMode: 'route',
|
||||||
|
expectState: 'drawn',
|
||||||
|
expectText: [routeDestEntry.name, 'MIN'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 13 — DSGN-02 route fidelity artifact: the drawn-route capture beside the
|
||||||
|
* navigate_map_overview mockup (the same composer as leg 8). */
|
||||||
|
{
|
||||||
|
const target = join(SHOTS, 'route-gallery-compare.png');
|
||||||
|
try {
|
||||||
|
await composeCompare(
|
||||||
|
target,
|
||||||
|
join(SHOTS, 'shell-route-fixture.png'),
|
||||||
|
`web/screenshots/shell-route-fixture.png (?dest=${demoPair.destinationId}&fixpair=${demoPair.pairId}, harness build)`,
|
||||||
|
);
|
||||||
|
log.push(`captured route-gallery-compare.png (${sizeOf(target)}B, DSGN-02 route side-by-side)`);
|
||||||
|
} catch (err) {
|
||||||
|
failures.push(`route-gallery-compare.png: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
server.close();
|
server.close();
|
||||||
@@ -399,7 +565,12 @@ const expected = [
|
|||||||
'shell-denied.png',
|
'shell-denied.png',
|
||||||
'shell-chat-pin.png',
|
'shell-chat-pin.png',
|
||||||
'shell-dest-preload.png',
|
'shell-dest-preload.png',
|
||||||
|
'shell-route-fixture.png',
|
||||||
|
'shell-route-alt.png',
|
||||||
|
'shell-route-pending.png',
|
||||||
|
'shell-route-dark.png',
|
||||||
'gallery-compare.png',
|
'gallery-compare.png',
|
||||||
|
'route-gallery-compare.png',
|
||||||
];
|
];
|
||||||
const missing = expected.filter((f) => !existsSync(join(SHOTS, f)));
|
const missing = expected.filter((f) => !existsSync(join(SHOTS, f)));
|
||||||
const undersized = expected.filter((f) => existsSync(join(SHOTS, f)) && sizeOf(join(SHOTS, f)) < MIN_BYTES);
|
const undersized = expected.filter((f) => existsSync(join(SHOTS, f)) && sizeOf(join(SHOTS, f)) < MIN_BYTES);
|
||||||
@@ -412,5 +583,5 @@ if (missing.length || undersized.length || failures.length) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`CAPTURE OK — 8/8 non-blank captures under web/screenshots/`);
|
console.log(`CAPTURE OK — ${expected.length}/${expected.length} non-blank captures under web/screenshots/`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user