From fc19cae39cac7419f7bb7b44977902ab8d4149e6 Mon Sep 17 00:00:00 2001 From: t8y2 Date: Tue, 22 Sep 2026 19:05:00 +0800 Subject: [PATCH] fix(sql-file): cache preview highlighting during progress updates Refs: #9537 --- .../SqlFileExecutionDialog.retry.spec.ts | 71 ++++++++++++++++++- .../sql-file/SqlFileExecutionDialog.vue | 3 +- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts index c659289eeb..b22ce48d6a 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.retry.spec.ts @@ -12,6 +12,8 @@ const mocks = vi.hoisted(() => ({ ensureConnected: vi.fn(), executeSqlFiles: vi.fn(), fetchSqlFileTargetOptions: vi.fn(), + highlight: vi.fn(), + highlighterState: undefined as undefined | { ready: boolean; appearance: string }, listenSqlFileProgress: vi.fn(), openFileDialog: vi.fn(), previewSqlFile: vi.fn(), @@ -40,7 +42,7 @@ vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) })); vi.mock("@/lib/common/utils", () => ({ uuid: mocks.uuid })); vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true })); vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.openFileDialog })); -vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: (sql: string) => sql }) })); +vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: mocks.highlight }) })); vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) })); vi.mock("@/composables/useExportTracker", () => ({ useExportTracker: () => ({ addSqlFileTask: mocks.addSqlFileTask, updateSqlFileTask: mocks.updateSqlFileTask }), @@ -175,6 +177,8 @@ function deferred() { beforeEach(() => { vi.clearAllMocks(); mocks.progressHandler = undefined; + mocks.highlighterState = reactive({ ready: false, appearance: "light" }); + mocks.highlight.mockReset().mockImplementation((sql: string) => (mocks.highlighterState!.ready ? `${sql}` : sql)); mocks.trackerTask = undefined; mocks.addSqlFileTask.mockImplementation((exportId: string, tableName: string, filePath: string) => { mocks.trackerTask = reactive({ @@ -241,6 +245,71 @@ afterEach(() => { }); describe("SqlFileExecutionDialog retries", () => { + it("reuses the unchanged preview across progress and failure updates while keeping controls responsive", async () => { + const onOpenChange = vi.fn(); + await mountReadyDialog(onOpenChange); + const gate = deferred(); + mocks.executeSqlFiles.mockImplementationOnce(() => gate.promise); + findButton("sqlFile.execute").click(); + await vi.waitFor(() => expect(mocks.executeSqlFiles).toHaveBeenCalledOnce()); + mocks.highlight.mockClear(); + + try { + for (let statementIndex = 1; statementIndex <= 100; statementIndex += 1) { + mocks.progressHandler?.(progress("run-1", "statementDone", { statementIndex, successCount: statementIndex, bytesRead: statementIndex, totalBytes: 200 })); + await nextTick(); + } + mocks.progressHandler?.(progress("run-1", "statementFailed", { statementIndex: 101, failureCount: 1, error: "statement rejected", statementSummary: "insert into missing_table values (1)" })); + await nextTick(); + + expect(mocks.highlight).not.toHaveBeenCalled(); + expect(root!.textContent).toContain("statement rejected"); + findButton("sqlFile.runInBackground").click(); + expect(onOpenChange).toHaveBeenCalledWith(false); + expect(mocks.cancelSqlFileExecution).not.toHaveBeenCalled(); + findButton("sqlFile.cancel").click(); + await vi.waitFor(() => expect(mocks.cancelSqlFileExecution).toHaveBeenCalledWith("run-1")); + } finally { + mocks.progressHandler?.(progress("run-1", "cancelled")); + gate.resolve(); + } + await vi.waitFor(() => expect(findButton("sqlFile.execute").disabled).toBe(false)); + }); + + it("refreshes the preview after highlighter initialization and appearance changes", async () => { + await mountReadyDialog(); + mocks.highlight.mockClear(); + expect(root!.querySelector("pre")!.textContent).toBe("select 1;"); + + mocks.highlighterState!.ready = true; + await nextTick(); + expect(mocks.highlight).toHaveBeenCalledOnce(); + expect(root!.querySelector("pre span")!.getAttribute("data-theme")).toBe("light"); + + mocks.highlighterState!.appearance = "dark"; + await nextTick(); + expect(mocks.highlight).toHaveBeenCalledTimes(2); + expect(root!.querySelector("pre span")!.getAttribute("data-theme")).toBe("dark"); + expect(root!.querySelector("pre")!.textContent).toBe("select 1;"); + }); + + it("refreshes the preview when switching files and reloading the same path", async () => { + mocks.previewSqlFile.mockImplementation(async (filePath: string) => ({ filePath, fileName: filePath.split("/").pop()!, sizeBytes: 9, preview: filePath.endsWith("first.sql") ? "select 1;" : "select 2;", canExecuteWithoutSelectedDatabase: true })); + await mountReadyDialog(); + mocks.highlight.mockClear(); + + findButton("second.sql").click(); + await nextTick(); + expect(mocks.highlight).toHaveBeenCalledOnce(); + expect(root!.querySelector("pre")!.textContent).toBe("select 2;"); + + mocks.openFileDialog.mockResolvedValueOnce(["/tmp/second.sql"]); + mocks.previewSqlFile.mockResolvedValueOnce({ filePath: "/tmp/second.sql", fileName: "second.sql", sizeBytes: 9, preview: "select 3;", canExecuteWithoutSelectedDatabase: true }); + findButton("sqlFile.browse").click(); + await vi.waitFor(() => expect(root!.querySelector("pre")!.textContent).toBe("select 3;")); + expect(mocks.highlight).toHaveBeenLastCalledWith("select 3;"); + }); + it("allows an ordinary execution to continue in the background and close after completion", async () => { const onOpenChange = vi.fn(); await mountReadyDialog(onOpenChange); diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue index 30a1f28bef..35107112d7 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue @@ -62,6 +62,7 @@ watch(previews, (list) => { const activePreview = computed(() => { return previews.value.find((item) => item.filePath === activePreviewPath.value) ?? previews.value[0] ?? null; }); +const activePreviewHtml = computed(() => (activePreview.value ? highlight(activePreview.value.preview) : "")); // Disambiguate files that share the same fileName. // Desktop: prepend parent directory segments until unique (e.g. migration/create.sql). @@ -847,7 +848,7 @@ watch(
{{ n }}
-

+