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

veeso / tui-realm-stdlib / 11316405772

13 Oct 2024 05:35PM UTC coverage: 73.26% (-0.2%) from 73.477%
11316405772

push

github

veeso
feat: tuirealm 2.x

0 of 7 new or added lines in 5 files covered. (0.0%)

16 existing lines in 6 files now uncovered.

2726 of 3721 relevant lines covered (73.26%)

1.98 hits per line

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

79.72
/src/components/input.rs
1
//! ## Input
2
//!
3
//! `Input` represents a read-write input field. This component supports different input types, input length
4
//! and handles input events related to cursor position, backspace, canc, ...
5

6
use super::props::{INPUT_INVALID_STYLE, INPUT_PLACEHOLDER, INPUT_PLACEHOLDER_STYLE};
7
use crate::utils::calc_utf8_cursor_position;
8
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
9
use tuirealm::props::{
10
    Alignment, AttrValue, Attribute, Borders, Color, InputType, Props, Style, TextModifiers,
11
};
12
use tuirealm::ratatui::{layout::Rect, widgets::Paragraph};
13
use tuirealm::{Frame, MockComponent, State, StateValue};
14

15
// -- states
16

17
#[derive(Default)]
4✔
18
pub struct InputStates {
19
    pub input: Vec<char>, // Current input
2✔
20
    pub cursor: usize,    // Input position
2✔
21
}
22

23
impl InputStates {
24
    /// ### append
25
    ///
26
    /// Append, if possible according to input type, the character to the input vec
27
    pub fn append(&mut self, ch: char, itype: &InputType, max_len: Option<usize>) {
35✔
28
        // Check if max length has been reached
29
        if self.input.len() < max_len.unwrap_or(usize::MAX) {
35✔
30
            // Check whether can push
31
            if itype.char_valid(self.input.iter().collect::<String>().as_str(), ch) {
33✔
32
                self.input.insert(self.cursor, ch);
24✔
33
                self.incr_cursor();
24✔
34
            }
35
        }
36
    }
35✔
37

38
    /// ### backspace
39
    ///
40
    /// Delete element at cursor -1; then decrement cursor by 1
41
    pub fn backspace(&mut self) {
3✔
42
        if self.cursor > 0 && !self.input.is_empty() {
3✔
43
            self.input.remove(self.cursor - 1);
2✔
44
            // Decrement cursor
45
            self.cursor -= 1;
2✔
46
        }
47
    }
3✔
48

49
    /// ### delete
50
    ///
51
    /// Delete element at cursor
52
    pub fn delete(&mut self) {
3✔
53
        if self.cursor < self.input.len() {
3✔
54
            self.input.remove(self.cursor);
1✔
55
        }
56
    }
3✔
57

58
    /// ### incr_cursor
59
    ///
60
    /// Increment cursor value by one if possible
61
    pub fn incr_cursor(&mut self) {
28✔
62
        if self.cursor < self.input.len() {
28✔
63
            self.cursor += 1;
28✔
64
        }
65
    }
28✔
66

67
    /// ### cursoro_at_begin
68
    ///
69
    /// Place cursor at the begin of the input
70
    pub fn cursor_at_begin(&mut self) {
1✔
71
        self.cursor = 0;
1✔
72
    }
1✔
73

74
    /// ### cursor_at_end
75
    ///
76
    /// Place cursor at the end of the input
77
    pub fn cursor_at_end(&mut self) {
2✔
78
        self.cursor = self.input.len();
2✔
79
    }
2✔
80

81
    /// ### decr_cursor
82
    ///
83
    /// Decrement cursor value by one if possible
84
    pub fn decr_cursor(&mut self) {
6✔
85
        if self.cursor > 0 {
6✔
86
            self.cursor -= 1;
4✔
87
        }
88
    }
6✔
89

90
    /// ### render_value
91
    ///
92
    /// Get value as string to render
93
    pub fn render_value(&self, itype: InputType) -> String {
2✔
94
        self.render_value_chars(itype).iter().collect::<String>()
2✔
95
    }
2✔
96

97
    /// ### render_value_chars
98
    ///
99
    /// Render value as a vec of chars
100
    pub fn render_value_chars(&self, itype: InputType) -> Vec<char> {
2✔
101
        match itype {
2✔
102
            InputType::Password(ch) | InputType::CustomPassword(ch, _, _) => {
1✔
103
                (0..self.input.len()).map(|_| ch).collect()
4✔
104
            }
105
            _ => self.input.clone(),
1✔
106
        }
107
    }
2✔
108

109
    /// ### get_value
110
    ///
111
    /// Get value as string
112
    pub fn get_value(&self) -> String {
35✔
113
        self.input.iter().collect()
35✔
114
    }
35✔
115
}
116

