1use super::complexity::{check_complexity_violation, ComplexityResult};
7use super::schema::{Action, Assertion, Playbook, Transition, WaitCondition};
8use std::time::{Duration, Instant};
9
10#[derive(Debug)]
12pub struct ExecutionResult {
13 pub success: bool,
15 pub final_state: String,
17 pub transitions_executed: Vec<TransitionResult>,
19 pub total_time: Duration,
21 pub metrics: ExecutionMetrics,
23 pub assertion_failures: Vec<AssertionFailure>,
25 pub complexity_result: Option<ComplexityResult>,
27}
28
29#[derive(Debug, Clone)]
31pub struct TransitionResult {
32 pub transition_id: String,
34 pub from_state: String,
36 pub to_state: String,
38 pub duration: Duration,
40 pub assertions_passed: bool,
42 pub assertion_results: Vec<AssertionResult>,
44}
45
46#[derive(Debug, Clone)]
48pub struct AssertionResult {
49 pub passed: bool,
51 pub description: String,
53 pub error: Option<String>,
55}
56
57#[derive(Debug, Clone)]
59pub struct AssertionFailure {
60 pub transition_id: String,
62 pub assertion_description: String,
64 pub error: String,
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct ExecutionMetrics {
71 pub transition_times: Vec<(usize, f64)>,
73 pub peak_memory_bytes: Option<u64>,
75 pub transition_count: usize,
77}
78
79pub trait ActionExecutor {
82 fn click(&mut self, selector: &str) -> Result<(), ExecutorError>;
84
85 fn type_text(&mut self, selector: &str, text: &str) -> Result<(), ExecutorError>;
87
88 fn wait(&mut self, condition: &WaitCondition) -> Result<(), ExecutorError>;
90
91 fn navigate(&mut self, url: &str) -> Result<(), ExecutorError>;
93
94 fn execute_script(&mut self, code: &str) -> Result<String, ExecutorError>;
96
97 fn screenshot(&mut self, name: &str) -> Result<(), ExecutorError>;
99
100 fn element_exists(&self, selector: &str) -> Result<bool, ExecutorError>;
102
103 fn get_text(&self, selector: &str) -> Result<String, ExecutorError>;
105
106 fn get_attribute(&self, selector: &str, attribute: &str) -> Result<String, ExecutorError>;
108
109 fn get_url(&self) -> Result<String, ExecutorError>;
111
112 fn evaluate(&self, expression: &str) -> Result<bool, ExecutorError>;
114}
115
116#[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
141pub 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 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 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 metrics.transition_times.push((
174 self.transition_count,
175 result.duration.as_secs_f64() * 1000.0,
176 ));
177
178 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 let complexity_result =
210 self.playbook.performance.complexity_class.map(|expected| {
211 check_complexity_violation(metrics.transition_times.clone(), expected)
212 });
213
214 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 fn trigger_event(&mut self, event: &str) -> Result<TransitionResult, ExecutorError> {
234 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 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 for action in &exit_actions {
255 self.execute_action(action)?;
256 }
257
258 for action in &transition_actions {
260 self.execute_action(action)?;
261 }
262
263 self.current_state = to_state.clone();
265 self.transition_count += 1;
266
267 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 for action in &entry_actions {
278 self.execute_action(action)?;
279 }
280
281 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 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 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 fn check_assertions(&self, assertions: &[Assertion]) -> Vec<AssertionResult> {
324 assertions
325 .iter()
326 .map(|assertion| self.check_assertion(assertion))
327 .collect()
328 }
329
330 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 pub fn current_state(&self) -> &str {
451 &self.current_state
452 }
453
454 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 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}