@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,58 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useCart } from '../cart'
|
|
4
|
+
import { useOrderEntryOperation } from './useOrderEntryOperation'
|
|
5
|
+
import { useOrderEntryUpload } from './useOrderEntryUpload'
|
|
6
|
+
|
|
7
|
+
export function useOrderEntry() {
|
|
8
|
+
const cart = useCart()
|
|
9
|
+
|
|
10
|
+
const [isOperationStarting, setIsOperationStarting] = useState(false)
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
uploadFile,
|
|
14
|
+
isUploading,
|
|
15
|
+
error: uploadError,
|
|
16
|
+
clearError: clearUploadError,
|
|
17
|
+
} = useOrderEntryUpload()
|
|
18
|
+
const {
|
|
19
|
+
startOperation,
|
|
20
|
+
status,
|
|
21
|
+
isLoading: isOperating,
|
|
22
|
+
error: operationError,
|
|
23
|
+
reset: resetOperation,
|
|
24
|
+
} = useOrderEntryOperation()
|
|
25
|
+
|
|
26
|
+
const reset = useCallback(() => {
|
|
27
|
+
clearUploadError()
|
|
28
|
+
resetOperation()
|
|
29
|
+
setIsOperationStarting(false)
|
|
30
|
+
}, [clearUploadError, resetOperation])
|
|
31
|
+
|
|
32
|
+
const submitFile = useCallback(
|
|
33
|
+
async (file: File) => {
|
|
34
|
+
const objectKey = await uploadFile(file)
|
|
35
|
+
if (!objectKey) return
|
|
36
|
+
setIsOperationStarting(true)
|
|
37
|
+
try {
|
|
38
|
+
await startOperation({
|
|
39
|
+
objectKey,
|
|
40
|
+
orderFormId: cart.id ?? '',
|
|
41
|
+
})
|
|
42
|
+
} finally {
|
|
43
|
+
setIsOperationStarting(false)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[uploadFile, startOperation, cart.id]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
submitFile,
|
|
51
|
+
status,
|
|
52
|
+
isLoading: isUploading || isOperationStarting || isOperating,
|
|
53
|
+
isUploading,
|
|
54
|
+
isProcessing: isOperationStarting || isOperating,
|
|
55
|
+
error: uploadError ? new Error(uploadError.message) : operationError,
|
|
56
|
+
reset,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { gql } from '@generated'
|
|
4
|
+
import type {
|
|
5
|
+
OrderEntryOperationQueryQuery,
|
|
6
|
+
OrderEntryOperationQueryQueryVariables,
|
|
7
|
+
StartOrderEntryOperationMutationMutation,
|
|
8
|
+
StartOrderEntryOperationMutationMutationVariables,
|
|
9
|
+
} from '@generated/graphql'
|
|
10
|
+
|
|
11
|
+
import { useLazyQuery } from '../graphql/useLazyQuery'
|
|
12
|
+
import { useQuery } from '../graphql/useQuery'
|
|
13
|
+
|
|
14
|
+
const StartOrderEntryOperationMutation = gql(`
|
|
15
|
+
mutation StartOrderEntryOperationMutation($data: IOrderEntryOperation!) {
|
|
16
|
+
startOrderEntryOperation(data: $data) {
|
|
17
|
+
operationId
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
`)
|
|
21
|
+
|
|
22
|
+
const OrderEntryOperationQuery = gql(`
|
|
23
|
+
query OrderEntryOperationQuery($operationId: String!) {
|
|
24
|
+
orderEntryOperation(operationId: $operationId) {
|
|
25
|
+
status
|
|
26
|
+
entityId
|
|
27
|
+
message
|
|
28
|
+
missingItems {
|
|
29
|
+
itemId
|
|
30
|
+
itemName
|
|
31
|
+
reason
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
|
|
37
|
+
const TERMINAL_STATUSES = new Set(['SUCCESS', 'PARTIAL_SUCCESS', 'FAILED'])
|
|
38
|
+
const POLL_TIMEOUT_MS = 60_000
|
|
39
|
+
|
|
40
|
+
export type OrderEntryOperationStatus = NonNullable<
|
|
41
|
+
OrderEntryOperationQueryQuery['orderEntryOperation']
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
export type UseOrderEntryOperationReturn = {
|
|
45
|
+
startOperation: (data: {
|
|
46
|
+
objectKey: string
|
|
47
|
+
orderFormId: string
|
|
48
|
+
sessionToken?: string
|
|
49
|
+
}) => Promise<void>
|
|
50
|
+
status: OrderEntryOperationStatus | null
|
|
51
|
+
isLoading: boolean
|
|
52
|
+
error: Error | null
|
|
53
|
+
reset: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useOrderEntryOperation(): UseOrderEntryOperationReturn {
|
|
57
|
+
const [operationId, setOperationId] = useState<string | null>(null)
|
|
58
|
+
const [timeoutError, setTimeoutError] = useState<Error | null>(null)
|
|
59
|
+
const pollStartRef = useRef<number | null>(null)
|
|
60
|
+
|
|
61
|
+
const [startOp, { isValidating: isStarting, error: startError }] =
|
|
62
|
+
useLazyQuery<
|
|
63
|
+
StartOrderEntryOperationMutationMutation,
|
|
64
|
+
StartOrderEntryOperationMutationMutationVariables
|
|
65
|
+
>(StartOrderEntryOperationMutation, {
|
|
66
|
+
data: { objectKey: '', orderFormId: '', sessionToken: null },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const { data: statusData, error: statusError } = useQuery<
|
|
70
|
+
OrderEntryOperationQueryQuery,
|
|
71
|
+
OrderEntryOperationQueryQueryVariables
|
|
72
|
+
>(
|
|
73
|
+
OrderEntryOperationQuery,
|
|
74
|
+
{ operationId: operationId ?? '' },
|
|
75
|
+
{
|
|
76
|
+
doNotRun: !operationId,
|
|
77
|
+
refreshInterval: (latestData) => {
|
|
78
|
+
const s = latestData?.orderEntryOperation?.status
|
|
79
|
+
if (s && TERMINAL_STATUSES.has(s)) return 0
|
|
80
|
+
if (
|
|
81
|
+
pollStartRef.current !== null &&
|
|
82
|
+
Date.now() - pollStartRef.current >= POLL_TIMEOUT_MS
|
|
83
|
+
) {
|
|
84
|
+
setTimeoutError(new Error('Operation timed out. Please try again.'))
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
return 2000
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const startOperation = useCallback(
|
|
93
|
+
async (data: {
|
|
94
|
+
objectKey: string
|
|
95
|
+
orderFormId: string
|
|
96
|
+
sessionToken?: string
|
|
97
|
+
}) => {
|
|
98
|
+
const result = await startOp({
|
|
99
|
+
data: {
|
|
100
|
+
...data,
|
|
101
|
+
sessionToken: data.sessionToken ?? null,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
const id = result?.startOrderEntryOperation?.operationId
|
|
105
|
+
if (id) {
|
|
106
|
+
pollStartRef.current = Date.now()
|
|
107
|
+
setTimeoutError(null)
|
|
108
|
+
setOperationId(id)
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[startOp]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const reset = useCallback(() => {
|
|
115
|
+
setOperationId(null)
|
|
116
|
+
setTimeoutError(null)
|
|
117
|
+
pollStartRef.current = null
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
120
|
+
const currentStatus = statusData?.orderEntryOperation?.status ?? null
|
|
121
|
+
const isPolling =
|
|
122
|
+
!!operationId && !!currentStatus && !TERMINAL_STATUSES.has(currentStatus)
|
|
123
|
+
const isPending = !!operationId && !currentStatus
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
startOperation,
|
|
127
|
+
status: operationId ? (statusData?.orderEntryOperation ?? null) : null,
|
|
128
|
+
isLoading: isStarting || isPolling || isPending,
|
|
129
|
+
error: (startError ?? statusError ?? timeoutError ?? null) as Error | null,
|
|
130
|
+
reset,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { gql } from '@generated'
|
|
4
|
+
import type {
|
|
5
|
+
UploadFileToOrderEntryMutationMutation,
|
|
6
|
+
UploadFileToOrderEntryMutationMutationVariables,
|
|
7
|
+
} from '@generated/graphql'
|
|
8
|
+
|
|
9
|
+
import { request } from '../graphql/request'
|
|
10
|
+
|
|
11
|
+
const UploadFileToOrderEntryMutation = gql(`
|
|
12
|
+
mutation UploadFileToOrderEntryMutation($data: IOrderEntryUpload!) {
|
|
13
|
+
uploadFileToOrderEntry(data: $data) {
|
|
14
|
+
objectKey
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
`)
|
|
18
|
+
|
|
19
|
+
export type OrderEntryUploadError = {
|
|
20
|
+
message: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type UseOrderEntryUploadReturn = {
|
|
24
|
+
isUploading: boolean
|
|
25
|
+
error: OrderEntryUploadError | null
|
|
26
|
+
uploadFile: (file: File) => Promise<string | null>
|
|
27
|
+
clearError: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reads a File object and returns its content as a Base64-encoded string.
|
|
32
|
+
*/
|
|
33
|
+
const readFileAsBase64 = (file: File): Promise<string> =>
|
|
34
|
+
new Promise((resolve, reject) => {
|
|
35
|
+
const reader = new FileReader()
|
|
36
|
+
reader.onload = () => {
|
|
37
|
+
const dataUrl = reader.result as string
|
|
38
|
+
// Strip the data URL prefix (e.g. "data:text/csv;base64,")
|
|
39
|
+
const base64 = dataUrl.split(',')[1] ?? ''
|
|
40
|
+
resolve(base64)
|
|
41
|
+
}
|
|
42
|
+
reader.onerror = () => reject(new Error('Failed to read file'))
|
|
43
|
+
reader.readAsDataURL(file)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hook to upload a file to the Order Entry Service via the
|
|
48
|
+
* `uploadFileToOrderEntry` GraphQL mutation (defined in @faststore/api).
|
|
49
|
+
*
|
|
50
|
+
* The file is Base64-encoded client-side and decoded server-side by the
|
|
51
|
+
* resolver, which then forwards the multipart request to the OES with the
|
|
52
|
+
* proper VtexIdclientAutCookie auth header.
|
|
53
|
+
*
|
|
54
|
+
* @returns `{ isUploading, error, uploadFile, clearError }`
|
|
55
|
+
* - `uploadFile(file)` resolves to the `objectKey` string on success, or `null` on error.
|
|
56
|
+
*/
|
|
57
|
+
export function useOrderEntryUpload(): UseOrderEntryUploadReturn {
|
|
58
|
+
const [isUploading, setIsUploading] = useState(false)
|
|
59
|
+
const [error, setError] = useState<OrderEntryUploadError | null>(null)
|
|
60
|
+
|
|
61
|
+
const uploadFile = useCallback(async (file: File): Promise<string | null> => {
|
|
62
|
+
setIsUploading(true)
|
|
63
|
+
setError(null)
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const fileContent = await readFileAsBase64(file)
|
|
67
|
+
|
|
68
|
+
const data = await request<
|
|
69
|
+
UploadFileToOrderEntryMutationMutation,
|
|
70
|
+
UploadFileToOrderEntryMutationMutationVariables
|
|
71
|
+
>(UploadFileToOrderEntryMutation, {
|
|
72
|
+
data: {
|
|
73
|
+
fileContent,
|
|
74
|
+
fileName: file.name,
|
|
75
|
+
mimeType: file.type || 'application/octet-stream',
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return data.uploadFileToOrderEntry?.objectKey ?? null
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const errorMessage =
|
|
82
|
+
err instanceof Error ? err.message : 'Failed to upload file'
|
|
83
|
+
|
|
84
|
+
setError({ message: errorMessage })
|
|
85
|
+
return null
|
|
86
|
+
} finally {
|
|
87
|
+
setIsUploading(false)
|
|
88
|
+
}
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
const clearError = useCallback(() => {
|
|
92
|
+
setError(null)
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
return { isUploading, error, uploadFile, clearError }
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { gql } from '@generated'
|
|
4
|
+
import type {
|
|
5
|
+
OrderFormItemsQueryQuery,
|
|
6
|
+
OrderFormItemsQueryQueryVariables,
|
|
7
|
+
} from '@generated/graphql'
|
|
8
|
+
|
|
9
|
+
import { useQuery } from '../graphql/useQuery'
|
|
10
|
+
|
|
11
|
+
const OrderFormItemsQuery = gql(`
|
|
12
|
+
query OrderFormItemsQuery($orderFormId: String!) {
|
|
13
|
+
orderFormItems(orderFormId: $orderFormId) {
|
|
14
|
+
id
|
|
15
|
+
name
|
|
16
|
+
price
|
|
17
|
+
listPrice
|
|
18
|
+
quantity
|
|
19
|
+
imageUrl
|
|
20
|
+
availability
|
|
21
|
+
seller
|
|
22
|
+
unitMultiplier
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`)
|
|
26
|
+
|
|
27
|
+
export type OrderFormCartItem = NonNullable<
|
|
28
|
+
OrderFormItemsQueryQuery['orderFormItems']
|
|
29
|
+
>[number]
|
|
30
|
+
|
|
31
|
+
export function useOrderFormItems() {
|
|
32
|
+
const [orderFormId, setOrderFormId] = useState<string | null>(null)
|
|
33
|
+
|
|
34
|
+
const { data, error, isValidating } = useQuery<
|
|
35
|
+
OrderFormItemsQueryQuery,
|
|
36
|
+
OrderFormItemsQueryQueryVariables
|
|
37
|
+
>(
|
|
38
|
+
OrderFormItemsQuery,
|
|
39
|
+
{ orderFormId: orderFormId ?? '' },
|
|
40
|
+
{ doNotRun: !orderFormId }
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const fetchOrderFormItems = useCallback((id: string) => {
|
|
44
|
+
setOrderFormId(id)
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const reset = useCallback(() => setOrderFormId(null), [])
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
fetchOrderFormItems,
|
|
51
|
+
items: orderFormId ? (data?.orderFormItems ?? null) : null,
|
|
52
|
+
isLoading: !!orderFormId && !data && !error,
|
|
53
|
+
isValidating,
|
|
54
|
+
error: error as Error | null,
|
|
55
|
+
reset,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -14,5 +14,21 @@
|
|
|
14
14
|
|
|
15
15
|
// Importing this component because is being used outside the context of the Section
|
|
16
16
|
@include meta.load-css("~@faststore/ui/src/components/molecules/Dropdown/styles.scss");
|
|
17
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/Card/styles.scss");
|
|
18
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/FileUploadCard/styles.scss");
|
|
19
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/FileUploadStatus/styles.scss");
|
|
17
20
|
@include meta.load-css("~@faststore/ui/src/components/organisms/QuickOrderDrawer/styles.scss");
|
|
18
21
|
@include meta.load-css("~@faststore/ui/src/components/organisms/SlideOver/styles.scss");
|
|
22
|
+
|
|
23
|
+
// QuickOrderDrawer renders via createPortal (document.body), outside .section scope.
|
|
24
|
+
// Wrapped in @layer components to match the same specificity as section-scoped styles.
|
|
25
|
+
@layer components {
|
|
26
|
+
@include meta.load-css("~@faststore/ui/src/components/atoms/Badge/styles.scss");
|
|
27
|
+
@include meta.load-css("~@faststore/ui/src/components/atoms/Button/styles.scss");
|
|
28
|
+
@include meta.load-css("~@faststore/ui/src/components/atoms/Icon/styles.scss");
|
|
29
|
+
@include meta.load-css("~@faststore/ui/src/components/atoms/Price/styles.scss");
|
|
30
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/Alert/styles.scss");
|
|
31
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/QuantitySelector/styles.scss");
|
|
32
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/Table/styles.scss");
|
|
33
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/Tooltip/styles.scss");
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
const mockUseAuth = vi.hoisted(() => vi.fn())
|
|
10
|
+
vi.mock('src/sdk/auth', () => ({ useAuth: mockUseAuth }))
|
|
11
|
+
|
|
12
|
+
import ProfileChallenge from '../../../src/components/auth/ProfileChallenge/ProfileChallenge'
|
|
13
|
+
|
|
14
|
+
describe('ProfileChallenge', () => {
|
|
15
|
+
it('renders children when authenticated', () => {
|
|
16
|
+
mockUseAuth.mockReturnValue({ isAuthenticated: true })
|
|
17
|
+
|
|
18
|
+
render(
|
|
19
|
+
<ProfileChallenge>
|
|
20
|
+
<span>Protected content</span>
|
|
21
|
+
</ProfileChallenge>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
expect(screen.queryByText('Protected content')).not.toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders fallback when not authenticated', () => {
|
|
28
|
+
mockUseAuth.mockReturnValue({ isAuthenticated: false })
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<ProfileChallenge fallback={<span>Please log in</span>}>
|
|
32
|
+
<span>Protected content</span>
|
|
33
|
+
</ProfileChallenge>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(screen.queryByText('Please log in')).not.toBeNull()
|
|
37
|
+
expect(screen.queryByText('Protected content')).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders nothing by default when not authenticated', () => {
|
|
41
|
+
mockUseAuth.mockReturnValue({ isAuthenticated: false })
|
|
42
|
+
|
|
43
|
+
render(
|
|
44
|
+
<ProfileChallenge>
|
|
45
|
+
<span>Protected content</span>
|
|
46
|
+
</ProfileChallenge>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(screen.queryByText('Protected content')).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { mapOrderFormItemsToProducts } from '../../../src/components/search/SearchInput/SearchInput'
|
|
4
|
+
|
|
5
|
+
const makeItem = (overrides = {}) => ({
|
|
6
|
+
id: 'sku-1',
|
|
7
|
+
name: 'Product A',
|
|
8
|
+
price: 100,
|
|
9
|
+
listPrice: 120,
|
|
10
|
+
quantity: 2,
|
|
11
|
+
imageUrl: 'https://example.com/img.jpg',
|
|
12
|
+
availability: 'available',
|
|
13
|
+
seller: '1',
|
|
14
|
+
unitMultiplier: 1,
|
|
15
|
+
...overrides,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('mapOrderFormItemsToProducts', () => {
|
|
19
|
+
it('maps a single available item correctly', () => {
|
|
20
|
+
const result = mapOrderFormItemsToProducts([makeItem()])
|
|
21
|
+
|
|
22
|
+
expect(result).toHaveLength(1)
|
|
23
|
+
expect(result[0]).toMatchObject({
|
|
24
|
+
id: 'sku-1',
|
|
25
|
+
name: 'Product A',
|
|
26
|
+
price: 100,
|
|
27
|
+
quantityUpdated: false,
|
|
28
|
+
image: { url: 'https://example.com/img.jpg', alternateName: 'Product A' },
|
|
29
|
+
inventory: 9999,
|
|
30
|
+
availability: 'available',
|
|
31
|
+
selectedCount: 2,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('sets inventory to 0 and availability to outOfStock for unavailable items', () => {
|
|
36
|
+
const result = mapOrderFormItemsToProducts([
|
|
37
|
+
makeItem({ availability: 'unavailable' }),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
expect(result[0].inventory).toBe(0)
|
|
41
|
+
expect(result[0].availability).toBe('outOfStock')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('falls back to empty string when imageUrl is null', () => {
|
|
45
|
+
const result = mapOrderFormItemsToProducts([makeItem({ imageUrl: null })])
|
|
46
|
+
|
|
47
|
+
expect(result[0].image.url).toBe('')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns empty array for empty input', () => {
|
|
51
|
+
expect(mapOrderFormItemsToProducts([])).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('maps multiple items preserving order', () => {
|
|
55
|
+
const items = [
|
|
56
|
+
makeItem({ id: 'a', name: 'A', quantity: 1 }),
|
|
57
|
+
makeItem({ id: 'b', name: 'B', quantity: 3 }),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const result = mapOrderFormItemsToProducts(items)
|
|
61
|
+
|
|
62
|
+
expect(result[0].id).toBe('a')
|
|
63
|
+
expect(result[1].id).toBe('b')
|
|
64
|
+
expect(result[0].selectedCount).toBe(1)
|
|
65
|
+
expect(result[1].selectedCount).toBe(3)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('always sets quantityUpdated to false', () => {
|
|
69
|
+
const result = mapOrderFormItemsToProducts([makeItem()])
|
|
70
|
+
expect(result[0].quantityUpdated).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
})
|