@percy/core 1.10.4 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -2,9 +2,15 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { createRequire } from 'module';
4
4
  import logger from '@percy/logger';
5
+ import { normalize } from '@percy/config/utils';
5
6
  import { getPackageJSON, Server } from './utils.js'; // need require.resolve until import.meta.resolve can be transpiled
6
7
 
7
- export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom'); // Create a Percy CLI API server instance
8
+ export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom'); // Returns a URL encoded string of nested query params
9
+
10
+ function encodeURLSearchParams(subj, prefix) {
11
+ return typeof subj === 'object' ? Object.entries(subj).map(([key, value]) => encodeURLSearchParams(value, prefix ? `${prefix}[${key}]` : key)).join('&') : `${prefix}=${encodeURIComponent(subj)}`;
12
+ } // Create a Percy CLI API server instance
13
+
8
14
 
9
15
  export function createPercyServer(percy, port) {
10
16
  let pkg = getPackageJSON(import.meta.url);
@@ -71,7 +77,7 @@ export function createPercyServer(percy, port) {
71
77
  });
72
78
  }) // get or set config options
73
79
  .route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
74
- config: req.body ? await percy.setConfig(req.body) : percy.config,
80
+ config: req.body ? percy.set(req.body) : percy.config,
75
81
  success: true
76
82
  })) // responds once idle (may take a long time)
