From 87c4c5f2e37e03241d4d79c8ac45aa581c7444cd Mon Sep 17 00:00:00 2001 From: Jigar Wala Date: Mon, 30 Oct 2023 18:11:37 +0530 Subject: [PATCH 1/5] Initial commit --- packages/core/src/config.js | 7 +++++-- packages/core/src/discovery.js | 9 +++++++++ packages/core/src/page.js | 4 ++-- packages/core/types/index.d.ts | 2 ++ packages/dom/README.md | 3 ++- packages/dom/src/serialize-dom.js | 12 +++++++++++- 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 51af87a16..b37c50602 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -44,8 +44,7 @@ export const configSchema = { default: true }, disableShadowDOM: { - type: 'boolean', - default: false + type: 'boolean' }, enableLayout: { type: 'boolean' @@ -53,6 +52,9 @@ export const configSchema = { domTransformation: { type: 'string' }, + reshuffleInvalidTags: { + type: 'boolean' + }, scope: { type: 'string' }, @@ -236,6 +238,7 @@ export const snapshotSchema = { disableShadowDOM: { $ref: '/config/snapshot#/properties/disableShadowDOM' }, domTransformation: { $ref: '/config/snapshot#/properties/domTransformation' }, enableLayout: { $ref: '/config/snapshot#/properties/enableLayout' }, + reshuffleInvalidTags: { $ref: '/config/snapshot#/properties/reshuffleInvalidTags' }, discovery: { type: 'object', additionalProperties: false, diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index 0ca3cecda..00a297d36 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -37,6 +37,8 @@ function debugSnapshotOptions(snapshot) { debugProp(snapshot, 'cliEnableJavaScript'); debugProp(snapshot, 'disableShadowDOM'); debugProp(snapshot, 'enableLayout'); + debugProp(snapshot, 'domTransformation'); + debugProp(snapshot, 'reshuffleInvalidTags'); debugProp(snapshot, 'deviceScaleFactor'); debugProp(snapshot, 'waitForTimeout'); debugProp(snapshot, 'waitForSelector'); @@ -92,6 +94,7 @@ function parseDomResources({ url, domSnapshot }) { // Calls the provided callback with additional resources function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { + let log = logger('core:snapshot'); resources = [...(resources?.values() ?? [])]; // find any root resource matching the provided dom snapshot @@ -107,6 +110,12 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { // inject Percy CSS if (snapshot.percyCSS) { + // check @percy/dom/serialize-dom.js + let elementsOutsideBodySerializationWarningRegex = /<\/body>/; + if (domSnapshot?.warnings.some(e => elementsOutsideBodySerializationWarningRegex.test(e))) { + log.warn('percyCSS might not work, please follow LINK'); + } + let css = createPercyCSSResource(root.url, snapshot.percyCSS); resources.push(css); diff --git a/packages/core/src/page.js b/packages/core/src/page.js index 0dc759251..fbed6ea83 100644 --- a/packages/core/src/page.js +++ b/packages/core/src/page.js @@ -141,7 +141,7 @@ export class Page { execute, ...snapshot }) { - let { name, width, enableJavaScript, disableShadowDOM, domTransformation } = snapshot; + let { name, width, enableJavaScript, disableShadowDOM, domTransformation, reshuffleInvalidTags } = snapshot; this.log.debug(`Taking snapshot: ${name}${width ? ` @${width}px` : ''}`, this.meta); // wait for any specified timeout @@ -182,7 +182,7 @@ export class Page { /* eslint-disable-next-line no-undef */ domSnapshot: PercyDOM.serialize(options), url: document.URL - }), { enableJavaScript, disableShadowDOM, domTransformation }); + }), { enableJavaScript, disableShadowDOM, domTransformation, reshuffleInvalidTags }); return { ...snapshot, ...capture }; } diff --git a/packages/core/types/index.d.ts b/packages/core/types/index.d.ts index 4055b1d7b..79b170316 100644 --- a/packages/core/types/index.d.ts +++ b/packages/core/types/index.d.ts @@ -40,6 +40,8 @@ interface CommonSnapshotOptions { enableJavaScript?: boolean; disableShadowDOM?: boolean; enableLayout?: boolean; + domTransformation?: string; + reshuffleInvalidTags?: boolean; devicePixelRatio?: number; scope?: string; } diff --git a/packages/dom/README.md b/packages/dom/README.md index fcbd2788f..04154bf58 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -37,7 +37,8 @@ const domSnapshot = await page.evaluate(() => PercyDOM.serialize(options)) - `enableJavaScript` — When true, does not serialize some DOM elements - `domTransformation` — Function to transform the DOM after serialization -- `disableShadowDOM` — disable shadow DOM capturing, this option can be passed to `percySnapshot` its part of per-snapshot config. +- `disableShadowDOM` — disable shadow DOM capturing, usually to be used when `enableJavascript: true` +- `reshuffleInvalidTags` — moves DOM tags which are outside `` to its inside to make the DOM compliant. ## Serialized Content diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index a1b902c84..f3d401990 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -64,7 +64,8 @@ export function serializeDOM(options) { enableJavaScript = options?.enable_javascript, domTransformation = options?.dom_transformation, stringifyResponse = options?.stringify_response, - disableShadowDOM = options?.disable_shadow_dom + disableShadowDOM = options?.disable_shadow_dom, + reshuffleInvalidTags = options?.reshuffle_invalid_tags } = options || {}; // keep certain records throughout serialization @@ -94,6 +95,15 @@ export function serializeDOM(options) { } if (!disableShadowDOM) { injectDeclarativeShadowDOMPolyfill(ctx); } + if (reshuffleInvalidTags) { + let clonedBody = ctx.clone.body; + while (clonedBody.nextSibling) { + let sibling = clonedBody.nextSibling; + clonedBody.append(sibling); + } + } else if (ctx.clone.body.nextSibling) { + ctx.warnings.add('DOM elements found outside '); + } let result = { html: serializeHTML(ctx), From 4c08d45886858bd0fe9fc301e754fe07dd19ff44 Mon Sep 17 00:00:00 2001 From: Jigar Wala Date: Mon, 30 Oct 2023 15:58:05 +0530 Subject: [PATCH 2/5] add hints array --- packages/core/src/config.js | 4 ++++ packages/core/src/discovery.js | 6 +++--- packages/dom/src/serialize-dom.js | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core/src/config.js b/packages/core/src/config.js index b37c50602..61924c3c6 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -401,6 +401,10 @@ export const snapshotSchema = { mimetype: { type: 'string' } } } + }, + hints: { + type: 'array', + items: { type: 'string' } } } }] diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index 00a297d36..b0e4ca885 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -111,9 +111,9 @@ function processSnapshotResources({ domSnapshot, resources, ...snapshot }) { // inject Percy CSS if (snapshot.percyCSS) { // check @percy/dom/serialize-dom.js - let elementsOutsideBodySerializationWarningRegex = /<\/body>/; - if (domSnapshot?.warnings.some(e => elementsOutsideBodySerializationWarningRegex.test(e))) { - log.warn('percyCSS might not work, please follow LINK'); + let domSnapshotHints = domSnapshot?.hints ?? []; + if (domSnapshotHints.includes('DOM elements found outside ')) { + log.warn('DOM elements found outside , percyCSS might not work'); } let css = createPercyCSSResource(root.url, snapshot.percyCSS); diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index f3d401990..6840f9903 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -72,6 +72,7 @@ export function serializeDOM(options) { let ctx = { resources: new Set(), warnings: new Set(), + hints: new Set(), cache: new Map(), enableJavaScript, disableShadowDOM @@ -102,13 +103,14 @@ export function serializeDOM(options) { clonedBody.append(sibling); } } else if (ctx.clone.body.nextSibling) { - ctx.warnings.add('DOM elements found outside '); + ctx.hints.add('DOM elements found outside '); } let result = { html: serializeHTML(ctx), warnings: Array.from(ctx.warnings), - resources: Array.from(ctx.resources) + resources: Array.from(ctx.resources), + hints: Array.from(ctx.hints) }; return stringifyResponse From bbc08b13aaca3228388cf611e9ae3695fa0b1450 Mon Sep 17 00:00:00 2001 From: Jigar Wala Date: Mon, 30 Oct 2023 15:59:20 +0530 Subject: [PATCH 3/5] revert --- packages/core/src/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 61924c3c6..ce1d5bef0 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -44,7 +44,8 @@ export const configSchema = { default: true }, disableShadowDOM: { - type: 'boolean' + type: 'boolean', + default: false }, enableLayout: { type: 'boolean' From 8cf766dc3135bcb338e6a961015d54d26e83ea38 Mon Sep 17 00:00:00 2001 From: Jigar Wala Date: Mon, 30 Oct 2023 17:06:00 +0530 Subject: [PATCH 4/5] adds and fix tests --- packages/dom/test/helpers.js | 13 +++++++++++-- packages/dom/test/serialize-dom.test.js | 25 ++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/dom/test/helpers.js b/packages/dom/test/helpers.js index 2877d3404..84938e170 100644 --- a/packages/dom/test/helpers.js +++ b/packages/dom/test/helpers.js @@ -3,7 +3,7 @@ export const chromeBrowser = 'CHROME'; export const firefoxBrowser = 'FIREFOX'; // create and cleanup testing DOM -export function withExample(html, options = { withShadow: true }) { +export function withExample(html, options = { withShadow: true, invalidTagsOutsideBody: false }) { let $test = document.getElementById('test'); if ($test) $test.remove(); @@ -16,7 +16,7 @@ export function withExample(html, options = { withShadow: true }) { document.body.appendChild($test); - if (options?.withShadow) { + if (options.withShadow) { $testShadow = document.createElement('div'); $testShadow.id = 'test-shadow'; let $shadow = $testShadow.attachShadow({ mode: 'open' }); @@ -24,6 +24,15 @@ export function withExample(html, options = { withShadow: true }) { document.body.appendChild($testShadow); } + + if (options.invalidTagsOutsideBody) { + let p = document.getElementById('invalid-p'); + p?.remove(); + p = document.createElement('p'); + p.id = 'invalid-p'; + p.innerText = 'P tag outside body'; + document.documentElement.append(p); + } return document; } diff --git a/packages/dom/test/serialize-dom.test.js b/packages/dom/test/serialize-dom.test.js index 1da2d53ee..5fcecfe6f 100644 --- a/packages/dom/test/serialize-dom.test.js +++ b/packages/dom/test/serialize-dom.test.js @@ -6,7 +6,8 @@ describe('serializeDOM', () => { expect(serializeDOM()).toEqual({ html: jasmine.any(String), warnings: jasmine.any(Array), - resources: jasmine.any(Array) + resources: jasmine.any(Array), + hints: jasmine.any(Array) }); }); @@ -27,7 +28,7 @@ describe('serializeDOM', () => { it('optionally returns a stringified response', () => { expect(serializeDOM({ stringifyResponse: true })) - .toMatch('{"html":".*","warnings":\\[\\],"resources":\\[\\]}'); + .toMatch('{"html":".*","warnings":\\[\\],"resources":\\[\\],"hints":\\[\\]}'); }); it('always has a doctype', () => { @@ -68,7 +69,7 @@ describe('serializeDOM', () => { expect($('h2.callback').length).toEqual(1); }); - it('applies dom transformations', () => { + it('applies default dom transformations', () => { withExample('