Skip to main content

ferridriver_bdd/
hook.rs

1//! Hook system: lifecycle hooks with tag filtering and ordering.
2
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use ferridriver::error::Result;
8
9use crate::filter::TagExpression;
10use crate::step::StepLocation;
11use crate::world::BrowserWorld;
12
13// ── Hook points ──
14
15/// When a hook fires in the BDD lifecycle.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum HookPoint {
18  BeforeAll,
19  AfterAll,
20  BeforeFeature,
21  AfterFeature,
22  BeforeScenario,
23  AfterScenario,
24  BeforeStep,
25  AfterStep,
26}
27
28// ── Hook handler variants ──
29
30/// The actual hook function, typed by scope.
31pub enum HookHandler {
32  /// Global hooks (BeforeAll/AfterAll): no world context.
33  Global(Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>),
34  /// Scenario-scoped hooks (BeforeScenario/AfterScenario, BeforeFeature/AfterFeature).
35  Scenario(
36    Arc<dyn for<'a> Fn(&'a mut BrowserWorld) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> + Send + Sync>,
37  ),
38  /// Step-scoped hooks (BeforeStep/AfterStep): receive step text.
39  Step(
40    Arc<
41      dyn for<'a> Fn(&'a mut BrowserWorld, &'a str) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>
42        + Send
43        + Sync,
44    >,
45  ),
46}
47
48// ── Hook definition ──
49
50/// A registered hook with metadata.
51pub struct Hook {
52  /// When this hook fires.
53  pub point: HookPoint,
54  /// Optional tag filter expression (e.g., "@smoke and not @wip").
55  /// If `None`, the hook runs for all scenarios.
56  pub tag_filter: Option<TagExpression>,
57  /// Execution order (lower runs first). Default: 0.
58  pub order: i32,
59  /// The handler function.
60  pub handler: HookHandler,
61  /// Source location for diagnostics.
62  pub location: StepLocation,
63}
64
65// ── Hook registry ──
66
67/// Collects and dispatches hooks.
68pub struct HookRegistry {
69  hooks: Vec<Hook>,
70}
71
72impl HookRegistry {
73  pub fn new() -> Self {
74    Self { hooks: Vec::new() }
75  }
76
77  /// Register a hook.
78  pub fn register(&mut self, hook: Hook) {
79    self.hooks.push(hook);
80  }
81
82  /// Get all hooks for a given point, filtered by tags, sorted by order.
83  pub fn get(&self, point: HookPoint, tags: &[String]) -> Vec<&Hook> {
84    let mut matched: Vec<&Hook> = self
85      .hooks
86      .iter()
87      .filter(|h| h.point == point)
88      .filter(|h| match &h.tag_filter {
89        None => true,
90        Some(expr) => expr.matches(tags),
91      })
92      .collect();
93
94    matched.sort_by_key(|h| h.order);
95    matched
96  }
97
98  /// Get global hooks (no tag filtering needed).
99  pub fn get_global(&self, point: HookPoint) -> Vec<&Hook> {
100    let mut matched: Vec<&Hook> = self.hooks.iter().filter(|h| h.point == point).collect();
101    matched.sort_by_key(|h| h.order);
102    matched
103  }
104
105  /// Run all global hooks for a given point.
106  pub async fn run_global(&self, point: HookPoint) -> Result<()> {
107    for hook in self.get_global(point) {
108      if let HookHandler::Global(handler) = &hook.handler {
109        handler().await?;
110      }
111    }
112    Ok(())
113  }
114
115  /// Run all scenario hooks for a given point with the given world and tags.
116  pub async fn run_scenario(&self, point: HookPoint, world: &mut BrowserWorld, tags: &[String]) -> Result<()> {
117    for hook in self.get(point, tags) {
118      if let HookHandler::Scenario(handler) = &hook.handler {
119        handler(world).await?;
120      }
121    }
122    Ok(())
123  }
124
125  /// Run suite-level hooks for a given point.
126  ///
127  /// `BeforeAll` / `AfterAll` may come from either a world-aware TS hook or
128  /// a world-less Rust hook, so this dispatch supports both variants.
129  pub async fn run_suite(&self, point: HookPoint, world: &mut BrowserWorld, tags: &[String]) -> Result<()> {
130    for hook in self.get(point, tags) {
131      match &hook.handler {
132        HookHandler::Scenario(handler) => handler(world).await?,
133        HookHandler::Global(handler) => handler().await?,
134        HookHandler::Step(_) => {},
135      }
136    }
137    Ok(())
138  }
139
140  /// Run all step hooks for a given point.
141  pub async fn run_step(
142    &self,
143    point: HookPoint,
144    world: &mut BrowserWorld,
145    step_text: &str,
146    tags: &[String],
147  ) -> Result<()> {
148    for hook in self.get(point, tags) {
149      match &hook.handler {
150        HookHandler::Step(handler) => handler(world, step_text).await?,
151        HookHandler::Scenario(handler) => handler(world).await?,
152        _ => {},
153      }
154    }
155    Ok(())
156  }
157}
158
159impl Default for HookRegistry {
160  fn default() -> Self {
161    Self::new()
162  }
163}
164
165pub fn runtime_hook_point(registration: &ferridriver_test::HookRegistration) -> Option<HookPoint> {
166  match (registration.phase, registration.scope) {
167    (ferridriver_test::HookPhase::Before, ferridriver_test::HookScope::Suite) => Some(HookPoint::BeforeAll),
168    (ferridriver_test::HookPhase::After, ferridriver_test::HookScope::Suite) => Some(HookPoint::AfterAll),
169    (ferridriver_test::HookPhase::Before, ferridriver_test::HookScope::Scenario) => Some(HookPoint::BeforeScenario),
170    (ferridriver_test::HookPhase::After, ferridriver_test::HookScope::Scenario) => Some(HookPoint::AfterScenario),
171    (ferridriver_test::HookPhase::Before, ferridriver_test::HookScope::Step) => Some(HookPoint::BeforeStep),
172    (ferridriver_test::HookPhase::After, ferridriver_test::HookScope::Step) => Some(HookPoint::AfterStep),
173  }
174}
175
176// ── Inventory registration ──
177
178/// What the `#[before]` / `#[after]` proc macros submit via `inventory::submit!`.
179pub struct HookRegistration {
180  pub point: HookPoint,
181  pub tag_filter: Option<String>,
182  pub order: i32,
183  pub handler_factory: fn() -> HookHandler,
184  pub file: &'static str,
185  pub line: u32,
186}
187
188inventory::collect!(HookRegistration);
189
190/// Convenience macro for submitting hook registrations from proc macro expansion.
191#[macro_export]
192macro_rules! submit_hook {
193  ($name:ident, $point:expr, $tag_filter:expr, $order:expr, $handler:ident,) => {
194    ferridriver_bdd::inventory::submit! {
195      ferridriver_bdd::hook::HookRegistration {
196        point: $point,
197        tag_filter: $tag_filter,
198        order: $order,
199        handler_factory: $handler,
200        file: file!(),
201        line: line!(),
202      }
203    }
204  };
205}