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
Browser cache tests verify the request side of caching: does the
browser actually respect the Cache-Control headers the server sends?
Per MDN Cache-Control
the directive set is identical to
RFC 9111, but runtime
behaviour differs subtly between Chromium, Firefox, and Safari. Scope:
behaviour a browser decides (served-from-cache, revalidation, SW strategy,
reload semantics). Asserting only which header the server emits needs no
browser - do that in the project's existing HTTP-level runner (supertest,
requests, RestAssured, curl -I).
Cache-Control header, ETag
304 round-trip, service-worker strategy, or reload semantics.page.on('response') before
page.goto.resp.headers()['cache-control'] and assert with a regex
toMatch, never an exact string (vendors append directives).Network.responseReceived.response.fromDiskCache / fromMemoryCache.304 with a matching If-None-Match.context.setOffline(true) and reload.npx playwright test across the Chromium / Firefox / WebKit matrix.import { test, expect } from '@playwright/test';
test('bundle immutable, /api/me uncached', async ({ page }) => {
const seen: Record<string, string> = {};
page.on('response', (resp) => {
const cc = resp.headers()['cache-control'] ?? '';
if (resp.url().match(/\.\w+\.js$/)) seen.bundle = cc;
if (resp.url().endsWith('/api/me')) seen.api = cc;
});
await page.goto('https://example.com/dashboard');
expect(seen.bundle).toMatch(/max-age=\d{6,}/); // ~10+ days
expect(seen.bundle).toContain('immutable'); // per RFC 8246
expect(seen.api).toMatch(/(no-store|private)/);
});The bundle assertion fails if the build drops immutable (silent perf
regression); the /api/me assertion catches a proxy adding a public
max-age - a leak of per-user data into shared caches.
The deeper recipes - served-from-cache detection via CDP, ETag revalidation round-trips, hard-reload semantics, and service-worker (Workbox) strategies - are in playwright-cache-recipes.md.
| Method | Returns |
|---|---|
resp.status() | HTTP status |
resp.headers() | All response headers |
resp.fromServiceWorker() | Whether a SW intercepted |
resp.request().headers() | Request headers (If-None-Match) |
resp.timing() | Cached fetches have minimal responseEnd - responseStart |
| Anti-pattern | Why it fails | Fix |
|---|---|---|
status() == 200 to "prove" a cache miss | 304 is also cache-related | Inspect headers / fromDiskCache |
| Fresh browser context per test | Cache starts empty; no "second load" | Reuse the context within a test |
Exact-string cache-control assertions | Vendor directives break it | Regex toMatch |
| Chromium-only runs | Safari + Firefox differ (SW, ITP) | Run the matrix in CI |
| No 304 test | ETag round-trip drift unnoticed | Test the second-load 304 path |
Mocking caches.match() | Bypasses the real storage layer | Real Cache API + Playwright |
fromDiskCache; some
assertions need raw CDP.navigator.serviceWorker.ready before asserting.