Skip to content

Commit c820819

Browse files
committed
Rewrite Claude plugin tests to use base.test_utils and add real claudechrome integration test
- All 4 test files now import get_plugin_dir, get_hook_script, parse_jsonl_output, run_hook from base.test_utils instead of chrome_test_helpers (reducing coupling) - Replace manual subprocess calls + inline JSONL parsing with run_hook() and parse_jsonl_output() helpers - claudechrome: replace placeholder test_full_pipeline_with_chrome_session with real integration test using chrome_session context manager, httpserver test page, and full output verification - claudechrome: add test_snapshot_hook_fails_without_chrome_session - Integration test class uses @pytest.mark.usefixtures("ensure_chrome_test_prereqs") instead of module-level pytestmark so unit tests run without Chrome https://claude.ai/code/session_013zgn8SbbiwJJAxC3UzAHZ6
1 parent 71bc652 commit c820819

4 files changed

Lines changed: 294 additions & 350 deletions

File tree

abx_plugins/plugins/claudechrome/tests/test_claudechrome.py

Lines changed: 175 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@
66
2. Config schema is valid and declares chrome dependency
77
3. Snapshot hook skips when disabled
88
4. Snapshot hook fails gracefully when API key is missing
9-
5. Templates exist
9+
5. Snapshot hook fails gracefully without Chrome session
10+
6. Templates exist
11+
7. Full integration: launches Chrome, runs Claude computer-use, produces output files
1012
"""
1113

1214
import json
13-
import os
14-
import subprocess
15-
import sys
1615
import tempfile
1716
from pathlib import Path
1817

18+
import sys
19+
1920
import pytest
2021

22+
sys.path.append(str(Path(__file__).resolve().parent.parent.parent))
23+
from base.test_utils import get_plugin_dir, get_hook_script, parse_jsonl_output, run_hook
24+
2125
from abx_plugins.plugins.chrome.tests.chrome_test_helpers import (
22-
get_plugin_dir,
23-
get_hook_script,
26+
get_test_env,
27+
chrome_session,
2428
)
2529

2630

@@ -35,6 +39,50 @@
3539
raise FileNotFoundError(f"Snapshot hook not found in {PLUGIN_DIR}")
3640
SNAPSHOT_HOOK = _SNAPSHOT_HOOK
3741
TEST_URL = "https://example.com"
42+
CHROME_STARTUP_TIMEOUT_SECONDS = 45
43+
44+
CLAUDECHROME_TEST_PAGE_HTML = """
45+
<!doctype html>
46+
<html>
47+
<head>
48+
<meta charset="utf-8" />
49+
<title>Claude Chrome Test Page</title>
50+
<style>
51+
body { margin: 20px; font-family: sans-serif; }
52+
.hidden-content { display: none; }
53+
#expand-btn {
54+
padding: 10px 20px;
55+
font-size: 16px;
56+
cursor: pointer;
57+
background: #4a90d9;
58+
color: white;
59+
border: none;
60+
border-radius: 4px;
61+
}
62+
</style>
63+
</head>
64+
<body>
65+
<h1>Test Page for Claude Chrome</h1>
66+
<p>This page has a button that reveals hidden content.</p>
67+
<button id="expand-btn" onclick="document.getElementById('hidden').style.display='block'; this.textContent='Expanded!';">
68+
Show More
69+
</button>
70+
<div id="hidden" class="hidden-content">
71+
<p>This content was hidden and is now visible after clicking the button.</p>
72+
</div>
73+
</body>
74+
</html>
75+
""".strip()
76+
77+
78+
@pytest.fixture
79+
def claudechrome_test_url(httpserver):
80+
"""Serve a test page with a 'Show More' button for Claude to click."""
81+
httpserver.expect_request("/").respond_with_data(
82+
CLAUDECHROME_TEST_PAGE_HTML,
83+
content_type="text/html",
84+
)
85+
return httpserver.url_for("/")
3886

3987

4088
class TestClaudeChromePlugin:
@@ -55,7 +103,6 @@ def test_hook_runs_at_correct_priorities(self):
55103

56104
def test_snapshot_runs_after_infiniscroll_before_singlefile(self):
57105
"""Snapshot hook priority 47 is after infiniscroll (45) and before singlefile (50)."""
58-
# Extract priority number from hook filename
59106
name = SNAPSHOT_HOOK.name
60107
priority = int(name.split("__")[1].split("_")[0])
61108
assert 45 < priority < 50, (
@@ -93,82 +140,138 @@ def test_templates_exist(self):
93140

94141
def test_snapshot_hook_skips_when_disabled(self):
95142
"""Snapshot hook should skip when CLAUDECHROME_ENABLED=false."""
96-
env = os.environ.copy()
97-
env["SNAP_DIR"] = tempfile.mkdtemp()
143+
env = get_test_env()
98144
env["CLAUDECHROME_ENABLED"] = "false"
99-
# Ensure NODE_MODULES_DIR is set so node can find puppeteer-core
100-
if "NODE_MODULES_DIR" not in env:
101-
from abx_plugins.plugins.chrome.tests.chrome_test_helpers import NODE_MODULES_DIR
102-
if NODE_MODULES_DIR:
103-
env["NODE_MODULES_DIR"] = str(NODE_MODULES_DIR)
104-
105-
result = subprocess.run(
106-
[
107-
"node",
108-
str(SNAPSHOT_HOOK),
109-
"--url", TEST_URL,
110-
"--snapshot-id", "test-snapshot",
111-
],
112-
capture_output=True,
113-
text=True,
114-
env=env,
115-
timeout=30,
116-
)
117145

118-
assert result.returncode == 0, f"Hook failed: {result.stderr}"
119-
assert "skipped" in result.stdout
146+
with tempfile.TemporaryDirectory() as tmpdir:
147+
env["SNAP_DIR"] = tmpdir
148+
returncode, stdout, stderr = run_hook(
149+
SNAPSHOT_HOOK, TEST_URL, "test-snapshot",
150+
cwd=tmpdir, env=env, timeout=30,
151+
)
152+
153+
assert returncode == 0, f"Hook failed: {stderr}"
154+
result = parse_jsonl_output(stdout)
155+
assert result is not None, f"Expected JSONL output, got: {stdout}"
156+
assert result["status"] == "skipped"
120157

121158
def test_snapshot_hook_fails_without_api_key(self):
122159
"""Snapshot hook should fail when ANTHROPIC_API_KEY is not set."""
123-
env = os.environ.copy()
124-
env["SNAP_DIR"] = tempfile.mkdtemp()
160+
env = get_test_env()
125161
env["CLAUDECHROME_ENABLED"] = "true"
126162
env.pop("ANTHROPIC_API_KEY", None)
127-
if "NODE_MODULES_DIR" not in env:
128-
from abx_plugins.plugins.chrome.tests.chrome_test_helpers import NODE_MODULES_DIR
129-
if NODE_MODULES_DIR:
130-
env["NODE_MODULES_DIR"] = str(NODE_MODULES_DIR)
131-
132-
result = subprocess.run(
133-
[
134-
"node",
135-
str(SNAPSHOT_HOOK),
136-
"--url", TEST_URL,
137-
"--snapshot-id", "test-snapshot",
138-
],
139-
capture_output=True,
140-
text=True,
141-
env=env,
142-
timeout=30,
143-
)
144163

145-
assert result.returncode == 1
146-
records = [
147-
json.loads(line)
148-
for line in result.stdout.strip().split("\n")
149-
if line.strip().startswith("{")
150-
]
151-
assert records
152-
assert records[-1]["type"] == "ArchiveResult"
153-
assert records[-1]["status"] == "failed"
154-
assert "ANTHROPIC_API_KEY" in records[-1]["output_str"]
164+
with tempfile.TemporaryDirectory() as tmpdir:
165+
env["SNAP_DIR"] = tmpdir
166+
returncode, stdout, stderr = run_hook(
167+
SNAPSHOT_HOOK, TEST_URL, "test-snapshot",
168+
cwd=tmpdir, env=env, timeout=30,
169+
)
170+
171+
assert returncode == 1
172+
result = parse_jsonl_output(stdout)
173+
assert result is not None, f"Expected JSONL output, got: {stdout}"
174+
assert result["status"] == "failed"
175+
assert "ANTHROPIC_API_KEY" in result["output_str"]
155176

177+
def test_snapshot_hook_fails_without_chrome_session(self):
178+
"""Snapshot hook should fail gracefully when no Chrome session exists."""
179+
env = get_test_env()
180+
env["CLAUDECHROME_ENABLED"] = "true"
181+
env["ANTHROPIC_API_KEY"] = "sk-ant-test-key"
182+
183+
with tempfile.TemporaryDirectory() as tmpdir:
184+
env["SNAP_DIR"] = tmpdir
185+
returncode, stdout, stderr = run_hook(
186+
SNAPSHOT_HOOK, TEST_URL, "test-no-chrome",
187+
cwd=tmpdir, env=env, timeout=30,
188+
)
189+
190+
assert returncode != 0, "Should fail when no Chrome session exists"
191+
# Hook may crash before emitting JSONL (puppeteer not loaded) or
192+
# emit a failed ArchiveResult — either is acceptable
193+
result = parse_jsonl_output(stdout)
194+
if result is not None:
195+
assert result["status"] == "failed"
196+
else:
197+
err_lower = stderr.lower()
198+
assert any(x in err_lower for x in ["chrome", "cdp", "puppeteer", "module"]), (
199+
f"Should mention chrome/CDP/puppeteer in error: {stderr}"
200+
)
156201

202+
203+
@pytest.mark.usefixtures("ensure_chrome_test_prereqs")
157204
class TestClaudeChromeIntegration:
158-
"""Integration tests requiring a Chrome session and API key.
159-
160-
These tests require:
161-
- Chrome session running (chrome plugin)
162-
- ANTHROPIC_API_KEY set
163-
- Claude for Chrome extension installed
164-
"""
165-
166-
def test_full_pipeline_with_chrome_session(self):
167-
"""Full pipeline: connect to Chrome, run Claude, capture output."""
168-
# This test is a placeholder - it requires a running Chrome session
169-
# which is set up by the chrome plugin during actual crawls.
170-
# It's included here for documentation and future CI with Chrome.
171-
pass
205+
"""Integration tests requiring Chrome session and ANTHROPIC_API_KEY."""
206+
207+
def test_full_pipeline_with_chrome_session(self, claudechrome_test_url):
208+
"""Full pipeline: launch Chrome, run Claude computer-use, verify output."""
209+
with tempfile.TemporaryDirectory() as tmpdir:
210+
with chrome_session(
211+
Path(tmpdir),
212+
crawl_id="test-claudechrome",
213+
snapshot_id="snap-claudechrome",
214+
test_url=claudechrome_test_url,
215+
timeout=CHROME_STARTUP_TIMEOUT_SECONDS,
216+
) as (chrome_launch_process, chrome_pid, snapshot_chrome_dir, env):
217+
# Create claudechrome output directory (sibling to chrome)
218+
output_dir = snapshot_chrome_dir.parent / "claudechrome"
219+
output_dir.mkdir()
220+
221+
# Configure claudechrome
222+
env["CLAUDECHROME_ENABLED"] = "true"
223+
env["CLAUDECHROME_MAX_ACTIONS"] = "3"
224+
env["CLAUDECHROME_TIMEOUT"] = "60"
225+
env["CLAUDECHROME_MODEL"] = "haiku"
226+
env["CLAUDECHROME_PROMPT"] = (
227+
"Look at the page. If you see a 'Show More' button, click it. "
228+
"Report what you did."
229+
)
230+
231+
returncode, stdout, stderr = run_hook(
232+
SNAPSHOT_HOOK,
233+
claudechrome_test_url,
234+
"snap-claudechrome",
235+
cwd=str(output_dir),
236+
env=env,
237+
timeout=120,
238+
)
239+
240+
result = parse_jsonl_output(stdout)
241+
assert result is not None, (
242+
f"Expected JSONL output.\nStdout: {stdout}\nStderr: {stderr}"
243+
)
244+
assert result["status"] == "succeeded", (
245+
f"Hook should succeed: {result}\nStderr: {stderr}"
246+
)
247+
assert returncode == 0, f"Hook failed (rc={returncode}): {stderr}"
248+
249+
# Verify output files were created
250+
assert (output_dir / "conversation.json").exists(), (
251+
f"conversation.json should exist. Files: {list(output_dir.iterdir())}"
252+
)
253+
assert (output_dir / "conversation.txt").exists(), (
254+
"conversation.txt should exist"
255+
)
256+
assert (output_dir / "screenshot_initial.png").exists(), (
257+
"screenshot_initial.png should exist"
258+
)
259+
assert (output_dir / "screenshot_final.png").exists(), (
260+
"screenshot_final.png should exist"
261+
)
262+
263+
# Verify conversation.json structure
264+
conversation_data = json.loads(
265+
(output_dir / "conversation.json").read_text()
266+
)
267+
assert conversation_data["url"] == claudechrome_test_url
268+
assert conversation_data["success"] is True
269+
assert "conversation" in conversation_data
270+
assert conversation_data["actionCount"] >= 0
271+
272+
# Verify screenshots are valid PNGs (start with PNG magic bytes)
273+
initial_png = (output_dir / "screenshot_initial.png").read_bytes()
274+
assert initial_png[:4] == b"\x89PNG", "Initial screenshot should be valid PNG"
172275

173276

174277
if __name__ == "__main__":

0 commit comments

Comments
 (0)