feat: 默认折叠设置卡片并迁移 cloze 默认识别 / collapse settings cards and migrate default cloze recognition

This commit is contained in:
Dusk 2026-04-25 23:16:40 +08:00
parent 0955ba952d
commit b6e59c8eec
7 changed files with 455 additions and 22 deletions

View file

@ -0,0 +1,88 @@
# 设置页默认折叠与填空题标记修复决策
本文锁定本轮“设置页全部默认折叠 + cloze 默认识别切换到 `H4 + #anki-cloze`”的最终实现策略。
## 1. 默认折叠策略
- `expandedCardIds` 默认改为空集合。
- 不改 `toggleCard()`
- 不改 `renderCard()`
- 不改局部刷新边界。
原因:
- 现有卡片折叠/展开与局部刷新行为已经正确,本轮只需要把首次进入设置页时的默认态改成“全部折叠”。
## 2. cloze 新默认决策
- 新默认常量:
- `DEFAULT_CLOZE_HEADING_LEVEL = 4`
- `DEFAULT_CLOZE_MARKER = "#anki-cloze"`
- `createDefaultCardTypeConfigs()` 中 cloze 默认改为:
- `headingLevel: 4`
- `extraMarker: "#anki-cloze"`
原因:
- 这样可以与现有 `#anki-list` 风格对齐。
- 同级 `H4` 下:
- 空标签留给普通问答
- `#anki-list` 留给 QA Group
- `#anki-cloze` 留给 Cloze
## 3. 旧默认迁移策略
- 仅当 cloze 识别配置仍等于旧默认时才迁移:
- `headingLevel: 5`
- `extraMarker: ""`
- 迁移后改为:
- `headingLevel: 4`
- `extraMarker: "#anki-cloze"`
- 如果用户已经自定义 cloze 的 heading level 或 extra marker则保留用户配置。
原因:
- 本轮目标是替换旧默认,而不是重写用户自定义识别规则。
## 4. legacy 快照的判定策略
- 对只有旧顶层字段、没有显式 cloze per-card recognition 配置的 legacy 快照:
- 如果 `clozeHeadingLevel` 仍是旧默认 `5`
- 则将其视为可迁移的旧默认,升级到 `4 + #anki-cloze`
原因:
- 旧结构没有 cloze marker 字段,无法精确区分“显式保留 H5”与“沿用旧默认 H5”。
- 仓库现实下只能把 legacy `H5` 视为旧默认并升级。
## 5. 识别与校验策略
- 保留现有 `resolveHeadingConfig()`
- 先 marker 匹配
- 再空 marker 默认回退
- 保留现有“同一个 heading 只能有一个 enabled 空 marker 默认行”的校验。
原因:
- 当前识别算法已经足够支持同级 `H4` 下的 marker 分流。
- 只要 cloze 改成 `#anki-cloze`,默认冲突自然消失。
## 6. 测试策略
- 设置页测试全部改为“默认折叠”前提。
- 需要读取 card 1 内容或触发局部刷新时,先点击展开对应卡片。
- 新增或更新设置模型测试,覆盖:
- cloze 新默认
- 旧默认迁移
- 用户自定义保留
- 新增识别测试,覆盖:
- `H4 + 空标签 -> basic`
- `H4 + #anki-list -> qa-group`
- `H4 + #anki-cloze -> cloze`
## 7. 不变边界
- 不改 Anki 字段映射。
- 不改卡片渲染逻辑。
- 不改 sticky 设置页结构。
- 不改 sync 执行逻辑,除默认识别配置变化自然影响扫描结果外,不新增行为。

View file

