@thorprovider/blocks 0.1.2 → 1.0.0
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/CHANGELOG.md +107 -0
- package/README.md +108 -23
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/e-commerce/navigation/back-to-top.test.tsx +27 -0
- package/src/blocks/e-commerce/navigation/back-to-top.tsx +71 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.test.tsx +174 -0
- package/src/blocks/e-commerce/navigation/breadcrumb.tsx +130 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.test.tsx +74 -0
- package/src/blocks/e-commerce/navigation/mobile-bottom-nav.tsx +78 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.test.tsx +35 -0
- package/src/blocks/e-commerce/navigation/skip-to-content.tsx +37 -0
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.test.tsx +78 -0
- package/src/blocks/e-commerce/product-list/product-showcase-1.tsx +277 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +204 -109
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +30 -14
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +150 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +102 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +154 -2
- package/src/blocks/marketing/navbars/navbar-4.tsx +152 -32
- package/src/index.ts +93 -46
- package/src/lib/contracts-curated.test.ts +42 -24
- package/src/lib/contracts.test.ts +143 -2
- package/src/lib/contracts.ts +395 -13
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -37
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1584
- package/dist/contracts.d.ts +0 -1584
- package/dist/contracts.js +0 -390
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -334
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -2786
- package/dist/index.d.ts +0 -2786
- package/dist/index.js +0 -17139
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17059
- package/dist/index.mjs.map +0 -1
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -131
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -137
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -65
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
type CarouselApi,
|
|
16
16
|
} from '../../../ui/carousel'
|
|
17
17
|
import { Progress } from '../../../ui/progress'
|
|
18
|
-
import { ProductCard3ReactionToggle } from '../product-list/product-card-3-reaction-toggle'
|
|
19
18
|
|
|
20
19
|
export interface Review {
|
|
21
20
|
id: string
|
|
@@ -194,11 +193,11 @@ export function ReviewRating2({
|
|
|
194
193
|
className="h-8 px-3 text-xs text-muted-foreground hover:bg-primary/10 hover:text-primary cursor-pointer"
|
|
195
194
|
aria-label={likesLabel}
|
|
196
195
|
>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
196
|
+
<ThumbsUp
|
|
197
|
+
className={cn(
|
|
198
|
+
'size-4',
|
|
199
|
+
isLiked[review.id] && 'text-primary',
|
|
200
|
+
)}
|
|
202
201
|
/>
|
|
203
202
|
{formatNumber((review.likes ?? 0) + (isLiked[review.id] ? 1 : 0))}
|
|
204
203
|
</Button>
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
5
|
+
import userEvent from '@testing-library/user-event'
|
|
6
|
+
import { Zap } from 'lucide-react'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { ShoppingCart, type ShoppingCartProps } from './shopping-cart'
|
|
10
|
+
import type { CartTotals, ShoppingCartItem } from '../../../lib/contracts'
|
|
11
|
+
|
|
12
|
+
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
|
13
|
+
|
|
14
|
+
// jsdom lacks browser APIs some shadcn/ui primitives probe on mount.
|
|
15
|
+
if (!window.matchMedia) {
|
|
16
|
+
window.matchMedia = ((query: string) => ({
|
|
17
|
+
matches: false,
|
|
18
|
+
media: query,
|
|
19
|
+
onchange: null,
|
|
20
|
+
addEventListener: () => {},
|
|
21
|
+
removeEventListener: () => {},
|
|
22
|
+
addListener: () => {},
|
|
23
|
+
removeListener: () => {},
|
|
24
|
+
dispatchEvent: () => false,
|
|
25
|
+
})) as unknown as typeof window.matchMedia
|
|
26
|
+
}
|
|
27
|
+
if (!('IntersectionObserver' in window)) {
|
|
28
|
+
;(window as Record<string, unknown>).IntersectionObserver = class {
|
|
29
|
+
observe() {}
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
takeRecords() {
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const usd = (amount: string) => ({ amount, currencyCode: 'USD' })
|
|
39
|
+
|
|
40
|
+
const items: ShoppingCartItem[] = [
|
|
41
|
+
{
|
|
42
|
+
id: 'line_1',
|
|
43
|
+
name: 'Taladro',
|
|
44
|
+
quantity: 2,
|
|
45
|
+
image: 'https://cdn.example/taladro.jpg',
|
|
46
|
+
unitPrice: usd('45.00'),
|
|
47
|
+
lineTotal: usd('90.00'),
|
|
48
|
+
},
|
|
49
|
+
{ id: 'line_2', name: 'Batería', quantity: 1, unitPrice: usd('20.00'), lineTotal: usd('20.00') },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const totals: CartTotals = {
|
|
53
|
+
subtotal: usd('110.00'),
|
|
54
|
+
discount: usd('10.00'),
|
|
55
|
+
tax: usd('8.00'),
|
|
56
|
+
shipping: usd('0'),
|
|
57
|
+
total: usd('108.00'),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const formatMoney = (money: { amount: string; currencyCode: string }) =>
|
|
61
|
+
`${money.currencyCode} ${money.amount}`
|
|
62
|
+
|
|
63
|
+
const renderMarkup = (props: ShoppingCartProps) => renderToStaticMarkup(<ShoppingCart {...props} />)
|
|
64
|
+
|
|
65
|
+
describe('ShoppingCart — renders from authoritative cart data', () => {
|
|
66
|
+
it('renders supplied backend totals verbatim (no local arithmetic)', () => {
|
|
67
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
68
|
+
expect(html).toContain('USD 110.00')
|
|
69
|
+
expect(html).toContain('USD 10.00')
|
|
70
|
+
expect(html).toContain('USD 8.00')
|
|
71
|
+
expect(html).toContain('Gratis')
|
|
72
|
+
expect(html).toContain('USD 108.00')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not invent subtotal or total when none are supplied', () => {
|
|
76
|
+
const html = renderMarkup({ items, promotions: [], formatMoney })
|
|
77
|
+
expect(html).not.toContain('Subtotal')
|
|
78
|
+
expect(html).not.toContain('Total')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders a controlled pending state', () => {
|
|
82
|
+
const html = renderMarkup({ items, promotions: [], totalsState: 'pending' })
|
|
83
|
+
expect(html).toContain('Actualizando totales')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('renders a controlled error state with a recovery action', () => {
|
|
87
|
+
const html = renderMarkup({ items, promotions: [], totalsState: 'error', onRetryTotals: () => {} })
|
|
88
|
+
expect(html).toContain('No se pudieron actualizar los totales')
|
|
89
|
+
expect(html).toContain('Reintentar')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('never renders a fake zero for absent or malformed required totals', () => {
|
|
93
|
+
const html = renderMarkup({
|
|
94
|
+
items,
|
|
95
|
+
promotions: [],
|
|
96
|
+
totals: { subtotal: { amount: '', currencyCode: 'USD' }, total: { amount: 'abc', currencyCode: 'USD' } },
|
|
97
|
+
formatMoney,
|
|
98
|
+
})
|
|
99
|
+
expect(html).not.toContain('USD 0')
|
|
100
|
+
expect(html).toContain('Totales no disponibles')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('preserves a legitimate zero total', () => {
|
|
104
|
+
const html = renderMarkup({
|
|
105
|
+
items,
|
|
106
|
+
promotions: [],
|
|
107
|
+
totals: { subtotal: usd('0'), total: usd('0') },
|
|
108
|
+
formatMoney,
|
|
109
|
+
})
|
|
110
|
+
expect(html).toContain('USD 0')
|
|
111
|
+
expect(html).not.toContain('Totales no disponibles')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('renders the authoritative line total verbatim, never unit price × quantity', () => {
|
|
115
|
+
const html = renderMarkup({
|
|
116
|
+
items: [
|
|
117
|
+
{ id: 'line_x', name: 'Pieza', quantity: 1, unitPrice: usd('10.00'), lineTotal: usd('10.00') },
|
|
118
|
+
{ id: 'line_y', name: 'Set', quantity: 3, unitPrice: usd('10.00'), lineTotal: usd('25.00') },
|
|
119
|
+
],
|
|
120
|
+
promotions: [],
|
|
121
|
+
totals,
|
|
122
|
+
formatMoney,
|
|
123
|
+
})
|
|
124
|
+
expect(html).toContain('>USD 10.00<')
|
|
125
|
+
expect(html).toContain('>USD 25.00<')
|
|
126
|
+
expect(html).not.toContain('>USD 30')
|
|
127
|
+
expect(html).not.toContain('—')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('shows an unavailable marker (not zero) for a malformed line total', () => {
|
|
131
|
+
const html = renderMarkup({
|
|
132
|
+
items: [{ id: 'line_x', name: 'Pieza', quantity: 1, unitPrice: usd('10.00'), lineTotal: { amount: '', currencyCode: 'USD' } }],
|
|
133
|
+
promotions: [],
|
|
134
|
+
totals,
|
|
135
|
+
formatMoney,
|
|
136
|
+
})
|
|
137
|
+
expect(html).toContain('—')
|
|
138
|
+
expect(html).not.toContain('USD 0')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('renders absolute quantities from props (cart contract, not a delta)', () => {
|
|
142
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
143
|
+
expect(html).toContain('>2<')
|
|
144
|
+
expect(html).toContain('>1<')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('ShoppingCart — optional fields are omitted, not emptied', () => {
|
|
149
|
+
it('omits an absent line image without emitting an empty src', () => {
|
|
150
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
151
|
+
expect(html).not.toMatch(/src=""/)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('renders no delivery estimate, express controls, or suggestions when absent', () => {
|
|
155
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
156
|
+
expect(html).not.toContain('Entrega estimada')
|
|
157
|
+
expect(html).not.toContain('Apple Pay')
|
|
158
|
+
expect(html).not.toContain('Google Pay')
|
|
159
|
+
expect(html).not.toContain('Compra rápida')
|
|
160
|
+
expect(html).not.toContain('Vistos recientemente')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('omits the promo entry affordance when no callback is supplied', () => {
|
|
164
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
165
|
+
expect(html).not.toContain('código de descuento')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('ShoppingCart — truthful evidence only', () => {
|
|
170
|
+
it('renders host-supplied suggestions with Money prices and omits absent fields', () => {
|
|
171
|
+
const html = renderMarkup({
|
|
172
|
+
items,
|
|
173
|
+
promotions: [],
|
|
174
|
+
totals,
|
|
175
|
+
formatMoney,
|
|
176
|
+
recentlyViewed: [
|
|
177
|
+
{ id: 'p1', name: 'Mouse', image: '/demo/mouse.svg', unitPrice: usd('5.00') },
|
|
178
|
+
{ id: 'p2', name: 'Cable' },
|
|
179
|
+
],
|
|
180
|
+
})
|
|
181
|
+
expect(html).toContain('Vistos recientemente')
|
|
182
|
+
expect(html).toContain('USD 5.00')
|
|
183
|
+
expect(html).not.toMatch(/src=""/)
|
|
184
|
+
expect(html).not.toContain('$')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('derives the display currency from the authoritative Money subtotal', () => {
|
|
188
|
+
const eur = (amount: string) => ({ amount, currencyCode: 'EUR' })
|
|
189
|
+
const html = renderMarkup({
|
|
190
|
+
items,
|
|
191
|
+
promotions: [],
|
|
192
|
+
totals: { subtotal: eur('110.00'), discount: eur('10.00'), tax: eur('8.00'), shipping: eur('0'), total: eur('108.00') },
|
|
193
|
+
})
|
|
194
|
+
expect(html).toMatch(/EUR\s110\.00/)
|
|
195
|
+
expect(html).toMatch(/EUR\s108\.00/)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('renders host-supplied promotion progress and activated copy from data', () => {
|
|
199
|
+
const html = renderMarkup({
|
|
200
|
+
items,
|
|
201
|
+
promotions: [{ threshold: 200, reward: 'Envío gratis', icon: Zap }],
|
|
202
|
+
totals,
|
|
203
|
+
formatMoney,
|
|
204
|
+
})
|
|
205
|
+
expect(html).toContain('Te faltan')
|
|
206
|
+
expect(html).toContain('Envío gratis')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('ShoppingCart — actions are reported, not owned', () => {
|
|
211
|
+
let container: HTMLElement
|
|
212
|
+
let root: Root
|
|
213
|
+
let user: ReturnType<typeof userEvent.setup>
|
|
214
|
+
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
container = document.createElement('div')
|
|
217
|
+
document.body.appendChild(container)
|
|
218
|
+
root = createRoot(container)
|
|
219
|
+
user = userEvent.setup()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
act(() => root.unmount())
|
|
224
|
+
container.remove()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const mount = (props: ShoppingCartProps) => {
|
|
228
|
+
act(() => {
|
|
229
|
+
root.render(<ShoppingCart {...props} />)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
it('reports absolute quantity changes keyed by the backend line id', async () => {
|
|
234
|
+
const onQuantityChange = vi.fn()
|
|
235
|
+
mount({ items, promotions: [], totals, onQuantityChange })
|
|
236
|
+
const increase = container.querySelector('button[aria-label="Aumentar cantidad, Taladro"]')!
|
|
237
|
+
await user.click(increase)
|
|
238
|
+
expect(onQuantityChange).toHaveBeenCalledWith('line_1', 3)
|
|
239
|
+
const decrease = container.querySelector('button[aria-label="Disminuir cantidad, Taladro"]')!
|
|
240
|
+
await user.click(decrease)
|
|
241
|
+
expect(onQuantityChange).toHaveBeenCalledWith('line_1', 1)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('reports removals keyed by the backend line id', async () => {
|
|
245
|
+
const onRemove = vi.fn()
|
|
246
|
+
mount({ items, promotions: [], totals, onRemove })
|
|
247
|
+
await user.click(container.querySelector('button[aria-label="Quitar Taladro"]')!)
|
|
248
|
+
expect(onRemove).toHaveBeenCalledWith('line_1')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('reports the promo code to the host and never resolves it locally', async () => {
|
|
252
|
+
const onApplyPromo = vi.fn()
|
|
253
|
+
mount({ items, promotions: [], totals, onApplyPromo })
|
|
254
|
+
const input = container.querySelector('input')!
|
|
255
|
+
await user.type(input, 'X10')
|
|
256
|
+
await user.click(container.querySelector('button[type="submit"]')!)
|
|
257
|
+
expect(onApplyPromo).toHaveBeenCalledWith('X10')
|
|
258
|
+
expect((input as HTMLInputElement).value).toBe('')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('does not invent an applied-discount row from the entered code', async () => {
|
|
262
|
+
const onApplyPromo = vi.fn()
|
|
263
|
+
mount({ items, promotions: [], totals, onApplyPromo, formatMoney })
|
|
264
|
+
const input = container.querySelector('input')!
|
|
265
|
+
await user.type(input, 'X10')
|
|
266
|
+
await user.click(container.querySelector('button[type="submit"]')!)
|
|
267
|
+
expect(onApplyPromo).toHaveBeenCalledTimes(1)
|
|
268
|
+
expect(container.querySelector('input')!.value).toBe('')
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('ShoppingCart — presentation and action availability', () => {
|
|
273
|
+
it('defaults to the compact collapsible card', () => {
|
|
274
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
275
|
+
expect(html).toContain('max-w-md')
|
|
276
|
+
expect(html).toContain('Contraer carrito')
|
|
277
|
+
expect(html).toContain('max-h-64')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('renders page presentation as a wide, always-expanded review with a heading', () => {
|
|
281
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney, presentation: 'page' })
|
|
282
|
+
expect(html).toContain('max-w-3xl')
|
|
283
|
+
expect(html).toContain('<h1')
|
|
284
|
+
expect(html).not.toContain('Contraer carrito')
|
|
285
|
+
expect(html).not.toContain('max-h-64')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('keeps the same data and action contract across presentations', () => {
|
|
289
|
+
for (const presentation of ['compact', 'page'] as const) {
|
|
290
|
+
const html = renderMarkup({
|
|
291
|
+
items,
|
|
292
|
+
promotions: [],
|
|
293
|
+
totals,
|
|
294
|
+
formatMoney,
|
|
295
|
+
presentation,
|
|
296
|
+
onApplyPromo: () => {},
|
|
297
|
+
})
|
|
298
|
+
expect(html).toContain('USD 108.00')
|
|
299
|
+
expect(html).toContain('Taladro')
|
|
300
|
+
expect(html).toContain('código de descuento')
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('offers a continue-shopping action in page presentation', () => {
|
|
305
|
+
const html = renderMarkup({
|
|
306
|
+
items,
|
|
307
|
+
promotions: [],
|
|
308
|
+
totals,
|
|
309
|
+
formatMoney,
|
|
310
|
+
presentation: 'page',
|
|
311
|
+
onContinueShopping: () => {},
|
|
312
|
+
})
|
|
313
|
+
expect(html).toContain('Seguir comprando')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('suppresses checkout when the checkout feature is unavailable', () => {
|
|
317
|
+
const html = renderMarkup({
|
|
318
|
+
items,
|
|
319
|
+
promotions: [],
|
|
320
|
+
totals,
|
|
321
|
+
formatMoney,
|
|
322
|
+
presentation: 'page',
|
|
323
|
+
checkoutUnavailable: true,
|
|
324
|
+
})
|
|
325
|
+
expect(html).not.toContain('Ver carrito y finalizar')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('disables checkout while the cart is not ready', () => {
|
|
329
|
+
const html = renderMarkup({
|
|
330
|
+
items,
|
|
331
|
+
promotions: [],
|
|
332
|
+
totals,
|
|
333
|
+
formatMoney,
|
|
334
|
+
presentation: 'page',
|
|
335
|
+
checkoutDisabled: true,
|
|
336
|
+
})
|
|
337
|
+
expect(html).toMatch(/<button[^>]*disabled[^>]*>[^<]*Ver carrito y finalizar/)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('ShoppingCart — no hardcoded currency or locale copy', () => {
|
|
342
|
+
it('formats every amount through the canonical Money path', () => {
|
|
343
|
+
const html = renderMarkup({
|
|
344
|
+
items,
|
|
345
|
+
promotions: [],
|
|
346
|
+
totals,
|
|
347
|
+
formatMoney,
|
|
348
|
+
recentlyViewed: [{ id: 'p1', name: 'Mouse', unitPrice: usd('5.00') }],
|
|
349
|
+
})
|
|
350
|
+
expect(html).toContain('USD 110.00')
|
|
351
|
+
expect(html).toContain('USD 5.00')
|
|
352
|
+
expect(html).not.toMatch(/\$\d/)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('uses overridable labels for actions, states, and summary copy', () => {
|
|
356
|
+
const html = renderMarkup({
|
|
357
|
+
items,
|
|
358
|
+
promotions: [],
|
|
359
|
+
totals,
|
|
360
|
+
titleLabel: 'Mi carrito',
|
|
361
|
+
emptyLabel: 'Nada por aquí',
|
|
362
|
+
checkoutLabel: 'Pagar',
|
|
363
|
+
})
|
|
364
|
+
expect(html).toContain('Mi carrito')
|
|
365
|
+
expect(html).toContain('Pagar')
|
|
366
|
+
const empty = renderMarkup({
|
|
367
|
+
items: [],
|
|
368
|
+
promotions: [],
|
|
369
|
+
emptyLabel: 'Nada por aquí',
|
|
370
|
+
titleLabel: 'Mi carrito',
|
|
371
|
+
})
|
|
372
|
+
expect(empty).toContain('Nada por aquí')
|
|
373
|
+
expect(empty).not.toContain('Taladro')
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
describe('ShoppingCart — accessibility', () => {
|
|
378
|
+
it('names every line mutation control', () => {
|
|
379
|
+
const html = renderMarkup({ items, promotions: [], totals, formatMoney })
|
|
380
|
+
expect(html).toContain('aria-label="Disminuir cantidad, Taladro"')
|
|
381
|
+
expect(html).toContain('aria-label="Aumentar cantidad, Taladro"')
|
|
382
|
+
expect(html).toContain('aria-label="Quitar Taladro"')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('names the promo input and apply action', () => {
|
|
386
|
+
const html = renderMarkup({ items, promotions: [], totals, onApplyPromo: () => {} })
|
|
387
|
+
expect(html).toContain('código de descuento')
|
|
388
|
+
expect(html).toMatch(/<label[^>]*for=/)
|
|
389
|
+
expect(html).toMatch(/<input[^>]*id=/)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('announces pending totals politely and errors as an alert', () => {
|
|
393
|
+
const pending = renderMarkup({ items, promotions: [], totalsState: 'pending' })
|
|
394
|
+
expect(pending).toContain('aria-live="polite"')
|
|
395
|
+
expect(pending).toContain('Actualizando totales')
|
|
396
|
+
const error = renderMarkup({ items, promotions: [], totalsState: 'error' })
|
|
397
|
+
expect(error).toMatch(/role="alert"/)
|
|
398
|
+
})
|
|
399
|
+
})
|