From 7f069c8d4d6dbcb802d2c7fa64f0abb1316d2cc8 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:20:08 -0700 Subject: [PATCH] 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 --- .../discard-all-failure-description.test.tsx | 26 ++++++++++++------- ...source-control-entry-failure-toast.test.ts | 26 ++++++++++--------- ...e-control-entry-mutation-failures.test.tsx | 23 +++++++++++----- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx b/src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx index 5c6c31a8501..df488e8b40e 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx @@ -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 const mocks = vi.hoisted(() => ({ - toastError: vi.fn(), - runDiscardAllForArea: vi.fn() + toastError: vi.fn<(title: string, options?: ToastOptions) => void>(), + runDiscardAllForArea: vi.fn() })) 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') diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts index 3f3324cf2b5..64f80931ca3 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts @@ -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[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 = {}): void { +function show(overrides: Partial = {}): void { showSourceControlEntryFailureToast({ operation: 'stage', filePath: 'src/app.ts', @@ -35,7 +37,7 @@ function show(overrides: Record = {}): void { worktreeId: 'wt-1', worktreeName: 'feature-a', ...overrides - } as Parameters[0]) + }) } describe('showSourceControlEntryFailureToast', () => { diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx index 2e552426d46..f15e555d513 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx @@ -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()