Ember.js 自定义组件编写与测试实践
1. 树菜单组件的数据模型
在开始编写树菜单的代码之前,需要了解组件所使用的底层数据模型。以下是树菜单的数据模型代码:
Montric.MainMenuModel = DS.Model.extend({
name: DS.attr('string'),
nodeType: DS.attr('string'),
parent: DS.belongsTo('mainMenu'),
children: DS.hasMany('mainMenu'),
chart: DS.belongsTo('chart'),
isSelected: false,
isExpanded: false,
hasChildren: function() {
return this.get('children').get('length') > 0;
}.property('children'),
isLeaf: function() {
return this.get('children').get('length') == 0;
}.property('children')
});
这里使用 Ember Data 模型对象作为发送到树菜单组件的数据模型。服务器通过 parent 和 children 属性设置节点之间的链接,Ember Data 确保从服务器加载数据后,模型对象按预期连接。同时,定义了 hasChildren 和 isLeaf 两个辅助属性,用于简化组件内的模板。树菜单组件由 tree-menu 和 tree-menu-item 两个子组件构成,先实现多选功能,后续再扩展支持单选。
2. 定义树菜单组件
树菜单组件由单个模板 components/tree-menu.hbs 组成,其作用是渲染每个顶级节点,模板代码如下:
{{#each node in rootNodes}}
{{tree-menu-node node=node}}
{{/each}}
该模板的实现简单易懂,目前无需为该组件实现组件类,因为没有触发任何操作,Ember.js 提供的默认实现已足够。
3. 定义树菜单项目和树菜单节点组件
树菜单项目组件相对复杂,需要支持渲染正确的展开三角图标,并设置所代表节点的 isExpanded 和 isSelected 属性。其模板代码如下:
{{#if node.hasChildren}}
{{#if node.isExpanded}}
<span class="downarrow" {{action "toggleExpanded"}}></span>
{{else}}
<span class="rightarrow" {{action "toggleExpanded"}}></span>
{{/if}}
<span {{action "toggleExpanded"}}>{{node.name}}</span>
{{else}}
{{view Ember.Checkbox checkedBinding="node.isSelected"}}
<span {{action "toggleSelected"}}>{{node.name}}</span>
{{/if}}
{{#if node.isExpanded}}
{{#each child in node.children}}
<div style="margin-left: 22px;">
{{tree-menu-node node=child}}
</div>
{{/each}}
{{/if}}
该模板会触发 toggleExpanded 和 toggleSelected 两个操作,因此需要重写默认的 Montric.TreeMenuNodeComponent 来捕获这些操作。如果节点有子节点,会渲染展开三角图标和节点名称,两者都可点击并触发 toggleExpanded 操作;如果节点没有子节点,会渲染复选框和节点名称,两者都可点击并触发 toggleSelected 操作。当 isExpanded 属性为 true 时,会递归渲染子节点。
以下是 Montric.TreeMenuNodeComponent 的类定义:
Montric.TreeMenuNodeComponent = Ember.Component.extend({
classNames: ['pointer'],
actions: {
toggleExpanded: function() {
this.toggleProperty('node.isExpanded');
},
toggleSelected: function() {
this.toggleProperty('node.isSelected');
}
}
});
toggleExpanded 函数用于切换节点的 isExpanded 属性, toggleSelected 函数用于切换节点的 isSelected 属性。
4. 支持单选功能
为支持单选,需要添加一个标志 allowMultipleSelections 来告知组件是允许多选还是单选,并将该属性传递给 tree-menu-node 组件。同时,将选中项分配给 tree-menu 组件的 selectedNode 属性,并将该属性传递给每个 tree-menu-node 组件。更新后的树菜单组件模板如下:
{{#each node in rootNodes}}
{{tree-menu-node node=node
allowMultipleSelections=allowMultipleSelections action="selectNode"
selectedNode=selectedNode}}
{{/each}}
更新后的 tree-menu-node 组件模板如下:
{{#if node.hasChildren}}
{{#if node.isExpanded}}
<span class="downarrow" {{action "toggleExpanded"}}></span>
{{else}}
<span class="rightarrow" {{action "toggleExpanded"}}></span>
{{/if}}
<span {{action "toggleExpanded"}}>{{node.name}}</span>
{{else}}
{{#if allowMultipleSelections}}
{{view Ember.Checkbox checkedBinding="node.isSelected"}}
<span {{action "toggleSelected"}}>{{node.name}}</span>
{{else}}
<span {{action "selectNode" node}}
{{bind-attr class=isSelected}}>{{node.name}}</span>
{{/if}}
{{/if}}
{{#if node.isExpanded}}
{{#each child in node.children}}
<div style="margin-left: 22px;">
{{tree-menu-node node=child
action="selectNode" selectedNode=selectedNode}}
</div>
{{/each}}
{{/if}}
当 allowMultipleSelections 为 false 时,点击叶节点会触发 selectNode 操作。如果当前叶节点是选中节点,会添加 CSS 类 is-selected 来标记为蓝色。
更新后的 Montric.TreeMenuNodeComponent 如下:
Montric.TreeMenuNodeComponent = Ember.Component.extend({
classNames: ['pointer'],
actions: {
toggleExpanded: function() {
this.toggleProperty('node.isExpanded');
},
toggleSelected: function() {
this.toggleProperty('node.isSelected');
},
selectNode: function(node) {
this.sendAction('action', node);
}
},
isSelected: function() {
return this.get('selectedNode') === this.get('node.id');
}.property('selectedNode', 'node.id')
});
新的 Montric.TreeMenuComponent 如下:
Montric.TreeMenuComponent = Ember.Component.extend({
classNames: ['selectableList'],
actions: {
selectNode: function(node) {
this.set('selectedNode', node.get('id'));
}
}
});
最后,将更新后的树菜单组件添加到 alert.hbs 模板中,指定单选并将选中节点映射到当前选中的 Alert 模型的 alertSource 属性:
{{tree-menu rootNodes=controllers.admin.rootNodes
allowMultipleSelections=false selectedNode=alertSource}}
5. 测试 Ember.js 应用
JavaScript 应用的测试领域仍在不断发展,有多种测试方式,包括单元测试、集成测试、性能测试、回归测试、黑盒测试和持续集成等。这里主要介绍使用 QUnit 和 PhantomJS 进行单元测试。
5.1 QUnit 简介
QUnit 是一个用于编写应用单元测试的框架,被 jQuery、jQuery UI 和 jQuery Mobile 等使用,Ember.js 框架的单元测试大多也用 QUnit 编写。由于在持续集成环境中运行需要浏览器的测试较为困难,使用 PhantomJS 可以在无头模式下执行测试,便于在 CI 环境中设置测试。
5.2 编写第一个 QUnit 测试
首先,从 QUnit 官网 下载 QUnit JavaScript 文件和 QUnit CSS 文件,创建一个名为 firstTest.html 的 HTML 文件,内容如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>First Test</title>
<link rel="stylesheet" href="qunit-1.11.0.css" type="text/css" charset="utf-8">
<script src="qunit-1.11.0.js" type="text/javascript" charset="utf-8"></script>
</head>
<body bgcolor="#ffffff">
<div id="qunit" style="z-index: 100;"></div>
<script type="text/javascript" charset="utf-8" src="firstTest.js"></script>
</body>
</html>
该 HTML 页面用于设置测试,需要包含 QUnit CSS 和 JavaScript 文件,并创建一个 div 元素用于显示测试结果。所有要通过该 HTML 文件运行的测试都需在 HTML 页面的 body 标签内指定。
接着,创建 firstTest.js 文件,编写一个测试来验证整数 1 是否等于字符串 "1" ,代码如下:
test("Test that QUnit is working as expected", function() {
ok( 1 == "1", "QUnit Test Passed!" );
});
每个测试通过 QUnit 的 test() 函数指定,第一个参数是测试名称,第二个参数是包含测试断言的回调函数。将 firstTest.html 文件拖到浏览器中即可执行测试。
以下是树菜单组件的结构关系图:
graph LR
A[tree-menu component] --> B[tree-menu-item component]
A --> C[tree-menu-item component]
A --> D[tree-menu-item component]
B --> E[tree-menu-item component]
B --> F[tree-menu-item component]
C --> G[tree-menu-item component]
D --> H[tree-menu-item component]
测试 Ember.js 应用的流程如下:
1. 下载 QUnit 和 PhantomJS。
2. 创建 firstTest.html 文件并配置 QUnit。
3. 编写 firstTest.js 测试文件。
4. 在浏览器中执行测试。
Ember.js 自定义组件编写与测试实践
6. 使用 PhantomJS 执行测试
PhantomJS 是一个无界面的浏览器,可用于在无头模式下执行测试,这对于持续集成环境非常有用。在使用 PhantomJS 执行 QUnit 测试之前,需要确保 PhantomJS 已安装在系统中,可从 PhantomJS 官网 下载安装。
为了让 PhantomJS 能够执行 QUnit 测试,需要创建一个 PhantomJS 脚本。以下是一个简单的 PhantomJS 脚本示例 run-qunit.js :
var page = require('webpage').create();
var system = require('system');
if (system.args.length !== 2) {
console.log('Usage: run-qunit.js URL');
phantom.exit(1);
}
var url = system.args[1];
page.open(url, function(status) {
if (status !== 'success') {
console.log('Unable to access network');
phantom.exit(1);
} else {
page.onConsoleMessage = function(msg) {
console.log(msg);
};
page.evaluate(function() {
QUnit.done(function(results) {
console.log('Tests completed in ' + results.runtime + 'ms, ' + results.total + ' tests run, ' + results.passed + ' passed, ' + results.failed + ' failed.');
if (results.failed > 0) {
phantom.exit(1);
} else {
phantom.exit(0);
}
});
});
}
});
这个脚本接受一个 URL 作为参数,打开该 URL 并执行其中的 QUnit 测试。测试完成后,会输出测试结果,并根据失败的测试数量决定退出状态码。
要使用这个脚本执行之前创建的 firstTest.html 测试,可以在命令行中运行以下命令:
phantomjs run-qunit.js file:///path/to/your/firstTest.html
其中 /path/to/your/firstTest.html 是 firstTest.html 文件的实际路径。
7. 集成测试
除了单元测试,集成测试也是确保应用功能正常的重要手段。集成测试主要测试多个组件或模块之间的交互是否正常。
在 Ember.js 中进行集成测试时,可以使用 QUnit 结合 Ember 的测试助手。以下是一个简单的集成测试示例,假设要测试树菜单组件的单选功能:
module('Integration - Tree Menu Single Selection', {
setup: function() {
// 初始化测试环境
Ember.run(function() {
// 创建必要的模型和控制器
});
},
teardown: function() {
// 清理测试环境
}
});
test('Single selection works correctly', function(assert) {
// 渲染树菜单组件
var treeMenu = Ember.View.create({
templateName: 'components/tree-menu',
rootNodes: [/* 根节点数据 */],
allowMultipleSelections: false,
selectedNode: null
});
treeMenu.appendTo('#ember-testing');
// 模拟点击一个叶节点
Ember.run(function() {
var leafNode = treeMenu.$('.leaf-node').first();
leafNode.click();
});
// 验证选中节点是否正确
assert.equal(treeMenu.get('selectedNode'), 'expected-node-id', 'Selected node is correct');
// 清理测试视图
treeMenu.destroy();
});
在这个示例中,首先创建一个测试模块,在 setup 函数中初始化测试环境,在 teardown 函数中清理环境。然后编写一个测试用例,渲染树菜单组件,模拟点击一个叶节点,并验证选中节点是否正确。
8. 性能测试与 Ember Instrumentation
性能测试用于评估应用的性能指标,如响应时间、吞吐量等。Ember Instrumentation 是 Ember.js 提供的一个工具,可用于快速测量应用的性能。
以下是一个使用 Ember Instrumentation 进行性能测试的示例:
Ember.Instrumentation.subscribe('render', {
before: function(name, timestamp, payload) {
// 记录渲染开始时间
payload.startTime = timestamp;
},
after: function(name, timestamp, payload) {
// 计算渲染时间
var renderTime = timestamp - payload.startTime;
console.log('Render time for ' + name + ': ' + renderTime + 'ms');
}
});
在这个示例中,订阅了 render 事件,在渲染开始时记录时间,渲染结束时计算渲染时间并输出。
9. 回归测试与持续集成
回归测试用于确保在对应用进行修改后,之前的功能仍然正常工作。持续集成(CI)是一种软件开发实践,通过频繁地将代码集成到共享仓库中,并自动运行测试,及时发现和解决问题。
可以使用工具如 Jenkins、GitLab CI/CD 等实现持续集成。以下是一个简单的 GitLab CI/CD 配置示例 .gitlab-ci.yml :
stages:
- test
test:
stage: test
image: node:latest
script:
- npm install
- npm run test
在这个配置中,定义了一个 test 阶段,使用 Node.js 镜像,安装依赖并运行测试脚本。
10. 总结
通过以上步骤,我们学习了如何编写 Ember.js 自定义树菜单组件,包括实现多选和单选功能。同时,介绍了使用 QUnit 和 PhantomJS 进行单元测试,以及如何进行集成测试、性能测试和回归测试。持续集成是保证应用质量的重要手段,可以通过工具实现自动化测试。
以下是 Ember.js 应用测试的方法总结表格:
| 测试类型 | 工具 | 说明 |
| ---- | ---- | ---- |
| 单元测试 | QUnit、PhantomJS | 测试单个组件或函数的功能 |
| 集成测试 | QUnit、Ember 测试助手 | 测试多个组件或模块之间的交互 |
| 性能测试 | Ember Instrumentation | 测量应用的性能指标 |
| 回归测试 | QUnit、持续集成工具 | 确保修改后之前的功能仍然正常 |
Ember.js 提供了丰富的工具和功能,通过合理运用这些工具,可以有效地编写和测试高质量的应用。
以下是 Ember.js 应用测试的整体流程图:
graph LR
A[开始] --> B[下载 QUnit 和 PhantomJS]
B --> C[创建测试文件]
C --> D[编写单元测试]
D --> E[使用 PhantomJS 执行测试]
E --> F{测试通过?}
F -- 是 --> G[进行集成测试]
F -- 否 --> D
G --> H[性能测试]
H --> I[回归测试]
I --> J[持续集成]
J --> K[结束]
超级会员免费看
43

被折叠的 条评论
为什么被折叠?



