使用 Nightmare 进行浏览器自动化测试

Nightmare is a high-level browser automation library. 和之前的 PhantomJS 很像,但是 Nightmare 是基于 Electron 的,也就是还是基于 Chromium 和 Node.js。但是感觉其写法比 PhantomJS 简单一些:

安装

1
npm i nightmare

使用

创建实例

1
2
3
var Nightmare = require('nightmare');

var nightmare = Nightmare({ show: true }); // options

这里的 options.show = true 可以在运行时打开 Electron 窗口,方便 debug,更多的 options 请看 new BrowserWindow(options)

操作页面

因为需要使用了 ES6 的 generator,所以这里还要加入 co,官网的 Examples 使用的是 vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var co = require('co');
var RES_CLS = 'r';

// Node 运行环境
var googleUrl = 'https://www.google.com/';
co(function* () {
var url = yield nightmare
// 打开 Google
.goto(googleUrl)
// 在输入框中填写内容 'csbun npm'
.type('input[name="q"]', 'csbun npm')
// 点击搜索
.click('input[type="submit"]')
// 等待页面返回
.wait('.' + RES_CLS)
// 页面操作
.evaluate(function (RES_CLS) {
// 这里是浏览器的运行环境
var resHref = '';
var resH3 = document.getElementsByClassName(RES_CLS)[0];
if (resH3) {
console.log(resH3);
var resA = resH3.getElementsByTagName('A')[0];
if (resA) {
resHref = resA.href;
}
}
// 返回给 Node
return resHref;
}, RES_CLS); // 传参
// 关闭
yield nightmare.end();
return url;
}).then(function (res) {
// 输出结果
console.log('res: ' + res);
});

运行上述代码,将获得 Google 搜索 csbun npm 的第一个链接: https://www.npmjs.com/~csbun

其他

evaluate

在上面的例子里面,我们可以看到,.evaluate(fn, arg1) 中的 fn 是浏览器的运行环境,是不能直接使用闭包中的其他变量的,必须通过 evaluate 的参数 arg1 传入。详情请看 官方解释

wait

wait(ms).wait(selector).wait(fn) 这三个方法在页面进行异步操作的时候比较好用。接口文档

例如发送请求,等待图片下载等,都可以通过上面的方式实现。

测试

有了 Nightmare,我们就可以结合 Mocha,给我们的项目编写测试例,下面将以我的一个小项目 resize-image 为例:

同样的,因为使用了 generator,我们加入 mocha-generators

1
2
3
4
5
6
/* test.js */
// enable generator `it('', function * () {})`
require('mocha-generators').install();

var Nightmare = require('nightmare');
var assert = require('assert');

本地服务器

然后我们需要一个本地服务器,基于 Node,我们可以很简单写就写一个出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* server.js */
var fs = require('fs');
var path = require('path');
var http = require('http');

var html = fs.readFileSync(path.join(__dirname, 'test.html'), 'utf8');

var pngFile = path.join(__dirname, '../example/google.png');
var pngStat = fs.statSync(pngFile);

var jsFile = path.join(__dirname, '../index.js');
var jsStat = fs.statSync(jsFile);

module.exports = http.createServer(function (req, res) {
if (req.url === '/index.js') {
res.writeHead(200, {
'Content-Type': 'text/javascript',
'Content-Length': jsStat.size
});
fs.createReadStream(jsFile).pipe(res);
} else if (req.url === '/google.png') {
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': pngStat.size
});
var readStream = fs.createReadStream(pngFile);
readStream.pipe(res);
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(html);
}
});

其中,google.png 只是一张普通的图片,index.js 则为 resize-image 的源码,test.html 则为一个简单的测试页面:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Resize Image</title>
<script type="text/javascript" src="index.js"></script>
</head>
<body>
<img id="img" src="./google.png">
</body>
</html>

测试代码

下面我们就能开始测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* test.js */
var PORT = 7500;
var ASSERT_SIZE = 200;

describe('resize-image', function () {
// start server
before(function (done) {
require('./server').listen(PORT, done);
});

// test `.resize`
it('.resize: Resize any image to ' + ASSERT_SIZE, function * () {
var nightmare = Nightmare(); // 不要 show 了

var min = yield nightmare
.goto('http://0.0.0.0:' + PORT + '/')
.evaluate(function (ASSERT_SIZE) {
var img = document.getElementById('img');
// resize
var base64 = window.ResizeImage.resize(img, ASSERT_SIZE, ASSERT_SIZE, ResizeImage.PNG);
// get resized image size
var resizedImg = new Image();
resizedImg.src = base64;
return Math.min(resizedImg.width, resizedImg.height);
}, ASSERT_SIZE)

yield nightmare.end();
assert.equal(min, ASSERT_SIZE);
});
});

运行 mocha,执行结果如下:

1
2
3
4
5
resize-image
✓ .resize: Resize any image to 200 (582ms)


1 passing (595ms)

完成!没问题!完整的例子请看 这里