playwright-test-agent 0.1.6 → 0.1.9
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
|
-
|
|
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.
|
|
4
|
-
"description": "Initialize Playwright Test
|
|
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.9",
|
|
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,58 @@
|
|
|
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
|
|
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
|
|
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
|
+
## Fast start
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
When the request contains a test objective and deployed URL, enter Planner immediately and run:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
```bash
|
|
15
|
+
playwright-cli open <deployed-url>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Before opening the page, do not inspect the local project and do not locate or read a seed file, Playwright config, test directory, agent definition, fixtures, source tree, or existing tests. Do not ask optional questions about roles, browsers, edge cases, expected results, or test data when Planner can discover or defer them.
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
Ask before Planner only when the objective or deployed URL itself is missing. During exploration, ask only when a real blocker is reached: required credentials, an invalid URL, ambiguous expected behavior that changes assertions, or authorization for a consequential action. Resume Planner after the answer. Do not guess credentials, probe nearby environments, bypass access controls, or wander through unrelated pages.
|
|
17
21
|
|
|
18
22
|
Keep credentials and tokens in environment variables or ignored secret files. Never copy them into plans, tests, screenshots, evidence, or chat.
|
|
19
23
|
|
|
20
24
|
## Planner
|
|
21
25
|
|
|
22
|
-
Planner
|
|
26
|
+
Planner investigates the live application with `playwright-cli` and saves a human-readable Markdown plan under `specs/`. Start with the supplied URL, then use compact `snapshot` or `find` output and refs for interaction. A seed is not a Planner prerequisite and must not be located before exploration.
|
|
23
27
|
|
|
24
|
-
The plan contains prerequisites, test data
|
|
28
|
+
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
29
|
|
|
26
|
-
Show the
|
|
30
|
+
Show the scenario list and exclusions to the user. Generator starts only after confirmation.
|
|
27
31
|
|
|
28
32
|
## Generator
|
|
29
33
|
|
|
30
|
-
Generator converts the
|
|
34
|
+
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
35
|
|
|
32
|
-
For API-only scenarios, Playwright `APIRequestContext`
|
|
36
|
+
For API-only scenarios, use Playwright `APIRequestContext` directly and assert status, headers, schema, stable business invariants, and safe mutation cleanup.
|
|
33
37
|
|
|
34
38
|
## Healer
|
|
35
39
|
|
|
36
|
-
Use Healer only for
|
|
40
|
+
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
41
|
|
|
38
|
-
Do not weaken assertions, add arbitrary sleeps, retry blindly, or skip
|
|
42
|
+
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
43
|
|
|
40
44
|
## Evidence
|
|
41
45
|
|
|
42
|
-
Keep
|
|
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.
|
|
46
|
+
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.
|
|
53
47
|
|
|
54
|
-
Report the plan path, generated
|
|
48
|
+
Report the plan path, generated tests, counts when tests ran, evidence paths, and failure classification. Never report skipped or unresolved tests as success.
|
|
55
49
|
|
|
56
50
|
## Initialization
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
Run from the target project:
|
|
59
53
|
|
|
60
54
|
```bash
|
|
61
55
|
npx playwright-test-agent init
|
|
62
56
|
```
|
|
63
57
|
|
|
64
|
-
The
|
|
58
|
+
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,319 @@
|
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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' ? 'FAST START: when the request contains a test objective and deployed URL, immediately run `playwright-cli open <url>` and investigate. Do not first locate or read a seed file, Playwright config, test directory, agent definition, source tree, fixtures, or existing tests. Do not ask optional planning questions before opening the page. Ask only when exploration reaches a real blocker such as required credentials, missing/incorrect URL, ambiguous expected behavior that changes assertions, or authorization for a consequential action. 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. When the request contains a test objective and deployed URL, begin Planner immediately; do not first inspect the project or locate a seed, config, test directory, agent definition, fixtures, or existing tests. Ask only at a real access, expected-behavior, or safety blocker. 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. When the request contains a test objective and deployed URL, start Planner immediately; do not first inspect the project or locate a seed, config, test directory, agent definition, fixtures, or existing tests. These roles use \`playwright-cli\` as their primary browser interface and must not create nested agents. Ask only at a real access, expected-behavior, or safety blocker; 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
|
|
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) +
|
|
103
|
+
return source.slice(0, start) + cliFirstInstructions(role) +
|
|
104
|
+
source.slice(end + ROLE_BLOCK_END.length);
|
|
65
105
|
}
|
|
66
|
-
|
|
67
|
-
if (source.length === 0) return `${managedBlock}\n`;
|
|
68
|
-
const separator = source.endsWith('\n') ? '\n' : '\n\n';
|
|
69
|
-
return `${source}${separator}${managedBlock}\n`;
|
|
106
|
+
return `${source.trimEnd()}\n\n${cliFirstInstructions(role)}\n`;
|
|
70
107
|
}
|
|
71
108
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
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"""`;
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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');
|
|
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"');
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
function patchClaudeRole(source, role) {
|
|
128
|
+
let updated = source;
|
|
129
|
+
const frontmatter = updated.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
130
|
+
if (!frontmatter) throw new Error('YAML frontmatter was not found in a Claude role definition');
|
|
131
|
+
|
|
132
|
+
let header = frontmatter[1];
|
|
133
|
+
const inlineTools = header.match(/^tools:[ \t]*(\S.*?)[ \t]*$/m);
|
|
134
|
+
const blockTools = header.match(/^tools:[ \t]*$/m);
|
|
135
|
+
if (inlineTools) {
|
|
136
|
+
const tools = inlineTools[1].split(',').map((tool) => tool.trim());
|
|
137
|
+
if (!tools.includes('Bash')) tools.push('Bash');
|
|
138
|
+
header = header.replace(inlineTools[0], `tools: ${tools.join(', ')}`);
|
|
139
|
+
} else if (blockTools) {
|
|
140
|
+
const blockStart = blockTools.index + blockTools[0].length;
|
|
141
|
+
const remainder = header.slice(blockStart);
|
|
142
|
+
const nextKey = remainder.search(/\r?\n(?=[A-Za-z0-9_-]+:\s*)/);
|
|
143
|
+
const blockEnd = nextKey === -1 ? header.length : blockStart + nextKey;
|
|
144
|
+
const toolsBlock = header.slice(blockStart, blockEnd);
|
|
145
|
+
if (!/^\s*-\s+Bash\s*$/m.test(toolsBlock)) {
|
|
146
|
+
header = header.slice(0, blockStart) + '\n - Bash' +
|
|
147
|
+
header.slice(blockStart);
|
|
119
148
|
}
|
|
120
|
-
|
|
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');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
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');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function normalizeTomlTableName(name) {
|
|
150
|
-
return name
|
|
151
|
-
.split('.')
|
|
152
|
-
.map((part) => part.trim().replace(/^(?:"([^"]+)"|'([^']+)')$/, '$1$2'))
|
|
153
|
-
.join('.');
|
|
154
|
-
}
|
|
155
|
-
|
|
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);
|
|
149
|
+
} else {
|
|
150
|
+
header = `${header.trimEnd()}\ntools: Bash`;
|
|
161
151
|
}
|
|
162
|
-
|
|
152
|
+
updated = updated.slice(0, frontmatter.index) + `---\n${header}\n---` +
|
|
153
|
+
updated.slice(frontmatter.index + frontmatter[0].length);
|
|
154
|
+
return withRoleBlock(updated, role);
|
|
163
155
|
}
|
|
164
156
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.
|
|
169
|
-
|
|
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
|
-
}
|
|
157
|
+
async function configureCliFirstRoles(projectDir) {
|
|
158
|
+
const roles = ['planner', 'generator', 'healer'];
|
|
159
|
+
for (const role of roles) {
|
|
160
|
+
const claudeFile = path.join(projectDir, '.claude', 'agents', `playwright-test-${role}.md`);
|
|
161
|
+
if (await exists(claudeFile)) {
|
|
162
|
+
const source = await readFile(claudeFile, 'utf8');
|
|
163
|
+
await writeFile(claudeFile, patchClaudeRole(source, role), 'utf8');
|
|
199
164
|
}
|
|
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
165
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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;
|
|
166
|
+
const codexFile = path.join(projectDir, '.codex', 'agents', `playwright_test_${role}.toml`);
|
|
167
|
+
if (await exists(codexFile)) {
|
|
168
|
+
const source = await readFile(codexFile, 'utf8');
|
|
169
|
+
await writeFile(codexFile, patchCodexRole(source, role), 'utf8');
|
|
234
170
|
}
|
|
235
171
|
}
|
|
236
|
-
while (preserved[0] === '') preserved.shift();
|
|
237
|
-
while (preserved.at(-1) === '') preserved.pop();
|
|
238
|
-
lines.splice(start, end - start, ...body, ...(preserved.length ? ['', ...preserved] : []), '');
|
|
239
172
|
}
|
|
240
173
|
|
|
241
|
-
async function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
await
|
|
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');
|
|
174
|
+
async function installPlaywrightCli(projectDir, run) {
|
|
175
|
+
await run(projectDir, 'npm', ['install', '-g', '@playwright/cli@latest']);
|
|
176
|
+
await run(projectDir, 'playwright-cli', ['install', '--skills=agents', '--global']);
|
|
177
|
+
await run(projectDir, 'playwright-cli', ['install', '--skills', '--global']);
|
|
178
|
+
await run(projectDir, 'playwright-cli', ['install-browser']);
|
|
271
179
|
}
|
|
272
|
-
|
|
273
|
-
async function
|
|
274
|
-
const configPath = path.join(projectDir, 'playwright.config.ts');
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
180
|
+
|
|
181
|
+
async function patchPlaywrightConfig(projectDir) {
|
|
182
|
+
const configPath = path.join(projectDir, 'playwright.config.ts');
|
|
183
|
+
let source = await readFile(configPath, 'utf8');
|
|
184
|
+
source = source.replace(/testDir:\s*['"]\.\/tests['"]/, "testDir: './playwright-tests'");
|
|
185
|
+
source = source.replace(
|
|
186
|
+
/reporter:\s*['"]html['"],?/,
|
|
187
|
+
"reporter: [['html', { outputFolder: '.playwright-evidence/report', open: 'never' }]],",
|
|
188
|
+
);
|
|
189
|
+
source = source.replace(
|
|
190
|
+
/use:\s*\{/,
|
|
191
|
+
"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',",
|
|
192
|
+
);
|
|
193
|
+
source = source.replace(
|
|
194
|
+
/trace:\s*['"][^'"]+['"]/,
|
|
195
|
+
"trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }",
|
|
196
|
+
);
|
|
197
|
+
await writeFile(configPath, source, 'utf8');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function withoutTraceScreenshots(source) {
|
|
201
|
+
const replacement = "trace: { mode: 'retain-on-failure', screenshots: false, snapshots: true, sources: true, attachments: true }";
|
|
202
|
+
const fromString = source.replace(/trace:\s*['"][^'"]+['"]/, replacement);
|
|
203
|
+
if (fromString !== source) return fromString;
|
|
204
|
+
|
|
205
|
+
return source.replace(/trace:\s*\{([\s\S]*?)\}/, (setting, body) => {
|
|
206
|
+
if (/screenshots\s*:/.test(body)) {
|
|
207
|
+
return setting.replace(/screenshots\s*:\s*(?:true|false)/, 'screenshots: false');
|
|
208
|
+
}
|
|
209
|
+
return setting.replace('{', '{ screenshots: false,');
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function disableTraceScreenshots(projectDir) {
|
|
214
|
+
const configPath = path.join(projectDir, 'playwright.config.ts');
|
|
215
|
+
if (!await exists(configPath)) return;
|
|
216
|
+
|
|
217
|
+
let source = await readFile(configPath, 'utf8');
|
|
218
|
+
const updated = withoutTraceScreenshots(source);
|
|
219
|
+
|
|
220
|
+
if (updated !== source) {
|
|
221
|
+
await writeFile(configPath, updated, 'utf8');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function configureClaudeMcp(projectDir) {
|
|
226
|
+
const file = path.join(projectDir, '.mcp.json');
|
|
227
|
+
if (!await exists(file)) return;
|
|
228
|
+
const config = JSON.parse(await readFile(file, 'utf8'));
|
|
229
|
+
const server = config.mcpServers?.['playwright-test'];
|
|
230
|
+
if (!server) return;
|
|
231
|
+
server.env = {
|
|
232
|
+
...(server.env ?? {}),
|
|
233
|
+
PLAYWRIGHT_MCP_OUTPUT_DIR: '.playwright-evidence/mcp',
|
|
234
|
+
};
|
|
235
|
+
await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function configureCodexAgents(projectDir) {
|
|
239
|
+
for (const name of ['planner', 'generator', 'healer']) {
|
|
240
|
+
const file = path.join(projectDir, '.codex', 'agents', `playwright_test_${name}.toml`);
|
|
241
|
+
if (!await exists(file)) continue;
|
|
242
|
+
let source = await readFile(file, 'utf8');
|
|
243
|
+
if (!source.includes('[mcp_servers.playwright-test.env]')) {
|
|
244
|
+
source = `${source.trimEnd()}\n\n[mcp_servers.playwright-test.env]\n` +
|
|
245
|
+
'PLAYWRIGHT_MCP_OUTPUT_DIR = ".playwright-evidence/mcp"\n';
|
|
246
|
+
await writeFile(file, source, 'utf8');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function initializePlaywright(projectDir, run) {
|
|
252
|
+
const configPath = path.join(projectDir, 'playwright.config.ts');
|
|
253
|
+
if (!await exists(configPath)) {
|
|
254
|
+
await run(projectDir, 'npm', [
|
|
255
|
+
'init',
|
|
256
|
+
'playwright@latest',
|
|
257
|
+
'--',
|
|
258
|
+
'--quiet',
|
|
259
|
+
'--lang=TypeScript',
|
|
260
|
+
'--browser=chromium',
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
const generatedExample = path.join(projectDir, 'tests', 'example.spec.ts');
|
|
264
|
+
const targetDir = path.join(projectDir, 'playwright-tests');
|
|
265
|
+
if (await exists(generatedExample)) {
|
|
266
|
+
await mkdir(targetDir, { recursive: true });
|
|
267
|
+
await rename(generatedExample, path.join(targetDir, 'example.spec.ts'));
|
|
268
|
+
}
|
|
269
|
+
await patchPlaywrightConfig(projectDir);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await disableTraceScreenshots(projectDir);
|
|
273
|
+
|
|
274
|
+
await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=codex']);
|
|
275
|
+
await run(projectDir, 'npx', ['--no-install', 'playwright', 'init-agents', '--loop=claude']);
|
|
276
|
+
await mkdir(path.join(projectDir, '.playwright-evidence', 'mcp'), { recursive: true });
|
|
299
277
|
await configureClaudeMcp(projectDir);
|
|
300
|
-
await
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
278
|
+
await configureCodexAgents(projectDir);
|
|
279
|
+
await configureCliFirstRoles(projectDir);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runStage(name, action) {
|
|
283
|
+
process.stdout.write(`[playwright-test-agent] ${name}...\n`);
|
|
284
|
+
try {
|
|
285
|
+
await action();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
throw new Error(`${name}: ${error.message}`, { cause: error });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function initializeProject({
|
|
292
|
+
projectDir = process.cwd(),
|
|
293
|
+
skillSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
|
|
294
|
+
run = defaultRun,
|
|
295
|
+
} = {}) {
|
|
296
|
+
const target = path.resolve(projectDir);
|
|
297
|
+
await mkdir(target, { recursive: true });
|
|
298
|
+
|
|
320
299
|
await runStage('installing skill', () => installSkill(target, skillSourceDir));
|
|
300
|
+
await runStage('installing Playwright CLI and browser', () => installPlaywrightCli(target, run));
|
|
321
301
|
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:
|
|
326
|
-
'
|
|
327
|
-
'
|
|
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
|
-
}
|
|
302
|
+
await runStage('updating project instructions', () => updateInstructionFiles(target));
|
|
303
|
+
|
|
304
|
+
process.stdout.write(
|
|
305
|
+
'Playwright Test Agent ready: Playwright CLI and skills installed globally, ' +
|
|
306
|
+
'agent definitions configured CLI-first for Codex and Claude, ' +
|
|
307
|
+
'tests in playwright-tests/, evidence in .playwright-evidence/.\n',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const isDirectInvocation = process.argv[1] &&
|
|
312
|
+
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
313
|
+
|
|
314
|
+
if (isDirectInvocation) {
|
|
315
|
+
initializeProject({ projectDir: process.argv[2] ?? process.cwd() }).catch((error) => {
|
|
316
|
+
console.error(`Playwright initialization failed: ${error.message}`);
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
});
|
|
319
|
+
}
|