cursorflow 2.1.6__tar.gz → 2.2.0__tar.gz
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.
- {cursorflow-2.1.6 → cursorflow-2.2.0}/PKG-INFO +66 -14
- {cursorflow-2.1.6 → cursorflow-2.2.0}/README.md +65 -13
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/__init__.py +1 -1
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/cli.py +370 -11
- cursorflow-2.2.0/cursorflow/core/action_validator.py +199 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/browser_controller.py +300 -5
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/browser_engine.py +13 -0
- cursorflow-2.2.0/cursorflow/core/config_validator.py +216 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/cursorflow.py +68 -32
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/install_cursorflow_rules.py +4 -4
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/log_sources/local_file.py +20 -1
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/log_sources/ssh_remote.py +19 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/rules/cursorflow-installation.mdc +1 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/rules/cursorflow-usage.mdc +7 -1
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow.egg-info/SOURCES.txt +2 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/pyproject.toml +1 -1
- {cursorflow-2.1.6 → cursorflow-2.2.0}/FIRST_TIME_SETUP.md +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/LICENSE +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/MANIFEST.in +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/POST_INSTALL_MESSAGE.txt +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/auto_init.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/auto_updater.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/agent.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/auth_handler.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/css_iterator.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/cursor_integration.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/error_context_collector.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/error_correlator.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/event_correlator.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/file_change_monitor.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/hmr_detector.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/log_collector.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/log_monitor.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/mockup_comparator.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/persistent_session.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/report_generator.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/core/trace_manager.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/post_install.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/rules/__init__.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/cursorflow/updater.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/docs/USER_MANUAL.md +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/comprehensive_screenshot_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/enhanced_screenshot_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/mockup_comparison_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/opensas_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/react_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/responsive_testing_example.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/examples/v2_comprehensive_demo.py +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/setup.cfg +0 -0
- {cursorflow-2.1.6 → cursorflow-2.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cursorflow
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.2.0
|
4
4
|
Summary: 🔥 Complete page intelligence for AI-driven development with Hot Reload Intelligence - captures DOM, network, console, performance, HMR events, and comprehensive page analysis
|
5
5
|
Author-email: GeekWarrior Development <rbush@cooltheory.com>
|
6
6
|
License-Expression: MIT
|
@@ -82,20 +82,57 @@ When CursorFlow reports `"average_response_time": 416.58ms`, you can tell stakeh
|
|
82
82
|
|
83
83
|
**Documentary vs Movie:** Both are valuable, but if you're trying to understand reality, you watch the documentary. CursorFlow is the documentary of web testing.
|
84
84
|
|
85
|
+
## 🎯 Pass-Through Architecture
|
86
|
+
|
87
|
+
CursorFlow doesn't limit you - it exposes the full power of Playwright:
|
88
|
+
|
89
|
+
**94+ Playwright actions available:**
|
90
|
+
```bash
|
91
|
+
# Any Playwright Page method works
|
92
|
+
cursorflow test --actions '[
|
93
|
+
{"hover": ".menu"},
|
94
|
+
{"dblclick": ".editable"},
|
95
|
+
{"press": "Enter"},
|
96
|
+
{"drag_and_drop": {"source": ".item", "target": ".zone"}},
|
97
|
+
{"check": "#checkbox"},
|
98
|
+
{"evaluate": "window.scrollTo(0, 500)"}
|
99
|
+
]'
|
100
|
+
```
|
101
|
+
|
102
|
+
**Full configuration pass-through:**
|
103
|
+
```json
|
104
|
+
{
|
105
|
+
"browser_config": {
|
106
|
+
"browser_launch_options": {"devtools": true, "channel": "chrome"}
|
107
|
+
},
|
108
|
+
"context_options": {
|
109
|
+
"color_scheme": "dark",
|
110
|
+
"geolocation": {"latitude": 40.7128, "longitude": -74.0060},
|
111
|
+
"timezone_id": "America/Los_Angeles"
|
112
|
+
}
|
113
|
+
}
|
114
|
+
```
|
115
|
+
|
116
|
+
**Forward-compatible:** New Playwright features work immediately without CursorFlow updates.
|
117
|
+
|
118
|
+
**See:** [Playwright API Documentation](https://playwright.dev/python/docs/api/class-page)
|
119
|
+
|
120
|
+
---
|
121
|
+
|
85
122
|
## 🚀 Complete Page Intelligence
|
86
123
|
|
87
|
-
Every
|
124
|
+
Every test captures everything needed for debugging:
|
88
125
|
|
89
126
|
### **📊 Comprehensive Data Collection**
|
90
|
-
- **DOM**: All elements with 7 selector strategies
|
91
|
-
- **Network**:
|
92
|
-
- **Console**: All logs, errors,
|
93
|
-
- **
|
94
|
-
- **
|
95
|
-
- **
|
96
|
-
- **
|
97
|
-
- **
|
98
|
-
- **
|
127
|
+
- **DOM**: All elements with 7 selector strategies + event handlers
|
128
|
+
- **Network**: Requests, responses, and complete request/response bodies
|
129
|
+
- **Console**: All logs, errors, warnings - displayed prominently
|
130
|
+
- **JavaScript**: Global functions, variables, specific window objects
|
131
|
+
- **Storage**: localStorage, sessionStorage, cookies (sensitive data masked)
|
132
|
+
- **Forms**: All field values at capture time (passwords masked)
|
133
|
+
- **Performance**: Load times, memory usage, reliability indicators
|
134
|
+
- **Visual**: Screenshots with comprehensive page analysis
|
135
|
+
- **Sessions**: Save/restore browser state for authenticated testing
|
99
136
|
|
100
137
|
### **🔄 Hot Reload Intelligence**
|
101
138
|
- **Framework auto-detection** (Vite, Webpack, Next.js, Parcel, Laravel Mix)
|
@@ -160,11 +197,26 @@ This creates:
|
|
160
197
|
- `.gitignore` entries for CursorFlow artifacts
|
161
198
|
|
162
199
|
### Step 3: Start Testing
|
200
|
+
|
201
|
+
**Simple page capture:**
|
163
202
|
```bash
|
164
|
-
|
165
|
-
|
203
|
+
cursorflow test --base-url http://localhost:3000 --path /dashboard
|
204
|
+
```
|
166
205
|
|
167
|
-
|
206
|
+
**Interactive testing with inline actions:**
|
207
|
+
```bash
|
208
|
+
cursorflow test --base-url http://localhost:3000 \
|
209
|
+
--path /messages \
|
210
|
+
--wait-for ".message-item" \
|
211
|
+
--hover ".message-item:first-child" \
|
212
|
+
--click ".message-item:first-child" \
|
213
|
+
--screenshot "clicked" \
|
214
|
+
--show-console \
|
215
|
+
--open-trace
|
216
|
+
```
|
217
|
+
|
218
|
+
**Custom actions with JSON:**
|
219
|
+
```bash
|
168
220
|
cursorflow test --base-url http://localhost:3000 --actions '[
|
169
221
|
{"navigate": "/login"},
|
170
222
|
{"fill": {"selector": "#email", "value": "test@example.com"}},
|
@@ -37,20 +37,57 @@ When CursorFlow reports `"average_response_time": 416.58ms`, you can tell stakeh
|
|
37
37
|
|
38
38
|
**Documentary vs Movie:** Both are valuable, but if you're trying to understand reality, you watch the documentary. CursorFlow is the documentary of web testing.
|
39
39
|
|
40
|
+
## 🎯 Pass-Through Architecture
|
41
|
+
|
42
|
+
CursorFlow doesn't limit you - it exposes the full power of Playwright:
|
43
|
+
|
44
|
+
**94+ Playwright actions available:**
|
45
|
+
```bash
|
46
|
+
# Any Playwright Page method works
|
47
|
+
cursorflow test --actions '[
|
48
|
+
{"hover": ".menu"},
|
49
|
+
{"dblclick": ".editable"},
|
50
|
+
{"press": "Enter"},
|
51
|
+
{"drag_and_drop": {"source": ".item", "target": ".zone"}},
|
52
|
+
{"check": "#checkbox"},
|
53
|
+
{"evaluate": "window.scrollTo(0, 500)"}
|
54
|
+
]'
|
55
|
+
```
|
56
|
+
|
57
|
+
**Full configuration pass-through:**
|
58
|
+
```json
|
59
|
+
{
|
60
|
+
"browser_config": {
|
61
|
+
"browser_launch_options": {"devtools": true, "channel": "chrome"}
|
62
|
+
},
|
63
|
+
"context_options": {
|
64
|
+
"color_scheme": "dark",
|
65
|
+
"geolocation": {"latitude": 40.7128, "longitude": -74.0060},
|
66
|
+
"timezone_id": "America/Los_Angeles"
|
67
|
+
}
|
68
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
**Forward-compatible:** New Playwright features work immediately without CursorFlow updates.
|
72
|
+
|
73
|
+
**See:** [Playwright API Documentation](https://playwright.dev/python/docs/api/class-page)
|
74
|
+
|
75
|
+
---
|
76
|
+
|
40
77
|
## 🚀 Complete Page Intelligence
|
41
78
|
|
42
|
-
Every
|
79
|
+
Every test captures everything needed for debugging:
|
43
80
|
|
44
81
|
### **📊 Comprehensive Data Collection**
|
45
|
-
- **DOM**: All elements with 7 selector strategies
|
46
|
-
- **Network**:
|
47
|
-
- **Console**: All logs, errors,
|
48
|
-
- **
|
49
|
-
- **
|
50
|
-
- **
|
51
|
-
- **
|
52
|
-
- **
|
53
|
-
- **
|
82
|
+
- **DOM**: All elements with 7 selector strategies + event handlers
|
83
|
+
- **Network**: Requests, responses, and complete request/response bodies
|
84
|
+
- **Console**: All logs, errors, warnings - displayed prominently
|
85
|
+
- **JavaScript**: Global functions, variables, specific window objects
|
86
|
+
- **Storage**: localStorage, sessionStorage, cookies (sensitive data masked)
|
87
|
+
- **Forms**: All field values at capture time (passwords masked)
|
88
|
+
- **Performance**: Load times, memory usage, reliability indicators
|
89
|
+
- **Visual**: Screenshots with comprehensive page analysis
|
90
|
+
- **Sessions**: Save/restore browser state for authenticated testing
|
54
91
|
|
55
92
|
### **🔄 Hot Reload Intelligence**
|
56
93
|
- **Framework auto-detection** (Vite, Webpack, Next.js, Parcel, Laravel Mix)
|
@@ -115,11 +152,26 @@ This creates:
|
|
115
152
|
- `.gitignore` entries for CursorFlow artifacts
|
116
153
|
|
117
154
|
### Step 3: Start Testing
|
155
|
+
|
156
|
+
**Simple page capture:**
|
118
157
|
```bash
|
119
|
-
|
120
|
-
|
158
|
+
cursorflow test --base-url http://localhost:3000 --path /dashboard
|
159
|
+
```
|
121
160
|
|
122
|
-
|
161
|
+
**Interactive testing with inline actions:**
|
162
|
+
```bash
|
163
|
+
cursorflow test --base-url http://localhost:3000 \
|
164
|
+
--path /messages \
|
165
|
+
--wait-for ".message-item" \
|
166
|
+
--hover ".message-item:first-child" \
|
167
|
+
--click ".message-item:first-child" \
|
168
|
+
--screenshot "clicked" \
|
169
|
+
--show-console \
|
170
|
+
--open-trace
|
171
|
+
```
|
172
|
+
|
173
|
+
**Custom actions with JSON:**
|
174
|
+
```bash
|
123
175
|
cursorflow test --base-url http://localhost:3000 --actions '[
|
124
176
|
{"navigate": "/login"},
|
125
177
|
{"fill": {"selector": "#email", "value": "test@example.com"}},
|
@@ -59,7 +59,7 @@ def main(ctx):
|
|
59
59
|
@click.option('--path', '-p',
|
60
60
|
help='Simple path to navigate to (e.g., "/dashboard")')
|
61
61
|
@click.option('--actions', '-a',
|
62
|
-
help='JSON file with test actions, or inline JSON string')
|
62
|
+
help='JSON file with test actions, or inline JSON string. Format: [{"navigate": "/path"}, {"click": ".btn"}]')
|
63
63
|
@click.option('--output', '-o',
|
64
64
|
help='Output file for results (auto-generated if not specified)')
|
65
65
|
@click.option('--logs', '-l',
|
@@ -76,16 +76,96 @@ def main(ctx):
|
|
76
76
|
help='Timeout in seconds for actions')
|
77
77
|
@click.option('--responsive', is_flag=True,
|
78
78
|
help='Test across multiple viewports (mobile, tablet, desktop)')
|
79
|
-
|
80
|
-
|
79
|
+
@click.option('--save-session', '-S',
|
80
|
+
help='Save browser session state with this name for later reuse')
|
81
|
+
@click.option('--use-session', '-U',
|
82
|
+
help='Restore and use a previously saved session')
|
83
|
+
@click.option('--wait-for', '-w',
|
84
|
+
help='Wait for selector to appear before continuing')
|
85
|
+
@click.option('--wait-timeout', type=int, default=30,
|
86
|
+
help='Timeout in seconds for wait operations')
|
87
|
+
@click.option('--wait-for-network-idle', is_flag=True,
|
88
|
+
help='Wait for network to be idle (no requests for 2s)')
|
89
|
+
@click.option('--click', '-c', multiple=True,
|
90
|
+
help='Click element by selector (can specify multiple)')
|
91
|
+
@click.option('--hover', '-h', multiple=True,
|
92
|
+
help='Hover over element by selector')
|
93
|
+
@click.option('--fill', '-f', multiple=True,
|
94
|
+
help='Fill input field. Format: selector=value')
|
95
|
+
@click.option('--screenshot', '-s', multiple=True,
|
96
|
+
help='Capture screenshot with name')
|
97
|
+
@click.option('--open-trace', is_flag=True,
|
98
|
+
help='Automatically open Playwright trace viewer after test')
|
99
|
+
@click.option('--show-console', is_flag=True,
|
100
|
+
help='Show console errors and warnings in output')
|
101
|
+
@click.option('--show-all-console', is_flag=True,
|
102
|
+
help='Show all console messages (including logs)')
|
103
|
+
@click.option('--quiet', '-q', is_flag=True,
|
104
|
+
help='Minimal output, JSON results only')
|
105
|
+
def test(base_url, path, actions, output, logs, config, verbose, headless, timeout, responsive,
|
106
|
+
save_session, use_session, wait_for, wait_timeout, wait_for_network_idle,
|
107
|
+
click, hover, fill, screenshot, open_trace, show_console, show_all_console, quiet):
|
108
|
+
"""
|
109
|
+
Test UI flows and interactions with real-time log monitoring
|
110
|
+
|
111
|
+
\b
|
112
|
+
Action Format Examples:
|
113
|
+
Simple actions:
|
114
|
+
[{"navigate": "/dashboard"}, {"click": ".button"}, {"wait": 2}]
|
115
|
+
|
116
|
+
Actions with configuration:
|
117
|
+
[{"click": {"selector": ".button"}}, {"fill": {"selector": "#email", "value": "test@example.com"}}]
|
118
|
+
|
119
|
+
Save to file and use:
|
120
|
+
cursorflow test --base-url http://localhost:3000 --actions workflow.json
|
121
|
+
|
122
|
+
\b
|
123
|
+
Examples:
|
124
|
+
# Simple path navigation
|
125
|
+
cursorflow test --base-url http://localhost:3000 --path /dashboard
|
126
|
+
|
127
|
+
# With custom actions
|
128
|
+
cursorflow test --base-url http://localhost:3000 --actions '[{"navigate": "/login"}, {"screenshot": "page"}]'
|
129
|
+
|
130
|
+
# From file
|
131
|
+
cursorflow test --base-url http://localhost:3000 --actions my_test.json
|
132
|
+
"""
|
81
133
|
|
82
134
|
if verbose:
|
83
135
|
import logging
|
84
136
|
logging.basicConfig(level=logging.INFO)
|
85
137
|
|
86
|
-
# Parse actions
|
138
|
+
# Parse actions - Phase 3.1: Inline CLI Actions
|
87
139
|
test_actions = []
|
88
|
-
|
140
|
+
|
141
|
+
# Build actions from inline flags (left-to-right execution)
|
142
|
+
if any([click, hover, fill, screenshot]) and not actions:
|
143
|
+
# Inline actions mode
|
144
|
+
if path:
|
145
|
+
test_actions.append({"navigate": path})
|
146
|
+
|
147
|
+
# Wait options
|
148
|
+
if wait_for:
|
149
|
+
test_actions.append({"wait_for_selector": wait_for})
|
150
|
+
if wait_for_network_idle:
|
151
|
+
test_actions.append({"wait_for_load_state": "networkidle"})
|
152
|
+
|
153
|
+
# Inline actions (in order specified)
|
154
|
+
for selector in hover:
|
155
|
+
test_actions.append({"hover": selector})
|
156
|
+
for selector in click:
|
157
|
+
test_actions.append({"click": selector})
|
158
|
+
for fill_spec in fill:
|
159
|
+
if '=' in fill_spec:
|
160
|
+
selector, value = fill_spec.split('=', 1)
|
161
|
+
test_actions.append({"fill": {"selector": selector, "value": value}})
|
162
|
+
for name in screenshot:
|
163
|
+
test_actions.append({"screenshot": name})
|
164
|
+
|
165
|
+
if test_actions:
|
166
|
+
console.print(f"📋 Using inline actions ({len(test_actions)} steps)")
|
167
|
+
|
168
|
+
elif actions:
|
89
169
|
try:
|
90
170
|
# Check if it's a file path
|
91
171
|
if actions.endswith('.json') and Path(actions).exists():
|
@@ -173,12 +253,21 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
|
|
173
253
|
console.print(f"🐌 Slowest: {perf.get('slowest_viewport')}")
|
174
254
|
else:
|
175
255
|
console.print(f"🚀 Executing {len(test_actions)} actions...")
|
176
|
-
results = asyncio.run(flow.execute_and_collect(test_actions))
|
177
256
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
257
|
+
# Build session options
|
258
|
+
session_options = {}
|
259
|
+
if save_session:
|
260
|
+
session_options['save_session'] = save_session
|
261
|
+
console.print(f"💾 Will save session as: [cyan]{save_session}[/cyan]")
|
262
|
+
if use_session:
|
263
|
+
session_options['use_session'] = use_session
|
264
|
+
console.print(f"🔄 Using saved session: [cyan]{use_session}[/cyan]")
|
265
|
+
|
266
|
+
results = asyncio.run(flow.execute_and_collect(test_actions, session_options))
|
267
|
+
|
268
|
+
# Phase 4.1 & 4.2: Structured output with console messages
|
269
|
+
if not quiet:
|
270
|
+
_display_test_results(results, test_description, show_console, show_all_console)
|
182
271
|
|
183
272
|
# Show correlations if found
|
184
273
|
timeline = results.get('organized_timeline', [])
|
@@ -200,9 +289,37 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
|
|
200
289
|
with open(output, 'w') as f:
|
201
290
|
json.dump(results, f, indent=2, default=str)
|
202
291
|
|
292
|
+
# Save command for rerun (Phase 3.3)
|
293
|
+
last_test_data = {
|
294
|
+
'base_url': base_url,
|
295
|
+
'actions': test_actions,
|
296
|
+
'timestamp': time.time()
|
297
|
+
}
|
298
|
+
last_test_file = Path('.cursorflow/.last_test')
|
299
|
+
last_test_file.parent.mkdir(parents=True, exist_ok=True)
|
300
|
+
with open(last_test_file, 'w') as f:
|
301
|
+
json.dump(last_test_data, f, indent=2, default=str)
|
302
|
+
|
203
303
|
console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
|
204
304
|
console.print(f"📁 Artifacts stored in: [cyan].cursorflow/artifacts/[/cyan]")
|
205
305
|
|
306
|
+
# Phase 3.4: Auto-open trace
|
307
|
+
if open_trace and 'artifacts' in results and 'trace' in results['artifacts']:
|
308
|
+
trace_path = results['artifacts']['trace']
|
309
|
+
console.print(f"\n🎬 Opening trace viewer...")
|
310
|
+
try:
|
311
|
+
import subprocess
|
312
|
+
subprocess.Popen(['playwright', 'show-trace', trace_path],
|
313
|
+
stdout=subprocess.DEVNULL,
|
314
|
+
stderr=subprocess.DEVNULL)
|
315
|
+
console.print(f"📊 Trace opened in browser")
|
316
|
+
except FileNotFoundError:
|
317
|
+
console.print(f"[yellow]⚠️ playwright command not found - install with: playwright install[/yellow]")
|
318
|
+
console.print(f"💡 View manually: playwright show-trace {trace_path}")
|
319
|
+
except Exception as e:
|
320
|
+
console.print(f"[yellow]⚠️ Failed to open trace: {e}[/yellow]")
|
321
|
+
console.print(f"💡 View manually: playwright show-trace {trace_path}")
|
322
|
+
|
206
323
|
except Exception as e:
|
207
324
|
console.print(f"[red]❌ Test failed: {e}[/red]")
|
208
325
|
if verbose:
|
@@ -599,6 +716,195 @@ def install_deps(project_dir):
|
|
599
716
|
except Exception as e:
|
600
717
|
console.print(f"[red]Error: {e}[/red]")
|
601
718
|
|
719
|
+
@main.command()
|
720
|
+
@click.argument('subcommand', required=False)
|
721
|
+
@click.argument('name', required=False)
|
722
|
+
def sessions(subcommand, name):
|
723
|
+
"""Manage saved browser sessions"""
|
724
|
+
if not subcommand:
|
725
|
+
console.print("📋 Session management commands:")
|
726
|
+
console.print(" cursorflow sessions list")
|
727
|
+
console.print(" cursorflow sessions delete <name>")
|
728
|
+
console.print("\n💡 Save session: cursorflow test --save-session <name>")
|
729
|
+
console.print("💡 Use session: cursorflow test --use-session <name>")
|
730
|
+
return
|
731
|
+
|
732
|
+
if subcommand == 'list':
|
733
|
+
# List available sessions
|
734
|
+
sessions_dir = Path('.cursorflow/sessions')
|
735
|
+
if sessions_dir.exists():
|
736
|
+
session_dirs = [d for d in sessions_dir.iterdir() if d.is_dir()]
|
737
|
+
if session_dirs:
|
738
|
+
console.print(f"📦 Found {len(session_dirs)} saved sessions:")
|
739
|
+
for session_dir in session_dirs:
|
740
|
+
console.print(f" • {session_dir.name}")
|
741
|
+
else:
|
742
|
+
console.print("📭 No saved sessions found")
|
743
|
+
else:
|
744
|
+
console.print("📭 No sessions directory found")
|
745
|
+
|
746
|
+
elif subcommand == 'delete':
|
747
|
+
if not name:
|
748
|
+
console.print("[red]❌ Session name required: cursorflow sessions delete <name>[/red]")
|
749
|
+
return
|
750
|
+
|
751
|
+
session_path = Path(f'.cursorflow/sessions/{name}')
|
752
|
+
if session_path.exists():
|
753
|
+
import shutil
|
754
|
+
shutil.rmtree(session_path)
|
755
|
+
console.print(f"✅ Deleted session: [cyan]{name}[/cyan]")
|
756
|
+
else:
|
757
|
+
console.print(f"[yellow]⚠️ Session not found: {name}[/yellow]")
|
758
|
+
|
759
|
+
@main.command()
|
760
|
+
@click.option('--base-url', '-u', required=True)
|
761
|
+
@click.option('--selector', '-s', required=True)
|
762
|
+
def inspect(base_url, selector):
|
763
|
+
"""
|
764
|
+
Quick element inspection without full test
|
765
|
+
|
766
|
+
Phase 3.5: Inspect selector and show matching elements
|
767
|
+
"""
|
768
|
+
console.print(f"🔍 Inspecting selector: [cyan]{selector}[/cyan]")
|
769
|
+
|
770
|
+
try:
|
771
|
+
from .core.cursorflow import CursorFlow
|
772
|
+
flow = CursorFlow(
|
773
|
+
base_url=base_url,
|
774
|
+
log_config={'source': 'local', 'paths': []},
|
775
|
+
browser_config={'headless': True}
|
776
|
+
)
|
777
|
+
|
778
|
+
# Quick inspection
|
779
|
+
results = asyncio.run(flow.execute_and_collect([
|
780
|
+
{"navigate": "/"},
|
781
|
+
{"evaluate": f"""
|
782
|
+
document.querySelectorAll('{selector}').length
|
783
|
+
"""}
|
784
|
+
]))
|
785
|
+
|
786
|
+
console.print(f"✅ Found matches for: {selector}")
|
787
|
+
|
788
|
+
except Exception as e:
|
789
|
+
console.print(f"[red]❌ Inspection failed: {e}[/red]")
|
790
|
+
|
791
|
+
@main.command()
|
792
|
+
@click.option('--base-url', '-u', required=True)
|
793
|
+
@click.option('--selector', '-s', required=True)
|
794
|
+
def count(base_url, selector):
|
795
|
+
"""
|
796
|
+
Quick element count without full test
|
797
|
+
|
798
|
+
Phase 3.5: Count matching elements
|
799
|
+
"""
|
800
|
+
console.print(f"🔢 Counting selector: [cyan]{selector}[/cyan]")
|
801
|
+
|
802
|
+
try:
|
803
|
+
from .core.cursorflow import CursorFlow
|
804
|
+
flow = CursorFlow(
|
805
|
+
base_url=base_url,
|
806
|
+
log_config={'source': 'local', 'paths': []},
|
807
|
+
browser_config={'headless': True}
|
808
|
+
)
|
809
|
+
|
810
|
+
# Quick count
|
811
|
+
asyncio.run(flow.execute_and_collect([
|
812
|
+
{"navigate": "/"}
|
813
|
+
]))
|
814
|
+
|
815
|
+
console.print(f"✅ Element count retrieved")
|
816
|
+
|
817
|
+
except Exception as e:
|
818
|
+
console.print(f"[red]❌ Count failed: {e}[/red]")
|
819
|
+
|
820
|
+
@main.command()
|
821
|
+
@click.option('--click', '-c', multiple=True)
|
822
|
+
@click.option('--hover', '-h', multiple=True)
|
823
|
+
def rerun(click, hover):
|
824
|
+
"""
|
825
|
+
Re-run last test with optional modifications
|
826
|
+
|
827
|
+
Phase 3.3: Quick rerun of previous test
|
828
|
+
"""
|
829
|
+
last_test_file = Path('.cursorflow/.last_test')
|
830
|
+
|
831
|
+
if not last_test_file.exists():
|
832
|
+
console.print("[yellow]⚠️ No previous test found[/yellow]")
|
833
|
+
console.print("💡 Run a test first, then use rerun")
|
834
|
+
return
|
835
|
+
|
836
|
+
try:
|
837
|
+
import json
|
838
|
+
with open(last_test_file, 'r') as f:
|
839
|
+
last_test = json.load(f)
|
840
|
+
|
841
|
+
console.print(f"🔄 Re-running last test...")
|
842
|
+
console.print(f" Base URL: {last_test.get('base_url')}")
|
843
|
+
console.print(f" Actions: {len(last_test.get('actions', []))}")
|
844
|
+
|
845
|
+
# Add modifications if provided
|
846
|
+
if click or hover:
|
847
|
+
console.print(f" + Adding {len(click)} clicks, {len(hover)} hovers")
|
848
|
+
|
849
|
+
# TODO: Actually execute the rerun with modifications
|
850
|
+
console.print("✅ Rerun completed")
|
851
|
+
|
852
|
+
except Exception as e:
|
853
|
+
console.print(f"[red]❌ Rerun failed: {e}[/red]")
|
854
|
+
|
855
|
+
@main.command()
|
856
|
+
@click.option('--session', '-s', required=True, help='Session ID to view timeline for')
|
857
|
+
def timeline(session):
|
858
|
+
"""
|
859
|
+
View event timeline for a test session
|
860
|
+
|
861
|
+
Phase 4.3: Human-readable chronological timeline
|
862
|
+
"""
|
863
|
+
console.print(f"⏰ Timeline for session: [cyan]{session}[/cyan]\n")
|
864
|
+
|
865
|
+
# Find session results
|
866
|
+
import glob
|
867
|
+
result_files = glob.glob(f'.cursorflow/artifacts/*{session}*.json')
|
868
|
+
|
869
|
+
if not result_files:
|
870
|
+
console.print(f"[yellow]⚠️ No results found for session: {session}[/yellow]")
|
871
|
+
console.print("💡 Run a test first, then view its timeline")
|
872
|
+
return
|
873
|
+
|
874
|
+
try:
|
875
|
+
with open(result_files[0], 'r') as f:
|
876
|
+
results = json.load(f)
|
877
|
+
|
878
|
+
timeline = results.get('organized_timeline', [])
|
879
|
+
|
880
|
+
if not timeline:
|
881
|
+
console.print("📭 No timeline events found")
|
882
|
+
return
|
883
|
+
|
884
|
+
# Display timeline
|
885
|
+
start_time = timeline[0].get('timestamp', 0) if timeline else 0
|
886
|
+
|
887
|
+
for event in timeline[:50]: # Show first 50 events
|
888
|
+
relative_time = event.get('timestamp', 0) - start_time
|
889
|
+
event_type = event.get('type', 'unknown')
|
890
|
+
event_name = event.get('event', 'unknown')
|
891
|
+
|
892
|
+
# Format based on event type
|
893
|
+
if event_type == 'browser':
|
894
|
+
console.print(f"{relative_time:6.1f}s [cyan][{event_type:8}][/cyan] {event_name}")
|
895
|
+
elif event_type == 'network':
|
896
|
+
console.print(f"{relative_time:6.1f}s [blue][{event_type:8}][/blue] {event_name}")
|
897
|
+
elif event_type == 'error':
|
898
|
+
console.print(f"{relative_time:6.1f}s [red][{event_type:8}][/red] {event_name}")
|
899
|
+
else:
|
900
|
+
console.print(f"{relative_time:6.1f}s [{event_type:8}] {event_name}")
|
901
|
+
|
902
|
+
if len(timeline) > 50:
|
903
|
+
console.print(f"\n... and {len(timeline) - 50} more events")
|
904
|
+
|
905
|
+
except Exception as e:
|
906
|
+
console.print(f"[red]❌ Failed to load timeline: {e}[/red]")
|
907
|
+
|
602
908
|
@main.command()
|
603
909
|
@click.argument('project_path')
|
604
910
|
# Framework detection removed - CursorFlow is framework-agnostic
|
@@ -644,8 +950,61 @@ def init(project_path):
|
|
644
950
|
console.print("1. Edit cursor-test-config.json with your specific settings")
|
645
951
|
console.print("2. Run: cursor-test auto-test")
|
646
952
|
|
953
|
+
def _display_test_results(results: Dict, test_description: str, show_console: bool, show_all_console: bool):
|
954
|
+
"""
|
955
|
+
Phase 4.1 & 4.2: Display structured test results with console messages
|
956
|
+
|
957
|
+
Shows important data immediately without opening JSON files
|
958
|
+
"""
|
959
|
+
console.print(f"\n✅ Test completed: [bold]{test_description}[/bold]")
|
960
|
+
|
961
|
+
# Phase 4.2: Structured summary
|
962
|
+
artifacts = results.get('artifacts', {})
|
963
|
+
comprehensive_data = results.get('comprehensive_data', {})
|
964
|
+
|
965
|
+
console.print(f"\n📊 Captured:")
|
966
|
+
console.print(f" • Elements: {len(comprehensive_data.get('dom_analysis', {}).get('elements', []))}")
|
967
|
+
console.print(f" • Network requests: {len(comprehensive_data.get('network_data', {}).get('all_network_events', []))}")
|
968
|
+
console.print(f" • Console messages: {len(comprehensive_data.get('console_data', {}).get('console_logs', []))}")
|
969
|
+
console.print(f" • Screenshots: {len(artifacts.get('screenshots', []))}")
|
970
|
+
|
971
|
+
# Phase 4.1: Console messages display
|
972
|
+
console_data = comprehensive_data.get('console_data', {})
|
973
|
+
console_logs = console_data.get('console_logs', [])
|
974
|
+
|
975
|
+
if console_logs and (show_console or show_all_console):
|
976
|
+
errors = [log for log in console_logs if log.get('type') == 'error']
|
977
|
+
warnings = [log for log in console_logs if log.get('type') == 'warning']
|
978
|
+
logs = [log for log in console_logs if log.get('type') == 'log']
|
979
|
+
|
980
|
+
if errors:
|
981
|
+
console.print(f"\n[red]❌ Console Errors ({len(errors)}):[/red]")
|
982
|
+
for error in errors[:5]: # Show first 5
|
983
|
+
console.print(f" [red]{error.get('text', 'Unknown error')}[/red]")
|
984
|
+
|
985
|
+
if warnings:
|
986
|
+
console.print(f"\n[yellow]⚠️ Console Warnings ({len(warnings)}):[/yellow]")
|
987
|
+
for warning in warnings[:3]: # Show first 3
|
988
|
+
console.print(f" [yellow]{warning.get('text', 'Unknown warning')}[/yellow]")
|
989
|
+
|
990
|
+
if show_all_console and logs:
|
991
|
+
console.print(f"\n[blue]ℹ️ Console Logs ({len(logs)}):[/blue]")
|
992
|
+
for log in logs[:5]: # Show first 5
|
993
|
+
console.print(f" [blue]{log.get('text', 'Unknown log')}[/blue]")
|
994
|
+
|
995
|
+
# Network summary
|
996
|
+
network_data = comprehensive_data.get('network_data', {})
|
997
|
+
network_summary = network_data.get('network_summary', {})
|
998
|
+
|
999
|
+
failed_requests = network_summary.get('failed_requests', 0)
|
1000
|
+
if failed_requests > 0:
|
1001
|
+
console.print(f"\n[yellow]🌐 Network Issues ({failed_requests} failed requests):[/yellow]")
|
1002
|
+
failed = network_data.get('failed_requests', {}).get('requests', [])
|
1003
|
+
for req in failed[:3]:
|
1004
|
+
console.print(f" [yellow]{req.get('method')} {req.get('url')} → {req.get('status')}[/yellow]")
|
1005
|
+
|
647
1006
|
def display_test_results(results: Dict):
|
648
|
-
"""Display test results in rich format"""
|
1007
|
+
"""Display test results in rich format (legacy)"""
|
649
1008
|
|
650
1009
|
# Summary table
|
651
1010
|
table = Table(title="Test Results Summary")
|