fields.client.spec.tsx 4.9 KB

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