Files
orca/mobile/src/source-control/mobile-commit-during-generation.test.tsx
T

124 lines
4.0 KiB
TypeScript

import { createElement, useRef, useState } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi, type Mock } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import type { MobileGenerateCommitMessageResult } from './mobile-commit-message-ai'
// Why: react-native is Flow source vitest will not parse; haptics reaches expo-haptics through it.
vi.mock('react-native', () => ({
Platform: { OS: 'ios', select: (options: Record<string, unknown>) => options.ios },
StyleSheet: { create: (value: unknown) => value, hairlineWidth: 1 }
}))
vi.mock('expo-router', () => ({
useRouter: () => ({
back: () => undefined,
push: () => undefined,
replace: () => undefined,
navigate: () => undefined,
dismissTo: () => undefined,
prefetch: () => undefined,
canGoBack: () => false,
setParams: () => undefined
})
}))
vi.mock('../platform/haptics', () => ({ triggerError: vi.fn(), triggerSuccess: vi.fn() }))
const ai = vi.hoisted(() => {
const held: {
resolve: ((result: MobileGenerateCommitMessageResult) => void) | null
cancel: Mock<() => void>
} = { resolve: null, cancel: vi.fn<() => void>() }
return held
})
vi.mock('./mobile-commit-message-ai', () => ({
requestMobileCommitMessage: () =>
new Promise<MobileGenerateCommitMessageResult>((resolve) => {
ai.resolve = resolve
}),
cancelMobileCommitMessage: async () => ai.cancel()
}))
import { useRouteHandoff } from '../navigation/route-handoff'
import { useMobileSourceControlRunners } from './use-mobile-source-control-runners'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mocked AI module and the injected senders are the only things that would reach the client.
const client = {} as RpcClient
type Runners = ReturnType<typeof useMobileSourceControlRunners>
const view: {
message: string
type: ((next: string) => void) | null
runners: Runners | null
} = { message: '', type: null, runners: null }
function Harness(): null {
const router = useRouteHandoff()
const [commitMessage, setCommitMessage] = useState('')
const [generatingMessage, setGeneratingMessage] = useState(false)
const mountedRef = useRef(true)
const busyActionRef = useRef<string | null>(null)
view.message = commitMessage
view.type = setCommitMessage
view.runners = useMobileSourceControlRunners({
client,
hostId: 'host-a',
worktreeId: 'wt-1',
status: null,
branchLabel: 'main',
commitMessage,
stagedEntries: [],
generatingMessage,
stageablePaths: [],
unstageablePaths: [],
router,
sendGitRequest: vi.fn(),
sendCommitRequest: vi.fn().mockResolvedValue({ success: true }),
runGitSyncSteps: vi.fn(),
loadStatus: vi.fn().mockResolvedValue(true),
mountedRef,
busyActionRef,
setBusyAction: vi.fn(),
setActionError: vi.fn(),
setCommitMessage,
setGeneratingMessage,
setShowActionSheet: vi.fn(),
setLocalBranches: vi.fn(),
setShowBranchPicker: vi.fn(),
setCreatedPrUrl: vi.fn(),
setCreatedPrWarning: vi.fn(),
recordCommitFailure: vi.fn()
})
return null
}
let renderer: ReactTestRenderer | null = null
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
ai.resolve = null
ai.cancel.mockClear()
})
describe('committing while the AI is still writing a message', () => {
it('cancels the generation so its late reply does not refill the emptied box', async () => {
act(() => {
renderer = create(createElement(Harness))
})
let generating: Promise<void> | undefined
act(() => {
generating = view.runners?.generateCommitMessage()
})
act(() => view.type?.('fix: my own words'))
await act(async () => {
await view.runners?.commit()
})
expect(ai.cancel).toHaveBeenCalledTimes(1)
expect(view.message).toBe('')
await act(async () => {
ai.resolve?.({ success: true, message: 'feat: generated for the old diff' })
await generating
})
expect(view.message).toBe('')
})
})