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

baidu / san / 8592945364

08 Apr 2024 12:52AM UTC coverage: 95.1% (+0.03%) from 95.074%
8592945364

push

github

errorrik
is proxy supported

1997 of 2190 branches covered (91.19%)

Branch coverage included in aggregate %.

16 of 16 new or added lines in 1 file covered. (100.0%)

12 existing lines in 1 file now uncovered.

3592 of 3687 relevant lines covered (97.42%)

704.8 hits per line

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

96.17
/src/view/component.js
1
/**
2
 * Copyright (c) Baidu Inc. All rights reserved.
3
 *
4
 * This source code is licensed under the MIT license.
5
 * See LICENSE file in the project root for license information.
6
 *
7
 * @file 组件类
8
 */
9

10

11
var each = require('../util/each');
12
var empty = require('../util/empty');
13
var guid = require('../util/guid');
14
var extend = require('../util/extend');
15
var nextTick = require('../util/next-tick');
16
var emitDevtool = require('../util/emit-devtool');
17
var ExprType = require('../parser/expr-type');
18
var parseExpr = require('../parser/parse-expr');
19
var parseTemplate = require('../parser/parse-template');
20
var unpackANode = require('../parser/unpack-anode');
21
var removeEl = require('../browser/remove-el');
22
var Data = require('../runtime/data');
23
var dataProxy = require('../runtime/data-proxy');
24
var evalExpr = require('../runtime/eval-expr');
25
var changeExprCompare = require('../runtime/change-expr-compare');
26
var DataChangeType = require('../runtime/data-change-type');
27
var svgTags = require('../browser/svg-tags');
28
var insertBefore = require('../browser/insert-before');
29
var un = require('../browser/un');
30
var preprocessComponents = require('./preprocess-components');
31
var createNode = require('./create-node');
32
var preheatEl = require('./preheat-el');
33
var parseComponentTemplate = require('./parse-component-template');
34
var preheatANode = require('./preheat-a-node');
35
var LifeCycle = require('./life-cycle');
36
var mergeANodeSourceAttrs = require('./merge-a-node-source-attrs');
37
var getANodeProp = require('./get-a-node-prop');
38
var isDataChangeByElement = require('./is-data-change-by-element');
39
var getEventListener = require('./get-event-listener');
40
var hydrateElementChildren = require('./hydrate-element-children');
41
var NodeType = require('./node-type');
42
var styleProps = require('./style-props');
43
var nodeSBindInit = require('./node-s-bind-init');
44
var nodeSBindUpdate = require('./node-s-bind-update');
45
var elementOwnAttached = require('./element-own-attached');
46
var elementOwnDetach = require('./element-own-detach');
47
var elementOwnDispose = require('./element-own-dispose');
48
var warnEventListenMethod = require('./warn-event-listen-method');
49
var elementDisposeChildren = require('./element-dispose-children');
50
var createDataTypesChecker = require('../util/create-data-types-checker');
51
var warn = require('../util/warn');
52
var handleError = require('../util/handle-error');
53
var DOMChildrenWalker = require('./dom-children-walker');
54

55

56
var proxySupported = typeof Proxy !== 'undefined';
1✔
57

58

59
/**
60
 * 组件类
61
 *
62
 * @class
63
 * @param {Object} options 初始化参数
64
 */
