@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
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from 'react'
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import type { ProductPresentation } from '../../../lib/contracts'
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
CategoryListing,
|
|
7
|
+
CategoryListingStatusView,
|
|
7
8
|
buildFilterChange,
|
|
8
9
|
reconcileSearchDraft,
|
|
9
10
|
toggleFilterValue,
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
} from './category-
|
|
13
|
-
|
|
14
|
-
const products:
|
|
15
|
-
{
|
|
16
|
-
|
|
11
|
+
type CategoryListingActiveFilter,
|
|
12
|
+
type CategoryListingProps,
|
|
13
|
+
} from './category-listing'
|
|
14
|
+
|
|
15
|
+
const products: ProductPresentation[] = [
|
|
16
|
+
{
|
|
17
|
+
id: 'pantalla-samsung-a54',
|
|
18
|
+
title: 'Pantalla Samsung Galaxy A54 OLED',
|
|
19
|
+
image: '/demo/pantalla.svg',
|
|
20
|
+
price: { amount: '49.99', currencyCode: 'USD' },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'bateria-iphone-13',
|
|
24
|
+
title: 'Batería iPhone 13',
|
|
25
|
+
price: { amount: '29.99', currencyCode: 'USD' },
|
|
26
|
+
},
|
|
17
27
|
]
|
|
18
28
|
|
|
19
29
|
const advertisedSorts = [
|
|
@@ -22,7 +32,7 @@ const advertisedSorts = [
|
|
|
22
32
|
{ value: 'newest', label: 'Más recientes' },
|
|
23
33
|
]
|
|
24
34
|
|
|
25
|
-
const activeChips:
|
|
35
|
+
const activeChips: CategoryListingActiveFilter[] = [
|
|
26
36
|
{ id: 'text', kind: 'text', label: 'pantalla', params: ['q'], value: 'pantalla' },
|
|
27
37
|
{ id: 'category:pantallas', kind: 'category', label: 'Pantallas', params: ['categories'], value: 'pantallas' },
|
|
28
38
|
{ id: 'price', kind: 'price', label: '10 – 40', params: ['priceMin', 'priceMax'] },
|
|
@@ -30,8 +40,29 @@ const activeChips: CategoryFilter2ActiveFilter[] = [
|
|
|
30
40
|
|
|
31
41
|
const countOccurrences = (html: string, needle: string) => html.split(needle).length - 1
|
|
32
42
|
|
|
43
|
+
function render(overrides: Partial<CategoryListingProps> = {}): string {
|
|
44
|
+
return renderToStaticMarkup(
|
|
45
|
+
<CategoryListing
|
|
46
|
+
products={products}
|
|
47
|
+
pagination={[]}
|
|
48
|
+
status={{ state: 'ready' }}
|
|
49
|
+
activeFilters={[]}
|
|
50
|
+
searchValue=''
|
|
51
|
+
selectedCategories={[]}
|
|
52
|
+
selectedCollections={[]}
|
|
53
|
+
selectedSort='featured'
|
|
54
|
+
selectedPage={1}
|
|
55
|
+
selectedPageSize={10}
|
|
56
|
+
showSearch={false}
|
|
57
|
+
showFilters
|
|
58
|
+
showSort
|
|
59
|
+
{...overrides}
|
|
60
|
+
/>,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/** Find every element whose prop `key` equals `value`, walking the returned
|
|
34
|
-
* element tree (no DOM needed — `
|
|
65
|
+
* element tree (no DOM needed — `CategoryListingStatusView` is hooks-free). */
|
|
35
66
|
function findAllByProp(
|
|
36
67
|
node: ReactNode,
|
|
37
68
|
key: string,
|
|
@@ -61,173 +92,232 @@ function findAllByProp(
|
|
|
61
92
|
return found
|
|
62
93
|
}
|
|
63
94
|
|
|
64
|
-
describe('
|
|
95
|
+
describe('CategoryListing — controlled canonical query', () => {
|
|
65
96
|
it('renders the query-selected search, category, sort and page values', () => {
|
|
66
|
-
const html =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
selectedPage={2}
|
|
80
|
-
/>,
|
|
81
|
-
)
|
|
97
|
+
const html = render({
|
|
98
|
+
categories: [
|
|
99
|
+
{ id: 'pantallas', label: 'Pantallas' },
|
|
100
|
+
{ id: 'baterias', label: 'Baterías' },
|
|
101
|
+
],
|
|
102
|
+
sortOptions: advertisedSorts,
|
|
103
|
+
pagination: [1, 2, 3],
|
|
104
|
+
showSearch: true,
|
|
105
|
+
searchValue: 'pantalla',
|
|
106
|
+
selectedCategories: ['pantallas'],
|
|
107
|
+
selectedSort: 'name',
|
|
108
|
+
selectedPage: 2,
|
|
109
|
+
})
|
|
82
110
|
|
|
83
111
|
expect(html).toContain('value="pantalla"')
|
|
84
|
-
// The sort control renders (its options are portaled client-side, so the
|
|
85
|
-
// advertised label set is asserted by the mapper test instead).
|
|
86
112
|
expect(html).toContain('aria-label="Ordenar por"')
|
|
87
113
|
expect(html).toContain('aria-checked="true"')
|
|
88
114
|
expect(html).toContain('aria-current="page"')
|
|
89
|
-
// The selected page is page 2; only one link is active.
|
|
90
115
|
expect(countOccurrences(html, 'aria-current="page"')).toBe(1)
|
|
91
116
|
})
|
|
92
117
|
|
|
93
118
|
it('hides the search control by default and shows it when requested', () => {
|
|
94
|
-
const hidden =
|
|
119
|
+
const hidden = render()
|
|
95
120
|
expect(hidden).not.toContain('type="search"')
|
|
96
121
|
|
|
97
|
-
const shown =
|
|
122
|
+
const shown = render({ showSearch: true })
|
|
98
123
|
expect(shown).toContain('type="search"')
|
|
99
124
|
expect(shown).toContain('aria-label="Buscar"')
|
|
100
125
|
})
|
|
101
126
|
|
|
102
127
|
it('renders collection controls when collections are supplied', () => {
|
|
103
|
-
const html =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
selectedCollections={['ofertas']}
|
|
108
|
-
/>,
|
|
109
|
-
)
|
|
128
|
+
const html = render({
|
|
129
|
+
collections: [{ id: 'ofertas', label: 'Ofertas' }],
|
|
130
|
+
selectedCollections: ['ofertas'],
|
|
131
|
+
})
|
|
110
132
|
expect(html).toContain('Colección')
|
|
111
133
|
expect(html).toContain('Ofertas')
|
|
112
134
|
expect(html).toContain('aria-checked="true"')
|
|
113
135
|
})
|
|
136
|
+
|
|
137
|
+
it('does not own private query state: the controlled values always win', () => {
|
|
138
|
+
const html = render({ selectedCategories: ['pantallas'], categories: [{ id: 'pantallas', label: 'Pantallas' }] })
|
|
139
|
+
expect(html).toContain('aria-checked="true"')
|
|
140
|
+
|
|
141
|
+
const cleared = render({ selectedCategories: [], categories: [{ id: 'pantallas', label: 'Pantallas' }] })
|
|
142
|
+
expect(cleared).not.toContain('aria-checked="true"')
|
|
143
|
+
})
|
|
114
144
|
})
|
|
115
145
|
|
|
116
|
-
describe('
|
|
146
|
+
describe('CategoryListing — no fabricated price or sort choices', () => {
|
|
117
147
|
it('omits the price control without a real backend range', () => {
|
|
118
|
-
|
|
119
|
-
expect(html).not.toContain('Rango de precio')
|
|
148
|
+
expect(render({ sortOptions: advertisedSorts })).not.toContain('Rango de precio')
|
|
120
149
|
})
|
|
121
150
|
|
|
122
151
|
it('shows the price control only from a real range', () => {
|
|
123
|
-
const html =
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
const html = render({
|
|
153
|
+
priceRange: { min: 0, max: 100, step: 5 },
|
|
154
|
+
selectedPriceRange: [10, 40],
|
|
155
|
+
})
|
|
126
156
|
expect(html).toContain('Rango de precio')
|
|
127
157
|
expect(html).toContain('value="10"')
|
|
128
158
|
expect(html).toContain('value="40"')
|
|
129
159
|
})
|
|
130
160
|
|
|
131
|
-
it('
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
it('hides the price control when showPrice is false even with a real range', () => {
|
|
162
|
+
const html = render({
|
|
163
|
+
showPrice: false,
|
|
164
|
+
priceRange: { min: 0, max: 100, step: 5 },
|
|
165
|
+
selectedPriceRange: [10, 40],
|
|
166
|
+
})
|
|
167
|
+
expect(html).not.toContain('Rango de precio')
|
|
137
168
|
})
|
|
138
169
|
|
|
139
|
-
it('renders
|
|
140
|
-
|
|
141
|
-
expect(
|
|
170
|
+
it('renders the sort control only when advertised options are supplied', () => {
|
|
171
|
+
expect(render({ sortOptions: advertisedSorts })).toContain('aria-label="Ordenar por"')
|
|
172
|
+
expect(render()).not.toContain('aria-label="Ordenar por"')
|
|
142
173
|
})
|
|
143
174
|
})
|
|
144
175
|
|
|
145
|
-
describe('
|
|
176
|
+
describe('CategoryListing — provider-paged products are not re-paginated', () => {
|
|
146
177
|
it('renders exactly the supplied page of products with backend page links', () => {
|
|
147
|
-
const html =
|
|
148
|
-
<CategoryFilter2 products={products} pagination={[1, 2, 3]} selectedPage={2} sortOptions={advertisedSorts} />,
|
|
149
|
-
)
|
|
178
|
+
const html = render({ pagination: [1, 2, 3], selectedPage: 2, sortOptions: advertisedSorts })
|
|
150
179
|
expect(html).toContain('Pantalla Samsung Galaxy A54 OLED')
|
|
151
180
|
expect(html).toContain('Batería iPhone 13')
|
|
152
181
|
expect(countOccurrences(html, 'data-slot="pagination-link"')).toBe(3)
|
|
153
|
-
// The second product has no image; the block omits src rather than emitting "".
|
|
154
182
|
expect(html).not.toContain('src=""')
|
|
155
183
|
})
|
|
156
184
|
})
|
|
157
185
|
|
|
158
|
-
describe('
|
|
159
|
-
it('renders
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
]
|
|
163
|
-
const html = renderToStaticMarkup(<CategoryFilter2 products={linked} />)
|
|
164
|
-
expect(html).toMatch(/<a[^>]*href="\/productos\/pantalla-samsung-a54"/)
|
|
165
|
-
expect(html).toContain('Pantalla')
|
|
166
|
-
// The card is not rendered as an article when it is a link.
|
|
186
|
+
describe('CategoryListing — canonical ProductCard reuse', () => {
|
|
187
|
+
it('renders results through the canonical card, not private card markup', () => {
|
|
188
|
+
const html = render()
|
|
189
|
+
expect(countOccurrences(html, 'data-slot="card"')).toBe(products.length)
|
|
167
190
|
expect(html).not.toContain('<article')
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
it('keeps the onClick path (article, no anchor) when href is absent', () => {
|
|
171
|
-
const html = renderToStaticMarkup(
|
|
172
|
-
<CategoryFilter2 products={products} onProductClick={() => {}} />,
|
|
173
|
-
)
|
|
174
|
-
expect(html).not.toMatch(/<a[^>]*href="\/productos\//)
|
|
175
|
-
expect(html).toContain('<article')
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('links only the cards that carry an href', () => {
|
|
179
|
-
const mixed: FilterProduct[] = [
|
|
180
|
-
{ id: 'p1', name: 'Pantalla', price: '$49.99', href: '/productos/p1' },
|
|
181
|
-
{ id: 'p2', name: 'Batería', price: '$29.99' },
|
|
182
|
-
]
|
|
183
|
-
const html = renderToStaticMarkup(<CategoryFilter2 products={mixed} />)
|
|
184
|
-
expect(countOccurrences(html, '<a ')).toBe(1)
|
|
185
|
-
expect(html).toMatch(/<a[^>]*href="\/productos\/p1"/)
|
|
186
|
-
expect(html).toContain('Batería')
|
|
191
|
+
expect(html).not.toContain('data-slot="category-listing-product-card"')
|
|
187
192
|
})
|
|
188
193
|
|
|
189
194
|
it('renders a neutral placeholder and no <img> when the image is absent', () => {
|
|
190
|
-
const html =
|
|
195
|
+
const html = render()
|
|
191
196
|
expect(html).toContain('data-slot="product-image-placeholder"')
|
|
192
|
-
// The product without an image never emits an <img> without src.
|
|
193
197
|
expect(html).not.toContain('src=""')
|
|
194
|
-
// One product has an image (Pantalla), one does not (Batería): exactly one img.
|
|
195
198
|
expect(countOccurrences(html, '<img')).toBe(1)
|
|
196
199
|
expect(html).toMatch(/<img[^>]*src="\/demo\/pantalla.svg"/)
|
|
197
200
|
})
|
|
201
|
+
|
|
202
|
+
it('does not render an inert add-to-cart control when the listing owns no cart action', () => {
|
|
203
|
+
const html = render()
|
|
204
|
+
expect(html).not.toContain('Agregar al carrito')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('CategoryListing — bounded presentations', () => {
|
|
209
|
+
it('renders the same products and controls in both presentations', () => {
|
|
210
|
+
for (const presentation of ['sidebar', 'toolbar'] as const) {
|
|
211
|
+
const html = render({
|
|
212
|
+
presentation,
|
|
213
|
+
categories: [{ id: 'pantallas', label: 'Pantallas' }],
|
|
214
|
+
sortOptions: advertisedSorts,
|
|
215
|
+
searchValue: 'pantalla',
|
|
216
|
+
showSearch: true,
|
|
217
|
+
selectedPage: 2,
|
|
218
|
+
pagination: [1, 2],
|
|
219
|
+
})
|
|
220
|
+
expect(html).toContain('Pantalla Samsung Galaxy A54 OLED')
|
|
221
|
+
expect(html).toContain('Batería iPhone 13')
|
|
222
|
+
expect(html).toContain('value="pantalla"')
|
|
223
|
+
expect(html).toContain('aria-label="Ordenar por"')
|
|
224
|
+
expect(html).toContain('aria-current="page"')
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('keeps the persistent sidebar only in the sidebar presentation', () => {
|
|
229
|
+
expect(render({ presentation: 'sidebar' })).toContain('<aside')
|
|
230
|
+
expect(render({ presentation: 'toolbar' })).not.toContain('<aside')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('renders the toolbar filter trigger and sort sheet controls on mobile', () => {
|
|
234
|
+
const html = render({
|
|
235
|
+
presentation: 'toolbar',
|
|
236
|
+
sortOptions: advertisedSorts,
|
|
237
|
+
selectedCategories: ['pantallas'],
|
|
238
|
+
categories: [{ id: 'pantallas', label: 'Pantallas' }],
|
|
239
|
+
})
|
|
240
|
+
expect(html).toContain('Filtros')
|
|
241
|
+
expect(html).toContain('aria-label="Ordenar por"')
|
|
242
|
+
expect(html).toContain('aria-expanded="false"')
|
|
243
|
+
})
|
|
198
244
|
})
|
|
199
245
|
|
|
200
|
-
describe('
|
|
246
|
+
describe('CategoryListing — generic facets and honest counts', () => {
|
|
247
|
+
const facetGroup = {
|
|
248
|
+
id: 'connectivity',
|
|
249
|
+
label: 'Conectividad',
|
|
250
|
+
options: [
|
|
251
|
+
{ value: 'wifi', label: 'Wi-Fi', count: 12 },
|
|
252
|
+
{ value: 'bluetooth', label: 'Bluetooth', count: 0 },
|
|
253
|
+
{ value: 'wired', label: 'Cable', count: Number.NaN },
|
|
254
|
+
{ value: 'other', label: 'Otro', count: -3 },
|
|
255
|
+
],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
it('renders generic facet groups with their real counts', () => {
|
|
259
|
+
const html = render({
|
|
260
|
+
facets: [facetGroup],
|
|
261
|
+
selectedFacets: { connectivity: ['wifi'] },
|
|
262
|
+
})
|
|
263
|
+
expect(html).toContain('data-slot="facet-group"')
|
|
264
|
+
expect(html).toContain('Conectividad')
|
|
265
|
+
expect(html).toContain('Wi-Fi')
|
|
266
|
+
expect(html).toContain('aria-checked="true"')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('preserves a real zero count and omits invalid counts', () => {
|
|
270
|
+
const html = render({ facets: [facetGroup] })
|
|
271
|
+
const counts = html.split('data-slot="facet-count"').slice(1)
|
|
272
|
+
expect(counts).toHaveLength(2)
|
|
273
|
+
expect(counts[0]).toContain('12')
|
|
274
|
+
expect(counts[1]).toContain('0')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('renders no facet chrome without provider metadata', () => {
|
|
278
|
+
const html = render()
|
|
279
|
+
expect(html).not.toContain('data-slot="facet-group"')
|
|
280
|
+
expect(html).not.toContain('facet-count')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('omits a supplied group with no options', () => {
|
|
284
|
+
const html = render({ facets: [{ id: 'empty', label: 'Vacío', options: [] }] })
|
|
285
|
+
expect(html).not.toContain('data-facet-id="empty"')
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('CategoryListing — emitted changes (pure reducer)', () => {
|
|
201
290
|
it('adds and removes a selection without duplicates', () => {
|
|
202
291
|
expect(toggleFilterValue([], 'a', true)).toEqual(['a'])
|
|
203
292
|
expect(toggleFilterValue(['a'], 'b', true)).toEqual(['a', 'b'])
|
|
204
293
|
expect(toggleFilterValue(['a', 'b'], 'a', false)).toEqual(['b'])
|
|
205
294
|
})
|
|
206
295
|
|
|
207
|
-
it('emits the canonical filter change including
|
|
296
|
+
it('emits the absolute canonical filter change including facets and price', () => {
|
|
208
297
|
expect(
|
|
209
298
|
buildFilterChange({
|
|
210
299
|
categories: ['pantallas'],
|
|
211
300
|
collections: ['ofertas'],
|
|
212
301
|
priceRange: [10, 40],
|
|
213
|
-
|
|
214
|
-
color: null,
|
|
302
|
+
facets: { connectivity: ['wifi'] },
|
|
215
303
|
}),
|
|
216
304
|
).toEqual({
|
|
217
305
|
categories: ['pantallas'],
|
|
218
306
|
collections: ['ofertas'],
|
|
219
307
|
priceRange: [10, 40],
|
|
220
|
-
|
|
221
|
-
|
|
308
|
+
facets: { connectivity: ['wifi'] },
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('omits price and facets when the host supplied none', () => {
|
|
313
|
+
expect(buildFilterChange({ categories: [], collections: [] })).toEqual({
|
|
314
|
+
categories: [],
|
|
315
|
+
collections: [],
|
|
222
316
|
})
|
|
223
317
|
})
|
|
224
318
|
})
|
|
225
319
|
|
|
226
|
-
describe('
|
|
227
|
-
// Simulates the real client loop: the shopper types into the controlled input
|
|
228
|
-
// (mutating the draft immediately and emitting the full value), while the
|
|
229
|
-
// URL-backed `searchValue` prop arrives later from the server. The draft must
|
|
230
|
-
// keep every typed character and never be reverted by a stale response.
|
|
320
|
+
describe('CategoryListing — controlled search draft', () => {
|
|
231
321
|
function type(seq: string) {
|
|
232
322
|
let draft = ''
|
|
233
323
|
let prevSearchValue: string | undefined = ''
|
|
@@ -243,12 +333,8 @@ describe('CategoryFilter2 — controlled search draft (E2 task 8.2 corrective)',
|
|
|
243
333
|
const before = draft
|
|
244
334
|
draft += ch
|
|
245
335
|
emitted.push(draft)
|
|
246
|
-
// A fast server round-trip can deliver the response to the PREVIOUS
|
|
247
|
-
// keystroke after the shopper has already typed past it; each delivery
|
|
248
|
-
// must leave the draft intact.
|
|
249
336
|
deliver(before)
|
|
250
337
|
}
|
|
251
|
-
// The final server response reflects the whole committed query.
|
|
252
338
|
deliver(seq)
|
|
253
339
|
return { draft, emitted }
|
|
254
340
|
}
|
|
@@ -260,21 +346,21 @@ describe('CategoryFilter2 — controlled search draft (E2 task 8.2 corrective)',
|
|
|
260
346
|
})
|
|
261
347
|
|
|
262
348
|
it('ignores a stale (unchanged) response that would revert the draft', () => {
|
|
263
|
-
expect(
|
|
264
|
-
reconcileSearchDraft({ searchValue: 'al', prevSearchValue: 'al', draft: 'alp' }),
|
|
265
|
-
).toBeNull()
|
|
349
|
+
expect(reconcileSearchDraft({ searchValue: 'al', prevSearchValue: 'al', draft: 'alp' })).toBeNull()
|
|
266
350
|
})
|
|
267
351
|
|
|
268
352
|
it('records a stale older response without reverting the draft', () => {
|
|
269
|
-
expect(
|
|
270
|
-
|
|
271
|
-
|
|
353
|
+
expect(reconcileSearchDraft({ searchValue: 'a', prevSearchValue: 'al', draft: 'alp' })).toEqual({
|
|
354
|
+
draft: 'alp',
|
|
355
|
+
prevSearchValue: 'a',
|
|
356
|
+
})
|
|
272
357
|
})
|
|
273
358
|
|
|
274
359
|
it('adopts an external navigation value when no local edit is pending', () => {
|
|
275
|
-
expect(
|
|
276
|
-
|
|
277
|
-
|
|
360
|
+
expect(reconcileSearchDraft({ searchValue: 'beta', prevSearchValue: '', draft: '' })).toEqual({
|
|
361
|
+
draft: 'beta',
|
|
362
|
+
prevSearchValue: 'beta',
|
|
363
|
+
})
|
|
278
364
|
})
|
|
279
365
|
|
|
280
366
|
it('does not adopt while an uncommitted edit is pending', () => {
|
|
@@ -282,95 +368,76 @@ describe('CategoryFilter2 — controlled search draft (E2 task 8.2 corrective)',
|
|
|
282
368
|
reconcileSearchDraft({ searchValue: 'other', prevSearchValue: 'alp', draft: 'alpha' }),
|
|
283
369
|
).toEqual({ draft: 'alpha', prevSearchValue: 'other' })
|
|
284
370
|
})
|
|
285
|
-
|
|
286
|
-
it('is a no-op for an unchanged prop', () => {
|
|
287
|
-
expect(reconcileSearchDraft({ searchValue: 'q', prevSearchValue: 'q', draft: 'q' })).toBeNull()
|
|
288
|
-
})
|
|
289
371
|
})
|
|
290
372
|
|
|
291
|
-
describe('
|
|
373
|
+
describe('CategoryListing — observable listing states', () => {
|
|
292
374
|
it('shows skeleton placeholders while loading instead of products', () => {
|
|
293
|
-
const html =
|
|
375
|
+
const html = render({ status: { state: 'loading' } })
|
|
294
376
|
expect(html).toContain('data-slot="skeleton"')
|
|
295
377
|
expect(html).toContain('aria-busy="true"')
|
|
296
378
|
expect(html).not.toContain('Pantalla Samsung Galaxy A54 OLED')
|
|
297
379
|
})
|
|
298
380
|
|
|
299
381
|
it('renders a clearable empty state, distinct from an error', () => {
|
|
300
|
-
const html =
|
|
301
|
-
<CategoryFilter2 products={[]} status={{ state: 'empty' }} onClearFilters={() => {}} />,
|
|
302
|
-
)
|
|
382
|
+
const html = render({ products: [], status: { state: 'empty' }, onClearFilters: () => {} })
|
|
303
383
|
expect(html).toContain('Sin resultados')
|
|
304
384
|
expect(html).toContain('Limpiar filtros')
|
|
305
385
|
expect(html).not.toContain('Reintentar')
|
|
306
386
|
})
|
|
307
387
|
|
|
308
388
|
it('renders an invalid-query repair state naming the invalid fields', () => {
|
|
309
|
-
const html =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
/>,
|
|
315
|
-
)
|
|
389
|
+
const html = render({
|
|
390
|
+
products: [],
|
|
391
|
+
status: { state: 'invalid-query', error: { code: 'invalid-query', fields: ['page'] } },
|
|
392
|
+
onRepair: () => {},
|
|
393
|
+
})
|
|
316
394
|
expect(html).toContain('Consulta no válida')
|
|
317
395
|
expect(html).toContain('page')
|
|
318
396
|
expect(html).toContain('Corregir búsqueda')
|
|
319
397
|
})
|
|
320
398
|
|
|
321
399
|
it('renders an unsupported-query repair state naming the unsupported fields', () => {
|
|
322
|
-
const html =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
/>,
|
|
328
|
-
)
|
|
400
|
+
const html = render({
|
|
401
|
+
products: [],
|
|
402
|
+
status: { state: 'unsupported-query', error: { code: 'unsupported-query', fields: ['priceMin'] } },
|
|
403
|
+
onRepair: () => {},
|
|
404
|
+
})
|
|
329
405
|
expect(html).toContain('Filtro no disponible')
|
|
330
406
|
expect(html).toContain('priceMin')
|
|
331
407
|
})
|
|
332
408
|
|
|
333
409
|
it('renders an unmounted-capability repair state naming the missing feature', () => {
|
|
334
|
-
const html =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
/>,
|
|
343
|
-
)
|
|
410
|
+
const html = render({
|
|
411
|
+
products: [],
|
|
412
|
+
status: {
|
|
413
|
+
state: 'unmounted-capability',
|
|
414
|
+
error: { code: 'unmounted-capability', fields: ['q'], feature: 'catalog-search' },
|
|
415
|
+
},
|
|
416
|
+
onRepair: () => {},
|
|
417
|
+
})
|
|
344
418
|
expect(html).toContain('Función no disponible')
|
|
345
419
|
expect(html).toContain('catalog-search')
|
|
346
420
|
})
|
|
347
421
|
|
|
348
422
|
it('renders an error state with a retry action and the failure message', () => {
|
|
349
|
-
const html =
|
|
350
|
-
<CategoryFilter2 products={[]} status={{ state: 'error', message: 'Network down' }} onRetry={() => {}} />,
|
|
351
|
-
)
|
|
423
|
+
const html = render({ products: [], status: { state: 'error', message: 'Network down' }, onRetry: () => {} })
|
|
352
424
|
expect(html).toContain('Network down')
|
|
353
425
|
expect(html).toContain('Reintentar')
|
|
354
426
|
})
|
|
355
427
|
|
|
356
428
|
it('renders an accessible removable chip per active filter', () => {
|
|
357
|
-
const html =
|
|
429
|
+
const html = render({ activeFilters: activeChips })
|
|
358
430
|
expect(html).toContain('aria-label="Filtros activos"')
|
|
359
431
|
expect(html).toContain('pantalla')
|
|
360
432
|
expect(html).toContain('Pantallas')
|
|
361
433
|
expect(html).toContain('aria-label="Quitar filtro: Pantallas"')
|
|
362
434
|
})
|
|
363
|
-
|
|
364
|
-
it('keeps the legacy ready surface when no status is supplied', () => {
|
|
365
|
-
const html = renderToStaticMarkup(<CategoryFilter2 products={products} sortOptions={advertisedSorts} />)
|
|
366
|
-
expect(html).toContain('Pantalla Samsung Galaxy A54 OLED')
|
|
367
|
-
})
|
|
368
435
|
})
|
|
369
436
|
|
|
370
|
-
describe('
|
|
437
|
+
describe('CategoryListingStatusView — state and chip interactions', () => {
|
|
371
438
|
it('removes exactly the clicked chip and preserves the others', () => {
|
|
372
439
|
const onRemoveFilter = vi.fn()
|
|
373
|
-
const tree =
|
|
440
|
+
const tree = CategoryListingStatusView({
|
|
374
441
|
status: { state: 'ready' },
|
|
375
442
|
activeFilters: activeChips,
|
|
376
443
|
onRemoveFilter,
|
|
@@ -388,7 +455,7 @@ describe('CategoryFilter2StatusView — state and chip interactions (task 4.5)',
|
|
|
388
455
|
|
|
389
456
|
it('repairs by emitting the exact bad fields of the typed error', () => {
|
|
390
457
|
const onRepair = vi.fn()
|
|
391
|
-
const tree =
|
|
458
|
+
const tree = CategoryListingStatusView({
|
|
392
459
|
status: {
|
|
393
460
|
state: 'unsupported-query',
|
|
394
461
|
error: { code: 'unsupported-query', fields: ['priceMin', 'inStock'], message: 'Unsupported' },
|
|
@@ -405,7 +472,7 @@ describe('CategoryFilter2StatusView — state and chip interactions (task 4.5)',
|
|
|
405
472
|
|
|
406
473
|
it('retries through the callback without emitting any query mutation', () => {
|
|
407
474
|
const onRetry = vi.fn()
|
|
408
|
-
const tree =
|
|
475
|
+
const tree = CategoryListingStatusView({ status: { state: 'error' }, onRetry })
|
|
409
476
|
|
|
410
477
|
const retry = findAllByProp(tree, 'data-action', 'retry')
|
|
411
478
|
retry[0].props.onClick?.()
|
|
@@ -416,7 +483,7 @@ describe('CategoryFilter2StatusView — state and chip interactions (task 4.5)',
|
|
|
416
483
|
|
|
417
484
|
it('clears active filters from the empty state', () => {
|
|
418
485
|
const onClearFilters = vi.fn()
|
|
419
|
-
const tree =
|
|
486
|
+
const tree = CategoryListingStatusView({
|
|
420
487
|
status: { state: 'empty' },
|
|
421
488
|
activeFilters: activeChips,
|
|
422
489
|
onClearFilters,
|
|
@@ -430,56 +497,44 @@ describe('CategoryFilter2StatusView — state and chip interactions (task 4.5)',
|
|
|
430
497
|
})
|
|
431
498
|
|
|
432
499
|
it('offers no repair/retry action when the host binds no callback', () => {
|
|
433
|
-
const tree =
|
|
500
|
+
const tree = CategoryListingStatusView({
|
|
434
501
|
status: { state: 'invalid-query', error: { code: 'invalid-query', fields: ['page'] } },
|
|
435
502
|
})
|
|
436
503
|
expect(findAllByProp(tree, 'data-action', 'repair')).toHaveLength(0)
|
|
437
504
|
})
|
|
438
505
|
})
|
|
439
506
|
|
|
440
|
-
describe('
|
|
441
|
-
it('renders pagination as native buttons
|
|
442
|
-
const html =
|
|
443
|
-
<CategoryFilter2 products={products} pagination={[1, 2, 3]} selectedPage={2} sortOptions={advertisedSorts} />,
|
|
444
|
-
)
|
|
445
|
-
// A bare `<a>` without href is not focusable and not a link role; the control
|
|
446
|
-
// must be a native button so it is reachable and operable by keyboard.
|
|
507
|
+
describe('CategoryListing — keyboard, labels and mobile controls', () => {
|
|
508
|
+
it('renders pagination as native buttons with aria-current, not href-less anchors', () => {
|
|
509
|
+
const html = render({ pagination: [1, 2, 3], selectedPage: 2 })
|
|
447
510
|
expect(html).toMatch(/<button[^>]*data-slot="pagination-link"/)
|
|
448
511
|
expect(html).not.toMatch(/<a[^>]*data-slot="pagination-link"/)
|
|
449
512
|
expect(countOccurrences(html, 'data-slot="pagination-link"')).toBe(3)
|
|
450
513
|
expect(countOccurrences(html, 'aria-current="page"')).toBe(1)
|
|
451
514
|
})
|
|
452
515
|
|
|
453
|
-
it('shows no pagination and no product count when the backend supplies no total
|
|
454
|
-
|
|
455
|
-
// absent total yields `[]`, and the block renders no page links and no
|
|
456
|
-
// manufactured count.
|
|
457
|
-
const html = renderToStaticMarkup(
|
|
458
|
-
<CategoryFilter2 products={products} pagination={[]} sortOptions={advertisedSorts} />,
|
|
459
|
-
)
|
|
516
|
+
it('shows no pagination and no product count when the backend supplies no total', () => {
|
|
517
|
+
const html = render({ pagination: [] })
|
|
460
518
|
expect(countOccurrences(html, 'data-slot="pagination-link"')).toBe(0)
|
|
461
519
|
expect(html).not.toContain('aria-label="Paginación"')
|
|
462
520
|
expect(html).not.toMatch(/\d+\s*(producto|resultado)/i)
|
|
463
521
|
})
|
|
464
522
|
|
|
465
523
|
it('wires the mobile filter trigger as a dialog trigger so focus can return on close', () => {
|
|
466
|
-
const html =
|
|
524
|
+
const html = render()
|
|
467
525
|
expect(html).toContain('aria-haspopup="dialog"')
|
|
468
526
|
expect(html).toContain('aria-expanded="false"')
|
|
469
527
|
expect(html).toMatch(/<button[^>]*lg:hidden[^>]*>[\s\S]*Filtros/)
|
|
470
528
|
})
|
|
471
529
|
|
|
472
530
|
it('labels the search, sort, view and chip controls', () => {
|
|
473
|
-
const html =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
activeFilters={activeChips}
|
|
481
|
-
/>,
|
|
482
|
-
)
|
|
531
|
+
const html = render({
|
|
532
|
+
sortOptions: advertisedSorts,
|
|
533
|
+
viewOptions: [10, 25],
|
|
534
|
+
showSearch: true,
|
|
535
|
+
searchValue: 'pantalla',
|
|
536
|
+
activeFilters: activeChips,
|
|
537
|
+
})
|
|
483
538
|
expect(html).toContain('aria-label="Buscar"')
|
|
484
539
|
expect(html).toContain('aria-label="Ordenar por"')
|
|
485
540
|
expect(html).toContain('aria-label="Ver"')
|
|
@@ -488,14 +543,11 @@ describe('CategoryFilter2 — keyboard, labels and mobile sheet (task 4.6)', ()
|
|
|
488
543
|
})
|
|
489
544
|
|
|
490
545
|
it('labels the filter-panel inputs and checkboxes it renders', () => {
|
|
491
|
-
const html =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
priceRange={{ min: 0, max: 100, step: 5 }}
|
|
497
|
-
/>,
|
|
498
|
-
)
|
|
546
|
+
const html = render({
|
|
547
|
+
categories: [{ id: 'pantallas', label: 'Pantallas' }],
|
|
548
|
+
collections: [{ id: 'ofertas', label: 'Ofertas' }],
|
|
549
|
+
priceRange: { min: 0, max: 100, step: 5 },
|
|
550
|
+
})
|
|
499
551
|
expect(html).toContain('>Mínimo</label>')
|
|
500
552
|
expect(html).toContain('>Máximo</label>')
|
|
501
553
|
expect(html).toMatch(/<label[^>]*for="[^"]*"[^>]*>\s*Pantallas\s*<\/label>/)
|
|
@@ -503,11 +555,47 @@ describe('CategoryFilter2 — keyboard, labels and mobile sheet (task 4.6)', ()
|
|
|
503
555
|
})
|
|
504
556
|
|
|
505
557
|
it('adds no positive tabindex and keeps search and pagination in the natural tab order', () => {
|
|
506
|
-
const html =
|
|
507
|
-
<CategoryFilter2 products={products} sortOptions={advertisedSorts} pagination={[1, 2]} showSearch />,
|
|
508
|
-
)
|
|
558
|
+
const html = render({ sortOptions: advertisedSorts, pagination: [1, 2], showSearch: true })
|
|
509
559
|
expect(html).not.toMatch(/tabindex="[1-9]/)
|
|
510
560
|
expect(html).not.toMatch(/<input[^>]*type="search"[^>]*tabindex="-1"/)
|
|
511
561
|
expect(html).not.toMatch(/<button[^>]*data-slot="pagination-link"[^>]*tabindex="-1"/)
|
|
512
562
|
})
|
|
513
563
|
})
|
|
564
|
+
|
|
565
|
+
describe('CategoryListing — one contract, two sectors', () => {
|
|
566
|
+
it('renders home goods through the same contract with its own real facets and range', () => {
|
|
567
|
+
const homeGoods = render({
|
|
568
|
+
products: [
|
|
569
|
+
{
|
|
570
|
+
id: 'jarron',
|
|
571
|
+
title: 'Jarrón de cerámica',
|
|
572
|
+
price: { amount: '42.00', currencyCode: 'USD' },
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
categories: [{ id: 'decoracion', label: 'Decoración' }],
|
|
576
|
+
collections: [{ id: 'hogar', label: 'Hogar' }],
|
|
577
|
+
priceRange: { min: 0, max: 200, step: 5 },
|
|
578
|
+
selectedPriceRange: [0, 200],
|
|
579
|
+
facets: [
|
|
580
|
+
{
|
|
581
|
+
id: 'material',
|
|
582
|
+
label: 'Material',
|
|
583
|
+
options: [
|
|
584
|
+
{ value: 'ceramic', label: 'Cerámica', count: 2 },
|
|
585
|
+
{ value: 'wood', label: 'Madera', count: 0 },
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
showSearch: false,
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
expect(homeGoods).toContain('Jarrón de cerámica')
|
|
593
|
+
expect(homeGoods).toContain('Decoración')
|
|
594
|
+
expect(homeGoods).toContain('Hogar')
|
|
595
|
+
expect(homeGoods).toContain('Material')
|
|
596
|
+
expect(homeGoods).toContain('Madera')
|
|
597
|
+
expect(homeGoods).toContain('data-slot="facet-count"')
|
|
598
|
+
expect(homeGoods).not.toContain('Auriculares')
|
|
599
|
+
expect(homeGoods).not.toContain('Condición')
|
|
600
|
+
})
|
|
601
|
+
})
|