pub mod disclosure_journey;
pub mod evaluate;
pub mod form_error;
pub mod link_inventory;
pub mod menu_journey;
pub mod modal_journey;
pub mod skip_link;
pub mod spa_navigation;
pub mod tab_walk;
pub mod tabs_journey;
use std::time::Instant;
use chromiumoxide::Page;
use crate::accessibility::AXTree;
use crate::audit::normalized::{AccessibilityJourney, InteractiveFinding};
use crate::cli::InteractiveMode;
use crate::error::Result;
use crate::patterns::{JourneyKind, PatternAnalysis};
pub struct RunContext<'a> {
pub page: &'a Page,
pub mode: InteractiveMode,
pub patterns: Option<&'a PatternAnalysis>,
pub ax_tree: &'a AXTree,
pub initial_url: &'a str,
pub locale: &'a str,
pub budget_ms: u64,
}
#[derive(Default)]
pub struct RunOutput {
pub journey: AccessibilityJourney,
pub findings: Vec<InteractiveFinding>,
}
pub const DEFAULT_BUDGET_MS: u64 = 5000;
fn journey_allowed(mode: InteractiveMode, journey: JourneyKind) -> bool {
match mode {
InteractiveMode::Off => false,
InteractiveMode::Basic => !matches!(journey, JourneyKind::FormErrorSubmit),
InteractiveMode::Full => true,
}
}
pub async fn run(ctx: RunContext<'_>) -> Result<Option<RunOutput>> {
if !ctx.mode.is_enabled() {
return Ok(None);
}
let deadline = Instant::now() + std::time::Duration::from_millis(ctx.budget_ms);
let mut out = RunOutput::default();
let max_steps = match ctx.mode {
InteractiveMode::Off => 0,
InteractiveMode::Basic => 25,
InteractiveMode::Full => 60,
};
let record = tab_walk::record(ctx.page, max_steps).await?;
out.findings
.extend(evaluate::tab_walk(&record.trace, &record.snapshots));
out.findings
.extend(evaluate::tab_walk_order(&record.trace, &record.dom_order));
out.journey.traces.push(record.trace);
if let Some(patterns) = ctx.patterns {
let mut skip_link_idx = 0usize;
let mut disclosure_idx = 0usize;
let mut modal_idx = 0usize;
let mut tabs_idx = 0usize;
let mut menu_idx = 0usize;
let mut form_idx = 0usize;
for candidate in &patterns.journey_candidates {
if Instant::now() >= deadline {
tracing::info!("Journey budget exhausted, stopping pattern journeys early.");
break;
}
if candidate.confidence < 0.7 || !journey_allowed(ctx.mode, candidate.required_journey)
{
continue;
}
let result = match candidate.required_journey {
JourneyKind::SkipLinkActivate => {
let idx = skip_link_idx;
skip_link_idx += 1;
skip_link::test(ctx.page, candidate, idx).await
}
JourneyKind::DisclosureToggle | JourneyKind::AccordionToggle => {
let idx = disclosure_idx;
disclosure_idx += 1;
disclosure_journey::test(ctx.page, candidate, idx).await
}
JourneyKind::ModalOpen => {
let idx = modal_idx;
modal_idx += 1;
modal_journey::test(ctx.page, candidate, idx).await
}
JourneyKind::TabsNavigate => {
let idx = tabs_idx;
tabs_idx += 1;
tabs_journey::test(ctx.page, candidate, idx).await
}
JourneyKind::MenuOpen => {
let idx = menu_idx;
menu_idx += 1;
menu_journey::test(ctx.page, candidate, idx).await
}
JourneyKind::FormErrorSubmit => {
let idx = form_idx;
form_idx += 1;
form_error::test(ctx.page, candidate, idx).await
}
};
match result {
Ok((trace, findings)) => {
out.journey.traces.push(trace);
out.findings.extend(findings);
}
Err(e) => {
tracing::warn!("Journey {:?} failed: {}", candidate.required_journey, e);
}
}
}
}
if matches!(ctx.mode, InteractiveMode::Full) && Instant::now() < deadline {
match spa_navigation::run(ctx.page, ctx.initial_url).await {
Ok(Some((trace, findings))) => {
out.journey.traces.push(trace);
out.findings.extend(findings);
}
Ok(None) => {} Err(e) => {
tracing::warn!("SPA-navigation journey failed: {}", e);
}
}
}
if matches!(ctx.mode, InteractiveMode::Full) {
out.findings
.extend(link_inventory::analyse(ctx.ax_tree, ctx.locale));
}
Ok(Some(out))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_mode_excludes_full_only_journeys() {
assert!(journey_allowed(
InteractiveMode::Basic,
JourneyKind::SkipLinkActivate
));
assert!(journey_allowed(
InteractiveMode::Basic,
JourneyKind::ModalOpen
));
assert!(!journey_allowed(
InteractiveMode::Basic,
JourneyKind::FormErrorSubmit
));
}
#[test]
fn full_mode_allows_full_scope_journeys() {
assert!(journey_allowed(
InteractiveMode::Full,
JourneyKind::FormErrorSubmit
));
}
}