@wix/zero-config-implementation 1.99.0 → 1.101.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.99.0",
7
+ "version": "1.101.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",
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "falconPackageHash": "905a961b05476216ee10b85e56b0d50818e5247abe594c0917619c0a"
109
+ "falconPackageHash": "7aa5841bf5ff6a0fa0800fc3d71a267d6e96dbe084125f5fd56fc333"
110
110
  }
@@ -131,6 +131,41 @@ describe('resolveLonghand', () => {
131
131
  expect(resolveLonghand('paddingTop', new Map())).toBeUndefined()
132
132
  })
133
133
 
134
+ describe('margin family', () => {
135
+ it('marginTop falls back through marginBlockStart → marginBlock → margin', () => {
136
+ expect(resolveLonghand('marginTop', new Map([['marginBlockStart', '1px']]))).toBe('1px')
137
+ expect(resolveLonghand('marginTop', new Map([['marginBlock', '4px 8px']]))).toBe('4px')
138
+ expect(resolveLonghand('marginTop', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
139
+ })
140
+
141
+ it('marginBottom falls back through marginBlockEnd → marginBlock → margin', () => {
142
+ expect(resolveLonghand('marginBottom', new Map([['marginBlockEnd', '2px']]))).toBe('2px')
143
+ expect(resolveLonghand('marginBottom', new Map([['marginBlock', '4px 8px']]))).toBe('8px')
144
+ expect(resolveLonghand('marginBottom', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
145
+ })
146
+
147
+ it('marginInlineStart falls back through marginLeft → marginInline → margin', () => {
148
+ expect(resolveLonghand('marginInlineStart', new Map([['marginLeft', '7px']]))).toBe('7px')
149
+ expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px']]))).toBe('12px')
150
+ expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px 24px']]))).toBe('12px')
151
+ expect(resolveLonghand('marginInlineStart', new Map([['margin', '1px 2px 3px 4px']]))).toBe('4px')
152
+ })
153
+
154
+ it('marginInlineEnd falls back through marginRight → marginInline → margin', () => {
155
+ expect(resolveLonghand('marginInlineEnd', new Map([['marginRight', '9px']]))).toBe('9px')
156
+ expect(resolveLonghand('marginInlineEnd', new Map([['marginInline', '12px 24px']]))).toBe('24px')
157
+ expect(resolveLonghand('marginInlineEnd', new Map([['margin', '1px 2px 3px 4px']]))).toBe('2px')
158
+ })
159
+
160
+ it('prefers the literal longhand over any fallback', () => {
161
+ const values = new Map([
162
+ ['marginTop', '5px'],
163
+ ['margin', '10px'],
164
+ ])
165
+ expect(resolveLonghand('marginTop', values)).toBe('5px')
166
+ })
167
+ })
168
+
134
169
  describe('padding family', () => {
135
170
  it('paddingTop falls back through paddingBlockStart → paddingBlock → padding', () => {
136
171
  expect(resolveLonghand('paddingTop', new Map([['paddingBlockStart', '1px']]))).toBe('1px')
@@ -215,6 +215,18 @@ const fromTextDecorationLine: Fallback = (values) => {
215
215
  return lineTokens.join(' ')
216
216
  }
217
217
 
218
+ /**
219
+ * `flex-flow: <flex-direction> || <flex-wrap>` — order is free and either
220
+ * part may be omitted. Pick out just the direction keyword for the schema.
221
+ */
222
+ const FLEX_DIRECTION_KEYWORDS = new Set(['row', 'row-reverse', 'column', 'column-reverse'])
223
+
224
+ const fromFlexFlowDirection: Fallback = (values) => {
225
+ const shorthand = values.get('flexFlow')
226
+ if (shorthand === undefined) return undefined
227
+ return splitTopLevelTokens(shorthand).find((token) => FLEX_DIRECTION_KEYWORDS.has(token.toLowerCase()))
228
+ }
229
+
218
230
  // ─────────────────────────────────────────────────────────────────────────────
219
231
  // Resolution table
220
232
  //
@@ -233,6 +245,12 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
233
245
  paddingInlineStart: [fromKey('paddingLeft'), fromAxis('paddingInline', 'start'), fromBox('padding', 'left')],
234
246
  paddingInlineEnd: [fromKey('paddingRight'), fromAxis('paddingInline', 'end'), fromBox('padding', 'right')],
235
247
 
248
+ // Margin — same block/inline convention as padding above.
249
+ marginTop: [fromKey('marginBlockStart'), fromAxis('marginBlock', 'start'), fromBox('margin', 'top')],
250
+ marginBottom: [fromKey('marginBlockEnd'), fromAxis('marginBlock', 'end'), fromBox('margin', 'bottom')],
251
+ marginInlineStart: [fromKey('marginLeft'), fromAxis('marginInline', 'start'), fromBox('margin', 'left')],
252
+ marginInlineEnd: [fromKey('marginRight'), fromAxis('marginInline', 'end'), fromBox('margin', 'right')],
253
+
236
254
  // Border radius — schema uses all-logical corner names; physical names map under LTR/horizontal-tb.
237
255
  borderStartStartRadius: [fromKey('borderTopLeftRadius'), fromCorner('borderRadius', 'tl')],
238
256
  borderStartEndRadius: [fromKey('borderTopRightRadius'), fromCorner('borderRadius', 'tr')],
@@ -255,6 +273,10 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
255
273
  // and color in one declaration. Pick out just the line tokens for the schema.
256
274
  textDecorationLine: [fromTextDecorationLine],
257
275
 
276
+ // Flex — `flex-flow: column wrap` carries both direction and wrap; pick out
277
+ // just the direction keyword for the schema.
278
+ flexDirection: [fromFlexFlowDirection],
279
+
258
280
  // Shorthand targets (e.g. `font`) live in css-shorthand-composer.ts and are
259
281
  // merged in here — composer-driven, conceptually the inverse of the rows above.
260
282
  ...SHORTHAND_FALLBACKS,
@@ -346,6 +346,72 @@ describe('toEditorReactComponent — cssProperties defaultValue', () => {
346
346
  defaultValue: 'var(--wst-primary-background-color)',
347
347
  })
348
348
  })
349
+
350
+ it('excludes width/height from the root element (sizing is handled via layout config)', () => {
351
+ const extractorData = new Map<string, unknown>()
352
+ extractorData.set('css-matcher', matcherDataFromCss('width: 100px; height: 50px; overflow: hidden;'))
353
+ extractorData.set('css-properties', {
354
+ relevant: [
355
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.width,
356
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.height,
357
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow,
358
+ ],
359
+ })
360
+
361
+ const rootElement: ExtractedElement = {
362
+ traceId: 'trace-root',
363
+ name: 'root',
364
+ tag: 'div',
365
+ attributes: {},
366
+ extractorData,
367
+ children: [],
368
+ }
369
+
370
+ const result = toEditorReactComponent(createComponent(rootElement))
371
+
372
+ expect(result.editorElement?.cssProperties?.width).toBeUndefined()
373
+ expect(result.editorElement?.cssProperties?.height).toBeUndefined()
374
+ expect(result.editorElement?.cssProperties?.overflow).toEqual({ defaultValue: 'hidden' })
375
+ })
376
+
377
+ it('keeps width/height on inner (non-root) elements', () => {
378
+ const rootExtractorData = new Map<string, unknown>()
379
+ rootExtractorData.set('css-matcher', matcherDataFromCss(''))
380
+ rootExtractorData.set('css-properties', { relevant: [] })
381
+
382
+ const childExtractorData = new Map<string, unknown>()
383
+ childExtractorData.set('css-matcher', matcherDataFromCss('width: 24px; height: 24px;'))
384
+ childExtractorData.set('css-properties', {
385
+ relevant: [CSS_PROPERTIES.CSS_PROPERTY_TYPE.width, CSS_PROPERTIES.CSS_PROPERTY_TYPE.height],
386
+ })
387
+
388
+ const childElement: ExtractedElement = {
389
+ traceId: 'trace-icon',
390
+ name: 'icon',
391
+ tag: 'img',
392
+ attributes: {},
393
+ extractorData: childExtractorData,
394
+ children: [],
395
+ }
396
+
397
+ const rootElement: ExtractedElement = {
398
+ traceId: 'trace-root',
399
+ name: 'root',
400
+ tag: 'div',
401
+ attributes: {},
402
+ extractorData: rootExtractorData,
403
+ children: [childElement],
404
+ }
405
+
406
+ const result = toEditorReactComponent(createComponent(rootElement))
407
+
408
+ expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.width).toEqual({
409
+ defaultValue: '24px',
410
+ })
411
+ expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.height).toEqual({
412
+ defaultValue: '24px',
413
+ })
414
+ })
349
415
  })
350
416
 
351
417
  describe('toEditorReactComponent selector conversion', () => {
@@ -659,6 +725,65 @@ describe('toEditorReactComponent — @property design tokens', () => {
659
725
  expect(editorElement?.cssProperties?.['sdf-safelight-color']).toBeUndefined()
660
726
  expect(editorElement?.cssProperties?.['--sdf-safelight-color']).toBeUndefined()
661
727
  })
728
+
729
+ it('places a declared custom property on its declaring element even when its only tracked usage was pruned from the tree', () => {
730
+ const buttonElement: ExtractedElement = {
731
+ traceId: 'trace-button',
732
+ name: 'button',
733
+ tag: 'button',
734
+ attributes: { class: 'play-button' },
735
+ extractorData: new Map<string, unknown>([
736
+ ['css-matcher', { matches: [], customProperties: { '--icon-color': 'red' } } as MatchedCssData],
737
+ ]),
738
+ children: [],
739
+ }
740
+ const rootElement: ExtractedElement = {
741
+ traceId: 'trace-root',
742
+ name: 'root',
743
+ tag: 'div',
744
+ attributes: {},
745
+ extractorData: new Map<string, unknown>(),
746
+ children: [buttonElement],
747
+ }
748
+ const component: ComponentInfoWithCss = {
749
+ componentName: 'IconButton',
750
+ props: {},
751
+ elements: [rootElement],
752
+ propUsages: new Map(),
753
+ css: [],
754
+ // The icon that actually consumes --icon-color has no semantic class, so it was
755
+ // already filtered out of `elements` upstream — its trace id survives only here.
756
+ varUsedByTraceId: new Map([['--icon-color', new Set(['trace-pruned-icon'])]]),
757
+ }
758
+
759
+ const editorElement = toEditorReactComponent(component).editorElement
760
+
761
+ expect(editorElement?.elements?.button?.inlineElement?.cssCustomProperties?.['icon-color']).toEqual({
762
+ displayName: 'Icon Color',
763
+ defaultValue: 'red',
764
+ cssPropertyType: 'color',
765
+ })
766
+ expect(editorElement?.cssCustomProperties?.['icon-color']).toBeUndefined()
767
+ })
768
+
769
+ it('falls back to the component root when a variable has no surviving declaration and only orphaned usage', () => {
770
+ const component: ComponentInfoWithCss = {
771
+ ...createComponentWithCss(`
772
+ @property --gap { syntax: "<length>"; inherits: true; initial-value: 4px; }
773
+ `),
774
+ // No element declares --gap locally; its only recorded usage is a trace id that
775
+ // never made it into `elements` (simulating a pruned consumer).
776
+ varUsedByTraceId: new Map([['--gap', new Set(['trace-orphan'])]]),
777
+ }
778
+
779
+ const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
780
+
781
+ expect(customProps?.gap).toEqual({
782
+ displayName: 'Gap',
783
+ defaultValue: '4px',
784
+ cssPropertyType: 'length',
785
+ })
786
+ })
662
787
  })
663
788
 
664
789
  describe('toEditorReactComponent — ActiveItemIndex display groups', () => {
@@ -25,7 +25,11 @@ import type {
25
25
  ExtractedElement,
26
26
  TrackingStores,
27
27
  } from '../information-extractors/react'
28
- import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
28
+ import {
29
+ ROOT_DISPLAY_CUSTOM_PROPERTY,
30
+ getDefaultDisplayForTag,
31
+ resolveDisplayValue,
32
+ } from '../information-extractors/react'
29
33
  import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
30
34
  import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
31
35
  import { resolveLonghand } from './css-longhand-resolver'
@@ -244,7 +248,7 @@ function buildElements(
244
248
  ...(data && Object.keys(data).length > 0 && { data }),
245
249
  // CSS properties from heuristic + matched CSS files
246
250
  ...(Object.keys(cssProps).length > 0 && { cssProperties: cssProps }),
247
- // CSS custom properties placed on the Nearest Common Ancestor of all elements that use each variable
251
+ // CSS custom properties for this element
248
252
  ...(Object.keys(cssCustomProps).length > 0 && { cssCustomProperties: cssCustomProps }),
249
253
  ...(states && { states }),
250
254
  // Recursively build nested elements
@@ -273,8 +277,9 @@ function buildElements(
273
277
 
274
278
  /**
275
279
  * Pre-computes a map of traceId → custom property items to assign to each element.
276
- * Each custom property is placed on the nearest common ancestor (Nearest Common Ancestor) of all elements
277
- * that use it via var(). Falls back to the element that defines it if unused.
280
+ * A variable is placed on the element that declares it; if none survived pruning, it
281
+ * falls back to the nearest common ancestor of its usages, or the root for a registered
282
+ * but undeclared @property.
278
283
  */
279
284
  function buildNearestCommonAncestorCustomProps(
280
285
  component: ComponentInfoWithCss,
@@ -307,23 +312,32 @@ function buildNearestCommonAncestorCustomProps(
307
312
  const nearestCommonAncestorCustomProps = new Map<string, Record<string, CssCustomPropertyItem>>()
308
313
 
309
314
  for (const varName of varNames) {
310
- if (varName === '--display') continue
315
+ if (varName === ROOT_DISPLAY_CUSTOM_PROPERTY) continue
311
316
 
312
317
  const registration = registeredByVar.get(varName)
313
318
  // Prefer a concrete declared value; fall back to the @property initial-value so a
314
319
  // component doesn't need an element-level `--x: ...` declaration.
315
320
  const defaultValue = allCustomPropertyValues.get(varName) ?? registration?.defaultValue
316
321
 
317
- const usedByTraceIds = component.varUsedByTraceId.get(varName) ?? new Set<string>()
322
+ // Declaration wins over usage; multiple declaring elements resolve to their NCA.
323
+ const definingTraceIds = findDefiningElements(component.elements, varName)
318
324
 
319
325
  let nearestCommonAncestorTraceId: string | undefined
320
- if (usedByTraceIds.size > 0) {
321
- nearestCommonAncestorTraceId = findNearestCommonAncestor(usedByTraceIds, parentMap)
326
+ if (definingTraceIds.size > 0) {
327
+ nearestCommonAncestorTraceId = findNearestCommonAncestor(definingTraceIds, parentMap)
322
328
  } else {
323
- // Fallback: the element whose CSS selector defines the variable; failing that, a
324
- // registered @property with no var() usage anchors on the component root.
329
+ // No surviving declaration fall back to usage NCA, restricted to trace IDs still in the tree.
330
+ const usedByTraceIds = component.varUsedByTraceId.get(varName) ?? new Set<string>()
331
+ const survivingUsedByTraceIds = new Set(
332
+ [...usedByTraceIds].filter((traceId) => traceId === rootTraceId || parentMap.has(traceId)),
333
+ )
334
+
325
335
  nearestCommonAncestorTraceId =
326
- findDefiningElement(component.elements, varName) ?? (registration ? rootTraceId : undefined)
336
+ survivingUsedByTraceIds.size > 0
337
+ ? findNearestCommonAncestor(survivingUsedByTraceIds, parentMap)
338
+ : registration
339
+ ? rootTraceId
340
+ : undefined
327
341
  }
328
342
 
329
343
  if (!nearestCommonAncestorTraceId) continue
@@ -502,17 +516,16 @@ function findNearestCommonAncestor(traceIds: Set<string>, parentMap: Map<string,
502
516
  }
503
517
 
504
518
  /**
505
- * Finds the traceId of the first element in the tree whose matched CSS rules
506
- * define the given custom property (fallback when var() usage is not tracked).
519
+ * Finds every surviving element whose matched CSS declares the given custom property.
507
520
  */
508
- function findDefiningElement(elements: ExtractedElement[], varName: string): string | undefined {
521
+ function findDefiningElements(elements: ExtractedElement[], varName: string): Set<string> {
522
+ const definingTraceIds = new Set<string>()
509
523
  for (const element of elements) {
510
524
  const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
511
- if (matcherData?.customProperties[varName] !== undefined) return element.traceId
512
- const childResult = findDefiningElement(element.children, varName)
513
- if (childResult !== undefined) return childResult
525
+ if (matcherData?.customProperties[varName] !== undefined) definingTraceIds.add(element.traceId)
526
+ for (const traceId of findDefiningElements(element.children, varName)) definingTraceIds.add(traceId)
514
527
  }
515
- return undefined
528
+ return definingTraceIds
516
529
  }
517
530
 
518
531
  // ─────────────────────────────────────────────────────────────────────────────
@@ -563,6 +576,10 @@ function toDisplayEnumValue(cssValue: string): DisplayEnumValue {
563
576
  return (CSS_DISPLAY_TO_ENUM[cssValue] ?? cssValue) as DisplayEnumValue
564
577
  }
565
578
 
579
+ // Root sizing is handled separately via layout config, so `width`/`height`
580
+ // are excluded from the root element's own `cssProperties`.
581
+ const ROOT_EXCLUDED_CSS_PROPERTIES = new Set<string>([CSS_PROPERTY_TYPE.width, CSS_PROPERTY_TYPE.height])
582
+
566
583
  function buildCssProperties(
567
584
  element: ExtractedElement | undefined,
568
585
  isRootElement: boolean,
@@ -576,6 +593,9 @@ function buildCssProperties(
576
593
  }
577
594
  const cssPropertyValues = element ? getMatchedPropertyValues(element) : new Map<string, string>()
578
595
  for (const propName of decidedProperties) {
596
+ if (isRootElement && ROOT_EXCLUDED_CSS_PROPERTIES.has(propName)) {
597
+ continue
598
+ }
579
599
  if (propName === CSS_PROPERTY_TYPE.display && element) {
580
600
  if (!isRootElement) {
581
601
  result[propName] = buildDisplayProperty(element)
@@ -5,6 +5,7 @@ import type { MatchedCssData } from '../../css/types'
5
5
  import type { ExtractedElement } from './core/tree-builder'
6
6
  import {
7
7
  enrichContainerProperties,
8
+ enrichFlexProperties,
8
9
  getCssPropertiesForTag,
9
10
  getDefaultDisplayForTag,
10
11
  hasContainerLikeDisplay,
@@ -252,6 +253,131 @@ describe('getCssPropertiesForTag', () => {
252
253
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
253
254
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
254
255
  })
256
+
257
+ it('gives block/replaced elements the full margin family regardless of box/text/media semantics', () => {
258
+ for (const tag of ['div', 'p', 'img']) {
259
+ const relevantProperties = getCssPropertiesForTag(tag)
260
+
261
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
262
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
263
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
264
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
265
+ }
266
+ })
267
+
268
+ it('gives non-replaced inline elements only the horizontal margins, per CSS margin-on-inline-boxes rules', () => {
269
+ for (const tag of ['span', 'a', 'em', 'b', 'i']) {
270
+ const relevantProperties = getCssPropertiesForTag(tag)
271
+
272
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
273
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
274
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
275
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
276
+ }
277
+ })
278
+
279
+ it('gives internal table display elements no margin at all, per CSS 2.1 §17.6.1', () => {
280
+ for (const tag of ['tr', 'thead', 'tbody', 'tfoot', 'col', 'colgroup', 'td', 'th']) {
281
+ const relevantProperties = getCssPropertiesForTag(tag)
282
+
283
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
284
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
285
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
286
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
287
+ }
288
+ })
289
+
290
+ it('gives box-generating elements width, height, overflow and mix-blend-mode', () => {
291
+ const relevantProperties = getCssPropertiesForTag('div')
292
+
293
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
294
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
295
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
296
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
297
+ })
298
+
299
+ it('gives media elements sizing, object-fit/position and mix-blend-mode', () => {
300
+ const relevantProperties = getCssPropertiesForTag('img')
301
+
302
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
303
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
304
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
305
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
306
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectPosition)
307
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
308
+ })
309
+
310
+ it('keeps text-only tags without width/height/overflow/object-fit', () => {
311
+ const relevantProperties = getCssPropertiesForTag('p')
312
+
313
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
314
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
315
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
316
+ })
317
+ })
318
+
319
+ describe('enrichFlexProperties', () => {
320
+ it('adds flexDirection/justifyContent/alignItems to a flex container', () => {
321
+ const elements = [createElementWithCssProperties('div', 'display: flex')]
322
+
323
+ const [enrichedElement] = enrichFlexProperties(elements)
324
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
325
+
326
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
327
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
328
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
329
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
330
+ })
331
+
332
+ it('does not add flex container properties to a non-flex/grid element', () => {
333
+ const elements = [createElementWithCssProperties('div', 'display: block')]
334
+
335
+ const [enrichedElement] = enrichFlexProperties(elements)
336
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
337
+
338
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
339
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
340
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
341
+ })
342
+
343
+ it('adds alignSelf to direct children of a flex/grid container', () => {
344
+ const child = createElementWithCssProperties('span', '')
345
+ const parent: ExtractedElement = {
346
+ ...createElementWithCssProperties('div', 'display: flex'),
347
+ children: [child],
348
+ }
349
+
350
+ const [enrichedParent] = enrichFlexProperties([parent])
351
+ const enrichedChild = enrichedParent.children[0]
352
+ const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
353
+
354
+ expect(childCssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
355
+ })
356
+
357
+ it('does not add alignSelf to children of a non-flex/grid container', () => {
358
+ const child = createElementWithCssProperties('span', '')
359
+ const parent: ExtractedElement = {
360
+ ...createElementWithCssProperties('div', 'display: block'),
361
+ children: [child],
362
+ }
363
+
364
+ const [enrichedParent] = enrichFlexProperties([parent])
365
+ const enrichedChild = enrichedParent.children[0]
366
+ const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
367
+
368
+ expect(childCssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
369
+ })
370
+
371
+ it('adds flex container properties to a grid container as well', () => {
372
+ const elements = [createElementWithCssProperties('div', 'display: grid')]
373
+
374
+ const [enrichedElement] = enrichFlexProperties(elements)
375
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
376
+
377
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
378
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
379
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
380
+ })
255
381
  })
256
382
 
257
383
  describe('getDefaultDisplayForTag', () => {
@@ -404,4 +530,14 @@ describe('enrichContainerProperties', () => {
404
530
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
405
531
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
406
532
  })
533
+
534
+ it('adds box properties to span when display is signaled only via the --display root convention, not a literal display declaration', () => {
535
+ const elements = [createElementWithCssProperties('span', '--display: block')]
536
+
537
+ const [enrichedElement] = enrichContainerProperties(elements)
538
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
539
+
540
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
541
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
542
+ })
407
543
  })