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

IgniteUI / igniteui-angular / 15906632793

26 Jun 2025 03:55PM UTC coverage: 91.396% (-0.04%) from 91.437%
15906632793

Pull #15888

github

web-flow
Merge a700ed49c into cd00bb324
Pull Request #15888: fix(igxSplitter): Assign pane props after zone is stable.

13384 of 15716 branches covered (85.16%)

9 of 9 new or added lines in 2 files covered. (100.0%)

130 existing lines in 11 files now uncovered.

27065 of 29613 relevant lines covered (91.4%)

36691.64 hits per line

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

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

33
import { EditorProvider } from '../core/edit-provider';
34
import { IgxSelectionAPIService } from '../core/selection';
35
import { IBaseCancelableBrowserEventArgs, IBaseEventArgs } from '../core/utils';
36
import { IgxLabelDirective } from '../directives/label/label.directive';
37
import { IgxDropDownItemBaseDirective } from '../drop-down/drop-down-item.base';
38
import { IGX_DROPDOWN_BASE, ISelectionEventArgs, Navigate } from '../drop-down/drop-down.common';
39
import { IgxInputGroupComponent } from '../input-group/input-group.component';
40
import { AbsoluteScrollStrategy } from '../services/overlay/scroll/absolute-scroll-strategy';
41
import { OverlaySettings } from '../services/overlay/utilities';
42
import { IgxDropDownComponent } from './../drop-down/drop-down.component';
43
import { IgxSelectItemComponent } from './select-item.component';
44
import { SelectPositioningStrategy } from './select-positioning-strategy';
45
import { IgxSelectBase } from './select.common';
46
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
47
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from '../directives/toggle/toggle.directive';
48
import { IgxOverlayService } from '../services/overlay/overlay';
49
import { IgxIconComponent } from '../icon/icon.component';
50
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
51
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
52
import { IgxInputDirective, IgxInputState } from '../directives/input/input.directive';
53

54
/** @hidden @internal */
55
@Directive({
56
    selector: '[igxSelectToggleIcon]',
57
    standalone: true
58
})
59
export class IgxSelectToggleIconDirective {
3✔
60
}
61

62
/** @hidden @internal */
63
@Directive({
64
    selector: '[igxSelectHeader]',
65
    standalone: true
66
})
67
export class IgxSelectHeaderDirective {
3✔
68
}
69

70
/** @hidden @internal */
71
@Directive({
72
    selector: '[igxSelectFooter]',
73
    standalone: true
74
})
75
export class IgxSelectFooterDirective {
3✔
76
}
77

78
/**
79
 * **Ignite UI for Angular Select** -
80
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
81
 *
82
 * The `igxSelect` provides an input with dropdown list allowing selection of a single item.
83
 *
84
 * Example:
85
 * ```html
86
 * <igx-select #select1 [placeholder]="'Pick One'">
87
 *   <label igxLabel>Select Label</label>
88
 *   <igx-select-item *ngFor="let item of items" [value]="item.field">
89
 *     {{ item.field }}
90
 *   </igx-select-item>
91
 * </igx-select>
92
 * ```
93
 */
94
@Component({
95
    selector: 'igx-select',
96
    templateUrl: './select.component.html',
97
    providers: [
98
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true },
99
        { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }
100
    ],
101
    styles: [`
102
        :host {
103
            display: block;
104
        }
105
    `],
106
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
107
})
108
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
3✔
109
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
110

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

114
    /** @hidden @internal */
115
    @ViewChild('input', { read: IgxInputDirective, static: true }) public input: IgxInputDirective;
116

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

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

124
    @ContentChildren(IgxSuffixDirective, { descendants: true })
125
    protected suffixes: QueryList<IgxSuffixDirective>;
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
    /**
141
     * Disables the component.
142
     * ```html
143
     * <igx-select [disabled]="'true'"></igx-select>
144
     * ```
145
     */
146
    @Input({ transform: booleanAttribute }) public disabled = false;
1,120✔
147

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

157
    /** @hidden @internal */
158
    @HostBinding('style.maxHeight')
159
    public override maxHeight = '256px';
1,120✔
160

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

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

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

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

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

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

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

268
    @ContentChild(IgxHintDirective, { read: ElementRef }) private hintElement: ElementRef;
269

270
    /** @hidden @internal */
271
    public override width: string;
272

273
    /** @hidden @internal do not use the drop-down container class */
274
    public override cssClass = false;
1,120✔
275

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

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

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

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

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

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

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

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

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

345
    constructor(
346
        elementRef: ElementRef,
347
        cdr: ChangeDetectorRef,
348
        @Inject(DOCUMENT) document: any,
349
        selection: IgxSelectionAPIService,
350
        @Inject(IgxOverlayService) protected overlayService: IgxOverlayService,
1,120✔
351
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) private _inputGroupType: IgxInputGroupType,
1,120✔
352
        private _injector: Injector,
