From f6abe9874da4971394970fe172066673a902913e Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 30 Sep 2026 11:10:34 +0530 Subject: [PATCH 1/5] fix(dom): emit root-relative URLs for serialized resources (PPLT-6109) resourceFromDataURL and resourceFromText built an absolute URL for every resource @percy/dom fabricates -- new URL(path, document.URL) with localhost swapped for a placeholder host. That bakes the capture host's scheme (usually http) into the DOM. On https real-device renders the resulting is mixed content, the browser drops it before any request, and pages built from constructed stylesheets render unstyled. The scheme cannot be repaired downstream once it is in the DOM text. The resource is same-origin by construction, so emit /__serialized__/ and let the browser resolve it against whatever origin serves the page at render time. The API already normalises a /-rooted resource URL to localhost and maps it onto the render host, so the client-side rewrite was duplicating that and pinning the scheme. rewriteLocalhostURL has no remaining callers and is not part of the bundle API, so it is removed with its specs. The serialize-dom "adds node details ... and rethrow it" spec triggered its error by setting window.URL = undefined, i.e. it relied on the new URL() call removed here. It now fails inside the img's own base64 step via a setAttribute fake keyed on data-percy-serialized-attribute-src, exercising the same clone-dom decoration path, and no longer leaves window.URL undefined when the assertion fails. Co-Authored-By: Claude Fable 5.1 --- packages/dom/src/utils.js | 20 ++++---- packages/dom/test/serialize-dom.test.js | 11 +++-- packages/dom/test/utils.test.js | 66 +++++-------------------- 3 files changed, 29 insertions(+), 68 deletions(-) diff --git a/packages/dom/src/utils.js b/packages/dom/src/utils.js index ee9ee9cad..6e4566426 100644 --- a/packages/dom/src/utils.js +++ b/packages/dom/src/utils.js @@ -14,20 +14,24 @@ export function resourceFromDataURL(uid, dataURL) { let [, mimetype] = data.split(':'); [mimetype] = mimetype.split(';'); - // build a URL for the serialized asset + // build a root-relative URL for the serialized asset. It is same-origin by + // construction (served from wherever the captured page itself ends up), so + // it never needs a host or scheme baked in -- PPLT-6109: an absolute URL + // here previously carried over whatever scheme the local capture host used + // (typically http), which survives every downstream hostname rewrite and + // becomes a mixed-content block on any https render. let [, ext] = mimetype.split('/'); - let path = `/__serialized__/${uid}.${ext}`; - let url = rewriteLocalhostURL(new URL(path, document.URL).toString()); + let url = `/__serialized__/${uid}.${ext}`; // return the url, base64 content, and mimetype return { url, content, mimetype }; } export function resourceFromText(uid, mimetype, data) { - // build a URL for the serialized asset + // build a root-relative URL for the serialized asset -- see + // resourceFromDataURL above for why this is relative, not absolute. let [, ext] = mimetype.split('/'); - let path = `/__serialized__/${uid}.${ext}`; - let url = rewriteLocalhostURL(new URL(path, document.URL).toString()); + let url = `/__serialized__/${uid}.${ext}`; // return the url, text content, and mimetype return { url, content: data, mimetype }; } @@ -53,10 +57,6 @@ export function styleSheetFromNode(node) { } } -export function rewriteLocalhostURL(url) { - return url.replace(/(http[s]{0,1}:\/\/)(localhost|127.0.0.1)[:\d+]*/, '$1render.percy.local'); -} - // Utility function to handle errors export function handleErrors(error, prefixMessage, element = null, additionalData = {}) { let elementData = {}; diff --git a/packages/dom/test/serialize-dom.test.js b/packages/dom/test/serialize-dom.test.js index f498526ba..e3d25099a 100644 --- a/packages/dom/test/serialize-dom.test.js +++ b/packages/dom/test/serialize-dom.test.js @@ -654,17 +654,22 @@ describe('serializeDOM', () => { describe('error handling', () => { it('adds node details in error message and rethrow it', () => { - let oldURL = window.URL; - window.URL = undefined; withExample(` Example Image `); + // Fail inside the img's own serialization step (the base64 serializer is + // the only thing that sets this attribute) so the decorated error names it. + let setAttribute = window.Element.prototype.setAttribute; + spyOn(window.Element.prototype, 'setAttribute').and.callFake(function(name, value) { + if (name === 'data-percy-serialized-attribute-src') throw new Error('boom'); + return setAttribute.call(this, name, value); + }); + expect(() => serializeDOM()).toThrowMatching((error) => { return error.message.includes('Error cloning node:') && error.message.includes('{"nodeName":"IMG","classNames":"test1 test2","id":"test"}'); }); - window.URL = oldURL; }); it('ignores canvas serialization errors when flag is enabled', () => { diff --git a/packages/dom/test/utils.test.js b/packages/dom/test/utils.test.js index 128e402dd..bda9e7332 100644 --- a/packages/dom/test/utils.test.js +++ b/packages/dom/test/utils.test.js @@ -1,4 +1,4 @@ -import { resourceFromDataURL, resourceFromText, rewriteLocalhostURL, styleSheetFromNode } from '../src/utils'; +import { resourceFromDataURL, resourceFromText, styleSheetFromNode } from '../src/utils'; describe('utils', () => { describe('styleSheetFromNode', () => { it('creates stylesheet properly', () => { @@ -47,38 +47,28 @@ describe('utils', () => { describe('resourceFromDataURL', () => { const uid = (Math.random() + 1).toString(36).substring(10); const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACbVJREFUeF7tXAWoFVEQnW+'; - it('If URL is localhost, replace it to render.percy.local', () => { + // PPLT-6109: the URL is root-relative regardless of document.URL -- it is + // same-origin by construction, so it never needs a host or scheme baked in. + it('builds a root-relative URL regardless of document.URL', () => { Object.defineProperty(window.document, 'URL', { writable: true, value: 'http://localhost' }); const result = resourceFromDataURL(uid, dataURL); expect(result).toEqual({ - url: `http://render.percy.local/__serialized__/${uid}.png`, + url: `/__serialized__/${uid}.png`, content: 'iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACbVJREFUeF7tXAWoFVEQnW+', mimetype: 'image/png' }); }); - it('If URL is 127.0.0.1, replace it to render.percy.local', () => { - Object.defineProperty(window.document, 'URL', { - writable: true, - value: 'http://127.0.0.1' - }); - const result = resourceFromDataURL(uid, dataURL); - expect(result).toEqual({ - url: `http://render.percy.local/__serialized__/${uid}.png`, - content: 'iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACbVJREFUeF7tXAWoFVEQnW+', - mimetype: 'image/png' - }); - }); - it('If URL is not localhost, return as is', () => { + it('builds the same root-relative URL for a non-localhost document.URL', () => { Object.defineProperty(window.document, 'URL', { writable: true, value: 'http://example.com' }); const result = resourceFromDataURL(uid, dataURL); expect(result).toEqual({ - url: `http://example.com/__serialized__/${uid}.png`, + url: `/__serialized__/${uid}.png`, content: 'iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACbVJREFUeF7tXAWoFVEQnW+', mimetype: 'image/png' }); @@ -87,63 +77,29 @@ describe('utils', () => { describe('resourceFromText', () => { const uid = (Math.random() + 1).toString(36).substring(10); const dataURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACbVJREFUeF7tXAWoFVEQnW+'; - it('Replace localhost to render.percy.local', () => { + it('builds a root-relative URL regardless of document.URL', () => { Object.defineProperty(window.document, 'URL', { writable: true, value: 'http://localhost' }); const result = resourceFromText(uid, 'image/png', dataURL); expect(result).toEqual({ - url: `http://render.percy.local/__serialized__/${uid}.png`, + url: `/__serialized__/${uid}.png`, content: dataURL, mimetype: 'image/png' }); }); - it('Replace 127.0.0.1 to render.percy.local', () => { - Object.defineProperty(window.document, 'URL', { - writable: true, - value: 'http://127.0.0.1' - }); - const result = resourceFromText(uid, 'image/png', dataURL); - expect(result).toEqual({ - url: `http://render.percy.local/__serialized__/${uid}.png`, - content: dataURL, - mimetype: 'image/png' - }); - }); - it('If URL is not localhost, return as is', () => { + it('builds the same root-relative URL for a non-localhost document.URL', () => { Object.defineProperty(window.document, 'URL', { writable: true, value: 'http://example.com' }); const result = resourceFromText(uid, 'image/png', dataURL); expect(result).toEqual({ - url: `http://example.com/__serialized__/${uid}.png`, + url: `/__serialized__/${uid}.png`, content: dataURL, mimetype: 'image/png' }); }); }); - describe('rewriteLocalhostURL', () => { - it('should replace with render.percy.local', () => { - const case1 = rewriteLocalhostURL('https://localhost/hello'); - expect(case1).toEqual('https://render.percy.local/hello'); - const case2 = rewriteLocalhostURL('http://localhost:4000/hello'); - expect(case2).toEqual('http://render.percy.local/hello'); - const case3 = rewriteLocalhostURL('http://localhost/hello'); - expect(case3).toEqual('http://render.percy.local/hello'); - const case4 = rewriteLocalhostURL('https://localhost:4000/hello'); - expect(case4).toEqual('https://render.percy.local/hello'); - }); - it('Should not replace url', () => { - const case1 = rewriteLocalhostURL('http://hello.com/localhost/'); - expect(case1).toEqual('http://hello.com/localhost/'); - const case2 = rewriteLocalhostURL('http://hello/world'); - expect(case2).toEqual('http://hello/world'); - const case3 = rewriteLocalhostURL('http://hellolocalhost:2000/world'); - expect(case3).toEqual('http://hellolocalhost:2000/world'); - const case4 = rewriteLocalhostURL('https://hellolocalhost:2000/world'); - expect(case4).toEqual('https://hellolocalhost:2000/world'); - }); - }); }); From 3b0a582733bf66f5563fbd4f3a5af77ffc7587e1 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 30 Sep 2026 11:10:35 +0530 Subject: [PATCH 2/5] fix(core): match relative serialized resource keys during asset discovery (PPLT-6109) parseDomResources keys the snapshot resource map by the URL string @percy/dom returns, which is now root-relative, but the discovery browser requests the resolved absolute form. The exact lookup missed, discovery fell through to a real network fetch, the in-memory uid 404'd, and the resource was aborted with ASSET_NOT_UPLOADED / Disallowed status code. lookupCacheResource now falls back to the request's pathname. Only @percy/dom-fabricated resources have relative keys (network-discovered and root resources are keyed absolute) and their paths carry no query string, so the pathname is their full identity. Done in the lookup rather than by resolving the key at map-build time, which would make the uploaded resourceUrl absolute again and defeat the fix. Co-Authored-By: Claude Fable 5.1 --- packages/core/src/discovery.js | 13 +++++++++++++ packages/core/test/unit/byte-lru.test.js | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index f31d4b7fc..76de4bc56 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -462,6 +462,19 @@ function readWarnThresholdBytes() { // cache here is guaranteed to be a ByteLRU when we enter this branch. export function lookupCacheResource(percy, snapshotResources, cache, url, width) { let resource = snapshotResources.get(url) || cache.get(url); + + // @percy/dom keys the resources it fabricates by a root-relative URL + // (/__serialized__/.) so no host or scheme is baked into the + // snapshot; the discovery browser requests the resolved absolute form. + // Match on pathname so provided content is found either way. + if (!resource) { + try { + resource = snapshotResources.get(new URL(url).pathname); + } catch (e) { + // url is not absolute -- nothing further to try + } + } + const disk = percy[DISK_SPILL_KEY]; if (!resource && disk) { resource = disk.get(url); diff --git a/packages/core/test/unit/byte-lru.test.js b/packages/core/test/unit/byte-lru.test.js index fd782f63b..c8be3a419 100644 --- a/packages/core/test/unit/byte-lru.test.js +++ b/packages/core/test/unit/byte-lru.test.js @@ -616,6 +616,21 @@ describe('Unit / lookupCacheResource', () => { expect(lookupCacheResource(percy, snapshotResources, cache, 'a')).toBe(local); }); + it('matches an absolute request against a root-relative snapshot key', () => { + // @percy/dom keys fabricated resources by /__serialized__/.; + // the discovery browser asks for the resolved absolute URL. + const { percy } = makePercy(undefined); + const provided = { url: '/__serialized__/_abc.css', provided: true, content: Buffer.from('P') }; + const snapshotResources = new Map([['/__serialized__/_abc.css', provided]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); + }); + + it('does not match an absolute request whose pathname is not a snapshot key', () => { + const { percy } = makePercy(undefined); + const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css' }]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_other.css')).toBeUndefined(); + }); + it('falls through to RAM cache when snapshot has no entry', () => { const { percy } = makePercy(undefined); const cache = new ByteLRU(); From 2f97434f2a745188d765625fbef3f6e65956aa9e Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 30 Sep 2026 17:53:59 +0530 Subject: [PATCH 3/5] fix(core): check both snapshot keys before the shared cache (PPLT-6109) lookupCacheResource consulted the shared RAM cache between the two snapshot lookups, so a cached entry for the absolute request URL would shadow the snapshot's own provided resource keyed by the root-relative path. The snapshot-local resource must always win over the cross-snapshot cache; run both snapshot lookups first, then the cache, then disk. Adds a spec that seeds the cache with the absolute URL and asserts the provided snapshot resource is returned. Addresses CodeRabbit review on #2445. Co-Authored-By: Claude Fable 5.1 --- packages/core/src/discovery.js | 7 +++++-- packages/core/test/unit/byte-lru.test.js | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index 76de4bc56..610cf395b 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -461,12 +461,14 @@ function readWarnThresholdBytes() { // ByteLRU tier is active (see createDiscoveryQueue 'start' handler), so the // cache here is guaranteed to be a ByteLRU when we enter this branch. export function lookupCacheResource(percy, snapshotResources, cache, url, width) { - let resource = snapshotResources.get(url) || cache.get(url); + let resource = snapshotResources.get(url); // @percy/dom keys the resources it fabricates by a root-relative URL // (/__serialized__/.) so no host or scheme is baked into the // snapshot; the discovery browser requests the resolved absolute form. - // Match on pathname so provided content is found either way. + // Match on pathname so provided content is found either way. Both snapshot + // lookups run before the shared cache so a snapshot's own provided bytes + // always win over whatever an earlier fetch of the same URL left cached. if (!resource) { try { resource = snapshotResources.get(new URL(url).pathname); @@ -474,6 +476,7 @@ export function lookupCacheResource(percy, snapshotResources, cache, url, width) // url is not absolute -- nothing further to try } } + if (!resource) resource = cache.get(url); const disk = percy[DISK_SPILL_KEY]; if (!resource && disk) { diff --git a/packages/core/test/unit/byte-lru.test.js b/packages/core/test/unit/byte-lru.test.js index c8be3a419..89c36d47f 100644 --- a/packages/core/test/unit/byte-lru.test.js +++ b/packages/core/test/unit/byte-lru.test.js @@ -625,6 +625,15 @@ describe('Unit / lookupCacheResource', () => { expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); }); + it('prefers a root-relative provided snapshot resource over a cached entry for the absolute URL', () => { + const { percy } = makePercy(undefined); + const provided = { url: '/__serialized__/_abc.css', provided: true, content: Buffer.from('SNAPSHOT') }; + const snapshotResources = new Map([['/__serialized__/_abc.css', provided]]); + const cache = new ByteLRU(); + cache.set('http://localhost:3000/__serialized__/_abc.css', { url: 'http://localhost:3000/__serialized__/_abc.css', content: Buffer.from('CACHED') }, 100); + expect(lookupCacheResource(percy, snapshotResources, cache, 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); + }); + it('does not match an absolute request whose pathname is not a snapshot key', () => { const { percy } = makePercy(undefined); const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css' }]]); From 1f9855005052d97face5ad770c7ad14e9af7ce52 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 30 Sep 2026 18:04:17 +0530 Subject: [PATCH 4/5] fix(core): keep the query string in the relative-key fallback (PPLT-6109) parseDomResources preserves query strings in snapshot keys, but the fallback matched on pathname alone, so a request like /asset.css?theme=dark could be served the bytes keyed as /asset.css. Match on the origin-less form of the request (pathname + search) so the fallback never loosens identity. Specs: a request carrying a query must not match a bare key; a key that includes a query is matched by a request carrying the same one. Addresses CodeRabbit review on #2445. Co-Authored-By: Claude Fable 5.1 --- packages/core/src/discovery.js | 10 ++++++---- packages/core/test/unit/byte-lru.test.js | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index 610cf395b..19a037c4c 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -466,12 +466,14 @@ export function lookupCacheResource(percy, snapshotResources, cache, url, width) // @percy/dom keys the resources it fabricates by a root-relative URL // (/__serialized__/.) so no host or scheme is baked into the // snapshot; the discovery browser requests the resolved absolute form. - // Match on pathname so provided content is found either way. Both snapshot - // lookups run before the shared cache so a snapshot's own provided bytes - // always win over whatever an earlier fetch of the same URL left cached. + // Match on the origin-less form (path + query) so provided content is found + // either way without loosening identity. Both snapshot lookups run before + // the shared cache so a snapshot's own provided bytes always win over + // whatever an earlier fetch of the same URL left cached. if (!resource) { try { - resource = snapshotResources.get(new URL(url).pathname); + let { pathname, search } = new URL(url); + resource = snapshotResources.get(pathname + search); } catch (e) { // url is not absolute -- nothing further to try } diff --git a/packages/core/test/unit/byte-lru.test.js b/packages/core/test/unit/byte-lru.test.js index 89c36d47f..f1740cabe 100644 --- a/packages/core/test/unit/byte-lru.test.js +++ b/packages/core/test/unit/byte-lru.test.js @@ -634,6 +634,19 @@ describe('Unit / lookupCacheResource', () => { expect(lookupCacheResource(percy, snapshotResources, cache, 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); }); + it('does not match a bare root-relative key when the absolute request carries a query string', () => { + const { percy } = makePercy(undefined); + const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css', provided: true }]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css?theme=dark')).toBeUndefined(); + }); + + it('matches a root-relative key that includes a query string when the request carries the same one', () => { + const { percy } = makePercy(undefined); + const provided = { url: '/asset.css?theme=dark', provided: true, content: Buffer.from('DARK') }; + const snapshotResources = new Map([['/asset.css?theme=dark', provided]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/asset.css?theme=dark')).toBe(provided); + }); + it('does not match an absolute request whose pathname is not a snapshot key', () => { const { percy } = makePercy(undefined); const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css' }]]); From 8e2323ad4071391b1d1e3524199f446ec78f7087 Mon Sep 17 00:00:00 2001 From: bhokaremoin Date: Wed, 30 Sep 2026 18:32:03 +0530 Subject: [PATCH 5/5] fix(core): apply the relative-key fallback only to same-origin requests (PPLT-6109) A root-relative href can only resolve against the page's own origin, so the pathname+query fallback must not answer an allowed cross-origin request that happens to share the path with a provided snapshot resource -- that request has to get its own origin's bytes. lookupCacheResource now takes the snapshot url and applies the fallback only when the request origin matches it; without a snapshot url the fallback is skipped. Exact-URL matches, including cross-origin provided resources, are unchanged. Specs: same-origin request still matches; a cross-origin request with the same path does not; no snapshot url means no fallback. Addresses CodeRabbit review on #2445. Co-Authored-By: Claude Fable 5.1 --- packages/core/src/discovery.js | 22 +++++++++++++--------- packages/core/test/unit/byte-lru.test.js | 24 +++++++++++++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index 19a037c4c..95e6cb39a 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -460,20 +460,24 @@ function readWarnThresholdBytes() { // the actual coldest entry if needed. DISK_SPILL_KEY is only set when the // ByteLRU tier is active (see createDiscoveryQueue 'start' handler), so the // cache here is guaranteed to be a ByteLRU when we enter this branch. -export function lookupCacheResource(percy, snapshotResources, cache, url, width) { +export function lookupCacheResource(percy, snapshotResources, cache, url, width, rootUrl) { let resource = snapshotResources.get(url); // @percy/dom keys the resources it fabricates by a root-relative URL // (/__serialized__/.) so no host or scheme is baked into the // snapshot; the discovery browser requests the resolved absolute form. - // Match on the origin-less form (path + query) so provided content is found - // either way without loosening identity. Both snapshot lookups run before - // the shared cache so a snapshot's own provided bytes always win over - // whatever an earlier fetch of the same URL left cached. - if (!resource) { + // Match on the origin-less form (path + query), but only for requests from + // the snapshot's own origin: a root-relative href can resolve nowhere else, + // and an allowed cross-origin request must never be answered with the + // snapshot's bytes. Both snapshot lookups run before the shared cache so a + // snapshot's own provided bytes always win over whatever an earlier fetch + // of the same URL left cached. + if (!resource && rootUrl) { try { - let { pathname, search } = new URL(url); - resource = snapshotResources.get(pathname + search); + let { origin, pathname, search } = new URL(url); + if (origin === new URL(rootUrl).origin) { + resource = snapshotResources.get(pathname + search); + } } catch (e) { // url is not absolute -- nothing further to try } @@ -670,7 +674,7 @@ export function createDiscoveryQueue(percy) { allowedHostnames: snapshot.discovery.allowedHostnames, disallowedHostnames: snapshot.discovery.disallowedHostnames, getResource: (u, width = null) => ( - lookupCacheResource(percy, snapshot.resources, cache, u, width) + lookupCacheResource(percy, snapshot.resources, cache, u, width, snapshot.url) ), saveResource: r => { const limitResources = process.env.LIMIT_SNAPSHOT_RESOURCES || false; diff --git a/packages/core/test/unit/byte-lru.test.js b/packages/core/test/unit/byte-lru.test.js index f1740cabe..fc4055e11 100644 --- a/packages/core/test/unit/byte-lru.test.js +++ b/packages/core/test/unit/byte-lru.test.js @@ -622,7 +622,7 @@ describe('Unit / lookupCacheResource', () => { const { percy } = makePercy(undefined); const provided = { url: '/__serialized__/_abc.css', provided: true, content: Buffer.from('P') }; const snapshotResources = new Map([['/__serialized__/_abc.css', provided]]); - expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css', undefined, 'http://localhost:3000/')).toBe(provided); }); it('prefers a root-relative provided snapshot resource over a cached entry for the absolute URL', () => { @@ -631,26 +631,40 @@ describe('Unit / lookupCacheResource', () => { const snapshotResources = new Map([['/__serialized__/_abc.css', provided]]); const cache = new ByteLRU(); cache.set('http://localhost:3000/__serialized__/_abc.css', { url: 'http://localhost:3000/__serialized__/_abc.css', content: Buffer.from('CACHED') }, 100); - expect(lookupCacheResource(percy, snapshotResources, cache, 'http://localhost:3000/__serialized__/_abc.css')).toBe(provided); + expect(lookupCacheResource(percy, snapshotResources, cache, 'http://localhost:3000/__serialized__/_abc.css', undefined, 'http://localhost:3000/')).toBe(provided); }); it('does not match a bare root-relative key when the absolute request carries a query string', () => { const { percy } = makePercy(undefined); const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css', provided: true }]]); - expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css?theme=dark')).toBeUndefined(); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css?theme=dark', undefined, 'http://localhost:3000/')).toBeUndefined(); }); it('matches a root-relative key that includes a query string when the request carries the same one', () => { const { percy } = makePercy(undefined); const provided = { url: '/asset.css?theme=dark', provided: true, content: Buffer.from('DARK') }; const snapshotResources = new Map([['/asset.css?theme=dark', provided]]); - expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/asset.css?theme=dark')).toBe(provided); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/asset.css?theme=dark', undefined, 'http://localhost:3000/')).toBe(provided); + }); + + it('does not apply the relative-key fallback to a request from another origin', () => { + // An allowed cross-origin request that happens to share the path must be + // served its own origin's bytes, never the snapshot's. + const { percy } = makePercy(undefined); + const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css', provided: true }]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'https://cdn.example.com/__serialized__/_abc.css', undefined, 'http://localhost:3000/')).toBeUndefined(); + }); + + it('does not apply the relative-key fallback when no snapshot url is given', () => { + const { percy } = makePercy(undefined); + const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css', provided: true }]]); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_abc.css')).toBeUndefined(); }); it('does not match an absolute request whose pathname is not a snapshot key', () => { const { percy } = makePercy(undefined); const snapshotResources = new Map([['/__serialized__/_abc.css', { url: '/__serialized__/_abc.css' }]]); - expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_other.css')).toBeUndefined(); + expect(lookupCacheResource(percy, snapshotResources, new ByteLRU(), 'http://localhost:3000/__serialized__/_other.css', undefined, 'http://localhost:3000/')).toBeUndefined(); }); it('falls through to RAM cache when snapshot has no entry', () => {