Gate the installability probe on every condition LAUNCH.md claims (Maestro 2c8f8887, F1-F3)
F1: the probe now gates each page on manifest HTTP 200, name, display, start_url, 192x192 and 512x512 icons, a maskable icon, and a service worker that is ready, registered and activated. It prints a PASS/FAIL line per condition and exits 1 if any condition fails on any page. LAUNCH.md lists exactly these gates; the unchecked claim that the worker controls the page is removed. F2: navigator.serviceWorker.ready is raced against a 10 second timeout; expiry fails the "service worker ready" condition. F3: the browser context uses Playwright's Pixel 5 mobile emulation (viewport, touch, isMobile, user agent), and LAUNCH.md says so. Re-run against production mealquest:743a155 on 2026-10-11: every condition PASS on all four pages, exit 0. Maestro task: 2c8f8887-85e5-4734-b90f-9678f17d3778 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+11
-10
@@ -36,18 +36,19 @@ Performance is recorded for reference but is not gated.
|
||||
Lighthouse 10 and later removed the PWA category, so installability was checked
|
||||
directly with a Playwright probe:
|
||||
[installability-check.mjs](lighthouse/2026-10-11/installability-check.mjs).
|
||||
It loads each of the four pages above in a signed-in mobile Chrome session
|
||||
(the cookie comes from the `CK` environment variable, never the file) and
|
||||
checks:
|
||||
It loads each of the four pages above in a signed-in Chrome session with
|
||||
Playwright's Pixel 5 mobile emulation (viewport, touch, `isMobile` and mobile
|
||||
user agent; the cookie comes from the `CK` environment variable, never the
|
||||
file) and gates every page on all of:
|
||||
|
||||
- **Manifest** — the linked manifest is fetched (HTTP 200) and has
|
||||
- **Manifest** — the linked manifest is fetched with HTTP 200 and has
|
||||
`name` "MealQuest", `display` "standalone", a `start_url`, and icons at
|
||||
192 px, 512 px and a `maskable` purpose.
|
||||
- **Service worker** — registered and activated, and controlling the page on
|
||||
the single-page-app routes.
|
||||
192x192, 512x512 and one with `maskable` purpose.
|
||||
- **Service worker** — `navigator.serviceWorker.ready` resolves within 10
|
||||
seconds (expiry is a failure), and a registration exists whose active worker
|
||||
is `activated`.
|
||||
|
||||
The script's pass condition gates the manifest fields and icons and the
|
||||
service-worker registration; the manifest HTTP status, the worker's activation
|
||||
state and whether it controls the page are printed per page for review.
|
||||
It prints a PASS or FAIL line per condition per page and exits 1 if any
|
||||
condition fails on any page.
|
||||
|
||||
**Result: PASS on all four pages.**
|
||||
|
||||
@@ -1,31 +1,53 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { chromium, devices } from 'playwright';
|
||||
const ck = process.env.CK;
|
||||
if (!ck) { console.error('CK (session cookie) is not set'); process.exit(2); }
|
||||
const origin = 'https://mealquest.cubecraftlabs.com';
|
||||
const pages = [['home','/'],['plan','/plan'],['discover','/discover'],['dinner','/dinner/01M4PKT9QYA6C4JCF67XE7RA6R']];
|
||||
const SW_READY_TIMEOUT_MS = 10000;
|
||||
const browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox'] });
|
||||
const ctx = await browser.newContext({ userAgent: 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Mobile Safari/537.36' });
|
||||
await ctx.addCookies([{ name: '__Host-mealquest_session', value: ck, url: 'https://mealquest.cubecraftlabs.com/' }]);
|
||||
// Mobile emulation: Pixel 5 viewport, device scale, touch, isMobile and user agent.
|
||||
const ctx = await browser.newContext({ ...devices['Pixel 5'] });
|
||||
await ctx.addCookies([{ name: '__Host-mealquest_session', value: ck, url: origin + '/' }]);
|
||||
let fail = 0;
|
||||
for (const [name, path] of pages) {
|
||||
const page = await ctx.newPage();
|
||||
page.on('console', m => { if (/sw|service|error/i.test(m.text())) console.log(' [console]', m.text().slice(0,120)); });
|
||||
page.on('pageerror', e => console.log(' [pageerror]', String(e).slice(0,120)));
|
||||
await page.goto('https://mealquest.cubecraftlabs.com' + path, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
await page.goto(origin + path, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
const href = await page.evaluate(() => document.querySelector('link[rel="manifest"]')?.href || null);
|
||||
const sw = await page.evaluate(async () => {
|
||||
try { await navigator.serviceWorker.ready; } catch (e) { return { err: String(e) }; }
|
||||
const sw = await page.evaluate(async (ms) => {
|
||||
const ready = await Promise.race([
|
||||
navigator.serviceWorker.ready.then(() => 'ready', e => 'error: ' + String(e)),
|
||||
new Promise(r => setTimeout(() => r('timeout after ' + ms + ' ms'), ms)),
|
||||
]);
|
||||
const regs = await navigator.serviceWorker.getRegistrations();
|
||||
return { registrations: regs.length, scope: regs[0]?.scope || null, state: regs[0]?.active?.state || null, controlling: !!navigator.serviceWorker.controller };
|
||||
});
|
||||
const manifest = href ? await page.evaluate(async (h) => { const r = await fetch(h); return { status: r.status, body: await r.json() }; }, href) : { status: 'no link' };
|
||||
const m = manifest.body || {};
|
||||
const icons = (m.icons || []).map(i => i.sizes + (i.purpose ? ':' + i.purpose : ''));
|
||||
const okSw = sw.registrations > 0;
|
||||
const okIcons = icons.some(s => s.startsWith('192')) && icons.some(s => s.startsWith('512')) && icons.some(s => s.includes('maskable'));
|
||||
const okManifest = m.name === 'MealQuest' && m.display === 'standalone' && !!m.start_url && okIcons;
|
||||
const pass = okSw && okManifest;
|
||||
return { ready, registrations: regs.length, state: regs[0]?.active?.state || null };
|
||||
}, SW_READY_TIMEOUT_MS);
|
||||
const manifest = href
|
||||
? await page.evaluate(async (h) => { const r = await fetch(h); return { status: r.status, body: r.ok ? await r.json() : {} }; }, href)
|
||||
: { status: 'no manifest link', body: {} };
|
||||
const m = manifest.body;
|
||||
const icons = m.icons || [];
|
||||
const hasSize = (s) => icons.some(i => (i.sizes || '').split(/\s+/).includes(s));
|
||||
const checks = [
|
||||
['manifest HTTP 200', manifest.status === 200, manifest.status],
|
||||
['manifest name "MealQuest"', m.name === 'MealQuest', m.name],
|
||||
['manifest display "standalone"', m.display === 'standalone', m.display],
|
||||
['manifest start_url present', !!m.start_url, m.start_url],
|
||||
['icon 192x192', hasSize('192x192'), icons.map(i => i.sizes).join(',')],
|
||||
['icon 512x512', hasSize('512x512'), icons.map(i => i.sizes).join(',')],
|
||||
['icon purpose maskable', icons.some(i => (i.purpose || '').split(/\s+/).includes('maskable')), icons.map(i => i.purpose || 'any').join(',')],
|
||||
['service worker ready', sw.ready === 'ready', sw.ready],
|
||||
['service worker registered', sw.registrations > 0, sw.registrations],
|
||||
['service worker activated', sw.state === 'activated', sw.state],
|
||||
];
|
||||
console.log(`${name} (${path}) url=${page.url().replace(origin, '')}:`);
|
||||
for (const [label, ok, actual] of checks) console.log(` ${ok ? 'PASS' : 'FAIL'} ${label} (got ${JSON.stringify(actual ?? null)})`);
|
||||
const pass = checks.every(([, ok]) => ok);
|
||||
if (!pass) fail++;
|
||||
console.log(`${name}: url=${page.url().replace('https://mealquest.cubecraftlabs.com','')} manifest=${manifest.status} name=${m.name} display=${m.display} icons=[${icons.join(',')}] SW=${JSON.stringify(sw)} => ${pass ? 'INSTALLABLE' : 'NOT INSTALLABLE'}`);
|
||||
console.log(` => ${pass ? 'INSTALLABLE' : 'NOT INSTALLABLE'}`);
|
||||
await page.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log(fail ? `FAILED on ${fail} of ${pages.length} pages` : `PASS on all ${pages.length} pages`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user