@thorprovider/blocks 1.0.0 → 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 +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- 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/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- 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} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- 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 +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -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/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-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
|
@@ -26,6 +26,12 @@ export interface ProductCardProps {
|
|
|
26
26
|
/** Host-resolved wishlist membership; reflects the rich presentation heart. */
|
|
27
27
|
wishlisted?: boolean
|
|
28
28
|
onAddToCart?: (item: ProductPresentation) => void
|
|
29
|
+
/**
|
|
30
|
+
* Hides the add-to-cart control for listings whose only action is opening the
|
|
31
|
+
* product. Without this a host with no cart callback would render an inert
|
|
32
|
+
* button.
|
|
33
|
+
*/
|
|
34
|
+
hideAddToCart?: boolean
|
|
29
35
|
onProductClick?: (item: ProductPresentation) => void
|
|
30
36
|
/**
|
|
31
37
|
* Host-owned wishlist toggle; reports the item and the next state from the
|
|
@@ -99,6 +105,7 @@ function RatingRow({ value }: { value: number }) {
|
|
|
99
105
|
|
|
100
106
|
function ClassicPresentation({
|
|
101
107
|
item,
|
|
108
|
+
hideAddToCart = false,
|
|
102
109
|
onAddToCart,
|
|
103
110
|
onProductClick,
|
|
104
111
|
addToCartLabel,
|
|
@@ -138,7 +145,9 @@ function ClassicPresentation({
|
|
|
138
145
|
width={400}
|
|
139
146
|
height={400}
|
|
140
147
|
/>
|
|
141
|
-
) :
|
|
148
|
+
) : (
|
|
149
|
+
<div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full rounded-md' />
|
|
150
|
+
)}
|
|
142
151
|
</div>
|
|
143
152
|
|
|
144
153
|
<div className='flex flex-1 flex-col'>
|
|
@@ -164,18 +173,20 @@ function ClassicPresentation({
|
|
|
164
173
|
</CardContent>
|
|
165
174
|
|
|
166
175
|
<CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
e
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
{hideAddToCart ? null : (
|
|
177
|
+
<Button
|
|
178
|
+
variant='outline'
|
|
179
|
+
size='sm'
|
|
180
|
+
className='h-8 w-full cursor-pointer px-3 text-sm text-xs'
|
|
181
|
+
disabled={!available}
|
|
182
|
+
onClick={(e) => {
|
|
183
|
+
e.stopPropagation()
|
|
184
|
+
onAddToCart?.(item)
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{addToCartLabel}
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
179
190
|
</CardFooter>
|
|
180
191
|
</Card>
|
|
181
192
|
</motion.div>
|
|
@@ -184,6 +195,7 @@ function ClassicPresentation({
|
|
|
184
195
|
|
|
185
196
|
function MinimalPresentation({
|
|
186
197
|
item,
|
|
198
|
+
hideAddToCart = false,
|
|
187
199
|
onAddToCart,
|
|
188
200
|
onProductClick,
|
|
189
201
|
addToCartLabel,
|
|
@@ -216,7 +228,9 @@ function MinimalPresentation({
|
|
|
216
228
|
className='size-full object-cover transition-transform duration-300 group-hover:scale-105'
|
|
217
229
|
loading='lazy'
|
|
218
230
|
/>
|
|
219
|
-
) :
|
|
231
|
+
) : (
|
|
232
|
+
<div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />
|
|
233
|
+
)}
|
|
220
234
|
</div>
|
|
221
235
|
|
|
222
236
|
<div className='flex flex-col gap-2'>
|
|
@@ -224,19 +238,21 @@ function MinimalPresentation({
|
|
|
224
238
|
{rating !== undefined && <RatingRow value={rating} />}
|
|
225
239
|
<div className='flex items-center justify-between gap-2'>
|
|
226
240
|
<PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
e
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
{hideAddToCart ? null : (
|
|
242
|
+
<Button
|
|
243
|
+
variant='ghost'
|
|
244
|
+
size='icon'
|
|
245
|
+
className='size-8 shrink-0 cursor-pointer'
|
|
246
|
+
aria-label={addToCartLabel}
|
|
247
|
+
disabled={!available}
|
|
248
|
+
onClick={(e) => {
|
|
249
|
+
e.stopPropagation()
|
|
250
|
+
onAddToCart?.(item)
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<ShoppingCart className='size-4' aria-hidden='true' />
|
|
254
|
+
</Button>
|
|
255
|
+
)}
|
|
240
256
|
</div>
|
|
241
257
|
</div>
|
|
242
258
|
</CardContent>
|
|
@@ -283,6 +299,7 @@ function WishlistHeart({
|
|
|
283
299
|
|
|
284
300
|
function RichPresentation({
|
|
285
301
|
item,
|
|
302
|
+
hideAddToCart = false,
|
|
286
303
|
wishlisted = false,
|
|
287
304
|
onAddToCart,
|
|
288
305
|
onProductClick,
|
|
@@ -314,7 +331,9 @@ function RichPresentation({
|
|
|
314
331
|
className='size-full object-cover'
|
|
315
332
|
loading='lazy'
|
|
316
333
|
/>
|
|
317
|
-
) :
|
|
334
|
+
) : (
|
|
335
|
+
<div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />
|
|
336
|
+
)}
|
|
318
337
|
|
|
319
338
|
{item.badge && (
|
|
320
339
|
<Badge className='absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs'>
|
|
@@ -361,18 +380,20 @@ function RichPresentation({
|
|
|
361
380
|
<div className='flex flex-col gap-2 p-4'>
|
|
362
381
|
<h3 className='font-medium text-balance'>{item.title}</h3>
|
|
363
382
|
<PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
e
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
{hideAddToCart ? null : (
|
|
384
|
+
<Button
|
|
385
|
+
variant='outline'
|
|
386
|
+
size='sm'
|
|
387
|
+
className='mt-2 w-full cursor-pointer text-xs'
|
|
388
|
+
disabled={!available}
|
|
389
|
+
onClick={(e) => {
|
|
390
|
+
e.stopPropagation()
|
|
391
|
+
onAddToCart?.(item)
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
{addToCartLabel}
|
|
395
|
+
</Button>
|
|
396
|
+
)}
|
|
376
397
|
</div>
|
|
377
398
|
</CardContent>
|
|
378
399
|
</Card>
|
|
@@ -391,6 +412,7 @@ export function ProductCard({
|
|
|
391
412
|
item,
|
|
392
413
|
presentation = 'classic',
|
|
393
414
|
wishlisted,
|
|
415
|
+
hideAddToCart,
|
|
394
416
|
onAddToCart,
|
|
395
417
|
onProductClick,
|
|
396
418
|
onToggleWishlist,
|
|
@@ -405,6 +427,7 @@ export function ProductCard({
|
|
|
405
427
|
item,
|
|
406
428
|
presentation,
|
|
407
429
|
wishlisted,
|
|
430
|
+
hideAddToCart,
|
|
408
431
|
onAddToCart,
|
|
409
432
|
onProductClick,
|
|
410
433
|
onToggleWishlist,
|
|
@@ -191,8 +191,8 @@ describe('ShoppingCart — truthful evidence only', () => {
|
|
|
191
191
|
promotions: [],
|
|
192
192
|
totals: { subtotal: eur('110.00'), discount: eur('10.00'), tax: eur('8.00'), shipping: eur('0'), total: eur('108.00') },
|
|
193
193
|
})
|
|
194
|
-
expect(html).toMatch(/EUR\
|
|
195
|
-
expect(html).toMatch(/EUR\
|
|
194
|
+
expect(html).toMatch(/(?:EUR|€)\s?110\.00/)
|
|
195
|
+
expect(html).toMatch(/(?:EUR|€)\s?108\.00/)
|
|
196
196
|
})
|
|
197
197
|
|
|
198
198
|
it('renders host-supplied promotion progress and activated copy from data', () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import { AtSign } from 'lucide-react'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FooterSection,
|
|
9
|
+
type FooterSectionProps,
|
|
10
|
+
} from './footer-section'
|
|
11
|
+
|
|
12
|
+
const presentations = [
|
|
13
|
+
'brand-columns',
|
|
14
|
+
'newsletter-grid',
|
|
15
|
+
'contact',
|
|
16
|
+
] as const satisfies readonly FooterSectionProps['presentation'][]
|
|
17
|
+
|
|
18
|
+
function createProps(
|
|
19
|
+
presentation: FooterSectionProps['presentation'] = 'contact',
|
|
20
|
+
): FooterSectionProps {
|
|
21
|
+
return {
|
|
22
|
+
presentation,
|
|
23
|
+
brandName: 'Thor Provider',
|
|
24
|
+
linkColumns: [
|
|
25
|
+
{
|
|
26
|
+
title: 'Repuestos',
|
|
27
|
+
links: [{ label: 'Catálogo', href: '/repuestos' }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
newsletter: {
|
|
31
|
+
title: 'Novedades para talleres',
|
|
32
|
+
buttonLabel: 'Suscribirme',
|
|
33
|
+
},
|
|
34
|
+
contact: {
|
|
35
|
+
heading: 'Contacto',
|
|
36
|
+
address: { label: 'Av. Talleres 123' },
|
|
37
|
+
phone: { label: '+34 900 123 456', href: 'tel:+34900123456' },
|
|
38
|
+
email: { label: 'ventas@thorprovider.es', href: 'mailto:ventas@thorprovider.es' },
|
|
39
|
+
},
|
|
40
|
+
socialLinks: [
|
|
41
|
+
{ label: 'Instagram', href: '/redes/instagram', icon: 'instagram' },
|
|
42
|
+
],
|
|
43
|
+
socialIcons: { instagram: AtSign },
|
|
44
|
+
onNewsletterSubmit: vi.fn(),
|
|
45
|
+
onLinkClick: vi.fn(),
|
|
46
|
+
onSocialClick: vi.fn(),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('FooterSection accessibility', () => {
|
|
51
|
+
it('uses consumer-supplied localized copy as the newsletter field label', () => {
|
|
52
|
+
render(<FooterSection {...createProps()} />)
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole('textbox', { name: 'Novedades para talleres' }),
|
|
56
|
+
).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('exposes named footer and navigation landmarks', () => {
|
|
60
|
+
render(<FooterSection {...createProps()} />)
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByRole('contentinfo', { name: 'Thor Provider footer' }),
|
|
64
|
+
).toBeDefined()
|
|
65
|
+
expect(screen.getByRole('navigation', { name: 'Repuestos' })).toBeDefined()
|
|
66
|
+
expect(screen.getByRole('navigation', { name: 'Social media' })).toBeDefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('keeps decorative newsletter and social icons hidden from assistive technology', () => {
|
|
70
|
+
render(<FooterSection {...createProps()} />)
|
|
71
|
+
|
|
72
|
+
const newsletterIcon = screen
|
|
73
|
+
.getByRole('button', { name: 'Suscribirme' })
|
|
74
|
+
.querySelector('svg')
|
|
75
|
+
const socialIcon = screen
|
|
76
|
+
.getByRole('link', { name: 'Instagram' })
|
|
77
|
+
.querySelector('svg')
|
|
78
|
+
|
|
79
|
+
expect(newsletterIcon?.getAttribute('aria-hidden')).toBe('true')
|
|
80
|
+
expect(socialIcon?.getAttribute('aria-hidden')).toBe('true')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('keeps every actionable item keyboard reachable in page order', async () => {
|
|
84
|
+
const user = userEvent.setup()
|
|
85
|
+
const props = createProps()
|
|
86
|
+
render(<FooterSection {...props} />)
|
|
87
|
+
|
|
88
|
+
const phone = screen.getByRole('link', { name: '+34 900 123 456' })
|
|
89
|
+
const email = screen.getByRole('link', { name: 'ventas@thorprovider.es' })
|
|
90
|
+
const newsletter = screen.getByRole('textbox', {
|
|
91
|
+
name: 'Novedades para talleres',
|
|
92
|
+
})
|
|
93
|
+
const submit = screen.getByRole('button', { name: 'Suscribirme' })
|
|
94
|
+
const columnLink = screen.getByRole('link', { name: 'Catálogo' })
|
|
95
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
96
|
+
|
|
97
|
+
for (const control of [phone, email, newsletter, submit, columnLink, socialLink]) {
|
|
98
|
+
await user.tab()
|
|
99
|
+
expect(document.activeElement).toBe(control)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
expect(columnLink.getAttribute('class')).toContain('focus-visible')
|
|
103
|
+
expect(socialLink.getAttribute('class')).toContain('focus-visible')
|
|
104
|
+
|
|
105
|
+
await user.keyboard('{Enter}')
|
|
106
|
+
expect(props.onSocialClick).toHaveBeenCalledWith('Instagram')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it.each(presentations)(
|
|
110
|
+
'keeps the %s layout constrained responsively',
|
|
111
|
+
(presentation) => {
|
|
112
|
+
render(<FooterSection {...createProps(presentation)} />)
|
|
113
|
+
|
|
114
|
+
const footer = screen.getByRole('contentinfo')
|
|
115
|
+
expect(footer.querySelector('[class*="md:grid-cols"]')).not.toBeNull()
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import { AtSign } from 'lucide-react'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { FooterSection, type FooterSectionProps } from './footer-section'
|
|
8
|
+
|
|
9
|
+
const sharedProps = {
|
|
10
|
+
brandName: 'Thor Provider',
|
|
11
|
+
brandDescription: 'Repuestos y soluciones para talleres.',
|
|
12
|
+
linkColumns: [
|
|
13
|
+
{
|
|
14
|
+
title: 'Repuestos',
|
|
15
|
+
links: [{ label: 'Catálogo de repuestos', href: '/repuestos' }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
newsletter: {
|
|
19
|
+
title: 'Recibe novedades',
|
|
20
|
+
placeholder: 'Correo electrónico',
|
|
21
|
+
buttonLabel: 'Suscribirme',
|
|
22
|
+
},
|
|
23
|
+
socialLinks: [
|
|
24
|
+
{
|
|
25
|
+
label: 'Instagram',
|
|
26
|
+
href: 'https://instagram.com/thorprovider',
|
|
27
|
+
icon: 'instagram',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
socialIcons: { instagram: AtSign },
|
|
31
|
+
contact: {
|
|
32
|
+
heading: 'Contacto',
|
|
33
|
+
phone: { label: '+56 2 2345 6789', href: 'tel:+56223456789' },
|
|
34
|
+
email: {
|
|
35
|
+
label: 'hola@thorprovider.cl',
|
|
36
|
+
href: 'mailto:hola@thorprovider.cl',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} satisfies Omit<FooterSectionProps, 'presentation'>
|
|
40
|
+
|
|
41
|
+
describe('FooterSection host actions', () => {
|
|
42
|
+
it('reports the entered newsletter email through the host callback', async () => {
|
|
43
|
+
const user = userEvent.setup()
|
|
44
|
+
const onNewsletterSubmit = vi.fn()
|
|
45
|
+
|
|
46
|
+
render(
|
|
47
|
+
<FooterSection
|
|
48
|
+
{...sharedProps}
|
|
49
|
+
presentation="newsletter-grid"
|
|
50
|
+
onNewsletterSubmit={onNewsletterSubmit}
|
|
51
|
+
/>,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
await user.type(
|
|
55
|
+
screen.getByPlaceholderText('Correo electrónico'),
|
|
56
|
+
'taller@thorprovider.cl',
|
|
57
|
+
)
|
|
58
|
+
await user.click(screen.getByRole('button', { name: 'Suscribirme' }))
|
|
59
|
+
|
|
60
|
+
expect(onNewsletterSubmit).toHaveBeenCalledTimes(1)
|
|
61
|
+
expect(onNewsletterSubmit).toHaveBeenCalledWith('taller@thorprovider.cl')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('keeps newsletter controls visible but disabled without a callback', () => {
|
|
65
|
+
render(<FooterSection {...sharedProps} presentation="newsletter-grid" />)
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
(screen.getByPlaceholderText('Correo electrónico') as HTMLInputElement)
|
|
69
|
+
.disabled,
|
|
70
|
+
).toBe(true)
|
|
71
|
+
expect(
|
|
72
|
+
(screen.getByRole('button', { name: 'Suscribirme' }) as HTMLButtonElement)
|
|
73
|
+
.disabled,
|
|
74
|
+
).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders footer-column and social destinations as text without callbacks', () => {
|
|
78
|
+
render(<FooterSection {...sharedProps} presentation="contact" />)
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('Catálogo de repuestos').closest('a')).toBeNull()
|
|
81
|
+
expect(screen.getByText('Instagram').closest('a')).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('reports host-routed footer and social actions while retaining their hrefs', async () => {
|
|
85
|
+
const user = userEvent.setup()
|
|
86
|
+
const onLinkClick = vi.fn()
|
|
87
|
+
const onSocialClick = vi.fn()
|
|
88
|
+
|
|
89
|
+
render(
|
|
90
|
+
<FooterSection
|
|
91
|
+
{...sharedProps}
|
|
92
|
+
presentation="contact"
|
|
93
|
+
onLinkClick={onLinkClick}
|
|
94
|
+
onSocialClick={onSocialClick}
|
|
95
|
+
/>,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const footerLink = screen.getByRole('link', {
|
|
99
|
+
name: 'Catálogo de repuestos',
|
|
100
|
+
})
|
|
101
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
102
|
+
expect(footerLink.getAttribute('href')).toBe('/repuestos')
|
|
103
|
+
expect(socialLink.getAttribute('href')).toBe(
|
|
104
|
+
'https://instagram.com/thorprovider',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await user.click(footerLink)
|
|
108
|
+
await user.click(socialLink)
|
|
109
|
+
|
|
110
|
+
expect(onLinkClick).toHaveBeenCalledWith({
|
|
111
|
+
label: 'Catálogo de repuestos',
|
|
112
|
+
href: '/repuestos',
|
|
113
|
+
})
|
|
114
|
+
expect(onSocialClick).toHaveBeenCalledWith('Instagram')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('preserves native telephone and email contact destinations', () => {
|
|
118
|
+
render(<FooterSection {...sharedProps} presentation="contact" />)
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen
|
|
122
|
+
.getByRole('link', { name: '+56 2 2345 6789' })
|
|
123
|
+
.getAttribute('href'),
|
|
124
|
+
).toBe('tel:+56223456789')
|
|
125
|
+
expect(
|
|
126
|
+
screen
|
|
127
|
+
.getByRole('link', { name: 'hola@thorprovider.cl' })
|
|
128
|
+
.getAttribute('href'),
|
|
129
|
+
).toBe('mailto:hola@thorprovider.cl')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { AtSign } from 'lucide-react'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
FooterSection,
|
|
8
|
+
type FooterSectionProps,
|
|
9
|
+
type FooterSocialLink,
|
|
10
|
+
} from './footer-section'
|
|
11
|
+
|
|
12
|
+
const baseProps = {
|
|
13
|
+
presentation: 'contact',
|
|
14
|
+
brandName: 'Thor Provider',
|
|
15
|
+
linkColumns: [],
|
|
16
|
+
} satisfies FooterSectionProps
|
|
17
|
+
|
|
18
|
+
const instagram = {
|
|
19
|
+
label: 'Instagram',
|
|
20
|
+
href: '/redes/instagram',
|
|
21
|
+
icon: 'instagram',
|
|
22
|
+
} satisfies FooterSocialLink
|
|
23
|
+
|
|
24
|
+
describe('FooterSection serializable social content and list identities', () => {
|
|
25
|
+
it('round-trips social identifiers as plain JSON data', () => {
|
|
26
|
+
const serialized = JSON.stringify([instagram])
|
|
27
|
+
|
|
28
|
+
expect(serialized).toBe(
|
|
29
|
+
'[{"label":"Instagram","href":"/redes/instagram","icon":"instagram"}]',
|
|
30
|
+
)
|
|
31
|
+
expect(JSON.parse(serialized)).toEqual([instagram])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders a host-bound icon for its serializable identifier', () => {
|
|
35
|
+
render(
|
|
36
|
+
<FooterSection
|
|
37
|
+
{...baseProps}
|
|
38
|
+
socialLinks={[instagram]}
|
|
39
|
+
socialIcons={{ instagram: AtSign }}
|
|
40
|
+
onSocialClick={() => undefined}
|
|
41
|
+
/>,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
45
|
+
expect(socialLink.getAttribute('href')).toBe('/redes/instagram')
|
|
46
|
+
expect(socialLink.querySelector('svg')).not.toBeNull()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('uses an optional accessible name without changing the raw social label', () => {
|
|
50
|
+
const onSocialClick = vi.fn()
|
|
51
|
+
const socialLink = { ...instagram, ariaLabel: 'Seguir en Instagram' }
|
|
52
|
+
|
|
53
|
+
render(
|
|
54
|
+
<FooterSection
|
|
55
|
+
{...baseProps}
|
|
56
|
+
socialLinks={[socialLink]}
|
|
57
|
+
socialIcons={{ instagram: AtSign }}
|
|
58
|
+
onSocialClick={onSocialClick}
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const control = screen.getByRole('link', { name: 'Seguir en Instagram' })
|
|
63
|
+
expect(control.textContent).toContain('Instagram')
|
|
64
|
+
fireEvent.click(control)
|
|
65
|
+
expect(onSocialClick).toHaveBeenCalledWith('Instagram')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('keeps the provider label visible when its icon identifier is unmapped', () => {
|
|
69
|
+
render(
|
|
70
|
+
<FooterSection
|
|
71
|
+
{...baseProps}
|
|
72
|
+
socialLinks={[instagram]}
|
|
73
|
+
socialIcons={{}}
|
|
74
|
+
onSocialClick={() => undefined}
|
|
75
|
+
/>,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
79
|
+
expect(socialLink.textContent).toContain('Instagram')
|
|
80
|
+
expect(socialLink.querySelector('svg')).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('rejects duplicate column identities instead of emitting duplicate keys', () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
render(
|
|
86
|
+
<FooterSection
|
|
87
|
+
{...baseProps}
|
|
88
|
+
linkColumns={[
|
|
89
|
+
{ title: 'Servicios', links: [{ label: 'Talleres' }] },
|
|
90
|
+
{ title: 'Servicios', links: [{ label: 'Repuestos' }] },
|
|
91
|
+
]}
|
|
92
|
+
/>,
|
|
93
|
+
),
|
|
94
|
+
).toThrow(/column.*keys must be unique/i)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('rejects duplicate link identities within a column', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
render(
|
|
100
|
+
<FooterSection
|
|
101
|
+
{...baseProps}
|
|
102
|
+
linkColumns={[
|
|
103
|
+
{
|
|
104
|
+
title: 'Servicios',
|
|
105
|
+
links: [
|
|
106
|
+
{ label: 'Talleres', href: '/talleres' },
|
|
107
|
+
{ label: 'Talleres', href: '/talleres' },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
]}
|
|
111
|
+
/>,
|
|
112
|
+
),
|
|
113
|
+
).toThrow(/link.*keys must be unique/i)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('rejects duplicate social identities instead of emitting duplicate keys', () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
render(
|
|
119
|
+
<FooterSection
|
|
120
|
+
{...baseProps}
|
|
121
|
+
socialLinks={[instagram, instagram]}
|
|
122
|
+
onSocialClick={() => undefined}
|
|
123
|
+
/>,
|
|
124
|
+
),
|
|
125
|
+
).toThrow(/social.*keys must be unique/i)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { AtSign } from 'lucide-react'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { FooterSection, type FooterSectionProps } from './footer-section'
|
|
7
|
+
|
|
8
|
+
const nativeProps = {
|
|
9
|
+
presentation: 'brand-columns',
|
|
10
|
+
linkBehavior: 'native',
|
|
11
|
+
brandName: 'Thor Provider',
|
|
12
|
+
linkColumns: [
|
|
13
|
+
{
|
|
14
|
+
title: 'Repuestos',
|
|
15
|
+
links: [{ label: 'Catálogo de repuestos', href: '/repuestos' }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
socialLinks: [
|
|
19
|
+
{ label: 'Instagram', href: '/redes/instagram', icon: 'instagram' },
|
|
20
|
+
],
|
|
21
|
+
socialIcons: { instagram: AtSign },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('FooterSection native link compatibility', () => {
|
|
25
|
+
it('keeps native column hrefs separate from host-controlled social links', () => {
|
|
26
|
+
render(
|
|
27
|
+
<FooterSection
|
|
28
|
+
{...(nativeProps as unknown as FooterSectionProps)}
|
|
29
|
+
/>,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(
|
|
33
|
+
screen
|
|
34
|
+
.getByRole('link', { name: 'Catálogo de repuestos' })
|
|
35
|
+
.getAttribute('href'),
|
|
36
|
+
).toBe('/repuestos')
|
|
37
|
+
expect(screen.getByText('Instagram').closest('a')).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('notifies native column callbacks without preventing browser navigation', () => {
|
|
41
|
+
const onLinkClick = vi.fn()
|
|
42
|
+
const props = {
|
|
43
|
+
...nativeProps,
|
|
44
|
+
onLinkClick,
|
|
45
|
+
} as unknown as FooterSectionProps
|
|
46
|
+
|
|
47
|
+
render(<FooterSection {...props} />)
|
|
48
|
+
|
|
49
|
+
const columnLink = screen.getByRole('link', {
|
|
50
|
+
name: 'Catálogo de repuestos',
|
|
51
|
+
})
|
|
52
|
+
const columnEvent = new MouseEvent('click', {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
cancelable: true,
|
|
55
|
+
})
|
|
56
|
+
columnLink.dispatchEvent(columnEvent)
|
|
57
|
+
|
|
58
|
+
expect(onLinkClick).toHaveBeenCalledWith(
|
|
59
|
+
nativeProps.linkColumns[0]?.links[0],
|
|
60
|
+
)
|
|
61
|
+
expect(columnEvent.defaultPrevented).toBe(false)
|
|
62
|
+
expect(screen.getByText('Instagram').closest('a')).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('keeps native social hrefs separate from host-controlled column links', () => {
|
|
66
|
+
const props = {
|
|
67
|
+
...nativeProps,
|
|
68
|
+
linkBehavior: 'host',
|
|
69
|
+
socialLinkBehavior: 'native',
|
|
70
|
+
} as unknown as FooterSectionProps
|
|
71
|
+
|
|
72
|
+
render(<FooterSection {...props} />)
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText('Catálogo de repuestos').closest('a')).toBeNull()
|
|
75
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
76
|
+
expect(socialLink.getAttribute('href')).toBe('/redes/instagram')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('notifies native social callbacks without preventing browser navigation', () => {
|
|
80
|
+
const onSocialClick = vi.fn()
|
|
81
|
+
const props = {
|
|
82
|
+
...nativeProps,
|
|
83
|
+
linkBehavior: 'host',
|
|
84
|
+
socialLinkBehavior: 'native',
|
|
85
|
+
onSocialClick,
|
|
86
|
+
} as unknown as FooterSectionProps
|
|
87
|
+
|
|
88
|
+
render(<FooterSection {...props} />)
|
|
89
|
+
|
|
90
|
+
const socialLink = screen.getByRole('link', { name: 'Instagram' })
|
|
91
|
+
const socialEvent = new MouseEvent('click', {
|
|
92
|
+
bubbles: true,
|
|
93
|
+
cancelable: true,
|
|
94
|
+
})
|
|
95
|
+
socialLink.dispatchEvent(socialEvent)
|
|
96
|
+
|
|
97
|
+
expect(onSocialClick).toHaveBeenCalledWith('Instagram')
|
|
98
|
+
expect(socialEvent.defaultPrevented).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import * as blockExports from '../../../index'
|
|
3
|
+
|
|
4
|
+
describe('Blocks footer package exports', () => {
|
|
5
|
+
it('exposes the canonical footer and omits retired numbered adapters', () => {
|
|
6
|
+
expect(blockExports.FooterSection).toEqual(expect.any(Function))
|
|
7
|
+
expect('FooterSection1' in blockExports).toBe(false)
|
|
8
|
+
expect('FooterSection3' in blockExports).toBe(false)
|
|
9
|
+
expect('FooterSection4' in blockExports).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
})
|