ghostrun-cli 1.0.0 → 1.0.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/CHANGELOG.md +64 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +90 -0
- package/README.md +259 -418
- package/REFERENCE.md +165 -0
- package/ghostrun.js +65 -28
- package/package.json +12 -4
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# GhostRun — Flow Actions Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for all actions you can use in recorded or hand-crafted `.flow.json` files.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Browser Actions
|
|
8
|
+
|
|
9
|
+
### Navigation
|
|
10
|
+
|
|
11
|
+
| Action | Fields | Description |
|
|
12
|
+
|--------|--------|-------------|
|
|
13
|
+
| `navigate` | `url` | Go to URL |
|
|
14
|
+
| `reload` | — | Reload the current page |
|
|
15
|
+
| `back` | — | Browser back |
|
|
16
|
+
| `forward` | — | Browser forward |
|
|
17
|
+
|
|
18
|
+
### Interaction
|
|
19
|
+
|
|
20
|
+
| Action | Fields | Description |
|
|
21
|
+
|--------|--------|-------------|
|
|
22
|
+
| `click` | `selector` | Left-click an element |
|
|
23
|
+
| `dblclick` | `selector` | Double-click an element |
|
|
24
|
+
| `fill` | `selector`, `value` | Clear field and type value |
|
|
25
|
+
| `type` | `selector`, `value`, `delay?` | Type with configurable key delay (ms) |
|
|
26
|
+
| `clear` | `selector` | Clear a field |
|
|
27
|
+
| `select` | `selector`, `value` | Select a dropdown option by value |
|
|
28
|
+
| `check` | `selector`, `value: "true"\|"false"` | Check/uncheck a checkbox |
|
|
29
|
+
| `focus` | `selector` | Focus an element |
|
|
30
|
+
| `hover` | `selector` | Mouse hover |
|
|
31
|
+
| `drag` | `selector`, `targetSelector` | Drag one element to another |
|
|
32
|
+
| `keyboard` | `key`, `selector?` | Press a key (e.g. `Enter`, `Tab`, `Control+a`) |
|
|
33
|
+
| `upload` | `selector`, `value` | Set file input (comma-separated paths) |
|
|
34
|
+
|
|
35
|
+
### Waiting
|
|
36
|
+
|
|
37
|
+
| Action | Fields | Description |
|
|
38
|
+
|--------|--------|-------------|
|
|
39
|
+
| `wait` | `selector` | Wait for element to appear |
|
|
40
|
+
| `wait:text` | `selector`, `value` | Wait until element contains text |
|
|
41
|
+
| `wait:url` | `value` | Wait for URL to match pattern |
|
|
42
|
+
| `wait:ms` | `value` | Wait for N milliseconds |
|
|
43
|
+
|
|
44
|
+
### Scrolling
|
|
45
|
+
|
|
46
|
+
| Action | Fields | Description |
|
|
47
|
+
|--------|--------|-------------|
|
|
48
|
+
| `scroll` | `selector?` | Scroll to element (or page) |
|
|
49
|
+
| `scroll:element` | `selector` | Scroll element into view |
|
|
50
|
+
| `scroll:bottom` | — | Scroll to bottom of page |
|
|
51
|
+
| `scroll:load` | `value?` | Scroll to bottom, wait for load (repeat N times) |
|
|
52
|
+
| `next:page` | `selector?` | Click next page link and wait |
|
|
53
|
+
|
|
54
|
+
### Assertions
|
|
55
|
+
|
|
56
|
+
| Action | Fields | Description |
|
|
57
|
+
|--------|--------|-------------|
|
|
58
|
+
| `assert:visible` | `selector` | Assert element is visible |
|
|
59
|
+
| `assert:hidden` | `selector` | Assert element is not visible |
|
|
60
|
+
| `assert:text` | `selector`, `value` | Assert element contains text |
|
|
61
|
+
| `assert:not-text` | `selector`, `value` | Assert element does NOT contain text |
|
|
62
|
+
| `assert:value` | `selector`, `value` | Assert input value |
|
|
63
|
+
| `assert:count` | `selector`, `value` | Assert number of matching elements |
|
|
64
|
+
| `assert:attr` | `selector`, `value: "attr=expected"` | Assert element attribute |
|
|
65
|
+
|
|
66
|
+
### Data Extraction
|
|
67
|
+
|
|
68
|
+
| Action | Fields | Description |
|
|
69
|
+
|--------|--------|-------------|
|
|
70
|
+
| `extract` | `selector`, `variable: "variableName"` | Extract element text → variable |
|
|
71
|
+
| `screenshot` | — | Capture screenshot at this step |
|
|
72
|
+
|
|
73
|
+
### Browser State
|
|
74
|
+
|
|
75
|
+
| Action | Fields | Description |
|
|
76
|
+
|--------|--------|-------------|
|
|
77
|
+
| `cookie:set` | `value: "name=value; domain=..."` | Set a cookie |
|
|
78
|
+
| `cookie:clear` | — | Clear all cookies |
|
|
79
|
+
| `storage:set` | `selector: "key"`, `value: "val"` | Set localStorage item |
|
|
80
|
+
| `eval` | `value` | Execute JavaScript on the page |
|
|
81
|
+
| `iframe:enter` | `selector` | Enter an iframe context |
|
|
82
|
+
| `iframe:exit` | — | Exit iframe context, return to main frame |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## API Actions
|
|
87
|
+
|
|
88
|
+
### HTTP Requests
|
|
89
|
+
|
|
90
|
+
| Action | Fields | Description |
|
|
91
|
+
|--------|--------|-------------|
|
|
92
|
+
| `http:request` | `method`, `url`, `headers?`, `body?`, `auth?`, `extract?` | Make an HTTP request. `auth` supports `{ type: "bearer", token: "{{var}}" }`. `extract` is a map of `variableName → $.jsonPath`. |
|
|
93
|
+
|
|
94
|
+
### Assertions
|
|
95
|
+
|
|
96
|
+
| Action | Fields | Description |
|
|
97
|
+
|--------|--------|-------------|
|
|
98
|
+
| `assert:response` | `assert: "status"`, `expected` | Assert HTTP status code |
|
|
99
|
+
| `assert:response` | `assert: "json:path"`, `path`, `expected` | Assert JSONPath value equals expected |
|
|
100
|
+
| `assert:response` | `assert: "json:exists"`, `path` | Assert JSONPath exists in response |
|
|
101
|
+
| `assert:response` | `assert: "header"`, `header`, `expected` | Assert response header value |
|
|
102
|
+
| `assert:response` | `assert: "body:contains"`, `expected` | Assert raw body contains string |
|
|
103
|
+
| `assert:response` | `assert: "time"`, `expected` | Assert response time < expected ms |
|
|
104
|
+
|
|
105
|
+
### Variables & Flow Control
|
|
106
|
+
|
|
107
|
+
| Action | Fields | Description |
|
|
108
|
+
|--------|--------|-------------|
|
|
109
|
+
| `set:variable` | `variable`, `value` | Set a named variable (supports `{{interpolation}}`) |
|
|
110
|
+
| `extract:json` | `variable`, `path` | Extract a value from the last response body via JSONPath |
|
|
111
|
+
| `env:switch` | `value` | Switch active environment mid-flow |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Variables
|
|
116
|
+
|
|
117
|
+
Use `{{variableName}}` in any `value`, `url`, `selector`, or `body` field:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{ "action": "fill", "selector": "#email", "value": "{{userEmail}}" }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Pass at runtime:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ghostrun run <id> --var userEmail=user@example.com
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Values extracted with `extract:` and `extract:json` are automatically available as variables in subsequent steps.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Limitations
|
|
134
|
+
|
|
135
|
+
| Interaction | Status | Notes |
|
|
136
|
+
|------------|--------|-------|
|
|
137
|
+
| Canvas drawing | ❌ | `<canvas>` elements — no visual capture |
|
|
138
|
+
| WebGL / Three.js | ❌ | GPU-rendered content |
|
|
139
|
+
| Browser native dialogs | ⚠️ Partial | `alert()`/`confirm()`/`prompt()` auto-dismissed |
|
|
140
|
+
| File download verification | ⚠️ Partial | Download triggers but content is not validated |
|
|
141
|
+
| WebRTC / media streams | ❌ | Camera, mic, screen capture APIs |
|
|
142
|
+
| Browser extensions | ❌ | Extension UI not accessible via Playwright |
|
|
143
|
+
| Shadow DOM (closed mode) | ⚠️ Limited | Open shadow DOM works; closed mode needs `eval` workaround |
|
|
144
|
+
| Multi-tab / popup flows | ⚠️ Partial | New tabs opened by click are not automatically followed |
|
|
145
|
+
| OS-level dialogs | ❌ | Native file picker, print dialog, OS auth prompts |
|
|
146
|
+
| CAPTCHAs | ❌ | By design — no circumvention |
|
|
147
|
+
| Biometric auth | ❌ | Touch ID, Face ID, WebAuthn |
|
|
148
|
+
| Browser gestures (pinch/zoom) | ❌ | Mobile multi-touch gestures |
|
|
149
|
+
| Hover-only menus (CSS `:hover`) | ✅ | Use `hover` action before clicking submenu items |
|
|
150
|
+
| Right-click context menus | ⚠️ Limited | Browser context menus inaccessible; app-level menus often work |
|
|
151
|
+
| Drag and drop | ✅ | Use `drag` with `selector` + `targetSelector` |
|
|
152
|
+
| Infinite scroll / lazy load | ✅ | Use `scroll:load` with repeat count |
|
|
153
|
+
|
|
154
|
+
**Workarounds:**
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
// Run JS directly
|
|
158
|
+
{ "action": "eval", "value": "document.querySelector('#btn').click()" }
|
|
159
|
+
|
|
160
|
+
// Shadow DOM
|
|
161
|
+
{ "action": "eval", "value": "document.querySelector('my-el').shadowRoot.querySelector('button').click()" }
|
|
162
|
+
|
|
163
|
+
// Timing-sensitive steps
|
|
164
|
+
{ "action": "wait:ms", "value": "500" }
|
|
165
|
+
```
|
package/ghostrun.js
CHANGED
|
@@ -3644,7 +3644,7 @@ var import_uuid = require("uuid");
|
|
|
3644
3644
|
var HOME_DIR = process.env.HOME || process.env.USERPROFILE || ".";
|
|
3645
3645
|
var DATA_PATH = path.join(HOME_DIR, ".ghostrun");
|
|
3646
3646
|
var DB_PATH = path.join(DATA_PATH, "data", "ghostrun.db");
|
|
3647
|
-
var DatabaseManager = class {
|
|
3647
|
+
var DatabaseManager = class _DatabaseManager {
|
|
3648
3648
|
db;
|
|
3649
3649
|
constructor() {
|
|
3650
3650
|
fs.mkdirSync(path.join(DATA_PATH, "data"), { recursive: true });
|
|
@@ -3982,35 +3982,72 @@ var DatabaseManager = class {
|
|
|
3982
3982
|
};
|
|
3983
3983
|
}
|
|
3984
3984
|
// ---- DB migrations ----
|
|
3985
|
+
//
|
|
3986
|
+
// Uses SQLite's built-in PRAGMA user_version as a schema version counter.
|
|
3987
|
+
// Each migration runs exactly once: we read the current version, apply every
|
|
3988
|
+
// migration whose index is >= that version (in order), then write the new version.
|
|
3989
|
+
//
|
|
3990
|
+
// HOW TO ADD A NEW MIGRATION:
|
|
3991
|
+
// 1. Append a new string to the MIGRATIONS array below.
|
|
3992
|
+
// 2. That's it. The runner handles the rest.
|
|
3993
|
+
//
|
|
3994
|
+
// Never edit or reorder existing entries — just append.
|
|
3995
|
+
static MIGRATIONS = [
|
|
3996
|
+
// v1: add diff_percent to steps
|
|
3997
|
+
"ALTER TABLE steps ADD COLUMN diff_percent REAL",
|
|
3998
|
+
// v2: add created_by to flows
|
|
3999
|
+
"ALTER TABLE flows ADD COLUMN created_by TEXT NOT NULL DEFAULT 'human'",
|
|
4000
|
+
// v3: add verified flag to flows
|
|
4001
|
+
"ALTER TABLE flows ADD COLUMN verified INTEGER NOT NULL DEFAULT 0",
|
|
4002
|
+
// v4: add captured_at to run_data
|
|
4003
|
+
"ALTER TABLE run_data ADD COLUMN captured_at TEXT DEFAULT (datetime('now'))",
|
|
4004
|
+
// v5: environments table
|
|
4005
|
+
`CREATE TABLE IF NOT EXISTS environments (
|
|
4006
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, base_url TEXT,
|
|
4007
|
+
variables TEXT NOT NULL DEFAULT '{}', is_active INTEGER NOT NULL DEFAULT 0,
|
|
4008
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4009
|
+
)`,
|
|
4010
|
+
// v6: api_responses table
|
|
4011
|
+
`CREATE TABLE IF NOT EXISTS api_responses (
|
|
4012
|
+
id TEXT PRIMARY KEY, run_id TEXT NOT NULL, step_number INTEGER NOT NULL,
|
|
4013
|
+
method TEXT NOT NULL, url TEXT NOT NULL, status_code INTEGER,
|
|
4014
|
+
response_time_ms INTEGER, response_headers TEXT, response_body TEXT,
|
|
4015
|
+
error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4016
|
+
)`,
|
|
4017
|
+
// v7: perf_runs table
|
|
4018
|
+
`CREATE TABLE IF NOT EXISTS perf_runs (
|
|
4019
|
+
id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, flow_name TEXT NOT NULL,
|
|
4020
|
+
config TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running',
|
|
4021
|
+
total_requests INTEGER, success_requests INTEGER, failed_requests INTEGER,
|
|
4022
|
+
avg_rps REAL, p50_ms INTEGER, p95_ms INTEGER, p99_ms INTEGER,
|
|
4023
|
+
min_ms INTEGER, max_ms INTEGER, per_step_stats TEXT,
|
|
4024
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')), completed_at TEXT
|
|
4025
|
+
)`
|
|
4026
|
+
// --- add new migrations below this line ---
|
|
4027
|
+
];
|
|
4028
|
+
// Number of migrations that existed before we introduced versioning.
|
|
4029
|
+
// Existing databases have these applied already (via old try/catch approach)
|
|
4030
|
+
// but their user_version is 0. We detect this and fast-forward rather than
|
|
4031
|
+
// re-running them (which would throw "duplicate column" errors).
|
|
4032
|
+
static LEGACY_MIGRATION_COUNT = 7;
|
|
4033
|
+
columnExists(table, column) {
|
|
4034
|
+
const cols = this.db.pragma(`table_info(${table})`);
|
|
4035
|
+
return cols.some((c) => c.name === column);
|
|
4036
|
+
}
|
|
3985
4037
|
runMigrations() {
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
try {
|
|
3991
|
-
this.db.exec("ALTER TABLE flows ADD COLUMN created_by TEXT NOT NULL DEFAULT 'human'");
|
|
3992
|
-
} catch {
|
|
3993
|
-
}
|
|
3994
|
-
try {
|
|
3995
|
-
this.db.exec("ALTER TABLE flows ADD COLUMN verified INTEGER NOT NULL DEFAULT 0");
|
|
3996
|
-
} catch {
|
|
3997
|
-
}
|
|
3998
|
-
try {
|
|
3999
|
-
this.db.exec("ALTER TABLE run_data ADD COLUMN captured_at TEXT DEFAULT (datetime('now'))");
|
|
4000
|
-
} catch {
|
|
4001
|
-
}
|
|
4002
|
-
try {
|
|
4003
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS environments (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, base_url TEXT, variables TEXT NOT NULL DEFAULT '{}', is_active INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
|
|
4004
|
-
} catch {
|
|
4005
|
-
}
|
|
4006
|
-
try {
|
|
4007
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS api_responses (id TEXT PRIMARY KEY, run_id TEXT NOT NULL, step_number INTEGER NOT NULL, method TEXT NOT NULL, url TEXT NOT NULL, status_code INTEGER, response_time_ms INTEGER, response_headers TEXT, response_body TEXT, error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
|
|
4008
|
-
} catch {
|
|
4009
|
-
}
|
|
4010
|
-
try {
|
|
4011
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS perf_runs (id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, flow_name TEXT NOT NULL, config TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', total_requests INTEGER, success_requests INTEGER, failed_requests INTEGER, avg_rps REAL, p50_ms INTEGER, p95_ms INTEGER, p99_ms INTEGER, min_ms INTEGER, max_ms INTEGER, per_step_stats TEXT, started_at TEXT NOT NULL DEFAULT (datetime('now')), completed_at TEXT)`);
|
|
4012
|
-
} catch {
|
|
4038
|
+
let currentVersion = this.db.pragma("user_version", { simple: true }) ?? 0;
|
|
4039
|
+
if (currentVersion === 0 && this.columnExists("steps", "diff_percent")) {
|
|
4040
|
+
currentVersion = _DatabaseManager.LEGACY_MIGRATION_COUNT;
|
|
4041
|
+
this.db.pragma(`user_version = ${currentVersion}`);
|
|
4013
4042
|
}
|
|
4043
|
+
if (currentVersion >= _DatabaseManager.MIGRATIONS.length) return;
|
|
4044
|
+
const applyAll = this.db.transaction(() => {
|
|
4045
|
+
for (let i = currentVersion; i < _DatabaseManager.MIGRATIONS.length; i++) {
|
|
4046
|
+
this.db.exec(_DatabaseManager.MIGRATIONS[i]);
|
|
4047
|
+
}
|
|
4048
|
+
this.db.pragma(`user_version = ${_DatabaseManager.MIGRATIONS.length}`);
|
|
4049
|
+
});
|
|
4050
|
+
applyAll();
|
|
4014
4051
|
}
|
|
4015
4052
|
// ---- Suites ----
|
|
4016
4053
|
createSuite(data) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghostrun-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Browser automation + API testing + load testing in one tool. Record flows, test REST APIs, run VU-based load tests, export to k6. Entirely local.",
|
|
5
5
|
"main": "ghostrun.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
"ghostrun-mcp": "mcp-server.js"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
|
-
"browser-automation",
|
|
12
|
-
"
|
|
11
|
+
"browser-automation",
|
|
12
|
+
"api-testing",
|
|
13
|
+
"load-testing",
|
|
14
|
+
"playwright",
|
|
15
|
+
"test-runner",
|
|
16
|
+
"cli",
|
|
17
|
+
"k6",
|
|
18
|
+
"local-first",
|
|
19
|
+
"mcp",
|
|
20
|
+
"e2e-testing"
|
|
13
21
|
],
|
|
14
22
|
"homepage": "https://ghostrun.builtbysharan.com",
|
|
15
23
|
"repository": {
|
|
16
24
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/
|
|
25
|
+
"url": "https://github.com/TechBuiltBySharan/ghostrun"
|
|
18
26
|
},
|
|
19
27
|
"license": "MIT",
|
|
20
28
|
"scripts": {
|