Skip to main content

jugar_probar/playbook/
executor.rs

1//! Playbook execution engine with transition assertions.
2//!
3//! Executes state machine transitions and verifies assertions.
4//! Tracks timing for complexity analysis.
5
6use super::complexity::{check_complexity_violation, ComplexityResult};
7use super::schema::{Action, Assertion, Playbook, Transition, WaitCondition};
8use std::time::{Duration, Instant};
9
10/// Result of executing a playbook.
11#[derive(Debug)]
12pub struct ExecutionResult {
13    /// Whether the playbook completed successfully
14    pub success: bool,
15    /// Current state after execution
16    pub final_state: String,
17    /// Executed transitions
18    pub transitions_executed: Vec<TransitionResult>,
19    /// Total execution time
20    pub total_time: Duration,
21    /// Performance metrics
22    pub metrics: ExecutionMetrics,
23    /// Assertion failures
24    pub assertion_failures: Vec<AssertionFailure>,
25    /// Complexity analysis (if performance budget specified)
26    pub complexity_result: Option<ComplexityResult>,
27}
28
29/// Result of a single transition execution.
30#[derive(Debug, Clone)]
31pub struct TransitionResult {
32    /// Transition ID
33    pub transition_id: String,
34    /// Source state
35    pub from_state: String,
36    /// Target state
37    pub to_state: String,
38    /// Execution time for this transition
39    pub duration: Duration,
40    /// Whether assertions passed
41    pub assertions_passed: bool,
42    /// Individual assertion results
43    pub assertion_results: Vec<AssertionResult>,
44}
45
46/// Result of a single assertion check.
47#[derive(Debug, Clone)]
48pub struct AssertionResult {
49    /// Whether the assertion passed
50    pub passed: bool,
51    /// Assertion description
52    pub description: String,
53    /// Error message if failed
54    pub error: Option<String>,
55}
56
57/// Details of an assertion failure.
58#[derive(Debug, Clone)]
59pub struct AssertionFailure {
60    /// Transition where failure occurred
61    pub transition_id: String,
62    /// Assertion that failed
63    pub assertion_description: String,
64    /// Error details
65    pub error: String,
66}
67
68/// Execution performance metrics.
69#[derive(Debug, Clone, Default)]
70pub struct ExecutionMetrics {
71    /// Time per transition (for complexity analysis)
72    pub transition_times: Vec<(usize, f64)>,
73    /// Peak memory usage (if tracked)
74    pub peak_memory_bytes: Option<u64>,
75    /// Total transitions executed
76    pub transition_count: usize,
77}
78
79/// Trait for executing actions in a playbook.
80/// Implement this for your specific testing environment.
81pub trait ActionExecutor {
82    /// Execute a click action.
83    fn click(&mut self, selector: &str) -> Result<(), ExecutorError>;
84
85    /// Execute a type action.
86    fn type_text(&mut self, selector: &str, text: &str) -> Result<(), ExecutorError>;
87
88    /// Execute a wait action.
89    fn wait(&mut self, condition: &WaitCondition) -> Result<(), ExecutorError>;
90
91    /// Execute a navigation action.
92    fn navigate(&mut self, url: &str) -> Result<(), ExecutorError>;
93
94    /// Execute a script action.
95    fn execute_script(&mut self, code: &str) -> Result<String, ExecutorError>;
96
97    /// Take a screenshot.
98    fn screenshot(&mut self, name: &str) -> Result<(), ExecutorError>;
99
100    /// Check if element exists.
101    fn element_exists(&self, selector: &str) -> Result<bool, ExecutorError>;
102
103    /// Get element text.
104    fn get_text(&self, selector: &str) -> Result<String, ExecutorError>;
105
106    /// Get element attribute.
107    fn get_attribute(&self, selector: &str, attribute: &str) -> Result<String, ExecutorError>;
108
109    /// Get current URL.
110    fn get_url(&self) -> Result<String, ExecutorError>;
111
112    /// Evaluate JavaScript expression.
113    fn evaluate(&self, expression: &str) -> Result<bool, ExecutorError>;
114}
115
116/// Errors during playbook execution.
117#[derive(Debug, Clone, thiserror::Error)]
118pub enum ExecutorError {
119    #[error("Element not found: {selector}")]
120    ElementNotFound { selector: String },
121
122    #[error("Timeout waiting for condition")]
123    Timeout,
124
125    #[error("Navigation failed: {url}")]
126    NavigationFailed { url: String },
127
128    #[error("Script execution failed: {message}")]
129    ScriptError { message: String },
130
131    #[error("Assertion failed: {message}")]
132    AssertionFailed { message: String },
133
134    #[error("Invalid transition: no transition from '{state}' with event '{event}'")]
135    InvalidTransition { state: String, event: String },
136
137    #[error("Performance budget exceeded: {message}")]
138    PerformanceBudgetExceeded { message: String },
139}
140
141/// Playbook execution engine.
142pub struct PlaybookExecutor<E: ActionExecutor> {
143    playbook: Playbook,
144    executor: E,
145    current_state: String,
146    transition_count: usize,
147}
148
149impl<E: ActionExecutor> PlaybookExecutor<E> {
150    /// Create a new executor for the given playbook.
151    pub fn new(playbook: Playbook, executor: E) -> Self {
152        let initial = playbook.machine.initial.clone();
153        Self {
154            playbook,
155            executor,
156            current_state: initial,
157            transition_count: 0,
158        }
159    }
160
161    /// Execute the playbook by following the given event sequence.
162    pub fn execute(&mut self, events: &[&str]) -> ExecutionResult {
163        let start = Instant::now();
164        let mut transitions_executed = Vec::new();
165        let mut assertion_failures = Vec::new();
166        let mut metrics = ExecutionMetrics::default();
167        let mut success = true;
168
169        for event in events {
170            match self.trigger_event(event) {
171                Ok(result) => {
172                    // Track timing for complexity analysis
173                    metrics.transition_times.push((
174                        self.transition_count,
175                        result.duration.as_secs_f64() * 1000.0,
176                    ));
177
178                    // Check for assertion failures
179                    if !result.assertions_passed {
180                        for ar in &result.assertion_results {
181                            if !ar.passed {
182                                assertion_failures.push(AssertionFailure {
183                                    transition_id: result.transition_id.clone(),
184                                    assertion_description: ar.description.clone(),
185                                    error: ar.error.clone().unwrap_or_default(),
186                                });
187                            }
188                        }
189                        success = false;
190                    }
191
192                    transitions_executed.push(result);
193                }
194                Err(e) => {
195                    assertion_failures.push(AssertionFailure {
196                        transition_id: format!("event:{}", event),
197                        assertion_description: "Transition execution".to_string(),
198                        error: e.to_string(),
199                    });
200                    success = false;
201                    break;
202                }
203            }
204        }
205
206        metrics.transition_count = transitions_executed.len();
207
208        // Check complexity if budget specified
209        let complexity_result =
210            self.playbook.performance.complexity_class.map(|expected| {
211                check_complexity_violation(metrics.transition_times.clone(), expected)
212            });
213
214        // Check for complexity violation
215        if let Some(ref cr) = complexity_result {
216            if cr.is_violation {
217                success = false;
218            }
219        }
220
221        ExecutionResult {
222            success,
223            final_state: self.current_state.clone(),
224            transitions_executed,
225            total_time: start.elapsed(),
226            metrics,
227            assertion_failures,
228            complexity_result,
229        }
230    }
231
232    /// Trigger an event and execute the corresponding transition.
233    fn trigger_event(&mut self, event: &str) -> Result<TransitionResult, ExecutorError> {
234        // Find matching transition and clone necessary data to avoid borrow issues
235        let transition = self.find_transition(event)?;
236        let transition_id = transition.id.clone();
237        let from_state = transition.from.clone();
238        let to_state = transition.to.clone();
239        let transition_actions = transition.actions.clone();
240        let transition_assertions = transition.assertions.clone();
241
242        let start = Instant::now();
243
244        // Clone exit actions to avoid borrow issues
245        let exit_actions = self
246            .playbook
247            .machine
248            .states
249            .get(&self.current_state)
250            .map(|s| s.on_exit.clone())
251            .unwrap_or_default();
252
253        // Execute exit actions for current state
254        for action in &exit_actions {
255            self.execute_action(action)?;
256        }
257
258        // Execute transition actions
259        for action in &transition_actions {
260            self.execute_action(action)?;
261        }
262
263        // Update current state
264        self.current_state = to_state.clone();
265        self.transition_count += 1;
266
267        // Clone entry actions to avoid borrow issues
268        let entry_actions = self
269            .playbook
270            .machine
271            .states
272            .get(&self.current_state)
273            .map(|s| s.on_entry.clone())
274            .unwrap_or_default();
275
276        // Execute entry actions for new state
277        for action in &entry_actions {
278            self.execute_action(action)?;
279        }
280
281        // Check assertions
282        let assertion_results = self.check_assertions(&transition_assertions);
283        let assertions_passed = assertion_results.iter().all(|r| r.passed);
284
285        let duration = start.elapsed();
286
287        Ok(TransitionResult {
288            transition_id,
289            from_state,
290            to_state,
291            duration,
292            assertions_passed,
293            assertion_results,
294        })
295    }
296
297    /// Find a transition matching the current state and event.
298    fn find_transition(&self, event: &str) -> Result<&Transition, ExecutorError> {
299        self.playbook
300            .machine
301            .transitions
302            .iter()
303            .find(|t| t.from == self.current_state && t.event == event)
304            .ok_or_else(|| ExecutorError::InvalidTransition {
305                state: self.current_state.clone(),
306                event: event.to_string(),
307            })
308    }
309
310    /// Execute a single action.
311    fn execute_action(&mut self, action: &Action) -> Result<(), ExecutorError> {
312        match action {
313            Action::Click { selector } => self.executor.click(selector),
314            Action::Type { selector, text } => self.executor.type_text(selector, text),
315            Action::Wait { condition } => self.executor.wait(condition),
316            Action::Navigate { url } => self.executor.navigate(url),
317            Action::Script { code } => self.executor.execute_script(code).map(|_| ()),
318            Action::Screenshot { name } => self.executor.screenshot(name),
319        }
320    }
321
322    /// Check all assertions and return results.
323    fn check_assertions(&self, assertions: &[Assertion]) -> Vec<AssertionResult> {
324        assertions
325            .iter()
326            .map(|assertion| self.check_assertion(assertion))
327            .collect()
328    }
329
330    /// Check a single assertion.
331    fn check_assertion(&self, assertion: &Assertion) -> AssertionResult {
332        match assertion {
333            Assertion::ElementExists { selector } => match self.executor.element_exists(selector) {
334                Ok(true) => AssertionResult {
335                    passed: true,
336                    description: format!("Element exists: {}", selector),
337                    error: None,
338                },
339                Ok(false) => AssertionResult {
340                    passed: false,
341                    description: format!("Element exists: {}", selector),
342                    error: Some(format!("Element not found: {}", selector)),
343                },
344                Err(e) => AssertionResult {
345                    passed: false,
346                    description: format!("Element exists: {}", selector),
347                    error: Some(e.to_string()),
348                },
349            },
350            Assertion::TextEquals { selector, expected } => {
351                match self.executor.get_text(selector) {
352                    Ok(actual) if actual == *expected => AssertionResult {
353                        passed: true,
354                        description: format!("Text equals '{}': {}", expected, selector),
355                        error: None,
356                    },
357                    Ok(actual) => AssertionResult {
358                        passed: false,
359                        description: format!("Text equals '{}': {}", expected, selector),
360                        error: Some(format!("Expected '{}', got '{}'", expected, actual)),
361                    },
362                    Err(e) => AssertionResult {
363                        passed: false,
364                        description: format!("Text equals '{}': {}", expected, selector),
365                        error: Some(e.to_string()),
366                    },
367                }
368            }
369            Assertion::TextContains {
370                selector,
371                substring,
372            } => match self.executor.get_text(selector) {
373                Ok(actual) if actual.contains(substring) => AssertionResult {
374                    passed: true,
375                    description: format!("Text contains '{}': {}", substring, selector),
376                    error: None,
377                },
378                Ok(actual) => AssertionResult {
379                    passed: false,
380                    description: format!("Text contains '{}': {}", substring, selector),
381                    error: Some(format!("'{}' not found in '{}'", substring, actual)),
382                },
383                Err(e) => AssertionResult {
384                    passed: false,
385                    description: format!("Text contains '{}': {}", substring, selector),
386                    error: Some(e.to_string()),
387                },
388            },
389            Assertion::AttributeEquals {
390                selector,
391                attribute,
392                expected,
393            } => match self.executor.get_attribute(selector, attribute) {
394                Ok(actual) if actual == *expected => AssertionResult {
395                    passed: true,
396                    description: format!("{}[{}] = '{}'", selector, attribute, expected),
397                    error: None,
398                },
399                Ok(actual) => AssertionResult {
400                    passed: false,
401                    description: format!("{}[{}] = '{}'", selector, attribute, expected),
402                    error: Some(format!("Expected '{}', got '{}'", expected, actual)),
403                },
404                Err(e) => AssertionResult {
405                    passed: false,
406                    description: format!("{}[{}] = '{}'", selector, attribute, expected),
407                    error: Some(e.to_string()),
408                },
409            },
410            Assertion::UrlMatches { pattern } => match self.executor.get_url() {
411                Ok(url) => {
412                    let matches = url.contains(pattern);
413                    AssertionResult {
414                        passed: matches,
415                        description: format!("URL matches '{}'", pattern),
416                        error: if matches {
417                            None
418                        } else {
419                            Some(format!("URL '{}' does not match '{}'", url, pattern))
420                        },
421                    }
422                }
423                Err(e) => AssertionResult {
424                    passed: false,
425                    description: format!("URL matches '{}'", pattern),
426                    error: Some(e.to_string()),
427                },
428            },
429            Assertion::Script { expression } => match self.executor.evaluate(expression) {
430                Ok(true) => AssertionResult {
431                    passed: true,
432                    description: format!("Script: {}", expression),
433                    error: None,
434                },
435                Ok(false) => AssertionResult {
436                    passed: false,
437                    description: format!("Script: {}", expression),
438                    error: Some("Expression evaluated to false".to_string()),
439                },
440                Err(e) => AssertionResult {
441                    passed: false,
442                    description: format!("Script: {}", expression),
443                    error: Some(e.to_string()),
444                },
445            },
446        }
447    }
448
449    /// Get the current state.
450    pub fn current_state(&self) -> &str {
451        &self.current_state
452    }
453
454    /// Reset to initial state.
455    pub fn reset(&mut self) {
456        self.current_state = self.playbook.machine.initial.clone();
457        self.transition_count = 0;
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464    use std::collections::HashMap;
465
466    /// Mock executor for testing
467    struct MockExecutor {
468        elements: HashMap<String, String>,
469        url: String,
470    }
471
472    impl MockExecutor {
473        fn new() -> Self {
474            let mut elements = HashMap::new();
475            elements.insert("#welcome".to_string(), "Welcome, User!".to_string());
476            elements.insert("#login-btn".to_string(), "Login".to_string());
477
478            Self {
479                elements,
480                url: "http://localhost/".to_string(),
481            }
482        }
483    }
484
485    impl ActionExecutor for MockExecutor {
486        fn click(&mut self, _selector: &str) -> Result<(), ExecutorError> {
487            Ok(())
488        }
489
490        fn type_text(&mut self, _selector: &str, _text: &str) -> Result<(), ExecutorError> {
491            Ok(())
492        }
493
494        fn wait(&mut self, _condition: &WaitCondition) -> Result<(), ExecutorError> {
495            Ok(())
496        }
497
498        fn navigate(&mut self, url: &str) -> Result<(), ExecutorError> {
499            self.url = url.to_string();
500            Ok(())
501        }
502
503        fn execute_script(&mut self, _code: &str) -> Result<String, ExecutorError> {
504            Ok("undefined".to_string())
505        }
506
507        fn screenshot(&mut self, _name: &str) -> Result<(), ExecutorError> {
508            Ok(())
509        }
510
511        fn element_exists(&self, selector: &str) -> Result<bool, ExecutorError> {
512            Ok(self.elements.contains_key(selector))
513        }
514
515        fn get_text(&self, selector: &str) -> Result<String, ExecutorError> {
516            self.elements
517                .get(selector)
518                .cloned()
519                .ok_or_else(|| ExecutorError::ElementNotFound {
520                    selector: selector.to_string(),
521                })
522        }
523
524        fn get_attribute(
525            &self,
526            _selector: &str,
527            _attribute: &str,
528        ) -> Result<String, ExecutorError> {
529            Ok("value".to_string())
530        }
531
532        fn get_url(&self) -> Result<String, ExecutorError> {
533            Ok(self.url.clone())
534        }
535
536        fn evaluate(&self, _expression: &str) -> Result<bool, ExecutorError> {
537            Ok(true)
538        }
539    }
540
541    #[test]
542    fn test_execute_simple_playbook() {
543        let yaml = r##"
544version: "1.0"
545machine:
546  id: "test"
547  initial: "start"
548  states:
549    start:
550      id: "start"
551    end:
552      id: "end"
553      final_state: true
554  transitions:
555    - id: "t1"
556      from: "start"
557      to: "end"
558      event: "go"
559      assertions:
560        - type: element_exists
561          selector: "#welcome"
562"##;
563        let playbook = Playbook::from_yaml(yaml).expect("parse");
564        let executor = MockExecutor::new();
565        let mut runner = PlaybookExecutor::new(playbook, executor);
566
567        let result = runner.execute(&["go"]);
568
569        assert!(result.success);
570        assert_eq!(result.final_state, "end");
571        assert_eq!(result.transitions_executed.len(), 1);
572        assert!(result.transitions_executed[0].assertions_passed);
573    }
574
575    #[test]
576    fn test_assertion_failure() {
577        let yaml = r##"
578version: "1.0"
579machine:
580  id: "test"
581  initial: "start"
582  states:
583    start:
584      id: "start"
585    end:
586      id: "end"
587      final_state: true
588  transitions:
589    - id: "t1"
590      from: "start"
591      to: "end"
592      event: "go"
593      assertions:
594        - type: element_exists
595          selector: "#nonexistent"
596"##;
597        let playbook = Playbook::from_yaml(yaml).expect("parse");
598        let executor = MockExecutor::new();
599        let mut runner = PlaybookExecutor::new(playbook, executor);
600
601        let result = runner.execute(&["go"]);
602
603        assert!(!result.success);
604        assert!(!result.assertion_failures.is_empty());
605    }
606
607    #[test]
608    fn test_invalid_transition() {
609        let yaml = r#"
610version: "1.0"
611machine:
612  id: "test"
613  initial: "start"
614  states:
615    start:
616      id: "start"
617    end:
618      id: "end"
619      final_state: true
620  transitions:
621    - id: "t1"
622      from: "start"
623      to: "end"
624      event: "go"
625"#;
626        let playbook = Playbook::from_yaml(yaml).expect("parse");
627        let executor = MockExecutor::new();
628        let mut runner = PlaybookExecutor::new(playbook, executor);
629
630        let result = runner.execute(&["invalid_event"]);
631
632        assert!(!result.success);
633        assert!(!result.assertion_failures.is_empty());
634    }
635
636    #[test]
637    fn test_current_state_and_reset() {
638        let yaml = r#"
639version: "1.0"
640machine:
641  id: "test"
642  initial: "start"
643  states:
644    start:
645      id: "start"
646    end:
647      id: "end"
648  transitions:
649    - id: "t1"
650      from: "start"
651      to: "end"
652      event: "go"
653"#;
654        let playbook = Playbook::from_yaml(yaml).expect("parse");
655        let executor = MockExecutor::new();
656        let mut runner = PlaybookExecutor::new(playbook, executor);
657
658        assert_eq!(runner.current_state(), "start");
659
660        runner.execute(&["go"]);
661        assert_eq!(runner.current_state(), "end");
662
663        runner.reset();
664        assert_eq!(runner.current_state(), "start");
665    }
666
667    #[test]
668    fn test_text_equals_assertion_pass() {
669        let yaml = r##"
670version: "1.0"
671machine:
672  id: "test"
673  initial: "start"
674  states:
675    start:
676      id: "start"
677    end:
678      id: "end"
679  transitions:
680    - id: "t1"
681      from: "start"
682      to: "end"
683      event: "go"
684      assertions:
685        - type: text_equals
686          selector: "#welcome"
687          expected: "Welcome, User!"
688"##;
689        let playbook = Playbook::from_yaml(yaml).expect("parse");
690        let executor = MockExecutor::new();
691        let mut runner = PlaybookExecutor::new(playbook, executor);
692
693        let result = runner.execute(&["go"]);
694        assert!(result.success);
695    }
696
697    #[test]
698    fn test_text_equals_assertion_fail() {
699        let yaml = r##"
700version: "1.0"
701machine:
702  id: "test"
703  initial: "start"
704  states:
705    start:
706      id: "start"
707    end:
708      id: "end"
709  transitions:
710    - id: "t1"
711      from: "start"
712      to: "end"
713      event: "go"
714      assertions:
715        - type: text_equals
716          selector: "#welcome"
717          expected: "Wrong Text"
718"##;
719        let playbook = Playbook::from_yaml(yaml).expect("parse");
720        let executor = MockExecutor::new();
721        let mut runner = PlaybookExecutor::new(playbook, executor);
722
723        let result = runner.execute(&["go"]);
724        assert!(!result.success);
725    }
726
727    #[test]
728    fn test_text_contains_assertion_pass() {
729        let yaml = r##"
730version: "1.0"
731machine:
732  id: "test"
733  initial: "start"
734  states:
735    start:
736      id: "start"
737    end:
738      id: "end"
739  transitions:
740    - id: "t1"
741      from: "start"
742      to: "end"
743      event: "go"
744      assertions:
745        - type: text_contains
746          selector: "#welcome"
747          substring: "Welcome"
748"##;
749        let playbook = Playbook::from_yaml(yaml).expect("parse");
750        let executor = MockExecutor::new();
751        let mut runner = PlaybookExecutor::new(playbook, executor);
752
753        let result = runner.execute(&["go"]);
754        assert!(result.success);
755    }
756
757    #[test]
758    fn test_text_contains_assertion_fail() {
759        let yaml = r##"
760version: "1.0"
761machine:
762  id: "test"
763  initial: "start"
764  states:
765    start:
766      id: "start"
767    end:
768      id: "end"
769  transitions:
770    - id: "t1"
771      from: "start"
772      to: "end"
773      event: "go"
774      assertions:
775        - type: text_contains
776          selector: "#welcome"
777          substring: "Goodbye"
778"##;
779        let playbook = Playbook::from_yaml(yaml).expect("parse");
780        let executor = MockExecutor::new();
781        let mut runner = PlaybookExecutor::new(playbook, executor);
782
783        let result = runner.execute(&["go"]);
784        assert!(!result.success);
785    }
786
787    #[test]
788    fn test_attribute_equals_assertion_pass() {
789        let yaml = r##"
790version: "1.0"
791machine:
792  id: "test"
793  initial: "start"
794  states:
795    start:
796      id: "start"
797    end:
798      id: "end"
799  transitions:
800    - id: "t1"
801      from: "start"
802      to: "end"
803      event: "go"
804      assertions:
805        - type: attribute_equals
806          selector: "#welcome"
807          attribute: "data-test"
808          expected: "value"
809"##;
810        let playbook = Playbook::from_yaml(yaml).expect("parse");
811        let executor = MockExecutor::new();
812        let mut runner = PlaybookExecutor::new(playbook, executor);
813
814        let result = runner.execute(&["go"]);
815        assert!(result.success);
816    }
817
818    #[test]
819    fn test_attribute_equals_assertion_fail() {
820        let yaml = r##"
821version: "1.0"
822machine:
823  id: "test"
824  initial: "start"
825  states:
826    start:
827      id: "start"
828    end:
829      id: "end"
830  transitions:
831    - id: "t1"
832      from: "start"
833      to: "end"
834      event: "go"
835      assertions:
836        - type: attribute_equals
837          selector: "#welcome"
838          attribute: "data-test"
839          expected: "wrong_value"
840"##;
841        let playbook = Playbook::from_yaml(yaml).expect("parse");
842        let executor = MockExecutor::new();
843        let mut runner = PlaybookExecutor::new(playbook, executor);
844
845        let result = runner.execute(&["go"]);
846        assert!(!result.success);
847    }
848
849    #[test]
850    fn test_url_matches_assertion_pass() {
851        let yaml = r##"
852version: "1.0"
853machine:
854  id: "test"
855  initial: "start"
856  states:
857    start:
858      id: "start"
859    end:
860      id: "end"
861  transitions:
862    - id: "t1"
863      from: "start"
864      to: "end"
865      event: "go"
866      assertions:
867        - type: url_matches
868          pattern: "localhost"
869"##;
870        let playbook = Playbook::from_yaml(yaml).expect("parse");
871        let executor = MockExecutor::new();
872        let mut runner = PlaybookExecutor::new(playbook, executor);
873
874        let result = runner.execute(&["go"]);
875        assert!(result.success);
876    }
877
878    #[test]
879    fn test_url_matches_assertion_fail() {
880        let yaml = r##"
881version: "1.0"
882machine:
883  id: "test"
884  initial: "start"
885  states:
886    start:
887      id: "start"
888    end:
889      id: "end"
890  transitions:
891    - id: "t1"
892      from: "start"
893      to: "end"
894      event: "go"
895      assertions:
896        - type: url_matches
897          pattern: "example.com"
898"##;
899        let playbook = Playbook::from_yaml(yaml).expect("parse");
900        let executor = MockExecutor::new();
901        let mut runner = PlaybookExecutor::new(playbook, executor);
902
903        let result = runner.execute(&["go"]);
904        assert!(!result.success);
905    }
906
907    #[test]
908    fn test_script_assertion_pass() {
909        let yaml = r##"
910version: "1.0"
911machine:
912  id: "test"
913  initial: "start"
914  states:
915    start:
916      id: "start"
917    end:
918      id: "end"
919  transitions:
920    - id: "t1"
921      from: "start"
922      to: "end"
923      event: "go"
924      assertions:
925        - type: script
926          expression: "true"
927"##;
928        let playbook = Playbook::from_yaml(yaml).expect("parse");
929        let executor = MockExecutor::new();
930        let mut runner = PlaybookExecutor::new(playbook, executor);
931
932        let result = runner.execute(&["go"]);
933        assert!(result.success);
934    }
935
936    #[test]
937    fn test_transition_with_actions() {
938        let yaml = r##"
939version: "1.0"
940machine:
941  id: "test"
942  initial: "start"
943  states:
944    start:
945      id: "start"
946    end:
947      id: "end"
948  transitions:
949    - id: "t1"
950      from: "start"
951      to: "end"
952      event: "go"
953      actions:
954        - type: click
955          selector: "#button"
956        - type: type
957          selector: "#input"
958          text: "hello"
959        - type: wait
960          condition:
961            type: duration
962            ms: 100
963        - type: navigate
964          url: "http://example.com"
965        - type: script
966          code: "console.log('test')"
967        - type: screenshot
968          name: "test_screenshot"
969"##;
970        let playbook = Playbook::from_yaml(yaml).expect("parse");
971        let executor = MockExecutor::new();
972        let mut runner = PlaybookExecutor::new(playbook, executor);
973
974        let result = runner.execute(&["go"]);
975        assert!(result.success);
976    }
977
978    #[test]
979    fn test_entry_exit_actions() {
980        let yaml = r##"
981version: "1.0"
982machine:
983  id: "test"
984  initial: "start"
985  states:
986    start:
987      id: "start"
988      on_exit:
989        - type: click
990          selector: "#exit-start"
991    middle:
992      id: "middle"
993      on_entry:
994        - type: click
995          selector: "#enter-middle"
996      on_exit:
997        - type: click
998          selector: "#exit-middle"
999    end:
1000      id: "end"
1001      on_entry:
1002        - type: click
1003          selector: "#enter-end"
1004  transitions:
1005    - id: "t1"
1006      from: "start"
1007      to: "middle"
1008      event: "step1"
1009    - id: "t2"
1010      from: "middle"
1011      to: "end"
1012      event: "step2"
1013"##;
1014        let playbook = Playbook::from_yaml(yaml).expect("parse");
1015        let executor = MockExecutor::new();
1016        let mut runner = PlaybookExecutor::new(playbook, executor);
1017
1018        let result = runner.execute(&["step1", "step2"]);
1019        assert!(result.success);
1020        assert_eq!(result.transitions_executed.len(), 2);
1021        assert_eq!(result.final_state, "end");
1022    }
1023
1024    #[test]
1025    fn test_multiple_transitions() {
1026        let yaml = r#"
1027version: "1.0"
1028machine:
1029  id: "test"
1030  initial: "a"
1031  states:
1032    a:
1033      id: "a"
1034    b:
1035      id: "b"
1036    c:
1037      id: "c"
1038  transitions:
1039    - id: "t1"
1040      from: "a"
1041      to: "b"
1042      event: "next"
1043    - id: "t2"
1044      from: "b"
1045      to: "c"
1046      event: "next"
1047"#;
1048        let playbook = Playbook::from_yaml(yaml).expect("parse");
1049        let executor = MockExecutor::new();
1050        let mut runner = PlaybookExecutor::new(playbook, executor);
1051
1052        let result = runner.execute(&["next", "next"]);
1053        assert!(result.success);
1054        assert_eq!(result.final_state, "c");
1055        assert_eq!(result.metrics.transition_count, 2);
1056    }
1057
1058    #[test]
1059    fn test_text_assertion_element_not_found() {
1060        let yaml = r##"
1061version: "1.0"
1062machine:
1063  id: "test"
1064  initial: "start"
1065  states:
1066    start:
1067      id: "start"
1068    end:
1069      id: "end"
1070  transitions:
1071    - id: "t1"
1072      from: "start"
1073      to: "end"
1074      event: "go"
1075      assertions:
1076        - type: text_equals
1077          selector: "#nonexistent"
1078          expected: "some text"
1079"##;
1080        let playbook = Playbook::from_yaml(yaml).expect("parse");
1081        let executor = MockExecutor::new();
1082        let mut runner = PlaybookExecutor::new(playbook, executor);
1083
1084        let result = runner.execute(&["go"]);
1085        assert!(!result.success);
1086    }
1087
1088    #[test]
1089    fn test_text_contains_element_not_found() {
1090        let yaml = r##"
1091version: "1.0"
1092machine:
1093  id: "test"
1094  initial: "start"
1095  states:
1096    start:
1097      id: "start"
1098    end:
1099      id: "end"
1100  transitions:
1101    - id: "t1"
1102      from: "start"
1103      to: "end"
1104      event: "go"
1105      assertions:
1106        - type: text_contains
1107          selector: "#nonexistent"
1108          substring: "text"
1109"##;
1110        let playbook = Playbook::from_yaml(yaml).expect("parse");
1111        let executor = MockExecutor::new();
1112        let mut runner = PlaybookExecutor::new(playbook, executor);
1113
1114        let result = runner.execute(&["go"]);
1115        assert!(!result.success);
1116    }
1117}