117
// -- Component
118

119
/// ## Input
120
///
121
/// Input list component
122
#[derive(Default)]
2✔
123
pub struct Input {
124
    props: Props,
1✔
125
    pub states: InputStates,
1✔
126
}
127

128
impl Input {
129
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
130
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
131
        self
1✔
132
    }
1✔
133

134
    pub fn background(mut self, bg: Color) -> Self {
1✔
135
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
136
        self
1✔
137
    }
1✔
138

139
    pub fn inactive(mut self, s: Style) -> Self {
1✔
140
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
1✔
141
        self
1✔
142
    }
1✔
143

144
    pub fn borders(mut self, b: Borders) -> Self {
1✔
145
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
146
        self
1✔
147
    }
1✔
148

149
    pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
1✔
150
        self.attr(
1✔
151
            Attribute::Title,
1✔
152
            AttrValue::Title((t.as_ref().to_string(), a)),
1✔
153
        );
154
        self
1✔
155
    }
1✔
156

157
    pub fn input_type(mut self, itype: InputType) -> Self {
1✔
158
        self.attr(Attribute::InputType, AttrValue::InputType(itype));
1✔
159
        self
1✔
160
    }
1✔
161

162
    pub fn input_len(mut self, ilen: usize) -> Self {
1✔
163
        self.attr(Attribute::InputLength, AttrValue::Length(ilen));
1✔
164
        self
1✔
165
    }
1✔
166

167
    pub fn value<S: AsRef<str>>(mut self, s: S) -> Self {
1✔
168
        self.attr(Attribute::Value, AttrValue::String(s.as_ref().to_string()));
1✔
169
        self
1✔
170
    }
1✔
171

172
    pub fn invalid_style(mut self, s: Style) -> Self {
×
173
        self.attr(Attribute::Custom(INPUT_INVALID_STYLE), AttrValue::Style(s));
×
174
        self
×
175
    }
×
176

177
    pub fn placeholder<S: AsRef<str>>(mut self, placeholder: S, style: Style) -> Self {
178
        self.attr(
179
            Attribute::Custom(INPUT_PLACEHOLDER),
180
            AttrValue::String(placeholder.as_ref().to_string()),
181
        );
182
        self.attr(
183
            Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
184
            AttrValue::Style(style),
185
        );
186
        self
187
    }
188

189
    fn get_input_len(&self) -> Option<usize> {
9✔
190
        self.props
18✔
191
            .get(Attribute::InputLength)
9✔
192
            .map(|x| x.unwrap_length())
9✔
193
    }
9✔
194

195
    fn get_input_type(&self) -> InputType {
27✔
196
        self.props
54✔
197
            .get_or(Attribute::InputType, AttrValue::InputType(InputType::Text))
27✔
198
            .unwrap_input_type()
199
    }
27✔
200

201
    /// ### is_valid
202
    ///
203
    /// Checks whether current input is valid
204
    fn is_valid(&self) -> bool {
18✔
205
        let value = self.states.get_value();
18✔
206
        self.get_input_type().validate(value.as_str())
18✔
207
    }
18✔
208
}
209

