@wix/zero-config-implementation 1.71.0 → 1.73.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.
Files changed (47) hide show
  1. package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
  2. package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
  3. package/dist/index.d.ts +23 -8
  4. package/dist/index.js +1 -1
  5. package/package.json +4 -3
  6. package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
  7. package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
  8. package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
  9. package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
  10. package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
  11. package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
  12. package/src/component-renderer.ts +12 -9
  13. package/src/converters/to-editor-component.test.ts +88 -0
  14. package/src/converters/to-editor-component.ts +35 -6
  15. package/src/index.ts +121 -54
  16. package/src/information-extractors/css/parse.test.ts +49 -1
  17. package/src/information-extractors/css/parse.ts +167 -3
  18. package/src/information-extractors/css/selector-matcher.ts +3 -1
  19. package/src/information-extractors/css/types.ts +11 -0
  20. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  21. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  22. package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
  23. package/src/information-extractors/react/extractors/css-properties.ts +248 -98
  24. package/src/information-extractors/react/extractors/index.ts +1 -0
  25. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  26. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  27. package/src/information-extractors/react/index.ts +1 -0
  28. package/src/information-extractors/react/types.ts +1 -1
  29. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  30. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  31. package/src/manifest-pipeline.ts +22 -15
  32. package/src/module-loader.test.ts +150 -0
  33. package/src/module-loader.ts +48 -8
  34. package/src/react-runtime-interceptor.ts +64 -0
  35. package/src/react-runtime-loader.ts +656 -0
  36. package/src/ref-elements/component-tag.ts +105 -0
  37. package/src/ref-elements/context.test.ts +179 -0
  38. package/src/ref-elements/context.ts +280 -0
  39. package/src/ref-elements/eligible-paths.ts +45 -0
  40. package/src/ref-elements/module-resolution.test.ts +50 -0
  41. package/src/ref-elements/module-resolution.ts +21 -0
  42. package/src/ref-elements/module-specifier.ts +17 -0
  43. package/src/ref-elements/path-utils.test.ts +14 -0
  44. package/src/ref-elements/path-utils.ts +55 -0
  45. package/src/ref-elements/types.ts +17 -0
  46. package/src/utils/css-class.ts +15 -0
  47. package/src/jsx-runtime-interceptor.ts +0 -245
@@ -1,7 +1,7 @@
1
1
  import { CSS_PROPERTIES } from '@wix/react-component-schema'
2
2
  import { camelCase } from 'case-anything'
3
3
  import { generate, lexer, parse, walk } from 'css-tree'
4
- import type { Atrule, CssNode, Declaration, FunctionNode, Rule, Selector } from 'css-tree'
4
+ import type { Atrule, CssNode, Declaration, FunctionNode, Rule, Selector, Value } from 'css-tree'
5
5
  import { stateNameFromModifier } from '../../utils/css-class'
6
6
  import type { CSSParserAPI, CSSProperty, NativePseudoClass, StateClassRule } from './types'
7
7
 
@@ -70,6 +70,12 @@ export function parseCss(cssString: string): CSSParserAPI {
70
70
  return buildDomSelector(parsed)
71
71
  },
72
72
 
73
+ getSelectorSpecificity(selector: string): [number, number, number] | null {
74
+ const parsed = parsedSelectors.get(selector)
75
+ if (!parsed) return null
76
+ return getSelectorSpecificity(parsed)
77
+ },
78
+
73
79
  getVarPropertyType(varName: string, defaultValue: string): string | undefined {
74
80
  const usages = this.getVarUsages(varName)
75
81
  if (usages.length === 0) return undefined
@@ -173,14 +179,133 @@ function extractProperty(declaration: Declaration, varUsagesByProperty: Map<stri
173
179
  }
174
180
 
175
181
  if (varRefs.length > 0) {
176
- return { name, value, varRefs }
182
+ return { name, value, valueAst: declaration.value, varRefs }
177
183
  }
178
- return { name, value }
184
+ return { name, value, valueAst: declaration.value }
179
185
  } catch {
180
186
  return null
181
187
  }
