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

veeso / tui-realm-stdlib / 20192640301

13 Dec 2025 01:20PM UTC coverage: 68.439% (+0.4%) from 68.002%
20192640301

Pull #44

github

web-flow
Merge 1c77b17ae into 84017eb33
Pull Request #44: Fix Styling inconsistencies

6 of 83 new or added lines in 10 files covered. (7.23%)

6 existing lines in 5 files now uncovered.

3038 of 4439 relevant lines covered (68.44%)

1.84 hits per line

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

65.89
/src/components/select.rs
1
//! ## Select
2
//!
3
//! `Select` represents a select field, like in HTML. The size for the component must be 3 (border + selected) + the quantity of rows
4
//! you want to display other options when opened (at least 3)
5

6
use tuirealm::command::{Cmd, CmdResult, Direction};
7
use tuirealm::props::{
8
    Alignment, AttrValue, Attribute, BorderSides, Borders, Color, PropPayload, PropValue, Props,
9
    Style, TextModifiers,
10
};
11
use tuirealm::ratatui::text::Line as Spans;
12
use tuirealm::ratatui::{
13
    layout::{Constraint, Direction as LayoutDirection, Layout, Rect},
14
    widgets::{Block, List, ListItem, ListState, Paragraph},
15
};
16
use tuirealm::{Frame, MockComponent, State, StateValue};
17

18
// -- states
19

20
/// ## SelectStates
21
///
22
/// Component states
23
#[derive(Default)]
24
pub struct SelectStates {
25
    /// Available choices
26
    pub choices: Vec<String>,
27
    /// Currently selected choice
28
    pub selected: usize,
29
    /// Choice selected before opening the tab
30
    pub previously_selected: usize,
31
    pub tab_open: bool,
32
}
33

34
impl SelectStates {
35
    /// ### next_choice
36
    ///
37
    /// Move choice index to next choice
38
    pub fn next_choice(&mut self, rewind: bool) {
11✔
39
        if self.tab_open {
11✔
40
            if rewind && self.selected + 1 >= self.choices.len() {
9✔
41
                self.selected = 0;
1✔
42
            } else if self.selected + 1 < self.choices.len() {
8✔
43
                self.selected += 1;
6✔
44
            }
6✔
45
        }
2✔
46
    }
11✔
47

48
    /// ### prev_choice
49
    ///
50
    /// Move choice index to previous choice
51
    pub fn prev_choice(&mut self, rewind: bool) {
11✔
52
        if self.tab_open {
11✔
53
            if rewind && self.selected == 0 && !self.choices.is_empty() {
8✔
54
                self.selected = self.choices.len() - 1;
1✔
55
            } else if self.selected > 0 {
7✔
56
                self.selected -= 1;
6✔
57
            }
6✔
58
        }
3✔
59
    }
11✔
60

61
    /// ### set_choices
62
    ///
63
    /// Set SelectStates choices from a vector of str
64
    /// In addition resets current selection and keep index if possible or set it to the first value
65
    /// available
66
    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
14✔
67
        self.choices = choices.into();
14✔
68
        // Keep index if possible
69
        if self.selected >= self.choices.len() {
14✔
70
            self.selected = match self.choices.len() {
2✔
71
                0 => 0,
1✔
72
                l => l - 1,
1✔
73
            };
74
        }
12✔
75
    }
14✔
76

77
    pub fn select(&mut self, i: usize) {
3✔
78
        if i < self.choices.len() {
3✔
79
            self.selected = i;
3✔
80
        }
3✔
81
    }
3✔
82

83
    /// ### close_tab
84
    ///
85
    /// Close tab
86
    pub fn close_tab(&mut self) {
6✔
87
        self.tab_open = false;
6✔
88
    }
6✔
89

90
    /// ### open_tab
91
    ///
92
    /// Open tab
93
    pub fn open_tab(&mut self) {
6✔
94
        self.previously_selected = self.selected;
6✔
95
        self.tab_open = true;
6✔
96
    }
6✔
97

98
    /// Cancel tab open
99
    pub fn cancel_tab(&mut self) {
1✔
100
        self.close_tab();
1✔
101
        self.selected = self.previously_selected;
1✔
102
    }
1✔
103

104
    /// ### is_tab_open
105
    ///
106
    /// Returns whether the tab is open
107
    #[must_use]
108
    pub fn is_tab_open(&self) -> bool {
23✔
109
        self.tab_open
23✔
110
    }
23✔
111
}
112

113
// -- component
114

115
#[derive(Default)]
116
#[must_use]
117
pub struct Select {
118
    props: Props,
119
    pub states: SelectStates,
120
}
121

