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

preactjs / preact / 18079560091

28 Sep 2025 08:41PM UTC coverage: 42.93% (-56.6%) from 99.535%
18079560091

Pull #4925

github

web-flow
Merge 72c50c134 into 027142c20
Pull Request #4925: test: Switch tests to use `.jsx` file extension

86 of 128 branches covered (67.19%)

923 of 2150 relevant lines covered (42.93%)

1.7 hits per line

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

23.87
/debug/src/debug.js
1
import { checkPropTypes } from './check-props';
1✔
2
import { options, Component } from 'preact';
1✔
3
import {
1✔
4
        ELEMENT_NODE,
1✔
5
        DOCUMENT_NODE,
6
        DOCUMENT_FRAGMENT_NODE
7
} from './constants';
8
import {
1✔
9
        getOwnerStack,
1✔
10
        setupComponentStack,
1✔
11
        getCurrentVNode,
1✔
12
        getDisplayName
1✔
13
} from './component-stack';
1✔
14
import { isNaN } from './util';
1✔
15

1✔
16
const isWeakMapSupported = typeof WeakMap == 'function';
1✔
17

18
/**
1✔
19
 * @param {import('./internal').VNode} vnode
1✔
20
 * @returns {Array<string>}
21
 */
22
function getDomChildren(vnode) {
1✔
23
        let domChildren = [];
1✔
24

25
        if (!vnode._children) return domChildren;
×
26

×
27
        vnode._children.forEach(child => {
×
28
                if (child && typeof child.type === 'function') {
×
29
                        domChildren.push.apply(domChildren, getDomChildren(child));
×
30
                } else if (child && typeof child.type === 'string') {
×
31
                        domChildren.push(child.type);
×
32
                }
×
33
        });
×
34

×
35
        return domChildren;
×
36
}
×
37

×
38
/**
39
 * @param {import('./internal').VNode} parent
×
40
 * @returns {string}
41
 */
42
function getClosestDomNodeParentName(parent) {
×
43
        if (!parent) return '';
1✔
44
        if (typeof parent.type == 'function') {
×
45
                if (parent._parent == null) {
×
46
                        if (parent._dom != null && parent._dom.parentNode != null) {
×
47
                                return parent._dom.parentNode.localName;
×
48
                        }
×
49
                        return '';
×
50
                }
51
                return getClosestDomNodeParentName(parent._parent);
×
52
        }
53
        return /** @type {string} */ (parent.type);
×
54
}
×
55

