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

NationalBankBelgium / stark / 9205123826

23 May 2024 08:55AM CUT coverage: 88.822%. Remained the same
9205123826

Pull #3800

github

web-flow
Merge 5208cd19f into 59c39b170
Pull Request #3800: chore(deps): bump zone.js from 0.11.8 to 0.14.6 in /showcase

1260 of 1528 branches covered (82.46%)

Branch coverage included in aggregate %.

3770 of 4135 relevant lines covered (91.17%)

193.16 hits per line

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

34.97
/packages/stark-ui/src/modules/generic-search/components/generic-search/generic-search.component.ts
1
import {
2
        AfterContentInit,
3
        Component,
4
        ContentChild,
5
        ElementRef,
6
        EventEmitter,
7
        Inject,
8
        Input,
9
        OnChanges,
10
        OnInit,
11
        Output,
12
        Renderer2,
13
        SimpleChanges,
14
        ViewEncapsulation
15
} from "@angular/core";
16
import { StarkSearchFormComponent } from "../../classes";
17
import {
18
        StarkFormButton,
19
        StarkFormCustomizablePredefinedButton,
20
        StarkFormDefaultPredefinedButton,
21
        StarkGenericSearchActionBarConfig,
22
        StarkGenericSearchFormButtonsConfig
23
} from "../../entities";
24
import {
25
        StarkAction,
26
        StarkActionBarConfig,
27
        StarkCustomizablePredefinedAction,
28
        StarkDefaultPredefinedAction
29
} from "@nationalbankbelgium/stark-ui/src/modules/action-bar";
30
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";
31
import { UntypedFormGroup } from "@angular/forms";
32
import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from "@angular/animations";
33
import { AbstractStarkUiComponent } from "@nationalbankbelgium/stark-ui/src/internal-common";
34
import isEqual from "lodash-es/isEqual";
35

36
/**
37
 * @ignore
38
 */
39
const componentName = "stark-generic-search";
1✔
40

41
/**
42
 * @ignore
43
 */
44
declare type UnusedLabelProps = "labelActivated" | "labelSwitchFunction";
45

46
/**
47
 * @ignore
48
 */
49
declare type UnusedIconProps = "iconActivated" | "iconSwitchFunction";
50

51
/**
52
 * @ignore
53
 */
54
declare type StarkDefaultPredefinedActionBarGenericAction = Required<
55
        Pick<StarkDefaultPredefinedAction, Exclude<keyof StarkDefaultPredefinedAction, UnusedLabelProps | UnusedIconProps>>
56
> &
57
        Pick<StarkDefaultPredefinedAction, UnusedIconProps>;
58

59
/**
60
 * @ignore
61
 */
62
declare type StarkCustomizablePredefinedActionBarGenericAction = Required<
63
        Pick<StarkCustomizablePredefinedAction, Exclude<keyof StarkCustomizablePredefinedAction, UnusedLabelProps | UnusedIconProps>>
64
> &
65
        Partial<Pick<StarkCustomizablePredefinedAction, UnusedIconProps>>;
66

67
/**
68
 * @ignore
69
 */
70
interface StarkGenericSearchActionBarConfigRequired extends StarkGenericSearchActionBarConfig, StarkActionBarConfig {
71
        search: StarkDefaultPredefinedActionBarGenericAction;
72
        new: StarkCustomizablePredefinedActionBarGenericAction;
73
        reset: StarkCustomizablePredefinedActionBarGenericAction;
74
}
75

76
/**
77
 * @ignore
78
 */
79
interface StarkGenericSearchFormButtonsConfigRequired extends StarkGenericSearchFormButtonsConfig {
80
        search: Required<StarkFormDefaultPredefinedButton>;
81
        new: Required<StarkFormCustomizablePredefinedButton>;
82
        reset: Required<StarkFormCustomizablePredefinedButton>;
83
        custom: StarkFormButton[];
84
}
85

86
/**
87
 * @ignore
88
 */
89
const formAnimations: AnimationTriggerMetadata = trigger("collapse", [
1✔
90
        state("closed", style({ opacity: 0, height: 0 })),
91
        transition("* <=> closed", animate(400))
92
]);
93

