use chromiumoxide::Page;
use super::{eval_bool, eval_string};
use crate::audit::normalized::{
InteractiveFinding, InteractiveFindingKind, InteractiveFindingValues, JourneyStep, JourneyTrace,
};
use crate::error::Result;
use crate::interaction::{live_regions, pointer, stability};
use crate::patterns::JourneyCandidate;
use crate::taxonomy::Severity;
async fn cart_badge_snapshot(page: &Page) -> Option<String> {
eval_string(
page,
r#"(function() {
var el = document.querySelector(
'[class*="cart-count"],[class*="cart-badge"],[data-cart-count],[class*="basket-count"]'
);
return el ? (el.textContent || '').trim() : null;
})()"#,
)
.await
}
async fn focus_in_dialog(page: &Page) -> bool {
eval_bool(
page,
r#"document.activeElement !== null &&
document.activeElement.closest('[role="dialog"],[aria-modal="true"]') !== null"#,
)
.await
.unwrap_or(false)
}
pub async fn test(
page: &Page,
candidate: &JourneyCandidate,
index: usize,
) -> Result<(JourneyTrace, Vec<InteractiveFinding>)> {
let journey_name = format!("add_to_cart_{index}");
let mut trace = JourneyTrace {
journey: journey_name.clone(),
steps: Vec::new(),
};
let mut findings: Vec<InteractiveFinding> = Vec::new();
let observing = live_regions::install(page).await;
let _ = live_regions::drain(page).await;
let badge_before = cart_badge_snapshot(page).await;
trace.steps.push(JourneyStep {
action: "check_baseline".to_string(),
target: None,
focus: None,
result: Some(format!(
"live_observer:{observing}, cart_badge:{badge_before:?}"
)),
snapshot_label: Some("before_click".to_string()),
});
let Some(trigger_id) = candidate.trigger_backend_id else {
return Ok((trace, findings));
};
if let Err(e) = pointer::synthetic_click_backend(page, trigger_id).await {
tracing::warn!("add_to_cart: click on backend node {trigger_id} failed: {e}");
return Ok((trace, findings));
}
trace.steps.push(JourneyStep {
action: "synthetic_click".to_string(),
target: Some(format!("backend_node:{trigger_id}")),
focus: None,
result: None,
snapshot_label: Some("after_click".to_string()),
});
let _ = stability::settle_after_action(page).await;
let events = live_regions::drain(page).await;
let announced = events.iter().any(|e| e.is_announcement());
let badge_after = cart_badge_snapshot(page).await;
let focus_moved_to_dialog = focus_in_dialog(page).await;
let badge_changed = matches!(
(&badge_before, &badge_after),
(Some(before), Some(after)) if before != after
);
trace.steps.push(JourneyStep {
action: "check_feedback".to_string(),
target: None,
focus: None,
result: Some(format!(
"announced:{announced}, live_events:{}, focus_in_dialog:{focus_moved_to_dialog}, \
badge_changed:{badge_changed}",
events.len()
)),
snapshot_label: Some("after_click".to_string()),
});
if announced || focus_moved_to_dialog {
return Ok((trace, findings));
}
if !observing {
trace.steps.push(JourneyStep {
action: "check_feedback".to_string(),
target: None,
focus: None,
result: Some("live_observer_unavailable".to_string()),
snapshot_label: Some("after_click".to_string()),
});
return Ok((trace, findings));
}
if badge_changed {
findings.push(InteractiveFinding::new(
"AddToCart",
InteractiveFindingKind::AddToCartNoStatusAnnouncement,
None,
Severity::High,
journey_name.clone(),
Some("before_click".to_string()),
Some("after_click".to_string()),
InteractiveFindingValues::default(),
));
} else {
findings.push(InteractiveFinding::new(
"AddToCart",
InteractiveFindingKind::AddToCartNoFeedbackDetected,
None,
Severity::Medium,
journey_name.clone(),
Some("before_click".to_string()),
Some("after_click".to_string()),
InteractiveFindingValues::default(),
));
}
Ok((trace, findings))
}