Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/event/dispatchEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ export function dispatchEvent(
return wrapEvent(() => target.dispatchEvent(event), target)
}

/**
* Dispatch a DOM event without wrapping it with the configured wrapper.
* This is used internally to trigger events that are not triggered natively by JSDOM.
* These should not be wrapped explicitly as they are already executed in the triggering wrapped scope.
*/
export function dispatchDOMEvent<K extends EventType>(
target: Element,
type: K,
init?: EventTypeInit<K>,
) {
const event = createEvent(type, target, init)
wrapEvent(() => target.dispatchEvent(event), target)
): boolean {
return target.dispatchEvent(createEvent(type, target, init))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the thing I'm worried about here is that there may be other reasons to wrap events beyond React adding act. If that's the case, then suddenly this event is no longer part of that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

angulars testing library for instance does
https://github.com/testing-library/angular-testing-library/blob/e6f4e26d8751a09056d0c610aa5be522cc73edfa/projects/testing-library/src/lib/testing-library.ts#L94

preact is the same as react, and vue and svelte appear not to do anything

and none of this includes what users may do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need to make this change up in https://github.com/testing-library/react-testing-library and in the preact one as well

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
36 changes: 36 additions & 0 deletions tests/event/wrapEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {configure, getConfig} from '@testing-library/dom'
import userEvent from '#src'
import {render} from '#testHelpers'

test('does not re-wrap internally dispatched events in the configured event wrapper', async () => {
const {
elements: [input, other],
} = render(`<input/><input/>`, {focus: false})

let depth = 0
let maxDepth = 0
const {eventWrapper: originalEventWrapper} = getConfig()
configure({
eventWrapper: cb => {
depth++
maxDepth = Math.max(maxDepth, depth)
try {
return cb()
} finally {
depth--
}
},
})

try {
const user = userEvent.setup()
await user.click(input)
await user.keyboard('hello')
// Blurring the now-modified field dispatches an internal `change` event.
await user.click(other)
} finally {
configure({eventWrapper: originalEventWrapper})
}

expect(maxDepth).toBe(1)
})
75 changes: 73 additions & 2 deletions tests/react/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useLayoutEffect, useRef, useState} from 'react'
import {render, screen, waitFor} from '@testing-library/react'
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'
import {act, render, screen, waitFor} from '@testing-library/react'
import userEvent from '#src'
import {getUISelection, getUIValue} from '#src/document'
import {addListeners} from '#testHelpers'
Expand Down Expand Up @@ -118,6 +118,77 @@ test('trigger onChange SyntheticEvent on input', async () => {
expect(changeHandler).toHaveBeenCalledTimes(6)
})


test('wrapping a bare blur in `act` keeps the internal `change` inside `act`', async () => {
function Comp() {
const [changes, setChanges] = useState(0)
const ref = useRef<HTMLInputElement>(null)
useEffect(() => {
const el = ref.current as HTMLInputElement
const onChange = () => setChanges(c => c + 1)
el.addEventListener('change', onChange)
return () => el.removeEventListener('change', onChange)
}, [])
return (
<>
<input ref={ref} aria-label="field" />
<span>{changes}</span>
</>
)
}

render(<Comp />)
const user = userEvent.setup()
await user.type(screen.getByLabelText('field'), 'hello')

// The blur is not a user-event action but it results in
// a user-event change event being fired, so the caller wraps it in `act`.
act(() => {
;(screen.getByLabelText('field') as HTMLInputElement).blur()
})

expect(screen.getByText('1')).toBeInTheDocument()
const actWarnings = (console.error as jest.Mock).mock.calls.filter(c =>
String(c[0]).includes('not wrapped in act'),
)
expect(actWarnings).toHaveLength(0)
})


test('user event methods already wrap the blur in an act, so the internal `change` is inside `act`', async () => {
function Comp() {
const [changes, setChanges] = useState(0)
const ref = useRef<HTMLInputElement>(null)
useEffect(() => {
const el = ref.current as HTMLInputElement
const onChange = () => setChanges(c => c + 1)
el.addEventListener('change', onChange)
return () => el.removeEventListener('change', onChange)
}, [])
return (
<>
<input ref={ref} aria-label="field" />
<span>{changes}</span>
<button>go here</button>
</>
)
}

render(<Comp />)
const user = userEvent.setup()
await user.type(screen.getByLabelText('field'), 'hello')

await user.tab();

expect(screen.getByRole('button')).toHaveFocus()

expect(screen.getByText('1')).toBeInTheDocument()
const actWarnings = (console.error as jest.Mock).mock.calls.filter(c =>
String(c[0]).includes('not wrapped in act'),
)
expect(actWarnings).toHaveLength(0)
})

describe('typing in a formatted input', () => {
function DollarInput({initialValue = ''}) {
const [val, setVal] = useState(initialValue)
Expand Down