mattrguerra/cortexmainPrivate on GitHub, published here in full

components.test.tsx

Language TypeScript8.0 KB178 lines

src/components/components.test.tsx

  1. import { act, fireEvent, render, screen } from '@testing-library/react'
  2. import { beforeEach, describe, expect, it } from 'vitest'
  3. import type { IpcClient } from '../ipc/client'
  4. import { createMockClient } from '../ipc/mock/mockClient'
  5. import { useStore } from '../state/store'
  6. import { Markdown } from './Markdown'
  7. import { prettyShortcut, SettingsPanel, shortcutFrom } from './SettingsPanel'
  8. import { Sidebar } from './Sidebar'
  9. const key = (code: string, mods: Partial<Record<'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey', boolean>> = {}) => ({
  10. code,
  11. metaKey: false,
  12. ctrlKey: false,
  13. altKey: false,
  14. shiftKey: false,
  15. ...mods,
  16. })
  17. describe('shortcut recorder', () => {
  18. it('builds global-shortcut strings', () => {
  19. expect(shortcutFrom(key('Space', { metaKey: true, shiftKey: true }), true)).toBe('CommandOrControl+Shift+Space')
  20. expect(shortcutFrom(key('KeyJ', { ctrlKey: true, altKey: true }), false)).toBe('CommandOrControl+Alt+J')
  21. expect(shortcutFrom(key('KeyJ'), true)).toBeNull() // a bare key is not a global shortcut
  22. expect(shortcutFrom(key('ArrowUp', { metaKey: true }), true)).toBeNull()
  23. expect(prettyShortcut('CommandOrControl+Shift+Space', true)).toBe('⌘⇧Space')
  24. expect(prettyShortcut('CommandOrControl+Shift+Space', false)).toBe('Ctrl+Shift+Space')
  25. })
  26. })
  27. describe('markdown', () => {
  28. it('renders code and tables, and never raw HTML', () => {
  29. const { container } = render(
  30. <Markdown
  31. text={
  32. '**bold** <script>alert(1)</script><img src=x onerror=alert(1)>\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n```bash\necho hi\n```'
  33. }
  34. />,
  35. )
  36. expect(container.querySelector('strong')?.textContent).toBe('bold')
  37. expect(container.querySelector('script')).toBeNull()
  38. expect(container.querySelector('img')).toBeNull()
  39. expect(container.querySelector('table')).not.toBeNull()
  40. expect(container.querySelector('.codeblock-bar')?.textContent).toContain('bash')
  41. expect(container.querySelector('.hljs-built_in, .hljs-keyword')).not.toBeNull()
  42. })
  43. it('links do not navigate the app', () => {
  44. const opened: string[] = []
  45. const client = { openUrl: async (url: string) => void opened.push(url) } as unknown as IpcClient
  46. useStore.setState({ client })
  47. render(<Markdown text="[site](https://example.com), [plain](http://example.com) and [bad](javascript:alert(1))" />)
  48. const link = screen.getByText('site') as HTMLAnchorElement
  49. const bad = screen.getByText('bad') as HTMLAnchorElement
  50. expect(link.title).toBe('https://example.com')
  51. expect(bad.getAttribute('href') ?? '').not.toMatch(/^javascript:/i)
  52. // fireEvent returns false when the click's default (navigation) was prevented.
  53. for (const a of [link, screen.getByText('plain'), bad]) expect(fireEvent.click(a)).toBe(false)
  54. expect(opened).toEqual(['https://example.com'])
  55. useStore.setState({ client: null })
  56. })
  57. })
  58. describe('the store streams a reply', () => {
  59. beforeEach(() => {
  60. useStore.setState({ client: null, view: null, streaming: null, drafts: [], toasts: [] })
  61. })
  62. it('creates the chat, streams, then settles with a cost', async () => {
  63. const client = createMockClient({ delay: 1, empty: true })
  64. await act(() => useStore.getState().init(client))
  65. let sent = false
  66. await act(async () => {
  67. sent = await useStore.getState().send('what does a cache layer buy me')
  68. })
  69. expect(sent).toBe(true)
  70. expect(useStore.getState().view?.messages[0].content).toBe('what does a cache layer buy me')
  71. await act(() => new Promise((r) => setTimeout(r, 400)))
  72. const s = useStore.getState()
  73. expect(s.streaming).toBeNull()
  74. const reply = s.view!.messages[1]
  75. expect(reply.status).toBe('complete')
  76. expect(reply.content).toContain('You asked')
  77. expect(reply.cost_nanousd).toBeGreaterThan(0)
  78. expect(s.conversations[0].title).toBe('what does a cache layer buy me')
  79. expect(s.spend!.month_nanousd).toBe(reply.cost_nanousd)
  80. })
  81. it('stop keeps the partial reply as cancelled', async () => {
  82. const client = createMockClient({ delay: 20, empty: true })
  83. await act(() => useStore.getState().init(client))
  84. await act(async () => {
  85. await useStore.getState().send('please write a long answer about caching')
  86. })
  87. await act(() => new Promise((r) => setTimeout(r, 90)))
  88. await act(() => useStore.getState().stop())
  89. await act(() => new Promise((r) => setTimeout(r, 60)))
  90. const reply = useStore.getState().view!.messages[1]
  91. expect(reply.status).toBe('cancelled')
  92. expect(reply.usage_source).toBe('estimated')
  93. expect(reply.content.length).toBeGreaterThan(0)
  94. })
  95. it('without a key, sending is refused with a clear message', async () => {
  96. const client = createMockClient({ empty: true, hasKey: false })
  97. await act(() => useStore.getState().init(client))
  98. let sent = true
  99. await act(async () => {
  100. sent = await useStore.getState().send('hi')
  101. })
  102. expect(sent).toBe(false)
  103. expect(useStore.getState().toasts[0].text).toMatch(/Add your DeepInfra API key/)
  104. })
  105. })
  106. describe('titles, labels and the key field', () => {
  107. beforeEach(() => {
  108. useStore.setState({ client: null, view: null, streaming: null, drafts: [], toasts: [], panel: null })
  109. })
  110. it('the open chat takes its title from the first message', async () => {
  111. const client = createMockClient({ delay: 1, empty: true })
  112. await act(() => useStore.getState().init(client))
  113. await act(async () => {
  114. await useStore.getState().send('Does this set read?')
  115. })
  116. await act(() => new Promise((r) => setTimeout(r, 200)))
  117. expect(useStore.getState().view?.conversation.title).toBe('Does this set read?')
  118. })
  119. it('a chat whose reply was refused is not labeled free', async () => {
  120. const client = createMockClient({ empty: true })
  121. await act(() => useStore.getState().init(client))
  122. const s = (await client.listConversations(false))[0]
  123. expect(s).toBeUndefined()
  124. const conv = await client.createConversation('deepinfra', 'google/gemma-3-12b-it', null, 'main')
  125. const list = await client.listConversations(false)
  126. const summary = list.find((x) => x.id === conv.id)!
  127. expect(summary.has_usage).toBe(false)
  128. // A refused reply is saved with no usage and no cost; a local model's reply has usage and no cost.
  129. const refused = { ...summary, message_count: 2 }
  130. const local = { ...summary, id: 'local', title: 'Local reply', message_count: 2, has_usage: true }
  131. act(() => useStore.setState({ conversations: [refused, local] }))
  132. const { container } = render(<Sidebar />)
  133. const costs = [...container.querySelectorAll('.conv-cost')].map((el) => el.textContent)
  134. expect(costs).toEqual(['', 'free'])
  135. })
  136. it('Enter in the key field saves it and confirms at once', async () => {
  137. const client = createMockClient({ empty: true, hasKey: false })
  138. await act(() => useStore.getState().init(client))
  139. render(<SettingsPanel />)
  140. const input = screen.getByTestId('api-key')
  141. fireEvent.change(input, { target: { value: 'di-test-key' } })
  142. await act(async () => {
  143. fireEvent.submit(input.closest('form')!)
  144. await new Promise((r) => setTimeout(r, 20))
  145. })
  146. expect(await client.hasApiKey('deepinfra')).toBe(true)
  147. expect(useStore.getState().keys.deepinfra).toBe(true)
  148. expect(useStore.getState().toasts.map((t) => t.text)).toContain('Key saved. DeepInfra accepted it.')
  149. })
  150. it('a refused key is not saved, and says so in one message', async () => {
  151. const client = createMockClient({ empty: true, hasKey: false })
  152. await act(() => useStore.getState().init(client))
  153. render(<SettingsPanel />)
  154. const input = screen.getByTestId('api-key')
  155. fireEvent.change(input, { target: { value: 'bad-key' } })
  156. await act(async () => {
  157. fireEvent.submit(input.closest('form')!)
  158. await new Promise((r) => setTimeout(r, 20))
  159. })
  160. expect(await client.hasApiKey('deepinfra')).toBe(false)
  161. const toasts = useStore.getState().toasts.map((t) => t.text)
  162. expect(toasts).toEqual(['DeepInfra rejected this key, so it was not saved. Copy the whole key again and paste it.'])
  163. })
  164. })