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

IgniteUI / igniteui-angular / 29587398373

17 Jul 2026 02:17PM UTC coverage: 90.116% (-0.02%) from 90.135%
29587398373

Pull #15125

github

web-flow
Merge ac93ee972 into 8ebabbb38
Pull Request #15125: refactor(*): bundle styles with components

14920 of 17397 branches covered (85.76%)

Branch coverage included in aggregate %.

30030 of 32483 relevant lines covered (92.45%)

34494.72 hits per line

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

95.41
/projects/igniteui-angular/select/src/select/select.component.ts
1
import {
2
    AfterContentChecked,
3
    AfterContentInit,
4
    AfterViewInit,
5
    booleanAttribute,
6
    Component,
7
    ContentChild,
8
    ContentChildren,
9
    Directive,
10
    ElementRef,
11
    EventEmitter,
12
    forwardRef,
13
    HostBinding,
14
    Injector,
15
    Input,
16
    OnDestroy,
17
    OnInit,
18
    Output,
19
    QueryList,
20
    TemplateRef,
21
    ViewChild,
22
    ViewChildren,
23
    inject,
24
    ChangeDetectionStrategy,
25
    ViewEncapsulation
26
} from '@angular/core';
27
import { NgTemplateOutlet } from '@angular/common';
28
import { AbstractControl, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
29
import { noop } from 'rxjs';
30
import { takeUntil } from 'rxjs/operators';
31

32
import {
33
    EditorProvider,
34
    IBaseCancelableBrowserEventArgs,
35
    IBaseEventArgs,
36
    AbsoluteScrollStrategy,
37
    AutoPositionStrategy,
38
    OverlaySettings
39
} from 'igniteui-angular/core';
40
import { IgxSelectItemComponent } from './select-item.component';
41
import { IgxSelectBase } from './select.common';
42
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE, IgxInputGroupComponent, IgxInputDirective, IgxInputState, IgxLabelDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
43
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from 'igniteui-angular/directives';
44
import { IgxOverlayService } from 'igniteui-angular/core';
45
import { IgxIconComponent } from 'igniteui-angular/icon';
46
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
47
import { IGX_DROPDOWN_BASE, IgxDropDownComponent, IgxDropDownItemBaseDirective, ISelectionEventArgs, Navigate } from 'igniteui-angular/drop-down';
48

49
/** @hidden @internal */
50
@Directive({
51
    selector: '[igxSelectToggleIcon]',
52
    standalone: true
53
})
54
export class IgxSelectToggleIconDirective {
3✔
55
}
56

57
/** @hidden @internal */
58
@Directive({
59
    selector: '[igxSelectHeader]',
60
    standalone: true
61
})
62
export class IgxSelectHeaderDirective {
3✔
63
}
64

65
/** @hidden @internal */
66
@Directive({
67
    selector: '[igxSelectFooter]',
68
    standalone: true
69
})
70
export class IgxSelectFooterDirective {
3✔
71
}
72

73
/**
74
 * **Ignite UI for Angular Select** -
75
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
76
 *
77
 * The `igxSelect` provides an input with dropdown list allowing selection of a single item.
78
 *
79
 * Example:
80
 * ```html
81
 * <igx-select #select1 [placeholder]="'Pick One'">
82
 *   <label igxLabel>Select Label</label>
83
 *   <igx-select-item *ngFor="let item of items" [value]="item.field">
84
 *     {{ item.field }}
85
 *   </igx-select-item>
86
 * </igx-select>
87
 * ```
88
 */
89
@Component({
90
    selector: 'igx-select',
91
    templateUrl: './select.component.html',
92
    providers: [
93
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true },
94
        { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }
95
    ],
96
    styleUrls: ['../../../drop-down/src/drop-down/drop-down.component.css', 'select.component.css'],
97
    encapsulation: ViewEncapsulation.None,
98
    changeDetection: ChangeDetectionStrategy.Eager,
99
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, IgxReadOnlyInputDirective, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
100
})
101
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
3✔
102
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
103
    protected overlayService = inject<IgxOverlayService>(IgxOverlayService);
1,191✔
104
    private _inputGroupType = inject<IgxInputGroupType>(IGX_INPUT_GROUP_TYPE, { optional: true });
1,191✔
105
    private _injector = inject(Injector);
1,191✔
106

107

108
    /** @hidden @internal */
109
    @ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;
110

111
    /** @hidden @internal */
112
    @ViewChild('input', { read: IgxInputDirective, static: true }) public input: IgxInputDirective;
