@wix/zero-config-implementation 1.85.0 → 1.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +82644 -30
- package/package.json +28 -4
- package/src/converters/active-item-index-builder.test.ts +149 -0
- package/src/converters/active-item-index-builder.ts +90 -0
- package/src/converters/to-editor-component.test.ts +64 -0
- package/src/converters/to-editor-component.ts +10 -0
- package/src/converters/utils.test.ts +14 -1
- package/src/converters/utils.ts +11 -0
- package/src/extensions/wix-services/service-stub.test.ts +149 -0
- package/src/extensions/wix-services/service-stub.ts +126 -0
- package/src/extensions/wix-services/wrapper.test.ts +63 -0
- package/src/extensions/wix-services/wrapper.ts +204 -0
- package/src/index.ts +2 -6
- package/src/information-extractors/ts/components.test.ts +37 -0
- package/src/information-extractors/ts/components.ts +74 -2
- package/src/information-extractors/ts/types.ts +5 -0
- package/dist/index-CQq7GOf9.js +0 -96
- package/dist/index-DBSnnglR.js +0 -100296
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.87.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",
|
|
@@ -39,8 +39,32 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@wix/
|
|
43
|
-
"@wix/
|
|
42
|
+
"@wix/react-component-schema": "1.8.0",
|
|
43
|
+
"@wix/seo-service": "^1.9.0",
|
|
44
|
+
"@wix/services-manager": "^1.0.7",
|
|
45
|
+
"@wix/services-manager-react": "^1.0.8",
|
|
46
|
+
"@wix/site-service-configuration": "^1.0.1",
|
|
47
|
+
"@wix/site-service-device-info": "^1.0.2",
|
|
48
|
+
"@wix/site-service-editor-context": "^1.0.1",
|
|
49
|
+
"@wix/site-service-experiments": "^1.0.3",
|
|
50
|
+
"@wix/site-service-locale": "^1.0.2",
|
|
51
|
+
"@wix/site-service-pages": "^1.0.2",
|
|
52
|
+
"@wix/site-service-provide-css": "^1.0.1",
|
|
53
|
+
"@wix/site-service-rendering-context": "^1.0.1",
|
|
54
|
+
"@wix/site-service-url": "^1.0.1",
|
|
55
|
+
"@wix/viewer-service-anchors": "^1.0.40",
|
|
56
|
+
"@wix/viewer-service-consent-policy": "^1.0.100",
|
|
57
|
+
"@wix/viewer-service-environment": "^1.0.47",
|
|
58
|
+
"@wix/viewer-service-link-utils": "^1.0.62",
|
|
59
|
+
"@wix/viewer-service-named-signals": "^1.0.16",
|
|
60
|
+
"@wix/viewer-service-pages": "^1.0.9",
|
|
61
|
+
"@wix/viewer-service-provide-css": "^1.0.31",
|
|
62
|
+
"@wix/viewer-service-renderer-configuration": "^1.0.7",
|
|
63
|
+
"@wix/viewer-service-sdk-state": "^1.0.28",
|
|
64
|
+
"@wix/viewer-service-site-scroll-blocker": "^1.0.63",
|
|
65
|
+
"@wix/viewer-service-topology": "^1.0.26",
|
|
66
|
+
"@wix/viewer-service-translations": "^1.0.15",
|
|
67
|
+
"@wix/viewer-service-url": "^1.0.69"
|
|
44
68
|
},
|
|
45
69
|
"devDependencies": {
|
|
46
70
|
"@faker-js/faker": "^10.2.0",
|
|
@@ -82,5 +106,5 @@
|
|
|
82
106
|
]
|
|
83
107
|
}
|
|
84
108
|
},
|
|
85
|
-
"falconPackageHash": "
|
|
109
|
+
"falconPackageHash": "1db79cb96c2d3ad1aa00f8c608385635757b7899b0f11f7a74f228e4"
|
|
86
110
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { DISPLAY_GROUPS } from '@wix/react-component-schema'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import type { CoupledProp } from '../information-extractors/react/types'
|
|
4
|
+
import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
|
|
5
|
+
|
|
6
|
+
function arrayProp(name: string): CoupledProp {
|
|
7
|
+
return { name, type: 'Array<Item>', resolvedType: { kind: 'array' } } as unknown as CoupledProp
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function activeIndexProp(name: string, targetArrayProp: string): CoupledProp {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
type: 'number',
|
|
14
|
+
resolvedType: { kind: 'primitive', value: 'number' },
|
|
15
|
+
activeItemIndexTarget: targetArrayProp,
|
|
16
|
+
} as unknown as CoupledProp
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function numberProp(name: string): CoupledProp {
|
|
20
|
+
return { name, type: 'number', resolvedType: { kind: 'primitive', value: 'number' } } as unknown as CoupledProp
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('buildActiveItemDisplayGroups', () => {
|
|
28
|
+
it('emits displayGroups when ActiveItemIndex pairs with an array prop', () => {
|
|
29
|
+
const props = {
|
|
30
|
+
tabs: arrayProp('tabs'),
|
|
31
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(buildActiveItemDisplayGroups(props)).toEqual({
|
|
35
|
+
tabs: {
|
|
36
|
+
displayName: 'Tab',
|
|
37
|
+
groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
|
|
38
|
+
arrayItems: {
|
|
39
|
+
arrayItems: 'tabs',
|
|
40
|
+
selectedItemIndex: 'activeTab',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('keeps only the first ActiveItemIndex link when multiple pairs exist', () => {
|
|
47
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
48
|
+
const props = {
|
|
49
|
+
tabs: arrayProp('tabs'),
|
|
50
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
51
|
+
slides: arrayProp('slides'),
|
|
52
|
+
activeSlide: activeIndexProp('activeSlide', 'slides'),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
expect(buildActiveItemDisplayGroups(props)).toEqual({
|
|
56
|
+
tabs: {
|
|
57
|
+
displayName: 'Tab',
|
|
58
|
+
groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
|
|
59
|
+
arrayItems: {
|
|
60
|
+
arrayItems: 'tabs',
|
|
61
|
+
selectedItemIndex: 'activeTab',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
66
|
+
'Multiple ActiveItemIndex<> props found; only "activeTab" -> "tabs" is used. Dropping "activeSlide" -> "slides".',
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps the first link and drops later indexes targeting the same array', () => {
|
|
71
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
72
|
+
const props = {
|
|
73
|
+
tabs: arrayProp('tabs'),
|
|
74
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
75
|
+
selectedTab: activeIndexProp('selectedTab', 'tabs'),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
expect(buildActiveItemDisplayGroups(props)).toEqual({
|
|
79
|
+
tabs: {
|
|
80
|
+
displayName: 'Tab',
|
|
81
|
+
groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
|
|
82
|
+
arrayItems: {
|
|
83
|
+
arrayItems: 'tabs',
|
|
84
|
+
selectedItemIndex: 'activeTab',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
89
|
+
'ActiveItemIndex<> link "selectedTab" -> "tabs" conflicts with an existing link — dropping.',
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns undefined when no ActiveItemIndex props exist', () => {
|
|
94
|
+
const props = {
|
|
95
|
+
tabs: arrayProp('tabs'),
|
|
96
|
+
count: numberProp('count'),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns undefined when the target array prop does not exist', () => {
|
|
103
|
+
const props = {
|
|
104
|
+
activeTab: activeIndexProp('activeTab', 'nonexistent'),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns undefined when the target prop is not an array type', () => {
|
|
111
|
+
const props = {
|
|
112
|
+
tabs: numberProp('tabs'),
|
|
113
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('buildActiveItemIndexDisplayFilters', () => {
|
|
121
|
+
it('hides active index props from Settings data panel', () => {
|
|
122
|
+
const props = {
|
|
123
|
+
tabs: arrayProp('tabs'),
|
|
124
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expect(buildActiveItemIndexDisplayFilters(props)).toEqual({
|
|
128
|
+
data: { hide: ['activeTab'] },
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns undefined when no ActiveItemIndex props exist', () => {
|
|
133
|
+
const props = {
|
|
134
|
+
tabs: arrayProp('tabs'),
|
|
135
|
+
count: numberProp('count'),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expect(buildActiveItemIndexDisplayFilters(props)).toBeUndefined()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('returns undefined when the target prop is not an array type', () => {
|
|
142
|
+
const props = {
|
|
143
|
+
tabs: numberProp('tabs'),
|
|
144
|
+
activeTab: activeIndexProp('activeTab', 'tabs'),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(buildActiveItemIndexDisplayFilters(props)).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest emission for the `ActiveItemIndex<'<arrayProp>'>` marker type.
|
|
3
|
+
*
|
|
4
|
+
* The TS extractor sets `activeItemIndexTarget` on index props. This module is the
|
|
5
|
+
* single place that interprets that marker for the generated manifest:
|
|
6
|
+
*
|
|
7
|
+
* - `buildActiveItemDisplayGroups` → `displayGroups.<arrayProp>` (`arrayItems` hat)
|
|
8
|
+
* - `buildActiveItemIndexDisplayFilters` → `displayFilters.data.hide` (hide raw index)
|
|
9
|
+
*
|
|
10
|
+
* `displayGroups` and `displayFilters` stay separate exports because they are
|
|
11
|
+
* orthogonal manifest fields — they only share the same trigger here.
|
|
12
|
+
*
|
|
13
|
+
* Default value `0` for unset index props is applied in `buildData` inside
|
|
14
|
+
* `to-editor-component`, not here.
|
|
15
|
+
*
|
|
16
|
+
* Each index prop and array prop may participate in at most one link. Conflicts
|
|
17
|
+
* are logged and dropped; only the first valid link is emitted (one hat per component).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { DisplayFilters, DisplayGroupItem } from '@wix/react-component-schema'
|
|
21
|
+
import { DISPLAY_GROUPS } from '@wix/react-component-schema'
|
|
22
|
+
import type { CoupledProp } from '../information-extractors/react'
|
|
23
|
+
import { formatDisplayName, singularize } from './utils'
|
|
24
|
+
|
|
25
|
+
interface ActiveItemIndexLink {
|
|
26
|
+
indexPropName: string
|
|
27
|
+
arrayPropName: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildActiveItemDisplayGroups(
|
|
31
|
+
props: Record<string, CoupledProp>,
|
|
32
|
+
): Record<string, DisplayGroupItem> | undefined {
|
|
33
|
+
const groups: Record<string, DisplayGroupItem> = {}
|
|
34
|
+
const activeItemIndexLinks = collectValidActiveItemIndexLinks(props)
|
|
35
|
+
|
|
36
|
+
for (const { indexPropName, arrayPropName } of activeItemIndexLinks) {
|
|
37
|
+
groups[arrayPropName] = {
|
|
38
|
+
displayName: formatDisplayName(singularize(arrayPropName)),
|
|
39
|
+
groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
|
|
40
|
+
arrayItems: {
|
|
41
|
+
arrayItems: arrayPropName,
|
|
42
|
+
selectedItemIndex: indexPropName,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Object.keys(groups).length > 0 ? groups : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildActiveItemIndexDisplayFilters(props: Record<string, CoupledProp>): DisplayFilters | undefined {
|
|
51
|
+
const hiddenIndexProps = collectValidActiveItemIndexLinks(props).map((link) => link.indexPropName)
|
|
52
|
+
return hiddenIndexProps.length > 0 ? { data: { hide: hiddenIndexProps } } : undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collectValidActiveItemIndexLinks(props: Record<string, CoupledProp>): ActiveItemIndexLink[] {
|
|
56
|
+
const links: ActiveItemIndexLink[] = []
|
|
57
|
+
const registeredIndexProps = new Set<string>()
|
|
58
|
+
const registeredArrayProps = new Set<string>()
|
|
59
|
+
|
|
60
|
+
for (const [indexPropName, prop] of Object.entries(props)) {
|
|
61
|
+
const arrayPropName = prop.activeItemIndexTarget
|
|
62
|
+
if (!arrayPropName) continue
|
|
63
|
+
|
|
64
|
+
const arrayProp = props[arrayPropName]
|
|
65
|
+
if (!arrayProp || arrayProp.resolvedType.kind !== 'array') continue
|
|
66
|
+
|
|
67
|
+
const conflictsWithRegisteredLink =
|
|
68
|
+
registeredIndexProps.has(indexPropName) || registeredArrayProps.has(arrayPropName)
|
|
69
|
+
if (conflictsWithRegisteredLink) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`ActiveItemIndex<> link "${indexPropName}" -> "${arrayPropName}" conflicts with an existing link — dropping.`,
|
|
72
|
+
)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hasRegisteredLink = links.length > 0
|
|
77
|
+
if (hasRegisteredLink) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Multiple ActiveItemIndex<> props found; only "${links[0].indexPropName}" -> "${links[0].arrayPropName}" is used. Dropping "${indexPropName}" -> "${arrayPropName}".`,
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
links.push({ indexPropName, arrayPropName })
|
|
85
|
+
registeredIndexProps.add(indexPropName)
|
|
86
|
+
registeredArrayProps.add(arrayPropName)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return links
|
|
90
|
+
}
|
|
@@ -318,3 +318,67 @@ describe('toEditorReactComponent — @property design tokens', () => {
|
|
|
318
318
|
expect(editorElement?.cssProperties?.['--sdf-safelight-color']).toBeUndefined()
|
|
319
319
|
})
|
|
320
320
|
})
|
|
321
|
+
|
|
322
|
+
describe('toEditorReactComponent — ActiveItemIndex display groups', () => {
|
|
323
|
+
it('emits displayGroups and hides the active index from Settings', () => {
|
|
324
|
+
const stringType = { kind: 'primitive' as const, value: 'string' as const }
|
|
325
|
+
const tabItemType = {
|
|
326
|
+
kind: 'object' as const,
|
|
327
|
+
properties: {
|
|
328
|
+
name: { name: 'name', type: 'string', required: false, resolvedType: stringType },
|
|
329
|
+
body: { name: 'body', type: 'string', required: false, resolvedType: stringType },
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const component: ComponentInfoWithCss = {
|
|
334
|
+
componentName: 'SimpleTabs',
|
|
335
|
+
props: {
|
|
336
|
+
tabs: {
|
|
337
|
+
name: 'tabs',
|
|
338
|
+
type: 'Array<TabItem>',
|
|
339
|
+
required: false,
|
|
340
|
+
resolvedType: { kind: 'array', elementType: tabItemType },
|
|
341
|
+
propPath: 'props.tabs',
|
|
342
|
+
logicOnly: false,
|
|
343
|
+
},
|
|
344
|
+
activeTab: {
|
|
345
|
+
name: 'activeTab',
|
|
346
|
+
type: 'number',
|
|
347
|
+
required: false,
|
|
348
|
+
resolvedType: { kind: 'primitive', value: 'number' },
|
|
349
|
+
activeItemIndexTarget: 'tabs',
|
|
350
|
+
propPath: 'props.activeTab',
|
|
351
|
+
logicOnly: false,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
elements: [
|
|
355
|
+
createSemanticElement({
|
|
356
|
+
traceId: 'trace-root',
|
|
357
|
+
name: 'root',
|
|
358
|
+
className: 'simple-tabs',
|
|
359
|
+
}),
|
|
360
|
+
],
|
|
361
|
+
propUsages: new Map(),
|
|
362
|
+
css: [],
|
|
363
|
+
varUsedByTraceId: new Map(),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const editorElement = toEditorReactComponent(component).editorElement
|
|
367
|
+
|
|
368
|
+
expect(editorElement?.displayGroups).toEqual({
|
|
369
|
+
tabs: {
|
|
370
|
+
displayName: 'Tab',
|
|
371
|
+
groupType: 'arrayItems',
|
|
372
|
+
arrayItems: {
|
|
373
|
+
arrayItems: 'tabs',
|
|
374
|
+
selectedItemIndex: 'activeTab',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
expect(editorElement?.displayFilters).toEqual({
|
|
379
|
+
data: { hide: ['activeTab'] },
|
|
380
|
+
})
|
|
381
|
+
expect(editorElement?.data?.activeTab?.defaultValue).toBe(0)
|
|
382
|
+
expect(editorElement?.data?.activeTab?.dataType).toBe('number')
|
|
383
|
+
})
|
|
384
|
+
})
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
} from '../information-extractors/react'
|
|
26
26
|
import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
|
|
27
27
|
import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
|
|
28
|
+
import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
|
|
28
29
|
import { resolveLonghand } from './css-longhand-resolver'
|
|
29
30
|
import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
|
|
30
31
|
import { buildDataItem } from './data-item-builder'
|
|
@@ -94,6 +95,9 @@ function buildEditorElement(
|
|
|
94
95
|
)
|
|
95
96
|
: undefined
|
|
96
97
|
|
|
98
|
+
const displayGroups = buildActiveItemDisplayGroups(component.props)
|
|
99
|
+
const displayFilters = buildActiveItemIndexDisplayFilters(component.props)
|
|
100
|
+
|
|
97
101
|
return {
|
|
98
102
|
selector: buildSelector(rootElement),
|
|
99
103
|
displayName: formatDisplayName(component.componentName),
|
|
@@ -109,6 +113,8 @@ function buildEditorElement(
|
|
|
109
113
|
cssProperties: buildCssProperties(rootElement),
|
|
110
114
|
cssCustomProperties: rootCustomProps,
|
|
111
115
|
...(rootStates && { states: rootStates }),
|
|
116
|
+
...(displayGroups && { displayGroups }),
|
|
117
|
+
...(displayFilters && { displayFilters }),
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -157,6 +163,10 @@ function buildData(
|
|
|
157
163
|
|
|
158
164
|
const result = buildDataItem(prop, defaultValue, propUsages, prop.propPath)
|
|
159
165
|
if (result.isErr()) throw result.error
|
|
166
|
+
// ActiveItemIndex props default to 0 when no default is specified
|
|
167
|
+
if (prop.activeItemIndexTarget && result.value.defaultValue === undefined) {
|
|
168
|
+
result.value.defaultValue = 0
|
|
169
|
+
}
|
|
160
170
|
data[name] = result.value
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { formatDisplayName } from './utils'
|
|
2
|
+
import { formatDisplayName, singularize } from './utils'
|
|
3
3
|
|
|
4
4
|
describe('formatDisplayName', () => {
|
|
5
5
|
it('maps a11y to Accessibility', () => {
|
|
@@ -47,3 +47,16 @@ describe('formatDisplayName', () => {
|
|
|
47
47
|
expect(formatDisplayName(longSingleWord).length).toBe(50)
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
|
+
|
|
51
|
+
describe('singularize', () => {
|
|
52
|
+
it.each([
|
|
53
|
+
['slides', 'slide'],
|
|
54
|
+
['entries', 'entry'],
|
|
55
|
+
['boxes', 'box'],
|
|
56
|
+
['tabs', 'tab'],
|
|
57
|
+
['glass', 'glass'],
|
|
58
|
+
['item', 'item'],
|
|
59
|
+
])('singularizes "%s" to "%s"', (input, expected) => {
|
|
60
|
+
expect(singularize(input)).toBe(expected)
|
|
61
|
+
})
|
|
62
|
+
})
|
package/src/converters/utils.ts
CHANGED
|
@@ -41,3 +41,14 @@ export function formatDisplayName(input: string): string {
|
|
|
41
41
|
const result = words.join(' ')
|
|
42
42
|
return result.slice(0, DISPLAY_NAME_MAX_LENGTH)
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Naive singularization for display names.
|
|
47
|
+
* Falls back to the original if no rule matches.
|
|
48
|
+
*/
|
|
49
|
+
export function singularize(name: string): string {
|
|
50
|
+
if (name.endsWith('ies')) return `${name.slice(0, -3)}y`
|
|
51
|
+
if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) return name.slice(0, -2)
|
|
52
|
+
if (name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1)
|
|
53
|
+
return name
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createServicesManager, createServicesMap } from '@wix/services-manager'
|
|
2
|
+
import type { ServiceDefinition } from '@wix/services-manager/types'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { createAutoStubFallbackManager } from './service-stub'
|
|
7
|
+
|
|
8
|
+
// Definitions are plain strings at runtime, so a cast string is a faithful stand-in.
|
|
9
|
+
function defineTestService(serviceId: string): ServiceDefinition<never, never> {
|
|
10
|
+
return serviceId as unknown as ServiceDefinition<never, never>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const UnregisteredDefinition = defineTestService('@wix/viewer-service-topology')
|
|
14
|
+
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: stubs are intentionally untyped at the call site
|
|
16
|
+
function stubFor(serviceId: string): any {
|
|
17
|
+
return createAutoStubFallbackManager().getService(defineTestService(serviceId))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('createAutoStubFallbackManager', () => {
|
|
21
|
+
it('returns a referentially stable stub across repeated lookups', () => {
|
|
22
|
+
const fallbackManager = createAutoStubFallbackManager()
|
|
23
|
+
|
|
24
|
+
expect(fallbackManager.getService(UnregisteredDefinition)).toBe(fallbackManager.getService(UnregisteredDefinition))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('notifies once per distinct service id', () => {
|
|
28
|
+
const onServiceStubbed = vi.fn()
|
|
29
|
+
const fallbackManager = createAutoStubFallbackManager({ onServiceStubbed })
|
|
30
|
+
|
|
31
|
+
fallbackManager.getService(UnregisteredDefinition)
|
|
32
|
+
fallbackManager.getService(UnregisteredDefinition)
|
|
33
|
+
fallbackManager.getService(defineTestService('@wix/seo-service'))
|
|
34
|
+
|
|
35
|
+
expect(onServiceStubbed.mock.calls).toEqual([['@wix/viewer-service-topology'], ['@wix/seo-service']])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('service stub value semantics', () => {
|
|
40
|
+
it('resolves nested reads and curried calls to memoized stubs', () => {
|
|
41
|
+
const pagesStub = stubFor('@wix/site-service-pages')
|
|
42
|
+
expect(pagesStub.currentPage.title).toBe(pagesStub.currentPage.title)
|
|
43
|
+
|
|
44
|
+
const translationsStub = stubFor('@wix/viewer-service-translations')
|
|
45
|
+
expect(translationsStub.translate('app')('greeting')).toBe(translationsStub.translate('app')('greeting'))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('keeps well-known symbol handlers referentially stable', () => {
|
|
49
|
+
const stub = stubFor('@wix/viewer-service-topology')
|
|
50
|
+
|
|
51
|
+
expect(stub.toString).toBe(stub.toString)
|
|
52
|
+
expect(stub[Symbol.iterator]).toBe(stub[Symbol.iterator])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('agrees between `in` and reads for symbols', () => {
|
|
56
|
+
const stub = stubFor('@wix/viewer-service-topology')
|
|
57
|
+
const unhandledSymbol = Symbol.for('some-library-symbol')
|
|
58
|
+
|
|
59
|
+
// Reporting a symbol as present while reading undefined would break
|
|
60
|
+
// `if (sym in service) service[sym]()`.
|
|
61
|
+
expect(unhandledSymbol in stub).toBe(false)
|
|
62
|
+
expect(stub[unhandledSymbol]).toBeUndefined()
|
|
63
|
+
|
|
64
|
+
expect(Symbol.iterator in stub).toBe(true)
|
|
65
|
+
expect(stub[Symbol.iterator]).toBeDefined()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('is not thenable, so awaiting it cannot hang', async () => {
|
|
69
|
+
const stub = stubFor('@wix/seo-service')
|
|
70
|
+
|
|
71
|
+
expect(stub.then).toBeUndefined()
|
|
72
|
+
expect('then' in stub).toBe(false)
|
|
73
|
+
await expect(Promise.resolve(stub)).resolves.toBe(stub)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('terminates on spread and key enumeration instead of recursing', () => {
|
|
77
|
+
const stub = stubFor('@wix/viewer-service-sdk-state')
|
|
78
|
+
|
|
79
|
+
expect(Object.keys(stub)).toEqual([])
|
|
80
|
+
expect({ ...stub }).toEqual({})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('reads as an empty string and iterates as empty', () => {
|
|
84
|
+
const stub = stubFor('@wix/viewer-service-topology')
|
|
85
|
+
|
|
86
|
+
expect(`${stub}`).toBe('')
|
|
87
|
+
expect([...stub]).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('does not crash React when handed to JSX as a child', () => {
|
|
91
|
+
// Unconverted, so React sees the Proxy itself — and throws on plain-object children.
|
|
92
|
+
const reactWarning = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
93
|
+
const stub = stubFor('@wix/viewer-service-topology')
|
|
94
|
+
|
|
95
|
+
expect(() => renderToStaticMarkup(React.createElement('span', null, stub))).not.toThrow()
|
|
96
|
+
|
|
97
|
+
reactWarning.mockRestore()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('used as a services-manager fallback', () => {
|
|
102
|
+
const RegisteredDefinition = defineTestService('@wix/registered-test-service')
|
|
103
|
+
|
|
104
|
+
function createManagerWithFallback(onServiceStubbed?: (serviceId: string) => void) {
|
|
105
|
+
return createServicesManager(
|
|
106
|
+
createServicesMap([
|
|
107
|
+
{
|
|
108
|
+
definition: RegisteredDefinition,
|
|
109
|
+
impl: (() => ({ realValue: 'from-real-service' })) as never,
|
|
110
|
+
config: {} as never,
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
createAutoStubFallbackManager({ onServiceStubbed }),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it('keeps extended bindings real and still stubs the rest', () => {
|
|
118
|
+
const ExtendedDefinition = defineTestService('@wix/extended-test-service')
|
|
119
|
+
const extended = createAutoStubFallbackManager().extend(
|
|
120
|
+
createServicesMap([
|
|
121
|
+
{
|
|
122
|
+
definition: ExtendedDefinition,
|
|
123
|
+
impl: (() => ({ realValue: 'from-extension' })) as never,
|
|
124
|
+
config: {} as never,
|
|
125
|
+
},
|
|
126
|
+
]),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(extended.getService(ExtendedDefinition)).toEqual({ realValue: 'from-extension' })
|
|
130
|
+
expect(() => extended.getService(UnregisteredDefinition)).not.toThrow()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('leaves registered services untouched', () => {
|
|
134
|
+
const servicesManager = createManagerWithFallback()
|
|
135
|
+
|
|
136
|
+
expect(servicesManager.getService(RegisteredDefinition)).toEqual({ realValue: 'from-real-service' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('substitutes a stub where an unregistered service would otherwise throw', () => {
|
|
140
|
+
const onServiceStubbed = vi.fn()
|
|
141
|
+
|
|
142
|
+
expect(() => createServicesManager(createServicesMap([])).getService(UnregisteredDefinition)).toThrow(
|
|
143
|
+
/is not provided/,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
expect(() => createManagerWithFallback(onServiceStubbed).getService(UnregisteredDefinition)).not.toThrow()
|
|
147
|
+
expect(onServiceStubbed).toHaveBeenCalledWith('@wix/viewer-service-topology')
|
|
148
|
+
})
|
|
149
|
+
})
|