@percy/dom 1.31.5-beta.2 → 1.31.5

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 +135 -186
  2. 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$1(uid, dataURL) {
10
+ function resourceFromDataURL(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 (ASYNC).
146
- async function serializeFrames({
145
+ // Recursively serializes iframe documents into srcdoc attributes.
146
+ 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 (AWAIT async call)
174
- let serialized = await serializeDOM({
173
+ // recersively serialize contents
174
+ let serialized = 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$1() {
199
+ function uid() {
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$1());
207
+ domElement.setAttribute('data-percy-element-id', uid());
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$1());
218
+ domElement.setAttribute('data-percy-element-id', uid());
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$1(), 'text/css', styles);
253
+ let resource = resourceFromText(uid(), '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$1(percyElementId, dataUrl);
419
+ let resource = resourceFromDataURL(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$1(videoId, dataUrl);
470
+ let resource = resourceFromDataURL(videoId, dataUrl);
471
471
  resources.add(resource);
472
472
 
473
473
  // use a data attribute to avoid making a real request
@@ -478,186 +478,137 @@
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
- };
481
+ /* global XPathResult */
482
+ const PSEDUO_ELEMENT_MARKER_ATTR = 'data-percy-pseudo-element-id';
483
+ function markElementIfNeeded(element, markWithId) {
484
+ if (markWithId && !element.getAttribute(PSEDUO_ELEMENT_MARKER_ATTR)) {
485
+ element.setAttribute(PSEDUO_ELEMENT_MARKER_ATTR, uid());
486
+ }
501
487
  }
502
488
 
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());
489
+ /**
490
+ * Get all elements matching the pseudoClassEnabledElements configuration
491
+ * @param {Document} dom - The document to search
492
+ * @param {Object} config - Configuration with id, className, and xpath arrays
493
+ * @param {boolean} markWithId - Whether to mark elements with PSEDUO_ELEMENT_MARKER_ATTR
494
+ * @returns {Array} Array of elements found
495
+ */
496
+ function getElementsToProcess(ctx, config, markWithId = false) {
497
+ const {
498
+ dom
499
+ } = ctx;
500
+ const elements = [];
501
+ if (config.id && Array.isArray(config.id)) {
502
+ for (const id of config.id) {
503
+ const element = dom.getElementById(id);
504
+ if (!element) {
505
+ ctx.warnings.add(`No element found with ID: ${id} for pseudo-class serialization`);
506
+ continue;
507
+ }
508
+ markElementIfNeeded(element, markWithId);
509
+ elements.push(element);
512
510
  }
513
- blobUrls.push({
514
- element: el,
515
- blobUrl: el.src,
516
- property: 'src',
517
- id: el.getAttribute('data-percy-element-id')
518
- });
519
511
  }
520
512
 
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());
513
+ // Process only first match per class name
514
+ if (config.className && Array.isArray(config.className)) {
515
+ for (const className of config.className) {
516
+ const elementCollection = dom.getElementsByClassName(className);
517
+ if (!elementCollection.length) {
518
+ ctx.warnings.add(`No element found with class name: ${className} for pseudo-class serialization`);
519
+ continue;
520
+ }
521
+ const element = elementCollection[0];
522
+ markElementIfNeeded(element, markWithId);
523
+ elements.push(element);
531
524
  }
532
- blobUrls.push({
533
- element: el,
534
- blobUrl: el.href,
535
- property: 'href',
536
- id: el.getAttribute('data-percy-element-id')
537
- });
538
525
  }
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
- });
526
+ if (config.xpath && Array.isArray(config.xpath)) {
527
+ for (const xpathExpression of config.xpath) {
528
+ try {
529
+ const element = dom.evaluate(xpathExpression, dom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
530
+ if (!element) {
531
+ ctx.warnings.add(`No element found for XPath: ${xpathExpression} for pseudo-class serialization`);
532
+ continue;
558
533
  }
534
+ markElementIfNeeded(element, markWithId);
535
+ } catch (err) {
536
+ console.warn(`Invalid XPath expression "${xpathExpression}". Error: ${err.message}`);
559
537
  }
560
538
  }
561
539
  }
562
- return blobUrls;
540
+ return elements;
563
541
  }
564
542
 
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
- }
543
+ /**
544
+ * Mark pseudo-class enabled elements with data-percy-element-id before cloning
545
+ * This must be called before the DOM is cloned
546
+ * @param {Document} dom - The document to mark
547
+ * @param {Object} config - Configuration with id and xpath arrays
548
+ */
549
+ function markPseudoClassElements(ctx, config) {
550
+ if (!config) return;
551
+ getElementsToProcess(ctx, config, true);
606
552
  }
607
553
 
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
- }
554
+ /**
555
+ * Convert CSSStyleDeclaration to CSS text with !important declarations
556
+ * @param {CSSStyleDeclaration} styles - Computed style declaration
557
+ * @returns {string} CSS text
558
+ */
559
+ function stylesToCSSText(styles) {
560
+ const cssProperties = [];
561
+ for (let i = 0; i < styles.length; i++) {
562
+ const property = styles[i];
563
+ const value = styles.getPropertyValue(property);
564
+ cssProperties.push(`${property}: ${value} !important;`);
625
565
  }
626
- return processedCount;
566
+ return cssProperties.join(' ');
627
567
  }
628
568
 
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`);
569
+ /**
570
+ * Process pseudo-class elements and add percy-pseudo-class CSS
571
+ * @param {Object} ctx - Serialization context
572
+ */
573
+ function serializePseudoClasses(ctx) {
574
+ if (!ctx.pseudoClassEnabledElements) {
575
+ return;
576
+ }
577
+ const elements = ctx.dom.querySelectorAll(`[${PSEDUO_ELEMENT_MARKER_ATTR}]`);
578
+ if (elements.length === 0) {
579
+ return;
580
+ }
581
+ const cssRules = [];
582
+ for (const element of elements) {
583
+ const percyElementId = element.getAttribute(PSEDUO_ELEMENT_MARKER_ATTR);
584
+ const cloneElement = ctx.clone.querySelector(`[${PSEDUO_ELEMENT_MARKER_ATTR}="${percyElementId}"]`);
585
+ if (!cloneElement) {
586
+ ctx.warnings.add(`Element not found for pseudo-class serialization with percy-element-id: ${percyElementId}`);
587
+ continue;
588
+ }
589
+ try {
590
+ // Get all computed styles including pseudo-classes
591
+ const computedStyles = window.getComputedStyle(element);
592
+ const cssText = stylesToCSSText(computedStyles);
593
+ const selector = `[${PSEDUO_ELEMENT_MARKER_ATTR}="${percyElementId}"]`;
594
+ cssRules.push(`${selector} { ${cssText} }`);
595
+ } catch (err) {
596
+ console.warn('Could not get computed styles for element', element, err);
597
+ }
637
598
  }
638
599
 
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`);
600
+ // Inject CSS into cloned document
601
+ if (cssRules.length > 0) {
602
+ const styleElement = ctx.dom.createElement('style');
603
+ styleElement.setAttribute('data-percy-pseudo-class-styles', 'true');
604
+ styleElement.textContent = cssRules.join('\n');
605
+ const head = ctx.clone.head || ctx.clone.querySelector('head');
606
+ if (head) {
607
+ head.appendChild(styleElement);
608
+ } else {
609
+ ctx.warnings.add('Could not inject pseudo-class styles: no <head> element found');
610
+ }
659
611
  }