113

114
    /** @hidden @internal */
115
    @ContentChildren(forwardRef(() => IgxSelectItemComponent), { descendants: true })
6✔
116
    public override children: QueryList<IgxSelectItemComponent>;
117

118
    @ContentChildren(IgxPrefixDirective, { descendants: true })
119
    protected prefixes: QueryList<IgxPrefixDirective>;
120

121
    @ContentChildren(IgxSuffixDirective, { descendants: true })
122
    protected suffixes: QueryList<IgxSuffixDirective>;
123

124
    @ContentChildren(IgxHintDirective, { descendants: true })
125
    protected contentHints: QueryList<IgxHintDirective>;
126

127
    @ViewChildren(IgxSuffixDirective)
128
    protected internalSuffixes: QueryList<IgxSuffixDirective>;
129

130
    /** @hidden @internal */
131
    @ContentChild(forwardRef(() => IgxLabelDirective), { static: true }) public label: IgxLabelDirective;
6✔
132

133
    /**
134
     * Sets input placeholder.
135
     *
136
     */
137
    @Input() public placeholder;
138

139
    /**
140
     * Disables the component.
141
     * ```html
142
     * <igx-select [disabled]="'true'"></igx-select>
143
     * ```
144
     */
145
    @Input({ transform: booleanAttribute }) public disabled = false;
1,191✔
146

147
    /**
148
     * Sets custom overlay settings for the select component.
149
     * ```html
150
     * <igx-select [overlaySettings]="customOverlaySettings"></igx-select>
151
     * ```
152
     */
153
    @Input()
154
    public overlaySettings: OverlaySettings;
155

156
    @HostBinding('class.igx-select')
157
    public defaultClass = true;
1,191✔
158

159
    /** @hidden @internal */
160
    @HostBinding('style.maxHeight')
161
    public override maxHeight = '256px';
1,191✔
162

163
    /**
164
     * Emitted before the dropdown is opened
165
     *
166
     * ```html
167
     * <igx-select opening='handleOpening($event)'></igx-select>
168
     * ```
169
     */
170
    @Output()
171
    public override opening = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,191✔
172

173
    /**
174
     * Emitted after the dropdown is opened
175
     *
176
     * ```html
177
     * <igx-select (opened)='handleOpened($event)'></igx-select>
178
     * ```
179
     */
180
    @Output()
181
    public override opened = new EventEmitter<IBaseEventArgs>();
1,191✔
182

183
    /**
184
     * Emitted before the dropdown is closed
185
     *
186
     * ```html
187
     * <igx-select (closing)='handleClosing($event)'></igx-select>
188
     * ```
189
     */
190
    @Output()
191
    public override closing = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,191✔
192

193
    /**
194
     * Emitted after the dropdown is closed
195
     *
196
     * ```html
197
     * <igx-select (closed)='handleClosed($event)'></igx-select>
198
     * ```
199
     */
200
    @Output()
201
    public override closed = new EventEmitter<IBaseEventArgs>();
1,191✔
202

203
    /**
204
     * The custom template, if any, that should be used when rendering the select TOGGLE(open/close) button
205
     *
206
     * ```typescript
207
     * // Set in typescript
208
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
209
     * myComponent.select.toggleIconTemplate = myCustomTemplate;
210
     * ```
211
     * ```html
212
     * <!-- Set in markup -->
213
     *  <igx-select #select>
214
     *      ...
215
     *      <ng-template igxSelectToggleIcon let-collapsed>
216
     *          <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
217
     *      </ng-template>
218
     *  </igx-select>
219
     * ```
220
     */
221
    @ContentChild(IgxSelectToggleIconDirective, { read: TemplateRef })
222
    public toggleIconTemplate: TemplateRef<any> = null;
1,191✔
223

224
    /**
225
     * The custom template, if any, that should be used when rendering the HEADER for the select items list
226
     *
227
     * ```typescript
228
     * // Set in typescript
229
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
230
     * myComponent.select.headerTemplate = myCustomTemplate;
231
     * ```
232
     * ```html
233
     * <!-- Set in markup -->
234
     *  <igx-select #select>
235
     *      ...
236
     *      <ng-template igxSelectHeader>
237
     *          <div class="select__header">
238
     *              This is a custom header
239
     *          </div>
240
     *      </ng-template>
241
     *  </igx-select>
242
     * ```
243
     */
244
    @ContentChild(IgxSelectHeaderDirective, { read: TemplateRef, static: false })
