test(mobile): centralize renderer setup (#13408)

This commit is contained in:
Neil 2026-08-09 18:31:00 -07:00 committed by GitHub
parent 954de38507
commit c56ff58dd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 508 additions and 1449 deletions

View File

@ -157,28 +157,12 @@ const RESET_SNAPSHOT = {
}
} as const
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => spy.mockRestore()
}
async function renderAccountsRoute(): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(AccountsScreen))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(AccountsScreen))
await Promise.resolve()
})
if (!renderer) {
throw new Error('Accounts route did not render')
}
@ -229,7 +213,6 @@ describe('accounts route Codex reset credit', () => {
let storedValues: Map<string, string>
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
resetCodexResetAttemptJournalForTests()
storedValues = new Map()
dependencies.alert.mockReset()

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { CodexResetCreditAction } from './CodexResetCreditAction'
vi.mock('react-native', () => ({
@ -19,35 +19,19 @@ const summary = {
expiryLabel: 'Expires in 5d'
}
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
function renderAction(busy: boolean, disabled: boolean): ReactTestRenderer {
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
act(() => {
renderer = create(
createElement(CodexResetCreditAction, {
summary,
scopeLabel: 'dev@example.com on the host',
busy,
disabled,
onPress: vi.fn()
})
)
})
} finally {
restore()
}
act(() => {
renderer = create(
createElement(CodexResetCreditAction, {
summary,
scopeLabel: 'dev@example.com on the host',
busy,
disabled,
onPress: vi.fn()
})
)
})
if (!renderer) {
throw new Error('Reset action did not render')
}
@ -55,10 +39,6 @@ function renderAction(busy: boolean, disabled: boolean): ReactTestRenderer {
}
describe('CodexResetCreditAction', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('exposes a 44pt touch target and enabled accessibility state', () => {
const renderer = renderAction(false, false)
const button = renderer.root.findByType('Pressable')

View File

@ -78,7 +78,6 @@ describe('HostProtocolGate', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
nativeTestState.openUrl.mockClear()
nativeTestState.platform.OS = 'ios'
probeMounts.count = 0

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HostProfile } from '../transport/types'
import { MobileHomeQuickActions } from './MobileHomeQuickActions'
@ -37,10 +37,6 @@ function host(id: string, name: string, endpoint: string): HostProfile {
describe('MobileHomeQuickActions', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MobileHostCard } from './MobileHostCard'
vi.mock('react-native', () => ({
@ -30,10 +30,6 @@ function suppressRendererDeprecation() {
describe('MobileHostCard', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ConnectionVerdict } from '../transport/connection-health'
import type { MobileConnectionPath } from '../transport/stable-logical-rpc-client'
import type { ConnectionState, HostCredentialStatus, HostProfile } from '../transport/types'
@ -39,10 +39,6 @@ const loaded: HostWorktreeInfo = {
describe('MobileHostCard', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -38,7 +38,6 @@ describe('MobileMarkdown file links', () => {
const onOpenFile = vi.fn()
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
onOpenFile.mockClear()
openURL.mockClear()
})

View File

@ -58,7 +58,6 @@ describe('NewWorktreeModal repo list', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
setCachedRepos('host-1', repos)
})

View File

@ -24,7 +24,6 @@ describe('PickerModal accessibility', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] !== 'string' || !args[0].includes('react-test-renderer is deprecated')) {
throw new Error(String(args[0]))

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types'
import { WorktreeAgentList } from './WorktreeAgentList'
@ -39,10 +39,6 @@ function agent(paneKey: string, parentPaneKey: string | null = null): RuntimeWor
describe('WorktreeAgentList', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -127,7 +127,6 @@ describe('memoized worktree rows', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
agentSpinnerRender.mockClear()
agentStateDotRender.mockClear()
})

View File

@ -51,14 +51,12 @@ function mountedDrawer(renderer: ReactTestRenderer) {
describe('BottomDrawer close lifecycle', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const originalConsoleError = console.error
vi.spyOn(console, 'error').mockImplementation((...args) => {
const message = args[0]
if (
typeof message === 'string' &&
(message.includes('react-test-renderer is deprecated') ||
message.includes('The current testing environment is not configured to support act'))
message.includes('The current testing environment is not configured to support act')
) {
return
}

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MobileAgentIcon } from './MobileAgentIcon'
vi.mock('react-native', () => ({
@ -35,10 +35,6 @@ vi.mock('./AgentIcons', () => ({
describe('MobileAgentIcon OMP gradient', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -32,18 +32,6 @@ vi.mock('./pr-comments-styles', () => ({ prCommentsStyles: {} }))
vi.mock('./mobile-pr-sidebar-styles', () => ({ mobilePrSidebarStyles: {} }))
vi.mock('../../theme/mobile-theme', () => ({ colors: { textSecondary: '#999' } }))
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
function comment(id: number): PRComment {
return {
id,
@ -65,14 +53,9 @@ function detailsWithComments(count: number): GitHubWorkItemDetails {
async function renderComments(details: GitHubWorkItemDetails): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(PRCommentsSection, { details, prState: 'open' }))
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(PRCommentsSection, { details, prState: 'open' }))
})
if (!renderer) {
throw new Error('PRCommentsSection did not render')
}
@ -107,7 +90,6 @@ describe('PRCommentsSection', () => {
afterEach(() => {
renderer?.unmount()
renderer = null
vi.restoreAllMocks()
})
it('resets pagination only when the user chooses a different audience filter', async () => {

View File

@ -83,18 +83,6 @@ vi.mock('../transport/client-context', () => ({
})
}))
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
function entry(name: string, isDirectory = false): MobileDirEntry {
return { name, isDirectory }
}
@ -113,21 +101,16 @@ function createMockClient(entriesByPath: Record<string, MobileDirEntry[]>): Mock
async function renderExplorer(): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileFileExplorerPanel, {
hostId: 'host-a',
worktreeId: 'worktree-a',
name: 'Example Worktree',
embedded: true
})
)
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(
createElement(MobileFileExplorerPanel, {
hostId: 'host-a',
worktreeId: 'worktree-a',
name: 'Example Worktree',
embedded: true
})
)
})
if (!renderer) {
throw new Error('MobileFileExplorerPanel did not render')
}

View File

@ -33,28 +33,11 @@ vi.mock('./mobile-file-preview-styles', () => ({
type PreviewProps = Parameters<typeof MobileFileMarkdownPreview>[0]
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
async function renderPreview(props: PreviewProps): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(MobileFileMarkdownPreview, props))
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(MobileFileMarkdownPreview, props))
})
if (!renderer) {
throw new Error('MobileFileMarkdownPreview did not render')
}

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const appState = vi.hoisted(() => ({
current: 'active',
@ -25,7 +25,6 @@ import { useNow } from './use-now'
describe('useNow', () => {
let renderer: ReactTestRenderer | null = null
let latest = 0
let consoleSpy: MockInstance
function Harness({ enabled = true }: { enabled?: boolean }): null {
latest = useNow(1_000, enabled)
@ -42,18 +41,10 @@ describe('useNow', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
globalThis.IS_REACT_ACT_ENVIRONMENT = true
appState.current = 'active'
appState.listener = null
appState.remove.mockClear()
latest = 0
const original = console.error
consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
act(() => {
renderer = create(createElement(Harness))
})
@ -63,7 +54,6 @@ describe('useNow', () => {
act(() => renderer?.unmount())
renderer = null
vi.useRealTimers()
consoleSpy.mockRestore()
})
it('ticks while active, pauses in the background, and refreshes immediately on resume', () => {

View File

@ -46,29 +46,12 @@ vi.mock('./transport/client-context', () => ({
usePrimeHosts: () => dependencies.primeHosts
}))
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
async function renderEditHostRoute(): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(EditHostScreen))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(EditHostScreen))
await Promise.resolve()
})
if (!renderer) {
throw new Error('Edit host route did not render')
}
@ -77,7 +60,6 @@ async function renderEditHostRoute(): Promise<ReactTestRenderer> {
describe('edit host route accessibility', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
dependencies.loadHosts.mockReset().mockResolvedValue([
{
id: 'host-1',

View File

@ -56,29 +56,12 @@ const HOST_FIXTURE = {
lastConnected: 1
}
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
async function renderEditHostRoute(): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(EditHostScreen))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(EditHostScreen))
await Promise.resolve()
})
if (!renderer) {
throw new Error('Edit host route did not render')
}
@ -135,7 +118,6 @@ function findText(renderer: ReactTestRenderer, match: string): boolean {
describe('edit host handleSave', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
dependencies.hostId = 'host-1'
dependencies.back.mockReset()
dependencies.forceReconnectHost.mockReset().mockResolvedValue(undefined)
@ -285,7 +267,6 @@ describe('edit host handleSave', () => {
describe('edit host load() error states', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
dependencies.hostId = 'host-1'
dependencies.back.mockReset()
dependencies.forceReconnectHost.mockReset().mockResolvedValue(undefined)

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MobileOnboardingPage } from './MobileOnboardingPage'
vi.mock('react-native', async () => {
@ -24,10 +24,6 @@ vi.mock('lucide-react-native', () => ({
describe('MobileOnboardingPage', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -56,7 +56,6 @@ describe('MobileOnboardingScreen', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
mocks.params = { hostId: 'paired-host', steps: 'session-view,notifications' }
mocks.replace.mockReset()
mocks.reducedMotionEnabled = false

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { radii, spacing } from '../theme/mobile-theme'
import { MobileNativeChatComposer } from './MobileNativeChatComposer'
@ -43,24 +43,9 @@ vi.mock('../components/BottomDrawer', async () => {
}
})
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
describe('MobileNativeChatComposer', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -71,21 +56,16 @@ describe('MobileNativeChatComposer', () => {
onChangeText: () => void,
isAttaching = false
) {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: ' hello ',
onChangeText,
onSend,
isAttaching
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: ' hello ',
onChangeText,
onSend,
isAttaching
})
)
})
}
function sendButton(): { props: { onPress: () => Promise<void> } } {
@ -130,20 +110,15 @@ describe('MobileNativeChatComposer', () => {
it('preserves leading whitespace so prose is not turned into a slash command', async () => {
const onSend = vi.fn().mockResolvedValue(true)
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: ' /clear is prose ',
onChangeText: vi.fn(),
onSend
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: ' /clear is prose ',
onChangeText: vi.fn(),
onSend
})
)
})
await act(async () => sendButton().props.onPress())
expect(onSend).toHaveBeenCalledWith(' /clear is prose')
})
@ -181,67 +156,57 @@ describe('MobileNativeChatComposer', () => {
invokeAction: vi.fn(),
recordCommand: vi.fn()
}
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'run the tests',
onChangeText: vi.fn(),
onSend,
sessionOptions: { isWorking: false, controller }
})
)
})
const modelPill = (): { props: { accessibilityState: { disabled: boolean } } } =>
renderer!.root.find(
(node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Model, Model'
) as { props: { accessibilityState: { disabled: boolean } } }
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: false })
// Start the send but don't await it — it stays in flight on purpose.
let pressed!: Promise<void>
await act(async () => {
pressed = sendButton().props.onPress()
await Promise.resolve()
})
expect(onSend).toHaveBeenCalled()
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: true })
await act(async () => {
releaseSend?.(true)
await pressed
})
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: false })
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'run the tests',
onChangeText: vi.fn(),
onSend,
sessionOptions: { isWorking: false, controller }
})
)
})
const modelPill = (): { props: { accessibilityState: { disabled: boolean } } } =>
renderer!.root.find(
(node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Model, Model'
) as { props: { accessibilityState: { disabled: boolean } } }
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: false })
// Start the send but don't await it — it stays in flight on purpose.
let pressed!: Promise<void>
await act(async () => {
pressed = sendButton().props.onPress()
await Promise.resolve()
})
expect(onSend).toHaveBeenCalled()
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: true })
await act(async () => {
releaseSend?.(true)
await pressed
})
expect(modelPill().props.accessibilityState).toMatchObject({ disabled: false })
})
it('blocks composer submission while a session-option command is pending', async () => {
const onSend = vi.fn().mockResolvedValue(true)
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'hello',
onChangeText: vi.fn(),
onSend,
sessionOptions: {
isWorking: false,
controller: {
snapshot: [],
pendingId: 'model',
setOption: vi.fn(),
invokeAction: vi.fn(),
recordCommand: vi.fn()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'hello',
onChangeText: vi.fn(),
onSend,
sessionOptions: {
isWorking: false,
controller: {
snapshot: [],
pendingId: 'model',
setOption: vi.fn(),
invokeAction: vi.fn(),
recordCommand: vi.fn()
}
})
)
})
} finally {
restore()
}
}
})
)
})
expect(sendButton().props).toMatchObject({ disabled: true })
await act(async () => sendButton().props.onPress())
expect(onSend).not.toHaveBeenCalled()
@ -268,21 +233,16 @@ describe('MobileNativeChatComposer', () => {
})
it('keeps the text input editable while the send is locked', async () => {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'half-typed',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
disabled: true
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: 'half-typed',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
disabled: true
})
)
})
// Revoking `editable` on a focused field resigns first responder on iOS and
// yanks the keyboard mid-typing (#10681) — the lock may only gate sending.
const input = renderer!.root.find((node) => node.type === 'TextInput') as {
@ -294,25 +254,20 @@ describe('MobileNativeChatComposer', () => {
it('renders a removable thumbnail for each pending image attachment', async () => {
const onRemoveAttachment = vi.fn()
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
attachments: [
{ id: 'img-1', path: '/tmp/a.png', previewUri: 'file:///a.png' },
{ id: 'img-2', path: '/tmp/b.png', previewUri: 'file:///b.png' }
],
onRemoveAttachment
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
attachments: [
{ id: 'img-1', path: '/tmp/a.png', previewUri: 'file:///a.png' },
{ id: 'img-2', path: '/tmp/b.png', previewUri: 'file:///b.png' }
],
onRemoveAttachment
})
)
})
const thumbs = renderer!.root.findAll((node) => node.type === 'Image') as Array<{
props: { source: { uri: string } }
}>
@ -327,42 +282,32 @@ describe('MobileNativeChatComposer', () => {
it('enables send with an attached image even when the text is empty', async () => {
const onSend = vi.fn().mockResolvedValue(true)
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend,
attachments: [{ id: 'img-1', path: '/tmp/a.png', previewUri: 'file:///a.png' }]
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend,
attachments: [{ id: 'img-1', path: '/tmp/a.png', previewUri: 'file:///a.png' }]
})
)
})
expect(sendButton().props).toMatchObject({ disabled: false })
await act(async () => sendButton().props.onPress())
expect(onSend).toHaveBeenCalledWith('')
})
it('moves the caret to the insert point after an autocomplete pick, then releases control', async () => {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '/c',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
agent: 'claude'
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '/c',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
agent: 'claude'
})
)
})
const input = () =>
renderer!.root.find((node) => node.type === 'TextInput') as {
props: {
@ -390,21 +335,16 @@ describe('MobileNativeChatComposer', () => {
})
it('serves the active agents shared command catalog with descriptions', async () => {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '/',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
agent: 'codex'
})
)
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '/',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
agent: 'codex'
})
)
})
const input = renderer!.root.find((node) => node.type === 'TextInput') as {
props: { onSelectionChange: (e: { nativeEvent: { selection: { end: number } } }) => void }
}
@ -428,45 +368,40 @@ describe('MobileNativeChatComposer', () => {
(node) => node.type === 'Pressable' && node.props.accessibilityLabel === 'Dictate'
) as { props: { onPress?: unknown; onPressIn?: unknown; onPressOut?: unknown } }
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
onMicPress,
dictationMode: 'hold',
onMicPressIn,
onMicPressOut
})
)
})
// Hold mode is walkie-talkie: press-in/out drive dictation, tap is inert.
expect(mic().props.onPress).toBeUndefined()
expect(mic().props.onPressIn).toBe(onMicPressIn)
expect(mic().props.onPressOut).toBe(onMicPressOut)
await act(async () => {
renderer = create(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
onMicPress,
dictationMode: 'hold',
onMicPressIn,
onMicPressOut
})
)
})
// Hold mode is walkie-talkie: press-in/out drive dictation, tap is inert.
expect(mic().props.onPress).toBeUndefined()
expect(mic().props.onPressIn).toBe(onMicPressIn)
expect(mic().props.onPressOut).toBe(onMicPressOut)
await act(async () => {
renderer!.update(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
onMicPress,
dictationMode: 'toggle',
onMicPressIn,
onMicPressOut
})
)
})
// Toggle mode: tap drives dictation, press-in/out inert.
expect(mic().props.onPress).toBe(onMicPress)
expect(mic().props.onPressIn).toBeUndefined()
expect(mic().props.onPressOut).toBeUndefined()
} finally {
restore()
}
await act(async () => {
renderer!.update(
createElement(MobileNativeChatComposer, {
value: '',
onChangeText: vi.fn(),
onSend: vi.fn().mockResolvedValue(true),
onMicPress,
dictationMode: 'toggle',
onMicPressIn,
onMicPressOut
})
)
})
// Toggle mode: tap drives dictation, press-in/out inert.
expect(mic().props.onPress).toBe(onMicPress)
expect(mic().props.onPressIn).toBeUndefined()
expect(mic().props.onPressOut).toBeUndefined()
})
})

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { MAX_TOOL_DETAIL_LENGTH } from './mobile-native-chat-tool-summary'
@ -38,9 +38,6 @@ function toolMessage(blocks: NativeChatMessage['blocks']): NativeChatMessage {
describe('MobileNativeChatMessage', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -50,20 +47,9 @@ describe('MobileNativeChatMessage', () => {
message: NativeChatMessage,
props: { toolsExpanded?: boolean } = {}
): ReactTestRenderer {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(MobileNativeChatMessage, { message, ...props }))
})
try {
act(() => {
renderer = create(createElement(MobileNativeChatMessage, { message, ...props }))
})
} finally {
spy.mockRestore()
}
return renderer!
}

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { MobileNativeChatOverlay } from './MobileNativeChatOverlay'
import type { MobileNativeChatController } from './use-mobile-native-chat-controller'
@ -16,17 +16,6 @@ function assistantTurn(id: string, text: string): NativeChatMessage {
return { id, role: 'assistant', blocks: [{ type: 'text', text }], timestamp: 0, source: 'hook' }
}
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
/** One render of the route: chat visible or not, the transcript it currently
* holds, and the agent-status stream behind it. */
type Tick = {
@ -69,24 +58,15 @@ function overlayElement(tick: Tick): ReturnType<typeof createElement> {
describe('MobileNativeChatOverlay streaming gate', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
})
async function render(tick: Tick): Promise<void> {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(overlayElement(tick))
})
} finally {
restore()
}
await act(async () => {
renderer = create(overlayElement(tick))
})
}
async function update(tick: Tick): Promise<void> {

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MobileNativeChatPermission } from './MobileNativeChatPermission'
vi.mock('react-native', () => ({
@ -15,10 +15,6 @@ vi.mock('lucide-react-native', () => ({ ShieldQuestion: 'ShieldQuestion' }))
describe('MobileNativeChatPermission', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -28,25 +24,14 @@ describe('MobileNativeChatPermission', () => {
let resolveResponse: (accepted: boolean) => void = () => {}
const response = new Promise<boolean>((resolve) => (resolveResponse = resolve))
const onRespond = vi.fn(() => response)
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(
createElement(MobileNativeChatPermission, {
permission: { title: 'Approve?', options: [{ label: 'Allow', send: '1' }] },
onRespond
})
)
})
try {
await act(async () => {
renderer = create(
createElement(MobileNativeChatPermission, {
permission: { title: 'Approve?', options: [{ label: 'Allow', send: '1' }] },
onRespond
})
)
})
} finally {
consoleSpy.mockRestore()
}
const button = renderer.root.findByType('Pressable')
act(() => {

View File

@ -149,7 +149,6 @@ describe('MobileNativeChatSessionOptionPickers', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
setOption.mockReset()
setOption.mockResolvedValue(true)
invokeAction.mockReset()

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { MobileNativeChatView } from './MobileNativeChatView'
@ -73,17 +73,6 @@ type Overrides = {
onSend?: (text: string) => Promise<boolean>
}
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
function assistantTurn(id: string, text: string): NativeChatMessage {
return { id, role: 'assistant', blocks: [{ type: 'text', text }], timestamp: 0, source: 'hook' }
}
@ -105,24 +94,15 @@ function chatViewElement(overrides: Overrides): ReturnType<typeof createElement>
describe('MobileNativeChatView', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
})
async function render(overrides: Overrides = {}): Promise<void> {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(chatViewElement(overrides))
})
} finally {
restore()
}
await act(async () => {
renderer = create(chatViewElement(overrides))
})
}
async function update(overrides: Overrides = {}): Promise<void> {

View File

@ -29,9 +29,7 @@ vi.mock('../components/MobileAgentIcon', () => ({ MobileAgentIcon: 'MobileAgentI
describe('QuickCommandsList search', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
beforeEach(() => {})
afterEach(() => {
act(() => renderer?.unmount())

View File

@ -1,6 +1,6 @@
import { createElement, type ReactNode } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { RpcClient } from '../transport/rpc-client'
import { MAX_QUICK_COMMANDS } from '../terminal/quick-commands'
@ -65,26 +65,15 @@ function deferred<T>() {
describe('QuickCommandsSheet', () => {
let renderer: ReactTestRenderer | null = null
let consoleSpy: MockInstance
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
mocks.alert.mockReset()
mocks.commands = []
mocks.persist.mockReset()
const originalConsoleError = console.error
consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
consoleSpy.mockRestore()
})
it('keeps the sheet open when a launch is rejected', async () => {

View File

@ -67,7 +67,6 @@ describe('useMobileNativeChatPermissionSend', () => {
let respond: ((text: string) => Promise<boolean>) | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
resetMobileNativeChatStaleInputForTests()
resetMobileNativeChatTerminalWritesForTests()
})

View File

@ -52,7 +52,6 @@ describe('useInitialSessionTerminalAutoCreate', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
stateRef = {
current: { autoCreatedForWorktree: null, sawSessionTabs: false }
}

View File

@ -13,18 +13,6 @@ vi.mock('expo-router', async () => {
}
})
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
describe('useLiveWorktreeName request volume', () => {
let renderer: ReactTestRenderer | null = null
let eventListener: ((payload: unknown) => void) | null = null
@ -54,15 +42,10 @@ describe('useLiveWorktreeName request volume', () => {
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(Harness))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(Harness))
await Promise.resolve()
})
}
async function emitEvent(payload: unknown): Promise<void> {
@ -75,7 +58,6 @@ describe('useLiveWorktreeName request volume', () => {
beforeEach(() => {
vi.useFakeTimers()
globalThis.IS_REACT_ACT_ENVIRONMENT = true
eventListener = null
sendRequest.mockClear().mockResolvedValue({
id: 'worktree-show',
@ -179,15 +161,10 @@ describe('useLiveWorktreeName request volume', () => {
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(FloatingHarness))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(FloatingHarness))
await Promise.resolve()
})
await act(async () => {
await vi.advanceTimersByTimeAsync(30_000)
})
@ -214,7 +191,6 @@ describe('useLiveWorktreeName request volume', () => {
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(
@ -241,7 +217,6 @@ describe('useLiveWorktreeName request volume', () => {
)
})
} finally {
restoreConsoleError()
act(() => renderer?.unmount())
}
@ -263,15 +238,10 @@ describe('useLiveWorktreeName request volume', () => {
return null
}
const mount = async (): Promise<void> => {
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(VerdictHarness))
await Promise.resolve()
})
} finally {
restoreConsoleError()
}
await act(async () => {
renderer = create(createElement(VerdictHarness))
await Promise.resolve()
})
}
await mount()

View File

@ -27,7 +27,6 @@ describe('useMissingWorktreeBounce', () => {
const bounce = vi.fn()
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
bounce.mockReset()
renderer = null
})

View File

@ -7,16 +7,7 @@ type Gate = (targetHandle: string) => Promise<boolean>
describe('useMobileAttachmentInputLeaseGate', () => {
let renderer: ReactTestRenderer | null = null
let errorSpy: ReturnType<typeof vi.spyOn> | null = null
beforeEach(() => {
const original = console.error
errorSpy = vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...(args as Parameters<typeof console.error>))
})
vi.useFakeTimers()
})
@ -24,7 +15,6 @@ describe('useMobileAttachmentInputLeaseGate', () => {
act(() => renderer?.unmount())
renderer = null
vi.useRealTimers()
errorSpy?.mockRestore()
})
function renderGate(args: {

View File

@ -30,7 +30,6 @@ describe('useMobileDefaultSessionViewPreference', () => {
let preference: MobileDefaultSessionViewPreference | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
vi.mocked(loadDefaultSessionView).mockReset().mockResolvedValue('terminal')
vi.mocked(saveDefaultSessionView).mockReset().mockResolvedValue(undefined)
})

View File

@ -63,7 +63,6 @@ describe('useMobileDiffReviewController', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
loadSnapshot.mockReset()
})

View File

@ -57,7 +57,6 @@ describe('useMobileDiffReviewDiffLoading', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
loadDiff.mockReset()
})

View File

@ -52,7 +52,6 @@ describe('useMobileDiffReviewSendActions', () => {
let saveCommentsAndReviewState: ReturnType<typeof vi.fn>
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
resetMobileNativeChatStaleInputForTests()
setActionError = vi.fn()
setSendSheet = vi.fn()
@ -81,20 +80,9 @@ describe('useMobileDiffReviewSendActions', () => {
async function mount(client: RpcClient): Promise<void> {
mountedClient = client
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness))
})
try {
await act(async () => {
renderer = create(createElement(Harness))
})
} finally {
consoleSpy.mockRestore()
}
}
it('heals a marked terminal BEFORE submitting the notes', async () => {

View File

@ -19,7 +19,6 @@ describe('useMobileFileTapHandlers', () => {
let handlers: Handlers | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
push.mockClear()
})

View File

@ -54,7 +54,6 @@ describe('useMobileNativeChatAnswerSend', () => {
beforeEach(() => {
onAccepted = vi.fn()
vi.useFakeTimers()
globalThis.IS_REACT_ACT_ENVIRONMENT = true
resetMobileNativeChatStaleInputForTests()
resetMobileNativeChatTerminalWritesForTests()
})
@ -93,20 +92,9 @@ describe('useMobileNativeChatAnswerSend', () => {
mountedClient = client
mountedOnSendError = onSendError
mountedAgent = agent
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { enabled: true }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { enabled: true }))
})
} finally {
consoleSpy.mockRestore()
}
}
async function setEnabled(enabled: boolean): Promise<void> {

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import type { AskPrompt } from './mobile-native-chat-ask'
import { useMobileNativeChatAskDismiss } from './use-mobile-native-chat-ask-dismiss'
@ -10,7 +10,6 @@ describe('useMobileNativeChatAskDismiss', () => {
let renders = 0
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
renders = 0
})
@ -45,20 +44,9 @@ describe('useMobileNativeChatAskDismiss', () => {
}
async function mount(props: Parameters<typeof Harness>[0]): Promise<void> {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, props))
})
try {
await act(async () => {
renderer = create(createElement(Harness, props))
})
} finally {
consoleSpy.mockRestore()
}
}
async function update(props: Parameters<typeof Harness>[0]): Promise<void> {

View File

@ -127,24 +127,12 @@ describe('useMobileNativeChatController handleNativeChatSend', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
vi.clearAllMocks()
resetMobileNativeChatStaleInputForTests()
captureSendOrigin.mockReturnValue(ORIGIN)
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(Harness))
})
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
spy.mockRestore()
}
})
afterEach(() => {
act(() => renderer?.unmount())
@ -364,24 +352,12 @@ describe('useMobileNativeChatController launch-draft wiring', () => {
}
function render(tab: unknown): void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(Harness, { tab }))
})
try {
act(() => {
renderer = create(createElement(Harness, { tab }))
})
} finally {
spy.mockRestore()
}
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
draftsArgs.length = 0
viewMode.isTabChatView = () => true
sessionState.messages = []
@ -487,25 +463,13 @@ describe('useMobileNativeChatController ask dismissal across a transcript reload
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
viewMode.isTabChatView = () => true
setTranscript('ready')
promptsState.ask = PROMPT
promptsState.detectedAsk = PROMPT
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(Harness))
})
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
spy.mockRestore()
}
})
afterEach(() => {
@ -757,22 +721,10 @@ describe('useMobileNativeChatController streaming scope', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
viewMode.isTabChatView = () => true
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(Harness))
})
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
spy.mockRestore()
}
})
afterEach(() => {

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { useMobileNativeChatDrafts } from './use-mobile-native-chat-drafts'
@ -23,10 +23,6 @@ describe('useMobileNativeChatDrafts launch draft', () => {
let renderer: ReactTestRenderer | null = null
let state: DraftState | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -65,20 +61,9 @@ describe('useMobileNativeChatDrafts launch draft', () => {
}
async function mount(tabId: string): Promise<void> {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { tabId }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { tabId }))
})
} finally {
consoleSpy.mockRestore()
}
}
it('prefills the composer from a host launch draft exactly once', async () => {

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { useMobileNativeChatDrafts } from './use-mobile-native-chat-drafts'
@ -30,10 +30,6 @@ describe('useMobileNativeChatDrafts', () => {
let renderer: ReactTestRenderer | null = null
let state: DraftState | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -69,20 +65,9 @@ describe('useMobileNativeChatDrafts', () => {
}
async function mount(tabId: string): Promise<void> {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { tabId }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { tabId }))
})
} finally {
consoleSpy.mockRestore()
}
}
async function switchTo(tabId: string): Promise<void> {

View File

@ -15,17 +15,6 @@ function rpcSuccess(files: string[]): Awaited<ReturnType<RpcClient['sendRequest'
}
}
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
describe('useMobileNativeChatFileSearch', () => {
let renderer: ReactTestRenderer | null = null
let state: SearchState | null = null
@ -35,19 +24,13 @@ describe('useMobileNativeChatFileSearch', () => {
state = useMobileNativeChatFileSearch({ client, worktreeId: 'wt-1' })
return null
}
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(Harness))
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(Harness))
})
}
beforeEach(() => {
vi.useFakeTimers()
globalThis.IS_REACT_ACT_ENVIRONMENT = true
state = null
})