122
impl Select {
123
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
124
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
125
        self
1✔
126
    }
1✔
127

128
    pub fn background(mut self, bg: Color) -> Self {
1✔
129
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
130
        self
1✔
131
    }
1✔
132

133
    pub fn borders(mut self, b: Borders) -> Self {
1✔
134
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
135
        self
1✔
136
    }
1✔
137

138
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
139
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
140
        self
1✔
141
    }
1✔
142

143
    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
1✔
144
        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
1✔
145
        self
1✔
146
    }
1✔
147

148
    pub fn highlighted_color(mut self, c: Color) -> Self {
1✔
149
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
1✔
150
        self
1✔
151
    }
1✔
152

153
    pub fn inactive(mut self, s: Style) -> Self {
×
154
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
155
        self
×
156
    }
×
157

158
    pub fn rewind(mut self, r: bool) -> Self {
1✔
159
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
1✔
160
        self
1✔
161
    }
1✔
162

163
    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
7✔
164
        self.attr(
7✔
165
            Attribute::Content,
7✔
166
            AttrValue::Payload(PropPayload::Vec(
167
                choices
7✔
168
                    .into_iter()
7✔
169
                    .map(|v| PropValue::Str(v.into()))
9✔
170
                    .collect(),
7✔
171
            )),
172
        );
173
        self
7✔
174
    }
7✔
175

176
    pub fn value(mut self, i: usize) -> Self {
1✔
177
        // Set state
178
        self.attr(
1✔
179
            Attribute::Value,
1✔
180
            AttrValue::Payload(PropPayload::One(PropValue::Usize(i))),
1✔
181
        );
182
        self
1✔
183
    }
1✔
184

185
    /// ### render_open_tab
186
    ///
187
    /// Render component when tab is open
188
    fn render_open_tab(&mut self, render: &mut Frame, area: Rect) {
×
189
        // Make choices
190
        let choices: Vec<ListItem> = self
×
191
            .states
×
192
            .choices
×
193
            .iter()
×
194
            .map(|x| ListItem::new(Spans::from(x.as_str())))
×
195
            .collect();
×
196
        let foreground = self
×
197
            .props
×
198
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
199
            .unwrap_color();
×
200
        let background = self
×
201
            .props
×
202
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
203
            .unwrap_color();
×
204
        let hg: Color = self
×
205
            .props
×
206
            .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground))
×
207
            .unwrap_color();
×
208
        // Prepare layout
209
        let chunks = Layout::default()
×
210
            .direction(LayoutDirection::Vertical)
×
211
            .margin(0)
×
212
            .constraints([Constraint::Length(2), Constraint::Min(1)].as_ref())
×
213
            .split(area);
×
214
        // Render like "closed" tab in chunk 0
215
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
216
            None => String::default(),
×
217
            Some(s) => s.clone(),
×
218
        };
219
        let focus = self
×
220
            .props
×
221
            .get_or(Attribute::Focus, AttrValue::Flag(false))
×
222
            .unwrap_flag();
×
223
        let inactive_style = self
×
224
            .props
×
225
            .get(Attribute::FocusStyle)
×
226
            .map(|x| x.unwrap_style());
×
227

NEW
228
        let normal_style = Style::default().bg(background).fg(foreground);
×
229

NEW
230
        let borders = self
×
NEW
231
            .props
×
NEW
232
            .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
NEW
233
            .unwrap_borders();
×
NEW
234
        let title = self
×
NEW
235
            .props
×
NEW
236
            .get_ref(Attribute::Title)
×
NEW
237
            .and_then(|x| x.as_title());
×
NEW
238
        let block_a = crate::utils::get_block(borders, title, focus, inactive_style)
×
NEW
239
            .borders(BorderSides::LEFT | BorderSides::TOP | BorderSides::RIGHT);
×
NEW
240
        let block_b = crate::utils::get_block::<&str>(borders, None, focus, inactive_style)
×
NEW
241
            .borders(BorderSides::LEFT | BorderSides::BOTTOM | BorderSides::RIGHT);
×
242

243
        let p: Paragraph = Paragraph::new(selected_text)
×
NEW
244
            .style(normal_style)
×
NEW
245
            .block(block_a);
×
UNCOV
246
        render.render_widget(p, chunks[0]);
×
247

248
        // Render the list of elements in chunks [1]
249
        // Make list
250
        let mut list = List::new(choices)
×
NEW
251
            .block(block_b)
×
252
            .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
×
NEW
253
            .style(normal_style)
