mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 04:17:54 +00:00
feat(cli): add browser automation commands
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, userEvent } from '@tests/setup/test-utils';
|
||||
import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card';
|
||||
|
||||
describe('CodexMcpServersCard', () => {
|
||||
it('blocks creating the reserved ccs_browser entry from the generic MCP editor', async () => {
|
||||
render(<CodexMcpServersCard entries={[]} onSave={vi.fn()} onDelete={vi.fn()} />);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('playwright');
|
||||
await userEvent.type(nameInput, 'ccs_browser');
|
||||
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
(_, element) =>
|
||||
element?.textContent?.includes(
|
||||
'ccs_browser is reserved for the CCS-managed browser tooling path.'
|
||||
) ?? false
|
||||
)[0]
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Save MCP server' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readCodexMcpServers } from '@/lib/codex-config';
|
||||
|
||||
describe('readCodexMcpServers', () => {
|
||||
it('marks the CCS browser MCP entry as managed by Browser settings', () => {
|
||||
const entries = readCodexMcpServers({
|
||||
mcp_servers: {
|
||||
ccs_browser: {
|
||||
command: 'npx',
|
||||
args: ['-y', '@playwright/mcp@0.0.70'],
|
||||
enabled: true,
|
||||
},
|
||||
playwright: {
|
||||
command: 'npx',
|
||||
args: ['-y', '@playwright/mcp@latest'],
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'ccs_browser',
|
||||
isCcsManaged: true,
|
||||
managementSurface: 'browser-settings',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'playwright',
|
||||
isCcsManaged: false,
|
||||
managementSurface: null,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, userEvent } from '@tests/setup/test-utils';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useBrowserConfig: vi.fn(),
|
||||
useRawConfig: vi.fn(),
|
||||
fetchConfig: vi.fn(),
|
||||
fetchStatus: vi.fn(),
|
||||
saveConfig: vi.fn(),
|
||||
fetchRawConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/settings/hooks', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('@/pages/settings/hooks')>('@/pages/settings/hooks');
|
||||
return {
|
||||
...actual,
|
||||
useBrowserConfig: mocks.useBrowserConfig,
|
||||
useRawConfig: mocks.useRawConfig,
|
||||
};
|
||||
});
|
||||
|
||||
import BrowserSection from '@/pages/settings/sections/browser';
|
||||
|
||||
describe('BrowserSection', () => {
|
||||
beforeEach(() => {
|
||||
mocks.fetchConfig.mockReset();
|
||||
mocks.fetchStatus.mockReset();
|
||||
mocks.saveConfig.mockReset();
|
||||
mocks.fetchRawConfig.mockReset();
|
||||
|
||||
mocks.useRawConfig.mockReturnValue({
|
||||
rawConfig: 'browser:\n claude:\n enabled: true\n',
|
||||
loading: false,
|
||||
copied: false,
|
||||
fetchRawConfig: mocks.fetchRawConfig,
|
||||
copyToClipboard: vi.fn(),
|
||||
});
|
||||
|
||||
mocks.useBrowserConfig.mockReturnValue({
|
||||
config: {
|
||||
claude: {
|
||||
enabled: true,
|
||||
userDataDir: '/tmp/browser-profile',
|
||||
devtoolsPort: 9222,
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
status: {
|
||||
claude: {
|
||||
enabled: true,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
state: 'ready',
|
||||
title: 'Claude Browser Attach is ready.',
|
||||
detail: 'CCS can reach the configured Chrome DevTools endpoint.',
|
||||
nextStep: 'Launch Claude.',
|
||||
effectiveUserDataDir: '/tmp/browser-profile',
|
||||
recommendedUserDataDir: '/tmp/browser-profile',
|
||||
devtoolsPort: 9222,
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
managedMcpServerPath: '/tmp/ccs-browser-server.cjs',
|
||||
launchCommands: {
|
||||
darwin:
|
||||
'open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="/tmp/browser-profile"',
|
||||
linux:
|
||||
'google-chrome --remote-debugging-port=9222 --user-data-dir="/tmp/browser-profile"',
|
||||
win32: 'chrome.exe --remote-debugging-port=9222 --user-data-dir="/tmp/browser-profile"',
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
title: 'Codex Browser Tools are enabled.',
|
||||
detail: 'CCS can inject managed Playwright MCP overrides.',
|
||||
nextStep: 'Use a Codex-target launch.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: true,
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
version: 'codex-cli 0.120.0',
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
statusLoading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
success: false,
|
||||
fetchConfig: mocks.fetchConfig,
|
||||
fetchStatus: mocks.fetchStatus,
|
||||
saveConfig: mocks.saveConfig,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the current draft for launch guidance and disables testing until the draft is saved', async () => {
|
||||
render(<BrowserSection />, { withSettingsProvider: true });
|
||||
|
||||
const pathInput = screen.getByLabelText('Chrome user-data directory');
|
||||
await userEvent.clear(pathInput);
|
||||
await userEvent.type(pathInput, '/tmp/new-browser-profile');
|
||||
|
||||
const launchCommand = screen.getByText(/new-browser-profile/);
|
||||
expect(launchCommand).toBeInTheDocument();
|
||||
|
||||
const testConnectionButton = screen.getByRole('button', { name: 'Test connection' });
|
||||
expect(testConnectionButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,10 @@ vi.mock('@/pages/settings/sections/websearch', () => ({
|
||||
default: () => <div>WebSearch Section</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/settings/sections/browser', () => ({
|
||||
default: () => <div>Browser Section</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/settings/sections/channels', () => ({
|
||||
default: () => <div>Channels Section</div>,
|
||||
}));
|
||||
@@ -70,6 +74,7 @@ describe('SettingsPage raw config panel', () => {
|
||||
});
|
||||
|
||||
it('keeps the current config editor visible while raw config refreshes', async () => {
|
||||
window.history.pushState({}, '', '/settings');
|
||||
mocks.useRawConfig.mockReturnValue({
|
||||
rawConfig: 'websearch:\n enabled: true\n',
|
||||
loading: true,
|
||||
@@ -84,4 +89,19 @@ describe('SettingsPage raw config panel', () => {
|
||||
expect(screen.getByLabelText('config editor')).toHaveValue('websearch:\n enabled: true\n');
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Browser section when the settings tab query is browser', async () => {
|
||||
window.history.pushState({}, '', '/settings?tab=browser');
|
||||
mocks.useRawConfig.mockReturnValue({
|
||||
rawConfig: 'browser:\n claude:\n enabled: false\n',
|
||||
loading: false,
|
||||
copied: false,
|
||||
fetchRawConfig: mocks.fetchRawConfig,
|
||||
copyToClipboard: mocks.copyToClipboard,
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
expect(await screen.findAllByText('Browser Section')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user