diff --git a/packages/cli-snapshot/src/snapshot.js b/packages/cli-snapshot/src/snapshot.js index afa8b0650..e06654c13 100644 --- a/packages/cli-snapshot/src/snapshot.js +++ b/packages/cli-snapshot/src/snapshot.js @@ -48,7 +48,8 @@ export const snapshot = command('snapshot', { ], percy: { - delayUploads: true + delayUploads: true, + projectType: 'web' }, config: { diff --git a/packages/client/src/client.js b/packages/client/src/client.js index 5445376c2..05c41c921 100644 --- a/packages/client/src/client.js +++ b/packages/client/src/client.js @@ -217,6 +217,18 @@ export class PercyClient { return this.get(`job_status?sync=true&type=${type}&id=${ids.join()}`); } + // Returns device details enabled on project associated with given token + async getDeviceDetails(buildId) { + try { + let url = 'discovery/device-details'; + if (buildId) url += `?build_id=${buildId}`; + const { data } = await this.get(url); + return data; + } catch (e) { + return []; + } + } + // Retrieves project builds optionally filtered. Requires a read access token. async getBuilds(project, filters = {}) { validateProjectPath(project); diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index d56f462c2..cedbaffc9 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -353,6 +353,20 @@ describe('PercyClient', () => { }); }); + describe('#getDeviceDetails()', () => { + it('in case of error return []', async () => { + api.reply('/discovery/device-details', () => [500]); + await expectAsync(client.getDeviceDetails()).toBeResolvedTo([]); + }); + + it('gets device details', async () => { + api.reply('/discovery/device-details', () => [200, { data: ['<>'] }]); + api.reply('/discovery/device-details?build_id=123', () => [200, { data: ['<>'] }]); + await expectAsync(client.getDeviceDetails()).toBeResolvedTo(['<>']); + await expectAsync(client.getDeviceDetails(123)).toBeResolvedTo(['<>']); + }); + }); + describe('#getBuilds()', () => { it('throws when missing a project path', async () => { await expectAsync(client.getBuilds()) diff --git a/packages/client/test/helpers.js b/packages/client/test/helpers.js index d000ed01f..002b3c241 100644 --- a/packages/client/test/helpers.js +++ b/packages/client/test/helpers.js @@ -139,6 +139,12 @@ export const api = { '/snapshots/4567?sync=true&response_format=sync-cli': () => [201, { name: 'test snapshot', diff_ratio: 0 + }], + '/discovery/device-details?build_id=123': () => [201, { + data: [] + }], + '/discovery/device-details': () => [201, { + data: [] }] }, diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index aa83369b8..b61cbb388 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -141,8 +141,9 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { // Triggers the capture of resource requests for a page by iterating over snapshot widths to resize // the page and calling any provided execute options. async function* captureSnapshotResources(page, snapshot, options) { + const log = logger('core:discovery'); let { discovery, additionalSnapshots = [], ...baseSnapshot } = snapshot; - let { capture, captureWidths, deviceScaleFactor, mobile } = options; + let { capture, captureWidths, deviceScaleFactor, mobile, captureForDevices } = options; // used to take snapshots and remove any discovered root resource let takeSnapshot = async (options, width) => { @@ -187,6 +188,18 @@ async function* captureSnapshotResources(page, snapshot, options) { let { widths, execute } = snap; let [width] = widths; + // iterate over device to trigger reqeusts and capture other dpr width + if (captureForDevices) { + for (const device of captureForDevices) { + yield waitForDiscoveryNetworkIdle(page, discovery); + // We are not adding these widths and pixels ratios in loop below because we want to explicitly reload the page after resize which we dont do below + yield* captureSnapshotResources(page, { ...snapshot, widths: [device.width] }, { + deviceScaleFactor: device.deviceScaleFactor, + mobile: true + }); + } + } + // iterate over widths to trigger reqeusts and capture other widths if (isBaseSnapshot || captureWidths) { for (let i = 0; i < widths.length - 1; i++) { @@ -212,13 +225,8 @@ async function* captureSnapshotResources(page, snapshot, options) { } // recursively trigger resource requests for any alternate device pixel ratio - if (deviceScaleFactor !== discovery.devicePixelRatio) { - yield waitForDiscoveryNetworkIdle(page, discovery); - - yield* captureSnapshotResources(page, snapshot, { - deviceScaleFactor: discovery.devicePixelRatio, - mobile: true - }); + if (discovery.devicePixelRatio) { + log.deprecated('discovery.devicePixelRatio is deprecated percy will now auto capture resource in all devicePixelRatio, Ignoring configuration'); } // wait for final network idle when not capturing DOM @@ -317,7 +325,8 @@ export function createDiscoveryQueue(percy) { try { yield* captureSnapshotResources(page, snapshot, { captureWidths: !snapshot.domSnapshot && percy.deferUploads, - capture: callback + capture: callback, + captureForDevices: percy.deviceDetails || [] }); } finally { // always close the page when done diff --git a/packages/core/src/percy.js b/packages/core/src/percy.js index 3bfd782a6..bdd51b0ef 100644 --- a/packages/core/src/percy.js +++ b/packages/core/src/percy.js @@ -157,6 +157,7 @@ export class Percy { if (!this.skipDiscovery) yield this.#discovery.start(); // start a local API server for SDK communication if (this.server) yield this.server.listen(); + if (this.projectType === 'web') this.deviceDetails = yield this.client.getDeviceDetails(this.build?.id); const snapshotType = this.projectType === 'web' ? 'snapshot' : 'comparison'; this.syncQueue = new WaitForJob(snapshotType, this); // log and mark this instance as started diff --git a/packages/core/test/discovery.test.js b/packages/core/test/discovery.test.js index 9a2ca323c..d6c9be137 100644 --- a/packages/core/test/discovery.test.js +++ b/packages/core/test/discovery.test.js @@ -665,72 +665,18 @@ describe('Discovery', () => { ])); }); - describe('higher pixel densities', () => { - let responsiveDOM; - beforeEach(() => { - responsiveDOM = dedent` - - - -

Hello Percy!

- - - - `; - - server.reply('/', () => [200, 'text/html', responsiveDOM]); - server.reply('/img-normal.png', () => [200, 'image/png', pixel]); - server.reply('/img-2x.png', () => new Promise(r => ( - setTimeout(r, 200, [200, 'image/png', pixel])))); - }); - - it('when domSnapshot present', async () => { + describe('devicePixelRatio', () => { + it('should warn about depreacted option', async () => { await percy.snapshot({ name: 'test responsive', url: 'http://localhost:8000', - domSnapshot: responsiveDOM, discovery: { devicePixelRatio: 2 }, widths: [400, 800] }); 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-normal.png'), - resource('/img-2x.png') - ])); - }); - - it('when option in config', async () => { - percy.config.discovery.devicePixelRatio = 2; - await percy.snapshot({ - name: 'test responsive', - url: 'http://localhost:8000', - domSnapshot: responsiveDOM, - widths: [400, 800] - }); - - 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-normal.png'), - resource('/img-2x.png') - ])); - - delete percy.config.discovery.devicePixelRatio; + expect(logger.stderr).toContain('[percy] Warning: discovery.devicePixelRatio is deprecated percy will now auto capture resource in all devicePixelRatio, Ignoring configuration'); }); }); @@ -2142,4 +2088,214 @@ describe('Discovery', () => { })); }); }); + + describe('Capture responsive assets =>', () => { + it('should capture js based assets', async () => { + api.reply('/discovery/device-details?build_id=123', () => [200, { data: [{ width: 375, deviceScaleFactor: 2 }, { width: 390, deviceScaleFactor: 3 }] }]); + // stop current instance to create a new one + await percy.stop(); + percy = await Percy.start({ + projectType: 'web', + token: 'PERCY_TOKEN', + snapshot: { widths: [1000] }, + discovery: { concurrency: 1 } + }); + let responsiveDOM = dedent` + + + + +

Responsive Images Example

+ Responsive Image + + + + `; + + server.reply('/', () => [200, 'text/html', responsiveDOM]); + server.reply('/default.jpg', () => [200, 'image/jpg', pixel]); + server.reply('/small.jpg', () => [200, 'image/jpg', pixel]); + server.reply('/medium.jpg', () => [200, 'image/jpg', pixel]); + server.reply('/large.jpg', () => [200, 'image/jpg', pixel]); + + await percy.snapshot({ + name: 'image srcset', + url: 'http://localhost:8000', + widths: [1024] + }); + + await percy.idle(); + + let resource = path => jasmine.objectContaining({ + attributes: jasmine.objectContaining({ + 'resource-url': `http://localhost:8000${path}` + }) + }); + + expect(captured[0]).toEqual(jasmine.arrayContaining([ + resource('/default.jpg'), + resource('/small.jpg'), + resource('/medium.jpg') + ])); + + expect(captured[0]).not.toContain(jasmine.objectContaining({ + attributes: jasmine.objectContaining({ + 'resource-url': 'http://localhost:8000/large.jpg' + }) + })); + }); + + it('handle cases when asset was changed on load', async () => { + api.reply('/discovery/device-details?build_id=123', () => [200, { data: [{ width: 390, deviceScaleFactor: 3 }] }]); + // stop current instance to create a new one + await percy.stop(); + percy = await Percy.start({ + projectType: 'web', + token: 'PERCY_TOKEN', + snapshot: { widths: [1000] }, + discovery: { concurrency: 1 } + }); + let responsiveDOM = dedent` + + + + +

Responsive Images Example

+ Responsive Image + + + + `; + + server.reply('/', () => [200, 'text/html', responsiveDOM]); + server.reply('/default.jpg', () => [200, 'image/jpg', pixel]); + server.reply('/small.jpg', () => [200, 'image/jpg', pixel]); + server.reply('/medium.jpg', () => [200, 'image/jpg', pixel]); + + await percy.snapshot({ + name: 'image srcset', + url: 'http://localhost:8000', + widths: [1024] + }); + + await percy.idle(); + + let resource = path => jasmine.objectContaining({ + attributes: jasmine.objectContaining({ + 'resource-url': `http://localhost:8000${path}` + }) + }); + + expect(captured[0]).toEqual(jasmine.arrayContaining([ + resource('/default.jpg'), + resource('/medium.jpg') + ])); + + expect(captured[0]).not.toContain(jasmine.objectContaining({ + attributes: jasmine.objectContaining({ + 'resource-url': 'http://localhost:8000/small.jpg' + }) + })); + }); + + it('captures responsive assets srcset + mediaquery', async () => { + api.reply('/discovery/device-details?build_id=123', () => [200, + { + data: [ + { width: 280, deviceScaleFactor: 2 }, { width: 600, deviceScaleFactor: 4 }, + { width: 450, deviceScaleFactor: 3 }, { width: 500, deviceScaleFactor: 5 } + ] + }]); + // stop current instance to create a new one + await percy.stop(); + percy = await Percy.start({ + projectType: 'web', + token: 'PERCY_TOKEN', + snapshot: { widths: [1000] }, + discovery: { concurrency: 1 } + }); + let responsiveDOM = dedent` + + + +

Hello Percy!

+ + + + `; + + let responsiveCSS = dedent` + body { background: url('/img-bg-1.gif'); } + @media (max-width: 600px) { + body { background: url('/img-bg-2.gif'); } + } + `; + + server.reply('/', () => [200, 'text/html', responsiveDOM]); + server.reply('/style.css', () => [200, 'text/css', responsiveCSS]); + 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]); + server.reply('/img-bg-1.gif', () => [200, 'image/gif', pixel]); + server.reply('/img-bg-2.gif', () => [200, 'image/gif', pixel]); + + await percy.snapshot({ + name: 'test responsive', + url: 'http://localhost:8000', + domSnapshot: responsiveDOM, + widths: [590] + }); + + 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-already-captured.png'), + resource('/img-fromsrcset.png'), + resource('/img-withdifferentcontenttype.gif'), + resource('/img-bg-1.gif'), + resource('/img-bg-2.gif') + ])); + + expect(captured[0]).not.toContain(jasmine.objectContaining({ + attributes: jasmine.objectContaining({ + 'resource-url': 'https://remote.resource.com/img-shouldnotcaptured.png' + }) + })); + }); + }); }); diff --git a/packages/core/test/snapshot.test.js b/packages/core/test/snapshot.test.js index 4dd7b94fe..9838d2952 100644 --- a/packages/core/test/snapshot.test.js +++ b/packages/core/test/snapshot.test.js @@ -125,7 +125,8 @@ describe('Snapshot', () => { expect(logger.stderr).toEqual([ '[percy] Warning: The snapshot option `devicePixelRatio` ' + - 'will be removed in 2.0.0. Use `discovery.devicePixelRatio` instead.' + 'will be removed in 2.0.0. Use `discovery.devicePixelRatio` instead.', + '[percy] Warning: discovery.devicePixelRatio is deprecated percy will now auto capture resource in all devicePixelRatio, Ignoring configuration' ]); });