×
254
            .highlight_style(
×
255
                Style::default()
×
256
                    .fg(hg)
×
257
                    .add_modifier(TextModifiers::REVERSED),
×
258
            );
259
        // Highlighted symbol
260
        let hg_str = self
×
261
            .props
×
262
            .get_ref(Attribute::HighlightedStr)
×
263
            .and_then(|x| x.as_string());
×
264
        if let Some(hg_str) = hg_str {
×
265
            list = list.highlight_symbol(hg_str);
×
266
        }
×
267
        let mut state: ListState = ListState::default();
×
268
        state.select(Some(self.states.selected));
×
269
        render.render_stateful_widget(list, chunks[1], &mut state);
×
270
    }
×
271

272
    /// ### render_closed_tab
273
    ///
274
    /// Render component when tab is closed
275
    fn render_closed_tab(&self, render: &mut Frame, area: Rect) {
×
276
        let foreground = self
×
277
            .props
×
278
            .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
279
            .unwrap_color();
×
280
        let background = self
×
281
            .props
×
282
            .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
283
            .unwrap_color();
×
284
        let inactive_style = self
×
285
            .props
×
286
            .get(Attribute::FocusStyle)
×
287
            .map(|x| x.unwrap_style());
×
288
        let focus = self
×
289
            .props
×
290
            .get_or(Attribute::Focus, AttrValue::Flag(false))
×
291
            .unwrap_flag();
×
292

NEW
293
        let normal_style = Style::default().bg(background).fg(foreground);
×
294

295
        let borders = self
×
296
            .props
×
297
            .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
298
            .unwrap_borders();
×
NEW
299
        let title = self
×
NEW
300
            .props
×
NEW
301
            .get_ref(Attribute::Title)
×
NEW
302
            .and_then(|x| x.as_title());
×
NEW
303
        let block = crate::utils::get_block(borders, title, focus, inactive_style);
×
304

305
        let selected_text: String = match self.states.choices.get(self.states.selected) {
×
306
            None => String::default(),
×
307
            Some(s) => s.clone(),
×
308
        };
NEW
309
        let p: Paragraph = Paragraph::new(selected_text)
×
NEW
310
            .style(normal_style)
×
NEW
311
            .block(block);
×
312
        render.render_widget(p, area);
×
313
    }
×
314

315
    fn rewindable(&self) -> bool {
8✔
316
        self.props
8✔
317
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
8✔
318
            .unwrap_flag()
8✔
319
    }
8✔
320
}
321

322
impl MockComponent for Select {
323
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
324
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
325
            if self.states.is_tab_open() {
×
326
                self.render_open_tab(render, area);
×
327
            } else {
×
328
                self.render_closed_tab(render, area);
×
329
            }
×
330
        }
×
331
    }
×
332

333
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
334
        self.props.get(attr)
×
335
    }
×
336

337
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
16✔
338
        match attr {
×
339
            Attribute::Content => {
340
                // Reset choices
341
                let choices: Vec<String> = value
7✔
342
                    .unwrap_payload()
7✔
343
                    .unwrap_vec()
7✔
344
                    .iter()
7✔
345
                    .map(|x| x.clone().unwrap_str())
9✔
346
                    .collect();
7✔
347
                self.states.set_choices(choices);
7✔
348
            }
349
            Attribute::Value => {
2✔
350
                self.states
2✔
351
                    .select(value.unwrap_payload().unwrap_one().unwrap_usize());
2✔
352
            }
2✔
353
            Attribute::Focus if self.states.is_tab_open() => {
×
354
                if let AttrValue::Flag(false) = value {
×
355
                    self.states.cancel_tab();
×
356
                }
×
357
                self.props.set(attr, value);
×
358
            }
359
            attr => {
7✔
360
                self.props.set(attr, value);
7✔
361
            }
7✔
362
        }
363
    }
16✔
364

365
    fn state(&self) -> State {
3✔
366
        if self.states.is_tab_open() {
3✔
367
            State::None
×
368
        } else {
369
            State::One(StateValue::Usize(self.states.selected))
3✔
370
        }
371
    }
3✔
372

