@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.
Files changed (32) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +99 -29
  3. package/@generated/cached-operations.json +3 -1
  4. package/@generated/gql.ts +29 -5
  5. package/@generated/graphql.ts +129 -8
  6. package/@generated/persisted-documents.json +5 -1
  7. package/@generated/schema.graphql +69 -0
  8. package/CHANGELOG.md +12 -0
  9. package/cms/faststore/pages/cms_content_type__landingpage.jsonc +6 -0
  10. package/cms/faststore/schema.json +123 -91
  11. package/cms/faststore/sections.json +5 -0
  12. package/package.json +6 -6
  13. package/src/components/auth/ProfileChallenge/ProfileChallenge.tsx +2 -2
  14. package/src/components/search/SearchInput/SearchInput.tsx +139 -332
  15. package/src/components/sections/Navbar/Navbar.tsx +1 -0
  16. package/src/components/sections/Navbar/section.module.scss +3 -0
  17. package/src/sdk/auth/index.ts +2 -2
  18. package/src/sdk/orderEntry/useOrderEntry.ts +58 -0
  19. package/src/sdk/orderEntry/useOrderEntryOperation.ts +132 -0
  20. package/src/sdk/orderEntry/useOrderEntryUpload.ts +96 -0
  21. package/src/sdk/orderEntry/useOrderFormItems.ts +57 -0
  22. package/src/styles/global/index.scss +16 -0
  23. package/test/components/auth/ProfileChallenge.test.tsx +51 -0
  24. package/test/components/search/SearchInput.test.ts +72 -0
  25. package/test/components/search/SearchInputComponent.test.tsx +276 -0
  26. package/test/sdk/auth/useAuth.test.ts +94 -0
  27. package/test/sdk/orderEntry/useOrderEntry.test.ts +139 -0
  28. package/test/sdk/orderEntry/useOrderEntryOperation.test.ts +205 -0
  29. package/test/sdk/orderEntry/useOrderEntryUpload.test.ts +142 -0
  30. package/test/sdk/orderEntry/useOrderFormItems.test.ts +132 -0
  31. package/test/server/index.test.ts +4 -0
  32. 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
+ })