cell.client.spec.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // @vitest-environment jsdom
  2. /**
  3. * TrajectoryCell presentation: kind tags, ellipsis-hosting text, Message
  4. * metric columns, own-duration formatting, and selected ring.
  5. */
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { cleanup, render, screen } from '@testing-library/react'
  8. import type { ComponentProps } from 'react'
  9. import {
  10. formatElapsedSeconds as formatElapsedSecondsWithLocale,
  11. TrajectoryCell as LocalizedTrajectoryCell,
  12. type TrajectoryCellKind,
  13. } from '../src/client/TrajectoryCell.tsx'
  14. import { formatDurationMillis as formatDurationMillisWithLocale } from '../src/client/trajectory-record.ts'
  15. import { t } from './locale.client.ts'
  16. const formatDurationMillis = (value: number | null) => formatDurationMillisWithLocale(value, t)
  17. const formatElapsedSeconds = (value: number | null) => formatElapsedSecondsWithLocale(value, t)
  18. function TrajectoryCell(props: Omit<ComponentProps<typeof LocalizedTrajectoryCell>, 't'>) {
  19. return <LocalizedTrajectoryCell {...props} t={t} />
  20. }
  21. afterEach(cleanup)
  22. describe('formatDurationMillis', () => {
  23. it('formats exact millisecond labels with thousands separators', () => {
  24. expect(formatDurationMillis(0)).toBe('0 ms')
  25. expect(formatDurationMillis(29)).toBe('29 ms')
  26. expect(formatDurationMillis(500)).toBe('500 ms')
  27. expect(formatDurationMillis(1_500)).toBe('1,500 ms')
  28. expect(formatDurationMillis(235_200)).toBe('235,200 ms')
  29. expect(formatDurationMillis(null)).toBe('—')
  30. expect(formatDurationMillis(Number.NaN)).toBe('—')
  31. })
  32. })
  33. describe('formatElapsedSeconds', () => {
  34. it('formats known durations and uses an em dash when absent', () => {
  35. expect(formatElapsedSeconds(null)).toBe('—')
  36. expect(formatElapsedSeconds(235)).toBe('235,000 ms')
  37. expect(formatElapsedSeconds(235.0)).toBe('235,000 ms')
  38. expect(formatElapsedSeconds(235.2)).toBe('235,200 ms')
  39. expect(formatElapsedSeconds(235.25)).toBe('235,250 ms')
  40. expect(formatElapsedSeconds(0)).toBe('0 ms')
  41. expect(formatElapsedSeconds(0.029)).toBe('29 ms')
  42. expect(formatElapsedSeconds(0.5)).toBe('500 ms')
  43. expect(formatElapsedSeconds(1.5)).toBe('1,500 ms')
  44. expect(formatElapsedSeconds(Number.NaN)).toBe('—')
  45. })
  46. })
  47. describe('TrajectoryCell', () => {
  48. it('renders index, kind tag, text, and time for a Tool row', () => {
  49. render(
  50. <TrajectoryCell
  51. index={6}
  52. kind="tool"
  53. text="bash · Read src/index.ts"
  54. timeSeconds={5}
  55. />,
  56. )
  57. expect(screen.getByText('#6')).toBeTruthy()
  58. expect(screen.getByText('TOOL')).toBeTruthy()
  59. expect(screen.getByText('bash · Read src/index.ts')).toBeTruthy()
  60. expect(screen.getByText('5,000 ms')).toBeTruthy()
  61. })
  62. it('Message rows expose Input / Output / Think metric columns before time', () => {
  63. const { container } = render(
  64. <TrajectoryCell
  65. index={3}
  66. kind="message"
  67. text="Let me now read the actual source files to understa..."
  68. timeSeconds={235.2}
  69. input={136}
  70. output={381}
  71. think={155}
  72. />,
  73. )
  74. expect(screen.getByText('Message')).toBeTruthy()
  75. expect(screen.getByText('136')).toBeTruthy()
  76. expect(screen.getByText('381')).toBeTruthy()
  77. expect(screen.getByText('155')).toBeTruthy()
  78. expect(screen.getByText('235,200 ms')).toBeTruthy()
  79. const texts = [...container.querySelectorAll('span')].map(el => el.textContent)
  80. expect(texts.indexOf('136')).toBeLessThan(texts.indexOf('381'))
  81. expect(texts.indexOf('381')).toBeLessThan(texts.indexOf('155'))
  82. expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('235,200 ms'))
  83. })
  84. it('selected marks the row for the brand-primary inset ring', () => {
  85. const { container } = render(
  86. <TrajectoryCell index={15} kind="message" text="pictur..." timeSeconds={123.6} selected />,
  87. )
  88. expect(container.firstElementChild?.getAttribute('data-selected')).toBe('true')
  89. })
  90. it.each([
  91. ['user', 'USER'],
  92. ['tool', 'TOOL'],
  93. ] as const)('kind %s shows the %s tag and no metric columns', (kind: TrajectoryCellKind, label: string) => {
  94. const { container } = render(
  95. <TrajectoryCell index={1} kind={kind} text="summary" timeSeconds={kind === 'user' ? 0 : null} input={1} output={2} think={3} />,
  96. )
  97. expect(screen.getByText(label)).toBeTruthy()
  98. expect(container.querySelector('[data-kind]')?.getAttribute('data-kind')).toBe(kind)
  99. expect(screen.queryByText('1')).toBeNull()
  100. expect(screen.queryByText('2')).toBeNull()
  101. expect(screen.queryByText('3')).toBeNull()
  102. })
  103. })