feat: add safe mode entry to native desktop menus

This commit is contained in:
xipian-air
2026-09-05 23:20:03 +08:00
parent 33dcf43548
commit e4275deba7
12 changed files with 298 additions and 20 deletions
@@ -128,7 +128,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
private restartRequest: Promise<void> | undefined
constructor(
private readonly restart: (target?: 'recovery') => Promise<void>,
private readonly restart: (target?: 'recovery' | 'safe-mode') => Promise<void>,
private readonly onRendererBoot: (report: RendererBootReport) => boolean | void = () => {},
private readonly logger: DesktopLogger | undefined = undefined,
workspaceVolumeQuery: WindowsVolumeQuery | undefined = undefined,
@@ -478,7 +478,17 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
await request
}
private async confirmAndRestart(target: 'normal' | 'recovery'): Promise<void> {
async requestSafeModeRestart(): Promise<void> {
if (this.quitting) return
if (this.restartRequest !== undefined) return await this.restartRequest
const request = this.confirmAndRestart('safe-mode').finally(() => {
if (this.restartRequest === request) this.restartRequest = undefined
})
this.restartRequest = request
await request
}
private async confirmAndRestart(target: 'normal' | 'recovery' | 'safe-mode'): Promise<void> {
const copy = desktopRestartConfirmationCopy(this.currentLocale, target)
const options: Electron.MessageBoxOptions = {
type: 'question',
@@ -491,7 +501,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
noLink: true,
}
const result = await this.showDesktopMessageBox(options)
if (result.response === 0) await this.restart(target === 'recovery' ? 'recovery' : undefined)
if (result.response === 0) await this.restart(target === 'normal' ? undefined : target)
}
/** @inheritdoc */
+17 -3
View File
@@ -540,11 +540,17 @@ async function start(): Promise<void> {
throw new Error('dsh-plugin-desktop: shutdown coordinator is not ready')
}
if (restartRequested) return
if (target === 'safe-mode') {
if (prepareSafeMode === undefined) throw new Error(`${BIN_NAME}: Safe Mode is unavailable`)
prepareSafeMode()
}
restartRequested = true
nativeExit.requestRelaunch(
target === 'recovery'
? desktopRecoveryRelaunchArguments()
: desktopDefaultRelaunchArguments(),
target === 'safe-mode'
? desktopSafeModeRelaunchArguments()
: target === 'recovery'
? desktopRecoveryRelaunchArguments()
: desktopDefaultRelaunchArguments(),
)
await shutdown.request(0)
}, (report) => {
@@ -833,6 +839,14 @@ async function start(): Promise<void> {
},
})
generation.own(() => { safeModeTray.dispose() })
} else {
const safeModeTray = runtime.registerTrayItem({
group: 'tools',
order: 100,
label: () => desktopTrayLabel(runtime.locale, 'enterSafeMode'),
invoke: () => runtime.requestSafeModeRestart(),
})
generation.own(() => { safeModeTray.dispose() })
}
const openCompatibilityProfileSelector = async (): Promise<'restart' | 'cancel' | 'unavailable'> => {
const profileActions = startupRecoveryProfileActions
+15 -1
View File
@@ -1,12 +1,14 @@
/** Desktop-owned native tray copy for the locales shipped by DSH. */
import type { DesktopLocale } from './runtime.ts'
import { desktopRecoveryCopy } from './recovery-copy.ts'
export type DesktopTrayLabelKey =
| 'addProfile'
| 'checkForUpdates'
| 'checkingForUpdates'
| 'downloadingUpdate'
| 'enterSafeMode'
| 'exportDiagnostics'
| 'exitSafeMode'
| 'openDesktop'
@@ -26,6 +28,7 @@ const labels: Record<DesktopLocale, Record<DesktopTrayLabelKey, (value: string)
checkForUpdates: () => 'Check for Updates…',
checkingForUpdates: () => 'Checking for Updates…',
downloadingUpdate: version => `Downloading DSH Desktop ${version}…`,
enterSafeMode: () => 'Enter Safe Mode…',
exportDiagnostics: () => 'Export Diagnostics…',
exitSafeMode: () => 'Exit Safe Mode and Restart…',
openDesktop: productName => `Open ${productName}`,
@@ -44,6 +47,7 @@ const labels: Record<DesktopLocale, Record<DesktopTrayLabelKey, (value: string)
checkForUpdates: () => '检查更新…',
checkingForUpdates: () => '正在检查更新…',
downloadingUpdate: version => `正在下载 DSH Desktop ${version}…`,
enterSafeMode: () => '进入安全模式…',
exportDiagnostics: () => '导出诊断信息…',
exitSafeMode: () => '退出安全模式并重启…',
openDesktop: productName => `打开 ${productName}`,
@@ -149,7 +153,17 @@ export function desktopDiagnosticsPrivacyCopy(locale: DesktopLocale): DesktopDia
/** Resolve the native confirmation shown before every ordinary relaunch request. */
export function desktopRestartConfirmationCopy(
locale: DesktopLocale,
target: 'normal' | 'recovery' = 'normal',
target: 'normal' | 'recovery' | 'safe-mode' = 'normal',
): DesktopRestartConfirmationCopy {
if (target === 'safe-mode') {
const copy = desktopRecoveryCopy(locale)
return {
title: copy.confirmSafeMode,
message: copy.confirmSafeModeMessage,
detail: copy.confirmSafeModeBody,
confirm: copy.confirmSafeModeAction,
cancel: copy.cancel,
}
}
return restartConfirmationCopy[locale][target]
}
@@ -2,9 +2,21 @@ import { describe, expect, it } from 'vitest'
import { desktopNativeCopy } from '../src/native-dialog-copy.ts'
import { desktopProfileCreateCopy } from '../src/profile-create-copy.ts'
import { desktopRecoveryCopy } from '../src/recovery-copy.ts'
import { desktopTrayLabel } from '../src/tray-locale.ts'
import { desktopRestartConfirmationCopy, desktopTrayLabel } from '../src/tray-locale.ts'
describe('Desktop product copy', () => {
it.each(['en', 'zh'] as const)('reuses the Safe Mode confirmation in %s native menus', locale => {
const copy = desktopRecoveryCopy(locale)
expect(desktopTrayLabel(locale, 'enterSafeMode')).toBe(locale === 'zh' ? '进入安全模式…' : 'Enter Safe Mode…')
expect(desktopRestartConfirmationCopy(locale, 'safe-mode')).toEqual({
title: copy.confirmSafeMode,
message: copy.confirmSafeModeMessage,
detail: copy.confirmSafeModeBody,
confirm: copy.confirmSafeModeAction,
cancel: copy.cancel,
})
})
it('uses Profile consistently for product-level configuration sets', () => {
expect(desktopProfileCreateCopy('zh')).toMatchObject({
title: '新建 Profile',
@@ -2,6 +2,7 @@ import { readFileSync, unlinkSync } from 'node:fs'
import { basename, dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DesktopShellSpec } from '../src/runtime.ts'
import { desktopTrayLabel } from '../src/tray-locale.ts'
import { DESKTOP_FRAME_HEIGHT } from '../src/window-chrome.ts'
const terminal = vi.hoisted(() => ({ open: vi.fn() }))
@@ -1863,7 +1864,7 @@ describe('Electron desktop runtime', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 1, checkboxChecked: false })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async (_target?: 'recovery') => {})
const restart = vi.fn(async (_target?: 'recovery' | 'safe-mode') => {})
const runtime = new ElectronDesktopRuntime(restart)
await runtime.requestRestart()
@@ -1883,7 +1884,7 @@ describe('Electron desktop runtime', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 0, checkboxChecked: false })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async (_target?: 'recovery') => {})
const restart = vi.fn(async (_target?: 'recovery' | 'safe-mode') => {})
const runtime = new ElectronDesktopRuntime(restart)
await runtime.requestRecoveryRestart()
@@ -1895,6 +1896,91 @@ describe('Electron desktop runtime', () => {
expect(restart).toHaveBeenCalledWith('recovery')
})
it.each([
{ platform: 'win32', surface: 'tray' },
{ platform: 'darwin', surface: 'tray' },
{ platform: 'darwin', surface: 'application' },
] as const)('enters Safe Mode from the localized $platform $surface menu', async ({ platform, surface }) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {})
const runtime = new ElectronDesktopRuntime(restart)
const registration = runtime.registerTrayItem({
group: 'tools',
order: 100,
label: () => desktopTrayLabel(runtime.locale, 'enterSafeMode'),
invoke: () => runtime.requestSafeModeRestart(),
})
const release = runtime.schedule(spec)
await runtime.mountScheduled()
type MenuCommand = { label?: string, enabled?: boolean, click?: () => void }
const menuItems = (): MenuCommand[] => surface === 'application'
? (electron.applicationMenuTemplates.at(-1)?.[0] as { submenu: MenuCommand[] }).submenu
: electron.menuTemplates.at(-1) as MenuCommand[]
expect(menuItems()).toEqual(expect.arrayContaining([
expect.objectContaining({ label: 'Enter Safe Mode…', enabled: true }),
]))
runtime.setLocalePreference('zh')
const command = menuItems().find(item => item.label === '进入安全模式…')
expect(command?.click).toEqual(expect.any(Function))
command?.click?.()
await vi.waitFor(() => { expect(restart).toHaveBeenCalledWith('safe-mode') })
expect(restart).toHaveBeenCalledOnce()
expect(electron.dialog.showMessageBox.mock.calls.at(-1)?.at(-1)).toMatchObject({
type: 'question', title: '进入安全模式?', buttons: ['重启到安全模式', '取消'], defaultId: 1, cancelId: 1,
})
registration.dispose()
expect(menuItems()).not.toEqual(expect.arrayContaining([
expect.objectContaining({ label: '进入安全模式…' }),
]))
await release()
})
it('cancels, coalesces, and suppresses Safe Mode restart requests while quitting', async () => {
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {})
const runtime = new ElectronDesktopRuntime(restart)
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 1, checkboxChecked: false })
await runtime.requestSafeModeRestart()
expect(restart).not.toHaveBeenCalled()
expect(electron.dialog.showMessageBox.mock.calls.at(-1)?.at(-1)).toMatchObject({
title: 'Enter Safe Mode?', buttons: ['Restart in Safe Mode', 'Cancel'], defaultId: 1, cancelId: 1,
})
await Promise.all([
runtime.requestSafeModeRestart(),
runtime.requestSafeModeRestart(),
runtime.requestRestart(),
runtime.requestRecoveryRestart(),
])
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledOnce()
expect(restart).toHaveBeenCalledWith('safe-mode')
runtime.prepareToQuit()
await runtime.requestSafeModeRestart()
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledOnce()
})
it('allows retrying Safe Mode after preparation fails', async () => {
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {}).mockRejectedValueOnce(new Error('Safe Mode preparation failed'))
const runtime = new ElectronDesktopRuntime(restart)
await expect(runtime.requestSafeModeRestart()).rejects.toThrow('Safe Mode preparation failed')
await runtime.requestSafeModeRestart()
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenLastCalledWith('safe-mode')
})
it('uses Electron networking and confirmation-gated macOS update handoff', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
const response = Response.json({ version: '2.1.0' })
@@ -131,6 +131,9 @@ describe('published package surface', () => {
expect(main).toContain('failed to remove the Safe Mode environment')
expect(main).toContain('desktopSafeModeRelaunchArguments()')
expect(main).toContain("desktopTrayLabel(runtime.locale, 'exitSafeMode')")
expect(main).toContain("desktopTrayLabel(runtime.locale, 'enterSafeMode')")
expect(main).toContain('invoke: () => runtime.requestSafeModeRestart()')
expect(main).toContain('prepareSafeMode()\n }\n restartRequested = true')
expect(main).toContain('notifyDesktopSafeModeActive(runtime, electronLogger)')
expect(main).toContain('safeModePaths !== undefined && DESKTOP_SAFE_MODE_DEFAULTS.settings.notifications.enabled')
expect(main).toContain('const setupWizardState = safeModePaths === undefined')
+13 -3
View File
@@ -128,7 +128,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
private restartRequest: Promise<void> | undefined
constructor(
private readonly restart: (target?: 'recovery') => Promise<void>,
private readonly restart: (target?: 'recovery' | 'safe-mode') => Promise<void>,
private readonly onRendererBoot: (report: RendererBootReport) => boolean | void = () => {},
private readonly logger: DesktopLogger | undefined = undefined,
workspaceVolumeQuery: WindowsVolumeQuery | undefined = undefined,
@@ -478,7 +478,17 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
await request
}
private async confirmAndRestart(target: 'normal' | 'recovery'): Promise<void> {
async requestSafeModeRestart(): Promise<void> {
if (this.quitting) return
if (this.restartRequest !== undefined) return await this.restartRequest
const request = this.confirmAndRestart('safe-mode').finally(() => {
if (this.restartRequest === request) this.restartRequest = undefined
})
this.restartRequest = request
await request
}
private async confirmAndRestart(target: 'normal' | 'recovery' | 'safe-mode'): Promise<void> {
const copy = desktopRestartConfirmationCopy(this.currentLocale, target)
const options: Electron.MessageBoxOptions = {
type: 'question',
@@ -491,7 +501,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
noLink: true,
}
const result = await this.showDesktopMessageBox(options)
if (result.response === 0) await this.restart(target === 'recovery' ? 'recovery' : undefined)
if (result.response === 0) await this.restart(target === 'normal' ? undefined : target)
}
/** @inheritdoc */
+17 -3
View File
@@ -513,11 +513,17 @@ async function start(): Promise<void> {
throw new Error('dsh-plugin-desktop: shutdown coordinator is not ready')
}
if (restartRequested) return
if (target === 'safe-mode') {
if (prepareSafeMode === undefined) throw new Error(`${BIN_NAME}: Safe Mode is unavailable`)
prepareSafeMode()
}
restartRequested = true
nativeExit.requestRelaunch(
target === 'recovery'
? desktopRecoveryRelaunchArguments()
: desktopDefaultRelaunchArguments(),
target === 'safe-mode'
? desktopSafeModeRelaunchArguments()
: target === 'recovery'
? desktopRecoveryRelaunchArguments()
: desktopDefaultRelaunchArguments(),
)
await shutdown.request(0)
}, (report) => {
@@ -791,6 +797,14 @@ async function start(): Promise<void> {
},
})
generation.own(() => { safeModeTray.dispose() })
} else {
const safeModeTray = runtime.registerTrayItem({
group: 'tools',
order: 100,
label: () => desktopTrayLabel(runtime.locale, 'enterSafeMode'),
invoke: () => runtime.requestSafeModeRestart(),
})
generation.own(() => { safeModeTray.dispose() })
}
const openCompatibilityProfileSelector = async (): Promise<'restart' | 'cancel' | 'unavailable'> => {
const profileActions = startupRecoveryProfileActions
+15 -1
View File
@@ -1,12 +1,14 @@
/** Desktop-owned native tray copy for the locales shipped by DSH. */
import type { DesktopLocale } from './runtime.ts'
import { desktopRecoveryCopy } from './recovery-copy.ts'
export type DesktopTrayLabelKey =
| 'addProfile'
| 'checkForUpdates'
| 'checkingForUpdates'
| 'downloadingUpdate'
| 'enterSafeMode'
| 'exportDiagnostics'
| 'exitSafeMode'
| 'openDesktop'
@@ -26,6 +28,7 @@ const labels: Record<DesktopLocale, Record<DesktopTrayLabelKey, (value: string)
checkForUpdates: () => 'Check for Updates…',
checkingForUpdates: () => 'Checking for Updates…',
downloadingUpdate: version => `Downloading DSH Desktop ${version}…`,
enterSafeMode: () => 'Enter Safe Mode…',
exportDiagnostics: () => 'Export Diagnostics…',
exitSafeMode: () => 'Exit Safe Mode and Restart…',
openDesktop: productName => `Open ${productName}`,
@@ -44,6 +47,7 @@ const labels: Record<DesktopLocale, Record<DesktopTrayLabelKey, (value: string)
checkForUpdates: () => '检查更新…',
checkingForUpdates: () => '正在检查更新…',
downloadingUpdate: version => `正在下载 DSH Desktop ${version}…`,
enterSafeMode: () => '进入安全模式…',
exportDiagnostics: () => '导出诊断信息…',
exitSafeMode: () => '退出安全模式并重启…',
openDesktop: productName => `打开 ${productName}`,
@@ -149,7 +153,17 @@ export function desktopDiagnosticsPrivacyCopy(locale: DesktopLocale): DesktopDia
/** Resolve the native confirmation shown before every ordinary relaunch request. */
export function desktopRestartConfirmationCopy(
locale: DesktopLocale,
target: 'normal' | 'recovery' = 'normal',
target: 'normal' | 'recovery' | 'safe-mode' = 'normal',
): DesktopRestartConfirmationCopy {
if (target === 'safe-mode') {
const copy = desktopRecoveryCopy(locale)
return {
title: copy.confirmSafeMode,
message: copy.confirmSafeModeMessage,
detail: copy.confirmSafeModeBody,
confirm: copy.confirmSafeModeAction,
cancel: copy.cancel,
}
}
return restartConfirmationCopy[locale][target]
}
+13 -1
View File
@@ -2,9 +2,21 @@ import { describe, expect, it } from 'vitest'
import { desktopNativeCopy } from '../src/native-dialog-copy.ts'
import { desktopProfileCreateCopy } from '../src/profile-create-copy.ts'
import { desktopRecoveryCopy } from '../src/recovery-copy.ts'
import { desktopTrayLabel } from '../src/tray-locale.ts'
import { desktopRestartConfirmationCopy, desktopTrayLabel } from '../src/tray-locale.ts'
describe('Desktop product copy', () => {
it.each(['en', 'zh'] as const)('reuses the Safe Mode confirmation in %s native menus', locale => {
const copy = desktopRecoveryCopy(locale)
expect(desktopTrayLabel(locale, 'enterSafeMode')).toBe(locale === 'zh' ? '进入安全模式…' : 'Enter Safe Mode…')
expect(desktopRestartConfirmationCopy(locale, 'safe-mode')).toEqual({
title: copy.confirmSafeMode,
message: copy.confirmSafeModeMessage,
detail: copy.confirmSafeModeBody,
confirm: copy.confirmSafeModeAction,
cancel: copy.cancel,
})
})
it('uses Profile consistently for product-level configuration sets', () => {
expect(desktopProfileCreateCopy('zh')).toMatchObject({
title: '新建 Profile',
@@ -2,6 +2,7 @@ import { unlinkSync } from 'node:fs'
import { basename, dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DesktopShellSpec } from '../src/runtime.ts'
import { desktopTrayLabel } from '../src/tray-locale.ts'
import { DESKTOP_FRAME_HEIGHT } from '../src/window-chrome.ts'
const terminal = vi.hoisted(() => ({ open: vi.fn() }))
@@ -1860,7 +1861,7 @@ describe('Electron desktop runtime', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 1, checkboxChecked: false })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async (_target?: 'recovery') => {})
const restart = vi.fn(async (_target?: 'recovery' | 'safe-mode') => {})
const runtime = new ElectronDesktopRuntime(restart)
await runtime.requestRestart()
@@ -1880,7 +1881,7 @@ describe('Electron desktop runtime', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 0, checkboxChecked: false })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async (_target?: 'recovery') => {})
const restart = vi.fn(async (_target?: 'recovery' | 'safe-mode') => {})
const runtime = new ElectronDesktopRuntime(restart)
await runtime.requestRecoveryRestart()
@@ -1892,6 +1893,91 @@ describe('Electron desktop runtime', () => {
expect(restart).toHaveBeenCalledWith('recovery')
})
it.each([
{ platform: 'win32', surface: 'tray' },
{ platform: 'darwin', surface: 'tray' },
{ platform: 'darwin', surface: 'application' },
] as const)('enters Safe Mode from the localized $platform $surface menu', async ({ platform, surface }) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {})
const runtime = new ElectronDesktopRuntime(restart)
const registration = runtime.registerTrayItem({
group: 'tools',
order: 100,
label: () => desktopTrayLabel(runtime.locale, 'enterSafeMode'),
invoke: () => runtime.requestSafeModeRestart(),
})
const release = runtime.schedule(spec)
await runtime.mountScheduled()
type MenuCommand = { label?: string, enabled?: boolean, click?: () => void }
const menuItems = (): MenuCommand[] => surface === 'application'
? (electron.applicationMenuTemplates.at(-1)?.[0] as { submenu: MenuCommand[] }).submenu
: electron.menuTemplates.at(-1) as MenuCommand[]
expect(menuItems()).toEqual(expect.arrayContaining([
expect.objectContaining({ label: 'Enter Safe Mode…', enabled: true }),
]))
runtime.setLocalePreference('zh')
const command = menuItems().find(item => item.label === '进入安全模式…')
expect(command?.click).toEqual(expect.any(Function))
command?.click?.()
await vi.waitFor(() => { expect(restart).toHaveBeenCalledWith('safe-mode') })
expect(restart).toHaveBeenCalledOnce()
expect(electron.dialog.showMessageBox.mock.calls.at(-1)?.at(-1)).toMatchObject({
type: 'question', title: '进入安全模式?', buttons: ['重启到安全模式', '取消'], defaultId: 1, cancelId: 1,
})
registration.dispose()
expect(menuItems()).not.toEqual(expect.arrayContaining([
expect.objectContaining({ label: '进入安全模式…' }),
]))
await release()
})
it('cancels, coalesces, and suppresses Safe Mode restart requests while quitting', async () => {
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {})
const runtime = new ElectronDesktopRuntime(restart)
electron.dialog.showMessageBox.mockResolvedValueOnce({ response: 1, checkboxChecked: false })
await runtime.requestSafeModeRestart()
expect(restart).not.toHaveBeenCalled()
expect(electron.dialog.showMessageBox.mock.calls.at(-1)?.at(-1)).toMatchObject({
title: 'Enter Safe Mode?', buttons: ['Restart in Safe Mode', 'Cancel'], defaultId: 1, cancelId: 1,
})
await Promise.all([
runtime.requestSafeModeRestart(),
runtime.requestSafeModeRestart(),
runtime.requestRestart(),
runtime.requestRecoveryRestart(),
])
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledOnce()
expect(restart).toHaveBeenCalledWith('safe-mode')
runtime.prepareToQuit()
await runtime.requestSafeModeRestart()
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledOnce()
})
it('allows retrying Safe Mode after preparation fails', async () => {
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const restart = vi.fn(async () => {}).mockRejectedValueOnce(new Error('Safe Mode preparation failed'))
const runtime = new ElectronDesktopRuntime(restart)
await expect(runtime.requestSafeModeRestart()).rejects.toThrow('Safe Mode preparation failed')
await runtime.requestSafeModeRestart()
expect(electron.dialog.showMessageBox).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenCalledTimes(2)
expect(restart).toHaveBeenLastCalledWith('safe-mode')
})
it('uses Electron networking and confirmation-gated macOS update handoff', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
const response = Response.json({ version: '2.1.0' })
+3
View File
@@ -102,6 +102,9 @@ describe('published package surface', () => {
expect(main).toContain('failed to remove the Safe Mode environment')
expect(main).toContain('desktopSafeModeRelaunchArguments()')
expect(main).toContain("desktopTrayLabel(runtime.locale, 'exitSafeMode')")
expect(main).toContain("desktopTrayLabel(runtime.locale, 'enterSafeMode')")
expect(main).toContain('invoke: () => runtime.requestSafeModeRestart()')
expect(main).toContain('prepareSafeMode()\n }\n restartRequested = true')
expect(main).toContain('notifyDesktopSafeModeActive(runtime, electronLogger)')
expect(main).toContain('safeModePaths !== undefined && DESKTOP_SAFE_MODE_DEFAULTS.settings.notifications.enabled')
expect(main).toContain('const setupWizardState = safeModePaths === undefined')