mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
fix: keep browser setup config-only
This commit is contained in:
1 parent
7ad8bbf0a1
commit
f36c4a6e32
10 files changed
+79
-421
No files matched your search
@@ -53,7 +53,6 @@ ccs help browser
|
|||||||
ccs browser setup
|
ccs browser setup
|
||||||
ccs browser status
|
ccs browser status
|
||||||
ccs browser doctor
|
ccs browser doctor
|
||||||
ccs browser doctor --fix
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `ccs browser setup` for the primary one-command setup path. Use `ccs browser status` for
|
Use `ccs browser setup` for the primary one-command setup path. Use `ccs browser status` for
|
||||||
@@ -124,21 +123,9 @@ That flow:
|
|||||||
1. enables Claude Browser Attach in the saved CCS browser config
|
1. enables Claude Browser Attach in the saved CCS browser config
|
||||||
2. keeps the configured DevTools port normalized
|
2. keeps the configured DevTools port normalized
|
||||||
3. creates the configured browser user-data directory if needed
|
3. creates the configured browser user-data directory if needed
|
||||||
4. tries to start a managed Chrome/Chromium attach session
|
4. prints the exact browser launch command for the current platform
|
||||||
5. re-checks readiness and reports the next step if Chrome still needs manual attention
|
5. re-checks readiness and reports the next step if Chrome still needs manual attention
|
||||||
|
|
||||||
If you want the same remediation flow from the diagnostic command, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs browser doctor --fix
|
|
||||||
```
|
|
||||||
|
|
||||||
If you only want to save config and browser-directory state without launching Chrome:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ccs browser setup --no-launch
|
|
||||||
```
|
|
||||||
|
|
||||||
## Launching Chrome For Claude Attach
|
## Launching Chrome For Claude Attach
|
||||||
|
|
||||||
Claude Browser Attach needs a browser launched with remote debugging.
|
Claude Browser Attach needs a browser launched with remote debugging.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
|
|||||||
|
|
||||||
### Recent Fixes
|
### Recent Fixes
|
||||||
|
|
||||||
- **2026-04-19**: **#1049** Browser setup now has a real remediation path instead of status/doctor-only guidance. CCS adds `ccs browser setup` as the primary one-command flow for Claude Browser Attach, supports `ccs browser doctor --fix` as a repair alias, shortens managed browser-path output to home-relative display paths where appropriate, and updates browser readiness guidance to point users at setup first while keeping browser doctor read-only by default.
|
- **2026-04-19**: **#1049** Browser setup now has a real remediation path instead of status/doctor-only guidance. CCS adds `ccs browser setup` as the primary one-command flow for Claude Browser Attach, shortens managed browser-path output to home-relative display paths where appropriate, and updates browser readiness guidance to point users at setup first while keeping browser doctor read-only by default.
|
||||||
- **2026-04-18**: **#1038** Legacy OpenAI-compatible provider writes no longer self-destruct on the next `ccs cliproxy restart`. CCS now preserves AI-provider-managed top-level sections such as `openai-compatibility` during CLIProxy config regeneration, and the legacy `openai-compat` manager now rewrites only its own YAML section instead of dumping the whole file and stripping the generated version header. Regression coverage now proves the legacy helper keeps the generated header intact and that OpenAI-compatible connectors survive regeneration.
|
- **2026-04-18**: **#1038** Legacy OpenAI-compatible provider writes no longer self-destruct on the next `ccs cliproxy restart`. CCS now preserves AI-provider-managed top-level sections such as `openai-compatibility` during CLIProxy config regeneration, and the legacy `openai-compat` manager now rewrites only its own YAML section instead of dumping the whole file and stripping the generated version header. Regression coverage now proves the legacy helper keeps the generated header intact and that OpenAI-compatible connectors survive regeneration.
|
||||||
- **2026-04-16**: **#1030** Browser automation is now a first-class CCS surface instead of an env-only/runtime-only feature. CCS adds `ccs help browser`, `ccs browser status`, and `ccs browser doctor`; a dedicated `Settings -> Browser` dashboard tab for Claude Browser Attach and Codex Browser Tools; a new `browser` section in `~/.ccs/config.yaml`; explicit readiness/next-step messaging for attach-mode Chrome sessions; and Codex UI guidance that marks the managed `ccs_browser` entry as CCS-owned and redirects browser setup away from the generic MCP editor.
|
- **2026-04-16**: **#1030** Browser automation is now a first-class CCS surface instead of an env-only/runtime-only feature. CCS adds `ccs help browser`, `ccs browser status`, and `ccs browser doctor`; a dedicated `Settings -> Browser` dashboard tab for Claude Browser Attach and Codex Browser Tools; a new `browser` section in `~/.ccs/config.yaml`; explicit readiness/next-step messaging for attach-mode Chrome sessions; and Codex UI guidance that marks the managed `ccs_browser` entry as CCS-owned and redirects browser setup away from the generic MCP editor.
|
||||||
- **2026-04-15**: **#969** Local CLIProxy bootstrap no longer depends on live GitHub reachability during normal dashboard and runtime startup. CCS now skips hidden auto-update lookups on standard CLIProxy bootstrap paths, fails fast with explicit `ccs cliproxy install` guidance when a service start needs a binary that is not installed locally, and keeps `ccs config` able to open the dashboard in limited mode instead of stalling behind blocked release downloads.
|
- **2026-04-15**: **#969** Local CLIProxy bootstrap no longer depends on live GitHub reachability during normal dashboard and runtime startup. CCS now skips hidden auto-update lookups on standard CLIProxy bootstrap paths, fails fast with explicit `ccs cliproxy install` guidance when a service start needs a binary that is not installed locally, and keeps `ccs config` able to open the dashboard in limited mode instead of stalling behind blocked release downloads.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function summarizeBrowserHealth(status: browserUtils.BrowserStatusPayload): {
|
|||||||
function writeCommandTable(writeLine: HelpWriter): void {
|
function writeCommandTable(writeLine: HelpWriter): void {
|
||||||
writeLine(subheader('Commands'));
|
writeLine(subheader('Commands'));
|
||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs browser setup', 'command')} Configure Claude Browser Attach and try to start the managed browser session`
|
` ${color('ccs browser setup', 'command')} Configure Claude Browser Attach and print the manual launch command`
|
||||||
);
|
);
|
||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs browser status', 'command')} Show Claude attach and Codex browser readiness`
|
` ${color('ccs browser status', 'command')} Show Claude attach and Codex browser readiness`
|
||||||
@@ -32,9 +32,6 @@ function writeCommandTable(writeLine: HelpWriter): void {
|
|||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs browser doctor', 'command')} Explain what is missing and how to fix it`
|
` ${color('ccs browser doctor', 'command')} Explain what is missing and how to fix it`
|
||||||
);
|
);
|
||||||
writeLine(
|
|
||||||
` ${color('ccs browser doctor --fix', 'command')} ${dim('# Alias for browser setup')}`
|
|
||||||
);
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,10 +102,6 @@ function writeSetupSummary(
|
|||||||
writeLine(` Config updated: ${result.configUpdated ? 'yes' : 'no'}`);
|
writeLine(` Config updated: ${result.configUpdated ? 'yes' : 'no'}`);
|
||||||
writeLine(` Created user-data dir: ${result.createdUserDataDir ? 'yes' : 'no'}`);
|
writeLine(` Created user-data dir: ${result.createdUserDataDir ? 'yes' : 'no'}`);
|
||||||
writeLine(` Browser MCP ready: ${result.mcpReady ? 'yes' : 'no'}`);
|
writeLine(` Browser MCP ready: ${result.mcpReady ? 'yes' : 'no'}`);
|
||||||
if (result.launchAttempted) {
|
|
||||||
writeLine(` Browser launch: ${result.launchStarted ? 'started' : 'failed'}`);
|
|
||||||
}
|
|
||||||
writeLine(` Launch command: ${result.launchCommand}`);
|
|
||||||
if (result.notes.length > 0) {
|
if (result.notes.length > 0) {
|
||||||
for (const note of result.notes) {
|
for (const note of result.notes) {
|
||||||
writeLine(` Note: ${note}`);
|
writeLine(` Note: ${note}`);
|
||||||
@@ -133,17 +126,11 @@ export async function showBrowserHelp(writeLine: HelpWriter = console.log): Prom
|
|||||||
writeLine('');
|
writeLine('');
|
||||||
writeLine(subheader('Examples'));
|
writeLine(subheader('Examples'));
|
||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs browser setup', 'command')} ${dim('# Configure and start the managed browser session')}`
|
` ${color('ccs browser setup', 'command')} ${dim('# Configure browser attach and print the manual launch command')}`
|
||||||
);
|
|
||||||
writeLine(
|
|
||||||
` ${color('ccs browser setup --no-launch', 'command')} ${dim('# Save setup only, do not start Chrome')}`
|
|
||||||
);
|
);
|
||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs browser doctor', 'command')} ${dim('# Detailed troubleshooting output')}`
|
` ${color('ccs browser doctor', 'command')} ${dim('# Detailed troubleshooting output')}`
|
||||||
);
|
);
|
||||||
writeLine(
|
|
||||||
` ${color('ccs browser doctor --fix', 'command')} ${dim('# Run the setup flow from doctor')}`
|
|
||||||
);
|
|
||||||
writeLine(
|
writeLine(
|
||||||
` ${color('ccs config', 'command')} ${dim('# Open Settings > Browser in the dashboard')}`
|
` ${color('ccs config', 'command')} ${dim('# Open Settings > Browser in the dashboard')}`
|
||||||
);
|
);
|
||||||
@@ -151,7 +138,7 @@ export async function showBrowserHelp(writeLine: HelpWriter = console.log): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isHelpRequest(args: string[]): boolean {
|
function isHelpRequest(args: string[]): boolean {
|
||||||
return args.length === 0 || args.includes('--help') || args.includes('-h');
|
return args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleBrowserCommand(
|
export async function handleBrowserCommand(
|
||||||
@@ -164,13 +151,20 @@ export async function handleBrowserCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subcommand = args[0];
|
const subcommand = args[0];
|
||||||
if (subcommand === 'setup' || (subcommand === 'doctor' && args.includes('--fix'))) {
|
if (subcommand === 'setup') {
|
||||||
await initUI();
|
if (args.includes('--no-launch')) {
|
||||||
const result = await browserUtils.runBrowserSetup({
|
await initUI();
|
||||||
launch: !args.includes('--no-launch'),
|
writeLine(color('`ccs browser setup` no longer supports `--no-launch`.', 'error'));
|
||||||
});
|
writeLine(` ${dim('Setup is config-only and already prints the manual launch command.')}`);
|
||||||
|
writeLine('');
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const label = subcommand === 'setup' ? 'ccs browser setup' : 'ccs browser doctor --fix';
|
await initUI();
|
||||||
|
const result = await browserUtils.runBrowserSetup();
|
||||||
|
|
||||||
|
const label = 'ccs browser setup';
|
||||||
writeLine(header(label));
|
writeLine(header(label));
|
||||||
writeLine('');
|
writeLine('');
|
||||||
writeIntro(writeLine);
|
writeIntro(writeLine);
|
||||||
@@ -181,6 +175,15 @@ export async function handleBrowserCommand(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'doctor' && (args.includes('--fix') || args.includes('-f'))) {
|
||||||
|
await initUI();
|
||||||
|
writeLine(color('`ccs browser doctor` is read-only.', 'error'));
|
||||||
|
writeLine(` ${dim('Run `ccs browser setup` for the browser remediation flow.')}`);
|
||||||
|
writeLine('');
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand !== 'status' && subcommand !== 'doctor') {
|
if (subcommand !== 'status' && subcommand !== 'doctor') {
|
||||||
await initUI();
|
await initUI();
|
||||||
writeLine(color(`Unknown browser subcommand: ${subcommand}`, 'error'));
|
writeLine(color(`Unknown browser subcommand: ${subcommand}`, 'error'));
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ export const COMMAND_FLAG_SUGGESTIONS: Readonly<Record<string, readonly string[]
|
|||||||
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
|
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
|
||||||
cursor: ['--help', '-h'],
|
cursor: ['--help', '-h'],
|
||||||
doctor: ['--fix', '-f', '--help', '-h'],
|
doctor: ['--fix', '-f', '--help', '-h'],
|
||||||
browser: ['setup', 'status', 'doctor', '--fix', '-f', '--no-launch', '--help', '-h'],
|
browser: ['setup', 'status', 'doctor', '--help', '-h'],
|
||||||
docker: ['--help', '-h', '--host'],
|
docker: ['--help', '-h', '--host'],
|
||||||
env: ['--format', '--shell', '--ide', '--help', '-h'],
|
env: ['--format', '--shell', '--ide', '--help', '-h'],
|
||||||
migrate: MIGRATE_FLAGS,
|
migrate: MIGRATE_FLAGS,
|
||||||
|
|||||||
@@ -192,11 +192,8 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
if (!subcommand || subcommand.startsWith('-')) {
|
if (!subcommand || subcommand.startsWith('-')) {
|
||||||
return completeSubcommands(['setup', 'status', 'doctor'], ['--help', '-h']);
|
return completeSubcommands(['setup', 'status', 'doctor'], ['--help', '-h']);
|
||||||
}
|
}
|
||||||
if (subcommand === 'setup') {
|
if (subcommand === 'setup' || subcommand === 'doctor') {
|
||||||
return completeSubcommands([], ['--no-launch', '--help', '-h']);
|
return completeSubcommands([], ['--help', '-h']);
|
||||||
}
|
|
||||||
if (subcommand === 'doctor') {
|
|
||||||
return completeSubcommands([], ['--fix', '-f', '--help', '-h', '--no-launch']);
|
|
||||||
}
|
}
|
||||||
return completeSubcommands([], ['--help', '-h']);
|
return completeSubcommands([], ['--help', '-h']);
|
||||||
case 'cursor':
|
case 'cursor':
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
import { spawn } from 'child_process';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import { getBrowserConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
|
import { getBrowserConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import type { BrowserConfig } from '../../config/unified-config-types';
|
import type { BrowserConfig } from '../../config/unified-config-types';
|
||||||
import { getCcsPathDisplay } from '../config-manager';
|
import { getNodePlatformKey } from './platform';
|
||||||
import { type BrowserStatusPayload, getBrowserStatus } from './browser-status';
|
import { type BrowserStatusPayload, getBrowserStatus } from './browser-status';
|
||||||
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
|
||||||
import { ensureBrowserMcp } from './mcp-installer';
|
import { ensureBrowserMcp } from './mcp-installer';
|
||||||
import {
|
import {
|
||||||
buildBrowserLaunchCommands,
|
ensureManagedBrowserUserDataDir,
|
||||||
getEffectiveClaudeBrowserAttachConfig,
|
getEffectiveClaudeBrowserAttachConfig,
|
||||||
getRecommendedBrowserUserDataDir,
|
getRecommendedBrowserUserDataDir,
|
||||||
isManagedClaudeBrowserAttachConfig,
|
isManagedClaudeBrowserAttachConfig,
|
||||||
type EffectiveClaudeBrowserAttachConfig,
|
|
||||||
} from './browser-settings';
|
} from './browser-settings';
|
||||||
|
|
||||||
export interface BrowserSetupOptions {
|
|
||||||
launch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrowserSetupResult {
|
export interface BrowserSetupResult {
|
||||||
configUpdated: boolean;
|
configUpdated: boolean;
|
||||||
createdUserDataDir: boolean;
|
createdUserDataDir: boolean;
|
||||||
launchAttempted: boolean;
|
|
||||||
launchStarted: boolean;
|
|
||||||
launchCommand: string;
|
|
||||||
launchError?: string;
|
|
||||||
mcpReady: boolean;
|
mcpReady: boolean;
|
||||||
overrideActive: boolean;
|
overrideActive: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
runtimeEnv?: BrowserRuntimeEnv;
|
launchCommand: string;
|
||||||
status: BrowserStatusPayload;
|
status: BrowserStatusPayload;
|
||||||
notes: string[];
|
notes: string[];
|
||||||
}
|
}
|
||||||
@@ -37,39 +25,33 @@ export interface BrowserSetupDeps {
|
|||||||
getBrowserConfig: typeof getBrowserConfig;
|
getBrowserConfig: typeof getBrowserConfig;
|
||||||
mutateUnifiedConfig: typeof mutateUnifiedConfig;
|
mutateUnifiedConfig: typeof mutateUnifiedConfig;
|
||||||
ensureBrowserMcp: typeof ensureBrowserMcp;
|
ensureBrowserMcp: typeof ensureBrowserMcp;
|
||||||
resolveBrowserRuntimeEnv: typeof resolveBrowserRuntimeEnv;
|
|
||||||
getBrowserStatus: typeof getBrowserStatus;
|
getBrowserStatus: typeof getBrowserStatus;
|
||||||
launchBrowserSession: (
|
|
||||||
config: EffectiveClaudeBrowserAttachConfig
|
|
||||||
) => Promise<{ launchCommand: string; started: boolean; error?: string }>;
|
|
||||||
sleep: (ms: number) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBrowserSetupDeps: BrowserSetupDeps = {
|
const defaultBrowserSetupDeps: BrowserSetupDeps = {
|
||||||
getBrowserConfig,
|
getBrowserConfig,
|
||||||
mutateUnifiedConfig,
|
mutateUnifiedConfig,
|
||||||
ensureBrowserMcp,
|
ensureBrowserMcp,
|
||||||
resolveBrowserRuntimeEnv,
|
|
||||||
getBrowserStatus,
|
getBrowserStatus,
|
||||||
launchBrowserSession,
|
|
||||||
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runBrowserSetup(
|
export async function runBrowserSetup(
|
||||||
options: BrowserSetupOptions = {},
|
|
||||||
deps: BrowserSetupDeps = defaultBrowserSetupDeps
|
deps: BrowserSetupDeps = defaultBrowserSetupDeps
|
||||||
): Promise<BrowserSetupResult> {
|
): Promise<BrowserSetupResult> {
|
||||||
const initialConfig = deps.getBrowserConfig();
|
const initialConfig = deps.getBrowserConfig();
|
||||||
const configUpdated = persistBrowserSetupConfig(deps, initialConfig);
|
const configUpdated = persistBrowserSetupConfig(deps, initialConfig);
|
||||||
const persistedConfig = deps.getBrowserConfig();
|
const persistedConfig = deps.getBrowserConfig();
|
||||||
const effectiveConfig = getEffectiveClaudeBrowserAttachConfig(persistedConfig);
|
const effectiveConfig = getEffectiveClaudeBrowserAttachConfig(persistedConfig);
|
||||||
const createdUserDataDir = ensureBrowserUserDataDir(effectiveConfig.userDataDir);
|
const createdUserDataDir = isManagedClaudeBrowserAttachConfig(effectiveConfig)
|
||||||
|
? ensureManagedBrowserUserDataDir(effectiveConfig).createdProfileDir
|
||||||
|
: false;
|
||||||
const mcpReady = deps.ensureBrowserMcp();
|
const mcpReady = deps.ensureBrowserMcp();
|
||||||
|
const status = await deps.getBrowserStatus();
|
||||||
const notes: string[] = [];
|
const notes: string[] = [];
|
||||||
|
|
||||||
if (effectiveConfig.overrideActive) {
|
if (effectiveConfig.overrideActive) {
|
||||||
notes.push(
|
notes.push(
|
||||||
`Current session is using ${effectiveConfig.source}; saved config may still be shadowed until that override is removed.`
|
`Current session is using ${effectiveConfig.source}; remove that override if you want saved browser settings to take effect in future shells.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,38 +59,20 @@ export async function runBrowserSetup(
|
|||||||
notes.push('CCS could not fully prepare the local browser MCP runtime.');
|
notes.push('CCS could not fully prepare the local browser MCP runtime.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtimeEnv = await tryResolveBrowserRuntime(effectiveConfig, deps);
|
if (!isManagedClaudeBrowserAttachConfig(effectiveConfig)) {
|
||||||
let launchAttempted = false;
|
notes.push(
|
||||||
let launchStarted = false;
|
'Setup did not create the current browser user-data dir because it is not the CCS-managed default path.'
|
||||||
let launchError: string | undefined;
|
);
|
||||||
const { launchCommand } = getPreferredLaunchInfo(effectiveConfig);
|
|
||||||
|
|
||||||
if (!runtimeEnv && options.launch !== false) {
|
|
||||||
launchAttempted = true;
|
|
||||||
const launchResult = await deps.launchBrowserSession(effectiveConfig);
|
|
||||||
launchStarted = launchResult.started;
|
|
||||||
launchError = launchResult.error;
|
|
||||||
runtimeEnv = launchStarted
|
|
||||||
? await waitForBrowserRuntime(effectiveConfig, deps)
|
|
||||||
: await tryResolveBrowserRuntime(effectiveConfig, deps);
|
|
||||||
|
|
||||||
if (launchError) {
|
|
||||||
notes.push(launchError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await deps.getBrowserStatus();
|
const platform = getNodePlatformKey();
|
||||||
return {
|
return {
|
||||||
configUpdated,
|
configUpdated,
|
||||||
createdUserDataDir,
|
createdUserDataDir,
|
||||||
launchAttempted,
|
|
||||||
launchStarted,
|
|
||||||
launchCommand,
|
|
||||||
launchError,
|
|
||||||
mcpReady,
|
mcpReady,
|
||||||
overrideActive: effectiveConfig.overrideActive,
|
overrideActive: effectiveConfig.overrideActive,
|
||||||
ready: Boolean(runtimeEnv) && mcpReady,
|
ready: status.claude.state === 'ready' && mcpReady,
|
||||||
runtimeEnv,
|
launchCommand: status.claude.launchCommands[platform],
|
||||||
status,
|
status,
|
||||||
notes,
|
notes,
|
||||||
};
|
};
|
||||||
@@ -135,214 +99,3 @@ function persistBrowserSetupConfig(deps: BrowserSetupDeps, currentConfig: Browse
|
|||||||
|
|
||||||
return JSON.stringify(deps.getBrowserConfig()) !== before;
|
return JSON.stringify(deps.getBrowserConfig()) !== before;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBrowserUserDataDir(userDataDir: string): boolean {
|
|
||||||
try {
|
|
||||||
fs.statSync(userDataDir);
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
const code = (error as NodeJS.ErrnoException).code;
|
|
||||||
if (code && code !== 'ENOENT') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true, mode: 0o700 });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryResolveBrowserRuntime(
|
|
||||||
config: EffectiveClaudeBrowserAttachConfig,
|
|
||||||
deps: BrowserSetupDeps
|
|
||||||
): Promise<BrowserRuntimeEnv | undefined> {
|
|
||||||
try {
|
|
||||||
return await deps.resolveBrowserRuntimeEnv({
|
|
||||||
profileDir: config.userDataDir,
|
|
||||||
devtoolsPort: config.hasExplicitDevtoolsPort ? String(config.devtoolsPort) : undefined,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForBrowserRuntime(
|
|
||||||
config: EffectiveClaudeBrowserAttachConfig,
|
|
||||||
deps: BrowserSetupDeps
|
|
||||||
): Promise<BrowserRuntimeEnv | undefined> {
|
|
||||||
const deadline = Date.now() + 10000;
|
|
||||||
|
|
||||||
while (Date.now() <= deadline) {
|
|
||||||
const runtimeEnv = await tryResolveBrowserRuntime(config, deps);
|
|
||||||
if (runtimeEnv) {
|
|
||||||
return runtimeEnv;
|
|
||||||
}
|
|
||||||
|
|
||||||
await deps.sleep(250);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreferredLaunchInfo(config: EffectiveClaudeBrowserAttachConfig): {
|
|
||||||
launchCommand: string;
|
|
||||||
} {
|
|
||||||
const userDataDirDisplay = isManagedClaudeBrowserAttachConfig(config)
|
|
||||||
? getCcsPathDisplay('browser', 'chrome-user-data')
|
|
||||||
: config.userDataDir;
|
|
||||||
const launchCommands = buildBrowserLaunchCommands(userDataDirDisplay, config.devtoolsPort);
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
return { launchCommand: launchCommands.win32 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
return { launchCommand: launchCommands.darwin };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { launchCommand: launchCommands.linux };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLaunchCandidates(config: EffectiveClaudeBrowserAttachConfig): Array<{
|
|
||||||
command: string;
|
|
||||||
args: string[];
|
|
||||||
displayCommand: string;
|
|
||||||
}> {
|
|
||||||
const launchCommands = buildBrowserLaunchCommands(config.userDataDir, config.devtoolsPort);
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
command: 'open',
|
|
||||||
args: [
|
|
||||||
'-na',
|
|
||||||
'Google Chrome',
|
|
||||||
'--args',
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.darwin,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
command: 'chrome.exe',
|
|
||||||
args: [
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.win32,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
command: 'google-chrome',
|
|
||||||
args: [
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.linux,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: 'google-chrome-stable',
|
|
||||||
args: [
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.linux.replace('google-chrome', 'google-chrome-stable'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: 'chromium',
|
|
||||||
args: [
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.linux.replace('google-chrome', 'chromium'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: 'chromium-browser',
|
|
||||||
args: [
|
|
||||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
|
||||||
`--user-data-dir=${config.userDataDir}`,
|
|
||||||
],
|
|
||||||
displayCommand: launchCommands.linux.replace('google-chrome', 'chromium-browser'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function launchBrowserSession(
|
|
||||||
config: EffectiveClaudeBrowserAttachConfig
|
|
||||||
): Promise<{ launchCommand: string; started: boolean; error?: string }> {
|
|
||||||
let lastError: string | undefined;
|
|
||||||
const candidates = getLaunchCandidates(config);
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
const result = await spawnDetached(candidate.command, candidate.args);
|
|
||||||
if (result.started) {
|
|
||||||
return {
|
|
||||||
launchCommand: candidate.displayCommand,
|
|
||||||
started: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.notFound) {
|
|
||||||
return {
|
|
||||||
launchCommand: candidate.displayCommand,
|
|
||||||
started: false,
|
|
||||||
error: `CCS could not start the browser automatically. ${result.error}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
lastError = result.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
launchCommand: getPreferredLaunchInfo(config).launchCommand,
|
|
||||||
started: false,
|
|
||||||
error:
|
|
||||||
lastError ??
|
|
||||||
'CCS could not find a supported Chrome/Chromium executable to launch automatically.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function spawnDetached(
|
|
||||||
command: string,
|
|
||||||
args: string[]
|
|
||||||
): Promise<{ started: boolean; notFound: boolean; error?: string }> {
|
|
||||||
return await new Promise((resolve) => {
|
|
||||||
try {
|
|
||||||
const child = spawn(command, args, {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
child.once('error', (error) => {
|
|
||||||
const errno = error as NodeJS.ErrnoException;
|
|
||||||
resolve({
|
|
||||||
started: false,
|
|
||||||
notFound: errno.code === 'ENOENT',
|
|
||||||
error: errno.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
child.once('spawn', () => {
|
|
||||||
child.unref();
|
|
||||||
resolve({ started: true, notFound: false });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
resolve({
|
|
||||||
started: false,
|
|
||||||
notFound: false,
|
|
||||||
error: (error as Error).message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -51,4 +51,4 @@ export type {
|
|||||||
} from './browser-status';
|
} from './browser-status';
|
||||||
|
|
||||||
export { runBrowserSetup } from './browser-setup';
|
export { runBrowserSetup } from './browser-setup';
|
||||||
export type { BrowserSetupOptions, BrowserSetupResult } from './browser-setup';
|
export type { BrowserSetupResult } from './browser-setup';
|
||||||
@@ -187,19 +187,10 @@ describe('browser command', () => {
|
|||||||
const setupSpy = spyOn(browserUtils, 'runBrowserSetup').mockResolvedValue({
|
const setupSpy = spyOn(browserUtils, 'runBrowserSetup').mockResolvedValue({
|
||||||
configUpdated: true,
|
configUpdated: true,
|
||||||
createdUserDataDir: true,
|
createdUserDataDir: true,
|
||||||
launchAttempted: true,
|
|
||||||
launchStarted: true,
|
|
||||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
|
||||||
mcpReady: true,
|
mcpReady: true,
|
||||||
overrideActive: false,
|
overrideActive: false,
|
||||||
ready: true,
|
ready: true,
|
||||||
runtimeEnv: {
|
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||||
CCS_BROWSER_USER_DATA_DIR: '/tmp/browser-profile',
|
|
||||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
|
||||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
|
||||||
},
|
|
||||||
status: {
|
status: {
|
||||||
claude: {
|
claude: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -248,67 +239,26 @@ describe('browser command', () => {
|
|||||||
expect(rendered.includes('ccs browser setup')).toBe(true);
|
expect(rendered.includes('ccs browser setup')).toBe(true);
|
||||||
expect(rendered.includes('Result: ready')).toBe(true);
|
expect(rendered.includes('Result: ready')).toBe(true);
|
||||||
expect(rendered.includes('Config updated: yes')).toBe(true);
|
expect(rendered.includes('Config updated: yes')).toBe(true);
|
||||||
expect(rendered.includes('Browser launch: started')).toBe(true);
|
|
||||||
expect(rendered.includes('Launch command: open -na "Google Chrome"')).toBe(true);
|
|
||||||
expect(process.exitCode).toBe(0);
|
expect(process.exitCode).toBe(0);
|
||||||
} finally {
|
} finally {
|
||||||
setupSpy.mockRestore();
|
setupSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('doctor --fix reuses the browser setup flow', async () => {
|
test('doctor rejects --fix and points users to setup', async () => {
|
||||||
const setupSpy = spyOn(browserUtils, 'runBrowserSetup').mockResolvedValue({
|
const rendered = await renderLines(['doctor', '--fix']);
|
||||||
configUpdated: false,
|
|
||||||
createdUserDataDir: false,
|
|
||||||
launchAttempted: false,
|
|
||||||
launchStarted: false,
|
|
||||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
|
||||||
mcpReady: false,
|
|
||||||
overrideActive: false,
|
|
||||||
ready: false,
|
|
||||||
status: {
|
|
||||||
claude: {
|
|
||||||
enabled: true,
|
|
||||||
source: 'config',
|
|
||||||
overrideActive: false,
|
|
||||||
state: 'browser_not_running',
|
|
||||||
title: 'Claude Browser Attach is not ready yet.',
|
|
||||||
detail: 'No running attach-mode Chrome session is using the managed browser profile yet.',
|
|
||||||
nextStep: 'Run `ccs browser setup` to configure and start the managed browser session.',
|
|
||||||
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',
|
|
||||||
linux: 'google-chrome --remote-debugging-port=9222',
|
|
||||||
win32: 'chrome.exe --remote-debugging-port=9222',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
codex: {
|
|
||||||
enabled: true,
|
|
||||||
state: 'enabled',
|
|
||||||
title: 'Codex Browser Tools are enabled.',
|
|
||||||
detail: 'CCS can inject the managed Playwright MCP overrides.',
|
|
||||||
nextStep: 'Use a Codex-target launch.',
|
|
||||||
serverName: 'ccs_browser',
|
|
||||||
supportsConfigOverrides: true,
|
|
||||||
binaryPath: '/usr/local/bin/codex',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
notes: ['CCS could not fully prepare the local browser MCP runtime.'],
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
expect(rendered.includes('`ccs browser doctor` is read-only.')).toBe(true);
|
||||||
const rendered = await renderLines(['doctor', '--fix', '--no-launch']);
|
expect(rendered.includes('Run `ccs browser setup` for the browser remediation flow.')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
expect(rendered.includes('ccs browser doctor --fix')).toBe(true);
|
test('literal browser help still renders the help page', async () => {
|
||||||
expect(rendered.includes('Result: action required')).toBe(true);
|
const rendered = await renderLines(['help']);
|
||||||
expect(rendered.includes('Note: CCS could not fully prepare the local browser MCP runtime.')).toBe(true);
|
|
||||||
expect(process.exitCode).toBe(1);
|
expect(rendered.includes('CCS Browser Help')).toBe(true);
|
||||||
} finally {
|
expect(rendered.includes('ccs browser setup')).toBe(true);
|
||||||
setupSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -144,11 +144,9 @@ describe('completion backend', () => {
|
|||||||
expect(suggestionValues(['browser'])).toEqual(
|
expect(suggestionValues(['browser'])).toEqual(
|
||||||
expect.arrayContaining(['setup', 'status', 'doctor'])
|
expect.arrayContaining(['setup', 'status', 'doctor'])
|
||||||
);
|
);
|
||||||
expect(suggestionValues(['browser', 'setup'])).toEqual(
|
expect(suggestionValues(['browser', 'setup'])).toEqual(expect.arrayContaining(['--help', '-h']));
|
||||||
expect.arrayContaining(['--no-launch', '--help', '-h'])
|
|
||||||
);
|
|
||||||
expect(suggestionValues(['browser', 'doctor'])).toEqual(
|
expect(suggestionValues(['browser', 'doctor'])).toEqual(
|
||||||
expect.arrayContaining(['--fix', '-f', '--no-launch'])
|
expect.arrayContaining(['--help', '-h'])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ describe('browser setup', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('enables Claude browser attach and returns ready without launching when runtime already resolves', async () => {
|
it('enables Claude browser attach and prepares the managed default path', async () => {
|
||||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||||
|
config.browser.claude.user_data_dir = '';
|
||||||
|
|
||||||
const deps: BrowserSetupDeps = {
|
const deps: BrowserSetupDeps = {
|
||||||
getBrowserConfig: () => config.browser,
|
getBrowserConfig: () => config.browser,
|
||||||
@@ -115,13 +116,6 @@ describe('browser setup', () => {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
ensureBrowserMcp: () => true,
|
ensureBrowserMcp: () => true,
|
||||||
resolveBrowserRuntimeEnv: async () => ({
|
|
||||||
CCS_BROWSER_USER_DATA_DIR: config.browser.claude.user_data_dir,
|
|
||||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
|
||||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
|
||||||
}),
|
|
||||||
getBrowserStatus: async () =>
|
getBrowserStatus: async () =>
|
||||||
({
|
({
|
||||||
claude: {
|
claude: {
|
||||||
@@ -154,26 +148,21 @@ describe('browser setup', () => {
|
|||||||
binaryPath: '/usr/local/bin/codex',
|
binaryPath: '/usr/local/bin/codex',
|
||||||
},
|
},
|
||||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||||
launchBrowserSession: async () => ({
|
|
||||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
|
||||||
started: false,
|
|
||||||
}),
|
|
||||||
sleep: async () => undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await runBrowserSetup({ launch: false }, deps);
|
const result = await runBrowserSetup(deps);
|
||||||
|
|
||||||
expect(result.configUpdated).toBe(true);
|
expect(result.configUpdated).toBe(true);
|
||||||
expect(result.createdUserDataDir).toBe(true);
|
expect(result.createdUserDataDir).toBe(true);
|
||||||
expect(result.launchAttempted).toBe(false);
|
|
||||||
expect(result.ready).toBe(true);
|
expect(result.ready).toBe(true);
|
||||||
expect(config.browser.claude.enabled).toBe(true);
|
expect(config.browser.claude.enabled).toBe(true);
|
||||||
|
expect(config.browser.claude.user_data_dir).not.toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('launches the browser session when runtime is not ready initially', async () => {
|
it('does not create a custom override path during setup', async () => {
|
||||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||||
let resolveCalls = 0;
|
config.browser.claude.enabled = true;
|
||||||
|
|
||||||
const deps: BrowserSetupDeps = {
|
const deps: BrowserSetupDeps = {
|
||||||
getBrowserConfig: () => config.browser,
|
getBrowserConfig: () => config.browser,
|
||||||
@@ -182,30 +171,16 @@ describe('browser setup', () => {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
ensureBrowserMcp: () => true,
|
ensureBrowserMcp: () => true,
|
||||||
resolveBrowserRuntimeEnv: async () => {
|
|
||||||
resolveCalls += 1;
|
|
||||||
if (resolveCalls < 2) {
|
|
||||||
throw new Error('Chrome reuse metadata not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
CCS_BROWSER_USER_DATA_DIR: config.browser.claude.user_data_dir,
|
|
||||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
|
||||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
|
||||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getBrowserStatus: async () =>
|
getBrowserStatus: async () =>
|
||||||
({
|
({
|
||||||
claude: {
|
claude: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
source: 'config',
|
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||||
overrideActive: false,
|
overrideActive: true,
|
||||||
state: 'ready',
|
state: 'browser_not_running',
|
||||||
title: 'Claude Browser Attach is ready.',
|
title: 'Claude Browser Attach could not find a running browser session.',
|
||||||
detail: 'ready',
|
detail: 'override active',
|
||||||
nextStep: 'Launch Claude.',
|
nextStep: 'Run `ccs browser setup` to configure and start the managed browser session.',
|
||||||
effectiveUserDataDir: config.browser.claude.user_data_dir,
|
effectiveUserDataDir: config.browser.claude.user_data_dir,
|
||||||
recommendedUserDataDir: config.browser.claude.user_data_dir,
|
recommendedUserDataDir: config.browser.claude.user_data_dir,
|
||||||
devtoolsPort: 9222,
|
devtoolsPort: 9222,
|
||||||
@@ -228,18 +203,13 @@ describe('browser setup', () => {
|
|||||||
binaryPath: '/usr/local/bin/codex',
|
binaryPath: '/usr/local/bin/codex',
|
||||||
},
|
},
|
||||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||||
launchBrowserSession: async () => ({
|
|
||||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
|
||||||
started: true,
|
|
||||||
}),
|
|
||||||
sleep: async () => undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await runBrowserSetup({}, deps);
|
const result = await runBrowserSetup(deps);
|
||||||
|
|
||||||
expect(result.launchAttempted).toBe(true);
|
expect(result.createdUserDataDir).toBe(false);
|
||||||
expect(result.launchStarted).toBe(true);
|
expect(result.overrideActive).toBe(false);
|
||||||
expect(result.ready).toBe(true);
|
expect(result.ready).toBe(false);
|
||||||
expect(resolveCalls).toBeGreaterThanOrEqual(2);
|
expect(result.notes[0]).toContain('did not create the current browser user-data dir');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user