diff --git a/src/features/database/ExportDialog.test.tsx b/src/features/database/ExportDialog.test.tsx new file mode 100644 index 000000000..3b06fa8c7 --- /dev/null +++ b/src/features/database/ExportDialog.test.tsx @@ -0,0 +1,37 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { commands } from "@/platform/ipc"; +import { ExportDialog } from "./ExportDialog"; +import { cellPreview } from "./cellPreview"; + +vi.mock("./store", () => ({ closeDialog: vi.fn() })); +vi.mock("@/workbench/contributions", () => ({ notifications: () => ({ notify: vi.fn() }) })); +vi.mock("@tauri-apps/api/path", () => ({ homeDir: async () => "/home/test" })); + +describe("large value export", () => { + it("bounds preview values while exporting every row in full", async () => { + const full = JSON.stringify({ text: "\u4e2d text ".repeat(200_000), tail: "complete" }); + const dbExportFile = vi.spyOn(commands, "dbExportFile").mockResolvedValue({ status: "ok", data: null }); + const rows = [[full], ["short"], [null]]; + render( + , + ); + const preview = screen.getByLabelText("Export preview"); + expect(JSON.parse(preview.textContent!)).toEqual([ + { payload: cellPreview(full) }, + { payload: "short" }, + { payload: null }, + ]); + await waitFor(() => expect(screen.getByLabelText("Output file")).toHaveValue("/home/test/big.json")); + fireEvent.click(screen.getByRole("button", { name: "Export to File" })); + await waitFor(() => expect(dbExportFile).toHaveBeenCalled()); + const [path, contents] = dbExportFile.mock.calls[0]!; + expect(path).toBe("/home/test/big.json"); + expect(JSON.parse(contents)).toEqual(rows.map(([payload]) => ({ payload }))); + dbExportFile.mockRestore(); + }); +}); diff --git a/src/features/database/ExportDialog.tsx b/src/features/database/ExportDialog.tsx index 269946355..b8d9b014b 100644 --- a/src/features/database/ExportDialog.tsx +++ b/src/features/database/ExportDialog.tsx @@ -17,6 +17,7 @@ import { import { CodePreview } from "./CodePreview"; import { closeDialog } from "./store"; import styles from "./Database.module.css"; +import { cellPreview } from "./cellPreview"; const PREVIEW_ROWS = 100; /** Formats with a tabular layout (the options apply). */ @@ -59,7 +60,15 @@ export function ExportDialog({ const tabular = TABULAR.has(format); const effective = tabular ? options : DEFAULT_EXPORT_OPTIONS; const preview = useMemo( - () => exportData(format, { ...data, rows: data.rows.slice(0, PREVIEW_ROWS) }, effective), + () => + exportData( + format, + { + ...data, + rows: data.rows.slice(0, PREVIEW_ROWS).map((row) => row.map((v) => (v === null ? null : cellPreview(v)))), + }, + effective, + ), [format, data, effective], ); const text = () => exportData(format, data, effective); diff --git a/src/features/database/Grid.test.tsx b/src/features/database/Grid.test.tsx index 2c7e73aa5..200defe81 100644 --- a/src/features/database/Grid.test.tsx +++ b/src/features/database/Grid.test.tsx @@ -2,7 +2,8 @@ import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap"; import { registerActions } from "@/platform/actions"; import { fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { CELL_PREVIEW_LIMIT, cellPreview, Grid } from "./Grid"; +import { Grid } from "./Grid"; +import { CELL_PREVIEW_LIMIT, cellPreview } from "./cellPreview"; import { exportData } from "./export"; vi.mock("@tanstack/react-virtual", () => ({ diff --git a/src/features/database/Grid.tsx b/src/features/database/Grid.tsx index 0865878c1..6711165e9 100644 --- a/src/features/database/Grid.tsx +++ b/src/features/database/Grid.tsx @@ -10,6 +10,7 @@ import { Dialog, Icon, showContextMenu, type MenuItem } from "@/ui"; import { parseDelimited, toCsv, toTsv, type Cell } from "./model"; import styles from "./Database.module.css"; import { tx } from "@/platform/i18n"; +import { CELL_PREVIEW_LIMIT, cellPreview } from "./cellPreview"; export interface GridColumn { name: string; @@ -69,17 +70,6 @@ interface Sel { c1: number; } -export const CELL_PREVIEW_LIMIT = 1000; - -/** Bound text layout without changing the underlying value or splitting a surrogate pair. */ -export function cellPreview(value: string): string { - if (value.length <= CELL_PREVIEW_LIMIT) return value; - let end = CELL_PREVIEW_LIMIT; - const last = value.charCodeAt(end - 1); - if (last >= 0xd800 && last <= 0xdbff) end--; - return value.slice(0, end) + "\u2026"; -} - const ROW_H = 24; const NUM_W = 48; diff --git a/src/features/database/cellPreview.ts b/src/features/database/cellPreview.ts new file mode 100644 index 000000000..c0a69b4d1 --- /dev/null +++ b/src/features/database/cellPreview.ts @@ -0,0 +1,10 @@ +export const CELL_PREVIEW_LIMIT = 1000; + +/** Bound text layout without changing the underlying value or splitting a surrogate pair. */ +export function cellPreview(value: string): string { + if (value.length <= CELL_PREVIEW_LIMIT) return value; + let end = CELL_PREVIEW_LIMIT; + const last = value.charCodeAt(end - 1); + if (last >= 0xd800 && last <= 0xdbff) end--; + return value.slice(0, end) + "\u2026"; +}