@percy/core 1.27.5-beta.0 → 1.27.5-beta.2

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/config.js CHANGED
@@ -176,6 +176,10 @@ export const configSchema = {
176
176
  disableCache: {
177
177
  type: 'boolean'
178
178
  },
179
+ captureMockedServiceWorker: {
180
+ type: 'boolean',
181
+ default: false
182
+ },
179
183
  requestHeaders: {
180
184
  type: 'object',
181
185
  normalize: false,
@@ -311,6 +315,9 @@ export const snapshotSchema = {
311
315
  disableCache: {
312
316
  $ref: '/config/discovery#/properties/disableCache'
313
317
  },
318
+ captureMockedServiceWorker: {
319
+ $ref: '/config/discovery#/properties/captureMockedServiceWorker'
320
+ },
314
321
  userAgent: {
315
322
  $ref: '/config/discovery#/properties/userAgent'
316
323
  },
package/dist/discovery.js CHANGED
@@ -42,6 +42,7 @@ function debugSnapshotOptions(snapshot) {
42
42
  debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
43
43
  debugProp(snapshot, 'discovery.authorization', JSON.stringify);
44
44
  debugProp(snapshot, 'discovery.disableCache');
45
+ debugProp(snapshot, 'discovery.captureMockedServiceWorker');
45
46
  debugProp(snapshot, 'discovery.userAgent');
46
47
  debugProp(snapshot, 'clientInfo');
47
48
  debugProp(snapshot, 'environmentInfo');
@@ -279,7 +280,7 @@ export function createDiscoveryQueue(percy) {
279
280
  let {
280
281
  concurrency
281
282
  } = percy.config.discovery;
282
- let queue = new Queue();
283
+ let queue = new Queue('discovery');
283
284
  let cache;
284
285
  return queue.set({
285
286
  concurrency
@@ -322,6 +323,7 @@ export function createDiscoveryQueue(percy) {
322
323
  requestHeaders: snapshot.discovery.requestHeaders,
323
324
  authorization: snapshot.discovery.authorization,
324
325
  userAgent: snapshot.discovery.userAgent,
326
+ captureMockedServiceWorker: snapshot.discovery.captureMockedServiceWorker,
325
327
  meta: snapshot.meta,
326
328
  // enable network inteception
327
329
  intercept: {
package/dist/network.js CHANGED
@@ -1,20 +1,31 @@
1
1
  import { request as makeRequest } from '@percy/client/utils';
2
2
  import logger from '@percy/logger';
3
3
  import mime from 'mime-types';
4
- import { createResource, hostnameMatches, normalizeURL, waitFor } from './utils.js';
4
+ import { DefaultMap, createResource, hostnameMatches, normalizeURL, waitFor } from './utils.js';
5
5
  const MAX_RESOURCE_SIZE = 25 * 1024 ** 2; // 25MB
6
6
  const ALLOWED_STATUSES = [200, 201, 301, 302, 304, 307, 308];
7
7
  const ALLOWED_RESOURCES = ['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Other'];
8
8
  const ABORTED_MESSAGE = 'Request was aborted by browser';
9
9
 
10
+ // RequestLifeCycleHandler handles life cycle of a requestId
11
+ // Ideal flow: requestWillBeSent -> requestPaused -> responseReceived -> loadingFinished / loadingFailed
12
+ // ServiceWorker flow: requestWillBeSent -> responseReceived -> loadingFinished / loadingFailed
13
+ class RequestLifeCycleHandler {
14
+ constructor() {
15
+ this.resolveRequestWillBeSent = null;
16
+ this.resolveResponseReceived = null;
17
+ this.requestWillBeSent = new Promise(resolve => this.resolveRequestWillBeSent = resolve);
18
+ this.responseReceived = new Promise(resolve => this.resolveResponseReceived = resolve);
19
+ }
20
+ }
10
21
  // The Interceptor class creates common handlers for dealing with intercepting asset requests
11
22
  // for a given page using various devtools protocol events and commands.
12
23
  export class Network {
13
24
  static TIMEOUT = undefined;
14
25
  log = logger('core:discovery');
26
+ #requestsLifeCycleHandler = new DefaultMap(() => new RequestLifeCycleHandler());
15
27
  #pending = new Map();
16
28
  #requests = new Map();
17
- #intercepts = new Map();
18
29
  #authentications = new Set();
19
30
  #aborted = new Set();
20
31
  constructor(page, options) {
@@ -22,6 +33,7 @@ export class Network {
22
33
  this.timeout = options.networkIdleTimeout ?? 100;
23
34
  this.authorization = options.authorization;
24
35
  this.requestHeaders = options.requestHeaders ?? {};
36
+ this.captureMockedServiceWorker = options.captureMockedServiceWorker ?? false;
25
37
  this.userAgent = options.userAgent ??
26
38
  // by default, emulate a non-headless browser
27
39
  page.session.browser.version.userAgent.replace('Headless', '');
@@ -36,7 +48,7 @@ export class Network {
36
48
  session.on('Network.loadingFinished', this._handleLoadingFinished);
37
49
  session.on('Network.loadingFailed', this._handleLoadingFailed);
38
50
  let commands = [session.send('Network.enable'), session.send('Network.setBypassServiceWorker', {
39
- bypass: true
51
+ bypass: !this.captureMockedServiceWorker
40
52
  }), session.send('Network.setCacheDisabled', {
41
53
  cacheDisabled: true
42
54
  }), session.send('Network.setUserAgentOverride', {
@@ -114,7 +126,6 @@ export class Network {
114
126
  this.#authentications.delete(interceptId);
115
127
  if (!keepPending) {
116
128
  this.#pending.delete(requestId);
117
- this.#intercepts.delete(requestId);
118
129
  }
119
130
  }
120
131
 
@@ -153,23 +164,18 @@ export class Network {
153
164
  requestId: interceptId,
154
165
  resourceType
155
166
  } = event;
167
+
168
+ // wait for request to be sent
169
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
156
170
  let pending = this.#pending.get(requestId);
157
171
  this.#pending.delete(requestId);
158
172
 
159
173
  // guard against redirects with the same requestId
160
- if ((pending === null || pending === void 0 ? void 0 : pending.request.url) === event.request.url && pending.request.method === event.request.method) {
161
- await this._handleRequest(session, {
162
- ...pending,
163
- resourceType,
164
- interceptId
165
- });
166
- } else {
167
- // track the session that intercepted the request
168
- this.#intercepts.set(requestId, {
169
- ...event,
170
- session
171
- });
172
- }
174
+ (pending === null || pending === void 0 ? void 0 : pending.request.url) === event.request.url && pending.request.method === event.request.method && (await this._handleRequest(session, {
175
+ ...pending,
176
+ resourceType,
177
+ interceptId
178
+ }));
173
179
  };
174
180
 
175
181
  // Called when a request will be sent. If the request has already been intercepted, handle it;
@@ -177,35 +183,32 @@ export class Network {
177
183
  _handleRequestWillBeSent = async event => {
178
184
  let {
179
185
  requestId,
180
- request
186
+ request,
187
+ type
181
188
  } = event;
182
189
 
183
190
  // do not handle data urls
184
191
  if (request.url.startsWith('data:')) return;
185
192
  if (this.intercept) {
186
- let intercept = this.#intercepts.get(requestId);
187
193
  this.#pending.set(requestId, event);
188
- if (intercept) {
189
- // handle the request with the session that intercepted it
190
- let {
191
- session,
192
- requestId: interceptId,
193
- resourceType
194
- } = intercept;
195
- await this._handleRequest(session, {
194
+ if (this.captureMockedServiceWorker) {
195
+ await this._handleRequest(undefined, {
196
196
  ...event,
197
- resourceType,
198
- interceptId
199
- });
200
- this.#intercepts.delete(requestId);
197
+ resourceType: type,
198
+ interceptId: requestId
199
+ }, true);
201
200
  }
202
201
  }
202
+ // release request
203
+ // note: we are releasing this, even if intercept is not set for network.js
204
+ // since, we want to process all-requests in-order doesn't matter if it should be intercepted or not
205
+ this.#requestsLifeCycleHandler.get(requestId).resolveRequestWillBeSent();
203
206
  };
204
207
 
205
208
  // Called when a pending request is paused. Handles associating redirected requests with
206
209
  // responses and calls this.onrequest with request info and callbacks to continue, respond,
207
210
  // or abort a request. One of the callbacks is required to be called and only one.
208
- _handleRequest = async (session, event) => {
211
+ _handleRequest = async (session, event, serviceWorker = false) => {
209
212
  let {
210
213
  request,
211
214
  requestId,
@@ -226,16 +229,21 @@ export class Network {
226
229
  request.interceptId = interceptId;
227
230
  request.redirectChain = redirectChain;
228
231
  this.#requests.set(requestId, request);
229
- await sendResponseResource(this, request, session);
232
+ if (!serviceWorker) {
233
+ await sendResponseResource(this, request, session);
234
+ }
230
235
  };
231
236
 
232
237
  // Called when a response has been received for a specific request. Associates the response with
233
238
  // the request data and adds a buffer method to fetch the response body when needed.
234
- _handleResponseReceived = (session, event) => {
239
+ _handleResponseReceived = async (session, event) => {
235
240
  let {
236
241
  requestId,
237
242
  response
238
243
  } = event;
244
+ // await on requestWillBeSent
245
+ // no explicitly wait on requestWillBePaused as we implictly wait on it, since it manipulates the lifeCycle of request using Fetch module
246
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
239
247
  let request = this.#requests.get(requestId);
240
248
  /* istanbul ignore if: race condition paranioa */
241
249
  if (!request) return;
@@ -246,12 +254,19 @@ export class Network {
246
254
  });
247
255
  return Buffer.from(result.body, result.base64Encoded ? 'base64' : 'utf-8');
248
256
  };
257
+ // release response
258
+ this.#requestsLifeCycleHandler.get(requestId).resolveResponseReceived();
249
259
  };
250
260
 
251
261
  // Called when a request streams events. These types of requests break asset discovery because
252
262
  // they never finish loading, so we untrack them to signal idle after the first event.
253
- _handleEventSourceMessageReceived = event => {
254
- let request = this.#requests.get(event.requestId);
263
+ _handleEventSourceMessageReceived = async event => {
264
+ let {
265
+ requestId
266
+ } = event;
267
+ // wait for request to be sent
268
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
269
+ let request = this.#requests.get(requestId);
255
270
  /* istanbul ignore else: race condition paranioa */
256
271
  if (request) this._forgetRequest(request);
257
272
  };
@@ -259,7 +274,12 @@ export class Network {
259
274
  // Called when a request has finished loading which triggers the this.onrequestfinished
260
275
  // callback. The request should have an associated response and be finished with any redirects.
261
276
  _handleLoadingFinished = async event => {
262
- let request = this.#requests.get(event.requestId);
277
+ let {
278
+ requestId
279
+ } = event;
280
+ // wait for upto 2 seconds or check if response has been sent
281
+ await this.#requestsLifeCycleHandler.get(requestId).responseReceived;
282
+ let request = this.#requests.get(requestId);
263
283
  /* istanbul ignore if: race condition paranioa */
264
284
  if (!request) return;
265
285
  await saveResponseResource(this, request);
@@ -267,7 +287,15 @@ export class Network {
267
287
  };
268
288
 
269
289
  // Called when a request has failed loading and triggers the this.onrequestfailed callback.
270
- _handleLoadingFailed = event => {
290
+ _handleLoadingFailed = async event => {
291
+ let {
292
+ requestId
293
+ } = event;
294
+ // wait for request to be sent
295
+ // note: we are waiting on requestWillBeSent and NOT responseReceived
296
+ // since, requests can be cancelled in-flight without Network.responseReceived having been triggered
297
+ // and in any case, order of processing for responseReceived and loadingFailed does not matter, as response capturing is done in loadingFinished
298
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
271
299
  let request = this.#requests.get(event.requestId);
272
300
  /* istanbul ignore if: race condition paranioa */
273
301
  if (!request) return;
package/dist/queue.js CHANGED
@@ -9,6 +9,7 @@ function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classEx
9
9
  function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }
10
10
  function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }
11
11
  import { yieldFor, generatePromise, AbortController } from './utils.js';
12
+ import logger from '@percy/logger';
12
13
 
13
14
  // Assigns a deffered promise and resolve & reject functions to an object
14
15
  function deferred(obj) {
@@ -46,12 +47,15 @@ var _end = /*#__PURE__*/new WeakMap();
46
47
  var _process = /*#__PURE__*/new WeakSet();
47
48
  var _until = /*#__PURE__*/new WeakSet();
48
49
  export class Queue {
49
- constructor() {
50
+ // item concurrency
51
+
52
+ constructor(name) {
50
53
  _classPrivateMethodInitSpec(this, _until);
51
54
  _classPrivateMethodInitSpec(this, _process);
52
55
  _classPrivateMethodInitSpec(this, _find);
53
56
  _classPrivateMethodInitSpec(this, _dequeue);
54
57
  _defineProperty(this, "concurrency", 10);
58
+ _defineProperty(this, "log", logger('core:queue'));
55
59
  _classPrivateFieldInitSpec(this, _handlers, {
56
60
  writable: true,
57
61
  value: {}
@@ -73,7 +77,9 @@ export class Queue {
73
77
  value: null
74
78
  });
75
79
  _defineProperty(this, "readyState", 0);
80
+ this.name = name;
76
81
  }
82
+
77
83
  // Configure queue properties
78
84
  set({
79
85
  concurrency
@@ -201,6 +207,7 @@ export class Queue {
201
207
  // clear and abort any queued tasks
202
208
  clear() {
203
209
  let tasks = [..._classPrivateFieldGet(this, _queued)];
210
+ this.log.debug(`Clearing ${this.name} queue, queued state: ${_classPrivateFieldGet(this, _queued).size}, pending state: ${_classPrivateFieldGet(this, _pending).size}`);
204
211
  _classPrivateFieldGet(this, _queued).clear();
205
212
  for (let task of tasks) {
206
213
  task.ctrl.abort();
@@ -235,6 +242,7 @@ export class Queue {
235
242
  // process items up to the latest queued item, starting the queue if necessary;
236
243
  // returns a generator that yields until the flushed item has finished processing
237
244
  flush(callback) {
245
+ this.log.debug(`Flushing ${this.name} queue, queued state: ${_classPrivateFieldGet(this, _queued).size}, pending state: ${_classPrivateFieldGet(this, _pending).size}`);
238
246
  let interrupt =
239
247
  // check for existing interrupts
240
248
  [..._classPrivateFieldGet(this, _pending)].find(t => t.stop) ?? [..._classPrivateFieldGet(this, _queued)].find(t => t.stop);
@@ -316,10 +324,8 @@ async function* _until2(task, callback) {
316
324
  pending = _classPrivateFieldGet(this, _pending).size;
317
325
  // calculate the position within queued when not pending
318
326
  if (task && task.pending == null) queued = positionOf(_classPrivateFieldGet(this, _queued), task);
319
- // calculate the position within pending when not stopping
320
- if (!(task !== null && task !== void 0 && task.stop) && (task === null || task === void 0 ? void 0 : task.pending) != null) pending = positionOf(_classPrivateFieldGet(this, _pending), task);
321
327
  // call the callback and return true when not queued or pending
322
- let position = (queued ?? 0) + (pending ?? 0);
328
+ let position = (queued ?? 0) + pending;
323
329
  callback === null || callback === void 0 ? void 0 : callback(position);
324
330
  return !position;
325
331
  }, {
package/dist/snapshot.js CHANGED
@@ -125,6 +125,7 @@ function getSnapshotOptions(options, {
125
125
  requestHeaders: config.discovery.requestHeaders,
126
126
  authorization: config.discovery.authorization,
127
127
  disableCache: config.discovery.disableCache,
128
+ captureMockedServiceWorker: config.discovery.captureMockedServiceWorker,
128
129
  userAgent: config.discovery.userAgent
129
130
  }
130
131
  }, options], (path, prev, next) => {
@@ -318,7 +319,7 @@ export function createSnapshotsQueue(percy) {
318
319
  let {
319
320
  concurrency
320
321
  } = percy.config.discovery;
321
- let queue = new Queue();
322
+ let queue = new Queue('snapshot');
322
323
  let build;
323
324
  return queue.set({
324
325
  concurrency
package/dist/utils.js CHANGED
@@ -324,4 +324,23 @@ export function serializeFunction(fn) {
324
324
  fnbody = fnbody.replace(/cov_.*?(;\n?|,)\s*/g, '');
325
325
  }
326
326
  return fnbody;
327
- }
327
+ }
328
+
329
+ // DefaultMap, which returns a default value for an uninitialized key
330
+ // Similar to defaultDict in python
331
+ export class DefaultMap extends Map {
332
+ constructor(getDefaultValue, ...mapConstructorArgs) {
333
+ super(...mapConstructorArgs);
334
+ if (typeof getDefaultValue !== 'function') {
335
+ throw new Error('getDefaultValue must be a function');
336
+ }
337
+ this.getDefaultValue = getDefaultValue;
338
+ }
339
+ get = key => {
340
+ if (!this.has(key)) {
341
+ this.set(key, this.getDefaultValue(key));
342
+ }
343
+ return super.get(key);
344
+ };
345
+ }
346
+ ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.27.5-beta.0",
3
+ "version": "1.27.5-beta.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "publishConfig": {
11
11
  "access": "public",
12
- "tag": "latest"
12
+ "tag": "beta"
13
13
  },
14
14
  "engines": {
15
15
  "node": ">=14"
@@ -43,11 +43,11 @@
43
43
  "test:types": "tsd"
44
44
  },
45
45
  "dependencies": {
46
- "@percy/client": "1.27.5-beta.0",
47
- "@percy/config": "1.27.5-beta.0",
48
- "@percy/dom": "1.27.5-beta.0",
49
- "@percy/logger": "1.27.5-beta.0",
50
- "@percy/webdriver-utils": "1.27.5-beta.0",
46
+ "@percy/client": "1.27.5-beta.2",
47
+ "@percy/config": "1.27.5-beta.2",
48
+ "@percy/dom": "1.27.5-beta.2",
49
+ "@percy/logger": "1.27.5-beta.2",
50
+ "@percy/webdriver-utils": "1.27.5-beta.2",
51
51
  "content-disposition": "^0.5.4",
52
52
  "cross-spawn": "^7.0.3",
53
53
  "extract-zip": "^2.0.1",
@@ -58,5 +58,5 @@
58
58
  "rimraf": "^3.0.2",
59
59
  "ws": "^8.0.0"
60
60
  },
61
- "gitHead": "5fed59aa21112e7854a414306504ce7243df7365"
61
+ "gitHead": "9ea4b1f10c134e2c8135f7dddda764a9edafb336"
62
62
  }
package/types/index.d.ts CHANGED
@@ -18,6 +18,7 @@ interface DiscoveryOptions {
18
18
  authorization?: AuthCredentials;
19
19
  allowedHostnames?: string[];
20
20
  disableCache?: boolean;
21
+ captureMockedServiceWorker?: boolean;
21
22
  }
22
23
 
23
24
  interface DiscoveryLaunchOptions {