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
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbp-e2e-tauri
|
|
3
|
+
description: WebDriverIO + tauri-driver E2E test authoring + execution for Tauri desktop apps. Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'webdriverio'.
|
|
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
|
+
# Tauri 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: WebDriverIO + tauri-driver on Tauri desktop apps. Dispatched when
|
|
16
|
+
`.codebyplan/e2e.json` records `framework: "webdriverio"`.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Rust toolchain: `rustup --version` (install via https://rustup.rs)
|
|
21
|
+
- `tauri-driver` binary (see Install below)
|
|
22
|
+
- Built Tauri binary: `cargo build` must complete before any tests run
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add -D @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/spec-reporter
|
|
28
|
+
cargo install tauri-driver
|
|
29
|
+
which tauri-driver && tauri-driver --version # verify
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## wdio.conf.ts
|
|
33
|
+
|
|
34
|
+
Place at `apps/desktop/wdio.conf.ts`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { spawn, spawnSync } from "child_process";
|
|
38
|
+
import type { Options } from "@wdio/types";
|
|
39
|
+
|
|
40
|
+
const BINARY_PATH = "./src-tauri/target/debug/your-app-name";
|
|
41
|
+
|
|
42
|
+
let tauriDriver: ReturnType<typeof spawn>;
|
|
43
|
+
|
|
44
|
+
export const config: Options.Testrunner = {
|
|
45
|
+
specs: ["./e2e/**/*.spec.ts"],
|
|
46
|
+
maxInstances: 1,
|
|
47
|
+
capabilities: [
|
|
48
|
+
{
|
|
49
|
+
"tauri:options": { application: BINARY_PATH },
|
|
50
|
+
maxInstances: 1,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
services: ["chromedriver"],
|
|
54
|
+
framework: "mocha",
|
|
55
|
+
reporters: ["spec"],
|
|
56
|
+
mochaOpts: { timeout: 60_000 },
|
|
57
|
+
|
|
58
|
+
beforeSession: async () => {
|
|
59
|
+
tauriDriver = spawn("tauri-driver", [], {
|
|
60
|
+
stdio: [null, process.stdout, process.stderr],
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
afterSession: async () => {
|
|
65
|
+
tauriDriver.kill();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Build Before Running
|
|
71
|
+
|
|
72
|
+
Always build the Tauri binary before running tests:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cargo build --manifest-path apps/desktop/src-tauri/Cargo.toml
|
|
76
|
+
pnpm --filter @codebyplan/desktop wdio run wdio.conf.ts
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Combined pnpm script:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"scripts": {
|
|
84
|
+
"e2e": "cargo build --manifest-path src-tauri/Cargo.toml && wdio run wdio.conf.ts",
|
|
85
|
+
"e2e:test": "wdio run wdio.conf.ts"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Pre-flight Probe (Step 6.5.2)
|
|
91
|
+
|
|
92
|
+
**Binary existence**: check the path set in `wdio.conf.ts` `capabilities[0]["tauri:options"].application`.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
test -f {BINARY_PATH} && echo "ok" || echo "missing"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
On failure:
|
|
99
|
+
|
|
100
|
+
> "Tauri binary not found at `{path}`. Please run `cd src-tauri && cargo build` (or
|
|
101
|
+
> `cargo build --release`). Reply 'ready' when the build finishes."
|
|
102
|
+
|
|
103
|
+
No auth probe needed — Tauri desktop apps typically skip network auth; adapt if the app
|
|
104
|
+
has a login form.
|
|
105
|
+
|
|
106
|
+
## Auth Probe (when has_auth)
|
|
107
|
+
|
|
108
|
+
`apps/desktop/e2e/_probe/auth.spec.ts`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { browser, $ } from "@wdio/globals";
|
|
112
|
+
import { expect } from "@wdio/globals";
|
|
113
|
+
|
|
114
|
+
describe("auth probe", () => {
|
|
115
|
+
it("can reach the main window", async () => {
|
|
116
|
+
const root = await $("[data-testid='app-root']");
|
|
117
|
+
await expect(root).toBeDisplayed();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Run: `pnpm exec wdio run wdio.conf.ts --spec e2e/_probe/auth.spec.ts`
|
|
123
|
+
|
|
124
|
+
## Spec-Writing Patterns
|
|
125
|
+
|
|
126
|
+
Use `data-testid` attributes for stable targeting (Tauri WebView renders HTML; SCSS
|
|
127
|
+
Modules mangle class names):
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { browser, $ } from "@wdio/globals";
|
|
131
|
+
import { expect } from "@wdio/globals";
|
|
132
|
+
|
|
133
|
+
describe("Desktop app", () => {
|
|
134
|
+
it("opens the main window", async () => {
|
|
135
|
+
const navBar = await $("[data-testid='nav']");
|
|
136
|
+
await expect(navBar).toBeDisplayed();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("navigates to settings", async () => {
|
|
140
|
+
await $("[data-testid='settings-link']").click();
|
|
141
|
+
await expect($("[data-testid='settings-panel']")).toBeDisplayed();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For CRUD: create + verify visible; edit + verify; delete + confirm + verify removed.
|
|
147
|
+
|
|
148
|
+
## Screenshot Capture
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await browser.saveScreenshot(`./e2e/screenshots/${testName}-${state}.png`);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Enumerate: `e2e/screenshots/*.png`.
|
|
155
|
+
|
|
156
|
+
## Run Command
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pnpm exec wdio run wdio.conf.ts --spec {spec}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## CI
|
|
163
|
+
|
|
164
|
+
Tauri desktop E2E on CI requires a display (Xvfb on Linux) and the full Rust toolchain:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
- name: Install Xvfb (Linux)
|
|
168
|
+
run: sudo apt-get install -y xvfb
|
|
169
|
+
|
|
170
|
+
- name: Build Tauri binary
|
|
171
|
+
run: cargo build --manifest-path apps/desktop/src-tauri/Cargo.toml
|
|
172
|
+
|
|
173
|
+
- name: Run WebDriverIO tests
|
|
174
|
+
run: xvfb-run -a pnpm --filter @codebyplan/desktop e2e:test
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use `ubuntu-latest` or `macos-latest` GitHub-hosted runners.
|
|
178
|
+
|
|
179
|
+
## Pitfalls
|
|
180
|
+
|
|
181
|
+
**Must build before run** — tauri-driver launches the binary; if absent or stale the
|
|
182
|
+
session fails immediately. **Binary path** — debug builds: `src-tauri/target/debug/`;
|
|
183
|
+
release builds: `src-tauri/target/release/`. **Port conflicts** — tauri-driver listens
|
|
184
|
+
on 4444 by default; ensure no other WebDriver session occupies the same port.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbp-e2e-vscode
|
|
3
|
+
description: VS Code extension E2E test authoring + execution using @vscode/test-cli and @vscode/test-electron. Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'vscode-test'.
|
|
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
|
+
# VS Code Extension 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: `@vscode/test-cli` + `@vscode/test-electron` for VS Code extensions.
|
|
16
|
+
Dispatched when `.codebyplan/e2e.json` records `framework: "vscode-test"`.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- VS Code installed (used as the test host)
|
|
21
|
+
- On Linux CI: Xvfb for a display server (extensions require a GUI)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add -D @vscode/test-cli @vscode/test-electron
|
|
27
|
+
pnpm exec vscode-test --version # verify
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## .vscode-test.mjs
|
|
31
|
+
|
|
32
|
+
Create at the extension package root (e.g. `apps/vscode/`):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { defineConfig } from "@vscode/test-cli";
|
|
36
|
+
|
|
37
|
+
export default defineConfig({
|
|
38
|
+
files: "e2e/**/*.test.js", // compiled JS output path
|
|
39
|
+
extensionDevelopmentPath: ".", // path to the extension package root
|
|
40
|
+
workspaceFolder: "test-fixtures/workspace", // optional fixture workspace
|
|
41
|
+
mocha: {
|
|
42
|
+
timeout: 20_000,
|
|
43
|
+
ui: "bdd",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
pnpm scripts:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"scripts": {
|
|
53
|
+
"test:e2e": "tsc -p tsconfig.test.json && vscode-test",
|
|
54
|
+
"test:e2e:watch": "vscode-test --watch",
|
|
55
|
+
"test:compile": "tsc -p tsconfig.test.json"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Extension Host Lifecycle
|
|
61
|
+
|
|
62
|
+
`@vscode/test-electron` downloads an isolated VS Code instance, installs the extension,
|
|
63
|
+
opens the workspace, and runs the Mocha suite inside the extension host process. Tests
|
|
64
|
+
import from `vscode` — the module is available because they run inside VS Code:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import * as vscode from "vscode";
|
|
68
|
+
import * as assert from "assert";
|
|
69
|
+
|
|
70
|
+
suite("Extension", () => {
|
|
71
|
+
test("extension activates", async () => {
|
|
72
|
+
const ext = vscode.extensions.getExtension("yourpublisher.yourextension");
|
|
73
|
+
assert.ok(ext, "extension not found");
|
|
74
|
+
await ext.activate();
|
|
75
|
+
assert.ok(ext.isActive);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("command is registered", async () => {
|
|
79
|
+
const commands = await vscode.commands.getCommands();
|
|
80
|
+
assert.ok(commands.includes("yourextension.yourCommand"), "command not registered");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Directory Structure
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
apps/vscode/
|
|
89
|
+
.vscode-test.mjs
|
|
90
|
+
e2e/
|
|
91
|
+
_probe/
|
|
92
|
+
activation.test.ts
|
|
93
|
+
commands/
|
|
94
|
+
my-command.test.ts
|
|
95
|
+
test-fixtures/
|
|
96
|
+
workspace/ # committed fixture files opened in tests
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Activation Probe
|
|
100
|
+
|
|
101
|
+
`apps/vscode/e2e/_probe/activation.test.ts`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import * as vscode from "vscode";
|
|
105
|
+
import * as assert from "assert";
|
|
106
|
+
|
|
107
|
+
suite("Activation probe", () => {
|
|
108
|
+
test("extension activates without error", async () => {
|
|
109
|
+
const ext = vscode.extensions.getExtension("yourpublisher.yourextension");
|
|
110
|
+
assert.ok(ext, "Extension not installed in test host");
|
|
111
|
+
if (!ext.isActive) {
|
|
112
|
+
await ext.activate();
|
|
113
|
+
}
|
|
114
|
+
assert.ok(ext.isActive, "Extension did not activate");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Pre-flight Probe (Step 6.5.2)
|
|
120
|
+
|
|
121
|
+
**Compiled output**: verify `e2e/**/*.test.js` files exist (TS must be compiled first).
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
ls apps/vscode/e2e/**/*.test.js 2>/dev/null | head -1
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
On missing output:
|
|
128
|
+
|
|
129
|
+
> "VS Code extension tests need to be compiled first. Please run
|
|
130
|
+
> `pnpm --filter @codebyplan/vscode test:compile`. Reply 'ready' when complete."
|
|
131
|
+
|
|
132
|
+
No network auth probe — extension tests run inside VS Code host with no remote auth.
|
|
133
|
+
|
|
134
|
+
## Spec-Writing Patterns
|
|
135
|
+
|
|
136
|
+
Write tests using the full `vscode` API:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import * as vscode from "vscode";
|
|
140
|
+
import * as assert from "assert";
|
|
141
|
+
|
|
142
|
+
suite("My Command", () => {
|
|
143
|
+
test("executes and returns expected result", async () => {
|
|
144
|
+
const result = await vscode.commands.executeCommand(
|
|
145
|
+
"yourextension.myCommand",
|
|
146
|
+
"testArg"
|
|
147
|
+
);
|
|
148
|
+
assert.strictEqual(result, "expectedValue");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("reads workspace configuration", () => {
|
|
152
|
+
const config = vscode.workspace.getConfiguration("yourextension");
|
|
153
|
+
const value = config.get<string>("someKey");
|
|
154
|
+
assert.ok(value !== undefined, "configuration key missing");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For diagnostic captures, use `vscode.window.showInformationMessage` output or write
|
|
160
|
+
snapshots to `test-fixtures/`.
|
|
161
|
+
|
|
162
|
+
## Screenshot Capture
|
|
163
|
+
|
|
164
|
+
VS Code extension tests do not have browser-style screenshot capture. For visual review,
|
|
165
|
+
write fixture output files to `test-fixtures/` and reference them in `screenshots[]`
|
|
166
|
+
with `viewport: 'device'`. `baseline_diff_pct: null` for all entries.
|
|
167
|
+
|
|
168
|
+
Enumerate screenshots: `apps/vscode/test-fixtures/**/*.png`.
|
|
169
|
+
|
|
170
|
+
## Run Command
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pnpm --filter @codebyplan/vscode test:e2e
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## CI (GitHub Actions)
|
|
177
|
+
|
|
178
|
+
Linux requires Xvfb:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
- name: Install dependencies
|
|
182
|
+
run: pnpm install
|
|
183
|
+
|
|
184
|
+
- name: Compile extension tests
|
|
185
|
+
run: pnpm --filter @codebyplan/vscode test:compile
|
|
186
|
+
|
|
187
|
+
- name: Run VS Code extension tests
|
|
188
|
+
run: xvfb-run -a pnpm --filter @codebyplan/vscode test:e2e
|
|
189
|
+
env:
|
|
190
|
+
DISPLAY: ':99.0'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
On macOS/Windows, Xvfb is not needed — `vscode-test` uses the native display.
|
|
194
|
+
|
|
195
|
+
## Pitfalls
|
|
196
|
+
|
|
197
|
+
**Wrong extensionDevelopmentPath** — if `.vscode-test.mjs` doesn't point to the package
|
|
198
|
+
root (where `package.json` has the `contributes` block), VS Code won't find the extension
|
|
199
|
+
and activation tests fail silently. **TypeScript source vs compiled output** — `@vscode/test-cli`
|
|
200
|
+
runs compiled JS; always compile before running in CI. **Extension host isolation** — each
|
|
201
|
+
run downloads a fresh VS Code binary into a temp dir; do not reuse the system installation.
|
|
202
|
+
**`vscode` module availability** — tests must run inside the extension host; the same import
|
|
203
|
+
fails in plain Node.js.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cbp-e2e-xcuitest
|
|
3
|
+
description: XCUITest native iOS E2E test authoring + execution for Expo apps targeting system dialogs, HealthKit, watchOS, or other areas Maestro cannot reach. Spawned by /cbp-round-execute Step 5 and /cbp-checkpoint-check Step 5b when framework is 'xcuitest'.
|
|
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
|
+
# XCUITest 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: XCUITest via the Expo `withXCUITests` plugin. Dispatched when
|
|
16
|
+
`.codebyplan/e2e.json` records `framework: "xcuitest"`.
|
|
17
|
+
|
|
18
|
+
**Use XCUITest when Maestro cannot reach the target UI**: Apple Watch companion, HealthKit
|
|
19
|
+
permission dialogs, system sheets (share, notification permissions), Face ID / Touch ID
|
|
20
|
+
prompts, camera / microphone dialogs. For standard UI flows, prefer Maestro.
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- macOS with Xcode 15+
|
|
25
|
+
- Active Apple Developer account (free tier sufficient for Simulator testing)
|
|
26
|
+
- Expo managed workflow with prebuild enabled
|
|
27
|
+
- `xcbeautify`: `brew install xcbeautify`
|
|
28
|
+
|
|
29
|
+
## Setup — Expo withXCUITests Plugin
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -D expo-xcuitest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`app.config.ts`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
plugins: [
|
|
39
|
+
["expo-xcuitest", { testTargetName: "AppUITests" }]
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After updating `app.config.ts`, regenerate the native project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
expo prebuild --platform ios --clean
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`--clean` ensures a fresh native project. Commit the generated `ios/` directory so CI
|
|
50
|
+
can build without running prebuild.
|
|
51
|
+
|
|
52
|
+
## Swift Test Class
|
|
53
|
+
|
|
54
|
+
`ios/AppUITests/AppUITests.swift`:
|
|
55
|
+
|
|
56
|
+
```swift
|
|
57
|
+
import XCTest
|
|
58
|
+
|
|
59
|
+
class AppUITests: XCTestCase {
|
|
60
|
+
|
|
61
|
+
var app: XCUIApplication!
|
|
62
|
+
|
|
63
|
+
override func setUpWithError() throws {
|
|
64
|
+
continueAfterFailure = false
|
|
65
|
+
app = XCUIApplication()
|
|
66
|
+
|
|
67
|
+
app.launchEnvironment["TEST_EMAIL"] = ProcessInfo.processInfo.environment["TEST_EMAIL"] ?? ""
|
|
68
|
+
app.launchEnvironment["TEST_PASSWORD"] = ProcessInfo.processInfo.environment["TEST_PASSWORD"] ?? ""
|
|
69
|
+
|
|
70
|
+
app.launch()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func testLoginFlow() throws {
|
|
74
|
+
let emailField = app.textFields["email-input"]
|
|
75
|
+
XCTAssertTrue(emailField.waitForExistence(timeout: 10))
|
|
76
|
+
|
|
77
|
+
emailField.tap()
|
|
78
|
+
emailField.typeText(app.launchEnvironment["TEST_EMAIL"]!)
|
|
79
|
+
|
|
80
|
+
let passwordField = app.secureTextFields["password-input"]
|
|
81
|
+
passwordField.tap()
|
|
82
|
+
passwordField.typeText(app.launchEnvironment["TEST_PASSWORD"]!)
|
|
83
|
+
|
|
84
|
+
app.buttons["sign-in-button"].tap()
|
|
85
|
+
|
|
86
|
+
let dashboard = app.staticTexts["Dashboard"]
|
|
87
|
+
XCTAssertTrue(dashboard.waitForExistence(timeout: 15))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## accessibilityIdentifier Targeting
|
|
93
|
+
|
|
94
|
+
React Native maps `testID` to `accessibilityIdentifier` on iOS:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<TextInput
|
|
98
|
+
testID="email-input" // becomes accessibilityIdentifier on iOS
|
|
99
|
+
accessibilityLabel="Email"
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
XCUITest queries by identifier:
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
app.textFields["email-input"] // TextInput
|
|
107
|
+
app.buttons["sign-in-button"] // TouchableOpacity / Pressable
|
|
108
|
+
app.staticTexts["Dashboard"] // Text component
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Pre-flight Probe (Step 6.5.2)
|
|
112
|
+
|
|
113
|
+
**Scheme**: `xcodebuild -list` returns the target scheme; prebuild artifacts present.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
xcodebuild -list -workspace ios/YourApp.xcworkspace 2>&1 | grep "Schemes" -A 5
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
On missing prebuild:
|
|
120
|
+
|
|
121
|
+
> "iOS prebuild missing. Run `pnpm expo prebuild --platform ios --clean`. Reply 'ready'
|
|
122
|
+
> when done."
|
|
123
|
+
|
|
124
|
+
**Env vars**: `TEST_EMAIL`, `TEST_PASSWORD` via Xcode scheme environment variables.
|
|
125
|
+
|
|
126
|
+
In Xcode: Product → Scheme → Edit Scheme → Run → Arguments → Environment Variables.
|
|
127
|
+
|
|
128
|
+
## Auth Probe (when has_auth)
|
|
129
|
+
|
|
130
|
+
Run only the login test method against the UITest target:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
xcodebuild test \
|
|
134
|
+
-workspace ios/YourApp.xcworkspace \
|
|
135
|
+
-scheme YourApp \
|
|
136
|
+
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
|
|
137
|
+
-only-testing:AppUITests/AppUITests/testLoginFlow \
|
|
138
|
+
TEST_EMAIL="$TEST_EMAIL" TEST_PASSWORD="$TEST_PASSWORD" \
|
|
139
|
+
| xcbeautify
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Spec-Writing Patterns
|
|
143
|
+
|
|
144
|
+
Use `waitForExistence(timeout:)` on every element — React Native renders asynchronously:
|
|
145
|
+
|
|
146
|
+
```swift
|
|
147
|
+
func testHealthKitPermissionDialog() throws {
|
|
148
|
+
app.buttons["request-health-access"].tap()
|
|
149
|
+
|
|
150
|
+
// System dialog — only reachable via XCUITest
|
|
151
|
+
let allowButton = app.alerts.buttons["Allow Full Access"]
|
|
152
|
+
XCTAssertTrue(allowButton.waitForExistence(timeout: 10))
|
|
153
|
+
allowButton.tap()
|
|
154
|
+
|
|
155
|
+
let confirmation = app.staticTexts["Health data linked"]
|
|
156
|
+
XCTAssertTrue(confirmation.waitForExistence(timeout: 15))
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Screenshot Capture
|
|
161
|
+
|
|
162
|
+
XCUITest captures screenshots via:
|
|
163
|
+
|
|
164
|
+
```swift
|
|
165
|
+
let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
|
|
166
|
+
screenshot.name = "after-health-permission"
|
|
167
|
+
screenshot.lifetime = .keepAlways
|
|
168
|
+
add(screenshot)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Attachments are written to the test results bundle under `DerivedData`. Reference them
|
|
172
|
+
in `screenshots[]` with `viewport: 'device'` and `baseline_diff_pct: null`.
|
|
173
|
+
|
|
174
|
+
Enumerate: `~/Library/Developer/Xcode/DerivedData/**/Attachments/*.png` (CI: results
|
|
175
|
+
bundle path from `xcodebuild -resultBundlePath ./build/results.xcresult`).
|
|
176
|
+
|
|
177
|
+
## Run Command
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
xcodebuild test \
|
|
181
|
+
-workspace ios/YourApp.xcworkspace \
|
|
182
|
+
-scheme YourApp \
|
|
183
|
+
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
|
|
184
|
+
TEST_EMAIL="$TEST_EMAIL" \
|
|
185
|
+
TEST_PASSWORD="$TEST_PASSWORD" \
|
|
186
|
+
| xcbeautify
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## pnpm Script
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"scripts": {
|
|
194
|
+
"xcuitest": "xcodebuild test -workspace ios/YourApp.xcworkspace -scheme YourApp -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' | xcbeautify"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## CI (GitHub Actions)
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
- name: Pre-boot simulator
|
|
203
|
+
run: xcrun simctl boot "iPhone 16"
|
|
204
|
+
|
|
205
|
+
- name: Run XCUITest
|
|
206
|
+
run: |
|
|
207
|
+
xcodebuild test \
|
|
208
|
+
-workspace ios/YourApp.xcworkspace \
|
|
209
|
+
-scheme YourApp \
|
|
210
|
+
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
|
|
211
|
+
TEST_EMAIL="${{ secrets.TEST_EMAIL }}" \
|
|
212
|
+
TEST_PASSWORD="${{ secrets.TEST_PASSWORD }}" \
|
|
213
|
+
| xcbeautify
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Pitfalls
|
|
217
|
+
|
|
218
|
+
**Simulator not booted** — pre-boot in CI setup step to avoid slow first run. **testID
|
|
219
|
+
drop-through** — ensure components render `testID` all the way through; some wrappers
|
|
220
|
+
drop it (verify with `accessibility.identifier` in the Xcode accessibility inspector).
|
|
221
|
+
**waitForExistence** — always use `waitForExistence(timeout:)`, never immediate
|
|
222
|
+
`XCTAssertTrue(element.exists)`. **Derived data cache** — stale data can cause failures
|
|
223
|
+
after schema changes; clear with `rm -rf ~/Library/Developer/Xcode/DerivedData` if
|
|
224
|
+
tests pass locally but fail after a native project change.
|
|
@@ -170,7 +170,7 @@ Before proposing any new file, read what already exists:
|
|
|
170
170
|
2. Glob `.claude/skills/*/SKILL.md` — read names and frontmatter descriptions
|
|
171
171
|
3. Glob `.claude/context/*.md` — read names and first heading
|
|
172
172
|
4. Glob `.claude/docs/architecture/*.md` — read names and first heading
|
|
173
|
-
5. Glob `.claude/agents/*/AGENT.md` — read names and frontmatter descriptions
|
|
173
|
+
5. Glob `.claude/agents/*.md` (and `.claude/agents/*/AGENT.md` for folder-form agents) — read names and frontmatter descriptions
|
|
174
174
|
|
|
175
175
|
**5b: Propose changes with update-first discipline (HARD RULE)**
|
|
176
176
|
|