@faststore/core 4.3.0-dev.1 → 4.3.0-dev.3
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 +99 -29
- package/@generated/cached-operations.json +3 -1
- package/@generated/gql.ts +29 -5
- package/@generated/graphql.ts +129 -8
- package/@generated/persisted-documents.json +5 -1
- package/@generated/schema.graphql +69 -0
- package/CHANGELOG.md +12 -0
- package/cms/faststore/pages/cms_content_type__landingpage.jsonc +6 -0
- package/cms/faststore/schema.json +123 -91
- package/cms/faststore/sections.json +5 -0
- package/package.json +6 -6
- package/src/components/auth/ProfileChallenge/ProfileChallenge.tsx +2 -2
- package/src/components/search/SearchInput/SearchInput.tsx +139 -332
- package/src/components/sections/Navbar/Navbar.tsx +1 -0
- package/src/components/sections/Navbar/section.module.scss +3 -0
- package/src/sdk/auth/index.ts +2 -2
- package/src/sdk/orderEntry/useOrderEntry.ts +58 -0
- package/src/sdk/orderEntry/useOrderEntryOperation.ts +132 -0
- package/src/sdk/orderEntry/useOrderEntryUpload.ts +96 -0
- package/src/sdk/orderEntry/useOrderFormItems.ts +57 -0
- package/src/styles/global/index.scss +16 -0
- package/test/components/auth/ProfileChallenge.test.tsx +51 -0
- package/test/components/search/SearchInput.test.ts +72 -0
- package/test/components/search/SearchInputComponent.test.tsx +276 -0
- package/test/sdk/auth/useAuth.test.ts +94 -0
- package/test/sdk/orderEntry/useOrderEntry.test.ts +139 -0
- package/test/sdk/orderEntry/useOrderEntryOperation.test.ts +205 -0
- package/test/sdk/orderEntry/useOrderEntryUpload.test.ts +142 -0
- package/test/sdk/orderEntry/useOrderFormItems.test.ts +132 -0
- package/test/server/index.test.ts +4 -0
- package/src/sdk/product/useBulkProductsQuery.ts +0 -128
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { act, render, waitFor } from '@testing-library/react'
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
// ─── next/dynamic: return a plain functional component ───────────────────────
|
|
10
|
+
vi.mock('next/dynamic', () => ({
|
|
11
|
+
default: () => () => null,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
// ─── next/router ─────────────────────────────────────────────────────────────
|
|
15
|
+
vi.mock('next/router', () => ({
|
|
16
|
+
useRouter: vi.fn(() => ({ push: vi.fn() })),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
// ─── lazy-loaded chunk ───────────────────────────────────────────────────────
|
|
20
|
+
vi.mock('src/components/search/SearchDropdown', () => ({ default: () => null }))
|
|
21
|
+
|
|
22
|
+
// ─── discovery.config ────────────────────────────────────────────────────────
|
|
23
|
+
vi.mock('discovery.config', () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
default: { localization: { enabled: false, defaultLocale: 'en-US' } },
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
// ─── @faststore/ui: lightweight stubs ────────────────────────────────────────
|
|
29
|
+
vi.mock('@faststore/ui', () => {
|
|
30
|
+
const stub =
|
|
31
|
+
(name: string) =>
|
|
32
|
+
({
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: React.PropsWithChildren<Record<string, unknown>>) =>
|
|
36
|
+
React.createElement('div', { 'data-ui': name, ...props }, children)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
SearchInput: React.forwardRef(stub('SearchInput')),
|
|
40
|
+
FileUploadCard: stub('FileUploadCard'),
|
|
41
|
+
QuickOrderDrawer: stub('QuickOrderDrawer'),
|
|
42
|
+
QuickOrderDrawerFooter: stub('QuickOrderDrawerFooter'),
|
|
43
|
+
QuickOrderDrawerHeader: stub('QuickOrderDrawerHeader'),
|
|
44
|
+
QuickOrderDrawerProducts: stub('QuickOrderDrawerProducts'),
|
|
45
|
+
Icon: stub('Icon'),
|
|
46
|
+
IconButton: stub('IconButton'),
|
|
47
|
+
useOnClickOutside: vi.fn(),
|
|
48
|
+
FileUploadErrorType: {
|
|
49
|
+
Unexpected: 'unexpected',
|
|
50
|
+
Unsupported: 'unsupported',
|
|
51
|
+
Unreadable: 'unreadable',
|
|
52
|
+
InvalidStructure: 'invalid-structure',
|
|
53
|
+
Empty: 'empty',
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ─── SDK mocks ────────────────────────────────────────────────────────────────
|
|
59
|
+
vi.mock('src/sdk/search/useSearchHistory', () => ({
|
|
60
|
+
default: vi.fn(() => ({ addToSearchHistory: vi.fn(), searchHistory: [] })),
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
vi.mock('src/sdk/search/useSuggestions', () => ({
|
|
64
|
+
default: vi.fn(() => ({ terms: [], products: [] })),
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
vi.mock('src/sdk/cart', () => ({
|
|
68
|
+
cartStore: { addItem: vi.fn() },
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
vi.mock('src/sdk/product/useFormattedPrice', () => ({
|
|
72
|
+
usePriceFormatter: vi.fn(() => (price: number) => `$${price}`),
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
vi.mock('src/sdk/search/formatSearchPath', () => ({
|
|
76
|
+
formatSearchPath: vi.fn((term: string) => `/s?q=${term}`),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
vi.mock('src/utils/utilities', () => ({
|
|
80
|
+
formatFileName: vi.fn((n: string) => n),
|
|
81
|
+
formatFileSize: vi.fn((s: number) => `${s}B`),
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
// ─── OES hooks ────────────────────────────────────────────────────────────────
|
|
85
|
+
const mockFetchOrderFormItems = vi.hoisted(() => vi.fn())
|
|
86
|
+
const mockResetOrderFormItems = vi.hoisted(() => vi.fn())
|
|
87
|
+
const mockSubmitFile = vi.hoisted(() => vi.fn())
|
|
88
|
+
const mockResetOES = vi.hoisted(() => vi.fn())
|
|
89
|
+
|
|
90
|
+
const oesState = vi.hoisted(() => ({
|
|
91
|
+
status: null as Record<string, string> | null,
|
|
92
|
+
isUploading: false,
|
|
93
|
+
isProcessing: false,
|
|
94
|
+
error: null as { message: string } | null,
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
const orderFormState = vi.hoisted(() => ({
|
|
98
|
+
items: null as unknown[] | null,
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
vi.mock('src/sdk/auth', () => ({
|
|
102
|
+
useAuth: vi.fn(() => ({ isAuthenticated: true })),
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
vi.mock('src/sdk/orderEntry/useOrderEntry', () => ({
|
|
106
|
+
useOrderEntry: vi.fn(() => ({
|
|
107
|
+
submitFile: mockSubmitFile,
|
|
108
|
+
status: oesState.status,
|
|
109
|
+
isUploading: oesState.isUploading,
|
|
110
|
+
isProcessing: oesState.isProcessing,
|
|
111
|
+
error: oesState.error,
|
|
112
|
+
reset: mockResetOES,
|
|
113
|
+
})),
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
vi.mock('src/sdk/orderEntry/useOrderFormItems', () => ({
|
|
117
|
+
useOrderFormItems: vi.fn(() => ({
|
|
118
|
+
fetchOrderFormItems: mockFetchOrderFormItems,
|
|
119
|
+
items: orderFormState.items,
|
|
120
|
+
reset: mockResetOrderFormItems,
|
|
121
|
+
})),
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
import SearchInput from '../../../src/components/search/SearchInput/SearchInput'
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
vi.clearAllMocks()
|
|
128
|
+
oesState.status = null
|
|
129
|
+
oesState.isUploading = false
|
|
130
|
+
oesState.isProcessing = false
|
|
131
|
+
oesState.error = null
|
|
132
|
+
orderFormState.items = null
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const Wrapper = ({ children }: React.PropsWithChildren) => (
|
|
136
|
+
<React.Suspense fallback={null}>{children}</React.Suspense>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
describe('SearchInput (OES integration)', () => {
|
|
140
|
+
it('renders without crashing', () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<Wrapper>
|
|
143
|
+
<SearchInput />
|
|
144
|
+
</Wrapper>
|
|
145
|
+
)
|
|
146
|
+
expect(container).toBeTruthy()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('calls fetchOrderFormItems when OES status is SUCCESS with entityId', async () => {
|
|
150
|
+
const { useOrderEntry } = await import('src/sdk/orderEntry/useOrderEntry')
|
|
151
|
+
vi.mocked(useOrderEntry).mockReturnValue({
|
|
152
|
+
submitFile: mockSubmitFile,
|
|
153
|
+
status: { status: 'SUCCESS', entityId: 'cart-new' },
|
|
154
|
+
isUploading: false,
|
|
155
|
+
isProcessing: false,
|
|
156
|
+
error: null,
|
|
157
|
+
reset: mockResetOES,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
render(
|
|
162
|
+
<Wrapper>
|
|
163
|
+
<SearchInput />
|
|
164
|
+
</Wrapper>
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(mockFetchOrderFormItems).toHaveBeenCalledWith('cart-new')
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('calls fetchOrderFormItems when OES status is PARTIAL_SUCCESS', async () => {
|
|
174
|
+
const { useOrderEntry } = await import('src/sdk/orderEntry/useOrderEntry')
|
|
175
|
+
vi.mocked(useOrderEntry).mockReturnValue({
|
|
176
|
+
submitFile: mockSubmitFile,
|
|
177
|
+
status: { status: 'PARTIAL_SUCCESS', entityId: 'cart-partial' },
|
|
178
|
+
isUploading: false,
|
|
179
|
+
isProcessing: false,
|
|
180
|
+
error: null,
|
|
181
|
+
reset: mockResetOES,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
await act(async () => {
|
|
185
|
+
render(
|
|
186
|
+
<Wrapper>
|
|
187
|
+
<SearchInput />
|
|
188
|
+
</Wrapper>
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(mockFetchOrderFormItems).toHaveBeenCalledWith('cart-partial')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does not call fetchOrderFormItems when OES status is FAILED', async () => {
|
|
198
|
+
const { useOrderEntry } = await import('src/sdk/orderEntry/useOrderEntry')
|
|
199
|
+
vi.mocked(useOrderEntry).mockReturnValue({
|
|
200
|
+
submitFile: mockSubmitFile,
|
|
201
|
+
status: { status: 'FAILED', entityId: null },
|
|
202
|
+
isUploading: false,
|
|
203
|
+
isProcessing: false,
|
|
204
|
+
error: null,
|
|
205
|
+
reset: mockResetOES,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
render(
|
|
210
|
+
<Wrapper>
|
|
211
|
+
<SearchInput />
|
|
212
|
+
</Wrapper>
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
expect(mockFetchOrderFormItems).not.toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('does not render attachment button when user is not authenticated', async () => {
|
|
220
|
+
const { useAuth } = await import('src/sdk/auth')
|
|
221
|
+
vi.mocked(useAuth).mockReturnValue({ isAuthenticated: false })
|
|
222
|
+
|
|
223
|
+
const { queryByRole } = render(
|
|
224
|
+
<Wrapper>
|
|
225
|
+
<SearchInput
|
|
226
|
+
quickOrderSettings={{
|
|
227
|
+
quickOrder: true,
|
|
228
|
+
attachmentButton: { icon: 'Paperclip', ariaLabel: 'Attach' },
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
</Wrapper>
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(queryByRole('button', { name: 'Attach' })).toBeNull()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('updates products when orderFormItems arrive', async () => {
|
|
238
|
+
const mockItems = [
|
|
239
|
+
{
|
|
240
|
+
id: 'sku-1',
|
|
241
|
+
name: 'Product A',
|
|
242
|
+
price: 100,
|
|
243
|
+
listPrice: 120,
|
|
244
|
+
quantity: 1,
|
|
245
|
+
imageUrl: null,
|
|
246
|
+
availability: 'available',
|
|
247
|
+
seller: '1',
|
|
248
|
+
unitMultiplier: 1,
|
|
249
|
+
},
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
const { useOrderFormItems } = await import(
|
|
253
|
+
'src/sdk/orderEntry/useOrderFormItems'
|
|
254
|
+
)
|
|
255
|
+
vi.mocked(useOrderFormItems).mockReturnValue({
|
|
256
|
+
fetchOrderFormItems: mockFetchOrderFormItems,
|
|
257
|
+
items: mockItems,
|
|
258
|
+
isLoading: false,
|
|
259
|
+
isValidating: false,
|
|
260
|
+
error: null,
|
|
261
|
+
reset: mockResetOrderFormItems,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
await act(async () => {
|
|
265
|
+
render(
|
|
266
|
+
<Wrapper>
|
|
267
|
+
<SearchInput />
|
|
268
|
+
</Wrapper>
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Effect ran — products were set and drawer should open
|
|
273
|
+
// We verify no errors were thrown during the effect
|
|
274
|
+
expect(true).toBe(true)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } 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
|
+
import { useAuth } from '../../../src/sdk/auth'
|
|
12
|
+
|
|
13
|
+
describe('useAuth', () => {
|
|
14
|
+
it('isAuthenticated is true when hasSalesChannel and person is set', () => {
|
|
15
|
+
mockUseSession.mockReturnValue({
|
|
16
|
+
person: { id: 'user-1', email: 'test@example.com' },
|
|
17
|
+
isValidating: false,
|
|
18
|
+
channel: JSON.stringify({
|
|
19
|
+
salesChannel: 1,
|
|
20
|
+
regionId: 'r1',
|
|
21
|
+
hasOnlyDefaultSalesChannel: false,
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const { result } = renderHook(() => useAuth())
|
|
26
|
+
|
|
27
|
+
expect(result.current.isAuthenticated).toBeTruthy()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('isAuthenticated is false when hasOnlyDefaultSalesChannel is true', () => {
|
|
31
|
+
mockUseSession.mockReturnValue({
|
|
32
|
+
person: { id: 'user-1' },
|
|
33
|
+
isValidating: false,
|
|
34
|
+
channel: JSON.stringify({
|
|
35
|
+
salesChannel: 1,
|
|
36
|
+
regionId: 'r1',
|
|
37
|
+
hasOnlyDefaultSalesChannel: true,
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const { result } = renderHook(() => useAuth())
|
|
42
|
+
|
|
43
|
+
expect(result.current.isAuthenticated).toBeFalsy()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('isAuthenticated is false when person is null', () => {
|
|
47
|
+
mockUseSession.mockReturnValue({
|
|
48
|
+
person: null,
|
|
49
|
+
isValidating: false,
|
|
50
|
+
channel: JSON.stringify({
|
|
51
|
+
salesChannel: 1,
|
|
52
|
+
regionId: 'r1',
|
|
53
|
+
hasOnlyDefaultSalesChannel: false,
|
|
54
|
+
}),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const { result } = renderHook(() => useAuth())
|
|
58
|
+
|
|
59
|
+
expect(result.current.isAuthenticated).toBeFalsy()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('isAuthenticated is false when salesChannel is falsy', () => {
|
|
63
|
+
mockUseSession.mockReturnValue({
|
|
64
|
+
person: { id: 'user-1' },
|
|
65
|
+
isValidating: false,
|
|
66
|
+
channel: JSON.stringify({
|
|
67
|
+
salesChannel: 0,
|
|
68
|
+
regionId: 'r1',
|
|
69
|
+
hasOnlyDefaultSalesChannel: false,
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const { result } = renderHook(() => useAuth())
|
|
74
|
+
|
|
75
|
+
expect(result.current.isAuthenticated).toBeFalsy()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('exposes profile and isValidating from session', () => {
|
|
79
|
+
const person = { id: 'user-1', email: 'test@example.com' }
|
|
80
|
+
mockUseSession.mockReturnValue({
|
|
81
|
+
person,
|
|
82
|
+
isValidating: true,
|
|
83
|
+
channel: JSON.stringify({
|
|
84
|
+
salesChannel: 1,
|
|
85
|
+
hasOnlyDefaultSalesChannel: false,
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() => useAuth())
|
|
90
|
+
|
|
91
|
+
expect(result.current.profile).toBe(person)
|
|
92
|
+
expect(result.current.isValidating).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { act, renderHook } from '@testing-library/react'
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUploadFile = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockStartOperation = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockReset = vi.hoisted(() => vi.fn())
|
|
11
|
+
const mockClearError = vi.hoisted(() => vi.fn())
|
|
12
|
+
|
|
13
|
+
const uploadState = vi.hoisted(() => ({
|
|
14
|
+
isUploading: false,
|
|
15
|
+
error: null as { message: string } | null,
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const operationState = vi.hoisted(() => ({
|
|
19
|
+
isLoading: false,
|
|
20
|
+
error: null as Error | null,
|
|
21
|
+
status: null as unknown,
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('src/sdk/cart', () => ({
|
|
25
|
+
useCart: vi.fn(() => ({ id: 'cart-123' })),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.mock('../../../src/sdk/orderEntry/useOrderEntryUpload', () => ({
|
|
29
|
+
useOrderEntryUpload: vi.fn(() => ({
|
|
30
|
+
uploadFile: mockUploadFile,
|
|
31
|
+
isUploading: uploadState.isUploading,
|
|
32
|
+
error: uploadState.error,
|
|
33
|
+
clearError: mockClearError,
|
|
34
|
+
})),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
vi.mock('../../../src/sdk/orderEntry/useOrderEntryOperation', () => ({
|
|
38
|
+
useOrderEntryOperation: vi.fn(() => ({
|
|
39
|
+
startOperation: mockStartOperation,
|
|
40
|
+
status: operationState.status,
|
|
41
|
+
isLoading: operationState.isLoading,
|
|
42
|
+
error: operationState.error,
|
|
43
|
+
reset: mockReset,
|
|
44
|
+
})),
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
import { useOrderEntry } from '../../../src/sdk/orderEntry/useOrderEntry'
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.clearAllMocks()
|
|
51
|
+
uploadState.isUploading = false
|
|
52
|
+
uploadState.error = null
|
|
53
|
+
operationState.isLoading = false
|
|
54
|
+
operationState.error = null
|
|
55
|
+
operationState.status = null
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('useOrderEntry', () => {
|
|
59
|
+
it('initializes with isLoading=false and no error', () => {
|
|
60
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
61
|
+
|
|
62
|
+
expect(result.current.isLoading).toBe(false)
|
|
63
|
+
expect(result.current.error).toBeNull()
|
|
64
|
+
expect(result.current.status).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('submitFile calls uploadFile then startOperation with cartId', async () => {
|
|
68
|
+
mockUploadFile.mockResolvedValueOnce('s3-key-abc')
|
|
69
|
+
mockStartOperation.mockResolvedValueOnce(undefined)
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
72
|
+
const file = new File(['data'], 'items.csv', { type: 'text/csv' })
|
|
73
|
+
|
|
74
|
+
await act(async () => {
|
|
75
|
+
await result.current.submitFile(file)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(mockUploadFile).toHaveBeenCalledWith(file)
|
|
79
|
+
expect(mockStartOperation).toHaveBeenCalledWith({
|
|
80
|
+
objectKey: 's3-key-abc',
|
|
81
|
+
orderFormId: 'cart-123',
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('submitFile stops when uploadFile returns null', async () => {
|
|
86
|
+
mockUploadFile.mockResolvedValueOnce(null)
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
89
|
+
const file = new File(['data'], 'items.csv', { type: 'text/csv' })
|
|
90
|
+
|
|
91
|
+
await act(async () => {
|
|
92
|
+
await result.current.submitFile(file)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(mockStartOperation).not.toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('exposes reset from useOrderEntryOperation', () => {
|
|
99
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
100
|
+
|
|
101
|
+
result.current.reset()
|
|
102
|
+
|
|
103
|
+
expect(mockReset).toHaveBeenCalledTimes(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('isUploading reflects upload state', () => {
|
|
107
|
+
uploadState.isUploading = true
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
110
|
+
|
|
111
|
+
expect(result.current.isUploading).toBe(true)
|
|
112
|
+
expect(result.current.isLoading).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('exposes upload error as Error when upload fails', () => {
|
|
116
|
+
uploadState.error = { message: 'Upload failed' }
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
119
|
+
|
|
120
|
+
expect(result.current.error?.message).toBe('Upload failed')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('exposes operation error when operation fails', () => {
|
|
124
|
+
operationState.error = new Error('Operation failed')
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
127
|
+
|
|
128
|
+
expect(result.current.error?.message).toBe('Operation failed')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('isProcessing reflects operation loading state', () => {
|
|
132
|
+
operationState.isLoading = true
|
|
133
|
+
|
|
134
|
+
const { result } = renderHook(() => useOrderEntry())
|
|
135
|
+
|
|
136
|
+
expect(result.current.isProcessing).toBe(true)
|
|
137
|
+
expect(result.current.isLoading).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { act, renderHook } from '@testing-library/react'
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockStartOp = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockUseLazyQuery = vi.hoisted(() =>
|
|
10
|
+
vi.fn(() => [mockStartOp, { isValidating: false, error: null }])
|
|
11
|
+
)
|
|
12
|
+
const mockUseQuery = vi.hoisted(() =>
|
|
13
|
+
vi.fn(() => ({ data: null, error: null }))
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
vi.mock('@generated', () => ({ gql: (s: unknown) => s }))
|
|
17
|
+
vi.mock('src/sdk/graphql/useLazyQuery', () => ({
|
|
18
|
+
useLazyQuery: mockUseLazyQuery,
|
|
19
|
+
}))
|
|
20
|
+
vi.mock('src/sdk/graphql/useQuery', () => ({ useQuery: mockUseQuery }))
|
|
21
|
+
|
|
22
|
+
import { useOrderEntryOperation } from '../../../src/sdk/orderEntry/useOrderEntryOperation'
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('useOrderEntryOperation', () => {
|
|
29
|
+
it('initializes with null status and not loading', () => {
|
|
30
|
+
mockUseLazyQuery.mockReturnValue([
|
|
31
|
+
mockStartOp,
|
|
32
|
+
{ isValidating: false, error: null },
|
|
33
|
+
])
|
|
34
|
+
mockUseQuery.mockReturnValue({ data: null, error: null })
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
37
|
+
|
|
38
|
+
expect(result.current.status).toBeNull()
|
|
39
|
+
expect(result.current.isLoading).toBe(false)
|
|
40
|
+
expect(result.current.error).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('sets isLoading (pending) after startOperation resolves with operationId', async () => {
|
|
44
|
+
mockStartOp.mockResolvedValueOnce({
|
|
45
|
+
startOrderEntryOperation: { operationId: 'op-123' },
|
|
46
|
+
})
|
|
47
|
+
mockUseLazyQuery.mockReturnValue([
|
|
48
|
+
mockStartOp,
|
|
49
|
+
{ isValidating: false, error: null },
|
|
50
|
+
])
|
|
51
|
+
mockUseQuery.mockReturnValue({ data: null, error: null })
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
54
|
+
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await result.current.startOperation({
|
|
57
|
+
objectKey: 'key-abc',
|
|
58
|
+
orderFormId: 'of-1',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(result.current.isLoading).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('isLoading is false and status is set when operation reaches terminal SUCCESS', async () => {
|
|
66
|
+
mockStartOp.mockResolvedValueOnce({
|
|
67
|
+
startOrderEntryOperation: { operationId: 'op-123' },
|
|
68
|
+
})
|
|
69
|
+
mockUseLazyQuery.mockReturnValue([
|
|
70
|
+
mockStartOp,
|
|
71
|
+
{ isValidating: false, error: null },
|
|
72
|
+
])
|
|
73
|
+
mockUseQuery.mockReturnValue({
|
|
74
|
+
data: {
|
|
75
|
+
orderEntryOperation: {
|
|
76
|
+
status: 'SUCCESS',
|
|
77
|
+
entityId: 'cart-1',
|
|
78
|
+
message: null,
|
|
79
|
+
missingItems: [],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
error: null,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
86
|
+
|
|
87
|
+
await act(async () => {
|
|
88
|
+
await result.current.startOperation({
|
|
89
|
+
objectKey: 'key',
|
|
90
|
+
orderFormId: 'of-1',
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(result.current.status?.status).toBe('SUCCESS')
|
|
95
|
+
expect(result.current.isLoading).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('isLoading is false and status is set when operation reaches FAILED', async () => {
|
|
99
|
+
mockStartOp.mockResolvedValueOnce({
|
|
100
|
+
startOrderEntryOperation: { operationId: 'op-fail' },
|
|
101
|
+
})
|
|
102
|
+
mockUseLazyQuery.mockReturnValue([
|
|
103
|
+
mockStartOp,
|
|
104
|
+
{ isValidating: false, error: null },
|
|
105
|
+
])
|
|
106
|
+
mockUseQuery.mockReturnValue({
|
|
107
|
+
data: {
|
|
108
|
+
orderEntryOperation: {
|
|
109
|
+
status: 'FAILED',
|
|
110
|
+
entityId: null,
|
|
111
|
+
message: 'error',
|
|
112
|
+
missingItems: [],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
error: null,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
119
|
+
|
|
120
|
+
await act(async () => {
|
|
121
|
+
await result.current.startOperation({
|
|
122
|
+
objectKey: 'key',
|
|
123
|
+
orderFormId: 'of-1',
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(result.current.status?.status).toBe('FAILED')
|
|
128
|
+
expect(result.current.isLoading).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('reset clears operationId and returns to not loading', async () => {
|
|
132
|
+
mockStartOp.mockResolvedValueOnce({
|
|
133
|
+
startOrderEntryOperation: { operationId: 'op-123' },
|
|
134
|
+
})
|
|
135
|
+
mockUseLazyQuery.mockReturnValue([
|
|
136
|
+
mockStartOp,
|
|
137
|
+
{ isValidating: false, error: null },
|
|
138
|
+
])
|
|
139
|
+
mockUseQuery.mockReturnValue({ data: null, error: null })
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
142
|
+
|
|
143
|
+
await act(async () => {
|
|
144
|
+
await result.current.startOperation({
|
|
145
|
+
objectKey: 'key',
|
|
146
|
+
orderFormId: 'of-1',
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
act(() => result.current.reset())
|
|
151
|
+
|
|
152
|
+
expect(result.current.isLoading).toBe(false)
|
|
153
|
+
expect(result.current.status).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('does not set operationId when startOperation returns no id', async () => {
|
|
157
|
+
mockStartOp.mockResolvedValueOnce({
|
|
158
|
+
startOrderEntryOperation: { operationId: null },
|
|
159
|
+
})
|
|
160
|
+
mockUseLazyQuery.mockReturnValue([
|
|
161
|
+
mockStartOp,
|
|
162
|
+
{ isValidating: false, error: null },
|
|
163
|
+
])
|
|
164
|
+
mockUseQuery.mockReturnValue({ data: null, error: null })
|
|
165
|
+
|
|
166
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
167
|
+
|
|
168
|
+
await act(async () => {
|
|
169
|
+
await result.current.startOperation({
|
|
170
|
+
objectKey: 'key',
|
|
171
|
+
orderFormId: 'of-1',
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(result.current.isLoading).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('returns startError when useLazyQuery exposes an error', () => {
|
|
179
|
+
const error = new Error('Start failed')
|
|
180
|
+
|
|
181
|
+
mockUseLazyQuery.mockReturnValue([
|
|
182
|
+
mockStartOp,
|
|
183
|
+
{ isValidating: false, error },
|
|
184
|
+
])
|
|
185
|
+
mockUseQuery.mockReturnValue({ data: null, error: null })
|
|
186
|
+
|
|
187
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
188
|
+
|
|
189
|
+
expect(result.current.error).toBe(error)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns statusError when useQuery exposes an error', () => {
|
|
193
|
+
const error = new Error('Poll failed')
|
|
194
|
+
|
|
195
|
+
mockUseLazyQuery.mockReturnValue([
|
|
196
|
+
mockStartOp,
|
|
197
|
+
{ isValidating: false, error: null },
|
|
198
|
+
])
|
|
199
|
+
mockUseQuery.mockReturnValue({ data: null, error })
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useOrderEntryOperation())
|
|
202
|
+
|
|
203
|
+
expect(result.current.error).toBe(error)
|
|
204
|
+
})
|
|
205
|
+
})
|