Pārlūkot izejas kodu

fix: fix signin and logout issue

Mohammad Mahdi Salimi 1 mēnesi atpakaļ
vecāks
revīzija
6d4d07aa95

+ 0 - 319
src/components/config-drawer.test.tsx

@@ -1,319 +0,0 @@
-import { clearCookies } from '@/test-utils/cookies'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render, type RenderResult } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { getCookie, setCookie } from '@/lib/cookies'
-import { DirectionProvider } from '@/context/direction-provider'
-import { LayoutProvider } from '@/context/layout-provider'
-import { ThemeProvider } from '@/context/theme-provider'
-import { SidebarProvider } from '@/components/ui/sidebar'
-import { ConfigDrawer } from './config-drawer'
-
-async function renderConfigDrawer({
-  sidebarDefaultOpen = true,
-}: {
-  sidebarDefaultOpen?: boolean
-} = {}) {
-  return await render(
-    <DirectionProvider>
-      <ThemeProvider>
-        <LayoutProvider>
-          <SidebarProvider defaultOpen={sidebarDefaultOpen}>
-            <ConfigDrawer />
-          </SidebarProvider>
-        </LayoutProvider>
-      </ThemeProvider>
-    </DirectionProvider>
-  )
-}
-
-async function openDrawer(screen: RenderResult) {
-  await userEvent.click(
-    screen.getByRole('button', { name: /^Open theme settings$/i })
-  )
-  await expect
-    .element(screen.getByText(/^Theme Settings$/i))
-    .toBeInTheDocument()
-}
-
-describe('ConfigDrawer (integration)', () => {
-  beforeEach(() => {
-    vi.clearAllMocks()
-
-    clearCookies()
-
-    document.documentElement.classList.remove('light', 'dark')
-    document.documentElement.removeAttribute('dir')
-  })
-
-  it('opens the drawer and renders the sections', async () => {
-    const screen = await renderConfigDrawer()
-
-    await openDrawer(screen)
-
-    const drawer = screen.getByRole('dialog', { name: /theme settings/i })
-
-    await expect.element(drawer).toBeInTheDocument()
-
-    await expect.element(drawer.getByText(/^Theme$/i)).toBeInTheDocument()
-    await expect.element(drawer.getByText(/^Layout$/i)).toBeInTheDocument()
-    await expect
-      .element(drawer.getByText(/^Sidebar$/i).first())
-      .toBeInTheDocument()
-    await expect.element(drawer.getByText(/^Direction$/i)).toBeInTheDocument()
-    await expect
-      .element(
-        screen.getByRole('button', {
-          name: /reset all settings to default values/i,
-        })
-      )
-      .toBeInTheDocument()
-  })
-
-  describe('theme preference', () => {
-    it('applies light theme to <html> and cookie', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select light/i })
-      )
-      await vi.waitFor(() =>
-        expect(document.documentElement.classList.contains('light')).toBe(true)
-      )
-      expect(getCookie('vite-ui-theme')).toBe('light')
-    })
-
-    it('applies dark theme to <html> and cookie', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-      await userEvent.click(screen.getByRole('radio', { name: /select dark/i }))
-      await vi.waitFor(() =>
-        expect(document.documentElement.classList.contains('dark')).toBe(true)
-      )
-      expect(getCookie('vite-ui-theme')).toBe('dark')
-    })
-
-    it('applies system theme: stores cookie and applies a resolved light or dark class', async () => {
-      // Pre-seed light so mounted theme is not system; re-selecting System alone would not fire setTheme.
-      setCookie('vite-ui-theme', 'light')
-
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select system/i })
-      )
-      await vi.waitFor(() => expect(getCookie('vite-ui-theme')).toBe('system'))
-      await vi.waitFor(() => {
-        const root = document.documentElement
-        const hasLight = root.classList.contains('light')
-        const hasDark = root.classList.contains('dark')
-        expect(hasLight !== hasDark).toBe(true)
-      })
-    })
-  })
-
-  describe('sidebar variant', () => {
-    it('selecting floating updates layout_variant cookie', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select floating/i })
-      )
-      await vi.waitFor(() =>
-        expect(getCookie('layout_variant')).toBe('floating')
-      )
-    })
-
-    it('selecting sidebar updates layout_variant cookie', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /^select sidebar$/i })
-      )
-      await vi.waitFor(() =>
-        expect(getCookie('layout_variant')).toBe('sidebar')
-      )
-    })
-
-    it('selecting inset updates layout_variant cookie after another variant', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select floating/i })
-      )
-      await vi.waitFor(() =>
-        expect(getCookie('layout_variant')).toBe('floating')
-      )
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select inset/i })
-      )
-      await vi.waitFor(() => expect(getCookie('layout_variant')).toBe('inset'))
-    })
-  })
-
-  it('selecting full layout sets collapsible to offcanvas and closes sidebar', async () => {
-    const screen = await renderConfigDrawer({ sidebarDefaultOpen: true })
-    await openDrawer(screen)
-
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select full layout/i })
-    )
-    await vi.waitFor(() =>
-      expect(getCookie('layout_collapsible')).toBe('offcanvas')
-    )
-    await vi.waitFor(() => expect(getCookie('sidebar_state')).toBe('false'))
-  })
-
-  describe('section reset buttons', () => {
-    it('resets theme via section control after choosing dark', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(screen.getByRole('radio', { name: /select dark/i }))
-      await vi.waitFor(() => expect(getCookie('vite-ui-theme')).toBe('dark'))
-
-      await userEvent.click(
-        screen.getByRole('button', {
-          name: /reset theme preference to default/i,
-        })
-      )
-      await vi.waitFor(() => expect(getCookie('vite-ui-theme')).toBe('system'))
-    })
-
-    it('resets direction via section control after choosing RTL', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select right to left/i })
-      )
-      await vi.waitFor(() =>
-        expect(document.documentElement.getAttribute('dir')).toBe('rtl')
-      )
-
-      await userEvent.click(
-        screen.getByRole('button', {
-          name: /reset text direction to default/i,
-        })
-      )
-      await vi.waitFor(() =>
-        expect(document.documentElement.getAttribute('dir')).toBe('ltr')
-      )
-      expect(getCookie('dir')).toBe('ltr')
-    })
-
-    it('resets sidebar style via section control after choosing floating', async () => {
-      const screen = await renderConfigDrawer()
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select floating/i })
-      )
-      await vi.waitFor(() =>
-        expect(getCookie('layout_variant')).toBe('floating')
-      )
-
-      await userEvent.click(
-        screen.getByRole('button', {
-          name: /reset sidebar style to default/i,
-        })
-      )
-      await vi.waitFor(() => expect(getCookie('layout_variant')).toBe('inset'))
-    })
-
-    it('resets layout via section control after choosing compact', async () => {
-      const screen = await renderConfigDrawer({ sidebarDefaultOpen: true })
-      await openDrawer(screen)
-
-      await userEvent.click(
-        screen.getByRole('radio', { name: /select compact/i })
-      )
-      await vi.waitFor(() => expect(getCookie('sidebar_state')).toBe('false'))
-
-      await userEvent.click(
-        screen.getByRole('button', {
-          name: /reset layout options to default/i,
-        })
-      )
-      await vi.waitFor(() => expect(getCookie('sidebar_state')).toBe('true'))
-      await vi.waitFor(() =>
-        expect(getCookie('layout_collapsible')).toBe('icon')
-      )
-    })
-  })
-
-  it('changes direction and applies it to <html dir>', async () => {
-    const screen = await renderConfigDrawer()
-
-    await openDrawer(screen)
-
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select right to left/i })
-    )
-    await vi.waitFor(() =>
-      expect(document.documentElement.getAttribute('dir')).toBe('rtl')
-    )
-    expect(getCookie('dir')).toBe('rtl')
-  })
-
-  it('updates layout: selecting non-default closes sidebar and changes layout cookie', async () => {
-    const screen = await renderConfigDrawer({ sidebarDefaultOpen: true })
-
-    await openDrawer(screen)
-
-    await expect
-      .element(screen.getByRole('radio', { name: /select default/i }))
-      .toHaveAttribute('data-state', 'checked')
-
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select compact/i })
-    )
-
-    await vi.waitFor(() => expect(getCookie('sidebar_state')).toBe('false'))
-    await vi.waitFor(() => expect(getCookie('layout_collapsible')).toBe('icon'))
-  })
-
-  it('reset restores defaults across sidebar/theme/layout/direction', async () => {
-    const screen = await renderConfigDrawer({ sidebarDefaultOpen: true })
-
-    await openDrawer(screen)
-
-    await userEvent.click(screen.getByRole('radio', { name: /select dark/i }))
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select right to left/i })
-    )
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select floating/i })
-    )
-    await userEvent.click(
-      screen.getByRole('radio', { name: /select full layout/i })
-    )
-
-    await vi.waitFor(() => expect(getCookie('vite-ui-theme')).toBe('dark'))
-    await vi.waitFor(() => expect(getCookie('dir')).toBe('rtl'))
-    await vi.waitFor(() => expect(getCookie('layout_variant')).toBe('floating'))
-    await vi.waitFor(() =>
-      expect(getCookie('layout_collapsible')).toBe('offcanvas')
-    )
-
-    await userEvent.click(
-      screen.getByRole('button', {
-        name: /reset all settings to default values/i,
-      })
-    )
-
-    await vi.waitFor(() => expect(getCookie('sidebar_state')).toBe('true'))
-    await vi.waitFor(() => expect(getCookie('dir')).toBeUndefined())
-    await vi.waitFor(() => expect(getCookie('vite-ui-theme')).toBeUndefined())
-    await vi.waitFor(() => expect(getCookie('layout_variant')).toBe('inset'))
-    await vi.waitFor(() => expect(getCookie('layout_collapsible')).toBe('icon'))
-    await vi.waitFor(() =>
-      expect(document.documentElement.getAttribute('dir')).toBe('ltr')
-    )
-  })
-})

