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

baidu / san / 20640330310

01 Jan 2026 02:31PM UTC coverage: 95.174% (-0.03%) from 95.201%
20640330310

push

github

errorrik
bump 3.15.5

2013 of 2206 branches covered (91.25%)

Branch coverage included in aggregate %.

3627 of 3720 relevant lines covered (97.5%)

712.13 hits per line

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

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

56

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

59

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

77

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

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

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

91
    var clazz = this.constructor;
1,311✔
92

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

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

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

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

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

127
    var proto = clazz.prototype;
1,311✔
128

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

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

147
    preheatANode(proto.aNode, this);
1,309✔
148

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

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

157

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

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

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

208

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

213
        for (var i = 0, l = this.source.events.length; i < l; i++) {
477✔
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;
477✔
235
        this.binds = this.source._b;
477✔
236
        this.attrs = this.source.attrs;
477✔
237

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

242
    this.tagName = this.tagName || 'div';
1,309✔
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,309✔
252

253

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

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

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

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

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

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

298
    // #[begin] error
299
    // 在初始化 + 数据绑定后,开始数据校验
300
    // NOTE: 只在开发版本中进行属性校验
301
    var dataTypes = this.dataTypes || clazz.dataTypes;
1,309✔
302
    if (dataTypes) {
1,309✔
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,268✔
313
    this._computedDepsIndex = {};
1,268✔
314
    for (var expr in this.computed) {
1,268✔
315
        if (this.computed.hasOwnProperty(expr) && !this._computedDeps[expr]) {
54✔
316
            this._calcComputed(expr);
45✔
317
        }
318
    }
319

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

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

334
            if (aNode.Clazz || this.components[aNode.tagName]) {
96✔
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;
90✔
343
                if (currentNode && currentNode.nodeType === 1) {
90✔
344
                    this.el = currentNode;
90✔
345
                    hydrateWalker.goNext();
90✔
346
                }
347

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

351
            this._attached();
96✔
352
            this._toPhase('attached');
96✔
353
        }
354
    }
355
    else if (this.el) {
1,166✔
356
        this._toPhase('created');
158✔
357
        
358
        if (aNode.Clazz || this.components[aNode.tagName]) {
158✔
359
            if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
5!
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);
153✔
368
        }
369

370
        this._attached();
158✔
371
        this._toPhase('attached');
158✔
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 = {};
490✔
385

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

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

394
            var slotBind = !child.textExpr && getANodeProp(child, 'slot');
268✔
395
            if (slotBind) {
268✔
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) {
175!
406
                target = this.sourceSlots.noname;
175✔
407
                if (!target) {
175✔
408
                    target = this.sourceSlots.noname = [];
150✔
409
                }
410
                target.push(child);
175✔
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,608✔
441
        this.lifeCycle = LifeCycle[name] || this.lifeCycle;
15,602✔
442
        if (typeof this[name] === 'function') {
15,602✔
443
            try {
146✔
444
                this[name]();
146✔
445
            }
446
            catch (e) {
447
                handleError(e, this, 'hook:' + name);
2✔
448
            }
449
        }
450

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

453
        // 通知devtool
454
        // #[begin] devtool
455
        emitDevtool('comp-' + name, this);
15,602✔
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
    set: emptyReturnTrue,
525
    get: function (obj, prop) {
526
        var value = obj[prop];
132✔
527
        if (value && typeof value === 'object') {
132✔
528
            return new Proxy(value, componentComputedProxyHandler);
39✔
529
        }
530
        return value;
93✔
531
    }
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];
135✔
543
    var isFirstCalc = false;
135✔
544
    if (!computedDeps) {
135✔
545
        isFirstCalc = true;
54✔
546
        computedDeps = this._computedDeps[computedExpr] = {};
54✔
547
    }
548

549
    var me = this;
135✔
550

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

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

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

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

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

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

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

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

605
    try {
135✔
606
        this.data.set(
135✔
607
            {type: ExprType.ACCESSOR, paths: [{type: ExprType.STRING, value: 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
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,239✔
759
        return;
160✔
760
    }
761

762
    var me = this;
1,079✔
763

764

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

770
    if (changes) {
1,079✔
771
        if (changes.length > 1) {
284✔
772
            changes = removeExcrescentChanges(changes);
84✔
773
        }
774
        
775
        if (this.source) {
284✔
776
            this._srcSbindData = nodeSBindUpdate(
282✔
777
                this.source.directives.bind,
778
                this._srcSbindData,
779
                this.scope,
780
                this.owner,
781
                changes,
782
                function (name, value) {
783
                    if (name in me.source._pi) {
14✔
784
                        return;
2✔
785
                    }
786

787
                    me.data.set(name, value, {
12✔
788
                        target: {
789
                            node: me.owner
790
                        }
791
                    });
792
                }
793
            );
794
        }
795

796
        for (var i = 0; i < changes.length; i++) {
284✔
797
            var change = changes[i];
411✔
798
            var changeExpr = change.expr;
411✔
799

800
            each(me.binds, function (bindItem) {
411✔
801
                var relation;
615✔
802
                var setExpr = bindItem.name;
615✔
803
                var updateExpr = bindItem.expr;
615✔
804

805
                if (!isDataChangeByElement(change, me, setExpr)
615✔
806
                    && (relation = changeExprCompare(changeExpr, updateExpr, me.scope))
807
                ) {
808
                    if (relation > 2) {
221✔
809
                        setExpr = {
42✔
810
                            type: ExprType.ACCESSOR,
811
                            paths: [
812
                                {
813
                                    type: ExprType.STRING,
814
                                    value: setExpr
815
                                }
816
                            ].concat(changeExpr.paths.slice(updateExpr.paths.length))
817
                        };
818
                        updateExpr = changeExpr;
42✔
819
                    }
820

821
                    if (relation >= 2 && change.type === DataChangeType.SPLICE) {
221✔
822
                        me.data.splice(setExpr, [change.index, change.deleteCount].concat(change.insertions), {
19✔
823
                            target: {
824
                                node: me.owner
825
                            }
826
                        });
827
                    }
828
                    else {
829
                        me.data.set(setExpr, evalExpr(updateExpr, me.scope, me.owner), {
202✔
830
                            target: {
831
                                node: me.owner
832
                            }
833
                        });
834
                    }
835
                }
836
            });
837

838
            each(me.attrs, function (bindItem) {
411✔
839
                if (changeExprCompare(changeExpr, bindItem.expr, me.scope)) {
72✔
840
                    me.data.set(
50✔
841
                        bindItem._data,
842
                        evalExpr(bindItem.expr, me.scope, me.owner)
843
                    );
844
                }
845
            });
846

847
            each(me.sourceSlotNameProps, function (bindItem) {
411✔
848
                needReloadForSlot = needReloadForSlot || changeExprCompare(changeExpr, bindItem.expr, me.scope);
142✔
849
                return !needReloadForSlot;
142✔
850
            });
851
        }
852

853
        if (needReloadForSlot) {
284✔
854
            this._initSourceSlots();
4✔
855
            this._repaintChildren();
4✔
856
        }
857
        else {
858
            var slotChildrenLen = this.slotChildren.length;
280✔
859
            while (slotChildrenLen--) {
280✔
860
                var slotChild = this.slotChildren[slotChildrenLen];
232✔
861

862
                if (slotChild.lifeCycle.disposed) {
232✔
863
                    this.slotChildren.splice(slotChildrenLen, 1);
6✔
864
                }
865
                else if (slotChild.isInserted) {
226✔
866
                    slotChild._update(changes, 1);
169✔
867
                }
868
            }
869
        }
870
    }
871

872
    var dataChanges = this._dataChanges;
1,079✔
873
    if (dataChanges) {
1,079✔
874
        // #[begin] devtool
875
        this._toPhase('beforeUpdate');
928✔
876
        // #[end]
877

878
        this._dataChanges = null;
928✔
879

880
        this._sbindData = nodeSBindUpdate(
928✔
881
            this.aNode.directives.bind,
882
            this._sbindData,
883
            this.data,
884
            this,
885
            dataChanges,
886
            function (name, value) {
887
                if (me._rootNode || (name in me.aNode._pi)) {
18✔
888
                    return;
3✔
889
                }
890

891
                getPropHandler(me.tagName, name)(me.el, value, name, me);
15✔
892
            }
893
        );
894

895
        var htmlDirective = this.aNode.directives.html;
928✔
896

897
        if (this._rootNode) {
928✔
898
            this._rootNode._update(dataChanges);
52✔
899
            this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
52✔
900
        }
901
        else if (htmlDirective) {
876✔
902
            var len = dataChanges.length;
1✔
903
            while (len--) {
1✔
904
                if (changeExprCompare(dataChanges[len].expr, htmlDirective.value, this.data)) {
1!
905
                    // #[begin] error
906
                    warnSetHTML(this.el);
1✔
907
                    // #[end]
908

909
                    this.el.innerHTML = evalExpr(htmlDirective.value, this.data, this);
1✔
910
                    break;
1✔
911
                }
912
            }
913
        }
914
        else {
915
            var dynamicProps = this.aNode._dp;
875✔
916
            for (var i = 0; i < dynamicProps.length; i++) {
875✔
917
                var prop = dynamicProps[i];
2,656✔
918

919
                for (var j = 0; j < dataChanges.length; j++) {
2,656✔
920
                    var change = dataChanges[j];
3,699✔
921
                    if (changeExprCompare(change.expr, prop.expr, this.data)
3,699!
922
                        || prop.hintExpr && changeExprCompare(change.expr, prop.hintExpr, this.data)
923
                    ) {
924
                        prop.handler(this.el, evalExpr(prop.expr, this.data, this), prop.name, this);
65✔
925
                        break;
65✔
926
                    }
927
                }
928
            }
929

930
            if (this.attrs && this.inheritAttrs) {
875✔
931
                var attrsData = this.data.get('$attrs');
18✔
932

933
                for (var i = 0; i < this.attrs.length; i++) {
18✔
934
                    var attr = this.attrs[i];
44✔
935

936
                    if (this.aNode._pi[attr.name] == null) {
44✔
937
                        for (var j = 0; j < dataChanges.length; j++) {
42✔
938
                            var changePaths = dataChanges[j].expr.paths;
62✔
939

940
                            if (changePaths[0].value === '$attrs' && changePaths[1].value === attr.name) {
62✔
941
                                getPropHandler(this.tagName, attr.name)(this.el, attrsData[attr.name], attr.name, this);
32✔
942
                                break;
32✔
943
                            }
944
                        }
945
                    }
946
                }
947
            }
948

949
            for (var i = 0; i < this.children.length; i++) {
875✔
950
                this.children[i]._update(dataChanges);
1,248✔
951
            }
952
        }
953

954
        if (needReloadForSlot) {
928✔
955
            this._initSourceSlots();
9✔
956
            this._repaintChildren();
9✔
957
        }
958

959
        for (var i = 0; i < this.implicitChildren.length; i++) {
928✔
960
            this.implicitChildren[i]._update(dataChanges);
4✔
961
        }
962

963
        if (typeof this.updated === 'function') {
928✔
964
            this.updated();
5✔
965
        }
966

967
        if (this.owner && this._updateBindxOwner(dataChanges)) {
928✔
968
            this.owner._update();
25✔
969
        }
970
    }
971

972
    this._notifyNeedReload = null;
1,079✔
973
};
974

975
Component.prototype._updateBindxOwner = function (dataChanges) {
1✔
976
    var me = this;
239✔
977
    var xbindUped;
239✔
978

979
    each(dataChanges, function (change) {
239✔
980
        each(me.binds, function (bindItem) {
341✔
981
            var changeExpr = change.expr;
558✔
982
            if (bindItem.x
558✔
983
                && !isDataChangeByElement(change, me.owner)
984
                && changeExprCompare(changeExpr, parseExpr(bindItem.name), me.data)
985
            ) {
986
                var updateScopeExpr = bindItem.expr;
32✔
987
                if (changeExpr.paths.length > 1) {
32✔
988
                    updateScopeExpr = {
13✔
989
                        type: ExprType.ACCESSOR,
990
                        paths: bindItem.expr.paths.concat(changeExpr.paths.slice(1))
991
                    };
992
                }
993

994
                xbindUped = 1;
32✔
995
                me.scope.set(
32✔
996
                    updateScopeExpr,
997
                    evalExpr(changeExpr, me.data, me),
998
                    {
999
                        target: {
1000
                            node: me,
1001
                            prop: bindItem.name
1002
                        }
1003
                    }
1004
                );
1005
            }
1006
        });
1007
    });
1008

1009
    return xbindUped;
239✔
1010
};
1011

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

1020
    if (this._rootNode) {
13!
1021
        var parentEl = this._rootNode.el.parentNode;
×
1022
        var beforeEl = this._rootNode.el.nextSibling;
×
1023
        this._rootNode.dispose(0, 1);
×
1024
        this.slotChildren = [];
×
1025

1026
        var aNode = this.aNode;
×
1027
        if (!aNode.Clazz && this.attrs && this.inheritAttrs) {
×
1028
            aNode = mergeANodeSourceAttrs(aNode, this.source);
×
1029
        }
1030

1031
        this._rootNode = createNode(aNode, this, this.data, this);
×
1032
        this._rootNode.attach(parentEl, beforeEl);
×
1033
        this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
×
1034
    }
1035
    else {
1036
        elementDisposeChildren(this.children, 0, 1);
13✔
1037
        this.children = [];
13✔
1038
        this.slotChildren = [];
13✔
1039

1040
        for (var i = 0, l = this.aNode.children.length; i < l; i++) {
13✔
1041
            var child = createNode(this.aNode.children[i], this, this.data, this);
52✔
1042
            this.children.push(child);
52✔
1043
            child.attach(this.el);
52✔
1044
        }
1045
    }
1046
};
1047

1048

1049
/**
1050
 * 初始化组件内部监听数据变化
1051
 *
1052
 * @private
1053
 * @param {Object} change 数据变化信息
1054
 */
1055
Component.prototype._initDataChanger = function () {
1✔
1056
    var me = this;
1,267✔
1057

1058
    this._dataChanger = function (change) {
1,267✔
1059
        if (me._afterLife.created) {
1,540✔
1060
            if (!me._dataChanges) {
1,312✔
1061
                nextTick(me._update, me);
930✔
1062
                me._dataChanges = [];
930✔
1063
            }
1064

1065
            me._dataChanges.push(change);
1,312✔
1066
        }
1067
        else if (me.lifeCycle.inited && me.owner) {
228✔
1068
            me._updateBindxOwner([change]);
1✔
1069
        }
1070

1071
        var changeItem = change.expr.paths[0].value;
1,540✔
1072
        var depComputeds = me._computedDepsIndex[changeItem];
1,540✔
1073
        if (depComputeds) {
1,540✔
1074
            for (var i = 0; i < depComputeds.length; i++) {
70✔
1075
                me._calcComputed(depComputeds[i]);
81✔
1076
            }
1077
        }
1078
    };
1079

1080
    this.data.listen(this._dataChanger);
1,267✔
1081
};
1082

1083

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

1095
    this.data.listen(function (change) {
9✔
1096
        if (changeExprCompare(change.expr, dataExpr, me.data)) {
16✔
1097
            var newValue = evalExpr(dataExpr, me.data, me);
13✔
1098

1099
            if (newValue !== value) {
13!
1100
                var oldValue = value;
13✔
1101
                value = newValue;
13✔
1102

1103
                try {
13✔
1104
                    listener.call(
13✔
1105
                        me,
1106
                        newValue,
1107
                        {
1108
                            oldValue: oldValue,
1109
                            newValue: newValue,
1110
                            change: change
1111
                        }
1112
                    );
1113
                }
1114
                catch (e) {
1115
                    handleError(e, me, 'watch:' + dataName);
1✔
1116
                }
1117
            }
1118
        }
1119
    });
1120
};
1121

1122
Component.prototype._getElAsRootNode = function () {
1✔
1123
    return this.el;
34✔
1124
};
1125

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

1138
        var aNode = this.aNode;
1,000✔
1139

1140
        if (aNode.Clazz || this.components[aNode.tagName]) {
1,000✔
1141
            // #[begin] devtool
1142
            this._toPhase('beforeCreate');
43✔
1143
            // #[end]
1144

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

1151
            this._rootNode = this._rootNode || createNode(aNode, this, this.data, this);
43✔
1152
            this._rootNode.attach(parentEl, beforeEl);
43✔
1153
            this._rootNode._getElAsRootNode && (this.el = this._rootNode._getElAsRootNode());
43✔
1154
            this._toPhase('created');
43✔
1155
        }
1156
        else {
1157
            if (!this.el) {
957✔
1158
                // #[begin] devtool
1159
                this._toPhase('beforeCreate');
952✔
1160
                // #[end]
1161

1162
                var props;
952✔
1163
                var doc = parentEl.ownerDocument;
952✔
1164
                if (aNode._ce && aNode._i > 2) {
952✔
1165
                    props = aNode._dp;
39✔
1166
                    this.el = (aNode._el || preheatEl(aNode, doc)).cloneNode(false);
39✔
1167
                }
1168
                else {
1169
                    props = aNode.props;
913✔
1170
                    this.el = svgTags[this.tagName] && doc.createElementNS
913✔
1171
                        ? doc.createElementNS('http://www.w3.org/2000/svg', this.tagName)
913✔
1172
                        : doc.createElement(this.tagName);
1173
                }
1174

1175
                if (this._sbindData) {
952✔
1176
                    for (var key in this._sbindData) {
1✔
1177
                        if (this._sbindData.hasOwnProperty(key)) {
6!
1178
                            getPropHandler(this.tagName, key)(
6✔
1179
                                this.el,
1180
                                this._sbindData[key],
1181
                                key,
1182
                                this
1183
                            );
1184
                        }
1185
                    }
1186
                }
1187

1188
                for (var i = 0, l = props.length; i < l; i++) {
952✔
1189
                    var prop = props[i];
2,941✔
1190
                    var value = evalExpr(prop.expr, this.data, this);
2,941✔
1191

1192
                    if (value || !styleProps[prop.name]) {
2,941✔
1193
                        prop.handler(this.el, value, prop.name, this);
1,108✔
1194
                    }
1195
                }
1196

1197
                if (this.attrs && this.inheritAttrs) {
952✔
1198
                    var attrsData = this.data.get('$attrs');
10✔
1199
                    for (var i = 0; i < this.attrs.length; i++) {
10✔
1200
                        var attr = this.attrs[i];
23✔
1201
                        if (this.aNode._pi[attr.name] == null) {
23✔
1202
                            getPropHandler(this.tagName, attr.name)(this.el, attrsData[attr.name], attr.name, this);
22✔
1203
                        }
1204
                    }
1205
                }
1206

1207
                this._toPhase('created');
952✔
1208
            }
1209

1210
            insertBefore(this.el, parentEl, beforeEl);
957✔
1211

1212
            if (!this._contentReady) {
957✔
1213
                var htmlDirective = aNode.directives.html;
952✔
1214

1215
                if (htmlDirective) {
952✔
1216
                    // #[begin] error
1217
                    warnSetHTML(this.el);
1✔
1218
                    // #[end]
1219

1220
                    this.el.innerHTML = evalExpr(htmlDirective.value, this.data, this);
1✔
1221
                }
1222
                else {
1223
                    for (var i = 0, l = aNode.children.length; i < l; i++) {
951✔
1224
                        var childANode = aNode.children[i];
1,246✔
1225
                        var child = childANode.Clazz
1,246✔
1226
                            ? new childANode.Clazz(childANode, this, this.data, this)
1,246✔
1227
                            : createNode(childANode, this, this.data, this);
1228
                        this.children.push(child);
1,245✔
1229
                        child.attach(this.el);
1,245✔
1230
                    }
1231
                }
1232

1233
                this._contentReady = 1;
951✔
1234
            }
1235

1236
            this._attached();
956✔
1237
        }
1238

1239
        this._toPhase('attached');
999✔
1240

1241
        // element 都是内部创建的,只有动态创建的 component 才会进入这个分支
1242
        if (this.owner && !this.parent) {
999✔
1243
            this.owner.implicitChildren.push(this);
6✔
1244
        }
1245
    }
1246
};
1247

