From 899769f504ce83df9820cd6252c3e7381a31968f Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Mon, 24 Aug 2026 11:12:22 +1000 Subject: [PATCH 1/2] fix nested `act`s --- src/__tests__/event-wrapper-nesting.js | 47 ++++++++++++++++++++++++++ src/pure.js | 20 ++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/event-wrapper-nesting.js diff --git a/src/__tests__/event-wrapper-nesting.js b/src/__tests__/event-wrapper-nesting.js new file mode 100644 index 00000000..16e1f7a2 --- /dev/null +++ b/src/__tests__/event-wrapper-nesting.js @@ -0,0 +1,47 @@ +import * as React from 'react' +import {render, screen, fireEvent} from '../' + +let mockActDepth = 0 +let mockMaxActDepth = 0 +jest.mock('react', () => { + const actual = jest.requireActual('react') + return { + ...actual, + act: jest.fn(cb => { + mockActDepth++ + mockMaxActDepth = Math.max(mockMaxActDepth, mockActDepth) + try { + return actual.act(cb) + } finally { + mockActDepth-- + } + }), + } +}) + +function Nested() { + return ( + <> + + fireEvent.change(screen.getByLabelText('inner'), { + target: {value: 'changed'}, + }) + } + /> + + + ) +} + +test('eventWrapper does not nest `act` for a re-entrant event dispatch', () => { + render() + expect(screen.getByLabelText('inner').value).toBe('') + + mockMaxActDepth = 0 + fireEvent.focus(screen.getByLabelText('outer')) + + expect(screen.getByLabelText('inner').value).toBe('changed') + expect(mockMaxActDepth).toBe(1) +}) diff --git a/src/pure.js b/src/pure.js index 054830fa..3ea36bfe 100644 --- a/src/pure.js +++ b/src/pure.js @@ -27,6 +27,8 @@ function jestFakeTimersAreEnabled() { return false } +let inEventWrapper = false + configureDTL({ unstable_advanceTimersWrapper: cb => { return act(cb) @@ -58,11 +60,19 @@ configureDTL({ } }, eventWrapper: cb => { - let result - act(() => { - result = cb() - }) - return result + if (inEventWrapper) { + return cb() + } + inEventWrapper = true + try { + let result + act(() => { + result = cb() + }) + return result + } finally { + inEventWrapper = false + } }, }) From b115de78e4e9dfa998fb5229b527cdda4b7f4258 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 26 Aug 2026 07:58:19 +1000 Subject: [PATCH 2/2] make test more representative of real life --- src/__tests__/event-wrapper-nesting.js | 81 +++++++++++++------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/src/__tests__/event-wrapper-nesting.js b/src/__tests__/event-wrapper-nesting.js index 16e1f7a2..f05d1345 100644 --- a/src/__tests__/event-wrapper-nesting.js +++ b/src/__tests__/event-wrapper-nesting.js @@ -1,47 +1,50 @@ import * as React from 'react' -import {render, screen, fireEvent} from '../' - -let mockActDepth = 0 -let mockMaxActDepth = 0 -jest.mock('react', () => { - const actual = jest.requireActual('react') - return { - ...actual, - act: jest.fn(cb => { - mockActDepth++ - mockMaxActDepth = Math.max(mockMaxActDepth, mockActDepth) - try { - return actual.act(cb) - } finally { - mockActDepth-- +import {render, fireEvent, getConfig} from '../' + +const {useState, useEffect, useRef} = React + +const dispatchDOMEvent = (target, type) => + getConfig().eventWrapper(() => + target.dispatchEvent(new Event(type, {bubbles: true})), + ) + +test('does not warn about act when there are nested acts', () => { + function Fixture() { + const [open, setOpen] = useState(false) + const [fromEffect, setFromEffect] = useState(0) + const [fromEvent, setFromEvent] = useState(0) + const targetRef = useRef(null) + + useEffect(() => { + // eslint-disable-next-line jest/no-conditional-in-test + if (!open) { + return } - }), + dispatchDOMEvent(targetRef.current, 'input') + setFromEffect(n => n + 1) + }, [open]) + + return ( + <> + + setFromEvent(n => n + 1)} /> + + ) } -}) -function Nested() { - return ( - <> - - fireEvent.change(screen.getByLabelText('inner'), { - target: {value: 'changed'}, - }) - } - /> - - - ) -} + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) + const {container} = render() + const button = container.querySelector('button') -test('eventWrapper does not nest `act` for a re-entrant event dispatch', () => { - render() - expect(screen.getByLabelText('inner').value).toBe('') + fireEvent.click(button) - mockMaxActDepth = 0 - fireEvent.focus(screen.getByLabelText('outer')) + expect(button).toHaveTextContent('open:true effect:1 event:1') + const actWarnings = errorSpy.mock.calls + .map(args => String(args[0])) + .filter(message => message.includes('not wrapped in act')) + expect(actWarnings).toEqual([]) - expect(screen.getByLabelText('inner').value).toBe('changed') - expect(mockMaxActDepth).toBe(1) + errorSpy.mockRestore() })