+ 0 - 218
src/components/confirm-dialog.test.tsx

@@ -1,218 +0,0 @@
-import type { SubmitEvent } from 'react'
-import { describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { ConfirmDialog } from './confirm-dialog'
-
-describe('ConfirmDialog', () => {
-  it('renders title, description, and default buttons', async () => {
-    const { getByRole, getByText } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete item'
-        desc='This action cannot be undone.'
-        handleConfirm={vi.fn()}
-      />
-    )
-
-    await expect
-      .element(getByRole('heading', { name: 'Delete item' }))
-      .toBeInTheDocument()
-    await expect
-      .element(getByText('This action cannot be undone.'))
-      .toBeInTheDocument()
-    await expect
-      .element(getByRole('button', { name: 'Cancel' }))
-      .toBeInTheDocument()
-    await expect
-      .element(getByRole('button', { name: 'Continue' }))
-      .toBeInTheDocument()
-  })
-
-  it('calls handleConfirm when the confirm button is clicked', async () => {
-    const handleConfirm = vi.fn()
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Sign out'
-        desc='Are you sure?'
-        confirmText='Sign out'
-        handleConfirm={handleConfirm}
-      />
-    )
-
-    await userEvent.click(getByRole('button', { name: 'Sign out' }))
-    expect(handleConfirm).toHaveBeenCalledOnce()
-  })
-
-  it('disables confirm when disabled is true', async () => {
-    const handleConfirm = vi.fn()
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Danger'
-        desc='...'
-        disabled
-        handleConfirm={handleConfirm}
-      />
-    )
-
-    const confirm = getByRole('button', { name: 'Continue' })
-    await expect.element(confirm).toBeDisabled()
-    expect(handleConfirm).not.toHaveBeenCalled()
-  })
-
-  it('when isLoading is true, disables cancel and confirm', async () => {
-    const handleConfirm = vi.fn()
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Loading'
-        desc='...'
-        isLoading
-        handleConfirm={handleConfirm}
-      />
-    )
-
-    await expect.element(getByRole('button', { name: 'Cancel' })).toBeDisabled()
-    await expect
-      .element(getByRole('button', { name: 'Continue' }))
-      .toBeDisabled()
-  })
-
-  it('supports custom button texts', async () => {
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete'
-        desc='...'
-        cancelBtnText='No'
-        confirmText='Yes'
-        handleConfirm={vi.fn()}
-      />
-    )
-
-    await expect
-      .element(getByRole('button', { name: 'No' }))
-      .toBeInTheDocument()
-    await expect
-      .element(getByRole('button', { name: 'Yes' }))
-      .toBeInTheDocument()
-  })
-
-  it('renders confirm as submit button linked to desc form when `form` is set', async () => {
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete tasks'
-        form='tasks-multi-delete-form'
-        desc={
-          <form id='tasks-multi-delete-form' className='space-y-4'>
-            <p>Type DELETE to confirm.</p>
-          </form>
-        }
-        confirmText='Delete'
-        destructive
-      />
-    )
-
-    const deleteBtn = getByRole('button', { name: 'Delete' })
-    await expect.element(deleteBtn).toHaveAttribute('type', 'submit')
-    await expect
-      .element(deleteBtn)
-      .toHaveAttribute('form', 'tasks-multi-delete-form')
-  })
-
-  it('submits the desc form when confirm is clicked (form prop, no handleConfirm)', async () => {
-    const handleFormSubmit = vi.fn((e: SubmitEvent<HTMLFormElement>) => {
-      e.preventDefault()
-    })
-
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete'
-        form='users-delete-form'
-        desc={
-          <form
-            id='users-delete-form'
-            onSubmit={handleFormSubmit}
-            className='space-y-4'
-          >
-            <p>Confirm deletion.</p>
-          </form>
-        }
-        confirmText='Delete'
-        destructive
-      />
-    )
-
-    await userEvent.click(getByRole('button', { name: 'Delete' }))
-
-    expect(handleFormSubmit).toHaveBeenCalledOnce()
-  })
-
-  it('submits the form when Enter key is pressed', async () => {
-    const handleFormSubmit = vi.fn((e: SubmitEvent<HTMLFormElement>) => {
-      e.preventDefault()
-    })
-
-    const { getByPlaceholder } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete'
-        form='users-delete-form'
-        desc={
-          <form
-            id='users-delete-form'
-            onSubmit={handleFormSubmit}
-            className='space-y-4'
-          >
-            <input type='text' name='username' placeholder='username' />
-          </form>
-        }
-        confirmText='Delete'
-        destructive
-      />
-    )
-
-    await userEvent.fill(getByPlaceholder('username'), 'test')
-    await userEvent.keyboard('{Enter}')
-    expect(handleFormSubmit).toHaveBeenCalledOnce()
-  })
-
-  it('does not submit the form when confirm is disabled (typed confirmation mismatch)', async () => {
-    const handleFormSubmit = vi.fn((e: SubmitEvent<HTMLFormElement>) => {
-      e.preventDefault()
-    })
-
-    const { getByRole } = await render(
-      <ConfirmDialog
-        open
-        onOpenChange={vi.fn()}
-        title='Delete'
-        form='users-delete-form'
-        disabled
-        desc={
-          <form id='users-delete-form' onSubmit={handleFormSubmit}>
-            <p>Enter username to enable Delete.</p>
-          </form>
-        }
-        confirmText='Delete'
-        destructive
-      />
-    )
-
-    const deleteBtn = getByRole('button', { name: 'Delete' })
-    await expect.element(deleteBtn).toBeDisabled()
-    expect(handleFormSubmit).not.toHaveBeenCalled()
-  })
-})

