@percy/core 1.12.0 → 1.13.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/network.js CHANGED
@@ -3,11 +3,11 @@ import logger from '@percy/logger';
3
3
  import { request as makeRequest } from '@percy/client/utils';
4
4
  import { normalizeURL, hostnameMatches, createResource, waitFor } from './utils.js';
5
5
  const MAX_RESOURCE_SIZE = 15 * 1024 ** 2; // 15MB
6
-
7
6
  const ALLOWED_STATUSES = [200, 201, 301, 302, 304, 307, 308];
8
- const ALLOWED_RESOURCES = ['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Other']; // The Interceptor class creates common handlers for dealing with intercepting asset requests
9
- // for a given page using various devtools protocol events and commands.
7
+ const ALLOWED_RESOURCES = ['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Other'];
10
8
 
9
+ // The Interceptor class creates common handlers for dealing with intercepting asset requests
10
+ // for a given page using various devtools protocol events and commands.
11
11
  export class Network {
12
12
  static TIMEOUT = 30000;
13
13
  log = logger('core:discovery');
@@ -15,18 +15,17 @@ export class Network {
15
15
  #requests = new Map();
16
16
  #intercepts = new Map();
17
17
  #authentications = new Set();
18
-
19
18
  constructor(page, options) {
20
19
  this.page = page;
21
20
  this.timeout = options.networkIdleTimeout ?? 100;
22
21
  this.authorization = options.authorization;
23
22
  this.requestHeaders = options.requestHeaders ?? {};
24
- this.userAgent = options.userAgent ?? // by default, emulate a non-headless browser
23
+ this.userAgent = options.userAgent ??
24
+ // by default, emulate a non-headless browser
25
25
  page.session.browser.version.userAgent.replace('Headless', '');
26
26
  this.intercept = options.intercept;
27
27
  this.meta = options.meta;
28
28
  }
29
-
30
29
  watch(session) {
31
30
  session.on('Network.requestWillBeSent', this._handleRequestWillBeSent);
32
31
  session.on('Network.responseReceived', this._handleResponseReceived.bind(this, session));
@@ -42,7 +41,6 @@ export class Network {
42
41
  }), session.send('Network.setExtraHTTPHeaders', {
43
42
  headers: this.requestHeaders
44
43
  })];
45
-
46
44
  if (this.intercept && session.isDocument) {
47
45
  session.on('Fetch.requestPaused', this._handleRequestPaused.bind(this, session));
48
46
  session.on('Fetch.authRequired', this._handleAuthRequired.bind(this, session));
@@ -53,11 +51,10 @@ export class Network {
53
51
  }]
54
52
  }));
55
53
  }
56
-
57
54
  return Promise.all(commands);
58
- } // Resolves after the timeout when there are no more in-flight requests.
59
-
55
+ }
60
56
 
57
+ // Resolves after the timeout when there are no more in-flight requests.
61
58
  async idle(filter = () => true, timeout = this.timeout) {
62
59
  let requests = [];
63
60
  this.log.debug(`Wait for ${timeout}ms idle`, this.meta);
@@ -65,7 +62,6 @@ export class Network {
65
62
  if (this.page.session.closedReason) {
66
63
  throw new Error(`Network error: ${this.page.session.closedReason}`);
67
64
  }
68
-
69
65
  requests = Array.from(this.#requests.values()).filter(filter);
70
66
  return requests.length === 0;
71
67
  }, {
@@ -78,34 +74,32 @@ export class Network {
78
74
  throw error;
79
75
  }
80
76
  });
81
- } // Throw a better network timeout error
82
-
77
+ }
83
78
 
79
+ // Throw a better network timeout error
84
80
  _throwTimeoutError(msg, filter = () => true) {
85
81
  if (this.log.shouldLog('debug')) {
86
82
  let reqs = Array.from(this.#requests.values()).filter(filter).map(r => r.url);
87
83
  msg += `\n\n ${['Active requests:', ...reqs].join('\n - ')}\n`;
88
84
  }
89
-
90
85
  throw new Error(msg);
91
- } // Called when a request should be removed from various trackers
92
-
86
+ }
93
87
 
