🎲 test: Replace Microtask Timing Assumptions in Client Specs (#14653)

* test: replace microtask timing assumptions in client specs

useIsActiveItem asserted MutationObserver delivery after a single awaited
microtask, and UploadSkillDialog queried the file input synchronously right
after render. Both assume work settles on a fixed tick, which does not hold
when the host is loaded, and both fail intermittently as a result.

Poll for the expected state instead, and assert in the unrelated-mutation
case that the observer still reacts to a real change afterwards.

* test: address review on client spec timing fixes

Return the narrowed input from the polling callback instead of asserting
through unknown, so the type check the helper performs is the one the
compiler sees.

Synchronize the unrelated-mutation case on a second observer rather than a
wait that was already satisfied before delivery, and drop the trailing
attribute dance that assertion no longer needs. Verified by removing the
attribute filter and setting the state unconditionally: the test now fails,
where before it passed.

Raise this file's Jest timeout above the aggregate wait budget, since a test
that chains two observer waits could otherwise be aborted at five seconds
while both waits were still within their limit.

* test: correct observer wait rationale and revert inert upload polling
This commit is contained in:
Marco Beretta 2026-08-06 18:48:35 +02:00 • committed by GitHub
parent 8d5c298bdf
commit dfe9ed0a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 13 deletions

View file

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { ReactNode } from 'react';
import type { FileConfigInput } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import UploadSkillDialog from '../UploadSkillDialog';
const mockMutate = jest.fn();

View file

@ -2,7 +2,7 @@
* @jest-environment @happy-dom/jest-environment
*/
import React from 'react';
import { act, render } from '@testing-library/react';
import { act, render, waitFor } from '@testing-library/react';
import useIsActiveItem from '../useIsActiveItem';
@ -11,9 +11,33 @@ function Probe() {
return <div ref={ref} data-testid="probe" data-active={isActive ? 'true' : 'false'} />;
}
/**
* 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 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
* mutation; this unfiltered observer proves delivery happened before the test asserts
* that the hook did not react.
*/
const nextAttributeMutation = (element: HTMLElement): Promise<void> =>
new Promise((resolve) => {
const observer = new MutationObserver(() => {
observer.disconnect();
resolve();
});
observer.observe(element, { attributes: true });
});
describe('useIsActiveItem', () => {
it('starts with isActive=false when data-active-item is absent', () => {
const { container } = render(<Probe />);
@ -24,39 +48,36 @@ describe('useIsActiveItem', () => {
const { container } = render(<Probe />);
const probe = getProbe(container);
await act(async () => {
act(() => {
probe.setAttribute('data-active-item', '');
// Allow the MutationObserver microtask to run
await Promise.resolve();
});
expect(probe.getAttribute('data-active')).toBe('true');
await waitFor(() => expect(probe.getAttribute('data-active')).toBe('true'), OBSERVER_WAIT);
});
it('flips isActive back to false when data-active-item is removed', async () => {
const { container } = render(<Probe />);
const probe = getProbe(container);
await act(async () => {
act(() => {
probe.setAttribute('data-active-item', '');
await Promise.resolve();
});
expect(probe.getAttribute('data-active')).toBe('true');
await waitFor(() => expect(probe.getAttribute('data-active')).toBe('true'), OBSERVER_WAIT);
await act(async () => {
act(() => {
probe.removeAttribute('data-active-item');
await Promise.resolve();
});
expect(probe.getAttribute('data-active')).toBe('false');
await waitFor(() => expect(probe.getAttribute('data-active')).toBe('false'), OBSERVER_WAIT);
});
it('ignores unrelated attribute mutations', async () => {
const { container } = render(<Probe />);
const probe = getProbe(container);
const delivered = nextAttributeMutation(probe);
await act(async () => {
probe.setAttribute('data-something-else', 'x');
await Promise.resolve();
await delivered;
});
expect(probe.getAttribute('data-active')).toBe('false');