fix(sql-file): cache preview highlighting during progress updates
Refs: #9537
This commit is contained in:
2 files changed
+72
-2
No files matched your search
@@ -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 ? `<span data-theme="${mocks.highlighterState!.appearance}">${sql}</span>` : 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);
|
||||
|
||||
@@ -62,6 +62,7 @@ watch(previews, (list) => {
|
||||
const activePreview = computed<SqlFilePreview | null>(() => {
|
||||
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(
|
||||
<div class="sticky left-0 z-10 select-none border-r bg-background/95 px-2 py-3 text-right font-mono leading-5 text-muted-foreground/70">
|
||||
<div v-for="n in previewLineCount(activePreview)" :key="n">{{ n }}</div>
|
||||
</div>
|
||||
<pre class="min-w-max flex-1 p-3 font-mono leading-5 whitespace-pre" v-html="highlight(activePreview.preview)"></pre>
|
||||
<pre class="min-w-max flex-1 p-3 font-mono leading-5 whitespace-pre" v-html="activePreviewHtml"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user