Build-an-X workflow that emits the offline-fallback test suite. Walks the eight Jake Archibald offline recipes per [web.dev/articles/offline-cookbook][off-cookbook] (`Cache only`, `Network only`, `Cache, falling back to network`, `Cache and network race`, `Network falling back to cache`, `Cache then network`, `Generic fallback`, `Service Worker side templating`), maps each recipe to its assertion shape, layers the Workbox `offlineFallback()` recipe per [developer.chrome.com/docs/workbox/modules/workbox-recipes][wb-recipes], and pins the offline storage strategy (Cache Storage vs IndexedDB vs Storage Manager `persist()`/`estimate()`) per [web.dev/learn/pwa/offline-data][off-data]. Output: a Playwright spec file with one test per route's chosen recipe + a coverage matrix mapping recipes to URL patterns. Use when a route's SW caching behavior is being chosen or changed, or when a "doesn't work offline" report needs the failing route's recipe localized.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Companion detail for offline-fallback-tests. The two poles (Cache only,
Network only) are inline in Step 2; these are the remaining six recipe
templates from the offline cookbook. Each recipe's "when to use" is quoted once
in the SKILL.md Step 1 table - here only the assertion shape is noted.
Serves from cache when present, network when not:
test('Cache, falling back to network: /img/x.png serves from cache when present, network when not', async ({ page, context }) => {
// Warm
await page.goto('https://localhost:3000/img/x.png');
await page.waitForLoadState('networkidle');
// Now offline - cache hits
await context.setOffline(true);
const offline = await page.evaluate(() =>
fetch('/img/x.png').then(r => r.status).catch(() => 0)
);
expect(offline).toBe(200);
});The first responder wins. Assert the call returns under a small-disk-read budget:
test('Cache and network race: response under 100ms when cached', async ({ page }) => {
await page.goto('https://localhost:3000/api/race-cache');
await page.waitForLoadState('networkidle');
const ms = await page.evaluate(async () => {
const t = performance.now();
await fetch('/api/race-cache');
return performance.now() - t;
});
expect(ms).toBeLessThan(100);
});Prefers network when online, falls back to cache when offline:
test('Network falling back to cache: / serves fresh when online, cached when offline', async ({ page, context }) => {
await page.goto('https://localhost:3000/');
await page.waitForLoadState('networkidle');
await context.setOffline(true);
const resp = await page.goto('https://localhost:3000/');
expect(resp?.status()).toBe(200);
await context.setOffline(false);
// When online, network is preferred - track that a request fires
let hits = 0;
page.on('request', req => { if (req.url().endsWith('/')) hits++; });
await page.reload();
expect(hits).toBeGreaterThanOrEqual(1);
});Two responses for one fetch: instant cache, then revalidated network. Assert two paint phases:
test('Cache then network: /api/feed renders cached list first, refreshes on network', async ({ page }) => {
await page.goto('https://localhost:3000/feed');
// Initial render uses cached list
await expect(page.locator('[data-testid="feed-list"]')).toBeVisible();
const initialCount = await page.locator('[data-testid="feed-item"]').count();
// Refresh fetches from network with newer items
await page.evaluate(() => fetch('/api/feed?force-refresh=1'));
await page.waitForTimeout(500);
const updated = await page.locator('[data-testid="feed-item"]').count();
expect(updated).toBeGreaterThanOrEqual(initialCount);
});Unknown route while offline serves offline.html (Workbox offlineFallback()
default):
test('Generic fallback: unknown route + offline serves offline.html', async ({ page, context }) => {
await page.goto('https://localhost:3000/');
await page.waitForLoadState('networkidle');
await context.setOffline(true);
const resp = await page.goto('https://localhost:3000/never-existed');
expect(resp?.status()).toBe(200);
await expect(page.locator('text=/offline|unavailable/i')).toBeVisible();
});Assert the SW synthesizes the response from a cached shell + JSON data:
test('SW-side templating: /reports/123 renders cached shell + JSON data', async ({ page }) => {
// The SW's fetch handler must compose template + data
await page.goto('https://localhost:3000/reports/123');
await expect(page.locator('h1')).toContainText('Report #123');
// Distinct origin proves SW synthesis (not pure network)
const src = await page.evaluate(() => navigator.serviceWorker.controller?.scriptURL);
expect(src).toBeTruthy();
});Source references: