@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/snapshot.js CHANGED
@@ -2,9 +2,10 @@ import logger from '@percy/logger';
2
2
  import PercyConfig from '@percy/config';
3
3
  import micromatch from 'micromatch';
4
4
  import { configSchema } from './config.js';
5
- import { request, hostnameMatches, createRootResource, createPercyCSSResource, createLogResource, yieldTo } from './utils.js'; // Throw a better error message for missing or invalid urls
5
+ import Queue from './queue.js';
6
+ import { request, hostnameMatches, yieldTo } from './utils.js'; // Throw a better error message for missing or invalid urls
6
7
 
7
- export function validURL(url, base) {
8
+ function validURL(url, base) {
8
9
  if (!url) {
9
10
  throw new Error('Missing required URL for snapshot');
10
11
  }
@@ -16,10 +17,11 @@ export function validURL(url, base) {
16
17
  }
17
18
  } // used to deserialize regular expression strings
18
19
 
20
+
19
21
  const RE_REGEXP = /^\/(.+)\/(\w+)?$/; // Returns true or false if a snapshot matches the provided include and exclude predicates. A
20
22
  // predicate can be an array of predicates, a regular expression, a glob pattern, or a function.
21
23
 
22
- export function snapshotMatches(snapshot, include, exclude) {
24
+ function snapshotMatches(snapshot, include, exclude) {
23
25
  var _include, _include2;
24
26
 
25
27
  // support an options object as the second argument
@@ -62,15 +64,16 @@ export function snapshotMatches(snapshot, include, exclude) {
62
64
  return !test(exclude, false) && test(include, true);
63
65
  } // Accepts an array of snapshots to filter and map with matching options.
64
66
 
65
- export function mapSnapshotOptions(percy, snapshots, config) {
67
+
68
+ function mapSnapshotOptions(snapshots, context) {
66
69
  if (!(snapshots !== null && snapshots !== void 0 && snapshots.length)) return []; // reduce options into a single function
67
70
 
68
- let applyOptions = [].concat((config === null || config === void 0 ? void 0 : config.options) || []).reduceRight((next, {
71
+ let applyOptions = [].concat((context === null || context === void 0 ? void 0 : context.options) || []).reduceRight((next, {
69
72
  include,
70
73
  exclude,
71
74
  ...opts
72
75
  }) => snap => next( // assign additional options to included snaphots
73
- snapshotMatches(snap, include, exclude) ? Object.assign(snap, opts) : snap), s => getSnapshotConfig(percy, s)); // reduce snapshots with overrides
76
+ snapshotMatches(snap, include, exclude) ? Object.assign(snap, opts) : snap), snap => getSnapshotOptions(snap, context)); // reduce snapshots with overrides
74
77
 
75
78
  return snapshots.reduce((acc, snapshot) => {
76
79
  var _snapshot;
@@ -80,42 +83,84 @@ export function mapSnapshotOptions(percy, snapshots, config) {
80
83
  url: snapshot
81
84
  }; // normalize the snapshot url and use it for the default name
82
85
 
83
- let url = validURL(snapshot.url, config === null || config === void 0 ? void 0 : config.baseUrl);
86
+ let url = validURL(snapshot.url, context === null || context === void 0 ? void 0 : context.baseUrl);
84
87
  (_snapshot = snapshot).name || (_snapshot.name = `${url.pathname}${url.search}${url.hash}`);
85
88
  snapshot.url = url.href; // use the snapshot when matching include/exclude
86
89
 
87
- if (snapshotMatches(snapshot, config)) {
90
+ if (snapshotMatches(snapshot, context)) {
88
91
  acc.push(applyOptions(snapshot));
89
92
  }
90
93
 
91
94
  return acc;
92
95
  }, []);
93
- } // Returns an array of derived snapshot options
96
+ } // Return snapshot options merged with defaults and global config.
94
97
 
95
- export async function* gatherSnapshots(percy, options) {
96
- let {
97
- baseUrl,
98
- snapshots
99
- } = options;
100
- if ('url' in options) snapshots = [options];
101
- if ('sitemap' in options) snapshots = yield getSitemapSnapshots(options); // validate evaluated snapshots
102
98
 
103
- if (typeof snapshots === 'function') {
104
- snapshots = yield* yieldTo(snapshots(baseUrl));
105
- snapshots = validateSnapshotOptions({
106
- baseUrl,
107
- snapshots
108
- }).snapshots;
109
- } // map snapshots with snapshot options
99
+ function getSnapshotOptions(options, {
100
+ config,
101
+ meta
102
+ }) {
103
+ return PercyConfig.merge([{
104
+ widths: configSchema.snapshot.properties.widths.default,
105
+ discovery: {
106
+ allowedHostnames: [validURL(options.url).hostname]
107
+ },
108
+ meta: { ...meta,
109
+ snapshot: {
110
+ name: options.name
111
+ }
112
+ }
113
+ }, config.snapshot, {
114
+ // only specific discovery options are used per-snapshot
115
+ discovery: {
116
+ allowedHostnames: config.discovery.allowedHostnames,
117
+ disallowedHostnames: config.discovery.disallowedHostnames,
118
+ networkIdleTimeout: config.discovery.networkIdleTimeout,
119
+ requestHeaders: config.discovery.requestHeaders,
120
+ authorization: config.discovery.authorization,
121
+ disableCache: config.discovery.disableCache,
122
+ userAgent: config.discovery.userAgent
123
+ }
124
+ }, options], (path, prev, next) => {
125
+ var _next, _next2;
126
+
127
+ switch (path.map(k => k.toString()).join('.')) {
128
+ case 'widths':
129
+ // dedup, sort, and override widths when not empty
130
+ return [path, !((_next = next) !== null && _next !== void 0 && _next.length) ? prev : [...new Set(next)].sort((a, b) => a - b)];
110
131
 
132
+ case 'percyCSS':
133
+ // concatenate percy css
134
+ return [path, [prev, next].filter(Boolean).join('\n')];
111
135
 
112
- snapshots = mapSnapshotOptions(percy, snapshots, options);
113
- if (!snapshots.length) throw new Error('No snapshots found');
114
- return snapshots;
136
+ case 'execute':
137
+ // shorthand for execute.beforeSnapshot
138
+ return Array.isArray(next) || typeof next !== 'object' ? [path.concat('beforeSnapshot'), next] : [path];
139
+
140
+ case 'discovery.disallowedHostnames':
141
+ // prevent disallowing the root hostname
142
+ return [path, !((_next2 = next) !== null && _next2 !== void 0 && _next2.length) ? prev : (prev ?? []).concat(next).filter(h => !hostnameMatches(h, options.url))];
143
+ } // ensure additional snapshots have complete names
144
+
145
+
146
+ if (path[0] === 'additionalSnapshots' && path.length === 2) {
147
+ let {
148
+ prefix = '',
149
+ suffix = '',
150
+ ...n
151
+ } = next;
152
+ next = {
153
+ name: `${prefix}${options.name}${suffix}`,
154
+ ...n
155
+ };
156
+ return [path, next];
157
+ }
158
+ });
115
159
  } // Validates and migrates snapshot options against the correct schema based on provided
116
160
  // properties. Eagerly throws an error when missing a URL for any snapshot, and warns about all
117
161
  // other invalid options which are also scrubbed from the returned migrated options.
118
162
 
163
+
119
164
  export function validateSnapshotOptions(options) {
120
165
  var _migrated$baseUrl;
121
166
 
@@ -161,7 +206,7 @@ export function validateSnapshotOptions(options) {
161
206
  } // Fetches a sitemap and parses it into a list of URLs for taking snapshots. Duplicate URLs,
162
207
  // including a trailing slash, are removed from the resulting list.
163
208
 
164
- export async function getSitemapSnapshots(options) {
209
+ async function getSitemapSnapshots(options) {
165
210
  return request(options.sitemap, (body, res) => {
166
211
  // validate sitemap content-type
167
212
  let [contentType] = res.headers['content-type'].split(';');
@@ -178,271 +223,192 @@ export async function getSitemapSnapshots(options) {
178
223
  return match === -1 || match === i;
179
224
  });
180
225
  });
181
- } // Return snapshot options merged with defaults and global options.
182
-
183
- export function getSnapshotConfig(percy, options) {
184
- return PercyConfig.merge([{
185
- widths: configSchema.snapshot.properties.widths.default,
186
- discovery: {
187
- allowedHostnames: [validURL(options.url).hostname]
188
- },
189
- meta: {
190
- snapshot: {
191
- name: options.name
192
- },
193
- build: percy.build
194
- }
195
- }, percy.config.snapshot, {
196
- // only specific discovery options are used per-snapshot
197
- discovery: {
198
- allowedHostnames: percy.config.discovery.allowedHostnames,
199
- disallowedHostnames: percy.config.discovery.disallowedHostnames,
200
- networkIdleTimeout: percy.config.discovery.networkIdleTimeout,
201
- requestHeaders: percy.config.discovery.requestHeaders,
202
- authorization: percy.config.discovery.authorization,
203
- disableCache: percy.config.discovery.disableCache,
204
- userAgent: percy.config.discovery.userAgent
205
- }
206
- }, options], (path, prev, next) => {
207
- var _next, _next2;
208
-
209
- switch (path.map(k => k.toString()).join('.')) {
210
- case 'widths':
211
- // dedup, sort, and override widths when not empty
212
- return [path, !((_next = next) !== null && _next !== void 0 && _next.length) ? prev : Array.from(new Set(next)).sort((a, b) => a - b)];
226
+ } // Returns an array of derived snapshot options
213
227
 
214
- case 'percyCSS':
215
- // concatenate percy css
216
- return [path, [prev, next].filter(Boolean).join('\n')];
217
228
 
218
- case 'execute':
219
- // shorthand for execute.beforeSnapshot
220
- return Array.isArray(next) || typeof next !== 'object' ? [path.concat('beforeSnapshot'), next] : [path];
229
+ export async function* gatherSnapshots(options, context) {
230
+ let {
231
+ baseUrl,
232
+ snapshots
233
+ } = options;
234
+ if ('url' in options) [snapshots, options] = [[options], {}];
235
+ if ('sitemap' in options) snapshots = yield getSitemapSnapshots(options); // validate evaluated snapshots
221
236
 
222
- case 'discovery.disallowedHostnames':
223
- // prevent disallowing the root hostname
224
- return [path, !((_next2 = next) !== null && _next2 !== void 0 && _next2.length) ? prev : (prev ?? []).concat(next).filter(h => !hostnameMatches(h, options.url))];
225
- } // ensure additional snapshots have complete names
237
+ if (typeof snapshots === 'function') {
238
+ snapshots = yield* yieldTo(snapshots(baseUrl));
239
+ snapshots = validateSnapshotOptions({
240
+ baseUrl,
241
+ snapshots
242
+ }).snapshots;
243
+ } // map snapshots with snapshot options
226
244
 
227
245
 
228
- if (path[0] === 'additionalSnapshots' && path.length === 2) {
229
- let {
230
- prefix = '',
231
- suffix = '',
232
- ...n
233
- } = next;
234
- next = {
235
- name: `${prefix}${options.name}${suffix}`,
236
- ...n
237
- };
238
- return [path, next];
239
- }
246
+ snapshots = mapSnapshotOptions(snapshots, { ...options,
247
+ ...context
240
248
  });
241
- } // Returns a complete and valid snapshot config object and logs verbose debug logs detailing various
242
- // snapshot options. When `showInfo` is true, specific messages will be logged as info logs rather
243
- // than debug logs.
244
-
245
- function debugSnapshotConfig(snapshot, showInfo) {
246
- let log = logger('core:snapshot'); // log snapshot info
247
-
248
- log.debug('---------', snapshot.meta);
249
- if (showInfo) log.info(`Snapshot found: ${snapshot.name}`, snapshot.meta);else log.debug(`Handling snapshot: ${snapshot.name}`, snapshot.meta); // will log debug info for an object property if its value is defined
250
-
251
- let debugProp = (obj, prop, format = String) => {
252
- let val = prop.split('.').reduce((o, k) => o === null || o === void 0 ? void 0 : o[k], obj);
249
+ if (!snapshots.length) throw new Error('No snapshots found');
250
+ return snapshots;
251
+ } // Merges snapshots and deduplicates resource arrays. Duplicate log resources are replaced, root
252
+ // resources are deduplicated by widths, and all other resources are deduplicated by their URL.
253
253
 
254
- if (val != null) {
255
- // join formatted array values with a space
256
- val = [].concat(val).map(format).join(', ');
257
- log.debug(`- ${prop}: ${val}`, snapshot.meta);
254
+ function mergeSnapshotOptions(prev = {}, next) {
255
+ let {
256
+ resources: oldResources = [],
257
+ ...existing
258
+ } = prev;
259
+ let {
260
+ resources: newResources = [],
261
+ widths = [],
262
+ width,
263
+ ...incoming
264
+ } = next; // prioritize singular widths over mutilple widths
265
+
266
+ widths = width ? [width] : widths; // deduplicate resources by associated widths and url
267
+
268
+ let resources = oldResources.reduce((all, resource) => {
269
+ if (resource.log || resource.widths.every(w => widths.includes(w))) return all;
270
+ if (!resource.root && all.some(r => r.url === resource.url)) return all;
271
+ resource.widths = resource.widths.filter(w => !widths.includes(w));
272
+ return all.concat(resource);
273
+ }, newResources.map(r => ({ ...r,
274
+ widths
275
+ }))); // sort resources after merging; roots first by min-width & logs last
276
+
277
+ resources.sort((a, b) => {
278
+ if (a.root && b.root) return Math.min(...b.widths) - Math.min(...a.widths);
279
+ return a.root || b.log ? -1 : a.log || b.root ? 1 : 0;
280
+ }); // overwrite resources and ensure unique widths
281
+
282
+ return PercyConfig.merge([existing, incoming, {
283
+ widths,
284
+ resources
285
+ }], (path, prev, next) => {
286
+ if (path[0] === 'resources') return [path, next];
287
+
288
+ if (path[0] === 'widths' && prev && next) {
289
+ return [path, [...new Set([...prev, ...next])]];
258
290
  }
259
- };
260
-
261
- debugProp(snapshot, 'url');
262
- debugProp(snapshot, 'scope');
263
- debugProp(snapshot, 'widths', v => `${v}px`);
264
- debugProp(snapshot, 'minHeight', v => `${v}px`);
265
- debugProp(snapshot, 'enableJavaScript');
266
- debugProp(snapshot, 'deviceScaleFactor');
267
- debugProp(snapshot, 'waitForTimeout');
268
- debugProp(snapshot, 'waitForSelector');
269
- debugProp(snapshot, 'execute.afterNavigation');
270
- debugProp(snapshot, 'execute.beforeResize');
271
- debugProp(snapshot, 'execute.afterResize');
272
- debugProp(snapshot, 'execute.beforeSnapshot');
273
- debugProp(snapshot, 'discovery.allowedHostnames');
274
- debugProp(snapshot, 'discovery.disallowedHostnames');
275
- debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
276
- debugProp(snapshot, 'discovery.authorization', JSON.stringify);
277
- debugProp(snapshot, 'discovery.disableCache');
278
- debugProp(snapshot, 'discovery.userAgent');
279
- debugProp(snapshot, 'clientInfo');
280
- debugProp(snapshot, 'environmentInfo');
281
- debugProp(snapshot, 'domSnapshot', Boolean);
282
-
283
- for (let added of snapshot.additionalSnapshots || []) {
284
- if (showInfo) log.info(`Snapshot found: ${added.name}`, snapshot.meta);else log.debug(`Additional snapshot: ${added.name}`, snapshot.meta);
285
- debugProp(added, 'waitForTimeout');
286
- debugProp(added, 'waitForSelector');
287
- debugProp(added, 'execute');
288
- }
289
- } // Calls the provided callback with additional resources
290
-
291
-
292
- function handleSnapshotResources(snapshot, map, callback) {
293
- let resources = [...map.values()]; // sort the root resource first
294
-
295
- let [root] = resources.splice(resources.findIndex(r => r.root), 1);
296
- resources.unshift(root); // inject Percy CSS
297
-
298
- if (snapshot.percyCSS) {
299
- let css = createPercyCSSResource(root.url, snapshot.percyCSS);
300
- resources.push(css); // replace root contents and associated properties
301
-
302
- Object.assign(root, createRootResource(root.url, root.content.replace(/(<\/body>)(?!.*\1)/is, `<link data-percy-specific-css rel="stylesheet" href="${css.pathname}"/>` + '$&')));
303
- } // include associated snapshot logs matched by meta information
304
-
305
-
306
- resources.push(createLogResource(logger.query(log => {
307
- var _log$meta$snapshot;
308
-
309
- return ((_log$meta$snapshot = log.meta.snapshot) === null || _log$meta$snapshot === void 0 ? void 0 : _log$meta$snapshot.name) === snapshot.meta.snapshot.name;
310
- })));
311
- return callback(snapshot, resources);
312
- } // Wait for a page's asset discovery network to idle
291
+ });
292
+ } // Creates a snapshots queue that manages a Percy build and uploads snapshots.
313
293
 
314
294
 
315
- function waitForDiscoveryNetworkIdle(page, options) {
295
+ export function createSnapshotsQueue(percy) {
316
296
  let {
317
- allowedHostnames,
318
- networkIdleTimeout
319
- } = options;
320
-
321
- let filter = r => hostnameMatches(allowedHostnames, r.url);
322
-
323
- return page.network.idle(filter, networkIdleTimeout);
324
- } // Used to cache resources across core instances
325
-
326
-
327
- const RESOURCE_CACHE_KEY = Symbol('resource-cache'); // Trigger resource requests for a page by iterating over snapshot widths and calling any provided
328
- // execute options. Additional resize options may be provided to capture resources mobile resources
329
-
330
- function* triggerResourceRequests(page, snapshot, options) {
331
- // copy widths to prevent mutation later
332
- let [initialWidth, ...widths] = snapshot.widths; // set the initial page size
333
-
334
- yield page.resize({
335
- width: initialWidth,
336
- height: snapshot.minHeight,
337
- ...options
338
- }); // navigate to the url
339
-
340
- yield page.goto(snapshot.url);
341
-
342
- if (snapshot.execute) {
343
- // when any execute options are provided, inject snapshot options
344
-
345
- /* istanbul ignore next: cannot detect coverage of injected code */
346
- yield page.eval((_, s) => window.__PERCY__.snapshot = s, snapshot);
347
- yield page.evaluate(snapshot.execute.afterNavigation);
348
- } // trigger resize events for other widths
349
-
350
-
351
- for (let width of widths) {
352
- var _snapshot$execute, _snapshot$execute2;
353
-
354
- yield page.evaluate((_snapshot$execute = snapshot.execute) === null || _snapshot$execute === void 0 ? void 0 : _snapshot$execute.beforeResize);
355
- yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
356
- yield page.resize({
357
- width,
358
- height: snapshot.minHeight,
359
- ...options
360
- });
361
- yield page.evaluate((_snapshot$execute2 = snapshot.execute) === null || _snapshot$execute2 === void 0 ? void 0 : _snapshot$execute2.afterResize);
362
- }
363
- } // Discovers resources for a snapshot using a browser page to intercept requests. The callback
364
- // function will be called with the snapshot name (for additional snapshots) and an array of
365
- // discovered resources. When additional snapshots are provided, the callback will be called once
366
- // for each snapshot.
367
-
368
-
369
- export async function* discoverSnapshotResources(percy, snapshot, callback) {
370
- debugSnapshotConfig(snapshot, percy.dryRun); // when dry-running, invoke the callback for each snapshot and immediately return
371
-
372
- let allSnapshots = [snapshot, ...(snapshot.additionalSnapshots || [])];
373
- if (percy.dryRun) return allSnapshots.map(s => callback(s)); // keep a global resource cache across snapshots
374
-
375
- let cache = percy[RESOURCE_CACHE_KEY] || (percy[RESOURCE_CACHE_KEY] = new Map()); // preload the root resource for existing dom snapshots
376
-
377
- let resources = new Map(snapshot.domSnapshot && [createRootResource(snapshot.url, snapshot.domSnapshot)].map(resource => [resource.url, resource])); // when no discovery browser is available, do not attempt to discover other resources
378
-
379
- if (percy.skipDiscovery && !snapshot.domSnapshot) {
380
- throw new Error('Cannot capture DOM snapshot when asset discovery is disabled');
381
- } else if (percy.skipDiscovery) {
382
- return handleSnapshotResources(snapshot, resources, callback);
383
- } // open a new browser page
384
-
385
-
386
- let page = yield percy.browser.page({
387
- enableJavaScript: snapshot.enableJavaScript ?? !snapshot.domSnapshot,
388
- networkIdleTimeout: snapshot.discovery.networkIdleTimeout,
389
- requestHeaders: snapshot.discovery.requestHeaders,
390
- authorization: snapshot.discovery.authorization,
391
- userAgent: snapshot.discovery.userAgent,
392
- meta: snapshot.meta,
393
- // enable network inteception
394
- intercept: {
395
- enableJavaScript: snapshot.enableJavaScript,
396
- disableCache: snapshot.discovery.disableCache,
397
- allowedHostnames: snapshot.discovery.allowedHostnames,
398
- disallowedHostnames: snapshot.discovery.disallowedHostnames,
399
- getResource: u => resources.get(u) || cache.get(u),
400
- saveResource: r => resources.set(r.url, r) && cache.set(r.url, r)
297
+ concurrency
298
+ } = percy.config.discovery;
299
+ let queue = new Queue();
300
+ let build;
301
+ return queue.set({
302
+ concurrency
303
+ }) // on start, create a new Percy build
304
+ .handle('start', async () => {
305
+ try {
306
+ build = percy.build = {};
307
+ let {
308
+ data
309
+ } = await percy.client.createBuild();
310
+ let url = data.attributes['web-url'];
311
+ let number = data.attributes['build-number'];
312
+ Object.assign(build, {
313
+ id: data.id,
314
+ url,
315
+ number
316
+ }); // immediately run the queue if not delayed or deferred
317
+
318
+ if (!percy.delayUploads && !percy.deferUploads) queue.run();
319
+ } catch (err) {
320
+ // immediately throw the error if not delayed or deferred
321
+ if (!percy.delayUploads && !percy.deferUploads) throw err;
322
+ Object.assign(build, {
323
+ error: 'Failed to create build'
324
+ });
325
+ percy.log.error(build.error);
326
+ percy.log.error(err);
327
+ queue.close(true);
401
328
  }
402
- });
329
+ }) // on end, maybe finalize the build and log about build info
330
+ .handle('end', async () => {
331
+ var _build, _build2;
403
332
 
404
- try {
405
- yield* triggerResourceRequests(page, snapshot); // trigger resource requests for any alternate device pixel ratio
406
-
407
- if (snapshot.discovery.devicePixelRatio) {
408
- // wait for any existing pending resource requests first
409
- yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
410
- yield* triggerResourceRequests(page, snapshot, {
411
- deviceScaleFactor: snapshot.discovery.devicePixelRatio,
412
- mobile: true
333
+ if (!percy.readyState) return;
334
+
335
+ if ((_build = build) !== null && _build !== void 0 && _build.failed) {
336
+ percy.log.warn(`Build #${build.number} failed: ${build.url}`, {
337
+ build
338
+ });
339
+ } else if ((_build2 = build) !== null && _build2 !== void 0 && _build2.id) {
340
+ await percy.client.finalizeBuild(build.id);
341
+ percy.log.info(`Finalized build #${build.number}: ${build.url}`, {
342
+ build
343
+ });
344
+ } else {
345
+ percy.log.warn('Build not created', {
346
+ build
413
347
  });
414
348
  }
349
+ }) // snapshots are unique by name alone
350
+ .handle('find', ({
351
+ name
352
+ }, snapshot) => snapshot.name === name) // when pushed, maybe flush old snapshots or possibly merge with existing snapshots
353
+ .handle('push', (snapshot, existing) => {
354
+ let {
355
+ name,
356
+ meta
357
+ } = snapshot; // log immediately when not deferred or dry-running
358
+
359
+ if (!percy.deferUploads) percy.log.info(`Snapshot taken: ${name}`, meta);
360
+ if (percy.dryRun) percy.log.info(`Snapshot found: ${name}`, meta); // immediately flush when uploads are delayed but not skipped
361
+
362
+ if (percy.delayUploads && !percy.deferUploads) queue.flush(); // overwrite any existing snapshot when not deferred or when resources is a function
363
+
364
+ if (!percy.deferUploads || typeof snapshot.resources === 'function') return snapshot; // merge snapshot options when uploads are deferred
365
+
366
+ return mergeSnapshotOptions(existing, snapshot);
367
+ }) // send snapshots to be uploaded to the build
368
+ .handle('task', async function* ({
369
+ resources,
370
+ ...snapshot
371
+ }) {
372
+ let {
373
+ name,
374
+ meta
375
+ } = snapshot; // yield to evaluated snapshot resources
376
+
377
+ snapshot.resources = typeof resources === 'function' ? yield* yieldTo(resources()) : resources; // upload the snapshot and log when deferred
378
+
379
+ let send = 'tag' in snapshot ? 'sendComparison' : 'sendSnapshot';
380
+ let response = yield percy.client[send](build.id, snapshot);
381
+ if (percy.deferUploads) percy.log.info(`Snapshot uploaded: ${name}`, meta);
382
+ return { ...snapshot,
383
+ response
384
+ };
385
+ }) // handle possible build errors returned by the API
386
+ .handle('error', (snapshot, error) => {
387
+ var _error$response;
388
+
389
+ let result = { ...snapshot,
390
+ error
391
+ };
392
+ let {
393
+ name,
394
+ meta
395
+ } = snapshot;
396
+ if (error.name === 'QueueClosedError') return result;
397
+ if (error.name === 'AbortError') return result;
398
+ let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.statusCode) === 422 && error.response.body.errors.find(e => {
399
+ var _e$source;
400
+
401
+ return ((_e$source = e.source) === null || _e$source === void 0 ? void 0 : _e$source.pointer) === '/data/attributes/build';
402
+ });
415
403
 
416
- if (snapshot.domSnapshot) {
417
- // ensure discovery has finished and handle resources
418
- yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
419
- handleSnapshotResources(snapshot, resources, callback);
420
- } else {
421
- let {
422
- enableJavaScript
423
- } = snapshot; // capture snapshots sequentially
424
-
425
- for (let snap of allSnapshots) {
426
- // will wait for timeouts, selectors, and additional network activity
427
- let {
428
- url,
429
- dom
430
- } = yield page.snapshot({
431
- enableJavaScript,
432
- ...snap
433
- });
434
- let root = createRootResource(url, dom); // use the normalized root url to prevent duplicates
435
-
436
- resources.set(root.url, root); // shallow merge with root snapshot options
437
-
438
- handleSnapshotResources({ ...snapshot,
439
- ...snap
440
- }, resources, callback); // remove the previously captured dom snapshot
441
-
442
- resources.delete(root.url);
443
- }
404
+ if (failed) {
405
+ build.error = error.message = failed.detail;
406
+ build.failed = true;
407
+ queue.close(true);
444
408
  }
445
- } finally {
446
- await page.close();
447
- }
409
+
410
+ percy.log.error(`Encountered an error uploading snapshot: ${name}`, meta);
411
+ percy.log.error(error, meta);
412
+ return result;
413
+ });
448
414
  }
package/dist/utils.js CHANGED
@@ -45,7 +45,10 @@ export function createPercyCSSResource(url, css) {
45
45
  } // Creates a log resource object.
46
46
 
47
47
  export function createLogResource(logs) {
48
- return createResource(`/percy.${Date.now()}.log`, JSON.stringify(logs), 'text/plain');
48
+ let [url, content] = [`/percy.${Date.now()}.log`, JSON.stringify(logs)];
49
+ return createResource(url, content, 'text/plain', {
50
+ log: true
51
+ });
49
52
  } // Returns true or false if the provided object is a generator or not
50
53
 
51
54
  export function isGenerator(subject) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.10.4",
3
+ "version": "1.11.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,10 +39,10 @@
39
39
  "test:types": "tsd"
40
40
  },
41
41
  "dependencies": {
42
- "@percy/client": "1.10.4",
43
- "@percy/config": "1.10.4",
44
- "@percy/dom": "1.10.4",
45
- "@percy/logger": "1.10.4",
42
+ "@percy/client": "1.11.0",
43
+ "@percy/config": "1.11.0",
44
+ "@percy/dom": "1.11.0",
45
+ "@percy/logger": "1.11.0",
46
46
  "content-disposition": "^0.5.4",
47
47
  "cross-spawn": "^7.0.3",
48
48
  "extract-zip": "^2.0.1",
@@ -53,5 +53,5 @@
53
53
  "rimraf": "^3.0.2",
54
54
  "ws": "^8.0.0"
55
55
  },
56
- "gitHead": "16a9a410bfcb8eab51b86a08cff12d8d35e4747e"
56
+ "gitHead": "0a5043cd677266390889063924f342af9b347055"
57
57
  }