@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.
Files changed (89) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +137 -51
  3. package/@generated/cached-operations.json +4 -0
  4. package/@generated/gql.ts +30 -0
  5. package/@generated/graphql.ts +141 -0
  6. package/@generated/persisted-documents.json +5 -0
  7. package/@generated/schema.graphql +100 -0
  8. package/CHANGELOG.md +26 -0
  9. package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
  10. package/cms/faststore/schema.json +89 -0
  11. package/discovery.config.default.js +3 -0
  12. package/discovery.config.js +10 -1
  13. package/discovery.config.local.example.js +62 -0
  14. package/package.json +6 -6
  15. package/public/icons.svg +4 -0
  16. package/src/Layout.tsx +6 -0
  17. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
  18. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
  19. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +52 -23
  20. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
  21. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
  22. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
  23. package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
  24. package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
  25. package/src/components/account/Layout/Layout.tsx +14 -5
  26. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
  27. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
  28. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
  29. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
  30. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
  31. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
  32. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
  33. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
  34. package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
  35. package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
  36. package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
  37. package/src/components/cms/global/Components.ts +10 -0
  38. package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
  39. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
  40. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
  41. package/src/components/sections/RecommendationShelf/index.ts +1 -0
  42. package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
  43. package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
  44. package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
  45. package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
  46. package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
  47. package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
  48. package/src/pages/[slug]/p.tsx +45 -0
  49. package/src/pages/pvt/account/quotes/index.tsx +264 -0
  50. package/src/sdk/account/changeContractToken.ts +119 -0
  51. package/src/sdk/account/clearPersistedSessionState.ts +26 -0
  52. package/src/sdk/account/getMyAccountRoutes.ts +7 -0
  53. package/src/sdk/account/refreshToken.ts +21 -8
  54. package/src/sdk/account/useAvailableContracts.ts +49 -0
  55. package/src/sdk/account/useSwitchContract.ts +60 -0
  56. package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
  57. package/src/sdk/analytics/utils.ts +9 -0
  58. package/src/utils/quoteStatus.ts +27 -0
  59. package/src/utils/retry.ts +69 -0
  60. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
  61. package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
  62. package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
  63. package/test/components/RecommendationShelf/vrn.test.ts +56 -0
  64. package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
  65. package/test/components/account/Layout.test.tsx +62 -0
  66. package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
  67. package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
  68. package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
  69. package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
  70. package/test/components/account/doLogout.browser.test.ts +102 -0
  71. package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
  72. package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
  73. package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
  74. package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
  75. package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
  76. package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
  77. package/test/components/quotes/quoteFilters.test.ts +185 -0
  78. package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
  79. package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
  80. package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
  81. package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
  82. package/test/sdk/account/refreshToken.browser.test.ts +34 -0
  83. package/test/sdk/account/refreshToken.test.ts +87 -0
  84. package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
  85. package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
  86. package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
  87. package/test/sdk/analytics/utils.test.ts +23 -0
  88. package/test/server/index.test.ts +5 -0
  89. 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
+ })