@wix/zero-config-implementation 1.65.0 → 1.67.0

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/index.d.ts CHANGED
@@ -422,6 +422,12 @@ export declare interface CSSParserAPI {
422
422
  * @param defaultValue - The initial value string of the custom property
423
423
  */
424
424
  getVarPropertyType: (varName: string, defaultValue: string) => string | undefined;
425
+ /**
426
+ * Returns every `:global(.modifier)` design-state class found in the CSS, paired
427
+ * with any native pseudo-class on the same base element within the same rule.
428
+ * Modifiers with no paired pseudo are custom (non-native) state triggers.
429
+ */
430
+ getStateClasses: () => StateClassRule[];
425
431
  }
426
432
 
427
433
  export declare interface CssPropertiesData {
@@ -1577,6 +1583,8 @@ PropsTwo extends ErrorProps,
1577
1583
  declare type MethodOptions<PluginArg extends Plugin> =
1578
1584
  ExternalPluginOptions<PluginArg>
1579
1585
 
1586
+ declare type NativePseudoClass = 'hover' | 'focus' | 'disabled' | 'invalid';
1587
+
1580
1588
  /**
1581
1589
  * Normalize each error in the `errors` option to `Error` instances
1582
1590
  */
@@ -2369,6 +2377,8 @@ export declare interface PropTrackerData {
2369
2377
  role?: string;
2370
2378
  boundProps: string[];
2371
2379
  concatenatedAttrs: Map<string, string>;
2380
+ /** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
2381
+ eventHandlers: string[];
2372
2382
  }
2373
2383
 
2374
2384
  export declare interface PropTrackerExtractorState {
@@ -2642,6 +2652,24 @@ CauseArg extends Cause,
2642
2652
  > = MainInstanceOptions<AggregateErrorsArg, CauseArg> &
2643
2653
  PluginsOptions<PluginsArg, ChildProps>
2644
2654
 
2655
+ /**
2656
+ * A `:global(.modifier)` design-state class found on a base element selector.
2657
+ * Produced by walking each CSS rule and pairing the global modifier with any
2658
+ * native pseudo-class present on the same base within that rule.
2659
+ */
2660
+ declare interface StateClassRule {
2661
+ /** DOM-matchable base selector (e.g. `.root`), used to map the modifier to an element. */
2662
+ baseSelector: string;
2663
+ /** Global modifier class name from `:global(.X)`, without the leading dot. */
2664
+ modifier: string;
2665
+ /**
2666
+ * Native pseudo-class paired with this modifier on the same base within the same
2667
+ * rule. When present, the modifier is the editor-trigger class for a native state;
2668
+ * when absent, the modifier denotes a custom (non-native) design state.
2669
+ */
2670
+ pseudoClass?: NativePseudoClass;
2671
+ }
2672
+
2645
2673
  /**
2646
2674
  * Unbound static method of a plugin
2647
2675
  */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-BLr9vmnH.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-Ps7eUbv5.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.65.0",
7
+ "version": "1.67.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -81,5 +81,5 @@
81
81
  ]
82
82
  }
83
83
  },
84
- "falconPackageHash": "978c6e182cced111caaef8f5bc88bdaadfdd813f14c0f9280352d625"
84
+ "falconPackageHash": "98b2efba108de51cc5bc3678f7b32253ba1a26e9b875964b6a8e725e"
85
85
  }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ExtractedElement } from '../information-extractors/react'
