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 () => {