1248
Component.prototype.detach = elementOwnDetach;
1✔
1249
Component.prototype.dispose = elementOwnDispose;
1✔
1250
Component.prototype._attached = elementOwnAttached;
1✔
1251
Component.prototype._leave = function () {
1✔
1252
    if (this.leaveDispose) {
1,250✔
1253
        if (!this.lifeCycle.disposed) {
1,238!
1254
            // #[begin] devtool
1255
            this._toPhase('beforeDetach');
1,238✔
1256
            // #[end]
1257
            this.data.unlisten();
1,238✔
1258
            this.dataChanger = null;
1,238✔
1259
            this._dataChanges = null;
1,238✔
1260

1261
            var len = this.implicitChildren.length;
1,238✔
1262
            while (len--) {
1,238✔
1263
                this.implicitChildren[len].dispose(0, 1);
6✔
1264
            }
1265

1266
            this.implicitChildren = null;
1,238✔
1267

1268
            this.source = null;
1,238✔
1269
            this.sourceSlots = null;
1,238✔
1270
            this.sourceSlotNameProps = null;
1,238✔
1271

1272
            // 这里不用挨个调用 dispose 了,因为 children 释放链会调用的
1273
            this.slotChildren = null;
1,238✔
1274

1275

1276
            if (this._rootNode) {
1,238✔
1277
                // 如果没有parent,说明是一个root component,一定要从dom树中remove
1278
                this._rootNode.dispose(this.disposeNoDetach && this.parent);
48✔
1279
            }
1280
            else {
1281
                var len = this.children.length;
1,190✔
1282
                while (len--) {
1,190✔
1283
                    this.children[len].dispose(1, 1);
1,573✔
1284
                }
1285

1286
                if (this._elFns) {
1,190✔
1287
                    len = this._elFns.length;
26✔
1288
                    while (len--) {
26✔
1289
                        var fn = this._elFns[len];
33✔
1290
                        un(this.el, fn[0], fn[1], fn[2]);
33✔
1291
                    }
1292
                    this._elFns = null;
26✔
1293
                }
1294

1295
                // #[begin] allua
1296
                /* istanbul ignore if */
1297
                if (this._inputTimer) {
1298
                    clearInterval(this._inputTimer);
1299
                    this._inputTimer = null;
1300
                }
1301
                // #[end]
1302

1303
                // 如果没有parent,说明是一个root component,一定要从dom树中remove
1304
                if (!this.disposeNoDetach || !this.parent) {
1,190✔
1305
                    removeEl(this.el);
809✔
1306
                }
1307
            }
1308

1309
            this._toPhase('detached');
1,238✔
1310

1311
            // #[begin] devtool
1312
            this._toPhase('beforeDispose');
1,238✔
1313
            // #[end]
1314

1315
            this._rootNode = null;
1,238✔
1316
            this.el = null;
1,238✔
1317
            this.owner = null;
1,238✔
1318
            this.scope = null;
1,238✔
1319
            this.children = null;
1,238✔
1320

1321
            this._toPhase('disposed');
1,238✔
1322

1323
            if (this._ondisposed) {
1,238✔
1324
                this._ondisposed();
15✔
1325
            }
1326
        }
1327
    }
1328
    else if (this.lifeCycle.attached) {
12✔
1329
        // #[begin] devtool
1330
        this._toPhase('beforeDetach');
11✔
1331
        // #[end]
1332

1333
        if (this._rootNode) {
11✔
1334
            if (this._rootNode.detach) {
5✔
1335
                this._rootNode.detach();
1✔
1336
            }
1337
            else {
1338
                this._rootNode.dispose();
4✔
1339
                this._rootNode = null;
4✔
1340
            }
1341
        }
1342
        else {
1343
            removeEl(this.el);
6✔
1344
        }
1345

1346
        this._toPhase('detached');
11✔
1347
    }
1348
};
1349

1350

1351
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