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');