@percy/core 1.10.4 → 1.12.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 +42 -4
- package/dist/config.js +92 -2
- package/dist/discovery.js +288 -126
- package/dist/network.js +178 -62
- package/dist/page.js +29 -14
- package/dist/percy.js +156 -279
- package/dist/queue.js +335 -99
- package/dist/session.js +2 -1
- package/dist/snapshot.js +243 -277
- package/dist/utils.js +4 -1
- package/package.json +6 -6
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'); //
|
|
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 ?
|
|
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
|
-
}) //
|
|
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:
|
|
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
|
|
4
|
-
import { normalizeURL, hostnameMatches,
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
}
|