/** * @license * Copyright 2025 AionUi (aionui.com) * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { FileChangeInfo, SnapshotInfo } from '@/common/types/platform/fileSnapshot'; import { ipcBridge } from '@/common'; import FileChangeList from '@/renderer/pages/conversation/Workspace/components/FileChangeList'; vi.mock('@/common', () => ({ ipcBridge: { fileSnapshot: { getBaselineContent: { invoke: vi.fn() }, }, fs: { readFile: { invoke: vi.fn() }, }, }, })); vi.mock('@/renderer/components/media/Diff2Html', () => ({ __esModule: true, default: ({ diff, title, file_path }: { diff: string; title: string; file_path: string }) => (
{diff}
), })); vi.mock('@arco-design/web-react', () => ({ Button: ({ icon, onClick, children, }: { icon?: React.ReactNode; onClick?: (event: Event) => void; children?: React.ReactNode; }) => ( ), Empty: ({ description }: { description?: React.ReactNode }) =>
{description}
, Spin: () => , Tooltip: ({ children }: { children?: React.ReactNode }) => <>{children}, })); vi.mock('@icon-park/react', () => ({ Down: () => , Minus: () => , Plus: () => , PreviewOpen: () => , Redo: () => , Refresh: () => , Right: () => , })); const t = (key: string, options?: { count?: number }) => { if (key === 'conversation.workspace.changes.summary') return `${options?.count ?? 0} changed`; return key; }; const baseProps = { t, workspace: 'C:\\Users\\demo\\repo', staged: [], loading: false, snapshotInfo: { mode: 'git-repo', branch: 'main' } satisfies SnapshotInfo, onRefresh: vi.fn(), onOpenDiff: vi.fn(), onStageFile: vi.fn(), onStageAll: vi.fn(), onUnstageFile: vi.fn(), onUnstageAll: vi.fn(), onDiscardFile: vi.fn(), onResetFile: vi.fn(), }; const change = (overrides?: Partial): FileChangeInfo => ({ file_path: 'C:\\Users\\demo\\repo\\src\\app.ts', relativePath: 'src/app.ts', operation: 'modify', ...overrides, }); describe('FileChangeList', () => { beforeEach(() => { vi.mocked(ipcBridge.fileSnapshot.getBaselineContent.invoke).mockReset(); vi.mocked(ipcBridge.fs.readFile.invoke).mockReset(); }); it('uses the normalized workspace path when expanding a modified file diff', async () => { vi.mocked(ipcBridge.fileSnapshot.getBaselineContent.invoke).mockResolvedValue('old line\n'); vi.mocked(ipcBridge.fs.readFile.invoke).mockResolvedValue('new line\n'); render(); fireEvent.click(screen.getByRole('button', { name: /src\/app\.ts/ })); await waitFor(() => { expect(screen.getByTestId('diff-viewer')).toHaveAttribute('data-file-path', 'C:\\Users\\demo\\repo\\src\\app.ts'); }); expect(ipcBridge.fs.readFile.invoke).toHaveBeenCalledWith({ path: 'C:\\Users\\demo\\repo\\src\\app.ts', workspace: 'C:\\Users\\demo\\repo', }); expect(screen.getByTestId('diff-viewer')).toHaveTextContent('-old line'); expect(screen.getByTestId('diff-viewer')).toHaveTextContent('+new line'); }); it('falls back to the backend file path when the normalized path cannot be read', async () => { vi.mocked(ipcBridge.fileSnapshot.getBaselineContent.invoke).mockResolvedValue('before\n'); vi.mocked(ipcBridge.fs.readFile.invoke) .mockRejectedValueOnce(new Error('missing normalized path')) .mockResolvedValueOnce('after\n'); render( ); fireEvent.click(screen.getByRole('button', { name: /src\/app\.ts/ })); await waitFor(() => { expect(screen.getByTestId('diff-viewer')).toHaveTextContent('+after'); }); expect(ipcBridge.fs.readFile.invoke).toHaveBeenNthCalledWith(1, { path: 'C:\\Users\\demo\\repo\\src\\app.ts', workspace: 'C:\\Users\\demo\\repo', }); expect(ipcBridge.fs.readFile.invoke).toHaveBeenNthCalledWith(2, { path: 'C:\\Temp\\aionui\\src\\app.ts', workspace: 'C:\\Users\\demo\\repo', }); }); it('does not render a misleading empty diff when file content cannot be read', async () => { vi.mocked(ipcBridge.fileSnapshot.getBaselineContent.invoke).mockResolvedValue('before\n'); vi.mocked(ipcBridge.fs.readFile.invoke).mockRejectedValue(new Error('file unavailable')); render(); fireEvent.click(screen.getByRole('button', { name: /src\/app\.ts/ })); await waitFor(() => { expect(ipcBridge.fs.readFile.invoke).toHaveBeenCalledTimes(2); }); expect(screen.queryByTestId('diff-viewer')).not.toBeInTheDocument(); }); });