@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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseAvailableContracts = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockSwitchContract = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockUseSwitchContract = vi.hoisted(() => vi.fn())
|
|
11
|
+
|
|
12
|
+
vi.mock('src/sdk/account/useAvailableContracts', () => ({
|
|
13
|
+
useAvailableContracts: mockUseAvailableContracts,
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
vi.mock('src/sdk/account/useSwitchContract', () => ({
|
|
17
|
+
useSwitchContract: mockUseSwitchContract,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('src/sdk/session', () => ({
|
|
21
|
+
useSession: () => ({
|
|
22
|
+
b2b: { customerId: 'a', unitId: 'unit-1' },
|
|
23
|
+
}),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
import { ContractSwitcher } from '../../../src/components/account/Drawer/OrganizationDrawer/ContractSwitcher'
|
|
27
|
+
|
|
28
|
+
const contracts = [
|
|
29
|
+
{ id: 'a', corporateName: 'Stellar Global', isActive: true, isDefault: true },
|
|
30
|
+
{ id: 'b', corporateName: 'Acme Foods', isActive: false },
|
|
31
|
+
{ id: 'c', corporateName: 'Beacon Security Corp', isActive: false },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
describe('ContractSwitcher', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
mockSwitchContract.mockResolvedValue(true)
|
|
37
|
+
mockUseSwitchContract.mockReturnValue({
|
|
38
|
+
switchContract: mockSwitchContract,
|
|
39
|
+
loading: false,
|
|
40
|
+
error: null,
|
|
41
|
+
enabled: true,
|
|
42
|
+
})
|
|
43
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
44
|
+
contracts,
|
|
45
|
+
loading: false,
|
|
46
|
+
error: null,
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.clearAllMocks()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('shows a loading state while contracts load', () => {
|
|
55
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
56
|
+
contracts: [],
|
|
57
|
+
loading: true,
|
|
58
|
+
error: null,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const { container } = render(
|
|
62
|
+
<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
container.querySelector('[data-fs-contract-switcher-loading]')
|
|
67
|
+
).toBeTruthy()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('shows an error state when the list fails to load', () => {
|
|
71
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
72
|
+
contracts: [],
|
|
73
|
+
loading: false,
|
|
74
|
+
error: new Error('boom'),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
78
|
+
|
|
79
|
+
expect(screen.getByRole('alert')).toBeTruthy()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('shows an empty state when there is no alternative contract', () => {
|
|
83
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
84
|
+
contracts: [{ id: 'a', corporateName: 'Stellar Global', isActive: true }],
|
|
85
|
+
loading: false,
|
|
86
|
+
error: null,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
container.querySelector('[data-fs-contract-switcher-empty]')
|
|
95
|
+
).toBeTruthy()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('shows the active contract as the current session and lists alternatives (REQ-04, REQ-05)', () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const currentCard = container.querySelector(
|
|
104
|
+
'[data-fs-contract-switcher-current-card]'
|
|
105
|
+
)
|
|
106
|
+
expect(currentCard?.textContent).toContain('Stellar Global')
|
|
107
|
+
expect(
|
|
108
|
+
currentCard?.querySelector('[data-fs-contract-switcher-default]')
|
|
109
|
+
).toBeTruthy()
|
|
110
|
+
|
|
111
|
+
// The active contract is not offered as a selectable alternative.
|
|
112
|
+
expect(screen.getByText('Acme Foods')).toBeTruthy()
|
|
113
|
+
expect(screen.getByText('Beacon Security Corp')).toBeTruthy()
|
|
114
|
+
const options = container.querySelectorAll(
|
|
115
|
+
'[data-fs-contract-switcher-option]'
|
|
116
|
+
)
|
|
117
|
+
expect(options.length).toBe(2)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('requires selecting a contract before Confirm is enabled, then switches (REQ-03)', async () => {
|
|
121
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
122
|
+
|
|
123
|
+
const confirm = screen.getByRole('button', { name: /confirm/i })
|
|
124
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(true)
|
|
125
|
+
|
|
126
|
+
fireEvent.click(screen.getByText('Acme Foods'))
|
|
127
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(false)
|
|
128
|
+
|
|
129
|
+
fireEvent.click(confirm)
|
|
130
|
+
|
|
131
|
+
await waitFor(() => expect(mockSwitchContract).toHaveBeenCalledWith('b'))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('keeps Confirm disabled until a different contract is selected', () => {
|
|
135
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
136
|
+
|
|
137
|
+
const confirm = screen.getByRole('button', { name: /confirm/i })
|
|
138
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(true)
|
|
139
|
+
|
|
140
|
+
fireEvent.click(screen.getByText('Acme Foods'))
|
|
141
|
+
|
|
142
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(false)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('keeps Confirm disabled when switching is unavailable', () => {
|
|
146
|
+
mockUseSwitchContract.mockReturnValue({
|
|
147
|
+
switchContract: mockSwitchContract,
|
|
148
|
+
loading: false,
|
|
149
|
+
error: null,
|
|
150
|
+
enabled: false,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
154
|
+
|
|
155
|
+
fireEvent.click(screen.getByText('Acme Foods'))
|
|
156
|
+
|
|
157
|
+
const confirm = screen.getByRole('button', { name: /confirm/i })
|
|
158
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps Confirm disabled when the selected contract is filtered out', () => {
|
|
162
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
163
|
+
|
|
164
|
+
fireEvent.click(screen.getByText('Acme Foods'))
|
|
165
|
+
|
|
166
|
+
const confirm = screen.getByRole('button', { name: /confirm/i })
|
|
167
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(false)
|
|
168
|
+
|
|
169
|
+
fireEvent.change(screen.getByLabelText('Search contracts'), {
|
|
170
|
+
target: { value: 'beacon' },
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
expect((confirm as HTMLButtonElement).disabled).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('filters the alternatives by search', () => {
|
|
177
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
178
|
+
|
|
179
|
+
fireEvent.change(screen.getByLabelText('Search contracts'), {
|
|
180
|
+
target: { value: 'acme' },
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
expect(screen.getByText('Acme Foods')).toBeTruthy()
|
|
184
|
+
expect(screen.queryByText('Beacon Security Corp')).toBeNull()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('shows an empty search result message when nothing matches', () => {
|
|
188
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
189
|
+
|
|
190
|
+
fireEvent.change(screen.getByLabelText('Search contracts'), {
|
|
191
|
+
target: { value: 'zzzz-no-match' },
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
expect(screen.getByText('No contracts match your search.')).toBeTruthy()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('clears the search input', () => {
|
|
198
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
199
|
+
|
|
200
|
+
const searchInput = screen.getByLabelText(
|
|
201
|
+
'Search contracts'
|
|
202
|
+
) as HTMLInputElement
|
|
203
|
+
|
|
204
|
+
fireEvent.change(searchInput, {
|
|
205
|
+
target: { value: 'acme' },
|
|
206
|
+
})
|
|
207
|
+
fireEvent.click(screen.getByLabelText('Clear search'))
|
|
208
|
+
|
|
209
|
+
expect(searchInput.value).toBe('')
|
|
210
|
+
expect(screen.getByText('Beacon Security Corp')).toBeTruthy()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('shows a switch error when the contract change fails', () => {
|
|
214
|
+
mockUseSwitchContract.mockReturnValue({
|
|
215
|
+
switchContract: mockSwitchContract,
|
|
216
|
+
loading: false,
|
|
217
|
+
error: new Error('switch failed'),
|
|
218
|
+
enabled: true,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
222
|
+
|
|
223
|
+
expect(screen.getByText(/couldn't switch your contract/i)).toBeTruthy()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('does not reload when the switch returns false', async () => {
|
|
227
|
+
mockSwitchContract.mockResolvedValue(false)
|
|
228
|
+
|
|
229
|
+
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
230
|
+
|
|
231
|
+
fireEvent.click(screen.getByText('Acme Foods'))
|
|
232
|
+
fireEvent.click(screen.getByRole('button', { name: /confirm/i }))
|
|
233
|
+
|
|
234
|
+
await waitFor(() => expect(mockSwitchContract).toHaveBeenCalledWith('b'))
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
9
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
10
|
+
|
|
11
|
+
vi.mock('../../../src/components/account/Menu', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: ({ items }: { items: Array<{ title: string; route: string }> }) => (
|
|
14
|
+
<ul>
|
|
15
|
+
{items.map((item) => (
|
|
16
|
+
<li key={item.route}>{item.title}</li>
|
|
17
|
+
))}
|
|
18
|
+
</ul>
|
|
19
|
+
),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
import Layout from '../../../src/components/account/Layout/Layout'
|
|
23
|
+
|
|
24
|
+
describe('Layout', () => {
|
|
25
|
+
it('hides the Quotes route when the user is not an org member', () => {
|
|
26
|
+
mockUseSession.mockReturnValue({ b2b: null })
|
|
27
|
+
|
|
28
|
+
render(<Layout accountName="Jane Doe">content</Layout>)
|
|
29
|
+
|
|
30
|
+
expect(screen.queryByText('Quotes')).toBeNull()
|
|
31
|
+
expect(screen.getByText('Profile')).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows the Quotes route when the user belongs to an org unit', () => {
|
|
35
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
36
|
+
|
|
37
|
+
render(<Layout accountName="Jane Doe">content</Layout>)
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('hides User Details when not a representative, regardless of org membership', () => {
|
|
43
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<Layout accountName="Jane Doe" isRepresentative={false}>
|
|
47
|
+
content
|
|
48
|
+
</Layout>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(screen.queryByText('User Details')).toBeNull()
|
|
52
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders children inside the layout content area', () => {
|
|
56
|
+
mockUseSession.mockReturnValue({ b2b: null })
|
|
57
|
+
|
|
58
|
+
render(<Layout accountName="Jane Doe">child content</Layout>)
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText('child content')).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -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,52 @@
|
|
|
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
|
+
const mockUseAccountNavigationLabels = vi.hoisted(() => vi.fn())
|
|
9
|
+
vi.mock('src/sdk/account/accountPageContext', () => ({
|
|
10
|
+
useAccountNavigationLabels: mockUseAccountNavigationLabels,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('src/components/ui/Link', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: ({
|
|
16
|
+
href,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
href: string
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
}) => <a href={href}>{children}</a>,
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
import { OrganizationDrawerBody } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody'
|
|
25
|
+
|
|
26
|
+
describe('OrganizationDrawerBody', () => {
|
|
27
|
+
it('hides the Quotes route when the user is not an org member', () => {
|
|
28
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
29
|
+
|
|
30
|
+
render(<OrganizationDrawerBody isRepresentative isOrgMember={false} />)
|
|
31
|
+
|
|
32
|
+
expect(screen.queryByText('Quotes')).toBeNull()
|
|
33
|
+
expect(screen.getByText('Profile')).toBeTruthy()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('shows the Quotes route when the user belongs to an org unit', () => {
|
|
37
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
38
|
+
|
|
39
|
+
render(<OrganizationDrawerBody isRepresentative isOrgMember />)
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('hides User Details when not a representative, regardless of org membership', () => {
|
|
45
|
+
mockUseAccountNavigationLabels.mockReturnValue(undefined)
|
|
46
|
+
|
|
47
|
+
render(<OrganizationDrawerBody isRepresentative={false} isOrgMember />)
|
|
48
|
+
|
|
49
|
+
expect(screen.queryByText('User Details')).toBeNull()
|
|
50
|
+
expect(screen.getByText('Quotes')).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -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
|
+
})
|