diff --git a/joi/package.json b/joi/package.json
index c336cce12..576c33d72 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 3ff76143c..a4c679773 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(
}
+ 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 000000000..9d18bf019
--- /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 51ea79c81..01aefbeb0 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 000000000..1b450706b
--- /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
+ }) => (
+
+ {children}
+
+ ),
+ 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-value')).toHaveTextContent(
+ 'Select an option'
+ )
+ })
+
+ it('renders options', () => {
+ 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()
+
+ await user.click(screen.getByTestId('select-trigger'))
+ await user.click(screen.getByTestId('select-item-option1'))
+
+ expect(onValueChange).toHaveBeenCalledWith('option1')
+ })
+
+ it('applies disabled class when disabled prop is true', () => {
+ render()
+ expect(screen.getByTestId('select-trigger')).toHaveClass('select__disabled')
+ })
+
+ it('applies block class when block prop is true', () => {
+ render()
+ expect(screen.getByTestId('select-trigger')).toHaveClass('w-full')
+ })
+})
diff --git a/joi/src/core/Slider/Slider.test.tsx b/joi/src/core/Slider/Slider.test.tsx
new file mode 100644
index 000000000..86bd8c623
--- /dev/null
+++ b/joi/src/core/Slider/Slider.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import { render, screen, fireEvent } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import { Slider } from './index'
+
+// Mock the styles
+jest.mock('./styles.scss', () => ({}))
+
+// Mock Radix UI Slider
+jest.mock('@radix-ui/react-slider', () => ({
+ Root: ({ children, onValueChange, ...props }: any) => (
+
+ onValueChange && onValueChange([parseInt(e.target.value)])
+ }
+ >
+
+ {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 000000000..72f3d8007
--- /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 000000000..b6dcf8a7b
--- /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 000000000..8bc64010f
--- /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()
+ const textareaElement = screen.getByPlaceholderText('Enter text here')
+ expect(textareaElement).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render()
+ const textareaElement = screen.getByRole('textbox')
+ expect(textareaElement).toHaveClass('textarea')
+ expect(textareaElement).toHaveClass('custom-class')
+ })
+
+ it('forwards ref correctly', () => {
+ const ref = React.createRef()
+ render()
+ expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
+ })
+
+ it('passes through additional props', () => {
+ render()
+ const textareaElement = screen.getByTestId('custom-textarea')
+ expect(textareaElement).toHaveAttribute('rows', '5')
+ })
+})
diff --git a/joi/src/core/Tooltip/Tooltip.test.tsx b/joi/src/core/Tooltip/Tooltip.test.tsx
new file mode 100644
index 000000000..880792b63
--- /dev/null
+++ b/joi/src/core/Tooltip/Tooltip.test.tsx
@@ -0,0 +1,121 @@
+import React from 'react'
+import '@testing-library/jest-dom'
+import { render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { Tooltip } from './index'
+
+declare const global: typeof globalThis
+
+// Mock the styles
+jest.mock('./styles.scss', () => ({}))
+
+class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+global.ResizeObserver = ResizeObserverMock
+
+describe('@joi/core/Tooltip', () => {
+ it('renders trigger content', () => {
+ render(
+ Hover me} content="Tooltip content" />
+ )
+ expect(screen.getByText('Hover me')).toBeInTheDocument()
+ })
+
+ it('shows tooltip content on hover', async () => {
+ const user = userEvent.setup()
+ render(
+ Hover me}
+ content={Tooltip content}
+ />
+ )
+
+ const trigger = screen.getByTestId('tooltip-trigger')
+ await user.hover(trigger)
+
+ await waitFor(() => {
+ const tooltipContents = screen.queryAllByTestId('tooltip-content')
+ expect(tooltipContents.length).toBeGreaterThan(0)
+ expect(tooltipContents[tooltipContents.length - 1]).toBeVisible()
+ })
+ })
+
+ it('does not show tooltip when disabled', async () => {
+ const user = userEvent.setup()
+ render(
+ Hover me}
+ content={Tooltip content}
+ disabled
+ />
+ )
+
+ const trigger = screen.getByTestId('tooltip-trigger')
+ await user.hover(trigger)
+
+ await waitFor(() => {
+ const tooltipContents = screen.queryAllByTestId('tooltip-content')
+ tooltipContents.forEach((content) => {
+ expect(content).not.toBeVisible()
+ })
+ })
+ })
+
+ it('renders arrow when withArrow is true', async () => {
+ const user = userEvent.setup()
+ render(
+ Hover me}
+ content={Tooltip content}
+ withArrow
+ />
+ )
+
+ const trigger = screen.getByTestId('tooltip-trigger')
+ await user.hover(trigger)
+
+ await waitFor(() => {
+ const tooltipContents = screen.queryAllByTestId('tooltip-content')
+ const visibleTooltip = tooltipContents.find((content) =>
+ content.matches(':not([style*="display: none"])')
+ )
+ expect(visibleTooltip?.closest('.tooltip__content')).toBeInTheDocument()
+ expect(
+ visibleTooltip
+ ?.closest('.tooltip__content')
+ ?.querySelector('.tooltip__arrow')
+ ).toBeInTheDocument()
+ })
+ })
+
+ it('does not render arrow when withArrow is false', async () => {
+ const user = userEvent.setup()
+ render(
+ Hover me}
+ content={Tooltip content}
+ withArrow={false}
+ />
+ )
+
+ const trigger = screen.getByTestId('tooltip-trigger')
+ await user.hover(trigger)
+
+ await waitFor(() => {
+ const tooltipContents = screen.queryAllByTestId('tooltip-content')
+ const visibleTooltip = tooltipContents.find((content) =>
+ content.matches(':not([style*="display: none"])')
+ )
+ expect(visibleTooltip?.closest('.tooltip__content')).toBeInTheDocument()
+ expect(
+ visibleTooltip
+ ?.closest('.tooltip__content')
+ ?.querySelector('.tooltip__arrow')
+ ).not.toBeInTheDocument()
+ })
+ })
+})