fields.client.spec.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. // @vitest-environment jsdom
  2. import { cleanup, fireEvent, render, screen } from '@testing-library/react'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { SecretField, ValueField } from '../src/client/fields.tsx'
  5. afterEach(cleanup)
  6. const frame = {
  7. id: 'field',
  8. label: 'Command timeout',
  9. hint: 'How long one command may run.',
  10. overriddenLabel: 'Overridden',
  11. resetLabel: 'Reset to default',
  12. invalidLabel: 'Enter a number.',
  13. disabled: false,
  14. overridden: false,
  15. invalid: false,
  16. }
  17. describe('ValueField', () => {
  18. it('stages every keystroke without writing', () => {
  19. const onEdit = vi.fn()
  20. render(<ValueField {...frame} text="60000" onEdit={onEdit} onReset={vi.fn()} />)
  21. fireEvent.change(screen.getByLabelText('Command timeout'), { target: { value: '9000' } })
  22. expect(onEdit).toHaveBeenCalledWith('9000')
  23. })
  24. it('renders the staged text it is given rather than a draft of its own', () => {
  25. const { rerender } = render(<ValueField {...frame} text="60000" onEdit={vi.fn()} onReset={vi.fn()} />)
  26. expect(screen.getByLabelText('Command timeout')).toHaveProperty('value', '60000')
  27. rerender(<ValueField {...frame} text="9000" onEdit={vi.fn()} onReset={vi.fn()} />)
  28. expect(screen.getByLabelText('Command timeout')).toHaveProperty('value', '9000')
  29. })
  30. it('offers the reset only while an override would stand', () => {
  31. const onReset = vi.fn()
  32. const { rerender } = render(<ValueField {...frame} text="9000" onEdit={vi.fn()} onReset={onReset} />)
  33. expect(screen.queryByRole('button', { name: 'Reset to default' })).toBeNull()
  34. rerender(<ValueField {...frame} overridden text="9000" onEdit={vi.fn()} onReset={onReset} />)
  35. fireEvent.click(screen.getByRole('button', { name: 'Reset to default' }))
  36. expect(screen.getByText('Overridden')).toBeTruthy()
  37. expect(onReset).toHaveBeenCalledOnce()
  38. })
  39. it('replaces the hint with the reason an invalid draft cannot be saved', () => {
  40. render(<ValueField {...frame} invalid text="soon" onEdit={vi.fn()} onReset={vi.fn()} />)
  41. expect(screen.getByText('Enter a number.')).toBeTruthy()
  42. expect(screen.queryByText('How long one command may run.')).toBeNull()
  43. expect(screen.getByLabelText('Command timeout').getAttribute('aria-invalid')).toBe('true')
  44. })
  45. it('hints a numeric keypad and renders a placeholder when asked', () => {
  46. render(
  47. <ValueField
  48. {...frame}
  49. numeric
  50. placeholder="https://api.deepseek.com"
  51. text=""
  52. onEdit={vi.fn()}
  53. onReset={vi.fn()}
  54. />,
  55. )
  56. const input = screen.getByLabelText('Command timeout')
  57. expect(input.getAttribute('inputmode')).toBe('numeric')
  58. expect(input).toHaveProperty('placeholder', 'https://api.deepseek.com')
  59. })
  60. it('disables the control and its reset while the document is read-only', () => {
  61. render(<ValueField {...frame} disabled overridden text="9000" onEdit={vi.fn()} onReset={vi.fn()} />)
  62. expect(screen.getByLabelText('Command timeout')).toHaveProperty('disabled', true)
  63. expect(screen.getByRole('button', { name: 'Reset to default' })).toHaveProperty('disabled', true)
  64. })
  65. })
  66. describe('SecretField', () => {
  67. const secret = {
  68. id: 'key',
  69. label: 'API key',
  70. hint: 'Stored outside the settings file.',
  71. disabled: false,
  72. }
  73. it('stages the draft and never renders it', () => {
  74. const onEdit = vi.fn()
  75. render(
  76. <SecretField
  77. {...secret}
  78. text=""
  79. configured={false}
  80. stateLabel="No key is configured."
  81. onEdit={onEdit}
  82. />,
  83. )
  84. const input = screen.getByLabelText('API key')
  85. fireEvent.change(input, { target: { value: 'ds-secret' } })
  86. expect(onEdit).toHaveBeenCalledWith('ds-secret')
  87. expect(input).toHaveProperty('type', 'password')
  88. })
  89. it('reports the configured state the Host holds', () => {
  90. const { rerender } = render(
  91. <SecretField
  92. {...secret}
  93. text=""
  94. configured={false}
  95. stateLabel="No key is configured."
  96. onEdit={vi.fn()}
  97. />,
  98. )
  99. expect(screen.getByText('No key is configured.')).toBeTruthy()
  100. rerender(
  101. <SecretField
  102. {...secret}
  103. text="ds-secret"
  104. configured
  105. stateLabel="A key is configured."
  106. onEdit={vi.fn()}
  107. />,
  108. )
  109. expect(screen.getByText('A key is configured.')).toBeTruthy()
  110. expect(screen.getByLabelText('API key')).toHaveProperty('value', 'ds-secret')
  111. })
  112. it('disables the control when it is told to', () => {
  113. render(
  114. <SecretField
  115. {...secret}
  116. disabled
  117. text=""
  118. configured
  119. stateLabel="A key is configured."
  120. onEdit={vi.fn()}
  121. />,
  122. )
  123. expect(screen.getByLabelText('API key')).toHaveProperty('disabled', true)
  124. })
  125. })