@thorprovider/blocks 0.1.3 → 2.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -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
+ })