Wraps browser-side Cache-Control testing with Playwright (Cypress for legacy stacks): asserting response Cache-Control headers from Network events, ETag round-trips (If-None-Match → 304), service-worker strategies (Workbox cacheFirst / networkFirst / staleWhileRevalidate), and reload semantics (normal vs hard). Covers MDN Cache-Control + RFC 9111. Use when auditing browser-tier caching in E2E tests. For CDN-edge purge use cdn-cache-purge-tests; for the reverse-proxy tier use varnish-test-vtc-syntax; for an app-tier store use redis-cache-tests; SWR directive semantics live in stale-while-revalidate-reference.
80
100%
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
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
(www.rfc-editor.org/rfc/rfc9111.html),
but the runtime behaviour differs subtly between Chromium,
Firefox, and Safari.
Cache-Control
header, ETag 304 round-trip, service-worker strategy, or
reload semantics.page.on('response')
(or page.on('request')) listener before page.goto.resp.headers()['cache-control'] and
assert with a regex toMatch, never an exact string.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 in CI.Playwright can inspect every request + response, including served-from-cache state.
import { test, expect } from '@playwright/test';
test('static assets have long Cache-Control', async ({ page }) => {
page.on('response', (resp) => {
if (resp.url().endsWith('.js')) {
const cc = resp.headers()['cache-control'];
expect(cc).toMatch(/max-age=\d{6,}/); // ≥ ~10 days
expect(cc).toContain('immutable'); // per RFC 8246
}
});
await page.goto('https://example.com');
});
test('API responses are not cached by default', async ({ page }) => {
page.on('response', (resp) => {
if (resp.url().includes('/api/')) {
const cc = resp.headers()['cache-control'];
expect(cc).toMatch(/(no-store|max-age=0|private)/);
}
});
await page.goto('https://example.com/dashboard');
});The deeper recipes - served-from-cache detection via CDP, ETag revalidation round-trips, hard-reload semantics, and service-worker (Workbox) strategies - live in references/playwright-cache-recipes.md.
A release ships hashed bundles (app.4f2a.js) that should cache
for a year, plus a /api/me endpoint that must never be cached.
One spec audits both:
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');
expect(seen.api).toMatch(/(no-store|private)/);
});The bundle assertion fails if the build drops immutable (a
silent perf regression); the /api/me assertion fails if a proxy
adds a public max-age, catching an accidental leak of per-user
data into shared caches.
npx playwright test cache-tests.spec.tsFor service-worker tests, increase the test timeout - SW registration is async.
Playwright's response event gives access to:
| Method | Returns |
|---|---|
resp.status() | HTTP status code |
resp.headers() | All response headers |
resp.fromServiceWorker() | Whether SW intercepted |
resp.request().headers() | Request headers (for If-None-Match) |
resp.timing() | Request timing (cached fetches have minimal responseEnd - responseStart) |
For the canonical "served from cache" assertion, fall back to
CDP Network.responseReceived.response.fromDiskCache or
fromMemoryCache.
jobs:
browser-cache-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci && npx playwright install --with-deps chromium
- run: npx playwright test tests/cache/| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Asserting on response.status() == 200 to "prove" cache miss | 304 is also cache-related; misses revalidation cases | Inspect headers / fromDiskCache |
| Per-test fresh browser context | Cache starts empty; can't test "second load" pattern | Reuse context within a test |
Asserting on cache-control matches exact string | Server adds vendor-specific directives; brittle | Use regex toMatch |
| Testing only Chromium | Safari + Firefox have differences (Service Worker, ITP) | Run matrix in CI |
Skipping immutable test for hashed assets | Browsers re-validate; perf regression silent | Per RFC 8246, hashed asset URLs should be immutable |
| No 304 test | ETag round-trip drift unnoticed | Test the second-load 304 path |
Mocking caches.match() | Bypasses the actual storage layer | Use real Cache API + Playwright |
| Hard reload behaviour assumed cross-browser | Safari hard-reload differs from Chrome | Test the actual target browsers |
fromDiskCache. Some assertions need raw CDP.navigator.serviceWorker.ready before assertions.cdn-cache-purge-tests.cache-coherence-patterns-reference,
stale-while-revalidate-reference.redis-cache-tests,
cdn-cache-purge-tests,
varnish-test-vtc-syntax.