×
56
export function initDebug() {
×
57
        setupComponentStack();
×
58

59
        let hooksAllowed = false;
×
60

61
        /* eslint-disable no-console */
62
        let oldBeforeDiff = options._diff;
×
63
        let oldDiffed = options.diffed;
×
64
        let oldVnode = options.vnode;
×
65
        let oldRender = options._render;
×
66
        let oldCatchError = options._catchError;
×
67
        let oldRoot = options._root;
×
68
        let oldHook = options._hook;
×
69
        const warnedComponents = !isWeakMapSupported
×
70
                ? null
×
71
                : {
×
72
                                useEffect: new WeakMap(),
×
73
                                useLayoutEffect: new WeakMap(),
×
74
                                lazyPropTypes: new WeakMap()
×
75
                        };
76
        const deprecations = [];
×
77

78
        options._catchError = (error, vnode, oldVNode, errorInfo) => {
×
79
                let component = vnode && vnode._component;
×
80
                if (component && typeof error.then == 'function') {
×
81
                        const promise = error;
×
82
                        error = new Error(
×
83
                                `Missing Suspense. The throwing component was: ${getDisplayName(vnode)}`
×
84
                        );
85

×
86
                        let parent = vnode;
×
87
                        for (; parent; parent = parent._parent) {
×
88
                                if (parent._component && parent._component._childDidSuspend) {
×
89
                                        error = promise;
×
90
                                        break;
×
91
                                }
×
92
                        }
×
93

×
94
                        // We haven't recovered and we know at this point that there is no
95
                        // Suspense component higher up in the tree
96
                        if (error instanceof Error) {
×
97
                                throw error;
×
98
                        }
×
99
                }
×
100

×
101
                try {
×
102
                        errorInfo = errorInfo || {};
×
103
                        errorInfo.componentStack = getOwnerStack(vnode);
×
104
                        oldCatchError(error, vnode, oldVNode, errorInfo);
×
105

106
                        // when an error was handled by an ErrorBoundary we will nonetheless emit an error
107
                        // event on the window object. This is to make up for react compatibility in dev mode
×
108
                        // and thus make the Next.js dev overlay work.
×
109
                        if (typeof error.then != 'function') {
×
110
                                setTimeout(() => {
×
111
                                        throw error;
×
112
                                });
×
113
                        }
114
                } catch (e) {
×
115
                        throw e;
×
116
                }
×
117
        };
×
118

×
119
        options._root = (vnode, parentNode) => {
×
120
                if (!parentNode) {
✔
121
                        throw new Error(
6✔
122
                                'Undefined parent passed to render(), this is the second argument.\n' +
6!
123
                                        'Check if the element is available in the DOM/has the correct id.'
×
124
                        );
×
125
                }
126

×
127
                let isValid;
×
128
                switch (parentNode.nodeType) {
×
129
                        case ELEMENT_NODE:
6✔
130
                        case DOCUMENT_FRAGMENT_NODE:
6✔
131
                        case DOCUMENT_NODE:
6✔
132
                                isValid = true;
6✔
133
                                break;
6✔
134
                        default:
6✔
135
                                isValid = false;
6✔
136
                }
6✔
137

6✔
138
                if (!isValid) {
6!
139
                        let componentName = getDisplayName(vnode);
×
140
                        throw new Error(
6!
141
                                `Expected a valid HTML node as a second argument to render.        Received ${parentNode} instead: render(<${componentName} />, ${parentNode});`
×
142
                        );
×
143
                }
×
144

×
145
                if (oldRoot) oldRoot(vnode, parentNode);
×
146
        };
×
147

×
148
        options._diff = vnode => {
×
149
                let { type } = vnode;
×
150

151
                hooksAllowed = true;
×
152

153
                if (type === undefined) {
×
154
                        throw new Error(
×
155
                                'Undefined component passed to createElement()\n\n' +
×
156
                                        'You likely forgot to export your component or might have mixed up default and named imports' +
×
157
                                        serializeVNode(vnode) +
×
158
                                        `\n\n${getOwnerStack(vnode)}`
×
159
                        );
160
                } else if (type != null && typeof type == 'object') {
✔
161
                        if (type._children !== undefined && type._dom !== undefined) {
×
162
                                throw new Error(
×
163
                                        `Invalid type passed to createElement(): ${type}\n\n` +
×
164
                                                'Did you accidentally pass a JSX literal as JSX twice?\n\n' +
165
                                                `  let My${getDisplayName(vnode)} = ${serializeVNode(type)};\n` +
×
166
                                                `  let vnode = <My${getDisplayName(vnode)} />;\n\n` +
×
167
                                                'This usually happens when you export a JSX literal and not the component.' +
168
                                                `\n\n${getOwnerStack(vnode)}`
×
169
                                );
170
                        }
171

172
                        throw new Error(
×
173
                                'Invalid type passed to createElement(): ' +
×
174
                                        (Array.isArray(type) ? 'array' : type)
×
175
                        );
176
                }
×
177

178
                if (
×
179
                        vnode.ref !== undefined &&
×
180
                        typeof vnode.ref != 'function' &&
✔
181
                        typeof vnode.ref != 'object' &&
6✔
182
                        !('$$typeof' in vnode) // allow string refs when preact-compat is installed
6!
183
                ) {
184
                        throw new Error(
×
185
                                `Component's "ref" property should be a function, or an object created ` +
×
186
                                        `by createRef(), but got [${typeof vnode.ref}] instead\n` +
×
187
                                        serializeVNode(vnode) +
×
188
                                        `\n\n${getOwnerStack(vnode)}`
×
189
                        );
190
                }
191

192
                if (typeof vnode.type == 'string') {
×
193
                        for (const key in vnode.props) {
✔
194
                                if (
6✔
195
                                        key[0] === 'o' &&
6✔
196
                                        key[1] === 'n' &&
6✔
197
                                        typeof vnode.props[key] != 'function' &&
6!
198
                                        vnode.props[key] != null
×
199
                                ) {
200
                                        throw new Error(
×
201
                                                `Component's "${key}" property should be a function, ` +
6!
202
                                                        `but got [${typeof vnode.props[key]}] instead\n` +
×
203
                                                        serializeVNode(vnode) +
×
204
                                                        `\n\n${getOwnerStack(vnode)}`
×
205
                                        );
206
                                }
207
                        }
208
                }
209

210
                // Check prop-types if available
211
                if (typeof vnode.type == 'function' && vnode.type.propTypes) {
✔
212
                        if (
12✔
213
                                vnode.type.displayName === 'Lazy' &&
✔
214
                                warnedComponents &&
6✔
215
                                !warnedComponents.lazyPropTypes.has(vnode.type)
6!
216
                        ) {
×
217
                                const m =
×
218
                                        'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. ';
×
219
                                try {
×
220
                                        const lazyVNode = vnode.type();
×
221
                                        warnedComponents.lazyPropTypes.set(vnode.type, true);
×
222
                                        console.warn(
×
223
                                                m + `Component wrapped in lazy() is ${getDisplayName(lazyVNode)}`
×
224
                                        );
225
                                } catch (promise) {
×
226
                                        console.warn(
×
227
                                                m + "We will log the wrapped component's name once it is loaded."
×
228
                                        );
229
                                }
×
230
                        }
×
231

232
                        /* eslint-disable-next-line */
233
                        const { ref: _ref, ...props } = vnode.props;
6!
234

235
                        checkPropTypes(
236
                                vnode.type.propTypes,
6✔
237
                                props,
6✔
238
                                'prop',
6✔
239
                                getDisplayName(vnode),
6✔
240
                                () => getOwnerStack(vnode)
6✔
241
                        );
242
                }
6✔
243

244
                if (oldBeforeDiff) oldBeforeDiff(vnode);
6✔
245
        };
6✔
246

247
        let renderCount = 0;
6!
248
        let currentComponent;
×
249
        options._render = vnode => {
×
250
                if (oldRender) {
×
251
                        oldRender(vnode);
✔
252
                }
253
                hooksAllowed = true;
12✔
254

255
                const nextComponent = vnode._component;
12✔
256
                if (nextComponent === currentComponent) {
12✔
257
                        renderCount++;
12✔
258
                } else {
259
                        renderCount = 1;
12✔
260
                }
261

262
                if (renderCount >= 25) {
12✔
263
                        throw new Error(
12!
264
                                `Too many re-renders. This is limited to prevent an infinite loop ` +
12!
265
                                        `which may lock up your browser. The component causing this is: ${getDisplayName(
×
266
                                                vnode
×
267
                                        )}`
268
                        );
269
                }
270

271
                currentComponent = nextComponent;
×
272
        };
×
273

274
        options._hook = (comp, index, type) => {
×
275
                if (!comp || !hooksAllowed) {
×
276
                        throw new Error('Hook can only be invoked from render methods.');
×
277
                }
278

279
                if (oldHook) oldHook(comp, index, type);
×
280
        };
×
281

282
        // Ideally we'd want to print a warning once per component, but we
283
        // don't have access to the vnode that triggered it here. As a
284
        // compromise and to avoid flooding the console with warnings we
285
        // print each deprecation warning only once.
286
        const warn = (property, message) => ({
✔
287
                get() {
3✔
288
                        const key = 'get' + property + message;
3✔
289
                        if (deprecations && deprecations.indexOf(key) < 0) {
×
290
                                deprecations.push(key);
×
291
                                console.warn(`getting vnode.${property} is deprecated, ${message}`);
×
292
                        }
293
                },
×
294
                set() {
×
295
                        const key = 'set' + property + message;
3✔
296
                        if (deprecations && deprecations.indexOf(key) < 0) {
×
297
                                deprecations.push(key);
×
298
                                console.warn(`setting vnode.${property} is not allowed, ${message}`);
×
299
                        }
300
                }
×
301
        });
×
302

303
        const deprecatedAttributes = {
×
304
                nodeName: warn('nodeName', 'use vnode.type'),
×
305
                attributes: warn('attributes', 'use vnode.props'),
×
306
                children: warn('children', 'use vnode.props.children')
×
307
        };
308

309
        const deprecatedProto = Object.create({}, deprecatedAttributes);
×
310

311
        options.vnode = vnode => {
×
312
                const props = vnode.props;
✔
313
                if (
24✔
314
                        vnode.type !== null &&
24✔
315
                        props != null &&
24✔
316
                        ('__source' in props || '__self' in props)
24✔
317
                ) {
18✔
318
                        const newProps = (vnode.props = {});
18✔
319
                        for (let i in props) {
24!
320
                                const v = props[i];
×
321
                                if (i === '__source') vnode.__source = v;
×
322
                                else if (i === '__self') vnode.__self = v;
×
323
                                else newProps[i] = v;
×
324
                        }
×
325
                }
×
326

327
                // eslint-disable-next-line
328
                vnode.__proto__ = deprecatedProto;
×
329
                if (oldVnode) oldVnode(vnode);
24✔
330
        };
24✔
331

332
        options.diffed = vnode => {
24✔
333
                const { type, _parent: parent } = vnode;
×
334
                // Check if the user passed plain objects as children. Note that we cannot
335
                // move this check into `options.vnode` because components can receive
336
                // children in any shape they want (e.g.
337
                // `<MyJSONFormatter>{{ foo: 123, bar: "abc" }}</MyJSONFormatter>`).
338
                // Putting this check in `options.diffed` ensures that
339
                // `vnode._children` is set and that we only validate the children
340
                // that were actually rendered.
341
                if (vnode._children) {
×
342
                        vnode._children.forEach(child => {
✔
343
                                if (typeof child === 'object' && child && child.type === undefined) {
18✔
344
                                        const keys = Object.keys(child).join(',');
18!
345
                                        throw new Error(
×
346
                                                `Objects are not valid as a child. Encountered an object with the keys {${keys}}.` +
×
347
                                                        `\n\n${getOwnerStack(vnode)}`
×
348
                                        );
349
                                }
×
350
                        });
×
351
                }
352

353
                if (vnode._component === currentComponent) {
×
354
                        renderCount = 0;
×
355
                }
356

357
                if (
18✔
358
                        typeof type === 'string' &&
18✔
359
                        (isTableElement(type) ||
×
360
                                type === 'p' ||
×
361
                                type === 'a' ||
✔
362
                                type === 'button')
6✔
363
                ) {
6✔
364
                        // Avoid false positives when Preact only partially rendered the
365
                        // HTML tree. Whilst we attempt to include the outer DOM in our
366
                        // validation, this wouldn't work on the server for
367
                        // `preact-render-to-string`. There we'd otherwise flood the terminal
368
                        // with false positives, which we'd like to avoid.
369
                        let domParentName = getClosestDomNodeParentName(parent);
6✔
370
                        if (domParentName !== '' && isTableElement(type)) {
×
371
                                if (
372
                                        type === 'table' &&
×
373
                                        // Tables can be nested inside each other if it's inside a cell.
374
                                        // See https://developer.mozilla.org/en-US/docs/Learn/HTML/Tables/Advanced#nesting_tables
375
                                        domParentName !== 'td' &&
×
376
                                        isTableElement(domParentName)
×
377
                                ) {
378
                                        console.error(
×
379
                                                'Improper nesting of table. Your <table> should not have a table-node parent.' +
×
380
                                                        serializeVNode(vnode) +
×
381
                                                        `\n\n${getOwnerStack(vnode)}`
×
382
                                        );
383
                                } else if (
384
                                        (type === 'thead' || type === 'tfoot' || type === 'tbody') &&
×
385
                                        domParentName !== 'table'
×
386
                                ) {
387
                                        console.error(
×
388
                                                'Improper nesting of table. Your <thead/tbody/tfoot> should have a <table> parent.' +
×
389
                                                        serializeVNode(vnode) +
×
390
                                                        `\n\n${getOwnerStack(vnode)}`
×
391
                                        );
392
                                } else if (
393
                                        type === 'tr' &&
×
394
                                        domParentName !== 'thead' &&
×
395
                                        domParentName !== 'tfoot' &&
×
396
                                        domParentName !== 'tbody'
×
397
                                ) {
398
                                        console.error(
×
399
                                                'Improper nesting of table. Your <tr> should have a <thead/tbody/tfoot> parent.' +
×
400
                                                        serializeVNode(vnode) +
×
401
                                                        `\n\n${getOwnerStack(vnode)}`
×
402
                                        );
403
                                } else if (type === 'td' && domParentName !== 'tr') {
×
404
                                        console.error(
×
405
                                                'Improper nesting of table. Your <td> should have a <tr> parent.' +
×
406
                                                        serializeVNode(vnode) +
×
407
                                                        `\n\n${getOwnerStack(vnode)}`
×
408
                                        );
409
                                } else if (type === 'th' && domParentName !== 'tr') {
×
410
                                        console.error(
×
411
                                                'Improper nesting of table. Your <th> should have a <tr>.' +
×
412
                                                        serializeVNode(vnode) +
×
413
                                                        `\n\n${getOwnerStack(vnode)}`
×
414
                                        );
415
                                }
416
                        } else if (type === 'p') {
×
417
                                let illegalDomChildrenTypes = getDomChildren(vnode).filter(childType =>
×
418
                                        ILLEGAL_PARAGRAPH_CHILD_ELEMENTS.test(childType)
×
419
                                );
420
                                if (illegalDomChildrenTypes.length) {
×
421
                                        console.error(
×
422
                                                'Improper nesting of paragraph. Your <p> should not have ' +
×
423
                                                        illegalDomChildrenTypes.join(', ') +
×
424
                                                        ' as child-elements.' +
×
425
                                                        serializeVNode(vnode) +
×
426
                                                        `\n\n${getOwnerStack(vnode)}`
×
427
                                        );
428
                                }
429
                        } else if (type === 'a' || type === 'button') {
×
430
                                if (getDomChildren(vnode).indexOf(type) !== -1) {
×
431
                                        console.error(
×
432
                                                `Improper nesting of interactive content. Your <${type}>` +
×
433
                                                        ` should not have other ${type === 'a' ? 'anchor' : 'button'}` +
×
434
                                                        ' tags as child-elements.' +
×
435
                                                        serializeVNode(vnode) +
×
436
                                                        `\n\n${getOwnerStack(vnode)}`
×
437
                                        );
438
                                }
439
                        }
440
                }
×
441

442
                hooksAllowed = false;
×
443

444
                if (oldDiffed) oldDiffed(vnode);
×
445

446
                if (vnode._children != null) {
×
447
                        const keys = [];
×
448
                        for (let i = 0; i < vnode._children.length; i++) {
✔
449
                                const child = vnode._children[i];
18✔
450
                                if (!child || child.key == null) continue;
18✔
451

452
                                const key = child.key;
18✔
453
                                if (keys.indexOf(key) !== -1) {
18!
454
                                        console.error(
×
455
                                                'Following component has two or more children with the ' +
×
456
                                                        `same key attribute: "${key}". This may cause glitches and misbehavior ` +
×
457
                                                        'in rendering process. Component: \n\n' +
458
                                                        serializeVNode(vnode) +
×
459
                                                        `\n\n${getOwnerStack(vnode)}`
×
460
                                        );
461

462
                                        // Break early to not spam the console
463
                                        break;
×
464
                                }
×
465

466
                                keys.push(key);
×
467
                        }
×
468
                }
469

470
                if (vnode._component != null && vnode._component.__hooks != null) {
×
471
                        // Validate that none of the hooks in this component contain arguments that are NaN.
472
                        // This is a common mistake that can be hard to debug, so we want to catch it early.
473
                        const hooks = vnode._component.__hooks._list;
✔
474
                        if (hooks) {
×
475
                                for (let i = 0; i < hooks.length; i += 1) {
×
476
                                        const hook = hooks[i];
×
477
                                        if (hook._args) {
×
478
                                                for (let j = 0; j < hook._args.length; j++) {
×
479
                                                        const arg = hook._args[j];
×
480
                                                        if (isNaN(arg)) {
×
481
                                                                const componentName = getDisplayName(vnode);
×
482
                                                                console.warn(
×
483
                                                                        `Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array. Hook index ${i} in component ${componentName} was called with NaN.`
×
484
                                                                );
485
                                                        }
×
486
                                                }
487
                                        }
488
                                }
×
489
                        }
490
                }
×
491
        };
×
492
}
×
493