+ 0 - 99
src/components/password-input.test.tsx

@@ -1,99 +0,0 @@
-import { useForm } from 'react-hook-form'
-import { describe, expect, it } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import {
-  Form,
-  FormControl,
-  FormField,
-  FormItem,
-  FormLabel,
-} from '@/components/ui/form'
-import { PasswordInput } from './password-input'
-
-describe('PasswordInput', () => {
-  it('renders the password input correctly', async () => {
-    const { getByPlaceholder, getByRole } = await render(
-      <PasswordInput placeholder='password' />
-    )
-
-    const passwordInput = getByPlaceholder('password')
-    const showPasswordButton = getByRole('button', { name: /show password/i })
-
-    await expect.element(passwordInput).toBeInTheDocument()
-    await expect.element(passwordInput).toHaveAttribute('type', 'password')
-    await expect.element(showPasswordButton).toBeVisible()
-  })
-
-  it('toggles the password visibility when the show password button is clicked', async () => {
-    const { getByPlaceholder, getByRole } = await render(
-      <PasswordInput placeholder='password' />
-    )
-
-    const passwordInput = getByPlaceholder('password')
-    const showPasswordButton = getByRole('button', { name: /show password/i })
-
-    await expect.element(passwordInput).toHaveAttribute('type', 'password')
-    await expect.element(showPasswordButton).toBeInTheDocument()
-
-    await userEvent.click(showPasswordButton)
-
-    await expect.element(passwordInput).toHaveAttribute('type', 'text')
-    const hidePasswordButton = getByRole('button', { name: /hide password/i })
-    await expect.element(hidePasswordButton).toBeInTheDocument()
-
-    await userEvent.click(hidePasswordButton)
-
-    await expect.element(passwordInput).toHaveAttribute('type', 'password')
-    await expect
-      .element(getByRole('button', { name: /show password/i }))
-      .toBeInTheDocument()
-  })
-
-  it('disables the show password button when the password input is disabled', async () => {
-    const { getByPlaceholder, getByRole } = await render(
-      <PasswordInput placeholder='password' disabled />
-    )
-
-    const passwordInput = getByPlaceholder('password')
-    const showPasswordButton = getByRole('button', { name: /show password/i })
-    await expect.element(showPasswordButton).toBeDisabled()
-    await expect.element(passwordInput).toBeDisabled()
-  })
-
-  it('works with FormLabel and react-hook-form field spread', async () => {
-    function PasswordInLabeledForm() {
-      const form = useForm<{ password: string }>({
-        defaultValues: { password: '' },
-      })
-
-      return (
-        <Form {...form}>
-          <form>
-            <FormField
-              control={form.control}
-              name='password'
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel>Password</FormLabel>
-                  <FormControl>
-                    <PasswordInput {...field} />
-                  </FormControl>
-                </FormItem>
-              )}
-            />
-          </form>
-        </Form>
-      )
-    }
-
-    const { getByLabelText } = await render(<PasswordInLabeledForm />)
-
-    const password = getByLabelText(/^Password$/i)
-    await expect.element(password).toHaveAttribute('type', 'password')
-
-    await userEvent.type(password, 'secret-value')
-
-    await expect.element(password).toHaveValue('secret-value')
-  })
-})

+ 0 - 56
src/components/sign-out-dialog.test.tsx

@@ -1,56 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { SignOutDialog } from './sign-out-dialog'
-
-const navigate = vi.fn()
-const reset = vi.fn()
-
-const MOCK_HREF = 'https://app.test/dashboard?tab=1'
-
-vi.mock('@/stores/auth-store', () => ({
-  useAuthStore: () => ({
-    auth: { reset },
-  }),
-}))
-
-vi.mock('@tanstack/react-router', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('@tanstack/react-router')>()
-  return {
-    ...actual,
-    useNavigate: () => navigate,
-    useLocation: () => ({ href: MOCK_HREF }),
-  }
-})
-
-describe('SignOutDialog', () => {
-  beforeEach(() => {
-    vi.clearAllMocks()
-  })
-
-  it('calls auth.reset and navigates to sign-in with current location as redirect', async () => {
-    const { getByRole } = await render(
-      <SignOutDialog open onOpenChange={vi.fn()} />
-    )
-
-    await userEvent.click(getByRole('button', { name: /^Sign out$/i }))
-
-    expect(reset).toHaveBeenCalledOnce()
-    expect(navigate).toHaveBeenCalledWith({
-      to: '/sign-in',
-      search: { redirect: MOCK_HREF },
-      replace: true,
-    })
-  })
-
-  it('does not call reset or navigate when Cancel is clicked', async () => {
-    const { getByRole } = await render(
-      <SignOutDialog open onOpenChange={vi.fn()} />
-    )
-
-    await userEvent.click(getByRole('button', { name: /^Cancel$/i }))
-
-    expect(reset).not.toHaveBeenCalled()
-    expect(navigate).not.toHaveBeenCalled()
-  })
-})

+ 2 - 2
src/components/sign-out-dialog.tsx

@@ -12,8 +12,8 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
   const location = useLocation()
   const { logout } = useAuthStore()
 