@ -0,0 +1,99 @@
# 设置页默认折叠与填空题标记差距审计
本文基于当前仓库真实实现,定位本轮两项行为调整的实际接入点:设置页默认折叠,以及填空题默认识别从 `H5 + 空标签` 切换为 `H4 + #anki-cloze`
## 1. 当前设置页仍有默认展开卡片
- 入口在 [src/presentation/settings/PluginSettingTab.ts](src/presentation/settings/PluginSettingTab.ts)。
- 当前 `expandedCardIds` 初始化仍是:
```ts
new Set<SettingsCardId>(["card-types", "commands"])
```
结果是:首次进入设置页时,`卡片类型及基础设置` 和 `命令说明` 默认展开和本轮“5 张卡片全部默认折叠”的目标不一致。
## 2. 当前点击展开/折叠与局部刷新边界已经正确
- `toggleCard()` 只改 `expandedCardIds`
- `renderCard()` 只重绘单卡 `bodyEl`
- 现有测试也覆盖了:
- 卡片展开/折叠不整页重建
- card 1 刷新不整页重建
- scope/deck 局部刷新
因此本轮只需要把默认展开集合改为空,并同步更新测试,不需要改卡片 shell 结构或刷新逻辑。
## 3. 当前 cloze 默认仍是旧规则 `H5 + 空标签`
- 当前默认值在 [src/application/config/PluginSettings.ts](src/application/config/PluginSettings.ts) 中仍是:
- `DEFAULT_CLOZE_HEADING_LEVEL = 5`
- `createDefaultCardTypeConfigs().cloze.extraMarker = ""`
这意味着当前默认识别规则仍是:
1. 普通问答:`H4 + 空标签`
2. 多级列表问答:`H4 + #anki-list`
3. 填空题:`H5 + 空标签`
与本轮要求的:
1. 普通问答:`H4 + 空标签`
2. 多级列表问答:`H4 + #anki-list`
3. 填空题:`H4 + #anki-cloze`
不一致。
## 4. 当前设置合并逻辑还没有旧默认迁移
- `normalizePluginSettings()` 通过 `mergeCardTypeConfigs()` 合并默认值、旧字段和 `cardTypeConfigs`
- 目前 `mergeCardTypeConfigs()` 只做:
- 回填默认值
- sanitize
- 衍生 legacy 字段
- 还没有“旧默认 cloze 配置自动迁移到新默认”的规则。
因此已有用户如果保存的是:
- `headingLevel: 5`
- `extraMarker: ""`
当前加载后仍会保持旧行为。
## 5. 当前识别优先级已经支持同级 marker 分流
- 真正识别入口在 [src/domain/manual-sync/services/CardIndexingService.ts](src/domain/manual-sync/services/CardIndexingService.ts)。
- `resolveHeadingConfig()` 当前规则是:
- 先在同级 enabled 配置中找 marker 精确尾随匹配
- 再退回到同级 enabled 的空 marker 默认行
- 匹配顺序是:
- `qa-group`
- `semantic-qa`
- `cloze`
- `basic`
这说明本轮无需重写识别算法,只要把 cloze 默认改成 `H4 + #anki-cloze`,同级分流就会自然成立。
## 6. 需要适配的测试切片
- `PluginSettingTab.test.ts` 当前大量测试默认依赖 `card-types` 已展开。
- 一旦默认改成全部折叠,相关测试必须先点击展开对应卡片,再断言局部刷新内容。
- `PluginSettings.test.ts` 当前默认值和 legacy snapshot 断言仍写着 cloze 的旧默认。
- `CardIndexingService.test.ts` 当前已有一些显式 `clozeHeadingLevel: 5` 的兼容测试,这些应保留;但还缺少“默认 H4 + #anki-cloze”路由断言。
## 7. 结论
当前真实差距是:
1. 设置页仍默认展开两张卡片。
2. cloze 默认仍是 `H5 + 空标签`
3. 设置合并逻辑没有旧默认迁移。
4. 识别算法本身已经支持同级 marker 优先,不需要重写。
5. 测试需要适配“默认全部折叠”与“默认 cloze marker 已改”的新事实。
最小修复路径是:
1. 把 `expandedCardIds` 默认改为空集合。
2. 把 cloze 默认改成 `H4 + #anki-cloze`
3. 在 `mergeCardTypeConfigs()` 中补旧默认迁移,只覆盖旧默认识别配置,不覆盖用户自定义 cloze 识别配置。
4. 更新设置页、设置模型、识别测试与局部刷新测试。

View file

