vicky469_aside/tests/commentEntryController.test.ts
vicky469 e94254ee2d feat(publish): add local Pages publishing
Add experimental Cloudflare Pages publishing through local Wrangler, pane-header publish actions, public HTML preview support, artifact safety checks, and state cleanup for rename/delete/unpublish flows.
2026-07-09 15:51:02 +08:00

366 lines
13 KiB
TypeScript

import * as assert from "node:assert/strict";
import test from "node:test";
import type { Editor, TFile } from "obsidian";
import type { Comment } from "../src/commentManager";
import { getPageCommentLabel } from "../src/core/anchors/commentAnchors";
import { CommentEntryController, type CommentEntryHost } from "../src/comments/commentEntryController";
import type { DraftComment, DraftSelection } from "../src/domain/drafts";
const ALL_COMMENTS_NOTE_PATH = "Aside/index.md";
function createFile(path: string): TFile {
return {
path,
basename: path.split("/").pop()?.replace(/\.[^.]+$/, "") ?? path,
extension: path.split(".").pop() ?? "",
} as TFile;
}
function createEditor(
selectedText: string,
from = { line: 2, ch: 4 },
to = { line: 2, ch: 8 },
): Editor {
return {
getSelection: () => selectedText,
getCursor: (which?: "from" | "to") => (which === "to" ? to : from),
} as unknown as Editor;
}
function isCommentableFilePath(path: string): boolean {
if (path === ALL_COMMENTS_NOTE_PATH) {
return false;
}
return path.endsWith(".md");
}
function isPageNoteCapableFilePath(path: string): boolean {
if (path === ALL_COMMENTS_NOTE_PATH) {
return false;
}
return path.endsWith(".md") || path.endsWith(".pdf") || path.endsWith(".html");
}
function createHost(options: { knownComments?: Comment[]; threadIdsByCommentId?: Record<string, string> } = {}) {
const draftCalls: Array<{
draft: DraftComment | null;
hostFilePath?: string | null;
skipCommentViewRefresh?: boolean;
refreshEditorDecorations?: boolean;
}> = [];
const loadedFiles: string[] = [];
const markedFiles: string[] = [];
const highlightedCommentIds: string[] = [];
const orphanedCommentIds: string[] = [];
const notices: string[] = [];
const knownComments = new Map((options.knownComments ?? []).map((comment) => [comment.id, comment]));
const host = {
getAllCommentsNotePath: () => ALL_COMMENTS_NOTE_PATH,
getFileByPath: (filePath) => createFile(filePath),
isCommentableFile: (file): file is TFile => !!file && isCommentableFilePath(file.path),
isPageNoteCapableFile: (file): file is TFile => !!file && isPageNoteCapableFilePath(file.path),
loadCommentsForFile: async (file) => {
loadedFiles.push(file.path);
},
getKnownCommentById: (commentId) => knownComments.get(commentId) ?? null,
getKnownThreadIdByCommentId: (commentId) => options.threadIdsByCommentId?.[commentId] ?? null,
markDraftFileActive: (file) => {
markedFiles.push(file.path);
},
setDraftComment: async (draft, hostFilePath, setDraftOptions) => {
draftCalls.push({
draft,
hostFilePath,
skipCommentViewRefresh: setDraftOptions?.skipCommentViewRefresh,
refreshEditorDecorations: setDraftOptions?.refreshEditorDecorations,
});
},
activateViewAndHighlightComment: async (commentId) => {
highlightedCommentIds.push(commentId);
},
getCommentsForFile: (filePath) =>
(options.knownComments ?? []).filter((comment) => comment.filePath === filePath),
orphanCommentThreadAnchor: async (commentId) => {
orphanedCommentIds.push(commentId);
return true;
},
createCommentId: () => "comment-1",
showNotice: (message) => {
notices.push(message);
},
} as CommentEntryHost & {
getCommentsForFile(filePath: string): Comment[];
orphanCommentThreadAnchor(commentId: string): Promise<boolean>;
};
return {
controller: new CommentEntryController(host),
draftCalls,
loadedFiles,
markedFiles,
highlightedCommentIds,
orphanedCommentIds,
notices,
};
}
test("comment entry controller starts a draft from editor selection", async () => {
const host = createHost();
const file = createFile("docs/architecture.md");
const started = await host.controller.startDraftFromEditorSelection(createEditor("beta"), file);
assert.equal(started, true);
assert.deepEqual(host.loadedFiles, []);
assert.deepEqual(host.markedFiles, [file.path]);
assert.equal(host.draftCalls.length, 1);
assert.equal(host.highlightedCommentIds.length, 1);
const draft = host.draftCalls[0].draft;
assert.ok(draft);
assert.equal(draft.id, "comment-1");
assert.equal(draft.filePath, file.path);
assert.equal(draft.selectedText, "beta");
assert.equal(draft.selectedTextHash, "");
assert.equal(draft.startLine, 2);
assert.equal(draft.startChar, 4);
assert.equal(draft.endLine, 2);
assert.equal(draft.endChar, 8);
assert.equal(draft.anchorKind, "selection");
assert.equal(host.draftCalls[0].hostFilePath, file.path);
assert.equal(host.draftCalls[0].skipCommentViewRefresh, true);
assert.equal(host.draftCalls[0].refreshEditorDecorations, undefined);
assert.deepEqual(host.highlightedCommentIds, ["comment-1"]);
assert.deepEqual(host.notices, []);
});
test("comment entry controller orphans an existing anchor when the editor selection matches it", async () => {
const file = createFile("docs/architecture.md");
const existingComment: Comment = {
id: "comment-existing",
filePath: file.path,
startLine: 2,
startChar: 4,
endLine: 2,
endChar: 8,
selectedText: "beta",
selectedTextHash: "hash:beta",
comment: "Existing note",
timestamp: 123,
anchorKind: "selection",
orphaned: false,
};
const host = createHost({ knownComments: [existingComment] });
const started = await host.controller.startDraftFromEditorSelection(
createEditor("beta", { line: 2, ch: 4 }, { line: 2, ch: 8 }),
file,
);
assert.equal(started, true);
assert.deepEqual(host.orphanedCommentIds, ["comment-existing"]);
assert.deepEqual(host.draftCalls, []);
assert.deepEqual(host.highlightedCommentIds, []);
assert.deepEqual(host.notices, []);
});
test("comment entry controller rejects empty editor selections", async () => {
const host = createHost();
const file = createFile("docs/architecture.md");
const started = await host.controller.startDraftFromEditorSelection(createEditor(" "), file);
assert.equal(started, false);
assert.deepEqual(host.loadedFiles, []);
assert.equal(host.draftCalls.length, 0);
assert.deepEqual(host.notices, ["Please select some text to add a comment."]);
});
test("comment entry controller rejects text-anchored drafts for non-markdown files", async () => {
const host = createHost();
const file = createFile("docs/diagram.pdf");
const started = await host.controller.startDraftFromEditorSelection(createEditor("label"), file);
assert.equal(started, false);
assert.deepEqual(host.loadedFiles, []);
assert.equal(host.draftCalls.length, 0);
assert.deepEqual(host.notices, ["Text-anchored side notes are only supported in Markdown files."]);
});
test("comment entry controller rejects text-anchored drafts for rendered HTML selections", async () => {
const host = createHost();
const file = createFile("public/page.html");
const selection: DraftSelection = {
file,
selectedText: "Revenue growth",
startLine: 3,
startChar: 5,
endLine: 3,
endChar: 19,
};
const started = await host.controller.startDraftFromResolvedSelection(selection);
assert.equal(started, false);
assert.deepEqual(host.markedFiles, []);
assert.equal(host.draftCalls.length, 0);
assert.deepEqual(host.highlightedCommentIds, []);
assert.deepEqual(host.notices, ["Text-anchored side notes are only supported in Markdown files."]);
});
test("comment entry controller starts page drafts for PDF files", async () => {
const host = createHost();
const file = createFile("docs/diagram.pdf");
const started = await host.controller.startPageCommentDraft(file);
assert.equal(started, true);
assert.deepEqual(host.loadedFiles, []);
assert.deepEqual(host.markedFiles, [file.path]);
assert.equal(host.draftCalls.length, 1);
assert.deepEqual(host.highlightedCommentIds, ["comment-1"]);
const draft = host.draftCalls[0].draft;
assert.ok(draft);
assert.equal(draft.anchorKind, "page");
assert.equal(draft.filePath, file.path);
assert.equal(draft.selectedText, getPageCommentLabel(file.path));
assert.equal(draft.selectedTextHash, "");
assert.equal(host.draftCalls[0].skipCommentViewRefresh, true);
assert.equal(host.draftCalls[0].refreshEditorDecorations, false);
assert.deepEqual(host.notices, []);
});
test("comment entry controller starts page drafts for markdown files", async () => {
const host = createHost();
const file = createFile("docs/architecture.md");
const started = await host.controller.startPageCommentDraft(file);
assert.equal(started, true);
assert.deepEqual(host.loadedFiles, []);
assert.deepEqual(host.markedFiles, [file.path]);
assert.equal(host.draftCalls.length, 1);
assert.deepEqual(host.highlightedCommentIds, ["comment-1"]);
const draft = host.draftCalls[0].draft;
assert.ok(draft);
assert.equal(draft.anchorKind, "page");
assert.equal(draft.selectedText, getPageCommentLabel(file.path));
assert.equal(draft.selectedTextHash, "");
assert.equal(host.draftCalls[0].skipCommentViewRefresh, true);
assert.equal(host.draftCalls[0].refreshEditorDecorations, false);
assert.deepEqual(host.notices, []);
});
test("comment entry controller starts an append-entry draft using the resolved vault file", async () => {
const existingComment: Comment = {
id: "thread-1",
filePath: "docs/architecture.md",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 8,
selectedText: "system",
selectedTextHash: "hash:system",
comment: "existing body",
timestamp: 123,
anchorKind: "selection",
orphaned: false,
};
const host = createHost({ knownComments: [existingComment] });
const started = await host.controller.startAppendEntryDraft("thread-1", "docs/host.md");
assert.equal(started, true);
assert.deepEqual(host.loadedFiles, [existingComment.filePath]);
assert.deepEqual(host.markedFiles, [existingComment.filePath]);
assert.deepEqual(host.highlightedCommentIds, ["comment-1"]);
assert.equal(host.draftCalls.length, 1);
const draft = host.draftCalls[0].draft;
assert.ok(draft);
assert.equal(draft.id, "comment-1");
assert.equal(draft.threadId, "thread-1");
assert.equal(draft.appendAfterCommentId, "thread-1");
assert.equal(draft.mode, "append");
assert.equal(draft.filePath, existingComment.filePath);
assert.equal(draft.comment, "");
assert.equal(host.draftCalls[0].hostFilePath, "docs/host.md");
assert.equal(host.draftCalls[0].skipCommentViewRefresh, true);
assert.deepEqual(host.notices, []);
});
test("comment entry controller rejects append-entry drafts for rendered HTML selection comments", async () => {
const existingComment: Comment = {
id: "thread-1",
filePath: "public/page.html",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 8,
selectedText: "system",
selectedTextHash: "hash:system",
comment: "existing body",
timestamp: 123,
anchorKind: "selection",
orphaned: false,
};
const host = createHost({ knownComments: [existingComment] });
const started = await host.controller.startAppendEntryDraft("thread-1", "public/page.html");
assert.equal(started, false);
assert.deepEqual(host.loadedFiles, []);
assert.deepEqual(host.markedFiles, []);
assert.deepEqual(host.highlightedCommentIds, []);
assert.equal(host.draftCalls.length, 0);
assert.deepEqual(host.notices, ["Unable to find that side note thread."]);
});
test("comment entry controller can start an append-entry draft from a child entry id", async () => {
const childComment: Comment = {
id: "entry-2",
filePath: "docs/architecture.md",
startLine: 4,
startChar: 2,
endLine: 4,
endChar: 8,
selectedText: "system",
selectedTextHash: "hash:system",
comment: "child body",
timestamp: 456,
anchorKind: "selection",
orphaned: false,
};
const host = createHost({
knownComments: [childComment],
threadIdsByCommentId: {
"entry-2": "thread-1",
},
});
const started = await host.controller.startAppendEntryDraft("entry-2", "docs/host.md");
assert.equal(started, true);
assert.deepEqual(host.loadedFiles, [childComment.filePath]);
assert.deepEqual(host.markedFiles, [childComment.filePath]);
assert.deepEqual(host.highlightedCommentIds, ["comment-1"]);
assert.equal(host.draftCalls.length, 1);
const draft = host.draftCalls[0].draft;
assert.ok(draft);
assert.equal(draft.id, "comment-1");
assert.equal(draft.threadId, "thread-1");
assert.equal(draft.appendAfterCommentId, "entry-2");
assert.equal(draft.mode, "append");
assert.equal(draft.filePath, childComment.filePath);
assert.equal(draft.comment, "");
assert.equal(host.draftCalls[0].hostFilePath, "docs/host.md");
assert.equal(host.draftCalls[0].skipCommentViewRefresh, true);
assert.deepEqual(host.notices, []);
});