210
impl MockComponent for Input {
211
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
212
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
213
            let mut foreground = self
×
214
                .props
215
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
216
                .unwrap_color();
217
            let mut background = self
×
218
                .props
219
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
220
                .unwrap_color();
221
            let modifiers = self
×
222
                .props
223
                .get_or(
224
                    Attribute::TextProps,
×
225
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
226
                )
227
                .unwrap_text_modifiers();
228
            let title = self
×
229
                .props
230
                .get_or(
231
                    Attribute::Title,
×
232
                    AttrValue::Title((String::default(), Alignment::Center)),
×
233
                )
234
                .unwrap_title();
235
            let borders = self
×
236
                .props
237
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
238
                .unwrap_borders();
239
            let focus = self
×
240
                .props
241
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
242
                .unwrap_flag();
243
            let inactive_style = self
×
244
                .props
245
                .get(Attribute::FocusStyle)
×
246
                .map(|x| x.unwrap_style());
×
247
            let itype = self.get_input_type();
×
248
            let mut block = crate::utils::get_block(borders, Some(title), focus, inactive_style);
×
249
            // Apply invalid style
250
            if focus && !self.is_valid() {
×
251
                if let Some(style) = self
×
252
                    .props
253
                    .get(Attribute::Custom(INPUT_INVALID_STYLE))
×
254
                    .map(|x| x.unwrap_style())
×
255
                {
256
                    let borders = self
×
257
                        .props
258
                        .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
259
                        .unwrap_borders()
260
                        .color(style.fg.unwrap_or(Color::Reset));
×
261
                    let title = self
×
262
                        .props
263
                        .get_or(
264
                            Attribute::Title,
×
265
                            AttrValue::Title((String::default(), Alignment::Center)),
×
266
                        )
267
                        .unwrap_title();
268
                    block = crate::utils::get_block(borders, Some(title), focus, None);
×
269
                    foreground = style.fg.unwrap_or(Color::Reset);
×
270
                    background = style.bg.unwrap_or(Color::Reset);
×
271
                }
272
            }
273
            let text_to_display = self.states.render_value(self.get_input_type());
×
274
            let show_placeholder = text_to_display.is_empty();
×
275
            // Choose whether to show placeholder; if placeholder is unset, show nothing
276
            let text_to_display = match show_placeholder {
×
277
                true => self
×
278
                    .props
279
                    .get_or(
280
                        Attribute::Custom(INPUT_PLACEHOLDER),
×
281
                        AttrValue::String(String::new()),
×
282
                    )
283
                    .unwrap_string(),
284
                false => text_to_display,
×
285
            };
286
            // Choose paragraph style based on whether is valid or not and if has focus and if should show placeholder
287
            let paragraph_style = match focus {
×
288
                true => Style::default()
×
289
                    .fg(foreground)
×
290
                    .bg(background)
×
291
                    .add_modifier(modifiers),
292
                false => inactive_style.unwrap_or_default(),
×
293
            };
294
            let paragraph_style = match show_placeholder {
×
295
                true => self
×
296
                    .props
297
                    .get_or(
298
                        Attribute::Custom(INPUT_PLACEHOLDER_STYLE),
×
299
                        AttrValue::Style(paragraph_style),
×
300
                    )
301
                    .unwrap_style(),
302
                false => paragraph_style,
×
303
            };
304
            // Create widget
305
            let block_inner_area = block.inner(area);
×
306
            let p: Paragraph = Paragraph::new(text_to_display)
×
307
                .style(paragraph_style)
×
308
                .block(block);
×
309
            render.render_widget(p, area);
×
310
            // Set cursor, if focus
311
            if focus {
×
312
                let x: u16 = block_inner_area.x
×
313
                    + calc_utf8_cursor_position(
×
314
                        &self.states.render_value_chars(itype)[0..self.states.cursor],
×
315
                    );
×
NEW
316
                render
×
NEW
317
                    .set_cursor_position(tuirealm::ratatui::prelude::Position { x, y: area.y + 1 });
×
318
            }
319
        }
×
320
    }
×
321

322
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
323
        self.props.get(attr)
×
324
    }
×
325

326
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
11✔
327
        let sanitize_input = matches!(
22✔
328
            attr,
11✔
329
            Attribute::InputLength | Attribute::InputType | Attribute::Value
330
        );
331
        // Check if new input
