Skip to content

Commit 4c41213

Browse files
Copilothotlong
andcommitted
Add P3.1, P3.2, P3.3 quality audit, field widget polish, and plugin robustness tests
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 2c2a55c commit 4c41213

8 files changed

Lines changed: 2410 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* P3.1 Component Quality Audit - API Consistency
11+
*
12+
* Verifies that all exported UI components follow consistent patterns:
13+
* - Accept className prop for customization
14+
* - Forward refs where applicable
15+
* - Support standard HTML attributes
16+
* - Use consistent variant/size naming via CVA
17+
*/
18+
19+
import { describe, it, expect } from 'vitest';
20+
import React from 'react';
21+
import { render, screen } from '@testing-library/react';
22+
import '@testing-library/jest-dom';
23+
24+
import {
25+
Badge,
26+
Button,
27+
Card,
28+
CardHeader,
29+
CardTitle,
30+
CardDescription,
31+
CardContent,
32+
CardFooter,
33+
Input,
34+
Label,
35+
Separator,
36+
Skeleton,
37+
Progress,
38+
Alert,
39+
AlertTitle,
40+
AlertDescription,
41+
} from '../index';
42+
43+
describe('P3.1 API Consistency Audit', () => {
44+
// ---------------------------------------------------------------
45+
// className override support
46+
// ---------------------------------------------------------------
47+
describe('className override support', () => {
48+
it('Badge accepts className', () => {
49+
const { container } = render(<Badge className="custom-badge">tag</Badge>);
50+
expect(container.firstElementChild!.className).toContain('custom-badge');
51+
});
52+
53+
it('Button accepts className', () => {
54+
render(<Button className="custom-btn">Click</Button>);
55+
const btn = screen.getByRole('button');
56+
expect(btn.className).toContain('custom-btn');
57+
});
58+
59+
it('Card accepts className', () => {
60+
const { container } = render(<Card className="custom-card">content</Card>);
61+
expect(container.firstElementChild!.className).toContain('custom-card');
62+
});
63+
64+
it('CardHeader accepts className', () => {
65+
const { container } = render(<CardHeader className="custom-hdr" />);
66+
expect(container.firstElementChild!.className).toContain('custom-hdr');
67+
});
68+
69+
it('CardTitle accepts className', () => {
70+
const { container } = render(<CardTitle className="custom-title">T</CardTitle>);
71+
expect(container.firstElementChild!.className).toContain('custom-title');
72+
});
73+
74+
it('CardDescription accepts className', () => {
75+
const { container } = render(<CardDescription className="custom-desc">D</CardDescription>);
76+
expect(container.firstElementChild!.className).toContain('custom-desc');
77+
});
78+
79+
it('CardContent accepts className', () => {
80+
const { container } = render(<CardContent className="custom-content" />);
81+
expect(container.firstElementChild!.className).toContain('custom-content');
82+
});
83+
84+
it('CardFooter accepts className', () => {
85+
const { container } = render(<CardFooter className="custom-footer" />);
86+
expect(container.firstElementChild!.className).toContain('custom-footer');
87+
});
88+
89+
it('Input accepts className', () => {
90+
render(<Input className="custom-input" data-testid="inp" />);
91+
expect(screen.getByTestId('inp').className).toContain('custom-input');
92+
});
93+
94+
it('Separator accepts className', () => {
95+
const { container } = render(<Separator className="custom-sep" />);
96+
expect(container.firstElementChild!.className).toContain('custom-sep');
97+
});
98+
99+
it('Skeleton accepts className', () => {
100+
const { container } = render(<Skeleton className="custom-skel" />);
101+
expect(container.firstElementChild!.className).toContain('custom-skel');
102+
});
103+
104+
it('Alert accepts className', () => {
105+
render(<Alert className="custom-alert">hi</Alert>);
106+
expect(screen.getByRole('alert').className).toContain('custom-alert');
107+
});
108+
});
109+
110+
// ---------------------------------------------------------------
111+
// Variant support
112+
// ---------------------------------------------------------------
113+
describe('variant support', () => {
114+
it('Badge renders default variant without explicit prop', () => {
115+
const { container } = render(<Badge>tag</Badge>);
116+
expect(container.firstElementChild!.className).toContain('bg-primary');
117+
});
118+
119+
it('Badge renders destructive variant', () => {
120+
const { container } = render(<Badge variant="destructive">err</Badge>);
121+
expect(container.firstElementChild!.className).toContain('bg-destructive');
122+
});
123+
124+
it('Badge renders outline variant', () => {
125+
const { container } = render(<Badge variant="outline">out</Badge>);
126+
expect(container.firstElementChild!.className).toContain('text-foreground');
127+
});
128+
129+
it('Button renders default variant without explicit prop', () => {
130+
render(<Button>Click</Button>);
131+
expect(screen.getByRole('button').className).toContain('bg-primary');
132+
});
133+
134+
it('Button renders destructive variant', () => {
135+
render(<Button variant="destructive">Del</Button>);
136+
expect(screen.getByRole('button').className).toContain('bg-destructive');
137+
});
138+
139+
it('Button renders ghost variant', () => {
140+
render(<Button variant="ghost">G</Button>);
141+
expect(screen.getByRole('button').className).toContain('hover:bg-accent');
142+
});
143+
144+
it('Button renders outline variant', () => {
145+
render(<Button variant="outline">O</Button>);
146+
expect(screen.getByRole('button').className).toContain('border');
147+
});
148+
149+
it('Alert renders default variant', () => {
150+
render(<Alert>info</Alert>);
151+
expect(screen.getByRole('alert').className).toContain('bg-background');
152+
});
153+
154+
it('Alert renders destructive variant', () => {
155+
render(<Alert variant="destructive">err</Alert>);
156+
expect(screen.getByRole('alert').className).toContain('border-destructive');
157+
});
158+
});
159+
160+
// ---------------------------------------------------------------
161+
// Size variants (Button)
162+
// ---------------------------------------------------------------
163+
describe('Button size variants', () => {
164+
it('renders default size', () => {
165+
render(<Button>D</Button>);
166+
expect(screen.getByRole('button').className).toContain('h-10');
167+
});
168+
169+
it('renders sm size', () => {
170+
render(<Button size="sm">S</Button>);
171+
expect(screen.getByRole('button').className).toContain('h-9');
172+
});
173+
174+
it('renders lg size', () => {
175+
render(<Button size="lg">L</Button>);
176+
expect(screen.getByRole('button').className).toContain('h-11');
177+
});
178+
179+
it('renders icon size', () => {
180+
render(<Button size="icon">I</Button>);
181+
expect(screen.getByRole('button').className).toContain('w-10');
182+
});
183+
});
184+
185+
// ---------------------------------------------------------------
186+
// HTML attribute pass-through
187+
// ---------------------------------------------------------------
188+
describe('HTML attribute pass-through', () => {
189+
it('Button supports disabled attribute', () => {
190+
render(<Button disabled>Dis</Button>);
191+
expect(screen.getByRole('button')).toBeDisabled();
192+
});
193+
194+
it('Button supports type attribute', () => {
195+
render(<Button type="submit">Sub</Button>);
196+
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
197+
});
198+
199+
it('Input supports placeholder', () => {
200+
render(<Input placeholder="Enter..." data-testid="inp" />);
201+
expect(screen.getByTestId('inp')).toHaveAttribute('placeholder', 'Enter...');
202+
});
203+
204+
it('Input supports disabled', () => {
205+
render(<Input disabled data-testid="inp" />);
206+
expect(screen.getByTestId('inp')).toBeDisabled();
207+
});
208+
209+
it('Badge supports data-* attributes', () => {
210+
const { container } = render(<Badge data-testid="b1">tag</Badge>);
211+
expect(container.querySelector('[data-testid="b1"]')).toBeInTheDocument();
212+
});
213+
});
214+
215+
// ---------------------------------------------------------------
216+
// Ref forwarding
217+
// ---------------------------------------------------------------
218+
describe('ref forwarding', () => {
219+
it('Button forwards ref', () => {
220+
const ref = React.createRef<HTMLButtonElement>();
221+
render(<Button ref={ref}>Ref</Button>);
222+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
223+
});
224+
225+
it('Input forwards ref', () => {
226+
const ref = React.createRef<HTMLInputElement>();
227+
render(<Input ref={ref} />);
228+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
229+
});
230+
231+
it('Card forwards ref', () => {
232+
const ref = React.createRef<HTMLDivElement>();
233+
render(<Card ref={ref}>C</Card>);
234+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
235+
});
236+
237+
it('Alert forwards ref', () => {
238+
const ref = React.createRef<HTMLDivElement>();
239+
render(<Alert ref={ref}>A</Alert>);
240+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
241+
});
242+
});
243+
244+
// ---------------------------------------------------------------
245+
// Composition patterns (Card sub-components)
246+
// ---------------------------------------------------------------
247+
describe('Card composition pattern', () => {
248+
it('renders full Card composition', () => {
249+
const { container } = render(
250+
<Card>
251+
<CardHeader>
252+
<CardTitle>Title</CardTitle>
253+
<CardDescription>Description</CardDescription>
254+
</CardHeader>
255+
<CardContent>Body</CardContent>
256+
<CardFooter>Footer</CardFooter>
257+
</Card>
258+
);
259+
expect(container.textContent).toContain('Title');
260+
expect(container.textContent).toContain('Description');
261+
expect(container.textContent).toContain('Body');
262+
expect(container.textContent).toContain('Footer');
263+
});
264+
});
265+
266+
// ---------------------------------------------------------------
267+
// Alert composition pattern
268+
// ---------------------------------------------------------------
269+
describe('Alert composition pattern', () => {
270+
it('renders Alert with title and description', () => {
271+
render(
272+
<Alert>
273+
<AlertTitle>Heads up!</AlertTitle>
274+
<AlertDescription>You can add components.</AlertDescription>
275+
</Alert>
276+
);
277+
expect(screen.getByRole('alert')).toBeInTheDocument();
278+
expect(screen.getByText('Heads up!')).toBeInTheDocument();
279+
expect(screen.getByText('You can add components.')).toBeInTheDocument();
280+
});
281+
});
282+
283+
// ---------------------------------------------------------------
284+
// Consistent defaults
285+
// ---------------------------------------------------------------
286+
describe('consistent defaults', () => {
287+
it('Separator defaults to horizontal orientation', () => {
288+
const { container } = render(<Separator />);
289+
const sep = container.firstElementChild!;
290+
expect(sep.getAttribute('data-orientation')).toBe('horizontal');
291+
});
292+
293+
it('Separator can be vertical', () => {
294+
const { container } = render(<Separator orientation="vertical" />);
295+
const sep = container.firstElementChild!;
296+
expect(sep.getAttribute('data-orientation')).toBe('vertical');
297+
});
298+
299+
it('Progress renders with zero value by default', () => {
300+
render(<Progress data-testid="prog" />);
301+
const prog = screen.getByTestId('prog');
302+
expect(prog).toBeInTheDocument();
303+
});
304+
305+
it('Progress accepts value prop', () => {
306+
render(<Progress value={50} data-testid="prog" />);
307+
expect(screen.getByTestId('prog')).toBeInTheDocument();
308+
});
309+
310+
it('Skeleton renders as a div', () => {
311+
const { container } = render(<Skeleton />);
312+
expect(container.firstElementChild!.tagName).toBe('DIV');
313+
expect(container.firstElementChild!.className).toContain('animate-pulse');
314+
});
315+
});
316+
});

0 commit comments

Comments
 (0)