sidebar-root.spec.tsx 3.7 KB

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