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
atproto codemirror markdown tauri rust react
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407import { ExportDialog } from "$components/export/ExportDialog/ExportDialog";import { DEFAULT_OPTIONS } from "$pdf/constants";import type { PdfExportOptions, PdfRenderResult } from "$pdf/types";import { resetAppStore, useAppStore } from "$state/stores/app";import { resetDocxExportStore, useDocxExportStore } from "$state/stores/docx-export";import { resetPdfExportStore, usePdfExportStore } from "$state/stores/pdf-export";import { resetTextExportStore, useTextExportStore } from "$state/stores/text-export";import { resetUiStore, useUiStore } from "$state/stores/ui";import type { RenderedFontFamily } from "$types";import { fireEvent, render, screen, waitFor } from "@testing-library/react";import { beforeEach, describe, expect, it, vi } from "vitest";
const mockHandleExportText = vi.fn();const mockHandleExportMarkdown = vi.fn();const mockHandleExportDocx = vi.fn();
const mockRenderResult: PdfRenderResult = { title: "Test Document", word_count: 100, nodes: [{ type: "paragraph", content: "Hello world" }],};
const mockOnExport = vi.fn();
vi.mock( "$components/export/preview/PdfPreview", () => ({ PdfPreviewPanel: () => <div data-testid="pdf-preview">Preview Content</div> }),);
vi.mock( "$hooks/useTextExport", () => ({ useTextExportUI: () => ({ handleExportText: mockHandleExportText, handleExportMarkdown: mockHandleExportMarkdown }), }),);
vi.mock("$hooks/useDocxExport", () => ({ useDocxExportUI: () => ({ handleExportDocx: mockHandleExportDocx }) }));
vi.mock("$hooks/useViewportTier", () => ({ useViewportTier: () => ({ isCompact: false, viewportWidth: 1400 }) }));
type RenderDialogArgs = { previewResult?: PdfRenderResult | null; renderedFontFamily?: RenderedFontFamily; onExport?: (options: PdfExportOptions) => Promise<void>; documentText?: string;};
function renderExportDialog( { previewResult = mockRenderResult, renderedFontFamily = "IBM Plex Sans Variable", onExport = mockOnExport, documentText = "", }: RenderDialogArgs = {},) { return render( <ExportDialog onExport={onExport} previewResult={previewResult} renderedFontFamily={renderedFontFamily} documentText={documentText} />, );}
function seedActiveDocument() { useAppStore.getState().setDocumentsForLocation(1, [{ location_id: 1, rel_path: "test.md", title: "My Document", word_count: 100, updated_at: "2024-01-01", }]); useAppStore.getState().applySessionState({ activeTabId: "tab-1", tabs: [{ id: "tab-1", docRef: { location_id: 1, rel_path: "test.md" }, title: "My Document", isModified: false }], });}
describe("PdfExportDialog", () => { beforeEach(() => { vi.clearAllMocks(); resetAppStore(); resetUiStore(); resetPdfExportStore(); resetTextExportStore(); resetDocxExportStore(); mockHandleExportText.mockResolvedValue(true); mockHandleExportMarkdown.mockResolvedValue(true); mockHandleExportDocx.mockResolvedValue(true); });
describe("visibility", () => { it("does not render when closed", () => { useUiStore.getState().setPdfExportDialogOpen(false); renderExportDialog(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); });
it("renders when open", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog(); expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByText("Export Document")).toBeInTheDocument(); }); });
describe("content layout", () => { it("renders header with close button", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog(); expect(screen.getByText("Export Document")).toBeInTheDocument(); expect(screen.getByLabelText("Close export dialog")).toBeInTheDocument(); });
it("renders options panel", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog(); expect(screen.getByText("PDF")).toBeInTheDocument(); expect(screen.getByText("DOCX")).toBeInTheDocument(); expect(screen.getByText("Plaintext")).toBeInTheDocument(); expect(screen.getByRole("combobox", { name: "Page Size" })).toBeInTheDocument(); expect(screen.getByRole("combobox", { name: "Orientation" })).toBeInTheDocument(); });
it("renders footer with cancel and export buttons", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog(); expect(screen.getByText("Cancel")).toBeInTheDocument(); expect(screen.getByText("Export PDF")).toBeInTheDocument(); });
it("renders preview panel when viewport is wide enough", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog(); expect(screen.getByTestId("pdf-preview")).toBeInTheDocument(); }); });
describe("interactions", () => { it("closes dialog when cancel button is clicked", () => { useUiStore.getState().setPdfExportDialogOpen(true); useTextExportStore.getState().failTextExport("text failed"); useDocxExportStore.getState().failDocxExport("docx failed"); renderExportDialog();
fireEvent.click(screen.getByText("Cancel"));
expect(useUiStore.getState().pdfExportDialogOpen).toBe(false); expect(usePdfExportStore.getState().pdfExportError).toBeNull(); expect(useTextExportStore.getState().textExportError).toBeNull(); expect(useDocxExportStore.getState().docxExportError).toBeNull(); });
it("closes dialog when close button is clicked", () => { useUiStore.getState().setPdfExportDialogOpen(true); useTextExportStore.getState().failTextExport("text failed"); useDocxExportStore.getState().failDocxExport("docx failed"); renderExportDialog();
fireEvent.click(screen.getByLabelText("Close export dialog"));
expect(useUiStore.getState().pdfExportDialogOpen).toBe(false); expect(usePdfExportStore.getState().pdfExportError).toBeNull(); expect(useTextExportStore.getState().textExportError).toBeNull(); expect(useDocxExportStore.getState().docxExportError).toBeNull(); });
it("calls onExport with current options when export is clicked", async () => { useUiStore.getState().setPdfExportDialogOpen(true); mockOnExport.mockResolvedValueOnce(void 0); renderExportDialog();
fireEvent.click(screen.getByText("Export PDF"));
await waitFor(() => { expect(mockOnExport).toHaveBeenCalledWith(DEFAULT_OPTIONS); }); });
it("disables export button while exporting", () => { useUiStore.getState().setPdfExportDialogOpen(true); usePdfExportStore.getState().startPdfExport(); renderExportDialog();
expect(screen.getByText("Exporting...")).toBeInTheDocument(); const exportingButton = screen.getByText("Exporting...").closest("button"); expect(exportingButton).toBeDisabled(); });
it("enables docx and plaintext export format tabs", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const docxTab = screen.getByRole("button", { name: "DOCX export tab" }); const txtTab = screen.getByRole("button", { name: "Plaintext export tab" });
expect(docxTab).toBeEnabled(); expect(txtTab).toBeEnabled(); expect(screen.getByText("Export PDF")).toBeEnabled(); });
it("keeps dialog open when text export does not succeed", async () => { seedActiveDocument(); mockHandleExportText.mockResolvedValueOnce(false); useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
fireEvent.click(screen.getByRole("button", { name: "Plaintext export tab" })); fireEvent.click(screen.getByText("Export Text"));
await waitFor(() => { expect(mockHandleExportText).toHaveBeenCalledOnce(); }); expect(useUiStore.getState().pdfExportDialogOpen).toBe(true); });
it("closes dialog when text export succeeds", async () => { seedActiveDocument(); mockHandleExportText.mockResolvedValueOnce(true); useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
fireEvent.click(screen.getByRole("button", { name: "Plaintext export tab" })); fireEvent.click(screen.getByText("Export Text"));
await waitFor(() => { expect(mockHandleExportText).toHaveBeenCalledOnce(); }); expect(useUiStore.getState().pdfExportDialogOpen).toBe(false); });
it("keeps dialog open when DOCX export does not succeed", async () => { mockHandleExportDocx.mockResolvedValueOnce(false); useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
fireEvent.click(screen.getByRole("button", { name: "DOCX export tab" })); fireEvent.click(screen.getByText("Export DOCX"));
await waitFor(() => { expect(mockHandleExportDocx).toHaveBeenCalledOnce(); }); expect(useUiStore.getState().pdfExportDialogOpen).toBe(true); }); });
describe("error handling", () => { it("displays error message when export fails", () => { useUiStore.getState().setPdfExportDialogOpen(true); usePdfExportStore.getState().failPdfExport("Export failed"); renderExportDialog();
expect(screen.getByText("Export failed")).toBeInTheDocument(); }); });
describe("options modifications", () => { it("updates page size when changed", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const pageSizeSelect = screen.getByRole("combobox", { name: "Page Size" }); fireEvent.change(pageSizeSelect, { target: { value: "LETTER" } });
expect(useUiStore.getState().pdfExportOptions.pageSize).toBe("LETTER"); });
it("updates orientation when changed", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const orientationSelect = screen.getByRole("combobox", { name: "Orientation" }); fireEvent.change(orientationSelect, { target: { value: "landscape" } });
expect(useUiStore.getState().pdfExportOptions.orientation).toBe("landscape"); });
it("updates font size when changed", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const fontSizeSlider = screen.getByRole("slider"); fireEvent.change(fontSizeSlider, { target: { value: "12" } });
expect(useUiStore.getState().pdfExportOptions.fontSize).toBe(12); });
it("updates header checkbox when toggled", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const headerCheckbox = screen.getByLabelText("Include Header"); fireEvent.click(headerCheckbox);
expect(useUiStore.getState().pdfExportOptions.includeHeader).toBe(true); });
it("updates footer checkbox when toggled", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
const footerCheckbox = screen.getByLabelText("Include Footer"); fireEvent.click(footerCheckbox);
expect(useUiStore.getState().pdfExportOptions.includeFooter).toBe(true); }); });
describe("document title display", () => { it("shows document title when available", () => { useUiStore.getState().setPdfExportDialogOpen(true); seedActiveDocument(); renderExportDialog();
expect(screen.getByText("My Document")).toBeInTheDocument(); }); });
describe("String tab", () => { it("renders String tab button", () => { useUiStore.getState().setPdfExportDialogOpen(true); renderExportDialog();
expect(screen.getByRole("button", { name: "String export tab" })).toBeInTheDocument(); });
it("shows connect prompt when no AT Proto session is active", () => { useUiStore.getState().setPdfExportDialogOpen(true); seedActiveDocument(); renderExportDialog();
fireEvent.click(screen.getByRole("button", { name: "String export tab" }));
expect(screen.getByText("Connect Tangled to publish")).toBeInTheDocument(); });
it("shows publish form when a session is active", () => { useUiStore.getState().setPdfExportDialogOpen(true); useUiStore.getState().setAtProtoSession({ did: "did:plc:alice", handle: "alice.bsky.social", sessionId: "sess-1", endpoint: "https://pds.example.com", }); seedActiveDocument(); renderExportDialog({ documentText: "# Hello world" });
fireEvent.click(screen.getByRole("button", { name: "String export tab" }));
expect(screen.getByLabelText("Filename")).toBeInTheDocument(); expect(screen.getByLabelText("Description")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Publish to Tangled" })).toBeInTheDocument(); });
it("pre-fills filename from the active document's filename", () => { useUiStore.getState().setPdfExportDialogOpen(true); useUiStore.getState().setAtProtoSession({ did: "did:plc:alice", handle: "alice.bsky.social", sessionId: "sess-1", endpoint: "https://pds.example.com", }); seedActiveDocument(); renderExportDialog({ documentText: "# Hello" });
fireEvent.click(screen.getByRole("button", { name: "String export tab" }));
const filenameInput = screen.getByLabelText("Filename") as HTMLInputElement; expect(filenameInput.value).toBe("test.md"); });
it("shows empty export state when no document is open", () => { useUiStore.getState().setPdfExportDialogOpen(true); useUiStore.getState().setAtProtoSession({ did: "did:plc:alice", handle: "alice.bsky.social", sessionId: "sess-1", endpoint: "https://pds.example.com", }); renderExportDialog();
fireEvent.click(screen.getByRole("button", { name: "String export tab" }));
expect(screen.getByText("Open a document to export.")).toBeInTheDocument(); });
it("disables publish button when filename is empty", () => { useUiStore.getState().setPdfExportDialogOpen(true); useUiStore.getState().setAtProtoSession({ did: "did:plc:alice", handle: "alice.bsky.social", sessionId: "sess-1", endpoint: "https://pds.example.com", }); seedActiveDocument(); renderExportDialog({ documentText: "# Hello" });
fireEvent.click(screen.getByRole("button", { name: "String export tab" }));
const filenameInput = screen.getByLabelText("Filename"); fireEvent.change(filenameInput, { target: { value: "" } });
expect(screen.getByRole("button", { name: "Publish to Tangled" })).toBeDisabled(); }); });});