@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,283 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+ import ProductOverview, {
4
+ galleryImages,
5
+ resolveAddToCartPayload,
6
+ variantLabelOf,
7
+ type ProductDetailProduct,
8
+ type ProductOverviewProps,
9
+ } from './product-overview'
10
+
11
+ const formatPrice = (money: { amount: string; currencyCode: string }) =>
12
+ `${money.currencyCode} ${money.amount}`
13
+
14
+ const product: ProductDetailProduct = {
15
+ id: 'p1',
16
+ handle: 'taladro',
17
+ title: 'Taladro inalámbrico',
18
+ description: 'Un taladro',
19
+ vendor: 'Acme',
20
+ options: [{ id: 'o1', name: 'Color', values: ['Rojo', 'Azul'] }],
21
+ variants: [
22
+ {
23
+ id: 'v-red',
24
+ title: 'Rojo',
25
+ availableForSale: true,
26
+ selectedOptions: [{ name: 'Color', value: 'Rojo' }],
27
+ price: { amount: '10.00', currencyCode: 'USD' },
28
+ },
29
+ {
30
+ id: 'v-blue',
31
+ title: 'Azul',
32
+ availableForSale: true,
33
+ selectedOptions: [{ name: 'Color', value: 'Azul' }],
34
+ price: { amount: '25.50', currencyCode: 'USD' },
35
+ },
36
+ {
37
+ id: 'v-sold',
38
+ title: 'Verde',
39
+ availableForSale: false,
40
+ selectedOptions: [{ name: 'Color', value: 'Verde' }],
41
+ price: { amount: '9.00', currencyCode: 'USD' },
42
+ },
43
+ {
44
+ id: 'v-noprice',
45
+ title: 'Gris',
46
+ availableForSale: true,
47
+ selectedOptions: [{ name: 'Color', value: 'Gris' }],
48
+ price: null,
49
+ },
50
+ {
51
+ id: 'v-free',
52
+ title: 'Blanco',
53
+ availableForSale: true,
54
+ selectedOptions: [{ name: 'Color', value: 'Blanco' }],
55
+ price: { amount: '0', currencyCode: 'USD' },
56
+ },
57
+ ],
58
+ images: [
59
+ { url: 'https://cdn.example/p1.jpg', altText: 'p1' },
60
+ { url: 'https://cdn.example/p2.jpg', altText: 'p2' },
61
+ ],
62
+ }
63
+
64
+ const singleImageProduct: ProductDetailProduct = {
65
+ ...product,
66
+ images: [{ url: 'https://cdn.example/p1.jpg', altText: 'p1' }],
67
+ }
68
+
69
+ const render = (props: ProductOverviewProps) => renderToStaticMarkup(<ProductOverview {...props} />)
70
+
71
+ describe('ProductOverview — canonical render (task 2.1)', () => {
72
+ it('renders title, images, option/variant selection, and the selected price', () => {
73
+ const html = render({ product, selectedVariantId: 'v-blue', formatPrice, onAddToCart: () => {} })
74
+ expect(html).toContain('Taladro inalámbrico')
75
+ expect(html).toContain('https://cdn.example/p1.jpg')
76
+ expect(html).toContain('Variantes')
77
+ expect(html).toContain('Azul')
78
+ expect(html).toContain('USD 25.50')
79
+ })
80
+ })
81
+
82
+ describe('ProductOverview — bounded presentations (tasks 2.2, 2.3)', () => {
83
+ it('renders the gallery presentation from the same canonical props with a different DOM structure', () => {
84
+ const standardHtml = render({ product, selectedVariantId: 'v-blue', formatPrice, onAddToCart: () => {} })
85
+ const galleryHtml = render({
86
+ product,
87
+ presentation: 'gallery',
88
+ selectedVariantId: 'v-blue',
89
+ formatPrice,
90
+ onAddToCart: () => {},
91
+ })
92
+ expect(galleryHtml).not.toBe(standardHtml)
93
+ expect(standardHtml).toContain('@container')
94
+ expect(galleryHtml).not.toContain('@container')
95
+ expect(galleryHtml).toContain('Taladro inalámbrico')
96
+ expect(galleryHtml).toContain('USD 25.50')
97
+ })
98
+
99
+ it('omits absent rich sections without empty chrome', () => {
100
+ const html = render({ product: singleImageProduct, presentation: 'rich', formatPrice })
101
+ expect(html).not.toContain('Características clave')
102
+ expect(html).not.toContain('Especificaciones técnicas')
103
+ expect(html).not.toContain('Resumen')
104
+ })
105
+
106
+ it('renders the rich sections only when the optional data is supplied', () => {
107
+ const html = render({
108
+ product,
109
+ presentation: 'rich',
110
+ selectedVariantId: 'v-blue',
111
+ formatPrice,
112
+ features: ['Batería de larga duración', 'Maletín incluido'],
113
+ specifications: { Voltaje: '18V', Protección: 'IP54' },
114
+ reviews: [{ id: 1, author: 'Ana', rating: 5, comment: 'Excelente', date: '2026-01-01' }],
115
+ rating: { value: 4, count: 3 },
116
+ })
117
+ expect(html).toContain('Características clave')
118
+ expect(html).toContain('Batería de larga duración')
119
+ expect(html).toContain('Resumen')
120
+ expect(html).toContain('Especificaciones')
121
+ expect(html).toContain('Reseñas (1)')
122
+ expect(html).toContain('Galería')
123
+ })
124
+
125
+ it('renders the specifications section when it is the only rich data', () => {
126
+ const html = render({
127
+ product,
128
+ presentation: 'rich',
129
+ formatPrice,
130
+ specifications: { Voltaje: '18V', Protección: 'IP54' },
131
+ })
132
+ expect(html).toContain('Especificaciones técnicas')
133
+ expect(html).toContain('IP54')
134
+ })
135
+
136
+ it('renders the reviews section when it is the only rich data', () => {
137
+ const html = render({
138
+ product,
139
+ presentation: 'rich',
140
+ formatPrice,
141
+ reviews: [{ id: 1, author: 'Ana', rating: 5, comment: 'Excelente', date: '2026-01-01' }],
142
+ })
143
+ expect(html).toContain('Ana')
144
+ expect(html).toContain('Excelente')
145
+ })
146
+ })
147
+
148
+ describe('ProductOverview — controlled selection and add-to-cart identity (task 2.4)', () => {
149
+ it('does not default to the first variant and prompts for an explicit selection', () => {
150
+ const html = render({ product, formatPrice, onAddToCart: () => {} })
151
+ expect(html).toContain('Selecciona una variante')
152
+ expect(html).not.toContain('USD 10.00')
153
+ })
154
+
155
+ it('shows the selected non-default variant at its current price', () => {
156
+ const html = render({ product, selectedVariantId: 'v-blue', formatPrice, onAddToCart: () => {} })
157
+ expect(html).toContain('USD 25.50')
158
+ expect(html).not.toContain('USD 10.00')
159
+ })
160
+
161
+ it('reports a missing price as unavailable instead of zero', () => {
162
+ const html = render({ product, selectedVariantId: 'v-noprice', formatPrice })
163
+ expect(html).toContain('Precio no disponible')
164
+ expect(html).not.toContain('USD 0')
165
+ })
166
+
167
+ it('preserves a backend-resolved valid zero', () => {
168
+ const html = render({ product, selectedVariantId: 'v-free', formatPrice })
169
+ expect(html).toContain('USD 0')
170
+ expect(html).not.toContain('Precio no disponible')
171
+ })
172
+
173
+ it('enables purchase only for an available, priced, explicitly selected variant', () => {
174
+ const onlyAvailable: ProductDetailProduct = { ...product, variants: [product.variants[0]] }
175
+ const enabled = render({
176
+ product: onlyAvailable,
177
+ selectedVariantId: 'v-red',
178
+ formatPrice,
179
+ onAddToCart: () => {},
180
+ })
181
+ expect(enabled).not.toContain('disabled=""')
182
+
183
+ const noSelection = render({ product: onlyAvailable, formatPrice, onAddToCart: () => {} })
184
+ expect(noSelection).toContain('disabled=""')
185
+ })
186
+ })
187
+
188
+ describe('ProductOverview — sector-neutral (task 2.5)', () => {
189
+ it('renders a non-fashion product with no size/color system and derives currency from Money', () => {
190
+ const electronics: ProductDetailProduct = {
191
+ id: 'e1',
192
+ title: 'Laptop Pro 14',
193
+ description: 'Una laptop',
194
+ vendor: 'Nimbus',
195
+ options: [{ id: 'o1', name: 'Capacidad', values: ['256 GB', '512 GB'] }],
196
+ variants: [
197
+ {
198
+ id: 'e-256',
199
+ title: '256 GB',
200
+ availableForSale: true,
201
+ selectedOptions: [{ name: 'Capacidad', value: '256 GB' }],
202
+ price: { amount: '256.00', currencyCode: 'EUR' },
203
+ },
204
+ ],
205
+ images: [{ url: 'https://cdn.example/e1.jpg', altText: 'e1' }],
206
+ }
207
+
208
+ const html = render({ product: electronics, selectedVariantId: 'e-256', formatPrice })
209
+ expect(html).toContain('Laptop Pro 14')
210
+ expect(html).toContain('256 GB')
211
+ expect(html).toContain('EUR 256.00')
212
+ expect(html).not.toContain('$')
213
+ expect(html).not.toContain('Talla')
214
+ })
215
+ })
216
+
217
+ describe('ProductOverview — purpose-gated wishlist affordance', () => {
218
+ const presentations = ['standard', 'gallery', 'rich'] as const
219
+
220
+ it('renders the wishlist button only when a toggle handler is wired', () => {
221
+ for (const presentation of presentations) {
222
+ const wired = render({
223
+ product,
224
+ presentation,
225
+ selectedVariantId: 'v-blue',
226
+ formatPrice,
227
+ onToggleWishlist: () => {},
228
+ })
229
+ const unwired = render({ product, presentation, selectedVariantId: 'v-blue', formatPrice })
230
+
231
+ expect(wired).toContain('Agregar a favoritos')
232
+ expect(unwired).not.toContain('Agregar a favoritos')
233
+ expect(unwired).not.toContain('Quitar de favoritos')
234
+ }
235
+ })
236
+
237
+ it('reflects the controlled wishlist state in the button label', () => {
238
+ const html = render({
239
+ product,
240
+ selectedVariantId: 'v-blue',
241
+ formatPrice,
242
+ isWishlisted: true,
243
+ onToggleWishlist: () => {},
244
+ })
245
+ expect(html).toContain('Quitar de favoritos')
246
+ expect(html).not.toContain('Agregar a favoritos')
247
+ })
248
+ })
249
+
250
+ describe('resolveAddToCartPayload', () => {
251
+ const variant = product.variants[1]
252
+
253
+ it('returns null when no variant is selected (no first-variant shortcut)', () => {
254
+ expect(resolveAddToCartPayload(null, 2)).toBeNull()
255
+ })
256
+
257
+ it('uses the selected backend variant id and defaults the quantity to 1', () => {
258
+ expect(resolveAddToCartPayload(variant, undefined)).toEqual({ variantId: 'v-blue', quantity: 1 })
259
+ })
260
+
261
+ it('carries the requested quantity', () => {
262
+ expect(resolveAddToCartPayload(variant, 3)).toEqual({ variantId: 'v-blue', quantity: 3 })
263
+ })
264
+ })
265
+
266
+ describe('variant helpers', () => {
267
+ it('prefers the variant title', () => {
268
+ expect(variantLabelOf(product.variants[0])).toBe('Rojo')
269
+ })
270
+
271
+ it('falls back to option values when a variant has no title', () => {
272
+ expect(variantLabelOf({ ...product.variants[0], title: '' })).toBe('Rojo')
273
+ })
274
+
275
+ it('uses the product images when present and never a fabricated URL', () => {
276
+ const withImages: ProductDetailProduct = {
277
+ ...product,
278
+ images: [{ url: 'https://cdn.example/p1.jpg', altText: 'p1' }],
279
+ }
280
+ expect(galleryImages(withImages, 'FALLBACK')).toEqual(['https://cdn.example/p1.jpg'])
281
+ expect(galleryImages({ ...product, images: [] }, 'FALLBACK')).toEqual(['FALLBACK'])
282
+ })
283
+ })