@percy/dom 1.32.8-beta.2 → 1.32.8-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bundle.js +70 -28
  2. package/package.json +2 -2
package/dist/bundle.js CHANGED
@@ -209,7 +209,9 @@
209
209
  ignoreIframeSelectors,
210
210
  forceShadowAsLightDOM,
211
211
  maxIframeDepth,
212
- iframeDepth = 0
212
+ iframeDepth = 0,
213
+ pseudoClassEnabledElements,
214
+ pseudoClassSerialization
213
215
  }) {
214
216
  maxIframeDepth = clampIframeDepth(maxIframeDepth);
215
217
  for (let frame of dom.querySelectorAll('iframe')) {
@@ -278,7 +280,9 @@
278
280
  forceShadowAsLightDOM,
279
281
  ignoreIframeSelectors,
280
282
  maxIframeDepth,
281
- iframeDepth: iframeDepth + 1
283
+ iframeDepth: iframeDepth + 1,
284
+ pseudoClassEnabledElements,
285
+ enablePseudoClassSerialization: pseudoClassSerialization
282
286
  });
283
287
 
284
288
  // append serialized warnings and resources
@@ -360,12 +364,7 @@
360
364
  // Custom elements with ElementInternals or closed shadow roots also get
361
365
  // stamped so the post-clone state-fallback can locate their clones.
362
366
  let tagName = (_domElement$tagName = domElement.tagName) === null || _domElement$tagName === void 0 ? void 0 : _domElement$tagName.toLowerCase();
363
- // Stylesheet <link>s are stamped so the pseudo-class serializer can locate
364
- // the sheet's clone element and anchor its rewritten interactive-state rules
365
- // immediately after it — preserving the sheet's original cascade position
366
- // instead of appending at the end of <head> (PER-10077).
367
- let isStylesheetLink = tagName === 'link' && /(^|\s)stylesheet(\s|$)/i.test(domElement.getAttribute('rel') || '');
368
- if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style', 'dialog'].includes(tagName) || isStylesheetLink || isCustomElement(domElement)) {
367
+ if (['input', 'textarea', 'select', 'iframe', 'canvas', 'video', 'style', 'dialog'].includes(tagName) || isCustomElement(domElement)) {
369
368
  if (!domElement.getAttribute('data-percy-element-id')) {
370
369
  domElement.setAttribute('data-percy-element-id', uid());
371
370
  }
@@ -433,7 +432,8 @@
433
432
  resources,
434
433
  cache,
435
434
  warnings,
436
- ignoreStyleSheetSerializationErrors
435
+ ignoreStyleSheetSerializationErrors,
436
+ styleSheetClones
437
437
  } = ctx;
438
438
  // in-memory CSSOM into their respective DOM nodes.
439
439
  let styleSheets = null;
@@ -462,6 +462,7 @@
462
462
  style.textContent = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
463
463
  cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling);
464
464
  cloneOwnerNode.remove();
465
+ styleSheetClones === null || styleSheetClones === void 0 || styleSheetClones.set(styleSheet.ownerNode, style);
465
466
  } catch (err) {
466
467
  handleErrors(err, 'Error serializing stylesheet: ', cloneOwnerNode, {
467
468
  styleId: styleId
@@ -778,6 +779,35 @@
778
779
  return re.test(selectorText);
779
780
  });
780
781
  }
