diff --git a/__playwright-tests__/fixtures/asset-paths.html b/__playwright-tests__/fixtures/asset-paths.html index eeb93a16..f367de0d 100644 --- a/__playwright-tests__/fixtures/asset-paths.html +++ b/__playwright-tests__/fixtures/asset-paths.html @@ -28,5 +28,16 @@
+ +

srcset

+ + +

blue < 768px <= pink

+ \ No newline at end of file diff --git a/package.json b/package.json index ea5032bb..78837c2e 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "fs-extra": "^11.1.1", "mime": "^3.0.0", "rrweb-snapshot": "^2.0.0-alpha.4", + "srcset": "^4.0.0", "ts-dedent": "^2.2.0" }, "peerDependencies": { diff --git a/src/write-archive/dom-snapshot.test.ts b/src/write-archive/dom-snapshot.test.ts index 335ca6c4..968531e9 100644 --- a/src/write-archive/dom-snapshot.test.ts +++ b/src/write-archive/dom-snapshot.test.ts @@ -9,6 +9,10 @@ function createSnapshot(url1: string, url2: string, url3: string) { return `{"type":0,"childNodes":[{"type":1,"name":"html","publicId":"","systemId":"","id":2},{"type":2,"tagName":"html","attributes":{},"childNodes":[{"type":2,"tagName":"head","attributes":{},"childNodes":[{"type":3,"textContent":" ","id":5},{"type":2,"tagName":"link","attributes":{"rel":"stylesheet","href":"/styles-test.css"},"childNodes":[],"id":6},{"type":3,"textContent":" ","id":7},{"type":2,"tagName":"style","attributes":{},"childNodes":[{"type":3,"textContent":".test1 { background-image: url(\\"${url1}\\"); }.test2 { background-image: url(\\"${url2}\\"); }.test2 { background-image: url(\\"${url3}\\"); }","isStyle":true,"id":9}],"id":8},{"type":3,"textContent":" ","id":10}],"id":4},{"type":3,"textContent":" ","id":11},{"type":2,"tagName":"body","attributes":{},"childNodes":[{"type":3,"textContent":" ","id":13},{"type":2,"tagName":"div","attributes":{"class":"image-container flex flex-wrap"},"childNodes":[{"type":3,"textContent":" ","id":15},{"type":2,"tagName":"img","attributes":{"src":"${url1}"},"childNodes":[],"id":16},{"type":3,"textContent":" ","id":17},{"type":2,"tagName":"img","attributes":{"src":"${url2}"},"childNodes":[],"id":18},{"type":3,"textContent":" ","id":19},{"type":2,"tagName":"img","attributes":{"src":"${url3}"},"childNodes":[],"id":20},{"type":3,"textContent":" ","id":21},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url1}'); no-repeat center;"},"childNodes":[],"id":22},{"type":3,"textContent":" ","id":23},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url2}'); no-repeat center;"},"childNodes":[],"id":24},{"type":3,"textContent":" ","id":25},{"type":2,"tagName":"div","attributes":{"style":"background: url('${url3}'); no-repeat center;"},"childNodes":[],"id":26},{"type":3,"textContent":" ","id":27}],"id":14},{"type":3,"textContent":" ","id":28}],"id":12}],"id":3}],"id":1}`; } +function createSrcsetSnapshot(url1: string, url2: string, url3: string) { + return `{"type":2,"tagName":"img","attributes":{"srcset":"${url2} 384w, ${url3} 1920w","sizes":"(min-width: 768px) 768px, 192px","src":"${url1}"},"childNodes":[],"id":61}`; +} + const snapshot = createSnapshot(relativeUrl, externalUrl, queryUrl); const expectedMappedSnapshot = createSnapshot(relativeUrl, externalUrl, queryUrlTransformed); @@ -32,5 +36,24 @@ describe('DOMSnapshot', () => { expect(mappedSnapshot).toEqual(snapshot); }); + + it('maps img srcsets', async () => { + const domSnapshot = new DOMSnapshot(createSrcsetSnapshot(relativeUrl, externalUrl, queryUrl)); + + const mappedSnapshot = await domSnapshot.mapAssetPaths(sourceMap); + + expect(mappedSnapshot).toEqual( + `{"type":2,"tagName":"img","attributes":{"src":"${queryUrlTransformed}"},"childNodes":[],"id":61}` + ); + }); + + it('does not change img srcsets when no mapped asset found in source map', async () => { + const origSnapshot = createSrcsetSnapshot(relativeUrl, externalUrl, queryUrl); + const domSnapshot = new DOMSnapshot(origSnapshot); + + const mappedSnapshot = await domSnapshot.mapAssetPaths(new Map()); + + expect(mappedSnapshot).toEqual(origSnapshot); + }); }); }); diff --git a/src/write-archive/dom-snapshot.ts b/src/write-archive/dom-snapshot.ts index 5251386d..10d01f07 100644 --- a/src/write-archive/dom-snapshot.ts +++ b/src/write-archive/dom-snapshot.ts @@ -2,6 +2,7 @@ /* eslint-disable no-param-reassign */ import type { serializedNodeWithId } from 'rrweb-snapshot'; import { NodeType } from 'rrweb-snapshot'; +import srcset from 'srcset'; // Matches `url(...)` function in CSS text, excluding data URLs const CSS_URL_REGEX = /url\((?!['"]?(?:data):)['"]?([^'")]*)['"]?\)/gi; @@ -63,6 +64,27 @@ export class DOMSnapshot { const mappedCssText = this.mapCssUrls(cssText, sourceMap); node.attributes._cssText = mappedCssText; } + + // When an image tag has the `srcset` attributes, the browser will choose one of the images + // from the `srcset` list to load at render time based on the viewport size. To support this, + // we parse the URLs in the `srcset` attribute and try to find a match in the asset map. + // If a match is found, we'll overwrite the `src` attribute with the mapped asset path, + // and we'll remove the `srcset` and `sizes` attributes because we'll only have captured + // the one asset that the browser decided to load when this was rendered. We don't want + // the browser to try to load one of the others when this snapshot is rendered in Chromatic + // because we won't have archived them. + if (node.tagName === 'img' && node.attributes.srcset) { + const srcsetValue = node.attributes.srcset as string; + const currentSrc = this.mapSrcsetUrls(srcsetValue, sourceMap); + if (currentSrc) { + node.attributes.src = currentSrc; + + // Remove srcset attributes since we'll only have the one that + // loaded on render archived + delete node.attributes.srcset; + delete node.attributes.sizes; + } + } } return node; @@ -88,6 +110,18 @@ export class DOMSnapshot { return cssUrl; }); } + + private mapSrcsetUrls(srcsetValue: string, sourceMap: Map) { + const parsedSrcset = srcset.parse(srcsetValue); + let currentSrc; + parsedSrcset.forEach((set) => { + if (sourceMap.has(set.url)) { + currentSrc = sourceMap.get(set.url); + } + }); + + return currentSrc; + } } /* eslint-enable no-underscore-dangle */ /* eslint-enable no-param-reassign */ diff --git a/yarn.lock b/yarn.lock index 7888ae01..5e55bdbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11920,6 +11920,11 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +srcset@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" + integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== + sshpk@^1.14.1: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"