mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 01:47:06 +08:00
feat: add safe mode entry to native desktop menus
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { 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' })
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user