Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/webwright/run/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from webwright.environments import get_environment
from webwright.models import get_model
from webwright.utils.serialize import UNSET, recursive_merge
from webwright.run.doctor import run_doctor


DEFAULT_CONFIGS = ["base.yaml", "model_openai.yaml"]
Expand Down Expand Up @@ -125,20 +126,32 @@ def run_one(
result["_output_dir"] = str(resolved_output_dir)
if close_exception is not None:
result["_close_exception"] = str(close_exception)
console.print(result.get("final_response") or result.get("submission") or "Task finished.")
console.print(
result.get("final_response") or result.get("submission") or "Task finished."
)
if run_exception is not None:
raise run_exception
return result


@app.command()
def main(
task: str = typer.Option(..., "-t", "--task", help="Natural language task description."),
task_id: str | None = typer.Option(None, "--task-id", help="Optional identifier used in the output directory name."),
start_url: str | None = typer.Option(None, "--start-url", help="Optional starting URL for the task."),
task: str = typer.Option(
..., "-t", "--task", help="Natural language task description."
),
task_id: str | None = typer.Option(
None, "--task-id", help="Optional identifier used in the output directory name."
),
start_url: str | None = typer.Option(
None, "--start-url", help="Optional starting URL for the task."
),
config_spec: list[str] = typer.Option(DEFAULT_CONFIGS, "-c", "--config"),
output_dir: Path | None = typer.Option(None, "-o", "--output-dir"),
debug: bool = typer.Option(False, "--debug", help="Launch headed local Playwright with devtools and keep it open for inspection."),
debug: bool = typer.Option(
False,
"--debug",
help="Launch headed local Playwright with devtools and keep it open for inspection.",
),
) -> Any:
return run_one(
task=task,
Expand All @@ -150,5 +163,13 @@ def main(
)


@app.command()
def doctor():
"""
Validate local Webwright setup.
"""
run_doctor()

Comment on lines +166 to +172

if __name__ == "__main__":
app()
147 changes: 147 additions & 0 deletions src/webwright/run/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from __future__ import annotations

import os
import subprocess
import sys
from importlib.util import find_spec
from pathlib import Path
from rich.console import Console
from rich.table import Table

console = Console()


def check_python():
version = sys.version_info

if version >= (3, 10):
return True, f"Python {version.major}.{version.minor}"

return False, ("Python 3.10+ required\nFix: install Python 3.10 or newer")


def check_playwright():
if find_spec("playwright") is not None:
return True, "playwright installed"

return False, ("playwright not installed\nFix: pip install playwright")


def check_chromium():
try:
result = subprocess.run(
["playwright", "install", "--dry-run"],
capture_output=True,
text=True,
)
Comment on lines +32 to +36

if result.returncode == 0:
return True, "chromium available"

return False, ("chromium missing\nFix: playwright install chromium")

except Exception as e:
return False, str(e)


def check_screenshot():
try:
from playwright.sync_api import sync_playwright

screenshot_path = Path("doctor_test.png")

with sync_playwright() as p:
browser = p.chromium.launch(headless=True)

page = browser.new_page()

page.set_content("<h1>Webwright Doctor</h1>")

page.screenshot(path=str(screenshot_path))

browser.close()

if screenshot_path.exists():
screenshot_path.unlink(missing_ok=True)

return True, "screenshot capture working"

return False, "screenshot file was not created"

except Exception:
return False, (
"unable to launch Chromium for screenshot validation\n"
"Fix: playwright install"
)
Comment on lines +71 to +75


def check_openai_key():
if os.getenv("OPENAI_API_KEY"):
return True, "OPENAI_API_KEY found"

return False, (
"OPENAI_API_KEY missing\nFix: set the OPENAI_API_KEY environment variable"
)


def check_plugin_manifests():
claude = Path(".claude-plugin/plugin.json")
codex = Path(".codex-plugin/plugin.json")

missing = []

if not claude.exists():
missing.append("Claude")

if not codex.exists():
missing.append("Codex")

if not missing:
return True, "plugin manifests found"

return False, (
f"missing plugin manifests: {', '.join(missing)}\n"
"Fix: configure Claude/Codex plugins"
)


CHECKS = [
("Python", check_python),
("Playwright", check_playwright),
("Chromium", check_chromium),
("Screenshot", check_screenshot),
("OpenAI Key", check_openai_key),
("Plugins", check_plugin_manifests),
]


def run_doctor():
table = Table(title="Webwright Doctor")

table.add_column("Check")
table.add_column("Status")
table.add_column("Details")

passed = 0

for name, fn in CHECKS:
ok, message = fn()

status = "PASS" if ok else "FAIL"

table.add_row(
name,
status,
message,
)

if ok:
passed += 1

console.print(table)

console.print(f"\n{passed}/{len(CHECKS)} checks passed")


if __name__ == "__main__":
run_doctor()
Comment on lines +141 to +147
94 changes: 94 additions & 0 deletions tests/unit/test_doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from pathlib import Path

from webwright.run.doctor import (
check_chromium,
check_openai_key,
check_playwright,
check_plugin_manifests,
check_python,
check_screenshot,
)


def test_check_python():
ok, message = check_python()

assert isinstance(ok, bool)
assert isinstance(message, str)


def test_check_playwright():
ok, message = check_playwright()

assert isinstance(ok, bool)
assert isinstance(message, str)

Comment on lines +20 to +25

def test_check_chromium():
ok, message = check_chromium()

assert isinstance(ok, bool)
assert isinstance(message, str)

Comment on lines +27 to +32

def test_check_screenshot():
ok, message = check_screenshot()

assert isinstance(ok, bool)
assert isinstance(message, str)

Comment on lines +34 to +39

def test_check_openai_key_exists(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "test-key")

ok, message = check_openai_key()

assert ok is True
assert "found" in message


def test_check_openai_key_missing(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)

ok, message = check_openai_key()

assert ok is False
assert "missing" in message


def test_plugin_manifests_exist(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)

claude_dir = tmp_path / ".claude-plugin"
codex_dir = tmp_path / ".codex-plugin"

claude_dir.mkdir()
codex_dir.mkdir()

(claude_dir / "plugin.json").write_text("{}")
(codex_dir / "plugin.json").write_text("{}")

ok, message = check_plugin_manifests()

assert ok is True
assert "found" in message


def test_plugin_manifests_missing(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)

ok, message = check_plugin_manifests()

assert ok is False
assert "missing" in message


def test_screenshot_file_cleanup():
screenshot_path = Path("doctor_test.png")

if screenshot_path.exists():
screenshot_path.unlink()

check_screenshot()

assert not screenshot_path.exists()
Comment on lines +86 to +94