import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FileSelectorPanel } from '../../src/content/components/file-selector-panel'; import { BatchExecutor, ExecutorState } from '../../src/core/executor'; import { crashRecovery } from '../../src/core/crash-recovery'; import type { FileItem, PlatformAdapter, RenameResult } from '../../src/types/platform'; import * as rulePresets from '../../src/utils/rule-presets'; type FileSelectorPanelPresetHarness = FileSelectorPanel & { ruleConfig: any; allFiles: FileItem[]; uncheckList: Set; newNameMap: Map; extractErrorMap: Map; episodeExtractAssistState: any; recentRulePresets: any[]; templateRulePresets: any[]; handleApplyRulePreset(event: CustomEvent<{ presetId: string }>): Promise; handleExecute(): Promise; handleAssistFocusFailure(event: CustomEvent<{ fileId: string }>): void; handleRenameRuleTemplate(event: CustomEvent<{ presetId: string; name: string }>): Promise; }; class PresetTestAdapter implements PlatformAdapter { readonly platform = 'aliyun' as const; getCurrentDirectoryKey(): string { return 'root'; } async getSelectedFiles(): Promise { return []; } async getAllFiles(): Promise { return []; } async renameFile(_fileId: string, newName: string): Promise { return { success: true, newName }; } async checkNameConflict(): Promise { return false; } async getFileInfo(fileId: string): Promise { return { id: fileId, name: `${fileId}.mkv`, ext: '.mkv', parentId: 'root', size: 1, mtime: Date.now(), }; } getConfig() { return { platform: 'aliyun' as const, requestInterval: 0, maxConcurrent: 1, maxRetries: 1, }; } } describe('FileSelectorPanel rule preset integration', () => { beforeEach(() => { vi.restoreAllMocks(); vi.stubGlobal('chrome', { storage: { local: { get: vi.fn(async () => ({})), set: vi.fn(async () => undefined), remove: vi.fn(async () => undefined), clear: vi.fn(async () => undefined), }, }, runtime: { sendMessage: vi.fn(async () => undefined), }, }); vi.stubGlobal('localStorage', { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), key: vi.fn(), length: 0, }); vi.stubGlobal('confirm', vi.fn(() => true)); vi.stubGlobal('alert', vi.fn()); vi.stubGlobal('prompt', vi.fn(() => null)); vi.spyOn(crashRecovery, 'saveOperationState').mockResolvedValue(); vi.spyOn(crashRecovery, 'clearOperationState').mockResolvedValue(); vi.spyOn(BatchExecutor.prototype, 'getState').mockReturnValue(ExecutorState.COMPLETED); }); it('applies a preset config and resets assist state', async () => { const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness; vi.spyOn(rulePresets, 'getRulePresetById').mockResolvedValue({ id: 'template-episode', name: '剧集模板', source: 'template', createdAt: 1, updatedAt: 1, config: { type: 'episodeExtract', params: { template: '{prefix}.S{season}E{episode}{ext}', prefix: 'Show', season: 2, offset: 1, leadingZeroCount: 2, helperPre: '第', helperPost: '集', }, }, }); panel.episodeExtractAssistState = { sampleFileId: 'old', sampleFileName: 'old-sample', fillTarget: 'helperPost', fillMode: 'segment', suggestedFailureFileId: 'old', }; await panel.handleApplyRulePreset(new CustomEvent('apply-rule-preset', { detail: { presetId: 'template-episode' } })); expect(panel.ruleConfig.type).toBe('episodeExtract'); expect(panel.ruleConfig.params.prefix).toBe('Show'); expect(panel.episodeExtractAssistState.sampleFileId).toBeNull(); expect(panel.episodeExtractAssistState.fillTarget).toBe('prefix'); expect(panel.episodeExtractAssistState.fillMode).toBe('full-name'); }); it('blocks invalid preset application and shows alert', async () => { const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness; const alertSpy = vi.spyOn(window, 'alert'); vi.spyOn(rulePresets, 'getRulePresetById').mockResolvedValue({ id: 'invalid-template', name: '失效模板', source: 'template', createdAt: 1, updatedAt: 1, config: { type: 'replace', params: { search: '', replace: '' }, }, }); panel.ruleConfig = { type: 'prefix', params: { prefix: 'Keep', separator: '.' }, }; await panel.handleApplyRulePreset(new CustomEvent('apply-rule-preset', { detail: { presetId: 'invalid-template' } })); expect(alertSpy).toHaveBeenCalledTimes(1); expect(panel.ruleConfig.type).toBe('prefix'); expect(panel.ruleConfig.params.prefix).toBe('Keep'); }); it('records recent presets after a successful execution', async () => { const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness; panel.adapter = new PresetTestAdapter(); panel.ruleConfig = { type: 'prefix', params: { prefix: 'Renamed-', separator: '' }, }; panel.allFiles = [ { id: 'file-1', name: 'episode-01.mkv', ext: '.mkv', parentId: 'root', size: 1, mtime: 1, }, ]; panel.uncheckList = new Set(); panel.newNameMap = new Map([['file-1', 'Renamed-episode-01.mkv']]); panel.extractErrorMap = new Map(); vi.spyOn(BatchExecutor.prototype, 'execute').mockResolvedValue({ success: [ { fileId: 'file-1', original: 'episode-01.mkv', renamed: 'Renamed-episode-01.mkv', index: 0, }, ], failed: [], }); const recentSpy = vi.spyOn(rulePresets, 'recordRecentRulePreset').mockResolvedValue([ { id: 'recent-1', source: 'recent', createdAt: 1, updatedAt: 2, config: panel.ruleConfig, }, ]); await panel.handleExecute(); expect(recentSpy).toHaveBeenCalledWith(panel.ruleConfig); expect(panel.recentRulePresets).toHaveLength(1); expect(panel.recentRulePresets[0].id).toBe('recent-1'); }); it('focuses the target failure item from preview navigation', async () => { const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness; panel.allFiles = [ { id: 'file-1', name: 'Show.S01E01.mkv', ext: '.mkv', parentId: 'root', size: 1, mtime: 1, }, { id: 'file-2', name: 'Show.S01E02.mkv', ext: '.mkv', parentId: 'root', size: 1, mtime: 2, }, ]; panel.uncheckList = new Set(); panel.episodeExtractAssistState = { sampleFileId: null, sampleFileName: null, fillTarget: 'prefix', fillMode: 'segment', suggestedFailureFileId: null, }; panel.handleAssistFocusFailure( new CustomEvent('episode-assist-focus-failure', { detail: { fileId: 'file-2' } }) ); expect(panel.episodeExtractAssistState.sampleFileId).toBe('file-2'); expect(panel.episodeExtractAssistState.sampleFileName).toBe('Show.S01E02.mkv'); expect(panel.episodeExtractAssistState.suggestedFailureFileId).toBe('file-2'); }); it('renames template presets and refreshes template list', async () => { const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness; panel.templateRulePresets = [ { id: 'template-1', name: '旧模板', source: 'template', createdAt: 1, updatedAt: 1, config: { type: 'prefix', params: { prefix: 'Show', separator: '.' }, }, }, ]; vi.spyOn(rulePresets, 'renameTemplateRulePreset').mockResolvedValue({ replaced: false, record: { id: 'template-1', name: '新模板', source: 'template', createdAt: 1, updatedAt: 2, config: { type: 'prefix', params: { prefix: 'Show', separator: '.' }, }, }, records: [ { id: 'template-1', name: '新模板', source: 'template', createdAt: 1, updatedAt: 2, config: { type: 'prefix', params: { prefix: 'Show', separator: '.' }, }, }, ], }); await panel.handleRenameRuleTemplate( new CustomEvent('rename-rule-template', { detail: { presetId: 'template-1', name: '新模板' } }) ); expect(panel.templateRulePresets).toHaveLength(1); expect(panel.templateRulePresets[0].name).toBe('新模板'); }); });