Something went wrong. Try again.
[mirror] A local-first desktop markdown writer with grammar checking, at proto integration, and more github.com/stormlightlabs/writer/releases
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331import { useMarkdownExport, useTextExport, useTextExportUI } from "$hooks/useTextExport";import type { TextExportResult } from "$ports";import { useAppStore } from "$state/stores/app";import { save } from "@tauri-apps/plugin-dialog";import { writeFile } from "@tauri-apps/plugin-fs";import * as logger from "@tauri-apps/plugin-log";import { act, renderHook } from "@testing-library/react";import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@tauri-apps/plugin-dialog", () => ({ save: vi.fn() }));
vi.mock("@tauri-apps/plugin-fs", () => ({ writeFile: vi.fn() }));
vi.mock("$state/stores/toasts", () => ({ showSuccessToast: vi.fn(), showErrorToast: vi.fn() }));
const { mockRenderMarkdownForText, mockRunCmd } = vi.hoisted(() => ({ mockRenderMarkdownForText: vi.fn(), mockRunCmd: vi.fn(async () => {}),}));
vi.mock( "$ports", () => ({ renderMarkdownForText: (...args: unknown[]) => mockRenderMarkdownForText(...args), runCmd: mockRunCmd }),);
const textRenderResult = { text: "Exported text content", title: "Test Document", word_count: 10 };
describe(useTextExport, () => { beforeEach(() => { vi.clearAllMocks(); useAppStore.getState().resetTextExport(); });
it("exports text successfully", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
let didExport = false; await act(async () => { didExport = await result.current(textRenderResult); });
expect(didExport).toBeTruthy(); expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "Test_Document.txt", }); expect(writeFile).toHaveBeenCalledOnce(); expect(useAppStore.getState().isExportingText).toBeFalsy(); expect(useAppStore.getState().textExportError).toBeNull(); });
it("returns false when user cancels save dialog", async () => { vi.mocked(save).mockResolvedValue(null);
const { result } = renderHook(() => useTextExport());
let didExport = false; await act(async () => { didExport = await result.current(textRenderResult); });
expect(didExport).toBeFalsy(); expect(save).toHaveBeenCalledOnce(); expect(writeFile).not.toHaveBeenCalled(); expect(vi.mocked(logger.info)).toHaveBeenCalledWith("Text export canceled before writing file"); });
it("uses default filename with timestamp when title is null", async () => { vi.useFakeTimers(); vi.setSystemTime(1234567890123); vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const resultWithoutTitle = { ...textRenderResult, title: null };
await act(async () => { await result.current(resultWithoutTitle); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "document_1234567890123.txt", }); vi.useRealTimers(); });
it("sanitizes filename allowing spaces, dashes, and dots", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const resultWithSpecialChars = { ...textRenderResult, title: "Version 1.1 - Final Draft" };
await act(async () => { await result.current(resultWithSpecialChars); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "Version_1.1_-_Final_Draft.txt", }); });
it("condenses multiple spaces into single underscore", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const resultWithMultipleSpaces = { ...textRenderResult, title: "My Document Title" };
await act(async () => { await result.current(resultWithMultipleSpaces); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "My_Document_Title.txt", }); });
it("removes leading and trailing underscores", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const resultWithUnderscores = { ...textRenderResult, title: " My Document " };
await act(async () => { await result.current(resultWithUnderscores); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "My_Document.txt", }); });
it("uses default filename when title only contains special characters", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const resultWithOnlySpecialChars = { ...textRenderResult, title: "@#$%^&*()!" };
await act(async () => { await result.current(resultWithOnlySpecialChars); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Text", extensions: ["txt"] }], defaultPath: "document.txt", }); });
it("handles export errors and sets error state", async () => { vi.mocked(save).mockRejectedValue(new Error("Disk full"));
const { result } = renderHook(() => useTextExport());
await act(async () => { await expect(result.current(textRenderResult)).rejects.toThrow("Disk full"); });
expect(useAppStore.getState().isExportingText).toBeFalsy(); expect(useAppStore.getState().textExportError).toBe("Disk full"); });
it("encodes text to Uint8Array correctly", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.txt");
const { result } = renderHook(() => useTextExport());
const unicodeResult = { text: "Hello 世界 🌍", title: "Unicode Test", word_count: 3 };
await act(async () => { await result.current(unicodeResult); });
expect(writeFile).toHaveBeenCalledOnce(); const writtenData = vi.mocked(writeFile).mock.calls[0][1] as Uint8Array; const decoder = new TextDecoder(); expect(decoder.decode(writtenData)).toBe("Hello 世界 🌍"); });});
describe(useMarkdownExport, () => { beforeEach(() => { vi.clearAllMocks(); useAppStore.getState().resetTextExport(); });
it("exports markdown successfully", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.md");
const { result } = renderHook(() => useMarkdownExport());
let didExport = false; await act(async () => { didExport = await result.current("# Markdown content", "Test Document"); });
expect(didExport).toBeTruthy(); expect(save).toHaveBeenCalledWith({ filters: [{ name: "Markdown", extensions: ["md"] }], defaultPath: "Test_Document.md", }); expect(writeFile).toHaveBeenCalledOnce(); expect(useAppStore.getState().isExportingText).toBeFalsy(); expect(useAppStore.getState().textExportError).toBeNull(); });
it("returns false when user cancels save dialog", async () => { vi.mocked(save).mockResolvedValue(null);
const { result } = renderHook(() => useMarkdownExport());
let didExport = false; await act(async () => { didExport = await result.current("# Markdown", "Test"); });
expect(didExport).toBeFalsy(); expect(writeFile).not.toHaveBeenCalled(); expect(useAppStore.getState().isExportingText).toBeFalsy(); });
it("uses default filename with timestamp when title is null", async () => { vi.useFakeTimers(); vi.setSystemTime(1234567890123); vi.mocked(save).mockResolvedValue("/tmp/output.md");
const { result } = renderHook(() => useMarkdownExport());
await act(async () => { await result.current("# Markdown", null); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Markdown", extensions: ["md"] }], defaultPath: "document_1234567890123.md", }); vi.useRealTimers(); });
it("sanitizes filename allowing spaces, dashes, and dots", async () => { vi.mocked(save).mockResolvedValue("/tmp/output.md");
const { result } = renderHook(() => useMarkdownExport());
await act(async () => { await result.current("# Markdown", "Version 1.1 - Final Draft"); });
expect(save).toHaveBeenCalledWith({ filters: [{ name: "Markdown", extensions: ["md"] }], defaultPath: "Version_1.1_-_Final_Draft.md", }); });
it("handles export errors and sets error state", async () => { vi.mocked(save).mockRejectedValue(new Error("Permission denied"));
const { result } = renderHook(() => useMarkdownExport());
await act(async () => { await expect(result.current("# Markdown", "Test")).rejects.toThrow("Permission denied"); });
expect(useAppStore.getState().isExportingText).toBeFalsy(); expect(useAppStore.getState().textExportError).toBe("Permission denied"); });});
describe(useTextExportUI, () => { beforeEach(() => { vi.clearAllMocks(); useAppStore.getState().resetTextExport(); mockRenderMarkdownForText.mockImplementation( ( _locationId: number, _relPath: string, _text: string, _profile: unknown, onOk: (value: TextExportResult) => void, ) => { onOk(textRenderResult); return { type: "None" }; }, ); });
it("returns false when no active tab exists", async () => { const { result } = renderHook(() => useTextExportUI({ activeTab: null, text: "Hello" }));
let didExport = true; await act(async () => { didExport = await result.current.handleExportText(); });
expect(didExport).toBeFalsy(); });
it("returns false when render fails", async () => { mockRenderMarkdownForText.mockImplementationOnce( ( _locationId: number, _relPath: string, _text: string, _profile: unknown, _onOk: (value: TextExportResult) => void, onErr: (error: Error) => void, ) => { onErr(new Error("Render failed")); return { type: "None" }; }, ); const activeTab = { id: "tab-1", title: "Doc", docRef: { location_id: 1, rel_path: "doc.md" }, isModified: false }; const { result } = renderHook(() => useTextExportUI({ activeTab, text: "Hello" }));
let didExport = true; await act(async () => { didExport = await result.current.handleExportText(); });
expect(didExport).toBeFalsy(); });});