1#![forbid(unsafe_code)]
2
3use ftui_core::event::Event;
31use web_time::{Duration, Instant};
32
33#[derive(Debug, Clone)]
35pub struct TimedEvent {
36 pub event: Event,
38 pub delay: Duration,
40}
41
42impl TimedEvent {
43 pub fn new(event: Event, delay: Duration) -> Self {
45 Self { event, delay }
46 }
47
48 pub fn immediate(event: Event) -> Self {
50 Self {
51 event,
52 delay: Duration::ZERO,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct MacroMetadata {
60 pub name: String,
62 pub terminal_size: (u16, u16),
64 pub total_duration: Duration,
66}
67
68#[derive(Debug, Clone)]
73pub struct InputMacro {
74 events: Vec<TimedEvent>,
76 metadata: MacroMetadata,
78}
79
80impl InputMacro {
81 pub fn new(events: Vec<TimedEvent>, metadata: MacroMetadata) -> Self {
83 Self { events, metadata }
84 }
85
86 pub fn from_events(name: impl Into<String>, events: Vec<Event>) -> Self {
90 let timed: Vec<TimedEvent> = events.into_iter().map(TimedEvent::immediate).collect();
91 Self {
92 metadata: MacroMetadata {
93 name: name.into(),
94 terminal_size: (80, 24),
95 total_duration: Duration::ZERO,
96 },
97 events: timed,
98 }
99 }
100
101 pub fn events(&self) -> &[TimedEvent] {
103 &self.events
104 }
105
106 #[inline]
108 pub fn metadata(&self) -> &MacroMetadata {
109 &self.metadata
110 }
111
112 #[inline]
114 pub fn len(&self) -> usize {
115 self.events.len()
116 }
117
118 #[inline]
120 pub fn is_empty(&self) -> bool {
121 self.events.is_empty()
122 }
123
124 #[inline]
126 pub fn total_duration(&self) -> Duration {
127 self.metadata.total_duration
128 }
129
130 pub fn bare_events(&self) -> Vec<Event> {
132 self.events.iter().map(|te| te.event.clone()).collect()
133 }
134
135 pub fn replay_with_timing<M: crate::program::Model>(
137 &self,
138 sim: &mut crate::simulator::ProgramSimulator<M>,
139 ) {
140 let mut player = MacroPlayer::new(self);
141 player.replay_with_timing(sim);
142 }
143
144 pub fn replay_with_sleeper<M, F>(
148 &self,
149 sim: &mut crate::simulator::ProgramSimulator<M>,
150 sleep: F,
151 ) where
152 M: crate::program::Model,
153 F: FnMut(Duration),
154 {
155 let mut player = MacroPlayer::new(self);
156 player.replay_with_sleeper(sim, sleep);
157 }
158}
159
160pub struct MacroRecorder {
165 name: String,
166 terminal_size: (u16, u16),
167 events: Vec<TimedEvent>,
168 start_time: Instant,
169 last_event_time: Instant,
170}
171
172impl MacroRecorder {
173 pub fn new(name: impl Into<String>) -> Self {
175 let now = Instant::now();
176 Self {
177 name: name.into(),
178 terminal_size: (80, 24),
179 events: Vec::new(),
180 start_time: now,
181 last_event_time: now,
182 }
183 }
184
185 #[must_use]
187 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
188 self.terminal_size = (width, height);
189 self
190 }
191
192 pub fn record_event(&mut self, event: Event) {
196 let now = Instant::now();
197 let delay = now.duration_since(self.last_event_time);
198 #[cfg(feature = "tracing")]
199 tracing::debug!(event = ?event, delay = ?delay, "macro record event");
200 self.events.push(TimedEvent::new(event, delay));
201 self.last_event_time = now;
202 }
203
204 pub fn record_event_with_delay(&mut self, event: Event, delay: Duration) {
206 #[cfg(feature = "tracing")]
207 tracing::debug!(event = ?event, delay = ?delay, "macro record event");
208 self.events.push(TimedEvent::new(event, delay));
209 self.last_event_time += delay;
211 }
212
213 pub fn event_count(&self) -> usize {
215 self.events.len()
216 }
217
218 pub fn finish(self) -> InputMacro {
220 let total_duration = self.last_event_time.duration_since(self.start_time);
221 InputMacro {
222 events: self.events,
223 metadata: MacroMetadata {
224 name: self.name,
225 terminal_size: self.terminal_size,
226 total_duration,
227 },
228 }
229 }
230}
231
232pub struct MacroPlayer<'a> {
238 input_macro: &'a InputMacro,
239 position: usize,
240 elapsed: Duration,
241}
242
243impl<'a> MacroPlayer<'a> {
244 pub fn new(input_macro: &'a InputMacro) -> Self {
246 Self {
247 input_macro,
248 position: 0,
249 elapsed: Duration::ZERO,
250 }
251 }
252
253 pub fn position(&self) -> usize {
255 self.position
256 }
257
258 pub fn elapsed(&self) -> Duration {
260 self.elapsed
261 }
262
263 pub fn is_done(&self) -> bool {
265 self.position >= self.input_macro.len()
266 }
267
268 pub fn remaining(&self) -> usize {
270 self.input_macro.len().saturating_sub(self.position)
271 }
272
273 pub fn step<M: crate::program::Model>(
277 &mut self,
278 sim: &mut crate::simulator::ProgramSimulator<M>,
279 ) -> bool {
280 if self.is_done() {
281 return false;
282 }
283
284 let timed = &self.input_macro.events[self.position];
285 #[cfg(feature = "tracing")]
286 tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
287 self.elapsed = self.elapsed.saturating_add(timed.delay);
288 sim.inject_events(std::slice::from_ref(&timed.event));
289 self.position += 1;
290 true
291 }
292
293 pub fn replay_all<M: crate::program::Model>(
297 &mut self,
298 sim: &mut crate::simulator::ProgramSimulator<M>,
299 ) {
300 while !self.is_done() && sim.is_running() {
301 self.step(sim);
302 }
303 }
304
305 pub fn replay_with_timing<M: crate::program::Model>(
310 &mut self,
311 sim: &mut crate::simulator::ProgramSimulator<M>,
312 ) {
313 self.replay_with_sleeper(sim, std::thread::sleep);
314 }
315
316 pub fn replay_with_sleeper<M, F>(
321 &mut self,
322 sim: &mut crate::simulator::ProgramSimulator<M>,
323 mut sleep: F,
324 ) where
325 M: crate::program::Model,
326 F: FnMut(Duration),
327 {
328 while !self.is_done() && sim.is_running() {
329 let timed = &self.input_macro.events[self.position];
330 if timed.delay > Duration::ZERO {
331 sleep(timed.delay);
332 }
333 self.step(sim);
334 }
335 }
336
337 pub fn replay_until<M: crate::program::Model>(
341 &mut self,
342 sim: &mut crate::simulator::ProgramSimulator<M>,
343 until: Duration,
344 ) {
345 while !self.is_done() && sim.is_running() {
346 let timed = &self.input_macro.events[self.position];
347 let next_elapsed = self.elapsed.saturating_add(timed.delay);
348 if next_elapsed > until {
349 break;
350 }
351 self.step(sim);
352 }
353 }
354
355 pub fn reset(&mut self) {
357 self.position = 0;
358 self.elapsed = Duration::ZERO;
359 }
360}
361
362#[derive(Debug, Clone)]
377pub struct MacroPlayback {
378 input_macro: InputMacro,
379 position: usize,
380 elapsed: Duration,
381 next_due: Duration,
382 speed: f64,
383 looping: bool,
384 start_logged: bool,
385 stop_logged: bool,
386 error_logged: bool,
387}
388
389const MAX_DUE_EVENTS_PER_ADVANCE: usize = 4096;
392
393impl MacroPlayback {
394 pub fn new(input_macro: InputMacro) -> Self {
396 let next_due = input_macro
397 .events()
398 .first()
399 .map(|e| e.delay)
400 .unwrap_or(Duration::ZERO);
401 Self {
402 input_macro,
403 position: 0,
404 elapsed: Duration::ZERO,
405 next_due,
406 speed: 1.0,
407 looping: false,
408 start_logged: false,
409 stop_logged: false,
410 error_logged: false,
411 }
412 }
413
414 pub fn set_speed(&mut self, speed: f64) {
416 self.speed = normalize_speed(speed);
417 }
418
419 #[must_use]
421 pub fn with_speed(mut self, speed: f64) -> Self {
422 self.set_speed(speed);
423 self
424 }
425
426 pub fn set_looping(&mut self, looping: bool) {
428 self.looping = looping;
429 }
430
431 #[must_use]
433 pub fn with_looping(mut self, looping: bool) -> Self {
434 self.set_looping(looping);
435 self
436 }
437
438 pub fn speed(&self) -> f64 {
440 self.speed
441 }
442
443 pub fn position(&self) -> usize {
445 self.position
446 }
447
448 pub fn elapsed(&self) -> Duration {
450 self.elapsed
451 }
452
453 pub fn is_done(&self) -> bool {
455 if self.input_macro.is_empty() {
456 return true;
457 }
458 if self.looping && self.input_macro.total_duration() > Duration::ZERO {
459 return false;
460 }
461 self.position >= self.input_macro.len()
462 }
463
464 pub fn reset(&mut self) {
466 self.position = 0;
467 self.elapsed = Duration::ZERO;
468 self.next_due = self
469 .input_macro
470 .events()
471 .first()
472 .map(|e| e.delay)
473 .unwrap_or(Duration::ZERO);
474 self.start_logged = false;
475 self.stop_logged = false;
476 self.error_logged = false;
477 }
478
479 pub fn advance(&mut self, delta: Duration) -> Vec<Event> {
481 if self.input_macro.is_empty() {
482 #[cfg(feature = "tracing")]
483 if !self.error_logged {
484 let meta = self.input_macro.metadata();
485 tracing::warn!(
486 macro_event = "playback_error",
487 reason = "macro_empty",
488 name = %meta.name,
489 events = 0usize,
490 duration_ms = self.input_macro.total_duration().as_millis() as u64,
491 );
492 self.error_logged = true;
493 }
494 return Vec::new();
495 }
496 if self.is_done() {
497 return Vec::new();
498 }
499
500 #[cfg(feature = "tracing")]
501 if !self.start_logged {
502 let meta = self.input_macro.metadata();
503 tracing::info!(
504 macro_event = "playback_start",
505 name = %meta.name,
506 events = self.input_macro.len(),
507 duration_ms = self.input_macro.total_duration().as_millis() as u64,
508 speed = self.speed,
509 looping = self.looping,
510 );
511 self.start_logged = true;
512 }
513
514 let scaled = scale_duration(delta, self.speed);
515 let total_duration = self.input_macro.total_duration();
516 if self.looping && total_duration > Duration::ZERO && scaled == Duration::MAX {
517 self.elapsed =
520 loop_elapsed_remainder(self.elapsed, total_duration).saturating_add(total_duration);
521 } else {
522 self.elapsed = self.elapsed.saturating_add(scaled);
523 }
524 let events = self.drain_due_events();
525
526 #[cfg(feature = "tracing")]
527 if self.is_done() && !self.stop_logged {
528 let meta = self.input_macro.metadata();
529 tracing::info!(
530 macro_event = "playback_stop",
531 reason = "completed",
532 name = %meta.name,
533 events = self.input_macro.len(),
534 elapsed_ms = self.elapsed.as_millis() as u64,
535 looping = self.looping,
536 );
537 self.stop_logged = true;
538 }
539
540 events
541 }
542
543 fn drain_due_events(&mut self) -> Vec<Event> {
544 let mut out = Vec::new();
545 let total_duration = self.input_macro.total_duration();
546 let can_loop = self.looping && total_duration > Duration::ZERO;
547 if can_loop && self.position >= self.input_macro.len() {
548 self.elapsed = loop_elapsed_remainder(self.elapsed, total_duration);
549 self.position = 0;
550 self.next_due = self
551 .input_macro
552 .events()
553 .first()
554 .map(|e| e.delay)
555 .unwrap_or(Duration::ZERO);
556 }
557
558 while out.len() < MAX_DUE_EVENTS_PER_ADVANCE
559 && self.position < self.input_macro.len()
560 && self.elapsed >= self.next_due
561 {
562 let timed = &self.input_macro.events[self.position];
563 #[cfg(feature = "tracing")]
564 tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
565 out.push(timed.event.clone());
566 self.position += 1;
567 if self.position < self.input_macro.len() {
568 self.next_due = self
569 .next_due
570 .saturating_add(self.input_macro.events[self.position].delay);
571 } else if can_loop {
572 self.elapsed = self.elapsed.saturating_sub(total_duration);
574 self.position = 0;
575 self.next_due = self
576 .input_macro
577 .events()
578 .first()
579 .map(|e| e.delay)
580 .unwrap_or(Duration::ZERO);
581 }
582 }
583
584 if can_loop && out.len() == MAX_DUE_EVENTS_PER_ADVANCE {
585 self.elapsed = loop_elapsed_remainder(self.elapsed, total_duration);
588 if self.position >= self.input_macro.len() {
589 self.position = 0;
590 self.next_due = self
591 .input_macro
592 .events()
593 .first()
594 .map(|e| e.delay)
595 .unwrap_or(Duration::ZERO);
596 }
597 }
598
599 out
600 }
601}
602
603fn normalize_speed(speed: f64) -> f64 {
604 if !speed.is_finite() {
605 return 1.0;
606 }
607 if speed <= 0.0 {
608 return 0.0;
609 }
610 speed
611}
612
613fn scale_duration(delta: Duration, speed: f64) -> Duration {
614 if delta == Duration::ZERO {
615 return Duration::ZERO;
616 }
617 let speed = normalize_speed(speed);
618 if speed == 0.0 {
619 return Duration::ZERO;
620 }
621 if speed == 1.0 {
622 return delta;
623 }
624 duration_from_secs_f64_saturating(delta.as_secs_f64() * speed)
625}
626
627fn duration_from_secs_f64_saturating(secs: f64) -> Duration {
628 if secs.is_nan() || secs <= 0.0 {
629 return Duration::ZERO;
630 }
631 Duration::try_from_secs_f64(secs).unwrap_or(Duration::MAX)
632}
633
634fn loop_elapsed_remainder(elapsed: Duration, total_duration: Duration) -> Duration {
635 let total_secs = total_duration.as_secs_f64();
636 if total_secs <= 0.0 {
637 return Duration::ZERO;
638 }
639 let elapsed_secs = elapsed.as_secs_f64() % total_secs;
640 duration_from_secs_f64_saturating(elapsed_secs)
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub enum RecordingState {
650 Idle,
652 Recording,
654 Paused,
656}
657
658pub struct EventRecorder {
682 inner: MacroRecorder,
683 state: RecordingState,
684 pause_start: Option<Instant>,
685 total_paused: Duration,
686 event_count: usize,
687}
688
689impl EventRecorder {
690 pub fn new(name: impl Into<String>) -> Self {
695 Self {
696 inner: MacroRecorder::new(name),
697 state: RecordingState::Idle,
698 pause_start: None,
699 total_paused: Duration::ZERO,
700 event_count: 0,
701 }
702 }
703
704 #[must_use]
706 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
707 self.inner = self.inner.with_terminal_size(width, height);
708 self
709 }
710
711 pub fn state(&self) -> RecordingState {
713 self.state
714 }
715
716 pub fn is_recording(&self) -> bool {
718 self.state == RecordingState::Recording
719 }
720
721 pub fn start(&mut self) {
723 match self.state {
724 RecordingState::Idle => {
725 self.state = RecordingState::Recording;
726 #[cfg(feature = "tracing")]
727 tracing::info!(
728 macro_event = "recorder_start",
729 name = %self.inner.name,
730 term_cols = self.inner.terminal_size.0,
731 term_rows = self.inner.terminal_size.1,
732 );
733 }
734 RecordingState::Paused => {
735 self.resume();
736 }
737 RecordingState::Recording => {} }
739 }
740
741 pub fn pause(&mut self) {
745 if self.state == RecordingState::Recording {
746 self.state = RecordingState::Paused;
747 self.pause_start = Some(Instant::now());
748 }
749 }
750
751 pub fn resume(&mut self) {
755 if self.state == RecordingState::Paused {
756 if let Some(pause_start) = self.pause_start.take() {
757 self.total_paused += pause_start.elapsed();
758 }
759 self.inner.last_event_time = Instant::now();
763 self.state = RecordingState::Recording;
764 }
765 }
766
767 pub fn record(&mut self, event: &Event) -> bool {
771 if self.state != RecordingState::Recording {
772 return false;
773 }
774 self.inner.record_event(event.clone());
775 self.event_count += 1;
776 true
777 }
778
779 pub fn record_with_delay(&mut self, event: &Event, delay: Duration) -> bool {
783 if self.state != RecordingState::Recording {
784 return false;
785 }
786 self.inner.record_event_with_delay(event.clone(), delay);
787 self.event_count += 1;
788 true
789 }
790
791 pub fn event_count(&self) -> usize {
793 self.event_count
794 }
795
796 pub fn total_paused(&self) -> Duration {
798 let mut total = self.total_paused;
799 if let Some(pause_start) = self.pause_start {
800 total += pause_start.elapsed();
801 }
802 total
803 }
804
805 pub fn finish(self) -> InputMacro {
809 self.finish_internal(true)
810 }
811
812 #[allow(unused_variables)]
813 fn finish_internal(self, log: bool) -> InputMacro {
814 let paused = self.total_paused();
815 let macro_data = self.inner.finish();
816 #[cfg(feature = "tracing")]
817 if log {
818 let meta = macro_data.metadata();
819 tracing::info!(
820 macro_event = "recorder_stop",
821 name = %meta.name,
822 events = macro_data.len(),
823 duration_ms = macro_data.total_duration().as_millis() as u64,
824 paused_ms = paused.as_millis() as u64,
825 term_cols = meta.terminal_size.0,
826 term_rows = meta.terminal_size.1,
827 );
828 }
829 macro_data
830 }
831
832 pub fn discard(self) -> usize {
836 self.event_count
837 }
838}
839
840#[derive(Debug, Clone)]
845pub struct RecordingFilter {
846 pub keys: bool,
848 pub mouse: bool,
850 pub resize: bool,
852 pub paste: bool,
854 pub focus: bool,
856}
857
858impl Default for RecordingFilter {
859 fn default() -> Self {
860 Self {
861 keys: true,
862 mouse: true,
863 resize: true,
864 paste: true,
865 focus: true,
866 }
867 }
868}
869
870impl RecordingFilter {
871 pub fn keys_only() -> Self {
873 Self {
874 keys: true,
875 mouse: false,
876 resize: false,
877 paste: false,
878 focus: false,
879 }
880 }
881
882 pub fn accepts(&self, event: &Event) -> bool {
884 match event {
885 Event::Key(_) => self.keys,
886 Event::Mouse(_) => self.mouse,
887 Event::Resize { .. } => self.resize,
888 Event::Paste(_) => self.paste,
889 Event::Focus(_) => self.focus,
890 Event::Clipboard(_) => true, Event::Tick => false, }
893 }
894}
895
896pub struct FilteredEventRecorder {
898 recorder: EventRecorder,
899 filter: RecordingFilter,
900 filtered_count: usize,
901}
902
903impl FilteredEventRecorder {
904 pub fn new(name: impl Into<String>, filter: RecordingFilter) -> Self {
906 Self {
907 recorder: EventRecorder::new(name),
908 filter,
909 filtered_count: 0,
910 }
911 }
912
913 #[must_use]
915 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
916 self.recorder = self.recorder.with_terminal_size(width, height);
917 self
918 }
919
920 pub fn start(&mut self) {
922 self.recorder.start();
923 }
924
925 pub fn pause(&mut self) {
927 self.recorder.pause();
928 }
929
930 pub fn resume(&mut self) {
932 self.recorder.resume();
933 }
934
935 pub fn state(&self) -> RecordingState {
937 self.recorder.state()
938 }
939
940 pub fn is_recording(&self) -> bool {
942 self.recorder.is_recording()
943 }
944
945 pub fn record(&mut self, event: &Event) -> bool {
949 if !self.filter.accepts(event) {
950 self.filtered_count += 1;
951 return false;
952 }
953 self.recorder.record(event)
954 }
955
956 pub fn filtered_count(&self) -> usize {
958 self.filtered_count
959 }
960
961 pub fn event_count(&self) -> usize {
963 self.recorder.event_count()
964 }
965
966 #[allow(unused_variables)]
968 pub fn finish(self) -> InputMacro {
969 let filtered = self.filtered_count;
970 let paused = self.recorder.total_paused();
971 let macro_data = self.recorder.finish_internal(false);
972 #[cfg(feature = "tracing")]
973 {
974 let meta = macro_data.metadata();
975 tracing::info!(
976 macro_event = "recorder_stop",
977 name = %meta.name,
978 events = macro_data.len(),
979 filtered,
980 duration_ms = macro_data.total_duration().as_millis() as u64,
981 paused_ms = paused.as_millis() as u64,
982 term_cols = meta.terminal_size.0,
983 term_rows = meta.terminal_size.1,
984 );
985 }
986 macro_data
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993 use crate::program::{Cmd, Model};
994 use crate::simulator::ProgramSimulator;
995 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
996 use ftui_render::frame::Frame;
997 use proptest::prelude::*;
998
999 struct Counter {
1002 value: i32,
1003 }
1004
1005 #[derive(Debug)]
1006 enum CounterMsg {
1007 Increment,
1008 Decrement,
1009 Quit,
1010 }
1011
1012 impl From<Event> for CounterMsg {
1013 fn from(event: Event) -> Self {
1014 match event {
1015 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
1016 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
1017 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
1018 _ => CounterMsg::Increment,
1019 }
1020 }
1021 }
1022
1023 impl Model for Counter {
1024 type Message = CounterMsg;
1025
1026 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1027 match msg {
1028 CounterMsg::Increment => {
1029 self.value += 1;
1030 Cmd::none()
1031 }
1032 CounterMsg::Decrement => {
1033 self.value -= 1;
1034 Cmd::none()
1035 }
1036 CounterMsg::Quit => Cmd::quit(),
1037 }
1038 }
1039
1040 fn view(&self, _frame: &mut Frame) {}
1041 }
1042
1043 fn key_event(c: char) -> Event {
1044 Event::Key(KeyEvent {
1045 code: KeyCode::Char(c),
1046 modifiers: Modifiers::empty(),
1047 kind: KeyEventKind::Press,
1048 })
1049 }
1050
1051 #[test]
1054 fn timed_event_immediate_has_zero_delay() {
1055 let te = TimedEvent::immediate(key_event('a'));
1056 assert_eq!(te.delay, Duration::ZERO);
1057 }
1058
1059 #[test]
1060 fn timed_event_new_preserves_delay() {
1061 let delay = Duration::from_millis(100);
1062 let te = TimedEvent::new(key_event('x'), delay);
1063 assert_eq!(te.delay, delay);
1064 }
1065
1066 #[test]
1069 fn macro_from_events_has_zero_delays() {
1070 let m = InputMacro::from_events("test", vec![key_event('+'), key_event('-')]);
1071 assert_eq!(m.len(), 2);
1072 assert!(!m.is_empty());
1073 assert_eq!(m.total_duration(), Duration::ZERO);
1074 for te in m.events() {
1075 assert_eq!(te.delay, Duration::ZERO);
1076 }
1077 }
1078
1079 #[test]
1080 fn macro_metadata() {
1081 let m = InputMacro::from_events("my_macro", vec![key_event('a')]);
1082 assert_eq!(m.metadata().name, "my_macro");
1083 assert_eq!(m.metadata().terminal_size, (80, 24));
1084 }
1085
1086 #[test]
1087 fn empty_macro() {
1088 let m = InputMacro::from_events("empty", vec![]);
1089 assert!(m.is_empty());
1090 assert_eq!(m.len(), 0);
1091 }
1092
1093 #[test]
1094 fn bare_events_extracts_events() {
1095 let events = vec![key_event('+'), key_event('-'), key_event('q')];
1096 let m = InputMacro::from_events("test", events.clone());
1097 let bare = m.bare_events();
1098 assert_eq!(bare.len(), 3);
1099 assert_eq!(bare, events);
1100 }
1101
1102 #[test]
1105 fn recorder_captures_events() {
1106 let mut rec = MacroRecorder::new("rec_test");
1107 rec.record_event(key_event('+'));
1108 rec.record_event(key_event('+'));
1109 rec.record_event(key_event('-'));
1110 assert_eq!(rec.event_count(), 3);
1111
1112 let m = rec.finish();
1113 assert_eq!(m.len(), 3);
1114 assert_eq!(m.metadata().name, "rec_test");
1115 }
1116
1117 #[test]
1118 fn recorder_with_terminal_size() {
1119 let rec = MacroRecorder::new("sized").with_terminal_size(120, 40);
1120 let m = rec.finish();
1121 assert_eq!(m.metadata().terminal_size, (120, 40));
1122 }
1123
1124 #[test]
1125 fn recorder_explicit_delays() {
1126 let mut rec = MacroRecorder::new("delayed");
1127 rec.record_event_with_delay(key_event('+'), Duration::from_millis(0));
1128 rec.record_event_with_delay(key_event('-'), Duration::from_millis(50));
1129 rec.record_event_with_delay(key_event('q'), Duration::from_millis(100));
1130
1131 let m = rec.finish();
1132 assert_eq!(m.events()[0].delay, Duration::from_millis(0));
1133 assert_eq!(m.events()[1].delay, Duration::from_millis(50));
1134 assert_eq!(m.events()[2].delay, Duration::from_millis(100));
1135 }
1136
1137 #[test]
1140 fn player_replays_all_events() {
1141 let m = InputMacro::from_events(
1142 "replay",
1143 vec![key_event('+'), key_event('+'), key_event('+')],
1144 );
1145
1146 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1147 sim.init();
1148
1149 let mut player = MacroPlayer::new(&m);
1150 assert_eq!(player.remaining(), 3);
1151 assert!(!player.is_done());
1152
1153 player.replay_all(&mut sim);
1154
1155 assert!(player.is_done());
1156 assert_eq!(player.remaining(), 0);
1157 assert_eq!(sim.model().value, 3);
1158 }
1159
1160 #[test]
1161 fn player_step_advances_position() {
1162 let m = InputMacro::from_events("step", vec![key_event('+'), key_event('+')]);
1163
1164 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1165 sim.init();
1166
1167 let mut player = MacroPlayer::new(&m);
1168 assert_eq!(player.position(), 0);
1169
1170 assert!(player.step(&mut sim));
1171 assert_eq!(player.position(), 1);
1172 assert_eq!(sim.model().value, 1);
1173
1174 assert!(player.step(&mut sim));
1175 assert_eq!(player.position(), 2);
1176 assert_eq!(sim.model().value, 2);
1177
1178 assert!(!player.step(&mut sim));
1179 }
1180
1181 #[test]
1182 fn player_stops_on_quit() {
1183 let m = InputMacro::from_events(
1184 "quit_test",
1185 vec![key_event('+'), key_event('q'), key_event('+')],
1186 );
1187
1188 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1189 sim.init();
1190
1191 let mut player = MacroPlayer::new(&m);
1192 player.replay_all(&mut sim);
1193
1194 assert_eq!(sim.model().value, 1);
1196 assert!(!sim.is_running());
1197 }
1198
1199 #[test]
1200 fn player_replay_until_respects_time() {
1201 let events = vec![
1202 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1203 TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1204 TimedEvent::new(key_event('+'), Duration::from_millis(100)),
1205 ];
1206 let m = InputMacro::new(
1207 events,
1208 MacroMetadata {
1209 name: "timed".to_string(),
1210 terminal_size: (80, 24),
1211 total_duration: Duration::from_millis(130),
1212 },
1213 );
1214
1215 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1216 sim.init();
1217
1218 let mut player = MacroPlayer::new(&m);
1219
1220 player.replay_until(&mut sim, Duration::from_millis(50));
1222 assert_eq!(sim.model().value, 2);
1223 assert_eq!(player.position(), 2);
1224
1225 player.replay_until(&mut sim, Duration::from_millis(200));
1227 assert_eq!(sim.model().value, 3);
1228 assert!(player.is_done());
1229 }
1230
1231 #[test]
1232 fn player_elapsed_tracks_virtual_time() {
1233 let events = vec![
1234 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1235 TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1236 ];
1237 let m = InputMacro::new(
1238 events,
1239 MacroMetadata {
1240 name: "elapsed".to_string(),
1241 terminal_size: (80, 24),
1242 total_duration: Duration::from_millis(30),
1243 },
1244 );
1245
1246 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1247 sim.init();
1248
1249 let mut player = MacroPlayer::new(&m);
1250 assert_eq!(player.elapsed(), Duration::ZERO);
1251
1252 player.step(&mut sim);
1253 assert_eq!(player.elapsed(), Duration::from_millis(10));
1254
1255 player.step(&mut sim);
1256 assert_eq!(player.elapsed(), Duration::from_millis(30));
1257 }
1258
1259 #[test]
1260 fn player_reset_restarts_playback() {
1261 let m = InputMacro::from_events("reset", vec![key_event('+'), key_event('+')]);
1262
1263 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1264 sim.init();
1265
1266 let mut player = MacroPlayer::new(&m);
1267 player.replay_all(&mut sim);
1268 assert_eq!(sim.model().value, 2);
1269 assert!(player.is_done());
1270
1271 player.reset();
1273 assert_eq!(player.position(), 0);
1274 assert!(!player.is_done());
1275
1276 let mut sim2 = ProgramSimulator::new(Counter { value: 10 });
1277 sim2.init();
1278 player.replay_all(&mut sim2);
1279 assert_eq!(sim2.model().value, 12);
1280 }
1281
1282 #[test]
1283 fn player_replay_with_sleeper_respects_delays() {
1284 let events = vec![
1285 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1286 TimedEvent::new(key_event('+'), Duration::from_millis(0)),
1287 TimedEvent::new(key_event('+'), Duration::from_millis(25)),
1288 ];
1289 let m = InputMacro::new(
1290 events,
1291 MacroMetadata {
1292 name: "timed_sleep".to_string(),
1293 terminal_size: (80, 24),
1294 total_duration: Duration::from_millis(35),
1295 },
1296 );
1297
1298 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1299 sim.init();
1300
1301 let mut player = MacroPlayer::new(&m);
1302 let mut sleeps = Vec::new();
1303 player.replay_with_sleeper(&mut sim, |d| sleeps.push(d));
1304
1305 assert_eq!(
1306 sleeps,
1307 vec![Duration::from_millis(10), Duration::from_millis(25)]
1308 );
1309 assert_eq!(sim.model().value, 3);
1310 }
1311
1312 #[test]
1315 fn playback_emits_due_events_in_order() {
1316 let events = vec![
1317 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1318 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1319 ];
1320 let m = InputMacro::new(
1321 events,
1322 MacroMetadata {
1323 name: "playback".to_string(),
1324 terminal_size: (80, 24),
1325 total_duration: Duration::from_millis(20),
1326 },
1327 );
1328
1329 let mut playback = MacroPlayback::new(m.clone());
1330 assert!(playback.advance(Duration::from_millis(5)).is_empty());
1331 let first = playback.advance(Duration::from_millis(5));
1332 assert_eq!(first.len(), 1);
1333 let second = playback.advance(Duration::from_millis(10));
1334 assert_eq!(second.len(), 1);
1335 assert!(playback.advance(Duration::from_millis(10)).is_empty());
1336 }
1337
1338 #[test]
1339 fn playback_speed_scales_time() {
1340 let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1341 let m = InputMacro::new(
1342 events,
1343 MacroMetadata {
1344 name: "speed".to_string(),
1345 terminal_size: (80, 24),
1346 total_duration: Duration::from_millis(10),
1347 },
1348 );
1349
1350 let mut playback = MacroPlayback::new(m.clone()).with_speed(2.0);
1351 let events = playback.advance(Duration::from_millis(5));
1352 assert_eq!(events.len(), 1);
1353 }
1354
1355 #[test]
1356 fn playback_speed_huge_value_does_not_panic() {
1357 let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1358 let m = InputMacro::new(
1359 events,
1360 MacroMetadata {
1361 name: "huge-speed".to_string(),
1362 terminal_size: (80, 24),
1363 total_duration: Duration::from_millis(10),
1364 },
1365 );
1366
1367 let mut playback = MacroPlayback::new(m).with_speed(f64::MAX);
1368 let events = playback.advance(Duration::from_millis(1));
1369 assert_eq!(events.len(), 1);
1370 }
1371
1372 #[test]
1373 fn playback_speed_huge_looping_multiple_advances_do_not_panic() {
1374 let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1375 let m = InputMacro::new(
1376 events,
1377 MacroMetadata {
1378 name: "huge-speed-looping".to_string(),
1379 terminal_size: (80, 24),
1380 total_duration: Duration::from_millis(10),
1381 },
1382 );
1383
1384 let mut playback = MacroPlayback::new(m)
1385 .with_speed(f64::MAX)
1386 .with_looping(true);
1387 let first = playback.advance(Duration::from_millis(1));
1388 assert_eq!(first.len(), 1);
1389 let second = playback.advance(Duration::from_millis(1));
1390 assert_eq!(second.len(), 1);
1391 }
1392
1393 #[test]
1394 fn playback_looping_handles_large_delta() {
1395 let events = vec![
1396 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1397 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1398 ];
1399 let m = InputMacro::new(
1400 events,
1401 MacroMetadata {
1402 name: "loop".to_string(),
1403 terminal_size: (80, 24),
1404 total_duration: Duration::from_millis(20),
1405 },
1406 );
1407
1408 let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1409 let events = playback.advance(Duration::from_millis(50));
1410 assert_eq!(events.len(), 5);
1411 }
1412
1413 #[test]
1414 fn playback_zero_duration_does_not_loop_forever() {
1415 let m = InputMacro::from_events("zero", vec![key_event('+'), key_event('+')]);
1416 let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1417
1418 let events = playback.advance(Duration::ZERO);
1419 assert_eq!(events.len(), 2);
1420 assert!(playback.advance(Duration::from_millis(10)).is_empty());
1421 }
1422
1423 #[test]
1424 fn macro_replay_with_sleeper_wrapper() {
1425 let events = vec![
1426 TimedEvent::new(key_event('+'), Duration::from_millis(5)),
1427 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1428 ];
1429 let m = InputMacro::new(
1430 events,
1431 MacroMetadata {
1432 name: "wrapper".to_string(),
1433 terminal_size: (80, 24),
1434 total_duration: Duration::from_millis(15),
1435 },
1436 );
1437
1438 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1439 sim.init();
1440
1441 let mut slept = Vec::new();
1442 m.replay_with_sleeper(&mut sim, |d| slept.push(d));
1443
1444 assert_eq!(
1445 slept,
1446 vec![Duration::from_millis(5), Duration::from_millis(10)]
1447 );
1448 assert_eq!(sim.model().value, 2);
1449 }
1450
1451 #[test]
1452 fn empty_macro_replay() {
1453 let m = InputMacro::from_events("empty", vec![]);
1454
1455 let mut sim = ProgramSimulator::new(Counter { value: 5 });
1456 sim.init();
1457
1458 let mut player = MacroPlayer::new(&m);
1459 assert!(player.is_done());
1460 player.replay_all(&mut sim);
1461 assert_eq!(sim.model().value, 5);
1462 }
1463
1464 #[test]
1465 fn macro_with_mixed_events() {
1466 let events = vec![
1467 key_event('+'),
1468 Event::Resize {
1469 width: 100,
1470 height: 50,
1471 },
1472 key_event('-'),
1473 Event::Focus(true),
1474 key_event('+'),
1475 ];
1476 let m = InputMacro::from_events("mixed", events);
1477
1478 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1479 sim.init();
1480
1481 let mut player = MacroPlayer::new(&m);
1482 player.replay_all(&mut sim);
1483
1484 assert_eq!(sim.model().value, 3);
1487 }
1488
1489 #[test]
1490 fn deterministic_replay() {
1491 let m = InputMacro::from_events(
1492 "determinism",
1493 vec![
1494 key_event('+'),
1495 key_event('+'),
1496 key_event('-'),
1497 key_event('+'),
1498 key_event('+'),
1499 ],
1500 );
1501
1502 let result1 = {
1504 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1505 sim.init();
1506 MacroPlayer::new(&m).replay_all(&mut sim);
1507 sim.model().value
1508 };
1509
1510 let result2 = {
1511 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1512 sim.init();
1513 MacroPlayer::new(&m).replay_all(&mut sim);
1514 sim.model().value
1515 };
1516
1517 assert_eq!(result1, result2);
1518 assert_eq!(result1, 3);
1519 }
1520
1521 #[test]
1524 fn event_recorder_starts_idle() {
1525 let rec = EventRecorder::new("test");
1526 assert_eq!(rec.state(), RecordingState::Idle);
1527 assert!(!rec.is_recording());
1528 assert_eq!(rec.event_count(), 0);
1529 }
1530
1531 #[test]
1532 fn event_recorder_start_activates() {
1533 let mut rec = EventRecorder::new("test");
1534 rec.start();
1535 assert_eq!(rec.state(), RecordingState::Recording);
1536 assert!(rec.is_recording());
1537 }
1538
1539 #[test]
1540 fn event_recorder_ignores_events_when_idle() {
1541 let mut rec = EventRecorder::new("test");
1542 assert!(!rec.record(&key_event('a')));
1543 assert_eq!(rec.event_count(), 0);
1544 }
1545
1546 #[test]
1547 fn event_recorder_records_when_active() {
1548 let mut rec = EventRecorder::new("test");
1549 rec.start();
1550 assert!(rec.record(&key_event('a')));
1551 assert!(rec.record(&key_event('b')));
1552 assert_eq!(rec.event_count(), 2);
1553
1554 let m = rec.finish();
1555 assert_eq!(m.len(), 2);
1556 }
1557
1558 #[test]
1559 fn event_recorder_pause_ignores_events() {
1560 let mut rec = EventRecorder::new("test");
1561 rec.start();
1562 rec.record(&key_event('a'));
1563 rec.pause();
1564 assert_eq!(rec.state(), RecordingState::Paused);
1565 assert!(!rec.is_recording());
1566
1567 assert!(!rec.record(&key_event('b')));
1569 assert_eq!(rec.event_count(), 1);
1570 }
1571
1572 #[test]
1573 fn event_recorder_resume_after_pause() {
1574 let mut rec = EventRecorder::new("test");
1575 rec.start();
1576 rec.record(&key_event('a'));
1577 rec.pause();
1578 rec.record(&key_event('b')); rec.resume();
1580 assert!(rec.is_recording());
1581 rec.record(&key_event('c'));
1582 assert_eq!(rec.event_count(), 2);
1583
1584 let m = rec.finish();
1585 assert_eq!(m.len(), 2);
1586 assert_eq!(m.bare_events()[0], key_event('a'));
1587 assert_eq!(m.bare_events()[1], key_event('c'));
1588 }
1589
1590 #[test]
1591 fn event_recorder_start_resumes_when_paused() {
1592 let mut rec = EventRecorder::new("test");
1593 rec.start();
1594 rec.pause();
1595 assert_eq!(rec.state(), RecordingState::Paused);
1596
1597 rec.start(); assert_eq!(rec.state(), RecordingState::Recording);
1599 }
1600
1601 #[test]
1602 fn event_recorder_pause_noop_when_idle() {
1603 let mut rec = EventRecorder::new("test");
1604 rec.pause();
1605 assert_eq!(rec.state(), RecordingState::Idle);
1606 }
1607
1608 #[test]
1609 fn event_recorder_resume_noop_when_idle() {
1610 let mut rec = EventRecorder::new("test");
1611 rec.resume();
1612 assert_eq!(rec.state(), RecordingState::Idle);
1613 }
1614
1615 #[test]
1616 fn event_recorder_discard() {
1617 let mut rec = EventRecorder::new("test");
1618 rec.start();
1619 rec.record(&key_event('a'));
1620 rec.record(&key_event('b'));
1621 let count = rec.discard();
1622 assert_eq!(count, 2);
1623 }
1624
1625 #[test]
1626 fn event_recorder_with_terminal_size() {
1627 let mut rec = EventRecorder::new("sized").with_terminal_size(120, 40);
1628 rec.start();
1629 rec.record(&key_event('x'));
1630 let m = rec.finish();
1631 assert_eq!(m.metadata().terminal_size, (120, 40));
1632 }
1633
1634 #[test]
1635 fn event_recorder_finish_produces_valid_macro() {
1636 let mut rec = EventRecorder::new("full_test");
1637 rec.start();
1638 rec.record(&key_event('+'));
1639 rec.record(&key_event('+'));
1640 rec.record(&key_event('-'));
1641
1642 let m = rec.finish();
1643 assert_eq!(m.len(), 3);
1644 assert_eq!(m.metadata().name, "full_test");
1645
1646 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1648 sim.init();
1649 MacroPlayer::new(&m).replay_all(&mut sim);
1650 assert_eq!(sim.model().value, 1); }
1652
1653 #[test]
1654 fn event_recorder_record_with_delay() {
1655 let mut rec = EventRecorder::new("delayed");
1656 rec.start();
1657 assert!(rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1658 assert!(rec.record_with_delay(&key_event('b'), Duration::from_millis(100)));
1659 assert_eq!(rec.event_count(), 2);
1660
1661 let m = rec.finish();
1662 assert_eq!(m.events()[0].delay, Duration::from_millis(50));
1663 assert_eq!(m.events()[1].delay, Duration::from_millis(100));
1664 }
1665
1666 #[test]
1667 fn event_recorder_record_with_delay_ignores_when_idle() {
1668 let mut rec = EventRecorder::new("test");
1669 assert!(!rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1670 assert_eq!(rec.event_count(), 0);
1671 }
1672
1673 #[test]
1676 fn filter_default_accepts_all() {
1677 let filter = RecordingFilter::default();
1678 assert!(filter.accepts(&key_event('a')));
1679 assert!(filter.accepts(&Event::Resize {
1680 width: 80,
1681 height: 24
1682 }));
1683 assert!(filter.accepts(&Event::Focus(true)));
1684 }
1685
1686 #[test]
1687 fn filter_keys_only() {
1688 let filter = RecordingFilter::keys_only();
1689 assert!(filter.accepts(&key_event('a')));
1690 assert!(!filter.accepts(&Event::Resize {
1691 width: 80,
1692 height: 24
1693 }));
1694 assert!(!filter.accepts(&Event::Focus(true)));
1695 }
1696
1697 #[test]
1698 fn filter_custom() {
1699 let filter = RecordingFilter {
1700 keys: true,
1701 mouse: false,
1702 resize: false,
1703 paste: true,
1704 focus: false,
1705 };
1706 assert!(filter.accepts(&key_event('a')));
1707 assert!(!filter.accepts(&Event::Resize {
1708 width: 80,
1709 height: 24
1710 }));
1711 assert!(!filter.accepts(&Event::Focus(false)));
1712 }
1713
1714 #[test]
1717 fn filtered_recorder_records_matching_events() {
1718 let mut rec = FilteredEventRecorder::new("filtered", RecordingFilter::default());
1719 rec.start();
1720 assert!(rec.record(&key_event('a')));
1721 assert_eq!(rec.event_count(), 1);
1722 assert_eq!(rec.filtered_count(), 0);
1723 }
1724
1725 #[test]
1726 fn filtered_recorder_skips_filtered_events() {
1727 let mut rec = FilteredEventRecorder::new("keys_only", RecordingFilter::keys_only());
1728 rec.start();
1729 assert!(rec.record(&key_event('a')));
1730 assert!(!rec.record(&Event::Focus(true)));
1731 assert!(!rec.record(&Event::Resize {
1732 width: 100,
1733 height: 50
1734 }));
1735 assert!(rec.record(&key_event('b')));
1736
1737 assert_eq!(rec.event_count(), 2);
1738 assert_eq!(rec.filtered_count(), 2);
1739 }
1740
1741 #[test]
1742 fn filtered_recorder_finish_produces_macro() {
1743 let mut rec = FilteredEventRecorder::new("test", RecordingFilter::keys_only());
1744 rec.start();
1745 rec.record(&key_event('+'));
1746 rec.record(&Event::Focus(true)); rec.record(&key_event('+'));
1748
1749 let m = rec.finish();
1750 assert_eq!(m.len(), 2);
1751
1752 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1753 sim.init();
1754 MacroPlayer::new(&m).replay_all(&mut sim);
1755 assert_eq!(sim.model().value, 2);
1756 }
1757
1758 #[test]
1759 fn filtered_recorder_pause_resume() {
1760 let mut rec = FilteredEventRecorder::new("test", RecordingFilter::default());
1761 rec.start();
1762 rec.record(&key_event('a'));
1763 rec.pause();
1764 assert!(!rec.record(&key_event('b'))); rec.resume();
1766 rec.record(&key_event('c'));
1767 assert_eq!(rec.event_count(), 2);
1768 }
1769
1770 #[test]
1771 fn filtered_recorder_with_terminal_size() {
1772 let mut rec = FilteredEventRecorder::new("sized", RecordingFilter::default())
1773 .with_terminal_size(200, 60);
1774 rec.start();
1775 rec.record(&key_event('x'));
1776 let m = rec.finish();
1777 assert_eq!(m.metadata().terminal_size, (200, 60));
1778 }
1779
1780 #[derive(Default)]
1783 struct EventSink {
1784 events: Vec<Event>,
1785 }
1786
1787 #[derive(Debug, Clone)]
1788 struct EventMsg(Event);
1789
1790 impl From<Event> for EventMsg {
1791 fn from(event: Event) -> Self {
1792 Self(event)
1793 }
1794 }
1795
1796 impl Model for EventSink {
1797 type Message = EventMsg;
1798
1799 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1800 self.events.push(msg.0);
1801 Cmd::none()
1802 }
1803
1804 fn view(&self, _frame: &mut Frame) {}
1805 }
1806
1807 proptest! {
1808 #[test]
1809 fn recorder_with_explicit_delays_roundtrips(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1810 let mut recorder = MacroRecorder::new("prop").with_terminal_size(80, 24);
1811 let mut expected_total = Duration::ZERO;
1812 let mut expected_events = Vec::with_capacity(pairs.len());
1813
1814 for (ch_idx, delay_ms) in &pairs {
1815 let ch = char::from(b'a' + *ch_idx);
1816 let delay = Duration::from_millis(*delay_ms as u64);
1817 expected_total += delay;
1818 let ev = key_event(ch);
1819 expected_events.push(ev.clone());
1820 recorder.record_event_with_delay(ev, delay);
1821 }
1822
1823 let m = recorder.finish();
1824 prop_assert_eq!(m.len(), pairs.len());
1825 prop_assert_eq!(m.metadata().terminal_size, (80, 24));
1826 prop_assert_eq!(m.total_duration(), expected_total);
1827 prop_assert_eq!(m.bare_events(), expected_events);
1828 }
1829
1830 #[test]
1831 fn player_replays_events_in_order(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1832 let mut timed = Vec::with_capacity(pairs.len());
1833 let mut total = Duration::ZERO;
1834 let mut expected_events = Vec::with_capacity(pairs.len());
1835
1836 for (ch_idx, delay_ms) in &pairs {
1837 let ch = char::from(b'a' + *ch_idx);
1838 let delay = Duration::from_millis(*delay_ms as u64);
1839 total += delay;
1840 let ev = key_event(ch);
1841 expected_events.push(ev.clone());
1842 timed.push(TimedEvent::new(ev, delay));
1843 }
1844
1845 let m = InputMacro::new(timed, MacroMetadata {
1846 name: "prop".to_string(),
1847 terminal_size: (80, 24),
1848 total_duration: total,
1849 });
1850
1851 let mut sim = ProgramSimulator::new(EventSink::default());
1852 sim.init();
1853 let mut player = MacroPlayer::new(&m);
1854 player.replay_all(&mut sim);
1855
1856 prop_assert_eq!(sim.model().events.clone(), expected_events);
1857 prop_assert_eq!(player.elapsed(), total);
1858 }
1859 }
1860}