@arroyavecommerce/theme-kinetic 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/docs/design/README.md +22 -0
  2. package/docs/design/kinetic-street-concept.html +558 -0
  3. package/package.json +45 -0
  4. package/src/blocks/_shared.tsx +174 -0
  5. package/src/blocks/carrusel.tsx +109 -0
  6. package/src/blocks/category-tiles.test.tsx +59 -0
  7. package/src/blocks/category-tiles.tsx +70 -0
  8. package/src/blocks/editorial-banner.test.tsx +96 -0
  9. package/src/blocks/editorial-banner.tsx +205 -0
  10. package/src/blocks/faq.test.tsx +68 -0
  11. package/src/blocks/faq.tsx +122 -0
  12. package/src/blocks/featured-collection.test.tsx +80 -0
  13. package/src/blocks/featured-collection.tsx +97 -0
  14. package/src/blocks/features.test.tsx +71 -0
  15. package/src/blocks/features.tsx +159 -0
  16. package/src/blocks/hero.test.tsx +75 -0
  17. package/src/blocks/hero.tsx +290 -0
  18. package/src/blocks/index.tsx +35 -0
  19. package/src/blocks/lookbook.test.tsx +80 -0
  20. package/src/blocks/lookbook.tsx +117 -0
  21. package/src/blocks/marquee.test.tsx +54 -0
  22. package/src/blocks/marquee.tsx +114 -0
  23. package/src/blocks/newsletter.test.tsx +60 -0
  24. package/src/blocks/newsletter.tsx +105 -0
  25. package/src/blocks/product-grid.test.tsx +160 -0
  26. package/src/blocks/product-grid.tsx +76 -0
  27. package/src/blocks/promo-countdown.tsx +75 -0
  28. package/src/blocks/promo.test.tsx +71 -0
  29. package/src/blocks/promo.tsx +120 -0
  30. package/src/blocks/rich-text.test.tsx +67 -0
  31. package/src/blocks/rich-text.tsx +75 -0
  32. package/src/blocks/spacer.test.tsx +54 -0
  33. package/src/blocks/spacer.tsx +56 -0
  34. package/src/blocks/testimonials.test.tsx +74 -0
  35. package/src/blocks/testimonials.tsx +104 -0
  36. package/src/blocks/video.test.tsx +159 -0
  37. package/src/blocks/video.tsx +226 -0
  38. package/src/catalog/buy-box.test.tsx +225 -0
  39. package/src/catalog/buy-box.tsx +353 -0
  40. package/src/catalog/config.test.ts +115 -0
  41. package/src/catalog/config.ts +167 -0
  42. package/src/catalog/filter-controls.test.tsx +149 -0
  43. package/src/catalog/filter-controls.tsx +187 -0
  44. package/src/catalog/gallery.test.tsx +201 -0
  45. package/src/catalog/gallery.tsx +212 -0
  46. package/src/catalog/lightbox.test.tsx +95 -0
  47. package/src/catalog/lightbox.tsx +184 -0
  48. package/src/catalog/pdp.test.tsx +309 -0
  49. package/src/catalog/pdp.tsx +354 -0
  50. package/src/catalog/plp-arreglos.test.tsx +235 -0
  51. package/src/catalog/plp-inline.tsx +54 -0
  52. package/src/catalog/plp-overlay.tsx +129 -0
  53. package/src/catalog/plp-parts.test.tsx +146 -0
  54. package/src/catalog/plp-parts.tsx +292 -0
  55. package/src/catalog/plp-pills.tsx +44 -0
  56. package/src/catalog/plp-sidebar.tsx +47 -0
  57. package/src/catalog/plp.test.tsx +319 -0
  58. package/src/catalog/plp.tsx +259 -0
  59. package/src/catalog/product-card.test.tsx +221 -0
  60. package/src/catalog/product-card.tsx +411 -0
  61. package/src/catalog/registry.tsx +16 -0
  62. package/src/catalog/related.test.tsx +52 -0
  63. package/src/catalog/related.tsx +40 -0
  64. package/src/catalog/reviews.test.tsx +56 -0
  65. package/src/catalog/reviews.tsx +116 -0
  66. package/src/catalog/skeletons.test.tsx +61 -0
  67. package/src/catalog/skeletons.tsx +103 -0
  68. package/src/chrome/announcement-bar.test.tsx +86 -0
  69. package/src/chrome/announcement-bar.tsx +173 -0
  70. package/src/chrome/drawer-movil.test.tsx +113 -0
  71. package/src/chrome/drawer-movil.tsx +342 -0
  72. package/src/chrome/footer.test.tsx +133 -0
  73. package/src/chrome/footer.tsx +603 -0
  74. package/src/chrome/header.test.tsx +165 -0
  75. package/src/chrome/header.tsx +592 -0
  76. package/src/chrome/index.ts +26 -0
  77. package/src/chrome/mega-menu.test.tsx +161 -0
  78. package/src/chrome/mega-menu.tsx +317 -0
  79. package/src/chrome/mini-cart.test.tsx +111 -0
  80. package/src/chrome/mini-cart.tsx +320 -0
  81. package/src/content/home.ts +19 -0
  82. package/src/content/seed.test.ts +33 -0
  83. package/src/content/seed.ts +28 -0
  84. package/src/context.ts +104 -0
  85. package/src/icons/icons.test.tsx +118 -0
  86. package/src/icons/index.ts +136 -0
  87. package/src/icons/payment-icons.tsx +241 -0
  88. package/src/icons/social-icons.tsx +108 -0
  89. package/src/icons/svg-base.tsx +68 -0
  90. package/src/index.ts +49 -0
  91. package/src/puck-config.tsx +404 -0
  92. package/src/smoke.test.ts +8 -0
  93. package/src/template.test.tsx +88 -0
  94. package/src/test/mock-ctx.test.tsx +77 -0
  95. package/src/test/mock-ctx.tsx +236 -0
  96. package/src/theme-defaults.test.ts +9 -0
  97. package/src/theme.test.ts +82 -0
  98. package/src/theme.ts +195 -0
  99. package/tsconfig.json +4 -0
