diff --git a/packages/dom/src/clone-dom.js b/packages/dom/src/clone-dom.js index 2f7c8b491..d7844b974 100644 --- a/packages/dom/src/clone-dom.js +++ b/packages/dom/src/clone-dom.js @@ -49,7 +49,7 @@ function cloneElementWithoutLifecycle(element) { } export function cloneNodeAndShadow(ctx) { - let { dom, disableShadowDOM, forceShadowAsLightDOM, resources, cache, enableJavaScript } = ctx; + let { dom, disableShadowDOM, forceShadowAsLightDOM, resources, cache, enableJavaScript, styleSheetClones } = ctx; // clones shadow DOM and light DOM for a given node let cloneNode = (node, parent) => { try { @@ -73,6 +73,10 @@ export function cloneNodeAndShadow(ctx) { let clone = cloneElementWithoutLifecycle(node); + if (styleSheetClones && (node.nodeName === 'LINK' || node.nodeName === 'STYLE')) { + styleSheetClones.set(node, clone); + } + // Custom-element :state() is captured by the fallback path in // serialize-custom-states.js (live el.matches against state names // discovered in CSS) — no clone-time fast path remains. diff --git a/packages/dom/src/prepare-dom.js b/packages/dom/src/prepare-dom.js index fe10a496a..bf494866f 100644 --- a/packages/dom/src/prepare-dom.js +++ b/packages/dom/src/prepare-dom.js @@ -11,15 +11,8 @@ export function markElement(domElement, disableShadowDOM, forceShadowAsLightDOM) // Custom elements with ElementInternals or closed shadow roots also get // stamped so the post-clone state-fallback can locate their clones. let tagName = domElement.tagName?.toLowerCase(); - // Stylesheet s are stamped so the pseudo-class serializer can locate - // the sheet's clone element and anchor its rewritten interactive-state rules - // immediately after it — preserving the sheet's original cascade position - // instead of appending at the end of
(PER-10077). - let isStylesheetLink = tagName === 'link' && - /(^|\s)stylesheet(\s|$)/i.test(domElement.getAttribute('rel') || ''); if ( ['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style', 'dialog'].includes(tagName) || - isStylesheetLink || isCustomElement(domElement) ) { if (!domElement.getAttribute('data-percy-element-id')) { diff --git a/packages/dom/src/serialize-cssom.js b/packages/dom/src/serialize-cssom.js index fb8cbc23c..3f2caae89 100644 --- a/packages/dom/src/serialize-cssom.js +++ b/packages/dom/src/serialize-cssom.js @@ -51,7 +51,7 @@ function createStyleResource(styleSheet) { } export function serializeCSSOM(ctx) { - let { dom, clone, resources, cache, warnings, ignoreStyleSheetSerializationErrors } = ctx; + let { dom, clone, resources, cache, warnings, ignoreStyleSheetSerializationErrors, styleSheetClones } = ctx; // in-memory CSSOM into their respective DOM nodes. let styleSheets = null; // catch error in case styleSheets property is not available (overwritten to throw error) @@ -81,6 +81,7 @@ export function serializeCSSOM(ctx) { cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling); cloneOwnerNode.remove(); + styleSheetClones?.set(styleSheet.ownerNode, style); } catch (err) { handleErrors(err, 'Error serializing stylesheet: ', cloneOwnerNode, { styleId: styleId diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index c9cb0bb6c..f287c31f4 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -112,6 +112,7 @@ export function serializeDOM(options) { warnings: new Set(), hints: new Set(), cache: new Map(), + styleSheetClones: new WeakMap(), shadowRootElements: [], enableJavaScript, disableShadowDOM, diff --git a/packages/dom/src/serialize-pseudo-classes.js b/packages/dom/src/serialize-pseudo-classes.js index 06571965f..877ddb759 100644 --- a/packages/dom/src/serialize-pseudo-classes.js +++ b/packages/dom/src/serialize-pseudo-classes.js @@ -510,11 +510,11 @@ function extractPseudoClassRules(ctx) { styleElement.textContent = rewrittenRules.join('\n'); if (owner === null) { - injectAtSheetPosition(ctx.clone, sheet, styleElement, ctx.clone.head || ctx.clone.querySelector('head')); + injectAtSheetPosition(ctx, ctx.clone, sheet, styleElement, ctx.clone.head || ctx.clone.querySelector('head')); } else { const cloneHost = cloneByPercyId.get(owner.getAttribute('data-percy-element-id')); if (cloneHost && cloneHost.shadowRoot) { - injectAtSheetPosition(cloneHost.shadowRoot, sheet, styleElement, cloneHost.shadowRoot); + injectAtSheetPosition(ctx, cloneHost.shadowRoot, sheet, styleElement, cloneHost.shadowRoot); } } } @@ -523,13 +523,16 @@ function extractPseudoClassRules(ctx) { // Insert an interactive-states ' + + '', + { withShadow: false } + ); + let link = document.querySelector('link[href="base/test/assets/hover-anchor.css"]'); + await new Promise((resolve, reject) => { + if (link.sheet) return resolve(); + link.addEventListener('load', resolve, { once: true }); + link.addEventListener('error', () => reject(new Error('hover-anchor.css failed to load')), { once: true }); + }); + + let html = serializeDOM({ enablePseudoClassSerialization: true }).html; + let sourceIdx = html.indexOf('hover-anchor.css'); + let copyIdx = html.indexOf('.lbtn[data-percy-hover]'); + let laterIdx = html.indexOf('.lbtn.later'); + expect(copyIdx).toBeGreaterThan(-1); + expect(copyIdx).toBeGreaterThan(sourceIdx); + expect(copyIdx).toBeLessThan(laterIdx); + }); }); describe(':state() CSS rewriting', () => {