@percy/dom
Advanced tools
Comparing version 1.16.0 to 2.0.0-alpha.0
@@ -9,17 +9,2 @@ (function() { | ||
// Returns a mostly random uid. | ||
function uid() { | ||
return `_${Math.random().toString(36).substr(2, 9)}`; | ||
} | ||
// Marks elements that are to be serialized later with a data attribute. | ||
function prepareDOM(dom) { | ||
for (let elem of dom.querySelectorAll('input, textarea, select, iframe, canvas, video, style')) { | ||
if (!elem.getAttribute('data-percy-element-id')) { | ||
elem.setAttribute('data-percy-element-id', uid()); | ||
} | ||
} | ||
return dom; | ||
} | ||
// Translates JavaScript properties of inputs into DOM attributes. | ||
@@ -58,2 +43,14 @@ function serializeInputElements(_ref) { | ||
} | ||
// find inputs inside shadow host and recursively serialize them. | ||
for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) { | ||
let percyElementId = shadowHost.getAttribute('data-percy-element-id'); | ||
let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) { | ||
serializeInputElements({ | ||
dom: shadowHost.shadowRoot, | ||
clone: cloneShadowHost.shadowRoot | ||
}); | ||
} | ||
} | ||
} | ||
@@ -80,2 +77,3 @@ | ||
for (let frame of dom.querySelectorAll('iframe')) { | ||
var _clone$head; | ||
let percyElementId = frame.getAttribute('data-percy-element-id'); | ||
@@ -87,3 +85,3 @@ let cloneEl = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
// rerendered and do not effect the visuals of a page | ||
if (clone.head.contains(cloneEl)) { | ||
if ((_clone$head = clone.head) !== null && _clone$head !== void 0 && _clone$head.contains(cloneEl)) { | ||
cloneEl.remove(); | ||
@@ -121,2 +119,17 @@ | ||
} | ||
// find iframes inside shadow host and recursively serialize them. | ||
for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) { | ||
let percyElementId = shadowHost.getAttribute('data-percy-element-id'); | ||
let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) { | ||
serializeFrames({ | ||
dom: shadowHost.shadowRoot, | ||
clone: cloneShadowHost.shadowRoot, | ||
warnings, | ||
resources, | ||
enableJavaScript | ||
}); | ||
} | ||
} | ||
} | ||
@@ -152,3 +165,3 @@ | ||
if (styleSheetsMatch(styleSheet, cloneOwnerNode.sheet)) continue; | ||
let style = clone.createElement('style'); | ||
let style = document.createElement('style'); | ||
style.type = 'text/css'; | ||
@@ -162,2 +175,14 @@ style.setAttribute('data-percy-element-id', styleId); | ||
} | ||
// find stylesheets inside shadow host and recursively serialize them. | ||
for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) { | ||
let percyElementId = shadowHost.getAttribute('data-percy-element-id'); | ||
let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) { | ||
serializeCSSOM({ | ||
dom: shadowHost.shadowRoot, | ||
clone: cloneShadowHost.shadowRoot | ||
}); | ||
} | ||
} | ||
} | ||
@@ -207,3 +232,4 @@ | ||
// create an image element in the cloned dom | ||
let img = clone.createElement('img'); | ||
// TODO: this works, verify if this is fine? | ||
let img = document.createElement('img'); | ||
// use a data attribute to avoid making a real request | ||
@@ -228,5 +254,23 @@ img.setAttribute('data-percy-serialized-attribute-src', resource.url); | ||
let cloneEl = clone.querySelector(`[data-percy-element-id=${percyElementId}]`); | ||
cloneEl.parentElement.insertBefore(img, cloneEl); | ||
// `parentElement` for elements directly under shadow root is `null` -> Incase of Nested Shadow DOM. | ||
if (cloneEl.parentElement) { | ||
cloneEl.parentElement.insertBefore(img, cloneEl); | ||
} else { | ||
clone.insertBefore(img, cloneEl); | ||
} | ||
cloneEl.remove(); | ||
} | ||
// find canvas inside shadow host and recursively serialize them. | ||
for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) { | ||
let percyElementId = shadowHost.getAttribute('data-percy-element-id'); | ||
let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) { | ||
serializeCanvas({ | ||
dom: shadowHost.shadowRoot, | ||
clone: cloneShadowHost.shadowRoot, | ||
resources | ||
}); | ||
} | ||
} | ||
} | ||
@@ -239,3 +283,4 @@ | ||
clone, | ||
resources | ||
resources, | ||
warnings | ||
} = _ref; | ||
@@ -254,3 +299,5 @@ for (let video of dom.querySelectorAll('video')) { | ||
dataUrl = canvas.toDataURL(); | ||
} catch {} | ||
} catch (e) { | ||
warnings.add(`data-percy-element-id="${videoId}" : ${e.toString()}`); | ||
} | ||
@@ -267,4 +314,152 @@ // if the canvas produces a blank image, skip | ||
} | ||
// find video inside shadow host and recursively serialize them. | ||
for (let shadowHost of dom.querySelectorAll('[data-percy-shadow-host]')) { | ||
let percyElementId = shadowHost.getAttribute('data-percy-element-id'); | ||
let cloneShadowHost = clone.querySelector(`[data-percy-element-id="${percyElementId}"]`); | ||
if (shadowHost.shadowRoot && cloneShadowHost.shadowRoot) { | ||
serializeVideos({ | ||
dom: shadowHost.shadowRoot, | ||
clone: cloneShadowHost.shadowRoot, | ||
resources, | ||
warnings | ||
}); | ||
} | ||
} | ||
} | ||
// Returns a mostly random uid. | ||
function uid() { | ||
return `_${Math.random().toString(36).substr(2, 9)}`; | ||
} | ||
function markElement(domElement) { | ||
var _domElement$tagName; | ||
// Mark elements that are to be serialized later with a data attribute. | ||
if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style'].includes((_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase())) { | ||
if (!domElement.getAttribute('data-percy-element-id')) { | ||
domElement.setAttribute('data-percy-element-id', uid()); | ||
} | ||
} | ||
// add special marker for shadow host | ||
if (domElement.shadowRoot) { | ||
if (!domElement.getAttribute('data-percy-shadow-host')) { | ||
domElement.setAttribute('data-percy-shadow-host', ''); | ||
} | ||
if (!domElement.getAttribute('data-percy-element-id')) { | ||
domElement.setAttribute('data-percy-element-id', uid()); | ||
} | ||
} | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
// returns document fragment | ||
const deepClone = host => { | ||
// clones shadow DOM and light DOM for a given node | ||
let cloneNode = (node, parent) => { | ||
let walkTree = (nextn, nextp) => { | ||
while (nextn) { | ||
cloneNode(nextn, nextp); | ||
nextn = nextn.nextSibling; | ||
} | ||
}; | ||
// mark the node before cloning | ||
markElement(node); | ||
let clone = node.cloneNode(); | ||
parent.appendChild(clone); | ||
// clone shadow DOM | ||
if (node.shadowRoot) { | ||
// create shadowRoot | ||
if (clone.shadowRoot) { | ||
// it may be set up in a custom element's constructor | ||
clone.shadowRoot.innerHTML = ''; | ||
} else { | ||
clone.attachShadow({ | ||
mode: 'open' | ||
}); | ||
} | ||
// clone stylesheets in shadowRoot | ||
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); | ||
} | ||
// clone dom elements | ||
walkTree(node.shadowRoot.firstChild, clone.shadowRoot); | ||
} | ||
// clone light DOM | ||
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 <style> tags. | ||
*/ | ||
const cloneNodeAndShadow = doc => { | ||
let mockDocumentFragment = deepClone(doc.documentElement); | ||
// convert document fragment to document object | ||
let cloneDocument = doc.cloneNode(); | ||
// dissolve document fragment in clone document | ||
cloneDocument.appendChild(mockDocumentFragment); | ||
return cloneDocument; | ||
}; | ||
/** | ||
* Use `getInnerHTML()` to serialize shadow dom as <template> tags. `innerHTML` and `outerHTML` don't do this. Buzzword: "declarative shadow dom" | ||
*/ | ||
const getOuterHTML = docElement => { | ||
// firefox doesn't serialize shadow DOM, we're awaiting API's by firefox to become ready and are not polyfilling it. | ||
if (!docElement.getInnerHTML) { | ||
return docElement.outerHTML; | ||
} | ||
// chromium gives us declarative shadow DOM serialization API | ||
let innerHTML = docElement.getInnerHTML({ | ||
includeShadowRoots: true | ||
}); | ||
docElement.textContent = ''; | ||
return docElement.outerHTML.replace('</html>', `${innerHTML}</html>`); | ||
}; | ||
// we inject declarative shadow dom polyfill to allow shadow dom to load in non chromium infrastructure browsers | ||
// Since only chromium currently supports declarative shadow DOM - https://caniuse.com/declarative-shadow-dom | ||
function injectDeclarativeShadowDOMPolyfill(ctx) { | ||
let clone = ctx.clone; | ||
let scriptEl = clone.createElement('script'); | ||
scriptEl.setAttribute('id', '__percy_shadowdom_helper'); | ||
scriptEl.setAttribute('data-percy-injected', true); | ||
scriptEl.innerHTML = ` | ||
function reversePolyFill(root=document){ | ||
root.querySelectorAll('template[shadowroot]').forEach(template => { | ||
const mode = template.getAttribute('shadowroot'); | ||
const shadowRoot = template.parentNode.attachShadow({ mode }); | ||
shadowRoot.appendChild(template.content); | ||
template.remove(); | ||
}); | ||
root.querySelectorAll('[data-percy-shadow-host]').forEach(shadowHost => reversePolyFill(shadowHost.shadowRoot)); | ||
} | ||
if (["interactive", "complete"].includes(document.readyState)) { | ||
reversePolyFill(); | ||
} else { | ||
document.addEventListener("DOMContentLoaded", () => reversePolyFill()); | ||
} | ||
`.replace(/(\n|\s{2}|\t)/g, ''); | ||
clone.body.appendChild(scriptEl); | ||
} | ||
// Returns a copy or new doctype for a document. | ||
@@ -290,3 +485,3 @@ function doctype(dom) { | ||
function serializeHTML(ctx) { | ||
let html = ctx.clone.documentElement.outerHTML; | ||
let html = getOuterHTML(ctx.clone.documentElement); | ||
// replace serialized data attributes with real attributes | ||
@@ -314,4 +509,4 @@ html = html.replace(/ data-percy-serialized-attribute-(\w+?)=/ig, ' $1='); | ||
}; | ||
ctx.dom = prepareDOM(dom); | ||
ctx.clone = ctx.dom.cloneNode(true); | ||
ctx.dom = dom; | ||
ctx.clone = cloneNodeAndShadow(ctx.dom); | ||
serializeInputElements(ctx); | ||
@@ -331,2 +526,3 @@ serializeFrames(ctx); | ||
} | ||
injectDeclarativeShadowDOMPolyfill(ctx); | ||
let result = { | ||
@@ -333,0 +529,0 @@ html: serializeHTML(ctx), |
{ | ||
"name": "@percy/dom", | ||
"version": "1.16.0", | ||
"version": "2.0.0-alpha.0", | ||
"license": "MIT", | ||
@@ -37,3 +37,3 @@ "repository": { | ||
}, | ||
"gitHead": "147d3a6f249771252ee6cf03c9af44e4e21e1c55" | ||
"gitHead": "6ebc3d59194d6cd25fceb5366ab9f230849a4f41" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25373
479
2