@faststore/core 4.6.0-dev.0 → 4.6.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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { render, screen } from '@testing-library/react'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ vi.mock('src/components/skeletons/ProductCardSkeleton', () => ({
9
+ __esModule: true,
10
+ default: () => <div data-testid="product-card-skeleton" />,
11
+ }))
12
+
13
+ import ProductGridSkeleton from '../../../src/components/skeletons/ProductGridSkeleton/ProductGridSkeleton'
14
+
15
+ describe('ProductGridSkeleton', () => {
16
+ it('renders the requested number of placeholder cards', () => {
17
+ render(<ProductGridSkeleton loading count={4} />)
18
+
19
+ expect(screen.getAllByTestId('product-card-skeleton')).toHaveLength(4)
20
+ })
21
+
22
+ it('renders children when not loading', () => {
23
+ render(
24
+ <ProductGridSkeleton loading={false}>
25
+ <div data-testid="grid-content">products</div>
26
+ </ProductGridSkeleton>
27
+ )
28
+
29
+ expect(screen.getByTestId('grid-content')).toBeTruthy()
30
+ expect(screen.queryByTestId('product-card-skeleton')).toBeNull()
31
+ })
32
+ })
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import React from 'react'
6
+ import { act, render, waitFor } from '@testing-library/react'
7
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
8
+
9
+ const replace = vi.fn()
10
+ const resetInfiniteScroll = vi.fn()
11
+ const useProductGalleryQuery = vi.fn()
12
+
13
+ vi.mock('next/router', () => ({
14
+ useRouter: () => ({ replace }),
15
+ }))
16
+
17
+ vi.mock('@faststore/sdk', () => ({
18
+ useSearch: () => ({
19
+ state: { term: 'refil', sort: 'score_desc', selectedFacets: [] },
20
+ pages: [0],
21
+ resetInfiniteScroll,
22
+ }),
23
+ }))
24
+
25
+ vi.mock('discovery.config', () => ({
26
+ __esModule: true,
27
+ default: {
28
+ experimental: { enableSearchSSR: false },
29
+ seo: { search: { bodyH1: 'Showing results for:' } },
30
+ },
31
+ }))
32
+
33
+ vi.mock('src/sdk/product/useProductGalleryQuery', () => ({
34
+ useProductGalleryQuery: (...args: unknown[]) =>
35
+ useProductGalleryQuery(...args),
36
+ }))
37
+
38
+ vi.mock('src/components/cms/RenderSections', () => ({
39
+ __esModule: true,
40
+ default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
41
+ }))
42
+
43
+ vi.mock('src/sdk/overrides/PageProvider', () => ({
44
+ __esModule: true,
45
+ default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
46
+ }))
47
+
48
+ vi.mock('src/components/templates/SearchPage/EmptySearch', () => ({
49
+ __esModule: true,
50
+ default: () => <div data-testid="empty-search" />,
51
+ }))
52
+
53
+ vi.mock('src/components/templates/SearchPage/SearchPage', () => ({
54
+ __esModule: true,
55
+ default: () => <div data-testid="search-page" />,
56
+ }))
57
+
58
+ import SearchWrapper from 'src/components/templates/SearchPage/SearchWrapper'
59
+
60
+ describe('SearchWrapper Intelligent Search redirect', () => {
61
+ beforeEach(() => {
62
+ replace.mockClear()
63
+ resetInfiniteScroll.mockClear()
64
+ })
65
+
66
+ it('calls router.replace once from an effect when redirect.url is present', async () => {
67
+ useProductGalleryQuery.mockReturnValue({
68
+ data: { redirect: { url: '/office' }, search: { products: null } },
69
+ })
70
+
71
+ await act(async () => {
72
+ render(
73
+ <SearchWrapper
74
+ itemsPerPage={12}
75
+ searchContentType={{} as never}
76
+ serverData={{ searchTerm: 'refil' } as never}
77
+ />
78
+ )
79
+ })
80
+
81
+ await waitFor(() => {
82
+ expect(replace).toHaveBeenCalledTimes(1)
83
+ expect(replace).toHaveBeenCalledWith('/office')
84
+ })
85
+ })
86
+
87
+ it('does not call router.replace again for the same redirect url on rerender', async () => {
88
+ useProductGalleryQuery.mockReturnValue({
89
+ data: { redirect: { url: '/office' }, search: { products: null } },
90
+ })
91
+
92
+ const { rerender } = render(
93
+ <SearchWrapper
94
+ itemsPerPage={12}
95
+ searchContentType={{} as never}
96
+ serverData={{ searchTerm: 'refil' } as never}
97
+ />
98
+ )
99
+
100
+ await waitFor(() => {
101
+ expect(replace).toHaveBeenCalledTimes(1)
102
+ })
103
+
104
+ await act(async () => {
105
+ rerender(
106
+ <SearchWrapper
107
+ itemsPerPage={12}
108
+ searchContentType={{} as never}
109
+ serverData={{ searchTerm: 'refil' } as never}
110
+ />
111
+ )
112
+ })
113
+
114
+ expect(replace).toHaveBeenCalledTimes(1)
115
+ })
116
+
117
+ it('does not redirect when there is no redirect url', async () => {
118
+ useProductGalleryQuery.mockReturnValue({
119
+ data: {
120
+ redirect: null,
121
+ search: { products: { pageInfo: { totalCount: 12 } } },
122
+ },
123
+ })
124
+
125
+ await act(async () => {
126
+ render(
127
+ <SearchWrapper
128
+ itemsPerPage={12}
129
+ searchContentType={{} as never}
130
+ serverData={{ searchTerm: 'desk' } as never}
131
+ />
132
+ )
133
+ })
134
+
135
+ expect(replace).not.toHaveBeenCalled()
136
+ })
137
+ })
@@ -0,0 +1,230 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { act, render, screen } from '@testing-library/react'
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ vi.mock('@faststore/sdk', () => ({
9
+ useSearch: () => ({
10
+ state: {
11
+ term: null,
12
+ sort: 'score_desc',
13
+ selectedFacets: [],
14
+ },
15
+ }),
16
+ }))
17
+
18
+ import GalleryPageHeightLock, {
19
+ buildGalleryPageHeightKey,
20
+ getGalleryViewportBucket,
21
+ getReservedGalleryHeight,
22
+ readGalleryPageHeight,
23
+ resolveReservedGalleryHeight,
24
+ } from 'src/components/ui/ProductGallery/GalleryPageHeightLock'
25
+
26
+ describe('GalleryPageHeightLock helpers', () => {
27
+ beforeEach(() => {
28
+ sessionStorage.clear()
29
+ })
30
+
31
+ afterEach(() => {
32
+ vi.restoreAllMocks()
33
+ })
34
+
35
+ it('maps viewport widths to layout buckets', () => {
36
+ Object.defineProperty(window, 'innerWidth', {
37
+ configurable: true,
38
+ value: 390,
39
+ })
40
+ expect(getGalleryViewportBucket()).toBe('mobile')
41
+
42
+ Object.defineProperty(window, 'innerWidth', {
43
+ configurable: true,
44
+ value: 700,
45
+ })
46
+ expect(getGalleryViewportBucket()).toBe('tablet')
47
+
48
+ Object.defineProperty(window, 'innerWidth', {
49
+ configurable: true,
50
+ value: 1024,
51
+ })
52
+ expect(getGalleryViewportBucket()).toBe('notebook')
53
+
54
+ Object.defineProperty(window, 'innerWidth', {
55
+ configurable: true,
56
+ value: 1440,
57
+ })
58
+ expect(getGalleryViewportBucket()).toBe('desktop')
59
+ })
60
+
61
+ it('builds stable storage keys that include viewport', () => {
62
+ const keyA = buildGalleryPageHeightKey(2, {
63
+ path: '/office',
64
+ term: null,
65
+ sort: 'score_desc',
66
+ selectedFacets: [],
67
+ viewport: 'desktop',
68
+ })
69
+ const keyB = buildGalleryPageHeightKey(2, {
70
+ path: '/office',
71
+ term: null,
72
+ sort: 'score_desc',
73
+ selectedFacets: [],
74
+ viewport: 'mobile',
75
+ })
76
+
77
+ expect(keyA).toMatch(/^__fs_gallery_page_h_p2_desktop_/)
78
+ expect(keyB).toMatch(/^__fs_gallery_page_h_p2_mobile_/)
79
+ expect(keyA).not.toBe(keyB)
80
+ })
81
+
82
+ it('keeps page and viewport distinct for long path + facets', () => {
83
+ // Truncating a sanitized JSON at 180 chars used to drop page/viewport and
84
+ // collide mobile↔desktop (and page 0↔1) on filtered PLPs.
85
+ const search = {
86
+ path: '/eletronicos/celulares-e-smartphones',
87
+ term: null,
88
+ sort: 'score_desc',
89
+ selectedFacets: [
90
+ { key: 'brand', value: 'samsung' },
91
+ { key: 'price', value: '1000-to-2000' },
92
+ ],
93
+ }
94
+
95
+ const mobile = buildGalleryPageHeightKey(0, {
96
+ ...search,
97
+ viewport: 'mobile',
98
+ })
99
+ const desktop = buildGalleryPageHeightKey(0, {
100
+ ...search,
101
+ viewport: 'desktop',
102
+ })
103
+ const page1 = buildGalleryPageHeightKey(1, {
104
+ ...search,
105
+ viewport: 'desktop',
106
+ })
107
+
108
+ expect(mobile).not.toBe(desktop)
109
+ expect(desktop).not.toBe(page1)
110
+ expect(mobile).toContain('_mobile_')
111
+ expect(desktop).toContain('_desktop_')
112
+ expect(page1).toMatch(/^__fs_gallery_page_h_p1_/)
113
+ })
114
+
115
+ it('reads and sums reserved gallery heights', () => {
116
+ const search = {
117
+ path: '/office',
118
+ term: null,
119
+ sort: 'score_desc',
120
+ selectedFacets: [],
121
+ viewport: 'desktop',
122
+ }
123
+ const key0 = buildGalleryPageHeightKey(0, search)
124
+ const key1 = buildGalleryPageHeightKey(1, search)
125
+ sessionStorage.setItem(key0, '800')
126
+ sessionStorage.setItem(key1, '750')
127
+
128
+ expect(readGalleryPageHeight(key0)).toBe(800)
129
+ expect(readGalleryPageHeight('missing')).toBeNull()
130
+ expect(getReservedGalleryHeight([0, 1, 2], search)).toBe(1550)
131
+ })
132
+
133
+ it('ignores invalid stored heights', () => {
134
+ sessionStorage.setItem('bad-key', '0')
135
+ expect(readGalleryPageHeight('bad-key')).toBeNull()
136
+ sessionStorage.setItem('nan-key', 'nope')
137
+ expect(readGalleryPageHeight('nan-key')).toBeNull()
138
+ })
139
+
140
+ it('resolveReservedGalleryHeight is zero once products have loaded', () => {
141
+ const search = {
142
+ path: '/office',
143
+ term: null,
144
+ sort: 'score_desc',
145
+ selectedFacets: [],
146
+ viewport: 'desktop',
147
+ }
148
+ sessionStorage.setItem(buildGalleryPageHeightKey(0, search), '800')
149
+ expect(resolveReservedGalleryHeight(true, [0], search)).toBe(0)
150
+ expect(resolveReservedGalleryHeight(false, [0], search)).toBe(800)
151
+ })
152
+ })
153
+
154
+ describe('GalleryPageHeightLock', () => {
155
+ beforeEach(() => {
156
+ sessionStorage.clear()
157
+ Object.defineProperty(window, 'innerWidth', {
158
+ configurable: true,
159
+ value: 1440,
160
+ })
161
+ })
162
+
163
+ it('applies minHeight while skeleton is showing when a height was saved', async () => {
164
+ const key = buildGalleryPageHeightKey(0, {
165
+ path: '/',
166
+ term: null,
167
+ sort: 'score_desc',
168
+ selectedFacets: [],
169
+ viewport: 'desktop',
170
+ })
171
+ // Component uses window.location.pathname — jsdom default is "/"
172
+ sessionStorage.setItem(key, '640')
173
+
174
+ const { container } = render(
175
+ <GalleryPageHeightLock page={0}>
176
+ <div data-testid="skeleton">loading</div>
177
+ </GalleryPageHeightLock>
178
+ )
179
+
180
+ const lock = container.querySelector(
181
+ '[data-fs-gallery-page-height-lock]'
182
+ ) as HTMLElement
183
+
184
+ // Height is applied after mount (hydration-safe).
185
+ await act(async () => {
186
+ await Promise.resolve()
187
+ })
188
+
189
+ expect(lock.style.minHeight).toBe('640px')
190
+ expect(screen.getByTestId('skeleton')).toBeTruthy()
191
+ })
192
+
193
+ it('releases minHeight once product cards are mounted', async () => {
194
+ const key = buildGalleryPageHeightKey(0, {
195
+ path: '/',
196
+ term: null,
197
+ sort: 'score_desc',
198
+ selectedFacets: [],
199
+ viewport: 'desktop',
200
+ })
201
+ sessionStorage.setItem(key, '640')
202
+
203
+ const { container, rerender } = render(
204
+ <GalleryPageHeightLock page={0}>
205
+ <div data-testid="skeleton">loading</div>
206
+ </GalleryPageHeightLock>
207
+ )
208
+
209
+ await act(async () => {
210
+ await Promise.resolve()
211
+ })
212
+
213
+ const lock = container.querySelector(
214
+ '[data-fs-gallery-page-height-lock]'
215
+ ) as HTMLElement
216
+ expect(lock.style.minHeight).toBe('640px')
217
+
218
+ await act(async () => {
219
+ rerender(
220
+ <GalleryPageHeightLock page={0}>
221
+ <div data-fs-product-card data-testid="card">
222
+ Product
223
+ </div>
224
+ </GalleryPageHeightLock>
225
+ )
226
+ })
227
+
228
+ expect(lock.style.minHeight).toBe('')
229
+ })
230
+ })