@faststore/core 4.8.1-dev.1 → 4.8.1-dev.2
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 +3 -3
- package/.turbo/turbo-test.log +126 -123
- package/@generated/gql.ts +3 -3
- package/@generated/graphql.ts +4 -2
- package/@generated/persisted-documents.json +1 -1
- package/@generated/schema.graphql +4 -0
- package/CHANGELOG.md +6 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +4 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +18 -3
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +1 -0
- package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +7 -0
- package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +7 -9
- package/src/components/account/Menu/Menu.tsx +24 -7
- package/src/components/navigation/Navbar/Navbar.tsx +17 -19
- package/src/components/navigation/Navbar/NavbarAccountArea.tsx +57 -0
- package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +16 -18
- package/src/pages/api/fs/switch-contract.ts +194 -0
- package/src/sdk/account/changeContractToken.ts +19 -63
- package/src/sdk/account/clearPersistedSessionState.ts +13 -7
- package/src/sdk/account/useAvailableContracts.ts +1 -0
- package/src/sdk/account/useSwitchContract.ts +11 -3
- package/src/sdk/session/index.ts +2 -1
- package/src/sdk/session/isSignInAreaResolved.ts +12 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +42 -0
- package/test/components/account/Menu.browser.test.tsx +123 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +148 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +2 -0
- package/test/components/account/OrganizationSignInButton.browser.test.tsx +49 -0
- package/test/components/navigation/Navbar.browser.test.tsx +183 -0
- package/test/components/navigation/NavbarAccountArea.browser.test.tsx +115 -0
- package/test/components/navigation/NavbarSlider.browser.test.tsx +143 -0
- package/test/pages/api/switch-contract.test.ts +385 -0
- package/test/sdk/account/changeContractToken.browser.test.ts +23 -71
- package/test/sdk/account/clearPersistedSessionState.browser.test.ts +24 -0
- package/test/sdk/account/useSwitchContract.browser.test.ts +21 -0
- package/test/sdk/session/isSignInAreaResolved.test.ts +22 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import fetch from 'isomorphic-unfetch'
|
|
2
2
|
|
|
3
|
-
import storeConfig from '../../../discovery.config'
|
|
4
|
-
|
|
5
3
|
export class ContractSwitchError extends Error {
|
|
6
4
|
constructor(message: string) {
|
|
7
5
|
super(message)
|
|
@@ -11,51 +9,20 @@ export class ContractSwitchError extends Error {
|
|
|
11
9
|
|
|
12
10
|
export const isContractSwitchEnabled = true
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
Name: string
|
|
16
|
-
Value: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type SwitchPropertiesResponse = {
|
|
20
|
-
authStatus?: string
|
|
21
|
-
expiresIn?: number
|
|
22
|
-
authCookie?: VtexAuthCookie | null
|
|
23
|
-
accountAuthCookie?: VtexAuthCookie | null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_AUTH_COOKIE_MAX_AGE = 86_400
|
|
27
|
-
const SWITCH_PROPERTIES_TIMEOUT_MS = 30_000
|
|
28
|
-
|
|
29
|
-
export function applyVtexAuthCookieFromSwitchResponse(
|
|
30
|
-
response: SwitchPropertiesResponse
|
|
31
|
-
): void {
|
|
32
|
-
if (globalThis.window === undefined) {
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const maxAge = response.expiresIn ?? DEFAULT_AUTH_COOKIE_MAX_AGE
|
|
37
|
-
const secure =
|
|
38
|
-
globalThis.window.location.protocol === 'https:' ? '; secure' : ''
|
|
39
|
-
|
|
40
|
-
const cookies = [response.authCookie, response.accountAuthCookie].filter(
|
|
41
|
-
(cookie): cookie is VtexAuthCookie =>
|
|
42
|
-
Boolean(cookie?.Name?.trim() && cookie?.Value)
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
for (const cookie of cookies) {
|
|
46
|
-
document.cookie = `${cookie.Name}=${cookie.Value}; path=/; max-age=${maxAge}; samesite=lax${secure}`
|
|
47
|
-
}
|
|
48
|
-
}
|
|
12
|
+
const SWITCH_CONTRACT_TIMEOUT_MS = 30_000
|
|
49
13
|
|
|
50
14
|
/**
|
|
51
|
-
* Switches the buyer's active contract
|
|
15
|
+
* Switches the buyer's active contract through `/api/fs/switch-contract`.
|
|
52
16
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
17
|
+
* The route relays the call to VTEX Identity and answers with a single
|
|
18
|
+
* `Set-Cookie` batch: the new auth cookie plus the expiry of the previous
|
|
19
|
+
* contract's checkout orderForm cookies, which are `HttpOnly` and therefore
|
|
20
|
+
* unreachable from here. Because both travel in the same response, there is no
|
|
21
|
+
* window where the browser holds the new credential alongside the old
|
|
22
|
+
* orderForm (B2BTEAM-3827).
|
|
55
23
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* on hard failures so `switchContract` can keep the previous contract active.
|
|
24
|
+
* Throws `ContractSwitchError` on any failure so `switchContract` can keep the
|
|
25
|
+
* previous contract active.
|
|
59
26
|
*/
|
|
60
27
|
export async function changeContractToken(
|
|
61
28
|
contractId: string
|
|
@@ -64,23 +31,16 @@ export async function changeContractToken(
|
|
|
64
31
|
throw new ContractSwitchError('Missing contractId for contract switch')
|
|
65
32
|
}
|
|
66
33
|
|
|
67
|
-
const an = encodeURIComponent(storeConfig.api.storeId)
|
|
68
|
-
const url = `/api/authenticator/storefront/credential/switch-properties?an=${an}`
|
|
69
|
-
|
|
70
34
|
let response: Response
|
|
71
35
|
try {
|
|
72
|
-
response = await fetch(
|
|
36
|
+
response = await fetch('/api/fs/switch-contract', {
|
|
73
37
|
method: 'POST',
|
|
74
38
|
credentials: 'include',
|
|
75
39
|
headers: {
|
|
76
40
|
'Content-Type': 'application/json',
|
|
77
41
|
},
|
|
78
|
-
body: JSON.stringify({
|
|
79
|
-
|
|
80
|
-
customerId: contractId,
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
-
signal: AbortSignal.timeout(SWITCH_PROPERTIES_TIMEOUT_MS),
|
|
42
|
+
body: JSON.stringify({ contractId }),
|
|
43
|
+
signal: AbortSignal.timeout(SWITCH_CONTRACT_TIMEOUT_MS),
|
|
84
44
|
})
|
|
85
45
|
} catch (error) {
|
|
86
46
|
if (
|
|
@@ -101,19 +61,15 @@ export async function changeContractToken(
|
|
|
101
61
|
)
|
|
102
62
|
}
|
|
103
63
|
|
|
104
|
-
|
|
64
|
+
// Reading the body to completion guarantees the `Set-Cookie` headers are
|
|
65
|
+
// committed before `switchContract` reloads the page.
|
|
66
|
+
const payload = (await response.json().catch(() => null)) as {
|
|
67
|
+
success?: boolean
|
|
68
|
+
} | null
|
|
105
69
|
|
|
106
|
-
if (payload
|
|
70
|
+
if (!payload?.success) {
|
|
107
71
|
throw new ContractSwitchError('Contract switch was not successful')
|
|
108
72
|
}
|
|
109
73
|
|
|
110
|
-
if (!payload.authCookie?.Name || !payload.authCookie?.Value) {
|
|
111
|
-
throw new ContractSwitchError(
|
|
112
|
-
'Contract switch succeeded but no auth cookie was returned'
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
applyVtexAuthCookieFromSwitchResponse(payload)
|
|
117
|
-
|
|
118
74
|
return true
|
|
119
75
|
}
|
|
@@ -4,20 +4,26 @@ import {
|
|
|
4
4
|
STORAGE_KEY_PERSON_ID,
|
|
5
5
|
} from 'src/utils/cookieCacheBusting'
|
|
6
6
|
|
|
7
|
+
/** Persisted `@faststore/sdk` stores that belong to the previous commercial context. */
|
|
8
|
+
export const PERSISTED_STORE_KEYS = ['fs::session', 'fs::cart'] as const
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
|
-
* Clears client-side
|
|
9
|
-
*
|
|
11
|
+
* Clears client-side persistence of the previous commercial context so a hard
|
|
12
|
+
* reload rehydrates from validateSession / validateCart under the new
|
|
13
|
+
* contract.
|
|
14
|
+
*
|
|
15
|
+
* The checkout orderForm cookies are `HttpOnly` and are expired by
|
|
16
|
+
* `/api/fs/switch-contract` instead (B2BTEAM-3827).
|
|
10
17
|
*/
|
|
11
18
|
export async function clearPersistedSessionState(): Promise<void> {
|
|
12
19
|
try {
|
|
13
20
|
const { del } = await import('idb-keyval')
|
|
14
|
-
await
|
|
21
|
+
await Promise.all(
|
|
22
|
+
PERSISTED_STORE_KEYS.map((key) => del(key).catch(() => {}))
|
|
23
|
+
)
|
|
15
24
|
} catch {}
|
|
16
25
|
|
|
17
|
-
if (typeof sessionStorage === 'undefined')
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
26
|
+
if (typeof sessionStorage === 'undefined') return
|
|
21
27
|
try {
|
|
22
28
|
sessionStorage.removeItem(SESSION_READY_KEY)
|
|
23
29
|
sessionStorage.removeItem(STORAGE_KEY_PERSON_ID)
|
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
* Orchestrates a full change of commercial context (REQ-06).
|
|
12
12
|
*
|
|
13
13
|
* Steps:
|
|
14
|
-
* 1. POST switch-
|
|
15
|
-
*
|
|
14
|
+
* 1. POST `/api/fs/switch-contract`, which swaps the auth cookie and expires
|
|
15
|
+
* the previous contract's checkout orderForm in one response. A failure
|
|
16
|
+
* here changes nothing, so the previous contract stays active.
|
|
17
|
+
* 2. Clear persisted session and cart state and reload the page so
|
|
18
|
+
* validateSession/validateCart run under the new contract.
|
|
16
19
|
*/
|
|
17
20
|
export const useSwitchContract = () => {
|
|
18
21
|
const [loading, setLoading] = useState(false)
|
|
@@ -31,7 +34,12 @@ export const useSwitchContract = () => {
|
|
|
31
34
|
return false
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
// The cookies were already swapped, so the reload has to happen even if
|
|
38
|
+
// clearing the local stores fails: from here on the server session is
|
|
39
|
+
// the authoritative source and validateCart rebuilds the cart.
|
|
40
|
+
try {
|
|
41
|
+
await clearPersistedSessionState()
|
|
42
|
+
} catch {}
|
|
35
43
|
|
|
36
44
|
if (globalThis.window !== undefined) {
|
|
37
45
|
globalThis.window.location.reload()
|
package/src/sdk/session/index.ts
CHANGED
|
@@ -282,8 +282,9 @@ export const useSession = ({ filter }: SessionOptions = { filter: true }) => {
|
|
|
282
282
|
channel,
|
|
283
283
|
isValidating,
|
|
284
284
|
isSessionReady,
|
|
285
|
+
hasValidated,
|
|
285
286
|
}),
|
|
286
|
-
[isValidating, session, channel, isSessionReady]
|
|
287
|
+
[isValidating, session, channel, isSessionReady, hasValidated]
|
|
287
288
|
)
|
|
288
289
|
}
|
|
289
290
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Navbar account area must not render "Sign in" for a B2B buyer whose
|
|
3
|
+
* session has not been validated yet on this page load (Design QA 26/Aug
|
|
4
|
+
* 00:02:55–00:03:52). `isSessionReady` alone is pre-seeded from sessionStorage.
|
|
5
|
+
*/
|
|
6
|
+
export const isSignInAreaResolved = ({
|
|
7
|
+
isSessionReady,
|
|
8
|
+
hasValidated,
|
|
9
|
+
}: {
|
|
10
|
+
isSessionReady: boolean
|
|
11
|
+
hasValidated: boolean
|
|
12
|
+
}): boolean => isSessionReady && hasValidated
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @vitest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import '@testing-library/jest-dom/vitest'
|
|
5
6
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
8
|
|
|
@@ -117,6 +118,47 @@ describe('ContractSwitcher', () => {
|
|
|
117
118
|
expect(options.length).toBe(2)
|
|
118
119
|
})
|
|
119
120
|
|
|
121
|
+
it('marks the default contract with a star and lists it first (Design QA 00:02:00)', async () => {
|
|
122
|
+
// Note: id 'a' is pinned to the current session (this file mocks
|
|
123
|
+
// useSession with b2b.customerId: 'a'), so it must stay on the active
|
|
124
|
+
// contract for it to resolve as the current one; Alpha (the default)
|
|
125
|
+
// and Aardvark get other ids and land in the alternatives list, which
|
|
126
|
+
// is what this test exercises.
|
|
127
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
128
|
+
contracts: [
|
|
129
|
+
{ id: 'a', corporateName: 'Beta', isActive: true, isDefault: false },
|
|
130
|
+
{ id: 'b', corporateName: 'Alpha', isActive: false, isDefault: true },
|
|
131
|
+
{
|
|
132
|
+
id: 'c',
|
|
133
|
+
corporateName: 'Aardvark',
|
|
134
|
+
isActive: false,
|
|
135
|
+
isDefault: false,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
loading: false,
|
|
139
|
+
error: null,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const { container } = render(
|
|
143
|
+
<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await screen.findByText('Alpha')
|
|
147
|
+
expect(
|
|
148
|
+
container.querySelectorAll('[data-fs-contract-switcher-default]')
|
|
149
|
+
).toHaveLength(1)
|
|
150
|
+
expect(
|
|
151
|
+
container.querySelector('[data-fs-contract-switcher-default]')
|
|
152
|
+
).toHaveAttribute('aria-label', 'Default contract')
|
|
153
|
+
|
|
154
|
+
const names = Array.from(
|
|
155
|
+
container.querySelectorAll(
|
|
156
|
+
'[data-fs-contract-switcher-option] [data-fs-contract-switcher-option-name]'
|
|
157
|
+
)
|
|
158
|
+
).map((el) => el.textContent)
|
|
159
|
+
expect(names[0]).toBe('Alpha') // default first, then alphabetical ('Aardvark')
|
|
160
|
+
})
|
|
161
|
+
|
|
120
162
|
it('requires selecting a contract before Confirm is enabled, then switches (REQ-03)', async () => {
|
|
121
163
|
render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
|
|
122
164
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '@testing-library/jest-dom/vitest'
|
|
6
|
+
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockUseScreenResize = vi.hoisted(() => vi.fn())
|
|
11
|
+
const mockOrganizationDrawer = vi.hoisted(() => vi.fn())
|
|
12
|
+
|
|
13
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
14
|
+
|
|
15
|
+
vi.mock('src/sdk/account/accountPageContext', () => ({
|
|
16
|
+
useAccountNavigationLabels: () => ({ switchLabel: 'Switch' }),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
vi.mock('src/sdk/ui/useScreenResize', () => ({
|
|
20
|
+
default: mockUseScreenResize,
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
vi.mock('next/router', () => ({
|
|
24
|
+
useRouter: () => ({ pathname: '/pvt/account' }),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
vi.mock(
|
|
28
|
+
'../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer',
|
|
29
|
+
() => ({
|
|
30
|
+
OrganizationDrawer: (props: Record<string, unknown>) => {
|
|
31
|
+
mockOrganizationDrawer(props)
|
|
32
|
+
return <div data-testid="organization-drawer" />
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
import Menu from '../../../src/components/account/Menu/Menu'
|
|
38
|
+
|
|
39
|
+
const items = [{ title: 'Orders', route: '/pvt/account/orders' }]
|
|
40
|
+
|
|
41
|
+
describe('Menu', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.clearAllMocks()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('does not render the Switch button without an organization unit', () => {
|
|
51
|
+
mockUseSession.mockReturnValue({ b2b: {} })
|
|
52
|
+
|
|
53
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
54
|
+
|
|
55
|
+
expect(screen.queryByRole('button', { name: 'Switch' })).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders the Switch button when the buyer has an organization unit', () => {
|
|
59
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
60
|
+
|
|
61
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
62
|
+
|
|
63
|
+
expect(screen.getByRole('button', { name: 'Switch' })).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('mounts the OrganizationDrawer with initialView "switch" when Switch is clicked', () => {
|
|
67
|
+
mockUseSession.mockReturnValue({
|
|
68
|
+
b2b: { unitId: 'unit-1', isRepresentative: true },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
72
|
+
|
|
73
|
+
expect(screen.queryByTestId('organization-drawer')).toBeNull()
|
|
74
|
+
|
|
75
|
+
fireEvent.click(screen.getByRole('button', { name: 'Switch' }))
|
|
76
|
+
|
|
77
|
+
expect(screen.getByTestId('organization-drawer')).toBeTruthy()
|
|
78
|
+
expect(mockOrganizationDrawer).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
isOpen: true,
|
|
81
|
+
initialView: 'switch',
|
|
82
|
+
isRepresentative: true,
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('passes isRepresentative as a boolean even when b2b.isRepresentative is undefined', () => {
|
|
88
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
89
|
+
|
|
90
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
91
|
+
fireEvent.click(screen.getByRole('button', { name: 'Switch' }))
|
|
92
|
+
|
|
93
|
+
expect(mockOrganizationDrawer).toHaveBeenCalledWith(
|
|
94
|
+
expect.objectContaining({ isRepresentative: false })
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('unmounts the OrganizationDrawer when closeDrawer is invoked', () => {
|
|
99
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
100
|
+
|
|
101
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
102
|
+
fireEvent.click(screen.getByRole('button', { name: 'Switch' }))
|
|
103
|
+
expect(screen.getByTestId('organization-drawer')).toBeTruthy()
|
|
104
|
+
|
|
105
|
+
const closeDrawer = mockOrganizationDrawer.mock.calls[0][0]
|
|
106
|
+
.closeDrawer as () => void
|
|
107
|
+
act(() => {
|
|
108
|
+
closeDrawer()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(screen.queryByTestId('organization-drawer')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('renders only the nav list on mobile (no account/switch area)', () => {
|
|
115
|
+
mockUseScreenResize.mockReturnValue({ isDesktop: false })
|
|
116
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
117
|
+
|
|
118
|
+
render(<Menu accountName="Jane" items={items} />)
|
|
119
|
+
|
|
120
|
+
expect(screen.queryByRole('button', { name: 'Switch' })).toBeNull()
|
|
121
|
+
expect(screen.getByText('Orders')).toBeTruthy()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @vitest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import '@testing-library/jest-dom/vitest'
|
|
5
6
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
6
7
|
import type { ReactNode } from 'react'
|
|
7
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -9,6 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
9
10
|
const mockFadeOut = vi.hoisted(() => vi.fn())
|
|
10
11
|
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
11
12
|
const mockCloseDrawer = vi.hoisted(() => vi.fn())
|
|
13
|
+
const mockUseAvailableContracts = vi.hoisted(() => vi.fn())
|
|
12
14
|
|
|
13
15
|
vi.mock('@faststore/ui', async (importOriginal) => {
|
|
14
16
|
const actual = await importOriginal<typeof import('@faststore/ui')>()
|
|
@@ -38,6 +40,10 @@ vi.mock('@faststore/ui', async (importOriginal) => {
|
|
|
38
40
|
|
|
39
41
|
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
40
42
|
|
|
43
|
+
vi.mock('src/sdk/account/useAvailableContracts', () => ({
|
|
44
|
+
useAvailableContracts: mockUseAvailableContracts,
|
|
45
|
+
}))
|
|
46
|
+
|
|
41
47
|
vi.mock(
|
|
42
48
|
'../../../src/components/account/Drawer/OrganizationDrawer/ContractSwitcher',
|
|
43
49
|
() => ({
|
|
@@ -82,6 +88,11 @@ describe('OrganizationDrawer', () => {
|
|
|
82
88
|
},
|
|
83
89
|
person: { givenName: 'Jane' },
|
|
84
90
|
})
|
|
91
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
92
|
+
contracts: [],
|
|
93
|
+
loading: false,
|
|
94
|
+
error: null,
|
|
95
|
+
})
|
|
85
96
|
})
|
|
86
97
|
|
|
87
98
|
afterEach(() => {
|
|
@@ -194,4 +205,141 @@ describe('OrganizationDrawer', () => {
|
|
|
194
205
|
screen.getByRole('heading', { name: 'Organization', level: 1 })
|
|
195
206
|
).toBeTruthy()
|
|
196
207
|
})
|
|
208
|
+
|
|
209
|
+
it('opens directly in the switcher when initialView is "switch"', () => {
|
|
210
|
+
render(
|
|
211
|
+
<OrganizationDrawer
|
|
212
|
+
isOpen
|
|
213
|
+
closeDrawer={vi.fn()}
|
|
214
|
+
isRepresentative
|
|
215
|
+
initialView="switch"
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
expect(screen.getByTestId('contract-switcher')).toBeInTheDocument()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('shows the default-contract star in the header when the active contract is the unit default', () => {
|
|
222
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
223
|
+
contracts: [
|
|
224
|
+
{
|
|
225
|
+
id: 'unit-1',
|
|
226
|
+
corporateName: 'Acme Contract',
|
|
227
|
+
isActive: true,
|
|
228
|
+
isDefault: true,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'other',
|
|
232
|
+
corporateName: 'Other Contract',
|
|
233
|
+
isActive: false,
|
|
234
|
+
isDefault: false,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
loading: false,
|
|
238
|
+
error: null,
|
|
239
|
+
})
|
|
240
|
+
mockUseSession.mockReturnValue({
|
|
241
|
+
b2b: {
|
|
242
|
+
unitId: 'unit-1',
|
|
243
|
+
unitName: 'Stellar Global',
|
|
244
|
+
contractName: 'Acme Contract',
|
|
245
|
+
customerId: 'unit-1',
|
|
246
|
+
userName: 'Jane Buyer',
|
|
247
|
+
userEmail: 'jane@example.com',
|
|
248
|
+
organizationManager: true,
|
|
249
|
+
},
|
|
250
|
+
person: { givenName: 'Jane' },
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
render(
|
|
254
|
+
<OrganizationDrawer
|
|
255
|
+
isOpen
|
|
256
|
+
closeDrawer={mockCloseDrawer}
|
|
257
|
+
isRepresentative={false}
|
|
258
|
+
/>
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(screen.getByLabelText('Default contract')).toBeTruthy()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('does not show the default-contract star when the active contract is not the unit default', () => {
|
|
265
|
+
mockUseAvailableContracts.mockReturnValue({
|
|
266
|
+
contracts: [
|
|
267
|
+
{
|
|
268
|
+
id: 'unit-1',
|
|
269
|
+
corporateName: 'Acme Contract',
|
|
270
|
+
isActive: true,
|
|
271
|
+
isDefault: false,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'other',
|
|
275
|
+
corporateName: 'Other Contract',
|
|
276
|
+
isActive: false,
|
|
277
|
+
isDefault: true,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
loading: false,
|
|
281
|
+
error: null,
|
|
282
|
+
})
|
|
283
|
+
mockUseSession.mockReturnValue({
|
|
284
|
+
b2b: {
|
|
285
|
+
unitId: 'unit-1',
|
|
286
|
+
unitName: 'Stellar Global',
|
|
287
|
+
contractName: 'Acme Contract',
|
|
288
|
+
customerId: 'unit-1',
|
|
289
|
+
userName: 'Jane Buyer',
|
|
290
|
+
userEmail: 'jane@example.com',
|
|
291
|
+
organizationManager: true,
|
|
292
|
+
},
|
|
293
|
+
person: { givenName: 'Jane' },
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
render(
|
|
297
|
+
<OrganizationDrawer
|
|
298
|
+
isOpen
|
|
299
|
+
closeDrawer={mockCloseDrawer}
|
|
300
|
+
isRepresentative={false}
|
|
301
|
+
/>
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
expect(screen.queryByLabelText('Default contract')).toBeNull()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('fetches the contract list only while open for an org member', () => {
|
|
308
|
+
const { rerender } = render(
|
|
309
|
+
<OrganizationDrawer
|
|
310
|
+
isOpen={false}
|
|
311
|
+
closeDrawer={mockCloseDrawer}
|
|
312
|
+
isRepresentative={false}
|
|
313
|
+
/>
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
expect(mockUseAvailableContracts).toHaveBeenLastCalledWith(false)
|
|
317
|
+
|
|
318
|
+
rerender(
|
|
319
|
+
<OrganizationDrawer
|
|
320
|
+
isOpen
|
|
321
|
+
closeDrawer={mockCloseDrawer}
|
|
322
|
+
isRepresentative={false}
|
|
323
|
+
/>
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
expect(mockUseAvailableContracts).toHaveBeenLastCalledWith(true)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('never fetches the contract list when the buyer has no organization unit', () => {
|
|
330
|
+
mockUseSession.mockReturnValue({
|
|
331
|
+
b2b: { userName: 'Jane Buyer' },
|
|
332
|
+
person: null,
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
render(
|
|
336
|
+
<OrganizationDrawer
|
|
337
|
+
isOpen
|
|
338
|
+
closeDrawer={mockCloseDrawer}
|
|
339
|
+
isRepresentative={false}
|
|
340
|
+
/>
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
expect(mockUseAvailableContracts).toHaveBeenLastCalledWith(false)
|
|
344
|
+
})
|
|
197
345
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import '@testing-library/jest-dom/vitest'
|
|
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
|
+
vi.mock('src/sdk/account/accountPageContext', () => ({
|
|
11
|
+
useAccountNavigationLabels: () => ({
|
|
12
|
+
companyLabel: 'Company',
|
|
13
|
+
contractLabel: 'Contract',
|
|
14
|
+
}),
|
|
15
|
+
}))
|
|
16
|
+
vi.mock(
|
|
17
|
+
'src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer',
|
|
18
|
+
() => ({
|
|
19
|
+
OrganizationDrawer: () => null,
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import { OrganizationSignInButton } from '../../../src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton'
|
|
24
|
+
|
|
25
|
+
const icon = { alt: 'account', icon: 'User' }
|
|
26
|
+
|
|
27
|
+
describe('OrganizationSignInButton', () => {
|
|
28
|
+
it('shows the active contract name when the session has one (Design QA 00:02:55)', () => {
|
|
29
|
+
mockUseSession.mockReturnValue({
|
|
30
|
+
person: { id: 'p1' },
|
|
31
|
+
b2b: { contractName: ' Caetano Corp ' },
|
|
32
|
+
})
|
|
33
|
+
render(<OrganizationSignInButton icon={icon} isRepresentative />)
|
|
34
|
+
expect(screen.getByRole('button', { name: 'account' })).toHaveTextContent(
|
|
35
|
+
'Caetano Corp'
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('falls back to the company label without a contract name', () => {
|
|
40
|
+
mockUseSession.mockReturnValue({
|
|
41
|
+
person: { id: 'p1' },
|
|
42
|
+
b2b: { contractName: '' },
|
|
43
|
+
})
|
|
44
|
+
render(<OrganizationSignInButton icon={icon} isRepresentative />)
|
|
45
|
+
expect(screen.getByRole('button', { name: 'account' })).toHaveTextContent(
|
|
46
|
+
'Company'
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
})
|