373
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
11✔
374
        match cmd {
8✔
375
            Cmd::Move(Direction::Down) => {
376
                // Increment choice
377
                self.states.next_choice(self.rewindable());
4✔
378
                // Return CmdResult On Change or None if tab is closed
379
                if self.states.is_tab_open() {
4✔
380
                    CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
3✔
381
                } else {
382
                    CmdResult::None
1✔
383
                }
384
            }
385
            Cmd::Move(Direction::Up) => {
386
                // Increment choice
387
                self.states.prev_choice(self.rewindable());
4✔
388
                // Return CmdResult On Change or None if tab is closed
389
                if self.states.is_tab_open() {
4✔
390
                    CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
3✔
391
                } else {
392
                    CmdResult::None
1✔
393
                }
394
            }
395
            Cmd::Cancel => {
396
                self.states.cancel_tab();
×
397
                CmdResult::Changed(self.state())
×
398
            }
399
            Cmd::Submit => {
400
                // Open or close tab
401
                if self.states.is_tab_open() {
3✔
402
                    self.states.close_tab();
2✔
403
                    CmdResult::Submit(self.state())
2✔
404
                } else {
405
                    self.states.open_tab();
1✔
406
                    CmdResult::None
1✔
407
                }
408
            }
409
            _ => CmdResult::None,
×
410
        }
411
    }
11✔
412
}
413

