use crate::App;
use crate::automation::file::{Instruction, PlushieFile};
use crate::test::TestSession;
use crate::{Error, Result as PlushieResult};
#[derive(Debug)]
pub struct RunResult {
pub passed: usize,
pub failures: Vec<(usize, String)>,
}
impl RunResult {
pub fn is_ok(&self) -> bool {
self.failures.is_empty()
}
}
pub fn run<A: App>(file: &PlushieFile, session: &mut TestSession<A>) -> RunResult {
let mut passed = 0;
let mut failures = Vec::new();
for (line_no, instruction) in &file.instructions {
match execute_instruction(session, instruction) {
Ok(()) => passed += 1,
Err(msg) => failures.push((*line_no, msg)),
}
}
RunResult { passed, failures }
}
pub fn run_with_model_debug<A: App>(file: &PlushieFile, session: &mut TestSession<A>) -> RunResult
where
A::Model: std::fmt::Debug,
{
let mut passed = 0;
let mut failures = Vec::new();
for (line_no, instruction) in &file.instructions {
let result = match instruction {
Instruction::AssertModel(expected) => {
let actual = format!("{:?}", session.model());
if actual.contains(expected.as_str()) {
Ok(())
} else {
Err(format!(
"expected model debug string to contain \"{expected}\", got {actual}"
))
}
}
other => execute_instruction(session, other),
};
match result {
Ok(()) => passed += 1,
Err(msg) => failures.push((*line_no, msg)),
}
}
RunResult { passed, failures }
}
pub fn run_with_backend<A: App>(file: &PlushieFile) -> PlushieResult {
let backend =
crate::automation::Backend::from_header(&file.header.backend).ok_or_else(|| {
Error::InvalidSettings(format!(
"unknown backend `{}` (expected mock, headless, or windowed)",
file.header.backend
))
})?;
match backend {
crate::automation::Backend::Mock | crate::automation::Backend::Headless => {
let mut session = TestSession::<A>::start().allow_diagnostics();
let result = run::<A>(file, &mut session);
if result.is_ok() {
Ok(())
} else {
Err(Error::Startup(format!(
"{} instruction(s) failed",
result.failures.len()
)))
}
}
crate::automation::Backend::Windowed => run_windowed::<A>(file),
}
}
#[cfg(feature = "wire")]
fn run_windowed<A: App>(file: &PlushieFile) -> PlushieResult {
crate::automation::runner_wire::run_windowed::<A>(file)
}
#[cfg(not(feature = "wire"))]
fn run_windowed<A: App>(_file: &PlushieFile) -> PlushieResult {
let _ = std::marker::PhantomData::<A>;
Err(Error::NoRunnerFeature)
}
fn execute_instruction<A: App>(
session: &mut TestSession<A>,
instruction: &Instruction,
) -> Result<(), String> {
match instruction {
Instruction::Click(sel) => {
session.click(sel.clone());
Ok(())
}
Instruction::TypeText(sel, text) => {
session.type_text(sel.clone(), text);
Ok(())
}
Instruction::TypeKey(key) => {
session.type_key(key.as_str());
Ok(())
}
Instruction::Press(key) => {
session.press(key.as_str());
Ok(())
}
Instruction::Release(key) => {
session.release(key.as_str());
Ok(())
}
Instruction::Toggle(sel, value) => {
match value {
Some(v) => session.set_toggle(sel.clone(), *v),
None => session.toggle(sel.clone()),
}
Ok(())
}
Instruction::Select(sel, value) => {
session.select(sel.clone(), value);
Ok(())
}
Instruction::Slide(sel, value) => {
session.slide(sel.clone(), *value);
Ok(())
}
Instruction::Scroll(sel, dx, dy) => {
session.scroll(sel.clone(), *dx, *dy);
Ok(())
}
Instruction::MoveTo(_x, _y) => {
Ok(())
}
Instruction::MoveToSelector(_sel) => {
Ok(())
}
Instruction::Wait(_ms) => {
Ok(())
}
Instruction::Expect(text) => {
let tree = session.tree();
if tree_contains_text(tree, text) {
Ok(())
} else {
Err(format!("expected text \"{text}\" not found in tree"))
}
}
Instruction::AssertText(sel, expected) => {
let actual = session.text_content(sel.clone());
if actual.as_deref() == Some(expected.as_str()) {
Ok(())
} else {
Err(format!(
"expected {sel} text \"{expected}\", got {actual:?}"
))
}
}
Instruction::AssertExists(sel) => {
if session.find(sel.clone()).is_some() {
Ok(())
} else {
Err(format!("expected {sel} to exist"))
}
}
Instruction::AssertNotExists(sel) => {
if session.find(sel.clone()).is_none() {
Ok(())
} else {
Err(format!("expected {sel} to NOT exist"))
}
}
Instruction::AssertModel(_expected) => {
Err(
"assert_model requires App::Model: Debug; use a wrapper runner with the bound"
.to_string(),
)
}
Instruction::Screenshot(_name) => {
Ok(())
}
Instruction::TreeHash(_name) => {
Ok(())
}
}
}
fn tree_contains_text(node: &plushie_core::protocol::TreeNode, text: &str) -> bool {
for key in &["content", "label", "value", "placeholder"] {
if node.props.get_str(key) == Some(text) {
return true;
}
}
node.children.iter().any(|c| tree_contains_text(c, text))
}