88
+ // Called when a request should be removed from various trackers
94
89
  _forgetRequest({
95
90
  requestId,
96
91
  interceptId
97
92
  }, keepPending) {
98
93
  this.#requests.delete(requestId);
99
94
  this.#authentications.delete(interceptId);
100
-
101
95
  if (!keepPending) {
102
96
  this.#pending.delete(requestId);
103
97
  this.#intercepts.delete(requestId);
104
98
  }
105
- } // Called when a request requires authentication. Responds to the auth request with any
106
- // provided authorization credentials.
107
-
99
+ }
108
100
 
101
+ // Called when a request requires authentication. Responds to the auth request with any
102
+ // provided authorization credentials.
109
103
  _handleAuthRequired = async (session, event) => {
110
104
  let {
111
105
  username,
@@ -115,14 +109,12 @@ export class Network {
115
109
  requestId
116
110
  } = event;
117
111
  let response = 'Default';
118
-
119
112
  if (this.#authentications.has(requestId)) {
120
113
  response = 'CancelAuth';
121
114
  } else if (username || password) {
122
115
  response = 'ProvideCredentials';
123
116
  this.#authentications.add(requestId);
124
117
  }
125
-
126
118
  await session.send('Fetch.continueWithAuth', {
127
119
  requestId: event.requestId,
128
120
  authChallengeResponse: {
@@ -131,9 +123,10 @@ export class Network {
131
123
  password
132
124
  }
133
125
  });
134
- }; // Called when a request is made. The request is paused until it is fulfilled, continued, or
135
- // aborted. If the request is already pending, handle it; otherwise set it to be intercepted.
126
+ };
136
127
 
128
+ // Called when a request is made. The request is paused until it is fulfilled, continued, or
129
+ // aborted. If the request is already pending, handle it; otherwise set it to be intercepted.
137
130
  _handleRequestPaused = async (session, event) => {
138
131
  let {
139
132
  networkId: requestId,
@@ -141,34 +134,37 @@ export class Network {
141
134
  resourceType
142
135
  } = event;
143
136
  let pending = this.#pending.get(requestId);
144
- this.#pending.delete(requestId); // guard against redirects with the same requestId
137
+ this.#pending.delete(requestId);
145
138
 
139
+ // guard against redirects with the same requestId
146
140
  if ((pending === null || pending === void 0 ? void 0 : pending.request.url) === event.request.url && pending.request.method === event.request.method) {
147
- await this._handleRequest(session, { ...pending,
141
+ await this._handleRequest(session, {
142
+ ...pending,
148
143
  resourceType,
149
144
  interceptId
150
145
  });
151
146
  } else {
152
147
  // track the session that intercepted the request
153
- this.#intercepts.set(requestId, { ...event,
148
+ this.#intercepts.set(requestId, {
149
+ ...event,
154
150
  session
155
151
  });
156
152
  }
157
- }; // Called when a request will be sent. If the request has already been intercepted, handle it;
158
- // otherwise set it to be pending until it is paused.
153
+ };
159
154
 
155
+ // Called when a request will be sent. If the request has already been intercepted, handle it;
156
+ // otherwise set it to be pending until it is paused.
160
157
  _handleRequestWillBeSent = async event => {
161
158
  let {
162
159
  requestId,
163
160
  request
164
- } = event; // do not handle data urls
161
+ } = event;
165
162
 
163
+ // do not handle data urls
166
164
  if (request.url.startsWith('data:')) return;
167
-
168
165
  if (this.intercept) {
169
166
  let intercept = this.#intercepts.get(requestId);
170
167
  this.#pending.set(requestId, event);
171
-
172
168
  if (intercept) {
173
169
  // handle the request with the session that intercepted it
174
170
  let {
@@ -176,17 +172,19 @@ export class Network {
176
172
  requestId: interceptId,
177
173
  resourceType
178
174
  } = intercept;
179
- await this._handleRequest(session, { ...event,
175
+ await this._handleRequest(session, {
176
+ ...event,
180
177
  resourceType,
181
178
  interceptId
182
179
  });
183
180
  this.#intercepts.delete(requestId);
184
181
  }
185
182
  }
186
- }; // Called when a pending request is paused. Handles associating redirected requests with
183
+ };
184
+
185
+ // Called when a pending request is paused. Handles associating redirected requests with
187
186
  // responses and calls this.onrequest with request info and callbacks to continue, respond,
188
187
  // or abort a request. One of the callbacks is required to be called and only one.
189
-
190
188
  _handleRequest = async (session, event) => {
191
189
  let {
192
190
  request,
@@ -194,24 +192,25 @@ export class Network {
194
192
  interceptId,
195
193
  resourceType
196
194
  } = event;
197
- let redirectChain = []; // if handling a redirected request, associate the response and add to its redirect chain
195
+ let redirectChain = [];
198
196
 
197
+ // if handling a redirected request, associate the response and add to its redirect chain
199
198
  if (event.redirectResponse && this.#requests.has(requestId)) {
200
199
  let req = this.#requests.get(requestId);
201
- redirectChain = [...req.redirectChain, req]; // clean up interim requests
202
-
200
+ redirectChain = [...req.redirectChain, req];
201
+ // clean up interim requests
203
202
  this._forgetRequest(req, true);
204
203
  }
205
-
206
204
  request.type = resourceType;
207
205
  request.requestId = requestId;
208
206
  request.interceptId = interceptId;
209
207
  request.redirectChain = redirectChain;
210
208
  this.#requests.set(requestId, request);
211
209
  await sendResponseResource(this, request, session);
212
- }; // Called when a response has been received for a specific request. Associates the response with
213
- // the request data and adds a buffer method to fetch the response body when needed.
210
+ };
214
211
 
212
+ // Called when a response has been received for a specific request. Associates the response with
213
+ // the request data and adds a buffer method to fetch the response body when needed.
215
214
  _handleResponseReceived = (session, event) => {
216
215
  let {
217
216
  requestId,
@@ -219,59 +218,58 @@ export class Network {
219
218
  } = event;
220
219
  let request = this.#requests.get(requestId);
221
220
  /* istanbul ignore if: race condition paranioa */
222
-
223
221
  if (!request) return;
224
222
  request.response = response;
225
-
226
223
  request.response.buffer = async () => {
227
224
  let result = await session.send('Network.getResponseBody', {
228
225
  requestId
229
226
  });
230
227
  return Buffer.from(result.body, result.base64Encoded ? 'base64' : 'utf-8');
231
228
  };
232
- }; // Called when a request streams events. These types of requests break asset discovery because
233
- // they never finish loading, so we untrack them to signal idle after the first event.
229
+ };
234
230
 
231
+ // Called when a request streams events. These types of requests break asset discovery because
232
+ // they never finish loading, so we untrack them to signal idle after the first event.
235
233
  _handleEventSourceMessageReceived = event => {
236
234
  let request = this.#requests.get(event.requestId);
237
235
  /* istanbul ignore else: race condition paranioa */
238
-
239
236
  if (request) this._forgetRequest(request);
240
- }; // Called when a request has finished loading which triggers the this.onrequestfinished
241
- // callback. The request should have an associated response and be finished with any redirects.
237
+ };
242
238
 
239
+ // Called when a request has finished loading which triggers the this.onrequestfinished
240
+ // callback. The request should have an associated response and be finished with any redirects.
243
241
  _handleLoadingFinished = async event => {
244
242
  let request = this.#requests.get(event.requestId);
245
243
  /* istanbul ignore if: race condition paranioa */
246
-
247
244
  if (!request) return;
248
245
  await saveResponseResource(this, request);
249
-
250
246
  this._forgetRequest(request);
251
- }; // Called when a request has failed loading and triggers the this.onrequestfailed callback.
247
+ };
252
248
 
249
+ // Called when a request has failed loading and triggers the this.onrequestfailed callback.
253
250
  _handleLoadingFailed = event => {
254
251
  let request = this.#requests.get(event.requestId);
255
252
  /* istanbul ignore if: race condition paranioa */
253
+ if (!request) return;
256
254
 
257
- if (!request) return; // do not log generic messages since the real error was likely logged elsewhere
258
-
255
+ // do not log generic messages since the real error was likely logged elsewhere
259
256
  if (event.errorText !== 'net::ERR_FAILED') {
260
257
  let message = `Request failed for ${request.url}: ${event.errorText}`;
261
- this.log.debug(message, { ...this.meta,
258
+ this.log.debug(message, {
259
+ ...this.meta,
262
260
  url: request.url
263
261
  });
264
262
  }
265
-
266
263
  this._forgetRequest(request);
267
264
  };
268
- } // Returns the normalized origin URL of a request
265
+ }
269
266
 
267
+ // Returns the normalized origin URL of a request
270
268
  function originURL(request) {
271
269
  return normalizeURL((request.redirectChain[0] || request).url);
272
- } // Send a response for a given request, responding with cached resources when able
273
-
270
+ }
274
271
 
272
+ // Send a response for a given request, responding with cached resources when able
275
273
  async function sendResponseResource(network, request, session) {
276
274
  let {
277
275
  disallowedHostnames,
@@ -279,14 +277,13 @@ async function sendResponseResource(network, request, session) {
279
277
  } = network.intercept;
280
278
  let log = network.log;
281
279
  let url = originURL(request);
282
- let meta = { ...network.meta,
280
+ let meta = {
281
+ ...network.meta,
283
282
  url
284
283
  };
285
-
286
284
  try {
287
285
  let resource = network.intercept.getResource(url);
288
286
  network.log.debug(`Handling request: ${url}`, meta);
289
-
290
287
  if (!(resource !== null && resource !== void 0 && resource.root) && hostnameMatches(disallowedHostnames, url)) {
291
288
  log.debug('- Skipping disallowed hostname', meta);
292
289
  await session.send('Fetch.failRequest', {
@@ -314,22 +311,21 @@ async function sendResponseResource(network, request, session) {
314
311
  if (session.closing && error.message.includes('close')) return;
315
312
  log.debug(`Encountered an error handling request: ${url}`, meta);
316
313
  log.debug(error);
317
- /* istanbul ignore next: catch race condition */
318
314
 
315
+ /* istanbul ignore next: catch race condition */
319
316
  await session.send('Fetch.failRequest', {
320
317
  requestId: request.interceptId,
321
318
  errorReason: 'Failed'
322
319
  }).catch(e => log.debug(e, meta));
323
320
  }
324
- } // Make a new request with Node based on a network request
325
-
321
+ }
326
322
 
323
+ // Make a new request with Node based on a network request
327
324
  function makeDirectRequest(network, request) {
328
325
  var _network$authorizatio;
329
-
330
- let headers = { ...request.headers
326
+ let headers = {
327
+ ...request.headers
331
328
  };
332
-
333
329
  if ((_network$authorizatio = network.authorization) !== null && _network$authorizatio !== void 0 && _network$authorizatio.username) {
334
330
  // include basic authorization username and password
335
331
  let {
@@ -339,14 +335,13 @@ function makeDirectRequest(network, request) {
339
335
  let token = Buffer.from([username, password || ''].join(':')).toString('base64');
340
336
  headers.Authorization = `Basic ${token}`;
341
337
  }
342
-
343
338
  return makeRequest(request.url, {
344
339
  buffer: true,
345
340
  headers
346
341
  });
347
- } // Save a resource from a request, skipping it if specific paramters are not met
348
-
342
+ }
349
343
 
344
+ // Save a resource from a request, skipping it if specific paramters are not met
350
345
  async function saveResponseResource(network, request) {
351
346
  let {
352
347
  disableCache,
@@ -356,18 +351,18 @@ async function saveResponseResource(network, request) {
356
351
  let log = network.log;
357
352
  let url = originURL(request);
358
353
  let response = request.response;
359
- let meta = { ...network.meta,
354
+ let meta = {
355
+ ...network.meta,
360
356
  url
361
357
  };
362
358
  let resource = network.intercept.getResource(url);
363
-
364
359
  if (!resource || !resource.root && disableCache) {
365
360
  try {
366
361
  log.debug(`Processing resource: ${url}`, meta);
367
362
  let shouldCapture = response && hostnameMatches(allowedHostnames, url);
368
363
  let body = shouldCapture && (await response.buffer());
369
- /* istanbul ignore if: first check is a sanity check */
370
364
 
365
+ /* istanbul ignore if: first check is a sanity check */
371
366
  if (!response) {
372
367
  return log.debug('- Skipping no response', meta);
373
368
  } else if (!shouldCapture) {
@@ -381,15 +376,15 @@ async function saveResponseResource(network, request) {
381
376
  } else if (!enableJavaScript && !ALLOWED_RESOURCES.includes(request.type)) {
382
377
  return log.debug(`- Skipping disallowed resource type [${request.type}]`, meta);
383
378
  }
379
+ let mimeType =
380
+ // ensure the mimetype is correct for text/plain responses
381
+ response.mimeType === 'text/plain' && mime.lookup(response.url) || response.mimeType;
384
382
 
385
- let mimeType = // ensure the mimetype is correct for text/plain responses
386
- response.mimeType === 'text/plain' && mime.lookup(response.url) || response.mimeType; // font responses from the browser may not be properly encoded, so request them directly
387
-
383
+ // font responses from the browser may not be properly encoded, so request them directly
388
384
  if (mimeType !== null && mimeType !== void 0 && mimeType.includes('font')) {
389
385
  log.debug('- Requesting asset directly');
390
386
  body = await makeDirectRequest(network, request);
391
387
  }
392
-
393
388
  resource = createResource(url, body, mimeType, {
394
389
  status: response.status,
395
390
  // 'Network.responseReceived' returns headers split by newlines, however
@@ -405,10 +400,8 @@ async function saveResponseResource(network, request) {
405
400
  log.debug(error);
406
401
  }
407
402
  }
408
-
409
403
  if (resource) {
410
404
  network.intercept.saveResource(resource);
411
405
  }
412
406
  }
413
-
414
407
  export default Network;
package/dist/page.js CHANGED
@@ -6,7 +6,6 @@ import { hostname, waitFor, waitForTimeout as sleep, serializeFunction } from '.
6
6
  export class Page {
7
7
  static TIMEOUT = 30000;
8
8
  log = logger('core:page');
9
-
10
9
  constructor(session, options) {
11
10
  this.session = session;
12
11
  this.browser = session.browser;
@@ -18,15 +17,15 @@ export class Page {
18
17
  session.on('Runtime.executionContextsCleared', this._handleExecutionContextsCleared);
19
18
  session.send('Runtime.enable').catch(session._handleClosedError);
20
19
  this.log.debug('Page created');
21
- } // Close the page
22
-
20
+ }
23
21
 
22
+ // Close the page
24
23
  async close() {
25
24
  await this.session.close();
26
25
  this.log.debug('Page closed', this.meta);
27
- } // Resize the page to the specified width and height
28
-
26
+ }
29
27
 
28
+ // Resize the page to the specified width and height
30
29
  async resize({
31
30
  width,
32
31
  height,
@@ -40,45 +39,42 @@ export class Page {
40
39
  height,
41
40
  width
42
41
  });
43
- } // Go to a URL and wait for navigation to occur
44
-
42
+ }
45
43
 
44
+ // Go to a URL and wait for navigation to occur
46
45
  async goto(url, {
47
46
  waitUntil = 'load'
48
47
  } = {}) {
49
48
  this.log.debug(`Navigate to: ${url}`, this.meta);
50
-
51
49
  let navigate = async () => {
52
50
  // set cookies before navigation so we can default the domain to this hostname
53
51
  if (this.session.browser.cookies.length) {
54
52
  let defaultDomain = hostname(url);
55
53
  await this.session.send('Network.setCookies', {
56
54
  // spread is used to make a shallow copy of the cookie
57
- cookies: this.session.browser.cookies.map(({ ...cookie
55
+ cookies: this.session.browser.cookies.map(({
56
+ ...cookie
58
57
  }) => {
59
58
  if (!cookie.url) cookie.domain || (cookie.domain = defaultDomain);
60
59
  return cookie;
61
60
  })
62
61
  });
63
- } // handle navigation errors
64
-
62
+ }
65
63
 
64
+ // handle navigation errors
66
65
  let res = await this.session.send('Page.navigate', {
67
66
  url
68
67
  });
69
68
  if (res.errorText) throw new Error(res.errorText);
70
69
  };
71
-
72
- let handlers = [// wait until navigation and the correct lifecycle
70
+ let handlers = [
71
+ // wait until navigation and the correct lifecycle
73
72
  ['Page.frameNavigated', e => this.session.targetId === e.frame.id], ['Page.lifecycleEvent', e => this.session.targetId === e.frameId && e.name === waitUntil]].map(([name, cond]) => {
74
73
  let handler = e => cond(e) && (handler.finished = true) && handler.off();
75
-
76
74
  handler.off = () => this.session.off(name, handler);
77
-
78
75
  this.session.on(name, handler);
79
76
  return handler;
80
77
  });
81
-
82
78
  try {
83
79
  // trigger navigation and poll for handlers to have finished
84
80
  await Promise.all([navigate(), waitFor(() => {
@@ -87,23 +83,22 @@ export class Page {
87
83
  }, Page.TIMEOUT)]);
88
84
  } catch (error) {
89
85
  // remove any unused handlers
90
- for (let handler of handlers) handler.off(); // assign context to unknown errors
91
-
86
+ for (let handler of handlers) handler.off();
92
87
 
88
+ // assign context to unknown errors
93
89
  if (!error.message.startsWith('Timeout')) {
94
90
  throw Object.assign(error, {
95
91
  message: `Navigation failed: ${error.message}`
96
92
  });
97
- } // throw a network error to show active requests
98
-
93
+ }
99
94
 
95
+ // throw a network error to show active requests
100
96
  this.network._throwTimeoutError(`Navigation failed: Timed out waiting for the page ${waitUntil} event`);
101
97
  }
102
-
103
98
  this.log.debug('Page navigated', this.meta);
104
- } // Evaluate JS functions within the page's execution context
105
-
99
+ }
106
100
 
101
+ // Evaluate JS functions within the page's execution context
107
102
  async eval(fn, ...args) {
108
103
  let {
109
104
  result,
@@ -118,29 +113,27 @@ export class Page {
118
113
  awaitPromise: true,
119
114
  userGesture: true
120
115
  });
121
-
122
116
  if (exceptionDetails) {
123
117
  throw exceptionDetails.exception.description;
124
118
  } else {
125
119
  return result.value;
126
120
  }
127
- } // Evaluate one or more scripts in succession
128
-
121
+ }
129
122
 
123
+ // Evaluate one or more scripts in succession
130
124
  async evaluate(scripts) {
131
125
  var _scripts;
132
-
133
126
  if (!((_scripts = scripts && (scripts = [].concat(scripts))) !== null && _scripts !== void 0 && _scripts.length)) return;
134
- this.log.debug('Evaluate JavaScript', { ...this.meta,
127
+ this.log.debug('Evaluate JavaScript', {
128
+ ...this.meta,
135
129
  scripts
136
130
  });
137
-
138
131
  for (let script of scripts) await this.eval(script);
139
- } // Takes a snapshot after waiting for any timeout, waiting for any selector, executing any
132
+ }
133
+
134
+ // Takes a snapshot after waiting for any timeout, waiting for any selector, executing any
140
135
  // scripts, and waiting for the network idle. Returns all other provided snapshot options along
141
136
  // with the captured URL and DOM snapshot.
142
-
143
-
144
137
  async snapshot({
145
138
  waitForTimeout,
146
139
  waitForSelector,
@@ -152,42 +145,42 @@ export class Page {
152
145
  width,
153
146
  enableJavaScript
154
147
  } = snapshot;
155
- this.log.debug(`Taking snapshot: ${name}${width ? ` @${width}px` : ''}`, this.meta); // wait for any specified timeout
148
+ this.log.debug(`Taking snapshot: ${name}${width ? ` @${width}px` : ''}`, this.meta);
156
149
 
150
+ // wait for any specified timeout
157
151
  if (waitForTimeout) {
158
152
  this.log.debug(`Wait for ${waitForTimeout}ms timeout`, this.meta);
159
153
  await sleep(waitForTimeout);
160
- } // wait for any specified selector
161
-
154
+ }
162
155
 
156
+ // wait for any specified selector
163
157
  if (waitForSelector) {
164
158
  this.log.debug(`Wait for selector: ${waitForSelector}`, this.meta);
165
159
  await this.eval(`await waitForSelector(${JSON.stringify(waitForSelector)}, ${Page.TIMEOUT})`);
166
- } // execute any javascript
167
-
160
+ }
168
161
 
162
+ // execute any javascript
169
163
  if (execute) {
170
164
  let execBefore = typeof execute === 'object' && !Array.isArray(execute);
171
165
  await this.evaluate(execBefore ? execute.beforeSnapshot : execute);
172
- } // wait for any final network activity before capturing the dom snapshot
166
+ }
173
167
 
168
+ // wait for any final network activity before capturing the dom snapshot
169
+ await this.network.idle();
174
170
 
175
- await this.network.idle(); // inject @percy/dom for serialization by evaluating the file contents which adds a global
171
+ // inject @percy/dom for serialization by evaluating the file contents which adds a global
176
172
  // PercyDOM object that we can later check against
177
-
178
173
  /* istanbul ignore next: no instrumenting injected code */
179
-
180
174
  if (await this.eval(() => !window.PercyDOM)) {
181
175
  this.log.debug('Inject @percy/dom', this.meta);
182
176
  let script = await fs.promises.readFile(PERCY_DOM, 'utf-8');
183
- await this.eval(new Function(script));
184
- /* eslint-disable-line no-new-func */
185
- } // serialize and capture a DOM snapshot
186
-
177
+ await this.eval(new Function(script)); /* eslint-disable-line no-new-func */
178
+ }
187
179
 
180
+ // serialize and capture a DOM snapshot
188
181
  this.log.debug('Serialize DOM', this.meta);
189
- /* istanbul ignore next: no instrumenting injected code */
190
182
 
183
+ /* istanbul ignore next: no instrumenting injected code */
191
184
  let capture = await this.eval((_, options) => ({
192
185
  /* eslint-disable-next-line no-undef */
193
186
  domSnapshot: PercyDOM.serialize(options),
@@ -195,19 +188,18 @@ export class Page {
195
188
  }), {
196
189
  enableJavaScript
197
190
  });
198
- return { ...snapshot,
191
+ return {
192
+ ...snapshot,
199
193
  ...capture
200
194
  };
201
- } // Initialize newly attached pages and iframes with page options
202
-
195
+ }
203
196
 
197
+ // Initialize newly attached pages and iframes with page options
204
198
  _handleAttachedToTarget = event => {
205
199
  let session = !event ? this.session : this.session.children.get(event.sessionId);
206
200
  /* istanbul ignore if: sanity check */
207
-
208
201
  if (!session) return;
209
202
  let commands = [this.network.watch(session)];
210
-
211
203
  if (session.isDocument) {
212
204
  session.on('Target.attachedToTarget', this._handleAttachedToTarget);
213
205
  commands.push(session.send('Page.enable'), session.send('Page.setLifecycleEventsEnabled', {
@@ -222,14 +214,15 @@ export class Page {
222
214
  flatten: true
223
215
  }));
224
216
  }
225
-
226
217
  return Promise.all(commands).catch(session._handleClosedError);
227
- }; // Keep track of the page's execution context id
218
+ };
228
219
 
220
+ // Keep track of the page's execution context id
229
221
  _handleExecutionContextCreated = event => {
230
222
  if (this.session.targetId === event.context.auxData.frameId) {
231
- this.contextId = event.context.id; // inject global percy config as soon as possible
223
+ this.contextId = event.context.id;
232
224
 
225
+ // inject global percy config as soon as possible
233
226
  this.eval(`window.__PERCY__ = ${JSON.stringify({
234
227
  config: this.browser.percy.config
235
228
  })};`).catch(this.session._handleClosedError);