mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
test: improve type safety and mock patterns in source-control tests
- Add proper type definitions for toast options and test data instead of using `as never` - Replace `mock.calls.at(-1)` with safer `mock.lastCall` pattern - Create `entry()` helper to construct typed test entries - Add explicit type annotations to mocked functions for better IDE support
This commit is contained in:
+17
-9
@@ -2,10 +2,18 @@
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { DiscardAllDeps, DiscardAllResult, DiscardAllArea } from './discard-all-sequence'
|
||||
|
||||
type ToastOptions = { description?: string }
|
||||
type DiscardAllRunner = (
|
||||
area: DiscardAllArea,
|
||||
paths: readonly string[],
|
||||
deps: DiscardAllDeps
|
||||
) => Promise<DiscardAllResult>
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
runDiscardAllForArea: vi.fn()
|
||||
toastError: vi.fn<(title: string, options?: ToastOptions) => void>(),
|
||||
runDiscardAllForArea: vi.fn<DiscardAllRunner>()
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { error: mocks.toastError, dismiss: vi.fn() } }))
|
||||
@@ -13,7 +21,8 @@ vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => undefined })
|
||||
vi.mock('@/runtime/runtime-git-client', () => ({ bulkUnstageRuntimeGitPaths: vi.fn() }))
|
||||
vi.mock('./discard-all-sequence', () => ({
|
||||
getDiscardAllPaths: () => [],
|
||||
runDiscardAllForArea: (...args: unknown[]) => mocks.runDiscardAllForArea(...args)
|
||||
runDiscardAllForArea: (area: DiscardAllArea, paths: readonly string[], deps: DiscardAllDeps) =>
|
||||
mocks.runDiscardAllForArea(area, paths, deps)
|
||||
}))
|
||||
|
||||
import { useSourceControlDiscardConfirmation } from './use-discard-confirmation'
|
||||
@@ -22,8 +31,7 @@ import type { SourceControlEntryGroups } from '../listing/section-order'
|
||||
const EMPTY_GROUPS: SourceControlEntryGroups = { unstaged: [], staged: [], untracked: [] }
|
||||
|
||||
function lastDescription(): string | undefined {
|
||||
const call = mocks.toastError.mock.calls.at(-1)
|
||||
return (call?.[1] as { description?: string })?.description
|
||||
return mocks.toastError.mock.lastCall?.[1]?.description
|
||||
}
|
||||
|
||||
function renderDiscard() {
|
||||
@@ -65,8 +73,8 @@ describe('discard-all failure descriptions', () => {
|
||||
|
||||
it('unwraps the IPC transport noise on a partial failure, like the per-row toast does', async () => {
|
||||
mocks.runDiscardAllForArea.mockImplementation(async (_area, _paths, handlers) => {
|
||||
;(handlers as { onError: (e: unknown) => void }).onError(new Error(WRAPPED))
|
||||
return { aborted: false, failed: ['a.ts'] }
|
||||
handlers.onError?.(new Error(WRAPPED))
|
||||
return { aborted: false, discarded: [], failed: ['a.ts'] }
|
||||
})
|
||||
|
||||
await confirmDiscardOf(['a.ts'])
|
||||
@@ -78,8 +86,8 @@ describe('discard-all failure descriptions', () => {
|
||||
// Why 'staged': `aborted` is set only by the bulkUnstage pre-step, which runs for staged entries.
|
||||
it('unwraps it on the aborted-before-discard path too', async () => {
|
||||
mocks.runDiscardAllForArea.mockImplementation(async (_area, _paths, handlers) => {
|
||||
;(handlers as { onError: (e: unknown) => void }).onError(new Error(WRAPPED))
|
||||
return { aborted: true, failed: [] }
|
||||
handlers.onError?.(new Error(WRAPPED))
|
||||
return { aborted: true, discarded: [], failed: [] }
|
||||
})
|
||||
|
||||
await confirmDiscardOf(['a.ts'], 'staged')
|
||||
|
||||
+14
-12
@@ -2,9 +2,16 @@
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
type ToastOptions = {
|
||||
id?: string
|
||||
description?: string
|
||||
duration?: number
|
||||
action?: { label: string; onClick: () => void }
|
||||
}
|
||||
|
||||
const { toastError, toastDismiss } = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
toastDismiss: vi.fn()
|
||||
toastError: vi.fn<(title: string, options?: ToastOptions) => void>(),
|
||||
toastDismiss: vi.fn<(id: string) => void>()
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: { error: toastError, dismiss: toastDismiss } }))
|
||||
|
||||
@@ -15,19 +22,14 @@ vi.mock('@/store', () => ({
|
||||
|
||||
import { showSourceControlEntryFailureToast } from './source-control-entry-failure-toast'
|
||||
|
||||
type ToastOptions = {
|
||||
id?: string
|
||||
description?: string
|
||||
duration?: number
|
||||
action?: { label: string; onClick: () => void }
|
||||
}
|
||||
type FailureToastInput = Parameters<typeof showSourceControlEntryFailureToast>[0]
|
||||
|
||||
function lastToast(): { title: string; options: ToastOptions } {
|
||||
const call = toastError.mock.calls.at(-1)
|
||||
return { title: String(call?.[0]), options: (call?.[1] ?? {}) as ToastOptions }
|
||||
const [title = '', options = {}] = toastError.mock.lastCall ?? []
|
||||
return { title, options }
|
||||
}
|
||||
|
||||
function show(overrides: Record<string, unknown> = {}): void {
|
||||
function show(overrides: Partial<FailureToastInput> = {}): void {
|
||||
showSourceControlEntryFailureToast({
|
||||
operation: 'stage',
|
||||
filePath: 'src/app.ts',
|
||||
@@ -35,7 +37,7 @@ function show(overrides: Record<string, unknown> = {}): void {
|
||||
worktreeId: 'wt-1',
|
||||
worktreeName: 'feature-a',
|
||||
...overrides
|
||||
} as Parameters<typeof showSourceControlEntryFailureToast>[0])
|
||||
})
|
||||
}
|
||||
|
||||
describe('showSourceControlEntryFailureToast', () => {
|
||||
|
||||
+16
-7
@@ -2,9 +2,12 @@
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GitStatusEntry } from '../../../../../../shared/git-status-types'
|
||||
|
||||
type ToastOptions = { description?: string; action?: { label: string; onClick: () => void } }
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
toastError: vi.fn<(title: string, options?: ToastOptions) => void>(),
|
||||
stagePath: vi.fn(),
|
||||
unstagePath: vi.fn(),
|
||||
discardPath: vi.fn()
|
||||
@@ -37,11 +40,17 @@ import type { SourceControlEntryGroups } from '../listing/section-order'
|
||||
|
||||
const EMPTY_GROUPS: SourceControlEntryGroups = { unstaged: [], staged: [], untracked: [] }
|
||||
|
||||
type ToastOptions = { description?: string; action?: { label: string; onClick: () => void } }
|
||||
function entry(
|
||||
path: string,
|
||||
status: GitStatusEntry['status'] = 'modified',
|
||||
area: GitStatusEntry['area'] = 'unstaged'
|
||||
): GitStatusEntry {
|
||||
return { path, status, area }
|
||||
}
|
||||
|
||||
function lastToast(): { title: string; options: ToastOptions } {
|
||||
const call = mocks.toastError.mock.calls.at(-1)
|
||||
return { title: String(call?.[0]), options: (call?.[1] ?? {}) as ToastOptions }
|
||||
const [title = '', options = {}] = mocks.toastError.mock.lastCall ?? []
|
||||
return { title, options }
|
||||
}
|
||||
|
||||
function renderMutations() {
|
||||
@@ -138,7 +147,7 @@ describe('source-control entry mutation failures', () => {
|
||||
const { result } = renderDiscard(discardSingle)
|
||||
|
||||
await act(async () => {
|
||||
result.current.requestDiscardEntry({ path: 'src/app.ts' } as never)
|
||||
result.current.requestDiscardEntry(entry('src/app.ts'))
|
||||
})
|
||||
await act(async () => {
|
||||
result.current.confirmPendingDiscard()
|
||||
@@ -156,7 +165,7 @@ describe('source-control entry mutation failures', () => {
|
||||
const { result } = renderDiscard(discardSingle)
|
||||
|
||||
await act(async () => {
|
||||
result.current.requestDiscardEntry({ path: 'src/app.ts' } as never)
|
||||
result.current.requestDiscardEntry(entry('src/app.ts'))
|
||||
})
|
||||
await act(async () => {
|
||||
result.current.confirmPendingDiscard()
|
||||
@@ -172,7 +181,7 @@ describe('source-control entry mutation failures', () => {
|
||||
const { result } = renderDiscard(discardSingle)
|
||||
|
||||
await act(async () => {
|
||||
result.current.requestDiscardEntry({ path: 'new.ts', status: 'untracked' } as never)
|
||||
result.current.requestDiscardEntry(entry('new.ts', 'untracked', 'untracked'))
|
||||
})
|
||||
await act(async () => {
|
||||
result.current.confirmPendingDiscard()
|
||||
|
||||
Reference in New Issue
Block a user