@percy/core 1.0.0-beta.68 → 1.0.0-beta.71

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/browser.js CHANGED
@@ -29,6 +29,10 @@ var _page = _interopRequireDefault(require("./page"));
29
29
 
30
30
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
31
31
 
32
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
33
+
34
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
35
+
32
36
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
33
37
 
34
38
  function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }
@@ -61,12 +65,12 @@ class Browser extends _events.default {
61
65
 
62
66
  _defineProperty(this, "closed", false);
63
67
 
64
- _callbacks.set(this, {
68
+ _classPrivateFieldInitSpec(this, _callbacks, {
65
69
  writable: true,
66
70
  value: new Map()
67
71
  });
68
72
 
69
- _lastid.set(this, {
73
+ _classPrivateFieldInitSpec(this, _lastid, {
70
74
  writable: true,
71
75
  value: 0
72
76
  });
@@ -124,7 +128,8 @@ class Browser extends _events.default {
124
128
 
125
129
  this.profile = await _fs.promises.mkdtemp(_path.default.join(_os.default.tmpdir(), 'percy-browser-')); // collect args to pass to the browser process
126
130
 
127
- let args = [...this.args, `--user-data-dir=${this.profile}`]; // spawn the browser process detached in its own group and session
131
+ let args = [...this.args, `--user-data-dir=${this.profile}`];
132
+ this.log.debug('Launching browser'); // spawn the browser process detached in its own group and session
128
133
 
129
134
  this.process = (0, _crossSpawn.default)(this.executable, args, {
130
135
  detached: process.platform !== 'win32'
@@ -139,6 +144,7 @@ class Browser extends _events.default {
139
144
  this.ws.on('message', data => this._handleMessage(data)); // get version information
140
145
 
141
146
  this.version = await this.send('Browser.getVersion');
147
+ this.log.debug(`Browser connected: ${this.version.product}`);
142
148
  }
143
149
 
144
150
  isConnected() {
@@ -150,7 +156,8 @@ class Browser extends _events.default {
150
156
  async close() {
151
157
  var _this$process4, _this$ws2;
152
158
 
153
- if (this._closed) return this._closed; // resolves when the browser has closed
159
+ if (this._closed) return this._closed;
160
+ this.log.debug('Closing browser'); // resolves when the browser has closed
154
161
 
155
162
  this._closed = Promise.all([new Promise(resolve => {
156
163
  /* istanbul ignore next: race condition paranoia */
@@ -177,6 +184,8 @@ class Browser extends _events.default {
177
184
  this.log.debug(error);
178
185
  });
179
186
  }
187
+
188
+ this.log.debug('Browser closed');
180
189
  }); // reject any pending callbacks
181
190
 
182
191
  for (let callback of _classPrivateFieldGet(this, _callbacks).values()) {
@@ -316,7 +325,7 @@ class Browser extends _events.default {
316
325
  } else if (data.method === 'Target.detachedFromTarget') {
317
326
  // remove the old session reference when detached from a target
318
327
  let session = this.sessions.get(data.params.sessionId);
319
- this.sessions.delete(session.sessionId);
328
+ this.sessions.delete(data.params.sessionId);
320
329
  session === null || session === void 0 ? void 0 : session._handleClose();
321
330
  }
322
331
 
package/dist/config.js CHANGED
@@ -4,18 +4,9 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.configMigration = configMigration;
7
+ exports.snapshotDOMSchema = exports.schemas = exports.migrations = exports.configSchema = void 0;
7
8
  exports.snapshotMigration = snapshotMigration;
8
- exports.getSnapshotConfig = getSnapshotConfig;
9
- exports.migrations = exports.schemas = exports.snapshotDOMSchema = exports.snapshotSchema = exports.configSchema = void 0;
10
-
11
- var _assert = require("assert");
12
-
13
- var _config = _interopRequireDefault(require("@percy/config"));
14
-
15
- var _utils = require("@percy/config/dist/utils");
16
-
17
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
-
9
+ exports.snapshotSchema = void 0;
19
10
  // Common config options used in Percy commands
20
11
  const configSchema = {
21
12
  snapshot: {
@@ -362,65 +353,5 @@ function snapshotMigration(config, util) {
362
353
 
363
354
  const schemas = [configSchema, snapshotSchema, snapshotDOMSchema];
364
355
  exports.schemas = schemas;
365
- const migrations = [['/config', configMigration], ['/snapshot', snapshotMigration]]; // Validate and merge per-snapshot configuration options with global configuration options.
366
-
367
- exports.migrations = migrations;
368
-
369
- function getSnapshotConfig(options, {
370
- snapshot,
371
- discovery
372
- }, log) {
373
- var _ref, _config$widths;
374
-
375
- // prune client and env info from being validated
376
- let {
377
- clientInfo,
378
- environmentInfo,
379
- ...config
380
- } = _config.default.migrate(options, '/snapshot'); // throw an error when missing required options
381
-
382
-
383
- (0, _assert.strict)(config.url, 'Missing required URL for snapshot');
384
- (0, _assert.strict)((_ref = (_config$widths = config.widths) !== null && _config$widths !== void 0 ? _config$widths : snapshot.widths) === null || _ref === void 0 ? void 0 : _ref.length, 'Missing required widths for snapshot'); // validate and scrub according to dom snaphot presence
385
-
386
- let errors = _config.default.validate(config, config.domSnapshot ? '/snapshot/dom' : '/snapshot');
387
-
388
- if (errors) {
389
- log.warn('Invalid snapshot options:');
390
-
391
- for (let e of errors) log.warn(`- ${e.path}: ${e.message}`);
392
- } // parse the URL to construct defaults
393
-
394
-
395
- let url = new URL(options.url); // inherit options from the config
396
-
397
- return (0, _utils.merge)([snapshot, {
398
- // default to the URL /pathname?search#hash
399
- name: `${url.pathname}${url.search}${url.hash}`,
400
- // add back client and environment information
401
- clientInfo,
402
- environmentInfo,
403
- // only specific discovery options are used per-snapshot
404
- discovery: {
405
- allowedHostnames: [url.hostname, ...discovery.allowedHostnames],
406
- requestHeaders: discovery.requestHeaders,
407
- authorization: discovery.authorization,
408
- disableCache: discovery.disableCache,
409
- userAgent: discovery.userAgent
410
- }
411
- }, config], (path, prev, next) => {
412
- switch (path.join('.')) {
413
- case 'widths':
414
- // override and sort widths
415
- return [path, next.sort((a, b) => a - b)];
416
-
417
- case 'percyCSS':
418
- // concatenate percy css
419
- return [path, [prev, next].filter(Boolean).join('\n')];
420
-
421
- case 'execute':
422
- // shorthand for execute.beforeSnapshot
423
- return Array.isArray(next) || typeof next !== 'object' ? [path.concat('beforeSnapshot'), next] : [path];
424
- }
425
- });
426
- }
356
+ const migrations = [['/config', configMigration], ['/snapshot', snapshotMigration]];
357
+ exports.migrations = migrations;
package/dist/discovery.js CHANGED
@@ -3,9 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createRequestHandler = createRequestHandler;
7
- exports.createRequestFinishedHandler = createRequestFinishedHandler;
8
6
  exports.createRequestFailedHandler = createRequestFailedHandler;
7
+ exports.createRequestFinishedHandler = createRequestFinishedHandler;
8
+ exports.createRequestHandler = createRequestHandler;
9
9
 
10
10
  var _logger = _interopRequireDefault(require("@percy/logger"));
11
11
 
@@ -34,10 +34,10 @@ function createRequestHandler(network, {
34
34
  let resource = getResource(url);
35
35
 
36
36
  if (resource !== null && resource !== void 0 && resource.root) {
37
- log.debug('-> Serving root resource', meta);
37
+ log.debug('- Serving root resource', meta);
38
38
  await request.respond(resource);
39
39
  } else if (resource && !disableCache) {
40
- log.debug('-> Resource cache hit', meta);
40
+ log.debug('- Resource cache hit', meta);
41
41
  await request.respond(resource);
42
42
  } else {
43
43
  await request.continue();
@@ -57,7 +57,7 @@ function createRequestFinishedHandler(network, {
57
57
  allowedHostnames,
58
58
  disableCache,
59
59
  getResource,
60
- addResource
60
+ saveResource
61
61
  }) {
62
62
  let log = (0, _logger.default)('core:discovery');
63
63
  return async request => {
@@ -80,17 +80,17 @@ function createRequestFinishedHandler(network, {
80
80
  /* istanbul ignore next: sanity check */
81
81
 
82
82
  if (!response) {
83
- return log.debug('-> Skipping no response', meta);
83
+ return log.debug('- Skipping no response', meta);
84
84
  } else if (!capture) {
85
- return log.debug('-> Skipping remote resource', meta);
85
+ return log.debug('- Skipping remote resource', meta);
86
86
  } else if (!body.length) {
87
- return log.debug('-> Skipping empty response', meta);
87
+ return log.debug('- Skipping empty response', meta);
88
88
  } else if (body.length > MAX_RESOURCE_SIZE) {
89
- return log.debug('-> Skipping resource larger than 15MB', meta);
89
+ return log.debug('- Skipping resource larger than 15MB', meta);
90
90
  } else if (!ALLOWED_STATUSES.includes(response.status)) {
91
- return log.debug(`-> Skipping disallowed status [${response.status}]`, meta);
91
+ return log.debug(`- Skipping disallowed status [${response.status}]`, meta);
92
92
  } else if (!enableJavaScript && !ALLOWED_RESOURCES.includes(request.type)) {
93
- return log.debug(`-> Skipping disallowed resource type [${request.type}]`, meta);
93
+ return log.debug(`- Skipping disallowed resource type [${request.type}]`, meta);
94
94
  }
95
95
 
96
96
  resource = (0, _utils.createResource)(url, body, response.mimeType, {
@@ -101,11 +101,11 @@ function createRequestFinishedHandler(network, {
101
101
  [key]: value.split('\n')
102
102
  }), {})
103
103
  });
104
- log.debug(`-> sha: ${resource.sha}`, meta);
105
- log.debug(`-> mimetype: ${resource.mimetype}`, meta);
104
+ log.debug(`- sha: ${resource.sha}`, meta);
105
+ log.debug(`- mimetype: ${resource.mimetype}`, meta);
106
106
  }
107
107
 
108
- addResource(resource);
108
+ saveResource(resource);
109
109
  } catch (error) {
110
110
  log.debug(`Encountered an error processing resource: ${url}`, meta);
111
111
  log.debug(error);
package/dist/install.js CHANGED
@@ -26,8 +26,8 @@ function formatBytes(int) {
26
26
 
27
27
 
28
28
  function formatTime(ms) {
29
- let minutes = (ms / 1000 / 60).toFixed().padStart(2, '0');
30
- let seconds = (ms / 1000).toFixed().padStart(2, '0');
29
+ let minutes = (ms / 1000 / 60).toString().split('.')[0].padStart(2, '0');
30
+ let seconds = (ms / 1000 % 60).toFixed().padStart(2, '0');
31
31
  return `${minutes}:${seconds}`;
32
32
  } // Formats progress as ":prefix [:bar] :ratio :percent :eta"
33
33
 
@@ -38,9 +38,7 @@ function formatProgress(prefix, total, start, progress) {
38
38
  let percent = Math.floor(ratio * 100).toFixed(0);
39
39
  let barLen = Math.round(width * ratio);
40
40
  let barContent = Array(Math.max(0, barLen + 1)).join('=') + Array(Math.max(0, width - barLen + 1)).join(' ');
41
- let elapsed = new Date() - start;
42
- /* istanbul ignore next: eta in testing is always 0 */
43
-
41
+ let elapsed = Date.now() - start;
44
42
  let eta = ratio >= 1 ? 0 : elapsed * (total / progress - 1);
45
43
  return `${prefix} [${barContent}] ` + `${formatBytes(progress)}/${formatBytes(total)} ` + `${percent}% ${formatTime(eta)}`;
46
44
  } // Returns an item from the map keyed by the current platform
@@ -144,7 +142,7 @@ async function install({
144
142
  response.on('data', chunk => {
145
143
  var _start, _progress;
146
144
 
147
- (_start = start) !== null && _start !== void 0 ? _start : start = new Date();
145
+ (_start = start) !== null && _start !== void 0 ? _start : start = Date.now();
148
146
  progress = ((_progress = progress) !== null && _progress !== void 0 ? _progress : 0) + chunk.length;
149
147
  log.progress(formatProgress(premsg, total, start, progress));
150
148
  });
package/dist/network.js CHANGED
@@ -13,6 +13,10 @@ var _discovery = require("./discovery");
13
13
 
14
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
15
 
16
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
17
+
18
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
19
+
16
20
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
17
21
 
18
22
  function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
@@ -37,22 +41,22 @@ class Network {
37
41
 
38
42
  _defineProperty(this, "log", (0, _logger.default)('core:network'));
39
43
 
40
- _pending.set(this, {
44
+ _classPrivateFieldInitSpec(this, _pending, {
41
45
  writable: true,
42
46
  value: new Map()
43
47
  });
44
48
 
45
- _requests.set(this, {
49
+ _classPrivateFieldInitSpec(this, _requests, {
46
50
  writable: true,
47
51
  value: new Map()
48
52
  });
49
53
 
50
- _intercepts.set(this, {
54
+ _classPrivateFieldInitSpec(this, _intercepts, {
51
55
  writable: true,
52
56
  value: new Map()
53
57
  });
54
58
 
55
- _authentications.set(this, {
59
+ _classPrivateFieldInitSpec(this, _authentications, {
56
60
  writable: true,
57
61
  value: new Set()
58
62
  });
@@ -323,7 +327,7 @@ class Network {
323
327
  let msg = 'Timed out waiting for network requests to idle.';
324
328
 
325
329
  if (this.log.shouldLog('debug')) {
326
- msg += `\n\n ${['Active requests:', ...requests.map(r => r.url)].join('\n -> ')}\n`;
330
+ msg += `\n\n ${['Active requests:', ...requests.map(r => r.url)].join('\n - ')}\n`;
327
331
  }
328
332
 
329
333
  throw new Error(msg);
package/dist/page.js CHANGED
@@ -230,7 +230,8 @@ class Page {
230
230
  execute,
231
231
  ...options
232
232
  }) {
233
- // wait for any specified timeout
233
+ this.log.debug(`Taking snapshot: ${name}`, this.meta); // wait for any specified timeout
234
+
234
235
  if (waitForTimeout) {
235
236
  this.log.debug(`Wait for ${waitForTimeout}ms timeout`, this.meta);
236
237
  await new Promise(resolve => setTimeout(resolve, waitForTimeout));
package/dist/percy.js CHANGED
@@ -9,6 +9,8 @@ var _client = _interopRequireDefault(require("@percy/client"));
9
9
 
10
10
  var _config = _interopRequireDefault(require("@percy/config"));
11
11
 
12
+ var _utils = require("@percy/config/dist/utils");
13
+
12
14
  var _logger = _interopRequireDefault(require("@percy/logger"));
13
15
 
14
16
  var _queue = _interopRequireDefault(require("./queue"));
@@ -17,12 +19,14 @@ var _browser = _interopRequireDefault(require("./browser"));
17
19
 
18
20
  var _server = _interopRequireDefault(require("./server"));
19
21
 
20
- var _config2 = require("./config");
21
-
22
- var _utils = require("./utils");
22
+ var _snapshot = require("./snapshot");
23
23
 
24
24
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25
25
 
26
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
27
+
28
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
29
+
26
30
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
27
31
 
28
32
  function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
@@ -31,8 +35,6 @@ function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!priva
31
35
 
32
36
  function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
33
37
 
34
- var _cache = /*#__PURE__*/new WeakMap();
35
-
36
38
  var _uploads = /*#__PURE__*/new WeakMap();
37
39
 
38
40
  var _snapshots = /*#__PURE__*/new WeakMap();
@@ -56,6 +58,8 @@ class Percy {
56
58
  deferUploads,
57
59
  // run without uploading anything
58
60
  skipUploads,
61
+ // implies `skipUploads` and also skips asset discovery
62
+ dryRun,
59
63
  // configuration filepath
60
64
  config,
61
65
  // provided to @percy/client
@@ -73,24 +77,20 @@ class Percy {
73
77
 
74
78
  _defineProperty(this, "readyState", null);
75
79
 
76
- _cache.set(this, {
77
- writable: true,
78
- value: new Map()
79
- });
80
-
81
- _uploads.set(this, {
80
+ _classPrivateFieldInitSpec(this, _uploads, {
82
81
  writable: true,
83
82
  value: new _queue.default()
84
83
  });
85
84
 
86
- _snapshots.set(this, {
85
+ _classPrivateFieldInitSpec(this, _snapshots, {
87
86
  writable: true,
88
87
  value: new _queue.default()
89
88
  });
90
89
 
91
90
  if (loglevel) this.loglevel(loglevel);
92
- this.deferUploads = skipUploads || deferUploads;
93
- this.skipUploads = skipUploads;
91
+ this.dryRun = !!dryRun;
92
+ this.skipUploads = this.dryRun || !!skipUploads;
93
+ this.deferUploads = this.skipUploads || !!deferUploads;
94
94
  this.config = _config.default.load({
95
95
  overrides: options,
96
96
  path: config
@@ -123,6 +123,36 @@ class Percy {
123
123
 
124
124
  address() {
125
125
  return `http://localhost:${this.port}`;
126
+ } // Set client & environment info, and override loaded config options
127
+
128
+
129
+ setConfig({
130
+ clientInfo,
131
+ environmentInfo,
132
+ ...config
133
+ }) {
134
+ this.client.addClientInfo(clientInfo);
135
+ this.client.addEnvironmentInfo(environmentInfo); // normalize config and do nothing if empty
136
+
137
+ config = _config.default.normalize(config, {
138
+ schema: '/config'
139
+ });
140
+ if (!config) return this.config; // validate provided config options
141
+
142
+ let errors = _config.default.validate(config);
143
+
144
+ if (errors) {
145
+ this.log.warn('Invalid config:');
146
+
147
+ for (let e of errors) this.log.warn(`- ${e.path}: ${e.message}`);
148
+ } // merge and override existing config options
149
+
150
+
151
+ this.config = (0, _utils.merge)([this.config, config], (path, prev, next) => {
152
+ // replace arrays instead of merging
153
+ return Array.isArray(next) && [path, next];
154
+ });
155
+ return this.config;
126
156
  } // Resolves once snapshot and upload queues are idle
127
157
 
128
158
 
@@ -186,7 +216,7 @@ class Percy {
186
216
  // when not deferred, wait until the build is created first
187
217
  if (!this.deferUploads) await buildTask; // launch the discovery browser
188
218
 
189
- await this.browser.launch(this.config.discovery.launchOptions); // if there is a server, start listening
219
+ if (!this.dryRun) await this.browser.launch(); // if there is a server, start listening
190
220
 
191
221
  await ((_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.listen(this.port)); // mark this process as running
192
222
 
@@ -226,17 +256,22 @@ class Percy {
226
256
  };
227
257
  if (force) this.log.info('Stopping percy...', meta); // close the snapshot queue and wait for it to empty
228
258
 
229
- if (_classPrivateFieldGet(this, _snapshots).close().length) {
230
- await _classPrivateFieldGet(this, _snapshots).empty(len => {
231
- this.log.progress(`Processing ${len}` + ` snapshot${len !== 1 ? 's' : ''}...`, !!len);
232
- });
259
+ if (_classPrivateFieldGet(this, _snapshots).close().size) {
260
+ await _classPrivateFieldGet(this, _snapshots).empty(s => !this.dryRun && this.log.progress(`Processing ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s));
233
261
  } // run, close, and wait for the upload queue to empty
234
262
 
235
263
 
236
- if (!this.skipUploads && _classPrivateFieldGet(this, _uploads).run().close().length) {
237
- await _classPrivateFieldGet(this, _uploads).empty(len => {
238
- this.log.progress(`Uploading ${len}` + ` snapshot${len !== 1 ? 's' : ''}...`, !!len);
264
+ if (!this.skipUploads && _classPrivateFieldGet(this, _uploads).run().close().size) {
265
+ await _classPrivateFieldGet(this, _uploads).empty(s => {
266
+ this.log.progress(`Uploading ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
239
267
  });
268
+ } // if dry-running, print the total number of snapshots that would be uploaded
269
+
270
+
271
+ if (this.dryRun && _classPrivateFieldGet(this, _uploads).size) {
272
+ let total = _classPrivateFieldGet(this, _uploads).size - 1; // subtract the build task
273
+
274
+ this.log.info(`Found ${total} snapshot${total !== 1 ? 's' : ''}`);
240
275
  } // close the any running server and browser
241
276
 
242
277
 
@@ -271,202 +306,62 @@ class Percy {
271
306
  snapshot(options) {
272
307
  if (this.readyState !== 1) {
273
308
  throw new Error('Not running');
274
- }
309
+ } // handle multiple snapshots
275
310
 
276
- let {
277
- url,
278
- name,
279
- discovery,
280
- domSnapshot,
281
- execute,
282
- waitForTimeout,
283
- waitForSelector,
284
- additionalSnapshots,
285
- ...conf
286
- } = (0, _config2.getSnapshotConfig)(options, this.config, this.log);
287
- let meta = {
288
- snapshot: {
289
- name
290
- },
291
- build: this.build
292
- };
293
311
 
294
- let maybeDebug = (val, msg) => {
295
- if (val != null) this.log.debug(msg(val), meta);
296
- }; // clear any existing pending upload for the same snapshot (for retries)
312
+ if (Array.isArray(options)) {
313
+ return Promise.all(options.map(o => this.snapshot(o)));
314
+ } // get derived snapshot config options
297
315
 
298
316
 
299
- _classPrivateFieldGet(this, _uploads).clear(`upload/${name}`); // resolves after asset discovery has finished and the upload has been queued
317
+ let snapshot = (0, _snapshot.getSnapshotConfig)(this, options); // clear any existing snapshot uploads of the same name (for retries)
300
318
 
319
+ for (let {
320
+ name
321
+ } of [snapshot, ...(snapshot.additionalSnapshots || [])]) {
322
+ _classPrivateFieldGet(this, _uploads).cancel(`upload/${name}`);
323
+ } // resolves after asset discovery has finished and uploads have been queued
301
324
 
302
- return _classPrivateFieldGet(this, _snapshots).push(`snapshot/${name}`, async () => {
303
- let resources = new Map();
304
- let root, page;
305
325
 
326
+ return _classPrivateFieldGet(this, _snapshots).push(`snapshot/${snapshot.name}`, async function* () {
306
327
  try {
307
- var _options$discovery, _options$discovery2, _options$discovery3, _options$discovery4, _options$discovery5, _conf$enableJavaScrip;
308
-
309
- this.log.debug('---------');
310
- this.log.debug('Handling snapshot:', meta);
311
- this.log.debug(`-> name: ${name}`, meta);
312
- this.log.debug(`-> url: ${url}`, meta);
313
- maybeDebug(conf.widths, v => `-> widths: ${v.join('px, ')}px`);
314
- maybeDebug(conf.minHeight, v => `-> minHeight: ${v}px`);
315
- maybeDebug(conf.enableJavaScript, v => `-> enableJavaScript: ${v}`);
316
- maybeDebug((_options$discovery = options.discovery) === null || _options$discovery === void 0 ? void 0 : _options$discovery.allowedHostnames, v => `-> discovery.allowedHostnames: ${v}`);
317
- maybeDebug((_options$discovery2 = options.discovery) === null || _options$discovery2 === void 0 ? void 0 : _options$discovery2.requestHeaders, v => `-> discovery.requestHeaders: ${JSON.stringify(v)}`);
318
- maybeDebug((_options$discovery3 = options.discovery) === null || _options$discovery3 === void 0 ? void 0 : _options$discovery3.authorization, v => `-> discovery.authorization: ${JSON.stringify(v)}`);
319
- maybeDebug((_options$discovery4 = options.discovery) === null || _options$discovery4 === void 0 ? void 0 : _options$discovery4.disableCache, v => `-> discovery.disableCache: ${v}`);
320
- maybeDebug((_options$discovery5 = options.discovery) === null || _options$discovery5 === void 0 ? void 0 : _options$discovery5.userAgent, v => `-> discovery.userAgent: ${v}`);
321
- maybeDebug(options.waitForTimeout, v => `-> waitForTimeout: ${v}`);
322
- maybeDebug(options.waitForSelector, v => `-> waitForSelector: ${v}`);
323
- maybeDebug(options.execute, v => `-> execute: ${v}`);
324
- maybeDebug(conf.clientInfo, v => `-> clientInfo: ${v}`);
325
- maybeDebug(conf.environmentInfo, v => `-> environmentInfo: ${v}`); // create the root resource if a dom snapshot was provided
326
-
327
- if (domSnapshot) {
328
- root = (0, _utils.createRootResource)(url, domSnapshot);
329
- } // open a new browser page
330
-
331
-
332
- page = await this.browser.page({
333
- networkIdleTimeout: this.config.discovery.networkIdleTimeout,
334
- enableJavaScript: (_conf$enableJavaScrip = conf.enableJavaScript) !== null && _conf$enableJavaScrip !== void 0 ? _conf$enableJavaScrip : !domSnapshot,
335
- requestHeaders: discovery.requestHeaders,
336
- authorization: discovery.authorization,
337
- userAgent: discovery.userAgent,
338
- meta,
339
- // enable network inteception
340
- intercept: {
341
- disableCache: discovery.disableCache,
342
- enableJavaScript: conf.enableJavaScript,
343
- allowedHostnames: discovery.allowedHostnames,
344
- getResource: url => {
345
- var _root;
346
-
347
- return url === ((_root = root) === null || _root === void 0 ? void 0 : _root.url) ? root : resources.get(url) || _classPrivateFieldGet(this, _cache).get(url);
348
- },
349
- addResource: resource => {
350
- if (resource.root) return;
351
- resources.set(resource.url, resource);
352
-
353
- _classPrivateFieldGet(this, _cache).set(resource.url, resource);
354
- }
355
- }
356
- }); // copy widths to prevent mutation
357
-
358
- let widths = conf.widths.slice(); // resolves when the network is idle for discoverable assets
359
-
360
- let discoveryIdle = () => page.network.idle(({
361
- url
362
- }) => (0, _utils.hostnameMatches)(discovery.allowedHostnames, url)); // set the initial page size
363
-
364
-
365
- await page.resize({
366
- width: widths.shift(),
367
- height: conf.minHeight
368
- }); // navigate to the url
369
-
370
- await page.goto(url); // evaluate any necessary javascript
371
-
372
- await page.evaluate(execute === null || execute === void 0 ? void 0 : execute.afterNavigation); // trigger resize events for other widths
373
-
374
- for (let width of widths) {
375
- await page.evaluate(execute === null || execute === void 0 ? void 0 : execute.beforeResize);
376
- await discoveryIdle(); // ensure discovery idles before resizing
377
-
378
- await page.resize({
379
- width,
380
- height: conf.minHeight
381
- });
382
- await page.evaluate(execute === null || execute === void 0 ? void 0 : execute.afterResize);
383
- } // create and add a percy-css resource
384
-
385
-
386
- let percyCSS = (0, _utils.createPercyCSSResource)(url, conf.percyCSS);
387
- if (percyCSS) resources.set(percyCSS.url, percyCSS);
388
-
389
- if (root) {
390
- // ensure discovery finishes before uploading
391
- await discoveryIdle();
392
- root = (0, _utils.injectPercyCSS)(root, percyCSS);
393
- this.log.info(`Snapshot taken: ${name}`, meta);
394
-
395
- this._scheduleUpload(name, conf, [root, ...resources.values()]);
328
+ yield* (0, _snapshot.discoverSnapshotResources)(this, snapshot, (snap, resources) => {
329
+ if (!this.dryRun) this.log.info(`Snapshot taken: ${snap.name}`, snap.meta);
330
+
331
+ this._scheduleUpload(snap, resources);
332
+ });
333
+ } catch (error) {
334
+ if (error.message === 'Canceled') {
335
+ this.log.error('Received a duplicate snapshot name, ' + `the previous snapshot was canceled: ${snapshot.name}`);
396
336
  } else {
397
- // capture additional snapshots sequentially
398
- let rootSnapshot = {
399
- name,
400
- waitForTimeout,
401
- waitForSelector,
402
- execute
403
- };
404
- let allSnapshots = [rootSnapshot, ...(additionalSnapshots || [])];
405
-
406
- for (let {
407
- name,
408
- prefix = '',
409
- suffix = '',
410
- ...opts
411
- } of allSnapshots) {
412
- name || (name = `${prefix}${rootSnapshot.name}${suffix}`);
413
- this.log.debug(`Taking snapshot: ${name}`, meta); // will wait for timeouts, selectors, and additional network activity
414
-
415
- let {
416
- url,
417
- dom
418
- } = await page.snapshot({ ...conf,
419
- ...opts
420
- });
421
- let root = (0, _utils.injectPercyCSS)((0, _utils.createRootResource)(url, dom), percyCSS);
422
- resources.delete(root.url); // remove any discovered root resource
423
-
424
- this.log.info(`Snapshot taken: ${name}`, meta);
425
-
426
- this._scheduleUpload(name, conf, [root, ...resources.values()]);
427
- }
337
+ this.log.error(`Encountered an error taking snapshot: ${snapshot.name}`, snapshot.meta);
338
+ this.log.error(error, snapshot.meta);
428
339
  }
429
- } catch (error) {
430
- this.log.error(`Encountered an error taking snapshot: ${name}`, meta);
431
- this.log.error(error, meta);
432
- } finally {
433
- var _page;
340
+ } // fixes an issue in Node 12 where implicit returns do not correctly resolve the async
341
+ // generator objects https://crbug.com/v8/10238
434
342
 
435
- await ((_page = page) === null || _page === void 0 ? void 0 : _page.close());
436
- }
437
- });
343
+
344
+ return; // eslint-disable-line no-useless-return
345
+ }.bind(this));
438
346
  } // Queues a snapshot upload with the provided configuration options and resources
439
347
 
440
348
 
441
- _scheduleUpload(name, conf, resources) {
442
- _classPrivateFieldGet(this, _uploads).push(`upload/${name}`, async () => {
349
+ _scheduleUpload(snapshot, resources) {
350
+ _classPrivateFieldGet(this, _uploads).push(`upload/${snapshot.name}`, async () => {
443
351
  try {
444
- // attach a log resource for debugging
445
- resources = resources.concat((0, _utils.createLogResource)(_logger.default.query(l => {
446
- var _l$meta$snapshot;
447
-
448
- return ((_l$meta$snapshot = l.meta.snapshot) === null || _l$meta$snapshot === void 0 ? void 0 : _l$meta$snapshot.name) === name;
449
- })));
450
- await this.client.sendSnapshot(this.build.id, { ...conf,
451
- name,
352
+ await this.client.sendSnapshot(this.build.id, { ...snapshot,
452
353
  resources
453
354
  });
454
355
  } catch (error) {
455
356
  var _error$response, _failed$detail;
456
357
 
457
- let meta = {
458
- snapshot: {
459
- name
460
- },
461
- build: this.build
462
- };
463
358
  let failed = ((_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.status) === 422 && error.response.body.errors.find(e => {
464
359
  var _e$source;
465
360
 
466
361
  return ((_e$source = e.source) === null || _e$source === void 0 ? void 0 : _e$source.pointer) === '/data/attributes/build';
467
362
  });
468
- this.log.error(`Encountered an error uploading snapshot: ${name}`, meta);
469
- this.log.error((_failed$detail = failed === null || failed === void 0 ? void 0 : failed.detail) !== null && _failed$detail !== void 0 ? _failed$detail : error, meta); // build failed at some point, stop accepting snapshots
363
+ this.log.error(`Encountered an error uploading snapshot: ${snapshot.name}`, snapshot.meta);
364
+ this.log.error((_failed$detail = failed === null || failed === void 0 ? void 0 : failed.detail) !== null && _failed$detail !== void 0 ? _failed$detail : error, snapshot.meta); // build failed at some point, stop accepting snapshots
470
365
 
471
366
  if (failed) {
472
367
  this.build.failed = true;
package/dist/queue.js CHANGED
@@ -7,6 +7,10 @@ exports.default = void 0;
7
7
 
8
8
  var _utils = require("./utils");
9
9
 
10
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
11
+
12
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
13
+
10
14
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
11
15
 
12
16
  function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
@@ -15,6 +19,19 @@ function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!priva
15
19
 
16
20
  function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
17
21
 
22
+ function isGenerator(obj) {
23
+ return typeof obj.next === 'function' && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function');
24
+ }
25
+
26
+ async function runGeneratorTask(task, arg) {
27
+ if (task.canceled) await task.generator.throw(new Error('Canceled'));
28
+ let {
29
+ done,
30
+ value
31
+ } = await task.generator.next(arg);
32
+ return done ? value : runGeneratorTask(task, value);
33
+ }
34
+
18
35
  var _queued = /*#__PURE__*/new WeakMap();
19
36
 
20
37
  var _pending = /*#__PURE__*/new WeakMap();
@@ -25,12 +42,12 @@ class Queue {
25
42
 
26
43
  _defineProperty(this, "closed", false);
27
44
 
28
- _queued.set(this, {
45
+ _classPrivateFieldInitSpec(this, _queued, {
29
46
  writable: true,
30
47
  value: new Map()
31
48
  });
32
49
 
33
- _pending.set(this, {
50
+ _classPrivateFieldInitSpec(this, _pending, {
34
51
  writable: true,
35
52
  value: new Map()
36
53
  });
@@ -40,6 +57,7 @@ class Queue {
40
57
 
41
58
  push(id, callback, priority) {
42
59
  if (this.closed) throw new Error('Closed');
60
+ this.cancel(id);
43
61
  let task = {
44
62
  id,
45
63
  callback,
@@ -51,8 +69,6 @@ class Queue {
51
69
  reject
52
70
  });
53
71
 
54
- _classPrivateFieldGet(this, _queued).delete(id);
55
-
56
72
  _classPrivateFieldGet(this, _queued).set(id, task);
57
73
 
58
74
  this._dequeue();
@@ -60,17 +76,23 @@ class Queue {
60
76
  return task.promise;
61
77
  }
62
78
 
63
- clear(id) {
64
- if (id != null) {
65
- _classPrivateFieldGet(this, _queued).delete(id);
66
- } else {
67
- _classPrivateFieldGet(this, _queued).clear();
68
- }
79
+ cancel(id) {
80
+ let pending = _classPrivateFieldGet(this, _pending).get(id);
81
+
82
+ if (pending) pending.canceled = true;
69
83
 
70
- return this.length;
84
+ _classPrivateFieldGet(this, _pending).delete(id);
85
+
86
+ _classPrivateFieldGet(this, _queued).delete(id);
71
87
  }
72
88
 
73
- get length() {
89
+ clear() {
90
+ _classPrivateFieldGet(this, _queued).clear();
91
+
92
+ return this.size;
93
+ }
94
+
95
+ get size() {
74
96
  return _classPrivateFieldGet(this, _queued).size + _classPrivateFieldGet(this, _pending).size;
75
97
  }
76
98
 
@@ -94,15 +116,17 @@ class Queue {
94
116
  }
95
117
 
96
118
  async idle() {
97
- await (0, _utils.waitFor)(() => !_classPrivateFieldGet(this, _pending).size, {
119
+ await (0, _utils.waitFor)(() => {
120
+ return !_classPrivateFieldGet(this, _pending).size;
121
+ }, {
98
122
  idle: 10
99
123
  });
100
124
  }
101
125
 
102
126
  async empty(onCheck) {
103
127
  await (0, _utils.waitFor)(() => {
104
- onCheck === null || onCheck === void 0 ? void 0 : onCheck(this.length);
105
- return !this.length;
128
+ onCheck === null || onCheck === void 0 ? void 0 : onCheck(this.size);
129
+ return !this.size;
106
130
  }, {
107
131
  idle: 10
108
132
  });
@@ -135,15 +159,21 @@ class Queue {
135
159
  _classPrivateFieldGet(this, _pending).set(task.id, task);
136
160
 
137
161
  let done = (callback, arg) => {
138
- _classPrivateFieldGet(this, _pending).delete(task.id);
139
-
162
+ if (!task.canceled) _classPrivateFieldGet(this, _pending).delete(task.id);
140
163
  callback(arg);
141
164
 
142
165
  this._dequeue();
143
166
  };
144
167
 
145
168
  try {
146
- return Promise.resolve(task.callback()).then(done.bind(null, task.resolve)).catch(done.bind(null, task.reject));
169
+ let result = task.callback();
170
+
171
+ if (isGenerator(result)) {
172
+ task.generator = result;
173
+ result = runGeneratorTask(task);
174
+ }
175
+
176
+ return Promise.resolve(result).then(done.bind(null, task.resolve)).catch(done.bind(null, task.reject));
147
177
  } catch (err) {
148
178
  done(task.reject, err);
149
179
  }
package/dist/server.js CHANGED
@@ -74,6 +74,7 @@ function createServer(routes) {
74
74
 
75
75
  context.routes = routes;
76
76
  context.server = _http.default.createServer((request, response) => {
77
+ request.params = new URLSearchParams(request.url.split('?')[1]);
77
78
  request.on('data', chunk => {
78
79
  request.body = (request.body || '') + chunk;
79
80
  });
@@ -123,6 +124,13 @@ function createPercyServer(percy) {
123
124
  loglevel: percy.loglevel(),
124
125
  build: percy.build
125
126
  }],
127
+ // remotely get and set percy config options
128
+ '/percy/config': ({
129
+ body
130
+ }) => [200, 'application/json', {
131
+ config: body ? percy.setConfig(body) : percy.config,
132
+ success: true
133
+ }],
126
134
  // responds when idle
127
135
  '/percy/idle': () => percy.idle().then(() => [200, 'application/json', {
128
136
  success: true
@@ -136,11 +144,16 @@ function createPercyServer(percy) {
136
144
  return [200, 'applicaton/javascript', content.concat(wrapper)];
137
145
  }),
138
146
  // forward snapshot requests
139
- '/percy/snapshot': ({
140
- body
141
- }) => percy.snapshot(body).then(() => [200, 'application/json', {
142
- success: true
143
- }]),
147
+ '/percy/snapshot': async ({
148
+ body,
149
+ params
150
+ }) => {
151
+ let snapshot = percy.snapshot(body);
152
+ if (!params.has('async')) await snapshot;
153
+ return [200, 'application/json', {
154
+ success: true
155
+ }];
156
+ },
144
157
  // stops the instance async at the end of the event loop
145
158
  '/percy/stop': () => setImmediate(() => percy.stop()) && [200, 'application/json', {
146
159
  success: true
package/dist/session.js CHANGED
@@ -13,6 +13,10 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
13
13
 
14
14
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
15
15
 
16
+ function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
17
+
18
+ function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }
19
+
16
20
  function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
17
21
 
18
22
  function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
@@ -30,7 +34,7 @@ class Session extends _events.default {
30
34
 
31
35
  super();
32
36
 
33
- _callbacks.set(this, {
37
+ _classPrivateFieldInitSpec(this, _callbacks, {
34
38
  writable: true,
35
39
  value: new Map()
36
40
  });
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.discoverSnapshotResources = discoverSnapshotResources;
7
+ exports.getSnapshotConfig = getSnapshotConfig;
8
+
9
+ var _logger = _interopRequireDefault(require("@percy/logger"));
10
+
11
+ var _config = _interopRequireDefault(require("@percy/config"));
12
+
13
+ var _utils = require("@percy/config/dist/utils");
14
+
15
+ var _utils2 = require("./utils");
16
+
17
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
+
19
+ // Validates and returns snapshot options merged with percy config options.
20
+ function getSnapshotConfig(percy, options) {
21
+ var _ref, _snapshot$widths;
22
+
23
+ if (!options.url) throw new Error('Missing required URL for snapshot');
24
+ let {
25
+ config
26
+ } = percy;
27
+ let uri = new URL(options.url);
28
+ let name = options.name || `${uri.pathname}${uri.search}${uri.hash}`;
29
+ let meta = {
30
+ snapshot: {
31
+ name
32
+ },
33
+ build: percy.build
34
+ };
35
+ let log = (0, _logger.default)('core:snapshot'); // migrate deprecated snapshot config options
36
+
37
+ let {
38
+ clientInfo,
39
+ environmentInfo,
40
+ ...snapshot
41
+ } = _config.default.migrate(options, '/snapshot'); // throw an error when missing required widths
42
+
43
+
44
+ if (!((_ref = (_snapshot$widths = snapshot.widths) !== null && _snapshot$widths !== void 0 ? _snapshot$widths : percy.config.snapshot.widths) !== null && _ref !== void 0 && _ref.length)) {
45
+ throw new Error('Missing required widths for snapshot');
46
+ } // validate and scrub according to dom snaphot presence
47
+
48
+
49
+ let errors = _config.default.validate(snapshot, snapshot.domSnapshot ? '/snapshot/dom' : '/snapshot');
50
+
51
+ if (errors) {
52
+ log.warn('Invalid snapshot options:', meta);
53
+
54
+ for (let e of errors) log.warn(`- ${e.path}: ${e.message}`, meta);
55
+ } // inherit options from the percy config
56
+
57
+
58
+ return (0, _utils.merge)([config.snapshot, {
59
+ name,
60
+ meta,
61
+ clientInfo,
62
+ environmentInfo,
63
+ // only specific discovery options are used per-snapshot
64
+ discovery: {
65
+ allowedHostnames: [uri.hostname, ...config.discovery.allowedHostnames],
66
+ networkIdleTimeout: config.discovery.networkIdleTimeout,
67
+ requestHeaders: config.discovery.requestHeaders,
68
+ authorization: config.discovery.authorization,
69
+ disableCache: config.discovery.disableCache,
70
+ userAgent: config.discovery.userAgent
71
+ }
72
+ }, snapshot], (path, prev, next) => {
73
+ switch (path.map(k => k.toString()).join('.')) {
74
+ case 'widths':
75
+ // override and sort widths
76
+ return [path, next.sort((a, b) => a - b)];
77
+
78
+ case 'percyCSS':
79
+ // concatenate percy css
80
+ return [path, [prev, next].filter(Boolean).join('\n')];
81
+
82
+ case 'execute':
83
+ // shorthand for execute.beforeSnapshot
84
+ return Array.isArray(next) || typeof next !== 'object' ? [path.concat('beforeSnapshot'), next] : [path];
85
+ } // ensure additional snapshots have complete names
86
+
87
+
88
+ if (path[0] === 'additionalSnapshots' && path.length === 2) {
89
+ let {
90
+ prefix = '',
91
+ suffix = '',
92
+ ...n
93
+ } = next;
94
+ next = {
95
+ name: `${prefix}${name}${suffix}`,
96
+ ...n
97
+ };
98
+ return [path, next];
99
+ }
100
+ });
101
+ } // Returns a complete and valid snapshot config object and logs verbose debug logs detailing various
102
+ // snapshot options. When `showInfo` is true, specific messages will be logged as info logs rather
103
+ // than debug logs.
104
+
105
+
106
+ function debugSnapshotConfig(snapshot, showInfo) {
107
+ let log = (0, _logger.default)('core:snapshot'); // log snapshot info
108
+
109
+ log.debug('---------', snapshot.meta);
110
+ 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
111
+
112
+ let debugProp = (obj, prop, format = String) => {
113
+ let val = prop.split('.').reduce((o, k) => o === null || o === void 0 ? void 0 : o[k], obj);
114
+
115
+ if (val != null) {
116
+ // join formatted array values with a space
117
+ val = [].concat(val).map(format).join(', ');
118
+ log.debug(`- ${prop}: ${val}`, snapshot.meta);
119
+ }
120
+ };
121
+
122
+ debugProp(snapshot, 'url');
123
+ debugProp(snapshot, 'widths', v => `${v}px`);
124
+ debugProp(snapshot, 'minHeight', v => `${v}px`);
125
+ debugProp(snapshot, 'enableJavaScript');
126
+ debugProp(snapshot, 'waitForTimeout');
127
+ debugProp(snapshot, 'waitForSelector');
128
+ debugProp(snapshot, 'execute.afterNavigation');
129
+ debugProp(snapshot, 'execute.beforeResize');
130
+ debugProp(snapshot, 'execute.afterResize');
131
+ debugProp(snapshot, 'execute.beforeSnapshot');
132
+ debugProp(snapshot, 'discovery.allowedHostnames');
133
+ debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
134
+ debugProp(snapshot, 'discovery.authorization', JSON.stringify);
135
+ debugProp(snapshot, 'discovery.disableCache');
136
+ debugProp(snapshot, 'discovery.userAgent');
137
+ debugProp(snapshot, 'clientInfo');
138
+ debugProp(snapshot, 'environmentInfo');
139
+ debugProp(snapshot, 'domSnapshot', Boolean);
140
+
141
+ for (let added of snapshot.additionalSnapshots || []) {
142
+ if (showInfo) log.info(`Snapshot found: ${added.name}`, snapshot.meta);else log.debug(`Additional snapshot: ${added.name}`, snapshot.meta);
143
+ debugProp(added, 'waitForTimeout');
144
+ debugProp(added, 'waitForSelector');
145
+ debugProp(added, 'execute');
146
+ }
147
+ } // Calls the provided callback with additional resources
148
+
149
+
150
+ function handleSnapshotResources(snapshot, map, callback) {
151
+ let resources = [...map.values()]; // sort the root resource first
152
+
153
+ let [root] = resources.splice(resources.findIndex(r => r.root), 1);
154
+ resources.unshift(root); // inject Percy CSS
155
+
156
+ if (snapshot.percyCSS) {
157
+ let css = (0, _utils2.createPercyCSSResource)(root.url, snapshot.percyCSS);
158
+ resources.push(css); // replace root contents and associated properties
159
+
160
+ Object.assign(root, (0, _utils2.createRootResource)(root.url, root.content.replace(/(<\/body>)(?!.*\1)/is, `<link data-percy-specific-css rel="stylesheet" href="${css.pathname}"/>` + '$&')));
161
+ } // include associated snapshot logs matched by meta information
162
+
163
+
164
+ resources.push((0, _utils2.createLogResource)(_logger.default.query(log => {
165
+ var _log$meta$snapshot;
166
+
167
+ return ((_log$meta$snapshot = log.meta.snapshot) === null || _log$meta$snapshot === void 0 ? void 0 : _log$meta$snapshot.name) === snapshot.meta.snapshot.name;
168
+ })));
169
+ return callback(snapshot, resources);
170
+ } // Wait for a page's asset discovery network to idle
171
+
172
+
173
+ function waitForDiscoveryNetworkIdle(page, options) {
174
+ let {
175
+ allowedHostnames,
176
+ networkIdleTimeout
177
+ } = options;
178
+
179
+ let filter = r => (0, _utils2.hostnameMatches)(allowedHostnames, r.url);
180
+
181
+ return page.network.idle(filter, networkIdleTimeout);
182
+ } // Used to cache resources across core instances
183
+
184
+
185
+ const RESOURCE_CACHE_KEY = Symbol('resource-cache'); // Discovers resources for a snapshot using a browser page to intercept requests. The callback
186
+ // function will be called with the snapshot name (for additional snapshots) and an array of
187
+ // discovered resources. When additional snapshots are provided, the callback will be called once
188
+ // for each snapshot.
189
+
190
+ async function* discoverSnapshotResources(percy, snapshot, callback) {
191
+ var _snapshot$enableJavaS;
192
+
193
+ debugSnapshotConfig(snapshot, percy.dryRun); // when dry-running, invoke the callback for each snapshot and immediately return
194
+
195
+ let allSnapshots = [snapshot, ...(snapshot.additionalSnapshots || [])];
196
+ if (percy.dryRun) return allSnapshots.map(s => callback(s)); // keep a global resource cache across snapshots
197
+
198
+ let cache = percy[RESOURCE_CACHE_KEY] || (percy[RESOURCE_CACHE_KEY] = new Map()); // copy widths to prevent mutation later
199
+
200
+ let widths = snapshot.widths.slice(); // preload the root resource for existing dom snapshots
201
+
202
+ let resources = new Map(snapshot.domSnapshot && [(0, _utils2.createRootResource)(snapshot.url, snapshot.domSnapshot)].map(resource => [resource.url, resource])); // open a new browser page
203
+
204
+ let page = yield percy.browser.page({
205
+ enableJavaScript: (_snapshot$enableJavaS = snapshot.enableJavaScript) !== null && _snapshot$enableJavaS !== void 0 ? _snapshot$enableJavaS : !snapshot.domSnapshot,
206
+ networkIdleTimeout: snapshot.discovery.networkIdleTimeout,
207
+ requestHeaders: snapshot.discovery.requestHeaders,
208
+ authorization: snapshot.discovery.authorization,
209
+ userAgent: snapshot.discovery.userAgent,
210
+ meta: snapshot.meta,
211
+ // enable network inteception
212
+ intercept: {
213
+ enableJavaScript: snapshot.enableJavaScript,
214
+ disableCache: snapshot.discovery.disableCache,
215
+ allowedHostnames: snapshot.discovery.allowedHostnames,
216
+ getResource: u => resources.get(u) || cache.get(u),
217
+ saveResource: r => resources.set(r.url, r) && cache.set(r.url, r)
218
+ }
219
+ });
220
+
221
+ try {
222
+ var _snapshot$execute;
223
+
224
+ // set the initial page size
225
+ yield page.resize({
226
+ width: widths.shift(),
227
+ height: snapshot.minHeight
228
+ }); // navigate to the url
229
+
230
+ yield page.goto(snapshot.url);
231
+ yield page.evaluate((_snapshot$execute = snapshot.execute) === null || _snapshot$execute === void 0 ? void 0 : _snapshot$execute.afterNavigation); // trigger resize events for other widths
232
+
233
+ for (let width of widths) {
234
+ var _snapshot$execute2, _snapshot$execute3;
235
+
236
+ yield page.evaluate((_snapshot$execute2 = snapshot.execute) === null || _snapshot$execute2 === void 0 ? void 0 : _snapshot$execute2.beforeResize);
237
+ yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
238
+ yield page.resize({
239
+ width,
240
+ height: snapshot.minHeight
241
+ });
242
+ yield page.evaluate((_snapshot$execute3 = snapshot.execute) === null || _snapshot$execute3 === void 0 ? void 0 : _snapshot$execute3.afterResize);
243
+ }
244
+
245
+ if (snapshot.domSnapshot) {
246
+ // ensure discovery has finished and handle resources
247
+ yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
248
+ handleSnapshotResources(snapshot, resources, callback);
249
+ } else {
250
+ // capture snapshots sequentially
251
+ for (let snap of allSnapshots) {
252
+ // shallow merge snapshot options
253
+ let options = { ...snapshot,
254
+ ...snap
255
+ }; // will wait for timeouts, selectors, and additional network activity
256
+
257
+ let {
258
+ url,
259
+ dom
260
+ } = yield page.snapshot(options); // handle resources and remove previously captured dom snapshots
261
+
262
+ resources.set(url, (0, _utils2.createRootResource)(url, dom));
263
+ handleSnapshotResources(options, resources, callback);
264
+ resources.delete(url);
265
+ }
266
+ } // page clean up
267
+
268
+
269
+ await page.close();
270
+ } catch (error) {
271
+ await page.close();
272
+ throw error;
273
+ }
274
+ }
package/dist/utils.js CHANGED
@@ -3,20 +3,19 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.hostname = hostname;
7
- exports.normalizeURL = normalizeURL;
8
- exports.createResource = createResource;
9
- exports.createRootResource = createRootResource;
10
6
  exports.createLogResource = createLogResource;
11
7
  exports.createPercyCSSResource = createPercyCSSResource;
12
- exports.injectPercyCSS = injectPercyCSS;
13
- exports.waitFor = waitFor;
8
+ exports.createResource = createResource;
9
+ exports.createRootResource = createRootResource;
10
+ exports.hostname = hostname;
14
11
  Object.defineProperty(exports, "hostnameMatches", {
15
12
  enumerable: true,
16
13
  get: function () {
17
14
  return _utils.hostnameMatches;
18
15
  }
19
16
  });
17
+ exports.normalizeURL = normalizeURL;
18
+ exports.waitFor = waitFor;
20
19
 
21
20
  var _utils = require("@percy/client/dist/utils");
22
21
 
@@ -53,33 +52,22 @@ function createRootResource(url, content) {
53
52
  return createResource(normalizeURL(url), content, 'text/html', {
54
53
  root: true
55
54
  });
56
- } // Creates a log resource object.
57
-
58
-
59
- function createLogResource(logs) {
60
- return createResource(`/percy.${Date.now()}.log`, JSON.stringify(logs), 'text/plain');
61
55
  } // Creates a Percy CSS resource object.
62
56
 
63
57
 
64
58
  function createPercyCSSResource(url, css) {
65
- if (css) {
66
- let {
67
- href,
68
- pathname
69
- } = new URL(`/percy-specific.${Date.now()}.css`, url);
70
- return createResource(href, css, 'text/css', {
71
- pathname
72
- });
73
- }
74
- } // returns a new root resource with the injected Percy CSS
59
+ let {
60
+ href,
61
+ pathname
62
+ } = new URL(`/percy-specific.${Date.now()}.css`, url);
63
+ return createResource(href, css, 'text/css', {
64
+ pathname
65
+ });
66
+ } // Creates a log resource object.
75
67
 
76
68
 
77
- function injectPercyCSS(root, percyCSS) {
78
- if (percyCSS) {
79
- return createRootResource(root.url, root.content.replace(/(<\/body>)(?!.*\1)/is, `<link data-percy-specific-css rel="stylesheet" href="${percyCSS.pathname}"/>` + '$&'));
80
- } else {
81
- return root;
82
- }
69
+ function createLogResource(logs) {
70
+ return createResource(`/percy.${Date.now()}.log`, JSON.stringify(logs), 'text/plain');
83
71
  } // Polls for the predicate to be truthy within a timeout or the returned promise rejects. If
84
72
  // the second argument is an options object and `idle` is provided, the predicate will be
85
73
  // checked again after the idle period. This helper is injected as an argument when using
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.0.0-beta.68",
3
+ "version": "1.0.0-beta.71",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -25,10 +25,10 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@percy/client": "1.0.0-beta.68",
29
- "@percy/config": "1.0.0-beta.68",
30
- "@percy/dom": "1.0.0-beta.68",
31
- "@percy/logger": "1.0.0-beta.68",
28
+ "@percy/client": "1.0.0-beta.71",
29
+ "@percy/config": "1.0.0-beta.71",
30
+ "@percy/dom": "1.0.0-beta.71",
31
+ "@percy/logger": "1.0.0-beta.71",
32
32
  "cross-spawn": "^7.0.3",
33
33
  "extract-zip": "^2.0.1",
34
34
  "rimraf": "^3.0.2",
@@ -39,5 +39,5 @@
39
39
  "url": "https://github.com/percy/cli",
40
40
  "directory": "packages/core"
41
41
  },
42
- "gitHead": "32ffd12d1db407b7c2b4ca8364f99f90b2cfa862"
42
+ "gitHead": "364d1df717fb19a26ccb024458df6e78a9c11f99"
43
43
  }
package/types/index.d.ts CHANGED
@@ -44,18 +44,24 @@ export interface SnapshotOptions extends CommonSnapshotOptions {
44
44
  discovery?: DiscoveryOptions;
45
45
  }
46
46
 
47
- export type PercyOptions<C = Pojo> = C & {
48
- token?: string,
47
+ type ClientEnvInfo = {
49
48
  clientInfo?: string,
50
- environmentInfo?: string,
49
+ environmentInfo?: string
50
+ }
51
+
52
+ export type PercyConfigOptions<C = Pojo> = C & {
53
+ snapshot?: CommonSnapshotOptions,
54
+ discovery?: AllDiscoveryOptions
55
+ }
56
+
57
+ export type PercyOptions<C = Pojo> = {
58
+ token?: string,
51
59
  server?: boolean,
52
60
  port?: number,
53
61
  concurrency?: number,
54
62
  loglevel?: LogLevel,
55
- config?: undefined | string | false,
56
- snapshot?: CommonSnapshotOptions,
57
- discovery?: AllDiscoveryOptions
58
- };
63
+ config?: undefined | string | false
64
+ } & ClientEnvInfo & PercyConfigOptions<C>;
59
65
 
60
66
  type SnapshotExec = () => void | Promise<void>;
61
67
 
@@ -70,6 +76,8 @@ declare class Percy {
70
76
  constructor(options?: PercyOptions);
71
77
  loglevel(): LogLevel;
72
78
  loglevel(level: LogLevel): void;
79
+ config: PercyConfigOptions;
80
+ setConfig(config: ClientEnvInfo & PercyConfigOptions): PercyConfigOptions;
73
81
  start(): Promise<void>;
74
82
  stop(force?: boolean): Promise<void>;
75
83
  idle(): Promise<void>;