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 +21 -0
- package/README.md +278 -0
- package/bin/ccqa.ts +2 -0
- package/package.json +38 -0
- package/src/claude/invoke.test.ts +167 -0
- package/src/claude/invoke.ts +238 -0
- package/src/cli/generate-setup.ts +215 -0
- package/src/cli/generate.ts +224 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/logger.ts +45 -0
- package/src/cli/run.ts +65 -0
- package/src/cli/trace-setup.ts +124 -0
- package/src/cli/trace.test.ts +233 -0
- package/src/cli/trace.ts +244 -0
- package/src/codegen/actions-to-script.ts +188 -0
- package/src/prompts/codegen.ts +73 -0
- package/src/prompts/trace.ts +278 -0
- package/src/runtime/test-helpers.ts +77 -0
- package/src/spec/parser.test.ts +135 -0
- package/src/spec/parser.ts +96 -0
- package/src/store/index.test.ts +107 -0
- package/src/store/index.ts +193 -0
- package/src/types.test.ts +96 -0
- package/src/types.ts +91 -0
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
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
|
+
});
|