@thorprovider/blocks 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/.turbo/turbo-test.log +32 -5
- package/AGENTS.md +1 -1
- package/README.md +52 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts.d.mts +1111 -90
- package/dist/contracts.d.ts +1111 -90
- package/dist/contracts.js +189 -6
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +173 -7
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +467 -89
- package/dist/index.d.ts +467 -89
- package/dist/index.js +2967 -2250
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2989 -2282
- package/dist/index.mjs.map +1 -1
- package/docs/standards/theming.md +32 -5
- package/package.json +4 -3
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
- package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +137 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +231 -89
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +65 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +19 -0
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +54 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +43 -13
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +112 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +101 -28
- package/src/index.ts +38 -0
- package/src/lib/cart.test.ts +15 -0
- package/src/lib/cart.ts +11 -0
- package/src/lib/contracts-curated.test.ts +207 -0
- package/src/lib/contracts-server-boundary.test.ts +52 -0
- package/src/lib/contracts.ts +182 -4
- package/src/lib/money.test.ts +64 -0
- package/src/lib/money.ts +65 -0
- package/styles.css +1 -0
- package/tsup.config.ts +0 -1
- package/vitest.config.ts +9 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import type { PaymentMethod } from '@thorprovider/types'
|
|
4
|
+
import { CheckoutForm1PaymentStep } from './checkout-form-1-payment-step'
|
|
5
|
+
import { CheckoutForm1ShippingStep } from './checkout-form-1-shipping-step'
|
|
6
|
+
import { CheckoutForm1OrderSummary } from './checkout-form-1-order-summary'
|
|
7
|
+
import type { CheckoutForm1FormData } from './checkout-form-1'
|
|
8
|
+
|
|
9
|
+
const formData: CheckoutForm1FormData = {
|
|
10
|
+
email: '',
|
|
11
|
+
firstName: '',
|
|
12
|
+
lastName: '',
|
|
13
|
+
phone: '',
|
|
14
|
+
address: '',
|
|
15
|
+
city: '',
|
|
16
|
+
state: '',
|
|
17
|
+
zipCode: '',
|
|
18
|
+
country: 'US',
|
|
19
|
+
paymentMethod: 'pp_card',
|
|
20
|
+
cardNumber: '',
|
|
21
|
+
expiryDate: '',
|
|
22
|
+
cvv: '',
|
|
23
|
+
cardName: '',
|
|
24
|
+
saveInfo: false,
|
|
25
|
+
sameAsBilling: true,
|
|
26
|
+
newsletter: false,
|
|
27
|
+
promoCode: '',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cardMethod: PaymentMethod = {
|
|
31
|
+
id: 'pp_card',
|
|
32
|
+
name: 'Tarjeta',
|
|
33
|
+
type: 'card',
|
|
34
|
+
isEnabled: true,
|
|
35
|
+
requiresSetup: true,
|
|
36
|
+
description: 'Paga con tarjeta',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const manualMethod: PaymentMethod = {
|
|
40
|
+
id: 'pp_system_default',
|
|
41
|
+
name: 'Pago manual TEST',
|
|
42
|
+
type: 'cash_on_delivery',
|
|
43
|
+
isEnabled: true,
|
|
44
|
+
requiresSetup: false,
|
|
45
|
+
description: 'Pago de prueba, sin tarjeta',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const noop = () => {}
|
|
49
|
+
|
|
50
|
+
const formatMoney = (money: { amount: string; currencyCode: string }) =>
|
|
51
|
+
`${money.currencyCode} ${money.amount}`
|
|
52
|
+
|
|
53
|
+
describe('CheckoutForm1 payment step — launch flow collects no raw card data', () => {
|
|
54
|
+
it('renders no card fields by default even for a card-type method', () => {
|
|
55
|
+
const html = renderToStaticMarkup(
|
|
56
|
+
<CheckoutForm1PaymentStep
|
|
57
|
+
formData={formData}
|
|
58
|
+
onChange={noop}
|
|
59
|
+
onCardNumberChange={noop}
|
|
60
|
+
onExpiryChange={noop}
|
|
61
|
+
paymentMethods={[cardMethod]}
|
|
62
|
+
/>,
|
|
63
|
+
)
|
|
64
|
+
expect(html).not.toContain('checkout-card-number')
|
|
65
|
+
expect(html).not.toContain('checkout-cvv')
|
|
66
|
+
expect(html).toContain('Paga con tarjeta')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('renders the approved manual TEST provider label only', () => {
|
|
70
|
+
const html = renderToStaticMarkup(
|
|
71
|
+
<CheckoutForm1PaymentStep
|
|
72
|
+
formData={{ ...formData, paymentMethod: 'pp_system_default' }}
|
|
73
|
+
onChange={noop}
|
|
74
|
+
onCardNumberChange={noop}
|
|
75
|
+
onExpiryChange={noop}
|
|
76
|
+
paymentMethods={[manualMethod]}
|
|
77
|
+
/>,
|
|
78
|
+
)
|
|
79
|
+
expect(html).toContain('Pago manual TEST')
|
|
80
|
+
expect(html).toContain('Pago de prueba, sin tarjeta')
|
|
81
|
+
expect(html).not.toContain('checkout-card-number')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('renders card fields only in the explicit legacy mode', () => {
|
|
85
|
+
const html = renderToStaticMarkup(
|
|
86
|
+
<CheckoutForm1PaymentStep
|
|
87
|
+
formData={formData}
|
|
88
|
+
onChange={noop}
|
|
89
|
+
onCardNumberChange={noop}
|
|
90
|
+
onExpiryChange={noop}
|
|
91
|
+
paymentMethods={[cardMethod]}
|
|
92
|
+
collectCardFields
|
|
93
|
+
/>,
|
|
94
|
+
)
|
|
95
|
+
expect(html).toContain('checkout-card-number')
|
|
96
|
+
expect(html).toContain('checkout-cvv')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('CheckoutForm1 shipping step — controlled eligible options', () => {
|
|
101
|
+
it('renders only the supplied eligible shipping options with their prices', () => {
|
|
102
|
+
const html = renderToStaticMarkup(
|
|
103
|
+
<CheckoutForm1ShippingStep
|
|
104
|
+
formData={formData}
|
|
105
|
+
onChange={noop}
|
|
106
|
+
countries={[{ value: 'US', label: 'Estados Unidos' }]}
|
|
107
|
+
shippingOptions={[
|
|
108
|
+
{ id: 'sm_std', name: 'Estándar', price: { amount: '5.00', currencyCode: 'USD' } },
|
|
109
|
+
{ id: 'sm_exp', name: 'Exprés', price: { amount: '15.00', currencyCode: 'USD' } },
|
|
110
|
+
]}
|
|
111
|
+
selectedShippingOptionId='sm_exp'
|
|
112
|
+
onSelectShippingOption={noop}
|
|
113
|
+
formatMoney={formatMoney}
|
|
114
|
+
/>,
|
|
115
|
+
)
|
|
116
|
+
expect(html).toContain('Estándar')
|
|
117
|
+
expect(html).toContain('Exprés')
|
|
118
|
+
expect(html).toContain('USD 5.00')
|
|
119
|
+
expect(html).toContain('USD 15.00')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders no shipping section when no options are supplied', () => {
|
|
123
|
+
const html = renderToStaticMarkup(
|
|
124
|
+
<CheckoutForm1ShippingStep formData={formData} onChange={noop} />,
|
|
125
|
+
)
|
|
126
|
+
expect(html).not.toContain('Shipping method')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('CheckoutForm1 order summary — authoritative totals and states', () => {
|
|
131
|
+
const items = [
|
|
132
|
+
{
|
|
133
|
+
id: 'line_1',
|
|
134
|
+
name: 'Taladro',
|
|
135
|
+
variant: 'Rojo',
|
|
136
|
+
quantity: 2,
|
|
137
|
+
lineTotal: { amount: '90.00', currencyCode: 'USD' },
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
it('renders backend-authoritative totals verbatim', () => {
|
|
142
|
+
const html = renderToStaticMarkup(
|
|
143
|
+
<CheckoutForm1OrderSummary
|
|
144
|
+
orderSummary={{
|
|
145
|
+
items,
|
|
146
|
+
totals: {
|
|
147
|
+
subtotal: { amount: '90.00', currencyCode: 'USD' },
|
|
148
|
+
tax: { amount: '7.20', currencyCode: 'USD' },
|
|
149
|
+
shipping: { amount: '5.00', currencyCode: 'USD' },
|
|
150
|
+
total: { amount: '102.20', currencyCode: 'USD' },
|
|
151
|
+
},
|
|
152
|
+
}}
|
|
153
|
+
formatMoney={formatMoney}
|
|
154
|
+
/>,
|
|
155
|
+
)
|
|
156
|
+
expect(html).toContain('USD 90.00')
|
|
157
|
+
expect(html).toContain('USD 7.20')
|
|
158
|
+
expect(html).toContain('USD 5.00')
|
|
159
|
+
expect(html).toContain('USD 102.20')
|
|
160
|
+
expect(html).not.toContain('Free shipping on orders over')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('renders a controlled pending state without inventing amounts', () => {
|
|
164
|
+
const html = renderToStaticMarkup(
|
|
165
|
+
<CheckoutForm1OrderSummary orderSummary={{ items }} totalsState='pending' />,
|
|
166
|
+
)
|
|
167
|
+
expect(html).toContain('Actualizando totales')
|
|
168
|
+
expect(html).not.toContain('Total')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('never renders a fake zero for absent or malformed required totals', () => {
|
|
172
|
+
const html = renderToStaticMarkup(
|
|
173
|
+
<CheckoutForm1OrderSummary
|
|
174
|
+
orderSummary={{
|
|
175
|
+
items,
|
|
176
|
+
totals: {
|
|
177
|
+
subtotal: { amount: '', currencyCode: 'USD' },
|
|
178
|
+
total: { amount: 'abc', currencyCode: 'USD' },
|
|
179
|
+
},
|
|
180
|
+
}}
|
|
181
|
+
formatMoney={formatMoney}
|
|
182
|
+
/>,
|
|
183
|
+
)
|
|
184
|
+
expect(html).not.toContain('USD 0')
|
|
185
|
+
expect(html).toContain('Totales no disponibles')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('preserves a legitimate zero total', () => {
|
|
189
|
+
const html = renderToStaticMarkup(
|
|
190
|
+
<CheckoutForm1OrderSummary
|
|
191
|
+
orderSummary={{
|
|
192
|
+
items,
|
|
193
|
+
totals: {
|
|
194
|
+
subtotal: { amount: '0', currencyCode: 'USD' },
|
|
195
|
+
total: { amount: '0', currencyCode: 'USD' },
|
|
196
|
+
},
|
|
197
|
+
}}
|
|
198
|
+
formatMoney={formatMoney}
|
|
199
|
+
/>,
|
|
200
|
+
)
|
|
201
|
+
expect(html).toContain('USD 0')
|
|
202
|
+
expect(html).not.toContain('Totales no disponibles')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('renders a controlled error state with a recovery action', () => {
|
|
206
|
+
const html = renderToStaticMarkup(
|
|
207
|
+
<CheckoutForm1OrderSummary
|
|
208
|
+
orderSummary={{ items }}
|
|
209
|
+
totalsState='error'
|
|
210
|
+
onRetryTotals={noop}
|
|
211
|
+
/>,
|
|
212
|
+
)
|
|
213
|
+
expect(html).toContain('No se pudieron actualizar los totales')
|
|
214
|
+
expect(html).toContain('Reintentar')
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -11,7 +11,7 @@ import { CheckoutForm1PaymentStep } from './checkout-form-1-payment-step'
|
|
|
11
11
|
import { CheckoutForm1OrderSummary } from './checkout-form-1-order-summary'
|
|
12
12
|
import { CheckoutForm1Progress } from './checkout-form-1-progress'
|
|
13
13
|
import { formatCardNumber, formatExpiryDate } from './card-formatting'
|
|
14
|
-
import type { PaymentMethod } from '@thorprovider/types'
|
|
14
|
+
import type { Money, PaymentMethod } from '@thorprovider/types'
|
|
15
15
|
import type { CheckoutFieldErrors } from './checkout-form-types'
|
|
16
16
|
|
|
17
17
|
export interface CheckoutForm1FormData {
|
|
@@ -44,17 +44,37 @@ export type CheckoutForm1FieldDescriptions = Partial<
|
|
|
44
44
|
export interface CheckoutForm1OrderSummaryItem {
|
|
45
45
|
id: string | number
|
|
46
46
|
name: string
|
|
47
|
-
variant
|
|
48
|
-
price: number
|
|
47
|
+
variant?: string
|
|
49
48
|
quantity: number
|
|
50
|
-
|
|
49
|
+
/** Canonical backend line total; preferred over `price * quantity`. */
|
|
50
|
+
lineTotal?: Money
|
|
51
|
+
/** @deprecated legacy numeric price, used only when `lineTotal` is absent. */
|
|
52
|
+
price?: number
|
|
53
|
+
/** Optional; an absent image is omitted rather than serialized as `""`. */
|
|
54
|
+
image?: string
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
export interface CheckoutForm1Totals {
|
|
58
|
+
subtotal: Money
|
|
59
|
+
discount?: Money
|
|
60
|
+
tax?: Money
|
|
61
|
+
shipping?: Money
|
|
62
|
+
total: Money
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type CheckoutForm1TotalsState = 'idle' | 'pending' | 'error'
|
|
66
|
+
|
|
53
67
|
export interface CheckoutForm1OrderSummary {
|
|
54
68
|
items: CheckoutForm1OrderSummaryItem[]
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
/** Backend-authoritative totals; rendered verbatim when present. */
|
|
70
|
+
totals?: CheckoutForm1Totals
|
|
71
|
+
/** @deprecated legacy numeric amounts, used only when `totals` is absent. */
|
|
72
|
+
shipping?: number
|
|
73
|
+
/** @deprecated */
|
|
74
|
+
tax?: number
|
|
75
|
+
/** @deprecated */
|
|
57
76
|
discount?: number
|
|
77
|
+
/** @deprecated */
|
|
58
78
|
promoDiscount?: number
|
|
59
79
|
}
|
|
60
80
|
|
|
@@ -63,6 +83,15 @@ export interface CheckoutForm1CountryOption {
|
|
|
63
83
|
label: string
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
export interface CheckoutForm1ShippingOption {
|
|
87
|
+
/** Backend shipping method id — the controlled selection identity. */
|
|
88
|
+
id: string
|
|
89
|
+
name: string
|
|
90
|
+
price?: Money
|
|
91
|
+
description?: string
|
|
92
|
+
estimatedDays?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
export type CheckoutForm1ChangeHandler = (
|
|
67
96
|
field: keyof CheckoutForm1FormData,
|
|
68
97
|
value: string | boolean
|
|
@@ -78,11 +107,35 @@ export interface CheckoutForm1Props {
|
|
|
78
107
|
title?: string
|
|
79
108
|
description?: string
|
|
80
109
|
orderSummary?: CheckoutForm1OrderSummary
|
|
110
|
+
/** Controlled state of the totals fetch/mutation. */
|
|
111
|
+
totalsState?: CheckoutForm1TotalsState
|
|
112
|
+
totalsErrorLabel?: string
|
|
113
|
+
retryLabel?: string
|
|
114
|
+
onRetryTotals?: () => void
|
|
81
115
|
countries?: CheckoutForm1CountryOption[]
|
|
116
|
+
/** Eligible shipping options for the cart's region/address. */
|
|
117
|
+
shippingOptions?: CheckoutForm1ShippingOption[]
|
|
118
|
+
/** Controlled selection of a shipping option id. */
|
|
119
|
+
selectedShippingOptionId?: string
|
|
120
|
+
onSelectShippingOption?: (id: string) => void
|
|
82
121
|
paymentMethods?: PaymentMethod[]
|
|
83
122
|
noPaymentMethodsLabel?: string
|
|
123
|
+
/** Override money formatting (locale, currency). Defaults to `Money.currencyCode`. */
|
|
124
|
+
formatMoney?: (money: Money) => string
|
|
125
|
+
/**
|
|
126
|
+
* Raw card fields are legacy-only and never collected in the launch flow.
|
|
127
|
+
* Defaults to `false`: the curated surface renders no card inputs.
|
|
128
|
+
*/
|
|
129
|
+
collectCardFields?: boolean
|
|
130
|
+
onApplyPromo?: (code: string) => void
|
|
131
|
+
promoPending?: boolean
|
|
132
|
+
promoApplyLabel?: string
|
|
133
|
+
promoPlaceholder?: string
|
|
134
|
+
/** Optional trust copy; rendered only when supplied (never fabricated). */
|
|
135
|
+
trustNotes?: string[]
|
|
84
136
|
backLabel?: string
|
|
85
137
|
completeOrderLabel?: string
|
|
138
|
+
processingLabel?: string
|
|
86
139
|
continueLabel?: string
|
|
87
140
|
values: CheckoutForm1FormData
|
|
88
141
|
onChange: CheckoutForm1ChangeHandler
|
|
@@ -94,17 +147,35 @@ export interface CheckoutForm1Props {
|
|
|
94
147
|
submitting?: boolean
|
|
95
148
|
/** Order-level error surfaced above the footer actions. */
|
|
96
149
|
submitError?: string
|
|
150
|
+
/** Optional recovery action for a failed submission. */
|
|
151
|
+
onRetrySubmit?: () => void
|
|
152
|
+
submitErrorActionLabel?: string
|
|
97
153
|
}
|
|
98
154
|
|
|
99
155
|
export default function CheckoutForm1({
|
|
100
156
|
title,
|
|
101
157
|
description,
|
|
102
158
|
orderSummary,
|
|
159
|
+
totalsState = 'idle',
|
|
160
|
+
totalsErrorLabel,
|
|
161
|
+
retryLabel,
|
|
162
|
+
onRetryTotals,
|
|
103
163
|
countries,
|
|
164
|
+
shippingOptions,
|
|
165
|
+
selectedShippingOptionId,
|
|
166
|
+
onSelectShippingOption,
|
|
104
167
|
paymentMethods,
|
|
105
168
|
noPaymentMethodsLabel,
|
|
169
|
+
formatMoney,
|
|
170
|
+
collectCardFields = false,
|
|
171
|
+
onApplyPromo,
|
|
172
|
+
promoPending = false,
|
|
173
|
+
promoApplyLabel,
|
|
174
|
+
promoPlaceholder,
|
|
175
|
+
trustNotes,
|
|
106
176
|
backLabel = 'Atrás',
|
|
107
177
|
completeOrderLabel = 'Completar pedido',
|
|
178
|
+
processingLabel = 'Procesando pedido…',
|
|
108
179
|
continueLabel = 'Continuar',
|
|
109
180
|
values,
|
|
110
181
|
onChange,
|
|
@@ -114,6 +185,8 @@ export default function CheckoutForm1({
|
|
|
114
185
|
onSubmit,
|
|
115
186
|
submitting = false,
|
|
116
187
|
submitError,
|
|
188
|
+
onRetrySubmit,
|
|
189
|
+
submitErrorActionLabel = 'Reintentar',
|
|
117
190
|
}: CheckoutForm1Props) {
|
|
118
191
|
const [step, setStep] = useState(1)
|
|
119
192
|
|
|
@@ -175,6 +248,10 @@ export default function CheckoutForm1({
|
|
|
175
248
|
formData={values}
|
|
176
249
|
onChange={onChange}
|
|
177
250
|
countries={countries}
|
|
251
|
+
shippingOptions={shippingOptions}
|
|
252
|
+
selectedShippingOptionId={selectedShippingOptionId}
|
|
253
|
+
onSelectShippingOption={onSelectShippingOption}
|
|
254
|
+
formatMoney={formatMoney}
|
|
178
255
|
fieldErrors={fieldErrors}
|
|
179
256
|
fieldDescriptions={fieldDescriptions}
|
|
180
257
|
/>
|
|
@@ -187,6 +264,7 @@ export default function CheckoutForm1({
|
|
|
187
264
|
onExpiryChange={handleExpiryChange}
|
|
188
265
|
paymentMethods={paymentMethods}
|
|
189
266
|
noPaymentMethodsLabel={noPaymentMethodsLabel}
|
|
267
|
+
collectCardFields={collectCardFields}
|
|
190
268
|
fieldErrors={fieldErrors}
|
|
191
269
|
fieldDescriptions={fieldDescriptions}
|
|
192
270
|
/>
|
|
@@ -195,9 +273,20 @@ export default function CheckoutForm1({
|
|
|
195
273
|
{submitError && (
|
|
196
274
|
<div
|
|
197
275
|
role="alert"
|
|
198
|
-
className="mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
|
276
|
+
className="mt-6 flex flex-wrap items-center justify-between gap-3 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
|
199
277
|
>
|
|
200
|
-
{submitError}
|
|
278
|
+
<span>{submitError}</span>
|
|
279
|
+
{onRetrySubmit ? (
|
|
280
|
+
<Button
|
|
281
|
+
type="button"
|
|
282
|
+
variant="outline"
|
|
283
|
+
size="sm"
|
|
284
|
+
onClick={onRetrySubmit}
|
|
285
|
+
disabled={submitting}
|
|
286
|
+
>
|
|
287
|
+
{submitErrorActionLabel}
|
|
288
|
+
</Button>
|
|
289
|
+
) : null}
|
|
201
290
|
</div>
|
|
202
291
|
)}
|
|
203
292
|
|
|
@@ -213,7 +302,7 @@ export default function CheckoutForm1({
|
|
|
213
302
|
{isLastStep ? (
|
|
214
303
|
<Button type="button" onClick={handleSubmit} disabled={submitting}>
|
|
215
304
|
<Lock className="size-4" />
|
|
216
|
-
{submitting ?
|
|
305
|
+
{submitting ? processingLabel : completeOrderLabel}
|
|
217
306
|
</Button>
|
|
218
307
|
) : (
|
|
219
308
|
<Button type="button" onClick={handleContinue} disabled={submitting}>
|
|
@@ -226,10 +315,24 @@ export default function CheckoutForm1({
|
|
|
226
315
|
|
|
227
316
|
{orderSummary && (
|
|
228
317
|
<div className="lg:col-span-1">
|
|
229
|
-
<CheckoutForm1OrderSummary
|
|
318
|
+
<CheckoutForm1OrderSummary
|
|
319
|
+
orderSummary={orderSummary}
|
|
320
|
+
totalsState={totalsState}
|
|
321
|
+
totalsErrorLabel={totalsErrorLabel}
|
|
322
|
+
retryLabel={retryLabel}
|
|
323
|
+
onRetryTotals={onRetryTotals}
|
|
324
|
+
promoCode={values.promoCode}
|
|
325
|
+
onPromoCodeChange={(value) => onChange('promoCode', value)}
|
|
326
|
+
onApplyPromo={onApplyPromo}
|
|
327
|
+
promoPending={promoPending}
|
|
328
|
+
promoApplyLabel={promoApplyLabel}
|
|
329
|
+
promoPlaceholder={promoPlaceholder}
|
|
330
|
+
trustNotes={trustNotes}
|
|
331
|
+
formatMoney={formatMoney}
|
|
332
|
+
/>
|
|
230
333
|
</div>
|
|
231
334
|
)}
|
|
232
335
|
</div>
|
|
233
336
|
</div>
|
|
234
337
|
)
|
|
235
|
-
}
|
|
338
|
+
}
|
|
@@ -102,8 +102,6 @@ describe('MobileCategoryDrawer', () => {
|
|
|
102
102
|
expect(html).toContain('Ofertas')
|
|
103
103
|
expect(html).toContain('Novedades')
|
|
104
104
|
expect(html).toContain('href="/colecciones/ofertas"')
|
|
105
|
-
expect(html).toContain('href="/colecciones"')
|
|
106
|
-
expect(html).toContain('Ver todas')
|
|
107
105
|
expect(html).toContain('grid grid-cols-2')
|
|
108
106
|
})
|
|
109
107
|
|
|
@@ -149,4 +147,46 @@ describe('MobileCategoryDrawer', () => {
|
|
|
149
147
|
}),
|
|
150
148
|
).toThrow()
|
|
151
149
|
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('MobileCategoryDrawer — view-all link requires a served href', () => {
|
|
153
|
+
const base = { categories, collections, open: true, onClose: () => {} }
|
|
154
|
+
|
|
155
|
+
it('omits the link and never emits an empty href when viewAllHref is omitted', () => {
|
|
156
|
+
const html = renderToStaticMarkup(<MobileCategoryDrawer {...base} />)
|
|
157
|
+
expect(html).not.toContain('Ver todas')
|
|
158
|
+
expect(html).not.toContain('href=""')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('omits the link when viewAllHref is an empty string', () => {
|
|
162
|
+
const html = renderToStaticMarkup(<MobileCategoryDrawer {...base} viewAllHref="" />)
|
|
163
|
+
expect(html).not.toContain('Ver todas')
|
|
164
|
+
expect(html).not.toContain('href=""')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('omits the link when viewAllHref is whitespace only', () => {
|
|
168
|
+
const html = renderToStaticMarkup(<MobileCategoryDrawer {...base} viewAllHref=" " />)
|
|
169
|
+
expect(html).not.toContain('Ver todas')
|
|
170
|
+
expect(html).not.toContain('href=""')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('renders the link when a served path is supplied', () => {
|
|
174
|
+
const html = renderToStaticMarkup(
|
|
175
|
+
<MobileCategoryDrawer {...base} viewAllHref="/tienda/todas" />,
|
|
176
|
+
)
|
|
177
|
+
expect(html).toContain('href="/tienda/todas"')
|
|
178
|
+
expect(html).toContain('Ver todas')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('renders a custom label with the served path', () => {
|
|
182
|
+
const html = renderToStaticMarkup(
|
|
183
|
+
<MobileCategoryDrawer
|
|
184
|
+
{...base}
|
|
185
|
+
viewAllHref="/tienda/todas"
|
|
186
|
+
viewAllLabel="Explorar todo"
|
|
187
|
+
/>,
|
|
188
|
+
)
|
|
189
|
+
expect(html).toContain('href="/tienda/todas"')
|
|
190
|
+
expect(html).toContain('Explorar todo')
|
|
191
|
+
})
|
|
152
192
|
})
|
|
@@ -59,7 +59,7 @@ export function MobileCategoryDrawer({
|
|
|
59
59
|
rootTitle = 'Categorías',
|
|
60
60
|
collectionsTitle = 'Colecciones',
|
|
61
61
|
viewAllLabel = 'Ver todas',
|
|
62
|
-
viewAllHref
|
|
62
|
+
viewAllHref,
|
|
63
63
|
closeAriaLabel = 'Cerrar menú',
|
|
64
64
|
backAriaLabel = 'Volver',
|
|
65
65
|
menuAriaLabel = 'Menú de categorías',
|
|
@@ -235,13 +235,15 @@ export function MobileCategoryDrawer({
|
|
|
235
235
|
>
|
|
236
236
|
{collectionsTitle}
|
|
237
237
|
</h2>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
238
|
+
{viewAllHref && viewAllHref.trim() ? (
|
|
239
|
+
<a
|
|
240
|
+
href={viewAllHref}
|
|
241
|
+
onClick={handleClose}
|
|
242
|
+
className="text-xs font-bold text-primary"
|
|
243
|
+
>
|
|
244
|
+
{viewAllLabel}
|
|
245
|
+
</a>
|
|
246
|
+
) : null}
|
|
245
247
|
</div>
|
|
246
248
|
<div className="grid grid-cols-2 gap-3">
|
|
247
249
|
{collections.map((collection) => (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
ProductCategory2,
|
|
5
|
+
type ProductCategory2Category,
|
|
6
|
+
} from './product-category-2'
|
|
7
|
+
|
|
8
|
+
const base: ProductCategory2Category = {
|
|
9
|
+
id: 'cat-1',
|
|
10
|
+
name: 'Herramientas',
|
|
11
|
+
description: 'Taladros y accesorios',
|
|
12
|
+
image: '/products/drill.png',
|
|
13
|
+
trending: false,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const render = (category: ProductCategory2Category) =>
|
|
17
|
+
renderToStaticMarkup(<ProductCategory2 categories={[category]} />)
|
|
18
|
+
|
|
19
|
+
// The count is the only text node that ends in "items" inside a span (the
|
|
20
|
+
// `items-center` utility lives in a class attribute and is never followed by
|
|
21
|
+
// `</span>`).
|
|
22
|
+
const hasItemCount = (html: string) => html.includes('items</span>')
|
|
23
|
+
|
|
24
|
+
describe('ProductCategory2 — optional itemCount', () => {
|
|
25
|
+
it('renders no count when itemCount is omitted (unknown projection)', () => {
|
|
26
|
+
const html = render(base)
|
|
27
|
+
expect(hasItemCount(html)).toBe(false)
|
|
28
|
+
// The tile itself still renders.
|
|
29
|
+
expect(html).toContain('Herramientas')
|
|
30
|
+
expect(html).toContain('Browse')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('preserves a real zero count', () => {
|
|
34
|
+
const html = render({ ...base, itemCount: 0 })
|
|
35
|
+
expect(html).toContain('0 items</span>')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders a positive count', () => {
|
|
39
|
+
const html = render({ ...base, itemCount: 7 })
|
|
40
|
+
expect(html).toContain('7 items</span>')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('omits a negative count', () => {
|
|
44
|
+
expect(hasItemCount(render({ ...base, itemCount: -1 }))).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('omits a non-finite count', () => {
|
|
48
|
+
expect(hasItemCount(render({ ...base, itemCount: Number.NaN }))).toBe(false)
|
|
49
|
+
expect(hasItemCount(render({ ...base, itemCount: Number.POSITIVE_INFINITY }))).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|