Skip to content

Commit

Permalink
✨ DOM events triggered on clone fixed (#1185)
Browse files Browse the repository at this point in the history
* clone to fragment

* test verbose

* generate sheet from cloned node

* add serialized attribute

* update tests

* mount on clone and remove sanity

* fix readme

* Revert style hotfix

* add doc to update revision for chromium

* fix lint

* update comment

* about:srcdoc sanity coverage

---------

Co-authored-by: Jigar Wala <jigarwala007@gmail.com>
  • Loading branch information
samarsault and itsjwala authored Feb 17, 2023
1 parent 66b8192 commit 66b4d4d
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 48 deletions.
7 changes: 5 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ use the following scripts for various development tasks:
- `yarn test` - run all tests, one package after another
- `yarn test:coverage` - run all tests with coverage, one package after another
- `yarn global:link` - links all packages being developed as global.
- requires `yarn build` to be run before
- requires `yarn build` to be run before consuming.
- we can then consume this package using
`[npm|yarn] link @percy/[core|cli..]`
`yarn link @percy/[core|cli..]`
- **Note**: linking is only required once, subsequent changes for development requires running build command.

- `yarn global:unlink` - unlinks all packages globally
Expand All @@ -75,6 +75,9 @@ Individual package scripts can be invoked using yarn's
$ yarn workspace @percy/core test
```

### How to update Chromium revision?

check in Core Package's readme [here](./packages/core#readme).

## Publish

Expand Down
34 changes: 34 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,37 @@ environment variable.

> **Warning!** Percy is only tested against the browser it downloads automatically. When providing a
> custom browser executable, you may experience unexpected issues.

### How to update Chromium revision?

`src/install.js`

```js
chromium.revisions = {
linux: '.*',
win64: '.*',
win32: '.*',
darwin: '.*',
darwinArm: '.*'
};
```

Nicely summarised in this [stackoverflow](https://stackoverflow.com/a/56366776) answer.

### Excerpt

check the [release information on Github](https://github.com/GoogleChrome/puppeteer/releases) where the expected Chromium version and revision is specified.
For example:

> [v1.17.0](https://github.com/GoogleChrome/puppeteer/releases/tag/v1.17.0)
Big Changes
Chromium 76.0.3803.0 (r662092)

1. Go to [Chromium browser snapshots](https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html)

2. Choose the directory of your platform (e.g., `Linux_x64`)

3. Copy the revision number into the "Filter:" field without the "r" (e.g., `662092`)

4. Fetch revision number for rest of the platform (`Win`, `Win_x64`, `Mac`, `Mac_Arm`), it should be nearby (Tip: verify the date of upload).
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
30 changes: 22 additions & 8 deletions packages/dom/src/serialize-cssom.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,31 @@ function styleSheetsMatch(sheetA, sheetB) {
return true;
}

export function serializeCSSOM({ dom, clone, warnings, resources, cache }) {
function styleSheetFromNode(node) {
/* istanbul ignore if: sanity check */
if (node.sheet) return node.sheet;

// Cloned style nodes don't have a sheet instance unless they are within
// a document; we get it by temporarily adding the rules to DOM
const tempStyle = document.createElement('style');
tempStyle.setAttribute('data-percy-style-helper', '');
tempStyle.innerHTML = node.innerHTML;
const clone = document.cloneNode();
clone.appendChild(tempStyle);
const sheet = tempStyle.sheet;
// Cleanup node
tempStyle.remove();

return sheet;
}

export function serializeCSSOM({ dom, clone, resources, cache }) {
// in-memory CSSOM into their respective DOM nodes.
for (let styleSheet of dom.styleSheets) {
if (isCSSOM(styleSheet)) {
let styleId = styleSheet.ownerNode.getAttribute('data-percy-element-id');
if (!styleId) {
let attributes = Array.from(styleSheet.ownerNode.attributes).map(attr => `${attr.name}: ${attr.value}`);
warnings.add(`stylesheet with attributes - [ ${attributes} ] - was not serialized`);
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 +69,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');
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
1 change: 1 addition & 0 deletions packages/dom/src/serialize-frames.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import serializeDOM from './serialize-dom';
// Adds a `<base>` element to the serialized iframe's `<head>`. This is necessary when
// embedded documents are serialized and their contents become root-relative.
function setBaseURI(dom) {
/* istanbul ignore if: sanity check */
if (!new URL(dom.baseURI).hostname) return;

let $base = document.createElement('base');
Expand Down
32 changes: 5 additions & 27 deletions packages/dom/test/serialize-css.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { withExample, withCSSOM, parseDOM, platforms, platformDOM, createShadowEl } from './helpers';
import serializeDOM from '@percy/dom';
import serializeCSSOM from '../src/serialize-cssom';
import { cloneNodeAndShadow } from '../src/clone-dom';

describe('serializeCSSOM', () => {
beforeEach(() => {
Expand All @@ -27,26 +25,6 @@ describe('serializeCSSOM', () => {
dom = platformDOM(platform);
});

it('skips serialization when data-percy-element-id is not found', () => {
if (platform !== 'plain') {
return;
}

let ctx = {
dom,
resources: new Set(),
warnings: new Set(),
cache: new Map(),
enableJavaScript: false,
disableShadowDOM: false
};
ctx.clone = cloneNodeAndShadow(ctx);
// remove marker id from all 3 stylesheets
Array.from(ctx.dom.styleSheets).forEach(stylesheet => { stylesheet.ownerNode.removeAttribute('data-percy-element-id'); });
serializeCSSOM(ctx);
expect(ctx.warnings).toHaveSize(3);
});

it(`${platform}: serializes CSSOM and does not mutate the orignal DOM`, () => {
let $cssom = parseDOM(serializeDOM(), platform)('[data-percy-cssom-serialized]');

Expand Down Expand Up @@ -96,7 +74,7 @@ describe('serializeCSSOM', () => {
const capture = serializeDOM();
let $ = parseDOM(capture, 'plain');
dom.adoptedStyleSheets = [];
expect($('body')[0].innerHTML).toMatch(`<link rel="stylesheet" href="${capture.resources[0].url}">`);
expect($('body')[0].innerHTML).toMatch(`<link rel="stylesheet" data-percy-adopted-stylesheets-serialized="true" href="${capture.resources[0].url}">`);
});

it('captures adoptedStylesheets', () => {
Expand Down Expand Up @@ -125,7 +103,7 @@ describe('serializeCSSOM', () => {

expect(resultShadowEl.innerHTML).toEqual([
'<template shadowroot="open">',
`<link rel="stylesheet" href="${capture.resources[0].url}">`,
`<link rel="stylesheet" data-percy-adopted-stylesheets-serialized="true" href="${capture.resources[0].url}">`,
'<p>Percy-0</p>',
'</template>'
].join(''));
Expand Down Expand Up @@ -164,15 +142,15 @@ describe('serializeCSSOM', () => {

expect(resultShadowEl.innerHTML).toMatch([
'<template shadowroot="open">',
`<link rel="stylesheet" href="${capture.resources[0].url}">`,
`<link rel="stylesheet" data-percy-adopted-stylesheets-serialized="true" href="${capture.resources[0].url}">`,
'<p>Percy-0</p>',
'</template>'
].join(''));

expect(resultShadowElChild.innerHTML).toMatch([
'<template shadowroot="open">',
`<link rel="stylesheet" href="${capture.resources[1].url}">`,
`<link rel="stylesheet" href="${capture.resources[0].url}">`,
`<link rel="stylesheet" data-percy-adopted-stylesheets-serialized="true" href="${capture.resources[1].url}">`,
`<link rel="stylesheet" data-percy-adopted-stylesheets-serialized="true" href="${capture.resources[0].url}">`,
'<p>Percy-1</p>',
'</template>'
].join(''));
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('callback-test')) {
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

0 comments on commit 66b4d4d

Please sign in to comment.