1,120✔
353
    ) {
354
        super(elementRef, cdr, document, selection);
1,120✔
355
    }
356

357
    //#region ControlValueAccessor
358

359
    /** @hidden @internal */
360
    public writeValue = (value: any) => {
1,120✔
361
        this.value = value;
2,407✔
362
    };
363

364
    /** @hidden @internal */
365
    public registerOnChange(fn: any): void {
366
        this._onChangeCallback = fn;
1,039✔
367
    }
368

369
    /** @hidden @internal */
370
    public registerOnTouched(fn: any): void {
371
        this._onTouchedCallback = fn;
1,039✔
372
    }
373

374
    /** @hidden @internal */
375
    public setDisabledState(isDisabled: boolean): void {
376
        this.disabled = isDisabled;
1,195✔
377
    }
378
    //#endregion
379

380
    /** @hidden @internal */
381
    public getEditElement(): HTMLInputElement {
382
        return this.input.nativeElement;
1,120✔
383
    }
384

385
    /** @hidden @internal */
386
    public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
387
        const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
393✔
388

389
        if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
393✔
390
            return;
2✔
391
        }
392

393
        if (newSelection === oldSelection) {
391✔
394
            this.toggleDirective.close();
23✔
395
            return;
23✔
396
        }
397

398
        const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner: this };
368✔
399
        this.selectionChanging.emit(args);
368✔
400

401
        if (args.cancel) {
368✔
402
            return;
110✔
403
        }
404

405
        this.setSelection(newSelection);
258✔
406
        this._value = newSelection.value;
258✔
407

408
        if (event) {
258✔
409
            this.toggleDirective.close();
111✔
410
        }
411

412
        this.cdr.detectChanges();
258✔
413
        this._onChangeCallback(this.value);
258✔
414
    }
415

416
    /** @hidden @internal */
417
    public getFirstItemElement(): HTMLElement {
418
        return this.children.first.element.nativeElement;
83✔
419
    }
420

421
    /**
422
     * Opens the select
423
     *
424
     * ```typescript
425
     * this.select.open();
426
     * ```
427
     */
428
    public override open(overlaySettings?: OverlaySettings) {
429
        if (this.disabled || this.items.length === 0) {
302✔
430
            return;
3✔
431
        }
432
        if (!this.selectedItem) {
299✔
433
            this.navigateFirst();
208✔
434
        }
435

436
        super.open(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
299✔
437
    }
438

439
    public inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
440
        const targetElement = event.target as HTMLElement;
233✔
441

442
        if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
233!
UNCOV
443
            return;
×
444
        }
445
        this.toggle(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
233✔
446
    }
447

448
    /** @hidden @internal */
449
    public ngAfterContentInit() {
450
        this._overlayDefaults = {
1,119✔
451
            target: this.getEditElement(),
452
            modal: false,
453
            positionStrategy: new SelectPositioningStrategy(this),
454
            scrollStrategy: new AbsoluteScrollStrategy(),
455
            excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement]
456
        };
457
        const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
1,119✔
458
            this.setSelection(this.items.find(x => x.value === this.value));
3,201✔
459
            this.cdr.detectChanges();
850✔
460
        });
461
        Promise.resolve().then(() => {
1,119✔
462
            if (!changes$.closed) {
1,119✔
463
                this.children.notifyOnChanges();
810✔
464
            }
465
        });
466
    }
467

468
    /**
469
     * Event handlers
470
     *
471
     * @hidden @internal
472
     */
473
    public handleOpening(e: ToggleViewCancelableEventArgs) {
474
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
299✔
475
        this.opening.emit(args);
299✔
476

477
        e.cancel = args.cancel;
299✔
478
        if (args.cancel) {
299!
UNCOV
479
            return;
×
480
        }
481
    }
482

483
    /** @hidden @internal */
484
    public override onToggleContentAppended(event: ToggleViewEventArgs) {
485
        const info = this.overlayService.getOverlayById(event.id);
298✔
486
        if (info?.settings?.positionStrategy instanceof SelectPositioningStrategy) {
298!
UNCOV
487
            return;
×
488
        }
489
        super.onToggleContentAppended(event);
298✔
490
    }
491

492
    /** @hidden @internal */
493
    public handleOpened() {
494
        this.updateItemFocus();
291✔
495
        this.opened.emit({ owner: this });
291✔
496
    }
497

498
    /** @hidden @internal */
499
    public handleClosing(e: ToggleViewCancelableEventArgs) {
500
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
253✔
501
        this.closing.emit(args);
253✔
502
        e.cancel = args.cancel;
253✔
503
    }
504

505
    /** @hidden @internal */
