codebyplan 1.11.1 → 1.12.0

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.
Files changed (56) hide show
  1. package/dist/cli.js +602 -345
  2. package/package.json +1 -1
  3. package/templates/README.md +1 -1
  4. package/templates/agents/cbp-cc-executor.md +1 -1
  5. package/templates/agents/cbp-e2e-maestro.md +202 -0
  6. package/templates/agents/cbp-e2e-playwright.md +229 -0
  7. package/templates/agents/cbp-e2e-tauri.md +184 -0
  8. package/templates/agents/cbp-e2e-vscode.md +203 -0
  9. package/templates/agents/cbp-e2e-xcuitest.md +224 -0
  10. package/templates/agents/cbp-improve-claude.md +1 -1
  11. package/templates/agents/cbp-round-executor.md +11 -11
  12. package/templates/agents/cbp-task-check.md +1 -1
  13. package/templates/agents/cbp-task-planner.md +2 -0
  14. package/templates/agents/cbp-testing-qa-agent.md +9 -9
  15. package/templates/context/testing/e2e.md +303 -0
  16. package/templates/hooks/cbp-statusline.mjs +44 -0
  17. package/templates/hooks/cbp-statusline.py +24 -2
  18. package/templates/hooks/cbp-statusline.sh +22 -2
  19. package/templates/hooks/validate-structure-lengths.sh +2 -0
  20. package/templates/hooks/validate-structure-smoke.sh +2 -1
  21. package/templates/hooks/validate-structure-templates.sh +1 -0
  22. package/templates/rules/README.md +8 -1
  23. package/templates/rules/context-file-loading.md +4 -1
  24. package/templates/rules/e2e-mandatory.md +70 -0
  25. package/templates/rules/supabase-branch-lifecycle.md +99 -0
  26. package/templates/settings.project.base.json +1 -2
  27. package/templates/skills/cbp-build-cc-agent/SKILL.md +16 -14
  28. package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +4 -4
  29. package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +8 -6
  30. package/templates/skills/cbp-build-cc-mode/SKILL.md +4 -4
  31. package/templates/skills/cbp-build-cc-settings/reference/cbp-conventions.md +1 -2
  32. package/templates/skills/cbp-checkpoint-check/SKILL.md +12 -8
  33. package/templates/skills/cbp-checkpoint-create/SKILL.md +2 -0
  34. package/templates/skills/cbp-checkpoint-end/SKILL.md +27 -5
  35. package/templates/skills/cbp-checkpoint-plan/SKILL.md +2 -2
  36. package/templates/skills/cbp-checkpoint-plan/reference/e2e-discovery-probe.md +5 -5
  37. package/templates/skills/cbp-e2e-setup/SKILL.md +254 -0
  38. package/templates/skills/cbp-e2e-setup/reference/maestro.md +200 -0
  39. package/templates/skills/cbp-e2e-setup/reference/playwright.md +212 -0
  40. package/templates/skills/cbp-e2e-setup/reference/tauri.md +147 -0
  41. package/templates/skills/cbp-e2e-setup/reference/vscode.md +154 -0
  42. package/templates/skills/cbp-e2e-setup/reference/xcuitest.md +185 -0
  43. package/templates/skills/cbp-frontend-ui/SKILL.md +6 -6
  44. package/templates/skills/cbp-frontend-ux/SKILL.md +1 -1
  45. package/templates/skills/cbp-git-worktree-remove/SKILL.md +17 -1
  46. package/templates/skills/cbp-round-execute/SKILL.md +30 -17
  47. package/templates/skills/cbp-session-start/SKILL.md +27 -2
  48. package/templates/skills/cbp-ship-main/SKILL.md +13 -0
  49. package/templates/skills/cbp-supabase-branch-check/SKILL.md +12 -5
  50. package/templates/skills/cbp-supabase-migrate/SKILL.md +139 -9
  51. package/templates/skills/cbp-supabase-migrate/reference/preflight-dry-run.md +1 -1
  52. package/templates/skills/cbp-supabase-setup/SKILL.md +13 -7
  53. package/templates/skills/cbp-supabase-setup/reference/branching-setup.md +2 -2
  54. package/templates/skills/cbp-task-check/SKILL.md +2 -2
  55. package/templates/skills/cbp-task-start/SKILL.md +2 -0
  56. package/templates/agents/cbp-test-e2e-agent.md +0 -363
