662. Config schema is valid and declares chrome dependency
773. Snapshot hook skips when disabled
884. 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
1214import json
13- import os
14- import subprocess
15- import sys
1615import tempfile
1716from pathlib import Path
1817
18+ import sys
19+
1920import 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+
2125from 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
3539 raise FileNotFoundError (f"Snapshot hook not found in { PLUGIN_DIR } " )
3640SNAPSHOT_HOOK = _SNAPSHOT_HOOK
3741TEST_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
4088class 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" )
157204class 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.\n Stdout: { stdout } \n Stderr: { stderr } "
243+ )
244+ assert result ["status" ] == "succeeded" , (
245+ f"Hook should succeed: { result } \n Stderr: { 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"\x89 PNG" , "Initial screenshot should be valid PNG"
172275
173276
174277if __name__ == "__main__" :
0 commit comments