ccqa 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kazuki Shibutani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # ccqa
2
+
3
+ **Your Claude subscription already includes a QA engineer.**
4
+
5
+ ccqa turns Claude Code into a browser test recorder.
6
+
7
+ Write a spec in Markdown, run `ccqa trace`, and Claude drives your app via [agent-browser](https://github.com/vercel-labs/agent-browser) — a lightweight headless browser CLI that runs anywhere without a browser driver or Playwright setup. Because the agent controls the browser through a simple CLI interface, it can handle login flows, intermediate screens, and dynamic UI the same way a human would.
8
+
9
+ Every action is recorded as structured data and compiled into a deterministic test script you can run in CI. No extra API key. Just `claude`.
10
+
11
+ ## How it works
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ A["Write spec\n(test-spec.md)"] --> B["ccqa trace\n(Claude drives browser)"]
16
+ B --> C["ccqa generate\n(LLM → test script)"]
17
+ C --> D["ccqa run\n(deterministic replay)"]
18
+ ```
19
+
20
+ `trace` invokes Claude Code with your spec. Claude drives the browser step by step via [agent-browser](https://github.com/vercel-labs/agent-browser), recording every action as structured data. `generate` compiles that data into a vitest-compatible script. `run` replays it deterministically — no LLM involved.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ bunx ccqa trace tasks/create-and-complete
26
+ ```
27
+
28
+ Or install globally:
29
+
30
+ ```bash
31
+ bun add -g ccqa
32
+ ```
33
+
34
+ Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [agent-browser](https://github.com/vercel-labs/agent-browser) installed globally:
35
+
36
+ ```bash
37
+ npm install -g @anthropic-ai/claude-code
38
+ bun add -g agent-browser
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ **1. Write a spec**
44
+
45
+ ```markdown
46
+ <!-- .ccqa/features/tasks/test-cases/create-and-complete/test-spec.md -->
47
+ ---
48
+ title: Create a task and mark it complete
49
+ baseUrl: http://localhost:3000
50
+ ---
51
+
52
+ ## Steps
53
+
54
+ ### Step 1: Log in
55
+ - **Instruction**: Fill in email and password, submit the form
56
+ - **Expected**: Redirected to /dashboard, user avatar visible in the header
57
+
58
+ ### Step 2: Create a new task
59
+ - **Instruction**: Click "New Task", fill in the title "Fix login bug", set priority to High, save
60
+ - **Expected**: Task appears in the task list with status "Open"
61
+
62
+ ### Step 3: Mark the task as complete
63
+ - **Instruction**: Open the task "Fix login bug", click "Mark as complete"
64
+ - **Expected**: Task status changes to "Done", task moves to the completed section
65
+ ```
66
+
67
+ **2. Trace — Claude drives the browser and records every action**
68
+
69
+ ```bash
70
+ ccqa trace tasks/create-and-complete
71
+ ```
72
+
73
+ ```
74
+ ▶ trace tasks/create-and-complete
75
+ spec Create a task and mark it complete
76
+ url http://localhost:3000
77
+ steps 3
78
+
79
+ Running agent-browser session...
80
+ ● step-01 Log in
81
+ ● step-02 Create a new task
82
+ ● step-03 Mark the task as complete
83
+
84
+ trace .ccqa/features/tasks/test-cases/create-and-complete/actions.json
85
+ actions 24
86
+ status PASSED
87
+ ```
88
+
89
+ **3. Generate — convert recorded actions into a replayable test**
90
+
91
+ ```bash
92
+ ccqa generate tasks/create-and-complete
93
+ ```
94
+
95
+ **4. Run — replay deterministically, no LLM involved**
96
+
97
+ ```bash
98
+ ccqa run tasks/create-and-complete
99
+ ```
100
+
101
+ ## Setup Specs — Reusable shared procedures
102
+
103
+ Setup specs let you define reusable procedures (login, data preparation, etc.) that run before your test steps. Define once, use across multiple test specs.
104
+
105
+ ### 1. Write a setup spec
106
+
107
+ ```markdown
108
+ <!-- .ccqa/setups/login/setup-spec.md -->
109
+ ---
110
+ title: "Login"
111
+ placeholders:
112
+ loginUrl:
113
+ dummy: "http://localhost:3000/login"
114
+ description: "Login page URL"
115
+ email:
116
+ dummy: "user@example.com"
117
+ description: "Email address"
118
+ password:
119
+ dummy: "secret"
120
+ description: "Password"
121
+ ---
122
+
123
+ ## Steps
124
+
125
+ ### Step 1: Open login page
126
+ - **Instruction**: Navigate to {{loginUrl}}
127
+ - **Expected**: Login form is displayed
128
+
129
+ ### Step 2: Enter credentials and log in
130
+ - **Instruction**: Enter email {{email}} and password {{password}}, then submit
131
+ - **Expected**: Login succeeds
132
+ ```
133
+
134
+ The `placeholders` section defines variables with `dummy` values. During `trace-setup`, the dummy values are used for actual browser operation. During `generate-setup`, they are reverse-replaced with `{{key}}` placeholders.
135
+
136
+ ### 2. Trace the setup
137
+
138
+ ```bash
139
+ ccqa trace-setup login
140
+ ```
141
+
142
+ ### 3. Generate and validate the setup
143
+
144
+ ```bash
145
+ ccqa generate-setup login
146
+ ```
147
+
148
+ This generates `test.dummy.spec.ts` with dummy values, runs vitest to validate, and applies auto-fix. On success, it reverse-replaces dummy values with placeholders and saves `test.spec.ts`.
149
+
150
+ If auto-fix fails, edit `test.dummy.spec.ts` manually and re-run:
151
+
152
+ ```bash
153
+ ccqa generate-setup login --from-dummy
154
+ ```
155
+
156
+ ### 4. Reference from test specs
157
+
158
+ ```markdown
159
+ ---
160
+ title: Create a task
161
+ baseUrl: http://localhost:3000
162
+ setups:
163
+ - name: login
164
+ params:
165
+ loginUrl: "http://localhost:3000/login"
166
+ email: "admin@example.com"
167
+ password: "AdminPass123"
168
+ ---
169
+
170
+ ## Steps
171
+ ### Step 1: Create a new task
172
+ ...
173
+ ```
174
+
175
+ When you run `ccqa trace` or `ccqa generate`, the setup's test body is loaded, placeholders are replaced with `params` values, and it runs before your test steps — sharing the same browser session.
176
+
177
+ ## What gets generated
178
+
179
+ `ab()` is a thin wrapper around [agent-browser](https://github.com/vercel-labs/agent-browser) — a headless browser CLI. Each call spawns `agent-browser <command>` as a subprocess and throws if it exits non-zero. No browser driver setup, no async/await, no `.waitFor()`.
180
+
181
+ ```typescript
182
+ // .ccqa/features/tasks/test-cases/create-and-complete/test.spec.ts
183
+ import { test } from "vitest";
184
+ import { ab, abWait, abAssertUrl, abAssertTextVisible, abAssertEnabled } from "/path/to/test-helpers.ts";
185
+
186
+ process.env.AGENT_BROWSER_SESSION = `ccqa-run-${Date.now()}`;
187
+
188
+ test("setup: login", () => {
189
+ ab("cookies", "clear");
190
+ ab("open", "http://localhost:3000/login");
191
+ ab("fill", "[placeholder='Email']", "admin@example.com");
192
+ ab("fill", "[type='password']", "AdminPass123");
193
+ ab("press", "Enter");
194
+ }, 3 * 60 * 1000);
195
+
196
+ test("Create a task", () => {
197
+ ab("open", "http://localhost:3000");
198
+
199
+ // Create a new task
200
+ ab("click", "[aria-label='New Task']");
201
+ ab("fill", "[placeholder='Task title']", "Fix login bug");
202
+ ab("select", "[aria-label='Priority']", "High");
203
+ ab("click", "[aria-label='Save']");
204
+ abAssertTextVisible("Fix login bug");
205
+ abAssertTextVisible("Open");
206
+ }, 5 * 60 * 1000);
207
+ ```
208
+
209
+ Setup and test share the same `AGENT_BROWSER_SESSION` — login state carries over. Each run starts with `cookies clear` to ensure a clean session.
210
+
211
+ ## Assertions
212
+
213
+ During `trace`, Claude verifies each step with at least two independent signals and emits structured assertions. These become typed helper calls in the generated script:
214
+
215
+ | Assert | What it checks |
216
+ |--------|---------------|
217
+ | `abAssertTextVisible(text)` | Text appears on page (waits up to 30s) |
218
+ | `abAssertUrl(pattern)` | Current URL contains pattern |
219
+ | `abAssertEnabled(selector)` | Button/input is enabled |
220
+ | `abAssertDisabled(selector)` | Button/input is disabled |
221
+ | `abAssertVisible(selector)` | Element is visible |
222
+ | `abAssertNotVisible(selector)` | Element is hidden |
223
+ | `abAssertChecked(selector)` | Checkbox is checked |
224
+ | `abAssertUnchecked(selector)` | Checkbox is unchecked |
225
+
226
+ Assertions are stability-aware: Claude skips timestamps, session IDs, and exact counts that vary between runs.
227
+
228
+ ## Auto-fix
229
+
230
+ If the generated script fails (timing issues, page not ready), `generate` uses an LLM to analyze the failure log and insert `sleep` at the right positions. Control how many attempts with `--max-retries`:
231
+
232
+ ```bash
233
+ ccqa generate tasks/create-and-complete --max-retries 5
234
+ ```
235
+
236
+ ## Commands
237
+
238
+ ```
239
+ ccqa trace <feature/spec> Record browser actions for a test spec
240
+ ccqa generate <feature/spec> Generate test script from recorded actions
241
+ ccqa run [feature/spec] Execute generated test scripts
242
+
243
+ ccqa trace-setup <name> Record browser actions for a setup spec
244
+ ccqa generate-setup <name> Generate and validate setup test script
245
+ --from-dummy Resume from manually edited test.dummy.spec.ts
246
+ ```
247
+
248
+ ## File structure
249
+
250
+ ```
251
+ .ccqa/
252
+ setups/
253
+ login/
254
+ setup-spec.md # Setup definition with placeholders
255
+ test.spec.ts # Generated setup script (with {{placeholders}})
256
+ features/
257
+ tasks/
258
+ test-cases/
259
+ create-and-complete/
260
+ test-spec.md # Test definition (references setups)
261
+ actions.json # Recorded actions from trace
262
+ test.spec.ts # Generated test script
263
+ ```
264
+
265
+ ## Why not write Playwright tests by hand?
266
+
267
+ | | ccqa | Hand-written Playwright |
268
+ |---|---|---|
269
+ | Write selectors | Claude picks them from ARIA snapshots | You inspect the DOM |
270
+ | Handle timing | Recorded wait commands, auto-fix sleep | `waitFor`, `expect().toBeVisible()` |
271
+ | Assertions | Auto-generated from verified signals | Written manually |
272
+ | Login / setup | Shared setup specs with placeholders | Custom fixtures per project |
273
+ | Update after UI change | Re-run `trace` | Find and update every affected locator |
274
+ | Runs in CI | Yes (deterministic replay, no LLM) | Yes |
275
+
276
+ ## License
277
+
278
+ MIT
package/bin/ccqa.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/cli/index.ts";
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "ccqa",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Browser test recorder powered by Claude Code and agent-browser",
6
+ "bin": {
7
+ "ccqa": "./bin/ccqa.ts"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/"
12
+ ],
13
+ "dependencies": {
14
+ "@anthropic-ai/claude-agent-sdk": "^0.2.87",
15
+ "@anthropic-ai/claude-code": "^2.1.87",
16
+ "commander": "^14.0.3",
17
+ "gray-matter": "^4.0.3",
18
+ "zod": "^4.3.6"
19
+ },
20
+ "peerDependencies": {
21
+ "vitest": ">=2.0.0",
22
+ "agent-browser": "*"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "agent-browser": {
26
+ "optional": false
27
+ },
28
+ "vitest": {
29
+ "optional": false
30
+ }
31
+ },
32
+ "devDependencies": {
33
+ "@types/bun": "latest"
34
+ },
35
+ "trustedDependencies": [
36
+ "agent-browser"
37
+ ]
38
+ }
@@ -0,0 +1,167 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { extractAbActionFromBashCommand, isBlockedAbSubcommand, hasRefSelector, shellTokenize } from "./invoke.ts";
3
+
4
+ describe("extractAbActionFromBashCommand", () => {
5
+ test("returns null for non-agent-browser commands", () => {
6
+ expect(extractAbActionFromBashCommand("ls -la")).toBeNull();
7
+ expect(extractAbActionFromBashCommand("echo hello")).toBeNull();
8
+ });
9
+
10
+ test("parses cookies clear", () => {
11
+ expect(
12
+ extractAbActionFromBashCommand("agent-browser --session s1 cookies clear"),
13
+ ).toBe("AB_ACTION|cookies_clear");
14
+ });
15
+
16
+ test("parses open", () => {
17
+ expect(
18
+ extractAbActionFromBashCommand("agent-browser --session s1 open https://example.com"),
19
+ ).toBe("AB_ACTION|open|https://example.com");
20
+ });
21
+
22
+ test("parses click with quoted selector", () => {
23
+ expect(
24
+ extractAbActionFromBashCommand(`agent-browser --session s1 click "[aria-label='Submit']" "Submit"`),
25
+ ).toBe("AB_ACTION|click|[aria-label='Submit']|Submit");
26
+ });
27
+
28
+ test("parses fill", () => {
29
+ expect(
30
+ extractAbActionFromBashCommand(`agent-browser --session s1 fill "[placeholder='Email']" "test@example.com" "Email"`),
31
+ ).toBe("AB_ACTION|fill|[placeholder='Email']|test@example.com|Email");
32
+ });
33
+
34
+ test("parses snapshot as null", () => {
35
+ expect(
36
+ extractAbActionFromBashCommand("agent-browser --session s1 snapshot"),
37
+ ).toBeNull();
38
+ });
39
+
40
+ test("parses press", () => {
41
+ expect(
42
+ extractAbActionFromBashCommand("agent-browser --session s1 press Enter"),
43
+ ).toBe("AB_ACTION|press|Enter");
44
+ });
45
+
46
+ test("parses wait", () => {
47
+ expect(
48
+ extractAbActionFromBashCommand(`agent-browser --session s1 wait --text "Done"`),
49
+ ).toBe("AB_ACTION|wait|--text|Done");
50
+ });
51
+ });
52
+
53
+ describe("isBlockedAbSubcommand", () => {
54
+ test("blocks eval with session", () => {
55
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 eval "document.querySelector('.btn').click()"`)).toBe(true);
56
+ });
57
+
58
+ test("blocks js with session", () => {
59
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 js "window.scrollTo(0, 100)"`)).toBe(true);
60
+ });
61
+
62
+ test("blocks eval without session flag", () => {
63
+ expect(isBlockedAbSubcommand(`agent-browser eval "document.click()"`)).toBe(true);
64
+ });
65
+
66
+ test("does not block click", () => {
67
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 click "[aria-label='Submit']"`)).toBe(false);
68
+ });
69
+
70
+ test("does not block snapshot", () => {
71
+ expect(isBlockedAbSubcommand("agent-browser --session s1 snapshot")).toBe(false);
72
+ });
73
+
74
+ test("does not block fill", () => {
75
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 fill "[placeholder='Email']" "test@example.com"`)).toBe(false);
76
+ });
77
+
78
+ test("blocks find command", () => {
79
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 find text "Select category" click`)).toBe(true);
80
+ });
81
+
82
+ test("blocks label command", () => {
83
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 label "Settings" click`)).toBe(true);
84
+ });
85
+
86
+ test("blocks textbox command", () => {
87
+ expect(isBlockedAbSubcommand(`agent-browser --session s1 textbox "Search"`)).toBe(true);
88
+ });
89
+
90
+ test("does not block non-agent-browser commands", () => {
91
+ expect(isBlockedAbSubcommand("ls -la")).toBe(false);
92
+ expect(isBlockedAbSubcommand("echo hello")).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe("hasRefSelector", () => {
97
+ test("detects @ref in click", () => {
98
+ expect(hasRefSelector(`agent-browser --session s1 click "@e14"`)).toBe(true);
99
+ });
100
+
101
+ test("detects @ref in check", () => {
102
+ expect(hasRefSelector(`agent-browser --session s1 check @e7`)).toBe(true);
103
+ });
104
+
105
+ test("detects @ref in fill", () => {
106
+ expect(hasRefSelector(`agent-browser --session s1 fill "@e6" "value"`)).toBe(true);
107
+ });
108
+
109
+ test("does not flag aria-label selector", () => {
110
+ expect(hasRefSelector(`agent-browser --session s1 click "[aria-label='Submit']"`)).toBe(false);
111
+ });
112
+
113
+ test("does not flag text= selector", () => {
114
+ expect(hasRefSelector(`agent-browser --session s1 click "text=Select category"`)).toBe(false);
115
+ });
116
+
117
+ test("does not flag non-agent-browser commands", () => {
118
+ expect(hasRefSelector("ls -la")).toBe(false);
119
+ });
120
+
121
+ test("does not flag snapshot (no args)", () => {
122
+ expect(hasRefSelector("agent-browser --session s1 snapshot")).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe("shellTokenize", () => {
127
+ test("splits simple tokens", () => {
128
+ expect(shellTokenize("click foo bar")).toEqual(["click", "foo", "bar"]);
129
+ });
130
+
131
+ test("preserves spaces inside double quotes", () => {
132
+ expect(shellTokenize(`click "[role='dialog'] button:last-child"`)).toEqual([
133
+ "click",
134
+ "[role='dialog'] button:last-child",
135
+ ]);
136
+ });
137
+
138
+ test("preserves spaces inside single quotes", () => {
139
+ expect(shellTokenize("fill 'text=hello world' value")).toEqual([
140
+ "fill",
141
+ "text=hello world",
142
+ "value",
143
+ ]);
144
+ });
145
+
146
+ test("handles empty string", () => {
147
+ expect(shellTokenize("")).toEqual([]);
148
+ });
149
+
150
+ test("handles multiple spaces", () => {
151
+ expect(shellTokenize("click foo")).toEqual(["click", "foo"]);
152
+ });
153
+ });
154
+
155
+ describe("extractAbActionFromBashCommand with compound selectors", () => {
156
+ test("parses click with compound selector containing spaces", () => {
157
+ expect(
158
+ extractAbActionFromBashCommand(`agent-browser --session s1 click "[role='dialog'] button:last-child"`),
159
+ ).toBe("AB_ACTION|click|[role='dialog'] button:last-child|");
160
+ });
161
+
162
+ test("parses fill with placeholder containing spaces", () => {
163
+ expect(
164
+ extractAbActionFromBashCommand(`agent-browser --session s1 fill ".modal-footer button" "OK"`),
165
+ ).toBe("AB_ACTION|fill|.modal-footer button|OK|");
166
+ });
167
+ });