Skip to content

Commit

Permalink
Added support for image srcset capturing (#1534)
Browse files Browse the repository at this point in the history
* Added support for image srcset capturing

* Added tests + addressed comments

* Fix breaking tests

* Remove default value captureSrcset

* Fix edge case
  • Loading branch information
chinmay-browserstack committed Mar 4, 2024
1 parent f3b0fad commit 76df992
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 8 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ export const configSchema = {
type: 'boolean',
default: false
},
captureSrcset: {
type: 'boolean'
},
requestHeaders: {
type: 'object',
normalize: false,
Expand Down Expand Up @@ -276,6 +279,7 @@ export const snapshotSchema = {
authorization: { $ref: '/config/discovery#/properties/authorization' },
disableCache: { $ref: '/config/discovery#/properties/disableCache' },
captureMockedServiceWorker: { $ref: '/config/discovery#/properties/captureMockedServiceWorker' },
captureSrcset: { $ref: '/config/discovery#/properties/captureSrcset' },
userAgent: { $ref: '/config/discovery#/properties/userAgent' },
devicePixelRatio: { $ref: '/config/discovery#/properties/devicePixelRatio' }
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function debugSnapshotOptions(snapshot) {
debugProp(snapshot, 'discovery.authorization', JSON.stringify);
debugProp(snapshot, 'discovery.disableCache');
debugProp(snapshot, 'discovery.captureMockedServiceWorker');
debugProp(snapshot, 'discovery.captureSrcset');
debugProp(snapshot, 'discovery.userAgent');
debugProp(snapshot, 'clientInfo');
debugProp(snapshot, 'environmentInfo');
Expand Down Expand Up @@ -171,6 +172,14 @@ async function* captureSnapshotResources(page, snapshot, options) {
yield page.evaluate(snapshot.execute.afterNavigation);
}

// Running before page idle since this will trigger many network calls
// so need to run as early as possible. plus it is just reading urls from dom srcset
// which will be already loaded after navigation complete
if (discovery.captureSrcset) {
await page.insertPercyDom();
yield page.eval('window.PercyDOM.loadAllSrcsetLinks()');
}

// iterate over additional snapshots for proper DOM capturing
for (let additionalSnapshot of [baseSnapshot, ...additionalSnapshots]) {
let isBaseSnapshot = additionalSnapshot === baseSnapshot;
Expand Down
20 changes: 12 additions & 8 deletions packages/core/src/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@ export class Page {
for (let script of scripts) await this.eval(script);
}

async insertPercyDom() {
// inject @percy/dom for serialization by evaluating the file contents which adds a global
// PercyDOM object that we can later check against
/* istanbul ignore next: no instrumenting injected code */
if (await this.eval(() => !window.PercyDOM)) {
this.log.debug('Inject @percy/dom', this.meta);
let script = await fs.promises.readFile(PERCY_DOM, 'utf-8');
await this.eval(new Function(script)); /* eslint-disable-line no-new-func */
}
}

// Takes a snapshot after waiting for any timeout, waiting for any selector, executing any
// scripts, and waiting for the network idle. Returns all other provided snapshot options along
// with the captured URL and DOM snapshot.
Expand Down Expand Up @@ -165,14 +176,7 @@ export class Page {
// wait for any final network activity before capturing the dom snapshot
await this.network.idle();

// inject @percy/dom for serialization by evaluating the file contents which adds a global
// PercyDOM object that we can later check against
/* istanbul ignore next: no instrumenting injected code */
if (await this.eval(() => !window.PercyDOM)) {
this.log.debug('Inject @percy/dom', this.meta);
let script = await fs.promises.readFile(PERCY_DOM, 'utf-8');
await this.eval(new Function(script)); /* eslint-disable-line no-new-func */
}
await this.insertPercyDom();

// serialize and capture a DOM snapshot
this.log.debug('Serialize DOM', this.meta);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function getSnapshotOptions(options, { config, meta }) {
authorization: config.discovery.authorization,
disableCache: config.discovery.disableCache,
captureMockedServiceWorker: config.discovery.captureMockedServiceWorker,
captureSrcset: config.discovery.captureSrcset,
userAgent: config.discovery.userAgent
}
}, options], (path, prev, next) => {
Expand Down
103 changes: 103 additions & 0 deletions packages/core/test/discovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2039,4 +2039,107 @@ describe('Discovery', () => {
]));
});
});

