diff --git a/ui/src/textbox.test.tsx b/ui/src/textbox.test.tsx
index 03c5f07f06..26a3469c75 100644
--- a/ui/src/textbox.test.tsx
+++ b/ui/src/textbox.test.tsx
@@ -13,18 +13,23 @@
// limitations under the License.
import { fireEvent, render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import React from 'react'
import { Textbox, XTextbox } from './textbox'
import { wave } from './ui'
-const name = 'textbox'
-const textboxProps: Textbox = { name }
+const
+ name = 'textbox',
+ pushMock = jest.fn()
+let textboxProps: Textbox = { name }
+wave.push = pushMock
describe('Textbox.tsx', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
- wave.args[name] = null
+ // Because we mutate props to programatically change "value" from Wave app we need to reset it before every test
+ textboxProps = { name }
})
it('Renders data-test attr', () => {
@@ -70,27 +75,86 @@ describe('Textbox.tsx', () => {
expect(wave.args[name]).toBe('default')
})
+ it('Sets args when "value" prop changes', () => {
+ const { rerender } = render()
+ expect(wave.args[name]).toBe('a')
+
+ rerender()
+ expect(wave.args[name]).toBe('b')
+ })
+
+ it('Sets args when typing new text and then "value" prop changes', () => {
+ const { getByTestId, rerender } = render()
+ expect(wave.args[name]).toBe('a')
+
+ userEvent.type(getByTestId(name), '{backspace}b')
+ expect(wave.args[name]).toBe('b')
+
+ rerender()
+ expect(wave.args[name]).toBe('c')
+ })
+
it('Calls sync on change - trigger specified', () => {
const { getByTestId } = render()
- const pushMock = jest.fn()
- wave.push = pushMock
-
fireEvent.change(getByTestId(name), { target: { value: 'aaa' } })
expect(pushMock).not.toBeCalled() // Not called immediately, but after specified timeout.
jest.runOnlyPendingTimers()
- expect(pushMock).toBeCalled()
+ expect(pushMock).toBeCalledTimes(1)
+ })
+
+ it('Debounces wave push', () => {
+ const { getByTestId } = render()
+
+ userEvent.type(getByTestId(name), 'a')
+ userEvent.type(getByTestId(name), 'a')
+ userEvent.type(getByTestId(name), 'a')
+ userEvent.type(getByTestId(name), 'a')
+ jest.runOnlyPendingTimers()
+
+ expect(pushMock).toBeCalledTimes(1)
})
it('Does not call sync on change - trigger not specified', () => {
const { getByTestId } = render()
- const pushMock = jest.fn()
- wave.push = pushMock
-
fireEvent.change(getByTestId(name), { target: { value: 'aaa' } })
expect(pushMock).not.toBeCalled()
})
+
+ it('Display new value when "value" prop changes', () => {
+ const { getByTestId, rerender } = render()
+ expect(getByTestId(name)).toHaveValue('A')
+
+ rerender()
+ expect(getByTestId(name)).toHaveValue('B')
+ })
+
+ it('Types value and then display new value when "value" prop changes', () => {
+ const { getByTestId, rerender } = render()
+ userEvent.type(getByTestId(name), '{backspace}A{Enter}')
+ expect(getByTestId(name)).toHaveValue('A')
+
+ rerender()
+ expect(getByTestId(name)).toHaveValue('B')
+ })
+
+ it('Display new value when "value" prop changes (masked)', () => {
+ const { getByTestId, rerender } = render()
+ expect(getByTestId(name)).toHaveValue('(123)')
+
+ rerender()
+ expect(getByTestId(name)).toHaveValue('(456)')
+ })
+
+ it('Types value and then display new value when "value" prop changes (masked)', () => {
+ const { getByTestId, rerender } = render()
+ userEvent.type(getByTestId(name), '{backspace}123{Enter}')
+ expect(getByTestId(name)).toHaveValue('(123)')
+
+ rerender()
+ expect(getByTestId(name)).toHaveValue('(456)')
+ })
})
\ No newline at end of file
diff --git a/ui/src/textbox.tsx b/ui/src/textbox.tsx
index 1dcbc509a8..b6c159f662 100644
--- a/ui/src/textbox.tsx
+++ b/ui/src/textbox.tsx
@@ -71,20 +71,25 @@ const DEBOUNCE_TIMEOUT = 500
export const
XTextbox = ({ model: m }: { model: Textbox }) => {
const
+ [value, setValue] = React.useState(m.value ?? ''),
+ debounceRef = React.useRef(debounce(DEBOUNCE_TIMEOUT, wave.push)),
onChange = ({ target }: React.FormEvent, v?: S) => {
v = v || (target as HTMLInputElement).value
wave.args[m.name] = v ?? (m.value || '')
- if (m.trigger) wave.push()
+ if (m.trigger) debounceRef.current()
+ setValue(v)
+ m.value = v
},
textFieldProps: Fluent.ITextFieldProps & { 'data-test': S } = {
'data-test': m.name,
label: m.label,
+ value,
errorMessage: m.error,
required: m.required,
disabled: m.disabled,
readOnly: m.readonly,
- onChange: m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange,
+ onChange,
iconProps: m.icon ? { iconName: m.icon } : undefined,
placeholder: m.placeholder,
prefix: m.prefix,
@@ -94,11 +99,13 @@ export const
type: m.password ? 'password' : undefined,
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- React.useEffect(() => { wave.args[m.name] = m.value || '' }, [])
+ React.useEffect(() => {
+ wave.args[m.name] = m.value ?? ''
+ setValue(m.value ?? '')
+ }, [m.value, m.name])
return m.mask
- ?
+ ?
: (
)
}
\ No newline at end of file