appearance-row.client.spec.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. // @vitest-environment jsdom
  2. import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
  5. import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client'
  6. import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
  7. import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
  8. import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
  9. import { AppearanceRow } from '../src/client/AppearanceRow.tsx'
  10. import type { AppearanceRowComponentProps } from '../src/client/AppearanceRow.tsx'
  11. import { createAppearanceRowStore } from '../src/client/settings-store.ts'
  12. import type { ThemePreference } from '../src/client/index.ts'
  13. // Every fixture carries the resource hook the resources plugin merges into GlobalStandardProps.
  14. const useResource = (() => ({ status: 'none' as const, value: undefined, failure: undefined, reload: () => {} })) as GlobalStandardProps['useResource']
  15. const usePanelInfo: GlobalStandardProps['usePanelInfo'] = selector => selector({ activePanelId: null })
  16. afterEach(cleanup)
  17. const COPY: Record<string, string> = {
  18. 'appearance.title': 'Appearance',
  19. 'appearance.light': 'Light',
  20. 'appearance.dark': 'Dark',
  21. 'appearance.system': 'System',
  22. }
  23. function emptySessions() {
  24. const store = createSnapshotStore<SessionListState>(
  25. { ids: [], byId: {}, phase: 'ready', subagentsByParent: {}, jobsBySession: {} })
  26. return bindSnapshotSelector(store)
  27. }
  28. function emptyWorkspaces() {
  29. const store = createSnapshotStore<WorkspaceSnapshot>({
  30. items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,
  31. })
  32. return bindSnapshotSelector(store)
  33. }
  34. type AttentionSnapshot = Parameters<Parameters<AppearanceRowComponentProps['useSessionStatus']>[0]>[0]
  35. const noAttention: AttentionSnapshot = new Map()
  36. const useSessionStatus: AppearanceRowComponentProps['useSessionStatus'] = selector => selector(noAttention)
  37. function mount(preference: ThemePreference = 'system') {
  38. // Real store instance — the sanctioned zero-machinery path for tests.
  39. const store = createAppearanceRowStore().create()
  40. store.actions.sync(preference, 0)
  41. const setTheme = vi.fn()
  42. const props: AppearanceRowComponentProps = {
  43. useSessions: emptySessions(),
  44. useSessionStatus,
  45. usePanelInfo, useSessionRetainInfo: () => undefined, useResource,
  46. useWorkspaces: emptyWorkspaces(),
  47. useStore: bindSnapshotSelector(store),
  48. actions: store.actions,
  49. t: (key: string) => COPY[key] ?? key,
  50. setTheme,
  51. }
  52. render(<AppearanceRow {...props} />)
  53. return { store, setTheme }
  54. }
  55. const pressed = (name: RegExp): string | null =>
  56. screen.getByRole('button', { name }).getAttribute('aria-pressed')
  57. describe('AppearanceRow', () => {
  58. it('renders the title and three cubes with the preference cube selected', () => {
  59. mount('dark')
  60. expect(screen.getByText('Appearance')).toBeDefined()
  61. expect(pressed(/Dark/)).toBe('true')
  62. expect(pressed(/Light/)).toBe('false')
  63. expect(pressed(/System/)).toBe('false')
  64. })
  65. it('click drives setTheme; selection follows the store mirror, not the click echo', () => {
  66. const b = mount('dark')
  67. fireEvent.click(screen.getByRole('button', { name: /Light/ }))
  68. expect(b.setTheme).toHaveBeenCalledWith('light')
  69. // No store write yet: selection is unchanged.
  70. expect(pressed(/Dark/)).toBe('true')
  71. act(() => { b.store.actions.sync('light', 1) })
  72. expect(pressed(/Light/)).toBe('true')
  73. expect(pressed(/Dark/)).toBe('false')
  74. })
  75. })