@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.
- package/CHANGELOG.md +155 -0
- package/README.md +228 -68
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
- 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 +447 -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-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} +119 -85
- 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/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +156 -128
- package/src/lib/contracts-curated.test.ts +47 -29
- package/src/lib/contracts.test.ts +198 -2
- package/src/lib/contracts.ts +410 -16
- 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 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- 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.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- 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 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- 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
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -0,0 +1,263 @@
|
|
|
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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { ProductCard, type ProductCardProps } from './product-card'
|
|
8
|
+
import type { ProductPresentation } from '../../../lib/contracts'
|
|
9
|
+
|
|
10
|
+
;(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true
|
|
11
|
+
|
|
12
|
+
// jsdom lacks the browser APIs the classic presentation's motion layer touches.
|
|
13
|
+
if (!window.matchMedia) {
|
|
14
|
+
window.matchMedia = ((query: string) => ({
|
|
15
|
+
matches: false,
|
|
16
|
+
media: query,
|
|
17
|
+
onchange: null,
|
|
18
|
+
addEventListener: () => {},
|
|
19
|
+
removeEventListener: () => {},
|
|
20
|
+
addListener: () => {},
|
|
21
|
+
removeListener: () => {},
|
|
22
|
+
dispatchEvent: () => false,
|
|
23
|
+
})) as unknown as typeof window.matchMedia
|
|
24
|
+
}
|
|
25
|
+
if (!('IntersectionObserver' in window)) {
|
|
26
|
+
;(window as Record<string, unknown>).IntersectionObserver = class {
|
|
27
|
+
observe() {}
|
|
28
|
+
unobserve() {}
|
|
29
|
+
disconnect() {}
|
|
30
|
+
takeRecords() {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseItem: ProductPresentation = {
|
|
37
|
+
id: 'p1',
|
|
38
|
+
title: 'Llave de impacto',
|
|
39
|
+
image: '/demo/impact.svg',
|
|
40
|
+
imageAlt: 'Llave de impacto inalámbrica',
|
|
41
|
+
price: { amount: '129.50', currencyCode: 'EUR' },
|
|
42
|
+
compareAtPrice: { amount: '159.00', currencyCode: 'EUR' },
|
|
43
|
+
rating: 4.5,
|
|
44
|
+
badge: 'Nuevo',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const renderMarkup = (props: ProductCardProps) => renderToStaticMarkup(<ProductCard {...props} />)
|
|
48
|
+
|
|
49
|
+
describe('ProductCard — renders from canonical data', () => {
|
|
50
|
+
it('renders one item cell from canonical Money, requiring no demo-only fields', () => {
|
|
51
|
+
const html = renderMarkup({ item: baseItem })
|
|
52
|
+
expect(html).toContain('Llave de impacto')
|
|
53
|
+
expect(html).toContain('129')
|
|
54
|
+
expect(html).not.toContain('$')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders exactly one cell, with no section wrapper or heading', () => {
|
|
58
|
+
const html = renderMarkup({ item: baseItem })
|
|
59
|
+
expect(html.match(/<h3/g)?.length).toBe(1)
|
|
60
|
+
expect(html).not.toContain('<section')
|
|
61
|
+
expect(html).not.toContain('<h2')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('ProductCard — optional fields are omitted, not emptied', () => {
|
|
66
|
+
it('renders without empty src, stars, or chip when optionals are absent', () => {
|
|
67
|
+
const html = renderMarkup({
|
|
68
|
+
item: { ...baseItem, image: undefined, imageAlt: undefined, rating: undefined, badge: undefined },
|
|
69
|
+
})
|
|
70
|
+
expect(html).not.toContain('src=""')
|
|
71
|
+
expect(html).not.toContain('4.5')
|
|
72
|
+
expect(html).not.toContain('Nuevo')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('falls back to the title as the image alt text', () => {
|
|
76
|
+
const html = renderMarkup({ item: { ...baseItem, imageAlt: undefined } })
|
|
77
|
+
expect(html).toContain('alt="Llave de impacto"')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('ProductCard — missing price is unavailable, never zero', () => {
|
|
82
|
+
it('shows the unavailable label, disables add-to-cart, and never renders a zero', () => {
|
|
83
|
+
const html = renderMarkup({
|
|
84
|
+
item: { ...baseItem, price: { amount: '', currencyCode: '' }, compareAtPrice: undefined },
|
|
85
|
+
})
|
|
86
|
+
expect(html).toContain('Precio no disponible')
|
|
87
|
+
expect(html).not.toContain('$0')
|
|
88
|
+
expect(html).toContain('disabled')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('treats a valid zero as a real price', () => {
|
|
92
|
+
const html = renderMarkup({
|
|
93
|
+
item: { ...baseItem, price: { amount: '0.00', currencyCode: 'USD' }, compareAtPrice: undefined },
|
|
94
|
+
})
|
|
95
|
+
expect(html).toContain('$0.00')
|
|
96
|
+
expect(html).not.toContain('Precio no disponible')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('ProductCard — no evidence is invented', () => {
|
|
101
|
+
it('shows no stars, chip, scarcity copy, or crossed-out price when none is supplied', () => {
|
|
102
|
+
const html = renderMarkup({
|
|
103
|
+
item: { id: 'p1', title: 'Producto 1', price: { amount: '10.00', currencyCode: 'EUR' } },
|
|
104
|
+
})
|
|
105
|
+
expect(html).not.toContain('line-through')
|
|
106
|
+
expect(html).not.toContain('Nuevo')
|
|
107
|
+
expect(html).not.toContain('¡Solo')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('ProductCard — compare-at only when valid and higher', () => {
|
|
112
|
+
it('renders a comparison price only when strictly higher', () => {
|
|
113
|
+
expect(renderMarkup({ item: baseItem })).toContain('159')
|
|
114
|
+
const equal = renderMarkup({
|
|
115
|
+
item: { ...baseItem, compareAtPrice: { amount: '129.50', currencyCode: 'EUR' } },
|
|
116
|
+
})
|
|
117
|
+
const lower = renderMarkup({
|
|
118
|
+
item: { ...baseItem, compareAtPrice: { amount: '99.00', currencyCode: 'EUR' } },
|
|
119
|
+
})
|
|
120
|
+
const invalid = renderMarkup({
|
|
121
|
+
item: { ...baseItem, compareAtPrice: { amount: '', currencyCode: '' } },
|
|
122
|
+
})
|
|
123
|
+
expect(equal).not.toContain('159')
|
|
124
|
+
expect(lower).not.toContain('99')
|
|
125
|
+
expect(invalid).not.toContain('line-through')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('ProductCard — bounded presentation surface', () => {
|
|
130
|
+
it('changes structure across presentations while keeping the same contract', () => {
|
|
131
|
+
const classic = renderMarkup({ item: baseItem })
|
|
132
|
+
const minimal = renderMarkup({ item: baseItem, presentation: 'minimal' })
|
|
133
|
+
const rich = renderMarkup({ item: baseItem, presentation: 'rich' })
|
|
134
|
+
for (const html of [classic, minimal, rich]) {
|
|
135
|
+
expect(html).toContain('129')
|
|
136
|
+
expect(html).toContain('Llave de impacto')
|
|
137
|
+
expect(html).toContain('Agregar al carrito')
|
|
138
|
+
}
|
|
139
|
+
expect(classic).toContain('>Agregar al carrito<')
|
|
140
|
+
expect(minimal).not.toContain('>Agregar al carrito<')
|
|
141
|
+
expect(minimal).toContain('aria-label="Agregar al carrito"')
|
|
142
|
+
expect(rich).toContain('aria-pressed')
|
|
143
|
+
expect(rich).toContain('>Agregar al carrito<')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('ProductCard — rich presentation degrades honestly', () => {
|
|
148
|
+
const heartTag = (html: string) => html.match(/<button[^>]*aria-pressed[^>]*>/)?.[0]
|
|
149
|
+
const isDisabled = (tag: string | undefined) => /\sdisabled(?:="")?(?=[\s>])/.test(tag ?? '')
|
|
150
|
+
|
|
151
|
+
it('reflects the wishlisted prop and disables the heart without wiring', () => {
|
|
152
|
+
const unwired = renderMarkup({ item: baseItem, presentation: 'rich' })
|
|
153
|
+
expect(unwired).toContain('aria-pressed="false"')
|
|
154
|
+
expect(isDisabled(heartTag(unwired))).toBe(true)
|
|
155
|
+
expect(unwired).not.toContain('Ver similares')
|
|
156
|
+
expect(renderMarkup({ item: baseItem, presentation: 'rich', wishlisted: true })).toContain(
|
|
157
|
+
'aria-pressed="true"',
|
|
158
|
+
)
|
|
159
|
+
const wired = renderMarkup({ item: baseItem, presentation: 'rich', onToggleWishlist: () => {} })
|
|
160
|
+
expect(isDisabled(heartTag(wired))).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('ProductCard — labels are overridable', () => {
|
|
165
|
+
it('uses supplied labels for actions and availability copy', () => {
|
|
166
|
+
const html = renderMarkup({
|
|
167
|
+
item: { ...baseItem, price: { amount: '', currencyCode: '' } },
|
|
168
|
+
addToCartLabel: 'Comprar ahora',
|
|
169
|
+
unavailablePriceLabel: 'Consultar precio',
|
|
170
|
+
addToWishlistLabel: 'Guardar',
|
|
171
|
+
viewSimilarLabel: 'Ver parecidos',
|
|
172
|
+
presentation: 'rich',
|
|
173
|
+
onViewSimilar: () => {},
|
|
174
|
+
})
|
|
175
|
+
expect(html).toContain('Comprar ahora')
|
|
176
|
+
expect(html).toContain('Consultar precio')
|
|
177
|
+
expect(html).toContain('Guardar')
|
|
178
|
+
expect(html).toContain('Ver parecidos')
|
|
179
|
+
expect(html).not.toContain('Agregar al carrito')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('ProductCard — actions are reported, not owned', () => {
|
|
184
|
+
let container: HTMLDivElement
|
|
185
|
+
let root: Root
|
|
186
|
+
let mounted: Root | undefined
|
|
187
|
+
|
|
188
|
+
const mount = (props: ProductCardProps) => {
|
|
189
|
+
root = createRoot(container)
|
|
190
|
+
mounted = root
|
|
191
|
+
act(() => root.render(<ProductCard {...props} />))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const click = (element: Element | undefined | null) => {
|
|
195
|
+
expect(element).toBeDefined()
|
|
196
|
+
act(() => {
|
|
197
|
+
element!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
container = document.createElement('div')
|
|
203
|
+
document.body.appendChild(container)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
afterEach(() => {
|
|
207
|
+
const root = mounted
|
|
208
|
+
if (root) act(() => root.unmount())
|
|
209
|
+
container.remove()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('reports add-to-cart and product clicks with the item identity', () => {
|
|
213
|
+
const onAddToCart = vi.fn()
|
|
214
|
+
const onProductClick = vi.fn()
|
|
215
|
+
mount({ item: baseItem, onAddToCart, onProductClick })
|
|
216
|
+
|
|
217
|
+
const addToCart = Array.from(container.querySelectorAll('button')).find((b) =>
|
|
218
|
+
b.textContent?.includes('Agregar al carrito'),
|
|
219
|
+
)
|
|
220
|
+
click(addToCart)
|
|
221
|
+
expect(onAddToCart).toHaveBeenCalledWith(baseItem)
|
|
222
|
+
|
|
223
|
+
click(container.querySelector('h3 button'))
|
|
224
|
+
expect(onProductClick).toHaveBeenCalledWith(baseItem)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('reports the wishlist toggle with the next state and reflects the prop', () => {
|
|
228
|
+
const onToggleWishlist = vi.fn()
|
|
229
|
+
mount({ item: baseItem, presentation: 'rich', wishlisted: false, onToggleWishlist })
|
|
230
|
+
|
|
231
|
+
const heart = () => container.querySelector('button[aria-pressed]')
|
|
232
|
+
expect(heart()!.getAttribute('aria-pressed')).toBe('false')
|
|
233
|
+
click(heart())
|
|
234
|
+
expect(onToggleWishlist).toHaveBeenCalledWith(baseItem, true)
|
|
235
|
+
expect(heart()!.getAttribute('aria-pressed')).toBe('false')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('keeps an unwired heart disabled and inert', () => {
|
|
239
|
+
mount({ item: baseItem, presentation: 'rich', wishlisted: false })
|
|
240
|
+
const heart = () => container.querySelector('button[aria-pressed]') as HTMLButtonElement
|
|
241
|
+
expect(heart().disabled).toBe(true)
|
|
242
|
+
click(heart())
|
|
243
|
+
expect(heart().getAttribute('aria-pressed')).toBe('false')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('reflects host-owned wishlist membership from the prop', () => {
|
|
247
|
+
const onToggleWishlist = vi.fn()
|
|
248
|
+
mount({ item: baseItem, presentation: 'rich', wishlisted: true, onToggleWishlist })
|
|
249
|
+
|
|
250
|
+
const heart = () => container.querySelector('button[aria-pressed]')
|
|
251
|
+
expect(heart()!.getAttribute('aria-pressed')).toBe('true')
|
|
252
|
+
click(heart())
|
|
253
|
+
expect(onToggleWishlist).toHaveBeenCalledWith(baseItem, false)
|
|
254
|
+
expect(heart()!.getAttribute('aria-pressed')).toBe('true')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('reports view-similar through its callback and omits the affordance without it', () => {
|
|
258
|
+
const onViewSimilar = vi.fn()
|
|
259
|
+
mount({ item: baseItem, presentation: 'rich', onViewSimilar })
|
|
260
|
+
click(container.querySelector('button[aria-label="Ver similares"]'))
|
|
261
|
+
expect(onViewSimilar).toHaveBeenCalledWith(baseItem)
|
|
262
|
+
})
|
|
263
|
+
})
|