panatgithub_AnkiHeadingSync/src/presentation/notices/NoticeService.test.ts
Dusk 67854cf154 feat: implement full bilingual i18n and structured error rendering
- 新增完整双语 i18n 支持,支持按 Obsidian 语言自动切换
- 实现结构化 warning / user error 渲染
- Added full bilingual i18n support with automatic switching based on Obsidian language settings
- Implemented structured warning and user error rendering
2026-04-21 13:34:27 +08:00

144 lines
No EOL
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { beforeEach, describe, expect, it, vi } from "vitest";
const { getLanguageMock, noticeRecords } = vi.hoisted(() => {
const hoistedGetLanguage = vi.fn(() => "en");
const hoistedNoticeRecords: Array<{ message: string; timeout?: number }> = [];
class HoistedNotice {
constructor(message: string, timeout?: number) {
hoistedNoticeRecords.push({ message, timeout });
}
}
return {
getLanguageMock: hoistedGetLanguage,
noticeRecords: hoistedNoticeRecords,
HoistedNotice,
};
});
vi.mock("obsidian", () => ({
getLanguage: getLanguageMock,
Notice: class {
constructor(message: string, timeout?: number) {
noticeRecords.push({ message, timeout });
}
},
}));
import type { ClearCurrentFileSyncedCardsResult, CleanupEmptyDecksResult } from "@/application/use-cases/cleanupResetTypes";
import type { ManualSyncResult } from "@/application/use-cases/manualSyncTypes";
import { NoticeService } from "./NoticeService";
function createManualSyncResult(overrides: Partial<ManualSyncResult> = {}): ManualSyncResult {
return {
scannedFiles: 3,
scannedCards: 8,
created: 2,
updated: 1,
migratedDecks: 1,
orphaned: 0,
uploadedMedia: 4,
skippedUnchangedCards: 5,
rewrittenMarkers: 2,
markerWriteConflictFiles: [],
warnings: [],
...overrides,
};
}
function createClearResult(overrides: Partial<ClearCurrentFileSyncedCardsResult> = {}): ClearCurrentFileSyncedCardsResult {
return {
trackedCards: 2,
trackedGroups: 1,
deletedNotes: 3,
removedMarkers: 4,
removedCardMarkers: 2,
removedGroupMarkers: 2,
deletedLocalRecords: 3,
conflictFiles: [],
failureFiles: [],
...overrides,
};
}
function createCleanupResult(overrides: Partial<CleanupEmptyDecksResult> = {}): CleanupEmptyDecksResult {
return {
candidateCount: 4,
selectedCount: 3,
deletedCount: 2,
deletedDeckNames: ["Deck A", "Deck B"],
skippedDeckNames: ["Deck C"],
...overrides,
};
}
describe("NoticeService", () => {
beforeEach(() => {
noticeRecords.length = 0;
getLanguageMock.mockReset();
getLanguageMock.mockReturnValue("en");
});
it("renders sync summary and deck warnings in English", () => {
const service = new NoticeService();
service.showSyncSummary("currentFile", createManualSyncResult({
markerWriteConflictFiles: ["notes/a.md", "notes/b.md"],
warnings: [
{ filePath: "notes/a.md", code: "deck_conflict_yaml_body", params: { marker: "TARGET DECK" } },
{ filePath: "notes/b.md", code: "deck_fallback_default" },
],
}));
expect(noticeRecords.map((entry) => entry.message)).toEqual([
"Current file sync completed: files 3, cards 8, created 2, updated 1, migrated decks 1, orphaned 0, media 4, skipped 5. Marker write conflicts: notes/a.md, notes/b.md. Warnings: 2.",
"Conflicting TARGET DECK declarations were found in YAML and body. This sync used the YAML value.",
"No explicit deck was found and a folder-based deck could not be generated, so the default deck was used.",
]);
});
it("renders rebuild summary and warnings in Simplified Chinese", () => {
getLanguageMock.mockReturnValue("zh");
const service = new NoticeService();
service.showRebuildSummary(createManualSyncResult({
warnings: [{ filePath: "notes/a.md", code: "deck_multiple_body_declarations", params: { marker: "MY DECK" } }],
}));
expect(noticeRecords.map((entry) => entry.message)).toEqual([
"卡片索引重建完成:文件 3卡片 8迁移牌组 1孤儿 0重写标记 2跳过 5。 警告1。",
"检测到文件中存在多个 MY DECK 声明,本次同步只使用第一个正文声明。",
]);
});
it("renders clear-current-file summaries with localized failures", () => {
getLanguageMock.mockReturnValue("zh");
const service = new NoticeService();
service.showClearCurrentFileSummary(createClearResult({
conflictFiles: ["notes/reset.md"],
failureFiles: [
{ filePath: "notes/a.md", key: "errors.markerRemoval.markdownFileNotFound" },
{ filePath: "notes/b.md", rawMessage: "permission denied" },
],
}));
expect(noticeRecords.map((entry) => entry.message)).toEqual([
"当前文件已同步卡片清空完成:已跟踪卡片 2已跟踪分组 1删除笔记 3移除标记 4移除 ID 标记 2移除 GI 标记 2删除本地记录 3。 标记移除冲突notes/reset.md。 失败项notes/a.md (Markdown 文件不存在。)、notes/b.md (permission denied)。",
]);
});
it("renders cleanup summary and skipped decks in English", () => {
const service = new NoticeService();
service.showCleanupEmptyDecksSummary(createCleanupResult({
skippedDeckNames: ["Deck C", "Deck D"],
}));
expect(noticeRecords.map((entry) => entry.message)).toEqual([
"Empty-deck cleanup completed: candidates 4, selected 3, deleted 2, skipped 2. Skipped decks: Deck C, Deck D.",
]);
});
});