94
/**
95
 * Component to display a generic search form with an action bar and form buttons for these actions:
96
 *
97
 * - **New:** emits in the newTriggered event emitter
98
 * - **Search:** emits in the searchTriggered event emitter
99
 * - **Reset:** emits in the resetTriggered event emitter
100
 *
101
 * **IMPORTANT: In the HTML, the component defining the search form content should be exported as `searchForm`
102
 * so that it is accessible from the Stark Generic Search component.**
103
 *
104
 * See [Angular Template Syntax: Template reference variables](https://v12.angular.io/guide/template-reference-variables)
105
 *
106
 * @example
107
 * <stark-generic-search formHtmlId="demo-generic-search-form"
108
 *                       (searchTriggered)="onSearch($event)"
109
 *                       (resetTriggered)="onReset($event)"
110
 *                       [isFormHidden]="hideSearch">
111
 *                                                               <!-- your search form component should be exported as 'searchForm' -->
112
 *                    <your-search-form-component #searchForm
113
 *                               [searchCriteria]="workingCopy"
114
 *                               (workingCopyChanged)="updateWorkingCopy($event)">
115
 *                    </your-search-form-component>
116
 * </stark-generic-search>
117
 */
118
@Component({
119
        selector: "stark-generic-search",
120
        templateUrl: "./generic-search.component.html",
121
        animations: [formAnimations],
122
        encapsulation: ViewEncapsulation.None,
123
        // We need to use host instead of @HostBinding: https://github.com/NationalBankBelgium/stark/issues/664
124
        host: {
125
                class: componentName
126
        }
127
})
128
export class StarkGenericSearchComponent extends AbstractStarkUiComponent implements OnInit, OnChanges, AfterContentInit {
1✔
129
        /**
130
         * Configuration object for the action bar to be shown above the generic form
131
         */
132
        @Input()
133
        public formActionBarConfig?: StarkGenericSearchActionBarConfig;
134

135
        /**
136
         * Configuration object for the buttons to be shown in the generic form
137
         */
138
        @Input()
139
        public formButtonsConfig?: StarkGenericSearchFormButtonsConfig;
140

141
        /**
142
         * Whether the search form should be hidden.
143
         *
144
         * Default: `false`
145
         */
146
        @Input()
147
        public isFormHidden = false;
4✔
148

149
        /**
150
         * HTML id of action bar component.
151
         */
152
        @Input()
153
        public formHtmlId = "stark-generic-search-form";
4✔
154

155
        /**
156
         * Whether the search form should be hidden once the search is triggered.
157
         *
158
         * Default: `false`
159
         */
160
        @Input()
161
        public hideOnSearch = false;
4✔
162

163
        /**
164
         * Callback function to be called when the "New" button is clicked (in case it is shown)
165
         */
166
        @Output()
167
        public readonly newTriggered = new EventEmitter<void>();
4✔
168

169
        /**
170
         * Callback function to be called when the "Reset" button is clicked.
171
         * The form model object is passed as parameter to this function.
172
         */
173
        @Output()
174
        public readonly resetTriggered = new EventEmitter<UntypedFormGroup>();
4✔
175

176
        /**
177
         * Callback function to be called when the "Search" button is clicked.
178
         * The form model object is passed as parameter to this function.
179
         */
180
        @Output()
181
        public readonly searchTriggered = new EventEmitter<UntypedFormGroup>();
4✔
182

183
        /**
184
         * Callback function to be called when the visibility of the generic form changes.
185
         * A boolean is passed as parameter to indicate whether the generic form is visible or not.
186
         */
187
        @Output()
188
        public readonly formVisibilityChanged = new EventEmitter<boolean>();
4✔
189

190
        /**
191
         * Reference to the child search form component. Such component is looked up by the `searchForm` template reference variable.
192
         *
193
         * **Therefore, the child search form component should be exported as `searchForm`.**
194
         *
195
         * See {@link https://v12.angular.io/guide/template-reference-variables|Angular Template Syntax: Template reference variables} for more info about template reference variables.
196
         */
197
        @ContentChild("searchForm", { static: true })
198
        public searchFormComponent!: StarkSearchFormComponent<unknown>;
199

200
        /**
201
         * Configuration object for the the {@link StarkActionBarComponent} to be shown in the search form.
202
         */
203
        public actionBarConfig: StarkActionBarConfig = { actions: [] };
4✔
204

205
        /**
206
         * Object containing normalized options that will be used to generate config for the the {@link StarkActionBarComponent}
207
         * to be shown in the search form.
208
         */
209
        public normalizedFormActionBarConfig!: StarkGenericSearchActionBarConfigRequired;
210

211
        /**
212
         * Object containing normalized options for the different buttons to be shown in the search form.
213
         */
214
        public normalizedFormButtonsConfig!: StarkGenericSearchFormButtonsConfigRequired;
215

216
        /**
217
         * Reference to the FormGroup instance bound to the search form.
218
         *
219
         * In the HTML, the component defining the search form content should be exported as `searchForm`
220
         * so that it is accessible from the Stark Generic Search component. See example above.
221
         */
222
        public get genericForm(): UntypedFormGroup {
223
                return this.searchFormComponent.searchForm;
12✔
224
        }
225

226
        /**
227
         * Class constructor
228
         * @param logger - The `StarkLoggingService` instance of the application.
229
         * @param renderer - Angular `Renderer2` wrapper for DOM manipulations.
230
         * @param elementRef - Reference to the DOM element where this component is attached to.
231
         */
232
        public constructor(
233
                @Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService,
4✔
234
                renderer: Renderer2,
235
                elementRef: ElementRef
236
        ) {
237
                super(renderer, elementRef);
4✔
238
        }
239

240
        /**
241
         * Component lifecycle hook
242
         */
243
        public ngAfterContentInit(): void {
244
                if (!this.searchFormComponent) {
4✔
245
                        throw new Error("StarkGenericSearchComponent: the searchForm content child is required.");
1✔
246
                }
247
        }
248

249
        /**
250
         * Component lifecycle hook
251
         */
252
        public override ngOnInit(): void {
253
                super.ngOnInit();
4✔
254

255
                this.normalizedFormButtonsConfig = this.normalizeFormButtonsConfig(this.formButtonsConfig);
4✔
256
                this.normalizedFormActionBarConfig = this.normalizeFormActionBarConfig(this.formActionBarConfig);
4✔
257
                this.actionBarConfig = this.buildActionBarConfig(this.normalizedFormActionBarConfig);
4✔
258

259
                this.logger.debug(componentName + ": component initialized");
4✔
260
        }
261

262
        /**
263
         * Component lifecycle hook
264
         * @param changesObj - Contains the changed properties
265
         */
266
        public ngOnChanges(changesObj: SimpleChanges): void {
267
                if (
×
268
                        changesObj["formButtonsConfig"] &&
×
269
                        !changesObj["formButtonsConfig"].isFirstChange() &&
270
                        !isEqual(this.formButtonsConfig, this.normalizedFormButtonsConfig)
271
                ) {
272
                        this.normalizedFormButtonsConfig = this.normalizeFormButtonsConfig(this.formButtonsConfig);
×
273
                }
274

275
                if (
×
276
                        changesObj["formActionBarConfig"] &&
×
277
                        !changesObj["formActionBarConfig"].isFirstChange() &&
278
                        !isEqual(this.formActionBarConfig, this.normalizedFormActionBarConfig)
279
                ) {
280
                        this.normalizedFormActionBarConfig = this.normalizeFormActionBarConfig(this.formActionBarConfig);
×
281
                        this.actionBarConfig = this.buildActionBarConfig(this.normalizedFormActionBarConfig);
×
282
                }
283
        }
284

285
        /**
286
         * Normalize the form buttons config
287
         * Set a default value for each property of each button if there is no value defined
288
         * @param config - Form buttons configuration
289
         */
290
        // eslint-disable-next-line sonarjs/cognitive-complexity
291
        public normalizeFormButtonsConfig(config?: StarkGenericSearchFormButtonsConfig): StarkGenericSearchFormButtonsConfigRequired {
292
                config = config || {};
4✔
293

294
                /**
295
                 * @ignore
296
                 */
297
                function normalizeButtonConfig(
298
                        buttonConfig: StarkFormCustomizablePredefinedButton,
299
                        defaultConfig: Required<StarkFormCustomizablePredefinedButton>
300
                ): Required<StarkFormCustomizablePredefinedButton> {
301
                        return {
×
302
                                icon: typeof buttonConfig.icon !== "undefined" ? buttonConfig.icon : defaultConfig.icon,
×
303
                                label: typeof buttonConfig.label !== "undefined" ? buttonConfig.label : defaultConfig.label,
×
304
                                isEnabled: typeof buttonConfig.isEnabled !== "undefined" ? buttonConfig.isEnabled : defaultConfig.isEnabled,
×
305
                                isVisible: typeof buttonConfig.isVisible !== "undefined" ? buttonConfig.isVisible : defaultConfig.isVisible,
×
306
                                className: typeof buttonConfig.className !== "undefined" ? buttonConfig.className : defaultConfig.className,
×
307
                                buttonColor: typeof buttonConfig.buttonColor !== "undefined" ? buttonConfig.buttonColor : defaultConfig.buttonColor
×
308
                        };
309
                }
310

311
                /**
312
                 * @ignore
313
                 */
314
                function normalizeDefaultButtonConfig(
315
                        buttonConfig: StarkFormDefaultPredefinedButton,
316
                        defaultConfig: Required<StarkFormDefaultPredefinedButton>
317
                ): Required<StarkFormDefaultPredefinedButton> {
318
                        return {
×
319
                                icon: typeof buttonConfig.icon !== "undefined" ? buttonConfig.icon : defaultConfig.icon,
×
320
                                label: typeof buttonConfig.label !== "undefined" ? buttonConfig.label : defaultConfig.label,
×
321
                                isEnabled: typeof buttonConfig.isEnabled !== "undefined" ? buttonConfig.isEnabled : defaultConfig.isEnabled,
×
322
                                className: typeof buttonConfig.className !== "undefined" ? buttonConfig.className : defaultConfig.className,
×
323
                                buttonColor: typeof buttonConfig.buttonColor !== "undefined" ? buttonConfig.buttonColor : defaultConfig.buttonColor
×
324
                        };
325
                }
326

327
                // set default values
328
                const normalizedConfig: StarkGenericSearchFormButtonsConfigRequired = {
4✔
329
                        search: {
330
                                icon: "",
331
                                label: "STARK.ICONS.SEARCH",
332
                                isEnabled: true,
333
                                className: "mat-raised-button",
334
                                buttonColor: "primary"
335
                        },
336
                        new: {
337
                                icon: "",
338
                                label: "STARK.ICONS.NEW_ITEM",
339
                                isEnabled: true,
340
                                isVisible: true,
341
                                className: "mat-stroked-button",
342
                                buttonColor: "primary"
343
                        },
344
                        reset: {
345
                                icon: "",
346
                                label: "STARK.ICONS.RESET",
347
                                isEnabled: true,
348
                                isVisible: true,
349
                                className: "mat-stroked-button",
350
                                buttonColor: "primary"
351
                        },
352
                        custom: []
353
                };
354

355
                if (config.search) {
4!
356
                        normalizedConfig.search = normalizeDefaultButtonConfig(config.search, normalizedConfig.search);
×
357
                }
358

359
                if (config.new) {
4!
360
                        normalizedConfig.new = normalizeButtonConfig(config.new, normalizedConfig.new);
×
361
                }
362

363
                if (config.reset) {
4!
364
                        normalizedConfig.reset = normalizeButtonConfig(config.reset, normalizedConfig.reset);
×
365
                }
366

367
                if (config.custom !== undefined) {
4!
368
                        normalizedConfig.custom = config.custom;
×
369
                }
370

371
                return normalizedConfig;
4✔
372
        }
373

374
        /**
375
         * Normalize the form action bar config
376
         * Set a default value for each property of each action if there is no value defined
377
         * @param config - Form action bar configuration
378
         */
379
        // eslint-disable-next-line sonarjs/cognitive-complexity
380
        public normalizeFormActionBarConfig(config?: StarkGenericSearchActionBarConfig): StarkGenericSearchActionBarConfigRequired {
381
                config = config || { actions: [] };
4✔
382

383
                /**
384
                 * @ignore
385
                 */
386
                function normalizeDefaultActionConfig(
387
                        actionConfig: StarkDefaultPredefinedAction,
388
                        defaultConfig: StarkDefaultPredefinedActionBarGenericAction
389
                ): StarkDefaultPredefinedActionBarGenericAction {
390
                        return {
×
391
                                icon: typeof actionConfig.icon !== "undefined" ? actionConfig.icon : defaultConfig.icon,
×
392
                                label: typeof actionConfig.label !== "undefined" ? actionConfig.label : defaultConfig.label,
×
393
                                isEnabled: typeof actionConfig.isEnabled !== "undefined" ? actionConfig.isEnabled : defaultConfig.isEnabled,
×
394
                                iconActivated: actionConfig.iconActivated,
395
                                iconSwitchFunction: actionConfig.iconSwitchFunction,
396
                                className: typeof actionConfig.className !== "undefined" ? actionConfig.className : defaultConfig.className,
×
397
                                buttonColor: typeof actionConfig.buttonColor !== "undefined" ? actionConfig.buttonColor : defaultConfig.buttonColor
×
398
                        };
399
                }
400

401
                /**
402
                 * @ignore
403
                 */
404
                function normalizeActionConfig(
405
                        actionConfig: StarkCustomizablePredefinedAction,
406
                        defaultConfig: StarkCustomizablePredefinedActionBarGenericAction
407
                ): StarkCustomizablePredefinedActionBarGenericAction {
408
                        return {
×
409
                                icon: typeof actionConfig.icon !== "undefined" ? actionConfig.icon : defaultConfig.icon,
×
410
                                label: typeof actionConfig.label !== "undefined" ? actionConfig.label : defaultConfig.label,
×
411
                                isEnabled: typeof actionConfig.isEnabled !== "undefined" ? actionConfig.isEnabled : defaultConfig.isEnabled,
×
412
                                isVisible: typeof actionConfig.isVisible !== "undefined" ? actionConfig.isVisible : defaultConfig.isVisible,
×
413
                                iconActivated: actionConfig.iconActivated,
414
                                iconSwitchFunction: actionConfig.iconSwitchFunction,
415
                                className: typeof actionConfig.className !== "undefined" ? actionConfig.className : defaultConfig.className,
×
416
                                buttonColor: typeof actionConfig.buttonColor !== "undefined" ? actionConfig.buttonColor : defaultConfig.buttonColor
×
417
                        };
418
                }
419

420
                // set default values
421
                const normalizedConfig: StarkGenericSearchActionBarConfigRequired = {
4✔
422
                        search: {
423
                                icon: "magnify",
424
                                label: "STARK.ICONS.SEARCH",
425
                                isEnabled: true,
426
                                className: "",
427
                                buttonColor: "primary"
428
                        },
429
                        new: {
430
                                icon: "note-plus",
431
                                label: "STARK.ICONS.NEW_ITEM",
432
                                isEnabled: true,
433
                                isVisible: true,
434
                                className: "",
435
                                buttonColor: "primary"
436
                        },
437
                        reset: {
438
                                icon: "undo",
439
                                label: "STARK.ICONS.RESET",
440
                                isEnabled: true,
441
                                isVisible: true,
442
                                className: "",
443
                                buttonColor: "primary"
444
                        },
445
                        actions: [],
446
                        isPresent: true // action bar is present by default, should be explicitly set to false to remove it
447
                };
448

449
                if (config.search) {
4!
450
                        normalizedConfig.search = normalizeDefaultActionConfig(config.search, normalizedConfig.search);
×
451
                }
452

453
                if (config.new) {
4!
454
                        normalizedConfig.new = normalizeActionConfig(config.new, normalizedConfig.new);
×
455
                }
456

457
                if (config.reset) {
4!
458
                        normalizedConfig.reset = normalizeActionConfig(config.reset, normalizedConfig.reset);
×
459
                }
460

461
                if (config.actions !== undefined) {
4✔
462
                        normalizedConfig.actions = config.actions;
4✔
463
                }
464

465
                if (config.isPresent !== undefined) {
4!
466
                        normalizedConfig.isPresent = config.isPresent;
×
467
                }
468

469
                return normalizedConfig;
4✔
470
        }
471

472
        /**
473
         * Build the action bar config object based on the searchFormActionBarConfig object and set the action calls to emit
474
         * values through the defined Output
475
         * @param searchFormActionBarConfig - Form action bar configuration
476
         */
477
        public buildActionBarConfig(searchFormActionBarConfig: StarkGenericSearchActionBarConfigRequired): StarkActionBarConfig {
478
                // TODO: replace this code with a Type to convert all optional props to required
479
                // see https://github.com/JakeGinnivan/TypeScript-Handbook/commit/a02ef7023f2fb513dc35d8de35be23b926ec82e3
480
                const predefinedSearchAction: StarkDefaultPredefinedActionBarGenericAction = searchFormActionBarConfig.search;
4✔
481
                const actionSearch: StarkAction = {
4✔
482
                        label: predefinedSearchAction.label,
483
                        icon: predefinedSearchAction.icon,
484
                        buttonColor: predefinedSearchAction.buttonColor,
485
                        isEnabled: predefinedSearchAction.isEnabled,
486
                        iconActivated: predefinedSearchAction.iconActivated,
487
                        iconSwitchFunction: predefinedSearchAction.iconSwitchFunction,
488
                        className: predefinedSearchAction.className,
489
                        id: "search-action-bar",
490
                        isVisible: true,
491
                        actionCall: (): void => {
492
                                this.searchTriggered.emit(this.genericForm);
×
493
                        }
494
                };
495

496
                const predefinedResetAction: StarkCustomizablePredefinedAction = <StarkCustomizablePredefinedAction>searchFormActionBarConfig.reset;
4✔
497
                const actionReset: StarkAction = {
4✔
498
                        label: <string>predefinedResetAction.label,
499
                        icon: <string>predefinedResetAction.icon,
500
                        isEnabled: <boolean>predefinedResetAction.isEnabled,
501
                        isVisible: <boolean>predefinedResetAction.isVisible,
502
                        iconActivated: predefinedResetAction.iconActivated,
503
                        iconSwitchFunction: predefinedResetAction.iconSwitchFunction,
504
                        className: predefinedResetAction.className,
505
                        id: "undo-action-bar",
506
                        actionCall: (): void => {
507
                                if (this.resetTriggered) {
×
508
                                        this.resetTriggered.emit(this.genericForm);
×
509
                                }
510
                        }
511
                };
512

513
                const predefinedNewAction: StarkCustomizablePredefinedAction = <StarkCustomizablePredefinedAction>searchFormActionBarConfig.new;
4✔
514
                const actionNew: StarkAction = {
4✔
515
                        label: <string>predefinedNewAction.label,
516
                        icon: <string>predefinedNewAction.icon,
517
                        isEnabled: <boolean>predefinedNewAction.isEnabled,
518
                        isVisible: <boolean>predefinedNewAction.isVisible,
519
                        iconActivated: predefinedNewAction.iconActivated,
520
                        iconSwitchFunction: predefinedNewAction.iconSwitchFunction,
521
                        className: predefinedNewAction.className,
522
                        id: "new-action-bar",
523
                        actionCall: (): void => {
524
                                if (this.newTriggered) {
×
525
                                        this.newTriggered.emit();
×
526
                                }
527
                        }
528
                };
529

530
                return {
4✔
531
                        isPresent: searchFormActionBarConfig.isPresent,
532
                        actions: [actionNew, actionSearch, actionReset, ...searchFormActionBarConfig.actions]
533
                };
534
        }
535

536
        /**
537
         * Emit the form through "searchTriggered" event emitter then hide the search form based on hideOnSearch variable
538
         */
539
        public triggerSearch(): void {
540
                this.searchTriggered.emit(this.genericForm);
×
541
                if (this.hideOnSearch) {
×
542
                        this.hideForm();
×
543
                }
544
        }
545

546
        /**
547
         * Hide the search form and emit a boolean through formVisibilityChanged event emitter that indicates if the search is displayed
548
         */
549
        public hideForm(): void {
550
                if (!this.isFormHidden) {
×
551
                        this.isFormHidden = true;
×
552

553
                        // by the moment, the callback is called only when the form is hidden
554
                        this.formVisibilityChanged.emit(!this.isFormHidden);
×
555
                }
556
        }
557

558
        /**
559
         * @ignore
560
         */
561
        public trackItemFn(_index: number, formButton: StarkFormButton): string {
562
                return formButton.id;
×
563
        }
564
}
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