eigent/test/unit/electron/preload/appCommands.test.ts
Douglas Lai 8f4f575db9
feat: add native menus, shortcuts, and window chrome (#1858)
Co-authored-by: 4pmtong <web_chentong@163.com>
2026-08-21 20:39:55 +08:00

195 lines
6.4 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import {
APP_COMMAND,
APP_COMMAND_CHANNEL,
APP_COMMAND_HANDLED_CHANNEL,
APP_SHELL_NOT_READY_CHANNEL,
APP_SHELL_READY_CHANNEL,
APP_SHELL_READY_PROBE_CHANNEL,
} from '@/shared/appCommands';
import { NATIVE_MENU_LOCALE_CHANNEL } from '@/shared/nativeMenu';
import {
WINDOW_CLOSE_REQUEST_CHANNEL,
WINDOW_CLOSE_RESPONSE_CHANNEL,
} from '@/shared/windowClose';
import { beforeAll, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
exposeInMainWorld: vi.fn(),
invoke: vi.fn(),
off: vi.fn(),
on: vi.fn(),
removeAllListeners: vi.fn(),
send: vi.fn(),
}));
vi.mock('electron', () => ({
contextBridge: { exposeInMainWorld: mocks.exposeInMainWorld },
ipcRenderer: {
invoke: mocks.invoke,
off: mocks.off,
on: mocks.on,
removeAllListeners: mocks.removeAllListeners,
send: mocks.send,
},
webUtils: { getPathForFile: vi.fn() },
}));
type ExposedElectronAPI = {
onAppCommand: (callback: (command: string) => void) => () => void;
onCloseRequest: (
callback: (request: { intent: 'close-window' | 'quit-app' }) => void
) => () => void;
respondToCloseRequest: (response: {
intent: 'close-window' | 'quit-app';
action: 'acknowledge' | 'confirm' | 'cancel';
}) => void;
setNativeMenuLocale: (locale: 'en-US' | 'zh-Hans') => void;
};
let electronAPI: ExposedElectronAPI;
beforeAll(async () => {
await import('../../../../electron/preload/index');
const exposure = mocks.exposeInMainWorld.mock.calls.find(
([name]) => name === 'electronAPI'
);
electronAPI = exposure?.[1] as ExposedElectronAPI;
});
describe('preload app shell bridge', () => {
it('re-announces READY when the first announcement preceded did-finish-load', async () => {
mocks.send.mockClear();
const unsubscribeCommand = electronAPI.onAppCommand(vi.fn());
const unsubscribeClose = electronAPI.onCloseRequest(vi.fn());
const probeListener = mocks.on.mock.calls.find(
([channel]) => channel === APP_SHELL_READY_PROBE_CHANNEL
)?.[1];
const initialReady = mocks.send.mock.calls.find(
([channel]) => channel === APP_SHELL_READY_CHANNEL
)?.[1] as { epoch: string } | undefined;
expect(initialReady?.epoch).toEqual(expect.any(String));
expect(probeListener).toEqual(expect.any(Function));
// Main rejects the first READY until did-finish-load, then sends this
// probe. The renderer must repeat READY for the same document epoch.
probeListener({});
const readyMessages = mocks.send.mock.calls.filter(
([channel]) => channel === APP_SHELL_READY_CHANNEL
);
expect(readyMessages).toEqual([
[APP_SHELL_READY_CHANNEL, initialReady],
[APP_SHELL_READY_CHANNEL, initialReady],
]);
unsubscribeCommand();
unsubscribeClose();
await Promise.resolve();
});
it('handshakes after both listeners, filters epochs, and sends handled receipts', async () => {
mocks.send.mockClear();
const callback = vi.fn();
const closeCallback = vi.fn();
const unsubscribeCommand = electronAPI.onAppCommand(callback);
const unsubscribeClose = electronAPI.onCloseRequest(closeCallback);
const commandListener = mocks.on.mock.calls
.filter(([channel]) => channel === APP_COMMAND_CHANNEL)
.at(-1)?.[1];
const ready = mocks.send.mock.calls.find(
([channel]) => channel === APP_SHELL_READY_CHANNEL
)?.[1] as { epoch: string } | undefined;
expect(ready?.epoch).toEqual(expect.any(String));
expect(commandListener).toEqual(expect.any(Function));
const request = {
commandId: APP_COMMAND.newProject,
requestId: 'request-1',
epoch: ready!.epoch,
};
commandListener({}, request);
commandListener({}, { ...request, epoch: 'stale-epoch' });
commandListener({}, APP_COMMAND.newProject);
expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith(APP_COMMAND.newProject);
expect(mocks.send).toHaveBeenCalledWith(
APP_COMMAND_HANDLED_CHANNEL,
request
);
unsubscribeCommand();
const unsubscribeReplacement = electronAPI.onAppCommand(vi.fn());
await Promise.resolve();
expect(mocks.send).not.toHaveBeenCalledWith(
APP_SHELL_NOT_READY_CHANNEL,
expect.anything()
);
unsubscribeReplacement();
unsubscribeClose();
await Promise.resolve();
expect(mocks.off).toHaveBeenCalledWith(
APP_COMMAND_CHANNEL,
commandListener
);
expect(mocks.send).toHaveBeenCalledWith(APP_SHELL_NOT_READY_CHANNEL, {
epoch: ready!.epoch,
});
});
it('sends typed close responses on the shared response channel', () => {
const response = { intent: 'quit-app', action: 'cancel' } as const;
electronAPI.respondToCloseRequest(response);
expect(mocks.send).toHaveBeenCalledWith(
WINDOW_CLOSE_RESPONSE_CHANNEL,
response
);
});
it('sends native-menu locale updates on their dedicated channel', () => {
electronAPI.setNativeMenuLocale('zh-Hans');
expect(mocks.send).toHaveBeenCalledWith(
NATIVE_MENU_LOCALE_CHANNEL,
'zh-Hans'
);
});
it('filters close requests and removes the exact listener on cleanup', () => {
const callback = vi.fn();
const unsubscribe = electronAPI.onCloseRequest(callback);
const listener = mocks.on.mock.calls
.filter(([channel]) => channel === WINDOW_CLOSE_REQUEST_CHANNEL)
.at(-1)?.[1];
expect(listener).toEqual(expect.any(Function));
listener({}, { intent: 'quit-app' });
listener({}, { intent: 'not-a-close-intent' });
expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith({ intent: 'quit-app' });
unsubscribe();
expect(mocks.off).toHaveBeenCalledWith(
WINDOW_CLOSE_REQUEST_CHANNEL,
listener
);
});
});