use chromiumoxide::Page;
use crate::audit::normalized::{
InteractiveFinding, InteractiveFindingKind, InteractiveFindingValues, JourneyStep, JourneyTrace,
};
use crate::error::Result;
use crate::interaction::{focus, pointer, stability};
use crate::patterns::JourneyCandidate;
use crate::taxonomy::Severity;
pub async fn test(
page: &Page,
candidate: &JourneyCandidate,
index: usize,
) -> Result<(JourneyTrace, Vec<InteractiveFinding>)> {
let journey_name = format!("skip_link_{index}");
let mut trace = JourneyTrace {
journey: journey_name.clone(),
steps: Vec::new(),
};
let mut findings: Vec<InteractiveFinding> = Vec::new();
let trigger_id = match candidate.trigger_backend_id {
Some(id) => id,
None => return Ok((trace, findings)),
};
if let Err(e) = pointer::synthetic_click_backend(page, trigger_id).await {
tracing::warn!("skip_link: 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_skip_click".to_string()),
});
let _ = stability::settle_after_action(page).await;
let snap = focus::capture_focus(page).await?;
let focus_selector = snap.selector.clone();
let focus_on_body = focus_selector.is_none()
|| focus_selector
.as_deref()
.map(|s| {
let low = s.to_lowercase();
low == "body" || low == "html"
})
.unwrap_or(false);
trace.steps.push(JourneyStep {
action: "check_focus".to_string(),
target: None,
focus: focus_selector.clone(),
result: Some(if focus_on_body {
"focus_not_moved".to_string()
} else {
"focus_moved".to_string()
}),
snapshot_label: Some("after_skip_link".to_string()),
});
if focus_on_body {
findings.push(InteractiveFinding::new(
"SkipLink",
InteractiveFindingKind::SkipLinkFocusNotMoved,
None,
Severity::High,
journey_name,
None,
Some("after_skip_link".to_string()),
InteractiveFindingValues::default(),
));
}
Ok((trace, findings))
}