@@ -0,0 +1,404 @@
1
+ import type { Config } from "@puckeditor/core"
2
+ import { StudioBlock } from "@arroyavecommerce/cms-studio"
3
+ import { heroBlock } from "./blocks/hero"
4
+ import { categoryTilesBlock } from "./blocks/category-tiles"
5
+ import { productGridBlock } from "./blocks/product-grid"
6
+ import { editorialBannerBlock } from "./blocks/editorial-banner"
7
+ import { newsletterBlock } from "./blocks/newsletter"
8
+ import { featuresBlock, ICONOS_FEATURE } from "./blocks/features"
9
+ import { marqueeBlock } from "./blocks/marquee"
10
+ import { featuredCollectionBlock } from "./blocks/featured-collection"
11
+ import { testimonialsBlock } from "./blocks/testimonials"
12
+ import { lookbookBlock } from "./blocks/lookbook"
13
+ import { faqBlock } from "./blocks/faq"
14
+ import { richTextBlock } from "./blocks/rich-text"
15
+ import { spacerBlock, TAMANOS_SPACER } from "./blocks/spacer"
16
+ import { promoBlock, PROMO_FONDOS } from "./blocks/promo"
17
+ import { videoBlock } from "./blocks/video"
18
+ import { FONDO_OPCIONES } from "./blocks/_shared"
19
+
20
+ /** Campo Puck reutilizable para los enlaces editables (`{ texto, href }`). */
21
+ const campoEnlace = {
22
+ type: "object" as const,
23
+ objectFields: {
24
+ texto: { type: "text" as const, label: "Texto" },
25
+ href: { type: "text" as const, label: "Enlace" },
26
+ },
27
+ }
28
+
29
+ /** Campo Puck reutilizable para el `fondo` por bloque (claro/oscuro/acento/superficie). */
30
+ const campoFondo = {
31
+ type: "select" as const,
32
+ label: "Fondo",
33
+ options: FONDO_OPCIONES.map((o) => ({ label: o.label, value: o.value })),
34
+ }
35
+
36
+ /** `fondo` de la banda promocional: los 4 comunes + `degradado`. */
37
+ const ETIQUETA_FONDO: Record<string, string> = {
38
+ claro: "Claro",
39
+ oscuro: "Oscuro",
40
+ acento: "Acento",
41
+ superficie: "Superficie",
42
+ degradado: "Degradado",
43
+ }
44
+ const campoFondoPromo = {
45
+ type: "select" as const,
46
+ label: "Fondo",
47
+ options: PROMO_FONDOS.map((v) => ({ label: ETIQUETA_FONDO[v] ?? v, value: v })),
48
+ }
49
+
50
+ /** Config de Puck para los bloques de Home de `theme-kinetic`. */
51
+ export const puckConfig: Config = {
52
+ categories: {
53
+ contenido: {
54
+ title: "Contenido",
55
+ components: ["hero", "editorialBanner", "lookbook", "video", "richText", "marquee", "promo", "newsletter"],
56
+ },
57
+ catalogo: {
58
+ title: "Catálogo",
59
+ components: ["categoryTiles", "productGrid", "featuredCollection"],
60
+ },
61
+ social: {
62
+ title: "Social y confianza",
63
+ components: ["features", "testimonials", "faq"],
64
+ },
65
+ estructura: {
66
+ title: "Estructura",
67
+ components: ["spacer"],
68
+ },
69
+ },
70
+ components: {
71
+ hero: {
72
+ label: heroBlock.label,
73
+ defaultProps: heroBlock.defaults,
74
+ fields: {
75
+ layout: {
76
+ type: "select",
77
+ label: "Diseño",
78
+ options: [
79
+ { label: "Full-bleed", value: "full-bleed" },
80
+ { label: "Split", value: "split" },
81
+ { label: "Centrado", value: "centered" },
82
+ { label: "Grid", value: "grid" },
83
+ ],
84
+ },
85
+ alineacion: {
86
+ type: "select",
87
+ label: "Alineación",
88
+ options: [
89
+ { label: "Izquierda", value: "izquierda" },
90
+ { label: "Centro", value: "centro" },
91
+ ],
92
+ },
93
+ fondo: campoFondo,
94
+ etiqueta: { type: "text", label: "Etiqueta" },
95
+ titulo: { type: "text", label: "Título" },
96
+ subtitulo: { type: "textarea", label: "Subtítulo" },
97
+ imagenUrl: { type: "text", label: "Imagen (URL)" },
98
+ enlacePrimario: { ...campoEnlace, label: "Enlace primario" },
99
+ enlaceSecundario: { ...campoEnlace, label: "Enlace secundario" },
100
+ },
101
+ render: (props) => <StudioBlock definition={heroBlock} props={props} />,
102
+ },
103
+ categoryTiles: {
104
+ label: categoryTilesBlock.label,
105
+ defaultProps: categoryTilesBlock.defaults,
106
+ fields: {
107
+ columnas: {
108
+ type: "select",
109
+ label: "Columnas",
110
+ options: [
111
+ { label: "3", value: 3 },
112
+ { label: "4", value: 4 },
113
+ ],
114
+ },
115
+ items: {
116
+ type: "array",
117
+ label: "Mosaicos",
118
+ arrayFields: {
119
+ titulo: { type: "text", label: "Título" },
120
+ imagenUrl: { type: "text", label: "Imagen (URL)" },
121
+ href: { type: "text", label: "Enlace" },
122
+ },
123
+ defaultItemProps: { titulo: "", imagenUrl: "", href: "" },
124
+ getItemSummary: (item) => item.titulo || "Mosaico",
125
+ },
126
+ },
127
+ render: (props) => <StudioBlock definition={categoryTilesBlock} props={props} />,
128
+ },
129
+ productGrid: {
130
+ label: productGridBlock.label,
131
+ defaultProps: productGridBlock.defaults,
132
+ fields: {
133
+ titulo: { type: "text", label: "Título" },
134
+ coleccionId: { type: "text", label: "ID de colección" },
135
+ columnas: {
136
+ type: "select",
137
+ label: "Columnas",
138
+ options: [
139
+ { label: "3", value: 3 },
140
+ { label: "4", value: 4 },
141
+ ],
142
+ },
143
+ card: {
144
+ type: "select",
145
+ label: "Tarjeta de producto",
146
+ options: [
147
+ { label: "Minimal", value: "minimal" },
148
+ { label: "Agregar rápido", value: "quick-add" },
149
+ { label: "Tallas", value: "tallas" },
150
+ { label: "Vista rápida", value: "quick-view" },
151
+ ],
152
+ },
153
+ },
154
+ render: (props) => <StudioBlock definition={productGridBlock} props={props} />,
155
+ },
156
+ editorialBanner: {
157
+ label: editorialBannerBlock.label,
158
+ defaultProps: editorialBannerBlock.defaults,
159
+ fields: {
160
+ posicionImagen: {
161
+ type: "select",
162
+ label: "Posición de la imagen",
163
+ options: [
164
+ { label: "Derecha", value: "derecha" },
165
+ { label: "Izquierda", value: "izquierda" },
166
+ { label: "Fondo", value: "fondo" },
167
+ ],
168
+ },
169
+ fondo: campoFondo,
170
+ titulo: { type: "text", label: "Título" },
171
+ texto: { type: "textarea", label: "Texto" },
172
+ imagenUrl: { type: "text", label: "Imagen (URL)" },
173
+ enlace: { ...campoEnlace, label: "Enlace" },
174
+ },
175
+ render: (props) => <StudioBlock definition={editorialBannerBlock} props={props} />,
176
+ },
177
+ newsletter: {
178
+ label: newsletterBlock.label,
179
+ defaultProps: newsletterBlock.defaults,
180
+ fields: {
181
+ fondo: {
182
+ type: "select",
183
+ label: "Fondo",
184
+ options: [
185
+ { label: "Fondo", value: "fondo" },
186
+ { label: "Acento", value: "acento" },
187
+ ],
188
+ },
189
+ titulo: { type: "text", label: "Título" },
190
+ texto: { type: "textarea", label: "Texto" },
191
+ placeholder: { type: "text", label: "Placeholder" },
192
+ textoBoton: { type: "text", label: "Texto del botón" },
193
+ notaLegal: { type: "textarea", label: "Nota legal" },
194
+ },
195
+ render: (props) => <StudioBlock definition={newsletterBlock} props={props} />,
196
+ },
197
+ features: {
198
+ label: featuresBlock.label,
199
+ defaultProps: featuresBlock.defaults,
200
+ fields: {
201
+ fondo: campoFondo,
202
+ items: {
203
+ type: "array",
204
+ label: "Items",
205
+ arrayFields: {
206
+ icono: {
207
+ type: "select",
208
+ label: "Icono",
209
+ options: ICONOS_FEATURE.map((v) => ({ label: v, value: v })),
210
+ },
211
+ titulo: { type: "text", label: "Título" },
212
+ texto: { type: "textarea", label: "Texto" },
213
+ },
214
+ defaultItemProps: { icono: "envio", titulo: "", texto: "" },
215
+ getItemSummary: (item) => item.titulo || "Item",
216
+ },
217
+ },
218
+ render: (props) => <StudioBlock definition={featuresBlock} props={props} />,
219
+ },
220
+ marquee: {
221
+ label: marqueeBlock.label,
222
+ defaultProps: marqueeBlock.defaults,
223
+ fields: {
224
+ mensajes: {
225
+ type: "array",
226
+ label: "Mensajes",
227
+ arrayFields: { texto: { type: "text", label: "Mensaje" } },
228
+ defaultItemProps: { texto: "" },
229
+ getItemSummary: (item) => item.texto || "Mensaje",
230
+ },
231
+ velocidad: {
232
+ type: "select",
233
+ label: "Velocidad",
234
+ options: [
235
+ { label: "Lenta", value: "lenta" },
236
+ { label: "Media", value: "media" },
237
+ { label: "Rápida", value: "rapida" },
238
+ ],
239
+ },
240
+ color: {
241
+ type: "select",
242
+ label: "Color",
243
+ options: [
244
+ { label: "Acento", value: "acento" },
245
+ { label: "Tinta", value: "tinta" },
246
+ { label: "Degradado", value: "degradado" },
247
+ ],
248
+ },
249
+ },
250
+ render: (props) => <StudioBlock definition={marqueeBlock} props={props} />,
251
+ },
252
+ featuredCollection: {
253
+ label: featuredCollectionBlock.label,
254
+ defaultProps: featuredCollectionBlock.defaults,
255
+ fields: {
256
+ fondo: campoFondo,
257
+ titulo: { type: "text", label: "Título" },
258
+ coleccionId: { type: "text", label: "ID de colección" },
259
+ card: {
260
+ type: "select",
261
+ label: "Tarjeta de producto",
262
+ options: [
263
+ { label: "Minimal", value: "minimal" },
264
+ { label: "Agregar rápido", value: "quick-add" },
265
+ { label: "Tallas", value: "tallas" },
266
+ { label: "Vista rápida", value: "quick-view" },
267
+ ],
268
+ },
269
+ verTodos: { ...campoEnlace, label: "Ver todos" },
270
+ },
271
+ render: (props) => <StudioBlock definition={featuredCollectionBlock} props={props} />,
272
+ },
273
+ testimonials: {
274
+ label: testimonialsBlock.label,
275
+ defaultProps: testimonialsBlock.defaults,
276
+ fields: {
277
+ fondo: campoFondo,
278
+ titulo: { type: "text", label: "Título" },
279
+ items: {
280
+ type: "array",
281
+ label: "Testimonios",
282
+ arrayFields: {
283
+ cita: { type: "textarea", label: "Cita" },
284
+ autor: { type: "text", label: "Autor" },
285
+ rating: { type: "number", label: "Rating (1–5)", min: 1, max: 5 },
286
+ },
287
+ defaultItemProps: { cita: "", autor: "", rating: 5 },
288
+ getItemSummary: (item) => item.autor || "Testimonio",
289
+ },
290
+ },
291
+ render: (props) => <StudioBlock definition={testimonialsBlock} props={props} />,
292
+ },
293
+ lookbook: {
294
+ label: lookbookBlock.label,
295
+ defaultProps: lookbookBlock.defaults,
296
+ fields: {
297
+ fondo: campoFondo,
298
+ titulo: { type: "text", label: "Título" },
299
+ items: {
300
+ type: "array",
301
+ label: "Piezas",
302
+ arrayFields: {
303
+ imagenUrl: { type: "text", label: "Imagen (URL)" },
304
+ titulo: { type: "text", label: "Título" },
305
+ href: { type: "text", label: "Enlace" },
306
+ },
307
+ defaultItemProps: { imagenUrl: "", titulo: "", href: "" },
308
+ getItemSummary: (item) => item.titulo || "Pieza",
309
+ },
310
+ },
311
+ render: (props) => <StudioBlock definition={lookbookBlock} props={props} />,
312
+ },
313
+ faq: {
314
+ label: faqBlock.label,
315
+ defaultProps: faqBlock.defaults,
316
+ fields: {
317
+ fondo: campoFondo,
318
+ titulo: { type: "text", label: "Título" },
319
+ items: {
320
+ type: "array",
321
+ label: "Preguntas",
322
+ arrayFields: {
323
+ pregunta: { type: "text", label: "Pregunta" },
324
+ respuesta: { type: "textarea", label: "Respuesta" },
325
+ },
326
+ defaultItemProps: { pregunta: "", respuesta: "" },
327
+ getItemSummary: (item) => item.pregunta || "Pregunta",
328
+ },
329
+ },
330
+ render: (props) => <StudioBlock definition={faqBlock} props={props} />,
331
+ },
332
+ richText: {
333
+ label: richTextBlock.label,
334
+ defaultProps: richTextBlock.defaults,
335
+ fields: {
336
+ fondo: campoFondo,
337
+ alineacion: {
338
+ type: "select",
339
+ label: "Alineación",
340
+ options: [
341
+ { label: "Izquierda", value: "izquierda" },
342
+ { label: "Centro", value: "centro" },
343
+ ],
344
+ },
345
+ titulo: { type: "text", label: "Título" },
346
+ cuerpo: { type: "textarea", label: "Cuerpo" },
347
+ },
348
+ render: (props) => <StudioBlock definition={richTextBlock} props={props} />,
349
+ },
350
+ spacer: {
351
+ label: spacerBlock.label,
352
+ defaultProps: spacerBlock.defaults,
353
+ fields: {
354
+ tamano: {
355
+ type: "select",
356
+ label: "Tamaño",
357
+ options: TAMANOS_SPACER.map((v) => ({ label: v.toUpperCase(), value: v })),
358
+ },
359
+ divisor: {
360
+ type: "radio",
361
+ label: "Divisor",
362
+ options: [
363
+ { label: "Sí", value: true },
364
+ { label: "No", value: false },
365
+ ],
366
+ },
367
+ },
368
+ render: (props) => <StudioBlock definition={spacerBlock} props={props} />,
369
+ },
370
+ promo: {
371
+ label: promoBlock.label,
372
+ defaultProps: promoBlock.defaults,
373
+ fields: {
374
+ fondo: campoFondoPromo,
375
+ titulo: { type: "text", label: "Título" },
376
+ texto: { type: "textarea", label: "Texto" },
377
+ enlace: { ...campoEnlace, label: "Enlace" },
378
+ cuentaRegresiva: { type: "text", label: "Cuenta regresiva (fecha ISO, opcional)" },
379
+ },
380
+ render: (props) => <StudioBlock definition={promoBlock} props={props} />,
381
+ },
382
+ video: {
383
+ label: videoBlock.label,
384
+ defaultProps: videoBlock.defaults,
385
+ fields: {
386
+ fondo: campoFondo,
387
+ titulo: { type: "text", label: "Título" },
388
+ texto: { type: "textarea", label: "Texto" },
389
+ videoUrl: { type: "text", label: "Video (MP4/webm o URL de YouTube/Vimeo)" },
390
+ poster: { type: "text", label: "Portada (URL)" },
391
+ autoplay: {
392
+ type: "radio",
393
+ label: "Reproducción automática (silenciada)",
394
+ options: [
395
+ { label: "Sí", value: true },
396
+ { label: "No", value: false },
397
+ ],
398
+ },
399
+ enlace: { ...campoEnlace, label: "Enlace" },
400
+ },
401
+ render: (props) => <StudioBlock definition={videoBlock} props={props} />,
402
+ },
403
+ },
404
+ }
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as t from "./index";
3
+
4
+ describe("smoke test", () => {
5
+ it("should export an object", () => {
6
+ expect(typeof t).toBe("object");
7
+ });
8
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { renderToStaticMarkup } from "react-dom/server"
3
+ import { cmsPageDataSchema, resolveChrome } from "@arroyavecommerce/cms-core"
4
+ import { assertTemplateDefaults } from "@arroyavecommerce/cms-storefront"
5
+ import { themeKinetic } from "./index"
6
+ import { mockCtx, fixtureListado, fixtureDetalle } from "./test/mock-ctx"
7
+
8
+ describe("themeKinetic", () => {
9
+ it("no tiene bloques con defaults inválidos", () => {
10
+ expect(assertTemplateDefaults(themeKinetic)).toEqual([])
11
+ })
12
+
13
+ it("registra los bloques de Home", () => {
14
+ expect(Object.keys(themeKinetic.blocks)).toEqual([
15
+ "hero",
16
+ "categoryTiles",
17
+ "productGrid",
18
+ "editorialBanner",
19
+ "newsletter",
20
+ "features",
21
+ "marquee",
22
+ "featuredCollection",
23
+ "testimonials",
24
+ "lookbook",
25
+ "faq",
26
+ "richText",
27
+ "spacer",
28
+ "promo",
29
+ "video",
30
+ ])
31
+ })
32
+
33
+ it("defaultHomePage cumple el schema cmsPageDataSchema", () => {
34
+ expect(cmsPageDataSchema.safeParse(themeKinetic.defaultHomePage).success).toBe(true)
35
+ })
36
+
37
+ it("tiene chrome registrado con los headers y footers esperados", () => {
38
+ expect(themeKinetic.chrome).toBeDefined()
39
+ expect(Object.keys(themeKinetic.chrome!.headers)).toEqual(["inline", "centrado", "split", "minimal"])
40
+ expect(Object.keys(themeKinetic.chrome!.footers)).toEqual(["moda", "minimal", "newsletter"])
41
+ })
42
+
43
+ it("resolveChrome resuelve el header inline y el footer moda", () => {
44
+ const config = {
45
+ version: 1 as const,
46
+ header: { variante: "inline", props: {} },
47
+ footer: { variante: "moda", props: {} },
48
+ menu: { items: [] },
49
+ }
50
+ expect(resolveChrome(themeKinetic.chrome!, config, "header").def).toBeDefined()
51
+ expect(resolveChrome(themeKinetic.chrome!, config, "footer").def).toBeDefined()
52
+ })
53
+
54
+ it("tiene catalogo registrado con pageConfigSettings de plp y pdp", () => {
55
+ expect(themeKinetic.catalogo).toBeDefined()
56
+ const settings = themeKinetic.catalogo!.pageConfigSettings
57
+ expect(settings.plp.map((s) => s.key)).toEqual([
58
+ "plp.filtros",
59
+ "plp.columnas",
60
+ "plp.card",
61
+ "plp.paginacion",
62
+ "plp.mostrarContador",
63
+ "plp.vista",
64
+ ])
65
+ expect(settings.pdp.map((s) => s.key)).toEqual([
66
+ "pdp.galeria",
67
+ "pdp.stickyBar",
68
+ "pdp.compraRapida",
69
+ "pdp.mostrarFavoritos",
70
+ "pdp.mostrarReseñas",
71
+ "pdp.mostrarRelacionados",
72
+ ])
73
+ })
74
+
75
+ it("catalogo.plp renderiza el PLP sin crashear e incluye el titulo del listado", () => {
76
+ const Plp = themeKinetic.catalogo!.plp
77
+ const ctx = mockCtx({ catalogo: { listado: fixtureListado() } })
78
+ const html = renderToStaticMarkup(<Plp ctx={ctx} pageConfig={null} />)
79
+ expect(html).toContain("Todos los productos")
80
+ })
81
+
82
+ it("catalogo.pdp renderiza el PDP sin crashear e incluye el titulo del producto", () => {
83
+ const Pdp = themeKinetic.catalogo!.pdp
84
+ const ctx = mockCtx({ catalogo: { detalle: fixtureDetalle() } })
85
+ const html = renderToStaticMarkup(<Pdp ctx={ctx} pageConfig={null} />)
86
+ expect(html).toContain("Sneaker Runner Blanco")
87
+ })
88
+ })
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { renderToStaticMarkup } from "react-dom/server"
3
+ import { mockCtx } from "./mock-ctx"
4
+
5
+ describe("mockCtx", () => {
6
+ describe("componentes.Imagen", () => {
7
+ it("debería renderizar img con src y alt cuando src tiene valor", () => {
8
+ const ctx = mockCtx()
9
+ const Imagen = ctx.componentes.Imagen
10
+ const html = renderToStaticMarkup(
11
+ <Imagen src="/x.jpg" alt="Foto" />
12
+ )
13
+ expect(html).toContain('src="/x.jpg"')
14
+ expect(html).toContain('alt="Foto"')
15
+ expect(html).toMatch(/<img/)
16
+ })
17
+
18
+ it("debería renderizar placeholder sin src cuando src está vacío", () => {
19
+ const ctx = mockCtx()
20
+ const Imagen = ctx.componentes.Imagen
21
+ const html = renderToStaticMarkup(
22
+ <Imagen src="" alt="Vacío" />
23
+ )
24
+ expect(html).not.toContain('src=')
25
+ expect(html).toContain('role="img"')
26
+ expect(html).toContain('aria-label="Vacío"')
27
+ expect(html).toContain("var(--cms-superficie)")
28
+ })
29
+
30
+ it("debería respetar className", () => {
31
+ const ctx = mockCtx()
32
+ const Imagen = ctx.componentes.Imagen
33
+ const html = renderToStaticMarkup(
34
+ <Imagen src="/x.jpg" alt="Foto" className="custom-class" />
35
+ )
36
+ expect(html).toContain('class="custom-class"')
37
+ })
38
+ })
39
+
40
+ describe("componentes.Enlace", () => {
41
+ it("debería renderizar a con href y children", () => {
42
+ const ctx = mockCtx()
43
+ const Enlace = ctx.componentes.Enlace
44
+ const html = renderToStaticMarkup(
45
+ <Enlace href="/store">Ver</Enlace>
46
+ )
47
+ expect(html).toContain('href="/store"')
48
+ expect(html).toContain("Ver")
49
+ expect(html).toMatch(/<a/)
50
+ })
51
+
52
+ it("debería respetar className", () => {
53
+ const ctx = mockCtx()
54
+ const Enlace = ctx.componentes.Enlace
55
+ const html = renderToStaticMarkup(
56
+ <Enlace href="/store" className="link-class">Ver</Enlace>
57
+ )
58
+ expect(html).toContain('class="link-class"')
59
+ })
60
+ })
61
+
62
+ describe("sesion", () => {
63
+ it("debería ser null por defecto", () => {
64
+ const ctx = mockCtx()
65
+ expect(ctx.sesion).toBeNull()
66
+ })
67
+ })
68
+
69
+ describe("overrides", () => {
70
+ it("debería permitir override de sesion", () => {
71
+ const ctx = mockCtx({
72
+ sesion: { autenticado: true, nombre: "Juan" }
73
+ })
74
+ expect(ctx.sesion).toEqual({ autenticado: true, nombre: "Juan" })
75
+ })
76
+ })
77
+ })