use super::{StepCategory, StepDef};
use crate::page::Page;
use std::sync::{Arc, OnceLock};
pub struct StepRegistry {
steps: Vec<Box<dyn StepDef>>,
}
impl StepRegistry {
fn build() -> Self {
let mut steps: Vec<Box<dyn StepDef>> = Vec::new();
super::navigation::register(&mut steps);
super::interaction::register(&mut steps);
super::wait::register(&mut steps);
super::assertion::register(&mut steps);
super::variable::register(&mut steps);
super::cookie::register(&mut steps);
super::storage::register(&mut steps);
super::screenshot::register(&mut steps);
super::javascript::register(&mut steps);
#[cfg(debug_assertions)]
{
for (i, step) in steps.iter().enumerate() {
let first_line = step.example().lines().next().unwrap_or("");
let example_body = first_line
.trim_start_matches("Given ")
.trim_start_matches("When ")
.trim_start_matches("Then ")
.trim_start_matches("And ");
assert!(
step.pattern().is_match(example_body),
"Step '{}' example '{}' does not match its own pattern '{}'",
step.description(),
first_line,
step.pattern().as_str()
);
for earlier in &steps[..i] {
if earlier.pattern().is_match(example_body) {
eprintln!(
"WARNING: step '{}' example '{}' is shadowed by earlier step '{}' (pattern '{}')",
step.description(),
step.example(),
earlier.description(),
earlier.pattern().as_str()
);
}
}
}
}
Self { steps }
}
pub fn global() -> &'static Self {
static INSTANCE: OnceLock<StepRegistry> = OnceLock::new();
INSTANCE.get_or_init(Self::build)
}
pub async fn execute(
&self,
page: &Arc<Page>,
body: &str,
data_table: Option<&[Vec<String>]>,
vars: &mut rustc_hash::FxHashMap<String, String>,
) -> crate::error::Result<Option<serde_json::Value>> {
for step in &self.steps {
if let Some(caps) = step.pattern().captures(body) {
return step.execute(page, &caps, data_table, vars).await;
}
}
let mut suggestions = Vec::new();
let body_lower = body.to_lowercase();
for step in &self.steps {
let desc_lower = step.description().to_lowercase();
if body_lower.split_whitespace().any(|w| desc_lower.contains(w)) {
suggestions.push(format!(" - {}", step.example()));
if suggestions.len() >= 3 {
break;
}
}
}
let hint = if suggestions.is_empty() {
String::new()
} else {
format!("\n\nDid you mean:\n{}", suggestions.join("\n"))
};
Err(crate::error::FerriError::invalid_argument(
"step",
format!("Unknown step: '{body}'{hint}"),
))
}
#[must_use]
pub fn reference(&self) -> String {
use std::fmt::Write;
let mut out = String::new();
let mut current_cat: Option<StepCategory> = None;
for step in &self.steps {
let cat = step.category();
if current_cat != Some(cat) {
current_cat = Some(cat);
let _ = write!(out, "\n## {cat:?}\n");
}
let _ = writeln!(out, "- {} — `{}`", step.description(), step.example());
}
out
}
#[must_use]
pub fn list(&self) -> Vec<StepInfo> {
self
.steps
.iter()
.map(|s| StepInfo {
category: format!("{:?}", s.category()),
description: s.description().to_string(),
example: s.example().to_string(),
})
.collect()
}
}
#[derive(Debug, serde::Serialize)]
pub struct StepInfo {
pub category: String,
pub description: String,
pub example: String,
}