@percy/dom 1.31.0-alpha.2 → 1.31.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.
Files changed (2) hide show
  1. package/dist/bundle.js +106 -24
  2. package/package.json +3 -3
package/dist/bundle.js CHANGED
@@ -314,9 +314,21 @@
314
314
  /* istanbul ignore next: tested, but coverage is stripped */
315
315
  if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
316
316
  // handle document and iframe
317
- clone.body.prepend(styleLink);
317
+ // We are checking if we have multiple stylesheets present for the same clone or clone.body then we add
318
+ // them in the same order in which we receive them.
319
+ const lastLink = clone.body.querySelector('link[data-percy-adopted-stylesheets-serialized]:last-of-type');
320
+ if (lastLink) {
321
+ lastLink.after(styleLink);
322
+ } else {
323
+ clone.body.prepend(styleLink);
324
+ }
318
325
  } else if (clone.constructor.name === 'ShadowRoot') {
319
- clone.prepend(styleLink);
326
+ const lastLink = clone.querySelector('link[data-percy-adopted-stylesheets-serialized]:last-of-type');
327
+ if (lastLink) {
328
+ lastLink.after(styleLink);
329
+ } else {
330
+ clone.prepend(styleLink);
331
+ }
320
332
  }
321
333
  }
322
334
  } else {
@@ -441,7 +453,7 @@
441
453
  base64Index += ';base64,'.length;
442
454
  return src.substring(base64Index);
443
455
  }
444
- function serializeBase64(node, resources) {
456
+ function serializeBase64(node, resources, cache) {
445
457
  let src = node.src;
446
458
  let isHrefUsed = false;
447
459
 
@@ -455,14 +467,28 @@
455
467
  let base64String = getBase64Substring(src.toString());
456
468
  // skip if src is not base64
457
469
  if (base64String == null) return;
458
-
459
- // create a resource from the serialized data url
460
- let resource = resourceFromText(uid(), mimetype, base64String);
461
- resources.add(resource);
470
+ if (!cache.has(base64String)) {
471
+ // create a resource from the serialized data url
472
+ let resource = resourceFromText(uid(), mimetype, base64String);
473
+ resources.add(resource);
474
+ cache.set(base64String, resource.url);
475
+ }
462
476
  if (isHrefUsed === true) {
463
- node.href.baseVal = resource.url;
477
+ if (node.hasAttribute('xlink:href')) {
478
+ node.removeAttribute('xlink:href');
479
+ node.setAttribute('data-percy-serialized-attribute-xlink:href', cache.get(base64String));
480
+ } else {
481
+ node.removeAttribute('href');
482
+ node.setAttribute('data-percy-serialized-attribute-href', cache.get(base64String));
483
+ }
464
484
  } else {
465
- node.src = resource.url;
485
+ // we use data-percy-serialized-attribute-src here instead of `src`.
486
+ // As soon as src is used the browser will try to load the resource,
487
+ // thus making a network call which would fail as this is a
488
+ // dynamic cached resource and not a resource that backend can serve.
489
+ // we later post converting domtree to html replace this with src
490
+ node.removeAttribute('src');
491
+ node.setAttribute('data-percy-serialized-attribute-src', cache.get(base64String));
466
492
  }
467
493
  }
468
494
 
@@ -478,11 +504,36 @@
478
504
  */
479
505
 
480
506
  const ignoreTags = ['NOSCRIPT'];
507
+
508
+ /**
509
+ * if a custom element has attribute callback then cloneNode calls a callback that can
510
+ * increase CPU load or some other change.
511
+ * So we want to make sure that it is not called when doing serialization.
512
+ */
513
+ function cloneElementWithoutLifecycle(element) {
514
+ if (!element.attributeChangedCallback || !element.tagName.includes('-')) {
515
+ return element.cloneNode(); // Standard clone for non-custom elements
516
+ }
517
+ const cloned = document.createElement('data-percy-custom-element-' + element.tagName);
518
+
519
+ // Clone attributes without triggering attributeChangedCallback
520
+ for (const attr of element.attributes) {
521
+ // handle src separately
522
+ if (attr.name.toLowerCase() === 'src') {
523
+ cloned.setAttribute('data-percy-serialized-attribute-src', attr.value);
524
+ } else {
525
+ cloned.setAttribute(attr.name, attr.value);
526
+ }
527
+ }
528
+ return cloned;
529
+ }
481
530
  function cloneNodeAndShadow(ctx) {
482
531
  let {
483
532
  dom,
484
533
  disableShadowDOM,
485
- resources
534
+ resources,
535
+ cache,
536
+ enableJavaScript
486
537
  } = ctx;
487
538
  // clones shadow DOM and light DOM for a given node
488
539
  let cloneNode = (node, parent) => {
@@ -498,15 +549,36 @@
498
549
 
499
550
  // mark the node before cloning
500
551
  markElement(node, disableShadowDOM);
501
- let clone = node.cloneNode();
552
+ let clone = cloneElementWithoutLifecycle(node);
553
+
554
+ // Handle <style> tag specifically for media queries
555
+ if (node.nodeName === 'STYLE' && !enableJavaScript) {
556
+ var _node$textContent;
557
+ let cssText = ((_node$textContent = node.textContent) === null || _node$textContent === void 0 ? void 0 : _node$textContent.trim()) || '';
558
+ if (!cssText && node.sheet) {
559
+ try {
560
+ const cssRules = node.sheet.cssRules;
561
+ if (cssRules && cssRules.length > 0) {
562
+ cssText = Array.from(cssRules).map(rule => rule.cssText).join('\n');
563
+ }
564
+ } catch (_) {
565
+ // ignore errors
566
+ }
567
+ }
568
+ if (cssText) {
569
+ clone.textContent = cssText;
570
+ clone.setAttribute('data-percy-cssom-serialized', 'true');
571
+ }
572
+ }
502
573
 
503
574
  // We apply any element transformations here to avoid another treeWalk
504
575
  applyElementTransformations(clone);
505
- serializeBase64(clone, resources);
576
+ serializeBase64(clone, resources, cache);
506
577
  parent.appendChild(clone);
507
578
 
508
579
  // shallow clone should not contain children
509
580
  if (clone.children) {
581
+ /* istanbul ignore next */
510
582
  Array.from(clone.children).forEach(child => clone.removeChild(child));
511
583
  }
512
584
 
@@ -547,26 +619,27 @@
547
619
  /**
548
620
  * Use `getInnerHTML()` to serialize shadow dom as <template> tags. `innerHTML` and `outerHTML` don't do this. Buzzword: "declarative shadow dom"
549
621
  */
550
- function getOuterHTML(docElement) {
551
- // All major browsers in latest versions supports getHTML API to get serialized DOM
552
- // https://developer.mozilla.org/en-US/docs/Web/API/Element/getHTML
553
- // old firefox doesn't serialize shadow DOM, we're awaiting API's by firefox to become ready and are not polyfilling it.
554
- // new firefox from 128 onwards serializes it using getHTML
555
- /* istanbul ignore if: Only triggered in firefox <= 128 and tests runs on latest */
556
- if (!docElement.getHTML) {
557
- return docElement.outerHTML;
558
- }
622
+ function getOuterHTML(docElement, {
623
+ shadowRootElements
624
+ }) {
559
625
  // chromium gives us declarative shadow DOM serialization API
560
626
  let innerHTML = '';
561
627
  /* istanbul ignore else if: Only triggered in chrome <= 128 and tests runs on latest */
562
628
  if (docElement.getHTML) {
629
+ // All major browsers in latest versions supports getHTML API to get serialized DOM
630
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/getHTML
563
631
  innerHTML = docElement.getHTML({
564
- serializableShadowRoots: true
632
+ serializableShadowRoots: true,
633
+ shadowRoots: shadowRootElements
565
634
  });
566
635
  } else if (docElement.getInnerHTML) {
567
636
  innerHTML = docElement.getInnerHTML({
568
637
  includeShadowRoots: true
569
638
  });
639
+ } else {
640
+ // old firefox doesn't serialize shadow DOM, we're awaiting API's by firefox to become ready and are not polyfilling it.
641
+ // new firefox from 128 onwards serializes it using getHTML
642
+ return docElement.outerHTML;
570
643
  }
571
644
  docElement.textContent = '';
572
645
  // Note: Here we are specifically passing replacer function to avoid any replacements due to
@@ -594,7 +667,11 @@
594
667
 
595
668
  // Serializes and returns the cloned DOM as an HTML string
596
669
  function serializeHTML(ctx) {
597
- let html = getOuterHTML(ctx.clone.documentElement);
670
+ let html = getOuterHTML(ctx.clone.documentElement, {
671
+ shadowRootElements: ctx.shadowRootElements
672
+ });
673
+ // this is replacing serialized data tag with real tag
674
+ html = html.replace(/(<\/?)data-percy-custom-element-/g, '$1');
598
675
  // replace serialized data attributes with real attributes
599
676
  html = html.replace(/ data-percy-serialized-attribute-(\w+?)=/ig, ' $1=');
600
677
  // include the doctype with the html string
@@ -612,6 +689,9 @@
612
689
  let percyElementId = shadowHost.getAttribute('data-percy-element-id');
613
690
  let cloneShadowHost = ctx.clone.querySelector(`[data-percy-element-id="${percyElementId}"]`);
614
691
  if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) {
692
+ // getHTML requires shadowRoot to be passed explicitly
693
+ // to serialize the shadow elements properly
694
+ ctx.shadowRootElements.push(cloneShadowHost.shadowRoot);
615
695
  serializeElements({
616
696
  ...ctx,
617
697
  dom: shadowHost.shadowRoot,
@@ -641,6 +721,7 @@
641
721
 
642
722
  // Serializes a document and returns the resulting DOM string.
643
723
  function serializeDOM(options) {
724
+ var _ctx$clone$body;
644
725
  let {
645
726
  dom = document,
646
727
  // allow snake_case or camelCase
@@ -657,6 +738,7 @@
657
738
  warnings: new Set(),
658
739
  hints: new Set(),
659
740
  cache: new Map(),
741
+ shadowRootElements: [],
660
742
  enableJavaScript,
661
743
  disableShadowDOM
662
744
  };
@@ -680,7 +762,7 @@
680
762
  let sibling = clonedBody.nextSibling;
681
763
  clonedBody.append(sibling);
682
764
  }
683
- } else if (ctx.clone.body.nextSibling) {
765
+ } else if ((_ctx$clone$body = ctx.clone.body) !== null && _ctx$clone$body !== void 0 && _ctx$clone$body.nextSibling) {
684
766
  ctx.hints.add('DOM elements found outside </body>');
685
767
  }
686
768
  let cookies = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/dom",
3
- "version": "1.31.0-alpha.2",
3
+ "version": "1.31.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": "alpha"
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": "1c7711f564fd4a112e1e1490a772db757df0535d"
38
+ "gitHead": "49895470c0dfa7242881db43e293317d1fb8f8b6"
39
39
  }