Skip to content

Commit 1ccd01b

Browse files
committed
radio tests
1 parent c8b1a02 commit 1ccd01b

File tree

4 files changed

+185
-3
lines changed

4 files changed

+185
-3
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import RadioCards from '../RadioCards';
4+
5+
describe('RadioCards', () => {
6+
const options = [
7+
{ id: 'config-1', value: '8-core CPU', label: '8-core CPU' },
8+
{ id: 'config-2', value: '16-core CPU', label: '16-core CPU' },
9+
{ id: 'config-3', value: '32-core CPU', label: '32-core CPU' }
10+
];
11+
12+
function renderRadioCards(props = {}) {
13+
return render(
14+
<RadioCards.Root name="test-group" {...props}>
15+
{options.map((option) => (
16+
<RadioCards.Item key={option.id} value={option.value} data-testid={`radio-item-${option.value}`}>
17+
{option.label}
18+
</RadioCards.Item>
19+
))}
20+
</RadioCards.Root>
21+
);
22+
}
23+
24+
it('renders all radio items with correct labels', () => {
25+
renderRadioCards();
26+
options.forEach((option) => {
27+
expect(screen.getByText(option.label)).toBeInTheDocument();
28+
});
29+
});
30+
31+
it('selects an item when clicked and only one is selected at a time', () => {
32+
renderRadioCards();
33+
const first = screen.getByTestId('radio-item-8-core CPU');
34+
const second = screen.getByTestId('radio-item-16-core CPU');
35+
const third = screen.getByTestId('radio-item-32-core CPU');
36+
37+
// Initially none selected
38+
expect(first).toHaveAttribute('aria-checked', 'false');
39+
expect(second).toHaveAttribute('aria-checked', 'false');
40+
expect(third).toHaveAttribute('aria-checked', 'false');
41+
42+
// Click first
43+
fireEvent.click(first);
44+
expect(first).toHaveAttribute('aria-checked', 'true');
45+
expect(second).toHaveAttribute('aria-checked', 'false');
46+
expect(third).toHaveAttribute('aria-checked', 'false');
47+
48+
// Click second
49+
fireEvent.click(second);
50+
expect(first).toHaveAttribute('aria-checked', 'false');
51+
expect(second).toHaveAttribute('aria-checked', 'true');
52+
expect(third).toHaveAttribute('aria-checked', 'false');
53+
});
54+
55+
it('supports defaultValue prop', () => {
56+
renderRadioCards({ defaultValue: '16-core CPU' });
57+
const second = screen.getByTestId('radio-item-16-core CPU');
58+
expect(second).toHaveAttribute('aria-checked', 'true');
59+
});
60+
61+
it('calls onValueChange when selection changes', () => {
62+
const handleChange = jest.fn();
63+
renderRadioCards({ onValueChange: handleChange });
64+
const third = screen.getByTestId('radio-item-32-core CPU');
65+
fireEvent.click(third);
66+
expect(handleChange).toHaveBeenCalledWith('32-core CPU');
67+
});
68+
69+
it('applies disabled prop to all items', () => {
70+
renderRadioCards({ disabled: true });
71+
options.forEach((option) => {
72+
const item = screen.getByTestId(`radio-item-${option.value}`);
73+
expect(item).toHaveAttribute('aria-disabled', 'true');
74+
});
75+
});
76+
77+
it('applies custom className to root', () => {
78+
const { container } = renderRadioCards({ className: 'custom-root' });
79+
expect(container.firstChild).toHaveClass('custom-root');
80+
});
81+
});