782
+ function splitSelectorList(selectorText) {
783
+ const parts = [];
784
+ let depth = 0;
785
+ let quote = null;
786
+ let start = 0;
787
+ for (let i = 0; i < selectorText.length; i++) {
788
+ const char = selectorText[i];
789
+ if (quote) {
790
+ if (char === '\\') i++;else if (char === quote) quote = null;
791
+ continue;
792
+ }
793
+ if (char === '"' || char === "'") quote = char;else if (char === '(' || char === '[') depth++;else if (char === ')' || char === ']') depth--;else if (char === ',' && depth === 0) {
794
+ parts.push(selectorText.slice(start, i));
795
+ start = i + 1;
796
+ }
797
+ }
798
+ parts.push(selectorText.slice(start));
799
+ return parts;
800
+ }
801
+ function rewritePseudoSelectorList(selectorText, pseudoList) {
802
+ const pseudoParts = [];
803
+ for (const part of splitSelectorList(selectorText)) {
804
+ const trimmed = part.trim();
805
+ if (trimmed && selectorContainsPseudo(trimmed, pseudoList)) {
806
+ pseudoParts.push(rewritePseudoSelector(trimmed));
807
+ }
808
+ }
809
+ return pseudoParts.length ? pseudoParts.join(', ') : null;
810
+ }
781
811
  function rewritePseudoSelector(selectorText) {
782
812
  let out = selectorText;
783
813
  for (const [pseudo, re] of PSEUDO_RES) out = out.replace(re, PSEUDO_TO_ATTR[pseudo]);
@@ -978,8 +1008,9 @@
978
1008
  // the data-attributes are copied through to the clone via cloneNode.
979
1009
  function markPseudoClassElements(ctx, config) {
980
1010
  ctx._liveMutations = [];
981
- markInteractiveStates(ctx);
982
1011
  markOpenPopovers(ctx);
1012
+ if (!ctx.pseudoClassSerialization) return;
1013
+ markInteractiveStates(ctx);
983
1014
  if (config) getElementsToProcess(ctx, config, true);
984
1015
  }
985
1016
 
@@ -1118,8 +1149,8 @@
1118
1149
  // interactive pseudo. Skips most rules on most stylesheets without
1119
1150
  // touching the regex bank.
1120
1151
  if (!rule.selectorText.includes(':')) continue;
1121
- if (!selectorContainsPseudo(rule.selectorText, ALL_INTERACTIVE_PSEUDO)) continue;
1122
- const rewrittenSelector = rewritePseudoSelector(rule.selectorText);
1152
+ const rewrittenSelector = rewritePseudoSelectorList(rule.selectorText, ALL_INTERACTIVE_PSEUDO);
1153
+ if (!rewrittenSelector) continue;
1123
1154
  const cssText = `${rewrittenSelector} { ${rule.style.cssText} }`;
1124
1155
  const wrapped = rule.wrapper ? `${rule.wrapper} { ${cssText} }` : cssText;
1125
1156
  (rewrittenRules || (rewrittenRules = [])).push(wrapped);
@@ -1156,11 +1187,11 @@
1156
1187
  // nosemgrep: javascript.browser.security.insecure-document-method.insecure-document-method
1157
1188
  styleElement.textContent = rewrittenRules.join('\n');
1158
1189
  if (owner === null) {
1159
- injectAtSheetPosition(ctx.clone, sheet, styleElement, ctx.clone.head || ctx.clone.querySelector('head'));
1190
+ injectAtSheetPosition(ctx, ctx.clone, sheet, styleElement, ctx.clone.head || ctx.clone.querySelector('head'));
1160
1191
  } else {
1161
1192
  const cloneHost = cloneByPercyId.get(owner.getAttribute('data-percy-element-id'));
1162
1193
  if (cloneHost && cloneHost.shadowRoot) {
1163
- injectAtSheetPosition(cloneHost.shadowRoot, sheet, styleElement, cloneHost.shadowRoot);
1194
+ injectAtSheetPosition(ctx, cloneHost.shadowRoot, sheet, styleElement, cloneHost.shadowRoot);
1164
1195
  }
1165
1196
  }
1166
1197
  }
@@ -1169,13 +1200,15 @@
1169
1200
  // Insert an interactive-states <style> immediately AFTER its source sheet's
1170
1201
  // clone element so the copy keeps the sheet's original source-order rank —
1171
1202
  // later stylesheets still win equal-specificity ties, exactly as on the live
