diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 2dbd2d807..ff1b7d0f8 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -144,10 +144,11 @@ test("external SQL file paths persist with open query tabs", async () => { const tabId = store.createTab("conn-1", "db", "draft.sql"); store.updateSql(tabId, "select 1;"); store.linkExternalSqlPath(tabId, "/tmp/draft.sql", "draft.sql"); - store.flushPendingPersist(); + await store.flushPendingPersist(); setActivePinia(createPinia()); store = useQueryStore(); + await store.initOpenTabs(); const tab = store.tabs.find((item) => item.id === tabId); assert.equal(tab?.externalSqlPath, "/tmp/draft.sql"); @@ -177,13 +178,14 @@ test("clean saved SQL tabs persist without duplicating SQL text", async () => { createdAt: "2026-06-27T00:00:00.000Z", updatedAt: "2026-06-27T00:00:00.000Z", }); - store.flushPendingPersist(); + await store.flushPendingPersist(); - const rawTabs = localStorage.getItem("dbx-open-tabs") ?? ""; + const rawTabs = localStorage.getItem("dbx-app-state:open_tabs") ?? ""; assert.equal(rawTabs.includes("large_table"), false); setActivePinia(createPinia()); store = useQueryStore(); + await store.initOpenTabs(); const tab = store.tabs.find((item) => item.savedSqlId === "saved-1"); assert.equal(tab?.sql, ""); @@ -209,13 +211,14 @@ test("dirty saved SQL tabs keep unsaved edits in open tab persistence", async () updatedAt: "2026-06-27T00:00:00.000Z", }); store.updateSql(tabId, "SELECT 2;"); - store.flushPendingPersist(); + await store.flushPendingPersist(); - const rawTabs = localStorage.getItem("dbx-open-tabs") ?? ""; + const rawTabs = localStorage.getItem("dbx-app-state:open_tabs") ?? ""; assert.equal(rawTabs.includes("SELECT 2;"), true); setActivePinia(createPinia()); store = useQueryStore(); + await store.initOpenTabs(); const tab = store.tabs.find((item) => item.savedSqlId === "saved-1"); assert.equal(tab?.sql, "SELECT 2;"); @@ -452,7 +455,7 @@ test("close other fixed tabs does not close regular tabs", () => { ); }); -test("close other tabs pauses on restored unsaved query tabs", () => { +test("close other tabs pauses on restored unsaved query tabs", async () => { const restoreStorage = installMemoryStorage(); try { localStorage.setItem( @@ -479,6 +482,7 @@ test("close other tabs pauses on restored unsaved query tabs", () => { localStorage.setItem("dbx-active-tab", "b"); setActivePinia(createPinia()); const store = useQueryStore(); + await store.initOpenTabs(); store.closeOtherTabs("b"); diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts index d7180518f..8beae79f7 100644 --- a/packages/app-tests/settingsStore.test.ts +++ b/packages/app-tests/settingsStore.test.ts @@ -9,7 +9,7 @@ import { AI_PROVIDER_PRESETS, DEFAULT_EDITOR_SETTINGS, normalizeAiConfig, normal const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size"; -function withMockLocalStorage(initial: Record, run: () => void) { +async function withMockLocalStorage(initial: Record, run: () => void | Promise) { const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage"); const values = new Map(Object.entries(initial)); const localStorageMock = { @@ -31,7 +31,7 @@ function withMockLocalStorage(initial: Record, run: () => void) }); try { - run(); + await run(); } finally { if (previousDescriptor) { Object.defineProperty(globalThis, "localStorage", previousDescriptor); @@ -60,26 +60,28 @@ test("defaults export batch size to 2000 rows", () => { assert.equal(normalizeEditorSettings({ exportBatchSize: 2000 }).exportBatchSize, 2000); }); -test("migrates the legacy saved export batch default to 2000 once", () => { - withMockLocalStorage({ "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }) }, () => { +test("migrates the legacy saved export batch default to 2000 once", async () => { + await withMockLocalStorage({ "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }) }, async () => { setActivePinia(createPinia()); const store = useSettingsStore(); + await store.initEditorSettings(); assert.equal(store.editorSettings.exportBatchSize, 2000); - assert.equal(localStorage.getItem("dbx-export-batch-size-default-migrated-v1"), "1"); - assert.equal(JSON.parse(localStorage.getItem("dbx-editor-settings") || "{}").exportBatchSize, 2000); + assert.equal(localStorage.getItem("dbx-editor-settings"), null); + assert.equal(JSON.parse(localStorage.getItem("dbx-app-state:editor_settings") || "{}").exportBatchSize, 2000); }); }); -test("keeps a manually saved 10000 export batch size after migration", () => { - withMockLocalStorage( +test("keeps a manually saved 10000 export batch size after migration", async () => { + await withMockLocalStorage( { "dbx-editor-settings": JSON.stringify({ exportBatchSize: 10000 }), "dbx-export-batch-size-default-migrated-v1": "1", }, - () => { + async () => { setActivePinia(createPinia()); const store = useSettingsStore(); + await store.initEditorSettings(); assert.equal(store.editorSettings.exportBatchSize, 10000); }, @@ -441,8 +443,8 @@ test("keeps SQL formatter default objects distinct", () => { assert.notEqual(normalized.sqlFormatter, DEFAULT_SQL_FORMATTER_SETTINGS); }); -test("does not leak default-loaded SQL formatter mutations into defaults", () => { - withMockLocalStorage({}, () => { +test("does not leak default-loaded SQL formatter mutations into defaults", async () => { + await withMockLocalStorage({}, async () => { setActivePinia(createPinia()); const store = useSettingsStore(); const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase; @@ -462,10 +464,11 @@ test("does not leak default-loaded SQL formatter mutations into defaults", () => }); }); -test("does not leak migrated SQL formatter mutations into defaults", () => { - withMockLocalStorage({ [OLD_FONT_SIZE_KEY]: "18" }, () => { +test("does not leak migrated SQL formatter mutations into defaults", async () => { + await withMockLocalStorage({ [OLD_FONT_SIZE_KEY]: "18" }, async () => { setActivePinia(createPinia()); const store = useSettingsStore(); + await store.initEditorSettings(); const editorDefaultKeywordCase = DEFAULT_EDITOR_SETTINGS.sqlFormatter.keywordCase; const formatterDefaultKeywordCase = DEFAULT_SQL_FORMATTER_SETTINGS.keywordCase; diff --git a/packages/app-tests/useDataGridExport.test.ts b/packages/app-tests/useDataGridExport.test.ts index b01647284..8132d1577 100644 --- a/packages/app-tests/useDataGridExport.test.ts +++ b/packages/app-tests/useDataGridExport.test.ts @@ -9,6 +9,7 @@ const apiMock = vi.hoisted(() => ({ cancelQueryResultExport: vi.fn(), startTableExport: vi.fn(), cancelTableExport: vi.fn(), + saveEditorSettings: vi.fn(async () => {}), exportQueryResultCsv: vi.fn(), exportQueryResultXlsx: vi.fn(), exportQueryResultJson: vi.fn(), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 062d38809..ff3652fdf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,10 +12,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; #[cfg(target_os = "macos")] +use tauri::menu::Menu; +#[cfg(target_os = "macos")] use tauri::menu::{AboutMetadata, MenuItem, PredefinedMenuItem, Submenu}; use tauri::RunEvent; use tauri::{ - menu::{Menu, MenuBuilder}, + menu::MenuBuilder, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, }; use tauri::{Emitter, Manager}; @@ -24,6 +26,7 @@ use tauri_plugin_deep_link::DeepLinkExt; const DESKTOP_TRAY_ID: &str = "main-tray"; const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested"; +#[cfg(target_os = "macos")] const APP_MENU_QUIT_ID: &str = "app-menu-quit"; pub struct CloseBehaviorState { @@ -39,10 +42,6 @@ impl CloseBehaviorState { self.confirmed_exit.store(true, Ordering::Relaxed); } - pub(crate) fn is_exit_confirmed(&self) -> bool { - self.confirmed_exit.load(Ordering::Relaxed) - } - fn take_confirmed_exit(&self) -> bool { self.confirmed_exit.swap(false, Ordering::Relaxed) } @@ -67,8 +66,8 @@ fn should_show_main_window_after_setup() -> bool { true } -fn should_confirm_app_exit_request(exit_code: Option, confirmed_exit: bool) -> bool { - exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit +fn should_confirm_app_exit_request(target_os: &str, exit_code: Option, confirmed_exit: bool) -> bool { + should_hide_window_on_close(target_os) && exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit } fn native_window_decorations_override(target_os: &str) -> Option { @@ -449,10 +448,11 @@ mod tests { #[test] fn only_user_requested_app_exit_needs_frontend_confirmation() { - assert!(should_confirm_app_exit_request(None, false)); - assert!(should_confirm_app_exit_request(Some(0), false)); - assert!(!should_confirm_app_exit_request(Some(0), true)); - assert!(!should_confirm_app_exit_request(Some(tauri::RESTART_EXIT_CODE), false)); + assert!(should_confirm_app_exit_request("windows", None, false)); + assert!(should_confirm_app_exit_request("macos", Some(0), false)); + assert!(!should_confirm_app_exit_request("windows", Some(0), true)); + assert!(!should_confirm_app_exit_request("windows", Some(tauri::RESTART_EXIT_CODE), false)); + assert!(!should_confirm_app_exit_request("linux", Some(0), false)); } #[test] @@ -1114,7 +1114,7 @@ pub fn run() { .try_state::() .map(|state| state.take_confirmed_exit()) .unwrap_or(false); - if should_confirm_app_exit_request(*code, confirmed_exit) { + if should_confirm_app_exit_request(std::env::consts::OS, *code, confirmed_exit) { api.prevent_exit(); request_app_close(app_handle, "quit"); }