diff --git a/apps/www/src/registry/components/alert/alert.test.tsx b/apps/www/src/registry/components/alert/alert.test.tsx index 41612781..0ffdb077 100644 --- a/apps/www/src/registry/components/alert/alert.test.tsx +++ b/apps/www/src/registry/components/alert/alert.test.tsx @@ -2,10 +2,36 @@ import { describe, expect, it } from 'vitest'; import { PdfAlert } from './alert'; describe('PdfAlert', () => { - it('renders without throwing', () => { - expect(() => PdfAlert({ title: 'Test' })).not.toThrow(); + const variants = ['info', 'success', 'warning', 'error'] as const; + + it('renders with title only', () => { + expect(() => PdfAlert({ title: 'Heads up' })).not.toThrow(); + }); + + it('renders with string children only', () => { + expect(() => PdfAlert({ children: 'Body text' })).not.toThrow(); + }); + + it('returns null when both title and children are missing', () => { + // alert.tsx:193 — early return null when there's nothing to render. + expect(PdfAlert({})).toBeNull(); }); - it('accepts variant prop', () => { - expect(() => PdfAlert({ title: 'Test', variant: 'success' })).not.toThrow(); + + it('renders all variants with title and children', () => { + for (const variant of variants) { + expect(() => PdfAlert({ variant, title: 'T', children: 'Body' })).not.toThrow(); + } + }); + + it('respects showIcon and showBorder toggles', () => { + expect(() => PdfAlert({ title: 'T', showIcon: false, showBorder: false })).not.toThrow(); + expect(() => PdfAlert({ title: 'T', showIcon: true, showBorder: true })).not.toThrow(); + expect(() => PdfAlert({ title: 'T', showIcon: false, showBorder: true })).not.toThrow(); + expect(() => PdfAlert({ title: 'T', showIcon: true, showBorder: false })).not.toThrow(); + }); + + it('accepts non-string children and style overrides', () => { + expect(() => PdfAlert({ title: 'T', children: null, style: { marginTop: 8 } })).not.toThrow(); + expect(() => PdfAlert({ title: 'T', style: { marginTop: 8, padding: 4 } })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/badge/badge.test.tsx b/apps/www/src/registry/components/badge/badge.test.tsx index 26aee5ce..8b117ecd 100644 --- a/apps/www/src/registry/components/badge/badge.test.tsx +++ b/apps/www/src/registry/components/badge/badge.test.tsx @@ -2,10 +2,53 @@ import { describe, expect, it } from 'vitest'; import { Badge } from './badge'; describe('Badge', () => { - it('renders without throwing', () => { - expect(() => Badge({ label: 'Test' })).not.toThrow(); + const variants = [ + 'default', + 'primary', + 'success', + 'warning', + 'destructive', + 'info', + 'outline', + ] as const; + const sizes = ['sm', 'md', 'lg'] as const; + + it('renders with label', () => { + expect(() => Badge({ label: 'New' })).not.toThrow(); + }); + + it('renders with string children when label is absent', () => { + expect(() => Badge({ children: 'Draft' })).not.toThrow(); + }); + + it('label takes precedence over children (both branches are safe)', () => { + // badge.tsx:131 — `label ?? children ?? ''` + expect(() => Badge({ label: 'A', children: 'B' })).not.toThrow(); + }); + + it('renders with neither label nor children (empty text fallback)', () => { + expect(() => Badge({})).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => Badge({ label: 'Test', variant: 'primary' })).not.toThrow(); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => Badge({ label: 'x', variant })).not.toThrow(); + } + }); + + it('renders all sizes', () => { + for (const size of sizes) { + expect(() => Badge({ label: 'x', size })).not.toThrow(); + } + }); + + it('accepts background and color overrides (tokens and raw hex)', () => { + expect(() => Badge({ label: 'x', background: '#ff0', color: '#000' })).not.toThrow(); + expect(() => Badge({ label: 'x', background: 'primary', color: 'foreground' })).not.toThrow(); + }); + + it('accepts a style override', () => { + expect(() => Badge({ label: 'x', style: { marginLeft: 4 } })).not.toThrow(); + expect(() => Badge({ label: 'x', style: { marginLeft: 4, opacity: 0.8 } })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/card/card.test.tsx b/apps/www/src/registry/components/card/card.test.tsx index dcb8eb8a..efccfbb6 100644 --- a/apps/www/src/registry/components/card/card.test.tsx +++ b/apps/www/src/registry/components/card/card.test.tsx @@ -2,12 +2,54 @@ import { describe, expect, it } from 'vitest'; import { PdfCard } from './card'; describe('PdfCard', () => { - it('renders without throwing', () => { - expect(() => PdfCard({ children: 'Content' })).not.toThrow(); + const variants = ['default', 'bordered', 'muted'] as const; + const paddings = ['sm', 'md', 'lg'] as const; + + it('renders with string children', () => { + expect(() => PdfCard({ children: 'Body' })).not.toThrow(); + }); + + it('renders with title and string children', () => { + expect(() => PdfCard({ title: 'T', children: 'Body' })).not.toThrow(); + }); + + it('renders with no content at all', () => { + expect(() => PdfCard({})).not.toThrow(); + }); + + it('renders with non-string children (null pass-through)', () => { + // card.tsx:88 — non-string children are passed through without wrapping. + expect(() => PdfCard({ children: null })).not.toThrow(); }); - it('accepts variant prop', () => { + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => PdfCard({ variant, children: 'x' })).not.toThrow(); + } + }); + + it('renders all padding presets', () => { + for (const padding of paddings) { + expect(() => PdfCard({ padding, children: 'x' })).not.toThrow(); + } + }); + + it('honors wrap prop in both modes', () => { + expect(() => PdfCard({ wrap: true, children: 'x' })).not.toThrow(); + expect(() => PdfCard({ wrap: false, children: 'x' })).not.toThrow(); + }); + + it('accepts a style override and combines with variant', () => { + expect(() => PdfCard({ children: 'x', style: { borderRadius: 12 } })).not.toThrow(); expect(() => - PdfCard({ title: 'Test', variant: 'bordered', children: 'Content' }) + PdfCard({ + title: 'T', + variant: 'bordered', + padding: 'lg', + wrap: true, + children: 'x', + style: { marginTop: 8 }, + }) ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/data-table/data-table.test.tsx b/apps/www/src/registry/components/data-table/data-table.test.tsx index 652897c4..81aa8886 100644 --- a/apps/www/src/registry/components/data-table/data-table.test.tsx +++ b/apps/www/src/registry/components/data-table/data-table.test.tsx @@ -1,11 +1,74 @@ import { describe, expect, it } from 'vitest'; import { DataTable } from './data-table'; +import type { DataTableColumn } from './data-table.types'; + +interface Row extends Record { + name: string; + score: number; + status: string; +} + +const columns: DataTableColumn[] = [ + { key: 'name', header: 'Name', align: 'left' }, + { key: 'score', header: 'Score', align: 'right', width: 80 }, + { key: 'status', header: 'Status', align: 'center' }, +]; + +const data: Row[] = [ + { name: 'Ada', score: 99, status: 'active' }, + { name: 'Grace', score: 88, status: 'active' }, + { name: 'Linus', score: 77, status: 'inactive' }, +]; describe('DataTable', () => { - it('renders without throwing', () => { - expect(() => DataTable({ columns: [], data: [] })).not.toThrow(); + it('renders with empty columns and data', () => { + expect(() => DataTable({ columns: [], data: [] })).not.toThrow(); + }); + + it('renders with columns but no rows', () => { + expect(() => DataTable({ columns, data: [] })).not.toThrow(); + }); + + it('renders with rows and all column aligns/widths', () => { + expect(() => DataTable({ columns, data })).not.toThrow(); }); - it('accepts size prop', () => { - expect(() => DataTable({ columns: [], data: [], size: 'compact' })).not.toThrow(); + + it('renders both size variants', () => { + expect(() => DataTable({ columns, data, size: 'default' })).not.toThrow(); + expect(() => DataTable({ columns, data, size: 'compact' })).not.toThrow(); + }); + + it('honors stripe, noWrap, and style overrides', () => { + expect(() => + DataTable({ + columns, + data, + stripe: true, + noWrap: true, + style: { marginTop: 8 }, + }) + ).not.toThrow(); + }); + + it('invokes custom cell render and footer render', () => { + const renderScore = (value: unknown) => + typeof value === 'number' ? `${value}%` : String(value ?? ''); + expect(() => + DataTable({ + columns: [ + { key: 'name', header: 'Name' }, + { key: 'score', header: 'Score', render: renderScore, renderFooter: renderScore }, + ], + data, + footer: { score: 264 }, + }) + ).not.toThrow(); + }); + + it('renders table variants through the underlying Table', () => { + // data-table.tsx:24 — variant is forwarded to . + for (const variant of ['grid', 'striped', 'minimal', 'bordered'] as const) { + expect(() => DataTable({ columns, data, variant })).not.toThrow(); + } }); }); diff --git a/apps/www/src/registry/components/divider/divider.test.tsx b/apps/www/src/registry/components/divider/divider.test.tsx index 8ced2981..4c5874e4 100644 --- a/apps/www/src/registry/components/divider/divider.test.tsx +++ b/apps/www/src/registry/components/divider/divider.test.tsx @@ -2,10 +2,48 @@ import { describe, expect, it } from 'vitest'; import { Divider } from './divider'; describe('Divider', () => { - it('renders without throwing', () => { + const variants = ['solid', 'dashed', 'dotted'] as const; + const thicknesses = ['thin', 'medium', 'thick'] as const; + const spacings = ['none', 'sm', 'md', 'lg'] as const; + + it('renders with no props', () => { expect(() => Divider({})).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => Divider({ variant: 'dashed' })).not.toThrow(); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => Divider({ variant })).not.toThrow(); + } + }); + + it('renders all thicknesses', () => { + for (const thickness of thicknesses) { + expect(() => Divider({ thickness })).not.toThrow(); + } + }); + + it('renders all spacing presets', () => { + for (const spacing of spacings) { + expect(() => Divider({ spacing })).not.toThrow(); + } + }); + + it('renders the labeled branch', () => { + expect(() => Divider({ label: 'OR' })).not.toThrow(); + // Labeled divider still honors variant/thickness/spacing. + for (const variant of variants) { + expect(() => + Divider({ label: 'Section', variant, thickness: 'medium', spacing: 'lg' }) + ).not.toThrow(); + } + }); + + it('accepts width (number and string), color token, and style override', () => { + expect(() => Divider({ width: 200 })).not.toThrow(); + expect(() => Divider({ width: '50%' })).not.toThrow(); + expect(() => Divider({ color: 'primary' })).not.toThrow(); + expect(() => Divider({ color: '#cccccc' })).not.toThrow(); + expect(() => Divider({ label: 'OR', color: 'primary', width: '75%' })).not.toThrow(); + expect(() => Divider({ style: { marginTop: 8 } })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/form/form.test.tsx b/apps/www/src/registry/components/form/form.test.tsx index 9a88ae4b..99de922e 100644 --- a/apps/www/src/registry/components/form/form.test.tsx +++ b/apps/www/src/registry/components/form/form.test.tsx @@ -1,11 +1,76 @@ import { describe, expect, it } from 'vitest'; import { PdfForm } from './form'; +import type { PdfFormGroup } from './form.types'; describe('PdfForm', () => { - it('renders without throwing', () => { + const variants = ['underline', 'box', 'outlined', 'ghost'] as const; + const layouts = ['single', 'two-column', 'three-column'] as const; + + const baseGroup: PdfFormGroup = { + title: 'Contact', + fields: [ + { label: 'Name' }, + { label: 'Email', hint: 'you@example.com' }, + { label: 'Phone', height: 24 }, + ], + }; + + it('renders with empty groups array', () => { expect(() => PdfForm({ groups: [] })).not.toThrow(); }); - it('accepts layout prop', () => { - expect(() => PdfForm({ groups: [], variant: 'box' })).not.toThrow(); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => PdfForm({ groups: [baseGroup], variant })).not.toThrow(); + } + }); + + it('renders all column layouts', () => { + for (const layout of layouts) { + expect(() => PdfForm({ groups: [{ ...baseGroup, layout }] })).not.toThrow(); + } + }); + + it('renders both labelPositions', () => { + for (const labelPosition of ['above', 'left'] as const) { + expect(() => PdfForm({ groups: [baseGroup], labelPosition })).not.toThrow(); + } + }); + + it('renders with title/subtitle (adds form divider)', () => { + // form.tsx:108 — title or subtitle triggers the formDivider row. + expect(() => + PdfForm({ title: 'Application', subtitle: 'Fill every field', groups: [baseGroup] }) + ).not.toThrow(); + expect(() => PdfForm({ title: 'Application', groups: [baseGroup] })).not.toThrow(); + expect(() => PdfForm({ subtitle: 'Only', groups: [baseGroup] })).not.toThrow(); + }); + + it('renders groups with no title, empty fields, and uneven column chunks', () => { + expect(() => PdfForm({ groups: [{ fields: [] }] })).not.toThrow(); + expect(() => + PdfForm({ + groups: [ + { fields: [{ label: 'A' }], layout: 'three-column' }, + { fields: [{ label: 'A' }, { label: 'B' }], layout: 'three-column' }, + { + fields: [ + { label: 'A' }, + { label: 'B' }, + { label: 'C' }, + { label: 'D' }, + { label: 'E' }, + ], + layout: 'two-column', + }, + ], + }) + ).not.toThrow(); + }); + + it('accepts noWrap and a style override', () => { + expect(() => + PdfForm({ groups: [baseGroup], noWrap: true, style: { padding: 12 } }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/graph/graph.test.tsx b/apps/www/src/registry/components/graph/graph.test.tsx index 574f56da..ada2dcaa 100644 --- a/apps/www/src/registry/components/graph/graph.test.tsx +++ b/apps/www/src/registry/components/graph/graph.test.tsx @@ -1,11 +1,82 @@ import { describe, expect, it } from 'vitest'; import { PdfGraph } from './graph'; +import type { GraphDataPoint, GraphSeries } from './graph.types'; describe('PdfGraph', () => { - it('renders without throwing', () => { + const variants = ['bar', 'horizontal-bar', 'line', 'area', 'pie', 'donut'] as const; + const legends = ['bottom', 'right', 'none'] as const; + + const points: GraphDataPoint[] = [ + { label: 'Q1', value: 120 }, + { label: 'Q2', value: 180 }, + { label: 'Q3', value: 90 }, + { label: 'Q4', value: 210 }, + ]; + + const series: GraphSeries[] = [ + { name: '2024', data: points }, + { name: '2025', data: points.map((p) => ({ ...p, value: p.value * 1.1 })) }, + ]; + + it('renders with empty data array', () => { expect(() => PdfGraph({ data: [] })).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => PdfGraph({ data: [], variant: 'bar' })).not.toThrow(); + + it('renders every variant with single-series point data', () => { + for (const variant of variants) { + expect(() => PdfGraph({ variant, data: points })).not.toThrow(); + } + }); + + it('renders bar/line/area variants with multi-series data', () => { + // Multi-series is only meaningful for bar/line/area — pie/donut flatten the first series. + for (const variant of ['bar', 'horizontal-bar', 'line', 'area'] as const) { + expect(() => PdfGraph({ variant, data: series })).not.toThrow(); + } + }); + + it('renders all legend positions', () => { + for (const legend of legends) { + expect(() => PdfGraph({ data: points, legend })).not.toThrow(); + } + }); + + it('accepts title, subtitle, axis labels, and custom colors', () => { + expect(() => + PdfGraph({ + variant: 'bar', + data: points, + title: 'Revenue', + subtitle: 'Fiscal 2025', + xLabel: 'Quarter', + yLabel: 'USD', + colors: ['#ff0000', '#00ff00', '#0000ff'], + showValues: true, + showGrid: true, + yTicks: 8, + }) + ).not.toThrow(); + }); + + it('accepts donut centerLabel, smooth line, and no-dots line', () => { + expect(() => PdfGraph({ variant: 'donut', data: points, centerLabel: 'Total' })).not.toThrow(); + expect(() => + PdfGraph({ variant: 'line', data: points, smooth: true, showDots: false }) + ).not.toThrow(); + }); + + it('accepts full-width sizing, noWrap, and style override', () => { + expect(() => + PdfGraph({ + data: points, + fullWidth: true, + containerPadding: 24, + wrapperPadding: 12, + width: 600, + height: 320, + noWrap: false, + style: { marginTop: 16 }, + }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/heading/heading.test.tsx b/apps/www/src/registry/components/heading/heading.test.tsx index 01ea12ff..ecd46011 100644 --- a/apps/www/src/registry/components/heading/heading.test.tsx +++ b/apps/www/src/registry/components/heading/heading.test.tsx @@ -2,10 +2,53 @@ import { describe, expect, it } from 'vitest'; import { Heading } from './heading'; describe('Heading', () => { - it('renders without throwing', () => { - expect(() => Heading({ level: 1, children: 'Title' })).not.toThrow(); + const levels = [1, 2, 3, 4, 5, 6] as const; + const weights = ['normal', 'medium', 'semibold', 'bold'] as const; + const trackings = ['tighter', 'tight', 'normal', 'wide', 'wider'] as const; + const transforms = ['uppercase', 'lowercase', 'capitalize'] as const; + const aligns = ['left', 'center', 'right'] as const; + + it('renders with minimal props', () => { + expect(() => Heading({ children: 'Title' })).not.toThrow(); + }); + + it('renders all levels', () => { + for (const level of levels) { + expect(() => Heading({ level, children: `h${level}` })).not.toThrow(); + } + }); + + it('clamps out-of-range levels to the 1-6 window', () => { + // level is typed as 1-6 but runtime clamps Math.round(level) to [1,6]. + // biome-ignore lint/suspicious/noExplicitAny: intentional out-of-range runtime input + expect(() => Heading({ level: 0 as any, children: 'under' })).not.toThrow(); + // biome-ignore lint/suspicious/noExplicitAny: intentional out-of-range runtime input + expect(() => Heading({ level: 9 as any, children: 'over' })).not.toThrow(); }); - it('accepts level prop', () => { - expect(() => Heading({ level: 3, children: 'Subtitle' })).not.toThrow(); + + it('renders all weights and trackings', () => { + for (const weight of weights) { + expect(() => Heading({ weight, children: 'x' })).not.toThrow(); + } + for (const tracking of trackings) { + expect(() => Heading({ tracking, children: 'x' })).not.toThrow(); + } + }); + + it('renders all transforms and alignments', () => { + for (const transform of transforms) { + expect(() => Heading({ transform, children: 'x' })).not.toThrow(); + } + for (const align of aligns) { + expect(() => Heading({ align, children: 'x' })).not.toThrow(); + } + }); + + it('accepts noMargin, keepWithNext, color, and style overrides', () => { + expect(() => Heading({ noMargin: true, children: 'x' })).not.toThrow(); + expect(() => Heading({ keepWithNext: false, children: 'x' })).not.toThrow(); + expect(() => Heading({ color: 'primary', children: 'x' })).not.toThrow(); + expect(() => Heading({ color: '#333333', children: 'x' })).not.toThrow(); + expect(() => Heading({ children: 'x', style: { marginLeft: 8 } })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/keep-together/keep-together.test.tsx b/apps/www/src/registry/components/keep-together/keep-together.test.tsx index ea77c582..0d90fe14 100644 --- a/apps/www/src/registry/components/keep-together/keep-together.test.tsx +++ b/apps/www/src/registry/components/keep-together/keep-together.test.tsx @@ -2,10 +2,37 @@ import { describe, expect, it } from 'vitest'; import { KeepTogether } from './keep-together'; describe('KeepTogether', () => { - it('renders without throwing', () => { + it('renders with string children', () => { expect(() => KeepTogether({ children: 'Content' })).not.toThrow(); }); - it('accepts style prop', () => { - expect(() => KeepTogether({ children: 'Content', style: { marginTop: 8 } })).not.toThrow(); + + it('renders with null/undefined children (no-op wrapper)', () => { + expect(() => KeepTogether({ children: null })).not.toThrow(); + expect(() => KeepTogether({})).not.toThrow(); + }); + + it('accepts minPresenceAhead', () => { + expect(() => KeepTogether({ children: 'x', minPresenceAhead: 0 })).not.toThrow(); + expect(() => KeepTogether({ children: 'x', minPresenceAhead: 80 })).not.toThrow(); + expect(() => KeepTogether({ children: 'x', minPresenceAhead: 9999 })).not.toThrow(); + }); + + it('accepts a style override and combines it with minPresenceAhead', () => { + expect(() => + KeepTogether({ children: 'x', style: { marginTop: 8, padding: 4 } }) + ).not.toThrow(); + expect(() => + KeepTogether({ + children: 'x', + minPresenceAhead: 40, + style: { backgroundColor: '#eee' }, + }) + ).not.toThrow(); + }); + + it('returns a React element', () => { + const result = KeepTogether({ children: 'x' }); + expect(result).not.toBeNull(); + expect(result).toBeDefined(); }); }); diff --git a/apps/www/src/registry/components/key-value/key-value.test.tsx b/apps/www/src/registry/components/key-value/key-value.test.tsx index 405350fc..1a8fc631 100644 --- a/apps/www/src/registry/components/key-value/key-value.test.tsx +++ b/apps/www/src/registry/components/key-value/key-value.test.tsx @@ -2,12 +2,72 @@ import { describe, expect, it } from 'vitest'; import { KeyValue } from './key-value'; describe('KeyValue', () => { - it('renders without throwing', () => { + const sizes = ['sm', 'md', 'lg'] as const; + const directions = ['horizontal', 'vertical'] as const; + const sampleItems = [ + { key: 'Name', value: 'Ada Lovelace' }, + { key: 'Role', value: 'Engineer' }, + { key: 'Email', value: 'ada@example.com' }, + ]; + + it('renders with empty items array', () => { expect(() => KeyValue({ items: [] })).not.toThrow(); }); - it('accepts direction prop', () => { + + it('renders with a single item', () => { + expect(() => KeyValue({ items: [{ key: 'K', value: 'V' }] })).not.toThrow(); + }); + + it('renders both directions', () => { + for (const direction of directions) { + expect(() => KeyValue({ items: sampleItems, direction })).not.toThrow(); + } + }); + + it('renders all sizes', () => { + for (const size of sizes) { + expect(() => KeyValue({ items: sampleItems, size })).not.toThrow(); + } + }); + + it('renders divided rows in both directions', () => { + expect(() => + KeyValue({ items: sampleItems, divided: true, direction: 'horizontal' }) + ).not.toThrow(); + expect(() => + KeyValue({ items: sampleItems, divided: true, direction: 'vertical' }) + ).not.toThrow(); + }); + + it('accepts labelFlex, labelColor, valueColor, boldValue, noWrap', () => { + expect(() => + KeyValue({ + items: sampleItems, + labelFlex: 2, + labelColor: 'mutedForeground', + valueColor: 'primary', + boldValue: true, + noWrap: true, + }) + ).not.toThrow(); + }); + + it('accepts custom divider styling and per-item overrides', () => { expect(() => - KeyValue({ items: [{ key: 'Name', value: 'Alice' }], direction: 'vertical' }) + KeyValue({ + items: [ + { key: 'K', value: 'V', valueColor: '#ff0000' }, + { key: 'K2', value: 'V2', valueStyle: { fontSize: 14 }, keyStyle: { fontSize: 10 } }, + ], + divided: true, + dividerColor: 'border', + dividerThickness: 2, + dividerMargin: 4, + }) ).not.toThrow(); }); + + it('accepts a style override', () => { + expect(() => KeyValue({ items: sampleItems, style: { marginTop: 12 } })).not.toThrow(); + }); }); diff --git a/apps/www/src/registry/components/link/link.test.tsx b/apps/www/src/registry/components/link/link.test.tsx index 23e10073..e273195e 100644 --- a/apps/www/src/registry/components/link/link.test.tsx +++ b/apps/www/src/registry/components/link/link.test.tsx @@ -2,12 +2,47 @@ import { describe, expect, it } from 'vitest'; import { Link } from './link'; describe('Link', () => { - it('renders without throwing', () => { + const variants = ['default', 'muted', 'primary'] as const; + const underlines = ['always', 'none'] as const; + const aligns = ['left', 'center', 'right'] as const; + + it('renders with required href', () => { expect(() => Link({ href: 'https://example.com', children: 'Click here' })).not.toThrow(); }); - it('accepts variant prop', () => { + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => Link({ href: 'https://example.com', variant, children: 'x' })).not.toThrow(); + } + }); + + it('renders all underline modes', () => { + for (const underline of underlines) { + expect(() => Link({ href: 'https://example.com', underline, children: 'x' })).not.toThrow(); + } + }); + + it('renders all alignments', () => { + for (const align of aligns) { + expect(() => Link({ href: 'https://example.com', align, children: 'x' })).not.toThrow(); + } + }); + + it('supports mailto, tel, and relative hrefs', () => { + expect(() => Link({ href: 'mailto:a@b.com', children: 'email' })).not.toThrow(); + expect(() => Link({ href: 'tel:+15551234567', children: 'call' })).not.toThrow(); + expect(() => Link({ href: '#section-1', children: 'anchor' })).not.toThrow(); + }); + + it('accepts color tokens, raw colors, and style overrides', () => { + expect(() => + Link({ href: 'https://example.com', color: 'primary', children: 'x' }) + ).not.toThrow(); + expect(() => + Link({ href: 'https://example.com', color: '#0000ff', children: 'x' }) + ).not.toThrow(); expect(() => - Link({ href: 'https://example.com', children: 'Click', variant: 'primary' }) + Link({ href: 'https://example.com', children: 'x', style: { marginLeft: 4 } }) ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/page-break/page-break.test.tsx b/apps/www/src/registry/components/page-break/page-break.test.tsx index 4c46d4c1..b38a556a 100644 --- a/apps/www/src/registry/components/page-break/page-break.test.tsx +++ b/apps/www/src/registry/components/page-break/page-break.test.tsx @@ -2,10 +2,24 @@ import { describe, expect, it } from 'vitest'; import { PageBreak } from './page-break'; describe('PageBreak', () => { - it('renders without throwing', () => { + it('renders with no props', () => { expect(() => PageBreak({})).not.toThrow(); }); - it('accepts style prop', () => { + + it('renders with a style override (single object)', () => { expect(() => PageBreak({ style: { marginBottom: 0 } })).not.toThrow(); }); + + it('accepts combined margin and padding style overrides', () => { + expect(() => + PageBreak({ style: { marginBottom: 0, marginTop: 8, paddingBottom: 4 } }) + ).not.toThrow(); + }); + + it('returns a React element, not null', () => { + // PageBreak is a forced-break marker and must always render the wrapping View. + const result = PageBreak({}); + expect(result).not.toBeNull(); + expect(result).toBeDefined(); + }); }); diff --git a/apps/www/src/registry/components/page-footer/page-footer.test.tsx b/apps/www/src/registry/components/page-footer/page-footer.test.tsx index 0fff1a50..f90143b8 100644 --- a/apps/www/src/registry/components/page-footer/page-footer.test.tsx +++ b/apps/www/src/registry/components/page-footer/page-footer.test.tsx @@ -2,10 +2,65 @@ import { describe, expect, it } from 'vitest'; import { PageFooter } from './page-footer'; describe('PageFooter', () => { - it('renders without throwing', () => { - expect(() => PageFooter({ leftText: 'My Company' })).not.toThrow(); + const variants = [ + 'simple', + 'centered', + 'branded', + 'minimal', + 'three-column', + 'detailed', + ] as const; + + it('renders with minimal props', () => { + expect(() => PageFooter({ leftText: 'Co' })).not.toThrow(); + }); + + it('renders with no props at all (empty footer)', () => { + // All text fields are optional — the component still renders the container. + expect(() => PageFooter({})).not.toThrow(); + }); + + it('renders every variant with full prop set', () => { + for (const variant of variants) { + expect(() => + PageFooter({ + variant, + leftText: 'Co', + centerText: 'Center', + rightText: 'Page 1', + address: '123 Main', + phone: '555-1212', + email: 'a@b.com', + website: 'example.com', + }) + ).not.toThrow(); + } }); - it('accepts variant prop', () => { - expect(() => PageFooter({ variant: 'centered', centerText: 'Page 1' })).not.toThrow(); + + it('sticky implies fixed and resets marginTop to 0', () => { + // page-footer.tsx:236-237 — sticky forces isFixed=true and mt=0. + expect(() => PageFooter({ leftText: 'x', sticky: true, pagePadding: 24 })).not.toThrow(); + expect(() => PageFooter({ leftText: 'x', sticky: true, pagePadding: 0 })).not.toThrow(); + expect(() => PageFooter({ leftText: 'x', fixed: true })).not.toThrow(); + }); + + it('accepts background and textColor as both tokens and raw hex', () => { + expect(() => + PageFooter({ leftText: 'x', background: 'muted', textColor: 'foreground' }) + ).not.toThrow(); + expect(() => + PageFooter({ leftText: 'x', background: '#fafafa', textColor: '#333' }) + ).not.toThrow(); + }); + + it('accepts marginTop override, noWrap, and style override', () => { + expect(() => + PageFooter({ + leftText: 'x', + marginTop: 32, + noWrap: false, + style: { paddingHorizontal: 16 }, + }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/page-header/page-header.test.tsx b/apps/www/src/registry/components/page-header/page-header.test.tsx index 60329cf6..c77649a9 100644 --- a/apps/www/src/registry/components/page-header/page-header.test.tsx +++ b/apps/www/src/registry/components/page-header/page-header.test.tsx @@ -2,10 +2,72 @@ import { describe, expect, it } from 'vitest'; import { PageHeader } from './page-header'; describe('PageHeader', () => { - it('renders without throwing', () => { + const variants = [ + 'simple', + 'centered', + 'minimal', + 'branded', + 'logo-left', + 'logo-right', + 'two-column', + ] as const; + + it('renders with only required title', () => { expect(() => PageHeader({ title: 'My Report' })).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => PageHeader({ title: 'My Report', variant: 'centered' })).not.toThrow(); + + it('renders all variants with base props', () => { + for (const variant of variants) { + expect(() => PageHeader({ title: 'Title', subtitle: 'Subtitle', variant })).not.toThrow(); + } + }); + + it('simple/minimal/logo-left render right-side text blocks', () => { + for (const variant of ['simple', 'minimal', 'logo-left'] as const) { + expect(() => + PageHeader({ + title: 'T', + variant, + rightText: 'Q4 2025', + rightSubText: 'v2.1', + }) + ).not.toThrow(); + } + }); + + it('two-column renders with contact info', () => { + // page-header.tsx:340 — two-column variant uses address/phone/email. + expect(() => + PageHeader({ + title: 'T', + variant: 'two-column', + address: '123 Main St', + phone: '555-1212', + email: 'team@example.com', + }) + ).not.toThrow(); + }); + + it('logo-left and logo-right accept a logo ReactNode', () => { + const logo = null; // ReactNode accepts null without throwing + expect(() => PageHeader({ title: 'T', variant: 'logo-left', logo })).not.toThrow(); + expect(() => PageHeader({ title: 'T', variant: 'logo-right', logo })).not.toThrow(); + }); + + it('accepts background, titleColor, marginBottom, fixed, noWrap, and style', () => { + expect(() => + PageHeader({ + title: 'T', + background: 'muted', + titleColor: 'primary', + marginBottom: 32, + fixed: true, + noWrap: false, + style: { paddingTop: 8 }, + }) + ).not.toThrow(); + expect(() => + PageHeader({ title: 'T', background: '#eeeeee', titleColor: '#000000' }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/page-number/page-number.test.tsx b/apps/www/src/registry/components/page-number/page-number.test.tsx index b2e89c38..fd7b1b7a 100644 --- a/apps/www/src/registry/components/page-number/page-number.test.tsx +++ b/apps/www/src/registry/components/page-number/page-number.test.tsx @@ -2,10 +2,48 @@ import { describe, expect, it } from 'vitest'; import { PdfPageNumber } from './page-number'; describe('PdfPageNumber', () => { - it('renders without throwing', () => { + const aligns = ['left', 'center', 'right'] as const; + const sizes = ['xs', 'sm', 'md'] as const; + + it('renders with no props', () => { expect(() => PdfPageNumber({})).not.toThrow(); }); - it('accepts align prop', () => { - expect(() => PdfPageNumber({ align: 'right' })).not.toThrow(); + + it('renders all alignments', () => { + for (const align of aligns) { + expect(() => PdfPageNumber({ align })).not.toThrow(); + } + }); + + it('renders all sizes', () => { + for (const size of sizes) { + expect(() => PdfPageNumber({ size })).not.toThrow(); + } + }); + + it('accepts custom format strings', () => { + // page-number.tsx:55 — formatPageNumber replaces {page} and {total} in the template. + expect(() => PdfPageNumber({ format: '{page} / {total}' })).not.toThrow(); + expect(() => PdfPageNumber({ format: 'Page {page}' })).not.toThrow(); + expect(() => PdfPageNumber({ format: 'Sheet {page} of {total}' })).not.toThrow(); + // No tokens — render function still runs and returns the literal string. + expect(() => PdfPageNumber({ format: 'Footer' })).not.toThrow(); + }); + + it('honors fixed, muted, and style overrides', () => { + expect(() => PdfPageNumber({ fixed: true })).not.toThrow(); + expect(() => PdfPageNumber({ muted: false })).not.toThrow(); + expect(() => PdfPageNumber({ muted: true })).not.toThrow(); + expect(() => PdfPageNumber({ style: { marginTop: 8 } })).not.toThrow(); + expect(() => + PdfPageNumber({ + align: 'right', + size: 'md', + format: 'Page {page} of {total}', + fixed: true, + muted: false, + style: { paddingRight: 12 }, + }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/pdf-image/pdf-image.test.tsx b/apps/www/src/registry/components/pdf-image/pdf-image.test.tsx index 34ed2a0c..7cac1714 100644 --- a/apps/www/src/registry/components/pdf-image/pdf-image.test.tsx +++ b/apps/www/src/registry/components/pdf-image/pdf-image.test.tsx @@ -1,11 +1,82 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { PdfImage } from './pdf-image'; describe('PdfImage', () => { - it('renders without throwing', () => { + const variants = [ + 'default', + 'full-width', + 'thumbnail', + 'avatar', + 'cover', + 'bordered', + 'rounded', + ] as const; + + it('renders with string src', () => { expect(() => PdfImage({ src: '/test.png' })).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => PdfImage({ src: '/test.png', variant: 'rounded' })).not.toThrow(); + + it('renders with object src (uri + method + headers + body)', () => { + // pdf-image.tsx:11 — object src supports HTTP method/headers/body. + expect(() => + PdfImage({ + src: { + uri: 'https://example.com/image.png', + method: 'GET', + headers: { Authorization: 'Bearer x' }, + }, + }) + ).not.toThrow(); + expect(() => + PdfImage({ + src: { uri: 'https://example.com/upload', method: 'POST', body: '{}' }, + }) + ).not.toThrow(); + }); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => PdfImage({ src: '/x.png', variant })).not.toThrow(); + } + }); + + it('renders all fit modes', () => { + for (const fit of ['cover', 'contain', 'fill', 'none'] as const) { + expect(() => PdfImage({ src: '/x.png', fit })).not.toThrow(); + } + }); + + it('resolves height from aspectRatio when height is omitted', () => { + // pdf-image.tsx:125 — aspectRatio divides numeric width to compute height. + expect(() => PdfImage({ src: '/x.png', width: 200, aspectRatio: 16 / 9 })).not.toThrow(); + }); + + it('warns on unsupported image formats', () => { + // pdf-image.tsx:78-85 — detectFormat + console.warn for webp/avif/etc. + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + PdfImage({ src: 'https://example.com/img.webp' }); + PdfImage({ src: 'data:image/avif;base64,AAAA' }); + expect(warnSpy).toHaveBeenCalled(); + expect(warnSpy.mock.calls.length).toBeGreaterThanOrEqual(2); + } finally { + warnSpy.mockRestore(); + } + }); + + it('accepts numeric and percentage width/height, borderRadius, noWrap, and style', () => { + expect(() => + PdfImage({ + src: '/x.png', + width: 300, + height: 200, + borderRadius: 12, + noWrap: false, + position: '0% 0%', + caption: 'Figure 1', + style: { marginTop: 8 }, + }) + ).not.toThrow(); + expect(() => PdfImage({ src: '/x.png', width: '50%', height: '100%' })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/qrcode/qrcode.test.tsx b/apps/www/src/registry/components/qrcode/qrcode.test.tsx index b62e01a5..999281d0 100644 --- a/apps/www/src/registry/components/qrcode/qrcode.test.tsx +++ b/apps/www/src/registry/components/qrcode/qrcode.test.tsx @@ -2,10 +2,53 @@ import { describe, expect, it } from 'vitest'; import { PdfQRCode } from './qrcode'; describe('PdfQRCode', () => { - it('renders without throwing', () => { + const errorLevels = ['L', 'M', 'Q', 'H'] as const; + + it('renders with only the required value', () => { expect(() => PdfQRCode({ value: 'https://pdfx.dev' })).not.toThrow(); }); - it('accepts size prop', () => { - expect(() => PdfQRCode({ value: 'https://pdfx.dev', size: 80 })).not.toThrow(); + + it('renders all error correction levels', () => { + for (const errorLevel of errorLevels) { + expect(() => PdfQRCode({ value: 'x', errorLevel })).not.toThrow(); + } + }); + + it('renders at multiple sizes', () => { + for (const size of [32, 64, 100, 256]) { + expect(() => PdfQRCode({ value: 'x', size })).not.toThrow(); + } + }); + + it('handles short and long payloads', () => { + expect(() => PdfQRCode({ value: 'x' })).not.toThrow(); + expect(() => PdfQRCode({ value: 'a'.repeat(500) })).not.toThrow(); + expect(() => PdfQRCode({ value: 'https://example.com/path?q=hello+world&n=42' })).not.toThrow(); + }); + + it('accepts zero margin (margin-only mode)', () => { + // qrcode.tsx:42-63 — zero margin should still produce a valid matrix. + expect(() => PdfQRCode({ value: 'x', margin: 0 })).not.toThrow(); + expect(() => PdfQRCode({ value: 'x', margin: 4 })).not.toThrow(); + }); + + it('accepts color (tokens and hex) and transparent background', () => { + // qrcode.tsx:84 — 'transparent' backgroundColor is a special case that + // skips the background rect. + expect(() => PdfQRCode({ value: 'x', color: 'primary' })).not.toThrow(); + expect(() => + PdfQRCode({ value: 'x', color: '#ff00ff', backgroundColor: '#00ff00' }) + ).not.toThrow(); + expect(() => PdfQRCode({ value: 'x', backgroundColor: 'transparent' })).not.toThrow(); + }); + + it('accepts caption and style override', () => { + expect(() => + PdfQRCode({ + value: 'x', + caption: 'Scan to verify', + style: { marginTop: 8, padding: 4 }, + }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/section/section.test.tsx b/apps/www/src/registry/components/section/section.test.tsx index 649dddd0..29b49dcd 100644 --- a/apps/www/src/registry/components/section/section.test.tsx +++ b/apps/www/src/registry/components/section/section.test.tsx @@ -2,10 +2,56 @@ import { describe, expect, it } from 'vitest'; import { Section } from './section'; describe('Section', () => { - it('renders without throwing', () => { + const variants = ['default', 'callout', 'highlight', 'card'] as const; + const spacings = ['none', 'sm', 'md', 'lg', 'xl'] as const; + const paddings = ['none', 'sm', 'md', 'lg'] as const; + + it('renders with minimal props', () => { expect(() => Section({ children: 'Content' })).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => Section({ children: 'Content', variant: 'callout' })).not.toThrow(); + + it('renders with null children', () => { + // PDFComponentProps marks children as required, but null is a valid ReactNode. + expect(() => Section({ children: null })).not.toThrow(); + }); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => Section({ variant, children: 'x' })).not.toThrow(); + } + }); + + it('renders all spacing and padding presets', () => { + for (const spacing of spacings) { + expect(() => Section({ spacing, children: 'x' })).not.toThrow(); + } + for (const padding of paddings) { + expect(() => Section({ padding, children: 'x' })).not.toThrow(); + } + }); + + it('border prop is only applied on the default variant', () => { + // section.tsx:120 — border is a no-op on callout/highlight/card variants. + expect(() => Section({ border: true, children: 'x' })).not.toThrow(); + expect(() => Section({ border: true, variant: 'callout', children: 'x' })).not.toThrow(); + }); + + it('accentColor applies only to callout and highlight variants', () => { + // section.tsx:116 — accentColor is gated by variant. + expect(() => + Section({ variant: 'callout', accentColor: 'primary', children: 'x' }) + ).not.toThrow(); + expect(() => + Section({ variant: 'highlight', accentColor: '#ff0000', children: 'x' }) + ).not.toThrow(); + // Ignored on other variants, must not throw. + expect(() => Section({ variant: 'card', accentColor: 'primary', children: 'x' })).not.toThrow(); + }); + + it('accepts background (token and hex), noWrap, and style override', () => { + expect(() => Section({ background: 'muted', children: 'x' })).not.toThrow(); + expect(() => Section({ background: '#f5f5f5', children: 'x' })).not.toThrow(); + expect(() => Section({ noWrap: true, children: 'x' })).not.toThrow(); + expect(() => Section({ children: 'x', style: { marginLeft: 12 } })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/signature/signature.test.tsx b/apps/www/src/registry/components/signature/signature.test.tsx index fa61513b..f913ffeb 100644 --- a/apps/www/src/registry/components/signature/signature.test.tsx +++ b/apps/www/src/registry/components/signature/signature.test.tsx @@ -2,10 +2,58 @@ import { describe, expect, it } from 'vitest'; import { PdfSignatureBlock } from './signature'; describe('PdfSignatureBlock', () => { - it('renders without throwing', () => { + const variants = ['single', 'double', 'inline'] as const; + + it('renders with no props (uses default label)', () => { + // signature.tsx:114 — label defaults to 'Signature'. expect(() => PdfSignatureBlock({})).not.toThrow(); }); - it('accepts variant prop', () => { + + it('renders all variants with base props', () => { + for (const variant of variants) { + expect(() => PdfSignatureBlock({ variant })).not.toThrow(); + } + }); + + it('renders single with full signer details', () => { + expect(() => + PdfSignatureBlock({ + variant: 'single', + label: 'Signed by', + name: 'Ada Lovelace', + title: 'CTO', + date: '2026-01-15', + }) + ).not.toThrow(); + }); + + it('renders double with signers tuple', () => { + expect(() => + PdfSignatureBlock({ + variant: 'double', + signers: [ + { label: 'Prepared by', name: 'A', title: 'Analyst', date: '2026-01-15' }, + { label: 'Approved by', name: 'B', title: 'Director', date: '2026-01-20' }, + ], + }) + ).not.toThrow(); + }); + + it('renders double without explicit signers (uses defaults)', () => { + // signature.tsx:139 — default signers used when prop omitted. expect(() => PdfSignatureBlock({ variant: 'double' })).not.toThrow(); }); + + it('renders inline with and without name', () => { + expect(() => + PdfSignatureBlock({ variant: 'inline', label: 'Signature', name: 'Ada' }) + ).not.toThrow(); + expect(() => PdfSignatureBlock({ variant: 'inline', label: 'X' })).not.toThrow(); + }); + + it('accepts a style override on every variant', () => { + for (const variant of variants) { + expect(() => PdfSignatureBlock({ variant, style: { marginTop: 20 } })).not.toThrow(); + } + }); }); diff --git a/apps/www/src/registry/components/stack/stack.test.tsx b/apps/www/src/registry/components/stack/stack.test.tsx index f739389a..af4ce4ba 100644 --- a/apps/www/src/registry/components/stack/stack.test.tsx +++ b/apps/www/src/registry/components/stack/stack.test.tsx @@ -2,10 +2,46 @@ import { describe, expect, it } from 'vitest'; import { Stack } from './stack'; describe('Stack', () => { - it('renders without throwing', () => { + const gaps = ['none', 'sm', 'md', 'lg', 'xl'] as const; + const directions = ['vertical', 'horizontal'] as const; + const aligns = ['start', 'center', 'end', 'stretch'] as const; + const justifies = ['start', 'center', 'end', 'between', 'around'] as const; + + it('renders with minimal props', () => { expect(() => Stack({ children: 'Content' })).not.toThrow(); }); - it('accepts direction prop', () => { - expect(() => Stack({ children: 'Content', direction: 'horizontal' })).not.toThrow(); + + it('renders with empty children', () => { + expect(() => Stack({ children: null })).not.toThrow(); + expect(() => Stack({ children: [] })).not.toThrow(); + }); + + it('renders all gap sizes', () => { + for (const gap of gaps) { + expect(() => Stack({ gap, children: 'x' })).not.toThrow(); + } + }); + + it('renders both directions', () => { + for (const direction of directions) { + expect(() => Stack({ direction, children: 'x' })).not.toThrow(); + } + }); + + it('renders all alignments and justifications', () => { + for (const align of aligns) { + expect(() => Stack({ align, children: 'x' })).not.toThrow(); + } + for (const justify of justifies) { + expect(() => Stack({ justify, children: 'x' })).not.toThrow(); + } + }); + + it('accepts wrap, noWrap, and style override', () => { + expect(() => Stack({ wrap: true, children: 'x' })).not.toThrow(); + expect(() => Stack({ noWrap: true, children: 'x' })).not.toThrow(); + expect(() => + Stack({ children: 'x', style: { padding: 8, backgroundColor: '#eee' } }) + ).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/text/text.test.tsx b/apps/www/src/registry/components/text/text.test.tsx index 61d41341..78349896 100644 --- a/apps/www/src/registry/components/text/text.test.tsx +++ b/apps/www/src/registry/components/text/text.test.tsx @@ -2,10 +2,58 @@ import { describe, expect, it } from 'vitest'; import { Text } from './text'; describe('Text', () => { - it('renders without throwing', () => { + const variants = ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl'] as const; + const weights = ['normal', 'medium', 'semibold', 'bold'] as const; + const decorations = ['underline', 'line-through', 'none'] as const; + const transforms = ['uppercase', 'lowercase', 'capitalize'] as const; + const aligns = ['left', 'center', 'right', 'justify'] as const; + + it('renders with minimal props', () => { expect(() => Text({ children: 'Hello world' })).not.toThrow(); }); - it('accepts variant prop', () => { - expect(() => Text({ children: 'Small text', variant: 'sm' })).not.toThrow(); + + it('renders with empty children', () => { + expect(() => Text({ children: '' })).not.toThrow(); + }); + + it('renders all variants', () => { + for (const variant of variants) { + expect(() => Text({ variant, children: 'x' })).not.toThrow(); + } + }); + + it('renders all weights', () => { + for (const weight of weights) { + expect(() => Text({ weight, children: 'x' })).not.toThrow(); + } + }); + + it('renders all decorations', () => { + for (const decoration of decorations) { + expect(() => Text({ decoration, children: 'x' })).not.toThrow(); + } + }); + + it('renders all transforms', () => { + for (const transform of transforms) { + expect(() => Text({ transform, children: 'x' })).not.toThrow(); + } + }); + + it('renders all alignments', () => { + for (const align of aligns) { + expect(() => Text({ align, children: 'x' })).not.toThrow(); + } + }); + + it('accepts italic, noMargin, and style overrides', () => { + expect(() => Text({ italic: true, noMargin: true, children: 'x' })).not.toThrow(); + expect(() => Text({ children: 'x', style: { marginTop: 4 } })).not.toThrow(); + expect(() => Text({ children: 'x', style: { marginTop: 4, padding: 2 } })).not.toThrow(); + }); + + it('resolves color token names and raw hex', () => { + expect(() => Text({ color: 'primary', children: 'x' })).not.toThrow(); + expect(() => Text({ color: '#ff0000', children: 'x' })).not.toThrow(); }); }); diff --git a/apps/www/src/registry/components/watermark/watermark.test.tsx b/apps/www/src/registry/components/watermark/watermark.test.tsx index 4b1756a8..14834f40 100644 --- a/apps/www/src/registry/components/watermark/watermark.test.tsx +++ b/apps/www/src/registry/components/watermark/watermark.test.tsx @@ -2,10 +2,40 @@ import { describe, expect, it } from 'vitest'; import { PdfWatermark } from './watermark'; describe('PdfWatermark', () => { - it('renders without throwing', () => { + const positions = ['center', 'top-left', 'top-right', 'bottom-left', 'bottom-right'] as const; + + it('renders with only the required text', () => { expect(() => PdfWatermark({ text: 'DRAFT' })).not.toThrow(); }); - it('accepts opacity prop', () => { - expect(() => PdfWatermark({ text: 'CONFIDENTIAL', opacity: 0.3 })).not.toThrow(); + + it('renders all positions', () => { + for (const position of positions) { + expect(() => PdfWatermark({ text: 'CONFIDENTIAL', position })).not.toThrow(); + } + }); + + it('accepts opacity across the full 0..1 range', () => { + expect(() => PdfWatermark({ text: 'DRAFT', opacity: 0 })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', opacity: 0.15 })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', opacity: 1 })).not.toThrow(); + }); + + it('accepts arbitrary rotation angles', () => { + for (const angle of [0, -45, 45, 90, 180, 270]) { + expect(() => PdfWatermark({ text: 'DRAFT', angle })).not.toThrow(); + } + }); + + it('accepts custom fontSize and color (tokens and hex)', () => { + expect(() => PdfWatermark({ text: 'DRAFT', fontSize: 12 })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', fontSize: 120 })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', color: 'primary' })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', color: '#ff0000' })).not.toThrow(); + }); + + it('honors fixed toggle and style override', () => { + expect(() => PdfWatermark({ text: 'DRAFT', fixed: true })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', fixed: false })).not.toThrow(); + expect(() => PdfWatermark({ text: 'DRAFT', style: { top: 20, left: 20 } })).not.toThrow(); }); });