Use when CDN or browser caching needs tests: purges and invalidations on Cloudflare, Fastly (surrogate keys) and CloudFront, end-to-end write, purge and fresh-read checks, propagation delay and cache-status headers, plus browser Cache-Control, ETag and service-worker caching with Playwright.
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Deeper browser-cache authoring recipes for
browser-cache-control.md. The basic
Cache-Control header assertion lives there; these cover
served-from-cache detection, ETag revalidation, hard reload, and
service workers.
Playwright doesn't expose "from disk cache" directly, but request timing reveals it:
test('static asset second load is from disk cache', async ({ page }) => {
await page.goto('https://example.com'); // first load (network)
const responses: Array<{ url: string; fromCache: boolean }> = [];
page.on('response', (resp) => {
responses.push({
url: resp.url(),
fromCache: resp.fromServiceWorker() || resp.request().redirectedFrom() !== null,
});
});
await page.reload();
// Playwright doesn't expose 'from disk cache' directly, but
// request timing reveals it:
const asset = responses.find((r) => r.url.endsWith('.js'));
// The Network panel `(disk cache)` annotation comes from
// timing.responseEnd === timing.responseStart for cached items.
});For a stronger check, use Chrome DevTools Protocol via Playwright:
const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.enable');
cdp.on('Network.responseReceived', (params) => {
if (params.response.url.endsWith('.js')) {
expect(params.response.fromDiskCache).toBe(true);
}
});
await page.reload();Per RFC 9111 §4.3.1:
test('ETag triggers 304 on revalidation', async ({ page }) => {
let firstEtag: string | undefined;
page.on('response', (resp) => {
if (resp.url() === 'https://example.com/api/feed') {
const etag = resp.headers()['etag'];
if (resp.status() === 200 && !firstEtag) firstEtag = etag;
else if (firstEtag) {
expect(resp.request().headers()['if-none-match']).toBe(firstEtag);
expect(resp.status()).toBe(304);
}
}
});
// First load
await page.goto('https://example.com/dashboard');
// Reload after TTL - browser should send If-None-Match
await page.waitForTimeout(2000);
await page.reload();
});Browsers send Cache-Control: no-cache on hard reload, bypassing
the cache. Test:
test('hard reload bypasses cache', async ({ page }) => {
await page.goto('https://example.com');
page.on('request', (req) => {
if (req.url().endsWith('.js')) {
expect(req.headers()['cache-control']).toMatch(/no-cache/);
}
});
// Playwright doesn't have a direct "hard reload"; simulate via CDP:
const cdp = await page.context().newCDPSession(page);
await cdp.send('Page.reload', { ignoreCache: true });
});Workbox provides standard strategies; test which is used:
test('offline page uses cache-first strategy', async ({ context, page }) => {
// Go online, populate cache
await page.goto('https://example.com');
// Go offline
await context.setOffline(true);
// Reload - should still work
await page.reload();
await expect(page.locator('h1')).toHaveText('Example');
});
test('api uses network-first with fallback', async ({ context, page }) => {
await page.goto('https://example.com/api-status');
await context.setOffline(true);
await page.reload();
// Stale cached response shown
await expect(page.locator('.api-status')).toBeVisible();
});