mirror of
https://github.com/vicky469/aside.git
synced 2026-07-22 07:01:57 +00:00
Removes the resolved/unresolve comment feature entirely. Comments are now always visible — there is no archived state or toggle. - Removes `resolved?: boolean` from CommentThread, Comment, and all storage/normalization paths - Removes resolveComment/unresolveComment from CommentManager, CommentMutationController, and public plugin API - Removes shouldShowResolvedComments/setShowResolvedComments from session, mutation, and persistence controllers - Removes sidebar resolve button, toolbar chip, and resolved empty states from AsideView - Removes setThreadResolved sync event generation; keeps the op in the union type as a backward-compatible no-op so old stored events parse without error - Deletes resolvedCommentVisibility.ts, commentSelectionVisibility.ts, and resolve-note-comment.mjs scripts - Updates README to remove resolved feature mentions - Updates all tests accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import * as assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import type { Comment } from "../src/commentManager";
|
|
import type { DraftComment } from "../src/domain/drafts";
|
|
import {
|
|
SidebarDraftEditorController,
|
|
estimateDraftTextareaRows,
|
|
getSidebarComments,
|
|
} from "../src/ui/views/sidebarDraftEditor";
|
|
import {
|
|
computePinnedDraftScrollTop,
|
|
pinDraftToTopOnMobile,
|
|
} from "../src/ui/views/sidebarDraftComment";
|
|
|
|
function createComment(overrides: Partial<Comment> = {}): Comment {
|
|
return {
|
|
id: overrides.id ?? "comment-1",
|
|
filePath: overrides.filePath ?? "docs/architecture.md",
|
|
startLine: overrides.startLine ?? 5,
|
|
startChar: overrides.startChar ?? 1,
|
|
endLine: overrides.endLine ?? 5,
|
|
endChar: overrides.endChar ?? 8,
|
|
selectedText: overrides.selectedText ?? "comment",
|
|
selectedTextHash: overrides.selectedTextHash ?? "hash:comment",
|
|
comment: overrides.comment ?? "Comment body",
|
|
timestamp: overrides.timestamp ?? 100,
|
|
anchorKind: overrides.anchorKind ?? "selection",
|
|
orphaned: overrides.orphaned ?? false,
|
|
};
|
|
}
|
|
|
|
function createDraft(overrides: Partial<DraftComment> = {}): DraftComment {
|
|
return {
|
|
...createComment(overrides),
|
|
mode: overrides.mode ?? "new",
|
|
};
|
|
}
|
|
|
|
function createDraftEditorController() {
|
|
return new SidebarDraftEditorController({
|
|
getAllIndexedComments: () => [],
|
|
updateDraftCommentText: () => {},
|
|
renderComments: async () => {},
|
|
scheduleDraftFocus: () => {},
|
|
openLinkSuggestModal: () => {},
|
|
openTagSuggestModal: () => {},
|
|
});
|
|
}
|
|
|
|
test("getSidebarComments replaces the persisted version of the draft and sorts consistently", () => {
|
|
const persistedComments = [
|
|
createComment({ id: "comment-b", filePath: "docs/b.md", startLine: 8, timestamp: 300 }),
|
|
createComment({ id: "draft-1", filePath: "docs/b.md", startLine: 12, timestamp: 400 }),
|
|
createComment({ id: "comment-page", filePath: "docs/a.md", anchorKind: "page", startLine: 20, startChar: 0, endLine: 20, endChar: 0, timestamp: 150 }),
|
|
createComment({ id: "comment-a", filePath: "docs/a.md", startLine: 3, timestamp: 200 }),
|
|
];
|
|
const draft = createDraft({
|
|
id: "draft-1",
|
|
filePath: "docs/b.md",
|
|
startLine: 12,
|
|
timestamp: 500,
|
|
comment: "Draft body",
|
|
});
|
|
|
|
const comments = getSidebarComments(persistedComments, draft);
|
|
|
|
assert.deepEqual(comments.map((comment) => ({
|
|
id: comment.id,
|
|
filePath: comment.filePath,
|
|
timestamp: comment.timestamp,
|
|
isDraft: "mode" in comment,
|
|
})), [
|
|
{ id: "comment-page", filePath: "docs/a.md", timestamp: 150, isDraft: false },
|
|
{ id: "comment-a", filePath: "docs/a.md", timestamp: 200, isDraft: false },
|
|
{ id: "comment-b", filePath: "docs/b.md", timestamp: 300, isDraft: false },
|
|
{ id: "draft-1", filePath: "docs/b.md", timestamp: 500, isDraft: true },
|
|
]);
|
|
});
|
|
|
|
test("getSidebarComments applies file filters to both persisted comments and drafts", () => {
|
|
const persistedComments = [
|
|
createComment({ id: "comment-a", filePath: "docs/a.md", timestamp: 100 }),
|
|
createComment({ id: "comment-b", filePath: "docs/b.md", timestamp: 200 }),
|
|
];
|
|
const draft = createDraft({
|
|
id: "draft-1",
|
|
filePath: "docs/c.md",
|
|
timestamp: 300,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
getSidebarComments(persistedComments, draft, ["docs/b.md"]).map((comment) => comment.id),
|
|
["comment-b"],
|
|
);
|
|
assert.deepEqual(
|
|
getSidebarComments(persistedComments, draft, ["docs/c.md"]).map((comment) => comment.id),
|
|
["draft-1"],
|
|
);
|
|
});
|
|
|
|
test("estimateDraftTextareaRows keeps draft editors within their intended bounds", () => {
|
|
assert.equal(estimateDraftTextareaRows("Short", false), 2);
|
|
assert.equal(estimateDraftTextareaRows("Short", true), 2);
|
|
|
|
const longLine = "x".repeat(2_000);
|
|
assert.equal(estimateDraftTextareaRows(longLine, false), 10);
|
|
assert.equal(estimateDraftTextareaRows(longLine, true), 18);
|
|
});
|
|
|
|
test("computePinnedDraftScrollTop only scrolls enough to keep the draft visible", () => {
|
|
assert.equal(computePinnedDraftScrollTop(120, 260, 460, 40, 420), 168);
|
|
assert.equal(computePinnedDraftScrollTop(120, 24, 224, 40, 420), 96);
|
|
assert.equal(computePinnedDraftScrollTop(120, 80, 280, 40, 420), 120);
|
|
assert.equal(computePinnedDraftScrollTop(0, 4, 204, 20, 420), 0);
|
|
});
|
|
|
|
test("computePinnedDraftScrollTop keeps draft actions above floating bottom controls", () => {
|
|
assert.equal(computePinnedDraftScrollTop(220, 660, 835, 143, 843, 777), 286);
|
|
});
|
|
|
|
test("pinDraftToTopOnMobile performs a minimal scroll adjustment when the draft is offscreen", () => {
|
|
const originalHTMLElement = globalThis.HTMLElement;
|
|
class FakeElement {}
|
|
Object.assign(globalThis, {
|
|
HTMLElement: FakeElement,
|
|
});
|
|
|
|
try {
|
|
const scrollCalls: Array<{ top: number; behavior: string }> = [];
|
|
const scrollContainer = Object.assign(new FakeElement(), {
|
|
scrollTop: 120,
|
|
getBoundingClientRect: () => ({ top: 40, bottom: 420 }),
|
|
scrollTo: (options: { top: number; behavior: string }) => {
|
|
scrollCalls.push(options);
|
|
},
|
|
}) as unknown as HTMLElement;
|
|
const draftEl = Object.assign(new FakeElement(), {
|
|
getBoundingClientRect: () => ({ top: 260, bottom: 460 }),
|
|
}) as unknown as HTMLElement;
|
|
const textarea = {
|
|
closest: (selector: string) => {
|
|
if (selector === ".aside-comment-draft") {
|
|
return draftEl;
|
|
}
|
|
if (selector === ".aside-view-container") {
|
|
return scrollContainer;
|
|
}
|
|
return null;
|
|
},
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
pinDraftToTopOnMobile(textarea);
|
|
|
|
assert.deepEqual(scrollCalls, [{
|
|
top: 168,
|
|
behavior: "auto",
|
|
}]);
|
|
} finally {
|
|
Object.assign(globalThis, {
|
|
HTMLElement: originalHTMLElement,
|
|
});
|
|
}
|
|
});
|
|
|
|
test("pinDraftToTopOnMobile skips no-op scroll corrections", () => {
|
|
const originalHTMLElement = globalThis.HTMLElement;
|
|
class FakeElement {}
|
|
Object.assign(globalThis, {
|
|
HTMLElement: FakeElement,
|
|
});
|
|
|
|
try {
|
|
const scrollCalls: Array<{ top: number; behavior: string }> = [];
|
|
const scrollContainer = Object.assign(new FakeElement(), {
|
|
scrollTop: 120,
|
|
getBoundingClientRect: () => ({ top: 40, bottom: 420 }),
|
|
scrollTo: (options: { top: number; behavior: string }) => {
|
|
scrollCalls.push(options);
|
|
},
|
|
}) as unknown as HTMLElement;
|
|
const draftEl = Object.assign(new FakeElement(), {
|
|
getBoundingClientRect: () => ({ top: 80, bottom: 280 }),
|
|
}) as unknown as HTMLElement;
|
|
const textarea = {
|
|
closest: (selector: string) => {
|
|
if (selector === ".aside-comment-draft") {
|
|
return draftEl;
|
|
}
|
|
if (selector === ".aside-view-container") {
|
|
return scrollContainer;
|
|
}
|
|
return null;
|
|
},
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
pinDraftToTopOnMobile(textarea);
|
|
|
|
assert.deepEqual(scrollCalls, []);
|
|
} finally {
|
|
Object.assign(globalThis, {
|
|
HTMLElement: originalHTMLElement,
|
|
});
|
|
}
|
|
});
|
|
|
|
test("sidebar draft editor controller applies bold formatting directly", () => {
|
|
const dispatchedEvents: string[] = [];
|
|
const controller = createDraftEditorController();
|
|
const textarea = {
|
|
value: "hello world",
|
|
selectionStart: 6,
|
|
selectionEnd: 11,
|
|
dispatchEvent: (event: Event) => {
|
|
dispatchedEvents.push(event.type);
|
|
return true;
|
|
},
|
|
setSelectionRange(start: number, end: number) {
|
|
textarea.selectionStart = start;
|
|
textarea.selectionEnd = end;
|
|
},
|
|
rows: 4,
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
controller.applyDraftBold("draft-1", textarea, false);
|
|
|
|
assert.equal(textarea.value, "hello **world**");
|
|
assert.equal(textarea.selectionStart, 8);
|
|
assert.equal(textarea.selectionEnd, 13);
|
|
assert.deepEqual(dispatchedEvents, ["input"]);
|
|
});
|
|
|
|
test("sidebar draft editor controller applies highlight formatting directly", () => {
|
|
const dispatchedEvents: string[] = [];
|
|
const controller = createDraftEditorController();
|
|
const textarea = {
|
|
value: "hello world",
|
|
selectionStart: 6,
|
|
selectionEnd: 11,
|
|
dispatchEvent: (event: Event) => {
|
|
dispatchedEvents.push(event.type);
|
|
return true;
|
|
},
|
|
setSelectionRange(start: number, end: number) {
|
|
textarea.selectionStart = start;
|
|
textarea.selectionEnd = end;
|
|
},
|
|
rows: 4,
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
controller.applyDraftHighlight("draft-1", textarea, false);
|
|
|
|
assert.equal(textarea.value, "hello ==world==");
|
|
assert.equal(textarea.selectionStart, 8);
|
|
assert.equal(textarea.selectionEnd, 13);
|
|
assert.deepEqual(dispatchedEvents, ["input"]);
|
|
});
|
|
|
|
test("sidebar draft editor controller continues markdown lists directly", () => {
|
|
const dispatchedEvents: string[] = [];
|
|
const controller = createDraftEditorController();
|
|
const textarea = {
|
|
value: "- first item",
|
|
selectionStart: 12,
|
|
selectionEnd: 12,
|
|
dispatchEvent: (event: Event) => {
|
|
dispatchedEvents.push(event.type);
|
|
return true;
|
|
},
|
|
setSelectionRange(start: number, end: number) {
|
|
textarea.selectionStart = start;
|
|
textarea.selectionEnd = end;
|
|
},
|
|
rows: 2,
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
const applied = controller.applyDraftListContinuation("draft-1", textarea, false);
|
|
|
|
assert.equal(applied, true);
|
|
assert.equal(textarea.value, "- first item\n- ");
|
|
assert.equal(textarea.selectionStart, 15);
|
|
assert.equal(textarea.selectionEnd, 15);
|
|
assert.deepEqual(dispatchedEvents, ["input"]);
|
|
});
|
|
|
|
test("sidebar draft editor controller leaves non-list enter handling native", () => {
|
|
const dispatchedEvents: string[] = [];
|
|
const controller = createDraftEditorController();
|
|
const textarea = {
|
|
value: "plain paragraph",
|
|
selectionStart: 15,
|
|
selectionEnd: 15,
|
|
dispatchEvent: (event: Event) => {
|
|
dispatchedEvents.push(event.type);
|
|
return true;
|
|
},
|
|
setSelectionRange(start: number, end: number) {
|
|
textarea.selectionStart = start;
|
|
textarea.selectionEnd = end;
|
|
},
|
|
rows: 2,
|
|
} as unknown as HTMLTextAreaElement;
|
|
|
|
const applied = controller.applyDraftListContinuation("draft-1", textarea, false);
|
|
|
|
assert.equal(applied, false);
|
|
assert.equal(textarea.value, "plain paragraph");
|
|
assert.equal(textarea.selectionStart, 15);
|
|
assert.equal(textarea.selectionEnd, 15);
|
|
assert.deepEqual(dispatchedEvents, []);
|
|
});
|