-  const handleSignOut = () => {
-    logout()
+  const handleSignOut = async () => {
+    await logout()
     // Preserve current location for redirect after sign-in
     const currentPath = location.href
     navigate({

+ 0 - 161
src/context/search-provider.test.tsx

@@ -1,161 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render, type RenderResult } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { SearchProvider } from '@/context/search-provider'
-
-const COMMAND_MENU_PLACEHOLDER = 'Type a command or search...'
-
-const mocks = vi.hoisted(() => ({
-  navigate: vi.fn(),
-  setTheme: vi.fn(),
-}))
-
-vi.mock('@tanstack/react-router', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('@tanstack/react-router')>()
-  return {
-    ...actual,
-    useNavigate: () => mocks.navigate,
-  }
-})
-
-vi.mock('@/context/theme-provider', () => ({
-  useTheme: () => ({ setTheme: mocks.setTheme }),
-}))
-
-type ShortcutModifier = 'Control' | 'Meta'
-
-async function renderWithSearchProvider() {
-  return await render(<SearchProvider>{null}</SearchProvider>)
-}
-
-/**
- * Open the palette by shortcut, retrying while the keydown listener may not be mounted yet.
- * Waits between attempts so a successful toggle is not immediately undone by a second chord.
- */
-async function openCommandPalette(
-  screen: RenderResult,
-  modifier: ShortcutModifier = 'Control'
-) {
-  await vi.waitFor(
-    async () => {
-      const isCommandPaletteOpen =
-        document.querySelector(
-          `[placeholder="${COMMAND_MENU_PLACEHOLDER}"]`
-        ) !== null
-
-      if (!isCommandPaletteOpen) {
-        await userEvent.keyboard(`{${modifier}>}k{/${modifier}}`)
-      }
-
-      await expect
-        .element(screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-        .toBeInTheDocument()
-    },
-    { interval: 50, timeout: 5000 }
-  )
-}
-
-describe('SearchProvider and CommandMenu', () => {
-  beforeEach(() => {
-    vi.clearAllMocks()
-  })
-
-  it('renders the command palette when the palette is open', async () => {
-    const screen = await renderWithSearchProvider()
-    const { getByPlaceholder, getByText } = screen
-
-    await openCommandPalette(screen)
-
-    await expect
-      .element(getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-      .toBeInTheDocument()
-    await expect.element(getByText('Theme')).toBeInTheDocument()
-    await expect.element(getByText('Light')).toBeInTheDocument()
-    await expect.element(getByText('Dark')).toBeInTheDocument()
-    await expect.element(getByText('System')).toBeInTheDocument()
-    await expect.element(getByText('Dashboard')).toBeInTheDocument()
-  })
-
-  it('does not show the dialog content when search is closed', async () => {
-    const { getByPlaceholder } = await renderWithSearchProvider()
-
-    await expect
-      .element(getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-      .not.toBeInTheDocument()
-  })
-
-  it.each([
-    ['Ctrl', 'Control'],
-    ['Cmd', 'Meta'],
-  ] as const)(
-    'opens the command menu when %s + K is pressed',
-    async (_label, modifier) => {
-      const screen = await renderWithSearchProvider()
-
-      await expect
-        .element(screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-        .not.toBeInTheDocument()
-
-      await openCommandPalette(screen, modifier)
-
-      await expect
-        .element(screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-        .toBeInTheDocument()
-    }
-  )
-
-  it('navigates to a top-level route and closes the palette when a nav item is selected', async () => {
-    const screen = await renderWithSearchProvider()
-
-    await openCommandPalette(screen)
-
-    await userEvent.click(screen.getByText('Tasks'))
-
-    expect(mocks.navigate).toHaveBeenCalledWith({ to: '/tasks' })
-    await expect
-      .element(screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-      .not.toBeInTheDocument()
-  })
-
-  it('navigates for nested sidebar items (group with sub-items)', async () => {
-    const screen = await renderWithSearchProvider()
-    const { getByPlaceholder, getByRole } = screen
-
-    await openCommandPalette(screen)
-
-    await userEvent.click(getByRole('option', { name: 'Settings Account' }))
-
-    expect(mocks.navigate).toHaveBeenCalledWith({ to: '/settings/account' })
-    await expect
-      .element(getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-      .not.toBeInTheDocument()
-  })
-
-  it('applies theme and closes the palette when a theme command is chosen', async () => {
-    const screen = await renderWithSearchProvider()
-
-    await openCommandPalette(screen)
-
-    await userEvent.click(screen.getByText('Dark'))
-
-    expect(mocks.setTheme).toHaveBeenCalledWith('dark')
-    await expect
-      .element(screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER))
-      .not.toBeInTheDocument()
-  })
-
-  it('shows empty state when the filter matches nothing', async () => {
-    const screen = await renderWithSearchProvider()
-
-    await openCommandPalette(screen)
-
-    await userEvent.fill(
-      screen.getByPlaceholder(COMMAND_MENU_PLACEHOLDER),
-      'zzzz-no-match-xxxx'
-    )
-
-    await expect
-      .element(screen.getByText('No results found.'))
-      .toBeInTheDocument()
-  })
-})

+ 2 - 2
src/core/api/axios.ts

@@ -33,9 +33,9 @@ api.interceptors.response.use(
     const status = error?.response?.status
 
     if (status === 401) {
-      const { logout } = useAuthStore.getState()
+      const { clearAuth } = useAuthStore.getState()
 
-      logout()
+      clearAuth()
 
       if (window.location.pathname !== '/sign-in') {
         const currentPath = window.location.pathname + window.location.search

+ 0 - 133
src/features/auth/sign-in/components/user-auth-form.test.tsx

@@ -1,133 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render, type RenderResult } from 'vitest-browser-react'
-import { type Locator, userEvent } from 'vitest/browser'
-import { UserAuthForm } from './user-auth-form'
-
-const FORM_MESSAGES = {
-  emailEmpty: 'Please enter your email.',
-  passwordEmpty: 'Please enter your password.',
-  passwordShort: 'Password must be at least 7 characters long.',
-} as const
-
-const navigate = vi.fn()
-const setUserMock = vi.fn()
-const setAccessTokenMock = vi.fn()
-
-vi.mock('@/stores/auth-store', () => ({
-  useAuthStore: () => ({
-    auth: {
-      setUser: setUserMock,
-      setAccessToken: setAccessTokenMock,
-    },
-  }),
-}))
-
-vi.mock('@tanstack/react-router', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('@tanstack/react-router')>()
-  return {
-    ...actual,
-    useNavigate: () => navigate,
-    Link: ({
-      children,
-      to,
-      className,
-      ...rest
-    }: {
-      children?: React.ReactNode
-      to: string
-      className?: string
-    }) => (
-      <a href={to} className={className} {...rest}>
-        {children}
-      </a>
-    ),
-  }
-})
-
-vi.mock('@/lib/utils', async (orig) => ({
-  ...(await orig()),
-  sleep: vi.fn(() => Promise.resolve()),
-}))
-
-describe('UserAuthForm', () => {
-  describe('Rendering without redirectTo', () => {
-    let screen: RenderResult
-    let emailInput: Locator
-    let passwordInput: Locator
-    let signInButton: Locator
-    let forgotPasswordLink: Locator
-
-    beforeEach(async () => {
-      vi.clearAllMocks()
-      screen = await render(<UserAuthForm />)
-      emailInput = screen.getByRole('textbox', { name: /^Email$/i })
-      passwordInput = screen.getByLabelText(/^Password$/i)
-      signInButton = screen.getByRole('button', { name: /^Sign in$/i })
-      forgotPasswordLink = screen.getByText(/^Forgot password\?$/i)
-    })
-
-    it('renders fields, submit button, and forgot password link', async () => {
-      await expect.element(emailInput).toBeInTheDocument()
-      await expect.element(passwordInput).toBeInTheDocument()
-      await expect.element(signInButton).toBeInTheDocument()
-      await expect.element(forgotPasswordLink).toBeInTheDocument()
-    })
-
-    it('shows validation messages when submitting empty form', async () => {
-      await userEvent.click(signInButton)
-
-      await expect
-        .element(screen.getByText(FORM_MESSAGES.emailEmpty))
-        .toBeInTheDocument()
-      await expect
-        .element(screen.getByText(FORM_MESSAGES.passwordEmpty))
-        .toBeInTheDocument()
-    })
-
-    it('authenticates and navigates to default route on success', async () => {
-      await userEvent.fill(emailInput, 'a@b.com')
-      await userEvent.fill(passwordInput, '1234567')
-
-      await userEvent.click(signInButton)
-
-      await vi.waitFor(() => expect(setUserMock).toHaveBeenCalledOnce())
-      expect(setUserMock).toHaveBeenCalledWith(
-        expect.objectContaining({
-          email: 'a@b.com',
-          accountNo: expect.any(String),
-          role: expect.any(Array),
-          exp: expect.any(Number),
-        })
-      )
-      expect(setAccessTokenMock).toHaveBeenCalledOnce()
-      expect(setAccessTokenMock).toHaveBeenCalledWith('mock-access-token')
-
-      await vi.waitFor(() =>
-        expect(navigate).toHaveBeenCalledWith({ to: '/', replace: true })
-      )
-    })
-  })
-
-  it('navigates to redirectTo when provided', async () => {
-    vi.clearAllMocks()
-
-    const { getByRole, getByLabelText } = await render(
-      <UserAuthForm redirectTo='/settings' />
-    )
-
-    await userEvent.fill(getByRole('textbox', { name: /Email/i }), 'a@b.com')
-    await userEvent.fill(getByLabelText('Password'), '1234567')
-
-    await userEvent.click(getByRole('button', { name: /Sign in/i }))
-
-    await vi.waitFor(() => expect(setUserMock).toHaveBeenCalledOnce())
-    expect(setAccessTokenMock).toHaveBeenCalledOnce()
-
-    await vi.waitFor(() =>
-      expect(navigate).toHaveBeenCalledWith({
-        to: '/settings',
-        replace: true,
-      })
-    )
-  })
-})

+ 0 - 123
src/features/tasks/components/tasks-import-dialog.test.tsx

@@ -1,123 +0,0 @@
-import { useState } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { TasksImportDialog } from './tasks-import-dialog'
-
-vi.mock('@/lib/show-submitted-data', () => ({ showSubmittedData: vi.fn() }))
-
-describe('TasksImportDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders the dialog with the correct title, description, file input and buttons', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole, getByText, getByLabelText } = await render(
-      <TasksImportDialog open onOpenChange={onOpenChange} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Import Tasks/i,
-    })
-    const desc = getByText('Import tasks quickly from a CSV file')
-    const fileInput = getByLabelText('File')
-    const closeButtons = getByRole('dialog')
-      .getByRole('button', { name: 'Close' })
-      .all()
-
-    const importButton = getByRole('button', { name: /^Import$/i })
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-    await expect.element(fileInput).toBeInTheDocument()
-    expect(closeButtons).toHaveLength(2)
-    await expect.element(importButton).toBeInTheDocument()
-  })
-
-  it('shows validation when submitting without a file', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole, getByText } = await render(
-      <TasksImportDialog open onOpenChange={onOpenChange} />
-    )
-
-    const importButton = getByRole('button', { name: /^Import$/i })
-    await userEvent.click(importButton)
-
-    await expect.element(getByText('Please upload a file.')).toBeInTheDocument()
-    expect(onOpenChange).not.toHaveBeenCalled()
-    expect(showSubmittedData).not.toHaveBeenCalled()
-  })
-
-  it('calls showSubmittedData and closes when a CSV file is imported', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole, getByLabelText } = await render(
-      <TasksImportDialog open onOpenChange={onOpenChange} />
-    )
-
-    const csv = new File(['a,b'], 'tasks.csv', { type: 'text/csv' })
-    await userEvent.upload(getByLabelText('File'), csv)
-
-    const importButton = getByRole('button', { name: /^Import$/i })
-    await userEvent.click(importButton)
-
-    expect(showSubmittedData).toHaveBeenCalledOnce()
-    expect(showSubmittedData).toHaveBeenCalledWith(
-      {
-        name: 'tasks.csv',
-        size: csv.size,
-        type: 'text/csv',
-      },
-      'You have imported the following file:'
-    )
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('closes the dialog when Close is clicked', async () => {
-    const onOpenChange = vi.fn()
-
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          <TasksImportDialog
-            open={open}
-            onOpenChange={(val) => {
-              onOpenChange(val)
-              setOpen(val)
-            }}
-          />
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const closeButtonX = getByRole('dialog')
-      .getByRole('button', {
-        name: /Close/i,
-      })
-      .nth(0)
-    await userEvent.click(closeButtonX)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-    expect(showSubmittedData).not.toHaveBeenCalled()
-
-    await userEvent.click(getByRole('button', { name: /Reopen/i }))
-    const closeButton = getByRole('dialog')
-      .getByRole('button', {
-        name: /Close/i,
-      })
-      .nth(1)
-    await userEvent.click(closeButton)
-
-    expect(onOpenChange).toHaveBeenCalledTimes(2)
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-    expect(showSubmittedData).not.toHaveBeenCalled()
-  })
-})

+ 0 - 163
src/features/tasks/components/tasks-multi-delete-dialog.test.tsx

@@ -1,163 +0,0 @@
-import { useState } from 'react'
-import { createTableMock } from '@/test-utils/tanstack-table'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { TasksMultiDeleteDialog } from './tasks-multi-delete-dialog'
-
-vi.mock('@/lib/utils', async (orig) => ({
-  ...(await orig()),
-  sleep: vi.fn(() => Promise.resolve()),
-}))
-
-describe('TasksMultiDeleteDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders the dialog with the correct title, description, input and buttons', async () => {
-    const { table } = createTableMock()
-
-    const { getByRole, getByText } = await render(
-      <TasksMultiDeleteDialog open onOpenChange={vi.fn()} table={table} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Delete 2 tasks/i,
-    })
-    const desc = getByText(
-      'Are you sure you want to delete the selected tasks?'
-    )
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-    await expect.element(confirmDeleteInput).toBeInTheDocument()
-    await expect.element(cancelButton).toBeInTheDocument()
-    await expect.element(deleteButton).toBeInTheDocument()
-    await expect.element(deleteButton).toBeDisabled()
-  })
-
-  it('keeps the delete button disabled until the confirm delete input is filled correctly', async () => {
-    const { table } = createTableMock()
-    const { getByRole } = await render(
-      <TasksMultiDeleteDialog open onOpenChange={vi.fn()} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'wrong-input')
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-  })
-
-  it('closes the dialog when the cancel button is clicked', async () => {
-    const onOpenChange = vi.fn()
-    const { table } = createTableMock()
-    const { getByRole } = await render(
-      <TasksMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('resets the confirm delete input when the dialog is closed and reopened', async () => {
-    const { table } = createTableMock()
-
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          {open ? (
-            <TasksMultiDeleteDialog
-              open={open}
-              onOpenChange={setOpen}
-              table={table}
-            />
-          ) : null}
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(confirmDeleteInput).toHaveValue('DELETE')
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    const reopenButton = getByRole('button', { name: /Reopen/i })
-    await userEvent.click(reopenButton)
-    await expect.element(confirmDeleteInput).toHaveValue('')
-  })
-
-  it('shows the submitted data when deleted successfully', async () => {
-    const { table, resetRowSelection } = createTableMock()
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <TasksMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.click(deleteButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    await vi.waitFor(() => expect(resetRowSelection).toHaveBeenCalledOnce())
-  })
-
-  it('deletes successfully when press Enter key on the confirm delete input', async () => {
-    const { table, resetRowSelection } = createTableMock()
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <TasksMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.keyboard('{Enter}')
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    await vi.waitFor(() => expect(resetRowSelection).toHaveBeenCalledOnce())
-  })
-})

+ 0 - 179
src/features/tasks/components/tasks-mutate-drawer.test.tsx

@@ -1,179 +0,0 @@
-import { useState } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { type Task } from '../data/schema'
-import { TasksMutateDrawer } from './tasks-mutate-drawer'
-
-vi.mock('@/lib/show-submitted-data', () => ({ showSubmittedData: vi.fn() }))
-
-const MOCK_TASK = {
-  id: 'task-1',
-  title: 'Existing task',
-  status: 'in progress',
-  label: 'feature',
-  priority: 'medium',
-} as const satisfies Task
-
-describe('TasksMutateDrawer', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders create title and description', async () => {
-    const { getByRole, getByText } = await render(
-      <TasksMutateDrawer open onOpenChange={vi.fn()} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Create Task/i,
-    })
-    const desc = getByText(/Add a new task/i)
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-  })
-
-  it('renders edit title, description, and prefilled title', async () => {
-    const { getByRole, getByText } = await render(
-      <TasksMutateDrawer open onOpenChange={vi.fn()} currentRow={MOCK_TASK} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Update Task/i,
-    })
-    const desc = getByText(/Update the task/i)
-
-    const titleInput = getByRole('textbox', { name: /Title/i })
-    const statusSelect = getByRole('combobox', { name: /Status/i })
-    const labelRadio = getByRole('radio', { name: MOCK_TASK.label })
-    const priorityRadio = getByRole('radio', { name: MOCK_TASK.priority })
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-    await expect.element(titleInput).toHaveValue(MOCK_TASK.title)
-    await expect
-      .element(statusSelect)
-      .toHaveTextContent(new RegExp(MOCK_TASK.status, 'i'))
-    await expect.element(labelRadio).toBeChecked()
-    await expect.element(priorityRadio).toBeChecked()
-  })
-
-  it('shows validation messages when submitting an empty form', async () => {
-    const { getByRole, getByText } = await render(
-      <TasksMutateDrawer open onOpenChange={vi.fn()} />
-    )
-
-    const saveButton = getByRole('button', { name: /Save changes/i })
-    await userEvent.click(saveButton)
-
-    await expect.element(getByText(/Title is required.$/i)).toBeInTheDocument()
-    await expect
-      .element(getByText(/Please select a status.$/i))
-      .toBeInTheDocument()
-    await expect
-      .element(getByText(/Please select a label.$/i))
-      .toBeInTheDocument()
-    await expect
-      .element(getByText(/Please choose a priority.$/i))
-      .toBeInTheDocument()
-  })
-
-  it('submits create form and shows submitted data', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <TasksMutateDrawer open onOpenChange={onOpenChange} />
-    )
-
-    const titleInput = getByRole('textbox', { name: /Title/i })
-    await userEvent.fill(titleInput, 'New task title')
-
-    const statusSelect = getByRole('combobox', { name: /Status/i })
-    await userEvent.click(statusSelect)
-    await userEvent.click(getByRole('option', { name: /Todo/i }))
-
-    await userEvent.click(getByRole('radio', { name: /^Bug$/i }))
-    await userEvent.click(getByRole('radio', { name: /^Low$/i }))
-
-    const saveButton = getByRole('button', { name: /Save changes/i })
-    await userEvent.click(saveButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    expect(showSubmittedData).toHaveBeenCalledOnce()
-    expect(showSubmittedData).toHaveBeenCalledWith({
-      title: 'New task title',
-      status: 'todo',
-      label: 'bug',
-      priority: 'low',
-    })
-  })
-
-  it('closes when Close is clicked', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <TasksMutateDrawer open onOpenChange={onOpenChange} />
-    )
-
-    const closeButtons = getByRole('dialog')
-      .getByRole('button', {
-        name: /Close/i,
-      })
-      .all()
-    expect(closeButtons).toHaveLength(2)
-    await userEvent.click(closeButtons[1])
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('resets entered values when the sheet is closed and reopened', async () => {
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          <TasksMutateDrawer open={open} onOpenChange={setOpen} />
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const titleInput = getByRole('textbox', { name: /Title/i })
-    await userEvent.fill(titleInput, 'Draft title')
-    await expect.element(titleInput).toHaveValue('Draft title')
-
-    const statusSelect = getByRole('combobox', { name: /Status/i })
-    await userEvent.click(statusSelect)
-    await userEvent.click(getByRole('option', { name: /Todo/i }))
-    await expect.element(statusSelect).toHaveTextContent(/Todo/i)
-
-    const labelRadio = getByRole('radio', { name: /^Documentation$/i })
-    await userEvent.click(labelRadio)
-    await expect.element(labelRadio).toBeChecked()
-
-    const priorityRadio = getByRole('radio', { name: /^High$/i })
-    await userEvent.click(priorityRadio)
-    await expect.element(priorityRadio).toBeChecked()
-
-    const closeButtons = getByRole('dialog')
-      .getByRole('button', {
-        name: /Close/i,
-      })
-      .all()
-    await userEvent.click(closeButtons[0])
-
-    const reopenButton = getByRole('button', { name: /Reopen/i })
-    await userEvent.click(reopenButton)
-
-    await expect.element(titleInput).toHaveValue('')
-    await expect.element(statusSelect).not.toHaveTextContent(/Todo/i)
-    await expect.element(labelRadio).not.toBeChecked()
-    await expect.element(priorityRadio).not.toBeChecked()
-  })
-})

