@percy/dom 1.32.6-beta.2 → 1.32.6
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.
- package/dist/bundle.js +59 -64
- package/package.json +3 -3
package/dist/bundle.js
CHANGED
|
@@ -359,7 +359,13 @@
|
|
|
359
359
|
// Mark elements that are to be serialized later with a data attribute.
|
|
360
360
|
// Custom elements with ElementInternals or closed shadow roots also get
|
|
361
361
|
// stamped so the post-clone state-fallback can locate their clones.
|
|
362
|
-
|
|
362
|
+
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)) {
|
|
363
369
|
if (!domElement.getAttribute('data-percy-element-id')) {
|
|
364
370
|
domElement.setAttribute('data-percy-element-id', uid());
|
|
365
371
|
}
|
|
@@ -752,16 +758,12 @@
|
|
|
752
758
|
const POPOVER_OPEN_ATTR = 'data-percy-popover-open';
|
|
753
759
|
const FOCUS_ATTR = 'data-percy-focus';
|
|
754
760
|
const FOCUS_WITHIN_ATTR = 'data-percy-focus-within';
|
|
755
|
-
const CHECKED_ATTR = 'data-percy-checked';
|
|
756
|
-
const DISABLED_ATTR = 'data-percy-disabled';
|
|
757
761
|
const HOVER_ATTR = 'data-percy-hover';
|
|
758
762
|
const ACTIVE_ATTR = 'data-percy-active';
|
|
759
|
-
const ALL_INTERACTIVE_PSEUDO = [':focus', ':focus-within', ':
|
|
763
|
+
const ALL_INTERACTIVE_PSEUDO = [':focus', ':focus-within', ':hover', ':active'];
|
|
760
764
|
const PSEUDO_TO_ATTR = {
|
|
761
765
|
':focus': '[data-percy-focus]',
|
|
762
766
|
':focus-within': '[data-percy-focus-within]',
|
|
763
|
-
':checked': '[data-percy-checked]',
|
|
764
|
-
':disabled': '[data-percy-disabled]',
|
|
765
767
|
':hover': '[data-percy-hover]',
|
|
766
768
|
':active': '[data-percy-active]'
|
|
767
769
|
};
|
|
@@ -769,7 +771,7 @@
|
|
|
769
771
|
// Boundary regex per pseudo-class. Lookahead `(?![-\w])` prevents :focus
|
|
770
772
|
// from matching the start of :focus-within / :focus-visible. Order matters:
|
|
771
773
|
// longer pseudos (:focus-within) are listed first so they win over :focus.
|
|
772
|
-
const PSEUDO_RES = [[':focus-within', /:focus-within(?![-\w])/g], [':focus', /:focus(?![-\w])/g], [':
|
|
774
|
+
const PSEUDO_RES = [[':focus-within', /:focus-within(?![-\w])/g], [':focus', /:focus(?![-\w])/g], [':hover', /:hover(?![-\w])/g], [':active', /:active(?![-\w])/g]];
|
|
773
775
|
function selectorContainsPseudo(selectorText, pseudoList) {
|
|
774
776
|
return pseudoList.some(pc => {
|
|
775
777
|
const re = PSEUDO_RES.find(([p]) => p === pc)[1];
|
|
@@ -830,30 +832,6 @@
|
|
|
830
832
|
stampOnce(ctx, focused, FOCUS_ATTR, 'true');
|
|
831
833
|
markFocusWithinAncestors(ctx, focused);
|
|
832
834
|
}
|
|
833
|
-
|
|
834
|
-
// Single walk of ctx.dom + shadow roots collecting BOTH :checked and
|
|
835
|
-
// :disabled in one pass. Previously two separate queryShadowAll calls
|
|
836
|
-
// each walked the tree and re-traversed every [data-percy-shadow-host]
|
|
837
|
-
// — on pages with many shadow hosts this doubled the per-snapshot
|
|
838
|
-
// walk cost. Also tracks which states were observed so the CSS rule
|
|
839
|
-
// extractor can skip work for selectors that have no matched elements.
|
|
840
|
-
ctx._stampedInteractive = ctx._stampedInteractive || new Set();
|
|
841
|
-
// walkShadowDOM only invokes the visitor with Document/Element/ShadowRoot
|
|
842
|
-
// scopes — each has querySelectorAll, so no defensive guard is needed here.
|
|
843
|
-
walkShadowDOM(ctx.dom, scope => {
|
|
844
|
-
try {
|
|
845
|
-
for (const el of scope.querySelectorAll(':checked')) {
|
|
846
|
-
stampOnce(ctx, el, CHECKED_ATTR, 'true');
|
|
847
|
-
ctx._stampedInteractive.add('checked');
|
|
848
|
-
}
|
|
849
|
-
} catch (e) {/* selector unsupported in this scope */}
|
|
850
|
-
try {
|
|
851
|
-
for (const el of scope.querySelectorAll(':disabled')) {
|
|
852
|
-
stampOnce(ctx, el, DISABLED_ATTR, 'true');
|
|
853
|
-
ctx._stampedInteractive.add('disabled');
|
|
854
|
-
}
|
|
855
|
-
} catch (e) {/* selector unsupported in this scope */}
|
|
856
|
-
});
|
|
857
835
|
}
|
|
858
836
|
|
|
859
837
|
// Walk the LIVE document and every shadow root (open, or closed via the CDP
|
|
@@ -871,8 +849,8 @@
|
|
|
871
849
|
}
|
|
872
850
|
|
|
873
851
|
// Auto-detect open native popovers page-wide, INCLUDING inside shadow roots.
|
|
874
|
-
// `:popover-open` is an unambiguous serialize-time state
|
|
875
|
-
//
|
|
852
|
+
// `:popover-open` is an unambiguous serialize-time state, so it is
|
|
853
|
+
// stamped automatically rather than only on
|
|
876
854
|
// pseudoClassEnabledElements. The renderer's popover-element-helper already
|
|
877
855
|
// re-opens any [popover][data-percy-popover-open] across shadow boundaries;
|
|
878
856
|
// without this stamp a popover open at snapshot time renders hidden via the
|
|
@@ -919,8 +897,8 @@
|
|
|
919
897
|
}
|
|
920
898
|
|
|
921
899
|
// Configured elements get :hover/:active stamped unconditionally — opting in
|
|
922
|
-
// IS the request to capture those forced states. :focus
|
|
923
|
-
//
|
|
900
|
+
// IS the request to capture those forced states. :focus is already
|
|
901
|
+
// covered by the page-wide markInteractiveStates pass.
|
|
924
902
|
function markElementInteractiveStates(ctx, element) {
|
|
925
903
|
stampOnce(ctx, element, HOVER_ATTR, 'true');
|
|
926
904
|
stampOnce(ctx, element, ACTIVE_ATTR, 'true');
|
|
@@ -1118,22 +1096,11 @@
|
|
|
1118
1096
|
}
|
|
1119
1097
|
function extractPseudoClassRules(ctx) {
|
|
1120
1098
|
const sheetEntries = collectStyleSheets(ctx.dom);
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
// `:checked` / `:disabled` selectors that found no live elements can't
|
|
1127
|
-
// match anything in the clone after rewriting, so we drop them from the
|
|
1128
|
-
// filter and avoid the rewrite cost. When the marker is absent (unit
|
|
1129
|
-
// tests that call serializePseudoClasses directly), fall back to the
|
|
1130
|
-
// full pseudo list to preserve prior behavior. `:focus`, `:focus-within`,
|
|
1131
|
-
// `:hover`, `:active` are kept regardless either way.
|
|
1132
|
-
const activePseudos = ctx._stampedInteractive ? ALL_INTERACTIVE_PSEUDO.filter(p => {
|
|
1133
|
-
if (p === ':checked') return ctx._stampedInteractive.has('checked');
|
|
1134
|
-
if (p === ':disabled') return ctx._stampedInteractive.has('disabled');
|
|
1135
|
-
return true;
|
|
1136
|
-
}) : ALL_INTERACTIVE_PSEUDO;
|
|
1099
|
+
|
|
1100
|
+
// Collect rewritten rules PER SOURCE SHEET (not merged per owner scope) so
|
|
1101
|
+
// each sheet's copies can be anchored back at that sheet's cascade position.
|
|
1102
|
+
// Discovery order is document order, which is also the order we inject in.
|
|
1103
|
+
const perSheet = [];
|
|
1137
1104
|
for (const {
|
|
1138
1105
|
sheet,
|
|
1139
1106
|
owner
|
|
@@ -1146,24 +1113,32 @@
|
|
|
1146
1113
|
continue;
|
|
1147
1114
|
}
|
|
1148
1115
|
if (!rules) continue;
|
|
1116
|
+
let rewrittenRules = null;
|
|
1149
1117
|
for (const rule of walkCSSRules(rules)) {
|
|
1150
1118
|
// Cheapest possible filter: a selector with no `:` can't contain any
|
|
1151
1119
|
// interactive pseudo. Skips most rules on most stylesheets without
|
|
1152
1120
|
// touching the regex bank.
|
|
1153
1121
|
if (!rule.selectorText.includes(':')) continue;
|
|
1154
|
-
if (!selectorContainsPseudo(rule.selectorText,
|
|
1122
|
+
if (!selectorContainsPseudo(rule.selectorText, ALL_INTERACTIVE_PSEUDO)) continue;
|
|
1155
1123
|
const rewrittenSelector = rewritePseudoSelector(rule.selectorText);
|
|
1156
1124
|
const cssText = `${rewrittenSelector} { ${rule.style.cssText} }`;
|
|
1157
1125
|
const wrapped = rule.wrapper ? `${rule.wrapper} { ${cssText} }` : cssText;
|
|
1158
|
-
|
|
1159
|
-
rulesByOwner.get(owner).push(wrapped);
|
|
1126
|
+
(rewrittenRules || (rewrittenRules = [])).push(wrapped);
|
|
1160
1127
|
}
|
|
1128
|
+
if (rewrittenRules) perSheet.push({
|
|
1129
|
+
sheet,
|
|
1130
|
+
owner,
|
|
1131
|
+
rewrittenRules
|
|
1132
|
+
});
|
|
1161
1133
|
}
|
|
1134
|
+
if (!perSheet.length) return;
|
|
1162
1135
|
|
|
1163
|
-
// Build a percyId → cloneEl index once for shadow-host injection — only
|
|
1164
|
-
//
|
|
1136
|
+
// Build a percyId → cloneEl index once for shadow-host injection — only when
|
|
1137
|
+
// at least one collected sheet lives inside a shadow root.
|
|
1165
1138
|
let cloneByPercyId = null;
|
|
1166
|
-
for (const
|
|
1139
|
+
for (const {
|
|
1140
|
+
owner
|
|
1141
|
+
} of perSheet) {
|
|
1167
1142
|
if (owner !== null) {
|
|
1168
1143
|
cloneByPercyId = new Map();
|
|
1169
1144
|
for (const el of ctx.clone.querySelectorAll('[data-percy-element-id]')) {
|
|
@@ -1172,25 +1147,45 @@
|
|
|
1172
1147
|
break;
|
|
1173
1148
|
}
|
|
1174
1149
|
}
|
|
1175
|
-
for (const
|
|
1150
|
+
for (const {
|
|
1151
|
+
sheet,
|
|
1152
|
+
owner,
|
|
1153
|
+
rewrittenRules
|
|
1154
|
+
} of perSheet) {
|
|
1176
1155
|
const styleElement = ctx.clone.createElement ? ctx.clone.createElement('style') : ctx.dom.createElement('style');
|
|
1177
1156
|
styleElement.setAttribute('data-percy-interactive-states', 'true');
|
|
1178
1157
|
// nosemgrep: javascript.browser.security.insecure-document-method.insecure-document-method
|
|
1179
1158
|
styleElement.textContent = rewrittenRules.join('\n');
|
|
1180
1159
|
if (owner === null) {
|
|
1181
|
-
|
|
1182
|
-
if (head) head.appendChild(styleElement);
|
|
1160
|
+
injectAtSheetPosition(ctx.clone, sheet, styleElement, ctx.clone.head || ctx.clone.querySelector('head'));
|
|
1183
1161
|
} else {
|
|
1184
|
-
const
|
|
1185
|
-
const cloneHost = cloneByPercyId.get(percyId);
|
|
1162
|
+
const cloneHost = cloneByPercyId.get(owner.getAttribute('data-percy-element-id'));
|
|
1186
1163
|
if (cloneHost && cloneHost.shadowRoot) {
|
|
1187
|
-
cloneHost.shadowRoot.
|
|
1164
|
+
injectAtSheetPosition(cloneHost.shadowRoot, sheet, styleElement, cloneHost.shadowRoot);
|
|
1188
1165
|
}
|
|
1189
1166
|
}
|
|
1190
1167
|
}
|
|
1191
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
// Insert an interactive-states <style> immediately AFTER its source sheet's
|
|
1171
|
+
// clone element so the copy keeps the sheet's original source-order rank —
|
|
1172
|
+
// later stylesheets still win equal-specificity ties, exactly as on the live
|
|
1173
|
+
// page (PER-10077). The sheet's ownerNode (<style> / <link rel=stylesheet>) is
|
|
1174
|
+
// stamped with a data-percy-element-id, which serialize-cssom preserves on any
|
|
1175
|
+
// node it rebuilds in place. Falls back to appending at the scope's end when
|
|
1176
|
+
// the sheet has no locatable clone anchor.
|
|
1177
|
+
function injectAtSheetPosition(scopeRoot, sheet, styleElement, fallbackTarget) {
|
|
1178
|
+
const anchorId = sheet.ownerNode.getAttribute('data-percy-element-id');
|
|
1179
|
+
const anchor = anchorId ? scopeRoot.querySelector(`[data-percy-element-id="${anchorId}"]`) : null;
|
|
1180
|
+
const anchorParent = anchor ? anchor.parentNode : null;
|
|
1181
|
+
if (anchorParent) {
|
|
1182
|
+
anchorParent.insertBefore(styleElement, anchor.nextSibling);
|
|
1183
|
+
} else if (fallbackTarget) {
|
|
1184
|
+
fallbackTarget.appendChild(styleElement);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1192
1187
|
function serializePseudoClasses(ctx) {
|
|
1193
|
-
// Auto-detect path runs unconditionally so `:focus`/`:
|
|
1188
|
+
// Auto-detect path runs unconditionally so `:focus`/`:focus-within`
|
|
1194
1189
|
// rules are rewritten regardless of whether the user configured a
|
|
1195
1190
|
// pseudoClassEnabledElements list (and regardless of whether that list
|
|
1196
1191
|
// matched anything on this page).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/dom",
|
|
3
|
-
"version": "1.32.6
|
|
3
|
+
"version": "1.32.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public",
|
|
12
|
-
"tag": "
|
|
12
|
+
"tag": "latest"
|
|
13
13
|
},
|
|
14
14
|
"main": "dist/bundle.js",
|
|
15
15
|
"browser": "dist/bundle.js",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"interactor.js": "^2.0.0-beta.10"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "e7dd935acf71273fd045ccceda6d1a37db8e4600"
|
|
39
39
|
}
|