From ac672476365a69c5a9d78b8a6fdd0e53591fe489 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 9 Sep 2024 16:47:47 +0700 Subject: [PATCH] test: finished all ui components test (#3588) * test: finished all ui components test * chore: update naming of mock handle change modal test --- joi/package.json | 1 + joi/src/core/Button/Button.test.tsx | 36 ++++-- joi/src/core/Checkbox/Checkbox.test.tsx | 50 ++++++++ joi/src/core/Input/Input.test.tsx | 53 +++++++++ joi/src/core/Input/index.tsx | 2 +- joi/src/core/Modal/Modal.test.tsx | 78 +++++++++++++ joi/src/core/Progress/Progress.test.tsx | 55 +++++++++ joi/src/core/Progress/index.tsx | 9 +- joi/src/core/ScrollArea/ScrollArea.test.tsx | 47 ++++++++ joi/src/core/Select/Select.test.tsx | 107 +++++++++++++++++ joi/src/core/Slider/Slider.test.tsx | 65 +++++++++++ joi/src/core/Switch/Switch.test.tsx | 52 +++++++++ joi/src/core/Tabs/Tabs.test.tsx | 99 ++++++++++++++++ joi/src/core/TextArea/TextArea.test.tsx | 34 ++++++ joi/src/core/Tooltip/Tooltip.test.tsx | 121 ++++++++++++++++++++ 15 files changed, 800 insertions(+), 9 deletions(-) create mode 100644 joi/src/core/Checkbox/Checkbox.test.tsx create mode 100644 joi/src/core/Input/Input.test.tsx create mode 100644 joi/src/core/Modal/Modal.test.tsx create mode 100644 joi/src/core/Progress/Progress.test.tsx create mode 100644 joi/src/core/ScrollArea/ScrollArea.test.tsx create mode 100644 joi/src/core/Select/Select.test.tsx create mode 100644 joi/src/core/Slider/Slider.test.tsx create mode 100644 joi/src/core/Switch/Switch.test.tsx create mode 100644 joi/src/core/Tabs/Tabs.test.tsx create mode 100644 joi/src/core/TextArea/TextArea.test.tsx create mode 100644 joi/src/core/Tooltip/Tooltip.test.tsx diff --git a/joi/package.json b/joi/package.json index c336cce12f..576c33d72a 100644 --- a/joi/package.json +++ b/joi/package.json @@ -52,6 +52,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "jest-environment-jsdom": "^29.7.0", "jest-transform-css": "^6.0.1", diff --git a/joi/src/core/Button/Button.test.tsx b/joi/src/core/Button/Button.test.tsx index 3ff76143c4..a4c6797732 100644 --- a/joi/src/core/Button/Button.test.tsx +++ b/joi/src/core/Button/Button.test.tsx @@ -6,7 +6,7 @@ import { Button, buttonConfig } from './index' // Mock the styles jest.mock('./styles.scss', () => ({})) -describe('Button', () => { +describe('@joi/core/Button', () => { it('renders with default props', () => { render() const button = screen.getByRole('button', { name: /click me/i }) @@ -14,6 +14,12 @@ describe('Button', () => { expect(button).toHaveClass('btn btn--primary btn--medium btn--solid') }) + it('applies custom className', () => { + render() + const badge = screen.getByText('Test Button') + expect(badge).toHaveClass('custom-class') + }) + it('renders as a child component when asChild is true', () => { render( ) - const button = screen.getByRole('button', { name: /custom class button/i }) - expect(button).toHaveClass( - 'btn btn--primary btn--medium btn--solid custom-class' - ) + it('fails when a new theme is added without updating the test', () => { + const expectedThemes = ['primary', 'ghost', 'icon', 'destructive'] + const actualThemes = Object.keys(buttonConfig.variants.theme) + expect(actualThemes).toEqual(expectedThemes) + }) + + it('fails when a new variant is added without updating the test', () => { + const expectedVariant = ['solid', 'soft', 'outline'] + const actualVariants = Object.keys(buttonConfig.variants.variant) + expect(actualVariants).toEqual(expectedVariant) + }) + + it('fails when a new size is added without updating the test', () => { + const expectedSizes = ['small', 'medium', 'large'] + const actualSizes = Object.keys(buttonConfig.variants.size) + expect(actualSizes).toEqual(expectedSizes) + }) + + it('fails when a new variant CVA is added without updating the test', () => { + const expectedVariantsCVA = ['theme', 'variant', 'size', 'block'] + const actualVariant = Object.keys(buttonConfig.variants) + expect(actualVariant).toEqual(expectedVariantsCVA) }) }) diff --git a/joi/src/core/Checkbox/Checkbox.test.tsx b/joi/src/core/Checkbox/Checkbox.test.tsx new file mode 100644 index 0000000000..ce81132d9e --- /dev/null +++ b/joi/src/core/Checkbox/Checkbox.test.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import { Checkbox } from './index' + +// Mock the styles +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/Checkbox', () => { + it('renders correctly with label', () => { + render() + expect(screen.getByLabelText('Test Checkbox')).toBeInTheDocument() + }) + + it('renders with helper description', () => { + render() + expect(screen.getByText('Helper text')).toBeInTheDocument() + }) + + it('renders error message when provided', () => { + render() + expect(screen.getByText('Error occurred')).toBeInTheDocument() + }) + + it('calls onChange when clicked', () => { + const mockOnChange = jest.fn() + render( + + ) + + fireEvent.click(screen.getByLabelText('Test Checkbox')) + expect(mockOnChange).toHaveBeenCalledTimes(1) + }) + + it('applies custom className', () => { + render() + expect(screen.getByRole('checkbox').parentElement).toHaveClass( + 'custom-class' + ) + }) + + it('disables the checkbox when disabled prop is true', () => { + render() + expect(screen.getByLabelText('Disabled Checkbox')).toBeDisabled() + }) +}) diff --git a/joi/src/core/Input/Input.test.tsx b/joi/src/core/Input/Input.test.tsx new file mode 100644 index 0000000000..55bed74bb1 --- /dev/null +++ b/joi/src/core/Input/Input.test.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import { Input } from './index' + +// Mock the styles import +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/Input', () => { + it('renders correctly', () => { + render() + expect(screen.getByPlaceholderText('Test input')).toBeInTheDocument() + }) + + it('applies custom className', () => { + render() + expect(screen.getByRole('textbox')).toHaveClass('custom-class') + }) + + it('aligns text to the right when textAlign prop is set', () => { + render() + expect(screen.getByRole('textbox')).toHaveClass('text-right') + }) + + it('renders prefix icon when provided', () => { + render(Prefix} />) + expect(screen.getByTestId('prefix-icon')).toBeInTheDocument() + }) + + it('renders suffix icon when provided', () => { + render(Suffix} />) + expect(screen.getByTestId('suffix-icon')).toBeInTheDocument() + }) + + it('renders clear icon when clearable is true', () => { + render() + expect(screen.getByTestId('cross-2-icon')).toBeInTheDocument() + }) + + it('calls onClick when input is clicked', () => { + const onClick = jest.fn() + render() + fireEvent.click(screen.getByRole('textbox')) + expect(onClick).toHaveBeenCalledTimes(1) + }) + + it('calls onClear when clear icon is clicked', () => { + const onClear = jest.fn() + render() + fireEvent.click(screen.getByTestId('cross-2-icon')) + expect(onClear).toHaveBeenCalledTimes(1) + }) +}) diff --git a/joi/src/core/Input/index.tsx b/joi/src/core/Input/index.tsx index 99b7fe8ab3..9f5e4c663f 100644 --- a/joi/src/core/Input/index.tsx +++ b/joi/src/core/Input/index.tsx @@ -42,7 +42,7 @@ const Input = forwardRef( )} {clearable && (
- +
)} ({})) + +describe('Modal', () => { + it('renders the modal with trigger and content', () => { + render( + Open Modal} + content={
Modal Content
} + /> + ) + + expect(screen.getByText('Open Modal')).toBeInTheDocument() + fireEvent.click(screen.getByText('Open Modal')) + expect(screen.getByText('Modal Content')).toBeInTheDocument() + }) + + it('renders the modal with title', () => { + render( + Open Modal} + content={
Modal Content
} + title="Modal Title" + /> + ) + + fireEvent.click(screen.getByText('Open Modal')) + expect(screen.getByText('Modal Title')).toBeInTheDocument() + }) + + it('renders full page modal', () => { + render( + Open Modal} + content={
Modal Content
} + fullPage + /> + ) + + fireEvent.click(screen.getByText('Open Modal')) + expect(screen.getByRole('dialog')).toHaveClass('modal__content--fullpage') + }) + + it('hides close button when hideClose is true', () => { + render( + Open Modal} + content={
Modal Content
} + hideClose + /> + ) + + fireEvent.click(screen.getByText('Open Modal')) + expect(screen.queryByLabelText('Close')).not.toBeInTheDocument() + }) + + it('calls onOpenChange when opening and closing the modal', () => { + const onOpenChangeMock = jest.fn() + render( + Open Modal} + content={
Modal Content
} + onOpenChange={onOpenChangeMock} + /> + ) + + fireEvent.click(screen.getByText('Open Modal')) + expect(onOpenChangeMock).toHaveBeenCalledWith(true) + + fireEvent.click(screen.getByLabelText('Close')) + expect(onOpenChangeMock).toHaveBeenCalledWith(false) + }) +}) diff --git a/joi/src/core/Progress/Progress.test.tsx b/joi/src/core/Progress/Progress.test.tsx new file mode 100644 index 0000000000..9d18bf019f --- /dev/null +++ b/joi/src/core/Progress/Progress.test.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { Progress } from './index' + +// Mock the styles +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/Progress', () => { + it('renders with default props', () => { + render() + const progressElement = screen.getByRole('progressbar') + expect(progressElement).toBeInTheDocument() + expect(progressElement).toHaveClass('progress') + expect(progressElement).toHaveClass('progress--medium') + expect(progressElement).toHaveAttribute('aria-valuenow', '50') + }) + + it('applies custom className', () => { + render() + const progressElement = screen.getByRole('progressbar') + expect(progressElement).toHaveClass('custom-class') + }) + + it('renders with different sizes', () => { + const { rerender } = render() + let progressElement = screen.getByRole('progressbar') + expect(progressElement).toHaveClass('progress--small') + + rerender() + progressElement = screen.getByRole('progressbar') + expect(progressElement).toHaveClass('progress--large') + }) + + it('sets the correct transform style based on value', () => { + render() + const progressElement = screen.getByRole('progressbar') + const indicatorElement = progressElement.firstChild as HTMLElement + expect(indicatorElement).toHaveStyle('transform: translateX(-25%)') + }) + + it('handles edge cases for value', () => { + const { rerender } = render() + let progressElement = screen.getByRole('progressbar') + let indicatorElement = progressElement.firstChild as HTMLElement + expect(indicatorElement).toHaveStyle('transform: translateX(-100%)') + expect(progressElement).toHaveAttribute('aria-valuenow', '0') + + rerender() + progressElement = screen.getByRole('progressbar') + indicatorElement = progressElement.firstChild as HTMLElement + expect(indicatorElement).toHaveStyle('transform: translateX(-0%)') + expect(progressElement).toHaveAttribute('aria-valuenow', '100') + }) +}) diff --git a/joi/src/core/Progress/index.tsx b/joi/src/core/Progress/index.tsx index 51ea79c814..01aefbeb0d 100644 --- a/joi/src/core/Progress/index.tsx +++ b/joi/src/core/Progress/index.tsx @@ -27,7 +27,14 @@ export interface ProgressProps const Progress = ({ className, size, value, ...props }: ProgressProps) => { return ( -
+
({})) + +class ResizeObserverMock { + observe() {} + unobserve() {} + disconnect() {} +} + +global.ResizeObserver = ResizeObserverMock + +describe('@joi/core/ScrollArea', () => { + it('renders children correctly', () => { + render( + +
Test Content
+
+ ) + + const child = screen.getByTestId('child') + expect(child).toBeInTheDocument() + expect(child).toHaveTextContent('Test Content') + }) + + it('applies custom className', () => { + const { container } = render() + + const root = container.firstChild as HTMLElement + expect(root).toHaveClass('scroll-area__root') + expect(root).toHaveClass('custom-class') + }) + + it('forwards ref to the Viewport component', () => { + const ref = React.createRef() + render() + + expect(ref.current).toBeInstanceOf(HTMLDivElement) + expect(ref.current).toHaveClass('scroll-area__viewport') + }) +}) diff --git a/joi/src/core/Select/Select.test.tsx b/joi/src/core/Select/Select.test.tsx new file mode 100644 index 0000000000..1b450706b2 --- /dev/null +++ b/joi/src/core/Select/Select.test.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Select } from './index' +import '@testing-library/jest-dom' + +// Mock the styles +jest.mock('./styles.scss', () => ({})) + +jest.mock('tailwind-merge', () => ({ + twMerge: (...classes: string[]) => classes.filter(Boolean).join(' '), +})) + +const mockOnValueChange = jest.fn() +jest.mock('@radix-ui/react-select', () => ({ + Root: ({ + children, + onValueChange, + }: { + children: React.ReactNode + onValueChange?: (value: string) => void + }) => { + mockOnValueChange.mockImplementation(onValueChange) + return
{children}
+ }, + Trigger: ({ + children, + className, + }: { + children: React.ReactNode + className?: string + }) => ( + + ), + Value: ({ placeholder }: { placeholder?: string }) => ( + {placeholder} + ), + Icon: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + Portal: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Content: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Viewport: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Item: ({ children, value }: { children: React.ReactNode; value: string }) => ( +
mockOnValueChange(value)} + > + {children} +
+ ), + ItemText: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + ItemIndicator: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + Arrow: () =>
, +})) +describe('@joi/core/Select', () => { + const options = [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ] + + it('renders with placeholder', () => { + render() + expect(screen.getByTestId('select-item-option1')).toBeInTheDocument() + expect(screen.getByTestId('select-item-option2')).toBeInTheDocument() + }) + + it('calls onValueChange when an option is selected', async () => { + const user = userEvent.setup() + const onValueChange = jest.fn() + render() + expect(screen.getByTestId('select-trigger')).toHaveClass('select__disabled') + }) + + it('applies block class when block prop is true', () => { + render( + {children} +
+ ), + Track: ({ children }: any) => ( +
{children}
+ ), + Range: () =>
, + Thumb: () =>
, +})) + +describe('@joi/core/Slider', () => { + it('renders correctly with default props', () => { + render() + expect(screen.getByTestId('slider-root')).toBeInTheDocument() + expect(screen.getByTestId('slider-track')).toBeInTheDocument() + expect(screen.getByTestId('slider-range')).toBeInTheDocument() + expect(screen.getByTestId('slider-thumb')).toBeInTheDocument() + }) + + it('passes props correctly to SliderPrimitive.Root', () => { + const props = { + name: 'test-slider', + min: 0, + max: 100, + value: [50], + step: 1, + disabled: true, + } + render() + const sliderRoot = screen.getByTestId('slider-root') + expect(sliderRoot).toHaveAttribute('name', 'test-slider') + expect(sliderRoot).toHaveAttribute('min', '0') + expect(sliderRoot).toHaveAttribute('max', '100') + expect(sliderRoot).toHaveAttribute('value', '50') + expect(sliderRoot).toHaveAttribute('step', '1') + expect(sliderRoot).toHaveAttribute('disabled', '') + }) + + it('calls onValueChange when value changes', () => { + const onValueChange = jest.fn() + render() + const input = screen.getByTestId('slider-root').querySelector('input') + fireEvent.change(input!, { target: { value: '75' } }) + expect(onValueChange).toHaveBeenCalledWith([75]) + }) +}) diff --git a/joi/src/core/Switch/Switch.test.tsx b/joi/src/core/Switch/Switch.test.tsx new file mode 100644 index 0000000000..72f3d8007a --- /dev/null +++ b/joi/src/core/Switch/Switch.test.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import { Switch } from './index' + +// Mock the styles +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/Switch', () => { + it('renders correctly', () => { + const { getByRole } = render() + const checkbox = getByRole('checkbox') + expect(checkbox).toBeInTheDocument() + }) + + it('applies custom className', () => { + const { container } = render() + expect(container.firstChild).toHaveClass('switch custom-class') + }) + + it('can be checked and unchecked', () => { + const { getByRole } = render() + const checkbox = getByRole('checkbox') as HTMLInputElement + + expect(checkbox.checked).toBe(false) + fireEvent.click(checkbox) + expect(checkbox.checked).toBe(true) + fireEvent.click(checkbox) + expect(checkbox.checked).toBe(false) + }) + + it('can be disabled', () => { + const { getByRole } = render() + const checkbox = getByRole('checkbox') as HTMLInputElement + expect(checkbox).toBeDisabled() + }) + + it('calls onChange when clicked', () => { + const handleChange = jest.fn() + const { getByRole } = render() + const checkbox = getByRole('checkbox') + + fireEvent.click(checkbox) + expect(handleChange).toHaveBeenCalledTimes(1) + }) + + it('can have a default checked state', () => { + const { getByRole } = render() + const checkbox = getByRole('checkbox') as HTMLInputElement + expect(checkbox.checked).toBe(true) + }) +}) diff --git a/joi/src/core/Tabs/Tabs.test.tsx b/joi/src/core/Tabs/Tabs.test.tsx new file mode 100644 index 0000000000..b6dcf8a7b8 --- /dev/null +++ b/joi/src/core/Tabs/Tabs.test.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import { Tabs, TabsContent } from './index' + +// Mock the Tooltip component +jest.mock('../Tooltip', () => ({ + Tooltip: ({ children, content, trigger }) => ( +
+ {trigger || children} +
+ ), +})) + +// Mock the styles +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/Tabs', () => { + const mockOptions = [ + { name: 'Tab 1', value: 'tab1' }, + { name: 'Tab 2', value: 'tab2' }, + { + name: 'Tab 3', + value: 'tab3', + disabled: true, + tooltipContent: 'Disabled tab', + }, + ] + + it('renders tabs correctly', () => { + render( + {}}> + Content 1 + Content 2 + Content 3 + + ) + + expect(screen.getByText('Tab 1')).toBeInTheDocument() + expect(screen.getByText('Tab 2')).toBeInTheDocument() + expect(screen.getByText('Tab 3')).toBeInTheDocument() + expect(screen.getByText('Content 1')).toBeInTheDocument() + }) + + it('changes tab content when clicked', () => { + const { rerender } = render( + {}}> + Content 1 + Content 2 + Content 3 + + ) + + expect(screen.getByText('Content 1')).toBeInTheDocument() + expect(screen.queryByText('Content 2')).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('Tab 2')) + + // Rerender with the new value to simulate the state change + rerender( + {}}> + Content 1 + Content 2 + Content 3 + + ) + + expect(screen.queryByText('Content 1')).not.toBeInTheDocument() + expect(screen.getByText('Content 2')).toBeInTheDocument() + }) + + it('disables tab when specified', () => { + render( + {}}> + Content 1 + Content 2 + Content 3 + + ) + + expect(screen.getByText('Tab 3')).toHaveAttribute('disabled') + }) + + it('renders tooltip for disabled tab', () => { + render( + {}}> + Content 1 + Content 2 + Content 3 + + ) + + const tooltipWrapper = screen.getByTestId('mock-tooltip') + expect(tooltipWrapper).toHaveAttribute( + 'data-tooltip-content', + 'Disabled tab' + ) + }) +}) diff --git a/joi/src/core/TextArea/TextArea.test.tsx b/joi/src/core/TextArea/TextArea.test.tsx new file mode 100644 index 0000000000..8bc64010fa --- /dev/null +++ b/joi/src/core/TextArea/TextArea.test.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { TextArea } from './index' + +// Mock the styles import +jest.mock('./styles.scss', () => ({})) + +describe('@joi/core/TextArea', () => { + it('renders correctly', () => { + render(