245
    public headerTemplate: TemplateRef<any> = null;
1,191✔
246

247
    /**
248
     * The custom template, if any, that should be used when rendering the FOOTER for the select items list
249
     *
250
     * ```typescript
251
     * // Set in typescript
252
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
253
     * myComponent.select.footerTemplate = myCustomTemplate;
254
     * ```
255
     * ```html
256
     * <!-- Set in markup -->
257
     *  <igx-select #select>
258
     *      ...
259
     *      <ng-template igxSelectFooter>
260
     *          <div class="select__footer">
261
     *              This is a custom footer
262
     *          </div>
263
     *      </ng-template>
264
     *  </igx-select>
265
     * ```
266
     */
267
    @ContentChild(IgxSelectFooterDirective, { read: TemplateRef, static: false })
268
    public footerTemplate: TemplateRef<any> = null;
1,191✔
269

270
    @ContentChild(IgxHintDirective, { read: ElementRef }) private hintElement: ElementRef;
271

272
    /** @hidden @internal */
273
    public override width: string;
274

275
    /** @hidden @internal */
276
    public override allowItemsFocus = false;
1,191✔
277

278
    /** @hidden @internal */
279
    public override height: string;
280

281
    private ngControl: NgControl = null;
1,191✔
282
    private _overlayDefaults: OverlaySettings;
283
    private _value: any;
284
    private _type = null;
1,191✔
285

286
    /**
287
     * Gets/Sets the component value.
288
     *
289
     * ```typescript
290
     * // get
291
     * let selectValue = this.select.value;
292
     * ```
293
     *
294
     * ```typescript
295
     * // set
296
     * this.select.value = 'London';
297
     * ```
298
     * ```html
299
     * <igx-select [value]="value"></igx-select>
300
     * ```
301
     */
302
    @Input()
303
    public get value(): any {
304
        return this._value;
9,845✔
305
    }
306
    public set value(v: any) {
307
        if (this._value === v) {
2,546✔
308
            return;
311✔
309
        }
310
        this._value = v;
2,235✔
311
        this.setSelection(this.items.find(x => x.value === this.value));
3,313✔
312
    }
313

314
    /**
315
     * Sets how the select will be styled.
316
     * The allowed values are `line`, `box` and `border`. Defaults to `box` if no input-group type is set.
317
     * ```html
318
     * <igx-select [type]="'border'"></igx-select>
319
     * ```
320
     */
321
    @Input()
322
    public get type(): IgxInputGroupType {
323
        return this._type || this._inputGroupType || 'box';
59,637✔
324
    }
325

326
    public set type(val: IgxInputGroupType) {
327
        this._type = val;
1,090✔
328
    }
329

330
    /** @hidden @internal */
331
    public get selectionValue() {
332
        const selectedItem = this.selectedItem;
29,817✔
333
        return selectedItem ? selectedItem.itemText : '';
29,817✔
334
    }
335

336
    /** @hidden @internal */
337
    public override get selectedItem(): IgxSelectItemComponent {
338
        return this.selection.first_item(this.id);
31,862✔
339
    }
340

341
    private _onChangeCallback: (_: any) => void = noop;
1,191✔
342
    private _onTouchedCallback: () => void = noop;
1,191✔
343

344
    //#region ControlValueAccessor
345

346
    /** @hidden @internal */
347
    public writeValue = (value: any) => {
1,191✔
348
        this.value = value;
2,530✔
349
    };
350

351
    /** @hidden @internal */
352
    public registerOnChange(fn: any): void {
353
        this._onChangeCallback = fn;
1,102✔
354
    }
355

356
    /** @hidden @internal */
357
    public registerOnTouched(fn: any): void {
358
        this._onTouchedCallback = fn;
1,102✔
359
    }
360

361
    /** @hidden @internal */
362
    public setDisabledState(isDisabled: boolean): void {
363
        this.disabled = isDisabled;
1,260✔
364
    }
365
    //#endregion
366

367
    /** @hidden @internal */
368
    public getEditElement(): HTMLInputElement {
369
        return this.input.nativeElement;
871✔
370
    }
371

372
    /** @hidden @internal */
373
    public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
374
        const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
399✔
375

376
        if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
399✔
377
            return;
2✔
378
        }
379

380
        if (newSelection === oldSelection) {
397✔
381
            this.toggleDirective.close();
24✔
382
            return;
24✔
383
        }
384

385
        const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner: this };