506
    public handleClosed() {
507
        this.focusItem(false);
250✔
508
        this.closed.emit({ owner: this });
250✔
509
    }
510

511
    /** @hidden @internal */
512
    public onBlur(): void {
513
        this._onTouchedCallback();
164✔
514
        if (this.ngControl && this.ngControl.invalid) {
164✔
515
            this.input.valid = IgxInputState.INVALID;
3✔
516
        } else {
517
            this.input.valid = IgxInputState.INITIAL;
161✔
518
        }
519
    }
520

521
    /** @hidden @internal */
522
    public onFocus(): void {
523
        this._onTouchedCallback();
233✔
524
    }
525

526
    /**
527
     * @hidden @internal
528
     */
529
    public override ngOnInit() {
530
        this.ngControl = this._injector.get<NgControl>(NgControl, null);
1,120✔
531
    }
532

533
    /**
534
     * @hidden @internal
535
     */
536
    public override ngAfterViewInit() {
537
        super.ngAfterViewInit();
1,119✔
538

539
        if (this.ngControl) {
1,119✔
540
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
1,034✔
541
            this.manageRequiredAsterisk();
1,034✔
542
        }
543

544
        this.cdr.detectChanges();
1,119✔
545
    }
546

547
    /** @hidden @internal */
548
    public ngAfterContentChecked() {
549
        if (this.inputGroup && this.prefixes?.length > 0) {
13,897✔
550
            this.inputGroup.prefixes = this.prefixes;
721✔
551
        }
552

553
        if (this.inputGroup) {
13,897✔
554
            const suffixesArray = this.suffixes?.toArray() ?? [];
13,897!
555
            const internalSuffixesArray = this.internalSuffixes?.toArray() ?? [];
13,897✔
556
            const mergedSuffixes = new QueryList<IgxSuffixDirective>();
13,897✔
557
            mergedSuffixes.reset([
13,897✔
558
                ...suffixesArray,
559
                ...internalSuffixesArray
560
            ]);
561
            this.inputGroup.suffixes = mergedSuffixes;
13,897✔
562
        }
563
    }
564

565
    /** @hidden @internal */
566
    public get toggleIcon(): string {
567
        return this.collapsed ? 'input_expand' : 'input_collapse';
29,345✔
568
    }
569

570
    /**
571
     * @hidden @internal
572
     * Prevent input blur - closing the items container on Header/Footer Template click.
573
     */
574
    public mousedownHandler(event) {
UNCOV
575
        event.preventDefault();
×
576
    }
577

578
    protected onStatusChanged() {
579
        this.manageRequiredAsterisk();
1,377✔
580
        if (this.ngControl && !this.disabled && this.isTouchedOrDirty) {
1,377✔
581
            if (this.hasValidators && this.inputGroup.isFocused) {
517✔
582
                this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
3✔
583
            } else {
584
                // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
585
                this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
514✔
586
            }
587
        } else {
588
            this.input.valid = IgxInputState.INITIAL;
860✔
589
        }
590
    }
591

592
    private get isTouchedOrDirty(): boolean {
593
        return (this.ngControl.control.touched || this.ngControl.control.dirty);
1,284✔
594
    }
595

596
    private get hasValidators(): boolean {
597
        return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
517✔
598
    }
599

600
    protected override navigate(direction: Navigate, currentIndex?: number) {
601
        if (this.collapsed && this.selectedItem) {
321✔
602
            this.navigateItem(this.selectedItem.itemIndex);
41✔
603
        }
604
        super.navigate(direction, currentIndex);
321✔
605
    }
606

607
    protected manageRequiredAsterisk(): void {
608
        const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
2,411✔
609
        if (this.ngControl && this.ngControl.control.validator) {
2,411✔
610
            // Run the validation with empty object to check if required is enabled.
611
            const error = this.ngControl.control.validator({} as AbstractControl);
18✔
612
            this.inputGroup.isRequired = error && error.required;
18✔
613
            this.cdr.markForCheck();
18✔
614

615
            // If validator is dynamically cleared and no required HTML attribute is set,
616
            // reset label's required class(asterisk) and IgxInputState #6896
617
        } else if (this.inputGroup.isRequired && this.ngControl && !this.ngControl.control.validator && !hasRequiredHTMLAttribute) {
2,393✔
618
            this.input.valid = IgxInputState.INITIAL;
2✔
619
            this.inputGroup.isRequired = false;
2✔
620
            this.cdr.markForCheck();
2✔
621
        }
622
    }
623

624
    private setSelection(item: IgxDropDownItemBaseDirective) {
625
        if (item && item.value !== undefined && item.value !== null) {
3,227✔
626
            this.selection.set(this.id, new Set([item]));
1,714✔
627
        } else {
628
            this.selection.clear(this.id);
1,513✔
629
        }
630
    }
631
}
632

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

© 2025 Coveralls, Inc