Skip to content

Commit 6340ce0

Browse files
authored
fix(cli): resolve initPage/initScript paths and surface load errors (microsoft#40451)
1 parent db7e666 commit 6340ce0

3 files changed

Lines changed: 84 additions & 9 deletions

File tree

packages/playwright-core/src/tools/backend/tab.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ export class Tab extends EventEmitter<TabEventsInterface> {
170170
const { default: func } = require(initPage);
171171
await func({ page: this.page });
172172
} catch (e) {
173-
debug('pw:tools:error')(e);
173+
const reason = e instanceof Error ? e.message : String(e);
174+
throw new Error(`Failed to load init page "${initPage}": ${reason}`, { cause: e });
174175
}
175176
}
176177
}

packages/playwright-core/src/tools/mcp/config.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ export async function resolveCLIConfigForMCP(cliOptions: CLIOptions, env?: NodeJ
115115
const cliOverrides = configFromCLIOptions(cliOptions);
116116
const configFile = cliOverrides.configFile ?? envOverrides.configFile;
117117
const configInFile = await loadConfig(configFile);
118+
const configDir = configFile ? path.dirname(path.resolve(configFile)) : process.cwd();
118119

119120
let result = defaultConfig;
120-
result = mergeConfig(result, configInFile);
121-
result = mergeConfig(result, envOverrides);
122-
result = mergeConfig(result, cliOverrides);
121+
result = mergeConfig(result, resolveConfigPaths(configInFile, configDir));
122+
result = mergeConfig(result, resolveConfigPaths(envOverrides, process.cwd()));
123+
result = mergeConfig(result, resolveConfigPaths(cliOverrides, process.cwd()));
123124

124125
const browser = await validateBrowserConfig(result.browser);
125126
if (browser.launchOptions.headless === undefined)
@@ -151,14 +152,17 @@ export async function resolveCLIConfigForCLI(daemonProfilesDir: string, sessionN
151152
const envOverrides = configFromEnv(env);
152153
const configFile = daemonOverrides.configFile ?? envOverrides.configFile;
153154
const configInFile = await loadConfig(configFile);
155+
const configDir = configFile ? path.dirname(path.resolve(configFile)) : process.cwd();
154156
const globalConfigPath = path.join((env ?? process.env)['PWTEST_CLI_GLOBAL_CONFIG'] ?? os.homedir(), '.playwright', 'cli.config.json');
155-
const globalConfigInFile = await loadConfig(fs.existsSync(globalConfigPath) ? globalConfigPath : undefined);
157+
const globalConfigExists = fs.existsSync(globalConfigPath);
158+
const globalConfigInFile = await loadConfig(globalConfigExists ? globalConfigPath : undefined);
159+
const globalConfigDir = globalConfigExists ? path.dirname(globalConfigPath) : process.cwd();
156160

157161
let result = defaultConfig;
158-
result = mergeConfig(result, globalConfigInFile);
159-
result = mergeConfig(result, configInFile);
160-
result = mergeConfig(result, envOverrides);
161-
result = mergeConfig(result, daemonOverrides);
162+
result = mergeConfig(result, resolveConfigPaths(globalConfigInFile, globalConfigDir));
163+
result = mergeConfig(result, resolveConfigPaths(configInFile, configDir));
164+
result = mergeConfig(result, resolveConfigPaths(envOverrides, process.cwd()));
165+
result = mergeConfig(result, resolveConfigPaths(daemonOverrides, process.cwd()));
162166

163167
if (result.browser.isolated === undefined)
164168
result.browser.isolated = !options.profile && !options.persistent && !result.browser.userDataDir && !result.browser.remoteEndpoint && !result.browser.cdpEndpoint && !result.extension;
@@ -408,6 +412,18 @@ export async function loadConfig(configFile: string | undefined): Promise<Config
408412
}
409413
}
410414

415+
// initPage/initScript paths are resolved against a per-source base dir
416+
// (config-file dir for entries loaded from a --config file, cwd for entries
417+
// supplied via CLI flags or PLAYWRIGHT_MCP_INIT_* env vars) so they keep
418+
// working when the CLI is invoked from a different cwd.
419+
function resolveConfigPaths(config: Config, baseDir: string): Config {
420+
if (config.browser?.initPage)
421+
config.browser.initPage = config.browser.initPage.map(p => path.resolve(baseDir, p));
422+
if (config.browser?.initScript)
423+
config.browser.initScript = config.browser.initScript.map(p => path.resolve(baseDir, p));
424+
return config;
425+
}
426+
411427
function pickDefined<T extends object>(obj: T | undefined): Partial<T> {
412428
return Object.fromEntries(
413429
Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined)

tests/mcp/init-page.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import fs from 'fs';
18+
import path from 'path';
1819

1920
import { test, expect } from './fixtures';
2021

@@ -36,6 +37,63 @@ test('--init-page', async ({ startClient }) => {
3637
});
3738
});
3839

40+
test('init-page relative path from --config resolves against the config dir', async ({ startClient }) => {
41+
// Regression for https://github.com/microsoft/playwright-cli/issues/290:
42+
// relative initPage entries in a --config file used to resolve against
43+
// cwd (or the require() caller), not the config file's directory. We put
44+
// the config + init page in a sibling subdir of cwd so a cwd-based
45+
// resolution would miss the file. Same resolution path also covers
46+
// browser.initScript.
47+
const configDir = test.info().outputPath('cfg');
48+
const cwd = test.info().outputPath('cwd');
49+
await fs.promises.mkdir(configDir, { recursive: true });
50+
await fs.promises.mkdir(cwd, { recursive: true });
51+
52+
const initPagePath = path.join(configDir, 'initPage.ts');
53+
await fs.promises.writeFile(initPagePath, `
54+
export default async ({ page }) => {
55+
await page.setContent('<div>From relative initPage</div>');
56+
};
57+
`);
58+
const configPath = path.join(configDir, 'config.json');
59+
await fs.promises.writeFile(configPath, JSON.stringify({
60+
browser: { initPage: ['./initPage.ts'] },
61+
}));
62+
63+
const { client } = await startClient({
64+
cwd,
65+
args: [`--config=${configPath}`],
66+
});
67+
expect(await client.callTool({
68+
name: 'browser_snapshot',
69+
arguments: {},
70+
})).toHaveResponse({
71+
inlineSnapshot: expect.stringContaining('From relative initPage'),
72+
});
73+
});
74+
75+
test('init-page surfaces load errors instead of silently dropping them', async ({ startClient }) => {
76+
// Regression for https://github.com/microsoft/playwright-cli/issues/290:
77+
// a failing init-page module used to be swallowed into debug logs, leaving
78+
// the user with no signal that their hook never ran.
79+
const initPagePath = test.info().outputPath('brokenInitPage.ts');
80+
await fs.promises.writeFile(initPagePath, `
81+
export default async () => {
82+
throw new Error('boom from initPage');
83+
};
84+
`);
85+
86+
const { client } = await startClient({
87+
args: [`--init-page=${initPagePath}`],
88+
});
89+
const response: any = await client.callTool({
90+
name: 'browser_snapshot',
91+
arguments: {},
92+
});
93+
expect(response.isError).toBe(true);
94+
expect(JSON.stringify(response.content)).toContain('boom from initPage');
95+
});
96+
3997
test('--init-page w/ --init-script', async ({ startClient, server }) => {
4098
server.setContent('/', `
4199
<div>Hello world</div>

0 commit comments

Comments
 (0)