src/components/ui/RadioGroup/fragments/RadioGroupLabel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type RadioGroupLabelProps = {
1111

1212
const RadioGroupLabel = ({ className = '', asChild = false, children, ...props }: RadioGroupLabelProps) => {
1313
const { rootClass } = React.useContext(RadioGroupContext);
14-
return <Primitive.label {...props} className={clsx(`${rootClass}-label`, rootClass, className)} asChild={asChild}> {children} </Primitive.label>;
14+
return <Primitive.label {...props} className={clsx(`${rootClass}-label`, className)} asChild={asChild}> {children} </Primitive.label>;
1515
};
1616

1717
export default RadioGroupLabel;

src/components/ui/RadioGroup/fragments/RadioGroupRoot.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ type RadioGroupRootProps = {
2020

2121
} & RadioGroupPrimitiveProps.Root;
2222

23-
const RadioGroupRoot = ({ children, className = '', customRootClass = '', variant = '', size = '', color = '', ...props }: RadioGroupRootProps) => {
23+
const RadioGroupRoot = ({ children, className = '', customRootClass = '', variant, size, color = '', ...props }: RadioGroupRootProps) => {
2424
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
2525
const dataAttributes = useCreateDataAttribute('radio-group', { variant, size });
2626

2727
const accentAttributes = useCreateDataAccentColorAttribute(color);
2828
const composedAttributes = useComposeAttributes(dataAttributes(), accentAttributes());
2929

3030
return <RadioGroupContext.Provider value={{ rootClass }}>
31-
<RadioGroupPrimitive.Root className={clsx(`${rootClass}-root`, rootClass, className)} {...composedAttributes} {...props}> {children} </RadioGroupPrimitive.Root>
31+
<RadioGroupPrimitive.Root className={clsx(`${rootClass}-root`, className)} {...composedAttributes()} {...props}> {children} </RadioGroupPrimitive.Root>
3232
</RadioGroupContext.Provider>;
3333
};
3434

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import RadioGroup from '../RadioGroup';
5+
6+
// Helper for the story pattern
7+
const options = [
8+
{ id: 'html', value: 'html', label: 'HTML' },
9+
{ id: 'css', value: 'css', label: 'CSS' },
10+
{ id: 'javascript', value: 'javascript', label: 'JavaScript' }
11+
];
12+
13+
// Explicitly type props to allow all RadioGroupRoot props, but make children optional
14+
function StoryRadioGroup(props: Omit<React.ComponentProps<typeof RadioGroup.Root>, 'children'> & { children?: React.ReactNode }) {
15+
return (
16+
<RadioGroup.Root {...props}>
17+
{props.children ??
18+
options.map((option) => (
19+
<RadioGroup.Label key={option.id} data-testid={`label-${option.value}`}>
20+
<RadioGroup.Item value={option.value} data-testid={`item-${option.value}`}>
21+
<RadioGroup.Indicator data-testid={`indicator-${option.value}`} />
22+
</RadioGroup.Item>
23+
{option.label}
24+
</RadioGroup.Label>
25+
))}
26+
</RadioGroup.Root>
27+
);
28+
}
29+
30+
describe('RadioGroup (fragments)', () => {
31+
it('renders without crashing (story pattern)', () => {
32+
render(<StoryRadioGroup />);
33+
options.forEach(opt => {
34+
expect(screen.getByText(opt.label)).toBeInTheDocument();
35+
expect(screen.getByTestId(`item-${opt.value}`)).toBeInTheDocument();
36+
});
37+
});
38+
39+
it('selects the correct radio on click (uncontrolled)', () => {
40+
render(<StoryRadioGroup defaultValue="css" name="test-group" />);
41+
const itemHtml = screen.getByTestId('item-html');
42+
const itemCss = screen.getByTestId('item-css');
43+
const itemJs = screen.getByTestId('item-javascript');
44+
// Only CSS is selected initially
45+
expect(itemHtml).toHaveAttribute('aria-checked', 'false');
46+
expect(itemCss).toHaveAttribute('aria-checked', 'true');
47+
expect(itemJs).toHaveAttribute('aria-checked', 'false');
48+
// Click HTML
49+
fireEvent.click(itemHtml);
50+
expect(itemHtml).toHaveAttribute('aria-checked', 'true');
51+
expect(itemCss).toHaveAttribute('aria-checked', 'false');
52+
expect(itemJs).toHaveAttribute('aria-checked', 'false');
53+
});
54+
55+
it('shows indicator only for selected item', () => {
56+
render(<StoryRadioGroup defaultValue="javascript" />);
57+
options.forEach(opt => {
58+
const indicator = screen.queryByTestId(`indicator-${opt.value}`);
59+
if (opt.value === 'javascript') {
60+
expect(indicator).toBeInTheDocument();
61+
// Optionally: expect(indicator).not.toBeEmptyDOMElement();
62+
} else {
63+
expect(indicator).not.toBeInTheDocument();
64+
}
65+
});
66+
});
67+
68+
it('renders label text and associates with item', () => {
69+
render(<StoryRadioGroup />);
70+
options.forEach(opt => {
71+
const label = screen.getByTestId(`label-${opt.value}`);
72+
expect(label).toHaveTextContent(opt.label);
73+
// The item is a button inside the label
74+
const item = screen.getByTestId(`item-${opt.value}`);
75+
expect(label).toContainElement(item);
76+
});
77+
});
78+
79+
it('passes custom className, variant, size, color to root', () => {
80+
render(
81+
<RadioGroup.Root className="custom-class" data-testid="radio-group" variant="filled" size="lg" color="primary">
82+
<RadioGroup.Item value="a">A</RadioGroup.Item>
83+
</RadioGroup.Root>
84+
);
85+
const group = screen.getByTestId('radio-group');
86+
console.log('group:', group);
87+
// Data attributes for variant/size/color
88+
expect(group.getAttribute('data-radio-group-variant')).toBe('filled');
89+
expect(group.getAttribute('data-radio-group-size')).toBe('lg');
90+
expect(group.getAttribute('data-rad-ui-accent-color')).toBe('primary');
91+
});
92+
93+
it('warns on direct usage of RadioGroup', () => {
94+
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
95+
render(<RadioGroup />);
96+
expect(spy).toHaveBeenCalledWith(
97+
'Direct usage of RadioGroup is not supported. Please use RadioGroup.Root and RadioGroup.Item instead.'
98+
);
99+
spy.mockRestore();
100+
});
101+
});

0 commit comments

Comments
 (0)