describe('Capture image srcset =>', () => {
it('make request call to capture resource', async () => {
let responsiveDOM = dedent`
<html>
<head><link href="style.css" rel="stylesheet"/></head>
<body>
<p>Hello Percy!<p>
<img srcset="/img-fromsrcset.png 400w, /img-throwserror.gif 600w, /img-withdifferentcontenttype.gif 800w"
sizes="(max-width: 600px) 400px, (max-width: 800px) 600px, 800px"
src="/img-already-captured.png">
</body>
</html>
`;
server.reply('/img-fromsrcset.png', () => [200, 'image/png', pixel]);
server.reply('/img-already-captured.png', () => [200, 'image/png', pixel]);
server.reply('/img-throwserror.gif', () => [404]);
server.reply('/img-withdifferentcontenttype.gif', () => [200, 'image/gif', pixel]);

let capturedResource = {
url: 'http://localhost:8000/img-already-captured.png',
content: 'R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
mimetype: 'image/png'
};
await percy.snapshot({
name: 'test responsive',
url: 'http://localhost:8000',
domSnapshot: {
html: responsiveDOM,
resources: [capturedResource]
},
discovery: {
captureSrcset: true
}
});

await percy.idle();

let resource = path => jasmine.objectContaining({
attributes: jasmine.objectContaining({
'resource-url': `http://localhost:8000${path}`
})
});

let paths = server.requests.map(r => r[0]);
expect(paths).toContain('/img-fromsrcset.png');
expect(paths).toContain('/img-withdifferentcontenttype.gif');
expect(paths).toContain('/img-throwserror.gif');
expect(captured[0]).toEqual(jasmine.arrayContaining([
resource('/img-fromsrcset.png'),
resource('/img-withdifferentcontenttype.gif')
]));
});

it('using snapshot command capture srcset', async () => {
let responsiveDOM = dedent`
<html>
<head></head>
<body>
<p>Hello Percy!<p>
<img srcset="/img-fromsrcset.png 2x, /img-throwserror.jpeg 3x, /img-withdifferentcontenttype.gif 4x, https://remote.resource.com/img-shouldnotcaptured.png 5x"
sizes="(max-width: 600px) 400px, (max-width: 800px) 600px, 800px"
src="/img-already-captured.png">
</body>
</html>
`;

server.reply('/', () => [200, 'text/html', responsiveDOM]);
server.reply('/img-fromsrcset.png', () => [200, 'image/png', pixel]);
server.reply('/img-already-captured.png', () => [200, 'image/png', pixel]);
server.reply('/img-throwserror.jpeg', () => [404]);
server.reply('/img-withdifferentcontenttype.gif', () => [200, 'image/gif', pixel]);

await percy.snapshot({
name: 'image srcset',
url: 'http://localhost:8000',
widths: [1024],
discovery: {
captureSrcset: true
}
});

await percy.idle();

let resource = path => jasmine.objectContaining({
attributes: jasmine.objectContaining({
'resource-url': `http://localhost:8000${path}`
})
});

expect(captured[0]).toEqual(jasmine.arrayContaining([
resource('/img-fromsrcset.png'),
resource('/img-withdifferentcontenttype.gif'),
resource('/img-already-captured.png')
]));

expect(captured[0]).not.toContain(jasmine.objectContaining({
attributes: jasmine.objectContaining({
'resource-url': 'https://remote.resource.com/img-shouldnotcaptured.png'
})
}));
});
});
});
1 change: 1 addition & 0 deletions packages/core/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface DiscoveryOptions {
allowedHostnames?: string[];
disableCache?: boolean;
captureMockedServiceWorker?: boolean;
captureSrcset?: boolean;
}

