Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 334 additions & 1 deletion src/engine/TemplateChoiceEngine.notice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ vi.mock("obsidian-dataview", () => ({
getAPI: vi.fn(),
}));

import { TFile, type App } from "obsidian";
import { TFile, TFolder, type App } from "obsidian";
import { Notice } from "obsidian";
import { TemplateChoiceEngine } from "./TemplateChoiceEngine";
import type { IChoiceExecutor } from "../IChoiceExecutor";
Expand Down Expand Up @@ -161,6 +161,7 @@ const createEngine = (
},
getAbstractFileByPath: vi.fn(),
getFiles: vi.fn(() => []),
createFolder: vi.fn(),
create: vi.fn(),
modify: vi.fn(),
},
Expand Down Expand Up @@ -332,3 +333,335 @@ describe("TemplateChoiceEngine file casing resolution", () => {
);
});
});

describe("TemplateChoiceEngine destination path resolution", () => {
beforeEach(() => {
settingsStore.setState(structuredClone(defaultSettingsState));
formatFileNameMock.mockReset();
formatFileContentMock.mockReset();
formatFileContentMock.mockResolvedValue("");
});

it("treats deep slash-separated filename formats as vault-relative paths", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce(
"03_Aufgabenmanagement/ToDos/Issue1116",
);
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "03_Aufgabenmanagement/ToDos/W-Tanso",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockImplementation(
(path: string) => {
if (path === "03_Aufgabenmanagement") {
const folder = new TFolder();
folder.path = "03_Aufgabenmanagement";
folder.name = "03_Aufgabenmanagement";
return folder;
}
return null;
},
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"03_Aufgabenmanagement/ToDos/Issue1116.md",
engine.choice.templatePath,
);
});

it("treats leading-slash filename formats as vault-relative paths", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce("/Projects/Issue1116");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "03_Aufgabenmanagement/ToDos/W-Tanso",
});

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"Projects/Issue1116.md",
engine.choice.templatePath,
);
});

it("does not drop default-folder prefix after duplicate-prefix stripping", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce("projects/docs/readme");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "projects",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockReturnValue(
null,
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"projects/docs/readme.md",
engine.choice.templatePath,
);
});

it("keeps Obsidian default location behavior for plain file names", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:name}}";

formatFileNameMock.mockResolvedValueOnce("Issue1116");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "03_Aufgabenmanagement/ToDos/W-Tanso",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockReturnValue(
null,
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"03_Aufgabenmanagement/ToDos/W-Tanso/Issue1116.md",
engine.choice.templatePath,
);
});

it("keeps relative subpaths under the default location when the first segment does not exist at vault root", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce("tasks/Issue1116");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "03_Aufgabenmanagement/ToDos/W-Tanso",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockImplementation(
(path: string) => {
if (path === "tasks") return null;
return null;
},
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"03_Aufgabenmanagement/ToDos/W-Tanso/tasks/Issue1116.md",
engine.choice.templatePath,
);
});

it("keeps deep relative subpaths under the default location when root segment is missing", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce("sub/tasks/Issue1116");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "03_Aufgabenmanagement/ToDos/W-Tanso",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockReturnValue(
null,
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"03_Aufgabenmanagement/ToDos/W-Tanso/sub/tasks/Issue1116.md",
engine.choice.templatePath,
);
});

it("does not treat root-level files as folder roots for vault-relative detection", async () => {
const { engine, app } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);

engine.choice.folder.enabled = false;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";

formatFileNameMock.mockResolvedValueOnce("notes/Session");
(app.fileManager.getNewFileParent as ReturnType<typeof vi.fn>).mockReturnValue({
path: "DailyNotes",
});
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockImplementation(
(path: string) => {
if (path === "notes") {
const file = new TFile();
file.path = "notes";
file.name = "notes";
file.basename = "notes";
file.extension = "";
return file;
}
return null;
},
);

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"DailyNotes/notes/Session.md",
engine.choice.templatePath,
);
});

it("never treats filename formats as vault-relative when create in folder is enabled", async () => {
const { engine } = createEngine("ignored", {
throwDuringFileName: false,
stubTemplateContent: true,
});
const createdFile = new TFile();
const createSpy = vi
.spyOn(
engine as unknown as {
createFileWithTemplate: (
filePath: string,
templatePath: string,
) => Promise<TFile | null>;
},
"createFileWithTemplate",
)
.mockResolvedValue(createdFile);
vi.spyOn(
engine as unknown as {
getFolderPath: () => Promise<string>;
},
"getFolderPath",
).mockResolvedValue("ConfiguredFolder");

engine.choice.folder.enabled = true;
engine.choice.fileNameFormat.enabled = true;
engine.choice.fileNameFormat.format = "{{VALUE:path}}";
formatFileNameMock.mockResolvedValueOnce("RootLike/Path/Issue1116");

await engine.run();

expect(createSpy).toHaveBeenCalledWith(
"ConfiguredFolder/RootLike/Path/Issue1116.md",
engine.choice.templatePath,
);
});
});
Loading