414
#[cfg(test)]
415
mod test {
416

417
    use super::*;
418

419
    use pretty_assertions::assert_eq;
420

421
    use tuirealm::props::{PropPayload, PropValue};
422

423
    #[test]
424
    fn test_components_select_states() {
1✔
425
        let mut states: SelectStates = SelectStates::default();
1✔
426
        assert_eq!(states.selected, 0);
1✔
427
        assert_eq!(states.choices.len(), 0);
1✔
428
        assert_eq!(states.tab_open, false);
1✔
429
        let choices: &[String] = &[
1✔
430
            "lemon".to_string(),
1✔
431
            "strawberry".to_string(),
1✔
432
            "vanilla".to_string(),
1✔
433
            "chocolate".to_string(),
1✔
434
        ];
1✔
435
        states.set_choices(choices);
1✔
436
        assert_eq!(states.selected, 0);
1✔
437
        assert_eq!(states.choices.len(), 4);
1✔
438
        // Move
439
        states.prev_choice(false);
1✔
440
        assert_eq!(states.selected, 0);
1✔
441
        states.next_choice(false);
1✔
442
        // Tab is closed!!!
443
        assert_eq!(states.selected, 0);
1✔
444
        states.open_tab();
1✔
445
        assert_eq!(states.is_tab_open(), true);
1✔
446
        // Now we can move
447
        states.next_choice(false);
1✔
448
        assert_eq!(states.selected, 1);
1✔
449
        states.next_choice(false);
1✔
450
        assert_eq!(states.selected, 2);
1✔
451
        // Forward overflow
452
        states.next_choice(false);
1✔
453
        states.next_choice(false);
1✔
454
        assert_eq!(states.selected, 3);
1✔
455
        states.prev_choice(false);
1✔
456
        assert_eq!(states.selected, 2);
1✔
457
        // Close tab
458
        states.close_tab();
1✔
459
        assert_eq!(states.is_tab_open(), false);
1✔
460
        states.prev_choice(false);
1✔
461
        assert_eq!(states.selected, 2);
1✔
462
        // Update
463
        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
1✔
464
        states.set_choices(choices);
1✔
465
        assert_eq!(states.selected, 1); // Move to first index available
1✔
466
        assert_eq!(states.choices.len(), 2);
1✔
467
        let choices = vec![];
1✔
468
        states.set_choices(choices);
1✔
469
        assert_eq!(states.selected, 0); // Move to first index available
1✔
470
        assert_eq!(states.choices.len(), 0);
1✔
471
        // Rewind
472
        let choices: &[String] = &[
1✔
473
            "lemon".to_string(),
1✔
474
            "strawberry".to_string(),
1✔
475
            "vanilla".to_string(),
1✔
476
            "chocolate".to_string(),
1✔
477
        ];
1✔
478
        states.set_choices(choices);
1✔
479
        states.open_tab();
1✔
480
        assert_eq!(states.selected, 0);
1✔
481
        states.prev_choice(true);
1✔
482
        assert_eq!(states.selected, 3);
1✔
483
        states.next_choice(true);
1✔
484
        assert_eq!(states.selected, 0);
1✔
485
        states.next_choice(true);
1✔
486
        assert_eq!(states.selected, 1);
1✔
487
        states.prev_choice(true);
1✔
488
        assert_eq!(states.selected, 0);
1✔
489
        // Cancel tab
490
        states.close_tab();
1✔
491
        states.select(2);
1✔
492
        states.open_tab();
1✔
493
        states.prev_choice(true);
1✔
494
        states.prev_choice(true);
1✔
495
        assert_eq!(states.selected, 0);
1✔
496
        states.cancel_tab();
1✔
497
        assert_eq!(states.selected, 2);
1✔
498
        assert_eq!(states.is_tab_open(), false);
1✔
499
    }
1✔
500

501
    #[test]
502
    fn test_components_select() {
1✔
503
        // Make component
504
        let mut component = Select::default()
1✔
505
            .foreground(Color::Red)
1✔
506
            .background(Color::Black)
1✔
507
            .borders(Borders::default())
1✔
508
            .highlighted_color(Color::Red)
1✔
509
            .highlighted_str(">>")
1✔
510
            .title("C'est oui ou bien c'est non?", Alignment::Center)
1✔
511
            .choices(["Oui!", "Non", "Peut-ĂȘtre"])
1✔
512
            .value(1)
1✔
513
            .rewind(false);
1✔
514
        assert_eq!(component.states.is_tab_open(), false);
1✔
515
        component.states.open_tab();
1✔
516
        assert_eq!(component.states.is_tab_open(), true);
1✔
517
        component.states.close_tab();
1✔
518
        assert_eq!(component.states.is_tab_open(), false);
1✔
519
        // Update
520
        component.attr(
1✔
521
            Attribute::Value,
1✔
522
            AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
1✔
523
        );
524
        // Get value
525
        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
1✔
526
        // Open tab
527
        component.states.open_tab();
1✔
528
        // Events
529
        // Move cursor
530
        assert_eq!(
1✔
531
            component.perform(Cmd::Move(Direction::Up)),
1✔
532
            CmdResult::Changed(State::One(StateValue::Usize(1))),
533
        );
534
        assert_eq!(
1✔
535
            component.perform(Cmd::Move(Direction::Up)),
1✔
536
            CmdResult::Changed(State::One(StateValue::Usize(0))),
537
        );
538
        // Upper boundary
539
        assert_eq!(
1✔
540
            component.perform(Cmd::Move(Direction::Up)),
1✔
541
            CmdResult::Changed(State::One(StateValue::Usize(0))),
542
        );
543
        // Move down
544
        assert_eq!(
1✔
545
            component.perform(Cmd::Move(Direction::Down)),
1✔
546
            CmdResult::Changed(State::One(StateValue::Usize(1))),
547
        );
548
        assert_eq!(
1✔
549
            component.perform(Cmd::Move(Direction::Down)),
1✔
550
            CmdResult::Changed(State::One(StateValue::Usize(2))),
551
        );
552
        // Lower boundary
553
        assert_eq!(
1✔
554
            component.perform(Cmd::Move(Direction::Down)),
1✔
555
            CmdResult::Changed(State::One(StateValue::Usize(2))),
556
        );
557
        // Press enter
558
        assert_eq!(
1✔
559
            component.perform(Cmd::Submit),
1✔
560
            CmdResult::Submit(State::One(StateValue::Usize(2))),
561
        );
562
        // Tab should be closed
563
        assert_eq!(component.states.is_tab_open(), false);
1✔
564
        // Re open
565
        assert_eq!(component.perform(Cmd::Submit), CmdResult::None);
1✔
566
        assert_eq!(component.states.is_tab_open(), true);
1✔
567
        // Move arrows
568
        assert_eq!(
1✔
569
            component.perform(Cmd::Submit),
1✔
570
            CmdResult::Submit(State::One(StateValue::Usize(2))),
571
        );
572
        assert_eq!(component.states.is_tab_open(), false);
1✔
573
        assert_eq!(
1✔
574
            component.perform(Cmd::Move(Direction::Down)),
1✔
575
            CmdResult::None
576
        );
577
        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
1✔
578
    }
1✔
579

580
    #[test]
581
    fn various_set_choice_types() {
1✔
582
        // static array of strings
583
        SelectStates::default().set_choices(&["hello".to_string()]);
1✔
584
        // vector of strings
585
        SelectStates::default().set_choices(vec!["hello".to_string()]);
1✔
586
        // boxed array of strings
587
        SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
1✔
588
    }
1✔
589

590
    #[test]
591
    fn various_choice_types() {
1✔
592
        // static array of static strings
593
        let _ = Select::default().choices(["hello"]);
1✔
594
        // static array of strings
595
        let _ = Select::default().choices(["hello".to_string()]);
1✔
596
        // vec of static strings
597
        let _ = Select::default().choices(vec!["hello"]);
1✔
598
        // vec of strings
599
        let _ = Select::default().choices(vec!["hello".to_string()]);
1✔
600
        // boxed array of static strings
601
        let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
1✔
602
        // boxed array of strings
603
        let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
1✔
604
    }
1✔
605
}
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