@percy/dom 1.31.4 → 1.31.5-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/bundle.js +213 -21
- package/package.json +3 -3
package/dist/bundle.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
process.env.__PERCY_BROWSERIFIED__ = true;
|
|
8
8
|
|
|
9
9
|
// Creates a resource object from an element's unique ID and data URL
|
|
10
|
-
function resourceFromDataURL(uid, dataURL) {
|
|
10
|
+
function resourceFromDataURL$1(uid, dataURL) {
|
|
11
11
|
// split dataURL into desired parts
|
|
12
12
|
let [data, content] = dataURL.split(',');
|
|
13
13
|
let [, mimetype] = data.split(':');
|
|
@@ -142,8 +142,8 @@
|
|
|
142
142
|
(_dom$querySelector = dom.querySelector('head')) === null || _dom$querySelector === void 0 || _dom$querySelector.prepend($base);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Recursively serializes iframe documents into srcdoc attributes.
|
|
146
|
-
function serializeFrames({
|
|
145
|
+
// Recursively serializes iframe documents into srcdoc attributes (ASYNC).
|
|
146
|
+
async function serializeFrames({
|
|
147
147
|
dom,
|
|
148
148
|
clone,
|
|
149
149
|
warnings,
|
|
@@ -170,8 +170,8 @@
|
|
|
170
170
|
// the frame has yet to load and wasn't built with js, it is unsafe to serialize
|
|
171
171
|
if (!builtWithJs && !frame.contentWindow.performance.timing.loadEventEnd) continue;
|
|
172
172
|
|
|
173
|
-
// recersively serialize contents
|
|
174
|
-
let serialized = serializeDOM({
|
|
173
|
+
// recersively serialize contents (AWAIT async call)
|
|
174
|
+
let serialized = await serializeDOM({
|
|
175
175
|
domTransformation: setBaseURI,
|
|
176
176
|
dom: frame.contentDocument,
|
|
177
177
|
enableJavaScript,
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// Returns a mostly random uid.
|
|
199
|
-
function uid() {
|
|
199
|
+
function uid$1() {
|
|
200
200
|
return `_${Math.random().toString(36).substr(2, 9)}`;
|
|
201
201
|
}
|
|
202
202
|
function markElement(domElement, disableShadowDOM, forceShadowAsLightDOM) {
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
// Mark elements that are to be serialized later with a data attribute.
|
|
205
205
|
if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) {
|
|
206
206
|
if (!domElement.getAttribute('data-percy-element-id')) {
|
|
207
|
-
domElement.setAttribute('data-percy-element-id', uid());
|
|
207
|
+
domElement.setAttribute('data-percy-element-id', uid$1());
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
domElement.setAttribute('data-percy-shadow-host', '');
|
|
216
216
|
}
|
|
217
217
|
if (!domElement.getAttribute('data-percy-element-id')) {
|
|
218
|
-
domElement.setAttribute('data-percy-element-id', uid());
|
|
218
|
+
domElement.setAttribute('data-percy-element-id', uid$1());
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
}
|
|
251
251
|
function createStyleResource(styleSheet) {
|
|
252
252
|
const styles = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
|
|
253
|
-
let resource = resourceFromText(uid(), 'text/css', styles);
|
|
253
|
+
let resource = resourceFromText(uid$1(), 'text/css', styles);
|
|
254
254
|
return resource;
|
|
255
255
|
}
|
|
256
256
|
function serializeCSSOM(ctx) {
|
|
@@ -374,9 +374,11 @@
|
|
|
374
374
|
img.setAttribute('data-percy-canvas-serialized', '');
|
|
375
375
|
img.setAttribute('data-percy-serialized-attribute-src', imageUrl);
|
|
376
376
|
|
|
377
|
-
// set
|
|
378
|
-
if (
|
|
379
|
-
|
|
377
|
+
// set a default max width to account for canvases that might resize with JS
|
|
378
|
+
// Check if width is "static" (fixed pixels) vs "dynamic" (%, vw, etc.)
|
|
379
|
+
const hasStaticWidth = canvas.style.width && canvas.style.width.match(/^\d+px$/);
|
|
380
|
+
if (!hasStaticWidth) {
|
|
381
|
+
img.style.maxWidth = img.style.maxWidth || '100%';
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
// insert the image into the cloned DOM and remove the cloned canvas element
|
|
@@ -414,7 +416,7 @@
|
|
|
414
416
|
if (!dataUrl || dataUrl === 'data:,') continue;
|
|
415
417
|
|
|
416
418
|
// create a resource for the canvas data
|
|
417
|
-
let resource = resourceFromDataURL(percyElementId, dataUrl);
|
|
419
|
+
let resource = resourceFromDataURL$1(percyElementId, dataUrl);
|
|
418
420
|
resources.add(resource);
|
|
419
421
|
|
|
420
422
|
// create and insert image element with the resource URL
|
|
@@ -465,7 +467,7 @@
|
|
|
465
467
|
if (!dataUrl || dataUrl === 'data:,') continue;
|
|
466
468
|
|
|
467
469
|
// create a resource from the serialized data url
|
|
468
|
-
let resource = resourceFromDataURL(videoId, dataUrl);
|
|
470
|
+
let resource = resourceFromDataURL$1(videoId, dataUrl);
|
|
469
471
|
resources.add(resource);
|
|
470
472
|
|
|
471
473
|
// use a data attribute to avoid making a real request
|
|
@@ -476,6 +478,188 @@
|
|
|
476
478
|
}
|
|
477
479
|
}
|
|
478
480
|
|
|
481
|
+
/* global fetch, FileReader */
|
|
482
|
+
|
|
483
|
+
// Helper: Generate unique ID for resources
|
|
484
|
+
function uid() {
|
|
485
|
+
return `_${Math.random().toString(36).substr(2, 9)}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Helper: Create resource from data URL
|
|
489
|
+
function resourceFromDataURL(id, dataURL) {
|
|
490
|
+
let [data, content] = dataURL.split(',');
|
|
491
|
+
let [, mimetype] = data.split(':');
|
|
492
|
+
[mimetype] = mimetype.split(';');
|
|
493
|
+
let [, ext] = mimetype.split('/');
|
|
494
|
+
let path = `/__serialized__/${id}.${ext}`;
|
|
495
|
+
let url = new URL(path, document.URL).toString();
|
|
496
|
+
return {
|
|
497
|
+
url,
|
|
498
|
+
content,
|
|
499
|
+
mimetype
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Find all blob URLs in the DOM
|
|
504
|
+
function findAllBlobUrls(dom) {
|
|
505
|
+
const blobUrls = [];
|
|
506
|
+
|
|
507
|
+
// Find blob URLs in src attributes (img, video, audio, source, iframe)
|
|
508
|
+
const elementsWithSrc = dom.querySelectorAll('[src^="blob:"]');
|
|
509
|
+
for (const el of elementsWithSrc) {
|
|
510
|
+
if (!el.getAttribute('data-percy-element-id')) {
|
|
511
|
+
el.setAttribute('data-percy-element-id', uid());
|
|
512
|
+
}
|
|
513
|
+
blobUrls.push({
|
|
514
|
+
element: el,
|
|
515
|
+
blobUrl: el.src,
|
|
516
|
+
property: 'src',
|
|
517
|
+
id: el.getAttribute('data-percy-element-id')
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Find blob URLs in href attributes
|
|
522
|
+
// Skip stylesheet links as they're handled by serializeCSSOM
|
|
523
|
+
const elementsWithHref = dom.querySelectorAll('[href^="blob:"]');
|
|
524
|
+
for (const el of elementsWithHref) {
|
|
525
|
+
// Skip <link rel="stylesheet"> elements - they're handled by serializeCSSOM
|
|
526
|
+
if (el.tagName === 'LINK' && el.getAttribute('rel') === 'stylesheet') {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (!el.getAttribute('data-percy-element-id')) {
|
|
530
|
+
el.setAttribute('data-percy-element-id', uid());
|
|
531
|
+
}
|
|
532
|
+
blobUrls.push({
|
|
533
|
+
element: el,
|
|
534
|
+
blobUrl: el.href,
|
|
535
|
+
property: 'href',
|
|
536
|
+
id: el.getAttribute('data-percy-element-id')
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Find blob URLs in inline styles (background-image, etc.)
|
|
541
|
+
const allElements = dom.querySelectorAll('*');
|
|
542
|
+
for (const el of allElements) {
|
|
543
|
+
const inlineStyle = el.getAttribute('style');
|
|
544
|
+
if (inlineStyle && inlineStyle.includes('blob:')) {
|
|
545
|
+
const blobMatches = inlineStyle.match(/url\(["']?(blob:[^"')]+)["']?\)/g);
|
|
546
|
+
if (blobMatches) {
|
|
547
|
+
if (!el.getAttribute('data-percy-element-id')) {
|
|
548
|
+
el.setAttribute('data-percy-element-id', uid());
|
|
549
|
+
}
|
|
550
|
+
for (const match of blobMatches) {
|
|
551
|
+
const urlMatch = match.match(/url\(["']?(blob:[^"')]+)["']?\)/);
|
|
552
|
+
blobUrls.push({
|
|
553
|
+
element: el,
|
|
554
|
+
blobUrl: urlMatch[1],
|
|
555
|
+
property: 'style',
|
|
556
|
+
id: el.getAttribute('data-percy-element-id')
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return blobUrls;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Convert blob URL to Percy resource (async)
|
|
566
|
+
async function convertBlobToResource(blobInfo) {
|
|
567
|
+
const {
|
|
568
|
+
element,
|
|
569
|
+
blobUrl,
|
|
570
|
+
property,
|
|
571
|
+
id
|
|
572
|
+
} = blobInfo;
|
|
573
|
+
try {
|
|
574
|
+
// Fetch the blob data
|
|
575
|
+
const response = await fetch(blobUrl);
|
|
576
|
+
const blob = await response.blob();
|
|
577
|
+
|
|
578
|
+
// Convert blob to data URL using FileReader
|
|
579
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
580
|
+
/* eslint-disable-next-line no-undef */
|
|
581
|
+
const reader = new FileReader();
|
|
582
|
+
reader.onloadend = () => resolve(reader.result);
|
|
583
|
+
reader.onerror = reject;
|
|
584
|
+
reader.readAsDataURL(blob);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Create Percy resource
|
|
588
|
+
const resource = resourceFromDataURL(id, dataUrl);
|
|
589
|
+
|
|
590
|
+
/* istanbul ignore else: property is always one of 'src', 'href', or 'style' */
|
|
591
|
+
if (property === 'src') {
|
|
592
|
+
element.removeAttribute('src');
|
|
593
|
+
element.setAttribute('data-percy-serialized-attribute-src', resource.url);
|
|
594
|
+
} else if (property === 'href') {
|
|
595
|
+
element.removeAttribute('href');
|
|
596
|
+
element.setAttribute('data-percy-serialized-attribute-href', resource.url);
|
|
597
|
+
} else if (property === 'style') {
|
|
598
|
+
const currentStyle = element.getAttribute('style');
|
|
599
|
+
const updatedStyle = currentStyle.replace(blobUrl, resource.url);
|
|
600
|
+
element.setAttribute('style', updatedStyle);
|
|
601
|
+
}
|
|
602
|
+
return resource;
|
|
603
|
+
} catch (err) {
|
|
604
|
+
throw new Error(`Failed to convert blob URL ${blobUrl}: ${err.message}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Process lazy-loaded images (data-src to src)
|
|
609
|
+
function processLazyLoadedImages(dom) {
|
|
610
|
+
const lazyImages = dom.querySelectorAll('img[data-src], source[data-src]');
|
|
611
|
+
let processedCount = 0;
|
|
612
|
+
for (const el of lazyImages) {
|
|
613
|
+
const dataSrc = el.getAttribute('data-src');
|
|
614
|
+
if (dataSrc) {
|
|
615
|
+
try {
|
|
616
|
+
const url = new URL(dataSrc, window.location.origin);
|
|
617
|
+
if (['http:', 'https:', 'data:', 'blob:'].includes(url.protocol)) {
|
|
618
|
+
el.setAttribute('src', url.href);
|
|
619
|
+
processedCount++;
|
|
620
|
+
}
|
|
621
|
+
} catch (e) {
|
|
622
|
+
// Invalid URL, skip
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return processedCount;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Preprocess dynamic resources (async) - runs before cloning
|
|
630
|
+
async function preprocessDynamicResources(dom, resources, warnings) {
|
|
631
|
+
const processedResources = [];
|
|
632
|
+
|
|
633
|
+
// 1. Process lazy-loaded images
|
|
634
|
+
const lazyCount = processLazyLoadedImages(dom);
|
|
635
|
+
if (lazyCount > 0) {
|
|
636
|
+
console.debug(`Percy: Processed ${lazyCount} lazy-loaded images`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// 2. Find all blob URLs
|
|
640
|
+
const blobUrls = findAllBlobUrls(dom);
|
|
641
|
+
if (blobUrls.length > 0) {
|
|
642
|
+
console.debug(`Percy: Found ${blobUrls.length} blob URLs to convert`);
|
|
643
|
+
|
|
644
|
+
// 3. Convert all blob URLs to resources (parallel)
|
|
645
|
+
const conversions = blobUrls.map(blobInfo => convertBlobToResource(blobInfo).then(resource => {
|
|
646
|
+
resources.add(resource);
|
|
647
|
+
processedResources.push(resource);
|
|
648
|
+
return resource;
|
|
649
|
+
}).catch(err => {
|
|
650
|
+
warnings.add(err.message);
|
|
651
|
+
console.warn(`Percy: ${err.message}`);
|
|
652
|
+
return null;
|
|
653
|
+
}));
|
|
654
|
+
|
|
655
|
+
// Wait for all conversions
|
|
656
|
+
await Promise.all(conversions);
|
|
657
|
+
const successCount = processedResources.length;
|
|
658
|
+
console.debug(`Percy: Successfully converted ${successCount}/${blobUrls.length} blob URLs`);
|
|
659
|
+
}
|
|
660
|
+
return processedResources;
|
|
661
|
+
}
|
|
662
|
+
|
|
479
663
|
// Drop loading attribute. We do not scroll page in discovery stage but we want to make sure that
|
|
480
664
|
// all resources are requested, so we drop loading attribute [as it can be set to lazy]
|
|
481
665
|
function dropLoadingAttribute(domElement) {
|
|
@@ -527,7 +711,7 @@
|
|
|
527
711
|
if (base64String == null) return;
|
|
528
712
|
if (!cache.has(base64String)) {
|
|
529
713
|
// create a resource from the serialized data url
|
|
530
|
-
let resource = resourceFromText(uid(), mimetype, base64String);
|
|
714
|
+
let resource = resourceFromText(uid$1(), mimetype, base64String);
|
|
531
715
|
resources.add(resource);
|
|
532
716
|
cache.set(base64String, resource.url);
|
|
533
717
|
}
|
|
@@ -747,9 +931,9 @@
|
|
|
747
931
|
// include the doctype with the html string
|
|
748
932
|
return doctype(ctx.dom) + html;
|
|
749
933
|
}
|
|
750
|
-
function serializeElements(ctx) {
|
|
934
|
+
async function serializeElements(ctx) {
|
|
751
935
|
serializeInputElements(ctx);
|
|
752
|
-
serializeFrames(ctx);
|
|
936
|
+
await serializeFrames(ctx); // AWAIT async iframe serialization
|
|
753
937
|
serializeVideos(ctx);
|
|
754
938
|
if (!ctx.enableJavaScript) {
|
|
755
939
|
serializeCSSOM(ctx);
|
|
@@ -764,7 +948,8 @@
|
|
|
764
948
|
// getHTML requires shadowRoot to be passed explicitly
|
|
765
949
|
// to serialize the shadow elements properly
|
|
766
950
|
ctx.shadowRootElements.push(cloneShadowHost.shadowRoot);
|
|
767
|
-
serializeElements({
|
|
951
|
+
await serializeElements({
|
|
952
|
+
// AWAIT recursive call for shadow DOM
|
|
768
953
|
...ctx,
|
|
769
954
|
dom: shadowHost.shadowRoot,
|
|
770
955
|
clone: cloneShadowHost.shadowRoot
|
|
@@ -792,8 +977,8 @@
|
|
|
792
977
|
window.resizeCount = 0;
|
|
793
978
|
}
|
|
794
979
|
|
|
795
|
-
// Serializes a document and returns the resulting DOM string.
|
|
796
|
-
function serializeDOM(options) {
|
|
980
|
+
// Serializes a document and returns the resulting DOM string (ASYNC).
|
|
981
|
+
async function serializeDOM(options) {
|
|
797
982
|
var _ctx$clone$body;
|
|
798
983
|
let {
|
|
799
984
|
dom = document,
|
|
@@ -822,8 +1007,15 @@
|
|
|
822
1007
|
forceShadowAsLightDOM
|
|
823
1008
|
};
|
|
824
1009
|
ctx.dom = dom;
|
|
1010
|
+
|
|
1011
|
+
// STEP 1: Preprocess dynamic resources (async) - before cloning
|
|
1012
|
+
await preprocessDynamicResources(ctx.dom, ctx.resources, ctx.warnings);
|
|
1013
|
+
|
|
1014
|
+
// STEP 2: Clone the DOM
|
|
825
1015
|
ctx.clone = cloneNodeAndShadow(ctx);
|
|
826
|
-
|
|
1016
|
+
|
|
1017
|
+
// STEP 3: Serialize elements (AWAIT async call)
|
|
1018
|
+
await serializeElements(ctx);
|
|
827
1019
|
if (domTransformation) {
|
|
828
1020
|
try {
|
|
829
1021
|
// eslint-disable-next-line no-eval
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/dom",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.5-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": "
|
|
12
|
+
"tag": "beta"
|
|
13
13
|
},
|
|
14
14
|
"main": "dist/bundle.js",
|
|
15
15
|
"browser": "dist/bundle.js",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"interactor.js": "^2.0.0-beta.10"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "074c9bfd98f79d863b3375bff9d72602cff2798d"
|
|
39
39
|
}
|