• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / tui-realm / 13281824380

12 Feb 2025 09:10AM UTC coverage: 91.538% (-1.0%) from 92.556%
13281824380

push

github

web-flow
Fix coverage CI (#90)

* chore(workflows/coverage): update "coverallsapp/github-action" to 2.x

* chore(workflows/coverage): remove deprecated option

* chore(workflows/coverage): remove unknown option "override"

* chore(workflows/coverage): replace coverage generation with cargo-llvm-cov

- replace "-Zprofile" with "-Cinstrument-coverage" as that option does not exist anymore
- replace "actions-rs/grcov" with manual approach as actions-rs is deprecated
- run with stable toolchain

* chore(workflows/coverage): try to replace install with github action

* chore(workflows/coverage): enable feature "crossterm"

* chore(workflows/coverage): set "--no-fail-fast" again

4381 of 4786 relevant lines covered (91.54%)

6.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.83
/src/core/application.rs
1
//! ## Application
2
//!
3
//! This module exposes the Application, which is the core struct of tui-realm.
4

5
use std::hash::Hash;
6
use std::time::{Duration, Instant};
7

8
use ratatui::Frame;
9
use thiserror::Error;
10

11
use super::{Subscription, View, WrappedComponent};
12
use crate::listener::{EventListener, EventListenerCfg, ListenerError};
13
use crate::ratatui::layout::Rect;
14
use crate::{AttrValue, Attribute, Event, Injector, State, Sub, SubEventClause, ViewError};
15

16
/// Result retuned by [`Application`].
17
/// Ok depends on method
18
/// Err is always [`ApplicationError`]
19
pub type ApplicationResult<T> = Result<T, ApplicationError>;
20

21
/// The application defines a tui-realm application.
22
/// It will handle events, subscriptions and the view too.
23
/// It provides functions to interact with the view (mount, umount, query, etc), but also
24
/// the main function: [`Application::tick`].
25
pub struct Application<ComponentId, Msg, UserEvent>
26
where
27
    ComponentId: Eq + PartialEq + Clone + Hash,
28
    Msg: PartialEq,
29
    UserEvent: Eq + PartialEq + Clone + PartialOrd + Send + 'static,
30
{
31
    listener: EventListener<UserEvent>,
32
    subs: Vec<Subscription<ComponentId, UserEvent>>,
33
    /// If true, subs won't be processed. (Default: False)
34
    sub_lock: bool,
35
    view: View<ComponentId, Msg, UserEvent>,
36
}
37

38
impl<K, Msg, UserEvent> Application<K, Msg, UserEvent>
39
where
40
    K: Eq + PartialEq + Clone + Hash,
41
    Msg: PartialEq,
42
    UserEvent: Eq + PartialEq + Clone + PartialOrd + Send + 'static,
43
{
44
    /// Initialize a new [`Application`].
45
    /// The event listener is immediately created and started.
46
    pub fn init(listener_cfg: EventListenerCfg<UserEvent>) -> Self {
15✔
47
        Self {
15✔
48
            listener: listener_cfg.start(),
15✔
49
            subs: Vec::new(),
15✔
50
            sub_lock: false,
15✔
51
            view: View::default(),
15✔
52
        }
15✔
53
    }
15✔
54

55
    /// Restart listener in case the previous listener has died or if you want to start a new one with a new configuration.
56
    ///
57
    /// > The listener has died if you received a [`ApplicationError::Listener(ListenerError::ListenerDied))`]
58
    pub fn restart_listener(
1✔
59
        &mut self,
1✔
60
        listener_cfg: EventListenerCfg<UserEvent>,
1✔
61
    ) -> ApplicationResult<()> {
1✔
62
        self.listener.stop()?;
1✔
63
        self.listener = listener_cfg.start();
1✔
64
        Ok(())
1✔
65
    }
1✔
66

67
    /// Lock ports. As long as Ports are locked, ports won't be polled.
68
    /// Locking ports will also prevent Tick events from being generated.
69
    pub fn lock_ports(&mut self) -> ApplicationResult<()> {
1✔
70
        self.listener.pause().map_err(ApplicationError::from)
1✔
71
    }
1✔
72

73
    /// Unlock Ports. Once called, the event listener will resume polling Ports.
74
    pub fn unlock_ports(&mut self) -> ApplicationResult<()> {
1✔
75
        self.listener.unpause().map_err(ApplicationError::from)
1✔
76
    }
1✔
77

78
    /// The tick method makes the application to run once.
79
    /// The workflow of the tick method is the following one:
80
    ///
81
    /// 1. The event listener is fetched according to the provided [`PollStrategy`]
82
    /// 2. All the received events are sent to the current active component
83
    /// 3. All the received events are forwarded to the subscribed components which satisfy the received events and conditions.
84
    /// 4. Returns messages to process
85
    ///
86
    /// As soon as function returns, you should call the [`Application::view`] method.
87
    ///
88
    /// > You can also call [`Application::view`] from the [`crate::Update`] if you need it
89
    pub fn tick(&mut self, strategy: PollStrategy) -> ApplicationResult<Vec<Msg>> {
13✔
90
        // Poll event listener
91
        let events = self.poll(strategy)?;
13✔
92
        // Forward to active element
93
        let mut messages: Vec<Msg> = events
13✔
94
            .iter()
13✔
95
            .filter_map(|x| self.forward_to_active_component(x.clone()))
24✔
96
            .collect();
13✔
97
        // Forward to subscriptions and extend vector
13✔
98
        if !self.sub_lock {
13✔
99
            messages.extend(self.forward_to_subscriptions(events));
12✔
100
        }
12✔
101
        Ok(messages)
13✔
102
    }
13✔
103

104
    // -- view bridge
105

106
    /// Add an injector to the view
107
    pub fn add_injector(&mut self, injector: Box<dyn Injector<K>>) {
1✔
108
        self.view.add_injector(injector);
1✔
109
    }
1✔
110

111
    /// Mount component to view and associate subscriptions for it.
112
    /// Returns error if component is already mounted
113
    /// NOTE: if subs vector contains duplicated, these will be discarded
114
    pub fn mount(
24✔
115
        &mut self,
24✔
116
        id: K,
24✔
117
        component: WrappedComponent<Msg, UserEvent>,
24✔
118
        subs: Vec<Sub<K, UserEvent>>,
24✔
119
    ) -> ApplicationResult<()> {
24✔
120
        // Mount
24✔
121
        self.view.mount(id.clone(), component)?;
24✔
122
        // Subscribe
123
        self.insert_subscriptions(id, subs);
23✔
124
        Ok(())
23✔
125
    }
24✔
126

127
    /// Umount component associated to `id` and remove ALL its SUBSCRIPTIONS.
128
    /// Returns Error if the component doesn't exist
129
    pub fn umount(&mut self, id: &K) -> ApplicationResult<()> {
3✔
130
        self.view.umount(id)?;
3✔
131
        self.unsubscribe_component(id);
2✔
132
        Ok(())
2✔
133
    }
3✔
134

135
    /// Remount provided component.
136
    /// Returns Err if failed to mount. It ignores whether the component already exists or not.
137
    /// If component had focus, focus is preserved
138
    pub fn remount(
1✔
139
        &mut self,
1✔
140
        id: K,
1✔
141
        component: WrappedComponent<Msg, UserEvent>,
1✔
142
        subs: Vec<Sub<K, UserEvent>>,
1✔
143
    ) -> ApplicationResult<()> {
1✔
144
        // remove subs
1✔
145
        self.unsubscribe_component(&id);
1✔
146
        // remount into view
1✔
147
        self.view.remount(id.clone(), component)?;
1✔
148
        // re-add subs
149
        self.insert_subscriptions(id, subs);
1✔
150
        Ok(())
1✔
151
    }
1✔
152

153
    /// Umount all components in the view and removed all associated subscriptions
154
    pub fn umount_all(&mut self) {
1✔
155
        self.view.umount_all();
1✔
156
        self.subs.clear();
1✔
157
    }
1✔
158

159
    /// Returns whether component `id` is mounted
160
    pub fn mounted(&self, id: &K) -> bool {
5✔
161
        self.view.mounted(id)
5✔
162
    }
5✔
163

164
    /// Render component called `id`
165
    pub fn view(&mut self, id: &K, f: &mut Frame, area: Rect) {
×
166
        self.view.view(id, f, area);
×
167
    }
×
168

169
    /// Query view component for a certain `AttrValue`
170
    /// Returns error if the component doesn't exist
171
    /// Returns None if the attribute doesn't exist.
172
    pub fn query(&self, id: &K, query: Attribute) -> ApplicationResult<Option<AttrValue>> {
2✔
173
        self.view.query(id, query).map_err(ApplicationError::from)
2✔
174
    }
2✔
175

176
    /// Set attribute for component `id`
177
    /// Returns error if the component doesn't exist
178
    pub fn attr(&mut self, id: &K, attr: Attribute, value: AttrValue) -> ApplicationResult<()> {
1✔
179
        self.view
1✔
180
            .attr(id, attr, value)
1✔
181
            .map_err(ApplicationError::from)
1✔
182
    }
1✔
183

184
    /// Get state for component `id`.
185
    /// Returns `Err` if component doesn't exist
186
    pub fn state(&self, id: &K) -> ApplicationResult<State> {
1✔
187
        self.view.state(id).map_err(ApplicationError::from)
1✔
188
    }
1✔
189

190
    /// Shorthand for `attr(id, Attribute::Focus(AttrValue::Flag(true)))`.
191
    /// It also sets the component as the current one having focus.
192
    /// Previous active component, if any, GETS PUSHED to the STACK
193
    /// Returns error: if component doesn't exist. Use `mounted()` to check if component exists
194
    ///
195
    /// > NOTE: users should always use this function to give focus to components.
196
    pub fn active(&mut self, id: &K) -> ApplicationResult<()> {
14✔
197
        self.view.active(id).map_err(ApplicationError::from)
14✔
198
    }
14✔
199

200
    /// Blur selected element AND DON'T PUSH CURRENT ACTIVE ELEMENT INTO THE STACK
201
    /// Shorthand for `attr(id, Attribute::Focus(AttrValue::Flag(false)))`.
202
    /// It also unset the current focus and give it to the first element in stack.
203
    /// Returns error: if no component has focus
204
    ///
205
    /// > NOTE: users should always use this function to remove focus to components.
206
    pub fn blur(&mut self) -> ApplicationResult<()> {
3✔
207
        self.view.blur().map_err(ApplicationError::from)
3✔
208
    }
3✔
209

210
    /// Get a reference to the id of the current active component in the view
211
    pub fn focus(&self) -> Option<&K> {
1✔
212
        self.view.focus()
1✔
213
    }
1✔
214

215
    // -- subs bridge
216

217
    /// Subscribe component to a certain event.
218
    /// Returns Error if the component doesn't exist or if the component is already subscribed to this event
219
    pub fn subscribe(&mut self, id: &K, sub: Sub<K, UserEvent>) -> ApplicationResult<()> {
3✔
220
        if !self.view.mounted(id) {
3✔
221
            return Err(ViewError::ComponentNotFound.into());
1✔
222
        }
2✔
223
        let subscription = Subscription::new(id.clone(), sub);
2✔
224
        if self.subscribed(id, subscription.event()) {
2✔
225
            return Err(ApplicationError::AlreadySubscribed);
1✔
226
        }
1✔
227
        self.subs.push(subscription);
1✔
228
        Ok(())
1✔
229
    }
3✔
230

231
    /// Unsubscribe a component from a certain event.
232
    /// Returns error if the component doesn't exist or if the component is not subscribed to this event
233
    pub fn unsubscribe(&mut self, id: &K, ev: SubEventClause<UserEvent>) -> ApplicationResult<()> {
2✔
234
        if !self.view.mounted(id) {
2✔
235
            return Err(ViewError::ComponentNotFound.into());
×
236
        }
2✔
237
        if !self.subscribed(id, &ev) {
2✔
238
            return Err(ApplicationError::NoSuchSubscription);
1✔
239
        }
1✔
240
        self.subs.retain(|s| s.target() != id && s.event() != &ev);
3✔
241
        Ok(())
1✔
242
    }
2✔
243

244
    /// Lock subscriptions. As long as the subscriptions are locked, events won't be propagated to
245
    /// subscriptions.
246
    pub fn lock_subs(&mut self) {
1✔
247
        self.sub_lock = true;
1✔
248
    }
1✔
249

250
    /// Unlock subscriptions. Application will now resume propagating events to subscriptions.
251
    pub fn unlock_subs(&mut self) {
1✔
252
        self.sub_lock = false;
1✔
253
    }
1✔
254

255
    // -- private
256

257
    /// remove all subscriptions for component
258
    fn unsubscribe_component(&mut self, id: &K) {
3✔
259
        self.subs.retain(|x| x.target() != id)
3✔
260
    }
3✔
261

262
    /// Returns whether component `id` is subscribed to event described by `clause`
263
    fn subscribed(&self, id: &K, clause: &SubEventClause<UserEvent>) -> bool {
21✔
264
        self.subs
21✔
265
            .iter()
21✔
266
            .any(|s| s.target() == id && s.event() == clause)
21✔
267
    }
21✔
268

269
    /// Insert subscriptions
270
    fn insert_subscriptions(&mut self, id: K, subs: Vec<Sub<K, UserEvent>>) {
24✔
271
        subs.into_iter().for_each(|x| {
24✔
272
            // Push only if not already subscribed
17✔
273
            let subscription = Subscription::new(id.clone(), x);
17✔
274
            if !self.subscribed(&id, subscription.event()) {
17✔
275
                self.subs.push(subscription);
16✔
276
            }
16✔
277
        });
24✔
278
    }
24✔
279

280
    /// Poll listener according to provided strategy
281
    fn poll(&mut self, strategy: PollStrategy) -> ApplicationResult<Vec<Event<UserEvent>>> {
13✔
282
        match strategy {
13✔
283
            PollStrategy::Once => self
1✔
284
                .poll_listener()
1✔
285
                .map(|x| x.map(|x| vec![x]).unwrap_or_default()),
1✔
286
            PollStrategy::TryFor(timeout) => self.poll_with_timeout(timeout),
1✔
287
            PollStrategy::UpTo(times) => self.poll_times(times),
11✔
288
        }
289
    }
13✔
290

291
    /// Poll event listener up to `t` times
292
    fn poll_times(&mut self, t: usize) -> ApplicationResult<Vec<Event<UserEvent>>> {
11✔
293
        let mut evs: Vec<Event<UserEvent>> = Vec::with_capacity(t);
11✔
294
        for _ in 0..t {
11✔
295
            match self.poll_listener() {
31✔
296
                Err(err) => return Err(err),
×
297
                Ok(None) => break,
11✔
298
                Ok(Some(ev)) => evs.push(ev),
20✔
299
            }
300
        }
301
        Ok(evs)
11✔
302
    }
11✔
303

304
    /// Poll event listener until `timeout` is elapsed
305
    fn poll_with_timeout(&mut self, timeout: Duration) -> ApplicationResult<Vec<Event<UserEvent>>> {
1✔
306
        let started = Instant::now();
1✔
307
        let mut evs: Vec<Event<UserEvent>> = Vec::new();
1✔
308
        while started.elapsed() < timeout {
31✔
309
            match self.poll_listener() {
30✔
310
                Err(err) => return Err(err),
×
311
                Ok(None) => continue,
27✔
312
                Ok(Some(ev)) => evs.push(ev),
3✔
313
            }
314
        }
315
        Ok(evs)
1✔
316
    }
1✔
317

318
    /// Poll event listener once
319
    fn poll_listener(&mut self) -> ApplicationResult<Option<Event<UserEvent>>> {
62✔
320
        self.listener.poll().map_err(ApplicationError::from)
62✔
321
    }
62✔
322

323
    /// Forward event to current active component, if any.
324
    fn forward_to_active_component(&mut self, ev: Event<UserEvent>) -> Option<Msg> {
24✔
325
        self.view
24✔
326
            .focus()
24✔
327
            .cloned()
24✔
328
            .and_then(|x| self.view.forward(&x, ev).ok().unwrap())
24✔
329
    }
24✔
330

331
    /// Forward events to subscriptions listening to the incoming event.
332
    fn forward_to_subscriptions(&mut self, events: Vec<Event<UserEvent>>) -> Vec<Msg> {
12✔
333
        let mut messages: Vec<Msg> = Vec::new();
12✔
334
        // NOTE: don't touch this code again and don't try to use iterators, cause it's not gonna work :)
335
        for ev in events.iter() {
22✔
336
            for sub in self.subs.iter() {
28✔
337
                // ! Active component must be different from sub !
338
                if self.view.has_focus(sub.target()) {
28✔
339
                    continue;
8✔
340
                }
20✔
341
                if !sub.forward(
20✔
342
                    ev,
20✔
343
                    |id, q| self.view.query(id, q).ok().flatten(),
20✔
344
                    |id| self.view.state(id).ok(),
20✔
345
                    |id| self.view.mounted(id),
20✔
346
                ) {
20✔
347
                    continue;
14✔
348
                }
6✔
349
                if let Some(msg) = self.view.forward(sub.target(), ev.clone()).ok().unwrap() {
6✔
350
                    messages.push(msg);
6✔
351
                }
6✔
352
            }
353
        }
354
        messages
12✔
355
    }
12✔
356
}
357

358
/// Poll strategy defines how to call `Application::poll` on the event listener.
359
pub enum PollStrategy {
360
    /// `Application::poll` function will be called once
361
    Once,
362
    /// The application will keep waiting for events for the provided duration
363
    TryFor(Duration),
364
    /// `Application::poll` function will be called up to `n` times, until it will return [`Option::None`].
365
    UpTo(usize),
366
}
367

368
// -- error
369

370
/// Error variants returned by [`Application`]
371
#[derive(Debug, Error)]
372
pub enum ApplicationError {
373
    #[error("already subscribed")]
374
    AlreadySubscribed,
375
    #[error("listener error: {0}")]
376
    Listener(ListenerError),
377
    #[error("no such subscription")]
378
    NoSuchSubscription,
379
    #[error("view error: {0}")]
380
    View(ViewError),
381
}
382

383
impl From<ListenerError> for ApplicationError {
384
    fn from(e: ListenerError) -> Self {
×
385
        Self::Listener(e)
×
386
    }
×
387
}
388

389
impl From<ViewError> for ApplicationError {
390
    fn from(e: ViewError) -> Self {
5✔
391
        Self::View(e)
5✔
392
    }
5✔
393
}
394

395
#[cfg(test)]
396
mod test {
397

398
    use std::time::Duration;
399

400
    use pretty_assertions::assert_eq;
401

402
    use super::*;
403
    use crate::event::{Key, KeyEvent};
404
    use crate::mock::{
405
        MockBarInput, MockComponentId, MockEvent, MockFooInput, MockInjector, MockMsg, MockPoll,
406
    };
407
    use crate::{StateValue, SubClause};
408

409
    #[test]
410
    fn should_initialize_application() {
1✔
411
        let application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
412
            Application::init(listener_config());
1✔
413
        assert!(application.subs.is_empty());
1✔
414
        assert_eq!(application.view.mounted(&MockComponentId::InputFoo), false);
1✔
415
        assert_eq!(application.sub_lock, false);
1✔
416
    }
1✔
417

418
    #[test]
419
    fn should_restart_listener() {
1✔
420
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
421
            Application::init(listener_config());
1✔
422
        assert!(application.restart_listener(listener_config()).is_ok());
1✔
423
    }
1✔
424

425
    #[test]
426
    fn should_manipulate_components() {
1✔
427
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
428
            Application::init(listener_config());
1✔
429
        // Mount
1✔
430
        assert!(application
1✔
431
            .mount(
1✔
432
                MockComponentId::InputFoo,
1✔
433
                Box::new(MockFooInput::default()),
1✔
434
                vec![]
1✔
435
            )
1✔
436
            .is_ok());
1✔
437
        // Remount with mount
438
        assert!(application
1✔
439
            .mount(
1✔
440
                MockComponentId::InputFoo,
1✔
441
                Box::new(MockFooInput::default()),
1✔
442
                vec![]
1✔
443
            )
1✔
444
            .is_err());
1✔
445
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
446
        assert_eq!(application.focus().unwrap(), &MockComponentId::InputFoo);
1✔
447
        // Remount
448
        assert!(application
1✔
449
            .remount(
1✔
450
                MockComponentId::InputFoo,
1✔
451
                Box::new(MockFooInput::default()),
1✔
452
                vec![]
1✔
453
            )
1✔
454
            .is_ok());
1✔
455
        assert!(application.view.has_focus(&MockComponentId::InputFoo));
1✔
456
        // Mount bar
457
        assert!(application
1✔
458
            .mount(
1✔
459
                MockComponentId::InputBar,
1✔
460
                Box::new(MockBarInput::default()),
1✔
461
                vec![]
1✔
462
            )
1✔
463
            .is_ok());
1✔
464
        // Mounted
465
        assert!(application.mounted(&MockComponentId::InputFoo));
1✔
466
        assert!(application.mounted(&MockComponentId::InputBar));
1✔
467
        assert_eq!(application.mounted(&MockComponentId::InputOmar), false);
1✔
468
        // Attribute and Query
469
        assert!(application
1✔
470
            .query(&MockComponentId::InputFoo, Attribute::InputLength)
1✔
471
            .ok()
1✔
472
            .unwrap()
1✔
473
            .is_none());
1✔
474
        assert!(application
1✔
475
            .attr(
1✔
476
                &MockComponentId::InputFoo,
1✔
477
                Attribute::InputLength,
1✔
478
                AttrValue::Length(8)
1✔
479
            )
1✔
480
            .is_ok());
1✔
481
        assert_eq!(
1✔
482
            application
1✔
483
                .query(&MockComponentId::InputFoo, Attribute::InputLength)
1✔
484
                .ok()
1✔
485
                .unwrap()
1✔
486
                .unwrap(),
1✔
487
            AttrValue::Length(8)
1✔
488
        );
1✔
489
        // State
490
        assert_eq!(
1✔
491
            application.state(&MockComponentId::InputFoo).ok().unwrap(),
1✔
492
            State::One(StateValue::String(String::default()))
1✔
493
        );
1✔
494
        // Active / blur
495
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
496
        assert!(application.active(&MockComponentId::InputBar).is_ok());
1✔
497
        assert!(application.active(&MockComponentId::InputOmar).is_err());
1✔
498
        assert!(application.blur().is_ok());
1✔
499
        assert!(application.blur().is_ok());
1✔
500
        // no focus
501
        assert!(application.blur().is_err());
1✔
502
        // Umount
503
        assert!(application.umount(&MockComponentId::InputFoo).is_ok());
1✔
504
        assert!(application.umount(&MockComponentId::InputFoo).is_err());
1✔
505
        assert!(application.umount(&MockComponentId::InputBar).is_ok());
1✔
506
    }
1✔
507

508
    #[test]
509
    fn should_subscribe_components() {
1✔
510
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
511
            Application::init(listener_config());
1✔
512
        assert!(application
1✔
513
            .mount(
1✔
514
                MockComponentId::InputFoo,
1✔
515
                Box::new(MockFooInput::default()),
1✔
516
                vec![
1✔
517
                    Sub::new(SubEventClause::Tick, SubClause::Always),
1✔
518
                    Sub::new(
1✔
519
                        SubEventClause::Tick,
1✔
520
                        SubClause::HasAttrValue(
1✔
521
                            MockComponentId::InputFoo,
1✔
522
                            Attribute::InputLength,
1✔
523
                            AttrValue::Length(8)
1✔
524
                        )
1✔
525
                    ), // NOTE: This event will be ignored
1✔
526
                    Sub::new(
1✔
527
                        SubEventClause::User(MockEvent::Bar),
1✔
528
                        SubClause::HasAttrValue(
1✔
529
                            MockComponentId::InputFoo,
1✔
530
                            Attribute::Focus,
1✔
531
                            AttrValue::Flag(true)
1✔
532
                        )
1✔
533
                    )
1✔
534
                ]
1✔
535
            )
1✔
536
            .is_ok());
1✔
537
        assert_eq!(application.subs.len(), 2);
1✔
538
        // Subscribe for another event
539
        assert!(application
1✔
540
            .subscribe(
1✔
541
                &MockComponentId::InputFoo,
1✔
542
                Sub::new(
1✔
543
                    SubEventClause::User(MockEvent::Foo),
1✔
544
                    SubClause::HasAttrValue(
1✔
545
                        MockComponentId::InputFoo,
1✔
546
                        Attribute::Focus,
1✔
547
                        AttrValue::Flag(false)
1✔
548
                    )
1✔
549
                )
1✔
550
            )
1✔
551
            .is_ok());
1✔
552
        assert_eq!(application.subs.len(), 3);
1✔
553
        // Try to re-subscribe
554
        assert!(application
1✔
555
            .subscribe(
1✔
556
                &MockComponentId::InputFoo,
1✔
557
                Sub::new(
1✔
558
                    SubEventClause::User(MockEvent::Foo),
1✔
559
                    SubClause::HasAttrValue(
1✔
560
                        MockComponentId::InputFoo,
1✔
561
                        Attribute::Focus,
1✔
562
                        AttrValue::Flag(false)
1✔
563
                    )
1✔
564
                )
1✔
565
            )
1✔
566
            .is_err());
1✔
567
        // Subscribe for unexisting component
568
        assert!(application
1✔
569
            .subscribe(
1✔
570
                &MockComponentId::InputBar,
1✔
571
                Sub::new(
1✔
572
                    SubEventClause::User(MockEvent::Foo),
1✔
573
                    SubClause::HasAttrValue(
1✔
574
                        MockComponentId::InputBar,
1✔
575
                        Attribute::Focus,
1✔
576
                        AttrValue::Flag(false)
1✔
577
                    )
1✔
578
                )
1✔
579
            )
1✔
580
            .is_err());
1✔
581
        // Unsubscribe element
582
        assert!(application
1✔
583
            .unsubscribe(
1✔
584
                &MockComponentId::InputFoo,
1✔
585
                SubEventClause::User(MockEvent::Foo)
1✔
586
            )
1✔
587
            .is_ok());
1✔
588
        // Unsubcribe twice
589
        assert!(application
1✔
590
            .unsubscribe(
1✔
591
                &MockComponentId::InputFoo,
1✔
592
                SubEventClause::User(MockEvent::Foo)
1✔
593
            )
1✔
594
            .is_err());
1✔
595
    }
1✔
596

597
    #[test]
598
    fn should_umount_all() {
1✔
599
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
600
            Application::init(listener_config());
1✔
601
        assert!(application
1✔
602
            .mount(
1✔
603
                MockComponentId::InputFoo,
1✔
604
                Box::new(MockFooInput::default()),
1✔
605
                vec![
1✔
606
                    Sub::new(SubEventClause::Tick, SubClause::Always),
1✔
607
                    Sub::new(
1✔
608
                        SubEventClause::User(MockEvent::Bar),
1✔
609
                        SubClause::HasAttrValue(
1✔
610
                            MockComponentId::InputFoo,
1✔
611
                            Attribute::Focus,
1✔
612
                            AttrValue::Flag(true)
1✔
613
                        )
1✔
614
                    )
1✔
615
                ]
1✔
616
            )
1✔
617
            .is_ok());
1✔
618
        assert!(application
1✔
619
            .mount(
1✔
620
                MockComponentId::InputBar,
1✔
621
                Box::new(MockFooInput::default()),
1✔
622
                vec![Sub::new(SubEventClause::Any, SubClause::Always)]
1✔
623
            )
1✔
624
            .is_ok());
1✔
625
        assert_eq!(application.subs.len(), 3);
1✔
626
        // Let's umount all
627
        application.umount_all();
1✔
628
        assert_eq!(application.mounted(&MockComponentId::InputFoo), false);
1✔
629
        assert_eq!(application.mounted(&MockComponentId::InputBar), false);
1✔
630
        assert!(application.subs.is_empty());
1✔
631
    }
1✔
632

633
    #[test]
634
    fn should_do_tick() {
1✔
635
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
636
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
637
        // Mount foo and bar
1✔
638
        assert!(application
1✔
639
            .mount(
1✔
640
                MockComponentId::InputFoo,
1✔
641
                Box::new(MockFooInput::default()),
1✔
642
                vec![]
1✔
643
            )
1✔
644
            .is_ok());
1✔
645
        assert!(application
1✔
646
            .mount(
1✔
647
                MockComponentId::InputBar,
1✔
648
                Box::new(MockBarInput::default()),
1✔
649
                vec![
1✔
650
                    Sub::new(SubEventClause::Tick, SubClause::Always),
1✔
651
                    Sub::new(
1✔
652
                        // NOTE: won't be thrown, since requires focus
1✔
653
                        SubEventClause::Keyboard(KeyEvent::from(Key::Enter)),
1✔
654
                        SubClause::HasAttrValue(
1✔
655
                            MockComponentId::InputBar,
1✔
656
                            Attribute::Focus,
1✔
657
                            AttrValue::Flag(true)
1✔
658
                        )
1✔
659
                    )
1✔
660
                ]
1✔
661
            )
1✔
662
            .is_ok());
1✔
663
        // Active FOO
664
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
665
        /*
666
         * Here we should:
667
         *
668
         * - receive an Enter from MockPoll, sent to FOO and will return a `FooSubmit`
669
         * - receive a Tick from MockPoll, sent to FOO, but won't return a msg
670
         * - the Tick will be sent also to BAR since is subscribed and will return a `BarTick`
671
         */
672
        assert_eq!(
1✔
673
            application
1✔
674
                .tick(PollStrategy::UpTo(5))
1✔
675
                .ok()
1✔
676
                .unwrap()
1✔
677
                .as_slice(),
1✔
678
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
679
        );
1✔
680
        // Active BAR
681
        assert!(application.active(&MockComponentId::InputBar).is_ok());
1✔
682
        // Wait 200ms (wait for poll)
683
        std::thread::sleep(Duration::from_millis(100));
1✔
684
        /*
1✔
685
         * Here we should:
1✔
686
         *
1✔
687
         * - receive an Enter from MockPoll, sent to BAR and will return a `BarSubmit`
1✔
688
         */
1✔
689
        assert_eq!(
1✔
690
            application
1✔
691
                .tick(PollStrategy::Once)
1✔
692
                .ok()
1✔
693
                .unwrap()
1✔
694
                .as_slice(),
1✔
695
            &[MockMsg::BarSubmit(String::from(""))]
1✔
696
        );
1✔
697
        // Let's try TryFor strategy
698
        let events = application
1✔
699
            .tick(PollStrategy::TryFor(Duration::from_millis(300)))
1✔
700
            .ok()
1✔
701
            .unwrap();
1✔
702
        assert!(events.len() >= 2);
1✔
703
    }
1✔
704

705
    #[test]
706
    fn should_not_propagate_event_when_subs_are_locked() {
1✔
707
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
708
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
709
        // Mount foo and bar
1✔
710
        assert!(application
1✔
711
            .mount(
1✔
712
                MockComponentId::InputFoo,
1✔
713
                Box::new(MockFooInput::default()),
1✔
714
                vec![]
1✔
715
            )
1✔
716
            .is_ok());
1✔
717
        assert!(application
1✔
718
            .mount(
1✔
719
                MockComponentId::InputBar,
1✔
720
                Box::new(MockBarInput::default()),
1✔
721
                vec![
1✔
722
                    Sub::new(SubEventClause::Tick, SubClause::Always),
1✔
723
                    Sub::new(
1✔
724
                        // NOTE: won't be thrown, since requires focus
1✔
725
                        SubEventClause::Keyboard(KeyEvent::from(Key::Enter)),
1✔
726
                        SubClause::HasAttrValue(
1✔
727
                            MockComponentId::InputBar,
1✔
728
                            Attribute::Focus,
1✔
729
                            AttrValue::Flag(true)
1✔
730
                        )
1✔
731
                    )
1✔
732
                ]
1✔
733
            )
1✔
734
            .is_ok());
1✔
735
        // Active FOO
736
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
737
        // lock subs
738
        application.lock_subs();
1✔
739
        assert_eq!(application.sub_lock, true);
1✔
740
        assert_eq!(
1✔
741
            application
1✔
742
                .tick(PollStrategy::UpTo(5))
1✔
743
                .ok()
1✔
744
                .unwrap()
1✔
745
                .as_slice(),
1✔
746
            &[MockMsg::FooSubmit(String::from(""))]
1✔
747
        );
1✔
748
        // unlock subs
749
        application.unlock_subs();
1✔
750
        assert_eq!(application.sub_lock, false);
1✔
751
    }
1✔
752

753
    #[test]
754
    fn should_not_propagate_events_if_has_attr_cond_is_not_satisfied() {
1✔
755
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
756
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
757
        // Mount foo and bar
1✔
758
        assert!(application
1✔
759
            .mount(
1✔
760
                MockComponentId::InputFoo,
1✔
761
                Box::new(MockFooInput::default()),
1✔
762
                vec![]
1✔
763
            )
1✔
764
            .is_ok());
1✔
765
        assert!(application
1✔
766
            .mount(
1✔
767
                MockComponentId::InputBar,
1✔
768
                Box::new(MockBarInput::default()),
1✔
769
                vec![Sub::new(
1✔
770
                    // NOTE: won't be thrown, since requires focus
1✔
771
                    SubEventClause::Tick,
1✔
772
                    SubClause::HasAttrValue(
1✔
773
                        MockComponentId::InputBar,
1✔
774
                        Attribute::Focus,
1✔
775
                        AttrValue::Flag(true)
1✔
776
                    )
1✔
777
                )]
1✔
778
            )
1✔
779
            .is_ok());
1✔
780
        // Active FOO
781
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
782
        assert_eq!(
1✔
783
            application
1✔
784
                .tick(PollStrategy::UpTo(5))
1✔
785
                .ok()
1✔
786
                .unwrap()
1✔
787
                .as_slice(),
1✔
788
            &[MockMsg::FooSubmit(String::from(""))]
1✔
789
        );
1✔
790
    }
1✔
791

792
    #[test]
793
    fn should_propagate_events_if_has_attr_cond_is_satisfied() {
1✔
794
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
795
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
796
        // Mount foo and bar
1✔
797
        assert!(application
1✔
798
            .mount(
1✔
799
                MockComponentId::InputFoo,
1✔
800
                Box::new(MockFooInput::default()),
1✔
801
                vec![]
1✔
802
            )
1✔
803
            .is_ok());
1✔
804
        assert!(application
1✔
805
            .mount(
1✔
806
                MockComponentId::InputBar,
1✔
807
                Box::new(MockBarInput::default()),
1✔
808
                vec![Sub::new(
1✔
809
                    SubEventClause::Tick,
1✔
810
                    SubClause::HasAttrValue(
1✔
811
                        MockComponentId::InputFoo,
1✔
812
                        Attribute::Focus,
1✔
813
                        AttrValue::Flag(true)
1✔
814
                    )
1✔
815
                )]
1✔
816
            )
1✔
817
            .is_ok());
1✔
818
        // Active FOO
819
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
820
        assert_eq!(
1✔
821
            application
1✔
822
                .tick(PollStrategy::UpTo(5))
1✔
823
                .ok()
1✔
824
                .unwrap()
1✔
825
                .as_slice(),
1✔
826
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
827
        );
1✔
828
    }
1✔
829

830
    #[test]
831
    fn should_not_propagate_events_if_has_state_cond_is_not_satisfied() {
1✔
832
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
833
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
834
        // Mount foo and bar
1✔
835
        assert!(application
1✔
836
            .mount(
1✔
837
                MockComponentId::InputFoo,
1✔
838
                Box::new(MockFooInput::default()),
1✔
839
                vec![]
1✔
840
            )
1✔
841
            .is_ok());
1✔
842
        assert!(application
1✔
843
            .mount(
1✔
844
                MockComponentId::InputBar,
1✔
845
                Box::new(MockBarInput::default()),
1✔
846
                vec![Sub::new(
1✔
847
                    SubEventClause::Tick,
1✔
848
                    SubClause::HasState(MockComponentId::InputFoo, State::None)
1✔
849
                )]
1✔
850
            )
1✔
851
            .is_ok());
1✔
852
        // Active FOO
853
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
854
        assert_eq!(
1✔
855
            application
1✔
856
                .tick(PollStrategy::UpTo(5))
1✔
857
                .ok()
1✔
858
                .unwrap()
1✔
859
                .as_slice(),
1✔
860
            &[MockMsg::FooSubmit(String::from(""))]
1✔
861
        );
1✔
862
    }
1✔
863

864
    #[test]
865
    fn should_propagate_events_if_has_state_cond_is_satisfied() {
1✔
866
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
867
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
868
        // Mount foo and bar
1✔
869
        assert!(application
1✔
870
            .mount(
1✔
871
                MockComponentId::InputFoo,
1✔
872
                Box::new(MockFooInput::default()),
1✔
873
                vec![]
1✔
874
            )
1✔
875
            .is_ok());
1✔
876
        assert!(application
1✔
877
            .mount(
1✔
878
                MockComponentId::InputBar,
1✔
879
                Box::new(MockBarInput::default()),
1✔
880
                vec![Sub::new(
1✔
881
                    SubEventClause::Tick,
1✔
882
                    SubClause::HasState(
1✔
883
                        MockComponentId::InputFoo,
1✔
884
                        State::One(StateValue::String(String::new()))
1✔
885
                    )
1✔
886
                )]
1✔
887
            )
1✔
888
            .is_ok());
1✔
889
        // Active FOO
890
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
891
        // No event should be generated
892
        assert_eq!(
1✔
893
            application
1✔
894
                .tick(PollStrategy::UpTo(5))
1✔
895
                .ok()
1✔
896
                .unwrap()
1✔
897
                .as_slice(),
1✔
898
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
899
        );
1✔
900
    }
1✔
901

902
    #[test]
903
    fn should_not_propagate_events_if_is_mounted_cond_is_not_satisfied() {
1✔
904
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
905
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
906
        // Mount foo and bar
1✔
907
        assert!(application
1✔
908
            .mount(
1✔
909
                MockComponentId::InputFoo,
1✔
910
                Box::new(MockFooInput::default()),
1✔
911
                vec![]
1✔
912
            )
1✔
913
            .is_ok());
1✔
914
        assert!(application
1✔
915
            .mount(
1✔
916
                MockComponentId::InputBar,
1✔
917
                Box::new(MockBarInput::default()),
1✔
918
                vec![Sub::new(
1✔
919
                    SubEventClause::Tick,
1✔
920
                    SubClause::IsMounted(MockComponentId::InputOmar)
1✔
921
                )]
1✔
922
            )
1✔
923
            .is_ok());
1✔
924
        // Active FOO
925
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
926
        assert_eq!(
1✔
927
            application
1✔
928
                .tick(PollStrategy::UpTo(5))
1✔
929
                .ok()
1✔
930
                .unwrap()
1✔
931
                .as_slice(),
1✔
932
            &[MockMsg::FooSubmit(String::from(""))]
1✔
933
        );
1✔
934
    }
1✔
935

936
    #[test]
937
    fn should_propagate_events_if_is_mounted_cond_is_not_satisfied() {
1✔
938
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
939
            Application::init(listener_config_with_tick(Duration::from_secs(60)));
1✔
940
        // Mount foo and bar
1✔
941
        assert!(application
1✔
942
            .mount(
1✔
943
                MockComponentId::InputFoo,
1✔
944
                Box::new(MockFooInput::default()),
1✔
945
                vec![]
1✔
946
            )
1✔
947
            .is_ok());
1✔
948
        assert!(application
1✔
949
            .mount(
1✔
950
                MockComponentId::InputBar,
1✔
951
                Box::new(MockBarInput::default()),
1✔
952
                vec![Sub::new(
1✔
953
                    SubEventClause::Tick,
1✔
954
                    SubClause::IsMounted(MockComponentId::InputFoo)
1✔
955
                )]
1✔
956
            )
1✔
957
            .is_ok());
1✔
958
        // Active FOO
959
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
960
        assert_eq!(
1✔
961
            application
1✔
962
                .tick(PollStrategy::UpTo(5))
1✔
963
                .ok()
1✔
964
                .unwrap()
1✔
965
                .as_slice(),
1✔
966
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
967
        );
1✔
968
    }
1✔
969

970
    #[test]
971
    fn should_lock_ports() {
1✔
972
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
973
            Application::init(listener_config_with_tick(Duration::from_millis(500)));
1✔
974
        // Mount foo and bar
1✔
975
        assert!(application
1✔
976
            .mount(
1✔
977
                MockComponentId::InputFoo,
1✔
978
                Box::new(MockFooInput::default()),
1✔
979
                vec![]
1✔
980
            )
1✔
981
            .is_ok());
1✔
982
        assert!(application
1✔
983
            .mount(
1✔
984
                MockComponentId::InputBar,
1✔
985
                Box::new(MockBarInput::default()),
1✔
986
                vec![Sub::new(
1✔
987
                    SubEventClause::Tick,
1✔
988
                    SubClause::IsMounted(MockComponentId::InputFoo)
1✔
989
                )]
1✔
990
            )
1✔
991
            .is_ok());
1✔
992
        // Active FOO
993
        assert!(application.active(&MockComponentId::InputFoo).is_ok());
1✔
994
        assert_eq!(
1✔
995
            application
1✔
996
                .tick(PollStrategy::UpTo(5))
1✔
997
                .ok()
1✔
998
                .unwrap()
1✔
999
                .as_slice(),
1✔
1000
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
1001
        );
1✔
1002
        // Lock ports
1003
        assert!(application.lock_ports().is_ok());
1✔
1004
        // Wait 1 sec
1005
        std::thread::sleep(Duration::from_millis(1000));
1✔
1006
        // Tick ( No tick event )
1✔
1007
        assert_eq!(
1✔
1008
            application
1✔
1009
                .tick(PollStrategy::UpTo(5))
1✔
1010
                .ok()
1✔
1011
                .unwrap()
1✔
1012
                .as_slice(),
1✔
1013
            &[]
1✔
1014
        );
1✔
1015
        // Unlock ports
1016
        assert!(application.unlock_ports().is_ok());
1✔
1017
        // Wait 100 ms
1018
        std::thread::sleep(Duration::from_millis(50));
1✔
1019
        // Tick
1✔
1020
        assert_eq!(
1✔
1021
            application
1✔
1022
                .tick(PollStrategy::UpTo(5))
1✔
1023
                .ok()
1✔
1024
                .unwrap()
1✔
1025
                .as_slice(),
1✔
1026
            &[MockMsg::FooSubmit(String::from("")), MockMsg::BarTick]
1✔
1027
        );
1✔
1028
    }
1✔
1029

1030
    #[test]
1031
    fn application_should_add_injectors() {
1✔
1032
        let mut application: Application<MockComponentId, MockMsg, MockEvent> =
1✔
1033
            Application::init(listener_config_with_tick(Duration::from_millis(500)));
1✔
1034
        application.add_injector(Box::new(MockInjector::default()));
1✔
1035
    }
1✔
1036

1037
    fn listener_config() -> EventListenerCfg<MockEvent> {
16✔
1038
        EventListenerCfg::default().add_port(
16✔
1039
            Box::new(MockPoll::<MockEvent>::default()),
16✔
1040
            Duration::from_millis(100),
16✔
1041
            1,
16✔
1042
        )
16✔
1043
    }
16✔
1044

1045
    fn listener_config_with_tick(tick: Duration) -> EventListenerCfg<MockEvent> {
10✔
1046
        listener_config().tick_interval(tick)
10✔
1047
    }
10✔
1048
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc