diff --git a/apps/desktop/src/app/chat/composer/attachments.test.tsx b/apps/desktop/src/app/chat/composer/attachments.test.tsx
index 8bde5b944592..52e8f6bf98e8 100644
--- a/apps/desktop/src/app/chat/composer/attachments.test.tsx
+++ b/apps/desktop/src/app/chat/composer/attachments.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, render, screen } from '@testing-library/react'
+import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { I18nProvider } from '@/i18n/context'
@@ -10,12 +10,17 @@ function makeAttachment(id: string, label = 'test.pdf'): ComposerAttachment {
return { id, kind: 'file', label }
}
-function renderWithI18n(ui: React.ReactNode) {
- return render(
- ({}), saveConfig: async () => ({ ok: true }) }}>
- {ui}
-
- )
+async function renderWithI18n(ui: React.ReactNode) {
+ let result: ReturnType
+ await act(async () => {
+ result = render(
+ ({}), saveConfig: async () => ({ ok: true }) }}>
+ {ui}
+
+ )
+ })
+
+ return result!
}
describe('AttachmentList', () => {
@@ -23,22 +28,22 @@ describe('AttachmentList', () => {
cleanup()
})
- it('renders valid attachments', () => {
+ it('renders valid attachments', async () => {
const attachments = [makeAttachment('a', 'doc.pdf'), makeAttachment('b', 'img.png')]
- renderWithI18n()
+ await renderWithI18n()
expect(screen.getByText('doc.pdf')).toBeDefined()
expect(screen.getByText('img.png')).toBeDefined()
})
- it('renders empty list without error', () => {
- const { container } = renderWithI18n()
+ it('renders empty list without error', async () => {
+ const { container } = await renderWithI18n()
const attachmentList = container.querySelector('[data-slot="composer-attachments"]')
expect(attachmentList).toBeDefined()
})
- it('does not crash when attachments array contains undefined entries', () => {
+ it('does not crash when attachments array contains undefined entries', async () => {
// Repro: session switch can leave stale/undefined entries in the
// attachments array, causing a TypeError at attachment.refText.
const attachments = [
@@ -47,21 +52,17 @@ describe('AttachmentList', () => {
makeAttachment('b', 'also-good.png')
]
- expect(() => {
- renderWithI18n()
- }).not.toThrow()
+ await expect(renderWithI18n()).resolves.toBeTruthy()
// Only valid attachments should render
expect(screen.getByText('good.pdf')).toBeDefined()
expect(screen.getByText('also-good.png')).toBeDefined()
})
- it('does not crash when attachments array contains null entries', () => {
+ it('does not crash when attachments array contains null entries', async () => {
const attachments = [null as unknown as ComposerAttachment, makeAttachment('a', 'valid.txt')]
- expect(() => {
- renderWithI18n()
- }).not.toThrow()
+ await expect(renderWithI18n()).resolves.toBeTruthy()
expect(screen.getByText('valid.txt')).toBeDefined()
})
diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
index 51e5539bac9e..650900a42e9d 100644
--- a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
+++ b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
@@ -19,7 +19,7 @@ describe('PreviewPane console state', () => {
vi.unstubAllGlobals()
})
- it('does not watch backend-only remote filesystem previews locally', () => {
+ it('does not watch backend-only remote filesystem previews locally', async () => {
const watchPreviewFile = vi.fn(async () => ({ id: 'watch-1', path: '/remote/file.txt' }))
const onPreviewFileChanged = vi.fn(() => vi.fn())
$connection.set({ mode: 'remote' } as never)
@@ -31,38 +31,43 @@ describe('PreviewPane console state', () => {
}
})
- render(
-
- )
+ await act(async () => {
+ render(
+
+ )
+ })
expect(watchPreviewFile).not.toHaveBeenCalled()
expect(onPreviewFileChanged).not.toHaveBeenCalled()
})
- it('does not rebuild the pane titlebar group for streamed console logs', () => {
+ it('does not rebuild the pane titlebar group for streamed console logs', async () => {
const setTitlebarToolGroup = vi.fn()
- const rendered = render(
-
- )
+ let rendered!: ReturnType
+ await act(async () => {
+ rendered = render(
+
+ )
+ })
const initialCalls = setTitlebarToolGroup.mock.calls.length
const webview = rendered.container.querySelector('webview')
diff --git a/apps/desktop/src/app/messaging/index.test.tsx b/apps/desktop/src/app/messaging/index.test.tsx
index a7d9273c0c90..b078a2043b1d 100644
--- a/apps/desktop/src/app/messaging/index.test.tsx
+++ b/apps/desktop/src/app/messaging/index.test.tsx
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -53,12 +53,16 @@ afterEach(() => {
async function renderMessaging() {
const { MessagingView } = await import('./index')
+ let result: ReturnType
+ await act(async () => {
+ result = render(
+
+
+
+ )
+ })
- return render(
-
-
-
- )
+ return result!
}
describe('MessagingView setup-guide link', () => {
@@ -82,7 +86,9 @@ describe('MessagingView setup-guide link', () => {
await renderMessaging()
const link = await screen.findByText('Open setup guide')
- fireEvent.click(link)
+ await act(async () => {
+ fireEvent.click(link)
+ })
await waitFor(() => expect(openExternalLink).toHaveBeenCalledWith(docsUrl))
})
diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx
index 66a216d14ae7..3e725b1480d3 100644
--- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx
+++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, render, waitFor } from '@testing-library/react'
+import { act, cleanup, render, waitFor } from '@testing-library/react'
import type { MutableRefObject } from 'react'
import { useEffect, useRef } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -43,6 +43,18 @@ function sessionInfo(overrides: Partial = {}): SessionInfo {
}
}
+// Wrap render() in act() so the Harness's useEffect (onReady callback +
+// internal state from usePromptActions) flushes synchronously instead of
+// spilling async state updates outside act().
+async function actRender(ui: React.ReactElement) {
+ let result: ReturnType
+ await act(async () => {
+ result = render(ui)
+ })
+
+ return result!
+}
+
interface HarnessHandle {
cancelRun: () => Promise
restoreToMessage: (messageId: string, target?: { text?: string; userOrdinal?: number | null }) => Promise
@@ -125,10 +137,14 @@ function Harness({
useEffect(() => {
onReady({
- cancelRun: actions.cancelRun,
- restoreToMessage: actions.restoreToMessage,
- steerPrompt: actions.steerPrompt,
- submitText: actions.submitText
+ cancelRun: (...args: Parameters) =>
+ act(async () => actions.cancelRun(...args)) as Promise,
+ restoreToMessage: (...args: Parameters) =>
+ act(async () => actions.restoreToMessage(...args)) as Promise,
+ steerPrompt: (...args: Parameters) =>
+ act(async () => actions.steerPrompt(...args)) as Promise,
+ submitText: (...args: Parameters) =>
+ act(async () => actions.submitText(...args)) as Promise
})
}, [actions.cancelRun, actions.restoreToMessage, actions.steerPrompt, actions.submitText, onReady])
@@ -153,7 +169,9 @@ describe('usePromptActions /title', () => {
)
let handle: HarnessHandle | null = null
- render( (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
+ await actRender(
+ (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />
+ )
await handle!.submitText('/title New title')
@@ -177,7 +195,9 @@ describe('usePromptActions /title', () => {
)
let handle: HarnessHandle | null = null
- render( (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
+ await actRender(
+ (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />
+ )
await handle!.submitText('/title Fresh chat')
@@ -195,7 +215,9 @@ describe('usePromptActions /title', () => {
const requestGateway = vi.fn(async () => ({ output: 'Title: Old title' }) as never)
let handle: HarnessHandle | null = null
- render( (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
+ await actRender(
+ (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />
+ )
await handle!.submitText('/title')
@@ -215,7 +237,9 @@ describe('usePromptActions /title', () => {
})
let handle: HarnessHandle | null = null
- render( (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />)
+ await actRender(
+ (handle = h)} refreshSessions={refreshSessions} requestGateway={requestGateway} />
+ )
await handle!.submitText('/title way too long title')
@@ -254,7 +278,7 @@ describe('usePromptActions slash.exec dispatch payloads', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={s => states.push(s)}
@@ -304,7 +328,7 @@ describe('usePromptActions slash.exec dispatch payloads', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={s => states.push(s)}
@@ -342,7 +366,7 @@ describe('usePromptActions slash.exec dispatch payloads', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -372,7 +396,7 @@ describe('usePromptActions desktop slash pickers', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -392,7 +416,7 @@ describe('usePromptActions desktop slash pickers', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
openMemoryGraph={openMemoryGraph}
@@ -425,7 +449,7 @@ describe('usePromptActions desktop slash pickers', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -455,7 +479,7 @@ describe('usePromptActions submit / queue drain semantics', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={s => seeds.push(s)}
@@ -488,7 +512,7 @@ describe('usePromptActions submit / queue drain semantics', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
@@ -530,7 +554,7 @@ describe('usePromptActions submit / queue drain semantics', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -574,7 +598,7 @@ describe('usePromptActions submit / queue drain semantics', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={s => seeds.push(s)}
@@ -596,7 +620,7 @@ describe('usePromptActions submit / queue drain semantics', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
@@ -622,7 +646,7 @@ describe('usePromptActions steerPrompt', () => {
const requestGateway = vi.fn(async () => ({ status: 'queued' }) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -641,7 +665,7 @@ describe('usePromptActions steerPrompt', () => {
const requestGateway = vi.fn(async () => ({ status: 'rejected' }) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -654,7 +678,7 @@ describe('usePromptActions steerPrompt', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -665,7 +689,7 @@ describe('usePromptActions steerPrompt', () => {
const requestGateway = vi.fn(async () => ({ status: 'queued' }) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -697,7 +721,7 @@ describe('usePromptActions restoreToMessage', () => {
let lastState: Record = {}
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={state => (lastState = state)}
@@ -732,7 +756,7 @@ describe('usePromptActions restoreToMessage', () => {
let lastState: Record = {}
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={state => (lastState = state)}
@@ -765,7 +789,7 @@ describe('usePromptActions restoreToMessage', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -793,7 +817,7 @@ describe('usePromptActions restoreToMessage', () => {
const requestGateway = vi.fn(async () => ({}) as never)
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -808,7 +832,7 @@ describe('usePromptActions restoreToMessage', () => {
let lastState: Record = {}
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={state => (lastState = state)}
@@ -882,7 +906,7 @@ describe('usePromptActions file attachment sync', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -936,7 +960,7 @@ describe('usePromptActions file attachment sync', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -965,7 +989,7 @@ describe('usePromptActions file attachment sync', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -1025,7 +1049,7 @@ describe('usePromptActions eager-upload races', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
await waitFor(() => expect(handle).not.toBeNull())
@@ -1083,7 +1107,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -1126,7 +1150,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -1159,7 +1183,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
onSeedState={s => states.push(s)}
@@ -1189,7 +1213,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -1233,7 +1257,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
(handle = h)}
refreshSessions={async () => undefined}
@@ -1273,7 +1297,7 @@ describe('usePromptActions sleep/wake session recovery', () => {
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
{
})
let handle: HarnessHandle | null = null
- render(
+ await actRender(
{
{ id: 'file:devis', kind: 'file', label: 'DEVIS_signed.pdf', path: '/Users/mahmoud/Downloads/DEVIS_signed.pdf' }
])
- render(
+ await actRender(
undefined} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -1622,7 +1646,7 @@ describe('usePromptActions eager attachment upload (drop-time)', () => {
$composerAttachments.set([{ id: 'file:x', kind: 'file', label: 'x.pdf', path: '/abs/x.pdf' }])
- render(
+ await actRender(
undefined} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
@@ -1646,7 +1670,7 @@ describe('usePromptActions eager attachment upload (drop-time)', () => {
}
])
- render(
+ await actRender(
undefined} refreshSessions={async () => undefined} requestGateway={requestGateway} />
)
diff --git a/apps/desktop/src/app/settings/provider-config-panel.test.tsx b/apps/desktop/src/app/settings/provider-config-panel.test.tsx
index 774d45f40793..fa95f9dcd70a 100644
--- a/apps/desktop/src/app/settings/provider-config-panel.test.tsx
+++ b/apps/desktop/src/app/settings/provider-config-panel.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { MemoryProviderConfig } from '@/types/hermes'
@@ -97,7 +97,12 @@ afterEach(() => {
async function renderPanel(provider = 'hindsight') {
const { ProviderConfigPanel } = await import('./provider-config-panel')
- return render()
+ let result: ReturnType
+ await act(async () => {
+ result = render()
+ })
+
+ return result!
}
describe('ProviderConfigPanel', () => {
@@ -115,9 +120,13 @@ describe('ProviderConfigPanel', () => {
await renderPanel()
expect(await screen.findByLabelText('API URL')).toBeTruthy()
- fireEvent.click(screen.getByRole('button', { name: /Hindsight settings/ }))
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /Hindsight settings/ }))
+ })
expect(screen.queryByLabelText('API URL')).toBeNull()
- fireEvent.click(screen.getByRole('button', { name: /Hindsight settings/ }))
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /Hindsight settings/ }))
+ })
expect(await screen.findByLabelText('API URL')).toBeTruthy()
})
@@ -125,9 +134,11 @@ describe('ProviderConfigPanel', () => {
await renderPanel()
const apiUrl = await screen.findByLabelText('API URL')
- fireEvent.change(apiUrl, { target: { value: 'http://localhost:8888' } })
- fireEvent.change(screen.getByLabelText('Bank ID'), { target: { value: 'ben-bank' } })
- fireEvent.click(screen.getByRole('button', { name: 'Save' }))
+ await act(async () => {
+ fireEvent.change(apiUrl, { target: { value: 'http://localhost:8888' } })
+ fireEvent.change(screen.getByLabelText('Bank ID'), { target: { value: 'ben-bank' } })
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
+ })
await waitFor(() =>
expect(saveMemoryProviderConfig).toHaveBeenCalledWith('hindsight', {
diff --git a/apps/desktop/src/app/settings/providers-settings.test.tsx b/apps/desktop/src/app/settings/providers-settings.test.tsx
index 8a894e27ab15..ac63be962ae2 100644
--- a/apps/desktop/src/app/settings/providers-settings.test.tsx
+++ b/apps/desktop/src/app/settings/providers-settings.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { atom } from 'nanostores'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -73,8 +73,12 @@ afterEach(() => {
async function renderProvidersSettings() {
const { ProvidersSettings } = await import('./providers-settings')
+ let result: ReturnType
+ await act(async () => {
+ result = render()
+ })
- return render()
+ return result!
}
describe('ProvidersSettings', () => {
@@ -82,7 +86,9 @@ describe('ProvidersSettings', () => {
await renderProvidersSettings()
const remove = await screen.findByRole('button', { name: 'Remove Nous Portal' })
- fireEvent.click(remove)
+ await act(async () => {
+ fireEvent.click(remove)
+ })
await waitFor(() => expect(disconnectOAuthProvider).toHaveBeenCalledWith('nous'))
expect(listOAuthProviders).toHaveBeenCalledTimes(2)
@@ -91,7 +97,9 @@ describe('ProvidersSettings', () => {
it('keeps provider selection separate from account removal', async () => {
await renderProvidersSettings()
- fireEvent.click(await screen.findByText('Nous Portal'))
+ await act(async () => {
+ fireEvent.click(await screen.findByText('Nous Portal'))
+ })
expect(startManualProviderOAuth).toHaveBeenCalledWith('nous')
expect(disconnectOAuthProvider).not.toHaveBeenCalled()
@@ -132,7 +140,9 @@ describe('ProvidersSettings', () => {
listOAuthProviders.mockResolvedValue({ providers: [] })
const { ProvidersSettings } = await import('./providers-settings')
- render()
+ await act(async () => {
+ render()
+ })
expect(await screen.findByText('WidgetAI')).toBeTruthy()
})
@@ -158,14 +168,18 @@ describe('ProvidersSettings', () => {
// Typing narrows the list to matching providers only.
const search = screen.getByPlaceholderText('Search providers…')
- fireEvent.change(search, { target: { value: 'mid' } })
+ await act(async () => {
+ fireEvent.change(search, { target: { value: 'mid' } })
+ })
await waitFor(() => expect(screen.queryByText('Acme')).toBeNull())
expect(screen.getByText('Middle')).toBeTruthy()
expect(screen.queryByText('Zebra')).toBeNull()
// A non-matching query shows the empty-state copy.
- fireEvent.change(search, { target: { value: 'nonesuch-xyz' } })
+ await act(async () => {
+ fireEvent.change(search, { target: { value: 'nonesuch-xyz' } })
+ })
expect(await screen.findByText('No providers match your search.')).toBeTruthy()
})
})
diff --git a/apps/desktop/src/app/skills/index.test.tsx b/apps/desktop/src/app/skills/index.test.tsx
index fe3e39a72c76..f4057a4f2b88 100644
--- a/apps/desktop/src/app/skills/index.test.tsx
+++ b/apps/desktop/src/app/skills/index.test.tsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { QueryClientProvider } from '@tanstack/react-query'
-import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -48,9 +48,11 @@ function toolset(overrides: Record = {}) {
}
}
-function renderSkills() {
- return import('./index').then(({ SkillsView }) =>
- render(
+async function renderSkills() {
+ const { SkillsView } = await import('./index')
+ let result: ReturnType
+ await act(async () => {
+ result = render(
// SkillsView reads skills/toolsets via useQuery, so it needs a provider.
@@ -58,7 +60,9 @@ function renderSkills() {
)
- )
+ })
+
+ return result!
}
beforeEach(() => {
@@ -83,7 +87,9 @@ describe('SkillsView toolset management', () => {
const sw = await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
expect(sw.getAttribute('aria-checked')).toBe('true')
- fireEvent.click(sw)
+ await act(async () => {
+ fireEvent.click(sw)
+ })
await waitFor(() => expect(toggleToolset).toHaveBeenCalledWith('web', false))
})
diff --git a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
index 0225e8cb22d0..1a4fcca270a7 100644
--- a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
+++ b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, render, screen } from '@testing-library/react'
+import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { $desktopBoot } from '@/store/boot'
@@ -61,7 +61,7 @@ const isRecoveryShown = () =>
Boolean(screen.queryByText(/use local gateway/i) || screen.queryByText(/retry/i) || screen.queryByText(/sign in/i))
describe('connecting overlay vs recovery surface', () => {
- it('hard initial-boot failure surfaces the recovery overlay (the working path)', () => {
+ it('hard initial-boot failure surfaces the recovery overlay (the working path)', async () => {
// failDesktopBoot() ran: error set, gateway never opened.
$desktopBoot.set({
...$desktopBoot.get(),
@@ -71,28 +71,35 @@ describe('connecting overlay vs recovery surface', () => {
})
setGatewayState('error')
- render(
- <>
-
-
- >
- )
+ await act(async () => {
+ render(
+ <>
+
+
+ >
+ )
+ })
expect(isRecoveryShown()).toBe(true)
// Connecting overlay bows out when boot.error is set.
expect(isConnectingShown()).toBe(false)
})
- it('post-boot socket drops do not re-cover the app with the initial CONNECTING overlay', () => {
+ it('post-boot socket drops do not re-cover the app with the initial CONNECTING overlay', async () => {
// 1. Initial boot succeeded: gateway opened, boot completed (no error).
setGatewayState('open')
- const { rerender } = render(
- <>
-
-
- >
- )
+ let rerender!: (ui: React.ReactElement) => void
+ await act(async () => {
+ const result = render(
+ <>
+
+
+ >
+ )
+
+ rerender = result.rerender
+ })
expect(isConnectingShown()).toBe(false)
@@ -100,12 +107,14 @@ describe('connecting overlay vs recovery surface', () => {
// bootCompleted is true, so useGatewayBoot routes this through
// scheduleReconnect() — boot.error stays NULL.
setGatewayState('closed')
- rerender(
- <>
-
-
- >
- )
+ await act(async () => {
+ rerender!(
+ <>
+
+
+ >
+ )
+ })
// The initial-boot connecting overlay stays out of the way, so settings and
// the composer remain reachable during the reconnect loop.
@@ -116,12 +125,14 @@ describe('connecting overlay vs recovery surface', () => {
// → error → closed. Until the escalation path sets boot.error, the app
// remains usable instead of modal-blocked.
setGatewayState('error')
- rerender(
- <>
-
-
- >
- )
+ await act(async () => {
+ rerender!(
+ <>
+
+
+ >
+ )
+ })
expect($desktopBoot.get().error).toBeNull()
expect(isConnectingShown()).toBe(false)
expect(isRecoveryShown()).toBe(false)
@@ -156,7 +167,7 @@ describe('connecting overlay vs recovery surface', () => {
expect(isRecoveryShown()).toBe(false)
})
- it('FIX: once the prolonged reconnect raises a recoverable boot error, the recovery overlay takes over', () => {
+ it('FIX: once the prolonged reconnect raises a recoverable boot error, the recovery overlay takes over', async () => {
// Mirrors what useGatewayBoot.scheduleReconnect() now does after ~45s of
// failed post-boot reconnects: it calls failDesktopBoot(), flipping the UI
// from the dead-end CONNECTING overlay to the recovery surface.
@@ -168,12 +179,14 @@ describe('connecting overlay vs recovery surface', () => {
visible: true
})
- render(
- <>
-
-
- >
- )
+ await act(async () => {
+ render(
+ <>
+
+
+ >
+ )
+ })
// Escape hatch is now reachable; the connecting overlay bows out.
expect(isRecoveryShown()).toBe(true)
diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts
index 7ec1b7f80949..4bdc3793fa53 100644
--- a/apps/desktop/vitest.config.ts
+++ b/apps/desktop/vitest.config.ts
@@ -11,6 +11,7 @@ export default defineConfig({
},
test: {
environment: "jsdom",
+ setupFiles: ["./vitest.setup.ts"],
include: ["src/**/*.test.{ts,tsx}"],
globals: true
},
diff --git a/apps/desktop/vitest.setup.ts b/apps/desktop/vitest.setup.ts
new file mode 100644
index 000000000000..a671ac3ae971
--- /dev/null
+++ b/apps/desktop/vitest.setup.ts
@@ -0,0 +1,6 @@
+import '@testing-library/react'
+
+// React 19 + Testing Library 16: opt into the act environment so render(),
+// fireEvent(), and findBy* queries automatically flush state updates without
+// spurious "not wrapped in act(...)" warnings.
+;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true