diff --git a/packages/playwright-core/src/tools/cli-daemon/program.ts b/packages/playwright-core/src/tools/cli-daemon/program.ts index 07c216829a998..0bc40ed7071b8 100644 --- a/packages/playwright-core/src/tools/cli-daemon/program.ts +++ b/packages/playwright-core/src/tools/cli-daemon/program.ts @@ -86,6 +86,28 @@ function globalConfigFile(): string { return path.join(process.env['PWTEST_CLI_GLOBAL_CONFIG'] ?? os.homedir(), '.playwright', 'cli.config.json'); } +const cliOutputGitignoreEntries = ['.playwright-cli/']; + +async function ensureGitignore(cwd: string, entries: string[]) { + const gitignorePath = path.join(cwd, '.gitignore'); + let existing = ''; + try { + existing = await fs.promises.readFile(gitignorePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') + throw error; + } + const existingLines = new Set(existing.split(/\r?\n/).map(line => line.trim())); + const missing = entries.filter(entry => !existingLines.has(entry)); + if (!missing.length) + return; + const needsNewline = existing.length > 0 && !existing.endsWith('\n'); + const header = '# Playwright CLI traces and snapshots (may contain credentials)\n'; + const chunk = `${needsNewline ? '\n' : ''}${header}${missing.join('\n')}\n`; + await fs.promises.appendFile(gitignorePath, chunk); + console.log(`✅ Added ${missing.map(entry => `\`${entry}\``).join(', ')} to \`.gitignore\`.`); +} + export async function initWorkspace(initSkills: string | undefined, initSkillsGlobal?: string) { const globalSkills = !!initSkillsGlobal; if (!globalSkills) { @@ -93,6 +115,7 @@ export async function initWorkspace(initSkills: string | undefined, initSkillsGl const playwrightDir = path.join(cwd, '.playwright'); await fs.promises.mkdir(playwrightDir, { recursive: true }); console.log(`✅ Workspace initialized at \`${cwd}\`.`); + await ensureGitignore(cwd, cliOutputGitignoreEntries); } const skills = initSkillsGlobal ?? initSkills; diff --git a/tests/mcp/cli-misc.spec.ts b/tests/mcp/cli-misc.spec.ts index a3e8630d56634..77fd55d80889a 100644 --- a/tests/mcp/cli-misc.spec.ts +++ b/tests/mcp/cli-misc.spec.ts @@ -40,6 +40,26 @@ test('install workspace', async ({ cli }, testInfo) => { expect(fs.existsSync(playwrightDir)).toBe(true); }); +test('install workspace gitignores CLI output dir', async ({ cli }, testInfo) => { + const { output } = await cli('install'); + expect(output).toContain('Added `.playwright-cli/` to `.gitignore`.'); + const gitignore = await fs.promises.readFile(testInfo.outputPath('.gitignore'), 'utf8'); + expect(gitignore).toContain('.playwright-cli/'); + + const second = await cli('install'); + expect(second.output).not.toContain('Added `.playwright-cli/` to `.gitignore`.'); + const gitignoreAfter = await fs.promises.readFile(testInfo.outputPath('.gitignore'), 'utf8'); + expect(gitignoreAfter.split('.playwright-cli/').length - 1).toBe(1); +}); + +test('install workspace appends CLI output dir to an existing gitignore', async ({ cli }, testInfo) => { + await fs.promises.writeFile(testInfo.outputPath('.gitignore'), 'node_modules/\n'); + await cli('install'); + const gitignore = await fs.promises.readFile(testInfo.outputPath('.gitignore'), 'utf8'); + expect(gitignore).toMatch(/^node_modules\/\n/); + expect(gitignore).toContain('.playwright-cli/'); +}); + test('install workspace w/skills', async ({ cli }, testInfo) => { const { output } = await cli('install', '--skills'); expect(output).toContain(`Skill installed to \`.claude${path.sep}skills${path.sep}playwright-cli\`.`); @@ -66,6 +86,8 @@ test('install w/--skills -g installs into the home directory', async ({ cli }, t const { output } = await cli('install', '--skills', '-g', { env: { HOME: fakeHome, USERPROFILE: fakeHome } }); expect(output).toContain('Skill installed to'); expect(output).not.toContain('Workspace initialized'); + expect(output).not.toContain('.gitignore'); + expect(fs.existsSync(testInfo.outputPath('.gitignore'))).toBe(false); const skillFile = path.join(fakeHome, '.claude', 'skills', 'playwright-cli', 'SKILL.md'); expect(fs.existsSync(skillFile)).toBe(true);