diff --git a/apps/desktop/src/components/editor/CloudSyncSelectionDialog.vue b/apps/desktop/src/components/editor/CloudSyncSelectionDialog.vue index ec00933598..b57d6a9fd5 100644 --- a/apps/desktop/src/components/editor/CloudSyncSelectionDialog.vue +++ b/apps/desktop/src/components/editor/CloudSyncSelectionDialog.vue @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import type { SyncCatalogItem, SyncSelection, SyncSnapshotCatalog } from "@/lib/backend/api"; import { transferCategoryForKey, type SettingsTransferCategoryId } from "@/lib/settings/settingsTransfer"; +import { clonePluginData } from "@/lib/plugins/pluginData"; type BackupSettingsCategoryId = Exclude | "ai"; @@ -271,7 +272,7 @@ function confirmSelection() { selection.value.desktopSettings = selectedSettings(selection.value.desktopSettings, selectableDesktopSettings(props.catalog)); selection.value.editorSettings = selectedSettings(selection.value.editorSettings, selectableEditorSettings(props.catalog)); } - emit("confirm", structuredClone(selection.value)); + emit("confirm", clonePluginData(selection.value)); emit("update:open", false); } diff --git a/apps/desktop/src/components/editor/__tests__/CloudSyncSelectionDialog.spec.ts b/apps/desktop/src/components/editor/__tests__/CloudSyncSelectionDialog.spec.ts new file mode 100644 index 0000000000..ccfd7facba --- /dev/null +++ b/apps/desktop/src/components/editor/__tests__/CloudSyncSelectionDialog.spec.ts @@ -0,0 +1,111 @@ +// @vitest-environment happy-dom + +import { createApp, defineComponent, h, isProxy, nextTick, ref } from "vue"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { SyncSelection, SyncSnapshotCatalog } from "@/lib/backend/api"; + +function passthrough(tag: string) { + return defineComponent({ + inheritAttrs: false, + setup(_, { attrs, slots }) { + return () => h(tag, attrs, slots.default?.()); + }, + }); +} + +vi.mock("vue-i18n", () => ({ + useI18n: () => ({ t: (key: string) => key }), +})); +vi.mock("@/components/ui/button", () => ({ Button: passthrough("button") })); +vi.mock("@/components/ui/dialog", () => ({ + Dialog: defineComponent({ + props: { open: Boolean }, + setup(props, { slots }) { + return () => (props.open ? h("div", slots.default?.()) : null); + }, + }), + DialogContent: passthrough("div"), + DialogDescription: passthrough("div"), + DialogFooter: passthrough("div"), + DialogHeader: passthrough("div"), + DialogTitle: passthrough("div"), +})); + +import CloudSyncSelectionDialog from "@/components/editor/CloudSyncSelectionDialog.vue"; + +const catalog: SyncSnapshotCatalog = { + exportedAt: "2026-09-29T00:00:00Z", + appVersion: "0.6.27", + hasEncryptedSecrets: false, + connections: [{ id: "connection-1", label: "Primary" }], + connectionSecrets: [], + tunnelProfiles: [], + tunnelSecrets: [], + savedSqlFolders: [], + savedSqlFiles: [], + desktopSettings: [], + editorSettings: [], + aiConfigs: [], + aiConfigsLocked: false, + pluginUiStorage: [], + pluginUiStorageLocked: false, + hasSidebarLayout: false, + hasPinnedTreeNodeIds: false, +}; + +let app: ReturnType | undefined; +let root: HTMLDivElement | undefined; + +afterEach(() => { + app?.unmount(); + root?.remove(); + app = undefined; + root = undefined; +}); + +describe.each([ + ["restore", "settings.syncSelectionRestoreAction"], + ["upload", "settings.syncSelectionUploadAction"], +] as const)("CloudSyncSelectionDialog %s confirmation", (mode, actionLabel) => { + it("emits a detached plain selection and closes", async () => { + const confirmed: SyncSelection[] = []; + const openUpdates: boolean[] = []; + + root = document.createElement("div"); + document.body.append(root); + app = createApp( + defineComponent({ + setup() { + const open = ref(true); + const reactiveCatalog = ref(catalog); + return () => + h(CloudSyncSelectionDialog, { + open: open.value, + mode, + catalog: reactiveCatalog.value, + "onUpdate:open": (value: boolean) => { + openUpdates.push(value); + open.value = value; + }, + onConfirm: (selection: SyncSelection) => confirmed.push(selection), + }); + }, + }), + ); + app.mount(root); + await nextTick(); + + const confirmButton = Array.from(root.querySelectorAll("button")).find((button) => button.textContent?.trim() === actionLabel); + expect(confirmButton).toBeDefined(); + + confirmButton?.click(); + await nextTick(); + + expect(confirmed).toHaveLength(1); + expect(isProxy(confirmed[0])).toBe(false); + expect(() => structuredClone(confirmed[0])).not.toThrow(); + expect(confirmed[0]).toMatchObject({ connections: ["connection-1"] }); + expect(openUpdates).toEqual([false]); + expect(root.textContent).not.toContain(actionLabel); + }); +});