332
        let new_input = match attr {
11✔
333
            Attribute::Value => Some(value.clone().unwrap_string()),
2✔
334
            _ => None,
9✔
335
        };
336
        self.props.set(attr, value);
11✔
337
        if sanitize_input {
11✔
338
            let input = match new_input {
6✔
339
                None => self.states.input.clone(),
4✔
340
                Some(v) => v.chars().collect(),
2✔
341
            };
342
            self.states.input = Vec::new();
6✔
343
            self.states.cursor = 0;
6✔
344
            let itype = self.get_input_type();
6✔
345
            let max_len = self.get_input_len();
6✔
346
            for ch in input.into_iter() {
33✔
347
                self.states.append(ch, &itype, max_len);
27✔
348
            }
6✔
349
        }
350
    }
11✔
351

352
    fn state(&self) -> State {
18✔
353
        // Validate input
354
        if self.is_valid() {
18✔
355
            State::One(StateValue::String(self.states.get_value()))
17✔
356
        } else {
357
            State::None
1✔
358
        }
359
    }
18✔
360

361
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
17✔
362
        match cmd {
17✔
363
            Cmd::Delete => {
364
                // Backspace and None
365
                let prev_input = self.states.input.clone();
3✔
366
                self.states.backspace();
3✔
367
                if prev_input != self.states.input {
3✔
368
                    CmdResult::Changed(self.state())
2✔
369
                } else {
370
                    CmdResult::None
1✔
371
                }
372
            }
3✔
373
            Cmd::Cancel => {
374
                // Delete and None
375
                let prev_input = self.states.input.clone();
3✔
376
                self.states.delete();
3✔
377
                if prev_input != self.states.input {
3✔
378
                    CmdResult::Changed(self.state())
1✔
379
                } else {
380
                    CmdResult::None
2✔
381
                }
382
            }
3✔
383
            Cmd::Submit => CmdResult::Submit(self.state()),
1✔
384
            Cmd::Move(Direction::Left) => {
385
                self.states.decr_cursor();
3✔
386
                CmdResult::None
3✔
387
            }
388
            Cmd::Move(Direction::Right) => {
389
                self.states.incr_cursor();
1✔
390
                CmdResult::None
1✔
391
            }
392
            Cmd::GoTo(Position::Begin) => {
393
                self.states.cursor_at_begin();
1✔
394
                CmdResult::None
1✔
395
            }
396
            Cmd::GoTo(Position::End) => {
397
                self.states.cursor_at_end();
2✔
398
                CmdResult::None
2✔
399
            }
400
            Cmd::Type(ch) => {
3✔
401
                // Push char to input
402
                let prev_input = self.states.input.clone();
3✔
403
                self.states
3✔
404
                    .append(ch, &self.get_input_type(), self.get_input_len());
3✔
405
                // Message on change
406
                if prev_input != self.states.input {
3✔
407
                    CmdResult::Changed(self.state())
2✔
408
                } else {
409
                    CmdResult::None
1✔
410
                }
411
            }
3✔
412
            _ => CmdResult::None,
×
413
        }
414
    }
17✔
415
}
416

