From ddb6a2eba851cdd420ece17c29df600069c307bf Mon Sep 17 00:00:00 2001 From: axb Date: Thu, 30 Jul 2026 14:43:29 +0800 Subject: [PATCH] fix(wework): restore main window close flow --- wework/AGENTS.md | 2 + wework/e2e/desktop/task-flow.e2e.mjs | 19 ++++++- wework/scripts/ai-verify.mjs | 6 ++- .../layout/WindowFrameControls.test.tsx | 17 +------ .../components/layout/WindowFrameControls.tsx | 10 +--- wework/src/e2e/automation.ts | 4 ++ .../workbench/RuntimeTaskCloseGuard.test.tsx | 50 +++++++++++++++++++ .../workbench/RuntimeTaskCloseGuard.tsx | 5 +- 8 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 wework/src/features/workbench/RuntimeTaskCloseGuard.test.tsx diff --git a/wework/AGENTS.md b/wework/AGENTS.md index f2773da0dd..6805f9d6a9 100644 --- a/wework/AGENTS.md +++ b/wework/AGENTS.md @@ -55,6 +55,7 @@ pnpm --filter wework ai:verify hover --session --selector '[data- pnpm --filter wework ai:verify pointer-move --session --selector 'body' pnpm --filter wework ai:verify wait-for --session --selector '[data-testid="..."]' --text '...' pnpm --filter wework ai:verify capture --session --output +pnpm --filter wework ai:verify request-close --session pnpm --filter wework ai:verify close-to-tray --session pnpm --filter wework ai:verify stop --session ``` @@ -65,6 +66,7 @@ pnpm --filter wework ai:verify stop --session - Begin with `snapshot`, use existing `data-testid` selectors, and assert a visible text or stable element after each critical action. - Execute the complete QA test plan in the isolated Tauri session, including the primary path, relevant boundary and error cases, and recovery. Document the environment, cases run, actual results, and evidence in the change handoff or pull request. - Use `capture` after the final assertion when a visual verification artifact is required. It renders the current WebView without macOS screen-recording permission. +- Use `request-close` to exercise the native main-window close request and its close-to-tray preference or confirmation flow. - Use `close-to-tray` only for window-lifecycle verification. It destroys the controlled WebView while leaving the isolated Tauri process running so native reopen behavior can be tested. - On failure, inspect `app.log`, `executor.log`, and Tauri logs under `test-results/ai-verify/`; do not silently downgrade to mocked verification. diff --git a/wework/e2e/desktop/task-flow.e2e.mjs b/wework/e2e/desktop/task-flow.e2e.mjs index afd91b6f6e..b6c45298f4 100644 --- a/wework/e2e/desktop/task-flow.e2e.mjs +++ b/wework/e2e/desktop/task-flow.e2e.mjs @@ -3639,6 +3639,18 @@ async function verifyBackgroundTaskWindowLifecycle({ const lifecycleScreenshotName = name => `window-lifecycle-${name}` setPhase('popout-window-lifecycle') await verifyPopoutWindowLifecycle(control, composerSelector) + setPhase('close-request-without-running-task') + await control.command('requestMainWindowClose', 'body') + await control.command('waitFor', '[data-testid="runtime-task-close-confirm-overlay"]', { + visible: true, + timeoutMs: DEFAULT_STEP_TIMEOUT_MS, + }) + await control.command('click', '[data-testid="runtime-task-close-cancel-button"]') + const closeCancelledSnapshot = JSON.parse(await control.command('snapshot', 'body')) + assert.ok( + !closeCancelledSnapshot.testIds.includes('runtime-task-close-confirm-overlay'), + 'The close-to-tray prompt remained open after cancelling' + ) setPhase('background-streaming-task') control.setScenario('window_lifecycle') await control.command('click', '[data-testid="new-chat-button"]') @@ -3703,7 +3715,12 @@ async function verifyBackgroundTaskWindowLifecycle({ 'The executor process was not alive before close' ) - await control.command('closeMainWindowToTray', 'body') + await control.command('requestMainWindowClose', 'body') + await control.command('waitFor', '[data-testid="runtime-task-close-confirm-overlay"]', { + visible: true, + timeoutMs: DEFAULT_STEP_TIMEOUT_MS, + }) + await control.command('click', '[data-testid="runtime-task-close-confirm-button"]') await waitForLogPattern(join(resultDir, `wework-tauri-${app.pid}.log`), /windowWillClose:/) assert.equal(processIsAlive(app.pid), true, 'Closing to tray terminated the Wework process') assert.equal( diff --git a/wework/scripts/ai-verify.mjs b/wework/scripts/ai-verify.mjs index 786196a23d..e993bba84f 100644 --- a/wework/scripts/ai-verify.mjs +++ b/wework/scripts/ai-verify.mjs @@ -27,7 +27,7 @@ const corsHeaders = { function usage() { console.error(`Usage: pnpm --filter wework ai:verify start - pnpm --filter wework ai:verify --session PATH [options] + pnpm --filter wework ai:verify --session PATH [options] Options: --codex-home-initialization true @@ -386,6 +386,7 @@ async function main() { snapshot: 'snapshot', click: 'click', 'close-to-tray': 'closeMainWindowToTray', + 'request-close': 'requestMainWindowClose', 'dismiss-popout': 'dismissPopoutWindow', drag: 'drag', 'drop-file': 'dropFile', @@ -422,7 +423,8 @@ async function main() { command === 'show-popout' || command === 'system-drag-drop' || command === 'window-focus-snapshot' || - command === 'close-to-tray' + command === 'close-to-tray' || + command === 'request-close' ? 'body' : null) if (!selector) throw new Error('--selector is required') diff --git a/wework/src/components/layout/WindowFrameControls.test.tsx b/wework/src/components/layout/WindowFrameControls.test.tsx index c319b1a1d5..8f4bdd2405 100644 --- a/wework/src/components/layout/WindowFrameControls.test.tsx +++ b/wework/src/components/layout/WindowFrameControls.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { describe, expect, test, vi, beforeEach } from 'vitest' -import { invoke } from '@tauri-apps/api/core' import { WindowFrameControls } from './WindowFrameControls' const mocks = vi.hoisted(() => { @@ -20,10 +19,6 @@ vi.mock('@tauri-apps/api/window', () => ({ getCurrentWindow: vi.fn(() => mocks.windowMock), })) -vi.mock('@tauri-apps/api/core', () => ({ - invoke: vi.fn().mockResolvedValue(undefined), -})) - describe('WindowFrameControls', () => { beforeEach(() => { vi.clearAllMocks() @@ -78,19 +73,9 @@ describe('WindowFrameControls', () => { expect(mocks.windowMock.maximize).not.toHaveBeenCalled() }) - test('close button invokes close_main_window_to_tray command', async () => { - render() - fireEvent.click(screen.getByTestId('window-close-button')) - await waitFor(() => expect(invoke).toHaveBeenCalledWith('close_main_window_to_tray')) - }) - - test('falls back to window.close when close_main_window_to_tray command fails', async () => { - const invokeMock = invoke as unknown as ReturnType - invokeMock.mockRejectedValueOnce(new Error('command unavailable')) - + test('close button requests the standard window close flow', async () => { render() fireEvent.click(screen.getByTestId('window-close-button')) - await waitFor(() => expect(invoke).toHaveBeenCalledWith('close_main_window_to_tray')) await waitFor(() => expect(mocks.windowMock.close).toHaveBeenCalledTimes(1)) }) diff --git a/wework/src/components/layout/WindowFrameControls.tsx b/wework/src/components/layout/WindowFrameControls.tsx index 3067132f8f..a4cbb95c25 100644 --- a/wework/src/components/layout/WindowFrameControls.tsx +++ b/wework/src/components/layout/WindowFrameControls.tsx @@ -1,7 +1,6 @@ import { Minus, Square, Copy, X } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { getCurrentWindow } from '@tauri-apps/api/window' -import { invoke } from '@tauri-apps/api/core' import { useTranslation } from '@/hooks/useTranslation' import { cn } from '@/lib/utils' @@ -60,14 +59,9 @@ export function WindowFrameControls({ className }: { className?: string }) { const handleClose = useCallback(async () => { try { - await invoke('close_main_window_to_tray') + await getCurrentWindow().close() } catch { - // Fallback to direct window close if the command is unavailable - try { - await getCurrentWindow().close() - } catch { - // Ignore - } + // Ignore } }, []) diff --git a/wework/src/e2e/automation.ts b/wework/src/e2e/automation.ts index ba59e47c9a..d5e98de885 100644 --- a/wework/src/e2e/automation.ts +++ b/wework/src/e2e/automation.ts @@ -725,6 +725,8 @@ async function executeDesktopControlCommand(command: DesktopControlCommand): Pro return invoke('capture_popout_webview') case 'closeMainWindowToTray': return '' + case 'requestMainWindowClose': + return '' case 'dispatchLocalModelSettingsChanged': window.dispatchEvent(new CustomEvent(LOCAL_MODEL_SETTINGS_CHANGED_EVENT)) return '' @@ -1077,6 +1079,8 @@ async function runDesktopControlClient(url: string): Promise { await postDesktopControlResult(url, { id: command.id, clientId, ok: true, value }) if (command.action === 'closeMainWindowToTray') { await closeMainWindowToTray() + } else if (command.action === 'requestMainWindowClose') { + await getCurrentWindow().close() } } catch (error) { await postDesktopControlResult(url, { diff --git a/wework/src/features/workbench/RuntimeTaskCloseGuard.test.tsx b/wework/src/features/workbench/RuntimeTaskCloseGuard.test.tsx new file mode 100644 index 0000000000..0424accf9a --- /dev/null +++ b/wework/src/features/workbench/RuntimeTaskCloseGuard.test.tsx @@ -0,0 +1,50 @@ +import { act, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, test, vi } from 'vitest' +import { RuntimeTaskCloseGuard } from './RuntimeTaskCloseGuard' + +const mocks = vi.hoisted(() => ({ + closeRequestHandler: undefined as (() => void) | undefined, + installRuntimeTaskCloseGuard: vi.fn(), + closeMainWindowToTray: vi.fn(), + updateAppPreferences: vi.fn(), + unlisten: vi.fn(), +})) + +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock('@/lib/runtime-environment', () => ({ + isTauriRuntime: () => true, +})) + +vi.mock('@/tauri/appPreferences', () => ({ + updateAppPreferences: mocks.updateAppPreferences, +})) + +vi.mock('@/tauri/runtimeTaskCloseGuard', () => ({ + closeMainWindowToTray: mocks.closeMainWindowToTray, + installRuntimeTaskCloseGuard: mocks.installRuntimeTaskCloseGuard, +})) + +describe('RuntimeTaskCloseGuard', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.closeRequestHandler = undefined + mocks.installRuntimeTaskCloseGuard.mockImplementation(async handler => { + mocks.closeRequestHandler = handler + return mocks.unlisten + }) + }) + + test('shows the close-to-tray prompt for every intercepted first close request', async () => { + render() + + await waitFor(() => expect(mocks.closeRequestHandler).toBeDefined()) + act(() => { + mocks.closeRequestHandler?.() + }) + + expect(screen.getByTestId('runtime-task-close-confirm-overlay')).toBeInTheDocument() + }) +}) diff --git a/wework/src/features/workbench/RuntimeTaskCloseGuard.tsx b/wework/src/features/workbench/RuntimeTaskCloseGuard.tsx index 96ab3f5849..8e220f7544 100644 --- a/wework/src/features/workbench/RuntimeTaskCloseGuard.tsx +++ b/wework/src/features/workbench/RuntimeTaskCloseGuard.tsx @@ -6,11 +6,9 @@ import { useTranslation } from '@/hooks/useTranslation' import { isTauriRuntime } from '@/lib/runtime-environment' import { updateAppPreferences } from '@/tauri/appPreferences' import { closeMainWindowToTray, installRuntimeTaskCloseGuard } from '@/tauri/runtimeTaskCloseGuard' -import { useRuntimeTaskLifecycleStoreSnapshot } from './runtimeTaskLifecycle' export function RuntimeTaskCloseGuard() { const { t } = useTranslation('common') - const lifecycle = useRuntimeTaskLifecycleStoreSnapshot() const [closeDialogOpen, setCloseDialogOpen] = useState(false) const [closing, setClosing] = useState(false) @@ -21,7 +19,6 @@ export function RuntimeTaskCloseGuard() { let cancelled = false void installRuntimeTaskCloseGuard(() => { - if (lifecycle.runningTaskKeys.size === 0) return setClosing(false) setCloseDialogOpen(true) }) @@ -40,7 +37,7 @@ export function RuntimeTaskCloseGuard() { cancelled = true unlisten?.() } - }, [lifecycle.runningTaskKeys.size]) + }, []) return (