65
function Component(options) { // eslint-disable-line
1✔
66
    // #[begin] error
67
    for (var key in Component.prototype) {
1,285✔
68
        if (this[key] !== Component.prototype[key]) {
29,555✔
69
            /* eslint-disable max-len */
70
            warn('\`' + key + '\` is a reserved key of san components. Overriding this property may cause unknown exceptions.');
1✔
71
            /* eslint-enable max-len */
72
        }
73
    }
74
    // #[end]
75

76

77
    options = options || {};
1,285✔
78
    this.lifeCycle = LifeCycle.start;
1,285✔
79
    this.id = guid++;
1,285✔
80

81
    if (typeof this.construct === 'function') {
1,285✔
82
        this.construct(options);
4✔
83
    }
84

85
    this.children = [];
1,285✔
86
    this.listeners = {};
1,285✔
87
    this.slotChildren = [];
1,285✔
88
    this.implicitChildren = [];
1,285✔
89

90
    var clazz = this.constructor;
1,285✔
91

92
    this.inheritAttrs = !(this.inheritAttrs === false || clazz.inheritAttrs === false);
1,285✔
93
    this.filters = this.filters || clazz.filters || {};
1,285✔
94
    this.computed = this.computed || clazz.computed || {};
1,285✔
95
    this.messages = this.messages || clazz.messages || {};
1,285✔
96
    this.ssr = this.ssr || clazz.ssr;
1,285✔
97

98
    if (options.transition) {
1,285✔
99
        this.transition = options.transition;
1✔
100
    }
101

102
    this.owner = options.owner;
1,285✔
103
    this.scope = options.scope;
1,285✔
104
    this.el = options.el;
1,285✔
105
    var parent = options.parent;
1,285✔
106
    if (parent) {
1,285✔
107
        this.parent = parent;
460✔
108
        this.parentComponent = parent.nodeType === NodeType.CMPT
460✔
109
            ? parent
460✔
110
            : parent && parent.parentComponent;
416✔
111
    }
112
    else if (this.owner) {
825✔
113
        this.parentComponent = this.owner;
6✔
114
        this.scope = this.owner.data;
6✔
115
    }
116

117
    this.sourceSlotNameProps = [];
1,285✔
118
    this.sourceSlots = {
1,285✔
119
        named: {}
120
    };
121

122
    // #[begin] devtool
123
    this._toPhase('beforeCompile');
1,285✔
124
    // #[end]
125

126
    var proto = clazz.prototype;
1,285✔
127

128
    // pre define components class
129
    /* istanbul ignore else  */
130
    if (!proto.hasOwnProperty('_cmptReady')) {
1,285✔
131
        preprocessComponents(clazz);
1,117✔
132
    }
133

134
    // compile
135
    if (!proto.hasOwnProperty('aNode')) {
1,285✔
136
        var aPack = clazz.aPack || proto.hasOwnProperty('aPack') && proto.aPack;
1,113✔
137
        if (aPack) {
1,113✔
138
            proto.aNode = unpackANode(aPack);
2✔
139
            clazz.aPack = proto.aPack = null;
2✔
140
        }
141
        else {
142
            proto.aNode = parseComponentTemplate(clazz);
1,111✔
143
        }
144
    }
145

146
    preheatANode(proto.aNode, this);
1,283✔
147

148
    this.tagName = proto.aNode.tagName;
1,283✔
149
    this.source = typeof options.source === 'string'
1,283✔
150
        ? parseTemplate(options.source).children[0]
1,283✔
151
        : options.source;
152

153
    preheatANode(this.source);
1,283✔
154
    proto.aNode._i++;
1,283✔
155

156

157
    // #[begin] hydrate
158
    // 组件反解,读取注入的组件数据
159
    if (this.el) {
1,283✔
160
        var firstCommentNode = this.el.firstChild;
153✔
161
        if (firstCommentNode && firstCommentNode.nodeType === 3) {
153✔
162
            firstCommentNode = firstCommentNode.nextSibling;
1✔
163
        }
164

165
        if (firstCommentNode && firstCommentNode.nodeType === 8) {
153✔
166
            var stumpMatch = firstCommentNode.data.match(/^\s*s-data:([\s\S]+)?$/);
152✔
167
            if (stumpMatch) {
152!
168
                var stumpText = stumpMatch[1];
152✔
169
                
170
                // fill component data
171
                // #[begin] allua
172
                options.data = (new Function('return '
173
                    + stumpText
174
                        .replace(/^[\s\n]*/, '')
175
                        .replace(
176
                            /"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.\d+Z"/g,
177
                            function (match, y, mon, d, h, m, s) {
178
                                return 'new Date(' + (+y) + ',' + (+mon) + ',' + (+d)
179
                                    + ',' + (+h) + ',' + (+m) + ',' + (+s) + ')';
180
                            }
181
                        )
182
                ))();
183
                // #[end]
184
                // #[begin] modern
185
                options.data = JSON.parse(
152✔
186
                    stumpText.replace(/\\([^\\\/"bfnrtu])/g, "$1"), 
187
                    function (key, value) {
188
                        if (typeof value === 'string') {
612✔
189
                            var ma = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.\d+Z/g.exec(value);
293✔
190
                            if (ma) {
293✔
191
                                return new Date(ma[1], ma[2], ma[3], ma[4], ma[5], ma[6]);
2✔
192
                            }
193
                        }
194
                        return value;
610✔
195
                    }
196
                );
197
                // #[end]
198

199
                if (firstCommentNode.previousSibling) {
152✔
200
                    removeEl(firstCommentNode.previousSibling);
1✔
201
                }
202
                removeEl(firstCommentNode);
152✔
203
            }
204
        }
205
    }
206
    // #[end]
207

208

209
    if (this.source) {
1,283✔
210
        // 组件运行时传入的结构,做slot解析
211
        this._initSourceSlots(1);
464✔
212

213
        for (var i = 0, l = this.source.events.length; i < l; i++) {
464✔
214
            var eventBind = this.source.events[i];
22✔
215
            // 保存当前实例的native事件,下面创建aNode时候做合并
216
            if (eventBind.modifier.native) {
22✔
217
                // native事件数组
218
                this.nativeEvents = this.nativeEvents || [];
6✔
219
                this.nativeEvents.push(eventBind);
6✔
220
            }
221
            else {
222
                // #[begin] error
223
                warnEventListenMethod(eventBind, options.owner);
16✔
224
                // #[end]
225

226
                this.on(
16✔
227
                    eventBind.name,
228
                    getEventListener(eventBind, options.owner, this.scope, 1),
229
                    eventBind
230
                );
231
            }
232
        }
233

234
        this.tagName = this.tagName || this.source.tagName;
464✔
235
        this.binds = this.source._b;
464✔
236
        this.attrs = this.source.attrs;
464✔
237

238
        // init s-bind data
239
        this._srcSbindData = nodeSBindInit(this.source.directives.bind, this.scope, this.owner);
464✔
240
    }
241

242
    this.tagName = this.tagName || 'div';
1,283✔
243
    // #[begin] allua
244
    // ie8- 不支持innerHTML输出自定义标签
245
    /* istanbul ignore if */
246
    if (ieOldThan9 && this.tagName.indexOf('-') > 0) {
247
        this.tagName = 'div';
248
    }
249
    // #[end]
250

251
    this._toPhase('compiled');
1,283✔
252

253

254
    // #[begin] devtool
255
    this._toPhase('beforeInit');
1,283✔
256
    // #[end]
257

258
    // init data
259
    var initData;
1,283✔
260
    try {
1,283✔
261
        initData = typeof this.initData === 'function' && this.initData();
1,283✔
262
    }
263
    catch (e) {
264
        handleError(e, this, 'initData');
1✔
265
    }
266
    initData = extend(initData || {}, options.data || this._srcSbindData);
1,283✔
267

268
    if (this.binds && this.scope) {
1,283✔
269
        for (var i = 0, l = this.binds.length; i < l; i++) {
464✔
270
            var bindInfo = this.binds[i];
489✔
271

272
            var value = evalExpr(bindInfo.expr, this.scope, this.owner);
489✔
273
            if (typeof value !== 'undefined') {
489✔
274
                // See: https://github.com/ecomfe/san/issues/191
275
                initData[bindInfo.name] = value;
434✔
276
            }
277
        }
278

279
        if (this.attrs) {
464✔
280
            initData.$attrs = {};
17✔
281
            for (var i = 0, l = this.attrs.length; i < l; i++) {
17✔
282
                var attr = this.attrs[i];
42✔
283
    
284
                var value = evalExpr(attr.expr, this.scope, this.owner);
42✔
285
                if (typeof value !== 'undefined') {
42!
286
                    // See: https://github.com/ecomfe/san/issues/191
287
                    initData.$attrs[attr.name] = value;
42✔
288
                }
289
            }
290
        }
291
    }
292

293
    this.data = new Data(initData);
1,283✔
294
    if (proxySupported) {
1,283!
295
        this.d = dataProxy(this.data);
1,283✔
296
    }
297

298
    // #[begin] error
299
    // 在初始化 + 数据绑定后,开始数据校验
300
    // NOTE: 只在开发版本中进行属性校验
301
    var dataTypes = this.dataTypes || clazz.dataTypes;
1,283✔
302
    if (dataTypes) {
1,283✔
303
        var dataTypeChecker = createDataTypesChecker(
59✔
304
            dataTypes,
305
            this.name || clazz.name
118✔
306
        );
307
        this.data.setTypeChecker(dataTypeChecker);
59✔
308
        this.data.checkDataTypes();
59✔
309
    }
310
    // #[end]
311

312
    this._computedDeps = {};
1,242✔
313
    this._computedDepsIndex = {};
1,242✔
314
    for (var expr in this.computed) {
1,242✔
315
        if (this.computed.hasOwnProperty(expr) && !this._computedDeps[expr]) {
50✔
316
            this._calcComputed(expr);
42✔
317
        }
318
    }
319

320
    this._initDataChanger();
1,241✔
321
    this._sbindData = nodeSBindInit(this.aNode.directives.bind, this.data, this);
1,241✔
322
    this._toPhase('inited');
1,241✔
323

324
    // #[begin] hydrate
325
    var hydrateWalker = options.hydrateWalker;
1,241✔
326
    var aNode = this.aNode;
1,241✔
327
    if (hydrateWalker) {
1,241✔
328
        if (this.ssr === 'client-render') {
95✔
329
            this.attach(hydrateWalker.target, hydrateWalker.current);
5✔
330
        }
331
        else {
332
            this._toPhase('created');
90✔
333

334
            if (aNode.Clazz || this.components[aNode.tagName]) {
90✔
335
                if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
6✔
336
                    aNode = mergeANodeSourceAttrs(aNode, this.source);
1✔
337
                }
338
                this._rootNode = createHydrateNode(aNode, this, this.data, this, hydrateWalker);
6✔
339
                this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
6✔
340
            }
341
            else {
342
                var currentNode = hydrateWalker.current;
84✔
343
                if (currentNode && currentNode.nodeType === 1) {
84✔
344
                    this.el = currentNode;
84✔
345
                    hydrateWalker.goNext();
84✔
346
                }
347

348
                hydrateElementChildren(this, this.data, this);
84✔
349
            }
350

351
            this._attached();
90✔
352
            this._toPhase('attached');
90✔
353
        }
354
    }
355
    else if (this.el) {
1,146✔
356
        this._toPhase('created');
153✔
357
        
358
        if (aNode.Clazz || this.components[aNode.tagName]) {
153✔
359
            if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
5!
UNCOV
360
                aNode = mergeANodeSourceAttrs(aNode, this.source);
×
361
            }
362
            hydrateWalker = new DOMChildrenWalker(this.el.parentNode, this.el);
5✔
363
            this._rootNode = createHydrateNode(aNode, this, this.data, this, hydrateWalker);
5✔
364
            this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
5✔
365
        }
366
        else {
367
            hydrateElementChildren(this, this.data, this);
148✔
368
        }
369

370
        this._attached();
153✔
371
        this._toPhase('attached');
153✔
372
    }
373
    // #[end]
374
}
375

376

377
/**
378
 * 初始化创建组件外部传入的插槽对象
379
 *
380
 * @protected
381
 * @param {boolean} isFirstTime 是否初次对sourceSlots进行计算
382
 */
383
Component.prototype._initSourceSlots = function (isFirstTime) {
1✔
384
    this.sourceSlots.named = {};
477✔
385

386
    // 组件运行时传入的结构,做slot解析
387
    if (this.source && this.scope) {
477✔
388
        var sourceChildren = this.source.children;
477✔
389

390
        for (var i = 0, l = sourceChildren.length; i < l; i++) {
477✔
391
            var child = sourceChildren[i];
257✔
392
            var target;
257✔
393

394
            var slotBind = !child.textExpr && getANodeProp(child, 'slot');
257✔
395
            if (slotBind) {
257✔
396
                isFirstTime && this.sourceSlotNameProps.push(slotBind);
93✔
397

398
                var slotName = evalExpr(slotBind.expr, this.scope, this.owner);
93✔
399
                target = this.sourceSlots.named[slotName];
93✔
400
                if (!target) {
93✔
401
                    target = this.sourceSlots.named[slotName] = [];
59✔
402
                }
403
                target.push(child);
93✔
404
            }
405
            else if (isFirstTime) {
164!
406
                target = this.sourceSlots.noname;
164✔
407
                if (!target) {
164✔
408
                    target = this.sourceSlots.noname = [];
139✔
409
                }
410
                target.push(child);
164✔
411
            }
412
        }
413
    }
414
};
415

416
/**
417
 * 类型标识
418
 *
419
 * @type {string}
420
 */
421
Component.prototype.nodeType = NodeType.CMPT;
1✔
422

423
/**
424
 * 在下一个更新周期运行函数
425
 *
426
 * @param {Function} fn 要运行的函数
427
 */
428
Component.prototype.nextTick = nextTick;
1✔
429

430
Component.prototype._ctx = (new Date()).getTime().toString(16);
1✔
431

432
/* eslint-disable operator-linebreak */
433
/**
434
 * 使节点到达相应的生命周期
435
 *
436
 * @protected
437
 * @param {string} name 生命周期名称
438
 */
439
Component.prototype._toPhase = function (name) {
1✔
440
    if (!this.lifeCycle[name]) {
15,293✔
441
        this.lifeCycle = LifeCycle[name] || this.lifeCycle;
15,287✔
442
        if (typeof this[name] === 'function') {
15,287✔
443
            try {
142✔
444
                this[name]();
142✔
445
            }
446
            catch (e) {
447
                handleError(e, this, 'hook:' + name);
2✔
448
            }
449
        }
450

451
        this._afterLife = this.lifeCycle;
15,287✔
452

453
        // 通知devtool
454
        // #[begin] devtool
455
        emitDevtool('comp-' + name, this);
15,287✔
456
        // #[end]
457
    }
458
};
459
/* eslint-enable operator-linebreak */
460

461

462
/**
463
 * 添加事件监听器
464
 *
465
 * @param {string} name 事件名
466
 * @param {Function} listener 监听器
467
 * @param {string?} declaration 声明式
468
 */
469
Component.prototype.on = function (name, listener, declaration) {
1✔
470
    if (typeof listener === 'function') {
19!
471
        if (!this.listeners[name]) {
19✔
472
            this.listeners[name] = [];
18✔
473
        }
474
        this.listeners[name].push({fn: listener, declaration: declaration});
19✔
475
    }
476
};
477

478
/**
479
 * 移除事件监听器
480
 *
481
 * @param {string} name 事件名
482
 * @param {Function=} listener 监听器
483
 */
484
Component.prototype.un = function (name, listener) {
1✔
485
    var nameListeners = this.listeners[name];
2✔
486
    var len = nameListeners && nameListeners.length;
2✔
487

488
    while (len--) {
2✔
489
        if (!listener || listener === nameListeners[len].fn) {
2✔
490
            nameListeners.splice(len, 1);
2✔
491
        }
492
    }
493
};
494

495

496
/**
497
 * 派发事件
498
 *
499
 * @param {string} name 事件名
500
 * @param {Object} event 事件对象
501
 */
502
Component.prototype.fire = function (name, event) {
1✔
503
    var me = this;
25✔
504
    // #[begin] devtool
505
    emitDevtool('comp-event', {
25✔
506
        name: name,
507
        event: event,
508
        target: this
509
    });
510
    // #[end]
511

512
    each(this.listeners[name], function (listener) {
25✔
513
        try {
22✔
514
            listener.fn.call(me, event);
22✔
515
        }
516
        catch (e) {
517
            handleError(e, me, 'event:' + name);
1✔
518
        }
519
    });
520
};
521

522

523
var componentComputedProxyHandler = {
1✔
524
    get: function (obj, prop) {
525
        var value = obj[prop];
132✔
526
        if (value && typeof value === 'object') {
132✔
527
            return new Proxy(value, componentComputedProxyHandler);
39✔
528
        }
529
        return value;
93✔
530
    }, 
531
    set: empty
532
};
533

534

535
/**
536
 * 计算 computed 属性的值
537
 *
538
 * @private
539
 * @param {string} computedExpr computed表达式串
540
 */
541
Component.prototype._calcComputed = function (computedExpr) {
1✔
542
    var computedDeps = this._computedDeps[computedExpr];
129✔
543
    var isFirstCalc = false;
129✔
544
    if (!computedDeps) {
129✔
545
        isFirstCalc = true;
50✔
546
        computedDeps = this._computedDeps[computedExpr] = {};
50✔
547
    }
548

549
    var me = this;
129✔
550

551
    var that = {
129✔
552
        data: {
553
            get: function (exprLiteral) {
554
                // #[begin] error
555
                if (!exprLiteral) {
96✔
556
                    throw new Error('[SAN ERROR] call get method in computed need argument');
1✔
557
                }
558
                // #[end]
559

560
                var expr = parseExpr(exprLiteral);
95✔
561
                var firstItem = expr.paths[0].value;
95✔
562

563
                if (!computedDeps[firstItem]) {
95✔
564
                    computedDeps[firstItem] = 1;
39✔
565
                    if (!me._computedDepsIndex[firstItem]) {
39✔
566
                        me._computedDepsIndex[firstItem] = [];
37✔
567
                    }
568
                    me._computedDepsIndex[firstItem].push(computedExpr);
39✔
569

570
                    if (me.computed[firstItem] && !me._computedDeps[firstItem]) {
39✔
571
                        me._calcComputed(firstItem);
4✔
572
                    }
573
                }
574

575
                return me.data.get(expr);
95✔
576
            }
577
        }
578
    };
579

580
    if (proxySupported) {
129!
581
        that.d = new Proxy(me.data.raw, {
129✔
582
            set: empty,
583
            get: function (obj, prop) {
584
                if (!computedDeps[prop]) {
109✔
585
                    computedDeps[prop] = 1;
36✔
586
                    if (!me._computedDepsIndex[prop]) {
36✔
587
                        me._computedDepsIndex[prop] = [];
32✔
588
                    }
589
                    me._computedDepsIndex[prop].push(computedExpr);
36✔
590

591
                    if (me.computed[prop] && !me._computedDeps[prop]) {
36✔
592
                        me._calcComputed(prop);
4✔
593
                    }
594
                }
595

596
                var value = obj[prop];
109✔
597
                if (value && typeof value === 'object') {
109✔
598
                    return new Proxy(value, componentComputedProxyHandler);
54✔
599
                }
600
                return value;
55✔
601
            }
602
        });
603
    }
604

605
    try {
129✔
606
        this.data.set(
129✔
607
            computedExpr, 
608
            this.computed[computedExpr].call(that),
609
            {silent: isFirstCalc}
610
        );
611
    }
612
    catch (e) {
613
        handleError(e, this, 'computed:' + computedExpr);
2✔
614
    }
615
};
616

617
/**
618
 * 派发消息
619
 * 组件可以派发消息,消息将沿着组件树向上传递,直到遇上第一个处理消息的组件
620
 *
621
 * @param {string} name 消息名称
622
 * @param {*?} value 消息值
623
 */
624
Component.prototype.dispatch = function (name, value) {
1✔
625
    var parentComponent = this.parentComponent;
27✔
626

627
    while (parentComponent) {
27✔
628
        var handler = parentComponent.messages[name] || parentComponent.messages['*'];
28✔
629
        if (typeof handler === 'function') {
28✔
630
            // #[begin] devtool
631
            emitDevtool('comp-message', {
27✔
632
                target: this,
633
                value: value,
634
                name: name,
635
                receiver: parentComponent
636
            });
637
            // #[end]
638

639
            try {
27✔
640
                handler.call(
27✔
641
                    parentComponent,
642
                    {target: this, value: value, name: name}
643
                );
644
            }
645
            catch (e) {
646
                handleError(e, parentComponent, 'message:' + (name || '*'));
1!
647
            }
648
            return;
27✔
649
        }
650

651
        parentComponent = parentComponent.parentComponent;
1✔
652
    }
653

654
    // #[begin] devtool
UNCOV
655
    emitDevtool('comp-message', {target: this, value: value, name: name});
×
656
    // #[end]
657
};
658

659
/**
660
 * 获取组件内部的 slot
661
 *
662
 * @param {string=} name slot名称,空为default slot
663
 * @return {Array}
664
 */
665
Component.prototype.slot = function (name) {
1✔
666
    var result = [];
24✔
667
    var me = this;
24✔
668

669
    function childrenTraversal(children) {
1✔
670
        each(children, function (child) {
70✔
671
            if (child.nodeType === NodeType.SLOT && child.owner === me) {
99✔
672
                if (child.isNamed && child.name === name
53✔
673
                    || !child.isNamed && !name
674
                ) {
675
                    result.push(child);
26✔
676
                }
677
            }
678
            else {
679
                childrenTraversal(child.children);
46✔
680
            }
681
        });
682
    }
683

684
    childrenTraversal(this.children);
24✔
685
    return result;
24✔
686
};
687

688
/**
689
 * 获取带有 san-ref 指令的子组件引用
690
 *
691
 * @param {string} name 子组件的引用名
692
 * @return {Component}
693
 */
694
Component.prototype.ref = function (name) {
1✔
695
    var refTarget;
77✔
696
    var owner = this;
77✔
697

698
    function childrenTraversal(children) {
1✔
699
        if (children) {
167✔
700
            for (var i = 0, l = children.length; i < l; i++) {
132✔
701
                elementTraversal(children[i]);
146✔
702
                if (refTarget) {
146✔
703
                    return;
99✔
704
                }
705
            }
706
        }
707
    }
708

709
    function elementTraversal(element) {
1✔
710
        var nodeType = element.nodeType;
148✔
711
        if (nodeType === NodeType.TEXT) {
148✔
712
            return;
22✔
713
        }
714

715
        if (element.owner === owner) {
126✔
716
            var ref;
117✔
717
            switch (element.nodeType) {
117✔
718
                case NodeType.ELEM:
99✔
719
                    ref = element.aNode.directives.ref;
23✔
720
                    if (ref && evalExpr(ref.value, element.scope, owner) === name) {
23✔
721
                        refTarget = element.el;
6✔
722
                    }
723
                    break;
23✔
724

725
                case NodeType.CMPT:
726
                    ref = element.source.directives.ref;
76✔
727
                    if (ref && evalExpr(ref.value, element.scope, owner) === name) {
76✔
728
                        refTarget = element;
68✔
729
                    }
730
            }
731

732
            if (refTarget) {
117✔
733
                return;
74✔
734
            }
735

736
            childrenTraversal(element.slotChildren);
43✔
737
        }
738

739
        if (refTarget) {
52✔
740
            return;
3✔
741
        }
742

743
        childrenTraversal(element.children);
49✔
744
    }
745

746
    this._rootNode ? elementTraversal(this._rootNode) : childrenTraversal(this.children);
77✔
747

748
    return refTarget;
77✔
749
};
750

751

752
/**
753
 * 视图更新函数
754
 *
755
 * @param {Array?} changes 数据变化信息
756
 */
757
Component.prototype._update = function (changes) {
1✔
758
    if (this.lifeCycle.disposed) {
1,208✔
759
        return;
150✔
760
    }
761

762
    var me = this;
1,058✔
763

764

765
    var needReloadForSlot = false;
1,058✔
766
    this._notifyNeedReload = function () {
1,058✔
767
        needReloadForSlot = true;
36✔
768
    };
769

770
    if (changes) {
1,058✔
771
        if (this.source) {
274✔
772
            this._srcSbindData = nodeSBindUpdate(
272✔
773
                this.source.directives.bind,
774
                this._srcSbindData,
775
                this.scope,
776
                this.owner,
777
                changes,
778
                function (name, value) {
779
                    if (name in me.source._pi) {
14✔
780
                        return;
2✔
781
                    }
782

783
                    me.data.set(name, value, {
12✔
784
                        target: {
785
                            node: me.owner
786
                        }
787
                    });
788
                }
789
            );
790
        }
791

792
        each(changes, function (change) {
274✔
793
            var changeExpr = change.expr;
411✔
794

795
            each(me.binds, function (bindItem) {
411✔
796
                var relation;
602✔
797
                var setExpr = bindItem.name;
602✔
798
                var updateExpr = bindItem.expr;
602✔
799

800
                if (!isDataChangeByElement(change, me, setExpr)
602✔
801
                    && (relation = changeExprCompare(changeExpr, updateExpr, me.scope))
802
                ) {
803
                    if (relation > 2) {
217✔
804
                        setExpr = {
41✔
805
                            type: ExprType.ACCESSOR,
806
                            paths: [
807
                                {
808
                                    type: ExprType.STRING,
809
                                    value: setExpr
810
                                }
811
                            ].concat(changeExpr.paths.slice(updateExpr.paths.length))
812
                        };
813
                        updateExpr = changeExpr;
41✔
814
                    }
815

816
                    if (relation >= 2 && change.type === DataChangeType.SPLICE) {
217✔
817
                        me.data.splice(setExpr, [change.index, change.deleteCount].concat(change.insertions), {
18✔
818
                            target: {
819
                                node: me.owner
820
                            }
821
                        });
822
                    }
823
                    else {
824
                        me.data.set(setExpr, evalExpr(updateExpr, me.scope, me.owner), {
199✔
825
                            target: {
826
                                node: me.owner
827
                            }
828
                        });
829
                    }
830
                }
831
            });
832

833
            each(me.attrs, function (bindItem) {
411✔
834
                if (changeExprCompare(changeExpr, bindItem.expr, me.scope)) {
54✔
835
                    me.data.set(
34✔
836
                        bindItem._data,
837
                        evalExpr(bindItem.expr, me.scope, me.owner)
838
                    );
839
                }
840
            });
841

842
            each(me.sourceSlotNameProps, function (bindItem) {
411✔
843
                needReloadForSlot = needReloadForSlot || changeExprCompare(changeExpr, bindItem.expr, me.scope);
142✔
844
                return !needReloadForSlot;
142✔
845
            });
846
        });
847

848
        if (needReloadForSlot) {
274✔
849
            this._initSourceSlots();
4✔
850
            this._repaintChildren();
4✔
851
        }
852
        else {
853
            var slotChildrenLen = this.slotChildren.length;
270✔
854
            while (slotChildrenLen--) {
270✔
855
                var slotChild = this.slotChildren[slotChildrenLen];
224✔
856

857
                if (slotChild.lifeCycle.disposed) {
224✔
858
                    this.slotChildren.splice(slotChildrenLen, 1);
6✔
859
                }
860
                else if (slotChild.isInserted) {
218✔
861
                    slotChild._update(changes, 1);
161✔
862
                }
863
            }
864
        }
865
    }
866

867
    var dataChanges = this._dataChanges;
1,058✔
868
    if (dataChanges) {
1,058✔
869
        // #[begin] devtool
870
        this._toPhase('beforeUpdate');
907✔
871
        // #[end]
872

873
        this._dataChanges = null;
907✔
874

875
        this._sbindData = nodeSBindUpdate(
907✔
876
            this.aNode.directives.bind,
877
            this._sbindData,
878
            this.data,
879
            this,
880
            dataChanges,
881
            function (name, value) {
882
                if (me._rootNode || (name in me.aNode._pi)) {
18✔
883
                    return;
3✔
884
                }
885

886
                getPropHandler(me.tagName, name)(me.el, value, name, me);
15✔
887
            }
888
        );
889

890
        var htmlDirective = this.aNode.directives.html;
907✔
891

892
        if (this._rootNode) {
907✔
893
            this._rootNode._update(dataChanges);
52✔
894
            this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
52✔
895
        }
896
        else if (htmlDirective) {
855✔
897
            var len = dataChanges.length;
1✔
898
            while (len--) {
1✔
899
                if (changeExprCompare(dataChanges[len].expr, htmlDirective.value, this.data)) {
1!
900
                    // #[begin] error
901
                    warnSetHTML(this.el);
1✔
902
                    // #[end]
903

904
                    this.el.innerHTML = evalExpr(htmlDirective.value, this.data, this);
1✔
905
                    break;
1✔
906
                }
907
            }
908
        }
909
        else {
910
            var dynamicProps = this.aNode._dp;
854✔
911
            for (var i = 0; i < dynamicProps.length; i++) {
854✔
912
                var prop = dynamicProps[i];
2,593✔
913

914
                for (var j = 0; j < dataChanges.length; j++) {
2,593✔
915
                    var change = dataChanges[j];
3,528✔
916
                    if (changeExprCompare(change.expr, prop.expr, this.data)
3,528!
917
                        || prop.hintExpr && changeExprCompare(change.expr, prop.hintExpr, this.data)
918
                    ) {
919
                        prop.handler(this.el, evalExpr(prop.expr, this.data, this), prop.name, this);
65✔
920
                        break;
65✔
921
                    }
922
                }
923
            }
924

925
            if (this.attrs && this.inheritAttrs) {
854✔
926
                var attrsData = this.data.get('$attrs');
10✔
927

928
                for (var i = 0; i < this.attrs.length; i++) {
10✔
929
                    var attr = this.attrs[i];
26✔
930

931
                    if (this.aNode._pi[attr.name] == null) {
26✔
932
                        for (var j = 0; j < dataChanges.length; j++) {
24✔
933
                            var changePaths = dataChanges[j].expr.paths;
34✔
934

935
                            if (changePaths[0].value === '$attrs' && changePaths[1].value === attr.name) {
34✔
936
                                getPropHandler(this.tagName, attr.name)(this.el, attrsData[attr.name], attr.name, this);
16✔
937
                                break;
16✔
938
                            }
939
                        }
940
                    }
941
                }
942
            }
943

944
            for (var i = 0; i < this.children.length; i++) {
854✔
945
                this.children[i]._update(dataChanges);
1,216✔
946
            }
947
        }
948

949
        if (needReloadForSlot) {
907✔
950
            this._initSourceSlots();
9✔
951
            this._repaintChildren();
9✔
952
        }
953

954
        for (var i = 0; i < this.implicitChildren.length; i++) {
907✔
955
            this.implicitChildren[i]._update(dataChanges);
4✔
956
        }
957

958
        if (typeof this.updated === 'function') {
907✔
959
            this.updated();
5✔
960
        }
961

962
        if (this.owner && this._updateBindxOwner(dataChanges)) {
907✔
963
            this.owner._update();
25✔
964
        }
965
    }
966

967
    this._notifyNeedReload = null;
1,058✔
968
};
969

970
Component.prototype._updateBindxOwner = function (dataChanges) {
1✔
971
    var me = this;
229✔
972
    var xbindUped;
229✔
973

974
    each(dataChanges, function (change) {
229✔
975
        each(me.binds, function (bindItem) {
317✔
976
            var changeExpr = change.expr;
515✔
977
            if (bindItem.x
515✔
978
                && !isDataChangeByElement(change, me.owner)
979
                && changeExprCompare(changeExpr, parseExpr(bindItem.name), me.data)
980
            ) {
981
                var updateScopeExpr = bindItem.expr;
32✔
982
                if (changeExpr.paths.length > 1) {
32✔
983
                    updateScopeExpr = {
13✔
984
                        type: ExprType.ACCESSOR,
985
                        paths: bindItem.expr.paths.concat(changeExpr.paths.slice(1))
986
                    };
987
                }
988

989
                xbindUped = 1;
32✔
990
                me.scope.set(
32✔
991
                    updateScopeExpr,
992
                    evalExpr(changeExpr, me.data, me),
993
                    {
994
                        target: {
995
                            node: me,
996
                            prop: bindItem.name
997
                        }
998
                    }
999
                );
1000
            }
1001
        });
1002
    });
1003

1004
    return xbindUped;
229✔
1005
};
1006

1007
/**
1008
 * 重新绘制组件的内容
1009
 * 当 dynamic slot name 发生变更或 slot 匹配发生变化时,重新绘制
1010
 * 在组件级别重绘有点粗暴,但是能保证视图结果正确性
1011
 */
1012
Component.prototype._repaintChildren = function () {
1✔
1013
    // TODO: repaint once?
1014

1015
    if (this._rootNode) {
13!
UNCOV
1016
        var parentEl = this._rootNode.el.parentNode;
×
UNCOV
1017
        var beforeEl = this._rootNode.el.nextSibling;
×
UNCOV
1018
        this._rootNode.dispose(0, 1);
×
UNCOV
1019
        this.slotChildren = [];
×
1020

UNCOV
1021
        var aNode = this.aNode;
×
UNCOV
1022
        if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
×
UNCOV
1023
            aNode = mergeANodeSourceAttrs(aNode, this.source);
×
1024
        }
1025

UNCOV
1026
        this._rootNode = createNode(aNode, this, this.data, this);
×
UNCOV
1027
        this._rootNode.attach(parentEl, beforeEl);
×
UNCOV
1028
        this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
×
1029
    }
1030
    else {
1031
        elementDisposeChildren(this.children, 0, 1);
13✔
1032
        this.children = [];
13✔
1033
        this.slotChildren = [];
13✔
1034

1035
        for (var i = 0, l = this.aNode.children.length; i < l; i++) {
13✔
1036
            var child = createNode(this.aNode.children[i], this, this.data, this);
52✔
1037
            this.children.push(child);
52✔
1038
            child.attach(this.el);
52✔
1039
        }
1040
    }
1041
};
1042

