From 8015d40a01974a65d01129bd980d73dc221c0be6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 3 Dec 2025 11:04:14 +0000 Subject: [PATCH] fix(trace viewer): prevent stale content for resource overrides Resources with overrides might have different content over time, so do not allow browser to cache them. --- .../utils/isomorphic/trace/snapshotServer.ts | 5 ++- .../utils/isomorphic/trace/snapshotStorage.ts | 13 +++++++ tests/library/trace-viewer.spec.ts | 39 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/utils/isomorphic/trace/snapshotServer.ts b/packages/playwright-core/src/utils/isomorphic/trace/snapshotServer.ts index 843667af33606..e2703a0060fc3 100644 --- a/packages/playwright-core/src/utils/isomorphic/trace/snapshotServer.ts +++ b/packages/playwright-core/src/utils/isomorphic/trace/snapshotServer.ts @@ -104,7 +104,10 @@ export class SnapshotServer { headers.set('Access-Control-Allow-Origin', '*'); headers.delete('Content-Length'); headers.set('Content-Length', String(content.size)); - headers.set('Cache-Control', 'public, max-age=31536000'); + if (this._snapshotStorage.hasResourceOverride(resource.request.url)) + headers.set('Cache-Control', 'no-store, no-cache, max-age=0'); + else + headers.set('Cache-Control', 'public, max-age=31536000'); const { status } = resource.response; const isNullBodyStatus = status === 101 || status === 204 || status === 205 || status === 304; return new Response(isNullBodyStatus ? null : content, { diff --git a/packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage.ts b/packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage.ts index 4b0206cfacb04..28eb3eedae7a6 100644 --- a/packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage.ts +++ b/packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage.ts @@ -28,6 +28,7 @@ export class SnapshotStorage { }>(); private _cache = new LRUCache(100_000_000); // 100MB per each trace private _contextToResources = new Map(); + private _resourceUrlsWithOverrides = new Set(); addResource(contextId: string, resource: ResourceSnapshot): void { resource.request.url = rewriteURLForCustomProtocol(resource.request.url); @@ -67,6 +68,18 @@ export class SnapshotStorage { // Resources are not necessarily sorted in the trace file, so sort them now. for (const resources of this._contextToResources.values()) resources.sort((a, b) => (a._monotonicTime || 0) - (b._monotonicTime || 0)); + // Resources that have overrides should not be cached, otherwise we might get stale content + // while serving snapshots with different override values. + for (const frameSnapshots of this._frameSnapshots.values()) { + for (const snapshot of frameSnapshots.raw) { + for (const override of snapshot.resourceOverrides) + this._resourceUrlsWithOverrides.add(override.url); + } + } + } + + hasResourceOverride(url: string) { + return this._resourceUrlsWithOverrides.has(url); } private _ensureResourcesForContext(contextId: string): ResourceSnapshot[] { diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 57cf3d1642681..d66c04f44ed15 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2129,3 +2129,42 @@ test('should not navigate on anchor clicks', async ({ runAndTrace, page, server await checkLink('link2'); await checkLink('link3'); }); + +test('should respect CSSOM changes', async ({ runAndTrace, page, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.setContent(''); + await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); + + await page.setContent(''); + await page.evaluate(() => { + window['rule'] = document.styleSheets[0].cssRules[0]; + void 0; + }); + await page.evaluate(() => { window['rule'].cssRules[0].style.color = 'black'; }); + await page.evaluate(() => { window['rule'].insertRule('button:not(.disabled) { color: green; }', 1); }); + + await page.route('**/style.css', route => { + route.fulfill({ body: 'button { color: red; }', }).catch(() => {}); + }); + await page.goto(server.EMPTY_PAGE); + await page.setContent(''); + await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); + }); + + const frame1 = await traceViewer.snapshotFrame('Set content', 0); + await expect(frame1.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); + const frame2 = await traceViewer.snapshotFrame('Evaluate', 0); + await expect(frame2.locator('button')).toHaveCSS('color', 'rgb(0, 0, 255)'); + + const frame3 = await traceViewer.snapshotFrame('Set content', 1); + await expect(frame3.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); + const frame4 = await traceViewer.snapshotFrame('Evaluate', 2); + await expect(frame4.locator('button')).toHaveCSS('color', 'rgb(0, 0, 0)'); + const frame5 = await traceViewer.snapshotFrame('Evaluate', 3); + await expect(frame5.locator('button')).toHaveCSS('color', 'rgb(0, 128, 0)'); + + const frame6 = await traceViewer.snapshotFrame('Set content', 2); + await expect(frame6.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)'); + const frame7 = await traceViewer.snapshotFrame('Evaluate', 4); + await expect(frame7.locator('button')).toHaveCSS('color', 'rgb(0, 0, 255)'); +});