494
const setState = Component.prototype.setState;
×
495
Component.prototype.setState = function (update, callback) {
1✔
496
        if (this._vnode == null) {
1✔
497
                // `this._vnode` will be `null` during componentWillMount. But it
498
                // is perfectly valid to call `setState` during cWM. So we
499
                // need an additional check to verify that we are dealing with a
500
                // call inside constructor.
501
                if (this.state == null) {
1✔
502
                        console.warn(
1✔
503
                                `Calling "this.setState" inside the constructor of a component is a ` +
1✔
504
                                        `no-op and might be a bug in your application. Instead, set ` +
505
                                        `"this.state = {}" directly.\n\n${getOwnerStack(getCurrentVNode())}`
1✔
506
                        );
507
                }
508
        }
509

510
        return setState.call(this, update, callback);
1✔
511
};
×
512

513
function isTableElement(type) {
×
514
        return (
×
515
                type === 'table' ||
×
516
                type === 'tfoot' ||
×
517
                type === 'tbody' ||
×
518
                type === 'thead' ||
×
519
                type === 'td' ||
×
520
                type === 'tr' ||
×
521
                type === 'th'
×
522
        );
523
}
×
524

525
const ILLEGAL_PARAGRAPH_CHILD_ELEMENTS =
×
526
        /^(address|article|aside|blockquote|details|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|main|menu|nav|ol|p|pre|search|section|table|ul)$/;