View File

@ -87,7 +87,6 @@ describe('useMobileNativeChatImageAttachments', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
pick.mockReset()
// Stale markers and write locks live at module scope (they outlive the
// screen), so they also outlive a test.
@ -101,20 +100,9 @@ describe('useMobileNativeChatImageAttachments', () => {
})
function mount(args: HookArgs): void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...a) => {
if (typeof a[0] === 'string' && a[0].includes('react-test-renderer is deprecated')) {
return
}
original(...a)
act(() => {
renderer = create(createElement(Harness, { args }))
})
try {
act(() => {
renderer = create(createElement(Harness, { args }))
})
} finally {
spy.mockRestore()
}
}
function update(args: HookArgs): void {

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it } from 'vitest'
import { useMobileNativeChatInputLease } from './use-mobile-native-chat-input-lease'
type Lease = ReturnType<typeof useMobileNativeChatInputLease>
@ -9,10 +9,6 @@ describe('useMobileNativeChatInputLease', () => {
let renderer: ReactTestRenderer | null = null
let lease: Lease | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
@ -25,65 +21,43 @@ describe('useMobileNativeChatInputLease', () => {
}
it('unlocks only after acknowledgement and clears on disconnect', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { connected: true }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { connected: true }))
})
expect(lease?.ready).toBe(false)
expect(lease?.lockReason).toBe('waiting')
act(() => lease?.markReady('terminal'))
expect(lease?.ready).toBe(true)
expect(lease?.lockReason).toBeNull()
expect(lease?.ready).toBe(false)
expect(lease?.lockReason).toBe('waiting')
act(() => lease?.markReady('terminal'))
expect(lease?.ready).toBe(true)
expect(lease?.lockReason).toBeNull()
act(() => lease?.clear())
expect(lease?.ready).toBe(false)
act(() => lease?.markReady('terminal'))
expect(lease?.ready).toBe(true)
act(() => lease?.clear())
expect(lease?.ready).toBe(false)
act(() => lease?.markReady('terminal'))
expect(lease?.ready).toBe(true)
await act(async () => renderer?.update(createElement(Harness, { connected: false })))
expect(lease?.ready).toBe(false)
expect(lease?.lockReason).toBe('disconnected')
} finally {
consoleSpy.mockRestore()
}
await act(async () => renderer?.update(createElement(Harness, { connected: false })))
expect(lease?.ready).toBe(false)
expect(lease?.lockReason).toBe('disconnected')
})
it('reports whether a clear actually dropped a lease', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { connected: true }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { connected: true }))
})
// The route reads this to tell a real teardown from one React never sees.
expect(lease?.clear('terminal')).toBe(false)
expect(lease?.clear()).toBe(false)
// The route reads this to tell a real teardown from one React never sees.
expect(lease?.clear('terminal')).toBe(false)
expect(lease?.clear()).toBe(false)
act(() => lease?.markReady('terminal'))
let dropped: boolean | undefined
act(() => {
dropped = lease?.clear('terminal')
})
expect(dropped).toBe(true)
expect(lease?.ready).toBe(false)
expect(lease?.clear('terminal')).toBe(false)
act(() => lease?.markReady('terminal'))
let dropped: boolean | undefined
act(() => {
dropped = lease?.clear('terminal')
})
expect(dropped).toBe(true)
expect(lease?.ready).toBe(false)
expect(lease?.clear('terminal')).toBe(false)
act(() => lease?.markReady('other'))
expect(lease?.clear()).toBe(true)
} finally {
consoleSpy.mockRestore()
}
act(() => lease?.markReady('other'))
expect(lease?.clear()).toBe(true)
})
})

