Skip to content

Commit c9eb0cd

Browse files
committed
feat: add mock_terminal_render_loop api
1 parent af91085 commit c9eb0cd

File tree

10 files changed

+265
-83
lines changed

10 files changed

+265
-83
lines changed

packages/iocraft-macros/src/lib.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl ToTokens for ParsedElement {
8484
})
8585
.unwrap_or_else(|| quote!(#decl_key));
8686

87-
let props = self
87+
let prop_assignments = self
8888
.props
8989
.iter()
9090
.filter_map(|FieldValue { member, expr, .. }| match member {
@@ -93,15 +93,15 @@ impl ToTokens for ParsedElement {
9393
Expr::Lit(lit) => match &lit.lit {
9494
Lit::Int(lit) if lit.suffix() == "pct" => {
9595
let value = lit.base10_parse::<f32>().unwrap();
96-
quote!(#member: ::iocraft::Percent(#value).into())
96+
quote!(_iocraft_props.#member = ::iocraft::Percent(#value).into())
9797
}
9898
Lit::Float(lit) if lit.suffix() == "pct" => {
9999
let value = lit.base10_parse::<f32>().unwrap();
100-
quote!(#member: ::iocraft::Percent(#value).into())
100+
quote!(_iocraft_props.#member = ::iocraft::Percent(#value).into())
101101
}
102-
_ => quote!(#member: (#expr).into()),
102+
_ => quote!(_iocraft_props.#member = (#expr).into()),
103103
},
104-
_ => quote!(#member: (#expr).into()),
104+
_ => quote!(_iocraft_props.#member = (#expr).into()),
105105
}),
106106
})
107107
.collect::<Vec<_>>();
@@ -121,12 +121,11 @@ impl ToTokens for ParsedElement {
121121
tokens.extend(quote! {
122122
{
123123
type Props<'a> = <#ty as ::iocraft::ElementType>::Props<'a>;
124+
let mut _iocraft_props: Props = Default::default();
125+
#(#prop_assignments;)*
124126
let mut _iocraft_element = ::iocraft::Element::<#ty>{
125127
key: ::iocraft::ElementKey::new(#key),
126-
props: Props{
127-
#(#props,)*
128-
..core::default::Default::default()
129-
},
128+
props: _iocraft_props,
130129
};
131130
#set_children
132131
_iocraft_element

packages/iocraft/src/components/box.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ impl BorderStyle {
136136
}
137137

138138
/// The props which can be passed to the [`Box`] component.
139+
#[non_exhaustive]
139140
#[with_layout_style_props]
140141
#[derive(Default, Props)]
141142
pub struct BoxProps<'a> {

packages/iocraft/src/components/context_provider.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{AnyElement, Component, ComponentUpdater, Context, Hooks, Props};
22

33
/// The props which can be passed to the [`ContextProvider`] component.
4+
#[non_exhaustive]
45
#[derive(Default, Props)]
56
pub struct ContextProviderProps<'a> {
67
/// The children of the component.

packages/iocraft/src/components/text.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub enum TextDecoration {
3737
}
3838

3939
/// The props which can be passed to the [`Text`] component.
40+
#[non_exhaustive]
4041
#[derive(Default, Props)]
4142
pub struct TextProps {
4243
/// The color to make the text.

packages/iocraft/src/components/text_input.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::{
1010
use unicode_width::UnicodeWidthStr;
1111

1212
/// The props which can be passed to the [`TextInput`] component.
13+
#[non_exhaustive]
1314
#[derive(Default, Props)]
1415
pub struct TextInputProps {
1516
/// The color to make the text.
@@ -166,6 +167,7 @@ impl Component for TextInput {
166167
#[cfg(test)]
167168
mod tests {
168169
use crate::prelude::*;
170+
use futures::stream::StreamExt;
169171
use macro_rules_attribute::apply;
170172
use smol_macros::test;
171173

@@ -191,10 +193,39 @@ mod tests {
191193

192194
#[apply(test!)]
193195
async fn test_text_input() {
194-
let canvases = mock_terminal_render_loop(element!(MyComponent))
195-
.await
196-
.unwrap();
197-
let actual = canvases.iter().map(|c| c.to_string()).collect::<Vec<_>>();
196+
let actual = element!(MyComponent)
197+
.mock_terminal_render_loop(MockTerminalConfig::with_events(futures::stream::iter(
198+
vec![
199+
TerminalEvent::Key(KeyEvent {
200+
code: KeyCode::Char('f'),
201+
modifiers: KeyModifiers::empty(),
202+
kind: KeyEventKind::Press,
203+
}),
204+
TerminalEvent::Key(KeyEvent {
205+
code: KeyCode::Char('f'),
206+
modifiers: KeyModifiers::empty(),
207+
kind: KeyEventKind::Release,
208+
}),
209+
TerminalEvent::Key(KeyEvent {
210+
code: KeyCode::Char('o'),
211+
modifiers: KeyModifiers::empty(),
212+
kind: KeyEventKind::Press,
213+
}),
214+
TerminalEvent::Key(KeyEvent {
215+
code: KeyCode::Char('o'),
216+
modifiers: KeyModifiers::empty(),
217+
kind: KeyEventKind::Repeat,
218+
}),
219+
TerminalEvent::Key(KeyEvent {
220+
code: KeyCode::Char('o'),
221+
modifiers: KeyModifiers::empty(),
222+
kind: KeyEventKind::Release,
223+
}),
224+
],
225+
)))
226+
.map(|c| c.to_string())
227+
.collect::<Vec<_>>()
228+
.await;
198229
let expected = vec!["\n", "foo\n"];
199230
assert_eq!(actual, expected);
200231
}

packages/iocraft/src/element.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use crate::{
22
component::{Component, ComponentHelper, ComponentHelperExt},
3+
mock_terminal_render_loop,
34
props::AnyProps,
4-
render, terminal_render_loop, Canvas, Terminal,
5+
render, terminal_render_loop, Canvas, MockTerminalConfig, Terminal,
56
};
67
use any_key::AnyHash;
78
use crossterm::{terminal, tty::IsTty};
9+
use futures::Stream;
810
use std::{
911
fmt::Debug,
1012
future::Future,
@@ -198,6 +200,63 @@ pub trait ElementExt: private::Sealed + Sized {
198200
/// is a TTY with [`stdout_is_tty`](crate::stdout_is_tty).
199201
fn render_loop(&mut self) -> impl Future<Output = io::Result<()>>;
200202

203+
/// Renders the element in a loop using a mock terminal, allowing you to simulate terminal
204+
/// events for testing purposes.
205+
///
206+
/// A stream of canvases is returned, allowing you to inspect the output of each render pass.
207+
///
208+
/// # Example
209+
///
210+
/// ```
211+
/// # use iocraft::prelude::*;
212+
/// # use futures::stream::StreamExt;
213+
/// # #[component]
214+
/// # fn MyTextInput() -> impl Into<AnyElement<'static>> {
215+
/// # element!(Box)
216+
/// # }
217+
/// async fn test_text_input() {
218+
/// let actual = element!(MyTextInput)
219+
/// .mock_terminal_render_loop(MockTerminalConfig::with_events(futures::stream::iter(
220+
/// vec![
221+
/// TerminalEvent::Key(KeyEvent {
222+
/// code: KeyCode::Char('f'),
223+
/// modifiers: KeyModifiers::empty(),
224+
/// kind: KeyEventKind::Press,
225+
/// }),
226+
/// TerminalEvent::Key(KeyEvent {
227+
/// code: KeyCode::Char('f'),
228+
/// modifiers: KeyModifiers::empty(),
229+
/// kind: KeyEventKind::Release,
230+
/// }),
231+
/// TerminalEvent::Key(KeyEvent {
232+
/// code: KeyCode::Char('o'),
233+
/// modifiers: KeyModifiers::empty(),
234+
/// kind: KeyEventKind::Press,
235+
/// }),
236+
/// TerminalEvent::Key(KeyEvent {
237+
/// code: KeyCode::Char('o'),
238+
/// modifiers: KeyModifiers::empty(),
239+
/// kind: KeyEventKind::Repeat,
240+
/// }),
241+
/// TerminalEvent::Key(KeyEvent {
242+
/// code: KeyCode::Char('o'),
243+
/// modifiers: KeyModifiers::empty(),
244+
/// kind: KeyEventKind::Release,
245+
/// }),
246+
/// ],
247+
/// )))
248+
/// .map(|c| c.to_string())
249+
/// .collect::<Vec<_>>()
250+
/// .await;
251+
/// let expected = vec!["\n", "foo\n"];
252+
/// assert_eq!(actual, expected);
253+
/// }
254+
/// ```
255+
fn mock_terminal_render_loop(
256+
&mut self,
257+
config: MockTerminalConfig,
258+
) -> impl Stream<Item = Canvas>;
259+
201260
/// Renders the element as fullscreen in a loop, allowing it to be dynamic and interactive.
202261
///
203262
/// This method should only be used if when stdio is a TTY terminal. If for example, stdout is
@@ -228,6 +287,13 @@ impl<'a> ElementExt for AnyElement<'a> {
228287
terminal_render_loop(self, Terminal::new()?).await
229288
}
230289

290+
fn mock_terminal_render_loop(
291+
&mut self,
292+
config: MockTerminalConfig,
293+
) -> impl Stream<Item = Canvas> {
294+
mock_terminal_render_loop(self, config)
295+
}
296+
231297
async fn fullscreen(&mut self) -> io::Result<()> {
232298
terminal_render_loop(self, Terminal::fullscreen()?).await
233299
}
@@ -255,6 +321,13 @@ impl<'a> ElementExt for &mut AnyElement<'a> {
255321
terminal_render_loop(&mut **self, Terminal::new()?).await
256322
}
257323

324+
fn mock_terminal_render_loop(
325+
&mut self,
326+
config: MockTerminalConfig,
327+
) -> impl Stream<Item = Canvas> {
328+
mock_terminal_render_loop(&mut **self, config)
329+
}
330+
258331
async fn fullscreen(&mut self) -> io::Result<()> {
259332
terminal_render_loop(&mut **self, Terminal::fullscreen()?).await
260333
}
@@ -285,6 +358,12 @@ where
285358
terminal_render_loop(self, Terminal::new()?).await
286359
}
287360

361+
fn mock_terminal_render_loop(
362+
&mut self,
363+
config: MockTerminalConfig,
364+
) -> impl Stream<Item = Canvas> {
365+
mock_terminal_render_loop(self, config)
366+
}
288367
async fn fullscreen(&mut self) -> io::Result<()> {
289368
terminal_render_loop(self, Terminal::fullscreen()?).await
290369
}
@@ -315,6 +394,13 @@ where
315394
terminal_render_loop(&mut **self, Terminal::new()?).await
316395
}
317396

397+
fn mock_terminal_render_loop(
398+
&mut self,
399+
config: MockTerminalConfig,
400+
) -> impl Stream<Item = Canvas> {
401+
mock_terminal_render_loop(&mut **self, config)
402+
}
403+
318404
async fn fullscreen(&mut self) -> io::Result<()> {
319405
terminal_render_loop(&mut **self, Terminal::fullscreen()?).await
320406
}

packages/iocraft/src/hooks/use_terminal_events.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ impl Hook for UseTerminalEventsImpl {
5858
#[cfg(test)]
5959
mod tests {
6060
use crate::prelude::*;
61+
use futures::stream::{self, StreamExt};
6162
use macro_rules_attribute::apply;
6263
use smol_macros::test;
6364

@@ -79,9 +80,16 @@ mod tests {
7980

8081
#[apply(test!)]
8182
async fn test_use_terminal_events() {
82-
let canvases = mock_terminal_render_loop(element!(MyComponent))
83-
.await
84-
.unwrap();
83+
let canvases: Vec<_> = element!(MyComponent)
84+
.mock_terminal_render_loop(MockTerminalConfig::with_events(stream::iter(vec![
85+
TerminalEvent::Key(KeyEvent {
86+
code: KeyCode::Char('f'),
87+
modifiers: KeyModifiers::empty(),
88+
kind: KeyEventKind::Press,
89+
}),
90+
])))
91+
.collect()
92+
.await;
8593
let actual = canvases.iter().map(|c| c.to_string()).collect::<Vec<_>>();
8694
let expected = vec!["", "received event\n"];
8795
assert_eq!(actual, expected);

packages/iocraft/src/hooks/use_terminal_size.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,37 @@ impl UseTerminalSize for Hooks<'_, '_> {
2121
size.get()
2222
}
2323
}
24+
25+
#[cfg(test)]
26+
mod tests {
27+
use crate::prelude::*;
28+
use futures::stream::StreamExt;
29+
use macro_rules_attribute::apply;
30+
use smol_macros::test;
31+
32+
#[component]
33+
fn MyComponent(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
34+
let mut system = hooks.use_context_mut::<SystemContext>();
35+
let (width, height) = hooks.use_terminal_size();
36+
37+
if width == 100 && height == 40 {
38+
system.exit();
39+
}
40+
41+
element! {
42+
Text(content: format!("{}x{}", width, height))
43+
}
44+
}
45+
46+
#[apply(test!)]
47+
async fn test_use_terminal_size() {
48+
let actual = element!(MyComponent)
49+
.mock_terminal_render_loop(MockTerminalConfig::with_events(futures::stream::iter(
50+
vec![TerminalEvent::Resize(100, 40)],
51+
)))
52+
.map(|c| c.to_string())
53+
.collect::<Vec<_>>()
54+
.await;
55+
assert_eq!(actual.last().unwrap(), "100x40\n");
56+
}
57+
}

0 commit comments

Comments
 (0)