cloud-drive-renamer/tests/unit/file-selector-panel-undo.test.ts
Zain bf75cdcb10 feat: 为批量重命名面板增加撤销最近一次操作功能
- 在平台适配器中统一暴露 getCurrentDirectoryKey() 方法,用于确定当前目录范围
- 新增 LastRenameOperation 数据模型和核心操作函数,支持单条撤销记录的创建、合并和范围校验
- 在 file-selector-panel 中集成撤销逻辑,支持执行后保存记录、retry 成功后合并记录以及撤销操作
- 在 config-panel 底部操作区新增撤销按钮,根据当前目录范围动态显示/隐藏
- 添加多语言支持,包含撤销相关文案
- 添加单元测试和集成测试,确保撤销功能的正确性和稳定性
2026-03-18 23:14:53 +08:00

202 行
5.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FileSelectorPanel } from '../../src/content/components/file-selector-panel';
import { FileItem, PlatformAdapter, RenameResult } from '../../src/types/platform';
import { BatchResults } from '../../src/types/core';
import { LastRenameOperation } from '../../src/types/undo';
type FileSelectorPanelTestHarness = FileSelectorPanel & {
lastRenameOperation: LastRenameOperation | null;
allFiles: FileItem[];
executionResults: BatchResults | null;
clearLastRenameOperationIfOutOfScope(): void;
updateLastRenameOperationFromExecute(results: BatchResults): void;
mergeLastRenameOperationFromRetry(results: BatchResults): void;
handleUndoLastRename(): Promise<void>;
};
class PanelMockAdapter implements PlatformAdapter {
readonly platform = 'quark' as const;
directoryKey = 'dir-a';
getCurrentDirectoryKey(): string {
return this.directoryKey;
}
async getSelectedFiles(): Promise<FileItem[]> {
return [];
}
async getAllFiles(): Promise<FileItem[]> {
return [];
}
async renameFile(_fileId: string, newName: string): Promise<RenameResult> {
return { success: true, newName };
}
async checkNameConflict(): Promise<boolean> {
return false;
}
async getFileInfo(fileId: string): Promise<FileItem> {
return {
id: fileId,
name: `${fileId}.txt`,
ext: '.txt',
parentId: this.directoryKey,
size: 0,
mtime: Date.now(),
};
}
getConfig() {
return {
platform: 'quark' as const,
requestInterval: 10,
maxRetries: 3,
};
}
}
describe('FileSelectorPanel undo helpers', () => {
beforeEach(() => {
vi.stubGlobal('chrome', {
storage: {
local: {
get: vi.fn(async () => ({})),
set: vi.fn(async () => undefined),
remove: vi.fn(async () => undefined),
},
},
});
});
it('clears last rename operation when current scope no longer matches', () => {
const panel = new FileSelectorPanel() as FileSelectorPanelTestHarness;
const adapter = new PanelMockAdapter();
panel.adapter = adapter;
panel.lastRenameOperation = {
platform: 'quark',
directoryKey: 'dir-b',
createdAt: 1,
updatedAt: 1,
items: [{ fileId: '1', original: 'a.txt', renamed: 'b.txt', index: 0 }],
};
panel.clearLastRenameOperationIfOutOfScope();
expect(panel.lastRenameOperation).toBeNull();
});
it('stores a new last rename operation after execute success results', () => {
const panel = new FileSelectorPanel() as FileSelectorPanelTestHarness;
const adapter = new PanelMockAdapter();
panel.adapter = adapter;
const results: BatchResults = {
success: [{ fileId: '1', original: 'old-a.txt', renamed: 'new-a.txt', index: 0 }],
failed: [],
};
panel.updateLastRenameOperationFromExecute(results);
expect(panel.lastRenameOperation?.directoryKey).toBe('dir-a');
expect(panel.lastRenameOperation?.items).toEqual([
{ fileId: '1', original: 'old-a.txt', renamed: 'new-a.txt', index: 0 },
]);
});
it('merges retry success items into the current last rename operation', () => {
const panel = new FileSelectorPanel() as FileSelectorPanelTestHarness;
const adapter = new PanelMockAdapter();
panel.adapter = adapter;
panel.lastRenameOperation = {
platform: 'quark',
directoryKey: 'dir-a',
createdAt: 1,
updatedAt: 1,
items: [{ fileId: '1', original: 'old-a.txt', renamed: 'new-a.txt', index: 0 }],
};
const retryResults: BatchResults = {
success: [{ fileId: '2', original: 'old-b.txt', renamed: 'new-b.txt', index: 1 }],
failed: [],
};
panel.mergeLastRenameOperationFromRetry(retryResults);
expect(panel.lastRenameOperation?.items.map((item) => item.fileId)).toEqual(['1', '2']);
});
it('undoes the last rename operation and clears the record after full success', async () => {
const panel = new FileSelectorPanel() as FileSelectorPanelTestHarness;
const adapter = new PanelMockAdapter();
panel.adapter = adapter;
panel.allFiles = [
{
id: '1',
name: 'new-a.txt',
ext: '.txt',
parentId: 'dir-a',
size: 0,
mtime: Date.now(),
},
];
panel.lastRenameOperation = {
platform: 'quark',
directoryKey: 'dir-a',
createdAt: 1,
updatedAt: 1,
items: [{ fileId: '1', original: 'old-a.txt', renamed: 'new-a.txt', index: 0 }],
};
await panel.handleUndoLastRename();
expect(panel.lastRenameOperation).toBeNull();
expect(panel.allFiles[0].name).toBe('old-a.txt');
expect(panel.executionResults?.success).toHaveLength(1);
});
it('undoes both initial success items and retry-merged success items together', async () => {
const panel = new FileSelectorPanel() as FileSelectorPanelTestHarness;
const adapter = new PanelMockAdapter();
panel.adapter = adapter;
panel.allFiles = [
{
id: '1',
name: 'new-a.txt',
ext: '.txt',
parentId: 'dir-a',
size: 0,
mtime: Date.now(),
},
{
id: '2',
name: 'new-b.txt',
ext: '.txt',
parentId: 'dir-a',
size: 0,
mtime: Date.now(),
},
];
panel.lastRenameOperation = {
platform: 'quark',
directoryKey: 'dir-a',
createdAt: 1,
updatedAt: 1,
items: [{ fileId: '1', original: 'old-a.txt', renamed: 'new-a.txt', index: 0 }],
};
panel.mergeLastRenameOperationFromRetry({
success: [{ fileId: '2', original: 'old-b.txt', renamed: 'new-b.txt', index: 1 }],
failed: [],
});
await panel.handleUndoLastRename();
expect(panel.lastRenameOperation).toBeNull();
expect(panel.allFiles.map((file) => file.name)).toEqual(['old-a.txt', 'old-b.txt']);
expect(panel.executionResults?.success).toHaveLength(2);
});
});