|
| 1 | +/** Test UI batch 6 — Card, AlertBanner, Badge (LoadingButton non esiste) */ |
| 2 | +import { describe, it, expect } from "vitest"; |
| 3 | +import fs from "node:fs"; |
| 4 | +import path from "node:path"; |
| 5 | + |
| 6 | +const WEB = path.resolve(__dirname, "../../../web"); |
| 7 | +function readSrc(rel: string) { return fs.readFileSync(path.join(WEB, rel), "utf-8"); } |
| 8 | + |
| 9 | +/* ── Card ── */ |
| 10 | +describe("Card", () => { |
| 11 | + const src = readSrc("app/components/Card.tsx"); |
| 12 | + |
| 13 | + it("export Card + CardGrid + StatCard + tipi CardVariant/CardPadding + 3 interface", () => { |
| 14 | + expect(src).toMatch(/export function Card\b/); |
| 15 | + expect(src).toMatch(/export function CardGrid\b/); |
| 16 | + expect(src).toMatch(/export function StatCard\b/); |
| 17 | + expect(src).toContain("export type CardVariant"); expect(src).toContain("export type CardPadding"); |
| 18 | + expect(src).toContain("export interface CardProps"); |
| 19 | + expect(src).toContain("export interface CardGridProps"); |
| 20 | + expect(src).toContain("export interface StatCardProps"); |
| 21 | + }); |
| 22 | + |
| 23 | + it("VARIANT_STYLE 3: default/outlined/elevated + borderRadius 12", () => { |
| 24 | + expect(src).toContain("VARIANT_STYLE"); |
| 25 | + for (const v of ["default", "outlined", "elevated"]) expect(src).toContain(`${v}:`); |
| 26 | + expect(src).toContain("borderRadius: 12"); |
| 27 | + expect(src).toContain("boxShadow: 'none'"); // default + outlined |
| 28 | + expect(src).toContain("background: 'transparent'"); // outlined |
| 29 | + }); |
| 30 | + |
| 31 | + it("PADDING_CLS 4 livelli + HEADER_PAD + FOOTER_PAD maps", () => { |
| 32 | + expect(src).toContain("PADDING_CLS"); expect(src).toContain("HEADER_PAD"); expect(src).toContain("FOOTER_PAD"); |
| 33 | + for (const p of ["none", "sm", "md", "lg"]) expect(src).toContain(`${p}:`); |
| 34 | + expect(src).toContain("p-3"); expect(src).toContain("p-5"); expect(src).toContain("p-7"); |
| 35 | + }); |
| 36 | + |
| 37 | + it("header: title + subtitle + action slot, footer con borderTop", () => { |
| 38 | + expect(src).toContain("hasHeader"); expect(src).toContain("{title}"); expect(src).toContain("{subtitle}"); |
| 39 | + expect(src).toContain("{action}"); expect(src).toContain("{footer}"); expect(src).toContain("borderTop"); |
| 40 | + }); |
| 41 | + |
| 42 | + it("hoverable + onClick → role='button' + tabIndex 0 + keyboard Enter/Space", () => { |
| 43 | + expect(src).toContain("hoverable"); expect(src).toContain("isClickable"); |
| 44 | + expect(src).toContain("card-interactive"); |
| 45 | + expect(src).toContain("role={isClickable ? 'button'"); |
| 46 | + expect(src).toContain("tabIndex={isClickable ? 0"); |
| 47 | + expect(src).toContain("'Enter'"); expect(src).toContain("' '"); |
| 48 | + expect(src).toContain("scale(0.995)"); // active |
| 49 | + }); |
| 50 | + |
| 51 | + it("CardGrid: cols 1-4 responsive + COLS/GAPS maps", () => { |
| 52 | + expect(src).toContain("COLS"); expect(src).toContain("GAPS"); |
| 53 | + expect(src).toContain("grid-cols-1"); expect(src).toContain("sm:grid-cols-2"); |
| 54 | + expect(src).toContain("lg:grid-cols-3"); expect(src).toContain("lg:grid-cols-4"); |
| 55 | + for (const g of ["gap-3", "gap-5", "gap-7"]) expect(src).toContain(g); |
| 56 | + }); |
| 57 | + |
| 58 | + it("StatCard: label + value + trend ↑↓ verde/rosso + sub opzionale", () => { |
| 59 | + expect(src).toContain("trendColor"); |
| 60 | + expect(src).toContain("var(--color-green)"); expect(src).toContain("var(--color-red)"); |
| 61 | + expect(src).toContain("Math.abs(trend)"); |
| 62 | + }); |
| 63 | +}); |
| 64 | + |
| 65 | +/* ── AlertBanner ── */ |
| 66 | +describe("AlertBanner", () => { |
| 67 | + const src = readSrc("app/components/AlertBanner.tsx"); |
| 68 | + |
| 69 | + it("export AlertBanner + AlertBannerStack + AlertType + 3 interface", () => { |
| 70 | + expect(src).toMatch(/export function AlertBanner\b/); |
| 71 | + expect(src).toMatch(/export function AlertBannerStack\b/); |
| 72 | + expect(src).toContain("export type AlertType"); |
| 73 | + expect(src).toContain("export interface AlertBannerProps"); |
| 74 | + expect(src).toContain("export interface BannerItem"); |
| 75 | + expect(src).toContain("export interface AlertBannerStackProps"); |
| 76 | + }); |
| 77 | + |
| 78 | + it("TYPE_STYLE + ACTION_STYLE per 4 tipi: info/warning/error/success", () => { |
| 79 | + expect(src).toContain("TYPE_STYLE"); expect(src).toContain("ACTION_STYLE"); |
| 80 | + for (const t of ["info", "warning", "error", "success"]) expect(src).toContain(`${t}:`); |
| 81 | + }); |
| 82 | + |
| 83 | + it("Icon component SVG per tipo: polyline (success), circle (error/info), path (warning)", () => { |
| 84 | + expect(src).toContain("function Icon"); |
| 85 | + expect(src).toContain("polyline"); expect(src).toContain("<circle"); expect(src).toContain("<path"); |
| 86 | + }); |
| 87 | + |
| 88 | + it("role='alert' + aria-label='Chiudi' + banner-slide-down animation", () => { |
| 89 | + expect(src).toContain('role="alert"'); expect(src).toContain('aria-label="Chiudi"'); |
| 90 | + expect(src).toContain("banner-slide-down"); |
| 91 | + expect(src).toContain("translateY(-100%)"); expect(src).toContain("translateY(0)"); |
| 92 | + }); |
| 93 | + |
| 94 | + it("dismissible + action CTA (label+onClick) + position fixed/inline + zIndex 100", () => { |
| 95 | + expect(src).toContain("dismissible"); expect(src).toContain("setVisible(false)"); |
| 96 | + expect(src).toContain("action.label"); expect(src).toContain("action.onClick"); |
| 97 | + expect(src).toContain("position: 'fixed'"); expect(src).toContain("position: 'relative'"); |
| 98 | + expect(src).toContain("zIndex: 100"); |
| 99 | + }); |
| 100 | + |
| 101 | + it("AlertBannerStack: banners.map position='inline' + onDismiss(id)", () => { |
| 102 | + expect(src).toContain("banners.map"); |
| 103 | + expect(src).toContain('position="inline"'); |
| 104 | + expect(src).toContain("onDismiss={() => onDismiss(b.id)}"); |
| 105 | + }); |
| 106 | +}); |
| 107 | + |
| 108 | +/* ── Badge ── */ |
| 109 | +describe("Badge", () => { |
| 110 | + const src = readSrc("app/components/Badge.tsx"); |
| 111 | + |
| 112 | + it("export Badge + BadgeGroup + StatusBadge + CountBadge", () => { |
| 113 | + expect(src).toMatch(/export function Badge\b/); |
| 114 | + expect(src).toMatch(/export function BadgeGroup\b/); |
| 115 | + expect(src).toMatch(/export function StatusBadge\b/); |
| 116 | + expect(src).toMatch(/export function CountBadge\b/); |
| 117 | + }); |
| 118 | + |
| 119 | + it("BadgeVariant 6 + BadgeSize 3 + tipi esportati", () => { |
| 120 | + expect(src).toContain("export type BadgeVariant"); expect(src).toContain("export type BadgeSize"); |
| 121 | + for (const v of ["default", "success", "warning", "error", "info", "outline"]) expect(src).toContain(`${v}:`); |
| 122 | + }); |
| 123 | + |
| 124 | + it("VARIANT + DOT_COLOR + SIZE_CLS + DOT_PX + RMV_PX + ICN_PX maps", () => { |
| 125 | + for (const m of ["VARIANT", "DOT_COLOR", "SIZE_CLS", "DOT_PX", "RMV_PX", "ICN_PX"]) |
| 126 | + expect(src).toContain(m); |
| 127 | + }); |
| 128 | + |
| 129 | + it("dot puro (no label) + dot prefix + removable aria-label='Rimuovi'", () => { |
| 130 | + expect(src).toContain("dot && !label"); expect(src).toContain("removable"); |
| 131 | + expect(src).toContain('aria-label="Rimuovi"'); expect(src).toContain("onRemove?.()"); |
| 132 | + }); |
| 133 | + |
| 134 | + it("STATUS_MAP: attivo/completato/errore/pending/merged → varianti", () => { |
| 135 | + expect(src).toContain("STATUS_MAP"); |
| 136 | + for (const s of ["attivo", "completato", "errore", "pending", "merged"]) expect(src).toContain(`${s}:`); |
| 137 | + }); |
| 138 | + |
| 139 | + it("CountBadge: count > max → '99+' + BadgeGroupProps wrap/gap", () => { |
| 140 | + expect(src).toContain("count > max"); expect(src).toContain("`${max}+`"); |
| 141 | + expect(src).toContain("export interface BadgeGroupProps"); expect(src).toContain("wrap"); |
| 142 | + }); |
| 143 | +}); |
0 commit comments