sidebar-root.spec.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. // @vitest-environment jsdom
  2. import { afterEach, describe, expect, it, vi } from 'vitest'
  3. import { cleanup, fireEvent, render, screen } from '@testing-library/react'
  4. import type { SidebarRootComponentProps, SidebarSectionOwnerProps, SidebarSettingsOwnerProps } from '../src/client/contract/slots.ts'
  5. import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
  6. import { en } from '../src/client/locales.ts'
  7. // English-dictionary translate stub: the shell renders the same copy the
  8. // assertions below query by accessible name.
  9. const t: SidebarRootComponentProps['t'] = key => (en as Record<string, string>)[key] ?? key
  10. afterEach(() => {
  11. cleanup()
  12. vi.useRealTimers()
  13. })
  14. // The shell never reads the global hooks itself, but they ride the standard
  15. // props share; stub them as never-called functions.
  16. const neverHook = (() => { throw new Error('shell must not read global hooks') }) as never
  17. function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; width?: number } = {}) {
  18. const startSession = vi.fn()
  19. const toggleSidebar = vi.fn()
  20. let regionOwner: SidebarSectionOwnerProps | undefined
  21. let settingsOwner: SidebarSettingsOwnerProps | undefined
  22. let current = { collapsed, width }
  23. const root = () => (
  24. <SidebarRoot
  25. collapsed={current.collapsed} width={current.width}
  26. useSessions={neverHook} useWorkspaces={neverHook}
  27. startSession={startSession} toggleSidebar={toggleSidebar} t={t}
  28. renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
  29. if (key === 'sidebar.settings') {
  30. settingsOwner = owner
  31. return <div data-testid="settings-seat" data-wide={owner.wide} />
  32. }
  33. regionOwner = owner as SidebarSectionOwnerProps
  34. return <div data-testid="region" data-wide={owner.wide} />
  35. }) as SidebarRootComponentProps['renderSlot']}
  36. />
  37. )
  38. const view = render(root())
  39. return {
  40. startSession,
  41. toggleSidebar,
  42. regionOwner: () => {
  43. if (regionOwner === undefined) throw new Error('region owner not rendered')
  44. return regionOwner
  45. },
  46. settingsOwner: () => {
  47. if (settingsOwner === undefined) throw new Error('settings owner not rendered')
  48. return settingsOwner
  49. },
  50. rerender(next: Partial<typeof current>) {
  51. current = { ...current, ...next }
  52. view.rerender(root())
  53. },
  54. }
  55. }
  56. describe('SidebarRoot shell', () => {
  57. it('routes New Session (capsule + wordmark) and the column toggle', () => {
  58. const b = mountShell()
  59. // Expanded, both the wordmark and the capsule start a session.
  60. const starters = screen.getAllByRole('button', { name: 'New session' })
  61. expect(starters).toHaveLength(2)
  62. for (const button of starters) fireEvent.click(button)
  63. expect(b.startSession).toHaveBeenCalledTimes(2)
  64. fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }))
  65. expect(b.toggleSidebar).toHaveBeenCalledOnce()
  66. })
  67. it('hands the region its wide flag and clamps expandSidebar to the collapsed state', () => {
  68. const b = mountShell()
  69. expect(b.regionOwner().wide).toBe(true)
  70. // The settings seat rides the same wide flag (ui-settings renders the row).
  71. expect(b.settingsOwner().wide).toBe(true)
  72. // Expanded: the request is a no-op (no accidental collapse).
  73. b.regionOwner().expandSidebar()
  74. expect(b.toggleSidebar).not.toHaveBeenCalled()
  75. })
  76. it('keeps the region mounted through collapse and expands on its request', () => {
  77. vi.useFakeTimers()
  78. const b = mountShell()
  79. b.rerender({ collapsed: true })
  80. // Wide content survives the crossfade window, then settles into the rail.
  81. expect(b.regionOwner().wide).toBe(true)
  82. vi.advanceTimersByTime(200)
  83. b.rerender({})
  84. expect(b.regionOwner().wide).toBe(false)
  85. expect(screen.getByTestId('region')).toBeTruthy()
  86. b.regionOwner().expandSidebar()
  87. expect(b.toggleSidebar).toHaveBeenCalledOnce()
  88. })
  89. it('renders statically collapsed on a cold start (no crossfade classes)', () => {
  90. const b = mountShell({ collapsed: true })
  91. expect(b.regionOwner().wide).toBe(false)
  92. expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
  93. })
  94. })