6fcda66b60
When a book's underlying file is missing, opening it in a dedicated reader window showed an error toast then navigated that window to /library, leaving a leftover library-in-reader-window the user had to close manually. Route the recovery through a new closeReaderWindowOrGoToLibrary() that closes the dedicated reader window (after ensuring the main library window is visible) and only falls back to /library navigation in the main window or on web. Also fix a related macOS issue: the reader's CloseRequested handler was running handleCloseBooks and calling currentWindow.destroy() on the main window, which tore down the active book and bypassed the Rust close-to-hide handler — making Cmd+W / traffic-light close quit the app from the reader page (vs. correctly hiding from the library page) and lose the active book even when the window did hide. Skip both the cleanup and destroy on macOS for the main window so the Rust handler hides it with the book intact, matching the macOS minimize-to-dock convention. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
|
|
vi.mock('@tauri-apps/api/window', () => ({
|
|
getCurrentWindow: vi.fn(),
|
|
getAllWindows: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/api/event', () => ({
|
|
emitTo: vi.fn().mockResolvedValue(undefined),
|
|
TauriEvent: { WINDOW_FOCUS: 'tauri://focus' },
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/plugin-process', () => ({
|
|
exit: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/plugin-os', () => ({
|
|
type: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/utils/event', () => ({
|
|
eventDispatcher: { dispatch: vi.fn() },
|
|
}));
|
|
|
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
import { type as osType } from '@tauri-apps/plugin-os';
|
|
import { tauriHandleOnCloseWindow } from '@/utils/window';
|
|
|
|
type CloseHandler = (event: { preventDefault: () => void }) => Promise<void> | void;
|
|
|
|
function makeWindow(label: string) {
|
|
let registered: CloseHandler | undefined;
|
|
const win = {
|
|
label,
|
|
destroy: vi.fn().mockResolvedValue(undefined),
|
|
hide: vi.fn().mockResolvedValue(undefined),
|
|
onCloseRequested: vi.fn().mockImplementation((handler: CloseHandler) => {
|
|
registered = handler;
|
|
return Promise.resolve(() => {});
|
|
}),
|
|
};
|
|
const trigger = async () => {
|
|
if (!registered) throw new Error('no handler registered');
|
|
await registered({ preventDefault: vi.fn() });
|
|
};
|
|
return { win, trigger };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
describe('tauriHandleOnCloseWindow', () => {
|
|
test('on macOS, leaves the main window alone — no book cleanup, no destroy', async () => {
|
|
// Rust hide-on-close handler hides the window; the user expects the active
|
|
// book to still be loaded when they bring the window back.
|
|
vi.mocked(osType).mockReturnValue('macos');
|
|
const { win, trigger } = makeWindow('main');
|
|
vi.mocked(getCurrentWindow).mockReturnValue(
|
|
win as unknown as ReturnType<typeof getCurrentWindow>,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
await tauriHandleOnCloseWindow(callback);
|
|
await trigger();
|
|
|
|
expect(callback).not.toHaveBeenCalled();
|
|
expect(win.destroy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('on Windows, destroys the main window', async () => {
|
|
vi.mocked(osType).mockReturnValue('windows');
|
|
const { win, trigger } = makeWindow('main');
|
|
vi.mocked(getCurrentWindow).mockReturnValue(
|
|
win as unknown as ReturnType<typeof getCurrentWindow>,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
await tauriHandleOnCloseWindow(callback);
|
|
await trigger();
|
|
|
|
expect(win.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
test('on Linux, destroys the main window', async () => {
|
|
vi.mocked(osType).mockReturnValue('linux');
|
|
const { win, trigger } = makeWindow('main');
|
|
vi.mocked(getCurrentWindow).mockReturnValue(
|
|
win as unknown as ReturnType<typeof getCurrentWindow>,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
await tauriHandleOnCloseWindow(callback);
|
|
await trigger();
|
|
|
|
expect(win.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
test('on macOS, dedicated reader windows still destroy after 300ms', async () => {
|
|
vi.mocked(osType).mockReturnValue('macos');
|
|
const { win, trigger } = makeWindow('reader-0');
|
|
vi.mocked(getCurrentWindow).mockReturnValue(
|
|
win as unknown as ReturnType<typeof getCurrentWindow>,
|
|
);
|
|
|
|
const callback = vi.fn();
|
|
await tauriHandleOnCloseWindow(callback);
|
|
await trigger();
|
|
|
|
expect(win.destroy).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(300);
|
|
expect(win.destroy).toHaveBeenCalled();
|
|
});
|
|
});
|