Skip to content

Commit 0e43eb6

Browse files
authored
Migrate Button, Radio, and DropdownMenu to ShadCN and Add Tests +3 issues resolved (#1709)
* Implementing Checkbox.tsx * simplifying and containing Checkbox.tsx * implementing SearchBar.tsx * adjusting border color * adding cypress test file for checkbox * added a test file for search bar * fixing issue #1279 * fixing spacing * spacing adjustments * mobile adjustment * last spacing adjustments * format adjustments * adding prop types for format fixing * adjusting the cypress test for searchbar * small format fix * keeping original width * adding active filters counters * implementing ShadCN button * implememnt radio.tsx using radioGroup from shadcn * adding hover effects to the sidbar sections * adjusting hover effect color * adapt DropdownMenu to shadcn * creating test files for the adapted components * adjusting dependencies * format adjustments * format adjustments * final adjustments * improving test coverege * fixing filtring logic and checkbox visuals * format adjustments * adding tests for icon placment and size * fixing linting issues
1 parent 547dcf2 commit 0e43eb6

18 files changed

+766
-133
lines changed

components/ui/checkbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function Checkbox({
2525
>
2626
<CheckboxPrimitive.Indicator
2727
data-slot='checkbox-indicator'
28-
className='flex items-center justify-center text-current transition-none'
28+
className='flex items-center justify-center text-current transition-none w-full h-full'
2929
>
3030
<CheckIcon className='size-3.5' />
3131
</CheckboxPrimitive.Indicator>

components/ui/collapsible.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-disable linebreak-style */
2+
/* eslint-disable react/react-in-jsx-scope */
3+
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
4+
5+
function Collapsible({
6+
...props
7+
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8+
return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
9+
}
10+
11+
function CollapsibleTrigger({
12+
...props
13+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14+
return (
15+
<CollapsiblePrimitive.CollapsibleTrigger
16+
data-slot='collapsible-trigger'
17+
{...props}
18+
/>
19+
);
20+
}
21+
22+
function CollapsibleContent({
23+
...props
24+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25+
return (
26+
<CollapsiblePrimitive.CollapsibleContent
27+
data-slot='collapsible-content'
28+
{...props}
29+
/>
30+
);
31+
}
32+
33+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

components/ui/radio-group.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable linebreak-style */
2+
/* eslint-disable react/prop-types */
3+
import * as React from 'react';
4+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
5+
import { CircleIcon } from 'lucide-react';
6+
7+
import { cn } from '@/lib/utils';
8+
9+
function RadioGroup({
10+
className,
11+
...props
12+
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13+
return (
14+
<RadioGroupPrimitive.Root
15+
data-slot='radio-group'
16+
className={cn('grid gap-3', className)}
17+
{...props}
18+
/>
19+
);
20+
}
21+
22+
function RadioGroupItem({
23+
className,
24+
...props
25+
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26+
return (
27+
<RadioGroupPrimitive.Item
28+
data-slot='radio-group-item'
29+
className={cn(
30+
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
31+
className,
32+
)}
33+
{...props}
34+
>
35+
<RadioGroupPrimitive.Indicator
36+
data-slot='radio-group-indicator'
37+
className='relative flex items-center justify-center'
38+
>
39+
<CircleIcon className='fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2' />
40+
</RadioGroupPrimitive.Indicator>
41+
</RadioGroupPrimitive.Item>
42+
);
43+
}
44+
45+
export { RadioGroup, RadioGroupItem };

cypress/components/Button.cy.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* eslint-disable linebreak-style */
2+
import React from 'react';
3+
import { Button } from '@/components/ui/button';
4+
5+
describe('Button Component', () => {
6+
it('renders with default variant and size', () => {
7+
cy.mount(<Button>Click me</Button>);
8+
9+
cy.get('button')
10+
.should('have.class', 'bg-primary')
11+
.and('have.class', 'h-9')
12+
.and('have.class', 'px-4')
13+
.and('have.class', 'py-2');
14+
});
15+
16+
it('renders with different variants', () => {
17+
cy.mount(
18+
<div className='flex gap-2'>
19+
<Button variant='default'>Default</Button>
20+
<Button variant='destructive'>Destructive</Button>
21+
<Button variant='outline'>Outline</Button>
22+
<Button variant='secondary'>Secondary</Button>
23+
<Button variant='ghost'>Ghost</Button>
24+
<Button variant='link'>Link</Button>
25+
</div>,
26+
);
27+
28+
cy.get('button').eq(0).should('have.class', 'bg-primary');
29+
cy.get('button').eq(1).should('have.class', 'bg-destructive');
30+
cy.get('button').eq(2).should('have.class', 'border');
31+
cy.get('button').eq(3).should('have.class', 'bg-secondary');
32+
cy.get('button').eq(4).should('have.class', 'hover:bg-accent');
33+
cy.get('button').eq(5).should('have.class', 'text-primary');
34+
});
35+
36+
it('renders with different sizes', () => {
37+
cy.mount(
38+
<div className='flex gap-2'>
39+
<Button size='default'>Default</Button>
40+
<Button size='sm'>Small</Button>
41+
<Button size='lg'>Large</Button>
42+
<Button size='icon'>Icon</Button>
43+
</div>,
44+
);
45+
46+
cy.get('button').eq(0).should('have.class', 'h-9');
47+
cy.get('button').eq(1).should('have.class', 'h-8');
48+
cy.get('button').eq(2).should('have.class', 'h-10');
49+
cy.get('button').eq(3).should('have.class', 'size-9');
50+
});
51+
52+
it('handles disabled state', () => {
53+
cy.mount(<Button disabled>Disabled Button</Button>);
54+
55+
cy.get('button')
56+
.should('be.disabled')
57+
.and('have.class', 'disabled:opacity-50')
58+
.and('have.class', 'disabled:pointer-events-none');
59+
});
60+
61+
it('renders with icon', () => {
62+
cy.mount(
63+
<Button>
64+
<svg data-testid='test-icon' />
65+
Button with Icon
66+
</Button>,
67+
);
68+
69+
cy.get('button').should('have.class', 'has-[>svg]:px-3');
70+
cy.get('[data-testid="test-icon"]').should('exist');
71+
});
72+
73+
it('applies custom className', () => {
74+
cy.mount(<Button className='custom-class'>Custom Button</Button>);
75+
76+
cy.get('button').should('have.class', 'custom-class');
77+
});
78+
79+
it('handles click events', () => {
80+
const onClickSpy = cy.spy().as('onClickSpy');
81+
82+
cy.mount(<Button onClick={onClickSpy}>Click me</Button>);
83+
84+
cy.get('button').click();
85+
cy.get('@onClickSpy').should('have.been.calledOnce');
86+
});
87+
88+
it('renders as child component when asChild is true', () => {
89+
cy.mount(
90+
<Button asChild>
91+
<a href='#test'>Link Button</a>
92+
</Button>,
93+
);
94+
95+
cy.get('a')
96+
.should('have.attr', 'href', '#test')
97+
.and('have.class', 'bg-primary');
98+
});
99+
});

cypress/components/Checkbox.cy.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,35 @@ describe('Checkbox Component', () => {
6666
.and('have.class', 'data-[state=checked]:border-blue-500')
6767
.and('have.class', 'data-[state=checked]:text-white');
6868
});
69+
70+
it('has properly centered icon when checked', () => {
71+
cy.mount(
72+
<Checkbox
73+
label='Test Checkbox'
74+
value='test'
75+
name='test-checkbox'
76+
checked={true}
77+
/>,
78+
);
79+
80+
// Check that the indicator container has proper centering classes
81+
cy.get('[data-slot="checkbox-indicator"]')
82+
.should('have.class', 'flex')
83+
.and('have.class', 'items-center')
84+
.and('have.class', 'justify-center')
85+
.and('have.class', 'w-full')
86+
.and('have.class', 'h-full');
87+
88+
// Check that the icon exists and has proper sizing
89+
cy.get('[data-slot="checkbox-indicator"] svg')
90+
.should('exist')
91+
.and('have.class', 'size-3.5');
92+
93+
// Verify the checkbox has the correct dimensions for centering
94+
cy.get('button[role="checkbox"]')
95+
.should('have.class', 'size-4')
96+
.and('have.class', 'rounded-[4px]');
97+
});
6998
});
7099

71100
describe('Dark Mode', () => {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import React from 'react';
3+
import DropdownMenu from '@/pages/tools/components/ui/DropdownMenu';
4+
import mockNextRouter, { MockRouter } from '../plugins/mockNextRouterUtils';
5+
6+
describe('DropdownMenu Component', () => {
7+
let mockRouter: MockRouter;
8+
const mockIcon = <svg data-testid='test-icon' />;
9+
const mockChildren = <div data-testid='test-content'>Test Content</div>;
10+
11+
beforeEach(() => {
12+
mockRouter = mockNextRouter();
13+
});
14+
15+
it('renders with basic props', () => {
16+
cy.mount(
17+
<DropdownMenu label='Test Menu' icon={mockIcon} testMode={true}>
18+
{mockChildren}
19+
</DropdownMenu>,
20+
);
21+
22+
cy.contains('Test Menu').should('be.visible');
23+
cy.get('[data-testid="test-icon"]').should('exist');
24+
});
25+
26+
it('shows content when clicked', () => {
27+
cy.mount(
28+
<DropdownMenu label='Test Menu' icon={mockIcon} testMode={true}>
29+
{mockChildren}
30+
</DropdownMenu>,
31+
);
32+
33+
cy.get('button').click();
34+
cy.get('[data-testid="test-content"]').should('be.visible');
35+
});
36+
37+
it('displays count badge when count is provided', () => {
38+
cy.mount(
39+
<DropdownMenu label='Test Menu' icon={mockIcon} count={5} testMode={true}>
40+
{mockChildren}
41+
</DropdownMenu>,
42+
);
43+
44+
cy.contains('5').should('be.visible');
45+
});
46+
47+
it('does not show count badge when count is 0', () => {
48+
cy.mount(
49+
<DropdownMenu label='Test Menu' icon={mockIcon} count={0} testMode={true}>
50+
{mockChildren}
51+
</DropdownMenu>,
52+
);
53+
54+
cy.contains('0').should('not.exist');
55+
});
56+
57+
it('rotates arrow icon when dropdown is toggled', () => {
58+
cy.mount(
59+
<DropdownMenu label='Test Menu' icon={mockIcon} testMode={true}>
60+
{mockChildren}
61+
</DropdownMenu>,
62+
);
63+
64+
// Initially arrow should point down
65+
cy.get('#arrow')
66+
.should('have.attr', 'style')
67+
.and('include', 'rotate(0deg)');
68+
69+
// Click to open
70+
cy.get('button').click();
71+
cy.get('#arrow')
72+
.should('have.attr', 'style')
73+
.and('include', 'rotate(180deg)');
74+
75+
// Click to close
76+
cy.get('button').click();
77+
cy.get('#arrow')
78+
.should('have.attr', 'style')
79+
.and('include', 'rotate(0deg)');
80+
});
81+
82+
it('toggles content visibility multiple times', () => {
83+
cy.mount(
84+
<DropdownMenu label='Test Menu' icon={mockIcon} testMode={true}>
85+
{mockChildren}
86+
</DropdownMenu>,
87+
);
88+
89+
// First toggle
90+
cy.get('button').click();
91+
cy.get('[data-testid="test-content"]').should('be.visible');
92+
93+
// Second toggle
94+
cy.get('button').click();
95+
cy.get('[data-testid="test-content"]').should('not.exist');
96+
97+
// Third toggle
98+
cy.get('button').click();
99+
cy.get('[data-testid="test-content"]').should('be.visible');
100+
});
101+
});

0 commit comments

Comments
 (0)