373✔
386
        this.selectionChanging.emit(args);
373✔
387

388
        if (args.cancel) {
373✔
389
            return;
111✔
390
        }
391

392
        this.setSelection(newSelection);
262✔
393
        this._value = newSelection.value;
262✔
394

395
        if (event) {
262✔
396
            this.toggleDirective.close();
111✔
397
        }
398

399
        this.cdr.detectChanges();
262✔
400
        this._onChangeCallback(this.value);
262✔
401
    }
402

403
    /** @hidden @internal */
404
    public getFirstItemElement(): HTMLElement {
405
        return this.children.first.element.nativeElement;
1✔
406
    }
407

408
    /**
409
     * Opens the select
410
     *
411
     * ```typescript
412
     * this.select.open();
413
     * ```
414
     */
415
    public override open(overlaySettings?: OverlaySettings) {
416
        if (this.disabled || this.items.length === 0) {
309✔
417
            return;
3✔
418
        }
419

420
        if (!this.selectedItem) {
306✔
421
            this.navigateFirst();
214✔
422
        }
423

424
        super.open(this.getMergedOverlaySettings(overlaySettings));
306✔
425
    }
426

427
    protected inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
428
        const targetElement = event.target as HTMLElement;
234✔
429

430
        if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
234!
431
            return;
×
432
        }
433
        this.toggle(this.getMergedOverlaySettings(overlaySettings));
234✔
434
    }
435

436
    /** @hidden @internal */
437
    public ngAfterContentInit() {
438
        const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
1,190✔
439
            this.setSelection(this.items.find(x => x.value === this.value));
3,273✔
440
            this.cdr.detectChanges();
864✔
441
        });
442
        Promise.resolve().then(() => {
1,190✔
443
            if (!changes$.closed) {
1,190✔
444
                this.children.notifyOnChanges();
828✔
445
            }
446
        });
447
    }
448

449
    /**
450
     * Event handlers
451
     *
452
     * @hidden @internal
453
     */
454
    public handleOpening(e: ToggleViewCancelableEventArgs) {
455
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
306✔
456
        this.opening.emit(args);
306✔
457

458
        e.cancel = args.cancel;
306✔
459
        if (args.cancel) {
306!
460
            return;
×
461
        }
462
    }
463

464
    /** @hidden @internal */
465
    public override onToggleContentAppended(event: ToggleViewEventArgs) {
466
        const info = this.overlayService.getOverlayById(event.id);
301✔
467
        if ((info?.settings?.positionStrategy as { isItemOverlapPositioning?: boolean })?.isItemOverlapPositioning) {
301!
468
            return;
×
469
        }
470
        super.onToggleContentAppended(event);
301✔
471
    }
472

473
    /** @hidden @internal */
474
    public handleOpened() {
475
        this.updateItemFocus();
294✔
476
        this.opened.emit({ owner: this });
294✔
477
    }
478

479
    /** @hidden @internal */
480
    public handleClosing(e: ToggleViewCancelableEventArgs) {
481
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
255✔
482
        this.closing.emit(args);
255✔
483
        e.cancel = args.cancel;
255✔
484
    }
485

486
    /** @hidden @internal */
487
    public handleClosed() {
488
        this.focusItem(false);
252✔
489
        this.closed.emit({ owner: this });
252✔
490
    }
491

492
    /** @hidden @internal */
493
    public onBlur(): void {
494
        this._onTouchedCallback();
166✔
495
        if (this.ngControl && this.ngControl.invalid) {
166✔
496
            this.input.valid = IgxInputState.INVALID;
3✔
497
        } else {
498
            this.input.valid = IgxInputState.INITIAL;
163✔
499
        }
500
    }
501

502
    /** @hidden @internal */
503
    public onFocus(): void {
504
        this._onTouchedCallback();
234✔
505
    }
506

507
    /**
508
     * @hidden @internal
509
     */
510
    public override ngOnInit() {
511
        this.ngControl = this._injector.get<NgControl>(NgControl, null);
1,191✔
512
    }
513

514
    /**
515
     * @hidden @internal
516
     */
517
    public override ngAfterViewInit() {
518
        super.ngAfterViewInit();
1,190✔
519

520
        if (this.ngControl) {
1,190✔
521
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
1,096✔
522
            this.manageRequiredAsterisk();
1,096✔
523
        }
524

525
        const targetElement = this.inputGroup.element.nativeElement.querySelector('.igx-input-group__bundle') as HTMLElement;
1,190✔
526
        this._overlayDefaults = {
1,190✔
527
            target: targetElement,
528
            modal: false,
529
            positionStrategy: new AutoPositionStrategy(),
530
            scrollStrategy: new AbsoluteScrollStrategy(),
531
            excludeFromOutsideClick: [targetElement]
532
        };
533

534
        this.cdr.detectChanges();
1,190✔
535
    }