1043

1044
/**
1045
 * 初始化组件内部监听数据变化
1046
 *
1047
 * @private
1048
 * @param {Object} change 数据变化信息
1049
 */
1050
Component.prototype._initDataChanger = function () {
1✔
1051
    var me = this;
1,241✔
1052

1053
    this._dataChanger = function (change) {
1,241✔
1054
        if (me._afterLife.created) {
1,477✔
1055
            if (!me._dataChanges) {
1,255✔
1056
                nextTick(me._update, me);
909✔
1057
                me._dataChanges = [];
909✔
1058
            }
1059

1060
            me._dataChanges.push(change);
1,255✔
1061
        }
1062
        else if (me.lifeCycle.inited && me.owner) {
222✔
1063
            me._updateBindxOwner([change]);
1✔
1064
        }
1065

1066
        var changeItem = change.expr.paths[0].value;
1,477✔
1067
        var depComputeds = me._computedDepsIndex[changeItem];
1,477✔
1068
        if (depComputeds) {
1,477✔
1069
            for (var i = 0; i < depComputeds.length; i++) {
68✔
1070
                me._calcComputed(depComputeds[i]);
79✔
1071
            }
1072
        }
1073
    };
1074

1075
    this.data.listen(this._dataChanger);
1,241✔
1076
};
1077