182
188
  }
183
189
 
190
+ type ResolvedCssValue = { kind: 'resolved'; value: string } | { kind: 'unresolved' } | { kind: 'invalid' }
191
+
192
+ function resolvedCssValue(value: string): ResolvedCssValue {
193
+ return { kind: 'resolved', value }
194
+ }
195
+
196
+ function unresolvedCssValue(): ResolvedCssValue {
197
+ return { kind: 'unresolved' }
198
+ }
199
+
200
+ function invalidCssValue(): ResolvedCssValue {
201
+ return { kind: 'invalid' }
202
+ }
203
+
204
+ function resolveValueSequence(
205
+ valueNodes: CssNode[],
206
+ customProperties: Record<string, string>,
207
+ seenVariables: Set<string>,
208
+ ): ResolvedCssValue {
209
+ const resolvedParts: string[] = []
210
+
211
+ for (const valueNode of valueNodes) {
212
+ const resolvedPart = resolveValueNode(valueNode, customProperties, seenVariables)
213
+ if (resolvedPart.kind !== 'resolved') return resolvedPart
214
+ if (resolvedPart.value.length > 0) {
215
+ resolvedParts.push(resolvedPart.value)
216
+ }
217
+ }
218
+
219
+ return resolvedCssValue(resolvedParts.join(' '))
220
+ }
221
+
222
+ function resolveVariableFunction(
223
+ functionNode: FunctionNode,
224
+ customProperties: Record<string, string>,
225
+ seenVariables: Set<string>,
226
+ ): ResolvedCssValue {
227
+ const functionArguments = [...functionNode.children]
228
+ const firstCommaIndex = functionArguments.findIndex(
229
+ (argumentNode) => argumentNode.type === 'Operator' && argumentNode.value === ',',
230
+ )
231
+
232
+ const variableReferenceNodes =
233
+ firstCommaIndex === -1 ? functionArguments : functionArguments.slice(0, firstCommaIndex)
234
+ const fallbackNodes = firstCommaIndex === -1 ? [] : functionArguments.slice(firstCommaIndex + 1)
235
+
236
+ const variableReference = variableReferenceNodes
237
+ .map((argumentNode) => generate(argumentNode))
238
+ .join('')
239
+ .trim()
240
+ const normalizedVariableName = variableReference.startsWith('--') ? variableReference : `--${variableReference}`
241
+
242
+ if (seenVariables.has(normalizedVariableName)) {
243
+ return invalidCssValue()
244
+ }
245
+
246
+ const customPropertyValue = customProperties[normalizedVariableName]
247
+ if (customPropertyValue !== undefined) {
248
+ try {
249
+ const customPropertyValueAst = parse(customPropertyValue, { context: 'value' }) as Value
250
+ const nextSeenVariables = new Set(seenVariables)
251
+ nextSeenVariables.add(normalizedVariableName)
252
+ const resolvedCustomPropertyValue = resolveValueSequence(
253
+ [...customPropertyValueAst.children],
254
+ customProperties,
255
+ nextSeenVariables,
256
+ )
257
+
258
+ if (resolvedCustomPropertyValue.kind === 'resolved') {
259
+ return resolvedCustomPropertyValue
260
+ }
261
+
262
+ if (resolvedCustomPropertyValue.kind === 'invalid') {
263
+ return fallbackNodes.length > 0
264
+ ? resolveValueSequence(fallbackNodes, customProperties, seenVariables)
265
+ : invalidCssValue()
266
+ }
267
+
268
+ return unresolvedCssValue()
269
+ } catch {
270
+ return invalidCssValue()
271
+ }
272
+ }
273
+
274
+ if (fallbackNodes.length > 0) {
275
+ return resolveValueSequence(fallbackNodes, customProperties, seenVariables)
276
+ }
277
+
278
+ return unresolvedCssValue()
279
+ }
280
+
281
+ function resolveValueNode(
282
+ valueNode: CssNode,
283
+ customProperties: Record<string, string>,
284
+ seenVariables: Set<string>,
285
+ ): ResolvedCssValue {
286
+ if (valueNode.type === 'Value') {
287
+ return resolveValueSequence([...valueNode.children], customProperties, seenVariables)
288
+ }
289
+
290
+ if (valueNode.type === 'Function' && valueNode.name === 'var') {
291
+ return resolveVariableFunction(valueNode, customProperties, seenVariables)
292
+ }
293
+
294
+ return resolvedCssValue(generate(valueNode).trim())
295
+ }
296
+
297
+ export function resolveCssPropertyValue(
298
+ property: CSSProperty,
299
+ customProperties: Record<string, string>,
300
+ ): string | undefined {
301
+ if (!property.valueAst || !property.varRefs || property.varRefs.length === 0) {
302
+ return property.value
303
+ }
304
+
305
+ const resolvedPropertyValue = resolveValueNode(property.valueAst, customProperties, new Set())
306
+ return resolvedPropertyValue.kind === 'resolved' ? resolvedPropertyValue.value : undefined
307
+ }
308
+
184
309
  /**
185
310
  * Extracts all CSS custom property names referenced via var() by walking
186
311
  * the declaration value AST for Function nodes named "var".
@@ -395,6 +520,45 @@ function buildDomSelector(selector: Selector): string | null {
395
520
  return result || null
396
521
  }
397
522
 
523
+ function getSelectorSpecificity(selector: Selector): [number, number, number] {
524
+ let idCount = 0
525
+ let classLikeCount = 0
526
+ let typeLikeCount = 0
527
+
528
+ for (const component of selector.children) {
529
+ if (component.type === 'IdSelector') {
530
+ idCount += 1
531
+ continue
532
+ }
533
+
534
+ if (component.type === 'ClassSelector' || component.type === 'AttributeSelector') {
535
+ classLikeCount += 1
536
+ continue
537
+ }
538
+
539
+ if (component.type === 'TypeSelector') {
540
+ if (component.name !== '*') {
541
+ typeLikeCount += 1
542
+ }
543
+ continue
544
+ }
545
+
546
+ if (component.type === 'PseudoElementSelector') {
547
+ typeLikeCount += 1
548
+ continue
549
+ }
550
+
551
+ if (component.type === 'PseudoClassSelector') {
552
+ if (component.name === 'where') {
553
+ continue
554
+ }
555
+ classLikeCount += 1
556
+ }
557
+ }
558
+
559
+ return [idCount, classLikeCount, typeLikeCount]
560
+ }
561
+
398
562
  /**
399
563
  * Checks whether a CSS value matches a given CSS type (e.g. 'color', 'length').
400
564
  * Uses css-tree's lexer; returns true when the value conforms to the type grammar.
@@ -67,6 +67,8 @@ export function matchCssSelectors(
67
67
  if (!domSelector) continue
68
68
 
69
69
  try {
70
+ const selectorSpecificity = cssInfo.api.getSelectorSpecificity(selector) ?? [0, 0, 0]
71
+
70
72
  findMatchingElements($, domSelector, cssInfo.isCssModule).each((_index, element) => {
71
73
  const traceId = $(element).attr(TRACE_ATTR)
72
74
  if (!traceId) return
@@ -89,7 +91,7 @@ export function matchCssSelectors(
89
91
 
90
92
  if (regular.length > 0) {
91
93
  const existing = matchesByTraceId.get(traceId) ?? []
92
- existing.push({ selector, properties: regular })
94
+ existing.push({ selector, properties: regular, specificity: selectorSpecificity })
93
95
  matchesByTraceId.set(traceId, existing)
94
96
 
95
97
  // Track which CSS custom properties are used (via var()) by this element
@@ -1,6 +1,10 @@
1
+ import type { CssNode } from 'css-tree'
2
+
1
3
  export interface CSSProperty {
2
4
  name: string
3
5
  value: string
6
+ /** Parsed css-tree AST for the property's value. */
7
+ valueAst?: CssNode
4
8
  /** CSS variable names referenced via var() in this property's value */
