@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,87 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockFetch = vi.hoisted(() => vi.fn())
|
|
4
|
+
|
|
5
|
+
vi.mock('isomorphic-unfetch', () => ({ __esModule: true, default: mockFetch }))
|
|
6
|
+
|
|
7
|
+
vi.mock('src/sdk/localization/useLocalizationConfig', () => ({
|
|
8
|
+
getStoreURL: () => 'https://b2bfaststore.vtexfaststore.com',
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('src/utils/utilities', () => ({
|
|
12
|
+
sanitizeHost: (origin: string) => origin.replace(/^https?:\/\//, ''),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
isRefreshTokenSuccessful,
|
|
17
|
+
refreshTokenRequest,
|
|
18
|
+
} from '../../../src/sdk/account/refreshToken'
|
|
19
|
+
|
|
20
|
+
describe('refreshTokenRequest (node)', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockFetch.mockResolvedValue({
|
|
23
|
+
status: 200,
|
|
24
|
+
json: async () => ({ status: 'success', refreshAfter: '123' }),
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.clearAllMocks()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('posts to the store origin with Host header on the server', async () => {
|
|
33
|
+
const result = await refreshTokenRequest()
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({ status: 'success', refreshAfter: '123' })
|
|
36
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
37
|
+
|
|
38
|
+
const [url, init] = mockFetch.mock.calls[0]
|
|
39
|
+
expect(url).toBe(
|
|
40
|
+
'https://b2bfaststore.vtexfaststore.com/api/vtexid/refreshtoken/webstore'
|
|
41
|
+
)
|
|
42
|
+
expect(init.method).toBe('POST')
|
|
43
|
+
expect(init.credentials).toBe('include')
|
|
44
|
+
expect(init.headers).toMatchObject({
|
|
45
|
+
'content-type': 'application/json',
|
|
46
|
+
Host: 'b2bfaststore.vtexfaststore.com',
|
|
47
|
+
})
|
|
48
|
+
expect(JSON.parse(init.body as string)).toEqual({})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('retries when the response is not 200 and returns undefined after failures', async () => {
|
|
52
|
+
mockFetch
|
|
53
|
+
.mockResolvedValueOnce({ status: 500, json: async () => ({}) })
|
|
54
|
+
.mockResolvedValueOnce({ status: 500, json: async () => ({}) })
|
|
55
|
+
.mockResolvedValueOnce({ status: 500, json: async () => ({}) })
|
|
56
|
+
|
|
57
|
+
const result = await refreshTokenRequest()
|
|
58
|
+
|
|
59
|
+
expect(result).toBeUndefined()
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledTimes(3)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('retries after network errors and succeeds on a later attempt', async () => {
|
|
64
|
+
mockFetch
|
|
65
|
+
.mockRejectedValueOnce(new Error('network'))
|
|
66
|
+
.mockResolvedValueOnce({
|
|
67
|
+
status: 200,
|
|
68
|
+
json: async () => ({ status: 'success' }),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const result = await refreshTokenRequest()
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual({ status: 'success' })
|
|
74
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('isRefreshTokenSuccessful', () => {
|
|
79
|
+
it('returns true when status is success (case-insensitive)', () => {
|
|
80
|
+
expect(isRefreshTokenSuccessful({ status: 'Success' })).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns false when status is missing or not success', () => {
|
|
84
|
+
expect(isRefreshTokenSuccessful(undefined)).toBe(false)
|
|
85
|
+
expect(isRefreshTokenSuccessful({ status: 'failed' })).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react'
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockUseQuery = vi.hoisted(() => vi.fn())
|
|
10
|
+
|
|
11
|
+
vi.mock('@generated', () => ({ gql: (query: unknown) => query }))
|
|
12
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
13
|
+
vi.mock('src/sdk/graphql/useQuery', () => ({ useQuery: mockUseQuery }))
|
|
14
|
+
|
|
15
|
+
import { useAvailableContracts } from '../../../src/sdk/account/useAvailableContracts'
|
|
16
|
+
|
|
17
|
+
const contracts = [
|
|
18
|
+
{ id: 'a', corporateName: 'Corp A', isActive: true },
|
|
19
|
+
{ id: 'b', corporateName: 'Corp B', isActive: false },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
describe('useAvailableContracts', () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('skips the query when the hook is disabled', () => {
|
|
28
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
29
|
+
mockUseQuery.mockReturnValue({
|
|
30
|
+
data: undefined,
|
|
31
|
+
error: null,
|
|
32
|
+
isValidating: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const { result } = renderHook(() => useAvailableContracts(false))
|
|
36
|
+
|
|
37
|
+
expect(result.current.contracts).toEqual([])
|
|
38
|
+
expect(result.current.loading).toBe(false)
|
|
39
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
40
|
+
expect.anything(),
|
|
41
|
+
{ orgUnitId: 'unit-1' },
|
|
42
|
+
expect.objectContaining({ doNotRun: true })
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('skips the query when orgUnitId is missing', () => {
|
|
47
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: '' } })
|
|
48
|
+
mockUseQuery.mockReturnValue({
|
|
49
|
+
data: undefined,
|
|
50
|
+
error: null,
|
|
51
|
+
isValidating: false,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useAvailableContracts(true))
|
|
55
|
+
|
|
56
|
+
expect(result.current.contracts).toEqual([])
|
|
57
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
58
|
+
expect.anything(),
|
|
59
|
+
{ orgUnitId: '' },
|
|
60
|
+
expect.objectContaining({ doNotRun: true })
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns loading while fetching contracts for the current org unit', () => {
|
|
65
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
66
|
+
mockUseQuery.mockReturnValue({
|
|
67
|
+
data: undefined,
|
|
68
|
+
error: null,
|
|
69
|
+
isValidating: false,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const { result } = renderHook(() => useAvailableContracts(true))
|
|
73
|
+
|
|
74
|
+
expect(result.current.loading).toBe(true)
|
|
75
|
+
expect(result.current.contracts).toEqual([])
|
|
76
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
77
|
+
expect.anything(),
|
|
78
|
+
{ orgUnitId: 'unit-1' },
|
|
79
|
+
expect.objectContaining({ doNotRun: false, dedupingInterval: 0 })
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns contracts when the query succeeds', () => {
|
|
84
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
85
|
+
mockUseQuery.mockReturnValue({
|
|
86
|
+
data: { availableContracts: contracts },
|
|
87
|
+
error: null,
|
|
88
|
+
isValidating: false,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(() => useAvailableContracts(true))
|
|
92
|
+
|
|
93
|
+
expect(result.current.contracts).toEqual(contracts)
|
|
94
|
+
expect(result.current.error).toBeNull()
|
|
95
|
+
expect(result.current.loading).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns query errors without loading', () => {
|
|
99
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
100
|
+
mockUseQuery.mockReturnValue({
|
|
101
|
+
data: undefined,
|
|
102
|
+
error: new Error('boom'),
|
|
103
|
+
isValidating: false,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const { result } = renderHook(() => useAvailableContracts(true))
|
|
107
|
+
|
|
108
|
+
expect(result.current.error).toBeInstanceOf(Error)
|
|
109
|
+
expect(result.current.loading).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('keeps loading while the query is validating', () => {
|
|
113
|
+
mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
|
|
114
|
+
mockUseQuery.mockReturnValue({
|
|
115
|
+
data: { availableContracts: contracts },
|
|
116
|
+
error: null,
|
|
117
|
+
isValidating: true,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const { result } = renderHook(() => useAvailableContracts(true))
|
|
121
|
+
|
|
122
|
+
expect(result.current.loading).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockReload = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockClearPersistedSessionState = vi.hoisted(() => vi.fn())
|
|
10
|
+
|
|
11
|
+
vi.mock('src/sdk/account/clearPersistedSessionState', () => ({
|
|
12
|
+
clearPersistedSessionState: mockClearPersistedSessionState,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
import * as changeContractTokenModule from '../../../src/sdk/account/changeContractToken'
|
|
16
|
+
import { useSwitchContract } from '../../../src/sdk/account/useSwitchContract'
|
|
17
|
+
|
|
18
|
+
describe('useSwitchContract', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockClearPersistedSessionState.mockResolvedValue(undefined)
|
|
21
|
+
mockReload.mockReset()
|
|
22
|
+
Object.defineProperty(globalThis.window, 'location', {
|
|
23
|
+
configurable: true,
|
|
24
|
+
value: { ...globalThis.window.location, reload: mockReload },
|
|
25
|
+
})
|
|
26
|
+
vi.spyOn(
|
|
27
|
+
changeContractTokenModule,
|
|
28
|
+
'changeContractToken'
|
|
29
|
+
).mockResolvedValue(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.clearAllMocks()
|
|
34
|
+
vi.restoreAllMocks()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not clear session or reload when switch-properties returns false', async () => {
|
|
38
|
+
const { result } = renderHook(() => useSwitchContract())
|
|
39
|
+
|
|
40
|
+
let ok: boolean | undefined
|
|
41
|
+
await act(async () => {
|
|
42
|
+
ok = await result.current.switchContract('contract-2')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(ok).toBe(false)
|
|
46
|
+
expect(mockClearPersistedSessionState).not.toHaveBeenCalled()
|
|
47
|
+
expect(mockReload).not.toHaveBeenCalled()
|
|
48
|
+
expect(result.current.enabled).toBe(true)
|
|
49
|
+
|
|
50
|
+
await waitFor(() => expect(result.current.error).toBeInstanceOf(Error))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('clears persisted session and reloads after a successful switch (REQ-06)', async () => {
|
|
54
|
+
vi.spyOn(
|
|
55
|
+
changeContractTokenModule,
|
|
56
|
+
'changeContractToken'
|
|
57
|
+
).mockResolvedValueOnce(true)
|
|
58
|
+
|
|
59
|
+
const { result } = renderHook(() => useSwitchContract())
|
|
60
|
+
|
|
61
|
+
let ok: boolean | undefined
|
|
62
|
+
await act(async () => {
|
|
63
|
+
ok = await result.current.switchContract('contract-2')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(ok).toBe(true)
|
|
67
|
+
expect(mockClearPersistedSessionState).toHaveBeenCalledTimes(1)
|
|
68
|
+
expect(mockReload).toHaveBeenCalledTimes(1)
|
|
69
|
+
expect(result.current.error).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('surfaces an error when switch-properties fails', async () => {
|
|
73
|
+
vi.spyOn(
|
|
74
|
+
changeContractTokenModule,
|
|
75
|
+
'changeContractToken'
|
|
76
|
+
).mockRejectedValueOnce(new Error('switch failed'))
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() => useSwitchContract())
|
|
79
|
+
|
|
80
|
+
let ok: boolean | undefined
|
|
81
|
+
await act(async () => {
|
|
82
|
+
ok = await result.current.switchContract('contract-2')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(ok).toBe(false)
|
|
86
|
+
expect(mockClearPersistedSessionState).not.toHaveBeenCalled()
|
|
87
|
+
expect(mockReload).not.toHaveBeenCalled()
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(result.current.error).toBeInstanceOf(Error))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('normalizes non-Error failures from switch-properties', async () => {
|
|
93
|
+
vi.spyOn(
|
|
94
|
+
changeContractTokenModule,
|
|
95
|
+
'changeContractToken'
|
|
96
|
+
).mockRejectedValueOnce('unexpected')
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(() => useSwitchContract())
|
|
99
|
+
|
|
100
|
+
let ok: boolean | undefined
|
|
101
|
+
await act(async () => {
|
|
102
|
+
ok = await result.current.switchContract('contract-2')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(ok).toBe(false)
|
|
106
|
+
|
|
107
|
+
await waitFor(() =>
|
|
108
|
+
expect(result.current.error?.message).toBe('Failed to switch contract')
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, waitFor } from '@testing-library/react'
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
vi.mock('@faststore/core/api', () => ({
|
|
9
|
+
gql: (s: string) => ({ __meta__: {}, query: s }),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const runStartRecommendationSession = vi.hoisted(() => vi.fn())
|
|
13
|
+
const useLazyQueryMock = vi.hoisted(() => vi.fn())
|
|
14
|
+
vi.mock('src/sdk/graphql/useLazyQuery', () => ({
|
|
15
|
+
useLazyQuery: useLazyQueryMock,
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const getCookie = vi.hoisted(() => vi.fn())
|
|
19
|
+
vi.mock('src/utils/getCookie', () => ({ getCookie }))
|
|
20
|
+
|
|
21
|
+
const storeConfigMock = vi.hoisted(() => ({
|
|
22
|
+
experimental: { enableRecommendations: true },
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('discovery.config', () => ({ default: storeConfigMock }))
|
|
25
|
+
|
|
26
|
+
import { useStartRecommendationSession } from 'src/sdk/analytics/hooks/useStartRecommendationSession'
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
useLazyQueryMock.mockReturnValue([runStartRecommendationSession, {}])
|
|
30
|
+
runStartRecommendationSession.mockResolvedValue(true)
|
|
31
|
+
storeConfigMock.experimental.enableRecommendations = true
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.clearAllMocks()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('useStartRecommendationSession', () => {
|
|
39
|
+
it('does not start a session when the session cookie already exists', async () => {
|
|
40
|
+
getCookie.mockReturnValue('already-started')
|
|
41
|
+
|
|
42
|
+
renderHook(() => useStartRecommendationSession())
|
|
43
|
+
|
|
44
|
+
await waitFor(() => {
|
|
45
|
+
expect(runStartRecommendationSession).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('starts a session when no session cookie is present', async () => {
|
|
50
|
+
getCookie.mockReturnValue(undefined)
|
|
51
|
+
|
|
52
|
+
renderHook(() => useStartRecommendationSession())
|
|
53
|
+
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(runStartRecommendationSession).toHaveBeenCalled()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('does not start a session when recommendations are disabled', async () => {
|
|
60
|
+
storeConfigMock.experimental.enableRecommendations = false
|
|
61
|
+
getCookie.mockReturnValue(undefined)
|
|
62
|
+
|
|
63
|
+
renderHook(() => useStartRecommendationSession())
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(runStartRecommendationSession).not.toHaveBeenCalled()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockGetCookie = vi.hoisted(() => vi.fn())
|
|
4
|
+
vi.mock('src/utils/getCookie', () => ({ getCookie: mockGetCookie }))
|
|
5
|
+
|
|
6
|
+
import { getUserIdFromCookie } from 'src/sdk/analytics/utils'
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.clearAllMocks()
|
|
10
|
+
vi.unstubAllGlobals()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('getUserIdFromCookie', () => {
|
|
14
|
+
it('returns the cookie value when present', () => {
|
|
15
|
+
mockGetCookie.mockReturnValue('user-123')
|
|
16
|
+
expect(getUserIdFromCookie()).toBe('user-123')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('falls back to an empty string when the cookie is missing', () => {
|
|
20
|
+
mockGetCookie.mockReturnValue(undefined)
|
|
21
|
+
expect(getUserIdFromCookie()).toBe('')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -78,10 +78,12 @@ const QUERIES = [
|
|
|
78
78
|
'userDetails',
|
|
79
79
|
'accountProfile',
|
|
80
80
|
'validateUser',
|
|
81
|
+
'availableContracts',
|
|
81
82
|
'isOrganizationMember',
|
|
82
83
|
'pickupPoints',
|
|
83
84
|
'orderEntryOperation',
|
|
84
85
|
'orderFormItems',
|
|
86
|
+
'recommendations',
|
|
85
87
|
]
|
|
86
88
|
|
|
87
89
|
const OPTIONAL_GENERATED_QUERIES = new Set(['accountName'])
|
|
@@ -94,6 +96,7 @@ const MUTATIONS = [
|
|
|
94
96
|
'processOrderAuthorization',
|
|
95
97
|
'uploadFileToOrderEntry',
|
|
96
98
|
'startOrderEntryOperation',
|
|
99
|
+
'startRecommendationSession',
|
|
97
100
|
]
|
|
98
101
|
|
|
99
102
|
describe('FastStore GraphQL Layer', () => {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { retry } from 'src/utils/retry'
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.clearAllMocks()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe('retry', () => {
|
|
10
|
+
it('resolves immediately when the value is already available', async () => {
|
|
11
|
+
const fn = vi.fn(() => 'ready')
|
|
12
|
+
|
|
13
|
+
await expect(retry(fn)).resolves.toBe('ready')
|
|
14
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('retries until a truthy value is returned', async () => {
|
|
18
|
+
const fn = vi
|
|
19
|
+
.fn<() => string>()
|
|
20
|
+
.mockReturnValueOnce('')
|
|
21
|
+
.mockReturnValueOnce('')
|
|
22
|
+
.mockReturnValue('later')
|
|
23
|
+
|
|
24
|
+
await expect(retry(fn, { attempts: 5, delayMs: 1 })).resolves.toBe('later')
|
|
25
|
+
expect(fn).toHaveBeenCalledTimes(3)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('gives up after exhausting the retry budget', async () => {
|
|
29
|
+
const fn = vi.fn(() => '')
|
|
30
|
+
|
|
31
|
+
await expect(retry(fn, { attempts: 2, delayMs: 1 })).resolves.toBe('')
|
|
32
|
+
// initial call + 2 retries
|
|
33
|
+
expect(fn).toHaveBeenCalledTimes(3)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('honours a custom `until` predicate', async () => {
|
|
37
|
+
const fn = vi
|
|
38
|
+
.fn<() => number | undefined>()
|
|
39
|
+
.mockReturnValueOnce(undefined)
|
|
40
|
+
.mockReturnValue(0)
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
retry(fn, { attempts: 5, delayMs: 1, until: (v) => v !== undefined })
|
|
44
|
+
).resolves.toBe(0)
|
|
45
|
+
expect(fn).toHaveBeenCalledTimes(2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('stops retrying once the signal is aborted', async () => {
|
|
49
|
+
const controller = new AbortController()
|
|
50
|
+
const fn = vi.fn(() => '')
|
|
51
|
+
controller.abort()
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
retry(fn, { attempts: 5, delayMs: 1, signal: controller.signal })
|
|
55
|
+
).resolves.toBe('')
|
|
56
|
+
// Only the initial call runs; the loop never starts because it is aborted.
|
|
57
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
58
|
+
})
|
|
59
|
+
})
|