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

veeso / tui-realm-stdlib / 16400389551

20 Jul 2025 01:25PM UTC coverage: 67.956% (-0.05%) from 68.002%
16400389551

Pull #36

github

web-flow
Merge a90ec48a1 into 84017eb33
Pull Request #36: fix(input): limit cursor positon to input area

0 of 5 new or added lines in 1 file covered. (0.0%)

3039 of 4472 relevant lines covered (67.96%)

1.83 hits per line

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

73.4
/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)]
18
pub struct InputStates {
19
    pub input: Vec<char>, // Current input
20
    pub cursor: usize,    // Input position
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
            }
24✔
35
        }
2✔
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
2✔
45
            self.cursor -= 1;
2✔
46
        }
2✔
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
        }
2✔
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
        }
28✔
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
        }
4✔
88
    }
6✔
89

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

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

111
    /// ### get_value
112
    ///
113
    /// Get value as string
114
    #[must_use]
115
    pub fn get_value(&self) -> String {
35✔
116
        self.input.iter().collect()
35✔
117
    }
35✔
118
}
119

120
// -- Component
121

122
/// ## Input
123
///
124
/// Input list component
125
#[derive(Default)]
126
#[must_use]
127
pub struct Input {
128
    props: Props,
129
    pub states: InputStates,
130
}
131

132
impl Input {
133
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
134
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
135
        self
1✔
136
    }
1✔
137

138
    pub fn background(mut self, bg: Color) -> Self {
1✔
139
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
140
        self
1✔
141
    }
1✔
142

143
    pub fn inactive(mut self, s: Style) -> Self {
1✔
144
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
1✔
145
        self
1✔
146
    }
1✔
147

148
    pub fn borders(mut self, b: Borders) -> Self {
1✔
149
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
150
        self
1✔
151
    }
1✔
152

153
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
154
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
155
        self
1✔
156
    }
1✔
157

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

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

168
    pub fn value<S: Into<String>>(mut self, s: S) -> Self {
1✔
169
        self.attr(Attribute::Value, AttrValue::String(s.into()));
1✔
170
        self
1✔
171
    }
1✔
172

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

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

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

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

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

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

314
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
315
        self.props.get(attr)
×
316
    }
×
317

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

344
    fn state(&self) -> State {
18✔
345
        // Validate input
346
        if self.is_valid() {
18✔
347
            State::One(StateValue::String(self.states.get_value()))
17✔
348
        } else {
349
            State::None
1✔
350
        }
351
    }
18✔
352

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

409
#[cfg(test)]
410
mod tests {
411

412
    use super::*;
413

414
    use pretty_assertions::assert_eq;
415

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

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

© 2026 Coveralls, Inc