3
+ import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
4
+
5
+ function elementWithClass(classAttribute: string): ExtractedElement {
6
+ return { attributes: { class: classAttribute } } as unknown as ExtractedElement
7
+ }
8
+
9
+ function classTrigger(baseSelector: string, modifier: string, isCssModule = false): CustomClassTrigger {
10
+ return { baseSelector, modifier, isCssModule }
11
+ }
12
+
13
+ describe('buildCustomStatesBlock', () => {
14
+ it('emits a custom-class state with className only (no props, no pseudoClass)', () => {
15
+ const states = buildCustomStatesBlock({
16
+ element: elementWithClass('custom-states'),
17
+ classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
18
+ })
19
+
20
+ expect(states).toEqual({ featured: { displayName: 'Featured', className: 'custom-states--featured' } })
21
+ })
22
+
23
+ it('derives a multi-word state name from the modifier and capital-cases the displayName', () => {
24
+ const states = buildCustomStatesBlock({
25
+ element: elementWithClass('widget'),
26
+ classTriggers: [classTrigger('.widget', 'widget--in-progress')],
27
+ })
28
+
29
+ expect(states?.['in-progress']).toEqual({
30
+ displayName: 'In Progress',
31
+ className: 'widget--in-progress',
32
+ })
33
+ })
34
+
35
+ it('does not apply a block-base class trigger to a BEM child element (plain CSS, exact match)', () => {
36
+ const states = buildCustomStatesBlock({
37
+ element: elementWithClass('custom-states__label'),
38
+ classTriggers: [classTrigger('.custom-states', 'custom-states--featured')],
39
+ })
40
+
41
+ expect(states).toBeUndefined()
42
+ })
43
+
44
+ it('matches a base class rendered as a double-underscore CSS-module hash (`root__HASH`)', () => {
45
+ // Vite emits `[name]__[hash]` for `styles.root`; the hash carries uppercase, so it is the
46
+ // base element — not a BEM child. (Regression: this used to be dropped as a child.)
47
+ const states = buildCustomStatesBlock({
48
+ element: elementWithClass('root__IWjkn wixui-test-comp-card test-comp-card'),
49
+ classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
50
+ })
51
+
52
+ expect(states).toEqual({ featured: { displayName: 'Featured', className: 'test-comp-card--featured' } })
53
+ })
54
+
55
+ it('still excludes a genuine BEM child (`root__label`) from a block-base trigger in a module', () => {
56
+ const states = buildCustomStatesBlock({
57
+ element: elementWithClass('root__label'),
58
+ classTriggers: [classTrigger('.root', 'test-comp-card--featured', true)],
59
+ })
60
+
61
+ expect(states).toBeUndefined()
62
+ })
63
+
64
+ it('returns undefined when no class trigger targets the element', () => {
65
+ const states = buildCustomStatesBlock({
66
+ element: elementWithClass('custom-states'),
67
+ classTriggers: [],
68
+ })
69
+
70
+ expect(states).toBeUndefined()
71
+ })
72
+ })
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Builds the custom (non-native) `states` entries for an element.
3
+ *
4
+ * The custom-class trigger feeds this: a `:global(.modifier)` class on the element
5
+ * with no paired native pseudo (CSS extractor `getStateClasses`). It emits the
6
+ * modifier as the state's editor-trigger `className`.
7
+ *
8
+ * Native states are built separately and take precedence on any key collision.
9
+ *
10
+ * Note: the prop trigger (`State<>` markers → `props` overrides) is intentionally
11
+ * not handled here yet — it is deferred to a follow-up PR, gated on the upstream
12
+ * marker-type agreement.
13
+ */
14
+
15
+ import type { States } from '@wix/react-component-schema'
16
+ import type { StateClassRule } from '../information-extractors/css/types'
17
+ import type { ExtractedElement } from '../information-extractors/react'
18
+ import { isGlobalSemanticClass, stateNameFromModifier } from '../utils/css-class'
19
+ import { formatDisplayName } from './utils'
20
+
21
+ /** A custom-class trigger tagged with whether its source file is a CSS module. */
22
+ export type CustomClassTrigger = StateClassRule & { isCssModule: boolean }
23
+
24
+ interface BuildCustomStatesParams {
25
+ element: ExtractedElement
26
+ /** Custom-class triggers (modifiers with no paired native pseudo) across all CSS files. */
27
+ classTriggers: CustomClassTrigger[]
28
+ }
29
+
30
+ export function buildCustomStatesBlock({ element, classTriggers }: BuildCustomStatesParams): States | undefined {
31
+ const states: States = {}
32
+
33
+ // Custom-class triggers that target this element.
34
+ for (const rule of classTriggers) {
35
+ if (!elementMatchesBase(element, rule.baseSelector, rule.isCssModule)) continue
36
+ const stateName = stateNameFromModifier(rule.modifier)
37
+ states[stateName] = {
38
+ displayName: formatDisplayName(stateName),
39
+ className: rule.modifier,
40
+ }
41
+ }
42
+
43
+ return Object.keys(states).length > 0 ? states : undefined
44
+ }
45
+
46
+ /**
47
+ * Checks whether an element's rendered class list matches a simple single-class
48
+ * base selector (e.g. `.root`, `.custom-states`).
49
+ *
50
+ * Exact class match always counts. For CSS-module files the Vite hash patterns also
51
+ * count: `localName_HASH`, `_localName_HASH`, and the double-underscore `localName__HASH`
52
+ * form. The catch is that `localName__HASH` is syntactically identical to a BEM child
53
+ * class (`localName__child`) — which denotes a *different* element and must NOT match the
54
+ * block's base selector. We disambiguate by shape: a real BEM child is an all-lowercase
55
+ * semantic name (`isGlobalSemanticClass`), whereas a module hash carries uppercase / other
56
+ * non-kebab characters. So only genuine BEM children are excluded; hashed base classes match.
57
+ */
58
+ function elementMatchesBase(element: ExtractedElement, baseSelector: string, isCssModule: boolean): boolean {
59
+ const match = baseSelector.match(/^\.([\w-]+)$/)
60
+ if (!match) return false
61
+
62
+ const localName = match[1]
63
+ const classList = element.attributes.class?.split(/\s+/).filter(Boolean) ?? []
64
+ return classList.some((className) => {
65
+ if (className === localName) return true
66
+ if (!isCssModule) return false
67
+ // Genuine BEM child (`localName__<lowercase-element>`) → a different element, exclude it.
68
+ // A double-underscore module hash (`localName__<hash>`) is NOT semantic, so it falls through.
69
+ const isBemChild =
70
+ (className.startsWith(`${localName}__`) || className.startsWith(`_${localName}__`)) &&
71
+ isGlobalSemanticClass(className.replace(/^_/, ''))
72
+ if (isBemChild) return false
73
+ return className.startsWith(`${localName}_`) || className.startsWith(`_${localName}_`)
74
+ })
75
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Builds the `states` block for the root EditorElement and inline elements.
3
+ *
4
+ * Native states only (hover/focus/disabled/invalid). Signals come from the
5
+ * React extractor (tag, role, event handlers). The CSS extractor is not
6
+ * consulted.
7
+ *
8
+ * className follows BEM by appending `--<state>` to the element's own
9
+ * semantic class. Component authors are expected to write inner elements
10
+ * with full BEM class names (e.g. `toggle__label`), so the resulting state
11
+ * className is `<semanticClass>--<state>`:
12
+ * - root `toggle` → `toggle--hover`
13
+ * - inner `toggle__label` → `toggle__label--hover`
14
+ *
15
+ * `props` is never emitted on native-state entries — that field is reserved
16
+ * for functional (non-native) states.
17
+ */
18
+
19
+ import type { States } from '@wix/react-component-schema'
20
+ import { NATIVE_STATE_TYPE } from '@wix/react-component-schema'
21
+ import {
22
+ type ExtractedElement,
23
+ type SupportedNativeStates,
24
+ inferSupportedNativeStates,
25
+ } from '../information-extractors/react'
26
+
27
+ const STATE_DISPLAY_NAMES = {
28
+ hover: 'Hover',
29
+ focus: 'Focus',
30
+ disabled: 'Disabled',
31
+ invalid: 'Invalid',
32
+ } as const
33
+
34
+ type NativeStateName = keyof SupportedNativeStates
35
+
36
+ const PSEUDO_CLASS_BY_STATE: Record<NativeStateName, NATIVE_STATE_TYPE> = {
37
+ hover: NATIVE_STATE_TYPE.hover,
38
+ focus: NATIVE_STATE_TYPE.focus,
39
+ disabled: NATIVE_STATE_TYPE.disabled,
40
+ invalid: NATIVE_STATE_TYPE.invalid,
41
+ }
42
+
43
+ export function buildStatesBlock(element: ExtractedElement, semanticClass: string): States | undefined {
44
+ const supported = inferSupportedNativeStates(element)
45
+ const states: States = {}
46
+
47
+ for (const stateName of Object.keys(supported) as NativeStateName[]) {
48
+ if (!supported[stateName]) continue
49
+
50
+ states[stateName] = {
51
+ displayName: STATE_DISPLAY_NAMES[stateName],
52
+ className: `${semanticClass}--${stateName}`,
53
+ pseudoClass: PSEUDO_CLASS_BY_STATE[stateName],
54
+ }
55
+ }
56
+
57
+ return Object.keys(states).length > 0 ? states : undefined
58
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  EditorElement,
7
7
  EditorReactComponent,
8
8
  ElementItem,
9
+ States,
9
10
  } from '@wix/react-component-schema'
10
11
  import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
11
12
  import { camelCase } from 'case-anything'
@@ -21,9 +22,32 @@ import type {
21
22
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
22
23
  import { findPreferredSemanticClass } from '../utils/css-class'
23
24
  import { resolveLonghand } from './css-longhand-resolver'
25
+ import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
24
26
  import { buildDataItem } from './data-item-builder'
27
+ import { buildStatesBlock } from './states-builder'
25
28
  import { formatDisplayName } from './utils'
26
29
 
30
+ /**
31
+ * Merges native and custom state blocks. Native states take precedence on any
32
+ * key collision (e.g. an author wrapping `isDisabled` in `State<>` does not
33
+ * shadow the inferred native `disabled` state).
34
+ */
35
+ function mergeStates(native: States | undefined, custom: States | undefined): States | undefined {
36
+ if (!native && !custom) return undefined
37
+ const merged: States = { ...custom, ...native }
38
+ return Object.keys(merged).length > 0 ? merged : undefined
39
+ }
40
+
41
+ /** Collects custom-class state triggers (modifiers with no paired native pseudo) across all CSS files. */
42
+ function collectCustomClassTriggers(component: ComponentInfoWithCss): CustomClassTrigger[] {
43
+ return component.css.flatMap((cssInfo) =>
44
+ cssInfo.api
45
+ .getStateClasses()
46
+ .filter((rule) => rule.pseudoClass === undefined)
47
+ .map((rule) => ({ ...rule, isCssModule: cssInfo.isCssModule })),
48
+ )
49
+ }
50
+
27
51
  // Props to exclude from data items (same as in element-data.ts)
28
52
  const EXCLUDED_PROPS = new Set(['id', 'className', 'elementProps', 'wix'])
29
53
 
@@ -42,6 +66,19 @@ function buildEditorElement(
42
66
  const childElements = rootElement?.children ?? []
43
67
  const rootCustomProps = rootElement ? (nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}) : {}
44
68
 
69
+ const customClassTriggers = collectCustomClassTriggers(component)
70
+
71
+ // Native states need a semantic block name to synthesize `<block>--<state>`; custom
72
+ // class triggers don't (they carry the className verbatim and match the element
73
+ // directly), so they're built even when the element resolves no semantic class.
74
+ const rootSemanticClass = getSemanticBlockName(rootElement)
75
+ const rootStates = rootElement
76
+ ? mergeStates(
77
+ rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass) : undefined,
78
+ buildCustomStatesBlock({ element: rootElement, classTriggers: customClassTriggers }),
79
+ )
80
+ : undefined
81
+
45
82
  return {
46
83
  selector: buildSelector(rootElement),
47
84
  displayName: formatDisplayName(component.componentName),
@@ -49,14 +86,21 @@ function buildEditorElement(
49
86
  elements: buildElements(
50
87
  childElements,
51
88
  nearestCommonAncestorCustomProps,
89
+ customClassTriggers,
52
90
  component.innerElementProps,
53
91
  component.propUsages,
54
92
  ),
55
93
  cssProperties: buildCssProperties(rootElement),
56
94
  cssCustomProperties: rootCustomProps,
95
+ ...(rootStates && { states: rootStates }),
57
96
  }
58
97
  }
59
98
 
99
+ function getSemanticBlockName(element: ExtractedElement | undefined): string | undefined {
100
+ if (!element?.attributes.class) return undefined
101
+ return findPreferredSemanticClass(element.attributes.class.split(' '))
102
+ }
103
+
60
104
  function buildSelector(rootElement?: ExtractedElement): string {
61
105
  if (rootElement?.attributes.class) {
62
106
  const semanticClass = findPreferredSemanticClass(rootElement.attributes.class.split(' '))
@@ -90,6 +134,7 @@ function buildData(
90
134
  function buildElements(
91
135
  elements: ExtractedElement[],
92
136
  nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
137
+ customClassTriggers: CustomClassTrigger[],
93
138
  innerElementProps?: CoupledComponentInfo['innerElementProps'],
94
139
  propUsages?: TrackingStores['propUsages'],
95
140
  ): Record<string, ElementItem> {
@@ -101,6 +146,12 @@ function buildElements(
101
146
  const cssProps = buildCssProperties(element)
102
147
  const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
103
148
 
149
+ const semanticClass = getSemanticBlockName(element)
150
+ const states = mergeStates(
151
+ semanticClass ? buildStatesBlock(element, semanticClass) : undefined,
152
+ buildCustomStatesBlock({ element, classTriggers: customClassTriggers }),
153
+ )
154
+
104
155
  result[element.name] = {
105
156
  elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
106
157
  inlineElement: {
@@ -113,10 +164,17 @@ function buildElements(
113
164
  ...(Object.keys(cssProps).length > 0 && { cssProperties: cssProps }),
114
165
  // CSS custom properties placed on the Nearest Common Ancestor of all elements that use each variable
115
166
  ...(Object.keys(cssCustomProps).length > 0 && { cssCustomProperties: cssCustomProps }),
167
+ ...(states && { states }),
116
168
  // Recursively build nested elements
117
169
  elements:
118
170
  element.children.length > 0
119
- ? buildElements(element.children, nearestCommonAncestorCustomProps, innerElementProps, propUsages)
171
+ ? buildElements(
172
+ element.children,
173
+ nearestCommonAncestorCustomProps,
174
+ customClassTriggers,
175
+ innerElementProps,
176
+ propUsages,
177
+ )
120
178
  : undefined,
121
179
  },
122
180
  }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseCss } from './parse'
3
+
4
+ describe('parseCss — getStateClasses', () => {
5
+ it('extracts a custom-class modifier with no paired pseudo', () => {
6
+ const stateClasses = parseCss(`
7
+ .root:global(.custom-states--featured) {
8
+ border: 2px solid orange;
9
+ }
10
+ `).getStateClasses()
11
+
12
+ expect(stateClasses).toEqual([
13
+ { baseSelector: '.root', modifier: 'custom-states--featured', pseudoClass: undefined },
14
+ ])
15
+ })
16
+
17
+ it('pairs a global modifier with a native pseudo on the same base in the same rule', () => {
18
+ const stateClasses = parseCss(`
19
+ .root:global(.toggle--hover),
20
+ .root:hover {
21
+ filter: brightness(1.05);
22
+ }
23
+ `).getStateClasses()
24
+
25
+ expect(stateClasses).toEqual([{ baseSelector: '.root', modifier: 'toggle--hover', pseudoClass: 'hover' }])
26
+ })
27
+
28
+ it('maps :focus-visible and :focus-within to the focus state', () => {
29
+ const stateClasses = parseCss(`
30
+ .root:global(.toggle--focus),
31
+ .root:focus-within {
32
+ outline: 2px solid;
33
+ }
34
+ `).getStateClasses()
35
+
36
+ expect(stateClasses).toEqual([{ baseSelector: '.root', modifier: 'toggle--focus', pseudoClass: 'focus' }])
37
+ })
38
+
39
+ it('keeps modifiers on distinct base elements separate', () => {
40
+ const stateClasses = parseCss(`
41
+ .root:global(.card--featured) { color: red; }
42
+ .label:global(.card__label--featured) { font-weight: 700; }
43
+ `).getStateClasses()
44
+
45
+ expect(stateClasses).toEqual([
46
+ { baseSelector: '.root', modifier: 'card--featured', pseudoClass: undefined },
47
+ { baseSelector: '.label', modifier: 'card__label--featured', pseudoClass: undefined },
48
+ ])
49
+ })
50
+
51
+ it('treats a global base class with a global modifier as base + modifier (BEM extension)', () => {
52
+ const stateClasses = parseCss(`
53
+ :global(.card):global(.card--featured) {
54
+ border: 2px solid orange;
55
+ }
56
+ `).getStateClasses()
57
+
58
+ expect(stateClasses).toEqual([{ baseSelector: '.card', modifier: 'card--featured', pseudoClass: undefined }])
59
+ })
60
+
61
+ it('resolves a global base for an inner BEM element (`__`) carrying a modifier', () => {
62
+ const stateClasses = parseCss(`
63
+ :global(.card__label):global(.card__label--highlight) {
64
+ font-weight: 700;
65
+ }
66
+ `).getStateClasses()
67
+
68
+ expect(stateClasses).toEqual([
69
+ { baseSelector: '.card__label', modifier: 'card__label--highlight', pseudoClass: undefined },
70
+ ])
71
+ })
72
+
73
+ it('pairs a native pseudo with a modifier when the base element is global', () => {
74
+ const stateClasses = parseCss(`
75
+ :global(.card):global(.card--hover),
76
+ :global(.card):hover {
77
+ filter: brightness(1.05);
78
+ }
79
+ `).getStateClasses()
80
+
81
+ expect(stateClasses).toEqual([{ baseSelector: '.card', modifier: 'card--hover', pseudoClass: 'hover' }])
82
+ })
83
+
84
+ it('anchors a modifier to the compound it sits on, ignoring a descendant combinator', () => {
85
+ // SCSS `.header { &:global(.card__title--selected) { .label { … } } }` compiles to a
86
+ // descendant selector; the modifier belongs to `.header`, not `.header .label`.
87
+ const stateClasses = parseCss(`
88
+ .header:global(.card__title--selected) .label {
89
+ text-decoration: underline;
90
+ }
91
+ `).getStateClasses()
92
+
93
+ expect(stateClasses).toEqual([
94
+ { baseSelector: '.header', modifier: 'card__title--selected', pseudoClass: undefined },
95
+ ])
96
+ })
97
+
98
+ it('pairs only the same-named modifier with a pseudo when a base carries both native and custom modifiers', () => {
99
+ // One rule, descendant-anchored: `--hover` pairs with `:hover`; `--selected` stays custom.
100
+ const stateClasses = parseCss(`
101
+ .header:hover .label,
102
+ .header:global(.card__title--hover) .label,
103
+ .header:global(.card__title--selected) .label {
104
+ text-decoration: underline;
105
+ }
106
+ `).getStateClasses()
107
+
108
+ expect(stateClasses).toEqual([
109
+ { baseSelector: '.header', modifier: 'card__title--hover', pseudoClass: 'hover' },
110
+ { baseSelector: '.header', modifier: 'card__title--selected', pseudoClass: undefined },
111
+ ])
112
+ })
113
+
114
+ it('ignores rules with no global modifier', () => {
115
+ const stateClasses = parseCss(`
116
+ .root:hover { cursor: pointer; }
117
+ .root { color: black; }
118
+ `).getStateClasses()
119
+
120
+ expect(stateClasses).toEqual([])
121
+ })
122
+ })