View File

@ -80,7 +80,6 @@ describe('useMobileNativeChatMessageSend', () => {
(clearInputWrite.mock.calls[0]?.[0] ?? {}) as { clearInput?: string }
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
sendWithOutcome.mockReset()
sendWithOutcome.mockResolvedValue('accepted')
clearInputWrite.mockReset()

View File

@ -10,7 +10,6 @@ describe('useMobileNativeChatReadability', () => {
let readable = false
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
readable = false
})
@ -34,21 +33,10 @@ describe('useMobileNativeChatReadability', () => {
readable = useMobileNativeChatReadability(client, worktreeId)
return null
}
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness))
await Promise.resolve()
})
try {
await act(async () => {
renderer = create(createElement(Harness))
await Promise.resolve()
})
} finally {
consoleSpy.mockRestore()
}
return sendRequest
}
@ -89,32 +77,21 @@ describe('useMobileNativeChatReadability', () => {
readable = useMobileNativeChatReadability(client, worktreeId)
return null
}
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { worktreeId: 'local-repo::/one' }))
await Promise.resolve()
})
try {
await act(async () => {
renderer = create(createElement(Harness, { worktreeId: 'local-repo::/one' }))
await Promise.resolve()
})
expect(readable).toBe(true)
expect(readable).toBe(true)
act(() => renderer?.update(createElement(Harness, { worktreeId: 'ssh-repo::/two' })))
expect(readable).toBe(false)
await act(async () => {
resolveNext({
ok: true,
result: { repos: [{ id: 'ssh-repo', connectionId: 'model-a-ssh' }] }
})
await Promise.resolve()
act(() => renderer?.update(createElement(Harness, { worktreeId: 'ssh-repo::/two' })))
expect(readable).toBe(false)
await act(async () => {
resolveNext({
ok: true,
result: { repos: [{ id: 'ssh-repo', connectionId: 'model-a-ssh' }] }
})
expect(readable).toBe(false)
} finally {
consoleSpy.mockRestore()
}
await Promise.resolve()
})
expect(readable).toBe(false)
})
})

