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 { SidebarInteractionController } from "../src/ui/views/sidebarInteractionController"; function createComment(overrides: Partial = {}): Comment { return { id: overrides.id ?? "comment-1", filePath: overrides.filePath ?? "docs/architecture.md", startLine: overrides.startLine ?? 4, startChar: overrides.startChar ?? 1, endLine: overrides.endLine ?? 4, endChar: overrides.endChar ?? 7, 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 { return { ...createComment(overrides), mode: overrides.mode ?? "edit", threadId: overrides.threadId, appendAfterCommentId: overrides.appendAfterCommentId, }; } class FakeNode {} class FakeSidebarElement extends FakeNode { public parentElement: FakeSidebarElement | null = null; public addClassCalls: string[] = []; public removeClassCalls: string[] = []; public scrollCalls = 0; constructor( private readonly options: { isCommentItem?: boolean; isDismissalExemptSurface?: boolean; isSectionChrome?: boolean; isToolbar?: boolean; isDeletedToolbarMode?: boolean; } = {}, ) { super(); } public closest(selector: string): FakeSidebarElement | null { if (selector === ".aside-comment-item") { return this.options.isCommentItem ? this : null; } if (selector === ".aside-comments-list-actions, .aside-sidebar-toolbar, .aside-active-file-filters") { return this.options.isSectionChrome ? this : null; } if (selector === ".aside-sidebar-toolbar") { return this.options.isToolbar ? this : null; } if (selector === ".aside-sidebar-toolbar.is-deleted-toolbar-mode") { return this.options.isToolbar && this.options.isDeletedToolbarMode ? this : null; } if (selector === ".suggestion-container, .modal-container, .prompt, .menu") { return this.options.isDismissalExemptSurface ? this : null; } return null; } public contains(target: unknown): boolean { return target === this; } public getAttribute(_name: string): string | null { return null; } public addClass(name: string): void { this.addClassCalls.push(name); } public removeClass(name: string): void { this.removeClassCalls.push(name); } public scrollIntoView(_options: unknown): void { this.scrollCalls += 1; } } type FakeCommentElement = { addClassCalls: string[]; removeClassCalls: string[]; scrollCalls: number; dataCommentId?: string; dataDraftId?: string; getAttribute(name: string): string | null; addClass(name: string): void; removeClass(name: string): void; scrollIntoView(options: unknown): void; }; function createCommentElement(ids: { commentId?: string; draftId?: string } = {}): FakeCommentElement { return { addClassCalls: [], removeClassCalls: [], scrollCalls: 0, dataCommentId: ids.commentId, dataDraftId: ids.draftId, getAttribute(name: string) { if (name === "data-comment-id") { return this.dataCommentId ?? null; } if (name === "data-draft-id") { return this.dataDraftId ?? null; } return null; }, addClass(name: string) { this.addClassCalls.push(name); }, removeClass(name: string) { this.removeClassCalls.push(name); }, scrollIntoView(_options: unknown) { this.scrollCalls += 1; }, }; } function createHarness() { const matchingCommentEl = createCommentElement({ commentId: "comment-1" }); const otherActiveEl = createCommentElement({ commentId: "comment-2" }); const htmlFile = { path: "tmp.recruitment.html", basename: "tmp.recruitment", extension: "html", }; let renderCalls = 0; const revealedComments: string[] = []; const openedCommentTargets: Array<{ filePath: string | null; commentId: string }> = []; const openedLinks: Array<{ href: string; sourcePath: string }> = []; const openedFiles: Array<{ path: string; active: boolean }> = []; const leafRequests: unknown[] = []; const htmlLeaf = { openFile: async ( file: { path: string }, openState?: { active?: boolean }, ) => { openedFiles.push({ path: file.path, active: openState?.active === true, }); }, }; const controller = new SidebarInteractionController({ app: { vault: { getName: () => "dev", getAbstractFileByPath: (path: string) => path === htmlFile.path ? htmlFile : null, }, metadataCache: { getFirstLinkpathDest: (linkpath: string, sourcePath: string) => linkpath === htmlFile.path && sourcePath === "chat-01.md" ? htmlFile : null, }, workspace: { activeLeaf: null, getLeaf: (request: unknown) => { leafRequests.push(request); return htmlLeaf; }, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => { if (selector.includes("comment-1")) { return matchingCommentEl; } return null; }, querySelectorAll: () => [otherActiveEl], contains: () => true, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => { renderCalls += 1; }, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async (comment) => { revealedComments.push(comment.id); }, openCommentById: async (filePath, commentId) => { openedCommentTargets.push({ filePath, commentId }); }, getPreferredFileLeaf: () => null, openLinkText: async (href, sourcePath) => { openedLinks.push({ href, sourcePath }); }, }); return { controller, matchingCommentEl, otherActiveEl, getRenderCalls: () => renderCalls, revealedComments, openedCommentTargets, openedLinks, openedFiles, leafRequests, }; } test("sidebar interaction controller highlights a comment by rerendering and scrolling it into view", async () => { const harness = createHarness(); harness.controller.highlightComment("comment-1"); await Promise.resolve(); assert.equal(harness.controller.getActiveCommentId(), "comment-1"); assert.equal(harness.getRenderCalls(), 1); assert.equal(harness.matchingCommentEl.scrollCalls, 1); }); test("sidebar interaction controller opens a comment by marking it active and revealing it", async () => { const harness = createHarness(); await harness.controller.openCommentInEditor(createComment({ id: "comment-1" })); assert.equal(harness.controller.getActiveCommentId(), "comment-1"); assert.deepEqual(harness.revealedComments, ["comment-1"]); assert.deepEqual(harness.otherActiveEl.removeClassCalls, ["active"]); assert.deepEqual(harness.matchingCommentEl.addClassCalls, ["active"]); }); test("sidebar interaction controller routes local side note protocol links through comment open flow", async () => { const harness = createHarness(); await harness.controller.openSidebarInternalLink( "obsidian://aside-comment?vault=dev&file=docs%2Ftarget.md&commentId=comment-9", "docs/source.md", {} as HTMLElement, ); assert.deepEqual(harness.openedCommentTargets, [{ filePath: "docs/target.md", commentId: "comment-9", }]); assert.deepEqual(harness.openedLinks, []); }); test("sidebar interaction controller opens local html comment links in an Obsidian tab", async () => { const harness = createHarness(); await harness.controller.openSidebarInternalLink( "tmp.recruitment.html", "chat-01.md", {} as HTMLElement, { openAsTab: true }, ); assert.deepEqual(harness.openedFiles, [{ path: "tmp.recruitment.html", active: true, }]); assert.deepEqual(harness.leafRequests, ["tab"]); assert.deepEqual(harness.openedLinks, []); }); test("sidebar interaction controller marks the targeted comment active", () => { const harness = createHarness(); harness.controller.setActiveComment("comment-1"); assert.equal(harness.controller.getActiveCommentId(), "comment-1"); }); test("sidebar interaction controller scrolls the rerendered draft card instead of a stale pre-render node", async () => { const staleDraftEl = createCommentElement({ draftId: "draft-1" }); const freshDraftEl = createCommentElement({ draftId: "draft-1" }); let renderCalls = 0; let renderCompleted = false; const rafCallbacks: Array<(time: number) => void> = []; const originalWindow = globalThis.window; const originalDocument = globalThis.document; Object.assign(globalThis, { window: { requestAnimationFrame(callback: (time: number) => void) { rafCallbacks.push(callback); return rafCallbacks.length; }, cancelAnimationFrame() {}, getSelection: () => null, }, document: { activeElement: null, }, }); try { const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => { if (!selector.includes("draft-1")) { return null; } if (selector.includes("textarea")) { return null; } return renderCompleted ? freshDraftEl : staleDraftEl; }, querySelectorAll: () => [], contains: () => true, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => createDraft({ id: "draft-1", filePath: "docs/architecture.md", }), renderComments: async () => { renderCalls += 1; renderCompleted = true; }, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); await controller.highlightAndFocusDraft("draft-1"); assert.equal(renderCalls, 1); assert.equal(staleDraftEl.scrollCalls, 0); assert.equal(freshDraftEl.scrollCalls, 1); assert.equal(controller.getActiveCommentId(), null); } finally { Object.assign(globalThis, { window: originalWindow, document: originalDocument, }); } }); test("sidebar interaction controller clears active state classes", () => { const harness = createHarness(); harness.controller.setActiveComment("comment-1"); harness.controller.clearActiveState(); assert.equal(harness.controller.getActiveCommentId(), null); assert.deepEqual(harness.otherActiveEl.removeClassCalls, ["active", "active"]); }); test("sidebar interaction controller claims the sidebar leaf immediately when focusing an existing draft textarea", () => { const setActiveLeafCalls: Array<{ leaf: unknown; options: unknown }> = []; const sidebarLeaf = { id: "sidebar-leaf" }; let activeLeaf: unknown = { id: "editor-leaf" }; const selectionCalls: Array<{ start: number; end: number }> = []; const textarea = { isConnected: true, value: "Draft text", focus(_options?: unknown) { documentStub.activeElement = this as unknown as Element; }, setSelectionRange(start: number, end: number) { selectionCalls.push({ start, end }); }, } as unknown as HTMLTextAreaElement; const rafCallbacks: Array<(time: number) => void> = []; const windowStub = { requestAnimationFrame(callback: (time: number) => void) { rafCallbacks.push(callback); return rafCallbacks.length; }, cancelAnimationFrame() {}, getSelection: () => null, }; const documentStub = { activeElement: null as Element | null, }; const originalWindow = globalThis.window; const originalDocument = globalThis.document; Object.assign(globalThis, { window: windowStub, document: documentStub, }); try { const controller = new SidebarInteractionController({ app: { workspace: { get activeLeaf() { return activeLeaf; }, setActiveLeaf: (leaf: unknown, options: unknown) => { setActiveLeafCalls.push({ leaf, options }); activeLeaf = leaf; }, }, } as never, leaf: sidebarLeaf as never, containerEl: { querySelector: (selector: string) => selector === '[data-draft-id="draft-1"] textarea' ? textarea : null, querySelectorAll: () => [], contains: () => true, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.scheduleDraftFocus("draft-1", 0); assert.deepEqual(setActiveLeafCalls, [{ leaf: sidebarLeaf, options: { focus: false }, }]); assert.equal(documentStub.activeElement, textarea); assert.deepEqual(selectionCalls, [{ start: textarea.value.length, end: textarea.value.length, }]); assert.equal(rafCallbacks.length, 0); } finally { Object.assign(globalThis, { window: originalWindow, document: originalDocument, }); } }); test("sidebar interaction controller leaves draft alone on sidebar background click", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { let currentDraft: DraftComment | null = createDraft({ id: "draft-1" }); const activeEl = createCommentElement({ draftId: "draft-1" }); const draftEl = new FakeSidebarElement(); const backgroundEl = new FakeSidebarElement(); const saveDraftCalls: string[] = []; let clearRevealedCommentSelectionCalls = 0; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => selector.includes("draft-1") ? draftEl : null, querySelectorAll: () => [activeEl], contains: () => true, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => currentDraft, renderComments: async () => {}, saveDraft: async (commentId) => { saveDraftCalls.push(commentId); currentDraft = null; }, cancelDraft: () => {}, clearRevealedCommentSelection: () => { clearRevealedCommentSelectionCalls += 1; }, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.setActiveComment("draft-1"); await controller.sidebarClickHandler({ target: backgroundEl } as unknown as MouseEvent); assert.deepEqual(saveDraftCalls, []); assert.equal(currentDraft?.id, "draft-1"); assert.equal(controller.getActiveCommentId(), "draft-1"); assert.equal(clearRevealedCommentSelectionCalls, 0); assert.deepEqual(activeEl.removeClassCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller leaves draft alone on document clicks outside the sidebar", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; const originalWindow = globalThis.window; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); const rafCallbacks: Array<(time: number) => void> = []; Object.assign(globalThis, { window: { requestAnimationFrame(callback: (time: number) => void) { rafCallbacks.push(callback); return rafCallbacks.length; }, cancelAnimationFrame() {}, getSelection: () => null, }, }); try { let currentDraft: DraftComment | null = createDraft({ id: "draft-1" }); const activeEl = createCommentElement({ draftId: "draft-1" }); const draftEl = new FakeSidebarElement(); const outsideEl = new FakeSidebarElement(); const saveDraftCalls: string[] = []; let clearRevealedCommentSelectionCalls = 0; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => selector.includes("draft-1") ? draftEl : null, querySelectorAll: () => [activeEl], contains: (target: unknown) => target !== outsideEl, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => currentDraft, renderComments: async () => {}, saveDraft: async (commentId) => { saveDraftCalls.push(commentId); currentDraft = null; }, cancelDraft: () => {}, clearRevealedCommentSelection: () => { clearRevealedCommentSelectionCalls += 1; }, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.setActiveComment("draft-1"); controller.documentMouseDownHandler({ button: 0, target: outsideEl, } as unknown as MouseEvent); await Promise.resolve(); assert.deepEqual(saveDraftCalls, []); assert.equal(currentDraft?.id, "draft-1"); assert.equal(controller.getActiveCommentId(), "draft-1"); assert.equal(clearRevealedCommentSelectionCalls, 0); assert.equal(rafCallbacks.length, 0); assert.deepEqual(activeEl.removeClassCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, window: originalWindow, }); } }); test("sidebar interaction controller defers revealed selection clearing until after outside editor clicks settle", () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; const originalWindow = globalThis.window; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); const rafCallbacks: Array<(time: number) => void> = []; Object.assign(globalThis, { window: { requestAnimationFrame(callback: (time: number) => void) { rafCallbacks.push(callback); return rafCallbacks.length; }, cancelAnimationFrame() {}, getSelection: () => null, }, }); try { const outsideEl = new FakeSidebarElement(); let clearRevealedCommentSelectionCalls = 0; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: (target: unknown) => target !== outsideEl, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => { clearRevealedCommentSelectionCalls += 1; }, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.documentMouseDownHandler({ button: 0, target: outsideEl, } as unknown as MouseEvent); assert.equal(clearRevealedCommentSelectionCalls, 0); assert.equal(rafCallbacks.length, 1); const callback = rafCallbacks.shift(); assert.ok(callback); callback?.(0); assert.equal(clearRevealedCommentSelectionCalls, 1); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, window: originalWindow, }); } }); test("sidebar interaction controller ignores document clicks that start inside the sidebar", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const currentDraft = createDraft({ id: "draft-1" }); const insideEl = new FakeSidebarElement(); const saveDraftCalls: string[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => selector.includes("draft-1") ? insideEl : null, querySelectorAll: () => [], contains: (target: unknown) => target === insideEl, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => currentDraft, renderComments: async () => {}, saveDraft: async (commentId) => { saveDraftCalls.push(commentId); }, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.documentMouseDownHandler({ button: 0, target: insideEl, } as unknown as MouseEvent); await Promise.resolve(); assert.deepEqual(saveDraftCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller keeps draft editing open while clicking a picker modal", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const currentDraft = createDraft({ id: "draft-1" }); const modalEl = new FakeSidebarElement({ isDismissalExemptSurface: true }); const saveDraftCalls: string[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: (target: unknown) => false && target === modalEl, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => currentDraft, renderComments: async () => {}, saveDraft: async (commentId) => { saveDraftCalls.push(commentId); }, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.documentMouseDownHandler({ button: 0, target: modalEl, } as unknown as MouseEvent); await Promise.resolve(); assert.deepEqual(saveDraftCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller leaves draft alone when clicking another comment", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { let currentDraft: DraftComment | null = createDraft({ id: "draft-1" }); const activeEl = createCommentElement({ draftId: "draft-1" }); const draftEl = new FakeSidebarElement(); const commentTargetEl = new FakeSidebarElement({ isCommentItem: true }); const saveDraftCalls: string[] = []; let clearRevealedCommentSelectionCalls = 0; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: (selector: string) => selector.includes("draft-1") ? draftEl : null, querySelectorAll: () => [activeEl], contains: () => true, } as never, getCurrentFile: () => ({ path: "docs/architecture.md" }) as never, getDraftForView: () => currentDraft, renderComments: async () => {}, saveDraft: async (commentId) => { saveDraftCalls.push(commentId); currentDraft = null; }, cancelDraft: () => {}, clearRevealedCommentSelection: () => { clearRevealedCommentSelectionCalls += 1; }, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, }); controller.setActiveComment("draft-1"); await controller.sidebarClickHandler({ target: commentTargetEl } as unknown as MouseEvent); assert.deepEqual(saveDraftCalls, []); assert.equal(currentDraft?.id, "draft-1"); assert.equal(controller.getActiveCommentId(), "draft-1"); assert.equal(clearRevealedCommentSelectionCalls, 0); assert.deepEqual(activeEl.removeClassCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller keeps deleted mode on sidebar background click", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const backgroundEl = new FakeSidebarElement(); const setShowDeletedCommentsCalls: boolean[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: () => true, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, shouldShowDeletedComments: () => true, setShowDeletedComments: async (showDeleted) => { setShowDeletedCommentsCalls.push(showDeleted); }, }); await controller.sidebarClickHandler({ target: backgroundEl } as unknown as MouseEvent); assert.deepEqual(setShowDeletedCommentsCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller keeps deleted mode while clicking a normal toolbar", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const toolbarEl = new FakeSidebarElement({ isSectionChrome: true, isToolbar: true, }); const setShowDeletedCommentsCalls: boolean[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: () => true, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, shouldShowDeletedComments: () => true, setShowDeletedComments: async (showDeleted) => { setShowDeletedCommentsCalls.push(showDeleted); }, }); await controller.sidebarClickHandler({ target: toolbarEl } as unknown as MouseEvent); assert.deepEqual(setShowDeletedCommentsCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller keeps deleted mode while clicking the deleted toolbar", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const toolbarEl = new FakeSidebarElement({ isSectionChrome: true, isToolbar: true, isDeletedToolbarMode: true, }); const setShowDeletedCommentsCalls: boolean[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: () => true, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, shouldShowDeletedComments: () => true, setShowDeletedComments: async (showDeleted) => { setShowDeletedCommentsCalls.push(showDeleted); }, }); await controller.sidebarClickHandler({ target: toolbarEl } as unknown as MouseEvent); assert.deepEqual(setShowDeletedCommentsCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } }); test("sidebar interaction controller keeps deleted mode on document clicks outside the sidebar", async () => { const originalHTMLElement = globalThis.HTMLElement; const originalNode = globalThis.Node; Object.assign(globalThis, { HTMLElement: FakeSidebarElement, Node: FakeNode, }); try { const outsideEl = new FakeSidebarElement(); const setShowDeletedCommentsCalls: boolean[] = []; const controller = new SidebarInteractionController({ app: { workspace: { activeLeaf: null, setActiveLeaf: () => {}, }, } as never, leaf: {} as never, containerEl: { querySelector: () => null, querySelectorAll: () => [], contains: (target: unknown) => target !== outsideEl, } as never, getCurrentFile: () => null, getDraftForView: () => null, renderComments: async () => {}, saveDraft: () => {}, cancelDraft: () => {}, clearRevealedCommentSelection: () => {}, revealComment: async () => {}, getPreferredFileLeaf: () => null, openLinkText: async () => {}, shouldShowDeletedComments: () => true, setShowDeletedComments: async (showDeleted) => { setShowDeletedCommentsCalls.push(showDeleted); }, }); controller.documentMouseDownHandler({ button: 0, target: outsideEl, } as unknown as MouseEvent); await Promise.resolve(); assert.deepEqual(setShowDeletedCommentsCalls, []); } finally { Object.assign(globalThis, { HTMLElement: originalHTMLElement, Node: originalNode, }); } });