77
83
  .route('get', '/percy/idle', async (req, res) => res.json(200, {
@@ -85,14 +91,46 @@ export function createPercyServer(percy, port) {
85
91
  let content = await fs.promises.readFile(PERCY_DOM, 'utf-8');
86
92
  let wrapper = '(window.PercyAgent = class { snapshot(n, o) { return PercyDOM.serialize(o); } });';
87
93
  return res.send(200, 'applicaton/javascript', content.concat(wrapper));
88
- }) // post one or more snapshots
94
+ }) // post one or more snapshots, optionally async
89
95
  .route('post', '/percy/snapshot', async (req, res) => {
90
96
  let snapshot = percy.snapshot(req.body);
91
97
  if (!req.url.searchParams.has('async')) await snapshot;
92
98
  return res.json(200, {
93
99
  success: true
94
100
  });
95
- }) // stops percy at the end of the current event loop
101
+ }) // post one or more comparisons, optionally waiting
102
+ .route('post', '/percy/comparison', async (req, res) => {
103
+ let upload = percy.upload(req.body);
104
+ if (req.url.searchParams.has('await')) await upload; // generate and include one or more redirect links to comparisons
105
+
106
+ let link = ({
107
+ name,
108
+ tag
109
+ }) => {
110
+ var _percy$build;
111
+
112
+ return [percy.client.apiUrl, '/comparisons/redirect?', encodeURLSearchParams(normalize({
113
+ buildId: (_percy$build = percy.build) === null || _percy$build === void 0 ? void 0 : _percy$build.id,
114
+ snapshot: {
115
+ name
116
+ },
117
+ tag
118
+ }, {
119
+ snake: true
120
+ }))].join('');
121
+ };
122
+
123
+ return res.json(200, Object.assign({
124
+ success: true
125
+ }, req.body ? Array.isArray(req.body) ? {
126
+ links: req.body.map(link)
127
+ } : {
128
+ link: link(req.body)
129
+ } : {}));
130
+ }) // flushes one or more snapshots from the internal queue
131
+ .route('post', '/percy/flush', async (req, res) => res.json(200, {
132
+ success: await percy.flush(req.body).then(() => true)
133
+ })) // stops percy at the end of the current event loop
96
134
  .route('/percy/stop', (req, res) => {
97
135
  setImmediate(() => percy.stop());
98
136
  return res.json(200, {
package/dist/config.js CHANGED
@@ -1,5 +1,14 @@
1
1
  // Common config options used in Percy commands
2
2
  export const configSchema = {
3
+ percy: {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ properties: {
7
+ deferUploads: {
8
+ type: 'boolean'
9
+ }
10
+ }
11
+ },
3
12
  snapshot: {
4
13
  type: 'object',
5
14
  additionalProperties: false,
@@ -10,7 +19,7 @@ export const configSchema = {
10
19
  items: {
11
20
  type: 'integer',
12
21
  maximum: 2000,
13
- minimum: 10
22
+ minimum: 120
14
23
  }
15
24
  },
16
25
  minHeight: {
@@ -388,6 +397,9 @@ export const snapshotSchema = {
388
397
  },
389
398
  domSnapshot: {
390
399
  type: 'string'
400
+ },
401
+ width: {
402
+ $ref: '/config/snapshot#/properties/widths/items'
391
403
  }
392
404
  },
393
405
  errors: {
@@ -470,9 +482,87 @@ export const snapshotSchema = {
470
482
  }
471
483
  }
472
484
  }
485
+ }; // Comparison upload options
486
+
487
+ export const comparisonSchema = {
488
+ type: 'object',
489
+ $id: '/comparison',
490
+ required: ['name', 'tag'],
491
+ additionalProperties: false,
492
+ properties: {
493
+ name: {
494
+ type: 'string'
495
+ },
496
+ externalDebugUrl: {
497
+ type: 'string'
498
+ },
499
+ tag: {
500
+ type: 'object',
501
+ additionalProperties: false,
502
+ required: ['name'],
503
+ properties: {
504
+ name: {
505
+ type: 'string'
506
+ },
507
+ osName: {
508
+ type: 'string'
509
+ },
510
+ osVersion: {
511
+ type: 'string'
512
+ },
513
+ width: {
514
+ type: 'integer',
515
+ maximum: 2000,
516
+ minimum: 120
517
+ },
518
+ height: {
519
+ type: 'integer',
520
+ minimum: 10
521
+ },
522
+ orientation: {
523
+ type: 'string',
524
+ enum: ['portrait', 'landscape']
525
+ }
526
+ }
527
+ },
528
+ tiles: {
529
+ type: 'array',
530
+ items: {
531
+ type: 'object',
532
+ additionalProperties: false,
533
+ properties: {
534
+ filepath: {
535
+ type: 'string'
536
+ },
537
+ content: {
538
+ type: 'string'
539
+ },
540
+ statusBarHeight: {
541
+ type: 'integer',
542
+ minimum: 0
543
+ },
544
+ navBarHeight: {
545
+ type: 'integer',
546
+ minimum: 0
547
+ },
548
+ headerHeight: {
549
+ type: 'integer',
550
+ minimum: 0
551
+ },
552
+ footerHeight: {
553
+ type: 'integer',
554
+ minimum: 0
555
+ },
556
+ fullscreen: {
557
+ type: 'boolean'
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
473
563
  }; // Grouped schemas for easier registration
474
564
 
475
- export const schemas = [configSchema, snapshotSchema]; // Config migrate function
565
+ export const schemas = [configSchema, snapshotSchema, comparisonSchema]; // Config migrate function
476
566
 
477
567
  export function configMigration(config, util) {
478
568
  /* eslint-disable curly */
package/dist/discovery.js CHANGED
@@ -1,140 +1,302 @@
1
- import mime from 'mime-types';
2
1
  import logger from '@percy/logger';
3
- import { request as makeRequest } from '@percy/client/utils';
4
- import { normalizeURL, hostnameMatches, createResource } from './utils.js';
5
- const MAX_RESOURCE_SIZE = 15 * 1024 ** 2; // 15MB
6
-
7
- const ALLOWED_STATUSES = [200, 201, 301, 302, 304, 307, 308];
8
- const ALLOWED_RESOURCES = ['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Other'];
9
- export function createRequestHandler(network, {
10
- disableCache,
11
- disallowedHostnames,
12
- getResource
13
- }) {
14
- let log = logger('core:discovery');
15
- return async request => {
16
- let url = request.url;
17
- let meta = { ...network.meta,
18
- url
19
- };
2
+ import Queue from './queue.js';
3
+ import { normalizeURL, hostnameMatches, createRootResource, createPercyCSSResource, createLogResource, yieldAll } from './utils.js'; // Logs verbose debug logs detailing various snapshot options.
20
4
 
21
- try {
22
- log.debug(`Handling request: ${url}`, meta);
23
- let resource = getResource(url);
24
-
25
- if (resource !== null && resource !== void 0 && resource.root) {
26
- log.debug('- Serving root resource', meta);
27
- await request.respond(resource);
28
- } else if (hostnameMatches(disallowedHostnames, url)) {
29
- log.debug('- Skipping disallowed hostname', meta);
30
- await request.abort(true);
31
- } else if (resource && !disableCache) {
32
- log.debug('- Resource cache hit', meta);
33
- await request.respond(resource);
34
- } else {
35
- await request.continue();
36
- }
37
- } catch (error) {
38
- log.debug(`Encountered an error handling request: ${url}`, meta);
39
- log.debug(error);
40
- /* istanbul ignore next: race condition */
5
+ function debugSnapshotOptions(snapshot) {
6
+ let log = logger('core:snapshot'); // log snapshot info
41
7
 
42
- await request.abort(error).catch(e => log.debug(e, meta));
8
+ log.debug('---------', snapshot.meta);
9
+ log.debug(`Received snapshot: ${snapshot.name}`, snapshot.meta); // will log debug info for an object property if its value is defined
10
+
11
+ let debugProp = (obj, prop, format = String) => {
12
+ let val = prop.split('.').reduce((o, k) => o === null || o === void 0 ? void 0 : o[k], obj);
13
+
14
+ if (val != null) {
15
+ // join formatted array values with a space
16
+ val = [].concat(val).map(format).join(', ');
17
+ log.debug(`- ${prop}: ${val}`, snapshot.meta);
43
18
  }
44
19
  };
45
- }
46
- export function createRequestFinishedHandler(network, {
47
- enableJavaScript,
48
- allowedHostnames,
49
- disableCache,
50
- getResource,
51
- saveResource
20
+
21
+ debugProp(snapshot, 'url');
22
+ debugProp(snapshot, 'scope');
23
+ debugProp(snapshot, 'widths', v => `${v}px`);
24
+ debugProp(snapshot, 'minHeight', v => `${v}px`);
25
+ debugProp(snapshot, 'enableJavaScript');
26
+ debugProp(snapshot, 'deviceScaleFactor');
27
+ debugProp(snapshot, 'waitForTimeout');
28
+ debugProp(snapshot, 'waitForSelector');
29
+ debugProp(snapshot, 'execute.afterNavigation');
30
+ debugProp(snapshot, 'execute.beforeResize');
31
+ debugProp(snapshot, 'execute.afterResize');
32
+ debugProp(snapshot, 'execute.beforeSnapshot');
33
+ debugProp(snapshot, 'discovery.allowedHostnames');
34
+ debugProp(snapshot, 'discovery.disallowedHostnames');
35
+ debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
36
+ debugProp(snapshot, 'discovery.authorization', JSON.stringify);
37
+ debugProp(snapshot, 'discovery.disableCache');
38
+ debugProp(snapshot, 'discovery.userAgent');
39
+ debugProp(snapshot, 'clientInfo');
40
+ debugProp(snapshot, 'environmentInfo');
41
+ debugProp(snapshot, 'domSnapshot', Boolean);
42
+
43
+ for (let added of snapshot.additionalSnapshots || []) {
44
+ log.debug(`Additional snapshot: ${added.name}`, snapshot.meta);
45
+ debugProp(added, 'waitForTimeout');
46
+ debugProp(added, 'waitForSelector');
47
+ debugProp(added, 'execute');
48
+ }
49
+ } // Wait for a page's asset discovery network to idle
50
+
51
+
52
+ function waitForDiscoveryNetworkIdle(page, options) {
53
+ let {
54
+ allowedHostnames,
55
+ networkIdleTimeout
56
+ } = options;
57
+
58
+ let filter = r => hostnameMatches(allowedHostnames, r.url);
59
+
60
+ return page.network.idle(filter, networkIdleTimeout);
61
+ } // Calls the provided callback with additional resources
62
+
63
+
64
+ function processSnapshotResources({
65
+ domSnapshot,
66
+ resources,
67
+ ...snapshot
52
68
  }) {
53
- let log = logger('core:discovery');
54
- return async request => {
55
- let origin = request.redirectChain[0] || request;
56
- let url = normalizeURL(origin.url);
57
- let meta = { ...network.meta,
58
- url
69
+ var _resources;
70
+
71
+ resources = [...(((_resources = resources) === null || _resources === void 0 ? void 0 : _resources.values()) ?? [])]; // find or create a root resource if one does not exist
72
+
73
+ let root = resources.find(r => r.content === domSnapshot);
74
+
75
+ if (!root) {
76
+ root = createRootResource(snapshot.url, domSnapshot);
77
+ resources.unshift(root);
78
+ } // inject Percy CSS
79
+
80
+
81
+ if (snapshot.percyCSS) {
82
+ let css = createPercyCSSResource(root.url, snapshot.percyCSS);
83
+ resources.push(css); // replace root contents and associated properties
84
+
85
+ Object.assign(root, createRootResource(root.url, root.content.replace(/(<\/body>)(?!.*\1)/is, `<link data-percy-specific-css rel="stylesheet" href="${css.pathname}"/>` + '$&')));
86
+ } // include associated snapshot logs matched by meta information
87
+
88
+
89
+ resources.push(createLogResource(logger.query(log => {
90
+ var _log$meta$snapshot;
91
+
92
+ return ((_log$meta$snapshot = log.meta.snapshot) === null || _log$meta$snapshot === void 0 ? void 0 : _log$meta$snapshot.name) === snapshot.meta.snapshot.name;
93
+ })));
94
+ return { ...snapshot,
95
+ resources
96
+ };
97
+ } // Triggers the capture of resource requests for a page by iterating over snapshot widths to resize
98
+ // the page and calling any provided execute options.
99
+
100
+
101
+ async function* captureSnapshotResources(page, snapshot, options) {
102
+ let {
103
+ discovery,
104
+ additionalSnapshots = [],
105
+ ...baseSnapshot
106
+ } = snapshot;
107
+ let {
108
+ capture,
109
+ captureWidths,
110
+ deviceScaleFactor,
111
+ mobile
112
+ } = options; // used to take snapshots and remove any discovered root resource
113
+
114
+ let takeSnapshot = async (options, width) => {
115
+ if (captureWidths) options = { ...options,
116
+ width
59
117
  };
118
+ let captured = await page.snapshot(options);
119
+ captured.resources.delete(normalizeURL(captured.url));
120
+ capture(processSnapshotResources(captured));
121
+ return captured;
122
+ }; // used to resize the using capture options
60
123
 
61
- try {
62
- var _resource;
63
-
64
- let resource = getResource(url); // process and cache the response and resource
65
-
66
- if (!((_resource = resource) !== null && _resource !== void 0 && _resource.root) && (!resource || disableCache)) {
67
- let headers = request.headers;
68
- let response = request.response;
69
- let capture = response && hostnameMatches(allowedHostnames, url);
70
- let body = capture && (await response.buffer());
71
- log.debug(`Processing resource: ${url}`, meta);
72
- /* istanbul ignore next: sanity check */
73
-
74
- if (!response) {
75
- return log.debug('- Skipping no response', meta);
76
- } else if (!capture) {
77
- return log.debug('- Skipping remote resource', meta);
78
- } else if (!body.length) {
79
- return log.debug('- Skipping empty response', meta);
80
- } else if (body.length > MAX_RESOURCE_SIZE) {
81
- return log.debug('- Skipping resource larger than 15MB', meta);
82
- } else if (!ALLOWED_STATUSES.includes(response.status)) {
83
- return log.debug(`- Skipping disallowed status [${response.status}]`, meta);
84
- } else if (!enableJavaScript && !ALLOWED_RESOURCES.includes(request.type)) {
85
- return log.debug(`- Skipping disallowed resource type [${request.type}]`, meta);
86
- } // Try to get the proper mimetype if the server or asset discovery browser is sending `text/plain`
87
-
88
-
89
- let mimeType = response.mimeType === 'text/plain' && mime.lookup(response.url) || response.mimeType; // font responses from the browser may not be properly encoded, so request them directly
90
-
91
- if (mimeType !== null && mimeType !== void 0 && mimeType.includes('font')) {
92
- var _network$authorizatio;
93
-
94
- log.debug('- Requesting asset directly');
95
-
96
- if (!headers.Authorization && (_network$authorizatio = network.authorization) !== null && _network$authorizatio !== void 0 && _network$authorizatio.username) {
97
- let token = Buffer.from([network.authorization.username, network.authorization.password || ''].join(':')).toString('base64');
98
- headers.Authorization = `Basic ${token}`;
99
- }
100
-
101
- body = await makeRequest(response.url, {
102
- buffer: true,
103
- headers
104
- });
105
- }
106
-
107
- resource = createResource(url, body, mimeType, {
108
- status: response.status,
109
- // 'Network.responseReceived' returns headers split by newlines, however
110
- // `Fetch.fulfillRequest` (used for cached responses) will hang with newlines.
111
- headers: Object.entries(response.headers).reduce((norm, [key, value]) => Object.assign(norm, {
112
- [key]: value.split('\n')
113
- }), {})
114
- });
115
- log.debug(`- sha: ${resource.sha}`, meta);
116
- log.debug(`- mimetype: ${resource.mimetype}`, meta);
124
+
125
+ let resizePage = width => page.resize({
126
+ height: snapshot.minHeight,
127
+ deviceScaleFactor,
128
+ mobile,
129
+ width
130
+ }); // navigate to the url
131
+
132
+
133
+ yield resizePage(snapshot.widths[0]);
134
+ yield page.goto(snapshot.url);
135
+
136
+ if (snapshot.execute) {
137
+ // when any execute options are provided, inject snapshot options
138
+
139
+ /* istanbul ignore next: cannot detect coverage of injected code */
140
+ yield page.eval((_, s) => window.__PERCY__.snapshot = s, snapshot);
141
+ yield page.evaluate(snapshot.execute.afterNavigation);
142
+ } // iterate over additional snapshots for proper DOM capturing
143
+
144
+
145
+ for (let additionalSnapshot of [baseSnapshot, ...additionalSnapshots]) {
146
+ let isBaseSnapshot = additionalSnapshot === baseSnapshot;
147
+ let snap = { ...baseSnapshot,
148
+ ...additionalSnapshot
149
+ };
150
+ let {
151
+ widths,
152
+ execute
153
+ } = snap;
154
+ let [width] = widths; // iterate over widths to trigger reqeusts and capture other widths
155
+
156
+ if (isBaseSnapshot || captureWidths) {
157
+ for (let i = 0; i < widths.length - 1; i++) {
158
+ if (captureWidths) yield takeSnapshot(snap, width);
159
+ yield page.evaluate(execute === null || execute === void 0 ? void 0 : execute.beforeResize);
160
+ yield waitForDiscoveryNetworkIdle(page, discovery);
161
+ yield resizePage(width = widths[i + 1]);
162
+ yield page.evaluate(execute === null || execute === void 0 ? void 0 : execute.afterResize);
117
163
  }
164
+ }
165
+
166
+ if (capture && !snapshot.domSnapshot) {
167
+ // capture this snapshot and update the base snapshot after capture
168
+ let captured = yield takeSnapshot(snap, width);
169
+ if (isBaseSnapshot) baseSnapshot = captured; // resize back to the initial width when capturing additional snapshot widths
118
170
 
119
- saveResource(resource);
120
- } catch (error) {
121
- log.debug(`Encountered an error processing resource: ${url}`, meta);
122
- log.debug(error);
171
+ if (captureWidths && additionalSnapshots.length) {
172
+ let l = additionalSnapshots.indexOf(additionalSnapshot) + 1;
173
+ if (l < additionalSnapshots.length) yield resizePage(snapshot.widths[0]);
174
+ }
123
175
  }
124
- };
125
- }
126
- export function createRequestFailedHandler(network) {
127
- let log = logger('core:discovery');
128
- return ({
129
- url,
130
- error
131
- }) => {
132
- // do not log generic failures since the real error was most likely
133
- // already logged from elsewhere
134
- if (error !== 'net::ERR_FAILED') {
135
- log.debug(`Request failed for ${url}: ${error}`, { ...network.meta,
136
- url
176
+ } // recursively trigger resource requests for any alternate device pixel ratio
177
+
178
+
179
+ if (deviceScaleFactor !== discovery.devicePixelRatio) {
180
+ yield waitForDiscoveryNetworkIdle(page, discovery);
181
+ yield* captureSnapshotResources(page, snapshot, {
182
+ deviceScaleFactor: discovery.devicePixelRatio,
183
+ mobile: true
184
+ });
185
+ } // wait for final network idle when not capturing DOM
186
+
187
+
188
+ if (capture && snapshot.domSnapshot) {
189
+ yield waitForDiscoveryNetworkIdle(page, discovery);
190
+ capture(processSnapshotResources(snapshot));
191
+ }
192
+ } // Pushes all provided snapshots to a discovery queue with the provided callback, yielding to each
193
+ // one concurrently. When skipping asset discovery, the callback is called immediately for each
194
+ // snapshot, also processing snapshot resources when not dry-running.
195
+
196
+
197
+ export async function* discoverSnapshotResources(queue, options, callback) {
198
+ let {
199
+ snapshots,
200
+ skipDiscovery,
201
+ dryRun
202
+ } = options;
203
+ yield* yieldAll(snapshots.reduce((all, snapshot) => {
204
+ debugSnapshotOptions(snapshot);
205
+
206
+ if (skipDiscovery) {
207
+ let {
208
+ additionalSnapshots,
209
+ ...baseSnapshot
210
+ } = snapshot;
211
+ additionalSnapshots = dryRun && additionalSnapshots || [];
212
+
213
+ for (let snap of [baseSnapshot, ...additionalSnapshots]) {
214
+ callback(dryRun ? snap : processSnapshotResources(snap));
215
+ }
216
+ } else {
217
+ all.push(queue.push(snapshot, callback));
218
+ }
219
+
220
+ return all;
221
+ }, []));
222
+ } // Used to cache resources across core instances
223
+
224
+ const RESOURCE_CACHE_KEY = Symbol('resource-cache'); // Creates an asset discovery queue that uses the percy browser instance to create a page for each
225
+ // snapshot which is used to intercept and capture snapshot resource requests.
226
+
227
+ export function createDiscoveryQueue(percy) {
228
+ let {
229
+ concurrency
230
+ } = percy.config.discovery;
231
+ let queue = new Queue();
232
+ let cache;
233
+ return queue.set({
234
+ concurrency
235
+ }) // on start, launch the browser and run the queue
236
+ .handle('start', async () => {
237
+ cache = percy[RESOURCE_CACHE_KEY] = new Map();
238
+ await percy.browser.launch();
239
+ queue.run();
240
+ }) // on end, close the browser
241
+ .handle('end', async () => {
242
+ await percy.browser.close();
243
+ }) // snapshots are unique by name; when deferred also by widths
244
+ .handle('find', ({
245
+ name,
246
+ widths
247
+ }, snapshot) => snapshot.name === name && (!percy.deferUploads || !widths || widths.join() === snapshot.widths.join())) // initialize the root resource for DOM snapshots
248
+ .handle('push', snapshot => {
249
+ let {
250
+ url,
251
+ domSnapshot
252
+ } = snapshot;
253
+ let root = domSnapshot && createRootResource(url, domSnapshot);
254
+ let resources = new Map(root ? [[root.url, root]] : []);
255
+ return { ...snapshot,
256
+ resources
257
+ };
258
+ }) // discovery resources for snapshots and call the callback for each discovered snapshot
259
+ .handle('task', async function* (snapshot, callback) {
260
+ percy.log.debug(`Discovering resources: ${snapshot.name}`, snapshot.meta); // create a new browser page
261
+
262
+ let page = yield percy.browser.page({
263
+ enableJavaScript: snapshot.enableJavaScript ?? !snapshot.domSnapshot,
264
+ networkIdleTimeout: snapshot.discovery.networkIdleTimeout,
265
+ requestHeaders: snapshot.discovery.requestHeaders,
266
+ authorization: snapshot.discovery.authorization,
267
+ userAgent: snapshot.discovery.userAgent,
268
+ meta: snapshot.meta,
269
+ // enable network inteception
270
+ intercept: {
271
+ enableJavaScript: snapshot.enableJavaScript,
272
+ disableCache: snapshot.discovery.disableCache,
273
+ allowedHostnames: snapshot.discovery.allowedHostnames,
274
+ disallowedHostnames: snapshot.discovery.disallowedHostnames,
275
+ getResource: u => snapshot.resources.get(u) || cache.get(u),
276
+ saveResource: r => snapshot.resources.set(r.url, r) && cache.set(r.url, r)
277
+ }
278
+ });
279
+
280
+ try {
281
+ yield* captureSnapshotResources(page, snapshot, {
282
+ captureWidths: !snapshot.domSnapshot && percy.deferUploads,
283
+ capture: callback
137
284
  });
285
+ } finally {
286
+ // always close the page when done
287
+ await page.close();
138
288
  }
139
- };
289
+ }).handle('error', ({
290
+ name,
291
+ meta
292
+ }, error) => {
293
+ if (error.name === 'AbortError' && queue.readyState < 3) {
294
+ // only error about aborted snapshots when not closed
295
+ percy.log.error('Received a duplicate snapshot, ' + `the previous snapshot was aborted: ${name}`, meta);
296
+ } else {
297
+ // log all other encountered errors
298
+ percy.log.error(`Encountered an error taking snapshot: ${name}`, meta);
299
+ percy.log.error(error, meta);
300
+ }
301
+ });
140
302
  }