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