@percy/dom 1.32.3-beta.0 → 1.32.3-beta.3

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 +75 -3
  2. package/package.json +2 -2
package/dist/bundle.js CHANGED
@@ -855,6 +855,49 @@
855
855
  } catch (e) {/* selector unsupported in this scope */}
856
856
  });
857
857
  }
858
+
859
+ // Walk the LIVE document and every shadow root (open, or closed via the CDP
860
+ // WeakMap) WITHOUT relying on data-percy-shadow-host markers. walkShadowDOM
861
+ // descends through those markers, but they are only stamped later during
862
+ // cloning — so during this pre-clone marking pass it cannot reach shadow
863
+ // content. We descend via the live shadowRoot directly instead.
864
+ function eachScopeIncludingShadow(root, visit) {
865
+ if (!root || typeof root.querySelectorAll !== 'function') return;
866
+ visit(root);
867
+ for (const el of root.querySelectorAll('*')) {
868
+ const shadow = getShadowRoot(el);
869
+ if (shadow) eachScopeIncludingShadow(shadow, visit);
870
+ }
871
+ }
872
+
873
+ // Auto-detect open native popovers page-wide, INCLUDING inside shadow roots.
874
+ // `:popover-open` is an unambiguous serialize-time state (like :checked /
875
+ // :disabled), so it is stamped automatically rather than only on
876
+ // pseudoClassEnabledElements. The renderer's popover-element-helper already
877
+ // re-opens any [popover][data-percy-popover-open] across shadow boundaries;
878
+ // without this stamp a popover open at snapshot time renders hidden via the
879
+ // UA `[popover]:not(:popover-open){display:none}` rule. If `:popover-open`
880
+ // is unsupported the selector throws — we stop querying and warn once.
881
+ function markOpenPopovers(ctx) {
882
+ let supported = true;
883
+ eachScopeIncludingShadow(ctx.dom, scope => {
884
+ if (!supported) return;
885
+ // Only the `:popover-open` SELECTOR can legitimately throw a SyntaxError
886
+ // (engines without popover support). Scope the try to the query and
887
+ // materialize the matches; stamp OUTSIDE the try. Otherwise a throw from
888
+ // stampOnce mid-iteration would be misreported as "unsupported" AND would
889
+ // silently skip every remaining scope.
890
+ let matches;
891
+ try {
892
+ matches = scope.querySelectorAll('[popover]:popover-open');
893
+ } catch (e) {
894
+ supported = false;
895
+ ctx.warnings.add('Browser does not support :popover-open pseudo-class.');
896
+ return;
897
+ }
898
+ for (const el of matches) stampOnce(ctx, el, POPOVER_OPEN_ATTR, 'true');
899
+ });
900
+ }
858
901
  function isPopoverOpen(ctx, element) {
859
902
  try {
860
903
  return element.matches(':popover-open');
@@ -959,6 +1002,7 @@
959
1002
  function markPseudoClassElements(ctx, config) {
960
1003
  ctx._liveMutations = [];
961
1004
  markInteractiveStates(ctx);
1005
+ markOpenPopovers(ctx);
962
1006
  if (config) getElementsToProcess(ctx, config, true);
963
1007
  }
964
1008
 
@@ -986,10 +1030,26 @@
986
1030
  return decls.join(' ');
987
1031
  }
988
1032
 
1033
+ // Resolve a nested rule's selector against its parent per CSS Nesting.
1034
+ // The CSSOM serializes nested selectors with the implicit nesting selector
1035
+ // made explicit, so every nested selector — including each item of a
1036
+ // selector list — carries its own `&`. Replacing every `&` with :is(parent)
1037
+ // therefore fully scopes the rule. Without this, a bare top-level `&`
1038
+ // resolves to :root on engines that support nesting, so component-scoped
1039
+ // interactive-state rules leak page-wide (PER-9775). :is(...) preserves the
1040
+ // parent's grouping/specificity.
1041
+ function resolveNestedSelector(selectorText, parentSelector) {
1042
+ if (!parentSelector) return selectorText;
1043
+ return selectorText.replace(/&/g, `:is(${parentSelector})`);
1044
+ }
1045
+
989
1046
  // Walk a CSSRule list yielding every reachable style rule. Nested rules
990
1047
  // inside @media/@layer/@supports are emitted with the at-rule prelude
991
1048
  // preserved as a wrapper string; flat-emitting would drop the guard.
992
- function walkCSSRules(ruleList) {
1049
+ // `parentSelector` carries the enclosing style rule's (already-resolved)
1050
+ // selector down through native CSS nesting so `&`-relative children are
1051
+ // resolved against it rather than leaking as a bare top-level `&`.
1052
+ function walkCSSRules(ruleList, parentSelector = null) {
993
1053
  const result = [];
994
1054
  for (let i = 0; i < ruleList.length; i++) {
995
1055
  const rule = ruleList[i];
@@ -998,7 +1058,19 @@
998
1058
  var _rule$media;
999
1059
  const conditionText = rule.conditionText || ((_rule$media = rule.media) === null || _rule$media === void 0 ? void 0 : _rule$media.mediaText);
1000
1060
  const atRulePrelude = conditionText && rule.cssText ? rule.cssText.split('{')[0].trim() : null;
1001
- for (const inner of walkCSSRules(rule.cssRules)) {
1061
+ // An at-rule (@media/@layer/@supports) keeps the current parent scope
1062
+ // and is re-applied as a wrapper. A style rule with nested children
1063
+ // (native CSS nesting) becomes the scope for its children — resolve
1064
+ // its own selector against any outer parent first so deeper nesting
1065
+ // composes correctly.
1066
+ // Default to the current scope (also covers at-rules and nested-decl
1067
+ // rules that have no selector of their own). A style rule with nested
1068
+ // children resolves its own selector first and becomes the new scope.
1069
+ let childParent = parentSelector;
1070
+ if (!atRulePrelude && rule.selectorText) {
1071
+ childParent = resolveNestedSelector(rule.selectorText, parentSelector);
1072
+ }
1073
+ for (const inner of walkCSSRules(rule.cssRules, childParent)) {
1002
1074
  if (atRulePrelude && inner.selectorText) {
1003
1075
  result.push({
1004
1076
  selectorText: inner.selectorText,
@@ -1014,7 +1086,7 @@
1014
1086
  // @charset, @counter-style, etc.) are skipped — they can't contain
1015
1087
  // interactive pseudos.
1016
1088
  result.push({
1017
- selectorText: rule.selectorText,
1089
+ selectorText: resolveNestedSelector(rule.selectorText, parentSelector),
1018
1090
  style: rule.style,
1019
1091
  wrapper: null
1020
1092
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/dom",
3
- "version": "1.32.3-beta.0",
3
+ "version": "1.32.3-beta.3",
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": "e8af175695b4afdb78cdd48b67037710f5c8b1c9"
38
+ "gitHead": "f2029c138431bb176dd41ea0d1df1f3a3120cc8d"
39
39
  }