playwright-test-agent 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,29 @@
1
- #!/usr/bin/env node
2
-
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { initializeProject } from '../playwright-test-agent/scripts/init-playwright.mjs';
6
-
7
- const usage = `Usage: playwright-test-agent init [target-directory]
8
-
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { initializeProject } from '../playwright-test-agent/scripts/init-playwright.mjs';
6
+
7
+ const usage = `Usage: playwright-test-agent init [target-directory]
8
+
9
9
  Initialize Playwright Test agents, install the playwright-test-agent skill,
10
- configure the project MCP servers, and update AGENTS.md and CLAUDE.md.`;
11
-
12
- const [command, targetDirectory, ...extraArguments] = process.argv.slice(2);
13
-
14
- if (command !== 'init' || extraArguments.length > 0) {
15
- console.error(usage);
16
- process.exitCode = 1;
17
- } else {
18
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
-
20
- try {
21
- await initializeProject({
22
- projectDir: path.resolve(targetDirectory ?? process.cwd()),
23
- skillSourceDir: path.join(packageRoot, 'playwright-test-agent'),
24
- });
25
- } catch (error) {
26
- console.error(`playwright-test-agent init failed: ${error.message}`);
27
- process.exitCode = 1;
28
- }
29
- }
10
+ install Playwright CLI globally, and update AGENTS.md and CLAUDE.md.`;
11
+
12
+ const [command, targetDirectory, ...extraArguments] = process.argv.slice(2);
13
+
14
+ if (command !== 'init' || extraArguments.length > 0) {
15
+ console.error(usage);
16
+ process.exitCode = 1;
17
+ } else {
18
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+
20
+ try {
21
+ await initializeProject({
22
+ projectDir: path.resolve(targetDirectory ?? process.cwd()),
23
+ skillSourceDir: path.join(packageRoot, 'playwright-test-agent'),
24
+ });
25
+ } catch (error) {
26
+ console.error(`playwright-test-agent init failed: ${error.message}`);
27
+ process.exitCode = 1;
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
- {
2
- "name": "playwright-test-agent",
3
- "version": "0.1.6",
4
- "description": "Initialize Playwright Test, its agents, MCP configuration, and the playwright-test-agent skill.",
5
- "type": "module",
6
- "bin": {
7
- "playwright-test-agent": "bin/playwright-test-agent.mjs"
8
- },
9
- "files": [
10
- "bin/",
11
- "playwright-test-agent/"
12
- ],
13
- "engines": {
14
- "node": ">=18"
15
- },
16
- "license": "UNLICENSED"
17
- }
1
+ {
2
+ "name": "playwright-test-agent",
3
+ "version": "0.1.7",
4
+ "description": "Initialize Playwright Test agents with a Playwright CLI-first browser workflow.",
5
+ "type": "module",
6
+ "bin": {
7
+ "playwright-test-agent": "bin/playwright-test-agent.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "playwright-test-agent/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }
@@ -1,64 +1,54 @@
1
1
  ---
2
2
  name: playwright-test-agent
3
- description: Use first when a user asks to investigate or test a website, browser workflow, HTTP API, or application feature with Playwright, including test planning, generation, execution, debugging, healing, and evidence collection.
3
+ description: Use first when a user asks to investigate or test a website, browser workflow, HTTP API, or application feature with Playwright, including planning, generation, execution, debugging, healing, and evidence collection.
4
4
  ---
5
5
 
6
6
  # Playwright Test Agent
7
7
 
8
- Use Playwright's Planner -> Generator -> Healer workflow for durable, auditable tests. `AGENTS.md` or `CLAUDE.md` defines how the active platform assumes or launches those roles; this Skill contains only their shared workflow.
8
+ Use Planner -> Generator -> Healer for durable tests. `AGENTS.md` or `CLAUDE.md` defines the platform-specific role routing. Generated role definitions use `playwright-cli` as their primary browser interface, so MCP tool injection is not required.
9
9
 
10
10
  ## Prepare the objective
11
11
 
12
- The main agent may inspect the local project for relevant requirements, routes, fixtures, and constraints. Do not require a setup audit or check for a seed, Playwright config, or generated files before beginning Planner.
12
+ The main agent may inspect the local project for relevant requirements, routes, fixtures, and constraints. Do not require a seed/config audit before planning.
13
13
 
14
- Before planning, ask the user for information that could materially change the test purpose, scope, safety, or expected behavior, such as the deployed URL, intended role, success criteria, allowed data mutations, and required environment. Do not ask for UI details Planner can safely discover.
14
+ Before Planner, ask for information that could materially change purpose, scope, safety, or expected behavior: deployed URL, intended role, success criteria, allowed data mutations, and environment constraints. Do not ask for UI details Planner can safely discover.
15
15
 
16
- If the request is sufficient, begin Planner immediately. If investigation reveals that a URL, credential, role, prerequisite, expected result, or authorization boundary is missing, incorrect, or contradictory, pause Planner and ask for the specific correction. Resume Planner with the corrected information and blocking observation. Do not guess credentials, probe nearby environments, bypass access controls, or wander through unrelated pages.
16
+ If the request is sufficient, begin Planner immediately. If investigation finds a missing, incorrect, or contradictory URL, credential, role, prerequisite, expected result, or authorization boundary, pause and ask for the specific correction. Resume Planner with the correction and blocking observation. Do not guess credentials, probe nearby environments, bypass access controls, or wander through unrelated pages.
17
17
 
18
18
  Keep credentials and tokens in environment variables or ignored secret files. Never copy them into plans, tests, screenshots, evidence, or chat.
19
19
 
20
20
  ## Planner
21
21
 
22
- Planner owns live application investigation and saves a human-readable Markdown plan under `specs/`. Start page setup immediately. Supply a seed only when the user or request explicitly identifies one; otherwise allow Playwright to use its initialized default. Do not perform a separate seed/config discovery pass.
22
+ Planner investigates the live application with `playwright-cli` and saves a human-readable Markdown plan under `specs/`. Use `open`/`goto`, compact `snapshot` or `find`, and refs for interaction. Supply a seed only when explicitly identified; do not perform a separate seed discovery pass.
23
23
 
24
- The plan contains prerequisites, test data requirements, independent scenarios, steps, observable expected results, exclusions, and intended test outputs. Reconnaissance must not create durable/shared data or perform purchases, submissions, messages, deletion, or other consequential actions unless the user authorized them.
24
+ The plan contains prerequisites, test data, independent scenarios, steps, observable expected results, exclusions, and intended output files. Reconnaissance must not mutate durable/shared data or perform consequential actions unless authorized.
25
25
 
26
- Show the completed scenario list and exclusions to the user. Generator may start only after the user confirms the plan.
26
+ Show the scenario list and exclusions to the user. Generator starts only after confirmation.
27
27
 
28
28
  ## Generator
29
29
 
30
- Generator converts the complete confirmed Markdown plan into executable Playwright tests and validates locators and assertions against the application. Generate only confirmed scenarios. Prefer independent tests, semantic locators, assertions for every expected result, and environment-based secrets.
30
+ Generator converts the confirmed plan into executable tests. Use `playwright-cli` to validate flows, locators, and assertions against the live application, then write the tests directly. Generate only confirmed scenarios; prefer independent tests, semantic locators, explicit assertions, and environment-based secrets.
31
31
 
32
- For API-only scenarios, Playwright `APIRequestContext` tests may be written directly. Assert status, headers, schema, stable business invariants, and safe mutation cleanup.
32
+ For API-only scenarios, use Playwright `APIRequestContext` directly and assert status, headers, schema, stable business invariants, and safe mutation cleanup.
33
33
 
34
34
  ## Healer
35
35
 
36
- Use Healer only for failing generated tests. Diagnose one failing test at a time, replay the failure, inspect the current application state, apply a test-side correction when justified, and rerun within the role's guardrails.
36
+ Use Healer only for failures. Run the failing test with `npx playwright test`, reproduce and inspect the current UI with `playwright-cli`, apply justified test-side changes, and rerun within guardrails.
37
37
 
38
- Do not weaken assertions, add arbitrary sleeps, retry blindly, or skip a test merely to obtain a pass. If behavior conflicts with the confirmed plan, preserve the evidence and ask whether it is a regression or intended product change. Classify unresolved failures as an application defect, test defect, environment/data problem, or product decision.
38
+ Do not weaken assertions, add arbitrary sleeps, retry blindly, or skip merely to pass. If behavior conflicts with the confirmed plan, preserve evidence and ask whether it is a regression or intended change. Classify unresolved failures as application defect, test defect, environment/data problem, or product decision.
39
39
 
40
40
  ## Evidence
41
41
 
42
- Keep generated evidence under `.playwright-evidence/`:
42
+ Keep evidence under `.playwright-evidence/`. Prefer DOM snapshots and targeted `find` output; take screenshots only when visual evidence is necessary. Keep trace DOM snapshots, sources, network data, and attachments, but set trace `screenshots: false` to avoid hundreds of screencast JPEGs.
43
43
 
44
- ```text
45
- .playwright-evidence/
46
- |-- mcp/
47
- |-- snapshots/
48
- |-- test-results/
49
- `-- report/
50
- ```
51
-
52
- Keep trace DOM snapshots, sources, network data, and attachments, but set trace `screenshots: false`. Trace screenshots are screencast frames and can create hundreds of JPEG resources. Explicit screenshots and failure evidence remain available when needed; avoid secrets and sensitive personal data.
53
-
54
- Report the plan path, generated test paths, pass/fail/flaky/skipped counts when tests ran, evidence paths, and failure classification. Never report a skipped or unresolved test as success.
44
+ Report the plan path, generated tests, counts when tests ran, evidence paths, and failure classification. Never report skipped or unresolved tests as success.
55
45
 
56
46
  ## Initialization
57
47
 
58
- When this Skill or the Playwright Test tools are unavailable, run from the target project:
48
+ Run from the target project:
59
49
 
60
50
  ```bash
61
51
  npx playwright-test-agent init
62
52
  ```
63
53
 
64
- The initializer installs the Skill, generates the official role definitions, and configures each platform. Restart the active agent session after initialization so newly configured tools are registered.
54
+ The command globally installs `@playwright/cli`, installs its Codex/Claude skills and browser, initializes Playwright Test agents, and reinjects CLI-first instructions into all six generated role definitions. Run it again after Playwright updates regenerate those definitions. Restart Codex or Claude Code after initialization so global skills and changed definitions are reloaded.
@@ -1,339 +1,301 @@
1
- #!/usr/bin/env node
2
-
3
- import { access, cp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { spawn } from 'node:child_process';
6
- import { fileURLToPath } from 'node:url';
7
-
1
+ #!/usr/bin/env node
2
+
3
+ import { access, cp, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
8
  const BLOCK_START = '<!-- playwright-test-agent:start -->';
9
9
  const BLOCK_END = '<!-- playwright-test-agent:end -->';
10
- const CODEX_INSTRUCTIONS = `${BLOCK_START}
11
- ## Playwright Test Agent
12
-
13
- For website, browser workflow, HTTP API, or application-feature testing, first load and follow \`.agents/skills/playwright-test-agent/SKILL.md\`.
14
-
15
- Codex must not start \`playwright_test_planner\`, \`playwright_test_generator\`, \`playwright_test_healer\`, or another Playwright subagent. The main agent performs Planner -> Generator -> Healer in the same session with the project-level \`playwright-test\` MCP tools.
16
-
17
- For each phase, read the matching generated role definition and follow its \`developer_instructions\` directly:
18
- - Planner: \`.codex/agents/playwright_test_planner.toml\`; call \`planner_setup_page\`, \`browser_*\`, then \`planner_save_plan\`.
19
- - Generator: \`.codex/agents/playwright_test_generator.toml\`; call \`generator_setup_page\`, \`browser_*\`, \`generator_read_log\`, then \`generator_write_test\`.
20
- - Healer: \`.codex/agents/playwright_test_healer.toml\`; call \`test_list\`, \`test_run\`, \`test_debug\`, and relevant \`browser_*\` tools.
21
-
22
- These TOML files are role instructions to read, not agents to launch. If the project-level \`playwright-test\` tools are absent, ask the user to rerun \`npx playwright-test-agent init\` and restart Codex; do not create a subagent fallback. Ask for missing or contradictory test-purpose, access, role, data, or safety information before or during Planner, then resume the same phase after clarification. The Skill contains the shared testing rules.
23
- ${BLOCK_END}`;
24
-
25
- const CLAUDE_INSTRUCTIONS = `${BLOCK_START}
26
- ## Playwright Test Agent
27
-
28
- For website, browser workflow, HTTP API, or application-feature testing, first load and follow \`.claude/skills/playwright-test-agent/SKILL.md\`.
29
-
30
- Claude Code uses the official \`playwright-test-planner\`, \`playwright-test-generator\`, and \`playwright-test-healer\` subagents. Start each required role directly and never insert a nested or same-role intermediary. Use Planner for all live website investigation, Generator only after the user confirms the saved plan, and Healer for failing generated tests. Ask for missing or contradictory test-purpose, access, role, data, or safety information before or during Planner, then resume the same phase after clarification. The Skill contains the shared testing rules.
31
- ${BLOCK_END}`;
32
-
33
- const exists = async (file) => {
34
- try {
35
- await access(file);
36
- return true;
37
- } catch {
38
- return false;
10
+ const ROLE_BLOCK_START = '<!-- playwright-test-agent:cli-first:start -->';
11
+ const ROLE_BLOCK_END = '<!-- playwright-test-agent:cli-first:end -->';
12
+ const cliFirstInstructions = (role) => `${ROLE_BLOCK_START}
13
+
14
+ ## Browser tool priority
15
+
16
+ Use the installed \`playwright-cli\` command as the primary browser interface. Read the globally installed \`playwright-cli\` skill when available. Start or attach a session with \`playwright-cli open\`/\`attach\`, navigate with \`goto\`, inspect compact state with \`snapshot\`, and interact through refs from the latest snapshot. Prefer \`snapshot\` and \`find\` over screenshots. Keep credentials in environment variables or ignored files and never print them.
17
+
18
+ CLI replaces any conflicting requirement above to call Playwright Test MCP setup, browser, save, generation, or debug tools. Do not stop merely because \`planner_*\`, \`generator_*\`, \`test_*\`, or \`browser_*\` MCP tools are absent, and do not spawn a nested or same-role agent.
19
+
20
+ ${role === 'planner' ? 'Explore with CLI and save the completed Markdown test plan directly under `specs/` using filesystem tools.' : ''}${role === 'generator' ? 'Use CLI to validate the confirmed plan against the live UI, then write executable Playwright test files directly with filesystem tools.' : ''}${role === 'healer' ? 'Run failures with `npx playwright test`, use CLI to inspect and reproduce the current UI, patch the tests directly, and rerun within the healer guardrails.' : ''}
21
+
22
+ ${ROLE_BLOCK_END}`;
23
+ const CODEX_INSTRUCTIONS = `${BLOCK_START}
24
+ ## Playwright Test Agent
25
+
26
+ For website, browser workflow, HTTP API, or application-feature testing, load \`.agents/skills/playwright-test-agent/SKILL.md\` first. Codex must not start Playwright subagents; the main agent performs Planner -> Generator -> Healer in this session. Use \`playwright-cli\` as the primary browser interface and read generated TOML role definitions only for responsibilities. Ask for missing or contradictory purpose, access, role, data, or safety information before or during planning; do not guess or create nested agents.
27
+
28
+ ${BLOCK_END}`;
29
+
30
+ const CLAUDE_INSTRUCTIONS = `${BLOCK_START}
31
+ ## Playwright Test Agent
32
+
33
+ For website, browser workflow, HTTP API, or application-feature testing, load \`.claude/skills/playwright-test-agent/SKILL.md\` first. Claude Code starts the official \`playwright-test-planner\`, \`playwright-test-generator\`, and \`playwright-test-healer\` directly. These roles use \`playwright-cli\` as their primary browser interface and must not create nested agents. Ask for missing or contradictory purpose, access, role, data, or safety information before or during planning; do not guess.
34
+
35
+ ${BLOCK_END}`;
36
+
37
+ const exists = async (file) => {
38
+ try {
39
+ await access(file);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ };
45
+
46
+ const defaultRun = (projectDir, command, args) => new Promise((resolve, reject) => {
47
+ const child = process.platform === 'win32'
48
+ ? spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', command, ...args], {
49
+ cwd: projectDir,
50
+ stdio: 'inherit',
51
+ })
52
+ : spawn(command, args, { cwd: projectDir, stdio: 'inherit' });
53
+ child.on('error', reject);
54
+ child.on('exit', (code) => code === 0
55
+ ? resolve()
56
+ : reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)));
57
+ });
58
+
59
+ function withManagedBlock(source, managedBlock) {
60
+ const start = source.indexOf(BLOCK_START);
61
+ const end = source.indexOf(BLOCK_END);
62
+
63
+ if ((start === -1) !== (end === -1) || (start !== -1 && end < start)) {
64
+ throw new Error('found an incomplete playwright-test-agent managed block');
65
+ }
66
+
67
+ if (start !== -1) {
68
+ return source.slice(0, start) + managedBlock + source.slice(end + BLOCK_END.length);
69
+ }
70
+
71
+ if (source.length === 0) return `${managedBlock}\n`;
72
+ const separator = source.endsWith('\n') ? '\n' : '\n\n';
73
+ return `${source}${separator}${managedBlock}\n`;
74
+ }
75
+
76
+ async function installSkill(projectDir, skillSourceDir) {
77
+ for (const root of ['.agents', '.claude']) {
78
+ const destination = path.join(projectDir, root, 'skills', 'playwright-test-agent');
79
+ await mkdir(destination, { recursive: true });
80
+ await cp(skillSourceDir, destination, { recursive: true, force: true });
81
+ }
82
+ }
83
+
84
+ async function updateInstructionFiles(projectDir) {
85
+ const files = [
86
+ ['AGENTS.md', CODEX_INSTRUCTIONS],
87
+ ['CLAUDE.md', CLAUDE_INSTRUCTIONS],
88
+ ];
89
+ for (const [name, managedBlock] of files) {
90
+ const file = path.join(projectDir, name);
91
+ const source = await exists(file) ? await readFile(file, 'utf8') : '';
92
+ await writeFile(file, withManagedBlock(source, managedBlock), 'utf8');
39
93
  }
40
- };
41
-
42
- const defaultRun = (projectDir, command, args) => new Promise((resolve, reject) => {
43
- const child = process.platform === 'win32'
44
- ? spawn(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', command, ...args], {
45
- cwd: projectDir,
46
- stdio: 'inherit',
47
- })
48
- : spawn(command, args, { cwd: projectDir, stdio: 'inherit' });
49
- child.on('error', reject);
50
- child.on('exit', (code) => code === 0
51
- ? resolve()
52
- : reject(new Error(`${command} ${args.join(' ')} exited with ${code}`)));
53
- });
54
-
55
- function withManagedBlock(source, managedBlock) {
56
- const start = source.indexOf(BLOCK_START);
57
- const end = source.indexOf(BLOCK_END);
94
+ }
58
95
 
96
+ function withRoleBlock(source, role) {
97
+ const start = source.indexOf(ROLE_BLOCK_START);
98
+ const end = source.indexOf(ROLE_BLOCK_END);
59
99
  if ((start === -1) !== (end === -1) || (start !== -1 && end < start)) {
60
- throw new Error('found an incomplete playwright-test-agent managed block');
100
+ throw new Error('found an incomplete playwright-test-agent CLI role block');
61
101
  }
62
-
63
102
  if (start !== -1) {
64
- return source.slice(0, start) + managedBlock + source.slice(end + BLOCK_END.length);
65
- }
66
-
67
- if (source.length === 0) return `${managedBlock}\n`;
68
- const separator = source.endsWith('\n') ? '\n' : '\n\n';
69
- return `${source}${separator}${managedBlock}\n`;
70
- }
71
-
72
- async function installSkill(projectDir, skillSourceDir) {
73
- for (const root of ['.agents', '.claude']) {
74
- const destination = path.join(projectDir, root, 'skills', 'playwright-test-agent');
75
- await mkdir(destination, { recursive: true });
76
- await cp(skillSourceDir, destination, { recursive: true, force: true });
77
- }
78
- }
79
-
80
- async function updateInstructionFiles(projectDir) {
81
- const files = [
82
- ['AGENTS.md', CODEX_INSTRUCTIONS],
83
- ['CLAUDE.md', CLAUDE_INSTRUCTIONS],
84
- ];
85
- for (const [name, managedBlock] of files) {
86
- const file = path.join(projectDir, name);
87
- const source = await exists(file) ? await readFile(file, 'utf8') : '';
88
- await writeFile(file, withManagedBlock(source, managedBlock), 'utf8');
89
- }
90
- }
91
-
92
- async function patchPlaywrightConfig(projectDir) {
93
- const configPath = path.join(projectDir, 'playwright.config.ts');
94
- let source = await readFile(configPath, 'utf8');
95
- source = source.replace(/testDir:\s*['"]\.\/tests['"]/, "testDir: './playwright-tests'");
96
- source = source.replace(
97
- /reporter:\s*['"]html['"],?/,
98
- "reporter: [['html', { outputFolder: '.playwright-evidence/report', open: 'never' }]],",
99
- );
100
- source = source.replace(
101
- /use:\s*\{/,
102
- "outputDir: '.playwright-evidence/test-results',\n snapshotPathTemplate: '.playwright-evidence/snapshots/{testFilePath}/{arg}{ext}',\n use: {\n screenshot: 'only-on-failure',\n video: 'retain-on-failure',",
103
- );
104
- source = source.replace(
105
- /trace:\s*['"][^'"]+['"]/,
106
- "trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }",
107
- );
108
- await writeFile(configPath, source, 'utf8');
109
- }
110
-
111
- function withoutTraceScreenshots(source) {
112
- const replacement = "trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }";
113
- const fromString = source.replace(/trace:\s*['"][^'"]+['"]/, replacement);
114
- if (fromString !== source) return fromString;
115
-
116
- return source.replace(/trace:\s*\{([\s\S]*?)\}/, (setting, body) => {
117
- if (/screenshots\s*:/.test(body)) {
118
- return setting.replace(/screenshots\s*:\s*(?:true|false)/, 'screenshots: false');
119
- }
120
- return setting.replace('{', '{ screenshots: false,');
121
- });
122
- }
123
-
124
- async function disableTraceScreenshots(projectDir) {
125
- const configPath = path.join(projectDir, 'playwright.config.ts');
126
- if (!await exists(configPath)) return;
127
-
128
- let source = await readFile(configPath, 'utf8');
129
- const updated = withoutTraceScreenshots(source);
130
-
131
- if (updated !== source) {
132
- await writeFile(configPath, updated, 'utf8');
103
+ return source.slice(0, start) + cliFirstInstructions(role) +
104
+ source.slice(end + ROLE_BLOCK_END.length);
133
105
  }
106
+ return `${source.trimEnd()}\n\n${cliFirstInstructions(role)}\n`;
134
107
  }
135
108
 
136
- async function configureClaudeMcp(projectDir) {
137
- const file = path.join(projectDir, '.mcp.json');
138
- if (!await exists(file)) return;
139
- const config = JSON.parse(await readFile(file, 'utf8'));
140
- const server = config.mcpServers?.['playwright-test'];
141
- if (!server) return;
142
- server.env = {
143
- ...(server.env ?? {}),
144
- PLAYWRIGHT_MCP_OUTPUT_DIR: '.playwright-evidence/mcp',
145
- };
146
- await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
109
+ function tomlMultilineBasicString(value) {
110
+ const escaped = value
111
+ .replace(/\\/g, '\\\\')
112
+ .replace(/"""/g, '\\"\\"\\"')
113
+ .replace(/\r\n?/g, '\n');
114
+ return `"""\n${escaped}\n"""`;
147
115
  }
148
116
 
149
- function normalizeTomlTableName(name) {
150
- return name
151
- .split('.')
152
- .map((part) => part.trim().replace(/^(?:"([^"]+)"|'([^']+)')$/, '$1$2'))
153
- .join('.');
117
+ function patchCodexRole(source, role) {
118
+ const pattern = /(^|\n)developer_instructions\s*=\s*"""\r?\n([\s\S]*?)\r?\n"""/;
119
+ const match = source.match(pattern);
120
+ if (!match) throw new Error('developer_instructions was not found in a Codex role definition');
121
+ const updated = withRoleBlock(match[2], role);
122
+ return source
123
+ .replace(pattern, `${match[1]}developer_instructions = ${tomlMultilineBasicString(updated)}`)
124
+ .replace(/^sandbox_mode\s*=\s*"[^"]+"/m, 'sandbox_mode = "workspace-write"');
154
125
  }
155
126
 
156
- function findTomlTables(lines, tableName) {
157
- const matches = [];
158
- for (let index = 0; index < lines.length; index += 1) {
159
- const match = lines[index].match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
160
- if (match && normalizeTomlTableName(match[1]) === tableName) matches.push(index);
127
+ function patchClaudeRole(source, role) {
128
+ let updated = source;
129
+ if (!/^\s*-\s+Bash\(playwright-cli:\*\)\s*$/m.test(updated)) {
130
+ const tools = updated.match(/^tools:\s*\r?\n/m);
131
+ if (!tools) throw new Error('tools list was not found in a Claude role definition');
132
+ const insertion = ' - Bash(playwright-cli:*)\n - Bash(npx:*)\n';
133
+ updated = updated.slice(0, tools.index + tools[0].length) + insertion +
134
+ updated.slice(tools.index + tools[0].length);
161
135
  }
162
- return matches;
136
+ return withRoleBlock(updated, role);
163
137
  }
164
138
 
165
- function listTomlTableNames(lines) {
166
- return lines
167
- .map((line) => line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/))
168
- .filter(Boolean)
169
- .map((match) => normalizeTomlTableName(match[1]));
170
- }
171
-
172
- function assignmentEnd(lines, start, limit) {
173
- let square = 0;
174
- let curly = 0;
175
- let quote = null;
176
- let escaped = false;
177
-
178
- for (let index = start; index < limit; index += 1) {
179
- for (const character of lines[index]) {
180
- if (escaped) {
181
- escaped = false;
182
- } else if (quote === '"' && character === '\\') {
183
- escaped = true;
184
- } else if (quote) {
185
- if (character === quote) quote = null;
186
- } else if (character === '"' || character === "'") {
187
- quote = character;
188
- } else if (character === '#') {
189
- break;
190
- } else if (character === '[') {
191
- square += 1;
192
- } else if (character === ']') {
193
- square -= 1;
194
- } else if (character === '{') {
195
- curly += 1;
196
- } else if (character === '}') {
197
- curly -= 1;
198
- }
139
+ async function configureCliFirstRoles(projectDir) {
140
+ const roles = ['planner', 'generator', 'healer'];
141
+ for (const role of roles) {
142
+ const claudeFile = path.join(projectDir, '.claude', 'agents', `playwright-test-${role}.md`);
143
+ if (await exists(claudeFile)) {
144
+ const source = await readFile(claudeFile, 'utf8');
145
+ await writeFile(claudeFile, patchClaudeRole(source, role), 'utf8');
199
146
  }
200
- if (square <= 0 && curly <= 0 && !quote) return index + 1;
201
- }
202
- return limit;
203
- }
204
-
205
- function replaceTomlTable(lines, tableName, assignments, removedKeys = []) {
206
- const matches = findTomlTables(lines, tableName);
207
- if (matches.length > 1) {
208
- throw new Error(`duplicate [${tableName}] tables in .codex/config.toml`);
209
- }
210
-
211
- const body = [`[${tableName}]`, ...Object.entries(assignments).map(([key, value]) => `${key} = ${value}`)];
212
- if (matches.length === 0) {
213
- while (lines.at(-1) === '') lines.pop();
214
- if (lines.length > 0) lines.push('');
215
- lines.push(...body, '');
216
- return;
217
- }
218
147
 
219
- const start = matches[0];
220
- let end = start + 1;
221
- while (end < lines.length && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(lines[end])) {
222
- end += 1;
223
- }
224
-
225
- const replacedKeys = new Set([...Object.keys(assignments), ...removedKeys]);
226
- const preserved = [];
227
- for (let index = start + 1; index < end;) {
228
- const match = lines[index].match(/^\s*([A-Za-z0-9_-]+)\s*=/);
229
- if (match && replacedKeys.has(match[1])) {
230
- index = assignmentEnd(lines, index, end);
231
- } else {
232
- preserved.push(lines[index]);
233
- index += 1;
148
+ const codexFile = path.join(projectDir, '.codex', 'agents', `playwright_test_${role}.toml`);
149
+ if (await exists(codexFile)) {
150
+ const source = await readFile(codexFile, 'utf8');
151
+ await writeFile(codexFile, patchCodexRole(source, role), 'utf8');
234
152
  }
235
153
  }
236
- while (preserved[0] === '') preserved.shift();
237
- while (preserved.at(-1) === '') preserved.pop();
238
- lines.splice(start, end - start, ...body, ...(preserved.length ? ['', ...preserved] : []), '');
239
154
  }
240
155
 
241
- async function configureCodexMainMcp(projectDir) {
242
- const codexDir = path.join(projectDir, '.codex');
243
- const file = path.join(codexDir, 'config.toml');
244
- await mkdir(codexDir, { recursive: true });
245
-
246
- const source = await exists(file) ? await readFile(file, 'utf8') : '';
247
- const lines = source.replace(/\r\n?/g, '\n').split('\n');
248
- const command = process.platform === 'win32' ? 'cmd' : 'npx';
249
- const args = process.platform === 'win32'
250
- ? '["/d", "/s", "/c", "npx", "--no-install", "playwright", "run-test-mcp-server"]'
251
- : '["--no-install", "playwright", "run-test-mcp-server"]';
252
-
253
- replaceTomlTable(lines, 'mcp_servers.playwright-test', {
254
- command: `"${command}"`,
255
- args,
256
- cwd: '"."',
257
- enabled: 'true',
258
- default_tools_approval_mode: '"approve"',
259
- }, ['enabled_tools', 'disabled_tools']);
260
- replaceTomlTable(lines, 'mcp_servers.playwright-test.env', {
261
- PLAYWRIGHT_MCP_OUTPUT_DIR: '".playwright-evidence/mcp"',
262
- });
263
- for (const tableName of new Set(listTomlTableNames(lines))) {
264
- if (tableName.startsWith('mcp_servers.playwright-test.tools.')) {
265
- replaceTomlTable(lines, tableName, { approval_mode: '"approve"' });
266
- }
267
- }
268
-
269
- while (lines.at(-1) === '') lines.pop();
270
- await writeFile(file, `${lines.join('\n')}\n`, 'utf8');
156
+ async function installPlaywrightCli(projectDir, run) {
157
+ await run(projectDir, 'npm', ['install', '-g', '@playwright/cli@latest']);
158
+ await run(projectDir, 'playwright-cli', ['install', '--skills=agents', '--global']);
159
+ await run(projectDir, 'playwright-cli', ['install', '--skills', '--global']);
160
+ await run(projectDir, 'playwright-cli', ['install-browser']);
271
161
  }
272
-
273
- async function initializePlaywright(projectDir, run) {
274
- const configPath = path.join(projectDir, 'playwright.config.ts');
275
- if (!await exists(configPath)) {
276
- await run(projectDir, 'npm', [
277
- 'init',
278
- 'playwright@latest',
279
- '--',
280
- '--quiet',
281
- '--lang=TypeScript',
282
- '--browser=chromium',
283
- ]);
284
-
285
- const generatedExample = path.join(projectDir, 'tests', 'example.spec.ts');
286
- const targetDir = path.join(projectDir, 'playwright-tests');
287
- if (await exists(generatedExample)) {
288
- await mkdir(targetDir, { recursive: true });
289
- await rename(generatedExample, path.join(targetDir, 'example.spec.ts'));
290
- }
291
- await patchPlaywrightConfig(projectDir);
292
- }
293
-
294
- await disableTraceScreenshots(projectDir);
295
-
296
- await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=codex']);
297
- await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=claude']);
298
- await mkdir(path.join(projectDir, '.playwright-evidence', 'mcp'), { recursive: true });
162
+
163
+ async function patchPlaywrightConfig(projectDir) {
164
+ const configPath = path.join(projectDir, 'playwright.config.ts');
165
+ let source = await readFile(configPath, 'utf8');
166
+ source = source.replace(/testDir:\s*['"]\.\/tests['"]/, "testDir: './playwright-tests'");
167
+ source = source.replace(
168
+ /reporter:\s*['"]html['"],?/,
169
+ "reporter: [['html', { outputFolder: '.playwright-evidence/report', open: 'never' }]],",
170
+ );
171
+ source = source.replace(
172
+ /use:\s*\{/,
173
+ "outputDir: '.playwright-evidence/test-results',\n snapshotPathTemplate: '.playwright-evidence/snapshots/{testFilePath}/{arg}{ext}',\n use: {\n screenshot: 'only-on-failure',\n video: 'retain-on-failure',",
174
+ );
175
+ source = source.replace(
176
+ /trace:\s*['"][^'"]+['"]/,
177
+ "trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }",
178
+ );
179
+ await writeFile(configPath, source, 'utf8');
180
+ }
181
+
182
+ function withoutTraceScreenshots(source) {
183
+ const replacement = "trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }";
184
+ const fromString = source.replace(/trace:\s*['"][^'"]+['"]/, replacement);
185
+ if (fromString !== source) return fromString;
186
+
187
+ return source.replace(/trace:\s*\{([\s\S]*?)\}/, (setting, body) => {
188
+ if (/screenshots\s*:/.test(body)) {
189
+ return setting.replace(/screenshots\s*:\s*(?:true|false)/, 'screenshots: false');
190
+ }
191
+ return setting.replace('{', '{ screenshots: false,');
192
+ });
193
+ }
194
+
195
+ async function disableTraceScreenshots(projectDir) {
196
+ const configPath = path.join(projectDir, 'playwright.config.ts');
197
+ if (!await exists(configPath)) return;
198
+
199
+ let source = await readFile(configPath, 'utf8');
200
+ const updated = withoutTraceScreenshots(source);
201
+
202
+ if (updated !== source) {
203
+ await writeFile(configPath, updated, 'utf8');
204
+ }
205
+ }
206
+
207
+ async function configureClaudeMcp(projectDir) {
208
+ const file = path.join(projectDir, '.mcp.json');
209
+ if (!await exists(file)) return;
210
+ const config = JSON.parse(await readFile(file, 'utf8'));
211
+ const server = config.mcpServers?.['playwright-test'];
212
+ if (!server) return;
213
+ server.env = {
214
+ ...(server.env ?? {}),
215
+ PLAYWRIGHT_MCP_OUTPUT_DIR: '.playwright-evidence/mcp',
216
+ };
217
+ await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
218
+ }
219
+
220
+ async function configureCodexAgents(projectDir) {
221
+ for (const name of ['planner', 'generator', 'healer']) {
222
+ const file = path.join(projectDir, '.codex', 'agents', `playwright_test_${name}.toml`);
223
+ if (!await exists(file)) continue;
224
+ let source = await readFile(file, 'utf8');
225
+ if (!source.includes('[mcp_servers.playwright-test.env]')) {
226
+ source = `${source.trimEnd()}\n\n[mcp_servers.playwright-test.env]\n` +
227
+ 'PLAYWRIGHT_MCP_OUTPUT_DIR = ".playwright-evidence/mcp"\n';
228
+ await writeFile(file, source, 'utf8');
229
+ }
230
+ }
231
+ }
232
+
233
+ async function initializePlaywright(projectDir, run) {
234
+ const configPath = path.join(projectDir, 'playwright.config.ts');
235
+ if (!await exists(configPath)) {
236
+ await run(projectDir, 'npm', [
237
+ 'init',
238
+ 'playwright@latest',
239
+ '--',
240
+ '--quiet',
241
+ '--lang=TypeScript',
242
+ '--browser=chromium',
243
+ ]);
244
+
245
+ const generatedExample = path.join(projectDir, 'tests', 'example.spec.ts');
246
+ const targetDir = path.join(projectDir, 'playwright-tests');
247
+ if (await exists(generatedExample)) {
248
+ await mkdir(targetDir, { recursive: true });
249
+ await rename(generatedExample, path.join(targetDir, 'example.spec.ts'));
250
+ }
251
+ await patchPlaywrightConfig(projectDir);
252
+ }
253
+
254
+ await disableTraceScreenshots(projectDir);
255
+
256
+ await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=codex']);
257
+ await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=claude']);
258
+ await mkdir(path.join(projectDir, '.playwright-evidence', 'mcp'), { recursive: true });
299
259
  await configureClaudeMcp(projectDir);
300
- await configureCodexMainMcp(projectDir);
301
- }
302
-
303
- async function runStage(name, action) {
304
- process.stdout.write(`[playwright-test-agent] ${name}...\n`);
305
- try {
306
- await action();
307
- } catch (error) {
308
- throw new Error(`${name}: ${error.message}`, { cause: error });
309
- }
310
- }
311
-
312
- export async function initializeProject({
313
- projectDir = process.cwd(),
314
- skillSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
315
- run = defaultRun,
316
- } = {}) {
317
- const target = path.resolve(projectDir);
318
- await mkdir(target, { recursive: true });
319
-
260
+ await configureCodexAgents(projectDir);
261
+ await configureCliFirstRoles(projectDir);
262
+ }
263
+
264
+ async function runStage(name, action) {
265
+ process.stdout.write(`[playwright-test-agent] ${name}...\n`);
266
+ try {
267
+ await action();
268
+ } catch (error) {
269
+ throw new Error(`${name}: ${error.message}`, { cause: error });
270
+ }
271
+ }
272
+
273
+ export async function initializeProject({
274
+ projectDir = process.cwd(),
275
+ skillSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
276
+ run = defaultRun,
277
+ } = {}) {
278
+ const target = path.resolve(projectDir);
279
+ await mkdir(target, { recursive: true });
280
+
320
281
  await runStage('installing skill', () => installSkill(target, skillSourceDir));
282
+ await runStage('installing Playwright CLI and browser', () => installPlaywrightCli(target, run));
321
283
  await runStage('initializing Playwright Test agents', () => initializePlaywright(target, run));
322
- await runStage('updating project instructions', () => updateInstructionFiles(target));
323
-
324
- process.stdout.write(
325
- 'Playwright Test Agent ready: skills installed for Codex and Claude, ' +
326
- 'tests in playwright-tests/, evidence in .playwright-evidence/. ' +
327
- 'Restart Codex so it loads the project-level playwright-test MCP server.\n',
328
- );
329
- }
330
-
331
- const isDirectInvocation = process.argv[1] &&
332
- path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
333
-
334
- if (isDirectInvocation) {
335
- initializeProject({ projectDir: process.argv[2] ?? process.cwd() }).catch((error) => {
336
- console.error(`Playwright initialization failed: ${error.message}`);
337
- process.exitCode = 1;
338
- });
339
- }
284
+ await runStage('updating project instructions', () => updateInstructionFiles(target));
285
+
286
+ process.stdout.write(
287
+ 'Playwright Test Agent ready: Playwright CLI and skills installed globally, ' +
288
+ 'agent definitions configured CLI-first for Codex and Claude, ' +
289
+ 'tests in playwright-tests/, evidence in .playwright-evidence/.\n',
290
+ );
291
+ }
292
+
293
+ const isDirectInvocation = process.argv[1] &&
294
+ path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
295
+
296
+ if (isDirectInvocation) {
297
+ initializeProject({ projectDir: process.argv[2] ?? process.cwd() }).catch((error) => {
298
+ console.error(`Playwright initialization failed: ${error.message}`);
299
+ process.exitCode = 1;
300
+ });
301
+ }