Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ DOM events triggered on clone fixed #1185

Merged
merged 15 commits into from
Feb 17, 2023
12 changes: 5 additions & 7 deletions packages/dom/src/clone-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,11 @@ const deepClone = (host, disableShadowDOM) => {
* Deep clone a document while also preserving shadow roots and converting adoptedStylesheets to <style> tags.
*/
const cloneNodeAndShadow = (ctx) => {
let cloneDocumentElement = deepClone(ctx.dom.documentElement, ctx.disableShadowDOM);
// TODO: we're not properly appending documentElement (html node) in the clone document, this can cause side effects in original document.
// convert document fragment to document object
let cloneDocument = ctx.dom.cloneNode();
// dissolve document fragment in clone document
cloneDocument.appendChild(cloneDocumentElement);
return cloneDocument;
let cloneDocumentFragment = deepClone(ctx.dom.documentElement, ctx.disableShadowDOM);
cloneDocumentFragment.documentElement = cloneDocumentFragment.firstChild;
cloneDocumentFragment.head = cloneDocumentFragment.querySelector('head');
cloneDocumentFragment.body = cloneDocumentFragment.querySelector('body');
return cloneDocumentFragment;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/dom/src/inject-polyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Since only chromium currently supports declarative shadow DOM - https://caniuse.com/declarative-shadow-dom
export function injectDeclarativeShadowDOMPolyfill(ctx) {
let clone = ctx.clone;
let scriptEl = clone.createElement('script');
let scriptEl = document.createElement('script');
scriptEl.setAttribute('id', '__percy_shadowdom_helper');
scriptEl.setAttribute('data-percy-injected', true);

Expand Down
24 changes: 22 additions & 2 deletions packages/dom/src/serialize-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ function styleSheetsMatch(sheetA, sheetB) {
return true;
}

function styleSheetFromNode(node) {
/* istanbul ignore if: sanity check */
if (node.sheet) return node.sheet;
/* istanbul ignore if: sanity check */
if (node.constructor.name !== 'HTMLStyleElement') return;
samarsault marked this conversation as resolved.
Show resolved Hide resolved

// Cloned style nodes inside don't have a sheet instance unless cloned along
// with document; we get it by temporarily adding the rules to DOM
const tempStyle = document.createElement('style');
tempStyle.id = 'style-percy-helper';
tempStyle.innerText = node.innerText;
document.head.appendChild(tempStyle);
const sheet = tempStyle.sheet;
// Cleanup node
tempStyle.remove();
samarsault marked this conversation as resolved.
Show resolved Hide resolved

return sheet;
}

export function serializeCSSOM({ dom, clone, warnings, resources, cache }) {
// in-memory CSSOM into their respective DOM nodes.
for (let styleSheet of dom.styleSheets) {
Expand All @@ -29,7 +48,7 @@ export function serializeCSSOM({ dom, clone, warnings, resources, cache }) {
continue;
}
let cloneOwnerNode = clone.querySelector(`[data-percy-element-id="${styleId}"]`);
if (styleSheetsMatch(styleSheet, cloneOwnerNode.sheet)) continue;
if (styleSheetsMatch(styleSheet, styleSheetFromNode(cloneOwnerNode))) continue;
let style = document.createElement('style');

style.type = 'text/css';
Expand All @@ -56,10 +75,11 @@ export function serializeCSSOM({ dom, clone, warnings, resources, cache }) {
resources.add(resource);
cache.set(sheet, resource.url);
}
styleLink.setAttribute('data-percy-adopted-stylesheets-serialized', 'true');
itsjwala marked this conversation as resolved.
Show resolved Hide resolved
styleLink.setAttribute('data-percy-serialized-attribute-href', cache.get(sheet));

/* istanbul ignore next: tested, but coverage is stripped */
if (clone.constructor.name === 'HTMLDocument') {
if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
// handle document and iframe
clone.body.prepend(styleLink);
} else if (clone.constructor.name === 'ShadowRoot') {
Expand Down
24 changes: 21 additions & 3 deletions packages/dom/test/serialize-dom.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withExample, replaceDoctype, createShadowEl, getTestBrowser, chromeBrowser } from './helpers';
import { withExample, replaceDoctype, createShadowEl, getTestBrowser, chromeBrowser, parseDOM } from './helpers';
import serializeDOM from '@percy/dom';

describe('serializeDOM', () => {
Expand Down Expand Up @@ -34,6 +34,25 @@ describe('serializeDOM', () => {
expect(serializeDOM().html).toMatch('<!DOCTYPE html>');
});

it('does not trigger DOM events on clone', () => {
class CallbackTestElement extends window.HTMLElement {
connectedCallback() {
const wrapper = document.createElement('h2');
wrapper.className = 'callback';
wrapper.innerText = 'Test';
this.appendChild(wrapper);
}
}

if (!window.customElements.get('cllback-test')) {
samarsault marked this conversation as resolved.
Show resolved Hide resolved
window.customElements.define('callback-test', CallbackTestElement);
}
withExample('<callback-test/>', { withShadow: false });
const $ = parseDOM(serializeDOM().html);

expect($('h2.callback').length).toEqual(1);
});

describe('shadow dom', () => {
it('renders open root as template tag', () => {
if (getTestBrowser() !== chromeBrowser) {
Expand Down Expand Up @@ -69,7 +88,7 @@ describe('serializeDOM', () => {
expect(html).not.toMatch('Hey Percy!');
});

it('renders single nested ', () => {
it('renders single nested', () => {
if (getTestBrowser() !== chromeBrowser) {
return;
}
Expand Down Expand Up @@ -173,7 +192,6 @@ describe('serializeDOM', () => {
class TestElement extends window.HTMLElement {
constructor() {
super();
// super();
// Create a shadow root
const shadow = this.shadowRoot || this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('h2');
Expand Down