@faststore/core 4.5.0-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 +132 -60
- package/@generated/cached-operations.json +2 -0
- package/@generated/gql.ts +18 -0
- package/@generated/graphql.ts +68 -0
- package/@generated/persisted-documents.json +3 -0
- package/@generated/schema.graphql +41 -0
- package/CHANGELOG.md +20 -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 +51 -26
- 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/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/pages/[slug]/p.tsx +45 -0
- package/src/sdk/account/changeContractToken.ts +119 -0
- package/src/sdk/account/clearPersistedSessionState.ts +26 -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/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/OrganizationDrawer.browser.test.tsx +197 -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/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/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 +3 -0
- package/test/utils/retry.test.ts +59 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
const mockFadeOut = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
11
|
+
const mockCloseDrawer = vi.hoisted(() => vi.fn())
|
|
12
|
+
|
|
13
|
+
vi.mock('@faststore/ui', async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal<typeof import('@faststore/ui')>()
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
useFadeEffect: () => ({ fade: 'in', fadeOut: mockFadeOut }),
|
|
19
|
+
SlideOver: ({
|
|
20
|
+
children,
|
|
21
|
+
isOpen,
|
|
22
|
+
onTransitionEnd,
|
|
23
|
+
}: {
|
|
24
|
+
children: ReactNode
|
|
25
|
+
isOpen: boolean
|
|
26
|
+
onTransitionEnd?: () => void
|
|
27
|
+
}) =>
|
|
28
|
+
isOpen ? (
|
|
29
|
+
<div
|
|
30
|
+
data-testid="organization-drawer"
|
|
31
|
+
onTransitionEnd={onTransitionEnd}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
) : null,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
40
|
+
|
|
41
|
+
vi.mock(
|
|
42
|
+
'../../../src/components/account/Drawer/OrganizationDrawer/ContractSwitcher',
|
|
43
|
+
() => ({
|
|
44
|
+
ContractSwitcher: ({
|
|
45
|
+
onBack,
|
|
46
|
+
onClose,
|
|
47
|
+
}: {
|
|
48
|
+
onBack: () => void
|
|
49
|
+
onClose: () => void
|
|
50
|
+
}) => (
|
|
51
|
+
<div data-testid="contract-switcher">
|
|
52
|
+
<button type="button" onClick={onBack}>
|
|
53
|
+
Back to menu
|
|
54
|
+
</button>
|
|
55
|
+
<button type="button" onClick={onClose}>
|
|
56
|
+
Close switcher
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
vi.mock(
|
|
64
|
+
'../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody',
|
|
65
|
+
() => ({
|
|
66
|
+
OrganizationDrawerBody: () => <nav data-fs-organization-drawer-body />,
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
import { OrganizationDrawer } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer'
|
|
71
|
+
|
|
72
|
+
describe('OrganizationDrawer', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
mockUseSession.mockReturnValue({
|
|
75
|
+
b2b: {
|
|
76
|
+
unitId: 'unit-1',
|
|
77
|
+
unitName: 'Stellar Global',
|
|
78
|
+
contractName: 'Acme Contract',
|
|
79
|
+
userName: 'Jane Buyer',
|
|
80
|
+
userEmail: 'jane@example.com',
|
|
81
|
+
organizationManager: true,
|
|
82
|
+
},
|
|
83
|
+
person: { givenName: 'Jane' },
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
vi.clearAllMocks()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders the menu view with contract details and manage footer', () => {
|
|
92
|
+
render(
|
|
93
|
+
<OrganizationDrawer
|
|
94
|
+
isOpen
|
|
95
|
+
closeDrawer={mockCloseDrawer}
|
|
96
|
+
isRepresentative={false}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('Acme Contract')).toBeTruthy()
|
|
101
|
+
expect(screen.getByText('Stellar Global')).toBeTruthy()
|
|
102
|
+
expect(screen.getByText('Jane Buyer')).toBeTruthy()
|
|
103
|
+
expect(screen.getByRole('link', { name: /manage/i })).toBeTruthy()
|
|
104
|
+
expect(screen.getByRole('button', { name: /change/i })).toBeTruthy()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('opens the contract switcher and returns to the menu on back', () => {
|
|
108
|
+
render(
|
|
109
|
+
<OrganizationDrawer
|
|
110
|
+
isOpen
|
|
111
|
+
closeDrawer={mockCloseDrawer}
|
|
112
|
+
isRepresentative={false}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
fireEvent.click(screen.getByRole('button', { name: /change/i }))
|
|
117
|
+
expect(screen.getByTestId('contract-switcher')).toBeTruthy()
|
|
118
|
+
|
|
119
|
+
fireEvent.click(screen.getByRole('button', { name: /back to menu/i }))
|
|
120
|
+
expect(screen.queryByTestId('contract-switcher')).toBeNull()
|
|
121
|
+
expect(screen.getByRole('button', { name: /change/i })).toBeTruthy()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('resets to the menu when the drawer closes', () => {
|
|
125
|
+
const { rerender } = render(
|
|
126
|
+
<OrganizationDrawer
|
|
127
|
+
isOpen
|
|
128
|
+
closeDrawer={mockCloseDrawer}
|
|
129
|
+
isRepresentative={false}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
fireEvent.click(screen.getByRole('button', { name: /change/i }))
|
|
134
|
+
expect(screen.getByTestId('contract-switcher')).toBeTruthy()
|
|
135
|
+
|
|
136
|
+
rerender(
|
|
137
|
+
<OrganizationDrawer
|
|
138
|
+
isOpen={false}
|
|
139
|
+
closeDrawer={mockCloseDrawer}
|
|
140
|
+
isRepresentative={false}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
rerender(
|
|
145
|
+
<OrganizationDrawer
|
|
146
|
+
isOpen
|
|
147
|
+
closeDrawer={mockCloseDrawer}
|
|
148
|
+
isRepresentative={false}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
expect(screen.queryByTestId('contract-switcher')).toBeNull()
|
|
153
|
+
expect(screen.getByRole('button', { name: /change/i })).toBeTruthy()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('falls back to person name when contract metadata is missing', () => {
|
|
157
|
+
mockUseSession.mockReturnValue({
|
|
158
|
+
b2b: {
|
|
159
|
+
unitId: 'unit-1',
|
|
160
|
+
userName: 'Jane Buyer',
|
|
161
|
+
},
|
|
162
|
+
person: { givenName: 'Jane' },
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
render(
|
|
166
|
+
<OrganizationDrawer
|
|
167
|
+
isOpen
|
|
168
|
+
closeDrawer={mockCloseDrawer}
|
|
169
|
+
isRepresentative={false}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(screen.getByRole('heading', { name: 'Jane', level: 1 })).toBeTruthy()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('hides contract switching when the buyer has no organization unit', () => {
|
|
177
|
+
mockUseSession.mockReturnValue({
|
|
178
|
+
b2b: {
|
|
179
|
+
userName: 'Jane Buyer',
|
|
180
|
+
},
|
|
181
|
+
person: null,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
render(
|
|
185
|
+
<OrganizationDrawer
|
|
186
|
+
isOpen
|
|
187
|
+
closeDrawer={mockCloseDrawer}
|
|
188
|
+
isRepresentative={false}
|
|
189
|
+
/>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
expect(screen.queryByRole('button', { name: /change/i })).toBeNull()
|
|
193
|
+
expect(
|
|
194
|
+
screen.getByRole('heading', { name: 'Organization', level: 1 })
|
|
195
|
+
).toBeTruthy()
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { OrganizationDrawerFooter } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter'
|
|
9
|
+
|
|
10
|
+
describe('OrganizationDrawerFooter', () => {
|
|
11
|
+
it('renders org and user details with optional email', () => {
|
|
12
|
+
render(
|
|
13
|
+
<OrganizationDrawerFooter
|
|
14
|
+
orgName="Stellar Global"
|
|
15
|
+
userName="Jane Buyer"
|
|
16
|
+
userEmail="jane@example.com"
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
expect(screen.getByText('Stellar Global')).toBeTruthy()
|
|
21
|
+
expect(screen.getByText('Jane Buyer')).toBeTruthy()
|
|
22
|
+
expect(screen.getByText('jane@example.com')).toBeTruthy()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('shows manage link for organization managers', () => {
|
|
26
|
+
render(
|
|
27
|
+
<OrganizationDrawerFooter
|
|
28
|
+
orgName="Stellar Global"
|
|
29
|
+
userName="Jane Buyer"
|
|
30
|
+
showManageLink
|
|
31
|
+
manageUrl="/pvt/organization-account/org-unit/unit-1"
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByRole('link', { name: /manage/i }).getAttribute('href')
|
|
37
|
+
).toBe('/pvt/organization-account/org-unit/unit-1')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('calls onLogoutClick from the logout button', () => {
|
|
41
|
+
const onLogoutClick = vi.fn()
|
|
42
|
+
|
|
43
|
+
render(
|
|
44
|
+
<OrganizationDrawerFooter
|
|
45
|
+
orgName="Stellar Global"
|
|
46
|
+
userName="Jane Buyer"
|
|
47
|
+
onLogoutClick={onLogoutClick}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
fireEvent.click(screen.getByRole('button', { name: /log out/i }))
|
|
52
|
+
expect(onLogoutClick).toHaveBeenCalledTimes(1)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { OrganizationDrawerHeader } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader'
|
|
9
|
+
|
|
10
|
+
describe('OrganizationDrawerHeader', () => {
|
|
11
|
+
it('renders contract name fallback initial and links when contractUrl is set', () => {
|
|
12
|
+
render(
|
|
13
|
+
<OrganizationDrawerHeader
|
|
14
|
+
contractName="Acme Corp"
|
|
15
|
+
contractUrl="/pvt/organization-account/org-unit/unit-1"
|
|
16
|
+
onCloseDrawer={vi.fn()}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
expect(screen.getByText('Acme Corp')).toBeTruthy()
|
|
21
|
+
expect(screen.getByLabelText('Acme Corp').getAttribute('href')).toBe(
|
|
22
|
+
'/pvt/organization-account/org-unit/unit-1'
|
|
23
|
+
)
|
|
24
|
+
expect(screen.getByText('A')).toBeTruthy()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('shows default star and Change action when configured', () => {
|
|
28
|
+
const onChangeContract = vi.fn()
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<OrganizationDrawerHeader
|
|
32
|
+
contractName="Acme Corp"
|
|
33
|
+
contractUrl={null}
|
|
34
|
+
isDefault
|
|
35
|
+
onChangeContract={onChangeContract}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
fireEvent.click(screen.getByRole('button', { name: /change/i }))
|
|
40
|
+
expect(onChangeContract).toHaveBeenCalledTimes(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders custom contract image and omits manage links without contractUrl', () => {
|
|
44
|
+
render(
|
|
45
|
+
<OrganizationDrawerHeader
|
|
46
|
+
contractName="Acme Corp"
|
|
47
|
+
contractUrl={null}
|
|
48
|
+
contractImage={<img alt="Logo" src="/logo.png" />}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(screen.getByAltText('Logo')).toBeTruthy()
|
|
53
|
+
expect(screen.queryByRole('link', { name: /acme corp/i })).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
vi.mock('discovery.config', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
api: { storeId: 'b2bfaststoredev' },
|
|
10
|
+
},
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const mockFetch = vi.hoisted(() => vi.fn())
|
|
14
|
+
vi.mock('isomorphic-unfetch', () => ({ __esModule: true, default: mockFetch }))
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ContractSwitchError,
|
|
18
|
+
applyVtexAuthCookieFromSwitchResponse,
|
|
19
|
+
changeContractToken,
|
|
20
|
+
isContractSwitchEnabled,
|
|
21
|
+
} from '../../../src/sdk/account/changeContractToken'
|
|
22
|
+
|
|
23
|
+
const successPayload = {
|
|
24
|
+
authStatus: 'Success',
|
|
25
|
+
expiresIn: 86_399,
|
|
26
|
+
authCookie: {
|
|
27
|
+
Name: 'VtexIdclientAutCookie_b2bfaststoredev',
|
|
28
|
+
Value: 'new-contract-token',
|
|
29
|
+
},
|
|
30
|
+
accountAuthCookie: null,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('changeContractToken', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
document.cookie = ''
|
|
36
|
+
mockFetch.mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
status: 200,
|
|
39
|
+
json: async () => successPayload,
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
document.cookie = ''
|
|
45
|
+
vi.clearAllMocks()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('is enabled', () => {
|
|
49
|
+
expect(isContractSwitchEnabled).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('rejects when contractId is missing', async () => {
|
|
53
|
+
await expect(changeContractToken('')).rejects.toThrow(ContractSwitchError)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('posts to switch-properties with the contract customerId', async () => {
|
|
57
|
+
const contractId = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
|
|
58
|
+
|
|
59
|
+
await expect(changeContractToken(contractId)).resolves.toBe(true)
|
|
60
|
+
|
|
61
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
62
|
+
const [url, init] = mockFetch.mock.calls[0]
|
|
63
|
+
expect(url).toBe(
|
|
64
|
+
'/api/authenticator/storefront/credential/switch-properties?an=b2bfaststoredev'
|
|
65
|
+
)
|
|
66
|
+
expect(init.method).toBe('POST')
|
|
67
|
+
expect(init.credentials).toBe('include')
|
|
68
|
+
expect(init.signal).toBeDefined()
|
|
69
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
70
|
+
properties: { customerId: contractId },
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('applies the auth cookie returned by switch-properties', async () => {
|
|
75
|
+
await changeContractToken('contract-1')
|
|
76
|
+
|
|
77
|
+
expect(document.cookie).toContain(
|
|
78
|
+
'VtexIdclientAutCookie_b2bfaststoredev=new-contract-token'
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('throws when the switch request fails', async () => {
|
|
83
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 401 })
|
|
84
|
+
|
|
85
|
+
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
86
|
+
/Failed to switch contract \(401\)/
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('throws when the switch request times out', async () => {
|
|
91
|
+
const timeoutError = new Error('The operation was aborted')
|
|
92
|
+
timeoutError.name = 'TimeoutError'
|
|
93
|
+
mockFetch.mockRejectedValueOnce(timeoutError)
|
|
94
|
+
|
|
95
|
+
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
96
|
+
/Contract switch timed out/
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('throws when authStatus is not success', async () => {
|
|
101
|
+
mockFetch.mockResolvedValueOnce({
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 200,
|
|
104
|
+
json: async () => ({ ...successPayload, authStatus: 'Failed' }),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
108
|
+
/Contract switch was not successful/
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('throws when authCookie is missing from a successful response', async () => {
|
|
113
|
+
mockFetch.mockResolvedValueOnce({
|
|
114
|
+
ok: true,
|
|
115
|
+
status: 200,
|
|
116
|
+
json: async () => ({
|
|
117
|
+
authStatus: 'Success',
|
|
118
|
+
authCookie: null,
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
123
|
+
/no auth cookie was returned/
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('applyVtexAuthCookieFromSwitchResponse', () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
document.cookie = ''
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
document.cookie = ''
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('sets auth and account auth cookies when present', () => {
|
|
138
|
+
applyVtexAuthCookieFromSwitchResponse({
|
|
139
|
+
expiresIn: 3600,
|
|
140
|
+
authCookie: {
|
|
141
|
+
Name: 'VtexIdclientAutCookie_b2bfaststoredev',
|
|
142
|
+
Value: 'store-token',
|
|
143
|
+
},
|
|
144
|
+
accountAuthCookie: {
|
|
145
|
+
Name: 'VtexIdclientAutCookie_account',
|
|
146
|
+
Value: 'account-token',
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
expect(document.cookie).toContain(
|
|
151
|
+
'VtexIdclientAutCookie_b2bfaststoredev=store-token'
|
|
152
|
+
)
|
|
153
|
+
expect(document.cookie).toContain(
|
|
154
|
+
'VtexIdclientAutCookie_account=account-token'
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
const mockDel = vi.hoisted(() => vi.fn())
|
|
8
|
+
|
|
9
|
+
vi.mock('idb-keyval', () => ({
|
|
10
|
+
del: mockDel,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import { SESSION_READY_KEY } from '../../../src/sdk/session/storageKeys'
|
|
14
|
+
import {
|
|
15
|
+
STORAGE_KEY_CACHE_BUST_LAST_VALUE,
|
|
16
|
+
STORAGE_KEY_PERSON_ID,
|
|
17
|
+
} from '../../../src/utils/cookieCacheBusting'
|
|
18
|
+
import { clearPersistedSessionState } from '../../../src/sdk/account/clearPersistedSessionState'
|
|
19
|
+
|
|
20
|
+
describe('clearPersistedSessionState', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockDel.mockResolvedValue(undefined)
|
|
23
|
+
sessionStorage.setItem(SESSION_READY_KEY, '1')
|
|
24
|
+
sessionStorage.setItem(STORAGE_KEY_PERSON_ID, 'person-1')
|
|
25
|
+
sessionStorage.setItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE, 'abc')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.clearAllMocks()
|
|
30
|
+
sessionStorage.clear()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('clears IndexedDB session and sessionStorage keys', async () => {
|
|
34
|
+
await clearPersistedSessionState()
|
|
35
|
+
|
|
36
|
+
expect(mockDel).toHaveBeenCalledWith('fs::session')
|
|
37
|
+
expect(sessionStorage.getItem(SESSION_READY_KEY)).toBeNull()
|
|
38
|
+
expect(sessionStorage.getItem(STORAGE_KEY_PERSON_ID)).toBeNull()
|
|
39
|
+
expect(sessionStorage.getItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE)).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockDel = vi.hoisted(() => vi.fn())
|
|
4
|
+
|
|
5
|
+
vi.mock('idb-keyval', () => ({ del: mockDel }))
|
|
6
|
+
|
|
7
|
+
import { clearPersistedSessionState } from '../../../src/sdk/account/clearPersistedSessionState'
|
|
8
|
+
|
|
9
|
+
describe('clearPersistedSessionState (node)', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('clears IndexedDB session when sessionStorage is unavailable', async () => {
|
|
15
|
+
mockDel.mockResolvedValue(undefined)
|
|
16
|
+
|
|
17
|
+
await expect(clearPersistedSessionState()).resolves.toBeUndefined()
|
|
18
|
+
expect(mockDel).toHaveBeenCalledWith('fs::session')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
9
|
+
vi.mock('isomorphic-unfetch', () => ({ __esModule: true, default: mockFetch }))
|
|
10
|
+
|
|
11
|
+
import { refreshTokenRequest } from '../../../src/sdk/account/refreshToken'
|
|
12
|
+
|
|
13
|
+
describe('refreshTokenRequest (browser)', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockFetch.mockResolvedValue({
|
|
16
|
+
status: 200,
|
|
17
|
+
json: async () => ({ status: 'success' }),
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.clearAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('uses a relative URL and omits Host header in the browser', async () => {
|
|
26
|
+
await refreshTokenRequest()
|
|
27
|
+
|
|
28
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
29
|
+
|
|
30
|
+
const [url, init] = mockFetch.mock.calls[0]
|
|
31
|
+
expect(url).toBe('/api/vtexid/refreshtoken/webstore')
|
|
32
|
+
expect(init.headers).toEqual({ 'content-type': 'application/json' })
|
|
33
|
+
})
|
|
34
|
+
})
|