660
- return processedResources;
661
612
  }
662
613
 
663
614
  // Drop loading attribute. We do not scroll page in discovery stage but we want to make sure that
@@ -711,7 +662,7 @@
711
662
  if (base64String == null) return;
712
663
  if (!cache.has(base64String)) {
713
664
  // create a resource from the serialized data url
714
- let resource = resourceFromText(uid$1(), mimetype, base64String);
665
+ let resource = resourceFromText(uid(), mimetype, base64String);
715
666
  resources.add(resource);
716
667
  cache.set(base64String, resource.url);
717
668
  }
@@ -931,9 +882,9 @@
931
882
  // include the doctype with the html string
932
883
  return doctype(ctx.dom) + html;
933
884
  }
934
- async function serializeElements(ctx) {
885
+ function serializeElements(ctx) {
935
886
  serializeInputElements(ctx);
936
- await serializeFrames(ctx); // AWAIT async iframe serialization
887
+ serializeFrames(ctx);
937
888
  serializeVideos(ctx);
938
889
  if (!ctx.enableJavaScript) {
939
890
  serializeCSSOM(ctx);
@@ -948,8 +899,7 @@
948
899
  // getHTML requires shadowRoot to be passed explicitly
949
900
  // to serialize the shadow elements properly
950
901
  ctx.shadowRootElements.push(cloneShadowHost.shadowRoot);
951
- await serializeElements({
952
- // AWAIT recursive call for shadow DOM
902
+ serializeElements({
953
903
  ...ctx,
954
904
  dom: shadowHost.shadowRoot,
955
905
  clone: cloneShadowHost.shadowRoot
@@ -977,8 +927,8 @@
977
927
  window.resizeCount = 0;
978
928
  }
979
929
 
980
- // Serializes a document and returns the resulting DOM string (ASYNC).
981
- async function serializeDOM(options) {
930
+ // Serializes a document and returns the resulting DOM string.
931
+ function serializeDOM(options) {
982
932
  var _ctx$clone$body;
983
933
  let {
984
934
  dom = document,
@@ -990,7 +940,8 @@
990
940
  reshuffleInvalidTags = options === null || options === void 0 ? void 0 : options.reshuffle_invalid_tags,
991
941
  ignoreCanvasSerializationErrors = options === null || options === void 0 ? void 0 : options.ignore_canvas_serialization_errors,
992
942
  ignoreStyleSheetSerializationErrors = options === null || options === void 0 ? void 0 : options.ignore_style_sheet_serialization_errors,
993
- forceShadowAsLightDOM = options === null || options === void 0 ? void 0 : options.force_shadow_dom_as_light_dom
943
+ forceShadowAsLightDOM = options === null || options === void 0 ? void 0 : options.force_shadow_dom_as_light_dom,
944
+ pseudoClassEnabledElements = options === null || options === void 0 ? void 0 : options.pseudo_class_enabled_elements
994
945
  } = options || {};
995
946
 
996
947
  // keep certain records throughout serialization
@@ -1004,18 +955,16 @@
1004
955
  disableShadowDOM,
1005
956
  ignoreCanvasSerializationErrors,
1006
957
  ignoreStyleSheetSerializationErrors,
1007
- forceShadowAsLightDOM
958
+ forceShadowAsLightDOM,
959
+ pseudoClassEnabledElements
1008
960
  };
1009
961
  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
962
+ markPseudoClassElements(ctx, pseudoClassEnabledElements);
1015
963
  ctx.clone = cloneNodeAndShadow(ctx);
964
+ serializeElements(ctx);
1016
965
 
1017
- // STEP 3: Serialize elements (AWAIT async call)
1018
- await serializeElements(ctx);
966
+ // STEP 4: Process pseudo-class enabled elements
967
+ serializePseudoClasses(ctx);
1019
968
  if (domTransformation) {
1020
969
  try {
1021
970
  // 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.2",
3
+ "version": "1.31.5",
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": "beta"
12
+ "tag": "latest"
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": "e1d5d3c9a2ffd7b4c3ee9d0afc1c30c442687417"
38
+ "gitHead": "835297c48a25843d8c719d6b476134c603721d13"
39
39
  }