From 92319334e72a59142b9b270482195f9accdaf8c8 Mon Sep 17 00:00:00 2001 From: Samarjeet Date: Tue, 6 Dec 2022 16:34:00 +0530 Subject: [PATCH 001/105] use declarative dom for serializing --- package.json | 2 + packages/dom/src/index.js | 31 ++ packages/dom/src/serialize-dom.js | 5 +- packages/dom/src/wc-clone.js | 68 ++++ yarn.lock | 559 +++++++++++++++++++++++++++++- 5 files changed, 644 insertions(+), 21 deletions(-) create mode 100644 packages/dom/src/wc-clone.js diff --git a/package.json b/package.json index 0e6ded240..12d444ab2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@babel/eslint-parser": "^7.14.7", "@babel/preset-env": "^7.14.7", "@babel/register": "^7.17.7", + "@percy/cypress": "^3.1.2", "@rollup/plugin-alias": "^4.0.0", "@rollup/plugin-babel": "^6.0.0", "@rollup/plugin-commonjs": "^21.0.0", @@ -35,6 +36,7 @@ "babel-plugin-istanbul": "^6.0.0", "babel-plugin-module-resolver": "^4.1.0", "cross-env": "^7.0.2", + "cypress": "^11.2.0", "eslint": "^7.30.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-babel": "^5.3.1", diff --git a/packages/dom/src/index.js b/packages/dom/src/index.js index 9860612f6..3c9fa6818 100644 --- a/packages/dom/src/index.js +++ b/packages/dom/src/index.js @@ -1,3 +1,34 @@ +if (!Element.prototype.getInnerHTML) { + function explore(element, opts = {}) { + const children = element.shadowRoot ? element.shadowRoot.children : element.children; + if (children.length === 0) + return element.outerHTML + + let contents = "" + for (const child of children) { + contents += explore(child) + } + + let [openTag, closeTag] = element.cloneNode().outerHTML.split(/\>\<" + closeTag + } else { + openTag += ">" + closeTag = "<" + closeTag + } + return openTag + contents + closeTag + }; + + Element.prototype.getInnerHTML = function() { + let content = "" + for (const child of this.children) + content = explore(child) + return content + } +} + export { default, serializeDOM, diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index cc80ff9db..cd1029668 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -4,6 +4,7 @@ import serializeFrames from './serialize-frames'; 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) { @@ -23,7 +24,7 @@ function doctype(dom) { // Serializes and returns the cloned DOM as an HTML string function serializeHTML(ctx) { - let html = ctx.clone.documentElement.outerHTML; + let html = getOuterHTML(ctx.clone.documentElement); // replace serialized data attributes with real attributes html = html.replace(/ data-percy-serialized-attribute-(\w+?)=/ig, ' $1='); // include the doctype with the html string @@ -48,7 +49,7 @@ export function serializeDOM(options) { }; ctx.dom = prepareDOM(dom); - ctx.clone = ctx.dom.cloneNode(true); + ctx.clone = cloneNodeAndShadow(ctx.dom); serializeInputs(ctx); serializeFrames(ctx); diff --git a/packages/dom/src/wc-clone.js b/packages/dom/src/wc-clone.js new file mode 100644 index 000000000..811184fe3 --- /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 '; let style = ''; - withExample(`
${link}${mod}${style}`); - withCSSOM('.box { height: 500px; }'); + const html = `
${link}${mod}${style}`; + const css = '.box { height: 500px; }'; - let modCSSRule = document.getElementById('mod').sheet.cssRules[0]; + if (shadowDom) { + withShadowExample(html); + withShadowCSSOM(css); + dom = getExampleShadowRoot(); + } else { + withExample(html); + withCSSOM(css); + } + + let modCSSRule = dom.getElementById('mod').sheet.cssRules[0]; if (modCSSRule) modCSSRule.style.cssText = 'width: 1000px'; // give the linked style a few milliseconds to load @@ -18,22 +29,23 @@ describe('serializeCSSOM', () => { }); it('serializes CSSOM and does not mutate the orignal DOM', () => { - let $cssom = parseDOM(serializeDOM())('[data-percy-cssom-serialized]'); + let $ = shadowDom ? parseDeclShadowDOM(serializeDOM()) : parseDOM(serializeDOM()); + let $cssom = $('[data-percy-cssom-serialized]'); // linked and unmodified stylesheets are not included expect($cssom).toHaveSize(2); expect($cssom[0].innerHTML).toBe('.box { height: 500px; }'); expect($cssom[1].innerHTML).toBe('.box { width: 1000px; }'); - expect(document.styleSheets[0].ownerNode.innerText).toBe(''); - expect(document.styleSheets[1].ownerNode.innerText).toBe(''); - expect(document.styleSheets[2].ownerNode.innerText).toBe('.box { width: 500px; }'); - expect(document.styleSheets[3].ownerNode.innerText).toBe('.box { background: green; }'); - expect(document.querySelectorAll('[data-percy-cssom-serialized]')).toHaveSize(0); + expect(dom.styleSheets[0].ownerNode.innerText).toBe(''); + expect(dom.styleSheets[1].ownerNode.innerText).toBe(''); + expect(dom.styleSheets[2].ownerNode.innerText).toBe('.box { width: 500px; }'); + expect(dom.styleSheets[3].ownerNode.innerText).toBe('.box { background: green; }'); + expect(dom.querySelectorAll('[data-percy-cssom-serialized]')).toHaveSize(0); }); it('does not break the CSSOM by adding new styles after serializing', () => { - let cssomSheet = document.styleSheets[0]; + let cssomSheet = dom.styleSheets[0]; // serialize DOM serializeDOM(); @@ -48,8 +60,9 @@ describe('serializeCSSOM', () => { }); it('does not serialize the CSSOM when JS is enabled', () => { - let $ = parseDOM(serializeDOM({ enableJavaScript: true })); - expect(document.styleSheets[0]).toHaveProperty('ownerNode.innerText', ''); + const serializedDOM = serializeDOM({ enableJavaScript: true }) + let $ = shadowDom ? parseDeclShadowDOM(serializedDOM) : parseDOM(serializedDOM); + expect(dom.styleSheets[0]).toHaveProperty('ownerNode.innerText', ''); expect($('[data-percy-cssom-serialized]')).toHaveSize(0); }); }); From f24abfe47f4a8379baa87c22cde3824984316013 Mon Sep 17 00:00:00 2001 From: Samarjeet Date: Tue, 3 Jan 2023 20:48:23 +0530 Subject: [PATCH 019/105] shadowDom global in other tests --- packages/dom/test/serialize-canvas.test.js | 10 ++++++---- packages/dom/test/serialize-inputs.test.js | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/dom/test/serialize-canvas.test.js b/packages/dom/test/serialize-canvas.test.js index 2e6a0f8ce..dc7b44305 100644 --- a/packages/dom/test/serialize-canvas.test.js +++ b/packages/dom/test/serialize-canvas.test.js @@ -19,10 +19,10 @@ function prepareTest(shadowDom = false) { let canvas; if (shadowDom) { - withShadowExample(html) + withShadowExample(html); canvas = getExampleShadowRoot().getElementById('canvas') } else { - withExample(html) + withExample(html); canvas = document.getElementById('canvas') } @@ -38,16 +38,18 @@ function prepareTest(shadowDom = false) { ctx.arc(90, 65, 5, 0, Math.PI * 2, true); ctx.stroke(); - return canvas + return canvas; } +let shadowDom = true; + describe('serializeCanvas', () => { let $, serialized, dataURL; beforeEach(() => { let canvas = prepareTest(true); serialized = serializeDOM(); - $ = parseDeclShadowDOM(serialized.html); + $ = shadowDom ? parseDeclShadowDOM(serialized.html) : parseDOM(serialized.html); dataURL = canvas.toDataURL(); }); diff --git a/packages/dom/test/serialize-inputs.test.js b/packages/dom/test/serialize-inputs.test.js index b2c62e5e6..fe1b31145 100644 --- a/packages/dom/test/serialize-inputs.test.js +++ b/packages/dom/test/serialize-inputs.test.js @@ -1,4 +1,3 @@ -import I from 'interactor.js'; import { withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM } from './helpers'; import serializeDOM from '@percy/dom'; @@ -65,7 +64,7 @@ async function prepareTest(shadowDom = false) { option.selected = false; } }); - return dom + return dom; //await I(arg) //.find('#name').type('Bob Boberson') @@ -78,14 +77,16 @@ async function prepareTest(shadowDom = false) { } +let shadowDom = true; + describe('serializeInputs', () => { let $, dom; beforeEach(async () => { - dom = await prepareTest(true) + dom = await prepareTest(shadowDom); // interact with the inputs to update properties (does not update attributes) - $ = parseDeclShadowDOM(serializeDOM()); + $ = shadowDom ? parseDeclShadowDOM(serializeDOM()) : parseDOM(serializeDOM()); }); it('serializes checked checkboxes', () => { From ae3f89d1d35b4514eb9c80ef6be66e07bdb5f2b6 Mon Sep 17 00:00:00 2001 From: Jigar Wala Date: Fri, 6 Jan 2023 16:34:08 +0530 Subject: [PATCH 020/105] update id update polyfill for browsers where we run scripts later fix polyfill --- packages/dom/src/inject-polyfill.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/dom/src/inject-polyfill.js b/packages/dom/src/inject-polyfill.js index 239c3069a..5edf6f63a 100644 --- a/packages/dom/src/inject-polyfill.js +++ b/packages/dom/src/inject-polyfill.js @@ -1,15 +1,14 @@ // 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 -// TODO: provide a way to exlude template tags which we should ignore export function injectDeclarativeShadowDOMPolyfill(ctx) { let clone = ctx.clone; let scriptEl = clone.createElement('script'); - scriptEl.setAttribute('id', '__percy_declarative_shadowdom_polyfill'); + scriptEl.setAttribute('id', '__percy_shadowdom_helper'); scriptEl.setAttribute('data-percy-injected', true); scriptEl.innerHTML = ` - function reversePolyFill(root){ + function reversePolyFill(root=document){ root.querySelectorAll('template[shadowroot]').forEach(template => { const mode = template.getAttribute('shadowroot'); const shadowRoot = template.parentNode.attachShadow({ mode }); @@ -20,7 +19,12 @@ export function injectDeclarativeShadowDOMPolyfill(ctx) { root.querySelectorAll('[data-percy-shadow-host]').forEach(shadowHost => reversePolyFill(shadowHost.shadowRoot)); } - document.addEventListener('DOMContentLoaded', event => reversePolyFill(document)); + + if (["interactive", "complete"].includes(document.readyState)) { + reversePolyFill(); + } else { + document.addEventListener("DOMContentLoaded", () => reversePolyFill()); + } `; clone.body.appendChild(scriptEl); From 70f0f5c032618afa5f231743c39005dae93b8fdd Mon Sep 17 00:00:00 2001 From: Samarjeet Date: Wed, 4 Jan 2023 00:46:22 +0530 Subject: [PATCH 021/105] karma separate shadow dom config --- packages/dom/package.json | 6 ++++-- scripts/test.js | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/dom/package.json b/packages/dom/package.json index b6af3d2eb..77e704d4a 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -18,8 +18,10 @@ "scripts": { "build": "node ../../scripts/build", "lint": "eslint --ignore-path ../../.gitignore .", - "test": "node ../../scripts/test", - "test:coverage": "yarn test --coverage" + "test": "yarn run test:plain && yarn run test:shadow", + "test:plain": "node ../../scripts/test", + "test:shadow": "node ../../scripts/test --shadow", + "test:coverage": "yarn test:plain --coverage" }, "rollup": { "output": { diff --git a/scripts/test.js b/scripts/test.js index b733898b8..e1579a94a 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -67,7 +67,8 @@ async function main({ browsers, coverage, reporter, - karma: karmaArgs + karma: karmaArgs, + shadow } = argv) { // determine arg defaults based on package.json values let pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'))); @@ -87,7 +88,7 @@ async function main({ await child('spawn', nycbin, ['report', '--check-coverage', ...flagify({ reporter })]); } else if (!process.send) { // test runners assume they have control over the entire process, so give them each forks - let flags = flagify({ coverage, karma: karmaArgs }); + let flags = flagify({ coverage, karma: karmaArgs, shadow }); let loader = url.pathToFileURL(path.resolve(filename, '../loader.js')).href; let opts = { execArgv: ['--loader', loader, ...process.execArgv] }; @@ -133,10 +134,23 @@ async function main({ let { Server: KarmaServer, config: { parseConfig } } = Karma; let configFile = path.resolve(filename, '../../karma.config.cjs'); - let karma = new KarmaServer(await parseConfig(configFile, karmaArgs, { + let config = await parseConfig(configFile, karmaArgs, { promiseConfig: true, throwErrors: true - })); + }); + + if (shadow) { + config.set({ + browsers: [ + 'ChromeHeadless' + ], + client: { + shadow: true + } + }); + } + + let karma = new KarmaServer(config); // attach any karma hooks if (pkg.karma) { From 86aea788c0976abffbe6100751fb4a44050e1279 Mon Sep 17 00:00:00 2001 From: Samarjeet Date: Wed, 4 Jan 2023 00:46:51 +0530 Subject: [PATCH 022/105] Use shadow mode in karma --- packages/dom/test/helpers.js | 2 ++ packages/dom/test/serialize-canvas.test.js | 8 ++++---- packages/dom/test/serialize-css.test.js | 4 ++-- packages/dom/test/serialize-frames.test.js | 4 ++-- packages/dom/test/serialize-inputs.test.js | 14 ++------------ packages/dom/test/serialize-videos.test.js | 8 ++++---- 6 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/dom/test/helpers.js b/packages/dom/test/helpers.js index 89cc4b4bd..f24a86f9e 100644 --- a/packages/dom/test/helpers.js +++ b/packages/dom/test/helpers.js @@ -90,6 +90,8 @@ export function parseDeclShadowDOM(domstring) { return selector => root.firstChild.content.querySelectorAll(selector); } +export const isShadowMode = !!window.__karma__.config.shadow; + // generic assert export function assert(condition, message) { if (!condition) throw new Error(message); diff --git a/packages/dom/test/serialize-canvas.test.js b/packages/dom/test/serialize-canvas.test.js index dc7b44305..68bbe31e6 100644 --- a/packages/dom/test/serialize-canvas.test.js +++ b/packages/dom/test/serialize-canvas.test.js @@ -1,4 +1,4 @@ -import { withExample, withShadowExample, parseDOM, parseDeclShadowDOM, getExampleShadowRoot } from './helpers'; +import { withExample, withShadowExample, parseDOM, parseDeclShadowDOM, getExampleShadowRoot, isShadowMode } from './helpers'; import serializeDOM from '@percy/dom'; function prepareTest(shadowDom = false) { @@ -41,13 +41,13 @@ function prepareTest(shadowDom = false) { return canvas; } -let shadowDom = true; +let shadowDom = isShadowMode; describe('serializeCanvas', () => { let $, serialized, dataURL; beforeEach(() => { - let canvas = prepareTest(true); + let canvas = prepareTest(shadowDom); serialized = serializeDOM(); $ = shadowDom ? parseDeclShadowDOM(serialized.html) : parseDOM(serialized.html); dataURL = canvas.toDataURL(); @@ -71,7 +71,7 @@ describe('serializeCanvas', () => { it('does not serialize canvas elements when JS is enabled', () => { serialized = serializeDOM({ enableJavaScript: true }); - $ = parseDeclShadowDOM(serialized.html); + $ = shadowDom ? parseDeclShadowDOM(serialized.html) : parseDOM(serialized.html); let $canvas = $('#canvas'); expect($canvas[0].tagName).toBe('CANVAS'); diff --git a/packages/dom/test/serialize-css.test.js b/packages/dom/test/serialize-css.test.js index e2528b189..c54dc1a43 100644 --- a/packages/dom/test/serialize-css.test.js +++ b/packages/dom/test/serialize-css.test.js @@ -1,7 +1,7 @@ -import { withExample, withCSSOM, parseDOM, withShadowCSSOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM } from './helpers'; +import { withExample, withCSSOM, parseDOM, withShadowCSSOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; import serializeDOM from '@percy/dom'; -let shadowDom = true; +let shadowDom = isShadowMode; describe('serializeCSSOM', () => { let dom = document; beforeEach(() => { diff --git a/packages/dom/test/serialize-frames.test.js b/packages/dom/test/serialize-frames.test.js index 63f9ab69b..aff6a3c15 100644 --- a/packages/dom/test/serialize-frames.test.js +++ b/packages/dom/test/serialize-frames.test.js @@ -1,8 +1,8 @@ import { when } from 'interactor.js'; -import { assert, withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM } from './helpers'; +import { assert, withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; import serializeDOM from '@percy/dom'; -let shadowDom = true; +let shadowDom = isShadowMode; describe('serializeFrames', () => { let $, serialized; diff --git a/packages/dom/test/serialize-inputs.test.js b/packages/dom/test/serialize-inputs.test.js index fe1b31145..4d8ba545b 100644 --- a/packages/dom/test/serialize-inputs.test.js +++ b/packages/dom/test/serialize-inputs.test.js @@ -1,4 +1,4 @@ -import { withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM } from './helpers'; +import { withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; import serializeDOM from '@percy/dom'; async function prepareTest(shadowDom = false) { @@ -65,19 +65,9 @@ async function prepareTest(shadowDom = false) { } }); return dom; - - //await I(arg) - //.find('#name').type('Bob Boberson') - //.find('#valueAttr').type('Replacement Value!', { range: [0, 500] }) - //.find('#feedback').type('This is my feedback... And it is not very helpful') - //.find('#radio').check() - //.find('#singleSelect').select(I.find.text('Maybe')) - //.find('#multiselect').select([I.find.text('Shelby GT350'), I.find.text('NA Miata')]) - //.find('#mailing').check(); - } -let shadowDom = true; +let shadowDom = isShadowMode; describe('serializeInputs', () => { let $, dom; diff --git a/packages/dom/test/serialize-videos.test.js b/packages/dom/test/serialize-videos.test.js index b7007a4e4..804ee09b7 100644 --- a/packages/dom/test/serialize-videos.test.js +++ b/packages/dom/test/serialize-videos.test.js @@ -1,4 +1,4 @@ -import { withShadowExample, parseDOM, parseDeclShadowDOM, withExample, getExampleShadowRoot } from './helpers'; +import { withShadowExample, parseDOM, parseDeclShadowDOM, withExample, getExampleShadowRoot, isShadowMode } from './helpers'; import serializeDOM from '@percy/dom'; let canPlay = $video => new Promise(resolve => { @@ -6,9 +6,9 @@ let canPlay = $video => new Promise(resolve => { else $video.addEventListener('canplay', resolve); }); -let shadowDom = true -let loadExample = shadowDom ? withShadowExample : withExample -let parse = shadowDom ? parseDeclShadowDOM : parseDOM +let shadowDom = isShadowMode; +let loadExample = shadowDom ? withShadowExample : withExample; +let parse = shadowDom ? parseDeclShadowDOM : parseDOM; describe('serializeVideos', () => { let $, serialized; From db609d9dcd473129a8d8e6b8d2da4dd18ba87a1b Mon Sep 17 00:00:00 2001 From: Samarjeet Date: Mon, 9 Jan 2023 18:47:31 +0530 Subject: [PATCH 023/105] refactor dom test to platform --- packages/dom/package.json | 6 +- packages/dom/test/helpers.js | 41 ++-- packages/dom/test/serialize-canvas.test.js | 120 ++++++----- packages/dom/test/serialize-css.test.js | 90 ++++----- packages/dom/test/serialize-frames.test.js | 198 +++++++++---------- packages/dom/test/serialize-inputs.test.js | 220 +++++++++++---------- packages/dom/test/serialize-videos.test.js | 77 ++++---- 7 files changed, 374 insertions(+), 378 deletions(-) diff --git a/packages/dom/package.json b/packages/dom/package.json index 77e704d4a..b6af3d2eb 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -18,10 +18,8 @@ "scripts": { "build": "node ../../scripts/build", "lint": "eslint --ignore-path ../../.gitignore .", - "test": "yarn run test:plain && yarn run test:shadow", - "test:plain": "node ../../scripts/test", - "test:shadow": "node ../../scripts/test --shadow", - "test:coverage": "yarn test:plain --coverage" + "test": "node ../../scripts/test", + "test:coverage": "yarn test --coverage" }, "rollup": { "output": { diff --git a/packages/dom/test/helpers.js b/packages/dom/test/helpers.js index f24a86f9e..ef4169a4d 100644 --- a/packages/dom/test/helpers.js +++ b/packages/dom/test/helpers.js @@ -1,5 +1,5 @@ // create and cleanup testing DOM -export function withExample(html) { +export function withExample(html, shadow = true) { let $test = document.getElementById('test'); if ($test) $test.remove(); @@ -8,29 +8,21 @@ export function withExample(html) { $test.innerHTML = `

Hello DOM testing

${html}`; document.body.appendChild($test); - return document; -} -export function withShadowExample(html) { - let $test = document.getElementById('test'); - if ($test) $test.remove(); + if (shadow) { + let $testShadow = document.getElementById('test-shadow'); + if ($testShadow) $testShadow.remove(); - $test = document.createElement('div'); - $test.id = 'test'; - let $shadow = $test.attachShadow({ mode: 'open' }) - $shadow.innerHTML = `

Hello DOM testing

${html}`; + $testShadow = document.createElement('div'); + $testShadow.id = 'test-shadow'; + let $shadow = $testShadow.attachShadow({ mode: 'open' }); + $shadow.innerHTML = `

Hello DOM testing

${html}`; - document.body.appendChild($test); + document.body.appendChild($testShadow); + } return document; } -export function getExampleShadowRoot() { - let $test = document.getElementById('test'); - if (!$test) return null; - - return $test.shadowRoot; -} - // create a stylesheet in the DOM and add rules using the CSSOM export function withCSSOM(rules = [], prepare = () => {}) { let $test = document.getElementById('test'); @@ -46,10 +38,12 @@ export function withCSSOM(rules = [], prepare = () => {}) { for (let rule of [].concat(rules)) { $style.sheet.insertRule(rule); } + + withShadowCSSOM(rules, prepare); } export function withShadowCSSOM(rules = [], prepare = () => {}) { - let $test = getExampleShadowRoot(); + let $test = document.getElementById('test-shadow').shadowRoot; let $style = $test.getElementById('test-style'); if ($style) $style.remove(); @@ -76,7 +70,10 @@ export function replaceDoctype(name, publicId = '', systemId = '') { } // parses a DOM string into a DOM object and returns a querySelectorAll shortcut -export function parseDOM(domstring) { +export function parseDOM(domstring, platform) { + if (platform === 'shadow') { + return parseDeclShadowDOM(domstring); + } if (domstring.html) domstring = domstring.html; let dom = new window.DOMParser().parseFromString(domstring, 'text/html'); return selector => dom.querySelectorAll(selector); @@ -85,13 +82,11 @@ export function parseDOM(domstring) { export function parseDeclShadowDOM(domstring) { if (domstring.html) domstring = domstring.html; let dom = new window.DOMParser().parseFromString(domstring, 'text/html'); - let root = dom.getElementById('test') + let root = dom.getElementById('test-shadow'); return selector => root.firstChild.content.querySelectorAll(selector); } -export const isShadowMode = !!window.__karma__.config.shadow; - // generic assert export function assert(condition, message) { if (!condition) throw new Error(message); diff --git a/packages/dom/test/serialize-canvas.test.js b/packages/dom/test/serialize-canvas.test.js index 68bbe31e6..db2d575a3 100644 --- a/packages/dom/test/serialize-canvas.test.js +++ b/packages/dom/test/serialize-canvas.test.js @@ -1,8 +1,14 @@ -import { withExample, withShadowExample, parseDOM, parseDeclShadowDOM, getExampleShadowRoot, isShadowMode } from './helpers'; +import { withExample, parseDOM } from './helpers'; import serializeDOM from '@percy/dom'; -function prepareTest(shadowDom = false) { - const html = ` +const platforms = ['plain', 'shadow']; +const pdom = (platform) => platform === 'shadow' ? document.getElementById('test-shadow').shadowRoot : document; + +describe('serializeCanvas', () => { + let serialized, cache = { shadow: {}, plain: {} }; + + beforeEach(() => { + withExample(` ` + ); + platforms.forEach((plat) => { + let dom = pdom(plat); + let canvas = dom.getElementById('canvas'); + let ctx = canvas.getContext('2d'); - let canvas; - - if (shadowDom) { - withShadowExample(html); - canvas = getExampleShadowRoot().getElementById('canvas') - } else { - withExample(html); - canvas = document.getElementById('canvas') - } - - let ctx = canvas.getContext('2d'); - - ctx.beginPath(); - ctx.arc(75, 75, 50, 0, Math.PI * 2, true); - ctx.moveTo(110, 75); - ctx.arc(75, 75, 35, 0, Math.PI, false); - ctx.moveTo(65, 65); - ctx.arc(60, 65, 5, 0, Math.PI * 2, true); - ctx.moveTo(95, 65); - ctx.arc(90, 65, 5, 0, Math.PI * 2, true); - ctx.stroke(); + ctx.beginPath(); + ctx.arc(75, 75, 50, 0, Math.PI * 2, true); + ctx.moveTo(110, 75); + ctx.arc(75, 75, 35, 0, Math.PI, false); + ctx.moveTo(65, 65); + ctx.arc(60, 65, 5, 0, Math.PI * 2, true); + ctx.moveTo(95, 65); + ctx.arc(90, 65, 5, 0, Math.PI * 2, true); + ctx.stroke(); - return canvas; -} + cache[plat].dataURL = canvas.toDataURL(); + }); -let shadowDom = isShadowMode; - -describe('serializeCanvas', () => { - let $, serialized, dataURL; - - beforeEach(() => { - let canvas = prepareTest(shadowDom); serialized = serializeDOM(); - $ = shadowDom ? parseDeclShadowDOM(serialized.html) : parseDOM(serialized.html); - dataURL = canvas.toDataURL(); }); - it('serializes canvas elements', () => { - let $canvas = $('#canvas'); - expect($canvas[0].tagName).toBe('IMG'); - expect($canvas[0].getAttribute('width')).toBe('150px'); - expect($canvas[0].getAttribute('height')).toBe('150px'); - expect($canvas[0].getAttribute('src')).toMatch('/__serialized__/\\w+\\.png'); - expect($canvas[0].getAttribute('style')).toBe('border: 5px solid black; max-width: 100%;'); - expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(true); + platforms.forEach((platform) => { + let $; + beforeEach(() => { + // console.log(`beforeEach ${platform}`); + $ = parseDOM(serialized.html, platform); + }); - expect(serialized.resources).toEqual([{ - url: $canvas[0].getAttribute('src'), - content: dataURL.split(',')[1], - mimetype: 'image/png' - }]); - }); + it(`${platform}: serializes canvas elements`, () => { + let $canvas = $('#canvas'); + expect($canvas[0].tagName).toBe('IMG'); + expect($canvas[0].getAttribute('width')).toBe('150px'); + expect($canvas[0].getAttribute('height')).toBe('150px'); + expect($canvas[0].getAttribute('src')).toMatch('/__serialized__/\\w+\\.png'); + expect($canvas[0].getAttribute('style')).toBe('border: 5px solid black; max-width: 100%;'); + expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(true); - it('does not serialize canvas elements when JS is enabled', () => { - serialized = serializeDOM({ enableJavaScript: true }); - $ = shadowDom ? parseDeclShadowDOM(serialized.html) : parseDOM(serialized.html); + expect(serialized.resources).toContain(jasmine.objectContaining({ + url: $canvas[0].getAttribute('src'), + content: cache[platform].dataURL.split(',')[1], + mimetype: 'image/png' + })); + }); - let $canvas = $('#canvas'); - expect($canvas[0].tagName).toBe('CANVAS'); - expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(false); - expect(serialized.resources).toEqual([]); - }); + it(`${platform}: does not serialize canvas elements when JS is enabled`, () => { + serialized = serializeDOM({ enableJavaScript: true }); + $ = parseDOM(serialized.html, platform); + + let $canvas = $('#canvas'); + expect($canvas[0].tagName).toBe('CANVAS'); + expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(false); + expect(serialized.resources).toEqual([]); + }); - it('does not serialize empty canvas elements', () => { - let $canvas = $('#empty'); - expect($canvas[0].tagName).toBe('CANVAS'); - expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(false); + it(`${platform}: does not serialize empty canvas elements`, () => { + let $canvas = $('#empty'); + expect($canvas[0].tagName).toBe('CANVAS'); + expect($canvas[0].matches('[data-percy-canvas-serialized]')).toBe(false); + }); }); }); diff --git a/packages/dom/test/serialize-css.test.js b/packages/dom/test/serialize-css.test.js index c54dc1a43..3f946572c 100644 --- a/packages/dom/test/serialize-css.test.js +++ b/packages/dom/test/serialize-css.test.js @@ -1,68 +1,68 @@ -import { withExample, withCSSOM, parseDOM, withShadowCSSOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; +import { withExample, withCSSOM, parseDOM } from './helpers'; import serializeDOM from '@percy/dom'; -let shadowDom = isShadowMode; +const platforms = ['plain', 'shadow']; +const pdom = (platform) => platform === 'shadow' ? document.getElementById('test-shadow').shadowRoot : document; + describe('serializeCSSOM', () => { - let dom = document; beforeEach(() => { let link = ''; let mod = ''; let style = ''; - const html = `
${link}${mod}${style}`; - const css = '.box { height: 500px; }'; - - if (shadowDom) { - withShadowExample(html); - withShadowCSSOM(css); - dom = getExampleShadowRoot(); - } else { - withExample(html); - withCSSOM(css); - } + withExample(`
${link}${mod}${style}`); + withCSSOM('.box { height: 500px; }'); - let modCSSRule = dom.getElementById('mod').sheet.cssRules[0]; - if (modCSSRule) modCSSRule.style.cssText = 'width: 1000px'; + platforms.forEach((platform) => { + let modCSSRule = pdom(platform).getElementById('mod').sheet.cssRules[0]; + if (modCSSRule) modCSSRule.style.cssText = 'width: 1000px'; + }); // give the linked style a few milliseconds to load return new Promise(r => setTimeout(r, 100)); }); - it('serializes CSSOM and does not mutate the orignal DOM', () => { - let $ = shadowDom ? parseDeclShadowDOM(serializeDOM()) : parseDOM(serializeDOM()); - let $cssom = $('[data-percy-cssom-serialized]'); + platforms.forEach((platform) => { + let dom; + beforeEach(() => { + dom = pdom(platform); + }); - // linked and unmodified stylesheets are not included - expect($cssom).toHaveSize(2); - expect($cssom[0].innerHTML).toBe('.box { height: 500px; }'); - expect($cssom[1].innerHTML).toBe('.box { width: 1000px; }'); + it(`${platform}: serializes CSSOM and does not mutate the orignal DOM`, () => { + let $cssom = parseDOM(serializeDOM(), platform)('[data-percy-cssom-serialized]'); - expect(dom.styleSheets[0].ownerNode.innerText).toBe(''); - expect(dom.styleSheets[1].ownerNode.innerText).toBe(''); - expect(dom.styleSheets[2].ownerNode.innerText).toBe('.box { width: 500px; }'); - expect(dom.styleSheets[3].ownerNode.innerText).toBe('.box { background: green; }'); - expect(dom.querySelectorAll('[data-percy-cssom-serialized]')).toHaveSize(0); - }); + // linked and unmodified stylesheets are not included + expect($cssom).toHaveSize(2); + expect($cssom[0].innerHTML).toBe('.box { height: 500px; }'); + expect($cssom[1].innerHTML).toBe('.box { width: 1000px; }'); - it('does not break the CSSOM by adding new styles after serializing', () => { - let cssomSheet = dom.styleSheets[0]; + expect(dom.styleSheets[0].ownerNode.innerText).toBe(''); + expect(dom.styleSheets[1].ownerNode.innerText).toBe(''); + expect(dom.styleSheets[2].ownerNode.innerText).toBe('.box { width: 500px; }'); + expect(dom.styleSheets[3].ownerNode.innerText).toBe('.box { background: green; }'); + expect(dom.querySelectorAll('[data-percy-cssom-serialized]')).toHaveSize(0); + }); - // serialize DOM - serializeDOM(); + it(`${platform}: does not break the CSSOM by adding new styles after serializing`, () => { + let cssomSheet = dom.styleSheets[0]; - // delete the old rule and create a new one - cssomSheet.deleteRule(0); - cssomSheet.insertRule('.box { height: 200px; width: 200px; background-color: blue; }'); + // serialize DOM + serializeDOM(); - expect(cssomSheet.cssRules).toHaveSize(1); - expect(cssomSheet.cssRules[0].cssText) - .toBe('.box { height: 200px; width: 200px; background-color: blue; }'); - }); + // delete the old rule and create a new one + cssomSheet.deleteRule(0); + cssomSheet.insertRule('.box { height: 200px; width: 200px; background-color: blue; }'); + + expect(cssomSheet.cssRules).toHaveSize(1); + expect(cssomSheet.cssRules[0].cssText) + .toBe('.box { height: 200px; width: 200px; background-color: blue; }'); + }); - it('does not serialize the CSSOM when JS is enabled', () => { - const serializedDOM = serializeDOM({ enableJavaScript: true }) - let $ = shadowDom ? parseDeclShadowDOM(serializedDOM) : parseDOM(serializedDOM); - expect(dom.styleSheets[0]).toHaveProperty('ownerNode.innerText', ''); - expect($('[data-percy-cssom-serialized]')).toHaveSize(0); + it(`${platform}: does not serialize the CSSOM when JS is enabled`, () => { + const serializedDOM = serializeDOM({ enableJavaScript: true }); + let $ = parseDOM(serializedDOM, platform); + expect(dom.styleSheets[0]).toHaveProperty('ownerNode.innerText', ''); + expect($('[data-percy-cssom-serialized]')).toHaveSize(0); + }); }); }); diff --git a/packages/dom/test/serialize-frames.test.js b/packages/dom/test/serialize-frames.test.js index aff6a3c15..c2a3dcfb0 100644 --- a/packages/dom/test/serialize-frames.test.js +++ b/packages/dom/test/serialize-frames.test.js @@ -1,11 +1,12 @@ import { when } from 'interactor.js'; -import { assert, withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; +import { assert, withExample, parseDOM } from './helpers'; import serializeDOM from '@percy/dom'; -let shadowDom = isShadowMode; +const platforms = ['plain', 'shadow']; +const pdom = (platform) => platform === 'shadow' ? document.getElementById('test-shadow').shadowRoot : document; describe('serializeFrames', () => { - let $, serialized; + let serialized, cache = { shadow: {}, plain: {} }; const getFrame = (id, dom = document) => when(() => { let $frame = dom.getElementById(id); @@ -16,7 +17,7 @@ describe('serializeFrames', () => { }, 5000); beforeEach(async function() { - const html = ` + withExample(` @@ -28,45 +29,37 @@ describe('serializeFrames', () => { - `; - let dom = document; - if (shadowDom) { - withShadowExample(html); - dom = getExampleShadowRoot(); - } else { - withExample(html); - } + `); - let $frameInput = await getFrame('frame-input', dom); - $frameInput.contentDocument.querySelector('input').value = 'iframe with an input'; + for (const platform of platforms) { + let dom = pdom(platform); + let $frameInput = await getFrame('frame-input', dom); + $frameInput.contentDocument.querySelector('input').value = 'iframe with an input'; - let $frameJS = await getFrame('frame-js-no-src', dom); - $frameJS.contentDocument.body.innerHTML = '

generated iframe

'; - let $ctx = $frameJS.contentDocument.getElementById('canvas').getContext('2d'); - $ctx.fillRect(0, 0, 10, 10); + let $frameJS = await getFrame('frame-js-no-src', dom); + $frameJS.contentDocument.body.innerHTML = '

generated iframe

'; + let $ctx = $frameJS.contentDocument.getElementById('canvas').getContext('2d'); + $ctx.fillRect(0, 0, 10, 10); - let $frameEmpty = await getFrame('frame-empty', dom); - $frameEmpty.contentDocument.querySelector('input').value = 'no document element'; - Object.defineProperty($frameEmpty.contentDocument, 'documentElement', { value: null }); + let $frameEmpty = await getFrame('frame-empty', dom); + $frameEmpty.contentDocument.querySelector('input').value = 'no document element'; + Object.defineProperty($frameEmpty.contentDocument, 'documentElement', { value: null }); - let $frameHead = document.createElement('iframe'); - $frameHead.id = 'frame-head'; - document.head.appendChild($frameHead); + let $frameHead = document.createElement('iframe'); + $frameHead.id = 'frame-head'; + document.head.appendChild($frameHead); - let $frameInject = document.createElement('iframe'); - $frameInject.id = 'frame-inject'; - $frameInject.src = 'javascript:false'; - $frameInject.sandbox = ''; - document.getElementById('test').appendChild($frameInject); + let $frameInject = document.createElement('iframe'); + $frameInject.id = 'frame-inject'; + $frameInject.src = 'javascript:false'; + $frameInject.sandbox = ''; + document.getElementById('test').appendChild($frameInject); - // ensure external frame has loaded for coverage - await getFrame('frame-external', dom); + // ensure external frame has loaded for coverage + await getFrame('frame-external', dom); - serialized = serializeDOM(); - if (shadowDom) { - $ = parseDeclShadowDOM(serialized.html); - } else { - $ = parseDOM(serialized.html); + serialized = serializeDOM(); + cache[platform].$ = parseDOM(serialized.html, platform); } }, 0); // frames may take a bit to load @@ -74,68 +67,75 @@ describe('serializeFrames', () => { document.querySelector('#frame-head').remove(); }); - it('serializes iframes created with JS', () => { - expect($('#frame-js')[0].getAttribute('src')).toBeNull(); - expect($('#frame-js')[0].getAttribute('srcdoc')).toBe([ - '', - ``, - '', - '

made with js src

', - '' - ].join('')); - - expect($('#frame-js-no-src')[0].getAttribute('src')).toBeNull(); - expect($('#frame-js-no-src')[0].getAttribute('srcdoc')).toMatch([ - '', - ``, - '', - '

generated iframe

', - '', - '' - ].join('')); - - // frame resources are serialized recursively - expect(serialized.resources).toEqual([{ - url: jasmine.stringMatching('/__serialized__/\\w+\\.png'), - content: jasmine.any(String), - mimetype: 'image/png' - }]); - }); - - it('serializes iframes that have been interacted with', () => { - expect($('#frame-input')[0].getAttribute('srcdoc')).toMatch(new RegExp([ - '^.*?', - '', - '$' - ].join(''))); - }); - - it('does not serialize iframes with CORS', () => { - expect($('#frame-external')[0].getAttribute('src')).toBe('https://example.com'); - expect($('#frame-external-fail')[0].getAttribute('src')).toBe('https://google.com'); - expect($('#frame-external')[0].getAttribute('srcdoc')).toBeNull(); - expect($('#frame-external-fail')[0].getAttribute('srcdoc')).toBeNull(); - }); - - it('does not serialize iframes created by JS when JS is enabled', () => { - const serializedDOM = serializeDOM({ enableJavaScript: true }).html; - $ = shadowDom ? parseDeclShadowDOM(serializedDOM) : parseDOM(serializedDOM); - expect($('#frame-js')[0].getAttribute('src')).not.toBeNull(); - expect($('#frame-js')[0].getAttribute('srcdoc')).toBeNull(); - expect($('#frame-js-no-src')[0].getAttribute('srcdoc')).toBeNull(); - }); - - it('does not serialize iframes without document elements', () => { - expect($('#frame-empty')[0]).toBeDefined(); - expect($('#frame-empty')[0].getAttribute('srcdoc')).toBe(''); - expect($('#frame-empty-self')).toHaveSize(0); - }); - - it('removes iframes from the head element', () => { - expect($('#frame-head')).toHaveSize(0); - }); - - it('removes inaccessible JS frames', () => { - expect($('#frame-inject')).toHaveSize(0); + platforms.forEach(platform => { + let $; + beforeEach(() => { + $ = cache[platform].$; + }); + + it(`${platform}: serializes iframes created with JS`, () => { + expect($('#frame-js')[0].getAttribute('src')).toBeNull(); + expect($('#frame-js')[0].getAttribute('srcdoc')).toBe([ + '', + ``, + '', + '

made with js src

', + '' + ].join('')); + + expect($('#frame-js-no-src')[0].getAttribute('src')).toBeNull(); + expect($('#frame-js-no-src')[0].getAttribute('srcdoc')).toMatch([ + '', + ``, + '', + '

generated iframe

', + '', + '' + ].join('')); + + // frame resources are serialized recursively + expect(serialized.resources).toEqual([{ + url: jasmine.stringMatching('/__serialized__/\\w+\\.png'), + content: jasmine.any(String), + mimetype: 'image/png' + }]); + }); + + it(`${platform}: serializes iframes that have been interacted with`, () => { + expect($('#frame-input')[0].getAttribute('srcdoc')).toMatch(new RegExp([ + '^.*?', + '', + '$' + ].join(''))); + }); + + it(`${platform}: does not serialize iframes with CORS`, () => { + expect($('#frame-external')[0].getAttribute('src')).toBe('https://example.com'); + expect($('#frame-external-fail')[0].getAttribute('src')).toBe('https://google.com'); + expect($('#frame-external')[0].getAttribute('srcdoc')).toBeNull(); + expect($('#frame-external-fail')[0].getAttribute('srcdoc')).toBeNull(); + }); + + it(`${platform}: does not serialize iframes created by JS when JS is enabled`, () => { + const serializedDOM = serializeDOM({ enableJavaScript: true }).html; + $ = parseDOM(serializedDOM, platform); + expect($('#frame-js')[0].getAttribute('src')).not.toBeNull(); + expect($('#frame-js')[0].getAttribute('srcdoc')).toBeNull(); + expect($('#frame-js-no-src')[0].getAttribute('srcdoc')).toBeNull(); + }); + + it(`${platform}: does not serialize iframes without document elements`, () => { + expect($('#frame-empty')[0]).toBeDefined(); + expect($('#frame-empty')[0].getAttribute('srcdoc')).toBe(''); + expect($('#frame-empty-self')).toHaveSize(0); + }); + + it(`${platform}: removes iframes from the head element`, () => { + expect($('#frame-head')).toHaveSize(0); + }); + + it(`${platform}: removes inaccessible JS frames`, () => { + expect($('#frame-inject')).toHaveSize(0); + }); }); }); diff --git a/packages/dom/test/serialize-inputs.test.js b/packages/dom/test/serialize-inputs.test.js index 4d8ba545b..3f5454fe7 100644 --- a/packages/dom/test/serialize-inputs.test.js +++ b/packages/dom/test/serialize-inputs.test.js @@ -1,8 +1,14 @@ -import { withExample, parseDOM, withShadowExample, getExampleShadowRoot, parseDeclShadowDOM, isShadowMode } from './helpers'; +import { withExample, parseDOM } from './helpers'; import serializeDOM from '@percy/dom'; -async function prepareTest(shadowDom = false) { - const html = ` +const platforms = ['plain', 'shadow']; +const pdom = (platform) => platform === 'shadow' ? document.getElementById('test-shadow').shadowRoot : document; + +describe('serializeInputs', () => { + let cache = { shadow: {}, plain: {} }; + + beforeEach(async () => { + withExample(`
@@ -38,113 +44,111 @@ async function prepareTest(shadowDom = false) {
- `; - - if (shadowDom) { - withShadowExample(html); - } else { - withExample(html); - } - - const dom = shadowDom ? getExampleShadowRoot() : document; - dom.querySelector('#name').value = 'Bob Boberson'; - dom.querySelector('#valueAttr').value = 'Replacement Value!'; - dom.querySelector('#feedback').value = 'This is my feedback... And it is not very helpful'; - dom.querySelector('#radio').checked = true; - dom.querySelector('#mailing').checked = true; - dom.querySelector('#singleSelect').value = 'maybe'; - - const selected = ['Shelby GT350', 'NA Miata']; - Array.from(dom.querySelector('#multiselect').options).forEach(function(option) { - // If the option's value is in the selected array, select it - // Otherwise, deselect it - if (selected.includes(option.innerText)) { - option.selected = true; - } else { - option.selected = false; - } - }); - return dom; -} - -let shadowDom = isShadowMode; - -describe('serializeInputs', () => { - let $, dom; - - beforeEach(async () => { - dom = await prepareTest(shadowDom); - + `); + + platforms.forEach((platform) => { + const dom = pdom(platform); + dom.querySelector('#name').value = 'Bob Boberson'; + dom.querySelector('#valueAttr').value = 'Replacement Value!'; + dom.querySelector('#feedback').value = 'This is my feedback... And it is not very helpful'; + dom.querySelector('#radio').checked = true; + dom.querySelector('#mailing').checked = true; + dom.querySelector('#singleSelect').value = 'maybe'; + + const selected = ['Shelby GT350', 'NA Miata']; + Array.from(dom.querySelector('#multiselect').options).forEach(function(option) { + // If the option's value is in the selected array, select it + // Otherwise, deselect it + if (selected.includes(option.innerText)) { + option.selected = true; + } else { + option.selected = false; + } + }); + cache[platform].dom = dom; + cache[platform].$ = parseDOM(serializeDOM(), platform); + }); // interact with the inputs to update properties (does not update attributes) - $ = shadowDom ? parseDeclShadowDOM(serializeDOM()) : parseDOM(serializeDOM()); - }); - - it('serializes checked checkboxes', () => { - expect($('#mailing')[0].checked).toBe(true); - }); - - it('leaves unchecked checkboxes alone', () => { - expect($('#nevercheckedradio')[0].checked).toBe(false); - }); - - it('serializes checked radio buttons', () => { - expect($('#radio')[0].checked).toBe(true); - }); - - it('serializes textareas', () => { - expect($('#feedback')[0].innerText).toBe('This is my feedback... And it is not very helpful'); - }); - - it('serializes input elements', () => { - expect($('#name')[0].getAttribute('value')).toBe('Bob Boberson'); - }); - - it('serializes single select elements', () => { - expect($('#singleSelect>:nth-child(1)')[0].selected).toBe(false); - expect($('#singleSelect>:nth-child(2)')[0].selected).toBe(false); - expect($('#singleSelect>:nth-child(3)')[0].selected).toBe(true); - }); - - it('serializes multi-select elements', () => { - expect($('#multiselect>:nth-child(1)')[0].selected).toBe(true); - expect($('#multiselect>:nth-child(2)')[0].selected).toBe(false); - expect($('#multiselect>:nth-child(3)')[0].selected).toBe(true); - }); - - it('does not mutate original select elements', () => { - let options = [ - ...dom.querySelector('#multiselect').options, - ...dom.querySelector('#singleSelect').options - ]; - - for (let $option of options) { - expect($option.getAttribute('selected')).toBeNull(); - } - }); - - it('serializes inputs with already present value attributes', () => { - expect($('#valueAttr')[0].getAttribute('value')).toBe('Replacement Value!'); - }); - - it('adds a guid data-attribute to the original DOM', () => { - expect(dom.querySelectorAll('[data-percy-element-id]')).toHaveSize(9); - }); - - it('adds matching guids to the orignal DOM and cloned DOM', () => { - let og = dom.querySelector('[data-percy-element-id]').getAttribute('data-percy-element-id'); - expect(og).toEqual($('[data-percy-element-id]')[0].getAttribute('data-percy-element-id')); - }); - - it('does not override previous guids when reserializing', () => { - let getUid = () => dom.querySelector('[data-percy-element-id]').getAttribute('data-percy-element-id'); - let first = getUid(); - - serializeDOM(); - expect(getUid()).toEqual(first); }); - it('does not mutate values in origial DOM', () => { - expect($('#name')[0].getAttribute('value')).toBe('Bob Boberson'); - expect(dom.querySelector('#name').getAttribute('value')).toBeNull(); + platforms.forEach((platform) => { + let $, dom; + beforeEach(() => { + // console.log('beforeEach', cache); + dom = cache[platform].dom; + $ = cache[platform].$; + // dom = pdom(platform); + // $ = parseDOM(platform) + }); + + it(`${platform}: serializes checked checkboxes`, () => { + expect($('#mailing')[0].checked).toBe(true); + }); + + it(`${platform}: leaves unchecked checkboxes alone`, () => { + expect($('#nevercheckedradio')[0].checked).toBe(false); + }); + + it(`${platform}: serializes checked radio buttons`, () => { + expect($('#radio')[0].checked).toBe(true); + }); + + it(`${platform}: serializes textareas`, () => { + expect($('#feedback')[0].innerText).toBe('This is my feedback... And it is not very helpful'); + }); + + it(`${platform}: serializes input elements`, () => { + expect($('#name')[0].getAttribute('value')).toBe('Bob Boberson'); + }); + + it(`${platform}: serializes single select elements`, () => { + expect($('#singleSelect>:nth-child(1)')[0].selected).toBe(false); + expect($('#singleSelect>:nth-child(2)')[0].selected).toBe(false); + expect($('#singleSelect>:nth-child(3)')[0].selected).toBe(true); + }); + + it(`${platform}: serializes multi-select elements`, () => { + expect($('#multiselect>:nth-child(1)')[0].selected).toBe(true); + expect($('#multiselect>:nth-child(2)')[0].selected).toBe(false); + expect($('#multiselect>:nth-child(3)')[0].selected).toBe(true); + }); + + it(`${platform}: does not mutate original select elements`, () => { + let options = [ + ...dom.querySelector('#multiselect').options, + ...dom.querySelector('#singleSelect').options + ]; + + for (let $option of options) { + expect($option.getAttribute('selected')).toBeNull(); + } + }); + + it(`${platform}: serializes inputs with already present value attributes`, () => { + expect($('#valueAttr')[0].getAttribute('value')).toBe('Replacement Value!'); + }); + + it(`${platform}: adds a guid data-attribute to the original DOM`, () => { + // plain platform has extra element #test-shadow + expect(dom.querySelectorAll('[data-percy-element-id]')).toHaveSize(platform === 'plain' ? 10 : 9); + }); + + it(`${platform}: adds matching guids to the orignal DOM and cloned DOM`, () => { + let og = dom.querySelector('[data-percy-element-id]').getAttribute('data-percy-element-id'); + expect(og).toEqual($('[data-percy-element-id]')[0].getAttribute('data-percy-element-id')); + }); + + it(`${platform}: does not override previous guids when reserializing`, () => { + let getUid = () => dom.querySelector('[data-percy-element-id]').getAttribute('data-percy-element-id'); + let first = getUid(); + + serializeDOM(); + expect(getUid()).toEqual(first); + }); + + it(`${platform}: does not mutate values in origial DOM`, () => { + expect($('#name')[0].getAttribute('value')).toBe('Bob Boberson'); + expect(dom.querySelector('#name').getAttribute('value')).toBeNull(); + }); }); }); diff --git a/packages/dom/test/serialize-videos.test.js b/packages/dom/test/serialize-videos.test.js index 804ee09b7..63f764e14 100644 --- a/packages/dom/test/serialize-videos.test.js +++ b/packages/dom/test/serialize-videos.test.js @@ -1,66 +1,67 @@ -import { withShadowExample, parseDOM, parseDeclShadowDOM, withExample, getExampleShadowRoot, isShadowMode } from './helpers'; +import { parseDOM, withExample } from './helpers'; import serializeDOM from '@percy/dom'; +const platforms = ['plain', 'shadow']; +const pdom = (platform) => platform === 'shadow' ? document.getElementById('test-shadow').shadowRoot : document; + let canPlay = $video => new Promise(resolve => { if ($video.readyState > 2) resolve(); else $video.addEventListener('canplay', resolve); }); -let shadowDom = isShadowMode; -let loadExample = shadowDom ? withShadowExample : withExample; -let parse = shadowDom ? parseDeclShadowDOM : parseDOM; - describe('serializeVideos', () => { let $, serialized; - it('serializes video elements', async () => { - loadExample(` -