From 7d79799d11dbef1ad58ff7a37df4d4f518d80eef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 20:30:10 +0000 Subject: [PATCH 1/5] Automate Playwright Chromium installation on first run The README previously required users to manually run 'playwright install chromium' after pip install. This was easy to forget, especially in CI environments. Now: - Both CLI and web interface automatically install Chromium on first run if missing - Provides user-friendly message indicating the installation is in progress - Falls back to manual installation instruction if auto-install fails - Updated README to reflect this new automatic behavior - Removed manual playwright install step from CI/CD example This improves the developer experience by eliminating a common setup step while maintaining the ability to manually reinstall if needed. https://claude.ai/code/session_01GrAGQV1uySMF68GJxLwWFt --- README.md | 10 +++++----- qa_agent/cli.py | 22 ++++++++++++++++++++++ qa_agent/web/__init__.py | 22 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9cae965..d3ac785 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,9 @@ Need targeted tests? Pass plain-English instructions and an LLM generates custom ```bash pip install qa-agent # standard testing (Playwright only) -playwright install chromium # required — downloads browser binaries ``` -Optional extras: +Chromium is installed automatically on first run. Optional extras: ```bash pip install "qa-agent[pdf]" # PDF reports (adds WeasyPrint) @@ -82,7 +81,7 @@ export ANTHROPIC_API_KEY=sk-ant-... # Anthropic (default) export OPENAI_API_KEY=sk-... # OpenAI ``` -> `playwright install chromium` must run once after every fresh install. See [Troubleshooting](#troubleshooting) if anything goes wrong. +> Chromium is installed automatically on the first run. If you need to reinstall it manually, run `playwright install chromium`. --- @@ -382,7 +381,6 @@ Six built-in suites cover keyboard navigation, mouse interaction, form handling, ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} # or OPENAI_API_KEY run: | pip install qa-agent - playwright install chromium qa-agent --output json --output-dir ./qa-results https://staging.example.com - name: Upload Results @@ -413,11 +411,13 @@ Exits with code `1` when critical or high severity issues are found, failing the ### Playwright browser not found +Chromium is installed automatically on first run. If reinstalling is needed: + ```bash playwright install chromium ``` -Must run once after every fresh install. Easy to forget in CI. +If automatic installation fails, run the above command manually. ### Web UI not working diff --git a/qa_agent/cli.py b/qa_agent/cli.py index a6e1224..850f8e6 100644 --- a/qa_agent/cli.py +++ b/qa_agent/cli.py @@ -2,6 +2,7 @@ import argparse import json +import subprocess import sys from pathlib import Path @@ -18,6 +19,25 @@ from .llm_client import LLMProvider +def ensure_chromium_installed() -> None: + """Ensure Chromium browser is installed. Install it if missing.""" + try: + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + p.chromium + except Exception: + print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) + try: + subprocess.run( + [sys.executable, "-m", "playwright", "install", "chromium"], + check=True, + capture_output=False, + ) + except subprocess.CalledProcessError: + print("Failed to install Chromium. Run: playwright install chromium", file=sys.stderr) + sys.exit(2) + + def parse_auth_config(auth_str: str | None, auth_file: str | None) -> AuthConfig | None: """Parse authentication configuration from string or file.""" if auth_file: @@ -49,6 +69,8 @@ def parse_auth_config(auth_str: str | None, auth_file: str | None) -> AuthConfig def main(): """Main entry point for the CLI.""" + ensure_chromium_installed() + parser = argparse.ArgumentParser( description="QA Agent - Automated Exploratory Testing Tool", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/qa_agent/web/__init__.py b/qa_agent/web/__init__.py index 6df69f1..c95551f 100644 --- a/qa_agent/web/__init__.py +++ b/qa_agent/web/__init__.py @@ -5,15 +5,37 @@ a clear, actionable error instead of a bare ``ModuleNotFoundError``. """ +import subprocess import sys +def _ensure_chromium_installed() -> None: + """Ensure Chromium browser is installed. Install it if missing.""" + try: + from playwright.sync_api import sync_playwright + with sync_playwright() as p: + p.chromium + except Exception: + print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) + try: + subprocess.run( + [sys.executable, "-m", "playwright", "install", "chromium"], + check=True, + capture_output=False, + ) + except subprocess.CalledProcessError: + print("Failed to install Chromium. Run: playwright install chromium", file=sys.stderr) + sys.exit(2) + + def serve_web_cli() -> None: """Entry-point wrapper for the ``qa-agent-web`` command. Imports the Flask-based server lazily so that a missing ``flask`` package produces a helpful error message rather than a traceback. """ + _ensure_chromium_installed() + try: import nh3 # noqa: F401 — verify optional dep is present before starting From 338f02fa1a90147ad059e464871110b5d92fce8d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 23 May 2026 20:32:15 +0000 Subject: [PATCH 2/5] Improve error handling for Chromium installation permissions - Capture stderr from playwright install to detect permission errors - Provide specific guidance for permission-denied failures - Suggest PLAYWRIGHT_BROWSERS_PATH environment variable as workaround - Include full error output for other failure types - Helps users troubleshoot installation issues in restricted environments https://claude.ai/code/session_01GrAGQV1uySMF68GJxLwWFt --- qa_agent/cli.py | 22 +++++++++++++++++++--- qa_agent/web/__init__.py | 22 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/qa_agent/cli.py b/qa_agent/cli.py index 850f8e6..cb73696 100644 --- a/qa_agent/cli.py +++ b/qa_agent/cli.py @@ -31,10 +31,26 @@ def ensure_chromium_installed() -> None: subprocess.run( [sys.executable, "-m", "playwright", "install", "chromium"], check=True, - capture_output=False, + capture_output=True, + text=True, ) - except subprocess.CalledProcessError: - print("Failed to install Chromium. Run: playwright install chromium", file=sys.stderr) + except subprocess.CalledProcessError as e: + err_msg = e.stderr.lower() if e.stderr else "" + if "permission" in err_msg or "denied" in err_msg: + print( + "Permission denied installing Chromium. Try:\n" + " playwright install chromium\n" + "Or set PLAYWRIGHT_BROWSERS_PATH to a writable directory:\n" + " export PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers\n" + " qa-agent ", + file=sys.stderr, + ) + else: + print( + f"Failed to install Chromium:\n{e.stderr or 'Unknown error'}\n\n" + "Try manually: playwright install chromium", + file=sys.stderr, + ) sys.exit(2) diff --git a/qa_agent/web/__init__.py b/qa_agent/web/__init__.py index c95551f..09a4587 100644 --- a/qa_agent/web/__init__.py +++ b/qa_agent/web/__init__.py @@ -21,10 +21,26 @@ def _ensure_chromium_installed() -> None: subprocess.run( [sys.executable, "-m", "playwright", "install", "chromium"], check=True, - capture_output=False, + capture_output=True, + text=True, ) - except subprocess.CalledProcessError: - print("Failed to install Chromium. Run: playwright install chromium", file=sys.stderr) + except subprocess.CalledProcessError as e: + err_msg = e.stderr.lower() if e.stderr else "" + if "permission" in err_msg or "denied" in err_msg: + print( + "Permission denied installing Chromium. Try:\n" + " playwright install chromium\n" + "Or set PLAYWRIGHT_BROWSERS_PATH to a writable directory:\n" + " export PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers\n" + " qa-agent-web", + file=sys.stderr, + ) + else: + print( + f"Failed to install Chromium:\n{e.stderr or 'Unknown error'}\n\n" + "Try manually: playwright install chromium", + file=sys.stderr, + ) sys.exit(2) From 120cc9e85aacb5be76b85cd5c69425a41ecfeb2e Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 23 May 2026 20:51:20 +0000 Subject: [PATCH 3/5] Fix chromium installation: proper detection and eliminate code duplication Critical fixes: - Fix broken chromium detection logic that only checked API attribute existence rather than verifying browser binaries are actually installed - Verify chromium by attempting to launch it, catching real 'not found' errors - Extract duplicated installation code to shared playwright_utils module - Add comprehensive test coverage for all installation scenarios Changes: - Create qa_agent/playwright_utils.py with working ensure_chromium_installed() - Update cli.py and web/__init__.py to use shared utility (removes 72 lines of duplication) - Add tests/test_playwright_utils.py with 6 test cases covering: * Already installed (no-op) * Missing + install succeeds * Missing + permission denied * Missing + other install error * Non-browser exceptions are re-raised * Import errors trigger installation This fixes the critical issues identified in code review that prevented the auto-installation feature from working as intended. --- qa_agent/cli.py | 37 +------- qa_agent/playwright_utils.py | 60 +++++++++++++ qa_agent/web/__init__.py | 38 +-------- tests/test_playwright_utils.py | 152 +++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+), 72 deletions(-) create mode 100644 qa_agent/playwright_utils.py create mode 100644 tests/test_playwright_utils.py diff --git a/qa_agent/cli.py b/qa_agent/cli.py index cb73696..b8559b2 100644 --- a/qa_agent/cli.py +++ b/qa_agent/cli.py @@ -2,7 +2,6 @@ import argparse import json -import subprocess import sys from pathlib import Path @@ -17,41 +16,7 @@ TestMode, ) from .llm_client import LLMProvider - - -def ensure_chromium_installed() -> None: - """Ensure Chromium browser is installed. Install it if missing.""" - try: - from playwright.sync_api import sync_playwright - with sync_playwright() as p: - p.chromium - except Exception: - print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) - try: - subprocess.run( - [sys.executable, "-m", "playwright", "install", "chromium"], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - err_msg = e.stderr.lower() if e.stderr else "" - if "permission" in err_msg or "denied" in err_msg: - print( - "Permission denied installing Chromium. Try:\n" - " playwright install chromium\n" - "Or set PLAYWRIGHT_BROWSERS_PATH to a writable directory:\n" - " export PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers\n" - " qa-agent ", - file=sys.stderr, - ) - else: - print( - f"Failed to install Chromium:\n{e.stderr or 'Unknown error'}\n\n" - "Try manually: playwright install chromium", - file=sys.stderr, - ) - sys.exit(2) +from .playwright_utils import ensure_chromium_installed def parse_auth_config(auth_str: str | None, auth_file: str | None) -> AuthConfig | None: diff --git a/qa_agent/playwright_utils.py b/qa_agent/playwright_utils.py new file mode 100644 index 0000000..ab5ef02 --- /dev/null +++ b/qa_agent/playwright_utils.py @@ -0,0 +1,60 @@ +"""Utilities for Playwright browser management.""" + +import subprocess +import sys + + +def ensure_chromium_installed() -> None: + """Ensure Chromium browser is installed. Install it if missing. + + This function verifies that Chromium browser binaries are actually + installed by attempting to launch the browser. If the browser is + not found, it automatically runs 'playwright install chromium'. + + Raises: + SystemExit: With code 2 if installation fails. + """ + try: + from playwright.sync_api import sync_playwright + + # Actually verify chromium is installed by trying to launch it + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + browser.close() + # If we got here, chromium is installed + return + except Exception as e: + # Check if this is a "browser not found" error + err_str = str(e).lower() + if "executable" not in err_str and "not found" not in err_str: + # This is some other error, re-raise it + raise + + # Browser not found, attempt to install + print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) + try: + subprocess.run( + [sys.executable, "-m", "playwright", "install", "chromium"], + check=True, + capture_output=True, + text=True, + ) + print("✓ Chromium installed successfully", file=sys.stderr) + except subprocess.CalledProcessError as e: + err_msg = e.stderr.lower() if e.stderr else "" + if "permission" in err_msg or "denied" in err_msg: + print( + "Permission denied installing Chromium. Try:\n" + " playwright install chromium\n" + "Or set PLAYWRIGHT_BROWSERS_PATH to a writable directory:\n" + " export PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers\n" + " qa-agent ", + file=sys.stderr, + ) + else: + print( + f"Failed to install Chromium:\n{e.stderr or 'Unknown error'}\n\n" + "Try manually: playwright install chromium", + file=sys.stderr, + ) + sys.exit(2) diff --git a/qa_agent/web/__init__.py b/qa_agent/web/__init__.py index 09a4587..eb4caf3 100644 --- a/qa_agent/web/__init__.py +++ b/qa_agent/web/__init__.py @@ -5,43 +5,9 @@ a clear, actionable error instead of a bare ``ModuleNotFoundError``. """ -import subprocess import sys - -def _ensure_chromium_installed() -> None: - """Ensure Chromium browser is installed. Install it if missing.""" - try: - from playwright.sync_api import sync_playwright - with sync_playwright() as p: - p.chromium - except Exception: - print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) - try: - subprocess.run( - [sys.executable, "-m", "playwright", "install", "chromium"], - check=True, - capture_output=True, - text=True, - ) - except subprocess.CalledProcessError as e: - err_msg = e.stderr.lower() if e.stderr else "" - if "permission" in err_msg or "denied" in err_msg: - print( - "Permission denied installing Chromium. Try:\n" - " playwright install chromium\n" - "Or set PLAYWRIGHT_BROWSERS_PATH to a writable directory:\n" - " export PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers\n" - " qa-agent-web", - file=sys.stderr, - ) - else: - print( - f"Failed to install Chromium:\n{e.stderr or 'Unknown error'}\n\n" - "Try manually: playwright install chromium", - file=sys.stderr, - ) - sys.exit(2) +from ..playwright_utils import ensure_chromium_installed def serve_web_cli() -> None: @@ -50,7 +16,7 @@ def serve_web_cli() -> None: Imports the Flask-based server lazily so that a missing ``flask`` package produces a helpful error message rather than a traceback. """ - _ensure_chromium_installed() + ensure_chromium_installed() try: import nh3 # noqa: F401 — verify optional dep is present before starting diff --git a/tests/test_playwright_utils.py b/tests/test_playwright_utils.py new file mode 100644 index 0000000..36c107f --- /dev/null +++ b/tests/test_playwright_utils.py @@ -0,0 +1,152 @@ +"""Tests for qa_agent/playwright_utils.py — chromium installation logic.""" + +from __future__ import annotations + +import subprocess +import sys +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from qa_agent.playwright_utils import ensure_chromium_installed + + +class TestEnsureChromiumInstalled: + """Tests for automatic Chromium installation.""" + + def test_chromium_already_installed(self): + """When chromium is installed, should not attempt to install.""" + mock_browser = MagicMock() + + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw: + mock_p = MagicMock() + mock_p.chromium.launch.return_value = mock_browser + mock_pw.return_value.__enter__.return_value = mock_p + + # Should not raise or print anything + ensure_chromium_installed() + + # Should have tried to launch chromium + mock_p.chromium.launch.assert_called_once_with(headless=True) + # Should have closed the browser + mock_browser.close.assert_called_once() + + def test_chromium_missing_install_succeeds(self, capsys): + """When chromium is missing, should install successfully.""" + # First call raises "executable not found", second call succeeds + mock_browser = MagicMock() + call_count = {"count": 0} + + def launch_side_effect(*args, **kwargs): + call_count["count"] += 1 + if call_count["count"] == 1: + raise Exception("Executable doesn't exist at /path/to/chromium") + return mock_browser + + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ + patch("qa_agent.playwright_utils.subprocess.run") as mock_run: + + mock_p = MagicMock() + mock_p.chromium.launch.side_effect = launch_side_effect + mock_pw.return_value.__enter__.return_value = mock_p + mock_run.return_value = Mock(returncode=0) + + ensure_chromium_installed() + + # Should have called subprocess to install + mock_run.assert_called_once_with( + [sys.executable, "-m", "playwright", "install", "chromium"], + check=True, + capture_output=True, + text=True, + ) + + # Should show installation message + captured = capsys.readouterr() + assert "Installing Chromium" in captured.err + assert "successfully" in captured.err + + def test_chromium_missing_permission_denied(self, capsys): + """When installation fails due to permissions, should show helpful message.""" + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ + patch("qa_agent.playwright_utils.subprocess.run") as mock_run: + + mock_p = MagicMock() + mock_p.chromium.launch.side_effect = Exception("Executable not found") + mock_pw.return_value.__enter__.return_value = mock_p + + # Simulate permission error + error = subprocess.CalledProcessError(1, "playwright") + error.stderr = "Permission denied writing to /usr/local/bin" + mock_run.side_effect = error + + with pytest.raises(SystemExit) as exc: + ensure_chromium_installed() + + assert exc.value.code == 2 + + captured = capsys.readouterr() + assert "Permission denied" in captured.err + assert "PLAYWRIGHT_BROWSERS_PATH" in captured.err + + def test_chromium_missing_install_fails_other_error(self, capsys): + """When installation fails for other reasons, should show error.""" + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ + patch("qa_agent.playwright_utils.subprocess.run") as mock_run: + + mock_p = MagicMock() + mock_p.chromium.launch.side_effect = Exception("Browser executable not found") + mock_pw.return_value.__enter__.return_value = mock_p + + # Simulate generic error + error = subprocess.CalledProcessError(1, "playwright") + error.stderr = "Network timeout downloading chromium" + mock_run.side_effect = error + + with pytest.raises(SystemExit) as exc: + ensure_chromium_installed() + + assert exc.value.code == 2 + + captured = capsys.readouterr() + assert "Failed to install Chromium" in captured.err + assert "Network timeout" in captured.err + + def test_other_exceptions_are_reraised(self): + """Exceptions that aren't 'browser not found' should be re-raised.""" + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw: + mock_p = MagicMock() + # Simulate an unrelated error (no "executable" or "not found" in message) + mock_p.chromium.launch.side_effect = RuntimeError("Network error") + mock_pw.return_value.__enter__.return_value = mock_p + + with pytest.raises(RuntimeError, match="Network error"): + ensure_chromium_installed() + + def test_import_error_triggers_installation(self, capsys): + """When playwright itself has issues, should attempt installation.""" + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ + patch("qa_agent.playwright_utils.subprocess.run") as mock_run: + + # First call raises import-related error, second succeeds + mock_browser = MagicMock() + call_count = {"count": 0} + + def context_side_effect(): + call_count["count"] += 1 + if call_count["count"] == 1: + raise ImportError("Browser executable not found") + + mock_p = MagicMock() + mock_p.chromium.launch.return_value = mock_browser + return mock_p + + mock_pw.return_value.__enter__.side_effect = context_side_effect + mock_run.return_value = Mock(returncode=0) + + ensure_chromium_installed() + + # Should have attempted installation + mock_run.assert_called_once() + captured = capsys.readouterr() + assert "Installing Chromium" in captured.err From 70040a0b3d9b19f6fbca2ff4e007bf8c33f4868e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 23:40:04 +0000 Subject: [PATCH 4/5] feat: add playwright_utils with ensure_chromium_installed; fix unit test suite - Add qa_agent/playwright_utils.py with ensure_chromium_installed() that auto-installs Chromium if the binary is missing; imports sync_playwright at module level so tests can patch it via the module namespace - Move ensure_chromium_installed() call in cli.py to after arg parsing so --help and --version work without touching the browser - Add tests/test_playwright_utils.py covering all installation scenarios - Patch ensure_chromium_installed in _cli_exit_helper.py so exit-code smoke tests run without a real browser - Add autouse conftest fixture that no-ops ensure_chromium_installed for all non-playwright-utils unit tests so the suite passes without Chromium https://claude.ai/code/session_018P7MGqvrRX1e9MmxgYqEAq --- qa_agent/cli.py | 3 ++ qa_agent/playwright_utils.py | 10 +++---- tests/_cli_exit_helper.py | 9 ++++-- tests/conftest.py | 21 +++++++++++++- tests/test_playwright_utils.py | 52 +++++++++++++++++----------------- 5 files changed, 60 insertions(+), 35 deletions(-) diff --git a/qa_agent/cli.py b/qa_agent/cli.py index b8559b2..deb000f 100644 --- a/qa_agent/cli.py +++ b/qa_agent/cli.py @@ -393,6 +393,9 @@ def main(): invocation_context="cli", ) + # Verify Chromium is available before launching the browser + ensure_chromium_installed() + # Run the agent agent = QAAgent(config) diff --git a/qa_agent/playwright_utils.py b/qa_agent/playwright_utils.py index ab5ef02..9393f67 100644 --- a/qa_agent/playwright_utils.py +++ b/qa_agent/playwright_utils.py @@ -3,20 +3,20 @@ import subprocess import sys +from playwright.sync_api import sync_playwright + def ensure_chromium_installed() -> None: """Ensure Chromium browser is installed. Install it if missing. - + This function verifies that Chromium browser binaries are actually installed by attempting to launch the browser. If the browser is not found, it automatically runs 'playwright install chromium'. - + Raises: SystemExit: With code 2 if installation fails. """ try: - from playwright.sync_api import sync_playwright - # Actually verify chromium is installed by trying to launch it with sync_playwright() as p: browser = p.chromium.launch(headless=True) @@ -29,7 +29,7 @@ def ensure_chromium_installed() -> None: if "executable" not in err_str and "not found" not in err_str: # This is some other error, re-raise it raise - + # Browser not found, attempt to install print("Installing Chromium browser (this may take a minute)...", file=sys.stderr) try: diff --git a/tests/_cli_exit_helper.py b/tests/_cli_exit_helper.py index 4883602..6e18a59 100644 --- a/tests/_cli_exit_helper.py +++ b/tests/_cli_exit_helper.py @@ -41,17 +41,20 @@ if scenario in _FINDINGS: session = MagicMock() session.findings_by_severity = _FINDINGS[scenario] - with patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ + with patch("qa_agent.cli.ensure_chromium_installed"), \ + patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ patch("qa_agent.cli.QAAgent.run", return_value=session): _cli_main() elif scenario == "runtime_error": - with patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ + with patch("qa_agent.cli.ensure_chromium_installed"), \ + patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ patch("qa_agent.cli.QAAgent.run", side_effect=RuntimeError("boom")): _cli_main() elif scenario == "keyboard_interrupt": - with patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ + with patch("qa_agent.cli.ensure_chromium_installed"), \ + patch("qa_agent.cli.QAAgent.__init__", return_value=None), \ patch("qa_agent.cli.QAAgent.run", side_effect=KeyboardInterrupt): _cli_main() diff --git a/tests/conftest.py b/tests/conftest.py index 32406c9..e0a9e34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -136,6 +136,25 @@ def factory(): # pytest fixtures # --------------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def _no_chromium_install(request): + """Prevent ensure_chromium_installed from touching real browsers in unit tests. + + Patches the function at every import site (cli, web) so tests that exercise + main() or serve_web_cli() don't attempt to download or launch Chromium. + test_playwright_utils.py tests the function directly and is unaffected because + it imports from qa_agent.playwright_utils, not from these call-site namespaces. + """ + if "test_playwright_utils" in request.fspath.basename: + yield + return + # Patch both the source and the cli call site so importlib.reload() in web + # tests still picks up the mock rather than the real function. + with patch("qa_agent.playwright_utils.ensure_chromium_installed"), \ + patch("qa_agent.cli.ensure_chromium_installed"): + yield + + @pytest.fixture def mock_page(): return _make_mock_page() diff --git a/tests/test_playwright_utils.py b/tests/test_playwright_utils.py index 36c107f..eda9a65 100644 --- a/tests/test_playwright_utils.py +++ b/tests/test_playwright_utils.py @@ -17,15 +17,15 @@ class TestEnsureChromiumInstalled: def test_chromium_already_installed(self): """When chromium is installed, should not attempt to install.""" mock_browser = MagicMock() - + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw: mock_p = MagicMock() mock_p.chromium.launch.return_value = mock_browser mock_pw.return_value.__enter__.return_value = mock_p - + # Should not raise or print anything ensure_chromium_installed() - + # Should have tried to launch chromium mock_p.chromium.launch.assert_called_once_with(headless=True) # Should have closed the browser @@ -36,23 +36,23 @@ def test_chromium_missing_install_succeeds(self, capsys): # First call raises "executable not found", second call succeeds mock_browser = MagicMock() call_count = {"count": 0} - + def launch_side_effect(*args, **kwargs): call_count["count"] += 1 if call_count["count"] == 1: raise Exception("Executable doesn't exist at /path/to/chromium") return mock_browser - + with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ patch("qa_agent.playwright_utils.subprocess.run") as mock_run: - + mock_p = MagicMock() mock_p.chromium.launch.side_effect = launch_side_effect mock_pw.return_value.__enter__.return_value = mock_p mock_run.return_value = Mock(returncode=0) - + ensure_chromium_installed() - + # Should have called subprocess to install mock_run.assert_called_once_with( [sys.executable, "-m", "playwright", "install", "chromium"], @@ -60,7 +60,7 @@ def launch_side_effect(*args, **kwargs): capture_output=True, text=True, ) - + # Should show installation message captured = capsys.readouterr() assert "Installing Chromium" in captured.err @@ -70,21 +70,21 @@ def test_chromium_missing_permission_denied(self, capsys): """When installation fails due to permissions, should show helpful message.""" with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ patch("qa_agent.playwright_utils.subprocess.run") as mock_run: - + mock_p = MagicMock() mock_p.chromium.launch.side_effect = Exception("Executable not found") mock_pw.return_value.__enter__.return_value = mock_p - + # Simulate permission error error = subprocess.CalledProcessError(1, "playwright") error.stderr = "Permission denied writing to /usr/local/bin" mock_run.side_effect = error - + with pytest.raises(SystemExit) as exc: ensure_chromium_installed() - + assert exc.value.code == 2 - + captured = capsys.readouterr() assert "Permission denied" in captured.err assert "PLAYWRIGHT_BROWSERS_PATH" in captured.err @@ -93,21 +93,21 @@ def test_chromium_missing_install_fails_other_error(self, capsys): """When installation fails for other reasons, should show error.""" with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ patch("qa_agent.playwright_utils.subprocess.run") as mock_run: - + mock_p = MagicMock() mock_p.chromium.launch.side_effect = Exception("Browser executable not found") mock_pw.return_value.__enter__.return_value = mock_p - + # Simulate generic error error = subprocess.CalledProcessError(1, "playwright") error.stderr = "Network timeout downloading chromium" mock_run.side_effect = error - + with pytest.raises(SystemExit) as exc: ensure_chromium_installed() - + assert exc.value.code == 2 - + captured = capsys.readouterr() assert "Failed to install Chromium" in captured.err assert "Network timeout" in captured.err @@ -119,7 +119,7 @@ def test_other_exceptions_are_reraised(self): # Simulate an unrelated error (no "executable" or "not found" in message) mock_p.chromium.launch.side_effect = RuntimeError("Network error") mock_pw.return_value.__enter__.return_value = mock_p - + with pytest.raises(RuntimeError, match="Network error"): ensure_chromium_installed() @@ -127,25 +127,25 @@ def test_import_error_triggers_installation(self, capsys): """When playwright itself has issues, should attempt installation.""" with patch("qa_agent.playwright_utils.sync_playwright") as mock_pw, \ patch("qa_agent.playwright_utils.subprocess.run") as mock_run: - + # First call raises import-related error, second succeeds mock_browser = MagicMock() call_count = {"count": 0} - + def context_side_effect(): call_count["count"] += 1 if call_count["count"] == 1: raise ImportError("Browser executable not found") - + mock_p = MagicMock() mock_p.chromium.launch.return_value = mock_browser return mock_p - + mock_pw.return_value.__enter__.side_effect = context_side_effect mock_run.return_value = Mock(returncode=0) - + ensure_chromium_installed() - + # Should have attempted installation mock_run.assert_called_once() captured = capsys.readouterr() From 12a81bbb310624131c91f3f385b866ed97fbea96 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 23:55:25 +0000 Subject: [PATCH 5/5] fix: move ensure_chromium_installed after arg parsing; remove from web CLI https://claude.ai/code/session_018P7MGqvrRX1e9MmxgYqEAq --- qa_agent/cli.py | 2 -- qa_agent/web/__init__.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/qa_agent/cli.py b/qa_agent/cli.py index deb000f..2e4af4f 100644 --- a/qa_agent/cli.py +++ b/qa_agent/cli.py @@ -50,8 +50,6 @@ def parse_auth_config(auth_str: str | None, auth_file: str | None) -> AuthConfig def main(): """Main entry point for the CLI.""" - ensure_chromium_installed() - parser = argparse.ArgumentParser( description="QA Agent - Automated Exploratory Testing Tool", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/qa_agent/web/__init__.py b/qa_agent/web/__init__.py index eb4caf3..6df69f1 100644 --- a/qa_agent/web/__init__.py +++ b/qa_agent/web/__init__.py @@ -7,8 +7,6 @@ import sys -from ..playwright_utils import ensure_chromium_installed - def serve_web_cli() -> None: """Entry-point wrapper for the ``qa-agent-web`` command. @@ -16,8 +14,6 @@ def serve_web_cli() -> None: Imports the Flask-based server lazily so that a missing ``flask`` package produces a helpful error message rather than a traceback. """ - ensure_chromium_installed() - try: import nh3 # noqa: F401 — verify optional dep is present before starting