fields.client.spec.tsx 4.7 KB

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