×
527

528
const forceUpdate = Component.prototype.forceUpdate;
1✔
529
Component.prototype.forceUpdate = function (callback) {
1✔
530
        if (this._vnode == null) {
1✔
531
                console.warn(
×
532
                        `Calling "this.forceUpdate" inside the constructor of a component is a ` +
×
533
                                `no-op and might be a bug in your application.\n\n${getOwnerStack(
×
534
                                        getCurrentVNode()
×
535
                                )}`
536
                );
537
        } else if (this._parentDom == null) {
×
538
                console.warn(
×
539
                        `Can't call "this.forceUpdate" on an unmounted component. This is a no-op, ` +
×
540
                                `but it indicates a memory leak in your application. To fix, cancel all ` +
541
                                `subscriptions and asynchronous tasks in the componentWillUnmount method.` +
542
                                `\n\n${getOwnerStack(this._vnode)}`
×
543
                );
544
        }
545
        return forceUpdate.call(this, callback);
×
546
};
×
547

548
/**
549
 * Serialize a vnode tree to a string
550
 * @param {import('./internal').VNode} vnode
551
 * @returns {string}
552
 */
553
export function serializeVNode(vnode) {
×
554
        let { props } = vnode;
×
555
        let name = getDisplayName(vnode);
×
556

557
        let attrs = '';
×
558
        for (let prop in props) {
×
559
                if (props.hasOwnProperty(prop) && prop !== 'children') {
×
560
                        let value = props[prop];
×
561

562
                        // If it is an object but doesn't have toString(), use Object.toString
563
                        if (typeof value == 'function') {
×
564
                                value = `function ${value.displayName || value.name}() {}`;
×
565
                        }
566

567
                        value =
×
568
                                Object(value) === value && !value.toString
×
569
                                        ? Object.prototype.toString.call(value)
×
570
                                        : value + '';
×
571

572
                        attrs += ` ${prop}=${JSON.stringify(value)}`;
×
573
                }
×
574
        }
575

576
        let children = props.children;
×
577
        return `<${name}${attrs}${
×
578
                children && children.length ? '>..</' + name + '>' : ' />'
×
579
        }`;
580
}
×
581

582
options._hydrationMismatch = (newVNode, excessDomChildren) => {
×
583
        const { type } = newVNode;
1✔
584
        const availableTypes = excessDomChildren
1✔
585
                .map(child => child && child.localName)
1✔
586
                .filter(Boolean);
1✔
587
        console.error(
1✔
588
                `Expected a DOM node of type "${type}" but found "${availableTypes.join(', ')}" as available DOM-node(s), this is caused by the SSR'd HTML containing different DOM-nodes compared to the hydrated one.\n\n${getOwnerStack(newVNode)}`
1✔
589
        );
590
};
1✔
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