View File

@ -31,31 +31,13 @@ describe('useMobileNativeChatSendError', () => {
return apiRef.current
}
/** react-test-renderer logs a deprecation notice on every render; keep real errors. */
function suppressRendererWarning(): () => void {
const original = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
return () => spy.mockRestore()
}
async function render(scopeKey: string | null = 'terminal-1'): Promise<void> {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(Harness, { scopeKey }))
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(Harness, { scopeKey }))
})
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
apiRef.current = null
showToast.mockClear()
vi.useFakeTimers()
@ -117,26 +99,16 @@ describe('useMobileNativeChatSendError', () => {
await act(async () => api().show('a'))
expect(api().message).toBe('a')
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
} finally {
restore()
}
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
expect(api().message).toBeNull()
})
it('falls back to the toast when the banner is not mounted', async () => {
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(Harness, { scopeKey: 'terminal-1', bannerMounted: false }))
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(Harness, { scopeKey: 'terminal-1', bannerMounted: false }))
})
// A deferred failure landing after the user left chat must still be seen.
await act(async () => api().show('Delivery unconfirmed'))
@ -149,14 +121,9 @@ describe('useMobileNativeChatSendError', () => {
// Captured while tab A was live; a 20s unconfirmed send resolves much later.
const showFromTabA = api().show
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
} finally {
restore()
}
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
await act(async () => showFromTabA('Message not sent'))
// The banner belongs to terminal-2 now, so A's failure must not paint there.
@ -168,14 +135,9 @@ describe('useMobileNativeChatSendError', () => {
await render('terminal-1')
const clearFromTabA = api().clear
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
} finally {
restore()
}
await act(async () => {
renderer?.update(createElement(Harness, { scopeKey: 'terminal-2' }))
})
await act(async () => api().show('b'))
// An accepted card action from tab A resolving late must not retire B's warning.
await act(async () => clearFromTabA())

View File

@ -44,7 +44,6 @@ describe('useMobileNativeChatSessionOptions', () => {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
clearMobileSessionOptionRecordsForTests()
dispatchCommand.mockReset()
dispatchCommand.mockResolvedValue('accepted')

View File

@ -24,7 +24,6 @@ describe('useMobileNativeChatSession', () => {
let emit: (frame: unknown) => void = () => {}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
state = null
})
@ -45,20 +44,9 @@ describe('useMobileNativeChatSession', () => {
}
async function mount(client: RpcClient): Promise<void> {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { client }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { client }))
})
} finally {
consoleSpy.mockRestore()
}
}
it('drops an older-page response captured before transcript replacement', async () => {
@ -445,7 +433,6 @@ describe('useMobileNativeChatSession transcriptLoading', () => {
}[] = []
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
renders.length = 0
})
@ -482,20 +469,9 @@ describe('useMobileNativeChatSession transcriptLoading', () => {
}
async function mountAt(client: RpcClient | null, sessionId: string | null): Promise<void> {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { client, sessionId }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { client, sessionId }))
})
} finally {
consoleSpy.mockRestore()
}
}
it('reports loading on the very first render, before the subscription effect runs', async () => {

View File

@ -14,7 +14,6 @@ describe('useMobileNativeChatStop', () => {
beforeEach(() => {
vi.useFakeTimers()
globalThis.IS_REACT_ACT_ENVIRONMENT = true
sendRequest.mockReset().mockResolvedValue({
ok: true,
result: { send: { accepted: true } }

View File

@ -17,7 +17,6 @@ describe('useMobileNativeChatTerminalStream', () => {
const hasTabsRecoveryNeedRef = { current: (): boolean => false }
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
subscriptionsRef.current = new Map([['terminal-1', () => {}]])
subscribingRef.current = new Set()
webReadyRef.current = new Set(['terminal-1'])
@ -85,113 +84,73 @@ describe('useMobileNativeChatTerminalStream', () => {
}
it('replaces output with a lease-only stream while covered, then restores output', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: false }))
})
await act(async () => {
notifyWebReadyRef.current('terminal-1', false)
})
expect(harnessRenderCount).toBe(1)
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: true }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: false }))
})
await act(async () => {
notifyWebReadyRef.current('terminal-1', false)
})
expect(harnessRenderCount).toBe(1)
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: true }))
})
expect(unsubscribe).toHaveBeenNthCalledWith(1, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(1, 'terminal-1')
expect(initializedRef.current.has('terminal-1')).toBe(false)
expect(unsubscribe).toHaveBeenNthCalledWith(1, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(1, 'terminal-1')
expect(initializedRef.current.has('terminal-1')).toBe(false)
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: false }))
})
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: false }))
})
expect(unsubscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
} finally {
consoleSpy.mockRestore()
}
expect(unsubscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
})
it('re-subscribes a covered stream torn down under chat (#10681)', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
subscribe.mockClear()
unsubscribe.mockClear()
// What a terminal.list prune / `end` frame does to the lease-only stream:
// the subscription is gone and the lease drops with it.
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: true, leaseReady: false }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
subscribe.mockClear()
unsubscribe.mockClear()
// What a terminal.list prune / `end` frame does to the lease-only stream:
// the subscription is gone and the lease drops with it.
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: true, leaseReady: false }))
})
expect(subscribe).toHaveBeenCalledOnce()
expect(subscribe).toHaveBeenCalledWith('terminal-1')
// Pins the rearm branch specifically: without it the action falls through to
// the resume tail, which also subscribes — but drops coverage on the way.
expect(unsubscribe).not.toHaveBeenCalled()
} finally {
consoleSpy.mockRestore()
}
expect(subscribe).toHaveBeenCalledOnce()
expect(subscribe).toHaveBeenCalledWith('terminal-1')
// Pins the rearm branch specifically: without it the action falls through to
// the resume tail, which also subscribes — but drops coverage on the way.
expect(unsubscribe).not.toHaveBeenCalled()
})
it('stops rearming a handle whose stream never comes back (#10681)', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
try {
subscribe.mockClear()
// A dead PTY answers every subscribe with `subscribed`+`end`, so the stream is
// gone again on each pass. Unbounded, that is a ~10s resubscribe loop.
for (let revision = 1; revision <= 6; revision += 1) {
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
renderer?.update(
createElement(Harness, {
showNativeChat: true,
leaseReady: false,
streamRevision: revision
})
)
})
subscribe.mockClear()
// A dead PTY answers every subscribe with `subscribed`+`end`, so the stream is
// gone again on each pass. Unbounded, that is a ~10s resubscribe loop.
for (let revision = 1; revision <= 6; revision += 1) {
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(
createElement(Harness, {
showNativeChat: true,
leaseReady: false,
streamRevision: revision
})
)
})
}
expect(subscribe.mock.calls.length).toBeLessThanOrEqual(3)
} finally {
consoleSpy.mockRestore()
}
expect(subscribe.mock.calls.length).toBeLessThanOrEqual(3)
})
it('does not charge the rearm budget for a subscribe its own gates turned away', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
try {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
@ -218,89 +177,66 @@ describe('useMobileNativeChatTerminalStream', () => {
subscribe.mockImplementation((handle: string) =>
subscriptionsRef.current.set(handle, () => {})
)
consoleSpy.mockRestore()
}
})
it('refills the rearm budget when terminal.list reports the handle again (#10681)', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
try {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
for (let revision = 1; revision <= 5; revision += 1) {
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(
createElement(Harness, {
showNativeChat: true,
leaseReady: false,
streamRevision: revision
})
)
})
}
subscribe.mockClear()
// Budget is spent, so a further teardown signal alone changes nothing.
await act(async () => {
renderer?.update(
createElement(Harness, { showNativeChat: true, leaseReady: false, streamRevision: 6 })
)
})
expect(subscribe).not.toHaveBeenCalled()
// A dead-but-listed handle must not buy a new budget on every list refresh,
// or the resubscribe loop this bound exists to stop comes straight back.
await act(async () => {
notifyListedHandlesRef.current(new Set(['terminal-1']))
notifyListedHandlesRef.current(new Set(['terminal-1']))
})
expect(subscribe).not.toHaveBeenCalled()
// The handle actually went away and came back: it may have a live PTY once more.
await act(async () => {
notifyListedHandlesRef.current(new Set())
notifyListedHandlesRef.current(new Set(['terminal-1']))
})
expect(subscribe).toHaveBeenCalledWith('terminal-1')
} finally {
consoleSpy.mockRestore()
}
})
it('rearms on a teardown the lease cannot report (#10681)', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
try {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
subscribe.mockClear()
// `end` with no preceding `subscribed` leaves the lease untouched, so
// `leaseReady` holds its value and only the revision bump can re-run us.
for (let revision = 1; revision <= 5; revision += 1) {
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(
createElement(Harness, { showNativeChat: true, leaseReady: true, streamRevision: 1 })
createElement(Harness, {
showNativeChat: true,
leaseReady: false,
streamRevision: revision
})
)
})
expect(subscribe).toHaveBeenCalledWith('terminal-1')
} finally {
consoleSpy.mockRestore()
}
subscribe.mockClear()
// Budget is spent, so a further teardown signal alone changes nothing.
await act(async () => {
renderer?.update(
createElement(Harness, { showNativeChat: true, leaseReady: false, streamRevision: 6 })
)
})
expect(subscribe).not.toHaveBeenCalled()
// A dead-but-listed handle must not buy a new budget on every list refresh,
// or the resubscribe loop this bound exists to stop comes straight back.
await act(async () => {
notifyListedHandlesRef.current(new Set(['terminal-1']))
notifyListedHandlesRef.current(new Set(['terminal-1']))
})
expect(subscribe).not.toHaveBeenCalled()
// The handle actually went away and came back: it may have a live PTY once more.
await act(async () => {
notifyListedHandlesRef.current(new Set())
notifyListedHandlesRef.current(new Set(['terminal-1']))
})
expect(subscribe).toHaveBeenCalledWith('terminal-1')
})
it('rearms on a teardown the lease cannot report (#10681)', async () => {
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
subscribe.mockClear()
// `end` with no preceding `subscribed` leaves the lease untouched, so
// `leaseReady` holds its value and only the revision bump can re-run us.
subscriptionsRef.current.delete('terminal-1')
await act(async () => {
renderer?.update(
createElement(Harness, { showNativeChat: true, leaseReady: true, streamRevision: 1 })
)
})
expect(subscribe).toHaveBeenCalledWith('terminal-1')
})
it('does not let a dead PTY buy a new budget with its own `subscribed` ack', async () => {
@ -411,34 +347,23 @@ describe('useMobileNativeChatTerminalStream', () => {
})
it('resumes a cold-start lease-only stream when WebView readiness arrives late', async () => {
const original = console.error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
webReadyRef.current.clear()
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: false }))
})
try {
webReadyRef.current.clear()
await act(async () => {
renderer = create(createElement(Harness, { showNativeChat: true }))
})
await act(async () => {
renderer?.update(createElement(Harness, { showNativeChat: false }))
})
expect(unsubscribe).toHaveBeenCalledOnce()
expect(subscribe).toHaveBeenCalledOnce()
expect(unsubscribe).toHaveBeenCalledOnce()
expect(subscribe).toHaveBeenCalledOnce()
webReadyRef.current.add('terminal-1')
await act(async () => {
notifyWebReadyRef.current('terminal-1', false)
})
webReadyRef.current.add('terminal-1')
await act(async () => {
notifyWebReadyRef.current('terminal-1', false)
})
expect(unsubscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
} finally {
consoleSpy.mockRestore()
}
expect(unsubscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
expect(subscribe).toHaveBeenNthCalledWith(2, 'terminal-1')
})
})