1078

1079
/**
1080
 * 监听组件的数据变化
1081
 *
1082
 * @param {string} dataName 变化的数据项
1083
 * @param {Function} listener 监听函数
1084
 */
1085
Component.prototype.watch = function (dataName, listener) {
1✔
1086
    var dataExpr = parseExpr(dataName);
9✔
1087
    var value = evalExpr(dataExpr, this.data, this);
9✔
1088
    var me = this;
9✔
1089

1090
    this.data.listen(function (change) {
9✔
1091
        if (changeExprCompare(change.expr, dataExpr, me.data)) {
16✔
1092
            var newValue = evalExpr(dataExpr, me.data, me);
13✔
1093

1094
            if (newValue !== value) {
13!
1095
                var oldValue = value;
13✔
1096
                value = newValue;
13✔
1097

1098
                try {
13✔
1099
                    listener.call(
13✔
1100
                        me,
1101
                        newValue,
1102
                        {
1103
                            oldValue: oldValue,
1104
                            newValue: newValue,
1105
                            change: change
1106
                        }
1107
                    );
1108
                }
1109
                catch (e) {
1110
                    handleError(e, me, 'watch:' + dataName);
1✔
1111
                }
1112
            }
1113
        }
1114
    });
1115
};
1116

1117
Component.prototype._getElAsRootNode = function () {
1✔
1118
    return this.el;
34✔
1119
};
1120

