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(
-
+