use car_browser::backend::BrowserBackend;
use car_browser::perception::pipeline::PerceptionPipeline;
use car_browser::perception::ui_map::{ElementSource, IconType, TextSource};
use car_browser::perception::VisionPerceptionPipeline;
use car_browser::ChromiumBackend;
const PAGE_HTML: &str = r##"<!doctype html><html><body style="margin:0;background:#fff">
<button id="b1" aria-label=""><canvas id="lbl" width="100" height="36"></canvas></button>
<button id="b2">☰</button>
<canvas id="free" width="340" height="48"></canvas>
<script>
var a=document.getElementById('lbl').getContext('2d');
a.fillStyle='#000';a.font='24px Helvetica';a.fillText('GOLABEL',6,26);
var b=document.getElementById('free').getContext('2d');
b.fillStyle='#000';b.font='28px Helvetica';b.fillText('CANVASONLY',6,34);
</script>
</body></html>"##;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "launches real Chrome; run with --ignored"]
async fn live_vision_perception() {
let ocr_available = car_vision::is_available();
eprintln!("\n=== car_vision::is_available() = {ocr_available} ===");
let page_path = std::env::temp_dir().join("car-live-perception.html");
std::fs::write(&page_path, PAGE_HTML).expect("write page");
let page_url = format!("file://{}", page_path.display());
let backend = ChromiumBackend::launch_with_viewport(1280, 720)
.await
.expect("launch chrome");
backend.navigate(&page_url).await.expect("navigate");
for _ in 0..40 {
if backend.is_page_loaded().await.unwrap_or(false) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
let ax = backend.get_accessibility_tree().await.expect("ax tree");
let shot = backend.capture_screenshot().await.expect("screenshot");
let vp = backend.get_viewport().expect("viewport");
let url = backend.get_current_url().expect("url");
eprintln!("ax_nodes={}, screenshot_bytes={}", ax.len(), shot.len());
let pipe = VisionPerceptionPipeline::new();
let map = pipe.perceive(&shot, &ax, &url, vp).await.expect("perceive");
eprintln!("--- format_summary ---\n{}", map.format_summary());
eprintln!(
"elements={} text_blocks={}",
map.elements.len(),
map.text_blocks.len()
);
for e in &map.elements {
eprintln!(
" el {} role={:?} name={:?} icon={:?} src={:?} bounds=({:.0},{:.0},{:.0},{:.0}) interactable={}",
e.id, e.role, e.name, e.icon_type, e.source,
e.bounds.x, e.bounds.y, e.bounds.width, e.bounds.height, e.is_interactable()
);
}
for t in &map.text_blocks {
eprintln!(
" text[{:?}] {:?} bounds=({:.0},{:.0},{:.0},{:.0})",
t.source, t.text, t.bounds.x, t.bounds.y, t.bounds.width, t.bounds.height
);
}
let _ = backend.shutdown().await;
let hamburger = map
.elements
.iter()
.find(|e| e.icon_type == Some(IconType::Menu));
assert!(
hamburger.is_some(),
"expected the ☰ button to classify as IconType::Menu"
);
if !ocr_available {
eprintln!(
"\n[SKIP] No OCR backend — skipping OCR label-recovery + invisible-text assertions."
);
return;
}
let recovered = map.elements.iter().find(|e| {
matches!(e.source, ElementSource::Merged { .. })
&& e.name
.as_deref()
.map(|n| n.to_uppercase().contains("GO"))
.unwrap_or(false)
});
assert!(
recovered.is_some(),
"expected OCR to recover the GOLABEL button's name (source Merged)"
);
let invisible = map
.text_blocks
.iter()
.any(|t| t.source == TextSource::Ocr && t.text.to_uppercase().contains("CANVAS"));
assert!(
invisible,
"expected the canvas-only text to surface as an Ocr text block"
);
eprintln!(
"\n[OK] icon classification + OCR label recovery + invisible text all verified live."
);
}