1121
/**
1122
 * 将组件attach到页面
1123
 *
1124
 * @param {HTMLElement} parentEl 要添加到的父元素
1125
 * @param {HTMLElement=} beforeEl 要添加到哪个元素之前
1126
 */
1127
Component.prototype.attach = function (parentEl, beforeEl) {
1✔
1128
    if (!this.lifeCycle.attached) {
986✔
1129
        // #[begin] devtool
1130
        this._toPhase('beforeAttach');
985✔
1131
        // #[end]
1132

1133
        var aNode = this.aNode;
985✔
1134

1135
        if (aNode.Clazz || this.components[aNode.tagName]) {
985✔
1136
            // #[begin] devtool
1137
            this._toPhase('beforeCreate');
43✔
1138
            // #[end]
1139

1140
            // aNode.Clazz 在 preheat 阶段为 if/else/for/fragment 等特殊标签或指令预热生成
1141
            // 这里不能用 this.components[aNode.tagName] 判断,因为可能特殊指令和组件在同一个节点上并存
1142
            if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
43✔
1143
                aNode = mergeANodeSourceAttrs(aNode, this.source);
1✔
1144
            }
1145

1146
            this._rootNode = this._rootNode || createNode(aNode, this, this.data, this);
43✔
1147
            this._rootNode.attach(parentEl, beforeEl);
43✔
1148
            this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
43✔
1149
            this._toPhase('created');
43✔
1150
        }
1151
        else {
1152
            if (!this.el) {
942✔
1153
                // #[begin] devtool
1154
                this._toPhase('beforeCreate');
937✔
1155
                // #[end]
1156

1157
                var props;
937✔
1158
                var doc = parentEl.ownerDocument;
937✔
1159
                if (aNode._ce && aNode._i > 2) {
937✔
1160
                    props = aNode._dp;
40✔
1161
                    this.el = (aNode._el || preheatEl(aNode, doc)).cloneNode(false);
40✔
1162
                }
1163
                else {
1164
                    props = aNode.props;
897✔
1165
                    this.el = svgTags[this.tagName] && doc.createElementNS
897✔
1166
                        ? doc.createElementNS('http://www.w3.org/2000/svg', this.tagName)
897✔
1167
                        : doc.createElement(this.tagName);
1168
                }
1169

1170
                if (this._sbindData) {
937✔
1171
                    for (var key in this._sbindData) {
1✔
1172
                        if (this._sbindData.hasOwnProperty(key)) {
6!
1173
                            getPropHandler(this.tagName, key)(
6✔
1174
                                this.el,
1175
                                this._sbindData[key],
1176
                                key,
1177
                                this
1178
                            );
1179
                        }
1180
                    }
1181
                }
1182

1183
                for (var i = 0, l = props.length; i < l; i++) {
937✔
1184
                    var prop = props[i];
2,896✔
1185
                    var value = evalExpr(prop.expr, this.data, this);
2,896✔
1186

1187
                    if (value || !styleProps[prop.name]) {
2,896✔
1188
                        prop.handler(this.el, value, prop.name, this);
1,093✔
1189
                    }
1190
                }
1191

1192
                if (this.attrs && this.inheritAttrs) {
937✔
1193
                    var attrsData = this.data.get('$attrs');
5✔
1194
                    for (var i = 0; i < this.attrs.length; i++) {
5✔
1195
                        var attr = this.attrs[i];
13✔
1196
                        if (this.aNode._pi[attr.name] == null) {
13✔
1197
                            getPropHandler(this.tagName, attr.name)(this.el, attrsData[attr.name], attr.name, this);
12✔
1198
                        }
1199
                    }
1200
                }
1201

1202
                this._toPhase('created');
937✔
1203
            }
1204

1205
            insertBefore(this.el, parentEl, beforeEl);
942✔
1206

1207
            if (!this._contentReady) {
942✔
1208
                var htmlDirective = aNode.directives.html;
937✔
1209

1210
                if (htmlDirective) {
937✔
1211
                    // #[begin] error
1212
                    warnSetHTML(this.el);
1✔
1213
                    // #[end]
1214

1215
                    this.el.innerHTML = evalExpr(htmlDirective.value, this.data, this);
1✔
1216
                }
1217
                else {
1218
                    for (var i = 0, l = aNode.children.length; i < l; i++) {
936✔
1219
                        var childANode = aNode.children[i];
1,223✔
1220
                        var child = childANode.Clazz
1,223✔
1221
                            ? new childANode.Clazz(childANode, this, this.data, this)
1,223✔
1222
                            : createNode(childANode, this, this.data, this);
1223
                        this.children.push(child);
1,222✔
1224
                        child.attach(this.el);
1,222✔
1225
                    }
1226
                }
1227

1228
                this._contentReady = 1;
936✔
1229
            }
1230

1231
            this._attached();
941✔
1232
        }
1233

1234
        this._toPhase('attached');
984✔
1235

1236
        // element 都是内部创建的,只有动态创建的 component 才会进入这个分支
1237
        if (this.owner && !this.parent) {
984✔
1238
            this.owner.implicitChildren.push(this);
6✔
1239
        }
1240
    }
