@wix/zero-config-implementation 1.105.0 → 1.106.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.105.0",
7
+ "version": "1.106.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": "0c8ba866b638c1cacaeaecfc2e82c36a40e6307f5e3896c922c1dc25"
109
+ "falconPackageHash": "257681de48c644877515f4fab42d8af3418a370d31f219d70153597a"
110
110
  }
@@ -0,0 +1,308 @@
1
+ import { CSS_PROPERTIES } from '@wix/react-component-schema'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { TRACE_ATTR } from '../component-renderer'
4
+ import { parseCss } from '../information-extractors/css/parse'
5
+ import { matchCssSelectors } from '../information-extractors/css/selector-matcher'
6
+ import type { ExtractedElement } from '../information-extractors/react'
7
+ import type { ComponentInfoWithCss, ExtractedCssInfo } from '../manifest-pipeline'
8
+ import { toEditorReactComponent } from './to-editor-component'
9
+
10
+ const { CSS_PROPERTY_TYPE } = CSS_PROPERTIES
11
+
12
+ interface ElementSpec {
13
+ name: string
14
+ tag: string
15
+ className: string
16
+ relevant: string[]
17
+ children?: ElementSpec[]
18
+ }
19
+
20
+ function toExtractedElement(spec: ElementSpec): ExtractedElement {
21
+ return {
22
+ traceId: `trace-${spec.name}`,
23
+ name: spec.name,
24
+ tag: spec.tag,
25
+ attributes: { class: spec.className },
26
+ extractorData: new Map<string, unknown>([['css-properties', { relevant: spec.relevant }]]),
27
+ children: (spec.children ?? []).map(toExtractedElement),
28
+ }
29
+ }
30
+
31
+ function toHtml(spec: ElementSpec): string {
32
+ const childrenHtml = (spec.children ?? []).map(toHtml).join('')
33
+ return `<${spec.tag} ${TRACE_ATTR}="trace-${spec.name}" class="${spec.className}">${childrenHtml}</${spec.tag}>`
34
+ }
35
+
36
+ /** Runs the CSS through the real selector matcher, then converts the matched tree. */
37
+ function convert(css: string, rootSpec: ElementSpec) {
38
+ const api = parseCss(css)
39
+ const cssInfo: ExtractedCssInfo = {
40
+ filePath: 'test.css',
41
+ api,
42
+ properties: new Map(),
43
+ customProperties: new Map(),
44
+ isCssModule: false,
45
+ }
46
+ const { elements, varUsedByTraceId } = matchCssSelectors(toHtml(rootSpec), [toExtractedElement(rootSpec)], [cssInfo])
47
+ const component: ComponentInfoWithCss = {
48
+ componentName: 'StateDefaults',
49
+ props: {},
50
+ elements,
51
+ propUsages: new Map(),
52
+ css: [cssInfo],
53
+ varUsedByTraceId,
54
+ }
55
+ const { editorElement } = toEditorReactComponent(component)
56
+ if (!editorElement) throw new Error('toEditorReactComponent produced no editorElement')
57
+ return editorElement
58
+ }
59
+
60
+ function rootButton(relevant: string[], children: ElementSpec[] = []): ElementSpec {
61
+ return { name: 'root', tag: 'button', className: 'widget', relevant, children }
62
+ }
63
+
64
+ describe('toEditorReactComponent — statesDefaultValues', () => {
65
+ it('keeps the regular default and moves a hover override into statesDefaultValues', () => {
66
+ const editorElement = convert(
67
+ `.widget { background: red; }
68
+ .widget:global(.widget--hover), .widget:hover { background: blue; }`,
69
+ rootButton([CSS_PROPERTY_TYPE.background]),
70
+ )
71
+
72
+ expect(editorElement.cssProperties?.background).toEqual({
73
+ defaultValue: 'red',
74
+ statesDefaultValues: { hover: 'blue' },
75
+ })
76
+ })
77
+
78
+ it('emits no statesDefaultValues when the hover rule does not touch the property', () => {
79
+ const editorElement = convert(
80
+ `.widget { background: red; }
81
+ .widget:hover { color: blue; }`,
82
+ rootButton([CSS_PROPERTY_TYPE.background]),
83
+ )
84
+
85
+ expect(editorElement.cssProperties?.background).toEqual({ defaultValue: 'red' })
86
+ })
87
+
88
+ it('omits a state value equal to the regular default', () => {
89
+ const editorElement = convert(
90
+ `.widget { background: red; }
91
+ .widget:hover { background: red; }`,
92
+ rootButton([CSS_PROPERTY_TYPE.background]),
93
+ )
94
+
95
+ expect(editorElement.cssProperties?.background).toEqual({ defaultValue: 'red' })
96
+ })
97
+
98
+ it('resolves a state shorthand into longhands without letting a regular longhand win', () => {
99
+ const editorElement = convert(
100
+ `.widget { padding-top: 1px; padding-bottom: 1px; }
101
+ .widget:hover { padding: 8px; }`,
102
+ rootButton([CSS_PROPERTY_TYPE.paddingTop, CSS_PROPERTY_TYPE.paddingBottom]),
103
+ )
104
+
105
+ expect(editorElement.cssProperties?.paddingTop).toEqual({
106
+ defaultValue: '1px',
107
+ statesDefaultValues: { hover: '8px' },
108
+ })
109
+ })
110
+
111
+ it('maps the custom `active` class trigger and the `:active` pseudo to the same state', () => {
112
+ const editorElement = convert(
113
+ `.widget { color: black; }
114
+ .widget:global(.widget--active), .widget:active { color: white; }`,
115
+ rootButton([CSS_PROPERTY_TYPE.color]),
116
+ )
117
+
118
+ expect(editorElement.states?.active).toEqual({ displayName: 'Active', className: 'widget--active' })
119
+ expect(editorElement.cssProperties?.color).toEqual({
120
+ defaultValue: 'black',
121
+ statesDefaultValues: { active: 'white' },
122
+ })
123
+ })
124
+
125
+ it('neither emits nor leaks a CSS state the element does not expose', () => {
126
+ // A plain <span> has no native states, so its `:hover` rule has nowhere to go.
127
+ const editorElement = convert(
128
+ `.widget__caption { color: black; }
129
+ .widget__caption:hover { color: blue; }`,
130
+ rootButton(
131
+ [],
132
+ [{ name: 'caption', tag: 'span', className: 'widget__caption', relevant: [CSS_PROPERTY_TYPE.color] }],
133
+ ),
134
+ )
135
+
136
+ expect(editorElement.elements?.caption?.inlineElement?.cssProperties?.color).toEqual({ defaultValue: 'black' })
137
+ })
138
+
139
+ it('emits statesDefaultValues on an inner element with its own native hover state', () => {
140
+ const editorElement = convert(
141
+ `.widget__action { color: black; }
142
+ .widget__action:hover { color: blue; }`,
143
+ rootButton(
144
+ [],
145
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.color] }],
146
+ ),
147
+ )
148
+
149
+ const actionElement = editorElement.elements?.action?.inlineElement
150
+ expect(actionElement?.states?.hover).toBeDefined()
151
+ expect(actionElement?.cssProperties?.color).toEqual({
152
+ defaultValue: 'black',
153
+ statesDefaultValues: { hover: 'blue' },
154
+ })
155
+ })
156
+
157
+ it("does not apply an ancestor's state rule to a descendant's regular default", () => {
158
+ const editorElement = convert(
159
+ `.widget__label { color: black; }
160
+ .widget:hover .widget__label { color: blue; }`,
161
+ rootButton([], [{ name: 'label', tag: 'span', className: 'widget__label', relevant: [CSS_PROPERTY_TYPE.color] }]),
162
+ )
163
+
164
+ expect(editorElement.elements?.label?.inlineElement?.cssProperties?.color).toEqual({ defaultValue: 'black' })
165
+ })
166
+
167
+ it('does not let a display declared only under hover change the regular display', () => {
168
+ const editorElement = convert(
169
+ `.widget__icon { display: inline; }
170
+ .widget:hover .widget__icon, .widget__icon:hover { display: none; }`,
171
+ rootButton([], [{ name: 'icon', tag: 'span', className: 'widget__icon', relevant: [CSS_PROPERTY_TYPE.display] }]),
172
+ )
173
+
174
+ expect(editorElement.elements?.icon?.inlineElement?.cssProperties?.display).toEqual({
175
+ display: { displayValues: ['none', 'inline'] },
176
+ })
177
+ })
178
+
179
+ it('does not let a conditional (@media) declaration beat the top-level one', () => {
180
+ const editorElement = convert(
181
+ `.widget { background: red; }
182
+ .widget:hover { background: blue; }
183
+ @media (prefers-reduced-motion: reduce) { .widget { background: gray; } .widget:hover { background: none; } }`,
184
+ rootButton([CSS_PROPERTY_TYPE.background]),
185
+ )
186
+
187
+ expect(editorElement.cssProperties?.background).toEqual({
188
+ defaultValue: 'red',
189
+ statesDefaultValues: { hover: 'blue' },
190
+ })
191
+ })
192
+
193
+ it('adds a state value to a custom property declared under that state on its own element', () => {
194
+ const editorElement = convert(
195
+ `.widget { --accent: red; color: var(--accent); }
196
+ .widget:hover { --accent: blue; }`,
197
+ rootButton([CSS_PROPERTY_TYPE.color]),
198
+ )
199
+
200
+ expect(editorElement.cssCustomProperties?.accent).toMatchObject({
201
+ defaultValue: 'red',
202
+ statesDefaultValues: { hover: 'blue' },
203
+ })
204
+ })
205
+
206
+ it("does not attach a descendant's state declaration to the ancestor a variable is placed on", () => {
207
+ // `--accent` is declared on both elements, so it is placed on their common ancestor (the root);
208
+ // the hover value belongs to the inner button's state, not the root's.
209
+ const editorElement = convert(
210
+ `.widget { --accent: red; }
211
+ .widget__action { --accent: green; }
212
+ .widget__action:hover { --accent: blue; }`,
213
+ rootButton([], [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [] }]),
214
+ )
215
+
216
+ expect(editorElement.cssCustomProperties?.accent).not.toHaveProperty('statesDefaultValues')
217
+ })
218
+
219
+ it('does not let a conditional (@media) longhand beat a top-level shorthand', () => {
220
+ const editorElement = convert(
221
+ `.widget { padding: 0; }
222
+ @media (max-width: 600px) { .widget { padding-top: 10px; } }`,
223
+ rootButton([CSS_PROPERTY_TYPE.paddingTop]),
224
+ )
225
+
226
+ expect(editorElement.cssProperties?.paddingTop).toEqual({ defaultValue: '0' })
227
+ })
228
+
229
+ it('does not let a conditional (@media) custom property beat the top-level one', () => {
230
+ const editorElement = convert(
231
+ `.widget { --accent: red; color: var(--accent); }
232
+ @media (max-width: 600px) { .widget { --accent: blue; } }`,
233
+ rootButton([CSS_PROPERTY_TYPE.color]),
234
+ )
235
+
236
+ expect(editorElement.cssCustomProperties?.accent).toMatchObject({ defaultValue: 'red' })
237
+ })
238
+
239
+ it('emits no control for a variable declared only under a state the element does not expose', () => {
240
+ // A <button> has native hover/disabled states but no `active` state (no `--active` class trigger).
241
+ const editorElement = convert(
242
+ `.widget { color: var(--accent); }
243
+ .widget:active { --accent: blue; }`,
244
+ rootButton([CSS_PROPERTY_TYPE.color]),
245
+ )
246
+
247
+ expect(editorElement.states?.active).toBeUndefined()
248
+ expect(editorElement.cssCustomProperties).not.toHaveProperty('accent')
249
+ })
250
+
251
+ it('does not let a global state modifier nested in :is() leak into the regular default', () => {
252
+ const editorElement = convert(
253
+ `.widget { color: black; }
254
+ .widget:is(:global(.widget--selected)) { color: blue; }`,
255
+ rootButton([CSS_PROPERTY_TYPE.color]),
256
+ )
257
+
258
+ expect(editorElement.cssProperties?.color).toEqual({ defaultValue: 'black' })
259
+ })
260
+
261
+ it('emits a state display value while keeping the display options', () => {
262
+ const editorElement = convert(
263
+ `.widget__action { display: block; }
264
+ .widget__action:global(.widget__action--collapsed) { display: none; }
265
+ .widget__action:global(.widget__action--expanded) { display: flex; }`,
266
+ rootButton(
267
+ [],
268
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.display] }],
269
+ ),
270
+ )
271
+
272
+ const actionElement = editorElement.elements?.action?.inlineElement
273
+ expect(actionElement?.states?.collapsed).toBeDefined()
274
+ expect(actionElement?.cssProperties?.display).toEqual({
275
+ display: { displayValues: ['none', 'block', 'flex'] },
276
+ statesDefaultValues: { collapsed: 'none', expanded: 'flex' },
277
+ })
278
+ })
279
+
280
+ it('resolves a state display that only changes the variable the regular display reads', () => {
281
+ const editorElement = convert(
282
+ `.widget__action { display: var(--layout); --layout: block; }
283
+ .widget__action:hover { --layout: none; }`,
284
+ rootButton(
285
+ [],
286
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.display] }],
287
+ ),
288
+ )
289
+
290
+ expect(editorElement.elements?.action?.inlineElement?.cssProperties?.display).toEqual({
291
+ display: { displayValues: ['none', 'block'] },
292
+ statesDefaultValues: { hover: 'none' },
293
+ })
294
+ })
295
+
296
+ describe('known limitations', () => {
297
+ it('drops a state longhand that cannot be recomposed into a regular shorthand target', () => {
298
+ // `font` is composed from its longhands only when every one is declared in the same layer.
299
+ const editorElement = convert(
300
+ `.widget { font: 400 16px/1.5 Arial; }
301
+ .widget:hover { font-weight: 700; }`,
302
+ rootButton([CSS_PROPERTY_TYPE.font]),
303
+ )
304
+
305
+ expect(editorElement.cssProperties?.font).not.toHaveProperty('statesDefaultValues')
306
+ })
307
+ })
308
+ })