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