Skip to content

Commit ad848c7

Browse files
authored
Merge branch 'main' into fix/eslint-warning
2 parents 624d56d + 1d76b2f commit ad848c7

File tree

19 files changed

+1193
-6
lines changed

19 files changed

+1193
-6
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@radui/ui",
3-
"version": "0.0.44",
3+
"version": "0.0.45",
44
"description": "",
55
"main": "dist",
66
"type": "module",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from 'react';
2+
import { render, fireEvent } from '@testing-library/react';
3+
import Checkbox from '../Checkbox';
4+
5+
const TickIcon = () => (
6+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
7+
<path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd" />
8+
</svg>
9+
);
10+
11+
describe('Checkbox', () => {
12+
it('renders and toggles (uncontrolled)', () => {
13+
const { container, queryByText } = render(
14+
<Checkbox.Root defaultChecked={false} className="test-class">
15+
<Checkbox.Indicator>
16+
<span>Checked</span>
17+
</Checkbox.Indicator>
18+
</Checkbox.Root>
19+
);
20+
const button = container.querySelector('button');
21+
expect(button).toHaveAttribute('aria-checked', 'false');
22+
expect(queryByText('Checked')).toBeNull();
23+
fireEvent.click(button!);
24+
expect(button).toHaveAttribute('aria-checked', 'true');
25+
expect(queryByText('Checked')).toBeInTheDocument();
26+
fireEvent.click(button!);
27+
expect(button).toHaveAttribute('aria-checked', 'false');
28+
});
29+
30+
it('supports controlled checked prop', () => {
31+
const onCheckedChange = jest.fn();
32+
const { container, queryByText, rerender } = render(
33+
<Checkbox.Root checked={false} onCheckedChange={onCheckedChange}>
34+
<Checkbox.Indicator>
35+
<span>Checked</span>
36+
</Checkbox.Indicator>
37+
</Checkbox.Root>
38+
);
39+
const button = container.querySelector('button');
40+
expect(button).toHaveAttribute('aria-checked', 'false');
41+
expect(queryByText('Checked')).toBeNull();
42+
fireEvent.click(button!);
43+
expect(onCheckedChange).toHaveBeenCalled();
44+
rerender(
45+
<Checkbox.Root checked={true} onCheckedChange={onCheckedChange}>
46+
<Checkbox.Indicator>
47+
<span>Checked</span>
48+
</Checkbox.Indicator>
49+
</Checkbox.Root>
50+
);
51+
expect(button).toHaveAttribute('aria-checked', 'true');
52+
expect(queryByText('Checked')).toBeInTheDocument();
53+
});
54+
55+
it('applies disabled and required props', () => {
56+
const { container } = render(
57+
<Checkbox.Root disabled required>
58+
<Checkbox.Indicator>
59+
<span>Checked</span>
60+
</Checkbox.Indicator>
61+
</Checkbox.Root>
62+
);
63+
const button = container.querySelector('button');
64+
expect(button).toBeDisabled();
65+
expect(button).toHaveAttribute('aria-required', 'true');
66+
});
67+
68+
it('passes name and value to the hidden input for form usage', () => {
69+
const { container } = render(
70+
<form>
71+
<Checkbox.Root name="testName" value="testValue" defaultChecked>
72+
<Checkbox.Indicator>
73+
<TickIcon />
74+
</Checkbox.Indicator>
75+
</Checkbox.Root>
76+
</form>
77+
);
78+
const input = container.querySelector('input[type="checkbox"]');
79+
expect(input).toHaveAttribute('name', 'testName');
80+
expect(input).toHaveAttribute('value', 'testValue');
81+
expect(input).toBeChecked();
82+
});
83+
84+
it('integrates with forms and submits checked value', () => {
85+
let submittedData: Record<string, FormDataEntryValue> | undefined;
86+
const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {
87+
e.preventDefault();
88+
const formData = new window.FormData(e.currentTarget);
89+
submittedData = Object.fromEntries(Array.from((formData as any).entries()));
90+
});
91+
const { container } = render(
92+
<form onSubmit={handleSubmit}>
93+
<Checkbox.Root name="accept" value="yes" required>
94+
<Checkbox.Indicator>
95+
<TickIcon />
96+
</Checkbox.Indicator>
97+
</Checkbox.Root>
98+
<button type="submit">Submit</button>
99+
</form>
100+
);
101+
const button = container.querySelector('button');
102+
fireEvent.click(button!); // check it
103+
const submitButton = container.querySelector('button[type="submit"]');
104+
if (submitButton) {
105+
fireEvent.click(submitButton);
106+
}
107+
expect(handleSubmit).toHaveBeenCalled();
108+
expect(submittedData).toEqual({ accept: 'yes' });
109+
});
110+
111+
it('applies className to the trigger', () => {
112+
const { container } = render(
113+
<Checkbox.Root className="my-checkbox">
114+
<Checkbox.Indicator>
115+
<TickIcon />
116+
</Checkbox.Indicator>
117+
</Checkbox.Root>
118+
);
119+
const button = container.querySelector('button');
120+
expect(button).toHaveClass('my-checkbox');
121+
});
122+
123+
it('warns and renders null on direct Checkbox usage', () => {
124+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
125+
const { container } = render(<Checkbox />);
126+
expect(warnSpy).toHaveBeenCalledWith(
127+
'Direct usage of Checkbox is not supported. Please use Checkbox.Root, Checkbox.Indicator instead.'
128+
);
129+
expect(container.firstChild).toBeNull();
130+
warnSpy.mockRestore();
131+
});
132+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import CheckboxCards from '../CheckboxCards';
4+
5+
describe('CheckboxCards', () => {
6+
it('renders items and content, and toggles checked state (uncontrolled)', () => {
7+
render(
8+
<CheckboxCards.Root name="fruits" defaultValue={['apple']}>
9+
<CheckboxCards.Item value="apple">
10+
Apple
11+
<CheckboxCards.Content>
12+
<CheckboxCards.Indicator />
13+
</CheckboxCards.Content>
14+
</CheckboxCards.Item>
15+
<CheckboxCards.Item value="banana">
16+
Banana
17+
<CheckboxCards.Content>
18+
<CheckboxCards.Indicator />
19+
</CheckboxCards.Content>
20+
</CheckboxCards.Item>
21+
</CheckboxCards.Root>
22+
);
23+
24+
// Apple is checked, Banana is not
25+
const appleCheckbox = screen.getByText('Apple').parentElement?.querySelector('[role="checkbox"]');
26+
const bananaCheckbox = screen.getByText('Banana').parentElement?.querySelector('[role="checkbox"]');
27+
expect(appleCheckbox).toHaveAttribute('aria-checked', 'true');
28+
expect(bananaCheckbox).toHaveAttribute('aria-checked', 'false');
29+
30+
// Click Banana to check it
31+
if (bananaCheckbox) fireEvent.click(bananaCheckbox);
32+
expect(bananaCheckbox).toHaveAttribute('aria-checked', 'true');
33+
34+
// Click Apple to uncheck it
35+
if (appleCheckbox) fireEvent.click(appleCheckbox);
36+
expect(appleCheckbox).toHaveAttribute('aria-checked', 'false');
37+
});
38+
39+
it('supports controlled usage', () => {
40+
const handleChange = jest.fn();
41+
const value = ['banana'];
42+
render(
43+
<CheckboxCards.Root name="fruits" value={value} onValueChange={handleChange}>
44+
<CheckboxCards.Item value="apple">
45+
Apple
46+
<CheckboxCards.Content>
47+
<CheckboxCards.Indicator />
48+
</CheckboxCards.Content>
49+
</CheckboxCards.Item>
50+
<CheckboxCards.Item value="banana">
51+
Banana
52+
<CheckboxCards.Content>
53+
<CheckboxCards.Indicator />
54+
</CheckboxCards.Content>
55+
</CheckboxCards.Item>
56+
</CheckboxCards.Root>
57+
);
58+
// Only banana is checked
59+
const checkboxes = screen.getAllByRole('checkbox');
60+
expect(checkboxes[0]).toHaveAttribute('aria-checked', 'false');
61+
expect(checkboxes[1]).toHaveAttribute('aria-checked', 'true');
62+
// Click apple
63+
fireEvent.click(checkboxes[0]);
64+
expect(handleChange).toHaveBeenCalledWith(['banana', 'apple']);
65+
// Click banana
66+
fireEvent.click(checkboxes[1]);
67+
expect(handleChange).toHaveBeenCalledWith([]);
68+
});
69+
70+
it('respects disabled and required props', () => {
71+
render(
72+
<CheckboxCards.Root name="fruits" required disabled>
73+
<CheckboxCards.Item value="apple">
74+
Apple
75+
<CheckboxCards.Content>
76+
<CheckboxCards.Indicator />
77+
</CheckboxCards.Content>
78+
</CheckboxCards.Item>
79+
</CheckboxCards.Root>
80+
);
81+
const checkbox = screen.getByRole('checkbox');
82+
expect(checkbox).toBeDisabled();
83+
expect(checkbox).toHaveAttribute('aria-required', 'true');
84+
// The hidden input should also be disabled and required
85+
const input = checkbox.parentElement?.parentElement?.querySelector('input[type="checkbox"]');
86+
expect(input).toBeDisabled();
87+
expect(input).toBeRequired();
88+
});
89+
90+
it('works in a form and submits checked values', () => {
91+
let entries: [string, FormDataEntryValue][] = [];
92+
const handleSubmit = jest.fn((e: React.FormEvent<HTMLFormElement>) => {
93+
e.preventDefault();
94+
const formData = new FormData(e.target as HTMLFormElement);
95+
// @ts-ignore
96+
entries = Array.from(formData.entries());
97+
});
98+
render(
99+
<form onSubmit={handleSubmit}>
100+
<CheckboxCards.Root name="fruits" defaultValue={['apple']}>
101+
<CheckboxCards.Item value="apple">
102+
Apple
103+
<CheckboxCards.Content>
104+
<CheckboxCards.Indicator />
105+
</CheckboxCards.Content>
106+
</CheckboxCards.Item>
107+
<CheckboxCards.Item value="banana">
108+
Banana
109+
<CheckboxCards.Content>
110+
<CheckboxCards.Indicator />
111+
</CheckboxCards.Content>
112+
</CheckboxCards.Item>
113+
</CheckboxCards.Root>
114+
<button type="submit">Submit</button>
115+
</form>
116+
);
117+
const bananaCheckbox = screen.getByText('Banana').parentElement?.querySelector('[role="checkbox"]');
118+
if (bananaCheckbox) fireEvent.click(bananaCheckbox);
119+
fireEvent.click(screen.getByText('Submit'));
120+
// The form should submit both apple and banana
121+
expect(handleSubmit).toHaveBeenCalled();
122+
const values = entries.filter(([key]) => key === 'fruits').map(([, value]) => value);
123+
expect(values).toEqual(expect.arrayContaining(['apple', 'banana']));
124+
});
125+
126+
it('CheckboxCards itself renders null and warns', () => {
127+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
128+
const { container } = render(<CheckboxCards />);
129+
expect(container).toBeEmptyDOMElement();
130+
expect(warn).toHaveBeenCalledWith(
131+
'Direct usage of CheckboxCards is not supported. Please use CheckboxCards.Root, CheckboxCards.Item instead.'
132+
);
133+
warn.mockRestore();
134+
});
135+
});

0 commit comments

Comments
 (0)