From 4d8f1455269bb633fc152648adda070e688015ed Mon Sep 17 00:00:00 2001 From: Marco Beretta Date: Mon, 24 Aug 2026 05:16:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20test:=20Make=20Active=20It?= =?UTF-8?q?em=20Observer=20Assertions=20Deterministic=20(#15147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two attribute-flip tests mutated inside act() and then raced a 4 second waitFor against MutationObserver delivery, so they failed once the client workspace gained enough suites for a worker to stall past that budget. Wait on actual observer delivery instead. The hook registers its observer on mount, so it is ahead of the test's in delivery order and has already reacted by the time the promise resolves. The new helper filters on data-active-item because React writes data-active onto the same element when it re-renders, and an unfiltered observer would resolve on that write instead. This removes the last wall-clock dependence in the file, so the 20 second jest timeout is no longer needed. --- .../hooks/__tests__/useIsActiveItem.spec.tsx | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/client/src/hooks/__tests__/useIsActiveItem.spec.tsx b/client/src/hooks/__tests__/useIsActiveItem.spec.tsx index ddce46a08e..eba51d2617 100644 --- a/client/src/hooks/__tests__/useIsActiveItem.spec.tsx +++ b/client/src/hooks/__tests__/useIsActiveItem.spec.tsx @@ -2,7 +2,7 @@ * @jest-environment @happy-dom/jest-environment */ import React from 'react'; -import { act, render, waitFor } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import useIsActiveItem from '../useIsActiveItem'; @@ -11,18 +11,25 @@ function Probe() { return
; } -/** - * Observer delivery lands on the next microtask in this environment; the wait exists so the - * test does not depend on that scheduling detail, with budget to ride out a stalled host. - */ -const OBSERVER_WAIT = { timeout: 4000 }; - -/** One test chains two OBSERVER_WAITs, whose budget exceeds Jest's 5s default. */ -jest.setTimeout(20_000); - const getProbe = (container: HTMLElement) => container.querySelector('[data-testid="probe"]') as HTMLDivElement; +/** + * Resolves once a `data-active-item` mutation has been delivered to observers. The hook + * registers its observer on mount, so it is always ahead of this one in delivery order, + * which means the hook has already reacted by the time this resolves. Filtering matters: + * React writes `data-active` onto the same element when it re-renders, and an unfiltered + * observer would resolve on that write instead of on the attribute under test. + */ +const nextActiveItemMutation = (element: HTMLElement): Promise => + new Promise((resolve) => { + const observer = new MutationObserver(() => { + observer.disconnect(); + resolve(); + }); + observer.observe(element, { attributes: true, attributeFilter: ['data-active-item'] }); + }); + /** * Resolves once an attribute mutation on `element` has been delivered to observers. The * hook's observer filters on `data-active-item`, so it is never notified of the unrelated @@ -48,26 +55,32 @@ describe('useIsActiveItem', () => { const { container } = render(); const probe = getProbe(container); - act(() => { + const delivered = nextActiveItemMutation(probe); + await act(async () => { probe.setAttribute('data-active-item', ''); + await delivered; }); - await waitFor(() => expect(probe.getAttribute('data-active')).toBe('true'), OBSERVER_WAIT); + expect(probe.getAttribute('data-active')).toBe('true'); }); it('flips isActive back to false when data-active-item is removed', async () => { const { container } = render(); const probe = getProbe(container); - act(() => { + const added = nextActiveItemMutation(probe); + await act(async () => { probe.setAttribute('data-active-item', ''); + await added; }); - await waitFor(() => expect(probe.getAttribute('data-active')).toBe('true'), OBSERVER_WAIT); + expect(probe.getAttribute('data-active')).toBe('true'); - act(() => { + const removed = nextActiveItemMutation(probe); + await act(async () => { probe.removeAttribute('data-active-item'); + await removed; }); - await waitFor(() => expect(probe.getAttribute('data-active')).toBe('false'), OBSERVER_WAIT); + expect(probe.getAttribute('data-active')).toBe('false'); }); it('ignores unrelated attribute mutations', async () => {