qa-helper-cli 0.1.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.
- qa_helper_cli-0.1.0/PKG-INFO +53 -0
- qa_helper_cli-0.1.0/README.md +43 -0
- qa_helper_cli-0.1.0/pyproject.toml +21 -0
- qa_helper_cli-0.1.0/setup.cfg +4 -0
- qa_helper_cli-0.1.0/src/qa_helper/__init__.py +3 -0
- qa_helper_cli-0.1.0/src/qa_helper/auth.py +84 -0
- qa_helper_cli-0.1.0/src/qa_helper/browser.py +65 -0
- qa_helper_cli-0.1.0/src/qa_helper/cli.py +189 -0
- qa_helper_cli-0.1.0/src/qa_helper/config.py +30 -0
- qa_helper_cli-0.1.0/src/qa_helper/executor.py +329 -0
- qa_helper_cli-0.1.0/src/qa_helper/heal.py +54 -0
- qa_helper_cli-0.1.0/src/qa_helper/interpreter.py +133 -0
- qa_helper_cli-0.1.0/src/qa_helper/llm.py +56 -0
- qa_helper_cli-0.1.0/src/qa_helper/parser.py +131 -0
- qa_helper_cli-0.1.0/src/qa_helper/report.py +202 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/PKG-INFO +53 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/SOURCES.txt +19 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/dependency_links.txt +1 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/entry_points.txt +2 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/requires.txt +3 -0
- qa_helper_cli-0.1.0/src/qa_helper_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qa-helper-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Execute QA test cases with AI-powered browser automation
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.24
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0
|
|
9
|
+
Requires-Dist: browser-use>=0.1
|
|
10
|
+
|
|
11
|
+
# qa-helper-cli
|
|
12
|
+
|
|
13
|
+
Execute AI-generated test cases against your app using browser automation.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install qa-helper-cli
|
|
19
|
+
playwright install chromium
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Authenticate with your QA Helper account
|
|
26
|
+
qa-helper login
|
|
27
|
+
|
|
28
|
+
# List all TCs in a markdown file (no login needed)
|
|
29
|
+
qa-helper testcases.md --list
|
|
30
|
+
|
|
31
|
+
# Run a specific TC
|
|
32
|
+
qa-helper testcases.md --tc TC-001
|
|
33
|
+
|
|
34
|
+
# Run multiple TCs
|
|
35
|
+
qa-helper testcases.md --tc TC-001 TC-003
|
|
36
|
+
|
|
37
|
+
# Run all TCs
|
|
38
|
+
qa-helper testcases.md
|
|
39
|
+
|
|
40
|
+
# Run against a specific app URL
|
|
41
|
+
qa-helper testcases.md --url https://staging.myapp.com
|
|
42
|
+
|
|
43
|
+
# Run with visible browser
|
|
44
|
+
qa-helper testcases.md --headed
|
|
45
|
+
|
|
46
|
+
# Clear credentials
|
|
47
|
+
qa-helper logout
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Python 3.10+
|
|
53
|
+
- A QA Helper account at [qa-helper.xyz](https://www.qa-helper.xyz)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# qa-helper-cli
|
|
2
|
+
|
|
3
|
+
Execute AI-generated test cases against your app using browser automation.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install qa-helper-cli
|
|
9
|
+
playwright install chromium
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Authenticate with your QA Helper account
|
|
16
|
+
qa-helper login
|
|
17
|
+
|
|
18
|
+
# List all TCs in a markdown file (no login needed)
|
|
19
|
+
qa-helper testcases.md --list
|
|
20
|
+
|
|
21
|
+
# Run a specific TC
|
|
22
|
+
qa-helper testcases.md --tc TC-001
|
|
23
|
+
|
|
24
|
+
# Run multiple TCs
|
|
25
|
+
qa-helper testcases.md --tc TC-001 TC-003
|
|
26
|
+
|
|
27
|
+
# Run all TCs
|
|
28
|
+
qa-helper testcases.md
|
|
29
|
+
|
|
30
|
+
# Run against a specific app URL
|
|
31
|
+
qa-helper testcases.md --url https://staging.myapp.com
|
|
32
|
+
|
|
33
|
+
# Run with visible browser
|
|
34
|
+
qa-helper testcases.md --headed
|
|
35
|
+
|
|
36
|
+
# Clear credentials
|
|
37
|
+
qa-helper logout
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Python 3.10+
|
|
43
|
+
- A QA Helper account at [qa-helper.xyz](https://www.qa-helper.xyz)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qa-helper-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Execute QA test cases with AI-powered browser automation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.24",
|
|
13
|
+
"python-dotenv>=1.0",
|
|
14
|
+
"browser-use>=0.1",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
qa-helper = "qa_helper.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Auth module — device code login flow."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
import webbrowser
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .config import load_config, save_config
|
|
10
|
+
|
|
11
|
+
SERVER_URL = "https://www.qa-helper.xyz"
|
|
12
|
+
POLL_INTERVAL = 2 # seconds between polls
|
|
13
|
+
POLL_TIMEOUT = 300 # 5 minutes max wait
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def login() -> None:
|
|
17
|
+
"""Start device code login flow.
|
|
18
|
+
|
|
19
|
+
Opens browser to authenticate with the QA Helper server.
|
|
20
|
+
Saves access token to ~/.qa-helper/config.json on success.
|
|
21
|
+
"""
|
|
22
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
23
|
+
response = await client.post(f"{SERVER_URL}/api/cli/auth/start")
|
|
24
|
+
|
|
25
|
+
if response.status_code != 200:
|
|
26
|
+
print(f"Error: Could not start login ({response.status_code}). Is the server reachable?", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
data = response.json()
|
|
30
|
+
device_code = data["device_code"]
|
|
31
|
+
login_url = data["login_url"]
|
|
32
|
+
|
|
33
|
+
print(f"\nOpening browser for login...")
|
|
34
|
+
print(f"If it didn't open, visit: {login_url}\n")
|
|
35
|
+
webbrowser.open(login_url)
|
|
36
|
+
|
|
37
|
+
print("Waiting for authentication", end="", flush=True)
|
|
38
|
+
|
|
39
|
+
elapsed = 0
|
|
40
|
+
while elapsed < POLL_TIMEOUT:
|
|
41
|
+
await asyncio.sleep(POLL_INTERVAL)
|
|
42
|
+
elapsed += POLL_INTERVAL
|
|
43
|
+
print(".", end="", flush=True)
|
|
44
|
+
|
|
45
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
46
|
+
poll = await client.post(
|
|
47
|
+
f"{SERVER_URL}/api/cli/auth/poll",
|
|
48
|
+
json={"device_code": device_code},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if poll.status_code != 200:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
result = poll.json()
|
|
55
|
+
status = result.get("status")
|
|
56
|
+
|
|
57
|
+
if status == "confirmed":
|
|
58
|
+
access_token = result["access_token"]
|
|
59
|
+
config = load_config()
|
|
60
|
+
config["server_url"] = SERVER_URL
|
|
61
|
+
config["access_token"] = access_token
|
|
62
|
+
save_config(config)
|
|
63
|
+
print(f"\nLogged in successfully.\n")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if status == "expired":
|
|
67
|
+
print(f"\nLogin timed out. Run 'qa-helper login' to try again.", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
# status == "pending" — keep polling
|
|
71
|
+
|
|
72
|
+
print(f"\nLogin timed out after {POLL_TIMEOUT // 60} minutes.", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def logout() -> None:
|
|
77
|
+
"""Clear saved access token from config."""
|
|
78
|
+
config = load_config()
|
|
79
|
+
if not config.get("access_token"):
|
|
80
|
+
print("Not logged in.")
|
|
81
|
+
return
|
|
82
|
+
config.pop("access_token", None)
|
|
83
|
+
save_config(config)
|
|
84
|
+
print("Logged out.")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Browser-use CLI wrapper — thin shell around browser-use subprocess commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BrowserUseCLI:
|
|
7
|
+
"""Wrapper around browser-use CLI commands."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, venv_path: str = ".venv", headed: bool = False):
|
|
10
|
+
self.venv_path = venv_path
|
|
11
|
+
self.headed = headed
|
|
12
|
+
self.activate_cmd = f"source {venv_path}/bin/activate && "
|
|
13
|
+
|
|
14
|
+
def run(self, command: str, capture_output: bool = True) -> dict:
|
|
15
|
+
"""Execute a browser-use command via subprocess."""
|
|
16
|
+
headed_flag = "--headed " if self.headed else ""
|
|
17
|
+
full_cmd = f"{self.activate_cmd} browser-use {headed_flag}{command}"
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
full_cmd,
|
|
20
|
+
shell=True,
|
|
21
|
+
capture_output=capture_output,
|
|
22
|
+
text=True,
|
|
23
|
+
timeout=30,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"command": command,
|
|
28
|
+
"exit_code": result.returncode,
|
|
29
|
+
"stdout": result.stdout.strip() if result.stdout else "",
|
|
30
|
+
"stderr": result.stderr.strip() if result.stderr else "",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def open(self, url: str) -> dict:
|
|
34
|
+
"""Open URL in browser."""
|
|
35
|
+
return self.run(f"open {url}")
|
|
36
|
+
|
|
37
|
+
def state(self) -> dict:
|
|
38
|
+
"""Get current page state."""
|
|
39
|
+
return self.run("state")
|
|
40
|
+
|
|
41
|
+
def click(self, index: int) -> dict:
|
|
42
|
+
"""Click element by index."""
|
|
43
|
+
return self.run(f"click {index}")
|
|
44
|
+
|
|
45
|
+
def input(self, index: int, text: str) -> dict:
|
|
46
|
+
"""Input text into element."""
|
|
47
|
+
# Escape special shell characters in text
|
|
48
|
+
escaped_text = text.replace('"', '\\"').replace("'", "\\'").replace("`", "\\`")
|
|
49
|
+
return self.run(f'input {index} "{escaped_text}"')
|
|
50
|
+
|
|
51
|
+
def upload(self, index: int, file_path: str) -> dict:
|
|
52
|
+
"""Upload file to input element."""
|
|
53
|
+
return self.run(f'upload {index} "{file_path}"')
|
|
54
|
+
|
|
55
|
+
def scroll_down(self) -> dict:
|
|
56
|
+
"""Scroll page down."""
|
|
57
|
+
return self.run("scroll down")
|
|
58
|
+
|
|
59
|
+
def screenshot(self, path: str) -> dict:
|
|
60
|
+
"""Take screenshot."""
|
|
61
|
+
return self.run(f"screenshot {path}")
|
|
62
|
+
|
|
63
|
+
def close(self) -> dict:
|
|
64
|
+
"""Close browser."""
|
|
65
|
+
return self.run("close")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""CLI entry point — argparse-based command dispatcher for qa-helper."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from .parser import parse_tcs_from_markdown
|
|
11
|
+
from .llm import NotLoggedInError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cmd_list(md_file: str) -> None:
|
|
15
|
+
"""Parse and print all TCs from a markdown file."""
|
|
16
|
+
if not os.path.exists(md_file):
|
|
17
|
+
print(f"Error: File not found: {md_file}", file=sys.stderr)
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
tcs = parse_tcs_from_markdown(md_file)
|
|
21
|
+
if not tcs:
|
|
22
|
+
print(f"No TCs found in {md_file}")
|
|
23
|
+
sys.exit(0)
|
|
24
|
+
|
|
25
|
+
print(f"\nFound {len(tcs)} test cases in {md_file}\n")
|
|
26
|
+
for tc in tcs:
|
|
27
|
+
print(f" {tc['id']:8s} [{tc['priority']:6s}] {tc['category']:20s} {tc['scenario']}")
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def cmd_execute(md_file: str, tc_ids: list[str] | None, app_url: str, headed: bool, venv: str) -> None:
|
|
32
|
+
"""Execute test cases from a markdown file."""
|
|
33
|
+
from .config import load_config
|
|
34
|
+
from .executor import execute_tc
|
|
35
|
+
from .interpreter import StepInterpreter
|
|
36
|
+
from .report import generate_html_report
|
|
37
|
+
|
|
38
|
+
if not os.path.exists(md_file):
|
|
39
|
+
print(f"Error: File not found: {md_file}", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
config = load_config()
|
|
43
|
+
|
|
44
|
+
if not config.get("access_token"):
|
|
45
|
+
print("Error: Not logged in. Run: qa-helper login", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
all_tcs = parse_tcs_from_markdown(md_file)
|
|
49
|
+
if not all_tcs:
|
|
50
|
+
print(f"No TCs found in {md_file}")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
print(f"\nFound {len(all_tcs)} TCs in {md_file}")
|
|
54
|
+
|
|
55
|
+
if tc_ids:
|
|
56
|
+
ids_upper = [t.upper() for t in tc_ids]
|
|
57
|
+
tcs = [tc for tc in all_tcs if tc["id"] in ids_upper]
|
|
58
|
+
missing = set(ids_upper) - {tc["id"] for tc in tcs}
|
|
59
|
+
if missing:
|
|
60
|
+
print(f"Error: TCs not found: {', '.join(sorted(missing))}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
else:
|
|
63
|
+
tcs = all_tcs
|
|
64
|
+
|
|
65
|
+
interpreter = StepInterpreter(headed=headed, venv_path=venv, config=config)
|
|
66
|
+
|
|
67
|
+
run_ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
68
|
+
output_dir = os.path.join("qa_run_result", f"run_{run_ts}")
|
|
69
|
+
screenshots_dir = os.path.join(output_dir, "screenshots")
|
|
70
|
+
os.makedirs(screenshots_dir, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
print(f"\nApp URL: {app_url}")
|
|
73
|
+
print(f"TCs: {', '.join(tc['id'] for tc in tcs)}")
|
|
74
|
+
print(f"Screenshots: {screenshots_dir}/")
|
|
75
|
+
print(f"{'=' * 60}")
|
|
76
|
+
|
|
77
|
+
results = []
|
|
78
|
+
for tc in tcs:
|
|
79
|
+
result = await execute_tc(tc, app_url, interpreter, screenshots_dir)
|
|
80
|
+
results.append(result)
|
|
81
|
+
|
|
82
|
+
interpreter.browser.close()
|
|
83
|
+
|
|
84
|
+
passed = sum(1 for r in results if r["status"] == "PASS")
|
|
85
|
+
healed = sum(1 for r in results if r["status"] == "HEALED")
|
|
86
|
+
failed = sum(1 for r in results if r["status"] in ("FAIL", "ERROR"))
|
|
87
|
+
|
|
88
|
+
print(f"\n{'=' * 60}")
|
|
89
|
+
print(" SUMMARY")
|
|
90
|
+
print(f"{'=' * 60}")
|
|
91
|
+
for r in results:
|
|
92
|
+
icon = {"PASS": "PASS", "HEALED": "HEALED", "FAIL": "FAIL"}.get(r["status"], "?")
|
|
93
|
+
print(f" [{icon}] {r['tc_id']:8s} {r['scenario'][:50]}")
|
|
94
|
+
print(f"\n {passed} Pass {healed} Healed {failed} Fail")
|
|
95
|
+
print(f"{'=' * 60}")
|
|
96
|
+
|
|
97
|
+
now = datetime.now().isoformat()
|
|
98
|
+
|
|
99
|
+
merged_selectors: dict = {}
|
|
100
|
+
for r in results:
|
|
101
|
+
for page_path, elements in r.get("selector_map", {}).items():
|
|
102
|
+
if page_path not in merged_selectors:
|
|
103
|
+
merged_selectors[page_path] = []
|
|
104
|
+
merged_selectors[page_path].extend(elements)
|
|
105
|
+
|
|
106
|
+
selectors_file = os.path.join(output_dir, "selectors.json")
|
|
107
|
+
with open(selectors_file, "w") as f:
|
|
108
|
+
json.dump({"app_url": app_url, "timestamp": now, "pages": merged_selectors}, f, indent=2)
|
|
109
|
+
|
|
110
|
+
json_report = os.path.join(output_dir, f"tc_report_{run_ts}.json")
|
|
111
|
+
with open(json_report, "w") as f:
|
|
112
|
+
json.dump({
|
|
113
|
+
"timestamp": now,
|
|
114
|
+
"source_file": md_file,
|
|
115
|
+
"app_url": app_url,
|
|
116
|
+
"summary": {"passed": passed, "healed": healed, "failed": failed},
|
|
117
|
+
"results": results,
|
|
118
|
+
}, f, indent=2)
|
|
119
|
+
|
|
120
|
+
html_report = os.path.join(output_dir, f"tc_report_{run_ts}.html")
|
|
121
|
+
html = generate_html_report(results, {
|
|
122
|
+
"timestamp": now,
|
|
123
|
+
"source_file": md_file,
|
|
124
|
+
"passed": passed,
|
|
125
|
+
"healed": healed,
|
|
126
|
+
"failed": failed,
|
|
127
|
+
})
|
|
128
|
+
with open(html_report, "w") as f:
|
|
129
|
+
f.write(html)
|
|
130
|
+
|
|
131
|
+
print(f"\n JSON report: {json_report}")
|
|
132
|
+
print(f" HTML report: {html_report} <- open this in a browser")
|
|
133
|
+
print(f" Selectors: {selectors_file}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> None:
|
|
137
|
+
"""Main CLI entry point."""
|
|
138
|
+
# Detect subcommand before argparse to avoid conflict with md_file positional
|
|
139
|
+
if len(sys.argv) >= 2 and sys.argv[1] == "login":
|
|
140
|
+
from . import auth
|
|
141
|
+
try:
|
|
142
|
+
asyncio.run(auth.login())
|
|
143
|
+
except NotImplementedError as e:
|
|
144
|
+
print(f"Not yet available: {e}", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if len(sys.argv) >= 2 and sys.argv[1] == "logout":
|
|
149
|
+
from . import auth
|
|
150
|
+
try:
|
|
151
|
+
auth.logout()
|
|
152
|
+
except NotImplementedError as e:
|
|
153
|
+
print(f"Not yet available: {e}", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
parser = argparse.ArgumentParser(
|
|
158
|
+
prog="qa-helper",
|
|
159
|
+
description="Execute QA test cases with AI-powered browser automation",
|
|
160
|
+
usage="qa-helper [login|logout] | qa-helper <file.md> [--list] [--tc TC-001 ...] [--url URL] [--headed]",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument("md_file", help="Path to markdown file containing test cases")
|
|
163
|
+
parser.add_argument("--list", action="store_true", help="List TCs and exit (no server required)")
|
|
164
|
+
parser.add_argument("--tc", nargs="*", dest="tc_ids", metavar="TC_ID", help="Specific TC IDs to run (e.g. TC-001 TC-003)")
|
|
165
|
+
parser.add_argument("--url", default="http://localhost:3000", metavar="URL", help="App URL to test against (default: http://localhost:3000)")
|
|
166
|
+
parser.add_argument("--headed", action="store_true", help="Run browser in visible window")
|
|
167
|
+
parser.add_argument("--venv", default=".venv", metavar="PATH", help="Virtual environment path for browser-use (default: .venv)")
|
|
168
|
+
|
|
169
|
+
args = parser.parse_args()
|
|
170
|
+
|
|
171
|
+
if args.list:
|
|
172
|
+
cmd_list(args.md_file)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
asyncio.run(cmd_execute(
|
|
177
|
+
md_file=args.md_file,
|
|
178
|
+
tc_ids=args.tc_ids,
|
|
179
|
+
app_url=args.url,
|
|
180
|
+
headed=args.headed,
|
|
181
|
+
venv=args.venv,
|
|
182
|
+
))
|
|
183
|
+
except NotLoggedInError as e:
|
|
184
|
+
print(f"\nError: {e}", file=sys.stderr)
|
|
185
|
+
sys.exit(1)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Config management — read/write ~/.qa-helper/config.json."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_config_path() -> Path:
|
|
9
|
+
"""Return path to config file."""
|
|
10
|
+
return Path.home() / ".qa-helper" / "config.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_config() -> dict:
|
|
14
|
+
"""Load config from ~/.qa-helper/config.json. Returns empty dict if not found."""
|
|
15
|
+
path = get_config_path()
|
|
16
|
+
if not path.exists():
|
|
17
|
+
return {}
|
|
18
|
+
try:
|
|
19
|
+
with open(path) as f:
|
|
20
|
+
return json.load(f)
|
|
21
|
+
except (json.JSONDecodeError, OSError):
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_config(config: dict) -> None:
|
|
26
|
+
"""Write config to ~/.qa-helper/config.json, creating directory if needed."""
|
|
27
|
+
path = get_config_path()
|
|
28
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
with open(path, "w") as f:
|
|
30
|
+
json.dump(config, f, indent=2)
|