1241
};
1242

1243
Component.prototype.detach = elementOwnDetach;
1✔
1244
Component.prototype.dispose = elementOwnDispose;
1✔
1245
Component.prototype._attached = elementOwnAttached;
1✔
1246
Component.prototype._leave = function () {
1✔
1247
    if (this.leaveDispose) {
1,223✔
1248
        if (!this.lifeCycle.disposed) {
1,211!
1249
            // #[begin] devtool
1250
            this._toPhase('beforeDetach');
1,211✔
1251
            // #[end]
1252
            this.data.unlisten();
1,211✔
1253
            this.dataChanger = null;
1,211✔
1254
            this._dataChanges = null;
1,211✔
1255

1256
            var len = this.implicitChildren.length;
1,211✔
1257
            while (len--) {
1,211✔
1258
                this.implicitChildren[len].dispose(0, 1);
6✔
1259
            }
1260

1261
            this.implicitChildren = null;
1,211✔
1262

1263
            this.source = null;
1,211✔
1264
            this.sourceSlots = null;
1,211✔
1265
            this.sourceSlotNameProps = null;
1,211✔
1266

1267
            // 这里不用挨个调用 dispose 了,因为 children 释放链会调用的
1268
            this.slotChildren = null;
1,211✔
1269

1270

1271
            if (this._rootNode) {
1,211✔
1272
                // 如果没有parent,说明是一个root component,一定要从dom树中remove
1273
                this._rootNode.dispose(this.disposeNoDetach && this.parent);
48✔
1274
            }
1275
            else {
1276
                var len = this.children.length;
1,163✔
1277
                while (len--) {
1,163✔
1278
                    this.children[len].dispose(1, 1);
1,528✔
1279
                }
1280

1281
                if (this._elFns) {
1,163✔
1282
                    len = this._elFns.length;
26✔
1283
                    while (len--) {
26✔
1284
                        var fn = this._elFns[len];
33✔
1285
                        un(this.el, fn[0], fn[1], fn[2]);
33✔
1286
                    }
1287
                    this._elFns = null;
26✔
1288
                }
1289

1290
                // #[begin] allua
1291
                /* istanbul ignore if */
1292
                if (this._inputTimer) {
1293
                    clearInterval(this._inputTimer);
1294
                    this._inputTimer = null;
1295
                }
1296
                // #[end]
1297

1298
                // 如果没有parent,说明是一个root component,一定要从dom树中remove
1299
                if (!this.disposeNoDetach || !this.parent) {
1,163✔
1300
                    removeEl(this.el);
795✔
1301
                }
1302
            }
1303

1304
            this._toPhase('detached');
1,211✔
1305

1306
            // #[begin] devtool
1307
            this._toPhase('beforeDispose');
1,211✔
1308
            // #[end]
1309

1310
            this._rootNode = null;
1,211✔
1311
            this.el = null;
1,211✔
1312
            this.owner = null;
1,211✔
1313
            this.scope = null;
1,211✔
1314
            this.children = null;
1,211✔
1315

1316
            this._toPhase('disposed');
1,211✔
1317

1318
            if (this._ondisposed) {
1,211✔
1319
                this._ondisposed();
15✔
1320
            }
1321
        }
1322
    }
1323
    else if (this.lifeCycle.attached) {
12✔
1324
        // #[begin] devtool
1325
        this._toPhase('beforeDetach');
11✔
1326
        // #[end]
1327

1328
        if (this._rootNode) {
11✔
1329
            if (this._rootNode.detach) {
5✔
1330
                this._rootNode.detach();
1✔
1331
            }
1332
            else {
1333
                this._rootNode.dispose();
4✔
1334
                this._rootNode = null;
4✔
1335
            }
1336
        }
1337
        else {
1338
            removeEl(this.el);
6✔
1339
        }
1340

1341
        this._toPhase('detached');
11✔
1342
    }
1343
};
1344

1345

1346
exports = module.exports = Component;
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