mirror of
https://github.com/vicky469/aside.git
synced 2026-07-22 17:42:04 +00:00
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.
366 lines
13 KiB
TypeScript
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, []);
|
|
});
|