@faststore/core 4.4.1-dev.0 → 4.5.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 +4 -4
- package/.turbo/turbo-test.log +137 -51
- package/@generated/cached-operations.json +4 -0
- package/@generated/gql.ts +30 -0
- package/@generated/graphql.ts +141 -0
- package/@generated/persisted-documents.json +5 -0
- package/@generated/schema.graphql +100 -0
- package/CHANGELOG.md +26 -0
- package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
- package/cms/faststore/schema.json +89 -0
- package/discovery.config.default.js +3 -0
- package/discovery.config.js +10 -1
- package/discovery.config.local.example.js +62 -0
- package/package.json +6 -6
- package/public/icons.svg +4 -0
- package/src/Layout.tsx +6 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +52 -23
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
- package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
- package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
- package/src/components/account/Layout/Layout.tsx +14 -5
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
- package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
- package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
- package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
- package/src/components/cms/global/Components.ts +10 -0
- package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
- package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
- package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
- package/src/components/sections/RecommendationShelf/index.ts +1 -0
- package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
- package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
- package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
- package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
- package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
- package/src/pages/[slug]/p.tsx +45 -0
- package/src/pages/pvt/account/quotes/index.tsx +264 -0
- package/src/sdk/account/changeContractToken.ts +119 -0
- package/src/sdk/account/clearPersistedSessionState.ts +26 -0
- package/src/sdk/account/getMyAccountRoutes.ts +7 -0
- package/src/sdk/account/refreshToken.ts +21 -8
- package/src/sdk/account/useAvailableContracts.ts +49 -0
- package/src/sdk/account/useSwitchContract.ts +60 -0
- package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
- package/src/sdk/analytics/utils.ts +9 -0
- package/src/utils/quoteStatus.ts +27 -0
- package/src/utils/retry.ts +69 -0
- package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
- package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
- package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
- package/test/components/RecommendationShelf/vrn.test.ts +56 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
- package/test/components/account/Layout.test.tsx +62 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
- package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
- package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
- package/test/components/account/doLogout.browser.test.ts +102 -0
- package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
- package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
- package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
- package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
- package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
- package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
- package/test/components/quotes/quoteFilters.test.ts +185 -0
- package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
- package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
- package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
- package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
- package/test/sdk/account/refreshToken.browser.test.ts +34 -0
- package/test/sdk/account/refreshToken.test.ts +87 -0
- package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
- package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
- package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
- package/test/sdk/analytics/utils.test.ts +23 -0
- package/test/server/index.test.ts +5 -0
- package/test/utils/retry.test.ts +59 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
const mockFetch = vi.hoisted(() => vi.fn())
|
|
8
|
+
const mockAssign = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockDel = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockSetReloadAfterLogoutReturn = vi.hoisted(() => vi.fn())
|
|
11
|
+
const mockGetStoreURL = vi.hoisted(() => vi.fn())
|
|
12
|
+
const mockExpireCookieClient = vi.hoisted(() => vi.fn())
|
|
13
|
+
|
|
14
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
15
|
+
vi.mock('idb-keyval', () => ({ del: mockDel }))
|
|
16
|
+
vi.mock('src/sdk/localization/useLocalizationConfig', () => ({
|
|
17
|
+
getStoreURL: mockGetStoreURL,
|
|
18
|
+
}))
|
|
19
|
+
vi.mock(
|
|
20
|
+
'../../../src/components/account/Drawer/OrganizationDrawer/useReloadAfterLogoutReturn',
|
|
21
|
+
() => ({
|
|
22
|
+
setReloadAfterLogoutReturn: mockSetReloadAfterLogoutReturn,
|
|
23
|
+
})
|
|
24
|
+
)
|
|
25
|
+
vi.mock('src/utils/clearCookies', () => ({
|
|
26
|
+
expireCookieClient: mockExpireCookieClient,
|
|
27
|
+
getCookieDomains: () => ['.example.com'],
|
|
28
|
+
getCookiePaths: () => ['/'],
|
|
29
|
+
getVtexCookieNames: (names: string[]) =>
|
|
30
|
+
names.filter((name) => /vtex/i.test(name)),
|
|
31
|
+
}))
|
|
32
|
+
vi.mock('../../../discovery.config', () => ({
|
|
33
|
+
default: {
|
|
34
|
+
secureSubdomain: 'https://secure.example.com',
|
|
35
|
+
api: { storeId: 'teststore' },
|
|
36
|
+
},
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
import { doLogout } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer'
|
|
40
|
+
|
|
41
|
+
describe('doLogout', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockFetch.mockResolvedValue({
|
|
44
|
+
json: async () => ({}),
|
|
45
|
+
})
|
|
46
|
+
mockGetStoreURL.mockReturnValue('https://store.example.com')
|
|
47
|
+
mockDel.mockResolvedValue(undefined)
|
|
48
|
+
|
|
49
|
+
Object.defineProperty(globalThis, 'indexedDB', {
|
|
50
|
+
configurable: true,
|
|
51
|
+
value: {
|
|
52
|
+
deleteDatabase: () => {
|
|
53
|
+
const request = {
|
|
54
|
+
onsuccess: null as (() => void) | null,
|
|
55
|
+
onerror: null,
|
|
56
|
+
onblocked: null,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
queueMicrotask(() => request.onsuccess?.())
|
|
60
|
+
|
|
61
|
+
return request
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
67
|
+
configurable: true,
|
|
68
|
+
value: {
|
|
69
|
+
hostname: 'store.example.com',
|
|
70
|
+
protocol: 'https:',
|
|
71
|
+
pathname: '/',
|
|
72
|
+
assign: mockAssign,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
document.cookie = 'VtexIdclientAutCookie_teststore=token; other=1'
|
|
77
|
+
sessionStorage.setItem('faststore_session_ready', '1')
|
|
78
|
+
sessionStorage.setItem('__fs_gallery_page_1', '1')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.clearAllMocks()
|
|
83
|
+
sessionStorage.clear()
|
|
84
|
+
document.cookie = ''
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('clears client storage, calls logout API, and redirects to VTEX logout', async () => {
|
|
88
|
+
await doLogout()
|
|
89
|
+
|
|
90
|
+
expect(mockDel).toHaveBeenCalledWith('fs::session')
|
|
91
|
+
expect(sessionStorage.getItem('faststore_session_ready')).toBeNull()
|
|
92
|
+
expect(sessionStorage.getItem('__fs_gallery_page_1')).toBeNull()
|
|
93
|
+
expect(mockFetch).toHaveBeenCalledWith('/api/fs/logout', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
credentials: 'include',
|
|
96
|
+
})
|
|
97
|
+
expect(mockSetReloadAfterLogoutReturn).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(mockAssign).toHaveBeenCalledWith(
|
|
99
|
+
'https://secure.example.com/api/vtexid/pub/logout?scope=teststore&returnUrl=https://store.example.com'
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
const mockUseScreenResize = vi.hoisted(() => vi.fn())
|
|
10
|
+
vi.mock('src/sdk/ui/useScreenResize', () => ({
|
|
11
|
+
default: mockUseScreenResize,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock(
|
|
15
|
+
'../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable',
|
|
16
|
+
() => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: () => <div data-testid="quotes-table" />,
|
|
19
|
+
Pagination: ({ page }: { page: number }) => (
|
|
20
|
+
<div data-testid="pagination">page {page}</div>
|
|
21
|
+
),
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
vi.mock(
|
|
26
|
+
'../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider',
|
|
27
|
+
() => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
default: () => <div data-testid="filter-slider" />,
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const mockUseUI = vi.hoisted(() => vi.fn())
|
|
34
|
+
vi.mock('@faststore/ui', async () => {
|
|
35
|
+
const actual =
|
|
36
|
+
await vi.importActual<typeof import('@faststore/ui')>('@faststore/ui')
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
useUI: mockUseUI,
|
|
40
|
+
SearchInputField: React.forwardRef(function MockSearchInputField(
|
|
41
|
+
{ onBlur, onKeyDown }: any,
|
|
42
|
+
ref: any
|
|
43
|
+
) {
|
|
44
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
45
|
+
React.useImperativeHandle(ref, () => ({ inputRef: inputRef.current }))
|
|
46
|
+
return (
|
|
47
|
+
<input
|
|
48
|
+
ref={inputRef}
|
|
49
|
+
placeholder="Search"
|
|
50
|
+
onBlur={onBlur}
|
|
51
|
+
onKeyDown={onKeyDown}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
import MyAccountListQuotes from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes'
|
|
59
|
+
|
|
60
|
+
const originalLocation = window.location
|
|
61
|
+
|
|
62
|
+
function stubLocation(search = '') {
|
|
63
|
+
Object.defineProperty(window, 'location', {
|
|
64
|
+
configurable: true,
|
|
65
|
+
value: { ...originalLocation, search, href: '' },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function baseFilters(overrides: Record<string, unknown> = {}) {
|
|
70
|
+
return {
|
|
71
|
+
page: 1,
|
|
72
|
+
status: [],
|
|
73
|
+
createdAtFrom: '',
|
|
74
|
+
createdAtTo: '',
|
|
75
|
+
expiresAtFrom: '',
|
|
76
|
+
expiresAtTo: '',
|
|
77
|
+
label: '',
|
|
78
|
+
...overrides,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('MyAccountListQuotes', () => {
|
|
83
|
+
const openFilter = vi.fn()
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
stubLocation()
|
|
87
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
88
|
+
mockUseUI.mockReturnValue({ openFilter, filter: false })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
vi.clearAllMocks()
|
|
93
|
+
Object.defineProperty(window, 'location', {
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: originalLocation,
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('renders the quotes table when there are results', () => {
|
|
100
|
+
render(
|
|
101
|
+
<MyAccountListQuotes
|
|
102
|
+
listQuotes={{ list: [{ id: 'q-1' }] } as any}
|
|
103
|
+
total={1}
|
|
104
|
+
perPage={25}
|
|
105
|
+
filters={baseFilters()}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(screen.getByTestId('quotes-table')).toBeTruthy()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('renders the "no quotes" empty state when there are no results and no filters', () => {
|
|
113
|
+
render(
|
|
114
|
+
<MyAccountListQuotes
|
|
115
|
+
listQuotes={{ list: [] } as any}
|
|
116
|
+
total={0}
|
|
117
|
+
perPage={25}
|
|
118
|
+
filters={baseFilters()}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
expect(screen.getByText("You don't have any quotes")).toBeTruthy()
|
|
123
|
+
expect(screen.queryByTestId('quotes-table')).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('renders the "no results" empty state when filters are active but nothing matches', () => {
|
|
127
|
+
render(
|
|
128
|
+
<MyAccountListQuotes
|
|
129
|
+
listQuotes={{ list: [] } as any}
|
|
130
|
+
total={0}
|
|
131
|
+
perPage={25}
|
|
132
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('No results found')).toBeTruthy()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('opens the filter slider and dispatches the current selected facets when Filters is clicked', () => {
|
|
140
|
+
render(
|
|
141
|
+
<MyAccountListQuotes
|
|
142
|
+
listQuotes={{ list: [] } as any}
|
|
143
|
+
total={0}
|
|
144
|
+
perPage={25}
|
|
145
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
146
|
+
/>
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByText('Filters'))
|
|
150
|
+
|
|
151
|
+
expect(openFilter).toHaveBeenCalledTimes(1)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('shows the active filter count badge', () => {
|
|
155
|
+
render(
|
|
156
|
+
<MyAccountListQuotes
|
|
157
|
+
listQuotes={{ list: [] } as any}
|
|
158
|
+
total={0}
|
|
159
|
+
perPage={25}
|
|
160
|
+
filters={baseFilters({ status: ['Draft'] })}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText('1')).toBeTruthy()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('renders the filter slider when the UI filter drawer is open', () => {
|
|
168
|
+
mockUseUI.mockReturnValue({ openFilter, filter: true })
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<MyAccountListQuotes
|
|
172
|
+
listQuotes={{ list: [] } as any}
|
|
173
|
+
total={0}
|
|
174
|
+
perPage={25}
|
|
175
|
+
filters={baseFilters()}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(screen.getByTestId('filter-slider')).toBeTruthy()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('shows pagination on desktop when there are results', () => {
|
|
183
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
184
|
+
|
|
185
|
+
render(
|
|
186
|
+
<MyAccountListQuotes
|
|
187
|
+
listQuotes={{ list: [{ id: 'q-1' }] } as any}
|
|
188
|
+
total={30}
|
|
189
|
+
perPage={25}
|
|
190
|
+
filters={baseFilters({ page: 2 })}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(screen.getAllByTestId('pagination').length).toBeGreaterThan(0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('navigates with the search term on blur', async () => {
|
|
198
|
+
render(
|
|
199
|
+
<MyAccountListQuotes
|
|
200
|
+
listQuotes={{ list: [] } as any}
|
|
201
|
+
total={0}
|
|
202
|
+
perPage={25}
|
|
203
|
+
filters={baseFilters()}
|
|
204
|
+
/>
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const input = screen.getByPlaceholderText('Search')
|
|
208
|
+
fireEvent.change(input, { target: { value: 'ACME' } })
|
|
209
|
+
fireEvent.blur(input)
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 350))
|
|
211
|
+
|
|
212
|
+
expect(window.location.href).toBe('/pvt/account/quotes?label=ACME')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('navigates without a label param when the search is cleared', async () => {
|
|
216
|
+
render(
|
|
217
|
+
<MyAccountListQuotes
|
|
218
|
+
listQuotes={{ list: [] } as any}
|
|
219
|
+
total={0}
|
|
220
|
+
perPage={25}
|
|
221
|
+
filters={baseFilters({ label: 'ACME' })}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const input = screen.getByPlaceholderText('Search')
|
|
226
|
+
fireEvent.change(input, { target: { value: '' } })
|
|
227
|
+
fireEvent.blur(input)
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 350))
|
|
229
|
+
|
|
230
|
+
expect(window.location.href).toBe('/pvt/account/quotes?')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockUseScreenResize = vi.hoisted(() => vi.fn())
|
|
10
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
11
|
+
vi.mock('src/sdk/ui/useScreenResize', () => ({
|
|
12
|
+
default: mockUseScreenResize,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
import MyAccountListQuotesTable, {
|
|
16
|
+
Pagination,
|
|
17
|
+
} from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable'
|
|
18
|
+
|
|
19
|
+
const originalLocation = window.location
|
|
20
|
+
|
|
21
|
+
function stubLocation() {
|
|
22
|
+
Object.defineProperty(window, 'location', {
|
|
23
|
+
configurable: true,
|
|
24
|
+
value: { ...originalLocation, search: '', href: '' },
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function baseFilters(page = 1) {
|
|
29
|
+
return {
|
|
30
|
+
page,
|
|
31
|
+
status: [],
|
|
32
|
+
createdAtFrom: '',
|
|
33
|
+
createdAtTo: '',
|
|
34
|
+
expiresAtFrom: '',
|
|
35
|
+
expiresAtTo: '',
|
|
36
|
+
label: '',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function baseQuote(overrides: Partial<Record<string, unknown>> = {}) {
|
|
41
|
+
return {
|
|
42
|
+
id: 'q-1',
|
|
43
|
+
status: 'Draft',
|
|
44
|
+
label: null,
|
|
45
|
+
createdAt: '2026-01-01T10:00:00Z',
|
|
46
|
+
expiresAt: '2099-01-01T00:00:00Z',
|
|
47
|
+
amount: 150.5,
|
|
48
|
+
createdBy: null,
|
|
49
|
+
...overrides,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('MyAccountListQuotesTable', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
stubLocation()
|
|
56
|
+
mockUseSession.mockReturnValue({
|
|
57
|
+
locale: 'en-US',
|
|
58
|
+
currency: { code: 'USD' },
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
vi.clearAllMocks()
|
|
64
|
+
Object.defineProperty(window, 'location', {
|
|
65
|
+
configurable: true,
|
|
66
|
+
value: originalLocation,
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('renders a row per quote with id, status and formatted amount', () => {
|
|
71
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
72
|
+
|
|
73
|
+
render(
|
|
74
|
+
<MyAccountListQuotesTable
|
|
75
|
+
listQuotes={{ list: [baseQuote()], paging: {} as any }}
|
|
76
|
+
total={1}
|
|
77
|
+
perPage={25}
|
|
78
|
+
filters={baseFilters()}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText('q-1')).toBeTruthy()
|
|
83
|
+
expect(screen.getByText('Draft')).toBeTruthy()
|
|
84
|
+
expect(screen.getByText('$150.50')).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders the label when present and omits it when absent', () => {
|
|
88
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
89
|
+
|
|
90
|
+
const { rerender } = render(
|
|
91
|
+
<MyAccountListQuotesTable
|
|
92
|
+
listQuotes={{
|
|
93
|
+
list: [baseQuote({ label: 'My favorite quote' })],
|
|
94
|
+
paging: {} as any,
|
|
95
|
+
}}
|
|
96
|
+
total={1}
|
|
97
|
+
perPage={25}
|
|
98
|
+
filters={baseFilters()}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
expect(screen.getByText('My favorite quote')).toBeTruthy()
|
|
102
|
+
|
|
103
|
+
rerender(
|
|
104
|
+
<MyAccountListQuotesTable
|
|
105
|
+
listQuotes={{ list: [baseQuote({ label: null })], paging: {} as any }}
|
|
106
|
+
total={1}
|
|
107
|
+
perPage={25}
|
|
108
|
+
filters={baseFilters()}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
expect(screen.queryByText('My favorite quote')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('hides the dates column on mobile', () => {
|
|
115
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: false })
|
|
116
|
+
|
|
117
|
+
render(
|
|
118
|
+
<MyAccountListQuotesTable
|
|
119
|
+
listQuotes={{ list: [baseQuote()], paging: {} as any }}
|
|
120
|
+
total={1}
|
|
121
|
+
perPage={25}
|
|
122
|
+
filters={baseFilters()}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(screen.queryByText('Creation date')).toBeNull()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('shows "Created by" only when createdBy is set', () => {
|
|
130
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
131
|
+
|
|
132
|
+
render(
|
|
133
|
+
<MyAccountListQuotesTable
|
|
134
|
+
listQuotes={{
|
|
135
|
+
list: [baseQuote({ createdBy: 'Ada Lovelace' })],
|
|
136
|
+
paging: {} as any,
|
|
137
|
+
}}
|
|
138
|
+
total={1}
|
|
139
|
+
perPage={25}
|
|
140
|
+
filters={baseFilters()}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
expect(screen.getByText('Created by')).toBeTruthy()
|
|
145
|
+
expect(screen.getByText('Ada Lovelace')).toBeTruthy()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('shows a relative expiry hint for quotes expiring soon', () => {
|
|
149
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
150
|
+
const soon = new Date(Date.now() + 3 * 3_600_000).toISOString()
|
|
151
|
+
|
|
152
|
+
render(
|
|
153
|
+
<MyAccountListQuotesTable
|
|
154
|
+
listQuotes={{
|
|
155
|
+
list: [baseQuote({ expiresAt: soon })],
|
|
156
|
+
paging: {} as any,
|
|
157
|
+
}}
|
|
158
|
+
total={1}
|
|
159
|
+
perPage={25}
|
|
160
|
+
filters={baseFilters()}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText(/hours left/)).toBeTruthy()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('omits the relative expiry hint for already-expired quotes', () => {
|
|
168
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
169
|
+
const past = new Date(Date.now() - 3_600_000).toISOString()
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<MyAccountListQuotesTable
|
|
173
|
+
listQuotes={{
|
|
174
|
+
list: [baseQuote({ expiresAt: past })],
|
|
175
|
+
paging: {} as any,
|
|
176
|
+
}}
|
|
177
|
+
total={1}
|
|
178
|
+
perPage={25}
|
|
179
|
+
filters={baseFilters()}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expect(screen.queryByText(/left/)).toBeNull()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('Pagination', () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
stubLocation()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
Object.defineProperty(window, 'location', {
|
|
194
|
+
configurable: true,
|
|
195
|
+
value: originalLocation,
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('renders the current index range', () => {
|
|
200
|
+
render(<Pagination page={2} total={60} perPage={25} />)
|
|
201
|
+
|
|
202
|
+
expect(screen.getByText('26 — 50 of 60')).toBeTruthy()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('caps the last index label at the total', () => {
|
|
206
|
+
render(<Pagination page={3} total={60} perPage={25} />)
|
|
207
|
+
|
|
208
|
+
expect(screen.getByText('51 — 60 of 60')).toBeTruthy()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('disables the previous button on the first page', () => {
|
|
212
|
+
render(<Pagination page={1} total={60} perPage={25} />)
|
|
213
|
+
|
|
214
|
+
const button = screen.getByRole('button', { name: 'Previous Page' })
|
|
215
|
+
expect((button as HTMLButtonElement).disabled).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('disables the next button on the last page', () => {
|
|
219
|
+
render(<Pagination page={3} total={60} perPage={25} />)
|
|
220
|
+
|
|
221
|
+
const button = screen.getByRole('button', { name: 'Next Page' })
|
|
222
|
+
expect((button as HTMLButtonElement).disabled).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('navigates without a page param when going back to page 1', () => {
|
|
226
|
+
render(<Pagination page={2} total={60} perPage={25} />)
|
|
227
|
+
|
|
228
|
+
screen.getByRole('button', { name: 'Previous Page' }).click()
|
|
229
|
+
|
|
230
|
+
expect(window.location.href).toBe('/pvt/account/quotes')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('navigates with the target page param otherwise', () => {
|
|
234
|
+
render(<Pagination page={1} total={60} perPage={25} />)
|
|
235
|
+
|
|
236
|
+
screen.getByRole('button', { name: 'Next Page' }).click()
|
|
237
|
+
|
|
238
|
+
expect(window.location.href).toBe('/pvt/account/quotes?page=2')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import MyAccountQuoteStatusBadge from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge'
|
|
9
|
+
|
|
10
|
+
describe('MyAccountQuoteStatusBadge', () => {
|
|
11
|
+
it('renders the mapped label and variant for a known status', () => {
|
|
12
|
+
render(<MyAccountQuoteStatusBadge status="Approved" />)
|
|
13
|
+
|
|
14
|
+
const badge = screen.getByText('Approved')
|
|
15
|
+
expect(badge.getAttribute('data-fs-my-account-badge-variant')).toBe(
|
|
16
|
+
'success'
|
|
17
|
+
)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('falls back to the raw status and a neutral variant when unknown', () => {
|
|
21
|
+
render(<MyAccountQuoteStatusBadge status="SomethingElse" />)
|
|
22
|
+
|
|
23
|
+
const badge = screen.getByText('SomethingElse')
|
|
24
|
+
expect(badge.getAttribute('data-fs-my-account-badge-variant')).toBe(
|
|
25
|
+
'neutral'
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
})
|