@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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { render, screen } from '@testing-library/react'
|
|
5
|
+
import userEvent from '@testing-library/user-event'
|
|
6
|
+
import StorefrontHero, { type FeaturedProduct } from './storefront-hero'
|
|
7
|
+
import { storefrontHeroPropsSchema } from '../../../lib/contracts'
|
|
8
|
+
|
|
9
|
+
// The showcase presentation mounts a carousel (embla) and motion elements, so
|
|
10
|
+
// the jsdom environment needs the browser observers those libraries read.
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
13
|
+
if (!window.matchMedia) {
|
|
14
|
+
window.matchMedia = ((query: string) => ({
|
|
15
|
+
matches: false,
|
|
16
|
+
media: query,
|
|
17
|
+
onchange: null,
|
|
18
|
+
addListener: () => {},
|
|
19
|
+
removeListener: () => {},
|
|
20
|
+
addEventListener: () => {},
|
|
21
|
+
removeEventListener: () => {},
|
|
22
|
+
dispatchEvent: () => false,
|
|
23
|
+
})) as unknown as typeof window.matchMedia
|
|
24
|
+
}
|
|
25
|
+
if (!('ResizeObserver' in globalThis)) {
|
|
26
|
+
;(globalThis as { ResizeObserver?: unknown }).ResizeObserver = class {
|
|
27
|
+
observe() {}
|
|
28
|
+
unobserve() {}
|
|
29
|
+
disconnect() {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!('IntersectionObserver' in globalThis)) {
|
|
33
|
+
;(globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = class {
|
|
34
|
+
observe() {}
|
|
35
|
+
unobserve() {}
|
|
36
|
+
disconnect() {}
|
|
37
|
+
takeRecords() {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const fullProduct: FeaturedProduct = {
|
|
45
|
+
id: 'p1',
|
|
46
|
+
name: 'Taladro percutor',
|
|
47
|
+
brand: 'INGCO',
|
|
48
|
+
price: 69.99,
|
|
49
|
+
originalPrice: 99.99,
|
|
50
|
+
rating: 4.8,
|
|
51
|
+
reviewCount: 236,
|
|
52
|
+
inStock: true,
|
|
53
|
+
isNew: true,
|
|
54
|
+
image: '/products/drill.png',
|
|
55
|
+
colors: ['#111111'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const bareProduct: FeaturedProduct = {
|
|
59
|
+
id: 'p2',
|
|
60
|
+
name: 'Decibelímetro',
|
|
61
|
+
price: 129.5,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const base = {
|
|
65
|
+
title: 'Potencia para cada proyecto',
|
|
66
|
+
description: 'Herramientas profesionales',
|
|
67
|
+
products: [] as FeaturedProduct[],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('StorefrontHero — compact: optional editorial image', () => {
|
|
71
|
+
it('renders no editorial image when none is supplied', () => {
|
|
72
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} />)
|
|
73
|
+
expect(html).not.toContain('<img')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('renders a native editorial image with its src and alt text', () => {
|
|
77
|
+
const html = renderToStaticMarkup(
|
|
78
|
+
<StorefrontHero {...base} heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico y accesorios' }} />,
|
|
79
|
+
)
|
|
80
|
+
expect(html).toContain('src="/hero-tools.png"')
|
|
81
|
+
expect(html).toContain('alt="Taladro inalámbrico y accesorios"')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('never renders an empty image src', () => {
|
|
85
|
+
const blank = renderToStaticMarkup(
|
|
86
|
+
<StorefrontHero {...base} heroImage={{ src: ' ', alt: 'Vacío' }} />,
|
|
87
|
+
)
|
|
88
|
+
expect(blank).not.toContain('<img')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('preserves the product grid when the editorial image is omitted', () => {
|
|
92
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
93
|
+
expect(html).toContain('Taladro percutor')
|
|
94
|
+
expect(html).toContain('src="/products/drill.png"')
|
|
95
|
+
expect(html).not.toContain('/hero-tools.png')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('keeps the product grid when the editorial image is supplied', () => {
|
|
99
|
+
const html = renderToStaticMarkup(
|
|
100
|
+
<StorefrontHero
|
|
101
|
+
{...base}
|
|
102
|
+
products={[fullProduct]}
|
|
103
|
+
heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico' }}
|
|
104
|
+
/>,
|
|
105
|
+
)
|
|
106
|
+
expect(html).toContain('src="/hero-tools.png"')
|
|
107
|
+
expect(html).toContain('Taladro percutor')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('StorefrontHero — compact: detail affordance', () => {
|
|
112
|
+
it('defaults the product-card action to "Ver producto"', () => {
|
|
113
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
114
|
+
expect(html).toContain('Ver producto')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('renders the add-to-cart control only when its handler is supplied', () => {
|
|
118
|
+
const withoutHandler = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
119
|
+
expect(withoutHandler).not.toContain('Agregar al carrito')
|
|
120
|
+
|
|
121
|
+
const withHandler = renderToStaticMarkup(
|
|
122
|
+
<StorefrontHero {...base} products={[fullProduct]} onAddToCart={() => {}} />,
|
|
123
|
+
)
|
|
124
|
+
expect(withHandler).toContain('Agregar al carrito')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('disables purchase and shows the out-of-stock label only from an explicit inStock: false', () => {
|
|
128
|
+
const available = renderToStaticMarkup(<StorefrontHero {...base} products={[bareProduct]} />)
|
|
129
|
+
expect(available).not.toContain('Agotado')
|
|
130
|
+
|
|
131
|
+
const outOfStock = renderToStaticMarkup(
|
|
132
|
+
<StorefrontHero {...base} products={[{ ...bareProduct, inStock: false }]} />,
|
|
133
|
+
)
|
|
134
|
+
expect(outOfStock).toContain('Agotado')
|
|
135
|
+
expect(outOfStock).toContain('disabled')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('StorefrontHero — compact: currency never hardcoded', () => {
|
|
140
|
+
it('formats prices through Intl with the supplied currency', () => {
|
|
141
|
+
const html = renderToStaticMarkup(
|
|
142
|
+
<StorefrontHero {...base} currency='EUR' products={[fullProduct]} />,
|
|
143
|
+
)
|
|
144
|
+
expect(html).toContain('69.99')
|
|
145
|
+
expect(html).toContain('99.99')
|
|
146
|
+
expect(html).not.toContain('$')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('renders a plain localized amount when no currency is supplied', () => {
|
|
150
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[bareProduct]} />)
|
|
151
|
+
expect(html).toContain('129.5')
|
|
152
|
+
expect(html).not.toContain('$')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('StorefrontHero — compact: truthful evidence only', () => {
|
|
157
|
+
it('omits brand, colors, and rating rows when the host supplies none', () => {
|
|
158
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[bareProduct]} />)
|
|
159
|
+
expect(html).not.toContain('Colores:')
|
|
160
|
+
expect(html).not.toContain('INGCO')
|
|
161
|
+
expect(html).not.toContain('(236)')
|
|
162
|
+
expect(html).not.toContain('Nuevo')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('renders rating and review count only when supplied', () => {
|
|
166
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
167
|
+
expect(html).toContain('(236)')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('renders the discount badge and struck original price only from a real price pair', () => {
|
|
171
|
+
const onSale = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
172
|
+
expect(onSale).toContain('% OFF')
|
|
173
|
+
expect(onSale).toContain('line-through')
|
|
174
|
+
|
|
175
|
+
const withoutPair = renderToStaticMarkup(
|
|
176
|
+
<StorefrontHero {...base} products={[{ ...bareProduct, originalPrice: 99 }]} />,
|
|
177
|
+
)
|
|
178
|
+
expect(withoutPair).not.toContain('% OFF')
|
|
179
|
+
expect(withoutPair).not.toContain('line-through')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('renders no invented claims with no optional evidence and a non-USD currency', () => {
|
|
183
|
+
const html = renderToStaticMarkup(
|
|
184
|
+
<StorefrontHero {...base} products={[bareProduct]} currency='EUR' />,
|
|
185
|
+
)
|
|
186
|
+
expect(html).toContain('Decibelímetro')
|
|
187
|
+
expect(html).toContain(new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(129.5))
|
|
188
|
+
expect(html).not.toContain('$')
|
|
189
|
+
expect(html).not.toContain('%')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('renders the product grid text-forward when the list is empty', () => {
|
|
193
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} />)
|
|
194
|
+
expect(html).toContain('Potencia para cada proyecto')
|
|
195
|
+
expect(html).not.toContain('src="/products/drill.png"')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('StorefrontHero — showcase: search affordance only with a handler', () => {
|
|
200
|
+
const showcase = { ...base, presentation: 'showcase' as const, products: [fullProduct] }
|
|
201
|
+
|
|
202
|
+
it('renders the search input only when onSearch is supplied', () => {
|
|
203
|
+
const withHandler = renderToStaticMarkup(<StorefrontHero {...showcase} onSearch={() => {}} />)
|
|
204
|
+
expect(withHandler).toContain('type="search"')
|
|
205
|
+
expect(withHandler).toContain('Buscar')
|
|
206
|
+
|
|
207
|
+
const withoutHandler = renderToStaticMarkup(<StorefrontHero {...showcase} />)
|
|
208
|
+
expect(withoutHandler).not.toContain('type="search"')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('produces a carousel structure (slide dots) absent from compact', () => {
|
|
212
|
+
const html = renderToStaticMarkup(<StorefrontHero {...showcase} />)
|
|
213
|
+
expect(html).toContain('Ir a la diapositiva 1')
|
|
214
|
+
expect(html).toContain('Taladro percutor')
|
|
215
|
+
|
|
216
|
+
const compact = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} />)
|
|
217
|
+
expect(compact).not.toContain('Ir a la diapositiva 1')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('formats slide prices with the supplied currency', () => {
|
|
221
|
+
const html = renderToStaticMarkup(<StorefrontHero {...showcase} currency='EUR' />)
|
|
222
|
+
expect(html).toContain(new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(69.99))
|
|
223
|
+
expect(html).not.toContain('$')
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('StorefrontHero — showcase: purchase affordance is handler-gated and stock-consistent', () => {
|
|
228
|
+
const purchaseLabel = 'Adquirir ahora'
|
|
229
|
+
const available: FeaturedProduct = { id: 'p1', name: 'Taladro percutor', price: 69.99, inStock: true }
|
|
230
|
+
const showcase = { ...base, presentation: 'showcase' as const, buyNowLabel: purchaseLabel }
|
|
231
|
+
|
|
232
|
+
it('renders no purchase-labeled control when no onBuyNow handler is supplied', () => {
|
|
233
|
+
render(<StorefrontHero {...showcase} products={[available]} />)
|
|
234
|
+
expect(screen.queryByRole('button', { name: purchaseLabel })).toBeNull()
|
|
235
|
+
expect(screen.queryByText(purchaseLabel)).toBeNull()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('does not offer an actionable purchase control for an out-of-stock product', async () => {
|
|
239
|
+
const user = userEvent.setup()
|
|
240
|
+
const onBuyNow = vi.fn()
|
|
241
|
+
render(<StorefrontHero {...showcase} products={[{ ...available, inStock: false }]} onBuyNow={onBuyNow} />)
|
|
242
|
+
const buy = screen.getByRole('button', { name: purchaseLabel }) as HTMLButtonElement
|
|
243
|
+
expect(buy.disabled).toBe(true)
|
|
244
|
+
await user.click(buy)
|
|
245
|
+
expect(onBuyNow).not.toHaveBeenCalled()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('renders an enabled purchase control that reports the product through the handler', async () => {
|
|
249
|
+
const user = userEvent.setup()
|
|
250
|
+
const onBuyNow = vi.fn()
|
|
251
|
+
render(<StorefrontHero {...showcase} products={[available]} onBuyNow={onBuyNow} />)
|
|
252
|
+
const buy = screen.getByRole('button', { name: purchaseLabel }) as HTMLButtonElement
|
|
253
|
+
expect(buy.disabled).toBe(false)
|
|
254
|
+
await user.click(buy)
|
|
255
|
+
expect(onBuyNow).toHaveBeenCalledWith(available)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('StorefrontHero — compact: host-supplied action label is handler-gated', () => {
|
|
260
|
+
const customLabel = 'Adquirir ahora'
|
|
261
|
+
|
|
262
|
+
it('omits a host-supplied action label when no onBuyNow handler is supplied', () => {
|
|
263
|
+
const html = renderToStaticMarkup(<StorefrontHero {...base} products={[fullProduct]} buyNowLabel={customLabel} />)
|
|
264
|
+
expect(html).not.toContain(customLabel)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('renders a host-supplied action label when onBuyNow is supplied', () => {
|
|
268
|
+
const html = renderToStaticMarkup(
|
|
269
|
+
<StorefrontHero {...base} products={[fullProduct]} buyNowLabel={customLabel} onBuyNow={() => {}} />,
|
|
270
|
+
)
|
|
271
|
+
expect(html).toContain(customLabel)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe('StorefrontHero — editorial: honest department and deal evidence', () => {
|
|
276
|
+
it('renders departments with the count row only when supplied', () => {
|
|
277
|
+
const html = renderToStaticMarkup(
|
|
278
|
+
<StorefrontHero
|
|
279
|
+
{...base}
|
|
280
|
+
presentation='editorial'
|
|
281
|
+
departments={[
|
|
282
|
+
{ id: 'd1', name: 'Herramientas', count: '120 referencias' },
|
|
283
|
+
{ id: 'd2', name: 'Medición' },
|
|
284
|
+
]}
|
|
285
|
+
/>,
|
|
286
|
+
)
|
|
287
|
+
expect(html).toContain('Herramientas')
|
|
288
|
+
expect(html).toContain('120 referencias')
|
|
289
|
+
expect(html).toContain('Medición')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('renders deal description and struck price only when supplied', () => {
|
|
293
|
+
const html = renderToStaticMarkup(
|
|
294
|
+
<StorefrontHero
|
|
295
|
+
{...base}
|
|
296
|
+
presentation='editorial'
|
|
297
|
+
featuredDeals={[
|
|
298
|
+
{ id: 'deal1', name: 'Combo taller', price: 199, originalPrice: 249 },
|
|
299
|
+
{ id: 'deal2', name: 'Rebate', description: 'Con descripción', price: 59 },
|
|
300
|
+
]}
|
|
301
|
+
/>,
|
|
302
|
+
)
|
|
303
|
+
expect(html).toContain('Con descripción')
|
|
304
|
+
expect(html).toContain('Combo taller')
|
|
305
|
+
expect(html).toContain('line-through')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('renders deal and locator buttons only with a real destination or handler', () => {
|
|
309
|
+
const html = renderToStaticMarkup(
|
|
310
|
+
<StorefrontHero
|
|
311
|
+
{...base}
|
|
312
|
+
presentation='editorial'
|
|
313
|
+
featuredDeals={[{ id: 'deal1', name: 'Combo taller', price: 199 }]}
|
|
314
|
+
storeLocator={{ title: 'Nuestras tiendas', description: 'Cerca de ti', ctaLabel: 'Ver tiendas' }}
|
|
315
|
+
/>,
|
|
316
|
+
)
|
|
317
|
+
expect(html).not.toContain('Comprar ahora')
|
|
318
|
+
expect(html).not.toContain('Ver tiendas')
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('StorefrontHero — promotional: no invented scarcity', () => {
|
|
323
|
+
const promotional = {
|
|
324
|
+
...base,
|
|
325
|
+
presentation: 'promotional' as const,
|
|
326
|
+
categories: [
|
|
327
|
+
{ id: 'c1', name: 'Electricidad', count: '2.5k+' },
|
|
328
|
+
{ id: 'c2', name: 'Fontanería' },
|
|
329
|
+
],
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
it('renders discount and countdown only when the host supplies them', () => {
|
|
333
|
+
const withEvidence = renderToStaticMarkup(
|
|
334
|
+
<StorefrontHero
|
|
335
|
+
{...promotional}
|
|
336
|
+
deals={[{ id: 'deal1', title: 'Semana eléctrica', discount: '15%', endTime: '2d : 15h' }]}
|
|
337
|
+
/>,
|
|
338
|
+
)
|
|
339
|
+
expect(withEvidence).toContain('Semana eléctrica')
|
|
340
|
+
expect(withEvidence).toContain('15%')
|
|
341
|
+
expect(withEvidence).toContain('2d : 15h')
|
|
342
|
+
|
|
343
|
+
const bare = renderToStaticMarkup(
|
|
344
|
+
<StorefrontHero {...promotional} deals={[{ id: 'deal1', title: 'Semana eléctrica' }]} />,
|
|
345
|
+
)
|
|
346
|
+
expect(bare).toContain('Semana eléctrica')
|
|
347
|
+
expect(bare).not.toContain('Termina en')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('renders the deals-bar shop-now button only with its handler', () => {
|
|
351
|
+
const bare = renderToStaticMarkup(
|
|
352
|
+
<StorefrontHero {...promotional} deals={[{ id: 'deal1', title: 'Semana eléctrica' }]} />,
|
|
353
|
+
)
|
|
354
|
+
expect(bare).not.toContain('Comprar ahora')
|
|
355
|
+
|
|
356
|
+
const withHandler = renderToStaticMarkup(
|
|
357
|
+
<StorefrontHero {...promotional} deals={[{ id: 'deal1', title: 'Semana eléctrica' }]} onShopNow={() => {}} />,
|
|
358
|
+
)
|
|
359
|
+
expect(withHandler).toContain('Comprar ahora')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('renders category counts and arrows only with evidence', () => {
|
|
363
|
+
const html = renderToStaticMarkup(<StorefrontHero {...promotional} />)
|
|
364
|
+
expect(html).toContain('2.5k+')
|
|
365
|
+
expect(html).toContain('Electricidad')
|
|
366
|
+
expect(html).toContain('Fontanería')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('renders the first deal when rotation has not advanced', () => {
|
|
370
|
+
const html = renderToStaticMarkup(
|
|
371
|
+
<StorefrontHero
|
|
372
|
+
{...promotional}
|
|
373
|
+
deals={[
|
|
374
|
+
{ id: 'deal1', title: 'Semana eléctrica' },
|
|
375
|
+
{ id: 'deal2', title: 'Mes de medición' },
|
|
376
|
+
]}
|
|
377
|
+
/>,
|
|
378
|
+
)
|
|
379
|
+
expect(html).toContain('Semana eléctrica')
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe('storefrontHeroPropsSchema — serializable contract', () => {
|
|
384
|
+
it('parses the canonical props subset across presentations', () => {
|
|
385
|
+
const parsed = storefrontHeroPropsSchema.parse({
|
|
386
|
+
title: 'Potencia para cada proyecto',
|
|
387
|
+
description: 'Herramientas profesionales',
|
|
388
|
+
presentation: 'promotional',
|
|
389
|
+
currency: 'EUR',
|
|
390
|
+
products: [fullProduct],
|
|
391
|
+
heroImage: { src: '/hero-tools.png', alt: 'Taladro inalámbrico' },
|
|
392
|
+
departments: [{ id: 'd1', name: 'Herramientas', count: '120 productos' }],
|
|
393
|
+
featuredDeals: [{ id: 'f1', name: 'Oferta', price: 49.9, originalPrice: 79.9 }],
|
|
394
|
+
deals: [{ id: 'deal1', title: 'Semana eléctrica', endTime: '2026-11-30' }],
|
|
395
|
+
categories: [{ id: 'c1', name: 'Taladros', count: '35' }],
|
|
396
|
+
rotationInterval: 3000,
|
|
397
|
+
})
|
|
398
|
+
expect(parsed.products).toHaveLength(1)
|
|
399
|
+
expect(parsed.presentation).toBe('promotional')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('rejects unknown presentation values', () => {
|
|
403
|
+
expect(() =>
|
|
404
|
+
storefrontHeroPropsSchema.parse({
|
|
405
|
+
title: 'x',
|
|
406
|
+
description: 'y',
|
|
407
|
+
presentation: 'mega',
|
|
408
|
+
products: [],
|
|
409
|
+
}),
|
|
410
|
+
).toThrow()
|
|
411
|
+
})
|
|
412
|
+
})
|