interface ScopeOptions {
Expand Down
2 changes: 2 additions & 0 deletions packages/dom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export {
// namespace alias
serializeDOM as serialize
} from './serialize-dom';

export { loadAllSrcsetLinks } from './serialize-image-srcset';
50 changes: 50 additions & 0 deletions packages/dom/src/serialize-image-srcset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function getSrcsets(dom) {
const links = new Set();

for (let img of dom.querySelectorAll('img[srcset]')) {
handleSrcSet(img.srcset, links);
}

for (let picture of dom.querySelectorAll('picture')) {
for (let source of picture.querySelectorAll('source')) {
handleSrcSet(source.srcset, links);
}
}
return Array.from(links);
}

function handleSrcSet(srcSet, links) {
let pattern = /,\s+/;

// We found couple of combination of srcset which needs different regex.
// example - https://url.com?param=a,b <--- here only separeting with , will cause incorrect capture.
// srcset = https://abc.com 320w,https://abc.com/a 400 <--- here srcset doesnot have space after comm.
if (!srcSet.match(pattern)) {
pattern = /,/;
}
srcSet = srcSet.split(pattern);
for (let src of srcSet) {
src = src.trim();
src = src.split(' ')[0];
links.add(getFormattedLink(src));
}
}
function getFormattedLink(src) {
const anchor = document.createElement('a');
anchor.href = src;
return anchor.href;
}

export function loadAllSrcsetLinks() {
const allImgTags = [];
const links = getSrcsets(document);
for (const link of links) {
const img = document.createElement('img');
img.src = link;
allImgTags.push(img);
}
// Adding to window so GC won't abort request
window.allImgTags = allImgTags;
return allImgTags;
}
export default loadAllSrcsetLinks;
61 changes: 61 additions & 0 deletions packages/dom/test/serialize-image-srcset.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { withExample, platforms } from './helpers';

import { loadAllSrcsetLinks } from '@percy/dom';

describe('loadAllSrcsetLinks', () => {
let imgTags;

platforms.forEach((platform) => {
it(`${platform}: capture url from img srcset`, async () => {
withExample(`
<img srcset="base/test/assets/example.webp, base/test/assets/example.png 100px" />
`);

imgTags = loadAllSrcsetLinks();

expect(imgTags.map(s => s.src)).toEqual(['http://localhost:9876/base/test/assets/example.webp', 'http://localhost:9876/base/test/assets/example.png']);
});

it(`${platform}: capture url from img srcset where there is no space after ,`, async () => {
withExample(`
<img srcset="base/test/assets/example.webp 200px,base/test/assets/example.png 100px" />
`);

imgTags = loadAllSrcsetLinks();

expect(imgTags.map(s => s.src)).toEqual(['http://localhost:9876/base/test/assets/example.webp', 'http://localhost:9876/base/test/assets/example.png']);
});

it(`${platform}: capture url from source of picture`, async () => {
withExample(`
<picture>
<source srcset='//locahost:9876/base/test/assets/example.webp, //localhost:9876/base/test/assets/example.png 2x' />
<source srcset='//locahost:9876/base/test/assets/example.jpeg 100px, //locahost:9876/base/test/assets/example1.jpeg 200px' />
</picture>
`);

imgTags = loadAllSrcsetLinks();

expect(imgTags.map(s => s.src)).toEqual([
'http://locahost:9876/base/test/assets/example.webp',
'http://localhost:9876/base/test/assets/example.png',
'http://locahost:9876/base/test/assets/example.jpeg',
'http://locahost:9876/base/test/assets/example1.jpeg'
]);
});

it(`${platform}: srcset inside shadowroot`, () => {
withExample(`
<img srcset="/base/test/assets/example.webp, /base/test/assets/example.png 100px, /base/test/assets/example1.png 2x" />
`, { withShadow: true });

imgTags = loadAllSrcsetLinks();

expect(imgTags.map(s => s.src)).toEqual([
'http://localhost:9876/base/test/assets/example.webp',
'http://localhost:9876/base/test/assets/example.png',
'http://localhost:9876/base/test/assets/example1.png'
]);
});
});
});

0 comments on commit 76df992

Please sign in to comment.