Test Workbox-built service workers - pin behavior of the named recipes (`pageCache`, `staticResourceCache`, `imageCache`, `googleFontsCache`, `offlineFallback`, `warmStrategyCache`) per [developer.chrome.com/docs/workbox/modules/workbox-recipes][wb-recipes]; validate `workbox-precaching` manifest injection (`__WB_MANIFEST` revisioning); assert `workbox-routing` route handler matches; assert `workbox-expiration` and `workbox-cacheable-response` plugin gates; and verify the `workbox-window` registration helper events (`installed`, `waiting`, `controlling`, `activated`). Use when a project already ships a Workbox-built service worker and its recipe behavior needs pinning against refactors or a version upgrade.
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 workbox-tests. The imageCache() 60-entry-cap test is
the representative core inline in Step 4; these are the other five recipe
templates. Each retains the recipe's test-invariant default (timeout, entry
count, TTL) but drops the verbatim doc prose. Defaults are the Workbox v7.x
values per wb-recipes - re-pin at the recipe page on a major upgrade.
Network-first for HTML navigations with a 3-second networkTimeoutSeconds
default - cache serves once network exceeds the timeout:
import { test, expect } from '@playwright/test';
test('pageCache() falls back to cache when network exceeds 3s', async ({ page, context }) => {
await page.goto('https://localhost:3000/');
await page.waitForLoadState('networkidle');
// Slow the network past the 3s networkTimeoutSeconds default
await context.route('**/*.html', async route => {
await new Promise(r => setTimeout(r, 5_000));
await route.continue();
});
await page.goto('https://localhost:3000/');
// Cached shell should serve before the 5s slow network resolves
await expect(page.locator('h1')).toBeVisible({ timeout: 4_500 });
});Serves the offline.html default on a navigation routing error while offline
(swap the target if the project overrides pageFallback):
test('offlineFallback() serves offline.html on navigation failure', 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-cached');
expect(resp?.status()).toBe(200);
await expect(page.locator('text=/offline/i')).toBeVisible();
});Stale-while-revalidate for stylesheets, cache-first for font files, with defaults of 30 font files cached for one year:
test('googleFontsCache stylesheet uses stale-while-revalidate', async ({ page, context }) => {
await page.goto('https://localhost:3000/');
await page.waitForLoadState('networkidle');
await context.setOffline(true);
const status = await page.evaluate(() =>
fetch('https://fonts.googleapis.com/css2?family=Inter').then(r => r.status).catch(() => 0)
);
// Stale cache must respond offline
expect(status).toBe(200);
});Stale-while-revalidate for CSS, JavaScript, and Web Worker requests:
test('staticResourceCache serves cached CSS offline', async ({ page, context }) => {
await page.goto('https://localhost:3000/');
await page.waitForLoadState('networkidle');
await context.setOffline(true);
const status = await page.evaluate(() =>
fetch('/styles/app.css').then(r => r.status).catch(() => 0)
);
expect(status).toBe(200);
});Loads the declared URL list into the cache during the SW install phase - pin which URLs are warmed:
test('warmStrategyCache() warms the declared URL list on install', async ({ context, page }) => {
await page.goto('https://localhost:3000/');
let [sw] = context.serviceWorkers();
if (!sw) sw = await context.waitForEvent('serviceworker');
// SW install phase warms a known URL - pin it
const warmed = await sw.evaluate(async () => {
const names = await caches.keys();
for (const n of names) {
const cache = await caches.open(n);
const keys = await cache.keys();
if (keys.some(k => k.url.endsWith('/critical-data.json'))) return true;
}
return false;
});
expect(warmed).toBe(true);
});Source reference: