diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index fb43398b9..c129e4fa9 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -5,6 +5,8 @@ import serializeCSSOM from './serialize-cssom'; import serializeCanvas from './serialize-canvas'; import serializeVideos from './serialize-video'; +import { cloneNodeAndShadow, getOuterHTML } from './wc-clone'; + // Returns a copy or new doctype for a document. function doctype(dom) { let { name = 'html', publicId = '', systemId = '' } = dom?.doctype ?? {}; @@ -32,7 +34,7 @@ export function serializeDOM(options) { prepareDOM(dom); - let clone = dom.cloneNode(true); + let clone = cloneNodeAndShadow(dom); serializeInputs(dom, clone); serializeFrames(dom, clone, { enableJavaScript }); serializeVideos(dom, clone); @@ -52,7 +54,7 @@ export function serializeDOM(options) { } } - return doctype(dom) + doc.outerHTML; + return doctype(dom) + getOuterHTML(doc); } export default serializeDOM; diff --git a/packages/dom/src/wc-clone.js b/packages/dom/src/wc-clone.js new file mode 100644 index 000000000..b9d3f9800 --- /dev/null +++ b/packages/dom/src/wc-clone.js @@ -0,0 +1,68 @@ +/** + * Custom deep clone function that replaces Percy's current clone behavior. + * This enables us to capture shadow DOM in snapshots. It takes advantage of `attachShadow`'s mode option set to open + * https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters + */ +const deepClone = host => { + let cloneNode = (node, parent) => { + let walkTree = (nextn, nextp) => { + while (nextn) { + cloneNode(nextn, nextp); + nextn = nextn.nextSibling; + } + }; + + let clone = node.cloneNode(); + parent.appendChild(clone); + + if (node.shadowRoot) { + if (clone.shadowRoot) { + // it may be set up in a custom element's constructor + clone.shadowRoot.innerHTML = ''; + } else { + clone.attachShadow({ + mode: 'open' + }); + } + + for (let sheet of node.shadowRoot.adoptedStyleSheets) { + let cssText = Array.from(sheet.rules).map(rule => rule.cssText).join('\n'); + let style = document.createElement('style'); + style.appendChild(document.createTextNode(cssText)); + clone.shadowRoot.prepend(style); + } + } + + if (node.shadowRoot) { + walkTree(node.shadowRoot.firstChild, clone.shadowRoot); + } + + walkTree(node.firstChild, clone); + }; + + let fragment = document.createDocumentFragment(); + cloneNode(host, fragment); + return fragment; +}; + + +/** + * Deep clone a document while also preserving shadow roots and converting adoptedStylesheets to