@ -96,8 +96,8 @@ describe("PluginSettings", () => {
},
cloze: {
enabled: true,
headingLevel: 5,
extraMarker: "",
headingLevel: 4,
extraMarker: "#anki-cloze",
noteType: "",
},
"semantic-qa": {
@ -174,6 +174,69 @@ describe("PluginSettings", () => {
});
});
it("migrates the old default cloze recognition to H4 + #anki-cloze", () => {
const settings = mergePluginSettings({
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 5,
extraMarker: "",
},
},
});
expect(settings.cardTypeConfigs.cloze).toEqual(expect.objectContaining({
headingLevel: 4,
extraMarker: "#anki-cloze",
}));
});
it("migrates legacy cloze H5 snapshots when no per-card recognition override exists", () => {
const settings = mergePluginSettings({
clozeHeadingLevel: 5,
clozeNoteType: "Legacy Cloze",
});
expect(settings.cardTypeConfigs.cloze).toEqual(expect.objectContaining({
headingLevel: 4,
extraMarker: "#anki-cloze",
noteType: "Legacy Cloze",
}));
});
it("preserves user customized cloze recognition settings", () => {
const customizedHeading = mergePluginSettings({
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 6,
extraMarker: "",
},
},
});
expect(customizedHeading.cardTypeConfigs.cloze).toEqual(expect.objectContaining({
headingLevel: 6,
extraMarker: "",
}));
const customizedMarker = mergePluginSettings({
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 4,
extraMarker: "#custom-cloze",
},
},
});
expect(customizedMarker.cardTypeConfigs.cloze).toEqual(expect.objectContaining({
headingLevel: 4,
extraMarker: "#custom-cloze",
}));
});
it("rejects invalid cached Anki note type lists", () => {
expectPluginUserError(() => {
validatePluginSettings({
@ -267,8 +330,8 @@ describe("PluginSettings", () => {
},
cloze: {
enabled: true,
headingLevel: 5,
extraMarker: "",
headingLevel: 4,
extraMarker: "#anki-cloze",
noteType: "Legacy Cloze",
},
"semantic-qa": {

View file

@ -30,9 +30,12 @@ export type AnkiModelFieldCache = Record<string, AnkiModelFieldCacheEntry>;
export const DEFAULT_OBSIDIAN_BACKLINK_LABEL = "Open in Obsidian";
const DEFAULT_BASIC_HEADING_LEVEL = 4;
const DEFAULT_CLOZE_HEADING_LEVEL = 5;
const LEGACY_DEFAULT_CLOZE_HEADING_LEVEL = 5;
const DEFAULT_CLOZE_HEADING_LEVEL = 4;
const DEFAULT_BASIC_NOTE_TYPE = "";
const DEFAULT_CLOZE_NOTE_TYPE = "";
const LEGACY_DEFAULT_CLOZE_MARKER = "";
const DEFAULT_CLOZE_MARKER = "#anki-cloze";
const DEFAULT_SEMANTIC_QA_NOTE_TYPE = "Semantic QA";
const DEFAULT_QA_GROUP_MARKER = "#anki-list";
const DEFAULT_SEMANTIC_QA_MARKER = "#anki-list-qa";
@ -375,7 +378,7 @@ function createDefaultCardTypeConfigs(): CardTypeConfigs {
cloze: {
enabled: true,
headingLevel: DEFAULT_CLOZE_HEADING_LEVEL,
extraMarker: "",
extraMarker: DEFAULT_CLOZE_MARKER,
noteType: DEFAULT_CLOZE_NOTE_TYPE,
},
"semantic-qa": {
@ -430,17 +433,53 @@ function mergeCardTypeConfigs(
? { ...fallbackConfig, ...rawConfig }
: fallbackConfig;
nextConfigs[configId] = {
const normalizedConfig: CardTypeConfig = {
enabled: typeof mergedConfig.enabled === "boolean" ? mergedConfig.enabled : fallbackConfig.enabled,
headingLevel: sanitizeHeadingLevel(mergedConfig.headingLevel, fallbackConfig.headingLevel),
extraMarker: sanitizeMarker(mergedConfig.extraMarker, fallbackConfig.extraMarker),
noteType: sanitizeNoteType(mergedConfig.noteType, fallbackConfig.noteType),
};
nextConfigs[configId] = configId === "cloze"
? migrateLegacyDefaultClozeConfig(normalizedConfig, rawConfig, legacySettings)
: normalizedConfig;
}
return nextConfigs;
}
function migrateLegacyDefaultClozeConfig(
config: CardTypeConfig,
rawConfig: Partial<CardTypeConfig> | null | undefined,
legacySettings: Partial<PluginSettings>,
): CardTypeConfig {
if (config.headingLevel === LEGACY_DEFAULT_CLOZE_HEADING_LEVEL && config.extraMarker === LEGACY_DEFAULT_CLOZE_MARKER) {
return {
...config,
headingLevel: DEFAULT_CLOZE_HEADING_LEVEL,
extraMarker: DEFAULT_CLOZE_MARKER,
};
}
if (!hasExplicitClozeRecognitionConfig(rawConfig) && legacySettings.clozeHeadingLevel === LEGACY_DEFAULT_CLOZE_HEADING_LEVEL) {
return {
...config,
headingLevel: DEFAULT_CLOZE_HEADING_LEVEL,
extraMarker: DEFAULT_CLOZE_MARKER,
};
}
return config;
}
function hasExplicitClozeRecognitionConfig(rawConfig: Partial<CardTypeConfig> | null | undefined): boolean {
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
return false;
}
return "headingLevel" in rawConfig || "extraMarker" in rawConfig;
}
function deriveLegacySettings(cardTypeConfigs: CardTypeConfigs): Pick<PluginSettings, "qaHeadingLevel" | "clozeHeadingLevel" | "qaGroupMarker" | "qaNoteType" | "clozeNoteType" | "semanticQaMarker" | "semanticQaNoteType"> {
return {
qaHeadingLevel: cardTypeConfigs.basic.headingLevel,

View file

@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_SETTINGS } from "@/application/config/PluginSettings";
import { buildGroupSrc } from "@/domain/manual-sync/entities/IndexedGroupCardBlock";
import type { CardState, GroupBlockState } from "@/domain/manual-sync/entities/PluginState";
import { hashString } from "@/domain/shared/hash";
@ -190,8 +191,14 @@ describe("CardIndexingService", () => {
content: ["#### Prompt", "Answer", "", "", "Remarks"].join("\n"),
},
{
qaHeadingLevel: 4,
clozeHeadingLevel: 5,
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 5,
extraMarker: "",
},
},
cardAnswerCutoffMode: "double-blank-lines",
fileStamp: "1:1",
knownCards: [],
@ -217,8 +224,14 @@ describe("CardIndexingService", () => {
content: ["#### Prompt", "Answer", "", "", "Remarks", "<!--ID: 42-->"].join("\n"),
},
{
qaHeadingLevel: 4,
clozeHeadingLevel: 5,
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 5,
extraMarker: "",
},
},
cardAnswerCutoffMode: "double-blank-lines",
fileStamp: "1:1",
knownCards: [],
@ -245,8 +258,14 @@ describe("CardIndexingService", () => {
content: ["##### Cloze", "{{c1::Answer}}", "", "", "Remarks"].join("\n"),
},
{
qaHeadingLevel: 4,
clozeHeadingLevel: 5,
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 5,
extraMarker: "",
},
},
cardAnswerCutoffMode: "double-blank-lines",
fileStamp: "1:1",
knownCards: [],
@ -261,6 +280,72 @@ describe("CardIndexingService", () => {
});
});
it("routes H4 headings by marker priority with the default card type configs", () => {
const service = new CardIndexingService();
const indexedFile = service.index(
{
path: "notes/example.md",
basename: "example",
content: [
"#### Basic",
"Answer",
"",
"#### Cloze #anki-cloze",
"{{c1::Answer}}",
"",
"#### Concepts #anki-list",
"- Alpha",
" - First answer",
].join("\n"),
},
{
cardTypeConfigs: DEFAULT_SETTINGS.cardTypeConfigs,
fileStamp: "1:1",
knownCards: [],
pendingWriteBack: [],
},
);
expect(indexedFile.cards.map((card) => card.cardType)).toEqual(["basic", "cloze"]);
expect(indexedFile.groupBlocks?.[0]).toMatchObject({
headingText: "Concepts #anki-list",
headingLevel: 4,
});
});
it("does not produce cloze cards when the cloze config is disabled", () => {
const service = new CardIndexingService();
const indexedFile = service.index(
{
path: "notes/example.md",
basename: "example",
content: [
"#### Cloze #anki-cloze",
"{{c1::Answer}}",
].join("\n"),
},
{
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
basic: {
...DEFAULT_SETTINGS.cardTypeConfigs.basic,
enabled: false,
},
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
enabled: false,
},
},
fileStamp: "1:1",
knownCards: [],
pendingWriteBack: [],
},
);
expect(indexedFile.cards).toHaveLength(0);
expect(indexedFile.groupBlocks ?? []).toHaveLength(0);
});
it("splits a tagged QA heading into semantic QA child cards with distinct titles and backlink anchors", () => {
const service = new CardIndexingService();
const indexedFile = service.index(
@ -488,8 +573,14 @@ describe("CardIndexingService", () => {
tags: ["📖::一人公司", "3地区"],
},
{
qaHeadingLevel: 4,
clozeHeadingLevel: 5,
cardTypeConfigs: {
...DEFAULT_SETTINGS.cardTypeConfigs,
cloze: {
...DEFAULT_SETTINGS.cardTypeConfigs.cloze,
headingLevel: 5,
extraMarker: "",
},
},
semanticQaMarker: "#anki-list-qa",
syncObsidianTagsToAnki: true,
fileStamp: "1:1",

View file

@ -573,6 +573,14 @@ async function flushPromises(): Promise<void> {
await Promise.resolve();
}
async function expandCard(tab: AnkiHeadingSyncSettingTab, cardId: string): Promise<void> {
if (queryByDataset(tab.containerEl, "settingsCardBody", cardId).style.display === "block") {
return;
}
await queryByDataset(tab.containerEl, "settingsCardToggle", cardId).trigger("click");
}
describe("PluginSettingTab", () => {
const originalResizeObserver = globalThis.ResizeObserver;
@ -593,7 +601,7 @@ describe("PluginSettingTab", () => {
Reflect.set(globalThis, "ResizeObserver", originalResizeObserver);
});
it("renders five cards with the required default expansion state", () => {
it("renders five cards collapsed by default", () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
@ -602,11 +610,34 @@ describe("PluginSettingTab", () => {
expect(queryByDataset(tab.containerEl, "settingsPageHeader", "true")).toBeDefined();
const cards = queryAllByDataset(tab.containerEl, "settingsCard");
expect(cards).toHaveLength(5);
expect(queryByDataset(tab.containerEl, "settingsCardBody", "card-types").style.display).toBe("block");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "commands").style.display).toBe("block");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "sync-content").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "card-types").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "commands").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "scope").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "deck").style.display).toBe("none");
for (const cardId of ["card-types", "sync-content", "scope", "deck", "commands"] as const) {
expect(queryByDataset(tab.containerEl, "settingsCardToggle", cardId).textContent.startsWith("▸ ")).toBe(true);
}
});
it("expands only the clicked card while others remain collapsed", async () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await queryByDataset(tab.containerEl, "settingsCardToggle", "scope").trigger("click");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "scope").style.display).toBe("block");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "card-types").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "sync-content").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "deck").style.display).toBe("none");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "commands").style.display).toBe("none");
await queryByDataset(tab.containerEl, "settingsCardToggle", "scope").trigger("click");
expect(queryByDataset(tab.containerEl, "settingsCardBody", "scope").style.display).toBe("none");
});
it("applies sticky styles to the opaque page header gap, mask, and card headers", () => {
@ -673,11 +704,12 @@ describe("PluginSettingTab", () => {
expect(resizeObserver?.disconnected).toBe(true);
});
it("renders card 1 as three readable card type blocks", () => {
it("renders card 1 as three readable card type blocks", async () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
expect(queryByDataset(tab.containerEl, "cardTypeList", "true")).toBeDefined();
expect(findElements(tab.containerEl, (element) => element.tag === "th")).toHaveLength(0);
@ -698,6 +730,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
await queryByDataset(tab.containerEl, "cardTypesRefresh", "true").trigger("click");
await flushPromises();
@ -755,6 +788,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
const markerInput = queryByDataset(tab.containerEl, "cardTypeMarker", "cloze");
markerInput.value = "#cloze-a";
@ -779,12 +813,22 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
const clozeHeading = queryByDataset(tab.containerEl, "cardTypeHeading", "cloze");
clozeHeading.value = "4";
await clozeHeading.trigger("change");
expect(plugin.settings.cardTypeConfigs.cloze.headingLevel).toBe(5);
vi.useFakeTimers();
const clozeMarker = queryByDataset(tab.containerEl, "cardTypeMarker", "cloze");
clozeMarker.value = "";
await clozeMarker.trigger("input");
vi.advanceTimersByTime(500);
await flushPromises();
vi.useRealTimers();
expect(plugin.settings.cardTypeConfigs.cloze.headingLevel).toBe(4);
expect(plugin.settings.cardTypeConfigs.cloze.extraMarker).toBe("#anki-cloze");
expect(collectTexts(tab.containerEl).some((text) => text.includes("同一个 H4 只能有一个启用的默认卡片类型"))).toBe(true);
});
@ -809,6 +853,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
const initialEmptyCount = getEmptyCallCount(tab.containerEl);
await queryByDataset(tab.containerEl, "cardTypesRefresh", "true").trigger("click");
@ -826,6 +871,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
await queryByDataset(tab.containerEl, "cardTypesRefresh", "true").trigger("click");
await flushPromises();
@ -876,6 +922,7 @@ describe("PluginSettingTab", () => {
const cachedTab = new AnkiHeadingSyncSettingTab(cachedPlugin as never);
cachedTab.display();
await expandCard(cachedTab, "card-types");
await flushPromises();
const basicNoteTypeSelect = queryByDataset(cachedTab.containerEl, "cardTypeNoteType", "basic");
@ -890,11 +937,12 @@ describe("PluginSettingTab", () => {
expect(collectTexts(cachedTab.containerEl).some((text) => text.includes("当前使用缓存的 3 个笔记模板,已选择并配置 1 个卡片模式。"))).toBe(true);
});
it("shows cache-empty status and skips background checking when there is no cached note type", () => {
it("shows cache-empty status and skips background checking when there is no cached note type", async () => {
const plugin = new FakePlugin();
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
expect(plugin.listNoteModelsCalls).toBe(0);
expect(plugin.getModelFieldNamesByModelNamesCalls).toBe(0);
@ -924,6 +972,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
await flushPromises();
expect(plugin.listNoteModelsCalls).toBe(1);
@ -955,6 +1004,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
await flushPromises();
expect(plugin.listNoteModelsCalls).toBe(1);
@ -986,6 +1036,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
await flushPromises();
expect(collectTexts(tab.containerEl).some((text) => text.includes("和本页缓存的 2 个模板不一致"))).toBe(true);
@ -1024,6 +1075,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
const basicNoteTypeSelect = queryByDataset(tab.containerEl, "cardTypeNoteType", "basic");
basicNoteTypeSelect.value = "Basic";
@ -1060,6 +1112,7 @@ describe("PluginSettingTab", () => {
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
expect(queryByDataset(tab.containerEl, "qaGroupWarning", "qa-group").textContent).toContain("第 02 组缺少答案字段");
const acceptWarning = queryByDataset(tab.containerEl, "qaGroupWarningAccept", "qa-group");

View file

@ -67,7 +67,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
private readonly debouncedTextSaves = new Map<string, ReturnType<typeof setTimeout>>();
private readonly textDraftValues = new Map<string, string>();
private readonly cardShells = new Map<SettingsCardId, SettingsCardShell>();
private readonly expandedCardIds = new Set<SettingsCardId>(["card-types", "commands"]);
private readonly expandedCardIds = new Set<SettingsCardId>();
private readonly expandedFolderPaths = new Set<string>();
private folderTree: FolderTreeNode[] = [];