5
9
  varRefs?: string[]
6
10
  }
@@ -8,6 +12,7 @@ export interface CSSProperty {
8
12
  export interface CssSelectorMatch {
9
13
  selector: string
10
14
  properties: CSSProperty[]
15
+ specificity: [number, number, number]
11
16
  }
12
17
 
13
18
  export interface MatchedCssData {
@@ -76,6 +81,12 @@ export interface CSSParserAPI {
76
81
  */
77
82
  getDomSelector: (selector: string) => string | null
78
83
 
84
+ /**
85
+ * Gets the selector specificity tuple as [ids, classes/attrs/pseudo-classes, elements/pseudo-elements].
86
+ * Returns null when the selector was not parsed.
87
+ */
88
+ getSelectorSpecificity: (selector: string) => [number, number, number] | null
89
+
79
90
  /**
80
91
  * Determines the CSS property type for a custom property based on how it is used.
81
92
  * If all usages of varName are within the same CSS property, returns that property name.
@@ -8,7 +8,7 @@
8
8
  import { camelCase } from 'case-anything'
9
9
  import { type DefaultTreeAdapterMap, parseFragment } from 'parse5'
10
10
  import { TRACE_ATTR } from '../../../../component-renderer'
11
- import { findPreferredSemanticClass } from '../../../../utils/css-class'
11
+ import { findPreferredSemanticClass, normalizeClassNames } from '../../../../utils/css-class'
12
12
  import { PRESETS_WRAPPER_CLASS_NAME } from '../../utils/mock-generator'
13
13
  import type { CssPropertiesData } from '../css-properties'
14
14
  import { addTextProperties } from '../css-properties'
@@ -174,7 +174,7 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
174
174
 
175
175
  const classAttr = getAttribute(element, 'class')
176
176
  if (classAttr) {
177
- const semanticClass = findPreferredSemanticClass(classAttr.split(' '))
177
+ const semanticClass = findPreferredSemanticClass(normalizeClassNames(classAttr))
178
178
  if (semanticClass) elementName = semanticClass
179
179
  }
180
180
 
@@ -197,7 +197,7 @@ function getElementNamePart(element: Element, getElementById: (id: string) => El
197
197
  function hasClassNameCriteria(element: Element, extractorData: Map<string, unknown>): boolean {
198
198
  const classAttr = getAttribute(element, 'class')
199
199
  if (classAttr) {
200
- const semanticClass = findPreferredSemanticClass(classAttr.split(' '))
200
+ const semanticClass = findPreferredSemanticClass(normalizeClassNames(classAttr))
201
201
  if (semanticClass) return true
202
202
  }
203
203
 
@@ -17,13 +17,12 @@ export interface RenderContext {
17
17
  store: ExtractorStore
18
18
  }
19
19
 
20
- /**
21
- * Event emitted for each DOM element created during render.
22
- */
23
20
  export interface CreateElementEvent {
24
- tag: string
21
+ type: unknown
22
+ isDomElement: boolean
23
+ tag?: string
25
24
  props: Record<string, unknown>
26
- traceId: string
25
+ traceId?: string
27
26
  children: unknown[]
28
27
  store: ExtractorStore
29
28
  }
@@ -1,7 +1,15 @@
1
+ import { CSS_PROPERTIES } from '@wix/react-component-schema'
1
2
  import { describe, expect, it } from 'vitest'
2
3
  import { parseCss } from '../../css/parse'
3
4
  import type { MatchedCssData } from '../../css/types'
4
- import { hasFlexOrGridDisplay } from './css-properties'
5
+ import type { ExtractedElement } from './core/tree-builder'
6
+ import {
7
+ enrichContainerProperties,
8
+ getCssPropertiesForTag,
9
+ getDefaultDisplayForTag,
10
+ hasContainerLikeDisplay,
11
+ hasFlexOrGridDisplay,
12
+ } from './css-properties'
5
13
 
6
14
  function matcherDataFromCss(declarations: string): MatchedCssData {
7
15
  const properties = parseCss(`.test { ${declarations} }`).getPropertiesForSelector('.test')
@@ -12,16 +20,49 @@ function matcherDataFromCss(declarations: string): MatchedCssData {
12
20
  }
13
21
  }
14
22
  return {
15
- matches: [{ selector: '.test', properties: properties.filter((property) => !property.name.startsWith('--')) }],
23
+ matches: [
24
+ {
25
+ selector: '.test',
26
+ properties: properties.filter((property) => !property.name.startsWith('--')),
27
+ specificity: [0, 1, 0],
28
+ },
29
+ ],
16
30
  customProperties,
17
31
  }
18
32
  }
19
33
 
34
+ function createElementWithCssProperties(
35
+ tag: string,
36
+ declarations: string,
37
+ attributes: Record<string, string> = {},
38
+ ): ExtractedElement {
39
+ const extractorData = new Map<string, unknown>()
40
+ extractorData.set('css-matcher', matcherDataFromCss(declarations))
41
+ extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes.role) })
42
+
43
+ return {
44
+ traceId: 'trace-1',
45
+ name: 'element',
46
+ tag,
47
+ attributes,
48
+ extractorData,
49
+ children: [],
50
+ }
51
+ }
52
+
20
53
  describe('hasFlexOrGridDisplay', () => {
21
54
  it('returns true for display: flex', () => {
22
55
  expect(hasFlexOrGridDisplay(matcherDataFromCss('display: flex'))).toBe(true)
23
56
  })
24
57
 
58
+ it('returns true for display: block flex', () => {
59
+ expect(hasFlexOrGridDisplay(matcherDataFromCss('display: block flex'))).toBe(true)
60
+ })
61
+
62
+ it('returns true for display: inline grid', () => {
63
+ expect(hasFlexOrGridDisplay(matcherDataFromCss('display: inline grid'))).toBe(true)
64
+ })
65
+
25
66
  it('returns false for display: block', () => {
26
67
  expect(hasFlexOrGridDisplay(matcherDataFromCss('display: block'))).toBe(false)
27
68
  })
@@ -42,3 +83,254 @@ describe('hasFlexOrGridDisplay', () => {
42
83
  expect(hasFlexOrGridDisplay(matcherDataFromCss('display: var(--d)'))).toBe(true)
43
84
  })
44
85
  })
