@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.
- package/dist/{index-C4cP1dwy.js → index-K3lIO4FC.js} +21613 -19661
- package/dist/{index-DT1jTZEG.js → index-NbI-_ozJ.js} +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
- package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
- package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
- package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
- package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
- package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
- package/src/component-renderer.ts +12 -9
- package/src/converters/to-editor-component.test.ts +88 -0
- package/src/converters/to-editor-component.ts +35 -6
- package/src/index.ts +121 -54
- package/src/information-extractors/css/parse.test.ts +49 -1
- package/src/information-extractors/css/parse.ts +167 -3
- package/src/information-extractors/css/selector-matcher.ts +3 -1
- package/src/information-extractors/css/types.ts +11 -0
- package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
- package/src/information-extractors/react/extractors/core/types.ts +4 -5
- package/src/information-extractors/react/extractors/css-properties.test.ts +294 -2
- package/src/information-extractors/react/extractors/css-properties.ts +248 -98
- package/src/information-extractors/react/extractors/index.ts +1 -0
- package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
- package/src/information-extractors/react/index.ts +1 -0
- package/src/information-extractors/react/types.ts +1 -1
- package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
- package/src/information-extractors/react/utils/mock-generator.ts +38 -13
- package/src/manifest-pipeline.ts +22 -15
- package/src/module-loader.test.ts +150 -0
- package/src/module-loader.ts +48 -8
- package/src/react-runtime-interceptor.ts +64 -0
- package/src/react-runtime-loader.ts +656 -0
- package/src/ref-elements/component-tag.ts +105 -0
- package/src/ref-elements/context.test.ts +179 -0
- package/src/ref-elements/context.ts +280 -0
- package/src/ref-elements/eligible-paths.ts +45 -0
- package/src/ref-elements/module-resolution.test.ts +50 -0
- package/src/ref-elements/module-resolution.ts +21 -0
- package/src/ref-elements/module-specifier.ts +17 -0
- package/src/ref-elements/path-utils.test.ts +14 -0
- package/src/ref-elements/path-utils.ts +55 -0
- package/src/ref-elements/types.ts +17 -0
- package/src/utils/css-class.ts +15 -0
- 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
|
|
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
|
|
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
|
-
|
|
21
|
+
type: unknown
|
|
22
|
+
isDomElement: boolean
|
|
23
|
+
tag?: string
|
|
25
24
|
props: Record<string, unknown>
|
|
26
|
-
traceId
|
|
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 {
|
|
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: [
|
|
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
|
+
})
|