@percy/core 1.31.15-alpha.0 → 1.31.15-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/browser.js +5 -17
- package/dist/install.js +6 -7
- package/dist/network.js +33 -261
- package/dist/percy.js +2 -1
- package/package.json +10 -10
- package/test/helpers/server.js +1 -7
package/dist/browser.js
CHANGED
|
@@ -10,20 +10,6 @@ import logger from '@percy/logger';
|
|
|
10
10
|
import install from './install.js';
|
|
11
11
|
import Session from './session.js';
|
|
12
12
|
import Page from './page.js';
|
|
13
|
-
|
|
14
|
-
// Chrome features Percy disables for v143 new-headless asset discovery.
|
|
15
|
-
const DISABLED_FEATURES = ['Translate',
|
|
16
|
-
// suppress translate prompt overlay
|
|
17
|
-
'OptimizationGuideModelDownloading',
|
|
18
|
-
// suppress background model fetches
|
|
19
|
-
'IsolateOrigins',
|
|
20
|
-
// [headless-only] keep cross-origin sub-resources on the page session for CDP capture
|
|
21
|
-
'site-per-process',
|
|
22
|
-
// companion to IsolateOrigins
|
|
23
|
-
'HttpsFirstBalancedModeAutoEnable',
|
|
24
|
-
// allow HTTP customer URLs (CI / local dev / staging)
|
|
25
|
-
'LocalNetworkAccessChecks' // allow loopback/RFC1918 sub-resources (Chrome 143 LNA gating)
|
|
26
|
-
];
|
|
27
13
|
export class Browser extends EventEmitter {
|
|
28
14
|
log = logger('core:browser');
|
|
29
15
|
sessions = new Map();
|
|
@@ -31,7 +17,9 @@ export class Browser extends EventEmitter {
|
|
|
31
17
|
closed = false;
|
|
32
18
|
#callbacks = new Map();
|
|
33
19
|
#lastid = 0;
|
|
34
|
-
args = [
|
|
20
|
+
args = [
|
|
21
|
+
// disable the translate popup and optimization downloads
|
|
22
|
+
'--disable-features=Translate,OptimizationGuideModelDownloading',
|
|
35
23
|
// disable several subsystems which run network requests in the background
|
|
36
24
|
'--disable-background-networking',
|
|
37
25
|
// disable task throttling of timer tasks from background pages
|
|
@@ -330,8 +318,8 @@ export class Browser extends EventEmitter {
|
|
|
330
318
|
let match = chunk.match(/^DevTools listening on (ws:\/\/.*)$/m);
|
|
331
319
|
if (match) cleanup(() => resolve(match[1]));
|
|
332
320
|
};
|
|
333
|
-
let handleExitClose = () => handleError(
|
|
334
|
-
let handleError = error => cleanup(() => reject(new Error(`Failed to launch browser. ${error.message}\n${stderr}'\n\n`)));
|
|
321
|
+
let handleExitClose = () => handleError();
|
|
322
|
+
let handleError = error => cleanup(() => reject(new Error(`Failed to launch browser. ${(error === null || error === void 0 ? void 0 : error.message) ?? ''}\n${stderr}'\n\n`)));
|
|
335
323
|
let cleanup = callback => {
|
|
336
324
|
clearTimeout(timeoutId);
|
|
337
325
|
this.process.stderr.off('data', handleData);
|
package/dist/install.js
CHANGED
|
@@ -162,14 +162,13 @@ export function chromium({
|
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
// revision from https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html
|
|
165
|
+
// default chromium revisions corresponds to v126.0.6478.184
|
|
167
166
|
chromium.revisions = {
|
|
168
|
-
linux: '
|
|
169
|
-
win64: '
|
|
170
|
-
win32: '
|
|
171
|
-
darwin: '
|
|
172
|
-
darwinArm: '
|
|
167
|
+
linux: '1300309',
|
|
168
|
+
win64: '1300297',
|
|
169
|
+
win32: '1300295',
|
|
170
|
+
darwin: '1300293',
|
|
171
|
+
darwinArm: '1300314'
|
|
173
172
|
};
|
|
174
173
|
|
|
175
174
|
// export the namespace by default
|
package/dist/network.js
CHANGED
|
@@ -6,12 +6,6 @@ const MAX_RESOURCE_SIZE = 25 * 1024 ** 2 * 0.63; // 25MB, 0.63 factor for accoun
|
|
|
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
|
-
// Chrome 143 omits Network.responseReceived for worker scripts; cap the wait
|
|
10
|
-
// so loadingFinished can clean up. Per-request — N timeouts accumulate to N*2s;
|
|
11
|
-
// PERCY_NETWORK_IDLE_WAIT_TIMEOUT (default 30s) caps cumulative impact.
|
|
12
|
-
const RESPONSE_RECEIVED_TIMEOUT = 2000;
|
|
13
|
-
// Cap idle() impact when a host accepts the TCP connection then stalls during a direct fetch.
|
|
14
|
-
const DIRECT_FETCH_TIMEOUT = 5000;
|
|
15
9
|
|
|
16
10
|
// Stable, machine-readable codes for abort errors thrown from this module.
|
|
17
11
|
// Consumers should prefer `error.code` over string matching on `error.message`.
|
|
@@ -67,14 +61,14 @@ export class Network {
|
|
|
67
61
|
#aborted = new Set();
|
|
68
62
|
#finishedUrls = new Set();
|
|
69
63
|
constructor(page, options) {
|
|
70
|
-
var _page$session$browser;
|
|
71
64
|
this.page = page;
|
|
72
65
|
this.timeout = options.networkIdleTimeout ?? 100;
|
|
73
66
|
this.authorization = options.authorization;
|
|
74
67
|
this.requestHeaders = options.requestHeaders ?? {};
|
|
75
68
|
this.captureMockedServiceWorker = options.captureMockedServiceWorker ?? false;
|
|
76
|
-
this.userAgent = options.userAgent ??
|
|
77
|
-
|
|
69
|
+
this.userAgent = options.userAgent ??
|
|
70
|
+
// by default, emulate a non-headless browser
|
|
71
|
+
page.session.browser.version.userAgent.replace('Headless', '');
|
|
78
72
|
this.fontDomains = options.fontDomains || [];
|
|
79
73
|
this.intercept = options.intercept;
|
|
80
74
|
this.meta = options.meta;
|
|
@@ -252,13 +246,6 @@ export class Network {
|
|
|
252
246
|
resourceType
|
|
253
247
|
} = event;
|
|
254
248
|
|
|
255
|
-
// Response-stage events arrive here when Fetch.continueRequest was called
|
|
256
|
-
// with interceptResponse:true (see sendResponseResource).
|
|
257
|
-
if (event.responseStatusCode != null || event.responseErrorReason != null) {
|
|
258
|
-
await this._handleResponsePaused(session, event);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
249
|
// wait for request to be sent
|
|
263
250
|
await this.#requestsLifeCycleHandler.get(requestId).requestWillBeSent;
|
|
264
251
|
let pending = this.#pending.get(requestId);
|
|
@@ -273,88 +260,6 @@ export class Network {
|
|
|
273
260
|
}));
|
|
274
261
|
};
|
|
275
262
|
|
|
276
|
-
// Response-stage interception is kept ONLY to detect oversized/malformed
|
|
277
|
-
// Content-Length and abort the request before Chrome streams a body it
|
|
278
|
-
// would never terminate (Chrome 143 quirk). For everything else we just
|
|
279
|
-
// continue — body capture happens later via Network.loadingFinished →
|
|
280
|
-
// Network.getResponseBody (the v126 path). Reading the body at this stage
|
|
281
|
-
// hangs worker-initiated fetches, so we don't.
|
|
282
|
-
_handleResponsePaused = async (session, event) => {
|
|
283
|
-
var _event$request;
|
|
284
|
-
let {
|
|
285
|
-
networkId: requestId,
|
|
286
|
-
requestId: interceptId,
|
|
287
|
-
responseHeaders,
|
|
288
|
-
responseStatusCode
|
|
289
|
-
} = event;
|
|
290
|
-
// request may be undefined when a response-stage pause arrives for a request
|
|
291
|
-
// whose request-stage tracking we never installed (service-worker-fulfilled,
|
|
292
|
-
// or a cleanup race). We still need to unpause Chrome regardless.
|
|
293
|
-
let request = this.#requests.get(requestId);
|
|
294
|
-
let url = request ? originURL(request) : ((_event$request = event.request) === null || _event$request === void 0 ? void 0 : _event$request.url) && normalizeURL(event.request.url);
|
|
295
|
-
let headersObj = headersArrayToObject(responseHeaders);
|
|
296
|
-
let {
|
|
297
|
-
tooLarge,
|
|
298
|
-
malformed,
|
|
299
|
-
rawValue
|
|
300
|
-
} = inspectContentLength(headersObj);
|
|
301
|
-
if (tooLarge || malformed) {
|
|
302
|
-
let meta = {
|
|
303
|
-
...this.meta,
|
|
304
|
-
url,
|
|
305
|
-
responseStatus: responseStatusCode
|
|
306
|
-
};
|
|
307
|
-
logAssetInstrumentation(this.log, 'asset_not_uploaded', 'resource_too_large', {
|
|
308
|
-
url,
|
|
309
|
-
size: rawValue,
|
|
310
|
-
snapshot: meta.snapshot
|
|
311
|
-
});
|
|
312
|
-
this.log.debug('- Skipping resource larger than 25MB', meta);
|
|
313
|
-
|
|
314
|
-
// Disposition first, then forget the request — so we never leave Chrome's
|
|
315
|
-
// Fetch state paused while Percy thinks the request is already done.
|
|
316
|
-
try {
|
|
317
|
-
await this.send(session, 'Fetch.failRequest', {
|
|
318
|
-
requestId: interceptId,
|
|
319
|
-
errorReason: 'Aborted'
|
|
320
|
-
});
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if (error.message === ABORTED_MESSAGE || error.message.includes('Invalid InterceptionId')) {
|
|
323
|
-
// benign race — request was already aborted upstream; nothing to un-pause
|
|
324
|
-
} else {
|
|
325
|
-
this.log.debug(`Failed to abort oversized response for ${url}: ${error.message}`);
|
|
326
|
-
// Last-resort: un-pause Chrome's Fetch so it doesn't leak the response.
|
|
327
|
-
try {
|
|
328
|
-
await this.send(session, 'Fetch.continueResponse', {
|
|
329
|
-
requestId: interceptId
|
|
330
|
-
});
|
|
331
|
-
} catch (continueError) {
|
|
332
|
-
this.log.debug(`Last-resort continueResponse also failed for ${url}: ${continueError.message}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (request) {
|
|
337
|
-
this._forgetRequest(request);
|
|
338
|
-
this.#requestsLifeCycleHandler.get(requestId).resolveResponseReceived();
|
|
339
|
-
}
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
return this._continueResponse(session, interceptId, url);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// Tell the browser to continue the paused response, swallowing expected
|
|
346
|
-
// races (request already aborted, interception ID no longer valid).
|
|
347
|
-
_continueResponse = async (session, interceptId, url) => {
|
|
348
|
-
try {
|
|
349
|
-
await this.send(session, 'Fetch.continueResponse', {
|
|
350
|
-
requestId: interceptId
|
|
351
|
-
});
|
|
352
|
-
} catch (error) {
|
|
353
|
-
if (error.message === ABORTED_MESSAGE || error.message.includes('Invalid InterceptionId')) return;
|
|
354
|
-
this.log.debug(`Failed to continue response for ${url}: ${error.message}`);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
263
|
// Called when a request will be sent. If the request has already been intercepted, handle it;
|
|
359
264
|
// otherwise set it to be pending until it is paused.
|
|
360
265
|
_handleRequestWillBeSent = async event => {
|
|
@@ -466,36 +371,11 @@ export class Network {
|
|
|
466
371
|
let {
|
|
467
372
|
requestId
|
|
468
373
|
} = event;
|
|
374
|
+
// wait for upto 2 seconds or check if response has been sent
|
|
375
|
+
await this.#requestsLifeCycleHandler.get(requestId).responseReceived;
|
|
469
376
|
let request = this.#requests.get(requestId);
|
|
470
377
|
/* istanbul ignore if: race condition paranoia */
|
|
471
378
|
if (!request) return;
|
|
472
|
-
if (!request.response) {
|
|
473
|
-
let timerId;
|
|
474
|
-
await Promise.race([this.#requestsLifeCycleHandler.get(requestId).responseReceived, new Promise(resolve => {
|
|
475
|
-
timerId = setTimeout(resolve, RESPONSE_RECEIVED_TIMEOUT);
|
|
476
|
-
})]);
|
|
477
|
-
clearTimeout(timerId);
|
|
478
|
-
}
|
|
479
|
-
if (!request.response) {
|
|
480
|
-
this.log.debug(`Skipping resource: responseReceived not received within ${RESPONSE_RECEIVED_TIMEOUT}ms - ${request.url}`);
|
|
481
|
-
// Chrome 143+ PlzDedicatedWorker: dedicated worker scripts fetch in the browser
|
|
482
|
-
// process and never surface a CDP response. resourceType varies ('Other' on v143,
|
|
483
|
-
// 'Script' on older Chrome) so we gate on hostname rather than type, and mirror
|
|
484
|
-
// sendResponseResource's disallowedHostnames-before-allowedHostnames precedence.
|
|
485
|
-
let url = originURL(request);
|
|
486
|
-
/* istanbul ignore else: the else only fires for PlzDedicatedWorker requests
|
|
487
|
-
whose worker-script fetch bypasses Fetch.requestPaused. Cross-origin assets
|
|
488
|
-
loaded via the document session still go through sendResponseResource
|
|
489
|
-
(which performs its own disallowedHostnames check), so the test harness
|
|
490
|
-
can't reliably reach this skip branch via integration tests. */
|
|
491
|
-
if (!hostnameMatches(this.intercept.disallowedHostnames, url) && hostnameMatches(this.intercept.allowedHostnames, url)) {
|
|
492
|
-
await captureResourceDirectly(this, request, session);
|
|
493
|
-
} else {
|
|
494
|
-
this.log.debug(`- Skipping direct-fetch fallback for ${url}: hostname not allowed`, this.meta);
|
|
495
|
-
}
|
|
496
|
-
this._forgetRequest(request);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
379
|
await saveResponseResource(this, request, session);
|
|
500
380
|
this._forgetRequest(request);
|
|
501
381
|
};
|
|
@@ -574,7 +454,7 @@ export class Network {
|
|
|
574
454
|
_initializeNetworkIdleWaitTimeout() {
|
|
575
455
|
// Per-instance timeout so concurrent pages with different env values
|
|
576
456
|
// (or env values changed mid-run by tests) don't stomp each other.
|
|
577
|
-
this.networkIdleWaitTimeout = parseInt(process.env.PERCY_NETWORK_IDLE_WAIT_TIMEOUT
|
|
457
|
+
this.networkIdleWaitTimeout = parseInt(process.env.PERCY_NETWORK_IDLE_WAIT_TIMEOUT) || 30000;
|
|
578
458
|
if (this.networkIdleWaitTimeout > 60000) {
|
|
579
459
|
this.log.warn('Setting PERCY_NETWORK_IDLE_WAIT_TIMEOUT over 60000ms is not recommended. ' + 'If your page needs more than 60000ms to idle due to CPU/Network load, ' + 'its recommended to increase CI resources where this cli is running.');
|
|
580
460
|
}
|
|
@@ -613,31 +493,6 @@ function originURL(request) {
|
|
|
613
493
|
return normalizeURL((request.redirectChain[0] || request).url);
|
|
614
494
|
}
|
|
615
495
|
|
|
616
|
-
// Convert Fetch event responseHeaders ([{name, value}, …]) to a header object.
|
|
617
|
-
function headersArrayToObject(arr) {
|
|
618
|
-
let out = {};
|
|
619
|
-
if (!Array.isArray(arr)) return out;
|
|
620
|
-
for (let {
|
|
621
|
-
name,
|
|
622
|
-
value
|
|
623
|
-
} of arr) out[name] = value;
|
|
624
|
-
return out;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Returns { tooLarge, malformed, rawValue } for Content-Length classification.
|
|
628
|
-
function inspectContentLength(headers) {
|
|
629
|
-
let key = headers && Object.keys(headers).find(k => k.toLowerCase() === 'content-length');
|
|
630
|
-
let rawValue = key ? headers[key] : undefined;
|
|
631
|
-
let parsed = parseInt(rawValue, 10);
|
|
632
|
-
let tooLarge = Number.isFinite(parsed) && parsed > MAX_RESOURCE_SIZE;
|
|
633
|
-
let malformed = rawValue !== undefined && rawValue !== null && String(rawValue).length > 0 && !Number.isFinite(parsed);
|
|
634
|
-
return {
|
|
635
|
-
tooLarge,
|
|
636
|
-
malformed,
|
|
637
|
-
rawValue
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
496
|
// Validate domain for auto-allowlisting feature
|
|
642
497
|
// Only validates domains that returned 200 status
|
|
643
498
|
async function validateDomainForAllowlist(network, hostname, url, statusCode) {
|
|
@@ -719,10 +574,8 @@ async function sendResponseResource(network, request, session) {
|
|
|
719
574
|
}))
|
|
720
575
|
});
|
|
721
576
|
} else {
|
|
722
|
-
// interceptResponse:true triggers a second pause at the response stage. See _handleResponsePaused.
|
|
723
577
|
await send('Fetch.continueRequest', {
|
|
724
|
-
requestId: request.interceptId
|
|
725
|
-
interceptResponse: true
|
|
578
|
+
requestId: request.interceptId
|
|
726
579
|
});
|
|
727
580
|
}
|
|
728
581
|
} catch (error) {
|
|
@@ -756,68 +609,21 @@ async function sendResponseResource(network, request, session) {
|
|
|
756
609
|
}
|
|
757
610
|
}
|
|
758
611
|
|
|
759
|
-
//
|
|
760
|
-
// expose a partial Network domain where Network.getCookies throws
|
|
761
|
-
// "Internal error", so prefer the page's session whenever available and
|
|
762
|
-
// fall back to the request's own session otherwise.
|
|
763
|
-
export function pickCookieSession(network, session) {
|
|
764
|
-
var _network$page;
|
|
765
|
-
return ((_network$page = network.page) === null || _network$page === void 0 ? void 0 : _network$page.session) ?? session;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Decide whether to attach a Basic auth header to the Node-side direct fetch.
|
|
769
|
-
// The browser's URLLoader origin-scopes Basic auth; this fallback runs in
|
|
770
|
-
// Node, so we re-enforce the same-origin rule explicitly to avoid leaking
|
|
771
|
-
// credentials cross-origin. Malformed URLs fall through to `false` defensively.
|
|
772
|
-
export function shouldAttachAuth(authorization, requestUrl, snapshotUrl) {
|
|
773
|
-
if (!(authorization !== null && authorization !== void 0 && authorization.username)) return false;
|
|
774
|
-
try {
|
|
775
|
-
return new URL(requestUrl).origin === new URL(snapshotUrl).origin;
|
|
776
|
-
} catch {
|
|
777
|
-
return false;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Race a promise against a timeout. Resolves with the promise's value if it
|
|
782
|
-
// settles within `ms`, otherwise rejects with `new Error(message)`. The
|
|
783
|
-
// internal timer is always cleared so the event loop can exit cleanly.
|
|
784
|
-
export function raceWithTimeout(promise, ms, message) {
|
|
785
|
-
let timerId;
|
|
786
|
-
return Promise.race([promise, new Promise((_, reject) => {
|
|
787
|
-
timerId = setTimeout(() => reject(new Error(message)), ms);
|
|
788
|
-
})]).finally(() => clearTimeout(timerId));
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Server Content-Type wins; URL-extension mime is the fallback; binary default last.
|
|
792
|
-
export function resolveDirectFetchMime(responseHeaders, urlForLookup) {
|
|
793
|
-
var _responseHeaders$cont;
|
|
794
|
-
let serverMime = responseHeaders === null || responseHeaders === void 0 || (_responseHeaders$cont = responseHeaders['content-type']) === null || _responseHeaders$cont === void 0 ? void 0 : _responseHeaders$cont.split(';')[0].trim();
|
|
795
|
-
return serverMime || mime.lookup(urlForLookup) || 'application/octet-stream';
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Make a new request with Node based on a network request. Cookies are read
|
|
799
|
-
// from the page session because worker/auxiliary sessions have a partial
|
|
800
|
-
// Network domain where Network.getCookies throws "Internal error".
|
|
612
|
+
// Make a new request with Node based on a network request
|
|
801
613
|
async function makeDirectRequest(network, request, session) {
|
|
802
|
-
var _network$
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
} = await cookieSession.send('Network.getCookies', {
|
|
809
|
-
urls: [request.url]
|
|
810
|
-
}));
|
|
811
|
-
} catch (error) {
|
|
812
|
-
network.log.debug(`Network.getCookies unavailable for ${request.url}: ${error.message}`);
|
|
813
|
-
}
|
|
614
|
+
var _network$authorizatio;
|
|
615
|
+
const {
|
|
616
|
+
cookies
|
|
617
|
+
} = await session.send('Network.getCookies', {
|
|
618
|
+
urls: [request.url]
|
|
619
|
+
});
|
|
814
620
|
let headers = {
|
|
815
621
|
// add default browser
|
|
816
622
|
accept: '*/*',
|
|
817
623
|
'sec-fetch-site': 'same-origin',
|
|
818
624
|
'sec-fetch-mode': 'cors',
|
|
819
625
|
'sec-fetch-dest': 'font',
|
|
820
|
-
'sec-ch-ua': '"Chromium";v="
|
|
626
|
+
'sec-ch-ua': '"Chromium";v="123", "Google Chrome";v="123", "Not?A_Brand";v="99"',
|
|
821
627
|
'sec-ch-ua-mobile': '?0',
|
|
822
628
|
'sec-ch-ua-platform': '"macOS"',
|
|
823
629
|
'sec-fetch-user': '?1',
|
|
@@ -826,7 +632,8 @@ async function makeDirectRequest(network, request, session) {
|
|
|
826
632
|
// add applicable cookies
|
|
827
633
|
cookie: cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ')
|
|
828
634
|
};
|
|
829
|
-
if (
|
|
635
|
+
if ((_network$authorizatio = network.authorization) !== null && _network$authorizatio !== void 0 && _network$authorizatio.username) {
|
|
636
|
+
// include basic authorization username and password
|
|
830
637
|
let {
|
|
831
638
|
username,
|
|
832
639
|
password
|
|
@@ -837,56 +644,12 @@ async function makeDirectRequest(network, request, session) {
|
|
|
837
644
|
return makeRequest(request.url, {
|
|
838
645
|
buffer: true,
|
|
839
646
|
headers
|
|
840
|
-
}
|
|
841
|
-
body,
|
|
842
|
-
status: res.statusCode,
|
|
843
|
-
headers: res.headers
|
|
844
|
-
}));
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Capture a resource via direct HTTP fetch when the browser-side response
|
|
848
|
-
// never surfaces — Chrome 143+ fetches dedicated worker scripts in the browser
|
|
849
|
-
// process (PlzDedicatedWorker) so loadingFinished fires without a body on CDP.
|
|
850
|
-
async function captureResourceDirectly(network, request, session) {
|
|
851
|
-
let log = network.log;
|
|
852
|
-
let url = originURL(request);
|
|
853
|
-
let meta = {
|
|
854
|
-
...network.meta,
|
|
855
|
-
url
|
|
856
|
-
};
|
|
857
|
-
try {
|
|
858
|
-
log.debug('- Requesting resource directly (responseReceived timeout fallback)', meta);
|
|
859
|
-
let {
|
|
860
|
-
body,
|
|
861
|
-
status,
|
|
862
|
-
headers: responseHeaders
|
|
863
|
-
} = await raceWithTimeout(makeDirectRequest(network, request, session), DIRECT_FETCH_TIMEOUT, `Direct fetch timed out after ${DIRECT_FETCH_TIMEOUT}ms`);
|
|
864
|
-
if (body.length > MAX_RESOURCE_SIZE) {
|
|
865
|
-
logAssetInstrumentation(log, 'asset_not_uploaded', 'resource_too_large', {
|
|
866
|
-
url,
|
|
867
|
-
size: body.length,
|
|
868
|
-
snapshot: meta.snapshot
|
|
869
|
-
});
|
|
870
|
-
log.debug('- Skipping resource larger than 25MB', meta);
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
let urlObj = new URL(url);
|
|
874
|
-
let mimeType = resolveDirectFetchMime(responseHeaders, urlObj.origin + urlObj.pathname);
|
|
875
|
-
let resource = createResource(url, body, mimeType, {
|
|
876
|
-
status,
|
|
877
|
-
headers: {
|
|
878
|
-
'content-type': [mimeType]
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
log.debug(`- Saving direct-fetched resource sha=${resource.sha} mimetype=${mimeType}`, meta);
|
|
882
|
-
network.intercept.saveResource(resource);
|
|
883
|
-
} catch (error) {
|
|
884
|
-
log.debug(`Direct fetch failed for ${url} - ${error.message}`, meta);
|
|
885
|
-
}
|
|
647
|
+
});
|
|
886
648
|
}
|
|
887
649
|
|
|
888
650
|
// Save a resource from a request, skipping it if specific parameters are not met
|
|
889
651
|
async function saveResponseResource(network, request, session) {
|
|
652
|
+
var _response$headers;
|
|
890
653
|
let {
|
|
891
654
|
disableCache,
|
|
892
655
|
allowedHostnames,
|
|
@@ -900,8 +663,19 @@ async function saveResponseResource(network, request, session) {
|
|
|
900
663
|
url,
|
|
901
664
|
responseStatus: response === null || response === void 0 ? void 0 : response.status
|
|
902
665
|
};
|
|
903
|
-
//
|
|
904
|
-
//
|
|
666
|
+
// Checking for content length more than 100MB, to prevent websocket error which is governed by
|
|
667
|
+
// maxPayload option of websocket defaulted to 100MB.
|
|
668
|
+
// If content-length is more than our allowed 25MB, no need to process that resouce we can return log.
|
|
669
|
+
let contentLength = (_response$headers = response.headers) === null || _response$headers === void 0 ? void 0 : _response$headers[Object.keys(response.headers).find(key => key.toLowerCase() === 'content-length')];
|
|
670
|
+
contentLength = parseInt(contentLength);
|
|
671
|
+
if (contentLength > MAX_RESOURCE_SIZE) {
|
|
672
|
+
logAssetInstrumentation(log, 'asset_not_uploaded', 'resource_too_large', {
|
|
673
|
+
url,
|
|
674
|
+
size: contentLength,
|
|
675
|
+
snapshot: meta.snapshot
|
|
676
|
+
});
|
|
677
|
+
return log.debug('- Skipping resource larger than 25MB', meta);
|
|
678
|
+
}
|
|
905
679
|
let resource = network.intercept.getResource(url);
|
|
906
680
|
if (!resource || !resource.root && !resource.provided && disableCache) {
|
|
907
681
|
try {
|
|
@@ -993,9 +767,7 @@ async function saveResponseResource(network, request, session) {
|
|
|
993
767
|
// so request them directly.
|
|
994
768
|
if ((_mimeType = mimeType) !== null && _mimeType !== void 0 && _mimeType.includes('font') || detectedMime && detectedMime.includes('font')) {
|
|
995
769
|
log.debug('- Requesting asset directly', meta);
|
|
996
|
-
(
|
|
997
|
-
body
|
|
998
|
-
} = await makeDirectRequest(network, request, session));
|
|
770
|
+
body = await makeDirectRequest(network, request, session);
|
|
999
771
|
log.debug('- Got direct response', meta);
|
|
1000
772
|
}
|
|
1001
773
|
resource = createResource(url, body, mimeType, {
|
package/dist/percy.js
CHANGED
|
@@ -73,7 +73,7 @@ export class Percy {
|
|
|
73
73
|
// options which will become accessible via the `.config` property
|
|
74
74
|
..._options
|
|
75
75
|
} = {}) {
|
|
76
|
-
var _config$percy, _config$percy2;
|
|
76
|
+
var _config$percy, _config$percy2, _config$percy3;
|
|
77
77
|
_classPrivateMethodInitSpec(this, _Percy_brand);
|
|
78
78
|
_defineProperty(this, "log", logger('core'));
|
|
79
79
|
_defineProperty(this, "readyState", null);
|
|
@@ -85,6 +85,7 @@ export class Percy {
|
|
|
85
85
|
});
|
|
86
86
|
labels ?? (labels = (_config$percy = config.percy) === null || _config$percy === void 0 ? void 0 : _config$percy.labels);
|
|
87
87
|
deferUploads ?? (deferUploads = (_config$percy2 = config.percy) === null || _config$percy2 === void 0 ? void 0 : _config$percy2.deferUploads);
|
|
88
|
+
archiveDir ?? (archiveDir = (_config$percy3 = config.percy) === null || _config$percy3 === void 0 ? void 0 : _config$percy3.archiveDir);
|
|
88
89
|
if (archiveDir) skipUploads = skipUploads != null ? skipUploads : true;
|
|
89
90
|
this.config = config;
|
|
90
91
|
this.cliStartTime = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.31.15-
|
|
3
|
+
"version": "1.31.15-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": "
|
|
12
|
+
"tag": "beta"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=14"
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"test:types": "tsd"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@percy/client": "1.31.15-
|
|
48
|
-
"@percy/config": "1.31.15-
|
|
49
|
-
"@percy/dom": "1.31.15-
|
|
50
|
-
"@percy/logger": "1.31.15-
|
|
51
|
-
"@percy/monitoring": "1.31.15-
|
|
52
|
-
"@percy/webdriver-utils": "1.31.15-
|
|
47
|
+
"@percy/client": "1.31.15-beta.0",
|
|
48
|
+
"@percy/config": "1.31.15-beta.0",
|
|
49
|
+
"@percy/dom": "1.31.15-beta.0",
|
|
50
|
+
"@percy/logger": "1.31.15-beta.0",
|
|
51
|
+
"@percy/monitoring": "1.31.15-beta.0",
|
|
52
|
+
"@percy/webdriver-utils": "1.31.15-beta.0",
|
|
53
53
|
"content-disposition": "^0.5.4",
|
|
54
54
|
"cross-spawn": "^7.0.3",
|
|
55
55
|
"extract-zip": "^2.0.1",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"yaml": "^2.4.1"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"@percy/cli-doctor": "1.31.15-
|
|
66
|
+
"@percy/cli-doctor": "1.31.15-beta.0"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "b2012ce5dae37e5009dd3f4190454bb9d9d118e3"
|
|
69
69
|
}
|
package/test/helpers/server.js
CHANGED
|
@@ -28,14 +28,8 @@ export function createTestServer({ default: defaultReply, ...replies }, port = 8
|
|
|
28
28
|
server.route(async (req, res, next) => {
|
|
29
29
|
let pathname = req.url.pathname;
|
|
30
30
|
if (req.url.search) pathname += req.url.search;
|
|
31
|
-
let reply = replies[pathname] || defaultReply;
|
|
32
|
-
// Chrome >=128 auto-fetches /favicon.ico on every navigation; reply 204
|
|
33
|
-
// by default so it doesn't pollute snapshot resources. Tests can still
|
|
34
|
-
// override via `server.reply('/favicon.ico', ...)`.
|
|
35
|
-
if (req.url.pathname === '/favicon.ico' && !replies['/favicon.ico']) {
|
|
36
|
-
return res.writeHead(204).end();
|
|
37
|
-
}
|
|
38
31
|
server.requests.push(req.body ? [pathname, req.body, req.headers] : [pathname, req.headers]);
|
|
32
|
+
let reply = replies[pathname] || defaultReply;
|
|
39
33
|
return reply ? await reply(req, res) : next();
|
|
40
34
|
});
|
|
41
35
|
|