@@ -0,0 +1,254 @@
1
+ ---
2
+ scope: org-shared
3
+ name: cbp-e2e-setup
4
+ description: Detect installed E2E frameworks, ask which to enable, record credentials source (gitignored env-file path + var names only, never secrets), and write/refresh .codebyplan/e2e.json. Interactive, idempotent.
5
+ argument-hint: "[--force]"
6
+ model: sonnet
7
+ effort: xhigh
8
+ allowed-tools: Read, Write, Edit, Bash(cat *), Bash(jq *), Bash(which *), Bash(test *), Bash(mkdir *), Bash(cp *), Bash(echo *), Bash(date *), Bash(mv *), Bash(git check-ignore *), AskUserQuestion, mcp__codebyplan__get_repos
9
+ ---
10
+
11
+ # E2E Setup
12
+
13
+ Configure `.codebyplan/e2e.json` so the E2E test pipeline knows which frameworks are
14
+ enabled, where each app lives, and where to read credentials at test time.
15
+
16
+ Invoke at any time. Already-configured frameworks are preserved unless `--force` is passed.
17
+ Pass `--force` to re-ask all questions including credentials blocks.
18
+
19
+ ## Arguments
20
+
21
+ Inspect `$ARGUMENTS` for `--force`. If present, set `force_mode = true`.
22
+ Absent: use idempotent mode — preserve existing credentials blocks, skip re-asking
23
+ already-configured frameworks.
24
+
25
+ ## Step 1 — Detect installed frameworks
26
+
27
+ Run both detection signals and merge:
28
+
29
+ **Signal A — DB tech_stack** via `mcp__codebyplan__get_repos` (match `repo_id` from
30
+ `.codebyplan/repo.json`). Scan `tech_stack[]` for: `playwright`, `maestro`, `xcuitest`,
31
+ `webdriverio`, `@wdio/cli`, `@vscode/test-cli`.
32
+
33
+ **Signal B — Filesystem probes:**
34
+
35
+ ```bash
36
+ test -f playwright.config.ts || test -f playwright.config.js # → playwright
37
+ test -f maestro/config.yaml || test -d maestro # → maestro
38
+ test -d ios && find ios -name '*UITests' -maxdepth 2 | grep -q . # → xcuitest
39
+ test -f wdio.conf.ts || test -f wdio.conf.js # → tauri (wdio)
40
+ test -f .vscode-test.mjs || test -d apps/vscode # → vscode
41
+ ```
42
+
43
+ **Signal C — Read existing `.codebyplan/e2e.json`** for idempotent merge:
44
+
45
+ ```bash
46
+ cat .codebyplan/e2e.json 2>/dev/null || echo '{}'
47
+ ```
48
+
49
+ A framework detected by A or B is "detected". A framework already in e2e.json is
50
+ "configured". A framework with `enabled: false` in e2e.json is "configured-disabled".
51
+
52
+ ## Step 2 — Ask which to enable
53
+
54
+ Display a summary table of detection results:
55
+
56
+ ```
57
+ Framework | Detected | Configured | Status
58
+ ----------- | -------- | ---------- | ------
59
+ playwright | yes | yes | enabled
60
+ maestro | no | no | absent
61
+ xcuitest | no | no | absent
62
+ tauri | no | no | absent
63
+ vscode | no | no | absent
64
+ ```
65
+
66
+ AskUserQuestion (multi-select):
67
+
68
+ ```
69
+ Which E2E frameworks should be enabled?
70
+ Detected frameworks are pre-checked. Undetected ones can still be enabled.
71
+
72
+ Select all that apply:
73
+ A) playwright (web — Next.js)
74
+ B) maestro (mobile — Expo/React Native)
75
+ C) xcuitest (iOS native — Apple Watch, HealthKit, system dialogs)
76
+ D) tauri (desktop — WebDriverIO + tauri-driver)
77
+ E) vscode (VS Code extension — @vscode/test-cli)
78
+ ```
79
+
80
+ In `--force` mode: re-ask even for frameworks already enabled.
81
+ Otherwise: frameworks already `enabled: true` in e2e.json are kept without asking.
82
+
83
+ ## Step 3 — Mobile platforms (conditional)
84
+
85
+ If maestro or xcuitest is in the enabled set and the framework is not yet configured
86
+ (or `--force`), AskUserQuestion:
87
+
88
+ ```
89
+ Mobile platform target for <framework>:
90
+ A) Android only
91
+ B) iOS only
92
+ C) Both Android and iOS
93
+ ```
94
+
95
+ Record the answer as `platforms: ["android"]`, `["ios"]`, or `["android", "ios"]` on the
96
+ framework config block.
97
+
98
+ ## Step 4 — Credentials source
99
+
100
+ For each enabled framework that touches auth (playwright, maestro, xcuitest):
101
+
102
+ **Idempotency gate** (skip if `--force` is absent AND
103
+ `credentials.frameworks[framework].email_var` AND
104
+ `credentials.frameworks[framework].password_var` are both set to non-empty strings —
105
+ an empty `{}` entry counts as unconfigured, so prompt for it):
106
+
107
+ ```
108
+ Credentials block for <framework> already configured — use --force to reset.
109
+ env_file: <path>
110
+ email_var: <var>
111
+ password_var: <var>
112
+ ```
113
+
114
+ Print the preserved values and continue to Step 5.
115
+
116
+ **Otherwise**, AskUserQuestion (one question per framework, step-by-step):
117
+
118
+ ```
119
+ Credentials source for <framework>
120
+
121
+ 1. Gitignored env-file path that holds the secrets
122
+ (default: .codebyplan/e2e.env — a dedicated file, separate from app .env.local)
123
+ Path: ___
124
+
125
+ 2. Email env var name (default: E2E_TEST_EMAIL for playwright, TEST_EMAIL for others)
126
+ Var name: ___
127
+
128
+ 3. Password env var name (default: E2E_TEST_PASSWORD for playwright, TEST_PASSWORD for others)
129
+ Var name: ___
130
+
131
+ 4. (Optional) Provision script path — the skill only records the path, never creates it
132
+ (convention: scripts/provision-e2e-user.ts — leave blank to skip)
133
+ Path: ___
134
+ ```
135
+
136
+ After collecting the env-file path, verify it is gitignored and capture the result —
137
+ this boolean is persisted as the required `credentials.gitignored` field:
138
+
139
+ ```bash
140
+ git check-ignore -q <env_file_path> && GITIGNORED=true || GITIGNORED=false
141
+ ```
142
+
143
+ If NOT ignored (`GITIGNORED=false`): warn and offer to append it:
144
+
145
+ ```
146
+ Warning: <path> is not in .gitignore.
147
+ This file will contain live credentials — committing it is a credential leak.
148
+ Append <path> to .gitignore? (Y/n)
149
+ ```
150
+
151
+ On yes: append the path to `.gitignore` and set `GITIGNORED=true`. On no: leave
152
+ `GITIGNORED=false` and record in the output but do not block.
153
+
154
+ Carry `gitignored: $GITIGNORED` into the credentials block assembled for Step 5 — the
155
+ `E2eCredentials.gitignored` field is required, so it must always be populated.
156
+
157
+ Never write secret values into e2e.json — only the path, var names, and provision_script
158
+ reference are persisted.
159
+
160
+ ## Step 5 — Write .codebyplan/e2e.json
161
+
162
+ Build the updated payload conforming to the `E2eConfig` schema
163
+ (`packages/codebyplan-package/src/lib/types.ts`).
164
+
165
+ For playwright, derive `base_url` from `.codebyplan/server.json`:
166
+
167
+ ```bash
168
+ jq -r '.port_allocations[] | select(.label == "Web Dev") | "http://localhost:\(.port)"' \
169
+ .codebyplan/server.json | head -1
170
+ ```
171
+
172
+ Match by the `Web Dev` label rather than `server_type == "nextjs"` — a repo can have
173
+ several `nextjs` allocations (e.g. one per sibling worktree), so array-position
174
+ `head -1` is not stable. Confirm the derived URL with the user before writing
175
+ (`Derived base_url: <url> — correct? (Y/n)`) and allow an override. Store as
176
+ `frameworks.playwright.base_url`.
177
+
178
+ Idempotency rule: the jq object-merge (`. + {...}`) REPLACES top-level keys, so build
179
+ `$CREDENTIALS_JSON` as a deep-merge of the existing block and the newly-collected data
180
+ BEFORE the write — otherwise a second run for an additional framework would clobber the
181
+ first framework's credentials. Assemble it in the shell:
182
+
183
+ ```bash
184
+ EXISTING_CREDS=$(jq -c '.credentials // {}' .codebyplan/e2e.json)
185
+ CREDENTIALS_JSON=$(echo "$EXISTING_CREDS" | jq -c --argjson new "$NEW_CREDS_JSON" '. * $new')
186
+ ```
187
+
188
+ The `*` operator deep-merges, so frameworks skipped by the idempotency gate keep their
189
+ prior entry. Then write atomically using jq temp+mv to avoid partial writes (only the two
190
+ schema fields `frameworks` and `credentials` are written — `E2eConfig` defines no other):
191
+
192
+ ```bash
193
+ jq --argjson frameworks "$FRAMEWORKS_JSON" \
194
+ --argjson credentials "$CREDENTIALS_JSON" \
195
+ '. + {frameworks: $frameworks, credentials: $credentials}' \
196
+ .codebyplan/e2e.json > .codebyplan/e2e.json.tmp \
197
+ && mv .codebyplan/e2e.json.tmp .codebyplan/e2e.json
198
+ ```
199
+
200
+ Framework config shape per framework:
201
+
202
+ | Field | playwright | maestro / xcuitest | tauri / vscode |
203
+ | ------------ | ------------------- | -------------------- | -------------- |
204
+ | `enabled` | true | true | true |
205
+ | `app` | `apps/web` (nextjs) | `apps/<expo-app>` | `apps/desktop` / `apps/vscode` |
206
+ | `config_path`| `playwright.config.ts` | `maestro/config.yaml` | `wdio.conf.ts` / `.vscode-test.mjs` |
207
+ | `auto_run` | false | false | false |
208
+ | `test_dir` | `apps/web/e2e` | — | — |
209
+ | `base_url` | from server.json | — | — |
210
+ | `platforms` | — | from Step 3 | — |
211
+
212
+ Disabled frameworks get `enabled: false`; all other fields are preserved from their
213
+ prior configured state.
214
+
215
+ ## Step 6 — Verify and report
216
+
217
+ Re-read `.codebyplan/e2e.json` and emit a per-framework summary:
218
+
219
+ ```
220
+ E2E Setup — Complete
221
+
222
+ Framework | Status | App | base_url / platforms | Creds source
223
+ ----------- | -------- | ---------- | --------------------- | ------------
224
+ playwright | enabled | apps/web | http://localhost:3010 | .codebyplan/e2e.env (E2E_TEST_EMAIL)
225
+ maestro | disabled | — | — | —
226
+ ...
227
+
228
+ e2e.json written to .codebyplan/e2e.json
229
+
230
+ Next steps per framework — see reference docs:
231
+ playwright → reference/playwright.md
232
+ maestro → reference/maestro.md
233
+ xcuitest → reference/xcuitest.md
234
+ tauri → reference/tauri.md
235
+ vscode → reference/vscode.md
236
+ ```
237
+
238
+ ## Key Rules
239
+
240
+ - Never write secret values into e2e.json — only env-file path + var names
241
+ - Gitignore guard runs before any credentials are persisted
242
+ - Preserved credentials blocks are printed verbatim so the user can verify them
243
+ - Atomic write (tmp + mv) — never leaves e2e.json in a partial state
244
+ - `auto_run: false` by default — the user opts in explicitly
245
+
246
+ ## Additional resources
247
+
248
+ - Playwright install + auth + CI: [reference/playwright.md](reference/playwright.md)
249
+ - Maestro install + flows + CI: [reference/maestro.md](reference/maestro.md)
250
+ - Tauri (WebDriverIO): [reference/tauri.md](reference/tauri.md)
251
+ - VS Code extension testing: [reference/vscode.md](reference/vscode.md)
252
+ - XCUITest (iOS native): [reference/xcuitest.md](reference/xcuitest.md)
253
+ - E2E schema types: `packages/codebyplan-package/src/lib/types.ts` (E2eConfig)
254
+ - Shared E2E conventions: `.claude/context/testing/e2e.md`
@@ -0,0 +1,200 @@
1
+ # Maestro Reference
2
+
3
+ Full install, config, flows, and CI walkthrough for Maestro on an Expo/React Native project.
4
+ Source: vendor/maestro/v2.6 + `.claude/context/testing/e2e.md`.
5
+
6
+ ## Prerequisites
7
+
8
+ - Java 17 or later: `java -version` (install via `brew install openjdk@17` on macOS)
9
+ - Android emulator (for android targets) or iOS Simulator (for ios targets)
10
+ - Expo app bundled and running on the target device/emulator
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ # macOS — recommended
16
+ curl -fsSL "https://get.maestro.mobile.dev" | bash
17
+
18
+ # Alternative: Homebrew tap
19
+ brew tap mobile-dev-inc/tap
20
+ brew install maestro
21
+ ```
22
+
23
+ Verify:
24
+
25
+ ```bash
26
+ maestro --version
27
+ ```
28
+
29
+ Update later with: `maestro upgrade`
30
+
31
+ ## maestro/config.yaml
32
+
33
+ Create at repo root under `maestro/config.yaml`:
34
+
35
+ ```yaml
36
+ # Maestro workspace configuration
37
+ # See: https://docs.maestro.dev/maestro-flows/workspace-management/repository-configuration
38
+
39
+ appId: com.yourorg.yourapp # must match app.config.ts / expo config
40
+ env:
41
+ TEST_EMAIL: ${TEST_EMAIL}
42
+ TEST_PASSWORD: ${TEST_PASSWORD}
43
+ APP_ID: com.yourorg.yourapp
44
+ ```
45
+
46
+ `appId` must match the value in `app.config.ts` `ios.bundleIdentifier` /
47
+ `android.package`. If they differ across platforms, use the Android package ID for
48
+ Maestro's `appId` on Android and the iOS bundle ID on iOS tests.
49
+
50
+ ## Shared login flow
51
+
52
+ Create `maestro/flows/_shared/login.yaml`:
53
+
54
+ ```yaml
55
+ appId: ${APP_ID}
56
+ ---
57
+ - launchApp:
58
+ clearState: true
59
+ - assertVisible: "Sign in"
60
+ - tapOn: "Email"
61
+ - inputText: ${TEST_EMAIL}
62
+ - tapOn: "Password"
63
+ - inputText: ${TEST_PASSWORD}
64
+ - tapOn: "Sign in"
65
+ - assertVisible:
66
+ text: ".*" # Replace with a post-login element (e.g. "Dashboard")
67
+ timeout: 15000
68
+ ```
69
+
70
+ Reference it from any other flow:
71
+
72
+ ```yaml
73
+ - runFlow: _shared/login.yaml
74
+ ```
75
+
76
+ ## Auth probe flow
77
+
78
+ `maestro/flows/_probe/auth.yaml` — minimal login verification:
79
+
80
+ ```yaml
81
+ appId: ${APP_ID}
82
+ tags:
83
+ - probe
84
+ ---
85
+ - launchApp:
86
+ clearState: true
87
+ - assertVisible: "Sign in"
88
+ - tapOn: "Email"
89
+ - inputText: ${TEST_EMAIL}
90
+ - tapOn: "Password"
91
+ - inputText: ${TEST_PASSWORD}
92
+ - tapOn: "Sign in"
93
+ - assertVisible:
94
+ text: ".*"
95
+ timeout: 15000
96
+ ```
97
+
98
+ Run the probe before the full suite: `maestro test maestro/flows/_probe/auth.yaml`
99
+
100
+ ## Platform targeting
101
+
102
+ Maestro v2.6 exposes `-p` / `--platform` as a global option (placed BEFORE the `test`
103
+ subcommand). Values: `android`, `ios`, or `web`.
104
+
105
+ Run on Android: start an Android emulator, then
106
+
107
+ ```bash
108
+ maestro --platform=android test maestro/flows/
109
+ ```
110
+
111
+ Run on iOS: boot an iOS Simulator, then
112
+
113
+ ```bash
114
+ maestro --platform=ios test maestro/flows/
115
+ ```
116
+
117
+ Omitting the flag is also valid — platform is then implicit from whichever single
118
+ emulator/simulator is currently running.
119
+
120
+ Target a specific device by UDID / emulator name:
121
+
122
+ ```bash
123
+ maestro test --device <device-id> maestro/flows/
124
+ ```
125
+
126
+ ## Directory structure
127
+
128
+ ```
129
+ maestro/
130
+ config.yaml
131
+ flows/
132
+ _shared/
133
+ login.yaml
134
+ open-side-menu.yaml
135
+ _probe/
136
+ auth.yaml
137
+ onboarding/
138
+ signup.yaml
139
+ home/
140
+ dashboard.yaml
141
+ ```
142
+
143
+ One subdirectory per app module under `maestro/flows/`. Shared flows under `_shared/`.
144
+
145
+ ## Screenshots
146
+
147
+ ```yaml
148
+ - takeScreenshot: "after-login"
149
+ ```
150
+
151
+ Configure a repo-local screenshots path in `maestro/config.yaml`:
152
+
153
+ ```yaml
154
+ screenshotsDir: maestro/screenshots
155
+ ```
156
+
157
+ ## pnpm scripts
158
+
159
+ Add to root `package.json`:
160
+
161
+ ```json
162
+ {
163
+ "scripts": {
164
+ "maestro:test": "maestro test maestro/flows/",
165
+ "maestro:test:probe": "maestro test maestro/flows/_probe/",
166
+ "maestro:studio": "maestro studio"
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## CI (GitHub Actions)
172
+
173
+ Maestro CI runs require a connected device. For GitHub Actions use
174
+ [Maestro Cloud](https://cloud.maestro.dev) or a self-hosted runner with a connected
175
+ device. A minimal Maestro Cloud step:
176
+
177
+ ```yaml
178
+ - name: Run Maestro flows
179
+ uses: mobile-dev-inc/action-maestro-cloud@v1
180
+ with:
181
+ api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
182
+ app-file: path/to/app.apk # or .ipa
183
+ flow-file: maestro/flows/
184
+ env:
185
+ TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
186
+ TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
187
+ ```
188
+
189
+ For local self-hosted runner, set `TEST_EMAIL` and `TEST_PASSWORD` as runner env vars.
190
+
191
+ ## Pitfalls
192
+
193
+ **App ID mismatch** — `appId` in config.yaml must exactly match the compiled bundle
194
+ identifier. Re-run `expo prebuild` if you changed the identifier after prebuild.
195
+
196
+ **clearState: true** — always clear app state in `launchApp` for the login flow so
197
+ each run starts from a signed-out state.
198
+
199
+ **Java version** — Maestro requires Java 17+. If `maestro --version` fails, check
200
+ `JAVA_HOME` or install via Homebrew.
@@ -0,0 +1,212 @@
1
+ # Playwright Reference
2
+
3
+ Full install, config, auth, and CI walkthrough for Playwright on a Next.js monorepo.
4
+ Source: vendor/playwright/v1.60 + `.claude/context/testing/e2e.md`.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pnpm add -D @playwright/test
10
+ pnpm exec playwright install chromium
11
+ ```
12
+
13
+ For CI with system dependencies:
14
+
15
+ ```bash
16
+ pnpm exec playwright install --with-deps chromium
17
+ ```
18
+
19
+ ## playwright.config.ts
20
+
21
+ Derive `baseURL` from `.codebyplan/server.json` at config-read time:
22
+
23
+ ```ts
24
+ import { defineConfig, devices } from "@playwright/test";
25
+ import { execSync } from "child_process";
26
+ import path from "path";
27
+
28
+ // Pull the Web Dev port from server.json so config stays in sync. Match by label
29
+ // rather than server_type — a repo can have several nextjs allocations, so
30
+ // array-position head -1 is not stable.
31
+ function getBaseUrl(): string {
32
+ try {
33
+ const raw = execSync(
34
+ "jq -r '.port_allocations[] | select(.label==\"Web Dev\") | .port' .codebyplan/server.json 2>/dev/null | head -1",
35
+ { encoding: "utf-8" }
36
+ ).trim();
37
+ const port = parseInt(raw, 10);
38
+ return `http://localhost:${port}`;
39
+ } catch {
40
+ return "http://localhost:3010"; // fallback
41
+ }
42
+ }
43
+
44
+ export default defineConfig({
45
+ testDir: "apps/web/e2e",
46
+ fullyParallel: false,
47
+ forbidOnly: !!process.env.CI,
48
+ retries: process.env.CI ? 2 : 0,
49
+ workers: 1, // serialize against shared remote Supabase — see e2e.md § Supabase Parallelism
50
+ reporter: process.env.CI ? "github" : "html",
51
+ globalSetup: "./apps/web/e2e/global-setup", // string path — resolved relative to config; safe under ESM
52
+ use: {
53
+ baseURL: getBaseUrl(),
54
+ trace: "on-first-retry",
55
+ screenshot: "only-on-failure",
56
+ },
57
+ projects: [
58
+ { name: "setup", testMatch: /global\.setup\.ts/ },
59
+ {
60
+ name: "web",
61
+ use: { ...devices["Desktop Chrome"], storageState: "apps/web/e2e/.auth/user.json" },
62
+ dependencies: ["setup"],
63
+ },
64
+ ],
65
+ webServer: {
66
+ command: "pnpm --filter @codebyplan/web dev",
67
+ url: getBaseUrl(),
68
+ reuseExistingServer: !process.env.CI,
69
+ timeout: 120_000,
70
+ },
71
+ });
72
+ ```
73
+
74
+ Key options:
75
+
76
+ | Option | Why |
77
+ | --- | --- |
78
+ | `workers: 1` | Prevents auth/RLS races on a shared remote Supabase project |
79
+ | `globalSetup` | Logs in once, writes `storageState` so tests start authenticated |
80
+ | `reuseExistingServer` | Skip dev-server startup when already running locally |
81
+
82
+ ## Auth — global setup + storage state
83
+
84
+ Create `apps/web/e2e/global-setup.ts`:
85
+
86
+ ```ts
87
+ import { chromium, FullConfig } from "@playwright/test";
88
+ import path from "path";
89
+
90
+ const AUTH_FILE = path.join(__dirname, ".auth/user.json");
91
+
92
+ export default async function globalSetup(config: FullConfig) {
93
+ const email = process.env.E2E_TEST_EMAIL;
94
+ const password = process.env.E2E_TEST_PASSWORD;
95
+
96
+ if (!email || !password) {
97
+ throw new Error(
98
+ "E2E_TEST_EMAIL and E2E_TEST_PASSWORD must be set.\n" +
99
+ "Copy .env.local.example to .env.local, then run: pnpm e2e:provision"
100
+ );
101
+ }
102
+
103
+ const { baseURL } = config.projects[0].use;
104
+ const browser = await chromium.launch();
105
+ const page = await browser.newPage();
106
+
107
+ await page.goto(`${baseURL}/login`);
108
+ await page.getByLabel(/email/i).fill(email);
109
+ await page.getByLabel(/password/i).fill(password);
110
+ await page.getByRole("button", { name: /sign in|log in/i }).click();
111
+ await page.waitForURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
112
+
113
+ // Warm up the first route to avoid cold-start timeouts in specs
114
+ await page.goto(baseURL!);
115
+ await page.context().storageState({ path: AUTH_FILE });
116
+ await browser.close();
117
+ }
118
+ ```
119
+
120
+ Gitignore the auth state — run before first use:
121
+
122
+ ```bash
123
+ mkdir -p apps/web/e2e/.auth
124
+ echo "apps/web/e2e/.auth/" >> .gitignore
125
+ ```
126
+
127
+ ## Auth probe spec
128
+
129
+ `apps/web/e2e/_probe/auth.spec.ts` — validates the login path directly (outside
130
+ storage-state flow) so credential failures are diagnosed cleanly:
131
+
132
+ ```ts
133
+ import { test, expect } from "@playwright/test";
134
+
135
+ test("auth probe: can log in with E2E_TEST_EMAIL/E2E_TEST_PASSWORD", async ({
136
+ page,
137
+ }) => {
138
+ const email = process.env.E2E_TEST_EMAIL;
139
+ const password = process.env.E2E_TEST_PASSWORD;
140
+ expect(email, "E2E_TEST_EMAIL env var is required").toBeTruthy();
141
+ expect(password, "E2E_TEST_PASSWORD env var is required").toBeTruthy();
142
+
143
+ await page.goto("/login");
144
+ await page.getByLabel(/email/i).fill(email!);
145
+ await page.getByLabel(/password/i).fill(password!);
146
+ await page.getByRole("button", { name: /sign in|log in/i }).click();
147
+
148
+ await expect(page).toHaveURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
149
+ });
150
+ ```
151
+
152
+ Run the probe before the full suite: `pnpm exec playwright test --project=web _probe/auth`.
153
+
154
+ ## Provision script convention
155
+
156
+ Every repo with Playwright auth ships `scripts/provision-e2e-user.ts` — an idempotent
157
+ script that creates the test user in the dev Supabase project. Wire it to `package.json`:
158
+
159
+ ```json
160
+ {
161
+ "scripts": {
162
+ "e2e:provision": "tsx scripts/provision-e2e-user.ts"
163
+ }
164
+ }
165
+ ```
166
+
167
+ The skill records the path; the repo author writes the script. See
168
+ `.claude/context/testing/e2e.md` § Provisioning Playwright credentials for the full
169
+ contract (idempotency, multi-tenant subdomain, `.env.local.example`).
170
+
171
+ ## CI secrets
172
+
173
+ Add these four secrets to the GitHub repo (Settings → Secrets → Actions):
174
+
175
+ | Secret | Purpose |
176
+ | --- | --- |
177
+ | `E2E_TEST_EMAIL` | Test account email |
178
+ | `E2E_TEST_PASSWORD` | Test account password |
179
+ | `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL |
180
+ | `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Supabase publishable (anon) key |
181
+
182
+ ## GitHub Actions snippet
183
+
184
+ ```yaml
185
+ - name: Install Playwright browsers
186
+ run: pnpm exec playwright install --with-deps chromium
187
+
188
+ - name: Run Playwright tests
189
+ run: pnpm exec playwright test
190
+ env:
191
+ E2E_TEST_EMAIL: ${{ secrets.E2E_TEST_EMAIL }}
192
+ E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }}
193
+ NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
194
+ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY }}
195
+ ```
196
+
197
+ ## Pitfalls
198
+
199
+ **Cold-start timeouts** — Next.js dev mode compiles routes lazily. The warmup fetch in
200
+ `globalSetup` (after `page.goto(baseURL!)`) primes the compiler before specs run.
201
+
202
+ **SCSS Module selectors** — prefer `[class*='componentName']` with `.first()` over
203
+ positional `.nth(N)` locators. Prefer `getByRole`/`getByLabel`/`getByTestId` when
204
+ accessible names are available.
205
+
206
+ **SCSS import errors in tests** — Playwright runs in Node, not webpack. If your test
207
+ imports a component that imports SCSS, configure `playwright.config.ts` to use
208
+ `@playwright/test`'s built-in transform or exclude such imports.
209
+
210
+ **Port mismatch** — before running, compare `playwright.config.ts` `baseURL` port with
211
+ `.codebyplan/server.json`. On mismatch, the `cbp-e2e-playwright` agent will ask which is
212
+ correct — do not guess.