536

537
    /** @hidden @internal */
538
    public ngAfterContentChecked() {
539
        if (this.inputGroup && this.prefixes?.length > 0) {
14,082✔
540
            this.inputGroup.prefixes = this.prefixes;
769✔
541
        }
542

543
        if (this.inputGroup) {
14,082✔
544
            const suffixesArray = this.suffixes?.toArray() ?? [];
14,082!
545
            const internalSuffixesArray = this.internalSuffixes?.toArray() ?? [];
14,082✔
546
            const mergedSuffixes = new QueryList<IgxSuffixDirective>();
14,082✔
547
            mergedSuffixes.reset([
14,082✔
548
                ...suffixesArray,
549
                ...internalSuffixesArray
550
            ]);
551
            this.inputGroup.suffixes = mergedSuffixes;
14,082✔
552
        }
553

554
        if (this.inputGroup) {
14,082✔
555
            this.inputGroup.hints = this.contentHints;
14,082✔
556
        }
557
    }
558

559
    /** @hidden @internal */
560
    public get toggleIcon(): string {
561
        return this.collapsed ? 'input_expand' : 'input_collapse';
29,817✔
562
    }
563

564
    /**
565
     * @hidden @internal
566
     * Prevent input blur - closing the items container on Header/Footer Template click.
567
     */
568
    public mousedownHandler(event) {
569
        event.preventDefault();
×
570
    }
571

572
    protected onStatusChanged() {
573
        this.manageRequiredAsterisk();
1,387✔
574

575
        if (this.ngControl && !this.ngControl.disabled && this.isTouchedOrDirty) {
1,387✔
576
            if (this.hasValidators && this.inputGroup.isFocused) {
516✔
577
                this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
1!
578
            } else {
579
                // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
580
                this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
515✔
581
            }
582
        } else {
583
            this.input.valid = IgxInputState.INITIAL;
871✔
584
        }
585
    }
586

587
    private get isTouchedOrDirty(): boolean {
588
        return (this.ngControl.control.touched || this.ngControl.control.dirty);
1,197✔
589
    }
590

591
    private get hasValidators(): boolean {
592
        return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
516✔
593
    }
594

595
    protected override navigate(direction: Navigate, currentIndex?: number) {
596
        if (this.collapsed && this.selectedItem) {
327✔
597
            this.navigateItem(this.selectedItem.itemIndex);
41✔
598
        }
599
        super.navigate(direction, currentIndex);
327✔
600
    }
601

602
    protected manageRequiredAsterisk(): void {
603
        const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
2,483✔
604
        let isRequired = false;
2,483✔
605

606
        if (this.ngControl && this.ngControl.control.validator) {
2,483✔
607
            const error = this.ngControl.control.validator({} as AbstractControl);
20✔
608
            isRequired = !!(error && error.required);
20✔
609
        }
610

611
        this.inputGroup.isRequired = isRequired;
2,483✔
612

613
        if (this.input?.nativeElement) {
2,483✔
614
            this.input.nativeElement.setAttribute('aria-required', isRequired.toString());
2,483✔
615
        }
616

617
        // Handle validator removal case
618
        if (!isRequired && !hasRequiredHTMLAttribute) {
2,483✔
619
            this.input.valid = IgxInputState.INITIAL;
2,459✔
620
        }
621

622
        this.cdr.markForCheck();
2,483✔
623
    }
624

625
    private getMergedOverlaySettings(overlaySettings?: OverlaySettings): OverlaySettings {
626
        const merged = Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings);
542✔
627
        if ((merged.positionStrategy as { isItemOverlapPositioning?: boolean })?.isItemOverlapPositioning) {
542✔
628
            merged.target = this.getEditElement();
20✔
629
        }
630
        return merged;
542✔
631
    }
632

633
    private setSelection(item: IgxDropDownItemBaseDirective) {
634
        if (item && item.value !== undefined && item.value !== null) {
3,361✔
635
            this.selection.set(this.id, new Set([item]));
1,777✔
636
        } else {
637
            this.selection.clear(this.id);
1,584✔
638
        }
639
    }
640
}
641

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