View File

@ -112,8 +112,6 @@ async function setAppState(state: string): Promise<void> {
describe('useMobileSessionTabsReconciliation', () => {
let renderer: ReactTestRenderer | null = null
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
async function mount(): Promise<void> {
await act(async () => {
renderer = create(createElement(Harness))
@ -124,14 +122,6 @@ describe('useMobileSessionTabsReconciliation', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(0)
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const originalConsoleError = console.error
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
lifecycle.appState = 'active'
lifecycle.focused = true
lifecycle.listeners.clear()
@ -157,7 +147,6 @@ describe('useMobileSessionTabsReconciliation', () => {
act(() => renderer?.unmount())
renderer = null
streamListener = null
consoleErrorSpy.mockRestore()
vi.useRealTimers()
})

View File

@ -52,7 +52,6 @@ describe('useMobileSessionViewMode', () => {
let controller: MobileSessionViewModeController | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
vi.mocked(loadDefaultSessionView).mockReset().mockResolvedValue('terminal')
vi.mocked(readSessionViewOverridesPreference)
.mockReset()

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { TerminalQuickCommand } from '../../../src/shared/types'
import type { RpcClient } from '../transport/rpc-client'
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
@ -49,24 +49,10 @@ function deferred<T>() {
describe('useQuickCommands', () => {
let renderer: ReactTestRenderer | null = null
let state: ReturnType<typeof useQuickCommands> | null = null
let consoleSpy: MockInstance
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const original = console.error
consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
state = null
consoleSpy.mockRestore()
})
async function mount(client: RpcClient, enabled = true): Promise<void> {

View File

@ -34,18 +34,6 @@ function createDeferred<T>(): Deferred<T> {
return { promise, resolve }
}
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
function createTerminalLiveInputModePreferenceHarness(): TerminalLiveInputModePreferenceHarness {
let current: ReturnType<typeof useTerminalLiveInputModePreference> | null = null
let renderer: ReactTestRenderer | null = null
@ -58,14 +46,9 @@ function createTerminalLiveInputModePreferenceHarness(): TerminalLiveInputModePr
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
restoreConsoleError()
}
act(() => {
renderer = create(createElement(Harness))
})
if (!current || !renderer) {
throw new Error('terminal live input mode preference hook did not render')
}

View File

@ -1,12 +1,11 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useThrottledLatestValue } from './use-throttled-latest-value'
describe('useThrottledLatestValue', () => {
let renderer: ReactTestRenderer | null = null
let latest: string | undefined
let consoleSpy: MockInstance
function Harness({ value }: { value: string | undefined }): null {
latest = useThrottledLatestValue(value, 50)
@ -25,22 +24,13 @@ describe('useThrottledLatestValue', () => {
beforeEach(() => {
vi.useFakeTimers()
globalThis.IS_REACT_ACT_ENVIRONMENT = true
latest = undefined
const original = console.error
consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
original(...args)
})
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
vi.useRealTimers()
consoleSpy.mockRestore()
})
it('emits the first frame immediately', () => {

View File

@ -1,6 +1,6 @@
import { createElement, type ReactElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState } from '../transport/types'
import { MobileGitHistoryList } from './MobileGitHistoryList'
@ -44,10 +44,6 @@ const compareResponse = {
describe('MobileGitHistoryList', () => {
let renderer: ReactTestRenderer | null = null
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -1,6 +1,6 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HostedReviewCreationEligibility } from '../../../src/shared/hosted-review'
import {
buildMobileHostedReviewEligibilityLoadKey,
@ -206,10 +206,6 @@ describe('eligibility request ordering', () => {
let renderer: ReactTestRenderer | null = null
let renderedState: MobileCreatePrEligibilityState = { kind: 'idle' }
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
act(() => renderer?.unmount())
renderer = null

View File

@ -41,18 +41,6 @@ vi.mock('lucide-react-native', () => ({
RefreshCw: 'RefreshCw'
}))
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
// Why: mounting TerminalWebView arms the web-ready watchdog; tests must
// unmount so the timer can't survive into later tests and fire in teardown.
let activeRenderer: ReactTestRenderer | null = null
@ -62,14 +50,9 @@ function createTerminalWebViewRenderer(
props: Record<string, unknown> = {}
) {
let renderer: ReactTestRenderer | null = null
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(TerminalWebView, { onEngineError, ...props }))
})
} finally {
restoreConsoleError()
}
act(() => {
renderer = create(createElement(TerminalWebView, { onEngineError, ...props }))
})
if (!renderer) {
throw new Error('TerminalWebView did not render')
}

View File

@ -25,18 +25,6 @@ function createDeferredBoolean(): DeferredBoolean {
return { promise, resolve: resolvePromise }
}
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
type AccessoryInputCommitHarnessOptions = {
readonly heldText?: string
readonly sentText?: string
@ -105,14 +93,9 @@ function createAccessoryInputCommitHarness({
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
restoreConsoleError()
}
act(() => {
renderer = create(createElement(Harness))
})
if (!commit || !renderer) {
throw new Error('terminal live accessory input hook did not render')
}

View File

@ -20,18 +20,6 @@ type TerminalLiveInputCommitHarnessOptions = {
readonly sendResult?: boolean
}
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => consoleErrorSpy.mockRestore()
}
function createTerminalLiveInputCommitHarness({
sendResult = true
}: TerminalLiveInputCommitHarnessOptions = {}): TerminalLiveInputCommitHarness {
@ -77,14 +65,9 @@ function createTerminalLiveInputCommitHarness({
return null
}
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(Harness))
})
} finally {
restoreConsoleError()
}
act(() => {
renderer = create(createElement(Harness))
})
if (!handlers || !renderer) {
throw new Error('terminal live input hook did not render')
}

View File

@ -1,6 +1,6 @@
import { createElement, type RefObject } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type {
TerminalLiveInputFocusTarget,
TerminalLiveInputFocusTimerRef
@ -20,17 +20,6 @@ type HarnessProps = {
type FocusHandlers = ReturnType<typeof useTerminalLiveInputFocus>
function suppressReactTestRendererWarning(): () => void {
const originalConsoleError = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => spy.mockRestore()
}
function createFocusTarget(initiallyFocused = false): TerminalLiveInputFocusTarget & {
readonly blur: ReturnType<typeof vi.fn>
readonly focus: ReturnType<typeof vi.fn>
@ -67,14 +56,9 @@ function createHarness(initialProps: HarnessProps): {
return null
}
const restoreWarning = suppressReactTestRendererWarning()
try {
act(() => {
renderer = create(createElement(Harness, initialProps))
})
} finally {
restoreWarning()
}
act(() => {
renderer = create(createElement(Harness, initialProps))
})
if (!handlers || !renderer) {
throw new Error('terminal live input focus harness did not render')
}
@ -113,10 +97,6 @@ function connectedProps(
}
describe('terminal live input focus hook', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
})
afterEach(() => {
vi.useRealTimers()
})

View File

@ -71,18 +71,6 @@ type Harness = {
readonly unmount: () => void
}
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
const firstArg = args[0]
if (typeof firstArg === 'string' && firstArg.includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => spy.mockRestore()
}
async function renderHarness(hostId: string): Promise<Harness> {
let hook: ReturnType<typeof useHostClient> | null = null
let closeHost: ((hostId: string) => void) | null = null
@ -94,14 +82,9 @@ async function renderHarness(hostId: string): Promise<Harness> {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
if (!hook || !closeHost || !renderer) {
throw new Error('harness did not render')
}
@ -124,7 +107,6 @@ async function renderHarness(hostId: string): Promise<Harness> {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
connectMock.mockReset()
loadHostsMock.mockReset()
})
@ -149,7 +131,6 @@ describe('useHostClient', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -169,7 +150,6 @@ describe('useHostClient', () => {
expect(selectedState).toBe('disconnected')
expect(connectMock).toHaveBeenCalledTimes(2)
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -188,7 +168,6 @@ describe('useHostClient', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -211,7 +190,6 @@ describe('useHostClient', () => {
})
expect(stateByRenderTick.get(2)).toBe('connecting')
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -263,7 +241,6 @@ describe('useHostClient', () => {
states.push(useHostClient(HOST.id).state)
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -277,7 +254,6 @@ describe('useHostClient', () => {
expect(states.at(-1)).toBe('connecting')
expect(states).not.toContain('disconnected')
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -296,7 +272,6 @@ describe('useHostClient', () => {
states.push(useHostClient(HOST.id).state)
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -311,7 +286,6 @@ describe('useHostClient', () => {
expect(states.at(-1)).toBe('connecting')
expect(states).not.toContain('disconnected')
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -333,14 +307,9 @@ describe('useHostClient', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
} finally {
restore()
}
act(() => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
expect(loadHostsMock).toHaveBeenCalledOnce()
if (!closeHost || !resolveHosts || !renderer) {
throw new Error('pending-open harness did not initialize')
@ -370,14 +339,9 @@ describe('useHostClient', () => {
useHostClient(HOST.id)
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
act(() => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
} finally {
restore()
}
act(() => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
expect(loadHostsMock).toHaveBeenCalledOnce()
act(() => renderer?.unmount())
await act(async () => {
@ -401,7 +365,6 @@ describe('useAllHostClients', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -410,7 +373,6 @@ describe('useAllHostClients', () => {
expect(connectMock).toHaveBeenCalledOnce()
expect(connectMock).toHaveBeenCalledWith(host2, expect.any(Function))
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -433,7 +395,6 @@ describe('useAllHostClients', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -446,7 +407,6 @@ describe('useAllHostClients', () => {
'host-997'
])
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -477,7 +437,6 @@ describe('useAllHostClients', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(
@ -504,7 +463,6 @@ describe('useAllHostClients', () => {
expect(clients.get('host-c')?.closeMock).toHaveBeenCalledOnce()
expect(clients.get('host-d')?.closeMock).not.toHaveBeenCalled()
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -520,7 +478,6 @@ describe('useAllHostClients', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -528,7 +485,6 @@ describe('useAllHostClients', () => {
})
expect(connectMock).toHaveBeenCalledTimes(2)
} finally {
restore()
act(() => renderer?.unmount())
}
})
@ -545,7 +501,6 @@ describe('useAllHostClients', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
@ -559,7 +514,6 @@ describe('useAllHostClients', () => {
})
expect(connectMock).toHaveBeenCalledOnce()
} finally {
restore()
act(() => renderer?.unmount())
}
})

View File

@ -4,17 +4,6 @@ import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from './rpc-client'
import { useHostStatusGates, type HostStatusGates } from './host-status-gates'
function suppressReactTestRendererDeprecationWarning(): () => void {
const originalConsoleError = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => spy.mockRestore()
}
describe('useHostStatusGates', () => {
it('clears every prior-host gate and ignores its late response while the client is replaced', async () => {
let resolveOldStatus: ((response: unknown) => void) | null = null
@ -43,7 +32,6 @@ describe('useHostStatusGates', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(Probe, { hostId: 'host-1', client: oldClient }))
@ -80,7 +68,6 @@ describe('useHostStatusGates', () => {
expect(oldSendRequest).toHaveBeenCalledOnce()
expect(newSendRequest).toHaveBeenCalledOnce()
} finally {
restore()
renderer?.unmount()
}
})
@ -102,7 +89,6 @@ describe('useHostStatusGates', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(Probe, { hostId: 'host-1' }))
@ -115,7 +101,6 @@ describe('useHostStatusGates', () => {
expect(sendRequest).toHaveBeenCalledOnce()
} finally {
restore()
renderer?.unmount()
}
})
@ -141,7 +126,6 @@ describe('useHostStatusGates', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(Probe, { connState: 'connected' }))
@ -181,7 +165,6 @@ describe('useHostStatusGates', () => {
statusPending: false
})
} finally {
restore()
renderer?.unmount()
}
})
@ -204,7 +187,6 @@ describe('useHostStatusGates', () => {
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(Probe, { client: firstClient }))
@ -217,7 +199,6 @@ describe('useHostStatusGates', () => {
})
expect(gates).toMatchObject({ hostCapabilities: [], statusPending: true })
} finally {
restore()
renderer?.unmount()
}
})

View File

@ -151,29 +151,13 @@ function TestApp({
)
}
function suppressRendererWarning(): () => void {
const originalConsoleError = console.error
const spy = vi.spyOn(console, 'error').mockImplementation((...args) => {
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
return
}
originalConsoleError(...args)
})
return () => spy.mockRestore()
}
async function renderScreen(screen: Screen, detailHostId?: string): Promise<ReactTestRenderer> {
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(TestApp, { screen, detailHostId }))
await Promise.resolve()
await Promise.resolve()
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(TestApp, { screen, detailHostId }))
await Promise.resolve()
await Promise.resolve()
})
if (!renderer) {
throw new Error('settings lifecycle harness did not render')
}
@ -203,7 +187,6 @@ function activeHostIds(): string[] {
}
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
context = null
routeFocus.effect = null
connectMock.mockReset()
@ -235,16 +218,11 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(NavigationStack, { settingsVisible: false }))
await Promise.resolve()
await Promise.resolve()
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(NavigationStack, { settingsVisible: false }))
await Promise.resolve()
await Promise.resolve()
})
if (!renderer) {
throw new Error('navigation stack harness did not render')
}
@ -354,20 +332,15 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(HostListApp, {
settingsHostIds: [retainedHostId, sharedHostId, removedHostId]
})
)
await Promise.resolve()
await Promise.resolve()
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(HostListApp, {
settingsHostIds: [retainedHostId, sharedHostId, removedHostId]
})
)
await Promise.resolve()
await Promise.resolve()
})
if (!renderer) {
throw new Error('host-list lifecycle harness did not render')
}
@ -451,14 +424,9 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
act(() => {
renderer = create(createElement(RetryApp, { settingsVisible: true }))
})
} finally {
restore()
}
act(() => {
renderer = create(createElement(RetryApp, { settingsVisible: true }))
})
if (!renderer || !resolveInitial || !resolveRetry) {
throw new Error('retry lifecycle harness did not initialize')
}
@ -517,14 +485,9 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
act(() => {
renderer = create(createElement(PendingApp, { visible: true }))
})
} finally {
restore()
}
act(() => {
renderer = create(createElement(PendingApp, { visible: true }))
})
if (!renderer || !resolveFirst || !resolveSecond) {
throw new Error('pending settings harness did not initialize')
}
@ -580,17 +543,12 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(
createElement(ReplacementApp, { detailVisible: false, settingsVisible: false })
)
await Promise.resolve()
})
} finally {
restore()
}
await act(async () => {
renderer = create(
createElement(ReplacementApp, { detailVisible: false, settingsVisible: false })
)
await Promise.resolve()
})
if (!renderer || !context) {
throw new Error('replacement lifecycle harness did not initialize')
}
@ -665,14 +623,9 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
act(() => {
renderer = create(createElement(ManualApp, { selected: false }))
})
} finally {
restore()
}
act(() => {
renderer = create(createElement(ManualApp, { selected: false }))
})
if (!renderer || !context) {
throw new Error('manual connection harness did not initialize')
}
@ -735,16 +688,11 @@ describe('settings host client lifecycle', () => {
}
let renderer: ReactTestRenderer | null = null
const restore = suppressRendererWarning()
try {
await act(async () => {
renderer = create(createElement(FocusStack))
await Promise.resolve()
await Promise.resolve()
})
} finally {
restore()
}
await act(async () => {
renderer = create(createElement(FocusStack))
await Promise.resolve()
await Promise.resolve()
})
if (!renderer || !routeFocus.effect) {
throw new Error('settings focus harness did not initialize')
}

View File

@ -9,6 +9,8 @@ export default defineConfig({
oxc: vitestOxcConfig,
test: {
environment: 'node',
setupFiles: ['./vitest.setup.ts'],
onConsoleLog: (log) => !log.includes('react-test-renderer is deprecated'),
// .tsx too: component tests exist (react-test-renderer + mocked react-native) and were
// silently never collected, so render-level regressions shipped untested.
include: ['src/**/*.test.ts', 'src/**/*.test.tsx']

1
mobile/vitest.setup.ts Normal file
View File

@ -0,0 +1 @@
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })