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