417
#[cfg(test)]
418
mod tests {
419

420
    use super::*;
421

422
    use pretty_assertions::assert_eq;
423

424
    #[test]
425
    fn test_components_input_states() {
2✔
426
        let mut states: InputStates = InputStates::default();
1✔
427
        states.append('a', &InputType::Text, Some(3));
1✔
428
        assert_eq!(states.input, vec!['a']);
1✔
429
        states.append('b', &InputType::Text, Some(3));
1✔
430
        assert_eq!(states.input, vec!['a', 'b']);
1✔
431
        states.append('c', &InputType::Text, Some(3));
1✔
432
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
433
        // Reached length
434
        states.append('d', &InputType::Text, Some(3));
1✔
435
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
436
        // Push char to numbers
437
        states.append('d', &InputType::Number, None);
1✔
438
        assert_eq!(states.input, vec!['a', 'b', 'c']);
1✔
439
        // move cursor
440
        // decr cursor
441
        states.decr_cursor();
1✔
442
        assert_eq!(states.cursor, 2);
1✔
443
        states.cursor = 1;
1✔
444
        states.decr_cursor();
1✔
445
        assert_eq!(states.cursor, 0);
1✔
446
        states.decr_cursor();
1✔
447
        assert_eq!(states.cursor, 0);
1✔
448
        // Incr
449
        states.incr_cursor();
1✔
450
        assert_eq!(states.cursor, 1);
1✔
451
        states.incr_cursor();
1✔
452
        assert_eq!(states.cursor, 2);
1✔
453
        states.incr_cursor();
1✔
454
        assert_eq!(states.cursor, 3);
1✔
455
        // Render value
456
        assert_eq!(states.render_value(InputType::Text).as_str(), "abc");
1✔
457
        assert_eq!(
1✔
458
            states.render_value(InputType::Password('*')).as_str(),
1✔
459
            "***"
460
        );
461
    }
2✔
462

463
    #[test]
464
    fn test_components_input_text() {
2✔
465
        // Instantiate Input with value
466
        let mut component: Input = Input::default()
7✔
467
            .background(Color::Yellow)
1✔
468
            .borders(Borders::default())
1✔
469
            .foreground(Color::Cyan)
1✔
470
            .inactive(Style::default())
1✔
471
            .input_len(5)
472
            .input_type(InputType::Text)
1✔
473
            .title("pippo", Alignment::Center)
1✔
474
            .value("home");
475
        // Verify initial state
476
        assert_eq!(component.states.cursor, 4);
1✔
477
        assert_eq!(component.states.input.len(), 4);
1✔
478
        // Get value
479
        assert_eq!(
1✔
480
            component.state(),
1✔
481
            State::One(StateValue::String(String::from("home")))
1✔
482
        );
483
        // Character
484
        assert_eq!(
1✔
485
            component.perform(Cmd::Type('/')),
1✔
486
            CmdResult::Changed(State::One(StateValue::String(String::from("home/"))))
1✔
487
        );
488
        assert_eq!(
1✔
489
            component.state(),
1✔
490
            State::One(StateValue::String(String::from("home/")))
1✔
491
        );
492
        assert_eq!(component.states.cursor, 5);
1✔
493
        // Verify max length (shouldn't push any character)
494
        assert_eq!(component.perform(Cmd::Type('a')), CmdResult::None);
1✔
495
        assert_eq!(
1✔
496
            component.state(),
1✔
497
            State::One(StateValue::String(String::from("home/")))
1✔
498
        );
499
        assert_eq!(component.states.cursor, 5);
1✔
500
        // Submit
501
        assert_eq!(
1✔
502
            component.perform(Cmd::Submit),
1✔
503
            CmdResult::Submit(State::One(StateValue::String(String::from("home/"))))
1✔
504
        );
505
        // Backspace
506
        assert_eq!(
1✔
507
            component.perform(Cmd::Delete),
1✔
508
            CmdResult::Changed(State::One(StateValue::String(String::from("home"))))
1✔
509
        );
510
        assert_eq!(
1✔
511
            component.state(),
1✔
512
            State::One(StateValue::String(String::from("home")))
1✔
513
        );
514
        assert_eq!(component.states.cursor, 4);
1✔
515
        // Check backspace at 0
516
        component.states.input = vec!['h'];
1✔
517
        component.states.cursor = 1;
1✔
518
        assert_eq!(
1✔
519
            component.perform(Cmd::Delete),
1✔
520
            CmdResult::Changed(State::One(StateValue::String(String::from(""))))
1✔
521
        );
522
        assert_eq!(
1✔
523
            component.state(),
1✔
524
            State::One(StateValue::String(String::from("")))
1✔
525
        );
526
        assert_eq!(component.states.cursor, 0);
1✔
527
        // Another one...
528
        assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
1✔
529
        assert_eq!(
1✔
530
            component.state(),
1✔
531
            State::One(StateValue::String(String::from("")))
1✔
532
        );
533
        assert_eq!(component.states.cursor, 0);
1✔
534
        // See del behaviour here
535
        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
1✔
536
        assert_eq!(
1✔
537
            component.state(),
1✔
538
            State::One(StateValue::String(String::from("")))
1✔
539
        );
540
        assert_eq!(component.states.cursor, 0);
1✔
541
        // Check del behaviour
542
        component.states.input = vec!['h', 'e'];
1✔
543
        component.states.cursor = 1;
1✔
544
        assert_eq!(
1✔
545
            component.perform(Cmd::Cancel),
1✔
546
            CmdResult::Changed(State::One(StateValue::String(String::from("h"))))
1✔
547
        );
548
        assert_eq!(
1✔
549
            component.state(),
1✔
550
            State::One(StateValue::String(String::from("h")))
1✔
551
        );
552
        assert_eq!(component.states.cursor, 1);
1✔
553
        // Another one (should do nothing)
554
        assert_eq!(component.perform(Cmd::Cancel), CmdResult::None);
1✔
555
        assert_eq!(
1✔
556
            component.state(),
1✔
557
            State::One(StateValue::String(String::from("h")))
1✔
558
        );
559
        assert_eq!(component.states.cursor, 1);
1✔
560
        // Move cursor right
561
        component.states.input = vec!['h', 'e', 'l', 'l', 'o'];
1✔
562
        // Update length to 16
563
        component.attr(Attribute::InputLength, AttrValue::Length(16));
1✔
564
        component.states.cursor = 1;
1✔
565
        assert_eq!(
1✔
566
            component.perform(Cmd::Move(Direction::Right)), // between 'e' and 'l'
1✔
567
            CmdResult::None
568
        );
569
        assert_eq!(component.states.cursor, 2);
1✔
570
        // Put a character here
571
        assert_eq!(
1✔
572
            component.perform(Cmd::Type('a')),
1✔
573
            CmdResult::Changed(State::One(StateValue::String(String::from("heallo"))))
1✔
574
        );
575
        assert_eq!(
1✔
576
            component.state(),
1✔
577
            State::One(StateValue::String(String::from("heallo")))
1✔
578
        );
579
        assert_eq!(component.states.cursor, 3);
1✔
580
        // Move left
581
        assert_eq!(
1✔
582
            component.perform(Cmd::Move(Direction::Left)),
1✔
583
            CmdResult::None
584
        );
585
        assert_eq!(component.states.cursor, 2);
1✔
586
        // Go at the end
587
        component.states.cursor = 6;
1✔
588
        // Move right
589
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
590
        assert_eq!(component.states.cursor, 6);
1✔
591
        // Move left
592
        assert_eq!(
1✔
593
            component.perform(Cmd::Move(Direction::Left)),
1✔
594
            CmdResult::None
595
        );
596
        assert_eq!(component.states.cursor, 5);
1✔
597
        // Go at the beginning
598
        component.states.cursor = 0;
1✔
599
        assert_eq!(
1✔
600
            component.perform(Cmd::Move(Direction::Left)),
1✔
601
            CmdResult::None
602
        );
603
        //assert_eq!(component.render().unwrap().cursor, 0); // Should stay
604
        assert_eq!(component.states.cursor, 0);
1✔
605
        // End - begin
606
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
607
        assert_eq!(component.states.cursor, 6);
1✔
608
        assert_eq!(
1✔
609
            component.perform(Cmd::GoTo(Position::Begin)),
1✔
610
            CmdResult::None
611
        );
612
        assert_eq!(component.states.cursor, 0);
1✔
613
        // Update value
614
        component.attr(Attribute::Value, AttrValue::String("new-value".to_string()));
1✔
615
        assert_eq!(
1✔
616
            component.state(),
1✔
617
            State::One(StateValue::String(String::from("new-value")))
1✔
618
        );
619
        // Invalidate input type
620
        component.attr(
1✔
621
            Attribute::InputType,
1✔
622
            AttrValue::InputType(InputType::Number),
1✔
623
        );
624
        assert_eq!(component.state(), State::None);
1✔
625
    }
2✔
626
}
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