diff --git a/.all-contributorsrc b/.all-contributorsrc
index a621a1e6..0891ae03 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1198,6 +1198,16 @@
"contributions": [
"bug"
]
+ },
+ {
+ "login": "karolis-cekaitis",
+ "name": "Karolis ฤekaitis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/89905443?v=4",
+ "profile": "https://github.com/karolis-cekaitis",
+ "contributions": [
+ "bug",
+ "doc"
+ ]
}
],
"commitConvention": "none",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 2411fb2a..37e650f0 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -164,6 +164,7 @@ Thanks goes to these wonderful people ([emoji key][emojis]):
 Rob Caldecott ๐ ๐ป |
 Tom Bertrand ๐ |
 Justin Hall ๐ |
+  Karolis ฤekaitis ๐ ๐ |
diff --git a/src/document/index.ts b/src/document/index.ts
index 7cb2e3d5..70be9b06 100644
--- a/src/document/index.ts
+++ b/src/document/index.ts
@@ -2,9 +2,9 @@ import {dispatchUIEvent} from '../event'
import {Config} from '../setup'
import {prepareSelectionInterceptor} from './selection'
import {
+ clearInitialValue,
getInitialValue,
prepareValueInterceptor,
- setInitialValue,
} from './value'
const isPrepared = Symbol('Node prepared with document state workarounds')
@@ -45,8 +45,11 @@ export function prepareDocument(document: Document) {
e => {
const el = e.target as HTMLInputElement
const initialValue = getInitialValue(el)
- if (typeof initialValue === 'string' && el.value !== initialValue) {
- dispatchUIEvent({} as Config, el, 'change')
+ if (initialValue !== undefined) {
+ if (el.value !== initialValue) {
+ dispatchUIEvent({} as Config, el, 'change')
+ }
+ clearInitialValue(el)
}
},
{
@@ -59,10 +62,6 @@ export function prepareDocument(document: Document) {
}
function prepareElement(el: Node | HTMLInputElement) {
- if ('value' in el) {
- setInitialValue(el)
- }
-
if (el[isPrepared]) {
return
}
@@ -75,6 +74,12 @@ function prepareElement(el: Node | HTMLInputElement) {
el[isPrepared] = isPrepared
}
-export {getUIValue, setUIValue, startTrackValue, endTrackValue} from './value'
+export {
+ getUIValue,
+ setUIValue,
+ startTrackValue,
+ endTrackValue,
+ clearInitialValue,
+} from './value'
export {getUISelection, setUISelection} from './selection'
export type {UISelectionRange} from './selection'
diff --git a/src/document/interceptor.ts b/src/document/interceptor.ts
index 4ff51e11..6d6ecc2f 100644
--- a/src/document/interceptor.ts
+++ b/src/document/interceptor.ts
@@ -28,6 +28,7 @@ export function prepareInterceptor<
*/
applyNative?: boolean
realArgs?: ImplReturn
+ then?: () => void
},
) {
const prototypeDescriptor = Object.getOwnPropertyDescriptor(
@@ -49,7 +50,11 @@ export function prepareInterceptor<
this: ElementType,
...args: Params
) {
- const {applyNative = true, realArgs} = interceptorImpl.call(this, ...args)
+ const {
+ applyNative = true,
+ realArgs,
+ then,
+ } = interceptorImpl.call(this, ...args)
const realFunc = ((!applyNative && objectDescriptor) ||
(prototypeDescriptor as PropertyDescriptor))[target] as (
@@ -62,6 +67,8 @@ export function prepareInterceptor<
} else {
realFunc.call(this, ...realArgs)
}
+
+ then?.()
}
;(intercept as Interceptable)[Interceptor] = Interceptor
diff --git a/src/document/value.ts b/src/document/value.ts
index c91a85c1..0a410034 100644
--- a/src/document/value.ts
+++ b/src/document/value.ts
@@ -32,13 +32,12 @@ function valueInterceptor(
if (isUI) {
this[UIValue] = String(v)
setPreviousValue(this, String(this.value))
- } else {
- trackOrSetValue(this, String(v))
}
return {
applyNative: !!isUI,
realArgs: sanitizeValue(this, v),
+ then: isUI ? undefined : () => trackOrSetValue(this, String(v)),
}
}
@@ -66,6 +65,10 @@ export function setUIValue(
element: HTMLInputElement | HTMLTextAreaElement,
value: string,
) {
+ if (element[InitialValue] === undefined) {
+ element[InitialValue] = element.value
+ }
+
element.value = {
[UIValue]: UIValue,
toString: () => value,
@@ -78,10 +81,10 @@ export function getUIValue(element: HTMLInputElement | HTMLTextAreaElement) {
: String(element[UIValue])
}
-export function setInitialValue(
+export function clearInitialValue(
element: HTMLInputElement | HTMLTextAreaElement,
) {
- element[InitialValue] = element.value
+ element[InitialValue] = undefined
}
export function getInitialValue(
@@ -123,7 +126,6 @@ function setCleanValue(
v: string,
) {
element[UIValue] = undefined
- element[InitialValue] = v
// Programmatically setting the value property
// moves the cursor to the end of the input.
diff --git a/src/keyboard/keyboardAction.ts b/src/keyboard/keyboardAction.ts
index 2a0f444e..d4887978 100644
--- a/src/keyboard/keyboardAction.ts
+++ b/src/keyboard/keyboardAction.ts
@@ -22,8 +22,8 @@ export async function keyboardAction(
for (let i = 0; i < actions.length; i++) {
await keyboardKeyAction(config, actions[i])
- if (typeof config.delay === 'number' && i < actions.length - 1) {
- await wait(config.delay)
+ if (i < actions.length - 1) {
+ await wait(config)
}
}
}
@@ -32,7 +32,7 @@ async function keyboardKeyAction(
config: Config,
{keyDef, releasePrevious, releaseSelf, repeat}: KeyboardAction,
) {
- const {document, keyboardState, delay} = config
+ const {document, keyboardState} = config
const getCurrentElement = () => getActive(document)
// Release the key automatically if it was pressed before.
@@ -50,8 +50,8 @@ async function keyboardKeyAction(
await keypress(keyDef, getCurrentElement, config)
}
- if (typeof delay === 'number' && i < repeat) {
- await wait(delay)
+ if (i < repeat) {
+ await wait(config)
}
}
diff --git a/src/options.ts b/src/options.ts
index 8d91e879..1e970b0c 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -120,6 +120,13 @@ export interface Options {
* Defaults to `true` when calling the APIs per `setup`.
*/
writeToClipboard?: boolean
+
+ /**
+ * A function to be called internally to advance your fake timers (if applicable)
+ *
+ * @example jest.advanceTimersByTime
+ */
+ advanceTimers?: ((delay: number) => Promise) | ((delay: number) => void)
}
/**
@@ -137,6 +144,7 @@ export const defaultOptionsDirect: Required = {
skipClick: false,
skipHover: false,
writeToClipboard: false,
+ advanceTimers: () => Promise.resolve(),
}
/**
diff --git a/src/pointer/pointerAction.ts b/src/pointer/pointerAction.ts
index d5323c2e..7ca73410 100644
--- a/src/pointer/pointerAction.ts
+++ b/src/pointer/pointerAction.ts
@@ -37,10 +37,8 @@ export async function pointerAction(config: Config, actions: PointerAction[]) {
? pointerPress(config, {...action, target, coords})
: pointerMove(config, {...action, target, coords}))
- if (typeof config.delay === 'number') {
- if (i < actions.length - 1) {
- await wait(config.delay)
- }
+ if (i < actions.length - 1) {
+ await wait(config)
}
}
diff --git a/src/utils/edit/input.ts b/src/utils/edit/input.ts
index cc858626..2fbedbe0 100644
--- a/src/utils/edit/input.ts
+++ b/src/utils/edit/input.ts
@@ -1,4 +1,5 @@
import {
+ clearInitialValue,
endTrackValue,
getUIValue,
setUIValue,
@@ -128,7 +129,7 @@ function editInputElement(
) {
let dataToInsert = data
const spaceUntilMaxLength = getSpaceUntilMaxLength(element)
- if (spaceUntilMaxLength !== undefined) {
+ if (spaceUntilMaxLength !== undefined && data.length > 0) {
if (spaceUntilMaxLength > 0) {
dataToInsert = data.substring(0, spaceUntilMaxLength)
} else {
@@ -169,6 +170,7 @@ function editInputElement(
if (isValidDateOrTimeValue(element, newValue)) {
commitInput(config, element, newOffset, {})
dispatchUIEvent(config, element, 'change')
+ clearInitialValue(element)
}
} else {
commitInput(config, element, newOffset, {
diff --git a/src/utils/misc/wait.ts b/src/utils/misc/wait.ts
index 69ad59b5..aac99961 100644
--- a/src/utils/misc/wait.ts
+++ b/src/utils/misc/wait.ts
@@ -1,3 +1,12 @@
-export function wait(time?: number) {
- return new Promise(resolve => setTimeout(() => resolve(), time))
+import {Config} from '../../setup'
+
+export function wait(config: Config) {
+ const delay = config.delay
+ if (typeof delay !== 'number') {
+ return
+ }
+ return Promise.all([
+ new Promise(resolve => setTimeout(() => resolve(), delay)),
+ config.advanceTimers(delay),
+ ])
}
diff --git a/tests/document/index.ts b/tests/document/index.ts
index 72c68603..f6b6268f 100644
--- a/tests/document/index.ts
+++ b/tests/document/index.ts
@@ -50,7 +50,7 @@ test('keep track of value in UI', async () => {
expect(getUIValue(element)).toBe('3.5')
})
-test('trigger `change` event if value changed since focus/set', async () => {
+test('trigger `change` event if value changed per user input', async () => {
const {element, getEvents} = render(
``,
{focus: false},
@@ -66,16 +66,25 @@ test('trigger `change` event if value changed since focus/set', async () => {
expect(getEvents('change')).toHaveLength(0)
element.focus()
- // Programmatically changing value sets initial value
+ // Programmatically changing value is ignored
element.value = '3'
+ // Value doesn't change
setUIValue(element, '3')
element.blur()
expect(getEvents('change')).toHaveLength(0)
element.focus()
+ setUIValue(element, '2')
+ // value is reset so there is no change in the end
+ element.value = '3'
+ element.blur()
+
+ expect(getEvents('change')).toHaveLength(0)
+
+ element.focus()
+ setUIValue(element, '2')
element.value = '2'
- setUIValue(element, '3')
element.blur()
expect(getEvents('change')).toHaveLength(1)
@@ -112,6 +121,20 @@ test('maintain selection range on elements without support for selection range',
expect(element.selectionStart).toBe(null)
})
+test('reset UI selection if value is programmatically set', async () => {
+ const {element} = render(``)
+
+ prepare(element)
+
+ setUIValue(element, 'abc')
+ setUISelection(element, {anchorOffset: 1, focusOffset: 2})
+
+ element.value = 'abcdef'
+ expect(element.selectionStart).toBe(6)
+ expect(getUISelection(element)).toHaveProperty('focusOffset', 6)
+ expect(getUISelection(element)).toHaveProperty('startOffset', 6)
+})
+
test('clear UI selection if selection is programmatically set', async () => {
const {element} = render(``)
diff --git a/tests/utils/edit/input.ts b/tests/utils/edit/input.ts
index ef146ca8..6ed3d3c6 100644
--- a/tests/utils/edit/input.ts
+++ b/tests/utils/edit/input.ts
@@ -235,10 +235,10 @@ test('prevent input on `beforeinput` event', () => {
cases(
'maxlength',
- ({html, data, expectedValue}) => {
+ ({html, data, inputType, expectedValue}) => {
const {element, eventWasFired} = render(html)
- input(createConfig(), element, data)
+ input(createConfig(), element, data, inputType)
expect(element).toHaveValue(expectedValue)
expect(eventWasFired('beforeinput')).toBe(true)
@@ -270,6 +270,12 @@ cases(
data: '',
expectedValue: '',
},
+ 'delete data when maxlength is reached': {
+ html: ``,
+ data: '',
+ inputType: 'deleteContentForward',
+ expectedValue: 'oo',
+ },
},
)
diff --git a/tests/utils/misc/wait.ts b/tests/utils/misc/wait.ts
new file mode 100644
index 00000000..3f88f9ce
--- /dev/null
+++ b/tests/utils/misc/wait.ts
@@ -0,0 +1,9 @@
+import {wait} from '#src/utils/misc/wait'
+
+test('advances timers when set', async () => {
+ jest.useFakeTimers()
+ jest.setTimeout(50)
+ // If this wasn't advancing fake timers, we'd timeout and fail the test
+ await wait(10000, jest.advanceTimersByTime)
+ jest.useRealTimers()
+})