// Offline end-to-end check against `pnpm preview` (production build, service worker on). Run: node scripts/offline-e2e.mjs // 1) first visit saves the app + the map pack, 2) with the network cut the app reloads, shows the offline notice and still routes. import { chromium } from 'playwright-core'; const URL = process.env.URL ?? 'http://localhost:4173/'; const browser = await chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, locale: 'pl-PL', isMobile: true, hasTouch: true }); const page = await ctx.newPage(); let step = 0; const ok = (cond, msg) => { step++; if (!cond) { console.error(`FAIL ${step}: ${msg}`); process.exitCode = 1; } else console.log(`ok ${step}: ${msg}`); }; const caches = () => page.evaluate(async () => { const o = {}; for (const n of await window.caches.keys()) o[n] = (await (await window.caches.open(n)).keys()).length; return o; }); await page.goto(URL); await page.click('text=Zaloguj jako Anna Kowalska'); await page.waitForSelector('#q', { state: 'attached', timeout: 60_000 }); ok(await page.evaluate(() => !!navigator.serviceWorker.controller), 'service worker controls the page'); let tiles = 0; for (let i = 0; i < 150 && tiles < 1000; i++) { tiles = (await caches())['osm-tiles'] ?? 0; if (tiles < 1000) await page.waitForTimeout(1000); } ok(tiles >= 1000, `offline map pack saved (${tiles} tiles)`); let rec = null; for (let i = 0; i < 60 && !rec; i++) { rec = JSON.parse((await page.evaluate(() => localStorage.getItem('kbb.offline'))) ?? 'null'); if (!rec) await page.waitForTimeout(500); } ok(rec && rec.failed === 0 && rec.tiles >= 1000, 'pack record stored'); await ctx.setOffline(true); await page.waitForSelector('.kbb-banner--warning', { timeout: 5000 }); ok(/Trasy na żywo/.test(await page.locator('.kbb-banner--warning').innerText()), 'offline notice says live routes are unavailable'); await page.click('.kbb-banner--warning .kbb-banner__close'); ok((await page.locator('.kbb-banner--warning').count()) === 0, 'offline notice can be dismissed'); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('#q', { state: 'attached', timeout: 30_000 }); ok(true, 'app reloads and opens with no network'); ok((await page.locator('.kbb-banner--warning').count()) === 1, 'offline notice shown again after the reload'); await page.click('nav >> text=Trasa'); await page.fill('#route-from', 'Sukiennice'); await page.fill('#route-to', 'Zamek Królewski na Wawelu'); await page.click('button[type=submit]:has-text("Wyznacz")'); await page.waitForSelector('.kbb-modes', { timeout: 10_000 }); ok(true, 'walking route is computed offline'); await page.click('.kbb-mode:has-text("Komunikacja")'); // scoped to the planner: the offline banner also says "Brak połączenia", and the loader shares the #tr-title heading const planner = page.locator('section[aria-labelledby=tr-title]'); await planner.locator('.kbb-jhead, .usa-alert').first().waitFor({ timeout: 20_000 }); const note = await page.locator('#tr-title + p').innerText(); ok(/Według rozkładu/.test(note), `transit planner answers from the saved timetable and says delays are unavailable ("${note.slice(0, 60)}")`); await ctx.setOffline(false); await page.waitForSelector('.kbb-banner--success', { timeout: 5000 }); ok(true, 'back-online notice appears'); await browser.close();