86
+
87
+ describe('hasContainerLikeDisplay', () => {
88
+ it('returns true for display: block', () => {
89
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: block'))).toBe(true)
90
+ })
91
+
92
+ it('returns true for display: inline-block', () => {
93
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: inline-block'))).toBe(true)
94
+ })
95
+
96
+ it('returns false for display: inline', () => {
97
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: inline'))).toBe(false)
98
+ })
99
+
100
+ it('returns true when var cannot be resolved', () => {
101
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: var(--d)'))).toBe(true)
102
+ })
103
+
104
+ it('returns false when a missing local var falls back to inline', () => {
105
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: var(--d, inline)'))).toBe(false)
106
+ })
107
+
108
+ it('returns true when a local custom property chain falls back to inline-flex', () => {
109
+ expect(
110
+ hasContainerLikeDisplay(
111
+ matcherDataFromCss(
112
+ 'display: var(--component-display, inline); --component-display: var(--theme-display, inline-flex)',
113
+ ),
114
+ ),
115
+ ).toBe(true)
116
+ })
117
+
118
+ it('returns false when a locally defined cyclic var falls back to inline', () => {
119
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: var(--d, inline); --d: var(--d)'))).toBe(false)
120
+ })
121
+
122
+ it('returns false when a cyclic custom property has its own inner fallback but display falls back to inline', () => {
123
+ expect(hasContainerLikeDisplay(matcherDataFromCss('display: var(--d, inline); --d: var(--d, inline-flex)'))).toBe(
124
+ false,
125
+ )
126
+ })
127
+
128
+ it('uses the last matched display declaration when selectors conflict', () => {
129
+ const matcherData: MatchedCssData = {
130
+ matches: [
131
+ { selector: '.label', properties: [{ name: 'display', value: 'block' }], specificity: [0, 1, 0] },
132
+ { selector: '.label.compact', properties: [{ name: 'display', value: 'inline' }], specificity: [0, 2, 0] },
133
+ ],
134
+ customProperties: {},
135
+ }
136
+
137
+ expect(hasContainerLikeDisplay(matcherData)).toBe(false)
138
+ })
139
+
140
+ it('uses selector specificity before source order when display rules conflict', () => {
141
+ const matcherData: MatchedCssData = {
142
+ matches: [
143
+ { selector: '.card.compact', properties: [{ name: 'display', value: 'flex' }], specificity: [0, 2, 0] },
144
+ { selector: '.compact', properties: [{ name: 'display', value: 'inline' }], specificity: [0, 1, 0] },
145
+ ],
146
+ customProperties: {},
147
+ }
148
+
149
+ expect(hasContainerLikeDisplay(matcherData)).toBe(true)
150
+ expect(hasFlexOrGridDisplay(matcherData)).toBe(true)
151
+ })
152
+ })
153
+
154
+ describe('getCssPropertiesForTag', () => {
155
+ it('treats ul as a box-generating element', () => {
156
+ const relevantProperties = getCssPropertiesForTag('ul')
157
+
158
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
159
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
160
+ })
161
+
162
+ it('treats ol as a box-generating element', () => {
163
+ const relevantProperties = getCssPropertiesForTag('ol')
164
+
165
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
166
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
167
+ })
168
+
169
+ it('treats table cells as box-generating elements', () => {
170
+ const relevantProperties = getCssPropertiesForTag('td')
171
+
172
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
173
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
174
+ })
175
+
176
+ it('derives box properties from default display for controls', () => {
177
+ const relevantProperties = getCssPropertiesForTag('button')
178
+
179
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
180
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
181
+ })
182
+
183
+ it('keeps media elements out of box properties even when their default display is non-inline', () => {
184
+ const relevantProperties = getCssPropertiesForTag('img')
185
+
186
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.borderTop)
187
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.background)
188
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
189
+ })
190
+
191
+ it('composes text and box properties for semantic text with box-generating default display', () => {
192
+ const relevantProperties = getCssPropertiesForTag('p')
193
+
194
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
195
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
196
+ })
197
+
198
+ it('falls back to box properties for unknown non-media tags', () => {
199
+ const relevantProperties = getCssPropertiesForTag('svg')
200
+
201
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
202
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
203
+ })
204
+ })
205
+
206
+ describe('getDefaultDisplayForTag', () => {
207
+ it('returns UA defaults for representative structural elements', () => {
208
+ expect(getDefaultDisplayForTag('html')).toBe('block')
209
+ expect(getDefaultDisplayForTag('legend')).toBe('block')
210
+ expect(getDefaultDisplayForTag('li')).toBe('list-item')
211
+ expect(getDefaultDisplayForTag('table')).toBe('table')
212
+ expect(getDefaultDisplayForTag('tbody')).toBe('table-row-group')
213
+ expect(getDefaultDisplayForTag('td')).toBe('table-cell')
214
+ })
215
+
216
+ it('returns UA defaults for representative inline and replaced elements', () => {
217
+ expect(getDefaultDisplayForTag('a')).toBe('inline')
218
+ expect(getDefaultDisplayForTag('q')).toBe('inline')
219
+ expect(getDefaultDisplayForTag('img')).toBe('inline')
220
+ expect(getDefaultDisplayForTag('iframe')).toBe('inline')
221
+ expect(getDefaultDisplayForTag('input')).toBe('inline-block')
222
+ expect(getDefaultDisplayForTag('textarea')).toBe('inline-block')
223
+ })
224
+
225
+ it('returns UA defaults for non-rendered metadata elements', () => {
226
+ expect(getDefaultDisplayForTag('head')).toBe('none')
227
+ expect(getDefaultDisplayForTag('meta')).toBe('none')
228
+ expect(getDefaultDisplayForTag('script')).toBe('none')
229
+ expect(getDefaultDisplayForTag('template')).toBe('none')
230
+ })
231
+
232
+ it('falls back to block for unknown tags', () => {
233
+ expect(getDefaultDisplayForTag('custom-element')).toBe('block')
234
+ })
235
+ })
236
+
237
+ describe('enrichContainerProperties', () => {
238
+ it('adds box properties to span when display becomes block', () => {
239
+ const elements = [createElementWithCssProperties('span', 'display: block')]
240
+
241
+ const [enrichedElement] = enrichContainerProperties(elements)
242
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
243
+
244
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
245
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
246
+ })
247
+
248
+ it('does not add box properties to span when display stays inline', () => {
249
+ const elements = [createElementWithCssProperties('span', 'display: inline')]
250
+
251
+ const [enrichedElement] = enrichContainerProperties(elements)
252
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
253
+
254
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
255
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
256
+ })
257
+
258
+ it('adds box properties to li when it relies on its default list-item display', () => {
259
+ const elements = [createElementWithCssProperties('li', '')]
260
+
261
+ const [enrichedElement] = enrichContainerProperties(elements)
262
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
263
+
264
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
265
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
266
+ })
267
+
268
+ it('adds box properties to strong when display becomes inline-flex', () => {
269
+ const elements = [createElementWithCssProperties('strong', 'display: inline-flex')]
270
+
271
+ const [enrichedElement] = enrichContainerProperties(elements)
272
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
273
+
274
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
275
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
276
+ })
277
+
278
+ it('does not add box properties when display var is missing locally and falls back to inline', () => {
279
+ const elements = [createElementWithCssProperties('span', 'display: var(--display, inline)')]
280
+
281
+ const [enrichedElement] = enrichContainerProperties(elements)
282
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
283
+
284
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
285
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
286
+ })
287
+
288
+ it('adds box properties when a local custom property chain falls back to inline-flex', () => {
289
+ const elements = [
290
+ createElementWithCssProperties(
291
+ 'span',
292
+ 'display: var(--component-display, inline); --component-display: var(--theme-display, inline-flex)',
293
+ ),
294
+ ]
295
+
296
+ const [enrichedElement] = enrichContainerProperties(elements)
297
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
298
+
299
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
300
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
301
+ })
302
+
303
+ it('does not add box properties when a locally defined cyclic display var falls back to inline', () => {
304
+ const elements = [
305
+ createElementWithCssProperties('span', 'display: var(--display, inline); --display: var(--display)'),
306
+ ]
307
+
308
+ const [enrichedElement] = enrichContainerProperties(elements)
309
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
310
+
311
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
312
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
313
+ })
314
+
315
+ it('does not add box properties when a cyclic display var has its own inner fallback but display falls back to inline', () => {
316
+ const elements = [
317
+ createElementWithCssProperties('span', 'display: var(--display, inline); --display: var(--display, inline-flex)'),
318
+ ]
319
+
320
+ const [enrichedElement] = enrichContainerProperties(elements)
321
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
322
+
323
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
324
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
325
+ })
326
+
327
+ it('adds box properties to role-based text elements when display becomes inline-flex', () => {
328
+ const elements = [createElementWithCssProperties('div', 'display: inline-flex', { role: 'heading' })]
329
+
330
+ const [enrichedElement] = enrichContainerProperties(elements)
331
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
332
+
333
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
334
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
335
+ })
336
+ })