@percy/core 1.27.7 → 1.28.0-beta.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/api.js CHANGED
@@ -5,6 +5,7 @@ import logger from '@percy/logger';
5
5
  import { normalize } from '@percy/config/utils';
6
6
  import { getPackageJSON, Server, percyAutomateRequestHandler, percyBuildEventHandler } from './utils.js';
7
7
  import WebdriverUtils from '@percy/webdriver-utils';
8
+ import { handleSyncJob } from './snapshot.js';
8
9
  // need require.resolve until import.meta.resolve can be transpiled
9
10
  export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom');
10
11
 
@@ -99,16 +100,29 @@ export function createPercyServer(percy, port) {
99
100
  })
100
101
  // post one or more snapshots, optionally async
101
102
  .route('post', '/percy/snapshot', async (req, res) => {
102
- let snapshot = percy.snapshot(req.body);
103
+ let data;
104
+ const snapshotPromise = {};
105
+ const snapshot = percy.snapshot(req.body, snapshotPromise);
103
106
  if (!req.url.searchParams.has('async')) await snapshot;
107
+ if (percy.syncMode(req.body)) data = await handleSyncJob(snapshotPromise[req.body.name], percy, 'snapshot');
104
108
  return res.json(200, {
105
- success: true
109
+ success: true,
110
+ data: data
106
111
  });
107
112
  })
108
113
  // post one or more comparisons, optionally waiting
109
114
  .route('post', '/percy/comparison', async (req, res) => {
110
- let upload = percy.upload(req.body);
111
- if (req.url.searchParams.has('await')) await upload;
115
+ let data;
116
+ if (percy.syncMode(req.body)) {
117
+ const snapshotPromise = new Promise((resolve, reject) => percy.upload(req.body, {
118
+ resolve,
119
+ reject
120
+ }));
121
+ data = await handleSyncJob(snapshotPromise, percy, 'comparison');
122
+ } else {
123
+ let upload = percy.upload(req.body);
124
+ if (req.url.searchParams.has('await')) await upload;
125
+ }
112
126
 
113
127
  // generate and include one or more redirect links to comparisons
114
128
  let link = ({
@@ -126,22 +140,38 @@ export function createPercyServer(percy, port) {
126
140
  snake: true
127
141
  }))].join('');
128
142
  };
129
- return res.json(200, Object.assign({
130
- success: true
131
- }, req.body ? Array.isArray(req.body) ? {
132
- links: req.body.map(link)
133
- } : {
134
- link: link(req.body)
135
- } : {}));
143
+ const response = {
144
+ success: true,
145
+ data: data
146
+ };
147
+ if (req.body) {
148
+ if (Array.isArray(req.body)) {
149
+ response.links = req.body.map(link);
150
+ } else {
151
+ response.link = link(req.body);
152
+ }
153
+ }
154
+ return res.json(200, response);
136
155
  })
137
156
  // flushes one or more snapshots from the internal queue
138
157
  .route('post', '/percy/flush', async (req, res) => res.json(200, {
139
158
  success: await percy.flush(req.body).then(() => true)
140
159
  })).route('post', '/percy/automateScreenshot', async (req, res) => {
160
+ let data;
141
161
  percyAutomateRequestHandler(req, percy);
142
- percy.upload(await WebdriverUtils.automateScreenshot(req.body));
162
+ let comparisonData = await WebdriverUtils.automateScreenshot(req.body);
163
+ if (percy.syncMode(comparisonData)) {
164
+ const snapshotPromise = new Promise((resolve, reject) => percy.upload(comparisonData, {
165
+ resolve,
166
+ reject
167
+ }));
168
+ data = await handleSyncJob(snapshotPromise, percy, 'comparison');
169
+ } else {
170
+ percy.upload(comparisonData);
171
+ }
143
172
  res.json(200, {
144
- success: true
173
+ success: true,
174
+ data: data
145
175
  });
146
176
  })
147
177
  // Recieves events from sdk's.
package/dist/config.js CHANGED
@@ -68,6 +68,9 @@ export const configSchema = {
68
68
  }
69
69
  }
70
70
  },
