@percy/core 1.31.9 → 1.31.10-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 +17 -1
- package/dist/config.js +38 -0
- package/dist/percy.js +5 -1
- package/dist/utils.js +120 -0
- package/package.json +9 -9
package/dist/api.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path, { dirname, resolve } from 'path';
|
|
3
3
|
import logger from '@percy/logger';
|
|
4
4
|
import { normalize } from '@percy/config/utils';
|
|
5
|
-
import { getPackageJSON, Server, percyAutomateRequestHandler, percyBuildEventHandler } from './utils.js';
|
|
5
|
+
import { getPackageJSON, Server, percyAutomateRequestHandler, percyBuildEventHandler, computeResponsiveWidths } from './utils.js';
|
|
6
6
|
import WebdriverUtils from '@percy/webdriver-utils';
|
|
7
7
|
import { handleSyncJob } from './snapshot.js';
|
|
8
8
|
// Previously, we used `createRequire(import.meta.url).resolve` to resolve the path to the module.
|
|
@@ -102,6 +102,22 @@ export function createPercyServer(percy, port) {
|
|
|
102
102
|
type: percy.client.tokenType()
|
|
103
103
|
});
|
|
104
104
|
})
|
|
105
|
+
// compute widths configuration with heights
|
|
106
|
+
.route('get', '/percy/widths-config', (req, res) => {
|
|
107
|
+
// Parse widths from query parameters (e.g., ?widths=375,1280)
|
|
108
|
+
const widthsParam = req.url.searchParams.get('widths');
|
|
109
|
+
const userPassedWidths = widthsParam ? widthsParam.split(',').map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w)) : [];
|
|
110
|
+
const eligibleWidths = {
|
|
111
|
+
mobile: percy.deviceDetails ? percy.deviceDetails.map(d => d.width) : [],
|
|
112
|
+
config: percy.config.snapshot.widths
|
|
113
|
+
};
|
|
114
|
+
const deviceDetails = percy.deviceDetails || [];
|
|
115
|
+
const widths = computeResponsiveWidths(userPassedWidths, eligibleWidths, deviceDetails);
|
|
116
|
+
return res.json(200, {
|
|
117
|
+
widths,
|
|
118
|
+
success: true
|
|
119
|
+
});
|
|
120
|
+
})
|
|
105
121
|
// get or set config options
|
|
106
122
|
.route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
|
|
107
123
|
config: req.body ? percy.set(req.body) : percy.config,
|
package/dist/config.js
CHANGED
|
@@ -917,6 +917,44 @@ export const snapshotSchema = {
|
|
|
917
917
|
items: {
|
|
918
918
|
type: 'string'
|
|
919
919
|
}
|
|
920
|
+
},
|
|
921
|
+
corsIframes: {
|
|
922
|
+
type: 'array',
|
|
923
|
+
items: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
additionalProperties: false,
|
|
926
|
+
properties: {
|
|
927
|
+
frameUrl: {
|
|
928
|
+
type: 'string',
|
|
929
|
+
description: 'The URL of the cross-origin iframe'
|
|
930
|
+
},
|
|
931
|
+
iframeData: {
|
|
932
|
+
type: 'object',
|
|
933
|
+
additionalProperties: false,
|
|
934
|
+
properties: {
|
|
935
|
+
percyElementId: {
|
|
936
|
+
type: 'string',
|
|
937
|
+
description: 'Unique identifier for the iframe element in the DOM'
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
iframeSnapshot: {
|
|
942
|
+
type: 'object',
|
|
943
|
+
required: ['html'],
|
|
944
|
+
additionalProperties: false,
|
|
945
|
+
properties: {
|
|
946
|
+
html: {
|
|
947
|
+
type: 'string',
|
|
948
|
+
description: 'Serialized HTML content of the iframe'
|
|
949
|
+
},
|
|
950
|
+
resources: {
|
|
951
|
+
$ref: '/snapshot/dom#/properties/domSnapshot/oneOf/1/properties/resources',
|
|
952
|
+
description: 'Resources discovered within the iframe'
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
920
958
|
}
|
|
921
959
|
}
|
|
922
960
|
}, {
|
package/dist/percy.js
CHANGED
|
@@ -13,7 +13,7 @@ import logger from '@percy/logger';
|
|
|
13
13
|
import { getProxy } from '@percy/client/utils';
|
|
14
14
|
import Browser from './browser.js';
|
|
15
15
|
import Pako from 'pako';
|
|
16
|
-
import { base64encode, generatePromise, yieldAll, yieldTo, redactSecrets, detectSystemProxyAndLog, checkSDKVersion } from './utils.js';
|
|
16
|
+
import { base64encode, generatePromise, yieldAll, yieldTo, redactSecrets, detectSystemProxyAndLog, checkSDKVersion, processCorsIframes } from './utils.js';
|
|
17
17
|
import { createPercyServer, createStaticServer } from './api.js';
|
|
18
18
|
import { gatherSnapshots, createSnapshotsQueue, validateSnapshotOptions } from './snapshot.js';
|
|
19
19
|
import { discoverSnapshotResources, createDiscoveryQueue } from './discovery.js';
|
|
@@ -449,6 +449,10 @@ export class Percy {
|
|
|
449
449
|
|
|
450
450
|
// validate options and add client & environment info
|
|
451
451
|
options = validateSnapshotOptions(options);
|
|
452
|
+
// process CORS iframes in domSnapshot before validation
|
|
453
|
+
if (options.domSnapshot) {
|
|
454
|
+
options.domSnapshot = processCorsIframes(options.domSnapshot);
|
|
455
|
+
}
|
|
452
456
|
this.client.addClientInfo(options.clientInfo);
|
|
453
457
|
this.client.addEnvironmentInfo(options.environmentInfo);
|
|
454
458
|
|
package/dist/utils.js
CHANGED
|
@@ -26,6 +26,87 @@ export function normalizeURL(url) {
|
|
|
26
26
|
return `${protocol}//${host}${pathname}${search}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Appends a search parameter to a URL. Returns the original URL if the value is not provided.
|
|
30
|
+
export function appendUrlSearchParam(urlString, key, value) {
|
|
31
|
+
if (!value) return urlString;
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(urlString);
|
|
34
|
+
url.searchParams.set(key, String(value));
|
|
35
|
+
return url.toString();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger('core:utils').debug(`Failed to append search param to URL: ${urlString}`, error);
|
|
38
|
+
return urlString;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Process CORS iframes in a single domSnapshot object
|
|
43
|
+
export function processCorsIframesInDomSnapshot(domSnapshot) {
|
|
44
|
+
var _domSnapshot$corsIfra;
|
|
45
|
+
if (!(domSnapshot !== null && domSnapshot !== void 0 && (_domSnapshot$corsIfra = domSnapshot.corsIframes) !== null && _domSnapshot$corsIfra !== void 0 && _domSnapshot$corsIfra.length)) {
|
|
46
|
+
return domSnapshot;
|
|
47
|
+
}
|
|
48
|
+
const crossOriginFrames = domSnapshot.corsIframes;
|
|
49
|
+
|
|
50
|
+
// Initialize resources array if it doesn't exist
|
|
51
|
+
if (!domSnapshot.resources) {
|
|
52
|
+
domSnapshot.resources = [];
|
|
53
|
+
}
|
|
54
|
+
for (const frame of crossOriginFrames) {
|
|
55
|
+
const {
|
|
56
|
+
iframeData,
|
|
57
|
+
iframeSnapshot,
|
|
58
|
+
frameUrl
|
|
59
|
+
} = frame;
|
|
60
|
+
|
|
61
|
+
// Validate required fields and skip malformed entries
|
|
62
|
+
if (!frameUrl || !(iframeSnapshot !== null && iframeSnapshot !== void 0 && iframeSnapshot.html)) {
|
|
63
|
+
logger('core:utils').debug('Skipping malformed corsIframes entry: missing frameUrl or iframeSnapshot.html', frame);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// width is only passed in case of responsiveSnapshotCapture
|
|
68
|
+
// Build frame URL with width parameter if available
|
|
69
|
+
const frameUrlWithWidth = domSnapshot.width ? appendUrlSearchParam(frameUrl, 'percy_width', domSnapshot.width) : frameUrl;
|
|
70
|
+
|
|
71
|
+
// Add iframe snapshot resources to main resources
|
|
72
|
+
if (iframeSnapshot !== null && iframeSnapshot !== void 0 && iframeSnapshot.resources) {
|
|
73
|
+
domSnapshot.resources.push(...iframeSnapshot.resources);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create a new resource for the iframe's HTML
|
|
77
|
+
const iframeResource = {
|
|
78
|
+
url: frameUrlWithWidth,
|
|
79
|
+
content: iframeSnapshot.html,
|
|
80
|
+
mimetype: 'text/html'
|
|
81
|
+
};
|
|
82
|
+
domSnapshot.resources.push(iframeResource);
|
|
83
|
+
|
|
84
|
+
// Update iframe src attribute in HTML
|
|
85
|
+
if (iframeData !== null && iframeData !== void 0 && iframeData.percyElementId) {
|
|
86
|
+
const escapedId = iframeData.percyElementId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
|
|
88
|
+
const regex = new RegExp(`(<iframe[^>]*data-percy-element-id=["']${escapedId}["'][^>]*>)`);
|
|
89
|
+
const match = domSnapshot.html.match(regex);
|
|
90
|
+
/* istanbul ignore next: iframe matching logic depends on DOM structure */
|
|
91
|
+
if (match) {
|
|
92
|
+
const iframeTag = match[1];
|
|
93
|
+
const newIframeTag = iframeTag.replace(/src="[^"]*"/i, `src="${frameUrlWithWidth}"`);
|
|
94
|
+
domSnapshot.html = domSnapshot.html.replace(iframeTag, newIframeTag);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return domSnapshot;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Process CORS iframes - handles both single object and array of domSnapshots
|
|
102
|
+
export function processCorsIframes(domSnapshot) {
|
|
103
|
+
if (!domSnapshot) return domSnapshot;
|
|
104
|
+
if (Array.isArray(domSnapshot)) {
|
|
105
|
+
return domSnapshot.map(snap => processCorsIframesInDomSnapshot(snap));
|
|
106
|
+
}
|
|
107
|
+
return processCorsIframesInDomSnapshot(domSnapshot);
|
|
108
|
+
}
|
|
109
|
+
|
|
29
110
|
/**
|
|
30
111
|
* Detects font MIME type from file content by checking magic bytes.
|
|
31
112
|
* Handles string-based signatures (WOFF, OTTO) and binary signatures (TTF).
|
|
@@ -737,4 +818,43 @@ export async function checkSDKVersion(clientInfo) {
|
|
|
737
818
|
} catch (error) {
|
|
738
819
|
log.debug('Could not check SDK version', error);
|
|
739
820
|
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Computes widths configuration with heights for responsive snapshot capture
|
|
825
|
+
* @param {Array<number>} userPassedWidths - Widths passed by the user
|
|
826
|
+
* @param {Object} eligibleWidths - Object containing mobile and config widths
|
|
827
|
+
* @param {Array<Object>} deviceDetails - Array of device objects with width and height
|
|
828
|
+
* @returns {Array<Object>} Array of width objects sorted in ascending order
|
|
829
|
+
*/
|
|
830
|
+
export function computeResponsiveWidths(userPassedWidths, eligibleWidths, deviceDetails) {
|
|
831
|
+
const widthHeightMap = new Map();
|
|
832
|
+
|
|
833
|
+
// Add mobile widths with their associated heights from deviceDetails
|
|
834
|
+
if (eligibleWidths.mobile.length !== 0) {
|
|
835
|
+
eligibleWidths.mobile.forEach(width => {
|
|
836
|
+
if (!widthHeightMap.has(width)) {
|
|
837
|
+
const deviceInfo = deviceDetails.find(device => device.width === width);
|
|
838
|
+
if (deviceInfo !== null && deviceInfo !== void 0 && deviceInfo.height) {
|
|
839
|
+
widthHeightMap.set(width, {
|
|
840
|
+
width,
|
|
841
|
+
height: deviceInfo.height
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Add user passed or config widths without height
|
|
849
|
+
// If a width exists in both mobile and user-passed/config, user-passed/config takes precedence (without height)
|
|
850
|
+
// This ensures consistency with percy-storybook SDK behavior
|
|
851
|
+
const otherWidths = userPassedWidths.length !== 0 ? userPassedWidths : eligibleWidths.config;
|
|
852
|
+
otherWidths.forEach(width => {
|
|
853
|
+
widthHeightMap.set(width, {
|
|
854
|
+
width
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// Convert to array and sort by width in ascending order
|
|
859
|
+
return Array.from(widthHeightMap.values()).sort((a, b) => a.width - b.width);
|
|
740
860
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/core",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.10-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"
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"test:types": "tsd"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@percy/client": "1.31.
|
|
47
|
-
"@percy/config": "1.31.
|
|
48
|
-
"@percy/dom": "1.31.
|
|
49
|
-
"@percy/logger": "1.31.
|
|
50
|
-
"@percy/monitoring": "1.31.
|
|
51
|
-
"@percy/webdriver-utils": "1.31.
|
|
46
|
+
"@percy/client": "1.31.10-beta.0",
|
|
47
|
+
"@percy/config": "1.31.10-beta.0",
|
|
48
|
+
"@percy/dom": "1.31.10-beta.0",
|
|
49
|
+
"@percy/logger": "1.31.10-beta.0",
|
|
50
|
+
"@percy/monitoring": "1.31.10-beta.0",
|
|
51
|
+
"@percy/webdriver-utils": "1.31.10-beta.0",
|
|
52
52
|
"content-disposition": "^0.5.4",
|
|
53
53
|
"cross-spawn": "^7.0.3",
|
|
54
54
|
"extract-zip": "^2.0.1",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"ws": "^8.17.1",
|
|
62
62
|
"yaml": "^2.4.1"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "8e9210d99f740d2da67960e85b2e745693038f62"
|
|
65
65
|
}
|