codebyplan 1.11.1 → 1.11.2
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.
- package/dist/cli.js +56 -5
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/agents/cbp-cc-executor.md +1 -1
- package/templates/agents/cbp-e2e-maestro.md +202 -0
- package/templates/agents/cbp-e2e-playwright.md +229 -0
- package/templates/agents/cbp-e2e-tauri.md +184 -0
- package/templates/agents/cbp-e2e-vscode.md +203 -0
- package/templates/agents/cbp-e2e-xcuitest.md +224 -0
- package/templates/agents/cbp-improve-claude.md +1 -1
- package/templates/agents/cbp-round-executor.md +11 -11
- package/templates/agents/cbp-task-check.md +1 -1
- package/templates/agents/cbp-task-planner.md +2 -0
- package/templates/agents/cbp-testing-qa-agent.md +9 -9
- package/templates/context/testing/e2e.md +303 -0
- package/templates/hooks/validate-structure-lengths.sh +2 -0
- package/templates/hooks/validate-structure-smoke.sh +2 -1
- package/templates/hooks/validate-structure-templates.sh +1 -0
- package/templates/rules/context-file-loading.md +4 -1
- package/templates/rules/e2e-mandatory.md +70 -0
- package/templates/skills/cbp-build-cc-agent/SKILL.md +16 -14
- package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +4 -4
- package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +8 -6
- package/templates/skills/cbp-build-cc-mode/SKILL.md +4 -4
- package/templates/skills/cbp-checkpoint-check/SKILL.md +12 -8
- package/templates/skills/cbp-checkpoint-plan/SKILL.md +2 -2
- package/templates/skills/cbp-checkpoint-plan/reference/e2e-discovery-probe.md +5 -5
- package/templates/skills/cbp-e2e-setup/SKILL.md +254 -0
- package/templates/skills/cbp-e2e-setup/reference/maestro.md +200 -0
- package/templates/skills/cbp-e2e-setup/reference/playwright.md +212 -0
- package/templates/skills/cbp-e2e-setup/reference/tauri.md +147 -0
- package/templates/skills/cbp-e2e-setup/reference/vscode.md +154 -0
- package/templates/skills/cbp-e2e-setup/reference/xcuitest.md +185 -0
- package/templates/skills/cbp-frontend-ui/SKILL.md +6 -6
- package/templates/skills/cbp-frontend-ux/SKILL.md +1 -1
- package/templates/skills/cbp-round-execute/SKILL.md +30 -17
- package/templates/skills/cbp-task-check/SKILL.md +2 -2
- package/templates/agents/cbp-test-e2e-agent.md +0 -363
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.11.
|
|
17
|
+
VERSION = "1.11.2";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -1195,6 +1195,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
1195
1195
|
JSON.stringify({}, null, 2) + "\n",
|
|
1196
1196
|
"utf-8"
|
|
1197
1197
|
);
|
|
1198
|
+
await writeFile5(
|
|
1199
|
+
join5(codebyplanDir, "e2e.json"),
|
|
1200
|
+
JSON.stringify({}, null, 2) + "\n",
|
|
1201
|
+
"utf-8"
|
|
1202
|
+
);
|
|
1198
1203
|
const statuslinePath = join5(codebyplanDir, "statusline.json");
|
|
1199
1204
|
let statuslineExists = false;
|
|
1200
1205
|
try {
|
|
@@ -1212,7 +1217,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
1212
1217
|
await writeLocalConfig(projectPath, { device_id: deviceId });
|
|
1213
1218
|
console.log(` Created ${codebyplanDir}/`);
|
|
1214
1219
|
console.log(
|
|
1215
|
-
` repo.json, server.json, git.json, shipment.json, vendor.json, statusline.json`
|
|
1220
|
+
` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
|
|
1216
1221
|
);
|
|
1217
1222
|
console.log(` device.local.json (gitignored)`);
|
|
1218
1223
|
const gitignorePath = join5(projectPath, ".gitignore");
|
|
@@ -2161,6 +2166,9 @@ var init_tech_detect = __esm({
|
|
|
2161
2166
|
"@playwright/test": { name: "Playwright", category: "testing" },
|
|
2162
2167
|
cypress: { name: "Cypress", category: "testing" },
|
|
2163
2168
|
supertest: { name: "Supertest", category: "testing" },
|
|
2169
|
+
webdriverio: { name: "WebdriverIO", category: "testing" },
|
|
2170
|
+
"@wdio/cli": { name: "WebdriverIO", category: "testing" },
|
|
2171
|
+
"@vscode/test-cli": { name: "VS Code Test CLI", category: "testing" },
|
|
2164
2172
|
// Build tools
|
|
2165
2173
|
turbo: { name: "Turborepo", category: "build" },
|
|
2166
2174
|
vite: { name: "Vite", category: "build" },
|
|
@@ -2245,7 +2253,28 @@ var init_tech_detect = __esm({
|
|
|
2245
2253
|
rule: { name: "shadcn/ui", category: "component-lib" }
|
|
2246
2254
|
},
|
|
2247
2255
|
{ file: "nx.json", rule: { name: "Nx", category: "build" } },
|
|
2248
|
-
{ file: "lerna.json", rule: { name: "Lerna", category: "build" } }
|
|
2256
|
+
{ file: "lerna.json", rule: { name: "Lerna", category: "build" } },
|
|
2257
|
+
{
|
|
2258
|
+
file: "playwright.config.ts",
|
|
2259
|
+
rule: { name: "Playwright", category: "testing" }
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
file: "playwright.config.js",
|
|
2263
|
+
rule: { name: "Playwright", category: "testing" }
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
file: "playwright.config.mjs",
|
|
2267
|
+
rule: { name: "Playwright", category: "testing" }
|
|
2268
|
+
},
|
|
2269
|
+
{ file: "wdio.conf.ts", rule: { name: "WebdriverIO", category: "testing" } },
|
|
2270
|
+
{
|
|
2271
|
+
file: "wdio.conf.js",
|
|
2272
|
+
rule: { name: "WebdriverIO", category: "testing" }
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
file: "maestro/config.yaml",
|
|
2276
|
+
rule: { name: "Maestro", category: "testing" }
|
|
2277
|
+
}
|
|
2249
2278
|
];
|
|
2250
2279
|
SYNTHETIC_CARRIER_NAME = "__capabilities__";
|
|
2251
2280
|
CAPABILITY_BEARER_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -4452,6 +4481,13 @@ async function runLocalMigration(projectPath) {
|
|
|
4452
4481
|
"utf-8"
|
|
4453
4482
|
);
|
|
4454
4483
|
filesChanged.push(".codebyplan/vendor.json");
|
|
4484
|
+
const e2eJson = {};
|
|
4485
|
+
await writeFile9(
|
|
4486
|
+
join13(projectPath, ".codebyplan", "e2e.json"),
|
|
4487
|
+
JSON.stringify(e2eJson, null, 2) + "\n",
|
|
4488
|
+
"utf-8"
|
|
4489
|
+
);
|
|
4490
|
+
filesChanged.push(".codebyplan/e2e.json");
|
|
4455
4491
|
if (!deviceWrittenByHelper) {
|
|
4456
4492
|
await writeFile9(
|
|
4457
4493
|
join13(projectPath, ".codebyplan", "device.local.json"),
|
|
@@ -4519,6 +4555,7 @@ var init_migrate_local_config = __esm({
|
|
|
4519
4555
|
// src/cli/config.ts
|
|
4520
4556
|
var config_exports = {};
|
|
4521
4557
|
__export(config_exports, {
|
|
4558
|
+
readE2eConfig: () => readE2eConfig,
|
|
4522
4559
|
readGitConfig: () => readGitConfig,
|
|
4523
4560
|
readRepoConfig: () => readRepoConfig,
|
|
4524
4561
|
readServerConfig: () => readServerConfig,
|
|
@@ -4685,6 +4722,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4685
4722
|
shipmentPayload.shipment = repoAny.shipment;
|
|
4686
4723
|
}
|
|
4687
4724
|
const vendorPayload = {};
|
|
4725
|
+
const e2ePayload = {};
|
|
4688
4726
|
if (dryRun) {
|
|
4689
4727
|
console.log(" Config would be updated (dry-run).");
|
|
4690
4728
|
return;
|
|
@@ -4695,10 +4733,11 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4695
4733
|
{ name: "server.json", payload: serverPayload },
|
|
4696
4734
|
{ name: "git.json", payload: gitPayload },
|
|
4697
4735
|
{ name: "shipment.json", payload: shipmentPayload },
|
|
4698
|
-
{ name: "vendor.json", payload: vendorPayload }
|
|
4736
|
+
{ name: "vendor.json", payload: vendorPayload },
|
|
4737
|
+
{ name: "e2e.json", payload: e2ePayload, createOnly: true }
|
|
4699
4738
|
];
|
|
4700
4739
|
let anyUpdated = false;
|
|
4701
|
-
for (const { name, payload } of files) {
|
|
4740
|
+
for (const { name, payload, createOnly } of files) {
|
|
4702
4741
|
const filePath = join14(codebyplanDir, name);
|
|
4703
4742
|
const newJson = JSON.stringify(payload, null, 2) + "\n";
|
|
4704
4743
|
let currentJson = "";
|
|
@@ -4706,6 +4745,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4706
4745
|
currentJson = await readFile13(filePath, "utf-8");
|
|
4707
4746
|
} catch {
|
|
4708
4747
|
}
|
|
4748
|
+
if (createOnly && currentJson !== "") continue;
|
|
4709
4749
|
if (currentJson === newJson) continue;
|
|
4710
4750
|
await writeFile10(filePath, newJson, "utf-8");
|
|
4711
4751
|
console.log(` Updated .codebyplan/${name}`);
|
|
@@ -4770,6 +4810,17 @@ async function readVendorConfig(projectPath) {
|
|
|
4770
4810
|
return null;
|
|
4771
4811
|
}
|
|
4772
4812
|
}
|
|
4813
|
+
async function readE2eConfig(projectPath) {
|
|
4814
|
+
try {
|
|
4815
|
+
const raw = await readFile13(
|
|
4816
|
+
join14(projectPath, ".codebyplan", "e2e.json"),
|
|
4817
|
+
"utf-8"
|
|
4818
|
+
);
|
|
4819
|
+
return JSON.parse(raw);
|
|
4820
|
+
} catch {
|
|
4821
|
+
return null;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4773
4824
|
var legacyBranchConfigWarned;
|
|
4774
4825
|
var init_config = __esm({
|
|
4775
4826
|
"src/cli/config.ts"() {
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This directory holds the installed content that the `codebyplan claude install`
|
|
|
11
11
|
| Path | Count | Shape |
|
|
12
12
|
| --------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
13
13
|
| `skills/` | 41 folders | each is a `SKILL.md` plus optional `templates/`, `reference/`, `examples/`, `scripts/` siblings |
|
|
14
|
-
| `agents/` |
|
|
14
|
+
| `agents/` | 16 files | flat `.md` agent prompts (NOT `AGENT.md` subdirs) |
|
|
15
15
|
| `hooks/` | 20 `.sh` + `hooks.json` + `README.md` | event hooks and manifest |
|
|
16
16
|
| `rules/` | 1+ files | flat `<name>.md` rule files; see `rules/README.md` for bar and format |
|
|
17
17
|
|
|
@@ -82,7 +82,7 @@ Before processing any change, build a fresh inventory:
|
|
|
82
82
|
|
|
83
83
|
1. Glob `.claude/rules/*.md` — read name + frontmatter
|
|
84
84
|
2. Glob `.claude/skills/*/SKILL.md` — read name + description
|
|
85
|
-
3. Glob `.claude/agents/*/AGENT.md` — read name + description
|
|
85
|
+
3. Glob `.claude/agents/*.md` (and `.claude/agents/*/AGENT.md` for folder-form agents) — read name + description
|
|
86
86
|
4. Glob `.claude/context/**/*.md` — read path + first heading
|
|
87
87
|
5. Glob `.claude/docs/architecture/*.md` — read path + first heading
|
|
88
88
|
6. Glob `.claude/hooks/*.sh` — read path + header block
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbp-e2e-maestro
|
|
3
|
+
description: Maestro E2E flow authoring + execution for Expo/React Native mobile apps (android + ios). Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'maestro'.
|
|
4
|
+
tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, mcp__codebyplan__get_repos
|
|
5
|
+
model: sonnet
|
|
6
|
+
effort: xhigh
|
|
7
|
+
scope: org-shared
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Maestro E2E Agent
|
|
11
|
+
|
|
12
|
+
Read `context/testing/e2e.md` for the shared contract (Input/Output, Step 6.5 preflight,
|
|
13
|
+
Step 7.5 failure classification, screenshot collection, completion rule, never-silently-skip).
|
|
14
|
+
|
|
15
|
+
Framework: Maestro on Expo / React Native. Dispatched when `.codebyplan/e2e.json`
|
|
16
|
+
records `framework: "maestro"`.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Java 17+: `java -version` (install via `brew install openjdk@17` on macOS)
|
|
21
|
+
- Android emulator OR iOS Simulator
|
|
22
|
+
- Expo app bundled and running on target device/emulator
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# macOS
|
|
28
|
+
curl -fsSL "https://get.maestro.mobile.dev" | bash
|
|
29
|
+
# or: brew tap mobile-dev-inc/tap && brew install maestro
|
|
30
|
+
maestro --version # verify
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## maestro/config.yaml
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
appId: com.yourorg.yourapp # must match app.config.ts ios.bundleIdentifier / android.package
|
|
37
|
+
env:
|
|
38
|
+
TEST_EMAIL: ${TEST_EMAIL}
|
|
39
|
+
TEST_PASSWORD: ${TEST_PASSWORD}
|
|
40
|
+
APP_ID: com.yourorg.yourapp
|
|
41
|
+
screenshotsDir: maestro/screenshots
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Shared Login Flow
|
|
45
|
+
|
|
46
|
+
`maestro/flows/_shared/login.yaml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
appId: ${APP_ID}
|
|
50
|
+
---
|
|
51
|
+
- launchApp:
|
|
52
|
+
clearState: true
|
|
53
|
+
- assertVisible: "Sign in"
|
|
54
|
+
- tapOn: "Email"
|
|
55
|
+
- inputText: ${TEST_EMAIL}
|
|
56
|
+
- tapOn: "Password"
|
|
57
|
+
- inputText: ${TEST_PASSWORD}
|
|
58
|
+
- tapOn: "Sign in"
|
|
59
|
+
- assertVisible:
|
|
60
|
+
text: ".*" # Replace with a post-login element (e.g. "Dashboard")
|
|
61
|
+
timeout: 15000
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Reference from other flows: `- runFlow: _shared/login.yaml`
|
|
65
|
+
|
|
66
|
+
## Auth Probe
|
|
67
|
+
|
|
68
|
+
`maestro/flows/_probe/auth.yaml`:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
appId: ${APP_ID}
|
|
72
|
+
tags:
|
|
73
|
+
- probe
|
|
74
|
+
---
|
|
75
|
+
- launchApp:
|
|
76
|
+
clearState: true
|
|
77
|
+
- assertVisible: "Sign in"
|
|
78
|
+
- tapOn: "Email"
|
|
79
|
+
- inputText: ${TEST_EMAIL}
|
|
80
|
+
- tapOn: "Password"
|
|
81
|
+
- inputText: ${TEST_PASSWORD}
|
|
82
|
+
- tapOn: "Sign in"
|
|
83
|
+
- assertVisible:
|
|
84
|
+
text: ".*"
|
|
85
|
+
timeout: 15000
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run probe: `maestro test maestro/flows/_probe/auth.yaml`
|
|
89
|
+
|
|
90
|
+
## Pre-flight Probes (Step 6.5.2)
|
|
91
|
+
|
|
92
|
+
**iOS**: `xcrun simctl list devices booted | grep -q Booted`
|
|
93
|
+
|
|
94
|
+
> "No iOS Simulator is booted. Open Simulator.app or run `xcrun simctl boot 'iPhone 15'`.
|
|
95
|
+
> Reply 'ready' when the simulator home screen is visible."
|
|
96
|
+
|
|
97
|
+
**Android**: `adb devices | grep -w device`
|
|
98
|
+
|
|
99
|
+
> "No Android device/emulator connected. Start an emulator from Android Studio or run
|
|
100
|
+
> `emulator -avd {name}`. Reply 'ready' when unlocked."
|
|
101
|
+
|
|
102
|
+
## Platform Targeting
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# iOS
|
|
106
|
+
maestro --platform=ios test maestro/flows/
|
|
107
|
+
|
|
108
|
+
# Android
|
|
109
|
+
maestro --platform=android test maestro/flows/
|
|
110
|
+
|
|
111
|
+
# Specific device
|
|
112
|
+
maestro test --device <device-id> maestro/flows/
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Directory Structure
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
maestro/
|
|
119
|
+
config.yaml
|
|
120
|
+
flows/
|
|
121
|
+
_shared/
|
|
122
|
+
login.yaml
|
|
123
|
+
open-side-menu.yaml
|
|
124
|
+
_probe/
|
|
125
|
+
auth.yaml
|
|
126
|
+
onboarding/
|
|
127
|
+
signup.yaml
|
|
128
|
+
home/
|
|
129
|
+
dashboard.yaml
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
One subdirectory per app module. Shared flows under `_shared/`. Probe under `_probe/`.
|
|
133
|
+
|
|
134
|
+
## Spec-Writing Patterns
|
|
135
|
+
|
|
136
|
+
**One flow per screen/feature.** Steps:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
appId: ${APP_ID}
|
|
140
|
+
tags:
|
|
141
|
+
- home
|
|
142
|
+
---
|
|
143
|
+
- runFlow: _shared/login.yaml
|
|
144
|
+
- assertVisible: "Dashboard"
|
|
145
|
+
- takeScreenshot: "dashboard-loaded"
|
|
146
|
+
- tapOn: "Create"
|
|
147
|
+
- assertVisible: "New item"
|
|
148
|
+
- takeScreenshot: "create-modal-open"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use text-based targeting first (`tapOn: "Button"`); use testID when ambiguous
|
|
152
|
+
(`tapOn: { id: "btn" }`). For CRUD: create + verify visible; edit + verify updated;
|
|
153
|
+
delete + confirm + verify removed.
|
|
154
|
+
|
|
155
|
+
## Screenshot Capture
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
- takeScreenshot: "flow-name-after-state"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Screenshots written to `maestro/screenshots/` (via `screenshotsDir` in `config.yaml`).
|
|
162
|
+
Enumerate: `maestro/screenshots/*.png`.
|
|
163
|
+
|
|
164
|
+
## Run Command
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
maestro test maestro/flows/{module}/{flow}.yaml --format=junit --output maestro/results.xml
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## pnpm Scripts
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"scripts": {
|
|
175
|
+
"maestro:test": "maestro test maestro/flows/",
|
|
176
|
+
"maestro:test:probe": "maestro test maestro/flows/_probe/",
|
|
177
|
+
"maestro:studio": "maestro studio"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## CI
|
|
183
|
+
|
|
184
|
+
Maestro CI requires a connected device. Use Maestro Cloud or a self-hosted runner:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
- uses: mobile-dev-inc/action-maestro-cloud@v1
|
|
188
|
+
with:
|
|
189
|
+
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
|
190
|
+
app-file: path/to/app.apk
|
|
191
|
+
flow-file: maestro/flows/
|
|
192
|
+
env:
|
|
193
|
+
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
|
194
|
+
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Pitfalls
|
|
198
|
+
|
|
199
|
+
**App ID mismatch** — `appId` must exactly match the compiled bundle identifier. Re-run
|
|
200
|
+
`expo prebuild` if the identifier changed after prebuild. **clearState: true** — always
|
|
201
|
+
clear app state in `launchApp` for the login flow. **Java version** — Maestro requires
|
|
202
|
+
Java 17+; check `JAVA_HOME` if `maestro --version` fails.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbp-e2e-playwright
|
|
3
|
+
description: Playwright E2E test authoring + execution for web app routes. Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'playwright'.
|
|
4
|
+
tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion, mcp__codebyplan__get_repos
|
|
5
|
+
model: sonnet
|
|
6
|
+
effort: xhigh
|
|
7
|
+
scope: org-shared
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Playwright E2E Agent
|
|
11
|
+
|
|
12
|
+
Read `context/testing/e2e.md` for the shared contract (Input/Output, Step 6.5 preflight,
|
|
13
|
+
Step 7.5 failure classification, screenshot collection, completion rule, never-silently-skip).
|
|
14
|
+
|
|
15
|
+
Framework: Playwright on Next.js web apps. Dispatched when `.codebyplan/e2e.json`
|
|
16
|
+
records `framework: "playwright"`.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add -D @playwright/test
|
|
22
|
+
pnpm exec playwright install chromium
|
|
23
|
+
# CI with system deps:
|
|
24
|
+
pnpm exec playwright install --with-deps chromium
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## playwright.config.ts
|
|
28
|
+
|
|
29
|
+
Derive `baseURL` from `.codebyplan/server.json` at config-read time. Match by label
|
|
30
|
+
(`"Web Dev"`) rather than array position — a monorepo can have several nextjs allocations.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
34
|
+
import { execSync } from "child_process";
|
|
35
|
+
|
|
36
|
+
function getBaseUrl(): string {
|
|
37
|
+
try {
|
|
38
|
+
const raw = execSync(
|
|
39
|
+
"jq -r '.port_allocations[] | select(.label==\"Web Dev\") | .port' .codebyplan/server.json 2>/dev/null | head -1",
|
|
40
|
+
{ encoding: "utf-8" }
|
|
41
|
+
).trim();
|
|
42
|
+
const port = parseInt(raw, 10);
|
|
43
|
+
return `http://localhost:${port}`;
|
|
44
|
+
} catch {
|
|
45
|
+
return "http://localhost:3010";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
testDir: "apps/web/e2e",
|
|
51
|
+
fullyParallel: false,
|
|
52
|
+
forbidOnly: !!process.env.CI,
|
|
53
|
+
retries: process.env.CI ? 2 : 0,
|
|
54
|
+
workers: 1, // serialize against shared remote Supabase — see e2e.md § Supabase Parallelism
|
|
55
|
+
reporter: process.env.CI ? "github" : "html",
|
|
56
|
+
globalSetup: "./apps/web/e2e/global-setup",
|
|
57
|
+
use: {
|
|
58
|
+
baseURL: getBaseUrl(),
|
|
59
|
+
trace: "on-first-retry",
|
|
60
|
+
screenshot: "only-on-failure",
|
|
61
|
+
},
|
|
62
|
+
projects: [
|
|
63
|
+
{ name: "setup", testMatch: /global\.setup\.ts/ },
|
|
64
|
+
{
|
|
65
|
+
name: "web",
|
|
66
|
+
use: { ...devices["Desktop Chrome"], storageState: "apps/web/e2e/.auth/user.json" },
|
|
67
|
+
dependencies: ["setup"],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
webServer: {
|
|
71
|
+
command: "pnpm --filter @codebyplan/web dev",
|
|
72
|
+
url: getBaseUrl(),
|
|
73
|
+
reuseExistingServer: !process.env.CI,
|
|
74
|
+
timeout: 120_000,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Auth — Global Setup + Storage State
|
|
80
|
+
|
|
81
|
+
`apps/web/e2e/global-setup.ts`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { chromium, FullConfig } from "@playwright/test";
|
|
85
|
+
import path from "path";
|
|
86
|
+
|
|
87
|
+
const AUTH_FILE = path.join(__dirname, ".auth/user.json");
|
|
88
|
+
|
|
89
|
+
export default async function globalSetup(config: FullConfig) {
|
|
90
|
+
const email = process.env.E2E_TEST_EMAIL;
|
|
91
|
+
const password = process.env.E2E_TEST_PASSWORD;
|
|
92
|
+
|
|
93
|
+
if (!email || !password) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"E2E_TEST_EMAIL and E2E_TEST_PASSWORD must be set.\n" +
|
|
96
|
+
"Copy .env.local.example to .env.local, then run: pnpm e2e:provision"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { baseURL } = config.projects[0].use;
|
|
101
|
+
const browser = await chromium.launch();
|
|
102
|
+
const page = await browser.newPage();
|
|
103
|
+
|
|
104
|
+
await page.goto(`${baseURL}/login`);
|
|
105
|
+
await page.getByLabel(/email/i).fill(email);
|
|
106
|
+
await page.getByLabel(/password/i).fill(password);
|
|
107
|
+
await page.getByRole("button", { name: /sign in|log in/i }).click();
|
|
108
|
+
await page.waitForURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
|
|
109
|
+
|
|
110
|
+
await page.goto(baseURL!); // cold-start warmup
|
|
111
|
+
await page.context().storageState({ path: AUTH_FILE });
|
|
112
|
+
await browser.close();
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Gitignore storage state before first use:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
mkdir -p apps/web/e2e/.auth
|
|
120
|
+
echo "apps/web/e2e/.auth/" >> .gitignore
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Auth Probe
|
|
124
|
+
|
|
125
|
+
`apps/web/e2e/_probe/auth.spec.ts` — validates the login path directly (outside storage-
|
|
126
|
+
state flow) so credential failures are diagnosed cleanly:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { test, expect } from "@playwright/test";
|
|
130
|
+
|
|
131
|
+
test("auth probe: can log in with E2E_TEST_EMAIL/E2E_TEST_PASSWORD", async ({ page }) => {
|
|
132
|
+
const email = process.env.E2E_TEST_EMAIL;
|
|
133
|
+
const password = process.env.E2E_TEST_PASSWORD;
|
|
134
|
+
expect(email, "E2E_TEST_EMAIL env var is required").toBeTruthy();
|
|
135
|
+
expect(password, "E2E_TEST_PASSWORD env var is required").toBeTruthy();
|
|
136
|
+
|
|
137
|
+
await page.goto("/login");
|
|
138
|
+
await page.getByLabel(/email/i).fill(email!);
|
|
139
|
+
await page.getByLabel(/password/i).fill(password!);
|
|
140
|
+
await page.getByRole("button", { name: /sign in|log in/i }).click();
|
|
141
|
+
|
|
142
|
+
await expect(page).toHaveURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run probe: `pnpm exec playwright test --project=web _probe/auth`
|
|
147
|
+
|
|
148
|
+
## Pre-flight Probes (Step 6.5.2)
|
|
149
|
+
|
|
150
|
+
**Dev server**: `curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/` — expect
|
|
151
|
+
200/3xx. On failure:
|
|
152
|
+
|
|
153
|
+
> "Dev server is not responding on port `{port}`. Please run `cd apps/{app} && pnpm dev`
|
|
154
|
+
> in a separate terminal, then reply 'ready' when the page loads in your browser."
|
|
155
|
+
|
|
156
|
+
**Port alignment**: parse `playwright.config.ts` `baseURL` port; compare to
|
|
157
|
+
`.codebyplan/server.json` `port_allocations[]`. On mismatch ask which is correct, then
|
|
158
|
+
propose an Edit to align them.
|
|
159
|
+
|
|
160
|
+
## Spec-Writing Patterns
|
|
161
|
+
|
|
162
|
+
**One spec file per page/flow.** Mandatory per spec:
|
|
163
|
+
|
|
164
|
+
- Smoke test: loads, title correct, no console errors.
|
|
165
|
+
- Primary user flow: main interaction.
|
|
166
|
+
- Visual regression: `toHaveScreenshot` at every primary state.
|
|
167
|
+
|
|
168
|
+
For forms: fill + submit + verify success; validation errors.
|
|
169
|
+
For CRUD: create + verify; edit + verify; delete + confirm + verify.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { test, expect } from "@playwright/test";
|
|
173
|
+
|
|
174
|
+
test.describe("Home page", () => {
|
|
175
|
+
test.beforeEach(async ({ page }) => {
|
|
176
|
+
await page.goto("/");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("loads and shows heading", async ({ page }) => {
|
|
180
|
+
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
|
|
181
|
+
await expect(page).toHaveScreenshot("home-loaded.png", { maxDiffPixelRatio: 0.001 });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Screenshot Capture
|
|
187
|
+
|
|
188
|
+
**Baseline regression** (preferred):
|
|
189
|
+
```ts
|
|
190
|
+
await expect(page).toHaveScreenshot("state-name.png", { maxDiffPixelRatio: 0.001 });
|
|
191
|
+
```
|
|
192
|
+
Baselines live beside spec under `{spec}.spec.ts-snapshots/`. Committed to git.
|
|
193
|
+
|
|
194
|
+
**Diagnostic** (intermediate states):
|
|
195
|
+
```ts
|
|
196
|
+
await page.screenshot({
|
|
197
|
+
path: `test-results/screenshots/${test.info().title}-after-submit.png`,
|
|
198
|
+
fullPage: true,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Enumerate PNGs: `test-results/**/*.png` and `{spec}.spec.ts-snapshots/`.
|
|
203
|
+
|
|
204
|
+
**Never run `--update-snapshots` automatically.** A diff is a `visual_regression` failure.
|
|
205
|
+
|
|
206
|
+
## Run Command
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pnpm exec playwright test {spec} --project=web --reporter=list
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Selector Conventions
|
|
213
|
+
|
|
214
|
+
Prefer `getByRole`, `getByLabel`, `getByTestId` over positional CSS. For SCSS Modules:
|
|
215
|
+
`[class*='componentName']` with `.first()`. After navigation, re-query selectors from
|
|
216
|
+
the new page state rather than holding stale `Locator` handles.
|
|
217
|
+
|
|
218
|
+
## CI Secrets
|
|
219
|
+
|
|
220
|
+
`E2E_TEST_EMAIL`, `E2E_TEST_PASSWORD`, `NEXT_PUBLIC_SUPABASE_URL`,
|
|
221
|
+
`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (or legacy `_ANON_KEY`).
|
|
222
|
+
|
|
223
|
+
## Pitfalls
|
|
224
|
+
|
|
225
|
+
**Cold-start timeouts** — warmup in `globalSetup` (after `page.goto(baseURL!)`) primes
|
|
226
|
+
Turbopack compilation. **Port mismatch** — compare `baseURL` port to `server.json` before
|
|
227
|
+
running. **Supabase parallelism** — remote Supabase requires `workers: 1` to prevent
|
|
228
|
+
auth/RLS races. **SCSS Module selectors** — use `[class*='componentName'].first()` or
|
|
229
|
+
role-based selectors.
|