71
+ sync: {
72
+ type: 'boolean'
73
+ },
71
74
  fullPage: {
72
75
  type: 'boolean',
73
76
  onlyAutomate: true
@@ -306,6 +309,9 @@ export const snapshotSchema = {
306
309
  enableLayout: {
307
310
  $ref: '/config/snapshot#/properties/enableLayout'
308
311
  },
312
+ sync: {
313
+ $ref: '/config/snapshot#/properties/sync'
314
+ },
309
315
  reshuffleInvalidTags: {
310
316
  $ref: '/config/snapshot#/properties/reshuffleInvalidTags'
311
317
  },
@@ -704,6 +710,9 @@ export const comparisonSchema = {
704
710
  domInfoSha: {
705
711
  type: 'string'
706
712
  },
713
+ sync: {
714
+ type: 'boolean'
715
+ },
707
716
  metadata: {
708
717
  type: 'object',
709
718
  additionalProperties: false,
package/dist/percy.js CHANGED
@@ -6,6 +6,7 @@ import { createPercyServer, createStaticServer } from './api.js';
6
6
  import { gatherSnapshots, createSnapshotsQueue, validateSnapshotOptions } from './snapshot.js';
7
7
  import { discoverSnapshotResources, createDiscoveryQueue } from './discovery.js';
8
8
  import { generatePromise, yieldAll, yieldTo } from './utils.js';
9
+ import { WaitForJob } from './wait-for-job.js';
9
10
 
10
11
  // A Percy instance will create a new build when started, handle snapshot creation, asset discovery,
11
12
  // and resource uploads, and will finalize the build when stopped. Snapshots are processed
@@ -150,6 +151,8 @@ export class Percy {
150
151
  if (!this.skipDiscovery) yield this.#discovery.start();
151
152
  // start a local API server for SDK communication
152
153
  if (this.server) yield this.server.listen();
154
+ const snapshotType = this.client.tokenType() === 'web' ? 'snapshot' : 'comparison';
155
+ this.syncQueue = new WaitForJob(snapshotType, this);
153
156
  // log and mark this instance as started
154
157
  this.log.info('Percy has started!');
155
158
  this.readyState = 1;
@@ -206,7 +209,7 @@ export class Percy {
206
209
  if (!this.readyState && this.browser.isConnected()) {
207
210
  await this.browser.close();
208
211
  }
209
-
212
+ if (this.syncQueue) this.syncQueue.stop();
210
213
  // not started or already stopped
211
214
  if (!this.readyState || this.readyState > 2) return;
212
215
 
@@ -254,14 +257,14 @@ export class Percy {
254
257
  // Takes one or more snapshots of a page while discovering resources to upload with the resulting
255
258
  // snapshots. Once asset discovery has completed for the provided snapshots, the queued task will
256
259
  // resolve and an upload task will be queued separately.
257
- snapshot(options) {
260
+ snapshot(options, snapshotPromise = {}) {
258
261
  var _this$build;
259
262
  if (this.readyState !== 1) {
260
263
  throw new Error('Not running');
261
264
  } else if ((_this$build = this.build) !== null && _this$build !== void 0 && _this$build.error) {
262
265
  throw new Error(this.build.error);
263
266
  } else if (Array.isArray(options)) {
264
- return yieldAll(options.map(o => this.yield.snapshot(o)));
267
+ return yieldAll(options.map(o => this.yield.snapshot(o, snapshotPromise)));
265
268
  }
266
269
 
267
270
  // accept a url for a sitemap or snapshot
@@ -309,6 +312,15 @@ export class Percy {
309
312
  config: this.config
310
313
  })
311
314
  }, snapshot => {
315
+ // attaching promise resolve reject so to wait for snapshot to complete
316
+ if (this.syncMode(snapshot)) {
317
+ snapshotPromise[snapshot.name] = new Promise((resolve, reject) => {
318
+ Object.assign(snapshot, {
319
+ resolve,
320
+ reject
321
+ });
322
+ });
323
+ }
312
324
  // push each finished snapshot to the snapshots queue
313
325
  this.#snapshots.push(snapshot);
314
326
  });
@@ -321,7 +333,7 @@ export class Percy {
321
333
  }
322
334
 
323
335
  // Uploads one or more snapshots directly to the current Percy build
324
- upload(options) {
336
+ upload(options, callback = null) {
325
337
  if (this.readyState !== 1) {
326
338
  throw new Error('Not running');
327
339
  } else if (Array.isArray(options)) {
@@ -353,6 +365,13 @@ export class Percy {
353
365
  this.client.addClientInfo(options.clientInfo);
354
366
  this.client.addEnvironmentInfo(options.environmentInfo);
355
367
 
368
+ // Sync CLI support, attached resolve, reject promise
369
+ if (this.syncMode(options)) {
370
+ Object.assign(options, {
371
+ ...callback
372
+ });
373
+ }
374
+
356
375
  // return an async generator to allow cancelation
357
376
  return async function* () {
358
377
  try {
@@ -369,5 +388,26 @@ export class Percy {
369
388
  }
370
389
  return tokenType !== 'web';
371
390
  }
391
+ syncMode(options) {
392
+ var _this$config, _this$config$snapshot;
393
+ let syncMode = false;
394
+ if ((_this$config = this.config) !== null && _this$config !== void 0 && (_this$config$snapshot = _this$config.snapshot) !== null && _this$config$snapshot !== void 0 && _this$config$snapshot.sync) syncMode = true;
395
+ if (options !== null && options !== void 0 && options.sync) syncMode = true;
396
+ if ((options === null || options === void 0 ? void 0 : options.sync) === false) syncMode = false;
397
+ if ((this.skipUploads || this.deferUploads || this.delayUploads) && syncMode) {
398
+ syncMode = false;
399
+ options.sync = false;
400
+ if (this.delayUploads && !this.skipUploads) {
401
+ this.log.warn('Synchronous CLI functionality is not compatible with the snapshot command. Kindly consider taking screenshots via SDKs to achieve synchronous results instead.');
402
+ } else {
403
+ let type = 'deferUploads option';
404
+ if (this.skipDiscovery && this.deferUploads) type = 'upload command';
405
+ if (this.skipUploads) type = 'skipUploads option';
406
+ this.log.warn(`The Synchronous CLI functionality is not compatible with ${type}.`);
407
+ }
408
+ }
409
+ if (syncMode) options.sync = syncMode;
410
+ return syncMode;
411
+ }
372
412
  }
373
413
  export default Percy;
package/dist/snapshot.js CHANGED
@@ -4,6 +4,7 @@ import micromatch from 'micromatch';
4
4
  import { configSchema } from './config.js';
5
5
  import Queue from './queue.js';
6
6
  import { request, hostnameMatches, yieldTo } from './utils.js';
7
+ import { JobData } from './wait-for-job.js';
7
8
 
8
9
  // Throw a better error message for missing or invalid urls
9
10
  function validURL(url, base) {
@@ -219,6 +220,22 @@ export function validateSnapshotOptions(options) {
219
220
  ...migrated
220
221
  };
221
222
  }
223
+ export async function handleSyncJob(jobPromise, percy, type) {
224
+ let data;
225
+ try {
226
+ const id = await jobPromise;
227
+ if (type === 'snapshot') {
228
+ data = await percy.client.getSnapshotDetails(id);
229
+ } else {
230
+ data = await percy.client.getComparisonDetails(id);
231
+ }
232
+ } catch (e) {
233
+ data = {
234
+ error: e.message
235
+ };
236
+ }
237
+ return data;
238
+ }
222
239
 
223
240
  // Fetches a sitemap and parses it into a list of URLs for taking snapshots. Duplicate URLs,
224
241
  // including a trailing slash, are removed from the resulting list.
@@ -411,6 +428,14 @@ export function createSnapshotsQueue(percy) {
411
428
  let send = 'tag' in snapshot ? 'sendComparison' : 'sendSnapshot';
412
429
  let response = yield percy.client[send](build.id, snapshot);
413
430
  if (percy.deferUploads) percy.log.info(`Snapshot uploaded: ${name}`, meta);
431
+
432
+ // Pushing to syncQueue, that will check for
433
+ // snapshot processing status, and will resolve once done
434
+ if (snapshot.sync) {
435
+ percy.log.info(`Waiting for snapshot '${name}' to be completed`);
436
+ const data = new JobData(response.data.id, null, snapshot.resolve, snapshot.reject);
437
+ percy.syncQueue.push(data);
438
+ }
414
439
  return {
415
440
  ...snapshot,
416
441
  response
@@ -448,6 +473,7 @@ export function createSnapshotsQueue(percy) {
448
473
  }
449
474
  percy.log.error(`Encountered an error uploading snapshot: ${name}`, meta);
450
475
  percy.log.error(error, meta);
476
+ if (snapshot.sync) snapshot.reject(error);
451
477
  return result;
452
478
  });
453
479
  }
package/dist/utils.js CHANGED
@@ -46,6 +46,7 @@ export function percyAutomateRequestHandler(req, percy) {
46
46
  ignoreRegionXpaths: (_percy$config$snapsho4 = percy.config.snapshot.ignoreRegions) === null || _percy$config$snapsho4 === void 0 ? void 0 : _percy$config$snapsho4.ignoreRegionXpaths,
47
47
  considerRegionSelectors: (_percy$config$snapsho5 = percy.config.snapshot.considerRegions) === null || _percy$config$snapsho5 === void 0 ? void 0 : _percy$config$snapsho5.considerRegionSelectors,
48
48
  considerRegionXpaths: (_percy$config$snapsho6 = percy.config.snapshot.considerRegions) === null || _percy$config$snapsho6 === void 0 ? void 0 : _percy$config$snapsho6.considerRegionXpaths,
49
+ sync: percy.config.snapshot.sync,
49
50
  version: 'v2'
50
51
  }, camelCasedOptions], (path, prev, next) => {
51
52
  switch (path.map(k => k.toString()).join('.')) {
@@ -0,0 +1,97 @@
1
+ import logger from '@percy/logger';
2
+ const MIN_POLLING_INTERVAL = 5_000;
3
+ // Poll atleast once in 2 min
4
+ const MAX_POLLING_INTERVAL_SECONDS = 120;
5
+ const THRESHOLD_OPTIMAL_POLL_TIME = 5;
6
+ const JOB_TIMEOUT = Number(process.env.SYNC_TIMEOUT) || 90_000;
7
+
8
+ // Job is either for snapshot or comparison only
9
+ export class WaitForJob {
10
+ log = logger('core:wait-for-job');
11
+ constructor(type, percy) {
12
+ this.percy = percy;
13
+ this.jobs = [];
14
+ if (type !== 'comparison' && type !== 'snapshot') throw new Error('Type should be either comparison or snapshot');
15
+ this.type = type;
16
+ this.timer = null;
17
+ this.exit = false;
18
+ this.running = false;
19
+ }
20
+ push(job) {
21
+ if (!(job instanceof JobData)) throw new Error('Invalid job passed, use JobData');
22
+ if (this.type === 'snapshot') job.timeout += 420_000; // For snapshot timeout after 08:30 min
23
+
24
+ this.jobs.push(job);
25
+ if (!this.running) this.run();
26
+ }
27
+ run(interval = MIN_POLLING_INTERVAL) {
28
+ if (this.exit) return;
29
+ this.running = true;
30
+ if (interval < MIN_POLLING_INTERVAL) {
31
+ interval = MIN_POLLING_INTERVAL;
32
+ }
33
+ this.log.debug(`Polling for ${this.type} status in ${interval}ms`);
34
+ this.timer = setTimeout(async () => {
35
+ let nextPoll = MAX_POLLING_INTERVAL_SECONDS;
36
+ const jobIds = this.jobs.map(job => job.id);
37
+ const response = await this.percy.client.getStatus(this.type, jobIds);
38
+ this.jobs = this.jobs.filter(job => {
39
+ if (response[job.id]) {
40
+ const jobStatus = response[job.id];
41
+ if (jobStatus.status) {
42
+ job.resolve(job.id);
43
+ return false;
44
+ } else if (jobStatus.error != null) {
45
+ job.reject(jobStatus.error);
46
+ return false;
47
+ } else if (Date.now() - job.timeout >= 0) {
48
+ job.reject(new Error(`Timeout waiting for ${this.type} with id ${job.id}`));
49
+ return false;
50
+ } else {
51
+ job.nextPoll = jobStatus.next_poll;
52
+ }
53
+ }
54
+ nextPoll = Math.min(nextPoll, job.nextPoll);
55
+ return true;
56
+ });
57
+ if (this.jobs.length === 0) {
58
+ this.running = false;
59
+ return;
60
+ }
61
+ const optimalNextPollTime = this.getOptimalPollTime(nextPoll);
62
+ this.run(optimalNextPollTime * 1000);
63
+ }, interval);
64
+ }
65
+
66
+ // If there are other snapshots which can be completed in next
67
+ // 5 seconds, calling after x seconds will reduce network call
68
+ getOptimalPollTime(lowestPollTime) {
69
+ let pollTime = lowestPollTime;
70
+ this.jobs.forEach(job => {
71
+ const jobPollTime = job.nextPoll;
72
+ if (jobPollTime - lowestPollTime <= THRESHOLD_OPTIMAL_POLL_TIME) {
73
+ pollTime = Math.max(pollTime, jobPollTime);
74
+ }
75
+ });
76
+ return pollTime;
77
+ }
78
+ stop() {
79
+ this.exit = true;
80
+ if (this.timer) {
81
+ clearTimeout(this.timer);
82
+ this.timer = null;
83
+ }
84
+ this.jobs.forEach(job => {
85
+ job.reject(new Error('Unable to process synchronous results as the CLI was exited while awaiting completion of the snapshot.'));
86
+ });
87
+ }
88
+ }
89
+ export class JobData {
90
+ constructor(id, nextPoll, resolve, reject) {
91
+ this.id = id;
92
+ this.nextPoll = nextPoll || 60;
93
+ this.timeout = Date.now() + JOB_TIMEOUT;
94
+ this.resolve = resolve;
95
+ this.reject = reject;
96
+ }
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.27.7",
3
+ "version": "1.28.0-beta.0",
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.7",
47
- "@percy/config": "1.27.7",
48
- "@percy/dom": "1.27.7",
49
- "@percy/logger": "1.27.7",
50
- "@percy/webdriver-utils": "1.27.7",
46
+ "@percy/client": "1.28.0-beta.0",
47
+ "@percy/config": "1.28.0-beta.0",
48
+ "@percy/dom": "1.28.0-beta.0",
49
+ "@percy/logger": "1.28.0-beta.0",
50
+ "@percy/webdriver-utils": "1.28.0-beta.0",
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": "eba048142b27b317491f15364b34764371fc0670"
61
+ "gitHead": "d188893f1b45f6e4a1b3196b326a47174340c6ce"
62
62
  }