screenforge 0.4.0__tar.gz → 0.5.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.
- screenforge-0.5.0/PKG-INFO +219 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/README.md +15 -13
- screenforge-0.5.0/cli/_version.py +1 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/action.py +13 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/workflow.py +15 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/parser.py +15 -1
- screenforge-0.5.0/cli/playground_sink.py +202 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/pyproject.toml +2 -1
- screenforge-0.5.0/screenforge.egg-info/PKG-INFO +219 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/SOURCES.txt +4 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_parser.py +12 -0
- screenforge-0.5.0/tests/test_playground_app.py +313 -0
- screenforge-0.5.0/tests/test_playground_sink.py +423 -0
- screenforge-0.5.0/tests/test_playground_sink_integration.py +202 -0
- screenforge-0.4.0/PKG-INFO +0 -43
- screenforge-0.4.0/cli/_version.py +0 -1
- screenforge-0.4.0/screenforge.egg-info/PKG-INFO +0 -43
- {screenforge-0.4.0 → screenforge-0.5.0}/LICENSE +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/dispatch.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/doctor.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/default.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/demo.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/dry_run.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/init.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/modes/plan.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/reporter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/session.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/shared.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/shorthand.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/cli/tool_protocol_handlers.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/android_adapter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/base_adapter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/ios_adapter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/adapters/web_adapter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/ai.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/ai_autonomous.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/ai_heal.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_hash.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_manager.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_stats.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/cache_storage.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/cache/embedding_loader.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/capabilities.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/case_memory.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/error_codes.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/exceptions.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/executor.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/failure_diagnosis.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/history_manager.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/logs.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/mcp_server.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/preflight.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/progress.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/run_reporter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/run_resume.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/runtime_modes.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/tool_protocol.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/visual_fallback.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/common/workflow_schema.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/config/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/config/config.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/config/env_loader.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/dependency_links.txt +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/entry_points.txt +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/requires.txt +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/screenforge.egg-info/top_level.txt +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/setup.cfg +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_autonomous.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_brain.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ai_heal.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_android_smoke_live.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_cache_manager.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_capabilities.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_cli_action_json.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_codegen_quality.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_dispatch.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_doctor_orphan_browser.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_error_codes.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_executor.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_failure_diagnosis.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_interaction_actions.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ios_smoke_live.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_mcp_ref_cache.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_ml_optional.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_run_reporter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_run_resume.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_runtime_modes.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_screenshot_annotator.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_shorthand.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_tool_protocol_diagnosis.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_ios.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_web.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_utils_xml.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_visual_fallback.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_adapter.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_dom_complex_live.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/tests/test_web_smoke_live.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/utils/__init__.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/utils/screenshot_annotator.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_ios.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_web.py +0 -0
- {screenforge-0.4.0 → screenforge-0.5.0}/utils/utils_xml.py +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: screenforge
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: AI-driven cross-platform UI automation engine with test script generation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/jhinzzz/ScreenForge
|
|
7
|
+
Project-URL: Repository, https://github.com/jhinzzz/ScreenForge
|
|
8
|
+
Project-URL: Issues, https://github.com/jhinzzz/ScreenForge/issues
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: playwright>=1.50
|
|
13
|
+
Requires-Dist: openai>=2.0
|
|
14
|
+
Requires-Dist: allure-pytest>=2.15
|
|
15
|
+
Requires-Dist: loguru>=0.7
|
|
16
|
+
Requires-Dist: pydantic>=2.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Requires-Dist: pillow>=12.0
|
|
20
|
+
Requires-Dist: lxml>=5.0
|
|
21
|
+
Requires-Dist: requests>=2.30
|
|
22
|
+
Requires-Dist: rich>=14.0
|
|
23
|
+
Requires-Dist: typer>=0.24
|
|
24
|
+
Requires-Dist: retry2>=0.9
|
|
25
|
+
Requires-Dist: opencv-python>=4.10
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: filelock>=3.12
|
|
28
|
+
Provides-Extra: android
|
|
29
|
+
Requires-Dist: uiautomator2>=3.5; extra == "android"
|
|
30
|
+
Requires-Dist: adbutils>=2.12; extra == "android"
|
|
31
|
+
Provides-Extra: ios
|
|
32
|
+
Requires-Dist: facebook-wda>=1.0; extra == "ios"
|
|
33
|
+
Provides-Extra: ml
|
|
34
|
+
Requires-Dist: torch>=2.0; extra == "ml"
|
|
35
|
+
Requires-Dist: transformers>=5.0; extra == "ml"
|
|
36
|
+
Requires-Dist: sentence-transformers>=5.0; extra == "ml"
|
|
37
|
+
Requires-Dist: scikit-learn>=1.8; extra == "ml"
|
|
38
|
+
Provides-Extra: playground
|
|
39
|
+
Requires-Dist: fastapi>=0.115; extra == "playground"
|
|
40
|
+
Requires-Dist: uvicorn>=0.34; extra == "playground"
|
|
41
|
+
Requires-Dist: websockets>=14.0; extra == "playground"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: screenforge[android,ios,ml,playground]; extra == "all"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# ScreenForge
|
|
47
|
+
|
|
48
|
+
[](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml)
|
|
49
|
+
[](https://www.python.org/downloads/)
|
|
50
|
+
[](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
|
|
51
|
+
|
|
52
|
+
**[中文](https://github.com/jhinzzz/ScreenForge/blob/main/README_CN.md)** | English
|
|
53
|
+
|
|
54
|
+
> Describe what to test. Watch it happen. Get a pytest script.
|
|
55
|
+
|
|
56
|
+
ScreenForge is an AI-driven UI automation engine that turns natural language into executable test scripts. Unlike record-and-replay tools, you don't perform the actions yourself — the AI does it for you.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
## Why ScreenForge?
|
|
61
|
+
|
|
62
|
+
| | Playwright Codegen | Browser Use | Midscene.js | **ScreenForge** |
|
|
63
|
+
|---|---|---|---|---|
|
|
64
|
+
| Need to perform actions yourself? | Yes | No | No | **No** |
|
|
65
|
+
| Generates replayable test scripts? | Yes | No | No | **Yes (pytest)** |
|
|
66
|
+
| Self-healing when UI changes? | No | No | No | **Yes** |
|
|
67
|
+
| Works as AI Agent tool (MCP)? | No | Yes | No | **Yes** |
|
|
68
|
+
|
|
69
|
+
**Core architecture**: Your AI Agent is the brain (understands requirements, makes decisions). ScreenForge is the hands (executes UI actions, generates code).
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install screenforge
|
|
75
|
+
|
|
76
|
+
# See the magic without any API key:
|
|
77
|
+
screenforge --demo
|
|
78
|
+
|
|
79
|
+
# For real usage, set your LLM key:
|
|
80
|
+
export OPENAI_API_KEY=sk-...
|
|
81
|
+
|
|
82
|
+
# Inspect the current page (returns DOM tree for your Agent to analyze):
|
|
83
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
84
|
+
|
|
85
|
+
# Execute a single action:
|
|
86
|
+
screenforge --action click --platform web --locator-type text --locator-value "Login"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
You (or your AI Agent) ScreenForge
|
|
93
|
+
│ │
|
|
94
|
+
├──── "Test the login" ─────►│
|
|
95
|
+
│ ├── inspect_ui (get DOM tree)
|
|
96
|
+
│◄── DOM tree ──────────────┤
|
|
97
|
+
│ │
|
|
98
|
+
├──── click #email ─────────►│
|
|
99
|
+
├──── input "user@..." ─────►│
|
|
100
|
+
├──── click "Sign In" ──────►│
|
|
101
|
+
│ │
|
|
102
|
+
│◄── pytest script ─────────┤
|
|
103
|
+
│◄── Allure report ─────────┤
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each step: **inspect → decide → act → verify**. The AI decides, ScreenForge executes.
|
|
107
|
+
|
|
108
|
+
## Features
|
|
109
|
+
|
|
110
|
+
- **Cross-platform**: Android (uiautomator2), iOS (wda), Web (Playwright)
|
|
111
|
+
- **Self-healing engine**: When tests break due to UI changes, the engine auto-repairs locators with confidence scoring and AST validation
|
|
112
|
+
- **L1/L2 semantic cache**: Same page + same instruction = instant response, no LLM call needed
|
|
113
|
+
- **Visual fallback**: When DOM can't locate elements (Canvas, games), VLM parses screenshots
|
|
114
|
+
- **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
|
|
115
|
+
- **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
|
|
116
|
+
- **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
|
|
117
|
+
|
|
118
|
+
## Agent Integration (Claude Code / Cursor / Codex)
|
|
119
|
+
|
|
120
|
+
ScreenForge exposes itself as a tool for AI Agents. The standard loop:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# 1. Get page structure (your Agent analyzes it)
|
|
124
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
125
|
+
|
|
126
|
+
# 2. Your Agent decides what to do, sends precise actions
|
|
127
|
+
screenforge --action click --platform web --locator-type text --locator-value "Login"
|
|
128
|
+
|
|
129
|
+
# 3. Verify the result, repeat
|
|
130
|
+
echo '{"operation":"inspect_ui","platform":"web"}' | screenforge --tool-stdin
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For batch operations, use workflows:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
screenforge --workflow ./workflows/login.yaml --platform web --json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Or start the MCP server for native Agent integration:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
screenforge --mcp-server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## GitHub Actions
|
|
146
|
+
|
|
147
|
+
Add ScreenForge to your CI pipeline:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- uses: jhinzzz/ScreenForge@v1
|
|
151
|
+
with:
|
|
152
|
+
platform: web
|
|
153
|
+
workflow: ./workflows/login.yaml
|
|
154
|
+
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Results are auto-uploaded as Allure artifacts. See [action.yml](https://github.com/jhinzzz/ScreenForge/blob/main/action.yml) for all inputs.
|
|
158
|
+
|
|
159
|
+
See [Agent Integration Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) for the complete protocol.
|
|
160
|
+
|
|
161
|
+
## Installation (from source)
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
git clone https://github.com/jhinzzz/ScreenForge.git
|
|
165
|
+
cd ScreenForge
|
|
166
|
+
python -m venv .venv && source .venv/bin/activate
|
|
167
|
+
pip install -e ".[all]"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Platform-specific extras:
|
|
171
|
+
- Web only: `pip install -e .` (default, includes Playwright)
|
|
172
|
+
- Android: `pip install -e ".[android]"`
|
|
173
|
+
- iOS: `pip install -e ".[ios]"`
|
|
174
|
+
- ML/cache: `pip install -e ".[ml]"` (sentence-transformers for semantic cache)
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Required: LLM API key (OpenAI-compatible endpoint)
|
|
180
|
+
export OPENAI_API_KEY=sk-...
|
|
181
|
+
|
|
182
|
+
# Optional: custom endpoint (defaults to api.openai.com)
|
|
183
|
+
export OPENAI_BASE_URL=https://api.openai.com/v1
|
|
184
|
+
|
|
185
|
+
# Optional: model (defaults to gpt-4o)
|
|
186
|
+
export MODEL_NAME=gpt-4o
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or create a `.env` file (copy from `.env_template`).
|
|
190
|
+
|
|
191
|
+
## Badge
|
|
192
|
+
|
|
193
|
+
If ScreenForge generates tests for your project, add this badge to your README:
|
|
194
|
+
|
|
195
|
+
```markdown
|
|
196
|
+
[](https://github.com/jhinzzz/ScreenForge)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
[](https://github.com/jhinzzz/ScreenForge)
|
|
200
|
+
|
|
201
|
+
## Learn More
|
|
202
|
+
|
|
203
|
+
| Resource | Description |
|
|
204
|
+
|----------|-------------|
|
|
205
|
+
| [Mobile Setup](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mobile-setup.md) | Android & iOS device connection guide |
|
|
206
|
+
| [MCP Setup (3 min)](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
|
|
207
|
+
| [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) | Integration protocol for AI Agents |
|
|
208
|
+
| [Capability Matrix](https://github.com/jhinzzz/ScreenForge/blob/main/docs/capability-matrix.md) | Supported platforms, actions, and locators |
|
|
209
|
+
| [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md) | Live Mirror — watch code + screenshots grow as the test runs |
|
|
210
|
+
| [Workflow Examples](https://github.com/jhinzzz/ScreenForge/tree/main/docs/workflows) | YAML workflow templates |
|
|
211
|
+
| [CHANGELOG](https://github.com/jhinzzz/ScreenForge/blob/main/CHANGELOG.md) | Version history |
|
|
212
|
+
|
|
213
|
+
## Contributing
|
|
214
|
+
|
|
215
|
+
See [CONTRIBUTING.md](https://github.com/jhinzzz/ScreenForge/blob/main/CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[MIT](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/jhinzzz/ScreenForge/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](LICENSE)
|
|
5
|
+
[](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
**[中文](
|
|
7
|
+
**[中文](https://github.com/jhinzzz/ScreenForge/blob/main/README_CN.md)** | English
|
|
8
8
|
|
|
9
9
|
> Describe what to test. Watch it happen. Get a pytest script.
|
|
10
10
|
|
|
11
11
|
ScreenForge is an AI-driven UI automation engine that turns natural language into executable test scripts. Unlike record-and-replay tools, you don't perform the actions yourself — the AI does it for you.
|
|
12
12
|
|
|
13
|
-

|
|
13
|
+

|
|
14
14
|
|
|
15
15
|
## Why ScreenForge?
|
|
16
16
|
|
|
@@ -68,6 +68,7 @@ Each step: **inspect → decide → act → verify**. The AI decides, ScreenForg
|
|
|
68
68
|
- **Visual fallback**: When DOM can't locate elements (Canvas, games), VLM parses screenshots
|
|
69
69
|
- **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
|
|
70
70
|
- **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
|
|
71
|
+
- **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
|
|
71
72
|
|
|
72
73
|
## Agent Integration (Claude Code / Cursor / Codex)
|
|
73
74
|
|
|
@@ -108,9 +109,9 @@ Add ScreenForge to your CI pipeline:
|
|
|
108
109
|
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
Results are auto-uploaded as Allure artifacts. See [action.yml](action.yml) for all inputs.
|
|
112
|
+
Results are auto-uploaded as Allure artifacts. See [action.yml](https://github.com/jhinzzz/ScreenForge/blob/main/action.yml) for all inputs.
|
|
112
113
|
|
|
113
|
-
See [Agent Integration Guide](docs/agent_guide.md) for the complete protocol.
|
|
114
|
+
See [Agent Integration Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) for the complete protocol.
|
|
114
115
|
|
|
115
116
|
## Installation (from source)
|
|
116
117
|
|
|
@@ -156,17 +157,18 @@ If ScreenForge generates tests for your project, add this badge to your README:
|
|
|
156
157
|
|
|
157
158
|
| Resource | Description |
|
|
158
159
|
|----------|-------------|
|
|
159
|
-
| [Mobile Setup](docs/mobile-setup.md) | Android & iOS device connection guide |
|
|
160
|
-
| [MCP Setup (3 min)](docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
|
|
161
|
-
| [Agent Guide](docs/agent_guide.md) | Integration protocol for AI Agents |
|
|
162
|
-
| [Capability Matrix](docs/capability-matrix.md) | Supported platforms, actions, and locators |
|
|
163
|
-
| [
|
|
164
|
-
| [
|
|
160
|
+
| [Mobile Setup](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mobile-setup.md) | Android & iOS device connection guide |
|
|
161
|
+
| [MCP Setup (3 min)](https://github.com/jhinzzz/ScreenForge/blob/main/docs/mcp-setup.md) | Connect to Claude Desktop / Cursor / Cline / Claude Code |
|
|
162
|
+
| [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md) | Integration protocol for AI Agents |
|
|
163
|
+
| [Capability Matrix](https://github.com/jhinzzz/ScreenForge/blob/main/docs/capability-matrix.md) | Supported platforms, actions, and locators |
|
|
164
|
+
| [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md) | Live Mirror — watch code + screenshots grow as the test runs |
|
|
165
|
+
| [Workflow Examples](https://github.com/jhinzzz/ScreenForge/tree/main/docs/workflows) | YAML workflow templates |
|
|
166
|
+
| [CHANGELOG](https://github.com/jhinzzz/ScreenForge/blob/main/CHANGELOG.md) | Version history |
|
|
165
167
|
|
|
166
168
|
## Contributing
|
|
167
169
|
|
|
168
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
|
|
170
|
+
See [CONTRIBUTING.md](https://github.com/jhinzzz/ScreenForge/blob/main/CONTRIBUTING.md) for guidelines. Issues and PRs welcome!
|
|
169
171
|
|
|
170
172
|
## License
|
|
171
173
|
|
|
172
|
-
[MIT](LICENSE)
|
|
174
|
+
[MIT](https://github.com/jhinzzz/ScreenForge/blob/main/LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
7
|
import cli.shared as _shared
|
|
8
|
+
from cli.playground_sink import build_sink_from_args, maybe_push_step
|
|
8
9
|
from cli.reporter import (
|
|
9
10
|
_apply_resume_summary,
|
|
10
11
|
_build_action_summary,
|
|
@@ -206,6 +207,18 @@ def run_action_default_mode(
|
|
|
206
207
|
|
|
207
208
|
history_manager.add_step(result["code_lines"], result["action_description"])
|
|
208
209
|
save_to_disk(output_script_path, history_manager.get_current_file_content())
|
|
210
|
+
# ★ Live-mirror bypass (opt-in --playground-sink). join_on_exit=True: a bare
|
|
211
|
+
# --action exits right after, so wait briefly for the last frame to land.
|
|
212
|
+
maybe_push_step(
|
|
213
|
+
build_sink_from_args(args, join_on_exit=True),
|
|
214
|
+
args=args,
|
|
215
|
+
reporter=reporter,
|
|
216
|
+
adapter=adapter,
|
|
217
|
+
action_data=action_data,
|
|
218
|
+
result=result,
|
|
219
|
+
step_index=None, # resolver picks: session counter, or 1 for a bare action
|
|
220
|
+
file_path=output_script_path, # normalized to abs path inside build_step_event
|
|
221
|
+
)
|
|
209
222
|
reporter.emit_event(
|
|
210
223
|
"action_executed",
|
|
211
224
|
step=1,
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import cli.shared as _shared
|
|
6
6
|
from cli.modes.dry_run import _build_resolution_hint, _preview_action_resolution
|
|
7
|
+
from cli.playground_sink import build_sink_from_args, maybe_push_step
|
|
7
8
|
from cli.reporter import (
|
|
8
9
|
_apply_resume_summary,
|
|
9
10
|
_build_reporter,
|
|
@@ -285,6 +286,9 @@ def run_workflow_default_mode(
|
|
|
285
286
|
history_manager = _shared.StepHistoryManager(initial_content=header)
|
|
286
287
|
save_to_disk(output_script_path, header)
|
|
287
288
|
executor = _shared.UIExecutor(device, platform=args.platform)
|
|
289
|
+
# Workflow is one process with many steps (the main use case for live mirror).
|
|
290
|
+
# Build the sink once; join_on_exit=False — the process lives across all steps.
|
|
291
|
+
sink = build_sink_from_args(args, join_on_exit=False)
|
|
288
292
|
|
|
289
293
|
executed_steps = 0
|
|
290
294
|
for index, step in enumerate(workflow.steps, start=1):
|
|
@@ -315,6 +319,17 @@ def run_workflow_default_mode(
|
|
|
315
319
|
result["code_lines"], result["action_description"]
|
|
316
320
|
)
|
|
317
321
|
save_to_disk(output_script_path, history_manager.get_current_file_content())
|
|
322
|
+
# ★ Live-mirror bypass: push this step with its loop counter as step_index.
|
|
323
|
+
maybe_push_step(
|
|
324
|
+
sink,
|
|
325
|
+
args=args,
|
|
326
|
+
reporter=reporter,
|
|
327
|
+
adapter=adapter,
|
|
328
|
+
action_data=action_data,
|
|
329
|
+
result=result,
|
|
330
|
+
step_index=index,
|
|
331
|
+
file_path=output_script_path, # normalized to abs path inside build_step_event
|
|
332
|
+
)
|
|
318
333
|
reporter.emit_event(
|
|
319
334
|
"action_executed",
|
|
320
335
|
step=index,
|
|
@@ -151,6 +151,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
151
151
|
default=7860,
|
|
152
152
|
help="Playground server port (default: 7860)",
|
|
153
153
|
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--playground-sink",
|
|
156
|
+
action="store_true",
|
|
157
|
+
help="Push each step's code + screenshot to a running playground (opt-in; off = zero cost)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--playground-url",
|
|
161
|
+
type=str,
|
|
162
|
+
default="http://127.0.0.1:7860",
|
|
163
|
+
help="Playground base URL for --playground-sink (default: http://127.0.0.1:7860)",
|
|
164
|
+
)
|
|
154
165
|
parser.add_argument(
|
|
155
166
|
"--device-url",
|
|
156
167
|
type=str,
|
|
@@ -270,7 +281,10 @@ def validate_cli_args(args: argparse.Namespace) -> None:
|
|
|
270
281
|
raise ValueError("--action cannot be combined with --goal or --workflow")
|
|
271
282
|
has_demo = bool(getattr(args, "demo", False))
|
|
272
283
|
has_init = bool(getattr(args, "init", False))
|
|
273
|
-
|
|
284
|
+
# --playground starts a standalone server (dispatch.py handles it after this
|
|
285
|
+
# validation, exactly like --init/--demo) — it needs no goal/workflow/action.
|
|
286
|
+
has_playground = bool(getattr(args, "playground", False))
|
|
287
|
+
if has_demo or has_init or has_playground:
|
|
274
288
|
return
|
|
275
289
|
has_session_end = bool(str(getattr(args, "session_end", "")).strip())
|
|
276
290
|
if has_session_end:
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Fire-and-forget visualization sink: short-lived action process → resident playground.
|
|
2
|
+
|
|
3
|
+
Red line (G5): any network error is swallowed silently. A sink push MUST NEVER
|
|
4
|
+
slow down the action or change its exit code — the 0/1 exit code is a contract to
|
|
5
|
+
the agent (see CLAUDE.md). The sink is a bypass observer hung after save_to_disk;
|
|
6
|
+
it does not touch execute_and_record, codegen, or disk persistence.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
import requests # already in requirements.txt (requests==2.32.5) — zero new deps
|
|
14
|
+
from loguru import logger as log
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from cli.session import load_session
|
|
18
|
+
|
|
19
|
+
DEFAULT_PLAYGROUND_URL = "http://127.0.0.1:7860"
|
|
20
|
+
|
|
21
|
+
# Latency ceiling on the contract-protected single-step --action path (red line #1:
|
|
22
|
+
# "never slow the action down"). A reachable-but-slow playground must not tax the
|
|
23
|
+
# action beyond this documented budget. (connect, read) is split so a hung peer
|
|
24
|
+
# can't stall on connect; _JOIN_TIMEOUT is the hard cap the single-step process
|
|
25
|
+
# waits for the last frame to land before sys.exit — kept ≤ read+ε and well under
|
|
26
|
+
# human-perceptible. Worst added latency on --action ≈ _JOIN_TIMEOUT.
|
|
27
|
+
_POST_TIMEOUT = (0.2, 0.25) # (connect, read) seconds
|
|
28
|
+
_JOIN_TIMEOUT = 0.3 # seconds; single-step last-frame grace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PlaygroundStepEvent(BaseModel):
|
|
32
|
+
"""One step pushed to the playground. Shape == the frontend SSE `step` contract."""
|
|
33
|
+
|
|
34
|
+
run_id: str
|
|
35
|
+
step_index: int # ⭐ time-travel seed: data accumulates/replays by this index
|
|
36
|
+
code_lines: list[str] = Field(default_factory=list)
|
|
37
|
+
action_description: str = ""
|
|
38
|
+
action: str = ""
|
|
39
|
+
locator_type: str = ""
|
|
40
|
+
locator_value: str = ""
|
|
41
|
+
extra_value: str = ""
|
|
42
|
+
success: bool = True
|
|
43
|
+
screenshot_b64: str = "" # empty = no screenshot this step (degrade, never crash)
|
|
44
|
+
file_path: str = "" # abs path of the generated test file (for "open in IDE")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PlaygroundSink:
|
|
48
|
+
"""Pushes each step to a running playground, best-effort.
|
|
49
|
+
|
|
50
|
+
Disabled by default: enabled=False means zero cost — no HTTP, no thread, and
|
|
51
|
+
(at the call sites) take_screenshot is never even invoked.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
base_url: str = DEFAULT_PLAYGROUND_URL,
|
|
57
|
+
enabled: bool = False,
|
|
58
|
+
join_on_exit: bool = False,
|
|
59
|
+
):
|
|
60
|
+
self.base_url = base_url.rstrip("/")
|
|
61
|
+
self.enabled = enabled
|
|
62
|
+
# Single-step --action exits the process right after push_step returns;
|
|
63
|
+
# join a short window so the daemon thread's last frame can land (§6 单步收尾).
|
|
64
|
+
self._join_on_exit = join_on_exit
|
|
65
|
+
|
|
66
|
+
def push_step(self, event: PlaygroundStepEvent) -> None:
|
|
67
|
+
"""Best-effort push. Hands off to a daemon thread; the caller returns at
|
|
68
|
+
once (arch#3: never block the action hot path)."""
|
|
69
|
+
if not self.enabled:
|
|
70
|
+
return
|
|
71
|
+
t = threading.Thread(target=self._post, args=(event,), daemon=True)
|
|
72
|
+
t.start()
|
|
73
|
+
if self._join_on_exit:
|
|
74
|
+
# Single-step --action exits right after this returns; wait a bounded
|
|
75
|
+
# grace (≤ _JOIN_TIMEOUT) for the last frame to land. This is the hard
|
|
76
|
+
# ceiling on added latency to the contract path — never grow it past a
|
|
77
|
+
# human-imperceptible budget (HIGH-1 from review: 0.6s was too generous).
|
|
78
|
+
t.join(timeout=_JOIN_TIMEOUT)
|
|
79
|
+
|
|
80
|
+
def _post(self, event: PlaygroundStepEvent) -> None:
|
|
81
|
+
try:
|
|
82
|
+
requests.post(
|
|
83
|
+
f"{self.base_url}/api/step",
|
|
84
|
+
json=event.model_dump(),
|
|
85
|
+
timeout=_POST_TIMEOUT, # (connect, read) split: a hung playground can't stall us
|
|
86
|
+
)
|
|
87
|
+
except Exception as e: # ConnectionError / Timeout / anything — swallow (G5)
|
|
88
|
+
log.debug(f"[playground-sink] skip (playground unreachable): {e}")
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def encode_screenshot(adapter) -> str:
|
|
92
|
+
"""Cross-platform: take_screenshot() -> bytes → base64. Can't grab → '' (degrade).
|
|
93
|
+
|
|
94
|
+
Platform-agnostic on purpose: all three adapters expose take_screenshot()
|
|
95
|
+
(base_adapter.py:17 abstract), so no per-platform branching is needed here.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
png = adapter.take_screenshot()
|
|
99
|
+
return base64.b64encode(png).decode() if png else ""
|
|
100
|
+
except Exception as e:
|
|
101
|
+
log.debug(f"[playground-sink] screenshot skip: {e}")
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_step_event(
|
|
106
|
+
*,
|
|
107
|
+
run_key: str,
|
|
108
|
+
step_index: int,
|
|
109
|
+
action_data: dict,
|
|
110
|
+
result: dict,
|
|
111
|
+
screenshot_b64: str,
|
|
112
|
+
file_path: str = "",
|
|
113
|
+
) -> PlaygroundStepEvent:
|
|
114
|
+
"""MANDATORY single construction point for every step event (code#4).
|
|
115
|
+
|
|
116
|
+
All three entry points (action / workflow / main) build events ONLY through
|
|
117
|
+
here. Adding a field later (e.g. a seed timestamp) is then one edit, not three
|
|
118
|
+
— preventing the P9-style schema split where one call site silently drifts.
|
|
119
|
+
|
|
120
|
+
file_path is normalized to an absolute path HERE (one idiom, one place) rather
|
|
121
|
+
than at each call site, so it happens inside maybe_push_step's G5 try/except.
|
|
122
|
+
Empty stays empty — abspath('') would wrongly yield the cwd, and the frontend
|
|
123
|
+
treats '' as "no openable file" (disables the IDE button), so guard it.
|
|
124
|
+
"""
|
|
125
|
+
return PlaygroundStepEvent(
|
|
126
|
+
run_id=run_key,
|
|
127
|
+
step_index=step_index,
|
|
128
|
+
code_lines=result.get("code_lines", []) or [],
|
|
129
|
+
action_description=result.get("action_description", ""),
|
|
130
|
+
action=action_data.get("action", ""),
|
|
131
|
+
locator_type=action_data.get("locator_type", ""),
|
|
132
|
+
locator_value=action_data.get("locator_value", ""),
|
|
133
|
+
extra_value=action_data.get("extra_value", ""),
|
|
134
|
+
success=bool(result.get("success", True)),
|
|
135
|
+
screenshot_b64=screenshot_b64,
|
|
136
|
+
file_path=os.path.abspath(file_path) if file_path else "",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_sink_from_args(args, *, join_on_exit: bool = False) -> "PlaygroundSink":
|
|
141
|
+
"""Construct a sink from parsed CLI args. Absent flags → disabled (zero cost)."""
|
|
142
|
+
return PlaygroundSink(
|
|
143
|
+
base_url=getattr(args, "playground_url", "") or DEFAULT_PLAYGROUND_URL,
|
|
144
|
+
enabled=bool(getattr(args, "playground_sink", False)),
|
|
145
|
+
join_on_exit=join_on_exit,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def maybe_push_step(
|
|
150
|
+
sink: "PlaygroundSink",
|
|
151
|
+
*,
|
|
152
|
+
args,
|
|
153
|
+
reporter,
|
|
154
|
+
adapter,
|
|
155
|
+
action_data: dict,
|
|
156
|
+
result: dict,
|
|
157
|
+
step_index: int | None = None,
|
|
158
|
+
file_path: str = "",
|
|
159
|
+
) -> None:
|
|
160
|
+
"""The ONE guarded entry point every call site uses (action / workflow / main).
|
|
161
|
+
|
|
162
|
+
Disabled-fast: returns before touching the adapter, so take_screenshot is
|
|
163
|
+
never called and there is zero device I/O or network on the hot path when the
|
|
164
|
+
sink is off. Wrapped in a blanket try/except as a belt-and-suspenders G5
|
|
165
|
+
guard — the bypass observer must never break the action it observes.
|
|
166
|
+
"""
|
|
167
|
+
if not sink.enabled:
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
run_key, resolved_index = resolve_playground_run_key(args, reporter)
|
|
171
|
+
event = build_step_event(
|
|
172
|
+
run_key=run_key,
|
|
173
|
+
step_index=step_index if step_index is not None else resolved_index,
|
|
174
|
+
action_data=action_data,
|
|
175
|
+
result=result,
|
|
176
|
+
screenshot_b64=PlaygroundSink.encode_screenshot(adapter),
|
|
177
|
+
file_path=file_path,
|
|
178
|
+
)
|
|
179
|
+
sink.push_step(event)
|
|
180
|
+
except Exception as e: # never let visualization break the observed action
|
|
181
|
+
log.debug(f"[playground-sink] push skipped: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def resolve_playground_run_key(args, reporter) -> tuple[str, int]:
|
|
185
|
+
"""Return (run_key, step_index) — the cross-process-stable playground timeline key.
|
|
186
|
+
|
|
187
|
+
Root cause (arch#1): run_reporter.py mints run_id as `timestamp_uuid`, unique
|
|
188
|
+
per short-lived process. In agent mode each --action is its own process, so
|
|
189
|
+
using reporter.run_id directly would shatter a 5-step flow into 5 single-step
|
|
190
|
+
buckets and the seed's timeline would be born broken.
|
|
191
|
+
|
|
192
|
+
--session-id present → use session_id as the key (one session = one timeline);
|
|
193
|
+
step_index comes from the session's persisted 'steps' counter (cli/session.py),
|
|
194
|
+
which dispatch.py increments AFTER each successful step, so steps+1 is the
|
|
195
|
+
1-based index of the step about to be pushed.
|
|
196
|
+
No session → a bare --action is inherently single-step: reporter.run_id, index 1.
|
|
197
|
+
"""
|
|
198
|
+
session_id = getattr(args, "session_id", "") or getattr(args, "session_end", "")
|
|
199
|
+
if session_id:
|
|
200
|
+
session = load_session(session_id)
|
|
201
|
+
return session_id, (session.get("steps", 0) + 1 if session else 1)
|
|
202
|
+
return reporter.run_id, 1
|