Преглед изворни кода

test: add tests for config drawer

satnaing пре 2 месеци
родитељ
комит
983d81e68c
2 измењених фајлова са 360 додато и 2 уклоњено
  1. 350 0
      src/components/config-drawer.test.tsx
  2. 10 2
      src/components/config-drawer.tsx

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

@@ -0,0 +1,350 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { render, type RenderResult } from 'vitest-browser-react'
+import { userEvent } from 'vitest/browser'
+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'
+
+function getCookieValue(name: string) {
+  const match = document.cookie.match(
+    new RegExp(
+      `(?:^|; )${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}=([^;]*)`
+    )
+  )
+  return match ? decodeURIComponent(match[1]) : null
+}
+
+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 vi.waitFor(() =>
+    expect(screen.getByText(/^Theme Settings$/i)).toBeInTheDocument()
+  )
+}
+
+describe('ConfigDrawer (integration)', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+
+    for (const cookie of document.cookie.split(';')) {
+      const name = cookie.split('=')[0]?.trim()
+      if (!name) continue
+      document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
+    }
+
+    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)
+
+    expect(screen.getByText(/^Theme$/i)).toBeInTheDocument()
+    expect(screen.getByText(/^Layout$/i)).toBeInTheDocument()
+    expect(screen.getByText(/^Direction$/i)).toBeInTheDocument()
+    expect(
+      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(getCookieValue('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(getCookieValue('vite-ui-theme')).toBe('dark')
+    })
+
+    it('applies system theme: stores cookie and applies a resolved light or dark class', async () => {
+      const screen = await renderConfigDrawer()
+      await openDrawer(screen)
+      // Initial state is already "system" with no cookie; switch away first so System triggers setTheme.
+      await userEvent.click(
+        screen.getByRole('radio', { name: /select light/i })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('vite-ui-theme')).toBe('light')
+      )
+      await userEvent.click(
+        screen.getByRole('radio', { name: /select system/i })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('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(getCookieValue('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(getCookieValue('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(getCookieValue('layout_variant')).toBe('floating')
+      )
+
+      await userEvent.click(
+        screen.getByRole('radio', { name: /select inset/i })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('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(getCookieValue('layout_collapsible')).toBe('offcanvas')
+    )
+    await vi.waitFor(() =>
+      expect(getCookieValue('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(getCookieValue('vite-ui-theme')).toBe('dark')
+      )
+
+      await userEvent.click(
+        screen.getByRole('button', {
+          name: /reset theme preference to default/i,
+        })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('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(getCookieValue('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(getCookieValue('layout_variant')).toBe('floating')
+      )
+
+      await userEvent.click(
+        screen.getByRole('button', {
+          name: /reset sidebar style to default/i,
+        })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('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(getCookieValue('sidebar_state')).toBe('false')
+      )
+
+      await userEvent.click(
+        screen.getByRole('button', {
+          name: /reset layout options to default/i,
+        })
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('sidebar_state')).toBe('true')
+      )
+      await vi.waitFor(() =>
+        expect(getCookieValue('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(getCookieValue('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)
+
+    expect(
+      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(getCookieValue('sidebar_state')).toBe('false')
+    )
+    await vi.waitFor(() =>
+      expect(getCookieValue('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(getCookieValue('vite-ui-theme')).toBe('dark'))
+    await vi.waitFor(() => expect(getCookieValue('dir')).toBe('rtl'))
+    await vi.waitFor(() =>
+      expect(getCookieValue('layout_variant')).toBe('floating')
+    )
+    await vi.waitFor(() =>
+      expect(getCookieValue('layout_collapsible')).toBe('offcanvas')
+    )
+
+    await userEvent.click(
+      screen.getByRole('button', {
+        name: /reset all settings to default values/i,
+      })
+    )
+
+    await vi.waitFor(() => expect(getCookieValue('sidebar_state')).toBe('true'))
+    await vi.waitFor(() => expect(getCookieValue('dir')).toBe(null))
+    await vi.waitFor(() => expect(getCookieValue('vite-ui-theme')).toBe(null))
+    await vi.waitFor(() =>
+      expect(getCookieValue('layout_variant')).toBe('inset')
+    )
+    await vi.waitFor(() =>
+      expect(getCookieValue('layout_collapsible')).toBe('icon')
+    )
+    await vi.waitFor(() =>
+      expect(document.documentElement.getAttribute('dir')).toBe('ltr')
+    )
+  })
+})

+ 10 - 2
src/components/config-drawer.tsx

@@ -47,7 +47,6 @@ export function ConfigDrawer() {
           size='icon'
           variant='ghost'
           aria-label='Open theme settings'
-          aria-describedby='config-drawer-description'
           className='rounded-full'
         >
           <Settings aria-hidden='true' />
@@ -56,7 +55,7 @@ export function ConfigDrawer() {
       <SheetContent className='flex flex-col'>
         <SheetHeader className='pb-0 text-start'>
           <SheetTitle>Theme Settings</SheetTitle>
-          <SheetDescription id='config-drawer-description'>
+          <SheetDescription>
             Adjust the appearance and layout to suit your preferences.
           </SheetDescription>
         </SheetHeader>
@@ -84,11 +83,14 @@ function SectionTitle({
   title,
   showReset = false,
   onReset,
+  resetAriaLabel,
   className,
 }: {
   title: string
   showReset?: boolean
   onReset?: () => void
+  /** Shown on the small per-section reset (RotateCcw) for accessibility and tests. */
+  resetAriaLabel?: string
   className?: string
 }) {
   return (
@@ -101,10 +103,12 @@ function SectionTitle({
       {title}
       {showReset && onReset && (
         <Button
+          type='button'
           size='icon'
           variant='secondary'
           className='size-4 rounded-full'
           onClick={onReset}
+          aria-label={resetAriaLabel}
         >
           <RotateCcw className='size-3' />
         </Button>
@@ -176,6 +180,7 @@ function ThemeConfig() {
         title='Theme'
         showReset={theme !== defaultTheme}
         onReset={() => setTheme(defaultTheme)}
+        resetAriaLabel='Reset theme preference to default'
       />
       <Radio
         value={theme}
@@ -219,6 +224,7 @@ function SidebarConfig() {
         title='Sidebar'
         showReset={defaultVariant !== variant}
         onReset={() => setVariant(defaultVariant)}
+        resetAriaLabel='Reset sidebar style to default'
       />
       <Radio
         value={variant}
@@ -269,6 +275,7 @@ function LayoutConfig() {
           setOpen(true)
           setCollapsible(defaultCollapsible)
         }}
+        resetAriaLabel='Reset layout options to default'
       />
       <Radio
         value={radioState}
@@ -319,6 +326,7 @@ function DirConfig() {
         title='Direction'
         showReset={defaultDir !== dir}
         onReset={() => setDir(defaultDir)}
+        resetAriaLabel='Reset text direction to default'
       />
       <Radio
         value={dir}