use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WebAction {
Navigate { url: String },
Click { selector: String },
Fill { selector: String, value: String },
Extract { selector: String, expected: String },
Screenshot { label: String },
WaitFor { selector: String, timeout_ms: u64 },
Scroll {
direction: ScrollDirection,
amount: i32,
},
Press { key: String },
Hover { selector: String },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ScrollDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SuccessCriterion {
UrlContains(String),
PageContains(String),
ElementVisible(String),
ExtractedDataMatches { key: String, expected: String },
VisualSimilarity { baseline: PathBuf, threshold: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebTask {
pub id: String,
pub name: String,
pub description: String,
pub actions: Vec<WebAction>,
pub success_criteria: Vec<SuccessCriterion>,
pub timeout_secs: u64,
}
impl WebTask {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
description: String::new(),
actions: Vec::new(),
success_criteria: Vec::new(),
timeout_secs: 60,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_action(mut self, action: WebAction) -> Self {
self.actions.push(action);
self
}
pub fn with_criterion(mut self, criterion: SuccessCriterion) -> Self {
self.success_criteria.push(criterion);
self
}
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
}
pub fn scenario_search_extract() -> WebTask {
WebTask::new("search-extract", "Search and Extract")
.with_description("Navigate to a search engine, search for a term, extract result titles")
.with_action(WebAction::Navigate {
url: "https://html.duckduckgo.com/html/".into(),
})
.with_action(WebAction::Fill {
selector: "input[name='q']".into(),
value: "Rust programming language".into(),
})
.with_action(WebAction::Click {
selector: "input[type='submit']".into(),
})
.with_action(WebAction::WaitFor {
selector: ".result__title".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Screenshot {
label: "search_results".into(),
})
.with_action(WebAction::Extract {
selector: ".result__title".into(),
expected: "Rust".into(),
})
.with_criterion(SuccessCriterion::PageContains("Rust".into()))
.with_timeout(30)
}
pub fn scenario_multi_step_navigation() -> WebTask {
WebTask::new("multi-step-nav", "Multi-Step Navigation")
.with_description("Navigate across multiple pages following internal links")
.with_action(WebAction::Navigate {
url: "https://en.wikipedia.org/wiki/Rust_(programming_language)".into(),
})
.with_action(WebAction::WaitFor {
selector: "#firstHeading".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Screenshot {
label: "wiki_rust_page".into(),
})
.with_action(WebAction::Extract {
selector: "#firstHeading".into(),
expected: "Rust".into(),
})
.with_action(WebAction::Click {
selector: "a[href='/wiki/Memory_safety']".into(),
})
.with_action(WebAction::WaitFor {
selector: "#firstHeading".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Screenshot {
label: "wiki_memory_safety".into(),
})
.with_criterion(SuccessCriterion::UrlContains("Memory_safety".into()))
.with_criterion(SuccessCriterion::PageContains("memory safety".into()))
.with_timeout(60)
}
pub fn scenario_form_fill() -> WebTask {
WebTask::new("form-fill", "Form Interaction")
.with_description("Fill and submit a multi-field form, verify confirmation")
.with_action(WebAction::Navigate {
url: "https://httpbin.org/forms/post".into(),
})
.with_action(WebAction::WaitFor {
selector: "form".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Fill {
selector: "input[name='custname']".into(),
value: "Selfware Test".into(),
})
.with_action(WebAction::Fill {
selector: "input[name='custtel']".into(),
value: "555-0123".into(),
})
.with_action(WebAction::Fill {
selector: "input[name='custemail']".into(),
value: "test@selfware.dev".into(),
})
.with_action(WebAction::Screenshot {
label: "form_filled".into(),
})
.with_action(WebAction::Click {
selector: "button[type='submit'], input[type='submit']".into(),
})
.with_action(WebAction::WaitFor {
selector: "body".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Screenshot {
label: "form_submitted".into(),
})
.with_criterion(SuccessCriterion::PageContains("Selfware Test".into()))
.with_timeout(30)
}
pub fn scenario_data_extraction() -> WebTask {
WebTask::new("data-extract", "Data Extraction")
.with_description("Visit a JSON API endpoint and extract structured data")
.with_action(WebAction::Navigate {
url: "https://httpbin.org/json".into(),
})
.with_action(WebAction::WaitFor {
selector: "body".into(),
timeout_ms: 10000,
})
.with_action(WebAction::Extract {
selector: "body".into(),
expected: "slideshow".into(),
})
.with_action(WebAction::Screenshot {
label: "json_data".into(),
})
.with_criterion(SuccessCriterion::PageContains("slideshow".into()))
.with_timeout(30)
}
pub fn all_scenarios() -> Vec<WebTask> {
vec![
scenario_search_extract(),
scenario_multi_step_navigation(),
scenario_form_fill(),
scenario_data_extraction(),
]
}
#[cfg(test)]
#[path = "../../../tests/unit/bench_harness/computer_control/tasks/tasks_test.rs"]
mod tests;