1172
- // page (PER-10077). The sheet's ownerNode (<style> / <link rel=stylesheet>) is
1173
- // stamped with a data-percy-element-id, which serialize-cssom preserves on any
1174
- // node it rebuilds in place. Falls back to appending at the scope's end when
1175
- // the sheet has no locatable clone anchor.
1176
- function injectAtSheetPosition(scopeRoot, sheet, styleElement, fallbackTarget) {
1177
- const anchorId = sheet.ownerNode.getAttribute('data-percy-element-id');
1178
- const anchor = anchorId ? scopeRoot.querySelector(`[data-percy-element-id="${anchorId}"]`) : null;
1203
+ // page (PER-10077).
1204
+ function injectAtSheetPosition(ctx, scopeRoot, sheet, styleElement, fallbackTarget) {
1205
+ var _ctx$styleSheetClones, _anchor;
1206
+ const ownerNode = sheet.ownerNode;
1207
+ let anchor = ownerNode ? (_ctx$styleSheetClones = ctx.styleSheetClones) === null || _ctx$styleSheetClones === void 0 ? void 0 : _ctx$styleSheetClones.get(ownerNode) : null;
1208
+ if (!((_anchor = anchor) !== null && _anchor !== void 0 && _anchor.parentNode) && ownerNode) {
1209
+ const anchorId = ownerNode.getAttribute('data-percy-element-id');
1210
+ anchor = anchorId ? scopeRoot.querySelector(`[data-percy-element-id="${anchorId}"]`) : null;
1211
+ }
1179
1212
  const anchorParent = anchor ? anchor.parentNode : null;
1180
1213
  if (anchorParent) {
1181
1214
  anchorParent.insertBefore(styleElement, anchor.nextSibling);
@@ -1184,10 +1217,11 @@
1184
1217
  }
1185
1218
  }
1186
1219
  function serializePseudoClasses(ctx) {
1187
- // Auto-detect path runs unconditionally so `:focus`/`:focus-within`
1188
- // rules are rewritten regardless of whether the user configured a
1189
- // pseudoClassEnabledElements list (and regardless of whether that list
1190
- // matched anything on this page).
1220
+ if (!ctx.pseudoClassSerialization) return;
1221
+
1222
+ // Auto-detect path rewrites `:focus`/`:focus-within` rules whether or not
1223
+ // a pseudoClassEnabledElements list was configured (and regardless of
1224
+ // whether that list matched anything on this page).
1191
1225
  extractPseudoClassRules(ctx);
1192
1226
  if (!ctx.pseudoClassEnabledElements) return;
1193
1227
  const elements = ctx.dom.querySelectorAll(`[${PSEUDO_ELEMENT_MARKER_ATTR}]`);
@@ -1384,7 +1418,8 @@
1384
1418
  forceShadowAsLightDOM,
1385
1419
  resources,
1386
1420
  cache,
1387
- enableJavaScript
1421
+ enableJavaScript,
1422
+ styleSheetClones
1388
1423
  } = ctx;
1389
1424
  // clones shadow DOM and light DOM for a given node
1390
1425
  let cloneNode = (node, parent) => {
@@ -1406,6 +1441,9 @@
1406
1441
  // mark the node before cloning
1407
1442
  markElement(node, disableShadowDOM, forceShadowAsLightDOM);
1408
1443
  let clone = cloneElementWithoutLifecycle(node);
1444
+ if (styleSheetClones && (node.nodeName === 'LINK' || node.nodeName === 'STYLE')) {
1445
+ styleSheetClones.set(node, clone);
1446
+ }
1409
1447
 
1410
1448
  // Custom-element :state() is captured by the fallback path in
1411
1449
  // serialize-custom-states.js (live el.matches against state names
@@ -1614,8 +1652,10 @@
1614
1652
  ignoreIframeSelectors = options === null || options === void 0 ? void 0 : options.ignore_iframe_selectors,
1615
1653
  maxIframeDepth = options === null || options === void 0 ? void 0 : options.max_iframe_depth,
1616
1654
  iframeDepth = (options === null || options === void 0 ? void 0 : options.iframe_depth) ?? 0,
1617
- pseudoClassEnabledElements = options === null || options === void 0 ? void 0 : options.pseudo_class_enabled_elements
1655
+ pseudoClassEnabledElements = options === null || options === void 0 ? void 0 : options.pseudo_class_enabled_elements,
1656
+ enablePseudoClassSerialization = options === null || options === void 0 ? void 0 : options.enable_pseudo_class_serialization
1618
1657
  } = options || {};
1658
+ const pseudoClassSerialization = !!(enablePseudoClassSerialization || pseudoClassEnabledElements);
1619
1659
 
1620
1660
  // keep certain records throughout serialization
1621
1661
  let ctx = {
@@ -1623,6 +1663,7 @@
1623
1663
  warnings: new Set(),
1624
1664
  hints: new Set(),
1625
1665
  cache: new Map(),
1666
+ styleSheetClones: new WeakMap(),
1626
1667
  shadowRootElements: [],
1627
1668
  enableJavaScript,
1628
1669
  disableShadowDOM,
@@ -1632,7 +1673,8 @@
1632
1673
  ignoreIframeSelectors,
1633
1674
  maxIframeDepth,
1634
1675
  iframeDepth,
1635
- pseudoClassEnabledElements
1676
+ pseudoClassEnabledElements,
1677
+ pseudoClassSerialization
1636
1678
  };
1637
1679
  ctx.dom = dom;
1638
1680
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/dom",
3
- "version": "1.32.8-beta.2",
3
+ "version": "1.32.8-beta.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,5 +35,5 @@
35
35
  "devDependencies": {
36
36
  "interactor.js": "^2.0.0-beta.10"
37
37
  },
38
- "gitHead": "3886c115aa583e2c5af3b16643dd34c379415f5b"
38
+ "gitHead": "8cb91228f629119207fa169f8db8388d66e0c2e0"
39
39
  }