diff --git a/web/app/components/base/content-dialog/index.spec.tsx b/web/app/components/base/content-dialog/index.spec.tsx
new file mode 100644
index 0000000000..a047fdf062
--- /dev/null
+++ b/web/app/components/base/content-dialog/index.spec.tsx
@@ -0,0 +1,59 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import ContentDialog from './index'
+
+describe('ContentDialog', () => {
+ it('renders children when show is true', async () => {
+ render(
+
+ Dialog body
+ ,
+ )
+
+ await screen.findByText('Dialog body')
+ expect(screen.getByText('Dialog body')).toBeInTheDocument()
+
+ const backdrop = document.querySelector('.bg-app-detail-overlay-bg')
+ expect(backdrop).toBeTruthy()
+ })
+
+ it('does not render children when show is false', () => {
+ render(
+
+ Hidden content
+ ,
+ )
+
+ expect(screen.queryByText('Hidden content')).toBeNull()
+ expect(document.querySelector('.bg-app-detail-overlay-bg')).toBeNull()
+ })
+
+ it('calls onClose when backdrop is clicked', async () => {
+ const onClose = vi.fn()
+ render(
+
+ Body
+ ,
+ )
+
+ const user = userEvent.setup()
+ const backdrop = document.querySelector('.bg-app-detail-overlay-bg') as HTMLElement | null
+ expect(backdrop).toBeTruthy()
+
+ await user.click(backdrop!)
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('applies provided className to the content panel', () => {
+ render(
+
+ Panel content
+ ,
+ )
+
+ const contentPanel = document.querySelector('.bg-app-detail-bg') as HTMLElement | null
+ expect(contentPanel).toBeTruthy()
+ expect(contentPanel?.className).toContain('my-panel-class')
+ expect(screen.getByText('Panel content')).toBeInTheDocument()
+ })
+})
diff --git a/web/app/components/base/dialog/index.spec.tsx b/web/app/components/base/dialog/index.spec.tsx
new file mode 100644
index 0000000000..c58724595f
--- /dev/null
+++ b/web/app/components/base/dialog/index.spec.tsx
@@ -0,0 +1,138 @@
+import { act, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import CustomDialog from './index'
+
+describe('CustomDialog Component', () => {
+ const setup = () => userEvent.setup()
+
+ it('should render children and title when show is true', async () => {
+ render(
+
+ Main Content
+ ,
+ )
+
+ const title = await screen.findByText('Modal Title')
+ const content = screen.getByTestId('dialog-content')
+
+ expect(title).toBeInTheDocument()
+ expect(content).toBeInTheDocument()
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
+ })
+
+ it('should not render anything when show is false', async () => {
+ render(
+
+ Content
+ ,
+ )
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ expect(screen.queryByText('Hidden Title')).not.toBeInTheDocument()
+ })
+
+ it('should apply the correct semantic tag to title using titleAs', async () => {
+ render(
+
+ Content
+ ,
+ )
+
+ const title = await screen.findByRole('heading', { level: 1 })
+ expect(title).toHaveTextContent('Semantic Title')
+ })
+
+ it('should render the footer only when the prop is provided', async () => {
+ const { rerender } = render(
+ Content,
+ )
+
+ await screen.findByRole('dialog')
+ expect(screen.queryByText('Footer Content')).not.toBeInTheDocument()
+
+ rerender(
+ Footer Content}>
+ Content
+ ,
+ )
+
+ expect(await screen.findByTestId('footer-node')).toBeInTheDocument()
+ })
+
+ it('should call onClose when Escape key is pressed', async () => {
+ const user = setup()
+ const onCloseMock = vi.fn()
+
+ render(
+
+ Content
+ ,
+ )
+
+ await screen.findByRole('dialog')
+
+ await act(async () => {
+ await user.keyboard('{Escape}')
+ })
+
+ expect(onCloseMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call onClose when the backdrop is clicked', async () => {
+ const user = setup()
+ const onCloseMock = vi.fn()
+
+ render(
+
+ Content
+ ,
+ )
+
+ await screen.findByRole('dialog')
+
+ const backdrop = document.querySelector('.bg-background-overlay-backdrop')
+ expect(backdrop).toBeInTheDocument()
+
+ await act(async () => {
+ await user.click(backdrop!)
+ })
+
+ expect(onCloseMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('should apply custom class names to internal elements', async () => {
+ render(
+
+ Content
+ ,
+ )
+
+ await screen.findByRole('dialog')
+
+ expect(document.querySelector('.custom-panel-container')).toBeInTheDocument()
+ expect(document.querySelector('.custom-title-style')).toBeInTheDocument()
+ expect(document.querySelector('.custom-body-style')).toBeInTheDocument()
+ expect(document.querySelector('.custom-footer-style')).toBeInTheDocument()
+ })
+
+ it('should maintain accessibility attributes (aria-modal)', async () => {
+ render(
+
+
+ ,
+ )
+
+ const dialog = await screen.findByRole('dialog')
+ // Headless UI should automatically set aria-modal="true"
+ expect(dialog).toHaveAttribute('aria-modal', 'true')
+ })
+})
diff --git a/web/app/components/base/fullscreen-modal/index.spec.tsx b/web/app/components/base/fullscreen-modal/index.spec.tsx
new file mode 100644
index 0000000000..cf1484fc63
--- /dev/null
+++ b/web/app/components/base/fullscreen-modal/index.spec.tsx
@@ -0,0 +1,214 @@
+import { act, fireEvent, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import FullScreenModal from './index'
+
+describe('FullScreenModal Component', () => {
+ it('should not render anything when open is false', () => {
+ render(
+
+ Content
+ ,
+ )
+ expect(screen.queryByTestId('modal-content')).not.toBeInTheDocument()
+ })
+
+ it('should render content when open is true', async () => {
+ render(
+
+ Content
+ ,
+ )
+ expect(await screen.findByTestId('modal-content')).toBeInTheDocument()
+ })
+
+ it('should not crash when provided with title and description props', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+ })
+
+ describe('Props Handling', () => {
+ it('should apply wrapperClassName to the dialog root', async () => {
+ render(
+
+ Content
+ ,
+ )
+
+ await screen.findByRole('dialog')
+ const element = document.querySelector('.custom-wrapper-class')
+ expect(element).toBeInTheDocument()
+ expect(element).toHaveClass('modal-dialog')
+ })
+
+ it('should apply className to the inner panel', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+ const panel = document.querySelector('.custom-panel-class')
+ expect(panel).toBeInTheDocument()
+ expect(panel).toHaveClass('h-full')
+ })
+
+ it('should handle overflowVisible prop', async () => {
+ const { rerender } = await act(async () => {
+ return render(
+
+ Content
+ ,
+ )
+ })
+ let panel = document.querySelector('.target-panel')
+ expect(panel).toHaveClass('overflow-visible')
+ expect(panel).not.toHaveClass('overflow-hidden')
+
+ await act(async () => {
+ rerender(
+
+ Content
+ ,
+ )
+ })
+ panel = document.querySelector('.target-panel')
+ expect(panel).toHaveClass('overflow-hidden')
+ expect(panel).not.toHaveClass('overflow-visible')
+ })
+
+ it('should render close button when closable is true', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+ const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
+ expect(closeButton).toBeInTheDocument()
+ })
+
+ it('should not render close button when closable is false', async () => {
+ await act(async () => {
+ render(
+
+ Content
+ ,
+ )
+ })
+ const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
+ expect(closeButton).not.toBeInTheDocument()
+ })
+ })
+
+ describe('Interactions', () => {
+ it('should call onClose when close button is clicked', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+
+ render(
+
+ Content
+ ,
+ )
+
+ const closeBtn = document.querySelector('.bg-components-button-tertiary-bg')
+ expect(closeBtn).toBeInTheDocument()
+
+ await user.click(closeBtn!)
+ expect(onClose).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call onClose when clicking the backdrop', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+
+ render(
+
+ Content
+ ,
+ )
+
+ const dialog = document.querySelector('.modal-dialog')
+ if (dialog) {
+ await user.click(dialog)
+ expect(onClose).toHaveBeenCalled()
+ }
+ else {
+ throw new Error('Dialog root not found')
+ }
+ })
+
+ it('should call onClose when Escape key is pressed', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+
+ render(
+
+ Content
+ ,
+ )
+
+ await user.keyboard('{Escape}')
+ expect(onClose).toHaveBeenCalled()
+ })
+
+ it('should not call onClose when clicking inside the content', async () => {
+ const user = userEvent.setup()
+ const onClose = vi.fn()
+
+ render(
+
+
+
+
+ ,
+ )
+
+ const innerButton = screen.getByRole('button', { name: 'Action' })
+ await user.click(innerButton)
+ expect(onClose).not.toHaveBeenCalled()
+
+ const contentPanel = document.querySelector('.bg-background-default-subtle')
+ await act(async () => {
+ fireEvent.click(contentPanel!)
+ })
+ expect(onClose).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('Default Props', () => {
+ it('should not throw if onClose is not provided', async () => {
+ const user = userEvent.setup()
+ render(Content)
+
+ const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
+ await user.click(closeButton!)
+ })
+ })
+})
diff --git a/web/app/components/base/new-audio-button/index.spec.tsx b/web/app/components/base/new-audio-button/index.spec.tsx
new file mode 100644
index 0000000000..a30b06535a
--- /dev/null
+++ b/web/app/components/base/new-audio-button/index.spec.tsx
@@ -0,0 +1,205 @@
+import { act, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import i18next from 'i18next'
+import { useParams, usePathname } from 'next/navigation'
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+import AudioBtn from './index'
+
+const mockPlayAudio = vi.fn()
+const mockPauseAudio = vi.fn()
+const mockGetAudioPlayer = vi.fn()
+
+vi.mock('next/navigation', () => ({
+ useParams: vi.fn(),
+ usePathname: vi.fn(),
+}))
+
+vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
+ AudioPlayerManager: {
+ getInstance: vi.fn(() => ({
+ getAudioPlayer: mockGetAudioPlayer,
+ })),
+ },
+}))
+
+describe('AudioBtn', () => {
+ const getButton = () => screen.getByRole('button')
+
+ const hoverAndCheckTooltip = async (expectedText: string) => {
+ const button = getButton()
+ await userEvent.hover(button)
+ expect(await screen.findByText(expectedText)).toBeInTheDocument()
+ }
+
+ const getAudioCallback = () => {
+ const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
+ const callback = lastCall?.find((arg: unknown) => typeof arg === 'function') as ((event: string) => void) | undefined
+ if (!callback)
+ throw new Error('Audio callback not found - ensure mockGetAudioPlayer was called with a callback argument')
+ return callback
+ }
+
+ beforeAll(() => {
+ i18next.init({})
+ })
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockGetAudioPlayer.mockReturnValue({
+ playAudio: mockPlayAudio,
+ pauseAudio: mockPauseAudio,
+ })
+ ; (useParams as ReturnType).mockReturnValue({})
+ ; (usePathname as ReturnType).mockReturnValue('/')
+ })
+
+ describe('URL Routing', () => {
+ it('should generate public URL when token is present', async () => {
+ ; (useParams as ReturnType).mockReturnValue({ token: 'test-token' })
+
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
+ expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/text-to-audio')
+ expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(true)
+ })
+
+ it('should generate app URL when appId is present', async () => {
+ ; (useParams as ReturnType).mockReturnValue({ appId: '123' })
+ ; (usePathname as ReturnType).mockReturnValue('/apps/123/chat')
+
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
+ expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/apps/123/text-to-audio')
+ expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(false)
+ })
+
+ it('should generate installed app URL correctly', async () => {
+ ; (useParams as ReturnType).mockReturnValue({ appId: '456' })
+ ; (usePathname as ReturnType).mockReturnValue('/explore/installed/app')
+
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
+ expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/installed-apps/456/text-to-audio')
+ })
+ })
+
+ describe('State Management', () => {
+ it('should start in initial state', async () => {
+ render()
+
+ await hoverAndCheckTooltip('play')
+ expect(getButton()).toHaveClass('action-btn')
+ expect(getButton()).not.toBeDisabled()
+ })
+
+ it('should transition to playing state', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('play')
+ })
+
+ await hoverAndCheckTooltip('playing')
+ expect(getButton()).toHaveClass('action-btn-active')
+ })
+
+ it('should transition to ended state', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('play')
+ })
+ act(() => {
+ getAudioCallback()('ended')
+ })
+
+ await hoverAndCheckTooltip('play')
+ expect(getButton()).not.toHaveClass('action-btn-active')
+ })
+
+ it('should handle paused event', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('play')
+ })
+ act(() => {
+ getAudioCallback()('paused')
+ })
+
+ await hoverAndCheckTooltip('play')
+ })
+
+ it('should handle error event', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('error')
+ })
+
+ await hoverAndCheckTooltip('play')
+ })
+
+ it('should handle loaded event', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('loaded')
+ })
+
+ await hoverAndCheckTooltip('loading')
+ })
+ })
+
+ describe('Play/Pause', () => {
+ it('should call playAudio when clicked', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(mockPlayAudio).toHaveBeenCalled())
+ })
+
+ it('should call pauseAudio when clicked while playing', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ act(() => {
+ getAudioCallback()('play')
+ })
+
+ await userEvent.click(getButton())
+ await waitFor(() => expect(mockPauseAudio).toHaveBeenCalled())
+ })
+
+ it('should disable button when loading', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(getButton()).toBeDisabled())
+ })
+ })
+
+ describe('Props', () => {
+ it('should pass props to audio player', async () => {
+ render()
+ await userEvent.click(getButton())
+
+ await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
+ const call = mockGetAudioPlayer.mock.calls[0]
+ expect(call[2]).toBe('msg-1')
+ expect(call[3]).toBe('hello')
+ expect(call[4]).toBe('en-US')
+ })
+ })
+})
diff --git a/web/app/components/base/notion-connector/index.spec.tsx b/web/app/components/base/notion-connector/index.spec.tsx
new file mode 100644
index 0000000000..7ee799d002
--- /dev/null
+++ b/web/app/components/base/notion-connector/index.spec.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import NotionConnector from './index'
+
+describe('NotionConnector', () => {
+ it('should render the layout and actual sub-components (Icons & Button)', () => {
+ const { container } = render()
+
+ // Verify Title & Tip translations
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTip')).toBeInTheDocument()
+
+ const notionWrapper = container.querySelector('.h-12.w-12')
+ const dotsWrapper = container.querySelector('.system-md-semibold')
+
+ expect(notionWrapper?.querySelector('svg')).toBeInTheDocument()
+ expect(dotsWrapper?.querySelector('svg')).toBeInTheDocument()
+
+ const button = screen.getByRole('button', {
+ name: /datasetcreation.stepone.connect/i,
+ })
+
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveClass('btn', 'btn-primary')
+ })
+
+ it('should trigger the onSetting callback when the real button is clicked', async () => {
+ const onSetting = vi.fn()
+ const user = userEvent.setup()
+ render()
+
+ const button = screen.getByRole('button', {
+ name: /datasetcreation.stepone.connect/i,
+ })
+
+ await user.click(button)
+
+ expect(onSetting).toHaveBeenCalledTimes(1)
+ })
+
+ it('should maintain the correct visual hierarchy classes', () => {
+ const { container } = render()
+
+ // Verify the outer container has the specific workflow-process-bg
+ const mainContainer = container.firstChild
+ expect(mainContainer).toHaveClass('bg-workflow-process-bg', 'rounded-2xl', 'p-6')
+ })
+})
diff --git a/web/app/components/base/skeleton/index.spec.tsx b/web/app/components/base/skeleton/index.spec.tsx
new file mode 100644
index 0000000000..8f0d9a6837
--- /dev/null
+++ b/web/app/components/base/skeleton/index.spec.tsx
@@ -0,0 +1,83 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import {
+ SkeletonContainer,
+ SkeletonPoint,
+ SkeletonRectangle,
+ SkeletonRow,
+} from './index'
+
+describe('Skeleton Components', () => {
+ describe('Individual Components', () => {
+ it('should forward attributes and render children in SkeletonContainer', () => {
+ render(
+
+ Content
+ ,
+ )
+ const element = screen.getByTestId('container')
+ expect(element).toHaveClass('flex', 'flex-col', 'custom-container')
+ expect(screen.getByText('Content')).toBeInTheDocument()
+ })
+
+ it('should forward attributes and render children in SkeletonRow', () => {
+ render(
+
+ Row Content
+ ,
+ )
+ const element = screen.getByTestId('row')
+ expect(element).toHaveClass('flex', 'items-center', 'custom-row')
+ expect(screen.getByText('Row Content')).toBeInTheDocument()
+ })
+
+ it('should apply base skeleton styles to SkeletonRectangle', () => {
+ render()
+ const element = screen.getByTestId('rect')
+ expect(element).toHaveClass('h-2', 'bg-text-quaternary', 'opacity-20', 'w-10')
+ })
+
+ it('should render the separator character correctly in SkeletonPoint', () => {
+ render()
+ const element = screen.getByTestId('point')
+ expect(element).toHaveTextContent('·')
+ expect(element).toHaveClass('text-text-quaternary')
+ })
+ })
+
+ describe('Composition & Layout', () => {
+ it('should render a full skeleton structure accurately', () => {
+ const { container } = render(
+
+
+
+
+
+
+ ,
+ )
+
+ const wrapper = container.firstChild as HTMLElement
+ expect(wrapper).toHaveClass('main-wrapper')
+
+ expect(container.querySelector('.rect-1')).toBeInTheDocument()
+ expect(container.querySelector('.rect-2')).toBeInTheDocument()
+
+ const row = container.querySelector('.flex.items-center')
+ expect(row).toContainElement(container.querySelector('.rect-1') as HTMLElement)
+ expect(row).toHaveTextContent('·')
+ })
+ })
+
+ it('should handle rest props like event listeners', async () => {
+ const onClick = vi.fn()
+ const user = userEvent.setup()
+ render()
+
+ const element = screen.getByTestId('clickable')
+
+ await user.click(element)
+ expect(onClick).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/web/app/components/base/slider/index.spec.tsx b/web/app/components/base/slider/index.spec.tsx
new file mode 100644
index 0000000000..c9ebabd63e
--- /dev/null
+++ b/web/app/components/base/slider/index.spec.tsx
@@ -0,0 +1,77 @@
+import { act, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import Slider from './index'
+
+describe('Slider Component', () => {
+ it('should render with correct default ARIA limits and current value', () => {
+ render()
+
+ const slider = screen.getByRole('slider')
+ expect(slider).toHaveAttribute('aria-valuemin', '0')
+ expect(slider).toHaveAttribute('aria-valuemax', '100')
+ expect(slider).toHaveAttribute('aria-valuenow', '50')
+ })
+
+ it('should apply custom min, max, and step values', () => {
+ render()
+
+ const slider = screen.getByRole('slider')
+ expect(slider).toHaveAttribute('aria-valuemin', '5')
+ expect(slider).toHaveAttribute('aria-valuemax', '20')
+ expect(slider).toHaveAttribute('aria-valuenow', '10')
+ })
+
+ it('should default to 0 if the value prop is NaN', () => {
+ render()
+
+ const slider = screen.getByRole('slider')
+ expect(slider).toHaveAttribute('aria-valuenow', '0')
+ })
+
+ it('should call onChange when arrow keys are pressed', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+
+ render()
+
+ const slider = screen.getByRole('slider')
+
+ await act(async () => {
+ slider.focus()
+ await user.keyboard('{ArrowRight}')
+ })
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenCalledWith(21, 0)
+ })
+
+ it('should not trigger onChange when disabled', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ render()
+
+ const slider = screen.getByRole('slider')
+
+ expect(slider).toHaveAttribute('aria-disabled', 'true')
+
+ await act(async () => {
+ slider.focus()
+ await user.keyboard('{ArrowRight}')
+ })
+
+ expect(onChange).not.toHaveBeenCalled()
+ })
+
+ it('should apply custom class names', () => {
+ render(
+ ,
+ )
+
+ const sliderWrapper = screen.getByRole('slider').closest('.outer-test')
+ expect(sliderWrapper).toBeInTheDocument()
+
+ const thumb = screen.getByRole('slider')
+ expect(thumb).toHaveClass('thumb-test')
+ })
+})
diff --git a/web/app/components/base/sort/index.spec.tsx b/web/app/components/base/sort/index.spec.tsx
new file mode 100644
index 0000000000..92ea2b44f9
--- /dev/null
+++ b/web/app/components/base/sort/index.spec.tsx
@@ -0,0 +1,141 @@
+import { render, screen, waitFor, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import * as React from 'react'
+import { describe, expect, it, vi } from 'vitest'
+import Sort from './index'
+
+const mockItems = [
+ { value: 'created_at', name: 'Date Created' },
+ { value: 'name', name: 'Name' },
+ { value: 'status', name: 'Status' },
+]
+
+describe('Sort component — real portal integration', () => {
+ const setup = (props = {}) => {
+ const onSelect = vi.fn()
+ const user = userEvent.setup()
+ const { container, rerender } = render(
+ ,
+ )
+
+ // helper: returns a non-null HTMLElement or throws with a clear message
+ const getTriggerWrapper = (): HTMLElement => {
+ const labelNode = screen.getByText('appLog.filter.sortBy')
+ // try to find a reasonable wrapper element; prefer '.block' but fallback to any ancestor div
+ const wrapper = labelNode.closest('.block') ?? labelNode.closest('div')
+ if (!wrapper)
+ throw new Error('Trigger wrapper element not found for "Sort by" label')
+ return wrapper as HTMLElement
+ }
+
+ // helper: returns right-side sort button element
+ const getSortButton = (): HTMLElement => {
+ const btn = container.querySelector('.rounded-r-lg')
+ if (!btn)
+ throw new Error('Sort button (rounded-r-lg) not found in rendered container')
+ return btn as HTMLElement
+ }
+
+ return { user, onSelect, rerender, getTriggerWrapper, getSortButton }
+ }
+
+ it('renders and shows selected item label and sort icon', () => {
+ const { getSortButton } = setup({ order: '' })
+
+ expect(screen.getByText('Date Created')).toBeInTheDocument()
+
+ const sortButton = getSortButton()
+ expect(sortButton).toBeInstanceOf(HTMLElement)
+ expect(sortButton.querySelector('svg')).toBeInTheDocument()
+ })
+
+ it('opens and closes the tooltip (portal mounts to document.body)', async () => {
+ const { user, getTriggerWrapper } = setup()
+
+ await user.click(getTriggerWrapper())
+ const tooltip = await screen.findByRole('tooltip')
+ expect(tooltip).toBeInTheDocument()
+ expect(document.body.contains(tooltip)).toBe(true)
+
+ // clicking the trigger again should close it
+ await user.click(getTriggerWrapper())
+ await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ })
+
+ it('renders options and calls onSelect with descending prefix when order is "-"', async () => {
+ const { user, onSelect, getTriggerWrapper } = setup({ order: '-' })
+
+ await user.click(getTriggerWrapper())
+ const tooltip = await screen.findByRole('tooltip')
+
+ mockItems.forEach((item) => {
+ expect(within(tooltip).getByText(item.name)).toBeInTheDocument()
+ })
+
+ await user.click(within(tooltip).getByText('Name'))
+ expect(onSelect).toHaveBeenCalledWith('-name')
+ await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ })
+
+ it('toggles sorting order: ascending -> descending via right-side button', async () => {
+ const { user, onSelect, getSortButton } = setup({ order: '', value: 'created_at' })
+ await user.click(getSortButton())
+ expect(onSelect).toHaveBeenCalledWith('-created_at')
+ })
+
+ it('toggles sorting order: descending -> ascending via right-side button', async () => {
+ const { user, onSelect, getSortButton } = setup({ order: '-', value: 'name' })
+ await user.click(getSortButton())
+ expect(onSelect).toHaveBeenCalledWith('name')
+ })
+
+ it('shows checkmark only for selected item in menu', async () => {
+ const { user, getTriggerWrapper } = setup({ value: 'status' })
+
+ await user.click(getTriggerWrapper())
+ const tooltip = await screen.findByRole('tooltip')
+
+ const statusRow = within(tooltip).getByText('Status').closest('.flex')
+ const nameRow = within(tooltip).getByText('Name').closest('.flex')
+
+ if (!statusRow)
+ throw new Error('Status option row not found in menu')
+ if (!nameRow)
+ throw new Error('Name option row not found in menu')
+
+ expect(statusRow.querySelector('svg')).toBeInTheDocument()
+ expect(nameRow.querySelector('svg')).not.toBeInTheDocument()
+ })
+
+ it('shows empty selection label when value is unknown', () => {
+ setup({ value: 'unknown_value' })
+ const label = screen.getByText('appLog.filter.sortBy')
+ const valueNode = label.nextSibling
+ if (!valueNode)
+ throw new Error('Expected a sibling node for the selection text')
+ expect(String(valueNode.textContent || '').trim()).toBe('')
+ })
+
+ it('handles undefined order prop without asserting a literal "undefined" prefix', async () => {
+ const { user, onSelect, getTriggerWrapper } = setup({ order: undefined })
+
+ await user.click(getTriggerWrapper())
+ const tooltip = await screen.findByRole('tooltip')
+
+ await user.click(within(tooltip).getByText('Name'))
+
+ expect(onSelect).toHaveBeenCalled()
+ expect(onSelect).toHaveBeenCalledWith(expect.stringMatching(/name$/))
+ })
+
+ it('clicking outside the open menu closes the portal', async () => {
+ const { user, getTriggerWrapper } = setup()
+ await user.click(getTriggerWrapper())
+ const tooltip = await screen.findByRole('tooltip')
+ expect(tooltip).toBeInTheDocument()
+
+ // click outside: body click should close the tooltip
+ await user.click(document.body)
+ await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
+ })
+})
diff --git a/web/app/components/base/switch/index.spec.tsx b/web/app/components/base/switch/index.spec.tsx
new file mode 100644
index 0000000000..b434ddd729
--- /dev/null
+++ b/web/app/components/base/switch/index.spec.tsx
@@ -0,0 +1,84 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import Switch from './index'
+
+describe('Switch', () => {
+ it('should render in unchecked state by default', () => {
+ render()
+ const switchElement = screen.getByRole('switch')
+ expect(switchElement).toBeInTheDocument()
+ expect(switchElement).toHaveAttribute('aria-checked', 'false')
+ })
+
+ it('should render in checked state when defaultValue is true', () => {
+ render()
+ const switchElement = screen.getByRole('switch')
+ expect(switchElement).toHaveAttribute('aria-checked', 'true')
+ })
+
+ it('should toggle state and call onChange when clicked', async () => {
+ const onChange = vi.fn()
+ const user = userEvent.setup()
+ render()
+
+ const switchElement = screen.getByRole('switch')
+
+ await user.click(switchElement)
+ expect(switchElement).toHaveAttribute('aria-checked', 'true')
+ expect(onChange).toHaveBeenCalledWith(true)
+ expect(onChange).toHaveBeenCalledTimes(1)
+
+ await user.click(switchElement)
+ expect(switchElement).toHaveAttribute('aria-checked', 'false')
+ expect(onChange).toHaveBeenCalledWith(false)
+ expect(onChange).toHaveBeenCalledTimes(2)
+ })
+
+ it('should not call onChange when disabled', async () => {
+ const onChange = vi.fn()
+ const user = userEvent.setup()
+ render()
+
+ const switchElement = screen.getByRole('switch')
+ expect(switchElement).toHaveClass('!cursor-not-allowed', '!opacity-50')
+
+ await user.click(switchElement)
+ expect(onChange).not.toHaveBeenCalled()
+ })
+
+ it('should apply correct size classes', () => {
+ const { rerender } = render()
+ // We only need to find the element once
+ const switchElement = screen.getByRole('switch')
+ expect(switchElement).toHaveClass('h-2.5', 'w-3.5', 'rounded-sm')
+
+ rerender()
+ expect(switchElement).toHaveClass('h-3', 'w-5')
+
+ rerender()
+ expect(switchElement).toHaveClass('h-4', 'w-7')
+
+ rerender()
+ expect(switchElement).toHaveClass('h-5', 'w-9')
+
+ rerender()
+ expect(switchElement).toHaveClass('h-6', 'w-11')
+ })
+
+ it('should apply custom className', () => {
+ render()
+ expect(screen.getByRole('switch')).toHaveClass('custom-test-class')
+ })
+
+ it('should apply correct background colors based on state', async () => {
+ const user = userEvent.setup()
+ render()
+ const switchElement = screen.getByRole('switch')
+
+ expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked')
+
+ await user.click(switchElement)
+ expect(switchElement).toHaveClass('bg-components-toggle-bg')
+ })
+})
diff --git a/web/app/components/base/tag/index.spec.tsx b/web/app/components/base/tag/index.spec.tsx
new file mode 100644
index 0000000000..76d2915ba8
--- /dev/null
+++ b/web/app/components/base/tag/index.spec.tsx
@@ -0,0 +1,104 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import Tag from './index'
+import '@testing-library/jest-dom/vitest'
+
+describe('Tag Component', () => {
+ describe('Rendering', () => {
+ it('should render with text children', () => {
+ const { container } = render(Hello World)
+ expect(container.firstChild).toHaveTextContent('Hello World')
+ })
+
+ it('should render with ReactNode children', () => {
+ render(Node)
+ expect(screen.getByTestId('child')).toBeInTheDocument()
+ })
+
+ it('should always apply base layout classes', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass(
+ 'inline-flex',
+ 'shrink-0',
+ 'items-center',
+ 'rounded-md',
+ 'px-2.5',
+ 'py-px',
+ 'text-xs',
+ 'leading-5',
+ )
+ })
+ })
+
+ describe('Color Variants', () => {
+ it.each([
+ { color: 'green', text: 'text-green-800', bg: 'bg-green-100' },
+ { color: 'yellow', text: 'text-yellow-800', bg: 'bg-yellow-100' },
+ { color: 'red', text: 'text-red-800', bg: 'bg-red-100' },
+ { color: 'gray', text: 'text-gray-800', bg: 'bg-gray-100' },
+ ])('should apply $color color classes', ({ color, text, bg }) => {
+ type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass(text, bg)
+ })
+
+ it('should default to green when no color specified', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('text-green-800', 'bg-green-100')
+ })
+
+ it('should not apply color classes for invalid color', () => {
+ type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
+ const { container } = render(Test)
+ const className = (container.firstChild as HTMLElement)?.className || ''
+
+ expect(className).not.toMatch(/text-(green|yellow|red|gray)-800/)
+ expect(className).not.toMatch(/bg-(green|yellow|red|gray)-100/)
+ })
+ })
+
+ describe('Boolean Props', () => {
+ it('should apply border when bordered is true', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('border-[1px]')
+ })
+
+ it('should not apply border by default', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).not.toHaveClass('border-[1px]')
+ })
+
+ it('should hide background when hideBg is true', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('bg-transparent')
+ })
+
+ it('should apply both bordered and hideBg together', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('border-[1px]', 'bg-transparent')
+ })
+
+ it('should override color background with hideBg', () => {
+ const { container } = render(Test)
+ const tag = container.firstChild
+ expect(tag).toHaveClass('bg-transparent', 'text-red-800')
+ })
+ })
+
+ describe('Custom Styling', () => {
+ it('should merge custom className', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('my-custom-class')
+ })
+
+ it('should preserve base classes with custom className', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toHaveClass('inline-flex', 'my-custom-class')
+ })
+
+ it('should handle empty className prop', () => {
+ const { container } = render(Test)
+ expect(container.firstChild).toBeInTheDocument()
+ })
+ })
+})