import { afterEach, describe, expect, test } from "bun:test"; import { act, useState } from "react"; import { PaneFooterProvider } from "../../../components/layout/pane/footer"; import { TestDialogProvider, testRender } from "../../../renderers/opentui/test-utils"; import { appReducer, createInitialState, type AppAction, type AppState } from "../../../state/app/context"; import { createTestPaneConfig, TestPaneProvider } from "../../../test-support/pane"; import { createTestPluginRuntime } from "../../../test-support/plugin-runtime"; import { Box, Text } from "../../../ui"; import { createQuickNotesPane } from "./quick-notes-pane"; import type { NotesFiles } from "./files"; import { NotesStoreRegistry } from "./store"; /** The tabs take a registry; while signed out it hands back the disk store. */ function registryFor(files: NotesFiles): NotesStoreRegistry { return new NotesStoreRegistry({ persistence: null, files, isSignedIn: () => false }); } import type { QuickNoteEntry } from "./model"; let testSetup: Awaited> | undefined; const TAB_A = "tab-a"; const TAB_B = "tab-b"; function createMockNotesFiles(options?: { loadDelayMs?: number }) { const saves: Array<{ key: string; text: string }> = []; const notes = new Map([ [TAB_A, ""], [TAB_B, "beta-note"], ]); const index: QuickNoteEntry[] = [ { id: TAB_A, title: "Alpha" }, { id: TAB_B, title: "Beta" }, ]; const loadDelayMs = options?.loadDelayMs ?? 0; return { saves, readOnly: false, owner: { kind: "user" }, async load(key: string) { if (loadDelayMs > 0) { await new Promise((resolve) => setTimeout(resolve, loadDelayMs)); } return notes.get(key) ?? ""; }, async save(key: string, text: string) { saves.push({ key, text }); notes.set(key, text); }, async delete() {}, quickNoteKey(id: string) { return id; }, async loadQuickNotesIndex() { return index; }, async saveQuickNotesIndex() {}, } as unknown as NotesFiles & { saves: Array<{ key: string; text: string }> }; } const PANE_INSTANCE_ID = "quick-notes:test"; const paneRuntime = createTestPluginRuntime(); function QuickNotesHarness({ QuickNotesPane, }: { QuickNotesPane: ReturnType; }) { const [focused, setFocused] = useState(true); // The open note is pane state, so the harness needs a reducer. const [paneState, setPaneState] = useState({}); const state = createInitialState(createTestPaneConfig("/tmp/quick-notes-test", { paneId: "quick-notes", instanceId: PANE_INSTANCE_ID, })); state.paneState = paneState; const dispatch = (action: AppAction) => setPaneState( (current) => appReducer({ ...state, paneState: current }, action).paneState, ); return ( {() => ( <> setFocused(false)}>blur-pane )} ); } afterEach(async () => { if (testSetup) { await act(async () => { testSetup!.renderer.destroy(); }); testSetup = undefined; } }); describe("createQuickNotesPane", () => { test("does not save stale buffer text to a new tab before its notes load", async () => { const notesFiles = createMockNotesFiles({ loadDelayMs: 50 }); const QuickNotesPane = createQuickNotesPane(registryFor(notesFiles)); testSetup = await testRender( , { width: 80, height: 24 }, ); await testSetup.renderOnce(); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); await testSetup!.renderOnce(); }); let frame = testSetup.captureCharFrame(); expect(frame).toContain("Alpha"); const placeholderRow = frame.split("\n").findIndex((line) => line.includes("Write notes")); const placeholderCol = frame.split("\n")[placeholderRow]?.indexOf("Write notes") ?? -1; await act(async () => { await testSetup!.mockMouse.click(placeholderCol + 1, placeholderRow); await testSetup!.renderOnce(); }); await act(async () => { await testSetup!.mockInput.typeText("alpha-note"); await testSetup!.renderOnce(); }); frame = testSetup.captureCharFrame(); expect(frame).toContain("alpha-note"); const betaRow = frame.split("\n").findIndex((line) => line.includes("Beta")); const betaCol = frame.split("\n")[betaRow]?.indexOf("Beta") ?? -1; await act(async () => { await testSetup!.mockMouse.click(betaCol + 1, betaRow); await testSetup!.renderOnce(); }); frame = testSetup.captureCharFrame(); const blurRow = frame.split("\n").findIndex((line) => line.includes("blur-pane")); const blurCol = frame.split("\n")[blurRow]?.indexOf("blur-pane") ?? -1; await act(async () => { await testSetup!.mockMouse.click(blurCol + 1, blurRow); await testSetup!.renderOnce(); }); expect(notesFiles.saves).toEqual([{ key: TAB_A, text: "alpha-note" }]); }); test("never presents an unreadable note as an empty editable one", async () => { const notesFiles = createMockNotesFiles(); const failing = { ...notesFiles, async load() { throw new Error("EACCES: permission denied"); }, } as unknown as NotesFiles & { saves: Array<{ key: string; text: string }> }; const QuickNotesPane = createQuickNotesPane(registryFor(failing)); testSetup = await testRender( , { width: 80, height: 24 }, ); for (let attempt = 0; attempt < 5; attempt++) { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); await testSetup!.renderOnce(); }); if (testSetup.captureCharFrame().includes("could not be read")) break; } const frame = testSetup.captureCharFrame(); expect(frame).toContain("could not be read"); expect(frame).not.toContain("Write notes"); // Clicking the body must not open an editor over content we failed to read. const errorRow = frame.split("\n").findIndex((line) => line.includes("could not be read")); await act(async () => { await testSetup!.mockMouse.click(2, errorRow); await testSetup!.mockInput.typeText("clobber"); await testSetup!.renderOnce(); }); const blurRow = testSetup.captureCharFrame().split("\n").findIndex((line) => line.includes("blur-pane")); await act(async () => { await testSetup!.mockMouse.click(2, blurRow); await testSetup!.renderOnce(); }); expect(notesFiles.saves).toEqual([]); }); });