@percy/core 1.27.6-alpha.0 → 1.27.6-beta.1

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
@@ -59,6 +59,15 @@ export const configSchema = {
59
59
  scope: {
60
60
  type: 'string'
61
61
  },
62
+ scopeOptions: {
63
+ type: 'object',
64
+ additionalProperties: false,
65
+ properties: {
66
+ scroll: {
67
+ type: 'boolean'
68
+ }
69
+ }
70
+ },
62
71
  freezeAnimation: {
63
72
  // for backward compatibility
64
73
  type: 'boolean',
@@ -176,6 +185,10 @@ export const configSchema = {
176
185
  disableCache: {
177
186
  type: 'boolean'
178
187
  },
188
+ captureMockedServiceWorker: {
189
+ type: 'boolean',
190
+ default: false
191
+ },
179
192
  requestHeaders: {
180
193
  type: 'object',
181
194
  normalize: false,
@@ -292,6 +305,9 @@ export const snapshotSchema = {
292
305
  reshuffleInvalidTags: {
293
306
  $ref: '/config/snapshot#/properties/reshuffleInvalidTags'
294
307
  },
308
+ scopeOptions: {
309
+ $ref: '/config/snapshot#/properties/scopeOptions'
310
+ },
295
311
  discovery: {
296
312
  type: 'object',
297
313
  additionalProperties: false,
@@ -311,6 +327,9 @@ export const snapshotSchema = {
311
327
  disableCache: {
312
328
  $ref: '/config/discovery#/properties/disableCache'
313
329
  },
330
+ captureMockedServiceWorker: {
331
+ $ref: '/config/discovery#/properties/captureMockedServiceWorker'
332
+ },
314
333
  userAgent: {
315
334
  $ref: '/config/discovery#/properties/userAgent'
316
335
  },
@@ -319,6 +338,9 @@ export const snapshotSchema = {
319
338
  }
320
339
  }
321
340
  }
341
+ },
342
+ dependencies: {
343
+ scopeOptions: ['scope']
322
344
  }
323
345
  },
324
346
  exec: {
@@ -693,6 +715,10 @@ export const comparisonSchema = {
693
715
  cliScreenshotEndTime: {
694
716
  type: 'integer',
695
717
  default: 0
718
+ },
719
+ screenshotType: {
720
+ type: 'string',
721
+ default: 'singlepage'
696
722
  }
697
723
  }
698
724
  },
package/dist/discovery.js CHANGED
@@ -32,6 +32,7 @@ function debugSnapshotOptions(snapshot) {
32
32
  debugProp(snapshot, 'deviceScaleFactor');
33
33
  debugProp(snapshot, 'waitForTimeout');
34
34
  debugProp(snapshot, 'waitForSelector');
35
+ debugProp(snapshot, 'scopeOptions.scroll');
35
36
  debugProp(snapshot, 'execute.afterNavigation');
36
37
  debugProp(snapshot, 'execute.beforeResize');
37
38
  debugProp(snapshot, 'execute.afterResize');
@@ -42,6 +43,7 @@ function debugSnapshotOptions(snapshot) {
42
43
  debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
43
44
  debugProp(snapshot, 'discovery.authorization', JSON.stringify);
44
45
  debugProp(snapshot, 'discovery.disableCache');
46
+ debugProp(snapshot, 'discovery.captureMockedServiceWorker');
45
47
  debugProp(snapshot, 'discovery.userAgent');
46
48
  debugProp(snapshot, 'clientInfo');
47
49
  debugProp(snapshot, 'environmentInfo');
@@ -322,6 +324,7 @@ export function createDiscoveryQueue(percy) {
322
324
  requestHeaders: snapshot.discovery.requestHeaders,
323
325
  authorization: snapshot.discovery.authorization,
324
326
  userAgent: snapshot.discovery.userAgent,
327
+ captureMockedServiceWorker: snapshot.discovery.captureMockedServiceWorker,
325
328
  meta: snapshot.meta,
326
329
  // enable network inteception
327
330
  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,19 @@ 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
+ // eslint-disable-next-line babel/no-unused-expressions
175
+ (pending === null || pending === void 0 ? void 0 : pending.request.url) === event.request.url && pending.request.method === event.request.method && (await this._handleRequest(session, {
176
+ ...pending,
177
+ resourceType,
178
+ interceptId
179
+ }));
173
180
  };
174
181
 
175
182
  // Called when a request will be sent. If the request has already been intercepted, handle it;
@@ -177,35 +184,32 @@ export class Network {
177
184
  _handleRequestWillBeSent = async event => {
178
185
  let {
179
186
  requestId,
180
- request
187
+ request,
188
+ type
181
189
  } = event;
182
190
 
183
191
  // do not handle data urls
184
192
  if (request.url.startsWith('data:')) return;
185
193
  if (this.intercept) {
186
- let intercept = this.#intercepts.get(requestId);
187
194
  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, {
195
+ if (this.captureMockedServiceWorker) {
196
+ await this._handleRequest(undefined, {
196
197
  ...event,
197
- resourceType,
198
- interceptId
199
- });
200
- this.#intercepts.delete(requestId);
198
+ resourceType: type,
199
+ interceptId: requestId
200
+ }, true);
201
201
  }
202
202
  }
203
+ // release request
204
+ // note: we are releasing this, even if intercept is not set for network.js
205
+ // since, we want to process all-requests in-order doesn't matter if it should be intercepted or not
206
+ this.#requestsLifeCycleHandler.get(requestId).resolveRequestWillBeSent();
203
207
  };
204
208
 
205
209
  // Called when a pending request is paused. Handles associating redirected requests with
206
210
  // responses and calls this.onrequest with request info and callbacks to continue, respond,
207
211
  // or abort a request. One of the callbacks is required to be called and only one.
208
- _handleRequest = async (session, event) => {
212
+ _handleRequest = async (session, event, serviceWorker = false) => {
209
213
  let {
210
214
  request,
211
215
  requestId,
@@ -226,16 +230,21 @@ export class Network {
226
230
  request.interceptId = interceptId;
227
231
  request.redirectChain = redirectChain;
228
232
  this.#requests.set(requestId, request);
229
- await sendResponseResource(this, request, session);
233
+ if (!serviceWorker) {
234
+ await sendResponseResource(this, request, session);
235
+ }
230
236
  };
231
237
 
232
238
  // Called when a response has been received for a specific request. Associates the response with
233
239
  // the request data and adds a buffer method to fetch the response body when needed.
234
- _handleResponseReceived = (session, event) => {
240
+ _handleResponseReceived = async (session, event) => {
235
241
  let {
236
242
  requestId,
237
243
  response
238
244
  } = event;
245
+ // await on requestWillBeSent
246
+ // no explicitly wait on requestWillBePaused as we implictly wait on it, since it manipulates the lifeCycle of request using Fetch module
247
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
239
248
  let request = this.#requests.get(requestId);
240
249
  /* istanbul ignore if: race condition paranioa */
241
250
  if (!request) return;
@@ -246,12 +255,19 @@ export class Network {
246
255
  });
247
256
  return Buffer.from(result.body, result.base64Encoded ? 'base64' : 'utf-8');
248
257
  };
258
+ // release response
259
+ this.#requestsLifeCycleHandler.get(requestId).resolveResponseReceived();
249
260
  };
250
261
 
251
262
  // Called when a request streams events. These types of requests break asset discovery because
252
263
  // 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);
264
+ _handleEventSourceMessageReceived = async event => {
265
+ let {
266
+ requestId
267
+ } = event;
268
+ // wait for request to be sent
269
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
270
+ let request = this.#requests.get(requestId);
255
271
  /* istanbul ignore else: race condition paranioa */
256
272
  if (request) this._forgetRequest(request);
257
273
  };
@@ -259,7 +275,12 @@ export class Network {
259
275
  // Called when a request has finished loading which triggers the this.onrequestfinished
260
276
  // callback. The request should have an associated response and be finished with any redirects.
261
277
  _handleLoadingFinished = async event => {
262
- let request = this.#requests.get(event.requestId);
278
+ let {
279
+ requestId
280
+ } = event;
281
+ // wait for upto 2 seconds or check if response has been sent
282
+ await this.#requestsLifeCycleHandler.get(requestId).responseReceived;
283
+ let request = this.#requests.get(requestId);
263
284
  /* istanbul ignore if: race condition paranioa */
264
285
  if (!request) return;
265
286
  await saveResponseResource(this, request);
@@ -267,7 +288,15 @@ export class Network {
267
288
  };
268
289
 
269
290
  // Called when a request has failed loading and triggers the this.onrequestfailed callback.
270
- _handleLoadingFailed = event => {
291
+ _handleLoadingFailed = async event => {
292
+ let {
293
+ requestId
294
+ } = event;
295
+ // wait for request to be sent
296
+ // note: we are waiting on requestWillBeSent and NOT responseReceived
297
+ // since, requests can be cancelled in-flight without Network.responseReceived having been triggered
298
+ // and in any case, order of processing for responseReceived and loadingFailed does not matter, as response capturing is done in loadingFinished
299
+ await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
271
300
  let request = this.#requests.get(event.requestId);
272
301
  /* istanbul ignore if: race condition paranioa */
273
302
  if (!request) return;
package/dist/queue.js CHANGED
@@ -324,10 +324,6 @@ async function* _until2(task, callback) {
324
324
  pending = _classPrivateFieldGet(this, _pending).size;
325
325
  // calculate the position within queued when not pending
326
326
  if (task && task.pending == null) queued = positionOf(_classPrivateFieldGet(this, _queued), task);
327
- // calculate the position within pending when not stopping
328
- // Commenting below line reason being currently if the task passed is found we will stop flushing
329
- // rest of the tasks but ideally it should wait for flushing of all the tasks till the last dequeued task.
330
- // if (!task?.stop && task?.pending != null) pending = positionOf(this.#pending, task);
331
327
  // call the callback and return true when not queued or pending
332
328
  let position = (queued ?? 0) + pending;
333
329
  callback === null || callback === void 0 ? void 0 : callback(position);
package/dist/server.js CHANGED
@@ -157,6 +157,11 @@ export class Server extends http.Server {
157
157
  });
158
158
  }
159
159
 
160
+ // return host bind address - defaults to "::"
161
+ get host() {
162
+ return process.env.PERCY_SERVER_HOST || '::';
163
+ }
164
+
160
165
  // return the listening port or any default port
161
166
  get port() {
162
167
  var _super$address;
@@ -166,7 +171,11 @@ export class Server extends http.Server {
166
171
  // return a string representation of the server address
167
172
  address() {
168
173
  let port = this.port;
169
- let host = 'http://localhost';
174
+ // we need to specifically map "::" to localhost on windows as even though we
175
+ // can listen to all interfaces using "::" we cant make a request on "::" as
176
+ // its an invalid ip address as per spec, but unix systems allow request to it and
177
+ // falls back to localhost
178
+ let host = `http://${this.host === '::' ? 'localhost' : this.host}`;
170
179
  return port ? `${host}:${port}` : host;
171
180
  }
172
181
 
@@ -175,7 +184,7 @@ export class Server extends http.Server {
175
184
  return new Promise((resolve, reject) => {
176
185
  let handle = err => off() && err ? reject(err) : resolve(this);
177
186
  let off = () => this.off('error', handle).off('listening', handle);
178
- super.listen(port, handle).once('error', handle);
187
+ super.listen(port, this.host, handle).once('error', handle);
179
188
  });
180
189
  }
181
190
 
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) => {
package/dist/utils.js CHANGED
@@ -44,7 +44,8 @@ export function percyAutomateRequestHandler(req, percy) {
44
44
  ignoreRegionSelectors: (_percy$config$snapsho3 = percy.config.snapshot.ignoreRegions) === null || _percy$config$snapsho3 === void 0 ? void 0 : _percy$config$snapsho3.ignoreRegionSelectors,
45
45
  ignoreRegionXpaths: (_percy$config$snapsho4 = percy.config.snapshot.ignoreRegions) === null || _percy$config$snapsho4 === void 0 ? void 0 : _percy$config$snapsho4.ignoreRegionXpaths,
46
46
  considerRegionSelectors: (_percy$config$snapsho5 = percy.config.snapshot.considerRegions) === null || _percy$config$snapsho5 === void 0 ? void 0 : _percy$config$snapsho5.considerRegionSelectors,
47
- considerRegionXpaths: (_percy$config$snapsho6 = percy.config.snapshot.considerRegions) === null || _percy$config$snapsho6 === void 0 ? void 0 : _percy$config$snapsho6.considerRegionXpaths
47
+ considerRegionXpaths: (_percy$config$snapsho6 = percy.config.snapshot.considerRegions) === null || _percy$config$snapsho6 === void 0 ? void 0 : _percy$config$snapsho6.considerRegionXpaths,
48
+ version: 'v2'
48
49
  }, camelCasedOptions], (path, prev, next) => {
49
50
  switch (path.map(k => k.toString()).join('.')) {
50
51
  case 'percyCSS':
@@ -324,4 +325,23 @@ export function serializeFunction(fn) {
324
325
  fnbody = fnbody.replace(/cov_.*?(;\n?|,)\s*/g, '');
325
326
  }
326
327
  return fnbody;
327
- }
328
+ }
329
+
330
+ // DefaultMap, which returns a default value for an uninitialized key
331
+ // Similar to defaultDict in python
332
+ export class DefaultMap extends Map {
333
+ constructor(getDefaultValue, ...mapConstructorArgs) {
334
+ super(...mapConstructorArgs);
335
+ if (typeof getDefaultValue !== 'function') {
336
+ throw new Error('getDefaultValue must be a function');
337
+ }
338
+ this.getDefaultValue = getDefaultValue;
339
+ }
340
+ get = key => {
341
+ if (!this.has(key)) {
342
+ this.set(key, this.getDefaultValue(key));
343
+ }
344
+ return super.get(key);
345
+ };
346
+ }
347
+ ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.27.6-alpha.0",
3
+ "version": "1.27.6-beta.1",
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": "alpha"
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.6-alpha.0",
47
- "@percy/config": "1.27.6-alpha.0",
48
- "@percy/dom": "1.27.6-alpha.0",
49
- "@percy/logger": "1.27.6-alpha.0",
50
- "@percy/webdriver-utils": "1.27.6-alpha.0",
46
+ "@percy/client": "1.27.6-beta.1",
47
+ "@percy/config": "1.27.6-beta.1",
48
+ "@percy/dom": "1.27.6-beta.1",
49
+ "@percy/logger": "1.27.6-beta.1",
50
+ "@percy/webdriver-utils": "1.27.6-beta.1",
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": "415a083dc13e9453990b042a41d6676f6618df0c"
61
+ "gitHead": "1a3bf9e775769215ef590faa40502ce0ee0a6f78"
62
62
  }
package/types/index.d.ts CHANGED
@@ -18,6 +18,11 @@ interface DiscoveryOptions {
18
18
  authorization?: AuthCredentials;
19
19
  allowedHostnames?: string[];
20
20
  disableCache?: boolean;
21
+ captureMockedServiceWorker?: boolean;
22
+ }
23
+
24
+ interface ScopeOptions {
25
+ scroll?: boolean;
21
26
  }
22
27
 
23
28
  interface DiscoveryLaunchOptions {
@@ -44,6 +49,7 @@ interface CommonSnapshotOptions {
44
49
  reshuffleInvalidTags?: boolean;
45
50
  devicePixelRatio?: number;
46
51
  scope?: string;
52
+ scopeOptions?: ScopeOptions;
47
53
  }
48
54
 
49
55
  export interface SnapshotOptions extends CommonSnapshotOptions {