@faststore/core 4.9.3-dev.2 → 4.10.0-dev.1
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/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +166 -135
- package/@generated/gql.ts +3 -3
- package/@generated/graphql.ts +12 -3
- package/@generated/persisted-documents.json +1 -1
- package/@generated/schema.graphql +21 -5
- package/CHANGELOG.md +22 -0
- package/cms/faststore/components/cms_component__bannernewsletter.jsonc +1 -1
- package/cms/faststore/components/cms_component__newsletter.jsonc +1 -1
- package/index.ts +3 -0
- package/package.json +6 -6
- package/src/components/region/RegionSlider/RegionSlider.tsx +1 -0
- package/src/components/ui/RichText/RichText.tsx +2 -0
- package/src/components/ui/RichText/RichTextImageNode.ts +163 -0
- package/src/sdk/deliveryPromise/provider.tsx +138 -9
- package/src/sdk/deliveryPromise/queries.ts +19 -2
- package/src/sdk/deliveryPromise/reducer.ts +7 -1
- package/test/components/ui/RichText/RichText.browser.test.tsx +321 -0
- package/test/sdk/deliveryPromise/provider.effect.test.tsx +440 -0
- package/test/sdk/deliveryPromise/provider.test.ts +99 -0
- package/test/sdk/deliveryPromise/queries.test.ts +123 -0
- package/test/sdk/deliveryPromise/reducer.test.ts +46 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const isContentPlatformSourceMock = vi.hoisted(() => vi.fn(() => true))
|
|
9
|
+
|
|
10
|
+
vi.mock('src/server/content/utils', () => ({
|
|
11
|
+
isContentPlatformSource: isContentPlatformSourceMock,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
import { RichText } from '../../../../src/components/ui/RichText/RichText'
|
|
15
|
+
|
|
16
|
+
function textNode(text: string) {
|
|
17
|
+
return {
|
|
18
|
+
detail: 0,
|
|
19
|
+
format: 0,
|
|
20
|
+
mode: 'normal',
|
|
21
|
+
style: '',
|
|
22
|
+
text,
|
|
23
|
+
type: 'text',
|
|
24
|
+
version: 1,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function paragraphNode(children: unknown[]) {
|
|
29
|
+
return {
|
|
30
|
+
children,
|
|
31
|
+
direction: 'ltr',
|
|
32
|
+
format: '',
|
|
33
|
+
indent: 0,
|
|
34
|
+
type: 'paragraph',
|
|
35
|
+
version: 1,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function headingNode(text: string, tag = 'h2') {
|
|
40
|
+
return {
|
|
41
|
+
children: [textNode(text)],
|
|
42
|
+
direction: 'ltr',
|
|
43
|
+
format: '',
|
|
44
|
+
indent: 0,
|
|
45
|
+
type: 'heading',
|
|
46
|
+
version: 1,
|
|
47
|
+
tag,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function linkNode(url: string, text: string) {
|
|
52
|
+
return {
|
|
53
|
+
children: [textNode(text)],
|
|
54
|
+
direction: 'ltr',
|
|
55
|
+
format: '',
|
|
56
|
+
indent: 0,
|
|
57
|
+
type: 'link',
|
|
58
|
+
version: 1,
|
|
59
|
+
rel: null,
|
|
60
|
+
target: null,
|
|
61
|
+
title: null,
|
|
62
|
+
url,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function listNode(itemText: string) {
|
|
67
|
+
return {
|
|
68
|
+
children: [
|
|
69
|
+
{
|
|
70
|
+
children: [textNode(itemText)],
|
|
71
|
+
direction: 'ltr',
|
|
72
|
+
format: '',
|
|
73
|
+
indent: 0,
|
|
74
|
+
type: 'listitem',
|
|
75
|
+
version: 1,
|
|
76
|
+
value: 1,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
direction: 'ltr',
|
|
80
|
+
format: '',
|
|
81
|
+
indent: 0,
|
|
82
|
+
type: 'list',
|
|
83
|
+
version: 1,
|
|
84
|
+
listType: 'bullet',
|
|
85
|
+
start: 1,
|
|
86
|
+
tag: 'ul',
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function imageNode(overrides: Record<string, unknown> = {}) {
|
|
91
|
+
return {
|
|
92
|
+
type: 'image',
|
|
93
|
+
version: 1,
|
|
94
|
+
src: 'https://storeaccount.vtexassets.com/assets/vtex.file-manager-graphql/images/abc123.png',
|
|
95
|
+
altText: 'A nice picture',
|
|
96
|
+
alignment: 'center',
|
|
97
|
+
widthPercent: 60,
|
|
98
|
+
...overrides,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function lexicalContent(children: unknown[]) {
|
|
103
|
+
return JSON.stringify({
|
|
104
|
+
root: {
|
|
105
|
+
children,
|
|
106
|
+
direction: 'ltr',
|
|
107
|
+
format: '',
|
|
108
|
+
indent: 0,
|
|
109
|
+
type: 'root',
|
|
110
|
+
version: 1,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function draftJsContent(text: string) {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
blocks: [
|
|
118
|
+
{
|
|
119
|
+
key: '1',
|
|
120
|
+
text,
|
|
121
|
+
type: 'unstyled',
|
|
122
|
+
depth: 0,
|
|
123
|
+
inlineStyleRanges: [],
|
|
124
|
+
entityRanges: [],
|
|
125
|
+
data: {},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
entityMap: {},
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe('RichText', () => {
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
isContentPlatformSourceMock.mockReturnValue(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('renders an image node with the correct src, alt, alignment and width', () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<RichText content={lexicalContent([imageNode()])} />
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const wrapper = container.querySelector('[data-fs-rich-text-image-wrapper]')
|
|
143
|
+
const img = container.querySelector<HTMLImageElement>(
|
|
144
|
+
'[data-fs-rich-text-image]'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
expect(wrapper?.getAttribute('data-fs-rich-text-image-alignment')).toBe(
|
|
148
|
+
'center'
|
|
149
|
+
)
|
|
150
|
+
expect(img?.getAttribute('alt')).toBe('A nice picture')
|
|
151
|
+
expect(img?.getAttribute('loading')).toBe('lazy')
|
|
152
|
+
expect(img?.getAttribute('style')).toBe('width: 60%;')
|
|
153
|
+
|
|
154
|
+
const src = img?.getAttribute('src') ?? ''
|
|
155
|
+
expect(src).toContain('width=432')
|
|
156
|
+
expect(src).toContain('aspect=true')
|
|
157
|
+
expect(src).toContain('quality=8')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('renders images together with text, headings, links and lists in document order', () => {
|
|
161
|
+
const { container } = render(
|
|
162
|
+
<RichText
|
|
163
|
+
content={lexicalContent([
|
|
164
|
+
headingNode('Title'),
|
|
165
|
+
paragraphNode([
|
|
166
|
+
textNode('See '),
|
|
167
|
+
linkNode('https://example.com', 'this link'),
|
|
168
|
+
]),
|
|
169
|
+
listNode('An item'),
|
|
170
|
+
imageNode(),
|
|
171
|
+
])}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const html = container.innerHTML
|
|
176
|
+
|
|
177
|
+
expect(container.querySelector('h2')?.textContent).toBe('Title')
|
|
178
|
+
expect(
|
|
179
|
+
container.querySelector('a[href="https://example.com"]')
|
|
180
|
+
).toBeTruthy()
|
|
181
|
+
expect(
|
|
182
|
+
container.querySelector('[data-fs-rich-text-list="true"]')
|
|
183
|
+
).toBeTruthy()
|
|
184
|
+
expect(container.querySelector('[data-fs-rich-text-image]')).toBeTruthy()
|
|
185
|
+
|
|
186
|
+
const headingIndex = html.indexOf('<h2')
|
|
187
|
+
const linkIndex = html.indexOf('href="https://example.com"')
|
|
188
|
+
const listIndex = html.indexOf('data-fs-rich-text-list')
|
|
189
|
+
const imageIndex = html.indexOf('data-fs-rich-text-image-wrapper')
|
|
190
|
+
|
|
191
|
+
expect(headingIndex).toBeLessThan(linkIndex)
|
|
192
|
+
expect(linkIndex).toBeLessThan(listIndex)
|
|
193
|
+
expect(listIndex).toBeLessThan(imageIndex)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('drops an image node with a missing src without breaking the rest of the content', () => {
|
|
197
|
+
const { container } = render(
|
|
198
|
+
<RichText
|
|
199
|
+
content={lexicalContent([
|
|
200
|
+
paragraphNode([textNode('Before')]),
|
|
201
|
+
imageNode({ src: '' }),
|
|
202
|
+
paragraphNode([textNode('After')]),
|
|
203
|
+
])}
|
|
204
|
+
/>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(container.querySelector('[data-fs-rich-text-image]')).toBeNull()
|
|
208
|
+
expect(
|
|
209
|
+
container.querySelector('[data-fs-rich-text-image-wrapper]')
|
|
210
|
+
).toBeNull()
|
|
211
|
+
expect(container.textContent).toContain('Before')
|
|
212
|
+
expect(container.textContent).toContain('After')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('does not throw and skips an image node with an unsupported/unexpected shape', () => {
|
|
216
|
+
expect(() =>
|
|
217
|
+
render(
|
|
218
|
+
<RichText
|
|
219
|
+
content={lexicalContent([
|
|
220
|
+
paragraphNode([textNode('Still here')]),
|
|
221
|
+
{
|
|
222
|
+
type: 'image',
|
|
223
|
+
version: 2,
|
|
224
|
+
imageUrl: 'https://storeaccount.vtexassets.com/assets/x.png',
|
|
225
|
+
},
|
|
226
|
+
])}
|
|
227
|
+
/>
|
|
228
|
+
)
|
|
229
|
+
).not.toThrow()
|
|
230
|
+
|
|
231
|
+
const { container } = render(
|
|
232
|
+
<RichText
|
|
233
|
+
content={lexicalContent([
|
|
234
|
+
paragraphNode([textNode('Still here')]),
|
|
235
|
+
{
|
|
236
|
+
type: 'image',
|
|
237
|
+
version: 2,
|
|
238
|
+
imageUrl: 'https://storeaccount.vtexassets.com/assets/x.png',
|
|
239
|
+
},
|
|
240
|
+
])}
|
|
241
|
+
/>
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
expect(container.querySelector('[data-fs-rich-text-image]')).toBeNull()
|
|
245
|
+
expect(container.textContent).toContain('Still here')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('renders multiple images, defaulting alignment/width when omitted and passing through non-VTEX URLs unmodified', () => {
|
|
249
|
+
const { container } = render(
|
|
250
|
+
<RichText
|
|
251
|
+
content={lexicalContent([
|
|
252
|
+
imageNode({ alignment: 'right', widthPercent: 30 }),
|
|
253
|
+
imageNode({
|
|
254
|
+
src: 'https://storeaccount.vtexassets.com/assets/vtex.file-manager-graphql/images/no-layout.png',
|
|
255
|
+
alignment: undefined,
|
|
256
|
+
widthPercent: undefined,
|
|
257
|
+
}),
|
|
258
|
+
imageNode({
|
|
259
|
+
src: 'https://cdn.example.com/external.png',
|
|
260
|
+
alignment: 'center',
|
|
261
|
+
widthPercent: 80,
|
|
262
|
+
}),
|
|
263
|
+
])}
|
|
264
|
+
/>
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const wrappers = container.querySelectorAll(
|
|
268
|
+
'[data-fs-rich-text-image-wrapper]'
|
|
269
|
+
)
|
|
270
|
+
expect(wrappers).toHaveLength(3)
|
|
271
|
+
|
|
272
|
+
expect(wrappers[0].getAttribute('data-fs-rich-text-image-alignment')).toBe(
|
|
273
|
+
'right'
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
expect(wrappers[1].getAttribute('data-fs-rich-text-image-alignment')).toBe(
|
|
277
|
+
'left'
|
|
278
|
+
)
|
|
279
|
+
expect(
|
|
280
|
+
wrappers[1]
|
|
281
|
+
.querySelector('[data-fs-rich-text-image]')
|
|
282
|
+
?.getAttribute('style')
|
|
283
|
+
).toBe('width: 100%;')
|
|
284
|
+
|
|
285
|
+
const externalImg = wrappers[2].querySelector('[data-fs-rich-text-image]')
|
|
286
|
+
expect(externalImg?.getAttribute('src')).toBe(
|
|
287
|
+
'https://cdn.example.com/external.png'
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('clamps widthPercent to 100 so the image never overflows its container', () => {
|
|
292
|
+
const { container } = render(
|
|
293
|
+
<RichText content={lexicalContent([imageNode({ widthPercent: 150 })])} />
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const img = container.querySelector('[data-fs-rich-text-image]')
|
|
297
|
+
expect(img?.getAttribute('style')).toBe('width: 100%;')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('keeps rendering existing hCMS Draft.js content unaffected', () => {
|
|
301
|
+
isContentPlatformSourceMock.mockReturnValue(false)
|
|
302
|
+
|
|
303
|
+
const { container } = render(
|
|
304
|
+
<RichText content={draftJsContent('Hello legacy content')} />
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
expect(container.textContent).toContain('Hello legacy content')
|
|
308
|
+
expect(container.querySelector('[data-fs-rich-text-image]')).toBeNull()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('keeps rendering existing Lexical content without images unaffected', () => {
|
|
312
|
+
const { container } = render(
|
|
313
|
+
<RichText
|
|
314
|
+
content={lexicalContent([paragraphNode([textNode('No images here')])])}
|
|
315
|
+
/>
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
expect(container.textContent).toContain('No images here')
|
|
319
|
+
expect(container.querySelector('[data-fs-rich-text-image]')).toBeNull()
|
|
320
|
+
})
|
|
321
|
+
})
|