1use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use crate::hook::HookPoint;
14use crate::registry::StepRegistry;
15use crate::scenario::{ScenarioExecution, ScenarioResult, ScenarioStatus, ScenarioStep, StepResult, StepStatus};
16use crate::translate::execute_bdd_step;
17use crate::world::BrowserWorld;
18
19pub struct StepEvent<'a> {
23 pub step: &'a ScenarioStep,
25 pub text: &'a str,
27 pub result: &'a StepResult,
29}
30
31pub trait StepObserver: Send + Sync {
37 fn on_step<'a>(&'a self, event: StepEvent<'a>) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
38}
39
40pub(crate) struct NoopObserver;
42
43impl StepObserver for NoopObserver {
44 fn on_step<'a>(&'a self, _event: StepEvent<'a>) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
45 Box::pin(std::future::ready(()))
46 }
47}
48
49#[derive(Clone)]
56pub struct ScenarioExecutor {
57 registry: Arc<StepRegistry>,
58 step_timeout: Duration,
59 strict: bool,
60 screenshot_on_failure: bool,
61}
62
63impl ScenarioExecutor {
64 pub fn new(registry: Arc<StepRegistry>, step_timeout: Duration, strict: bool, screenshot_on_failure: bool) -> Self {
66 Self {
67 registry,
68 step_timeout,
69 strict,
70 screenshot_on_failure,
71 }
72 }
73
74 pub async fn run_scenario(&self, world: &mut BrowserWorld, scenario: &ScenarioExecution) -> ScenarioResult {
78 self.run_scenario_observed(world, scenario, &NoopObserver).await
79 }
80
81 pub async fn run_scenario_observed(
87 &self,
88 world: &mut BrowserWorld,
89 scenario: &ScenarioExecution,
90 observer: &dyn StepObserver,
91 ) -> ScenarioResult {
92 let start = Instant::now();
93
94 if world.registry_arc().as_ref().map(Arc::as_ptr) != Some(Arc::as_ptr(&self.registry)) {
97 world.set_registry(Arc::clone(&self.registry));
98 }
99
100 if let Some(dir) = scenario.feature_path.parent() {
102 world.set_feature_dir(dir.to_path_buf());
103 }
104
105 if let Some(values) = &scenario.example_values {
107 for (key, val) in values {
108 world.set_var(key, val);
109 }
110 }
111
112 let mut step_results: Vec<StepResult> = Vec::with_capacity(scenario.steps.len());
113 let mut had_failure = false;
114 let mut failure_message: Option<String> = None;
115
116 if let Err(e) = self
118 .registry
119 .hooks()
120 .run_scenario(HookPoint::BeforeScenario, world, &scenario.tags)
121 .await
122 {
123 return ScenarioResult {
124 feature_name: scenario.feature_name.clone(),
125 feature_path: scenario.feature_path.display().to_string(),
126 scenario_name: scenario.name.clone(),
127 status: ScenarioStatus::Failed,
128 steps: Vec::new(),
129 duration: start.elapsed(),
130 attempt: 1,
131 tags: scenario.tags.clone(),
132 error: Some(format!("BeforeScenario hook failed: {}", e.display_named())),
133 failure_screenshot: None,
134 };
135 }
136
137 for step in &scenario.steps {
139 if had_failure {
140 let sr = StepResult {
141 keyword: step.keyword.clone(),
142 text: step.text.clone(),
143 status: StepStatus::Skipped,
144 duration: Duration::ZERO,
145 error: Some("skipped due to previous failure".to_string()),
146 };
147 observer
148 .on_step(StepEvent {
149 step,
150 text: &step.text,
151 result: &sr,
152 })
153 .await;
154 step_results.push(sr);
155 continue;
156 }
157
158 let text = world.interpolate(&step.text);
159 let step_start = Instant::now();
160
161 if let Err(e) = self
163 .registry
164 .hooks()
165 .run_step(HookPoint::BeforeStep, world, &text, &scenario.tags)
166 .await
167 {
168 tracing::warn!("BeforeStep hook failed: {}", e.display_named());
169 }
170
171 let result = execute_bdd_step(&self.registry, world, &text, step, self.step_timeout, self.strict).await;
173
174 let step_duration = step_start.elapsed();
175
176 let sr = match result {
177 Ok(()) => StepResult {
178 keyword: step.keyword.clone(),
179 text: text.clone(),
180 status: StepStatus::Passed,
181 duration: step_duration,
182 error: None,
183 },
184 Err(e) if e.pending && !self.strict => StepResult {
185 keyword: step.keyword.clone(),
186 text: text.clone(),
187 status: StepStatus::Pending,
188 duration: step_duration,
189 error: Some(e.to_string()),
190 },
191 Err(e) => {
192 let msg = e.to_string();
193 had_failure = true;
194 failure_message = Some(msg.clone());
195 StepResult {
196 keyword: step.keyword.clone(),
197 text: text.clone(),
198 status: StepStatus::Failed,
199 duration: step_duration,
200 error: Some(msg),
201 }
202 },
203 };
204
205 observer
206 .on_step(StepEvent {
207 step,
208 text: &text,
209 result: &sr,
210 })
211 .await;
212 step_results.push(sr);
213
214 if let Err(e) = self
216 .registry
217 .hooks()
218 .run_step(HookPoint::AfterStep, world, &text, &scenario.tags)
219 .await
220 {
221 tracing::warn!("AfterStep hook failed: {}", e.display_named());
222 }
223 }
224
225 if let Err(e) = self
227 .registry
228 .hooks()
229 .run_scenario(HookPoint::AfterScenario, world, &scenario.tags)
230 .await
231 {
232 tracing::warn!("AfterScenario hook failed: {}", e.display_named());
233 }
234
235 let failure_screenshot = if had_failure && self.screenshot_on_failure {
237 world
238 .page()
239 .screenshot(ferridriver::options::ScreenshotOptions::default())
240 .await
241 .ok()
242 } else {
243 None
244 };
245
246 let status = if had_failure {
247 ScenarioStatus::Failed
248 } else if step_results.iter().any(|s| s.status == StepStatus::Pending) {
249 ScenarioStatus::Undefined
250 } else {
251 ScenarioStatus::Passed
252 };
253
254 ScenarioResult {
255 feature_name: scenario.feature_name.clone(),
256 feature_path: scenario.feature_path.display().to_string(),
257 scenario_name: scenario.name.clone(),
258 status,
259 steps: step_results,
260 duration: start.elapsed(),
261 attempt: 1,
262 tags: scenario.tags.clone(),
263 error: failure_message,
264 failure_screenshot,
265 }
266 }
267
268 pub async fn run_step(
274 &self,
275 world: &mut BrowserWorld,
276 text: &str,
277 table: Option<&crate::data_table::DataTable>,
278 docstring: Option<&str>,
279 ) -> StepResult {
280 if world.registry_arc().as_ref().map(Arc::as_ptr) != Some(Arc::as_ptr(&self.registry)) {
282 world.set_registry(Arc::clone(&self.registry));
283 }
284
285 let interpolated = world.interpolate(text);
286 let start = Instant::now();
287
288 let result = match self.registry.find_match(&interpolated) {
290 Ok(step_match) => {
291 let handler = &step_match.def.handler;
292 match tokio::time::timeout(self.step_timeout, handler(world, step_match.params, table, docstring)).await {
293 Ok(r) => r,
294 Err(_) => Err(crate::step::StepError::from(format!(
295 "step timed out after {}ms",
296 self.step_timeout.as_millis()
297 ))),
298 }
299 },
300 Err(crate::step::MatchError::Undefined { text: t, suggestions }) => {
301 let mut msg = format!("undefined step: \"{t}\"");
302 if !suggestions.is_empty() {
303 msg.push_str("\n did you mean:");
304 for s in &suggestions {
305 msg.push_str(&format!("\n - {s}"));
306 }
307 }
308 if self.strict {
309 Err(crate::step::StepError::from(msg))
310 } else {
311 Err(crate::step::StepError::pending(msg))
312 }
313 },
314 Err(crate::step::MatchError::Ambiguous {
315 text: t,
316 matches,
317 expressions,
318 }) => {
319 let mut msg = format!("ambiguous step: \"{t}\" matched {} definitions:", matches.len());
320 for (i, (loc, expr)) in matches.iter().zip(expressions.iter()).enumerate() {
321 msg.push_str(&format!("\n {}. {} ({})", i + 1, expr, loc));
322 }
323 Err(crate::step::StepError::from(msg))
324 },
325 };
326
327 let duration = start.elapsed();
328
329 match result {
330 Ok(()) => StepResult {
331 keyword: String::new(),
332 text: interpolated,
333 status: StepStatus::Passed,
334 duration,
335 error: None,
336 },
337 Err(e) if e.pending && !self.strict => StepResult {
338 keyword: String::new(),
339 text: interpolated,
340 status: StepStatus::Pending,
341 duration,
342 error: Some(e.to_string()),
343 },
344 Err(e) => StepResult {
345 keyword: String::new(),
346 text: interpolated,
347 status: StepStatus::Failed,
348 duration,
349 error: Some(e.to_string()),
350 },
351 }
352 }
353
354 pub fn registry(&self) -> &StepRegistry {
356 &self.registry
357 }
358}