@percy/dom 1.31.5-beta.0 → 1.31.5-beta.2

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.
Files changed (2) hide show
  1. package/dist/bundle.js +208 -18
  2. package/package.json +2 -2
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) {
@@ -416,7 +416,7 @@
416
416
  if (!dataUrl || dataUrl === 'data:,') continue;
417
417
 
418
418
  // create a resource for the canvas data
419
- let resource = resourceFromDataURL(percyElementId, dataUrl);
419
+ let resource = resourceFromDataURL$1(percyElementId, dataUrl);
420
420
  resources.add(resource);
421
421
 
422
422
  // create and insert image element with the resource URL
@@ -467,7 +467,7 @@
467
467
  if (!dataUrl || dataUrl === 'data:,') continue;
468
468
 
469
469
  // create a resource from the serialized data url
470
- let resource = resourceFromDataURL(videoId, dataUrl);
470
+ let resource = resourceFromDataURL$1(videoId, dataUrl);
471
471
  resources.add(resource);
472
472
 
473
473
  // use a data attribute to avoid making a real request
@@ -478,6 +478,188 @@
478
478
  }
479
479
  }
480
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
+
481
663
  // Drop loading attribute. We do not scroll page in discovery stage but we want to make sure that
482
664
  // all resources are requested, so we drop loading attribute [as it can be set to lazy]
483
665
  function dropLoadingAttribute(domElement) {
@@ -529,7 +711,7 @@
529
711
  if (base64String == null) return;
530
712
  if (!cache.has(base64String)) {
531
713
  // create a resource from the serialized data url
532
- let resource = resourceFromText(uid(), mimetype, base64String);
714
+ let resource = resourceFromText(uid$1(), mimetype, base64String);
533
715
  resources.add(resource);
534
716
  cache.set(base64String, resource.url);
535
717
  }
@@ -749,9 +931,9 @@
749
931
  // include the doctype with the html string
750
932
  return doctype(ctx.dom) + html;
751
933
  }
752
- function serializeElements(ctx) {
934
+ async function serializeElements(ctx) {
753
935
  serializeInputElements(ctx);
754
- serializeFrames(ctx);
936
+ await serializeFrames(ctx); // AWAIT async iframe serialization
755
937
  serializeVideos(ctx);
756
938
  if (!ctx.enableJavaScript) {
757
939
  serializeCSSOM(ctx);
@@ -766,7 +948,8 @@
766
948
  // getHTML requires shadowRoot to be passed explicitly
767
949
  // to serialize the shadow elements properly
768
950
  ctx.shadowRootElements.push(cloneShadowHost.shadowRoot);
769
- serializeElements({
951
+ await serializeElements({
952
+ // AWAIT recursive call for shadow DOM
770
953
  ...ctx,
771
954
  dom: shadowHost.shadowRoot,
772
955
  clone: cloneShadowHost.shadowRoot
@@ -794,8 +977,8 @@
794
977
  window.resizeCount = 0;
795
978
  }
796
979
 
797
- // Serializes a document and returns the resulting DOM string.
798
- function serializeDOM(options) {
980
+ // Serializes a document and returns the resulting DOM string (ASYNC).
981
+ async function serializeDOM(options) {
799
982
  var _ctx$clone$body;
800
983
  let {
801
984
  dom = document,
@@ -824,8 +1007,15 @@
824
1007
  forceShadowAsLightDOM
825
1008
  };
826
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
827
1015
  ctx.clone = cloneNodeAndShadow(ctx);
828
- serializeElements(ctx);
1016
+
1017
+ // STEP 3: Serialize elements (AWAIT async call)
1018
+ await serializeElements(ctx);
829
1019
  if (domTransformation) {
830
1020
  try {
831
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.5-beta.0",
3
+ "version": "1.31.5-beta.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,5 +35,5 @@
35
35
  "devDependencies": {
36
36
  "interactor.js": "^2.0.0-beta.10"
37
37
  },
38
- "gitHead": "06352b66b8e5ebb2281e1111d71b3972a6713ed3"
38
+ "gitHead": "e1d5d3c9a2ffd7b4c3ee9d0afc1c30c442687417"
39
39
  }