+ 0 - 376
src/hooks/use-table-url-state.test.ts

@@ -1,376 +0,0 @@
-import { type Mock, describe, expect, it, vi } from 'vitest'
-import { renderHook } from 'vitest-browser-react'
-import { type NavigateFn, useTableUrlState } from '@/hooks/use-table-url-state'
-
-function lastNavigateOpts(navigate: Mock<NavigateFn>) {
-  const calls = navigate.mock.calls
-  return calls[calls.length - 1]?.[0]
-}
-
-function applyLastSearchFn(
-  navigate: Mock<NavigateFn>,
-  prev: Record<string, unknown>
-) {
-  const opts = lastNavigateOpts(navigate)
-  if (!opts) return undefined
-  const s = opts.search
-  if (typeof s === 'function') {
-    return s(prev) as Record<string, unknown>
-  }
-  return s as Record<string, unknown>
-}
-
-describe('useTableUrlState', () => {
-  it('derives pagination from search with defaults', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 3, pageSize: 25 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    expect(result.current.pagination).toEqual({
-      pageIndex: 2,
-      pageSize: 25,
-    })
-  })
-
-  it('uses default page and pageSize when search omits them', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: {},
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    expect(result.current.pagination).toEqual({
-      pageIndex: 0,
-      pageSize: 10,
-    })
-  })
-
-  it('clamps negative effective page via pageIndex', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 0 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    expect(result.current.pagination.pageIndex).toBe(0)
-  })
-
-  it('onPaginationChange omits page and pageSize from search when they match defaults', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const prev = { page: 2, pageSize: 20, filter: 'q' }
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: prev,
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    await act(() => {
-      result.current.onPaginationChange({
-        pageIndex: 0,
-        pageSize: 10,
-      })
-    })
-
-    expect(applyLastSearchFn(navigate, prev)).toMatchObject({
-      page: undefined,
-      pageSize: undefined,
-      filter: 'q',
-    })
-  })
-
-  it('onPaginationChange writes non-default page and pageSize', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const prev = { filter: 'x' }
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { ...prev, page: 1, pageSize: 10 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    await act(() => {
-      result.current.onPaginationChange({
-        pageIndex: 2,
-        pageSize: 25,
-      })
-    })
-
-    expect(applyLastSearchFn(navigate, prev)).toMatchObject({
-      page: 3,
-      pageSize: 25,
-      filter: 'x',
-    })
-  })
-
-  it('supports custom pagination search keys', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { p: 2, ps: 5 },
-        navigate,
-        pagination: {
-          pageKey: 'p',
-          pageSizeKey: 'ps',
-          defaultPage: 1,
-          defaultPageSize: 10,
-        },
-      })
-    )
-
-    expect(result.current.pagination).toEqual({
-      pageIndex: 1,
-      pageSize: 5,
-    })
-  })
-
-  it('reads globalFilter from search and onGlobalFilterChange updates URL and clears page', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 2, filter: 'hello' },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        globalFilter: { enabled: true, key: 'filter' },
-      })
-    )
-
-    expect(result.current.globalFilter).toBe('hello')
-
-    await act(() => {
-      result.current.onGlobalFilterChange?.('  next  ')
-    })
-
-    expect(applyLastSearchFn(navigate, { page: 2, filter: 'hello' })).toEqual({
-      page: undefined,
-      filter: 'next',
-    })
-  })
-
-  it('clears filter key in URL when global filter becomes empty after trim', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { filter: 'x' },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        globalFilter: { enabled: true, key: 'filter' },
-      })
-    )
-
-    await act(() => {
-      result.current.onGlobalFilterChange?.('   ')
-    })
-
-    expect(applyLastSearchFn(navigate, { filter: 'x' })).toMatchObject({
-      filter: undefined,
-    })
-  })
-
-  it('does not trim global filter when trim is false', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: {},
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        globalFilter: { enabled: true, key: 'filter', trim: false },
-      })
-    )
-
-    await act(() => {
-      result.current.onGlobalFilterChange?.('  spaced  ')
-    })
-
-    expect(applyLastSearchFn(navigate, {})).toMatchObject({
-      filter: '  spaced  ',
-    })
-  })
-
-  it('omits globalFilter and onGlobalFilterChange when global filter is disabled', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { filter: 'ignored' },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        globalFilter: { enabled: false },
-      })
-    )
-
-    expect(result.current.globalFilter).toBeUndefined()
-    expect(result.current.onGlobalFilterChange).toBeUndefined()
-  })
-
-  it('builds array column filters from search', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { status: ['todo', 'done'], priority: ['high'] },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        columnFilters: [
-          { columnId: 'status', searchKey: 'status', type: 'array' },
-          { columnId: 'priority', searchKey: 'priority', type: 'array' },
-        ],
-      })
-    )
-
-    expect(result.current.columnFilters).toEqual([
-      { id: 'status', value: ['todo', 'done'] },
-      { id: 'priority', value: ['high'] },
-    ])
-  })
-
-  it('builds string column filters from search', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result } = await renderHook(() =>
-      useTableUrlState({
-        search: { q: '  find me  ' },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        columnFilters: [{ columnId: 'title', searchKey: 'q', type: 'string' }],
-      })
-    )
-
-    expect(result.current.columnFilters).toEqual([
-      { id: 'title', value: '  find me  ' },
-    ])
-  })
-
-  it('onColumnFiltersChange merges serialized filters into search and clears page', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const prev = { page: 3, status: ['old'], other: 1 }
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: prev,
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        columnFilters: [
-          { columnId: 'status', searchKey: 'status', type: 'array' },
-          { columnId: 'priority', searchKey: 'priority', type: 'array' },
-        ],
-      })
-    )
-
-    await act(() => {
-      result.current.onColumnFiltersChange([
-        { id: 'status', value: ['todo'] },
-        { id: 'priority', value: [] },
-      ])
-    })
-
-    expect(applyLastSearchFn(navigate, prev)).toEqual({
-      page: undefined,
-      status: ['todo'],
-      priority: undefined,
-      other: 1,
-    })
-  })
-
-  it('ensurePageInRange navigates with replace when current page exceeds pageCount', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 5 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    await act(() => {
-      result.current.ensurePageInRange(2)
-    })
-
-    expect(navigate).toHaveBeenCalledTimes(1)
-    expect(lastNavigateOpts(navigate)?.replace).toBe(true)
-    expect(applyLastSearchFn(navigate, { page: 5, filter: 'x' })).toMatchObject(
-      {
-        page: undefined,
-        filter: 'x',
-      }
-    )
-  })
-
-  it('ensurePageInRange resets to last page when resetTo is last', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 9 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    await act(() => {
-      result.current.ensurePageInRange(3, { resetTo: 'last' })
-    })
-
-    expect(lastNavigateOpts(navigate)?.replace).toBe(true)
-    expect(applyLastSearchFn(navigate, { page: 9 })).toMatchObject({
-      page: 3,
-    })
-  })
-
-  it('ensurePageInRange does not navigate when page is in range', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { page: 2 },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-      })
-    )
-
-    await act(() => {
-      result.current.ensurePageInRange(5)
-    })
-
-    expect(navigate).not.toHaveBeenCalled()
-  })
-
-  it('uses custom serialize and deserialize for column filters', async () => {
-    const navigate = vi.fn() as Mock<NavigateFn>
-    const { result, act } = await renderHook(() =>
-      useTableUrlState({
-        search: { tag: 'a|b' },
-        navigate,
-        pagination: { defaultPage: 1, defaultPageSize: 10 },
-        columnFilters: [
-          {
-            columnId: 'tag',
-            searchKey: 'tag',
-            type: 'array',
-            deserialize: (v) => (typeof v === 'string' ? v.split('|') : []),
-            serialize: (v) => (Array.isArray(v) ? v.join('|') : v),
-          },
-        ],
-      })
-    )
-
-    expect(result.current.columnFilters).toEqual([
-      { id: 'tag', value: ['a', 'b'] },
-    ])
-
-    await act(() => {
-      result.current.onColumnFiltersChange([{ id: 'tag', value: ['x', 'y'] }])
-    })
-
-    expect(applyLastSearchFn(navigate, { tag: 'a|b' })).toMatchObject({
-      tag: 'x|y',
-    })
-  })
-})

