@percy/core 1.12.0 → 1.14.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 +60 -45
- package/dist/browser.js +82 -68
- package/dist/config.js +16 -9
- package/dist/discovery.js +65 -60
- package/dist/install.js +29 -27
- package/dist/network.js +72 -79
- package/dist/page.js +47 -54
- package/dist/percy.js +85 -85
- package/dist/queue.js +103 -146
- package/dist/server.js +51 -88
- package/dist/session.js +8 -15
- package/dist/snapshot.js +105 -92
- package/dist/utils.js +60 -58
- package/package.json +6 -6
package/dist/utils.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
import { sha256hash } from '@percy/client/utils';
|
|
3
3
|
export { request, getPackageJSON, hostnameMatches } from '@percy/client/utils';
|
|
4
|
-
export { Server, createServer } from './server.js';
|
|
4
|
+
export { Server, createServer } from './server.js';
|
|
5
5
|
|
|
6
|
+
// Returns the hostname portion of a URL.
|
|
6
7
|
export function hostname(url) {
|
|
7
8
|
return new URL(url).hostname;
|
|
8
|
-
}
|
|
9
|
+
}
|
|
9
10
|
|
|
11
|
+
// Normalizes a URL by stripping hashes to ensure unique resources.
|
|
10
12
|
export function normalizeURL(url) {
|
|
11
13
|
let {
|
|
12
14
|
protocol,
|
|
@@ -15,25 +17,29 @@ export function normalizeURL(url) {
|
|
|
15
17
|
search
|
|
16
18
|
} = new URL(url);
|
|
17
19
|
return `${protocol}//${host}${pathname}${search}`;
|
|
18
|
-
}
|
|
19
|
-
// other additional resources attributes.
|
|
20
|
+
}
|
|
20
21
|
|
|
22
|
+
// Creates a local resource object containing the resource URL, mimetype, content, sha, and any
|
|
23
|
+
// other additional resources attributes.
|
|
21
24
|
export function createResource(url, content, mimetype, attrs) {
|
|
22
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
...attrs,
|
|
23
27
|
sha: sha256hash(content),
|
|
24
28
|
mimetype,
|
|
25
29
|
content,
|
|
26
30
|
url
|
|
27
31
|
};
|
|
28
|
-
}
|
|
29
|
-
// here as a convenience since root resources are usually created outside of asset discovery.
|
|
32
|
+
}
|
|
30
33
|
|
|
34
|
+
// Creates a root resource object with an additional `root: true` property. The URL is normalized
|
|
35
|
+
// here as a convenience since root resources are usually created outside of asset discovery.
|
|
31
36
|
export function createRootResource(url, content) {
|
|
32
37
|
return createResource(normalizeURL(url), content, 'text/html', {
|
|
33
38
|
root: true
|
|
34
39
|
});
|
|
35
|
-
}
|
|
40
|
+
}
|
|
36
41
|
|
|
42
|
+
// Creates a Percy CSS resource object.
|
|
37
43
|
export function createPercyCSSResource(url, css) {
|
|
38
44
|
let {
|
|
39
45
|
href,
|
|
@@ -42,21 +48,24 @@ export function createPercyCSSResource(url, css) {
|
|
|
42
48
|
return createResource(href, css, 'text/css', {
|
|
43
49
|
pathname
|
|
44
50
|
});
|
|
45
|
-
}
|
|
51
|
+
}
|
|
46
52
|
|
|
53
|
+
// Creates a log resource object.
|
|
47
54
|
export function createLogResource(logs) {
|
|
48
55
|
let [url, content] = [`/percy.${Date.now()}.log`, JSON.stringify(logs)];
|
|
49
56
|
return createResource(url, content, 'text/plain', {
|
|
50
57
|
log: true
|
|
51
58
|
});
|
|
52
|
-
}
|
|
59
|
+
}
|
|
53
60
|
|
|
61
|
+
// Returns true or false if the provided object is a generator or not
|
|
54
62
|
export function isGenerator(subject) {
|
|
55
63
|
return typeof (subject === null || subject === void 0 ? void 0 : subject.next) === 'function' && (typeof subject[Symbol.iterator] === 'function' || typeof subject[Symbol.asyncIterator] === 'function');
|
|
56
|
-
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Iterates over the provided generator and resolves to the final value when done. With an
|
|
57
67
|
// AbortSignal, the generator will throw with the abort reason when aborted. Also accepts an
|
|
58
68
|
// optional node-style callback, called before the returned promise resolves.
|
|
59
|
-
|
|
60
69
|
export async function generatePromise(gen, signal, cb) {
|
|
61
70
|
try {
|
|
62
71
|
if (typeof signal === 'function') [cb, signal] = [signal];
|
|
@@ -68,27 +77,24 @@ export async function generatePromise(gen, signal, cb) {
|
|
|
68
77
|
done: true,
|
|
69
78
|
value: await gen
|
|
70
79
|
} : await gen.next();
|
|
71
|
-
|
|
72
80
|
while (!done) {
|
|
73
81
|
var _signal;
|
|
74
|
-
|
|
75
82
|
({
|
|
76
83
|
done,
|
|
77
84
|
value
|
|
78
85
|
} = (_signal = signal) !== null && _signal !== void 0 && _signal.aborted ? await gen.throw(signal.reason) : await gen.next(value));
|
|
79
86
|
}
|
|
80
|
-
|
|
81
87
|
if (!cb) return value;
|
|
82
88
|
return cb(null, value);
|
|
83
89
|
} catch (error) {
|
|
84
90
|
if (!cb) throw error;
|
|
85
91
|
return cb(error);
|
|
86
92
|
}
|
|
87
|
-
}
|
|
93
|
+
}
|
|
88
94
|
|
|
95
|
+
// Bare minimum AbortController polyfill for Node < 16.14
|
|
89
96
|
export class AbortController {
|
|
90
97
|
signal = new EventEmitter();
|
|
91
|
-
|
|
92
98
|
abort(reason = new AbortError()) {
|
|
93
99
|
if (this.signal.aborted) return;
|
|
94
100
|
Object.assign(this.signal, {
|
|
@@ -97,9 +103,9 @@ export class AbortController {
|
|
|
97
103
|
});
|
|
98
104
|
this.signal.emit('abort', reason);
|
|
99
105
|
}
|
|
106
|
+
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
// Similar to DOMException[AbortError] but accepts additional properties
|
|
103
109
|
export class AbortError extends Error {
|
|
104
110
|
constructor(msg = 'This operation was aborted', props) {
|
|
105
111
|
Object.assign(super(msg), {
|
|
@@ -107,44 +113,44 @@ export class AbortError extends Error {
|
|
|
107
113
|
...props
|
|
108
114
|
});
|
|
109
115
|
}
|
|
116
|
+
}
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// An async generator that yields after every event loop until the promise settles
|
|
113
119
|
export async function* yieldTo(subject) {
|
|
114
120
|
// yield to any provided generator or return non-promise values
|
|
115
121
|
if (isGenerator(subject)) return yield* subject;
|
|
116
|
-
if (typeof (subject === null || subject === void 0 ? void 0 : subject.then) !== 'function') return subject;
|
|
122
|
+
if (typeof (subject === null || subject === void 0 ? void 0 : subject.then) !== 'function') return subject;
|
|
117
123
|
|
|
124
|
+
// update local variables with the provided promise
|
|
118
125
|
let result,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/* eslint-disable-next-line no-unmodified-loop-condition */
|
|
126
|
+
error,
|
|
127
|
+
pending = !!subject.then(r => result = r, e => error = e).finally(() => pending = false);
|
|
122
128
|
|
|
129
|
+
/* eslint-disable-next-line no-unmodified-loop-condition */
|
|
123
130
|
while (pending) yield new Promise(r => setImmediate(r));
|
|
124
|
-
|
|
125
131
|
if (error) throw error;
|
|
126
132
|
return result;
|
|
127
|
-
}
|
|
133
|
+
}
|
|
128
134
|
|
|
135
|
+
// An async generator that runs provided generators concurrently
|
|
129
136
|
export async function* yieldAll(all) {
|
|
130
137
|
let res = new Array(all.length).fill();
|
|
131
138
|
all = all.map(yieldTo);
|
|
132
|
-
|
|
133
139
|
while (true) {
|
|
134
140
|
res = await Promise.all(all.map((g, i) => {
|
|
135
141
|
var _res$i, _res$i2;
|
|
136
|
-
|
|
137
142
|
return (_res$i = res[i]) !== null && _res$i !== void 0 && _res$i.done ? res[i] : g.next((_res$i2 = res[i]) === null || _res$i2 === void 0 ? void 0 : _res$i2.value);
|
|
138
143
|
}));
|
|
139
144
|
let vals = res.map(r => r === null || r === void 0 ? void 0 : r.value);
|
|
140
145
|
if (res.some(r => !(r !== null && r !== void 0 && r.done))) yield vals;else return vals;
|
|
141
146
|
}
|
|
142
|
-
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// An async generator that infinitely yields to the predicate function until a truthy value is
|
|
143
150
|
// returned. When a timeout is provided, an error will be thrown during the next iteration after the
|
|
144
151
|
// timeout has been exceeded. If an idle option is provided, the predicate will be yielded to a
|
|
145
152
|
// second time, after the idle period, to ensure the yielded value is still truthy. The poll option
|
|
146
153
|
// determines how long to wait before yielding to the predicate function during each iteration.
|
|
147
|
-
|
|
148
154
|
export async function* yieldFor(predicate, options = {}) {
|
|
149
155
|
if (Number.isInteger(options)) options = {
|
|
150
156
|
timeout: options
|
|
@@ -156,7 +162,6 @@ export async function* yieldFor(predicate, options = {}) {
|
|
|
156
162
|
} = options;
|
|
157
163
|
let start = Date.now();
|
|
158
164
|
let done, value;
|
|
159
|
-
|
|
160
165
|
while (true) {
|
|
161
166
|
if (timeout && Date.now() - start >= timeout) {
|
|
162
167
|
throw new Error(`Timeout of ${timeout}ms exceeded.`);
|
|
@@ -168,88 +173,85 @@ export async function* yieldFor(predicate, options = {}) {
|
|
|
168
173
|
return value;
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
|
-
}
|
|
176
|
+
}
|
|
172
177
|
|
|
178
|
+
// Promisified version of `yieldFor` above.
|
|
173
179
|
export function waitFor() {
|
|
174
180
|
return generatePromise(yieldFor(...arguments));
|
|
175
|
-
}
|
|
181
|
+
}
|
|
176
182
|
|
|
183
|
+
// Promisified version of `setTimeout` (no callback argument).
|
|
177
184
|
export function waitForTimeout() {
|
|
178
185
|
return new Promise(resolve => setTimeout(resolve, ...arguments));
|
|
179
|
-
}
|
|
186
|
+
}
|
|
180
187
|
|
|
188
|
+
// Browser-specific util to wait for a query selector to exist within an optional timeout.
|
|
181
189
|
/* istanbul ignore next: tested, but coverage is stripped */
|
|
182
|
-
|
|
183
190
|
async function waitForSelector(selector, timeout) {
|
|
184
191
|
try {
|
|
185
192
|
return await waitFor(() => document.querySelector(selector), timeout);
|
|
186
193
|
} catch {
|
|
187
194
|
throw new Error(`Unable to find: ${selector}`);
|
|
188
195
|
}
|
|
189
|
-
}
|
|
196
|
+
}
|
|
190
197
|
|
|
198
|
+
// Browser-specific util to wait for an xpath selector to exist within an optional timeout.
|
|
191
199
|
/* istanbul ignore next: tested, but coverage is stripped */
|
|
192
|
-
|
|
193
|
-
|
|
194
200
|
async function waitForXPath(selector, timeout) {
|
|
195
201
|
try {
|
|
196
202
|
let xpath = () => document.evaluate(selector, document, null, 9, null);
|
|
197
|
-
|
|
198
203
|
return await waitFor(() => xpath().singleNodeValue, timeout);
|
|
199
204
|
} catch {
|
|
200
205
|
throw new Error(`Unable to find: ${selector}`);
|
|
201
206
|
}
|
|
202
|
-
}
|
|
203
|
-
// after each window segment has been scrolled.
|
|
207
|
+
}
|
|
204
208
|
|
|
209
|
+
// Browser-specific util to scroll to the bottom of a page, optionally calling the provided function
|
|
210
|
+
// after each window segment has been scrolled.
|
|
205
211
|
/* istanbul ignore next: tested, but coverage is stripped */
|
|
206
|
-
|
|
207
|
-
|
|
208
212
|
async function scrollToBottom(options, onScroll) {
|
|
209
213
|
if (typeof options === 'function') [onScroll, options] = [options];
|
|
210
|
-
|
|
211
214
|
let size = () => Math.ceil(document.body.scrollHeight / window.innerHeight);
|
|
212
|
-
|
|
213
215
|
for (let s, i = 1; i < (s = size()); i++) {
|
|
214
216
|
var _onScroll;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
window.scrollTo({
|
|
218
|
+
...options,
|
|
217
219
|
top: window.innerHeight * i
|
|
218
220
|
});
|
|
219
221
|
await ((_onScroll = onScroll) === null || _onScroll === void 0 ? void 0 : _onScroll(i, s));
|
|
220
222
|
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
+
}
|
|
223
224
|
|
|
224
|
-
|
|
225
|
+
// Used to test if a string looks like a function
|
|
226
|
+
const FUNC_REG = /^(async\s+)?(function\s*)?(\w+\s*)?\(.*?\)\s*(\{|=>)/is;
|
|
225
227
|
|
|
228
|
+
// Serializes the provided function with percy helpers for use in evaluating browser scripts
|
|
226
229
|
export function serializeFunction(fn) {
|
|
227
230
|
// stringify or convert a function body into a complete function
|
|
228
|
-
let fnbody = typeof fn === 'string' && !FUNC_REG.test(fn) ? `async function eval() {\n${fn}\n}` : fn.toString();
|
|
231
|
+
let fnbody = typeof fn === 'string' && !FUNC_REG.test(fn) ? `async function eval() {\n${fn}\n}` : fn.toString();
|
|
229
232
|
|
|
233
|
+
// we might have a function shorthand if this fails
|
|
230
234
|
/* eslint-disable-next-line no-new, no-new-func */
|
|
231
|
-
|
|
232
235
|
try {
|
|
233
236
|
new Function(`(${fnbody})`);
|
|
234
237
|
} catch (error) {
|
|
235
238
|
fnbody = fnbody.startsWith('async ') ? fnbody.replace(/^async/, 'async function') : `function ${fnbody}`;
|
|
236
|
-
/* eslint-disable-next-line no-new, no-new-func */
|
|
237
239
|
|
|
240
|
+
/* eslint-disable-next-line no-new, no-new-func */
|
|
238
241
|
try {
|
|
239
242
|
new Function(`(${fnbody})`);
|
|
240
243
|
} catch (error) {
|
|
241
244
|
throw new Error('The provided function is not serializable');
|
|
242
245
|
}
|
|
243
|
-
}
|
|
244
|
-
|
|
246
|
+
}
|
|
245
247
|
|
|
248
|
+
// wrap the function body with percy helpers
|
|
246
249
|
fnbody = 'function withPercyHelpers() {\n' + ['const { config, snapshot } = window.__PERCY__ ?? {};', `return (${fnbody})({`, ' config, snapshot, generatePromise, yieldFor,', ' waitFor, waitForTimeout, waitForSelector, waitForXPath,', ' scrollToBottom', '}, ...arguments);', `${isGenerator}`, `${generatePromise}`, `${yieldFor}`, `${waitFor}`, `${waitForTimeout}`, `${waitForSelector}`, `${waitForXPath}`, `${scrollToBottom}`].join('\n') + '\n}';
|
|
247
|
-
/* istanbul ignore else: ironic. */
|
|
248
250
|
|
|
251
|
+
/* istanbul ignore else: ironic. */
|
|
249
252
|
if (fnbody.includes('cov_')) {
|
|
250
253
|
// remove coverage statements during testing
|
|
251
254
|
fnbody = fnbody.replace(/cov_.*?(;\n?|,)\s*/g, '');
|
|
252
255
|
}
|
|
253
|
-
|
|
254
256
|
return fnbody;
|
|
255
257
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"test:types": "tsd"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@percy/client": "1.
|
|
43
|
-
"@percy/config": "1.
|
|
44
|
-
"@percy/dom": "1.
|
|
45
|
-
"@percy/logger": "1.
|
|
42
|
+
"@percy/client": "1.14.0",
|
|
43
|
+
"@percy/config": "1.14.0",
|
|
44
|
+
"@percy/dom": "1.14.0",
|
|
45
|
+
"@percy/logger": "1.14.0",
|
|
46
46
|
"content-disposition": "^0.5.4",
|
|
47
47
|
"cross-spawn": "^7.0.3",
|
|
48
48
|
"extract-zip": "^2.0.1",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"rimraf": "^3.0.2",
|
|
54
54
|
"ws": "^8.0.0"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "fd72688e449d6dd3eafd346fc07879cb3bb01a4e"
|
|
57
57
|
}
|