589c5b06fe
- Auto-fixed 70 lint errors (import sorting, aliased errors, datetime.UTC)
- Fixed 85 remaining errors manually:
- E501: wrapped long lines in queen_profiles, catalog, routes_credentials
- F821: added missing TYPE_CHECKING imports for AgentHost, ToolRegistry,
HookContext, HookResult; added runtime imports where needed
- F811: removed duplicate method definitions in queen_lifecycle_tools
- F841/B007: removed unused variables in discovery.py
- W291: removed trailing whitespace in queen nodes
- E402: moved import to top of queen_memory_v2.py
- Fixed AgentRuntime -> AgentHost in example template type annotations
- Reformatted 343 files with ruff format
328 lines
12 KiB
Python
328 lines
12 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Browser Edge Case Test Template
|
|
|
|
This script provides a template for testing and debugging browser tool failures
|
|
on specific websites. Use this to reproduce, isolate, and verify fixes.
|
|
|
|
Usage:
|
|
1. Copy this file: cp test_case.py test_#[number]_[site].py
|
|
2. Fill in the CONFIG section with your test details
|
|
3. Run: uv run python test_#[number]_[site].py
|
|
|
|
Example:
|
|
uv run python test_01_linkedin_scroll.py
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# Add tools to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "tools" / "src"))
|
|
|
|
from gcu.browser.bridge import BeelineBridge
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CONFIG: Fill in these values for your test case
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
TEST_CASE = {
|
|
"number": 1,
|
|
"name": "LinkedIn Nested Scroll Container",
|
|
"site": "https://www.linkedin.com/feed",
|
|
"simple_site": "https://example.com",
|
|
"category": "scroll", # scroll, click, input, snapshot, navigation
|
|
"symptom": "scroll() returns success but page doesn't move",
|
|
}
|
|
|
|
BRIDGE_PORT = 9229
|
|
CONTEXT_NAME = "edge-case-test"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# TEST FUNCTIONS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def test_simple_site(bridge: BeelineBridge, tab_id: int) -> dict:
|
|
"""Test that the tool works on a simple site (baseline)."""
|
|
print("\n--- Baseline Test (Simple Site) ---")
|
|
|
|
await bridge.navigate(tab_id, TEST_CASE["simple_site"], wait_until="load")
|
|
await asyncio.sleep(1)
|
|
|
|
# Adjust this based on category
|
|
if TEST_CASE["category"] == "scroll":
|
|
result = await bridge.scroll(tab_id, "down", 100)
|
|
print(f" Scroll result: {result}")
|
|
return result
|
|
elif TEST_CASE["category"] == "click":
|
|
# Add click test
|
|
pass
|
|
elif TEST_CASE["category"] == "snapshot":
|
|
result = await bridge.snapshot(tab_id, timeout_s=5.0)
|
|
print(f" Snapshot length: {len(result.get('tree', ''))}")
|
|
return result
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
async def test_problematic_site(bridge: BeelineBridge, tab_id: int) -> dict:
|
|
"""Test the tool on the problematic site."""
|
|
print("\n--- Problem Site Test ---")
|
|
|
|
await bridge.navigate(tab_id, TEST_CASE["site"], wait_until="load", timeout_ms=30000)
|
|
await asyncio.sleep(2)
|
|
|
|
# Adjust this based on category
|
|
if TEST_CASE["category"] == "scroll":
|
|
# Get scroll positions before
|
|
before = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
const results = { window: { y: window.scrollY } };
|
|
document.querySelectorAll('*').forEach((el, i) => {
|
|
const style = getComputedStyle(el);
|
|
if ((style.overflowY === 'scroll' || style.overflowY === 'auto') &&
|
|
el.scrollHeight > el.clientHeight) {
|
|
results['el_' + i] = {
|
|
tag: el.tagName,
|
|
scrollTop: el.scrollTop,
|
|
class: el.className.substring(0, 30)
|
|
};
|
|
}
|
|
});
|
|
return results;
|
|
})();
|
|
""",
|
|
)
|
|
print(f" Before scroll: {before.get('result', {})}")
|
|
|
|
# Try to scroll
|
|
result = await bridge.scroll(tab_id, "down", 500)
|
|
print(f" Scroll result: {result}")
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
# Get scroll positions after
|
|
after = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
const results = { window: { y: window.scrollY } };
|
|
document.querySelectorAll('*').forEach((el, i) => {
|
|
const style = getComputedStyle(el);
|
|
if ((style.overflowY === 'scroll' || style.overflowY === 'auto') &&
|
|
el.scrollHeight > el.clientHeight) {
|
|
results['el_' + i] = {
|
|
tag: el.tagName,
|
|
scrollTop: el.scrollTop,
|
|
class: el.className.substring(0, 30)
|
|
};
|
|
}
|
|
});
|
|
return results;
|
|
})();
|
|
""",
|
|
)
|
|
print(f" After scroll: {after.get('result', {})}")
|
|
|
|
# Check if anything changed
|
|
before_data = before.get("result", {}) or {}
|
|
after_data = after.get("result", {}) or {}
|
|
|
|
changed = False
|
|
for key in after_data:
|
|
if key in before_data:
|
|
b_val = before_data[key].get("scrollTop", 0) if isinstance(before_data[key], dict) else 0
|
|
a_val = after_data[key].get("scrollTop", 0) if isinstance(after_data[key], dict) else 0
|
|
if a_val != b_val:
|
|
print(f" ✓ CHANGE DETECTED: {key} scrolled from {b_val} to {a_val}")
|
|
changed = True
|
|
|
|
if not changed:
|
|
print(" ✗ NO CHANGE: Scroll did not affect any container")
|
|
|
|
return {"ok": changed, "scroll_result": result}
|
|
|
|
elif TEST_CASE["category"] == "snapshot":
|
|
start = time.perf_counter()
|
|
try:
|
|
result = await bridge.snapshot(tab_id, timeout_s=15.0)
|
|
elapsed = time.perf_counter() - start
|
|
tree_len = len(result.get("tree", ""))
|
|
print(f" Snapshot completed in {elapsed:.2f}s, {tree_len} chars")
|
|
return {"ok": True, "elapsed": elapsed, "tree_length": tree_len}
|
|
except asyncio.TimeoutError:
|
|
print(" ✗ SNAPSHOT TIMED OUT")
|
|
return {"ok": False, "error": "timeout"}
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
async def detect_root_cause(bridge: BeelineBridge, tab_id: int) -> dict:
|
|
"""Run detection scripts to identify the root cause."""
|
|
print("\n--- Root Cause Detection ---")
|
|
|
|
detections = {}
|
|
|
|
# Detection 1: Nested scrollable containers
|
|
scroll_check = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
const candidates = [];
|
|
document.querySelectorAll('*').forEach(el => {
|
|
const style = getComputedStyle(el);
|
|
if (style.overflow.includes('scroll') || style.overflow.includes('auto')) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 100 && rect.height > 100) {
|
|
candidates.push({
|
|
tag: el.tagName,
|
|
area: rect.width * rect.height,
|
|
class: el.className.substring(0, 30)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
candidates.sort((a, b) => b.area - a.area);
|
|
return {
|
|
count: candidates.length,
|
|
largest: candidates[0]
|
|
};
|
|
})();
|
|
""",
|
|
)
|
|
detections["nested_scroll"] = scroll_check.get("result", {})
|
|
print(f" Nested scroll containers: {detections['nested_scroll']}")
|
|
|
|
# Detection 2: Shadow DOM
|
|
shadow_check = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
const withShadow = [];
|
|
document.querySelectorAll('*').forEach(el => {
|
|
if (el.shadowRoot) {
|
|
withShadow.push(el.tagName);
|
|
}
|
|
});
|
|
return { count: withShadow.length, elements: withShadow.slice(0, 5) };
|
|
})();
|
|
""",
|
|
)
|
|
detections["shadow_dom"] = shadow_check.get("result", {})
|
|
print(f" Shadow DOM: {detections['shadow_dom']}")
|
|
|
|
# Detection 3: iframes
|
|
iframe_check = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
const iframes = document.querySelectorAll('iframe');
|
|
return { count: iframes.length };
|
|
})();
|
|
""",
|
|
)
|
|
detections["iframes"] = iframe_check.get("result", {})
|
|
print(f" iframes: {detections['iframes']}")
|
|
|
|
# Detection 4: DOM size
|
|
dom_check = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
return {
|
|
elements: document.querySelectorAll('*').length,
|
|
body_children: document.body.children.length
|
|
};
|
|
})();
|
|
""",
|
|
)
|
|
detections["dom_size"] = dom_check.get("result", {})
|
|
print(f" DOM size: {detections['dom_size']}")
|
|
|
|
# Detection 5: Framework detection
|
|
framework_check = await bridge.evaluate(
|
|
tab_id,
|
|
"""
|
|
(function() {
|
|
return {
|
|
react: !!document.querySelector('[data-reactroot], [data-reactid]'),
|
|
vue: !!document.querySelector('[data-v-]'),
|
|
angular: !!document.querySelector('[ng-app], [ng-version]')
|
|
};
|
|
})();
|
|
""",
|
|
)
|
|
detections["frameworks"] = framework_check.get("result", {})
|
|
print(f" Frameworks: {detections['frameworks']}")
|
|
|
|
return detections
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def main():
|
|
print("=" * 70)
|
|
print(f"EDGE CASE TEST #{TEST_CASE['number']}: {TEST_CASE['name']}")
|
|
print("=" * 70)
|
|
print(f"Site: {TEST_CASE['site']}")
|
|
print(f"Category: {TEST_CASE['category']}")
|
|
print(f"Symptom: {TEST_CASE['symptom']}")
|
|
|
|
bridge = BeelineBridge()
|
|
|
|
try:
|
|
print("\n--- Starting Bridge ---")
|
|
await bridge.start()
|
|
|
|
# Wait for extension connection
|
|
for i in range(10):
|
|
await asyncio.sleep(1)
|
|
if bridge.is_connected:
|
|
print("✓ Extension connected!")
|
|
break
|
|
print(f"Waiting for extension... ({i + 1}/10)")
|
|
else:
|
|
print("✗ Extension not connected. Ensure Chrome with Beeline extension is running.")
|
|
return
|
|
|
|
# Create browser context
|
|
context = await bridge.create_context(CONTEXT_NAME)
|
|
tab_id = context.get("tabId")
|
|
group_id = context.get("groupId")
|
|
print(f"✓ Created tab: {tab_id}")
|
|
|
|
# Run tests
|
|
baseline_result = await test_simple_site(bridge, tab_id)
|
|
problem_result = await test_problematic_site(bridge, tab_id)
|
|
detections = await detect_root_cause(bridge, tab_id)
|
|
|
|
# Summary
|
|
print("\n" + "=" * 70)
|
|
print("SUMMARY")
|
|
print("=" * 70)
|
|
print(f"Baseline test: {'✓ PASS' if baseline_result.get('ok') else '✗ FAIL'}")
|
|
print(f"Problem test: {'✓ PASS' if problem_result.get('ok') else '✗ FAIL'}")
|
|
print(f"Root cause indicators: {list(k for k, v in detections.items() if v)}")
|
|
|
|
# Cleanup
|
|
print("\n--- Cleanup ---")
|
|
await bridge.destroy_context(group_id)
|
|
print("✓ Context destroyed")
|
|
|
|
finally:
|
|
await bridge.stop()
|
|
print("✓ Bridge stopped")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|