+ 0 - 34
src/lib/cookies.test.ts

@@ -1,34 +0,0 @@
-import { clearCookies } from '@/test-utils/cookies'
-import { beforeEach, describe, expect, it } from 'vitest'
-import { getCookie, removeCookie, setCookie } from './cookies'
-
-const COOKIE_PREFIX = 'test_cookie_'
-
-describe('cookies', () => {
-  const uniqueName = () =>
-    `${COOKIE_PREFIX}${Math.random().toString(36).slice(2)}`
-
-  beforeEach(() => {
-    clearCookies(COOKIE_PREFIX)
-  })
-
-  it('stores a value that can be read back', () => {
-    const name = uniqueName()
-    const value = 'hello-world'
-
-    setCookie(name, value)
-
-    expect(getCookie(name)).toBe(value)
-  })
-
-  it('clears a value so it is no longer readable', () => {
-    const name = uniqueName()
-
-    setCookie(name, 'x')
-    expect(getCookie(name)).toBe('x')
-
-    removeCookie(name)
-
-    expect(getCookie(name)).toBeUndefined()
-  })
-})

+ 0 - 90
src/lib/handle-server-error.test.ts

@@ -1,90 +0,0 @@
-import { AxiosError } from 'axios'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { handleServerError } from './handle-server-error'
-
-const toastError = vi.hoisted(() => vi.fn())
-
-vi.mock('sonner', () => ({
-  toast: {
-    error: toastError,
-  },
-}))
-
-beforeEach(() => {
-  vi.mocked(toastError).mockClear()
-})
-
-describe('handleServerError', () => {
-  it('shows a generic message when the error is not recognised', () => {
-    handleServerError(new Error('network'))
-
-    expect(toastError).toHaveBeenCalledWith('Something went wrong!')
-  })
-
-  it('maps a plain object with status 204 to the no-content message', () => {
-    handleServerError({ status: 204 })
-
-    expect(toastError).toHaveBeenCalledWith('No content.')
-  })
-
-  it('prefers the API title when the error is an Axios error with response data', () => {
-    const error = new AxiosError('Bad request')
-    error.response = {
-      status: 422,
-      data: { title: 'Validation failed' },
-    } as AxiosError['response']
-
-    handleServerError(error)
-
-    expect(toastError).toHaveBeenCalledWith('Validation failed')
-  })
-
-  it('falls back to the generic message when Axios response has no data.title', () => {
-    const error = new AxiosError('Request failed')
-    error.response = {
-      status: 500,
-      data: {},
-    } as AxiosError['response']
-
-    handleServerError(error)
-
-    expect(toastError).toHaveBeenCalledWith('Something went wrong!')
-  })
-
-  it('falls back to the generic message when Axios data.title is an empty string', () => {
-    const error = new AxiosError('Bad request')
-    error.response = {
-      status: 400,
-      data: { title: '' },
-    } as AxiosError['response']
-
-    handleServerError(error)
-
-    expect(toastError).toHaveBeenCalledWith('Something went wrong!')
-  })
-
-  it('logs the error to the console in development', () => {
-    const log = vi.spyOn(console, 'log').mockImplementation(() => {})
-    const err = new Error('logged')
-
-    handleServerError(err)
-
-    expect(log).toHaveBeenCalledTimes(1)
-    expect(log).toHaveBeenCalledWith(err)
-
-    log.mockRestore()
-  })
-
-  it('does not log the error to the console in production', () => {
-    vi.stubEnv('DEV', false)
-
-    const log = vi.spyOn(console, 'log').mockImplementation(() => {})
-    const err = new Error('not logged')
-
-    handleServerError(err)
-
-    expect(log).not.toHaveBeenCalled()
-
-    log.mockRestore()
-  })
-})

