feat: implement browser open functionality and related tests
This commit is contained in:
113
tests/browser-open-service.test.ts
Normal file
113
tests/browser-open-service.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const state = {
|
||||
currentUrl: 'about:blank',
|
||||
};
|
||||
|
||||
const page = {
|
||||
url: vi.fn(() => state.currentUrl),
|
||||
goto: vi.fn(async (url: string) => {
|
||||
state.currentUrl = url;
|
||||
}),
|
||||
bringToFront: vi.fn(async () => {}),
|
||||
title: vi.fn(async () => '百度一下,你就知道'),
|
||||
};
|
||||
|
||||
const context = {
|
||||
pages: vi.fn(() => [page]),
|
||||
newPage: vi.fn(async () => page),
|
||||
};
|
||||
|
||||
const browser = {
|
||||
contexts: vi.fn(() => [context]),
|
||||
disconnect: vi.fn(async () => {}),
|
||||
close: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
page,
|
||||
context,
|
||||
browser,
|
||||
launchLocalChrome: vi.fn(async () => {}),
|
||||
isChromeRunning: vi.fn(async () => true),
|
||||
connectOverCDP: vi.fn(async () => browser),
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@electron/utils/chrome/launchLocalChrome', () => ({
|
||||
launchLocalChrome: mocks.launchLocalChrome,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/chrome/isChromeRunning', () => ({
|
||||
isChromeRunning: mocks.isChromeRunning,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/service/logger', () => ({
|
||||
default: mocks.logger,
|
||||
}));
|
||||
|
||||
vi.mock('playwright', () => ({
|
||||
chromium: {
|
||||
connectOverCDP: mocks.connectOverCDP,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('browser open service', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
mocks.state.currentUrl = 'about:blank';
|
||||
mocks.page.url.mockImplementation(() => mocks.state.currentUrl);
|
||||
mocks.page.goto.mockImplementation(async (url: string) => {
|
||||
mocks.state.currentUrl = url;
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts an explicit open-url intent and normalizes the URL', async () => {
|
||||
const { extractBrowserOpenIntent } = await import('../electron/service/browser-open-service');
|
||||
|
||||
expect(extractBrowserOpenIntent('打开 http://www.baidu.com。')).toEqual({
|
||||
url: 'http://www.baidu.com/',
|
||||
});
|
||||
expect(extractBrowserOpenIntent('请帮我分析一下 http://www.baidu.com')).toBeNull();
|
||||
expect(extractBrowserOpenIntent('打开 javascript:alert(1)')).toBeNull();
|
||||
});
|
||||
|
||||
it('launches chrome via cdp and opens the requested page', async () => {
|
||||
const { openUrlInBrowser } = await import('../electron/service/browser-open-service');
|
||||
|
||||
const result = await openUrlInBrowser('http://www.baidu.com');
|
||||
|
||||
expect(mocks.launchLocalChrome).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.connectOverCDP).toHaveBeenCalledWith('http://127.0.0.1:9222');
|
||||
expect(mocks.page.bringToFront).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.page.goto).toHaveBeenCalledWith('http://www.baidu.com/', expect.objectContaining({
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 15000,
|
||||
}));
|
||||
expect(mocks.browser.disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
url: 'http://www.baidu.com/',
|
||||
pageUrl: 'http://www.baidu.com/',
|
||||
title: '百度一下,你就知道',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps cdp connection errors with a clearer message', async () => {
|
||||
mocks.connectOverCDP.mockRejectedValueOnce(new Error('connect ECONNREFUSED 127.0.0.1:9222'));
|
||||
|
||||
const { openUrlInBrowser } = await import('../electron/service/browser-open-service');
|
||||
|
||||
await expect(openUrlInBrowser('http://www.baidu.com')).rejects.toThrow(
|
||||
'Chrome 已启动但远程调试端口 9222 不可用',
|
||||
);
|
||||
});
|
||||
});
|
||||
144
tests/browser-shortcut.test.ts
Normal file
144
tests/browser-shortcut.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
appendMessage: vi.fn(),
|
||||
setActiveRun: vi.fn(),
|
||||
clearActiveRun: vi.fn(),
|
||||
extractBrowserOpenIntent: vi.fn(),
|
||||
openUrlInBrowser: vi.fn(),
|
||||
appendTranscriptLine: vi.fn(),
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../electron/gateway/session-store', () => ({
|
||||
sessionStore: {
|
||||
appendMessage: mocks.appendMessage,
|
||||
setActiveRun: mocks.setActiveRun,
|
||||
clearActiveRun: mocks.clearActiveRun,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@electron/service/browser-open-service', () => ({
|
||||
extractBrowserOpenIntent: mocks.extractBrowserOpenIntent,
|
||||
openUrlInBrowser: mocks.openUrlInBrowser,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/token-usage-writer', () => ({
|
||||
appendTranscriptLine: mocks.appendTranscriptLine,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/service/logger', () => ({
|
||||
default: mocks.logger,
|
||||
}));
|
||||
|
||||
function flushAsyncTasks(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
describe('gateway browser shortcut', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
mocks.extractBrowserOpenIntent.mockImplementation((text: string) => (
|
||||
text === '打开 http://www.baidu.com'
|
||||
? { url: 'http://www.baidu.com/' }
|
||||
: null
|
||||
));
|
||||
});
|
||||
|
||||
it('starts a browser-open run and emits a final assistant message on success', async () => {
|
||||
mocks.openUrlInBrowser.mockResolvedValue({
|
||||
url: 'http://www.baidu.com/',
|
||||
pageUrl: 'http://www.baidu.com/',
|
||||
title: '百度一下,你就知道',
|
||||
});
|
||||
|
||||
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
|
||||
const broadcast = vi.fn();
|
||||
|
||||
const handled = maybeHandleBrowserOpenMessage(
|
||||
'agent:test:main',
|
||||
'run-1',
|
||||
{ role: 'user', content: '打开 http://www.baidu.com' },
|
||||
broadcast,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(mocks.setActiveRun).toHaveBeenCalledWith(
|
||||
'agent:test:main',
|
||||
'run-1',
|
||||
expect.any(AbortController),
|
||||
);
|
||||
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(mocks.openUrlInBrowser).toHaveBeenCalledWith(
|
||||
'http://www.baidu.com/',
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
expect(mocks.appendMessage).toHaveBeenCalledWith(
|
||||
'agent:test:main',
|
||||
expect.objectContaining({
|
||||
role: 'assistant',
|
||||
content: '已为你打开 http://www.baidu.com/(百度一下,你就知道)',
|
||||
}),
|
||||
);
|
||||
expect(broadcast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'chat:final',
|
||||
sessionKey: 'agent:test:main',
|
||||
runId: 'run-1',
|
||||
}));
|
||||
});
|
||||
|
||||
it('emits a failure assistant message when browser open fails', async () => {
|
||||
mocks.openUrlInBrowser.mockRejectedValue(new Error('No browser context available'));
|
||||
|
||||
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
|
||||
const broadcast = vi.fn();
|
||||
|
||||
const handled = maybeHandleBrowserOpenMessage(
|
||||
'agent:test:main',
|
||||
'run-2',
|
||||
{ role: 'user', content: '打开 http://www.baidu.com' },
|
||||
broadcast,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(mocks.appendMessage).toHaveBeenCalledWith(
|
||||
'agent:test:main',
|
||||
expect.objectContaining({
|
||||
role: 'assistant',
|
||||
content: '打开失败:No browser context available',
|
||||
}),
|
||||
);
|
||||
expect(broadcast).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'chat:final',
|
||||
runId: 'run-2',
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns false when the user message is not an explicit browser-open command', async () => {
|
||||
mocks.extractBrowserOpenIntent.mockReturnValue(null);
|
||||
|
||||
const { maybeHandleBrowserOpenMessage } = await import('../electron/gateway/browser-shortcut');
|
||||
|
||||
expect(
|
||||
maybeHandleBrowserOpenMessage(
|
||||
'agent:test:main',
|
||||
'run-3',
|
||||
{ role: 'user', content: '帮我总结一下百度首页' },
|
||||
vi.fn(),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(mocks.openUrlInBrowser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user