From dfe9ed0a94c8f04fda7ee75cc74def2773e77ffa Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:48:35 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=B2=20test:=20Replace=20Microtask=20Ti?= =?UTF-8?q?ming=20Assumptions=20in=20Client=20Specs=20(#14653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../__tests__/UploadSkillDialog.spec.tsx | 2 +- .../hooks/__tests__/useIsActiveItem.spec.tsx | 45 ++++++++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/client/src/components/Skills/dialogs/__tests__/UploadSkillDialog.spec.tsx b/client/src/components/Skills/dialogs/__tests__/UploadSkillDialog.spec.tsx index 803124adbc..d75fd7fc58 100644 --- a/client/src/components/Skills/dialogs/__tests__/UploadSkillDialog.spec.tsx +++ b/client/src/components/Skills/dialogs/__tests__/UploadSkillDialog.spec.tsx @@ -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(); diff --git a/client/src/hooks/__tests__/useIsActiveItem.spec.tsx b/client/src/hooks/__tests__/useIsActiveItem.spec.tsx index c77252ce40..ddce46a08e 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 } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import useIsActiveItem from '../useIsActiveItem'; @@ -11,9 +11,33 @@ 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 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 => + 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(); @@ -24,39 +48,36 @@ describe('useIsActiveItem', () => { const { container } = render(); 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(); 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(); 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');