+ 0 - 28
src/lib/utils.test.ts

@@ -1,28 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { getPageNumbers } from './utils'
-
-describe('getPageNumbers', () => {
-  it('returns all pages when total is at most 5', () => {
-    expect(getPageNumbers(1, 3)).toEqual([1, 2, 3])
-    expect(getPageNumbers(3, 5)).toEqual([1, 2, 3, 4, 5])
-  })
-
-  it('shows ellipsis near the beginning', () => {
-    expect(getPageNumbers(1, 10)).toEqual([1, 2, 3, 4, '...', 10])
-    expect(getPageNumbers(3, 10)).toEqual([1, 2, 3, 4, '...', 10])
-  })
-
-  it('shows ellipsis near the end', () => {
-    expect(getPageNumbers(10, 10)).toEqual([1, '...', 7, 8, 9, 10])
-    expect(getPageNumbers(9, 10)).toEqual([1, '...', 7, 8, 9, 10])
-  })
-
-  it('shows ellipsis on both side in the middle', () => {
-    expect(getPageNumbers(5, 10)).toEqual([1, '...', 4, 5, 6, '...', 10])
-  })
-
-  it('handles current page greater than total pages', () => {
-    expect(getPageNumbers(6, 5)).toEqual([1, 2, 3, 4, 5])
-    expect(getPageNumbers(11, 10)).toEqual([1, '...', 7, 8, 9, 10])
-  })
-})