From 88c5eaf8dbc17c0b0c87f63bb2a561454c5858ad Mon Sep 17 00:00:00 2001 From: Austin Joyner Date: Mon, 18 Apr 2022 11:55:01 -0400 Subject: [PATCH 1/2] Modify cloneNode to handle shadow dom. --- packages/dom/src/serialize-dom.js | 6 ++- packages/dom/src/wc-clone.js | 64 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 packages/dom/src/wc-clone.js diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index fb43398b9..a372f400e 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, customOuterHTML } 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) + customOuterHTML(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..696c15906 --- /dev/null +++ b/packages/dom/src/wc-clone.js @@ -0,0 +1,64 @@ +/** + * 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; +}; + +/** + * Sets up the document clone to mirror the result of Node.cloneNode() + * using the deep clone function able of cloning shadow dom + */ +const cloneNodeAndShadow = doc => { + let clonedDocumentElementFragment = deepClone(doc.documentElement); + clonedDocumentElementFragment.head = document.createDocumentFragment(); + clonedDocumentElementFragment.documentElement = clonedDocumentElementFragment.firstChild; + return clonedDocumentElementFragment; +}; + +const customOuterHTML = docElement => { + return `${docElement.getInnerHTML()}`; +}; + + +export { cloneNodeAndShadow, customOuterHTML } \ No newline at end of file From 7575b93baeae006716b60d52be7ee797ddcfa7b3 Mon Sep 17 00:00:00 2001 From: Austin Joyner Date: Wed, 20 Apr 2022 15:59:08 -0400 Subject: [PATCH 2/2] Clean up custom shadow serializing functions. --- packages/dom/src/serialize-dom.js | 4 ++-- packages/dom/src/wc-clone.js | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index a372f400e..c129e4fa9 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -5,7 +5,7 @@ import serializeCSSOM from './serialize-cssom'; import serializeCanvas from './serialize-canvas'; import serializeVideos from './serialize-video'; -import { cloneNodeAndShadow, customOuterHTML } from './wc-clone'; +import { cloneNodeAndShadow, getOuterHTML } from './wc-clone'; // Returns a copy or new doctype for a document. function doctype(dom) { @@ -54,7 +54,7 @@ export function serializeDOM(options) { } } - return doctype(dom) + customOuterHTML(doc); + return doctype(dom) + getOuterHTML(doc); } export default serializeDOM; diff --git a/packages/dom/src/wc-clone.js b/packages/dom/src/wc-clone.js index 696c15906..b9d3f9800 100644 --- a/packages/dom/src/wc-clone.js +++ b/packages/dom/src/wc-clone.js @@ -45,20 +45,24 @@ const deepClone = host => { return fragment; }; + /** - * Sets up the document clone to mirror the result of Node.cloneNode() - * using the deep clone function able of cloning shadow dom + * Deep clone a document while also preserving shadow roots and converting adoptedStylesheets to