diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index ba15c00b6..5a63c8011 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -230,12 +230,14 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { const kept = []; for (let index = 0; index < resources.length; index++) { const resource = resources[index]; + // Never compress in place: these objects are shared with the resource + // cache, which must keep replaying the original bytes to the browser. + let uploaded = resource; try { - const alreadyZipped = isGzipped(resource.content); - /* istanbul ignore next: very hard to mock true */ - if (!alreadyZipped) { - resource.content = Pako.gzip(resource.content); - resource.sha = sha256hash(resource.content); + /* istanbul ignore else: an already-gzipped resource is very hard to mock */ + if (!isGzipped(resource.content)) { + const content = Pako.gzip(resource.content); + uploaded = { ...resource, content, sha: sha256hash(content) }; log.debug(`- Gzipped resource: ${resource.url}`); } } catch (error) { @@ -247,9 +249,8 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { continue; } - // resource.content is guaranteed by the try block above (either just - // assigned from Pako.gzip, or alreadyZipped was true). - const size = resource.content.length; + // Either the gzipped copy or, when already gzipped, the resource itself. + const size = uploaded.content.length; // Root (DOM HTML) and log resources are required for a valid snapshot; // shipping an oversized one and letting the API surface a clear error // is better than silently dropping it here. @@ -263,7 +264,7 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { ); continue; } - kept.push(resource); + kept.push(uploaded); } resources = kept; } diff --git a/packages/core/test/discovery.test.js b/packages/core/test/discovery.test.js index d9008c842..fd6ba579d 100644 --- a/packages/core/test/discovery.test.js +++ b/packages/core/test/discovery.test.js @@ -839,6 +839,33 @@ describe('Discovery', () => { ); }); + it('keeps cached resources uncompressed when PERCY_GZIP is enabled', async () => { + // A cached stylesheet compressed in place is replayed as gzip bytes under its + // original text/css type, so the browser never parses it or its @font-face. + process.env.PERCY_GZIP = true; + + server.reply('/style.css', () => [200, 'text/css', [ + '@font-face { font-family: "test"; src: url("/font.woff") format("woff"); }', + 'body { font-family: "test", "sans-serif"; }' + ].join('')]); + + await percy.snapshot({ name: 'one', url: 'http://localhost:8000', domSnapshot: testDOM }); + await percy.snapshot({ name: 'two', url: 'http://localhost:8000', domSnapshot: testDOM }); + + await percy.idle(); + + let font = jasmine.objectContaining({ + id: sha256hash(Pako.gzip('')), + attributes: jasmine.objectContaining({ + 'resource-url': 'http://localhost:8000/font.woff' + }) + }); + + expect(captured[0]).toEqual(jasmine.arrayContaining([font])); + // snapshot two's stylesheet comes from the cache and must still be parseable + expect(captured[1]).toEqual(jasmine.arrayContaining([font])); + }); + it('captures resource larger than 25MB raw when PERCY_GZIP is enabled', async () => { process.env.PERCY_GZIP = true; const largeCSS = 'A'.repeat(30_000_000);