Wraps CDN cache-purge testing patterns for Cloudflare (POST /zones/{zone_id}/purge_cache, single-file / everything / cache-tags / hostname / prefix), Fastly (POST purge-by-key / purge-all, surrogate-keys via Surrogate-Key header), and CloudFront (CreateInvalidation API + paths). Covers end-to-end test patterns (write origin → trigger purge → assert edge serves fresh), purge-propagation delay testing (typically 1-30s globally), surrogate-key + cache-tag patterns for group-purge, and Cache-Status header verification (cf-cache-status: HIT/MISS/BYPASS). Also owns the client tier: browser-side Cache-Control verification with Playwright (served-from-cache via CDP, ETag 304 round-trips, Workbox service-worker strategies, reload semantics) in references/browser-cache-control.md. Use when designing or auditing CDN cache-invalidation workflows or browser-tier caching behaviour in E2E tests.
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
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.