From 2b7eb412c78fc17019b743c62f7505f43f648188 Mon Sep 17 00:00:00 2001 From: AnRil Date: Fri, 22 May 2026 23:26:11 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20export/import=20=E2=80=94=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?error=20toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: при отмене save-dialog или open-dialog DataCard показывал тост «Не удалось сохранить» / «Файл не подошёл». Но cancel — это не ошибка. Расширил IPC возврат: { ok, canceled, path?, error? }. UI теперь различает: ok → success toast, !ok && canceled → молча, !ok && !canceled → error toast. +9 тестов на validateSettingsPatch для voicePromptsEnabled, meetingAutoPause, lastSeenVersion (semver-regex / null-сброс / malformed). Итого 159 → 168 тестов. Settings → About теперь показывает текущую версию приложения (раньше была только кнопка «Что нового»). Загружается через IPC.getAppVersion при mount. --- src/main/ipc.ts | 16 +++++--- src/main/validate.test.ts | 59 +++++++++++++++++++++++++++++ src/preload/index.ts | 15 ++++++-- src/renderer/src/pages/Settings.tsx | 6 ++- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index df8c1f2..99a3c19 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -352,12 +352,16 @@ export function registerIpc(): void { defaultPath, filters: [{ name: 'JSON', extensions: ['json'] }] }) - if (result.canceled || !result.filePath) return { ok: false, path: null } + // Cancel — это не ошибка. Возвращаем canceled=true чтобы UI мог + // ничего не показывать (без error toast). + if (result.canceled || !result.filePath) { + return { ok: false, canceled: true, path: null } + } try { writeFileSync(result.filePath, exportState(), 'utf-8') - return { ok: true, path: result.filePath } + return { ok: true, canceled: false, path: result.filePath } } catch (e) { - return { ok: false, path: null, error: String(e) } + return { ok: false, canceled: false, path: null, error: String(e) } } }) @@ -369,7 +373,7 @@ export function registerIpc(): void { filters: [{ name: 'JSON', extensions: ['json'] }] }) if (result.canceled || result.filePaths.length === 0) { - return { ok: false } + return { ok: false, canceled: true } } try { const raw = readFileSync(result.filePaths[0], 'utf-8') @@ -378,9 +382,9 @@ export function registerIpc(): void { broadcastState() broadcastHistoryChanged() } - return { ok } + return { ok, canceled: false } } catch (e) { - return { ok: false, error: String(e) } + return { ok: false, canceled: false, error: String(e) } } }) } diff --git a/src/main/validate.test.ts b/src/main/validate.test.ts index 41f7ade..0a3a326 100644 --- a/src/main/validate.test.ts +++ b/src/main/validate.test.ts @@ -266,6 +266,65 @@ describe('validateSettingsPatch', () => { expect(validateSettingsPatch({ snoozeMinutes: -5 })).toBeNull() }) + it('accepts voicePromptsEnabled boolean', () => { + expect(validateSettingsPatch({ voicePromptsEnabled: true })).toEqual({ + voicePromptsEnabled: true + }) + expect(validateSettingsPatch({ voicePromptsEnabled: false })).toEqual({ + voicePromptsEnabled: false + }) + }) + + it('rejects non-boolean voicePromptsEnabled in patch', () => { + expect(validateSettingsPatch({ voicePromptsEnabled: 'yes' })).toBeNull() + expect(validateSettingsPatch({ voicePromptsEnabled: 1 })).toBeNull() + }) + + it('accepts meetingAutoPause boolean', () => { + expect(validateSettingsPatch({ meetingAutoPause: true })).toEqual({ + meetingAutoPause: true + }) + }) + + it('rejects non-boolean meetingAutoPause', () => { + expect(validateSettingsPatch({ meetingAutoPause: 'yes' })).toBeNull() + }) + + describe('lastSeenVersion', () => { + it('accepts valid semver', () => { + const r = validateSettingsPatch({ lastSeenVersion: '0.5.7' }) + expect(r?.lastSeenVersion).toBe('0.5.7') + expect(validateSettingsPatch({ lastSeenVersion: '10.20.30' })).toEqual({ + lastSeenVersion: '10.20.30' + }) + }) + + it('accepts pre-release suffix', () => { + const r = validateSettingsPatch({ lastSeenVersion: '0.5.7-beta.1' }) + expect(r?.lastSeenVersion).toBe('0.5.7-beta.1') + }) + + it('treats null/undefined as reset to undefined', () => { + const r1 = validateSettingsPatch({ lastSeenVersion: null }) + expect(r1).toEqual({ lastSeenVersion: undefined }) + const r2 = validateSettingsPatch({ lastSeenVersion: undefined }) + // 'lastSeenVersion' is `in raw` even if undefined — both treated reset. + expect(r2).toEqual({ lastSeenVersion: undefined }) + }) + + it('rejects malformed strings', () => { + expect(validateSettingsPatch({ lastSeenVersion: '0.5' })).toBeNull() + expect(validateSettingsPatch({ lastSeenVersion: 'v0.5.7' })).toBeNull() + expect(validateSettingsPatch({ lastSeenVersion: 'beta' })).toBeNull() + expect(validateSettingsPatch({ lastSeenVersion: '' })).toBeNull() + }) + + it('rejects non-strings', () => { + expect(validateSettingsPatch({ lastSeenVersion: 42 })).toBeNull() + expect(validateSettingsPatch({ lastSeenVersion: ['1', '0', '0'] })).toBeNull() + }) + }) + describe('quietHours subobject', () => { const baseQh = { enabled: true, diff --git a/src/preload/index.ts b/src/preload/index.ts index 587e3da..1422656 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -122,10 +122,17 @@ const api = { ipcRenderer.invoke(IPC.clearHistory, beforeTs), // Export / Import — открывают native save/open dialogs из main process. - exportState: (): Promise<{ ok: boolean; path: string | null }> => - ipcRenderer.invoke(IPC.exportState), - importState: (): Promise<{ ok: boolean; error?: string }> => - ipcRenderer.invoke(IPC.importState), + exportState: (): Promise<{ + ok: boolean + canceled: boolean + path: string | null + error?: string + }> => ipcRenderer.invoke(IPC.exportState), + importState: (): Promise<{ + ok: boolean + canceled: boolean + error?: string + }> => ipcRenderer.invoke(IPC.importState), onTick: (h: Handler): Unsub => on(IPC.evtTick, h), onFire: (h: Handler): Unsub => on(IPC.evtFire, h), diff --git a/src/renderer/src/pages/Settings.tsx b/src/renderer/src/pages/Settings.tsx index 26b77ed..4150227 100644 --- a/src/renderer/src/pages/Settings.tsx +++ b/src/renderer/src/pages/Settings.tsx @@ -261,7 +261,8 @@ function DataCard(): JSX.Element { const r = await window.api.exportState() if (r.ok && r.path) { setToast(t('settings.data.export.ok', { path: r.path })) - } else if (!r.ok) { + } else if (!r.ok && !r.canceled) { + // canceled — пользователь сам передумал, тост не нужен. setToast(t('settings.data.export.err')) } } finally { @@ -275,7 +276,8 @@ function DataCard(): JSX.Element { try { const r = await window.api.importState() if (r.ok) setToast(t('settings.data.import.ok')) - else if ('error' in r && r.error !== undefined) { + else if (!r.canceled) { + // canceled — пользователь не выбрал файл, не показываем error. setToast(t('settings.data.import.err')) } } finally {