cursorflow 2.1.5__py3-none-any.whl → 2.2.0__py3-none-any.whl
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/__init__.py +1 -1
- cursorflow/auto_init.py +17 -13
- cursorflow/cli.py +389 -23
- cursorflow/core/action_validator.py +199 -0
- cursorflow/core/browser_controller.py +300 -5
- cursorflow/core/browser_engine.py +13 -0
- cursorflow/core/config_validator.py +216 -0
- cursorflow/core/cursorflow.py +68 -32
- cursorflow/install_cursorflow_rules.py +14 -11
- cursorflow/log_sources/local_file.py +20 -1
- cursorflow/log_sources/ssh_remote.py +19 -0
- cursorflow/rules/cursorflow-installation.mdc +2 -1
- cursorflow/rules/cursorflow-usage.mdc +7 -1
- cursorflow/updater.py +4 -3
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/METADATA +70 -15
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/RECORD +20 -18
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/WHEEL +0 -0
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-2.1.5.dist-info → cursorflow-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
"""
|
2
|
+
Action Format Validation
|
3
|
+
|
4
|
+
Validates action dictionaries before execution to provide clear error messages.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict, Any, List, Optional
|
8
|
+
|
9
|
+
|
10
|
+
class ActionValidationError(Exception):
|
11
|
+
"""Raised when action format is invalid"""
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class ActionValidator:
|
16
|
+
"""
|
17
|
+
Validates action format before execution
|
18
|
+
|
19
|
+
Actions should be dictionaries with a single key indicating the action type,
|
20
|
+
or have an explicit 'type' key.
|
21
|
+
|
22
|
+
Valid formats:
|
23
|
+
{"click": ".selector"}
|
24
|
+
{"click": {"selector": ".element"}}
|
25
|
+
{"type": "click", "selector": ".element"}
|
26
|
+
{"navigate": "/path"}
|
27
|
+
{"wait": 2}
|
28
|
+
"""
|
29
|
+
|
30
|
+
# CursorFlow-specific action types (not direct Playwright methods)
|
31
|
+
CURSORFLOW_ACTION_TYPES = {
|
32
|
+
'navigate', 'screenshot', 'capture', 'authenticate'
|
33
|
+
}
|
34
|
+
|
35
|
+
# Common Playwright Page methods (for documentation/validation)
|
36
|
+
COMMON_PLAYWRIGHT_ACTIONS = {
|
37
|
+
'click', 'dblclick', 'hover', 'focus', 'blur',
|
38
|
+
'fill', 'type', 'press', 'select_option',
|
39
|
+
'check', 'uncheck', 'set_checked',
|
40
|
+
'drag_and_drop', 'tap',
|
41
|
+
'wait', 'wait_for_selector', 'wait_for_timeout', 'wait_for_load_state',
|
42
|
+
'goto', 'reload', 'go_back', 'go_forward',
|
43
|
+
'scroll', 'set_viewport_size', 'bring_to_front',
|
44
|
+
'evaluate', 'evaluate_handle', 'query_selector'
|
45
|
+
}
|
46
|
+
|
47
|
+
# All known valid actions (CursorFlow + Playwright)
|
48
|
+
# Note: This is not exhaustive - we pass through to Playwright dynamically
|
49
|
+
KNOWN_ACTION_TYPES = CURSORFLOW_ACTION_TYPES | COMMON_PLAYWRIGHT_ACTIONS
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def validate(cls, action: Any) -> Dict[str, Any]:
|
53
|
+
"""
|
54
|
+
Validate action format and return normalized action
|
55
|
+
|
56
|
+
Args:
|
57
|
+
action: The action to validate (should be dict)
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
Validated and normalized action dict
|
61
|
+
|
62
|
+
Raises:
|
63
|
+
ActionValidationError: If action format is invalid
|
64
|
+
"""
|
65
|
+
# Check if action is a dict
|
66
|
+
if not isinstance(action, dict):
|
67
|
+
raise ActionValidationError(
|
68
|
+
f"Action must be a dictionary, got {type(action).__name__}: {action}\n"
|
69
|
+
f"Expected format: {{'click': '.selector'}} or {{'type': 'click', 'selector': '.element'}}"
|
70
|
+
)
|
71
|
+
|
72
|
+
# Check if action is empty
|
73
|
+
if not action:
|
74
|
+
raise ActionValidationError(
|
75
|
+
"Action dictionary is empty\n"
|
76
|
+
f"Expected format: {{'click': '.selector'}}"
|
77
|
+
)
|
78
|
+
|
79
|
+
# Get action type
|
80
|
+
action_type = cls._extract_action_type(action)
|
81
|
+
|
82
|
+
# Validate action type (permissive - warns for unknown, doesn't block)
|
83
|
+
if action_type not in cls.KNOWN_ACTION_TYPES:
|
84
|
+
# Log warning but allow it (might be valid Playwright method)
|
85
|
+
import logging
|
86
|
+
logger = logging.getLogger(__name__)
|
87
|
+
logger.warning(
|
88
|
+
f"Unknown action type '{action_type}' - will attempt to pass through to Playwright. "
|
89
|
+
f"Common actions: {', '.join(sorted(list(cls.COMMON_PLAYWRIGHT_ACTIONS)[:10]))}... "
|
90
|
+
f"See: https://playwright.dev/python/docs/api/class-page"
|
91
|
+
)
|
92
|
+
|
93
|
+
return action
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def _extract_action_type(cls, action: dict) -> str:
|
97
|
+
"""
|
98
|
+
Extract action type from action dict
|
99
|
+
|
100
|
+
Supports:
|
101
|
+
{"type": "click", "selector": ".btn"} # Explicit type key with string value
|
102
|
+
{"click": ".selector"} # Action type is the key
|
103
|
+
{"click": {"selector": ".btn"}} # Action type with config dict
|
104
|
+
{"type": {"selector": "#field"}} # 'type' as action (typing), not explicit type
|
105
|
+
"""
|
106
|
+
# Check if 'type' key exists AND has a string value (explicit type specification)
|
107
|
+
# If type key has a dict value, it's the action itself (typing action)
|
108
|
+
if 'type' in action and isinstance(action['type'], str):
|
109
|
+
return action['type']
|
110
|
+
|
111
|
+
# Otherwise, first key is the action type
|
112
|
+
keys = list(action.keys())
|
113
|
+
if not keys:
|
114
|
+
raise ActionValidationError("Action has no keys")
|
115
|
+
|
116
|
+
action_type = keys[0]
|
117
|
+
|
118
|
+
# First key should be the action type (string)
|
119
|
+
if not isinstance(action_type, str):
|
120
|
+
raise ActionValidationError(
|
121
|
+
f"Action type must be a string, got {type(action_type).__name__}: {action_type}"
|
122
|
+
)
|
123
|
+
|
124
|
+
return action_type
|
125
|
+
|
126
|
+
@classmethod
|
127
|
+
def validate_list(cls, actions: Any) -> List[Dict[str, Any]]:
|
128
|
+
"""
|
129
|
+
Validate list of actions
|
130
|
+
|
131
|
+
Args:
|
132
|
+
actions: Should be a list of action dicts
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
List of validated actions
|
136
|
+
|
137
|
+
Raises:
|
138
|
+
ActionValidationError: If format is invalid
|
139
|
+
"""
|
140
|
+
if not isinstance(actions, list):
|
141
|
+
raise ActionValidationError(
|
142
|
+
f"Actions must be a list, got {type(actions).__name__}: {actions}\n"
|
143
|
+
f"Expected format: [{{'click': '.btn'}}, {{'wait': 2}}]"
|
144
|
+
)
|
145
|
+
|
146
|
+
if not actions:
|
147
|
+
raise ActionValidationError(
|
148
|
+
"Actions list is empty\n"
|
149
|
+
f"Expected at least one action like: [{{'navigate': '/'}}]"
|
150
|
+
)
|
151
|
+
|
152
|
+
validated = []
|
153
|
+
for i, action in enumerate(actions):
|
154
|
+
try:
|
155
|
+
validated.append(cls.validate(action))
|
156
|
+
except ActionValidationError as e:
|
157
|
+
raise ActionValidationError(
|
158
|
+
f"Invalid action at index {i}: {e}"
|
159
|
+
)
|
160
|
+
|
161
|
+
return validated
|
162
|
+
|
163
|
+
@classmethod
|
164
|
+
def get_example_actions(cls) -> str:
|
165
|
+
"""Get example action formats for help text"""
|
166
|
+
return """
|
167
|
+
Action Format Examples:
|
168
|
+
|
169
|
+
Common CursorFlow actions:
|
170
|
+
{"navigate": "/dashboard"}
|
171
|
+
{"click": ".button"}
|
172
|
+
{"screenshot": "page-loaded"}
|
173
|
+
|
174
|
+
Any Playwright Page method:
|
175
|
+
{"hover": ".menu-item"}
|
176
|
+
{"dblclick": ".editable"}
|
177
|
+
{"press": "Enter"}
|
178
|
+
{"drag_and_drop": {"source": ".item", "target": ".dropzone"}}
|
179
|
+
{"focus": "#input"}
|
180
|
+
{"check": "#checkbox"}
|
181
|
+
{"evaluate": "window.scrollTo(0, 100)"}
|
182
|
+
|
183
|
+
See full Playwright API:
|
184
|
+
https://playwright.dev/python/docs/api/class-page
|
185
|
+
|
186
|
+
CursorFlow passes actions directly to Playwright, giving you access
|
187
|
+
to 94+ methods without artificial limitations.
|
188
|
+
|
189
|
+
Complete workflow:
|
190
|
+
[
|
191
|
+
{"navigate": "/login"},
|
192
|
+
{"fill": {"selector": "#username", "value": "admin"}},
|
193
|
+
{"fill": {"selector": "#password", "value": "pass123"}},
|
194
|
+
{"click": "#submit"},
|
195
|
+
{"wait_for_selector": ".dashboard"},
|
196
|
+
{"screenshot": "logged-in"}
|
197
|
+
]
|
198
|
+
"""
|
199
|
+
|
@@ -79,8 +79,8 @@ class BrowserController:
|
|
79
79
|
|
80
80
|
self.playwright = await async_playwright().start()
|
81
81
|
|
82
|
-
# Browser configuration -
|
83
|
-
|
82
|
+
# Browser configuration - smart defaults with pass-through
|
83
|
+
default_browser_config = {
|
84
84
|
"headless": self.config.get("headless", True),
|
85
85
|
"slow_mo": 0 if self.config.get("headless", True) else 100,
|
86
86
|
"args": [
|
@@ -92,16 +92,42 @@ class BrowserController:
|
|
92
92
|
]
|
93
93
|
}
|
94
94
|
|
95
|
+
# Pass-through architecture: Merge user options with defaults
|
96
|
+
# Users can override ANY Playwright launch option
|
97
|
+
# See: https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch
|
98
|
+
user_browser_options = self.config.get("browser_launch_options", {})
|
99
|
+
|
100
|
+
# Validate user options (warns about typos, validates types)
|
101
|
+
from .config_validator import ConfigValidator
|
102
|
+
if user_browser_options:
|
103
|
+
user_browser_options = ConfigValidator.validate_browser_options(user_browser_options)
|
104
|
+
|
105
|
+
browser_config = {**default_browser_config, **user_browser_options}
|
106
|
+
|
95
107
|
self.browser = await self.playwright.chromium.launch(**browser_config)
|
96
108
|
|
97
|
-
# Context configuration
|
109
|
+
# Context configuration - smart defaults with pass-through
|
98
110
|
viewport = self.config.get("viewport", {"width": 1440, "height": 900})
|
99
|
-
|
111
|
+
default_context_config = {
|
100
112
|
"viewport": viewport,
|
101
113
|
"ignore_https_errors": True,
|
102
114
|
"record_video_dir": ".cursorflow/artifacts/videos" if self.config.get("record_video") else None
|
103
115
|
}
|
104
116
|
|
117
|
+
# Pass-through architecture: Merge user options with defaults
|
118
|
+
# Users can use ANY Playwright context option:
|
119
|
+
# - geolocation, permissions, timezone, locale
|
120
|
+
# - color_scheme, reduced_motion, http_credentials
|
121
|
+
# - user_agent, extra_http_headers, offline
|
122
|
+
# See: https://playwright.dev/python/docs/api/class-browser#browser-new-context
|
123
|
+
user_context_options = self.config.get("context_options", {})
|
124
|
+
|
125
|
+
# Validate user options (warns about typos, validates types)
|
126
|
+
if user_context_options:
|
127
|
+
user_context_options = ConfigValidator.validate_context_options(user_context_options)
|
128
|
+
|
129
|
+
context_config = {**default_context_config, **user_context_options}
|
130
|
+
|
105
131
|
self.context = await self.browser.new_context(**context_config)
|
106
132
|
self.page = await self.context.new_page()
|
107
133
|
|
@@ -275,6 +301,9 @@ class BrowserController:
|
|
275
301
|
}
|
276
302
|
self.network_requests.append(response_data)
|
277
303
|
|
304
|
+
# Capture response body asynchronously (Phase 1.4: Network Response Body Capture)
|
305
|
+
asyncio.create_task(self._capture_response_body_async(response, response_data))
|
306
|
+
|
278
307
|
# Log failed requests for correlation
|
279
308
|
if response.status >= 400:
|
280
309
|
self.logger.warning(f"Failed Response: {response.status} {response.url}")
|
@@ -682,8 +711,47 @@ class BrowserController:
|
|
682
711
|
self.logger.error(f"Browser cleanup failed: {e}")
|
683
712
|
return None
|
684
713
|
|
714
|
+
async def _capture_response_body_async(self, response, response_data: Dict):
|
715
|
+
"""
|
716
|
+
Async wrapper to capture response body without blocking event handlers
|
717
|
+
|
718
|
+
Phase 1.4: Network Response Body Capture
|
719
|
+
Captures request/response bodies for complete debugging data
|
720
|
+
"""
|
721
|
+
try:
|
722
|
+
# Get response body
|
723
|
+
body = await response.body()
|
724
|
+
decoded_body = body.decode('utf-8', errors='ignore')
|
725
|
+
|
726
|
+
# Update the response_data dict directly (it's already in self.network_requests)
|
727
|
+
response_data["response_body"] = decoded_body[:5000] # Capture more for debugging
|
728
|
+
response_data["response_body_size"] = len(decoded_body)
|
729
|
+
response_data["response_body_truncated"] = len(decoded_body) > 5000
|
730
|
+
|
731
|
+
# Parse JSON responses automatically
|
732
|
+
content_type = response.headers.get("content-type", "")
|
733
|
+
if "application/json" in content_type:
|
734
|
+
try:
|
735
|
+
import json
|
736
|
+
response_data["response_body_json"] = json.loads(decoded_body)
|
737
|
+
|
738
|
+
# Log key data for debugging undefined values
|
739
|
+
self.logger.debug(f"JSON Response from {response.url[:50]}: {len(response_data['response_body_json'])} keys")
|
740
|
+
except json.JSONDecodeError as e:
|
741
|
+
response_data["json_parse_error"] = str(e)
|
742
|
+
self.logger.warning(f"Failed to parse JSON response from {response.url}: {e}")
|
743
|
+
|
744
|
+
# Log error responses
|
745
|
+
if response.status >= 400:
|
746
|
+
error_preview = decoded_body[:200].replace('\n', ' ')
|
747
|
+
self.logger.error(f"Error response ({response.status}) from {response.url}: {error_preview}")
|
748
|
+
|
749
|
+
except Exception as e:
|
750
|
+
self.logger.debug(f"Response body capture failed for {response.url}: {e}")
|
751
|
+
response_data["body_capture_error"] = str(e)
|
752
|
+
|
685
753
|
async def _capture_response_body(self, response):
|
686
|
-
"""
|
754
|
+
"""Legacy method - captures response body for specific cases"""
|
687
755
|
try:
|
688
756
|
body = await response.body()
|
689
757
|
decoded_body = body.decode('utf-8', errors='ignore')
|
@@ -723,6 +791,227 @@ class BrowserController:
|
|
723
791
|
req["body_capture_error"] = str(e)
|
724
792
|
break
|
725
793
|
|
794
|
+
async def _capture_javascript_context(self) -> Dict[str, Any]:
|
795
|
+
"""
|
796
|
+
Phase 2.2: JavaScript Context Capture
|
797
|
+
|
798
|
+
Captures global JavaScript scope including:
|
799
|
+
- Global functions (enumerate window properties that are functions)
|
800
|
+
- Global variables (enumerate window properties that are not functions)
|
801
|
+
- Specific window objects (configurable list to serialize)
|
802
|
+
"""
|
803
|
+
try:
|
804
|
+
# Get list of objects to capture from config
|
805
|
+
capture_objects = self.config.get("capture_window_objects", [])
|
806
|
+
|
807
|
+
context_data = await self.page.evaluate("""
|
808
|
+
(captureObjects) => {
|
809
|
+
const context = {
|
810
|
+
global_functions: [],
|
811
|
+
global_variables: [],
|
812
|
+
window_property_count: 0,
|
813
|
+
window_objects: {}
|
814
|
+
};
|
815
|
+
|
816
|
+
// Enumerate window properties
|
817
|
+
const windowProps = Object.getOwnPropertyNames(window);
|
818
|
+
context.window_property_count = windowProps.length;
|
819
|
+
|
820
|
+
// Categorize by type
|
821
|
+
windowProps.forEach(prop => {
|
822
|
+
try {
|
823
|
+
const value = window[prop];
|
824
|
+
|
825
|
+
// Skip built-in browser objects (too many)
|
826
|
+
if (prop.startsWith('webkit') || prop.startsWith('moz') ||
|
827
|
+
prop.startsWith('chrome') || prop === 'constructor') {
|
828
|
+
return;
|
829
|
+
}
|
830
|
+
|
831
|
+
if (typeof value === 'function') {
|
832
|
+
// Skip native functions (toString contains '[native code]')
|
833
|
+
const funcStr = value.toString();
|
834
|
+
if (!funcStr.includes('[native code]')) {
|
835
|
+
context.global_functions.push(prop);
|
836
|
+
}
|
837
|
+
} else if (value !== null && typeof value !== 'undefined' &&
|
838
|
+
typeof value !== 'function' && typeof value !== 'object') {
|
839
|
+
// Primitive global variables
|
840
|
+
context.global_variables.push({
|
841
|
+
name: prop,
|
842
|
+
type: typeof value,
|
843
|
+
value: String(value).substring(0, 100) // Truncate long values
|
844
|
+
});
|
845
|
+
}
|
846
|
+
} catch (e) {
|
847
|
+
// Skip properties that throw on access
|
848
|
+
}
|
849
|
+
});
|
850
|
+
|
851
|
+
// Capture specific window objects (configurable)
|
852
|
+
captureObjects.forEach(objName => {
|
853
|
+
try {
|
854
|
+
const obj = window[objName];
|
855
|
+
if (obj && typeof obj === 'object') {
|
856
|
+
// Serialize object (handle circular references)
|
857
|
+
context.window_objects[objName] = JSON.parse(
|
858
|
+
JSON.stringify(obj, (key, value) => {
|
859
|
+
// Handle circular references
|
860
|
+
if (typeof value === 'object' && value !== null) {
|
861
|
+
if (key && typeof value === 'object' && Object.keys(value).length > 50) {
|
862
|
+
return '[Large Object]';
|
863
|
+
}
|
864
|
+
}
|
865
|
+
// Handle functions
|
866
|
+
if (typeof value === 'function') {
|
867
|
+
return '[Function]';
|
868
|
+
}
|
869
|
+
return value;
|
870
|
+
})
|
871
|
+
);
|
872
|
+
}
|
873
|
+
} catch (e) {
|
874
|
+
context.window_objects[objName] = {
|
875
|
+
error: `Failed to serialize: ${e.message}`
|
876
|
+
};
|
877
|
+
}
|
878
|
+
});
|
879
|
+
|
880
|
+
return context;
|
881
|
+
}
|
882
|
+
""", capture_objects)
|
883
|
+
|
884
|
+
return context_data
|
885
|
+
|
886
|
+
except Exception as e:
|
887
|
+
self.logger.error(f"JavaScript context capture failed: {e}")
|
888
|
+
return {
|
889
|
+
"error": str(e),
|
890
|
+
"global_functions": [],
|
891
|
+
"global_variables": [],
|
892
|
+
"window_objects": {}
|
893
|
+
}
|
894
|
+
|
895
|
+
async def _capture_storage_state(self) -> Dict[str, Any]:
|
896
|
+
"""
|
897
|
+
Phase 2.3: Storage State Capture
|
898
|
+
|
899
|
+
Captures browser storage state:
|
900
|
+
- localStorage
|
901
|
+
- sessionStorage
|
902
|
+
- cookies
|
903
|
+
|
904
|
+
Masks sensitive keys based on configuration.
|
905
|
+
"""
|
906
|
+
try:
|
907
|
+
# Get masking configuration
|
908
|
+
sensitive_keys = self.config.get("sensitive_storage_keys", [
|
909
|
+
"authToken", "apiKey", "sessionId", "password", "secret", "token"
|
910
|
+
])
|
911
|
+
|
912
|
+
storage_data = await self.page.evaluate("""
|
913
|
+
(sensitiveKeys) => {
|
914
|
+
const storage = {
|
915
|
+
localStorage: {},
|
916
|
+
sessionStorage: {},
|
917
|
+
cookies: []
|
918
|
+
};
|
919
|
+
|
920
|
+
// Capture localStorage
|
921
|
+
for (let i = 0; i < localStorage.length; i++) {
|
922
|
+
const key = localStorage.key(i);
|
923
|
+
const value = localStorage.getItem(key);
|
924
|
+
|
925
|
+
// Mask sensitive keys
|
926
|
+
const isSensitive = sensitiveKeys.some(pattern =>
|
927
|
+
key.toLowerCase().includes(pattern.toLowerCase())
|
928
|
+
);
|
929
|
+
|
930
|
+
storage.localStorage[key] = isSensitive ? '****' : value;
|
931
|
+
}
|
932
|
+
|
933
|
+
// Capture sessionStorage
|
934
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
935
|
+
const key = sessionStorage.key(i);
|
936
|
+
const value = sessionStorage.getItem(key);
|
937
|
+
|
938
|
+
const isSensitive = sensitiveKeys.some(pattern =>
|
939
|
+
key.toLowerCase().includes(pattern.toLowerCase())
|
940
|
+
);
|
941
|
+
|
942
|
+
storage.sessionStorage[key] = isSensitive ? '****' : value;
|
943
|
+
}
|
944
|
+
|
945
|
+
// Capture cookies (just names, not values for security)
|
946
|
+
storage.cookies = document.cookie.split(';').map(c => c.trim().split('=')[0]);
|
947
|
+
|
948
|
+
return storage;
|
949
|
+
}
|
950
|
+
""", sensitive_keys)
|
951
|
+
|
952
|
+
return storage_data
|
953
|
+
|
954
|
+
except Exception as e:
|
955
|
+
self.logger.error(f"Storage state capture failed: {e}")
|
956
|
+
return {
|
957
|
+
"error": str(e),
|
958
|
+
"localStorage": {},
|
959
|
+
"sessionStorage": {},
|
960
|
+
"cookies": []
|
961
|
+
}
|
962
|
+
|
963
|
+
async def _capture_form_state(self) -> Dict[str, Any]:
|
964
|
+
"""
|
965
|
+
Phase 2.4: Form State Capture
|
966
|
+
|
967
|
+
Captures all form field values at time of capture.
|
968
|
+
Automatically masks password fields.
|
969
|
+
"""
|
970
|
+
try:
|
971
|
+
form_data = await self.page.evaluate("""
|
972
|
+
() => {
|
973
|
+
const forms = {};
|
974
|
+
|
975
|
+
// Get all forms on page
|
976
|
+
document.querySelectorAll('form').forEach(form => {
|
977
|
+
const formId = form.id || form.name || `form_${forms.length}`;
|
978
|
+
const formData = {};
|
979
|
+
|
980
|
+
// Get all form inputs
|
981
|
+
form.querySelectorAll('input, select, textarea').forEach(field => {
|
982
|
+
const fieldName = field.name || field.id || `field_${field.type}`;
|
983
|
+
|
984
|
+
// Mask password fields
|
985
|
+
if (field.type === 'password') {
|
986
|
+
formData[fieldName] = '****';
|
987
|
+
}
|
988
|
+
// Checkbox/radio
|
989
|
+
else if (field.type === 'checkbox' || field.type === 'radio') {
|
990
|
+
formData[fieldName] = field.checked;
|
991
|
+
}
|
992
|
+
// Select dropdowns
|
993
|
+
else if (field.tagName === 'SELECT') {
|
994
|
+
formData[fieldName] = field.value;
|
995
|
+
}
|
996
|
+
// Text inputs, textareas
|
997
|
+
else {
|
998
|
+
formData[fieldName] = field.value;
|
999
|
+
}
|
1000
|
+
});
|
1001
|
+
|
1002
|
+
forms[formId] = formData;
|
1003
|
+
});
|
1004
|
+
|
1005
|
+
return forms;
|
1006
|
+
}
|
1007
|
+
""")
|
1008
|
+
|
1009
|
+
return form_data
|
1010
|
+
|
1011
|
+
except Exception as e:
|
1012
|
+
self.logger.error(f"Form state capture failed: {e}")
|
1013
|
+
return {"error": str(e)}
|
1014
|
+
|
726
1015
|
def _categorize_http_error(self, status_code: int) -> str:
|
727
1016
|
"""Categorize HTTP errors for better debugging (v2.0 enhancement)"""
|
728
1017
|
if 400 <= status_code < 500:
|
@@ -872,6 +1161,9 @@ class BrowserController:
|
|
872
1161
|
"dom_analysis": dom_analysis,
|
873
1162
|
"network_data": network_data,
|
874
1163
|
"console_data": console_data,
|
1164
|
+
"javascript_context": javascript_context,
|
1165
|
+
"storage_state": storage_state,
|
1166
|
+
"form_state": form_state,
|
875
1167
|
"performance_data": performance_data,
|
876
1168
|
"page_state": page_state,
|
877
1169
|
|
@@ -1750,6 +2042,9 @@ class BrowserController:
|
|
1750
2042
|
// v2.0 Enhancement: Accessibility data
|
1751
2043
|
accessibility: getAccessibilityData(element),
|
1752
2044
|
|
2045
|
+
// Phase 2.1: Event Handlers
|
2046
|
+
event_handlers: getEventHandlers(element),
|
2047
|
+
|
1753
2048
|
// v2.0 Enhancement: Visual context
|
1754
2049
|
visual_context: visualContext,
|
1755
2050
|
|
@@ -192,6 +192,19 @@ class BrowserEngine:
|
|
192
192
|
async def _execute_action(self, action: Dict) -> Dict:
|
193
193
|
"""Execute a single test action"""
|
194
194
|
|
195
|
+
# Validate action format
|
196
|
+
from .action_validator import ActionValidator, ActionValidationError
|
197
|
+
|
198
|
+
try:
|
199
|
+
action = ActionValidator.validate(action)
|
200
|
+
except ActionValidationError as e:
|
201
|
+
return {
|
202
|
+
'action': 'unknown',
|
203
|
+
'success': False,
|
204
|
+
'error': f"Invalid action format: {e}"
|
205
|
+
}
|
206
|
+
|
207
|
+
# Extract action type safely
|
195
208
|
action_type = action.get('type') or list(action.keys())[0]
|
196
209
|
action_config = action.get(action_type, action)
|
197
210
|
|