設置Spectron和測試運行器
安裝 spectron和mocha
cnpm install --save-dev spectron
cnpm install --save-dev mocha
...
"devDependencies": {
"mocha": "^8.0.1",
"spectron": "^11.0.0"
}
spectron :
https://github.com/electron-userland/spectron#accessibility-testing
mocha
官網:https://mochajs.org/
https://www.jianshu.com/p/4f7731b1a40b
assert
http://nodejs.cn/api/assert.html
創建一個Mocha的腳本
在package.json 文件創建一個Mocha的腳本
"scripts": {
"start": "electron .",
"test": "mocha" //創建一個Mocha的腳本,運行本地的安裝的Mocha
},
...
編寫Spectron測試代碼
項目結構
|__Clipmaster-charp13-Sperctron
|__ app
|__ main.js
|__ index.html
|__ renderer.js
|__ ...
|__ package.json
|__ test
|__ spec.js
運行 cnpm test 指令時, Mocha框架默認執行根目錄下test
文件夾內所有的JavaScript文件,所有我們
新建./test/spec.js:配合測試運行器(Mocha)編寫Spectron測試代碼
- spec.js 文件
const assert = require('assert'); //引入Nodde 內置的斷言庫
const path = require('path'); //引入Nodde 文集路徑輔助工具
const Application = require('spectron').Application; //引入Spectron的應用驅動程序
const electronPath = require('electron'); //已入electron,這讓我們可以訪問本地的Electron的開發版本
const app = new Application({
path: electronPath, //創建Spectron的Application對象,告訴他使用本地的Electron開發版本
// The following line tells spectron to look and use the main.js file
// and the package.json located 1 level above.
//應用自身的根目錄作為應用的起始點和當前項目package.json文件路徑
args: [path.join(__dirname, '..')]
});
//mocha:定義一組測試
describe('Clipmaster 9000', function () {
this.timeout(10000); //由於應用程序需要花費一些時間,因此增加Mocha的默認超時時間
beforeEach(() => {
return app.start(); //在每個應用之前啟動應用
});
afterEach(() => {
if (app && app.isRunning()) {
return app.stop(); //結束每個測試后停止應用
}
});
//定義一個測試(一個測試用例)
it ('啟動一個窗口', async function(){
let count = await app.client.getWindowCount();
return assert.equal(count, 1);
})
});
- mocha只有兩個主要的api。
- describe(name, fn) 定義一組測試
- it(name, fn) 定義一項測試
運行測試:cnpm test
PS E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron> cnpm test
> clipmaster-9000@1.0.0 test E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron
> mocha
Clipmaster 9000
√ 啟動一個窗口
1 passing (6s)
1 passing (6s)
測試用例通過
測試標題是否正確
- 測試代碼
//mocha:定義一組測試
describe('Clipmaster 9000', function () {
...
it('窗口標題是否正確', async () =>{
//waitUntilWindowLoaded等待窗口加載完html、css、js后獲取標題
let title = await app.client.waitUntilWindowLoaded().getTitle();
return assert.equal(title, 'Clipmaster 9000');
})
});
- 運行測試
PS E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron> cnpm test
> clipmaster-9000@1.0.0 test E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron
> mocha
Clipmaster 9000
√ 啟動一個窗口
√ 窗口標題是否正確
2 passing (9s)
- 測試不通過
把斷言改為
return assert.equal(title, 'Clipmaster');
PS E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron> cnpm test
> clipmaster-9000@1.0.0 test E:\Kzone\CodeLib\electron\electron-action\Clipmaster-charp13-Sperctron
> mocha
Clipmaster 9000
√ 啟動一個窗口
1) 窗口標題是否正確
1 passing (9s)
1 failing
1) Clipmaster 9000
窗口標題是否正確:
AssertionError [ERR_ASSERTION]: 'Clipmaster 9000' == 'Clipmaster'
+ expected - actual
-Clipmaster 9000
+Clipmaster
at Context.<anonymous> (test\spec.js:37:23)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
npm ERR! Test failed. See above for more details.
測試 Electron BrowseWindow API
檢測應用加載后,開發者工具是否處於關閉狀態
it('不要打開開發者工具', async () => {
let devToolsAreOpen = await app.client
.waitUntilWindowLoaded()
.browserWindow.isDevToolsOpened();
return assert.equal(devToolsAreOpen, false);
})
使用Spectron遍歷和測試DOM
it('應用啟動是不存在剪貼項', async () => {
await app.client.waitUntilWindowLoaded();
let clippings = await app.client.$$('.clippings-list-item'); //document.querySelectorAll
return assert.equal(clippings.length, 0);
})
it('點擊"copy-from-clipboard"按鈕時,增加一項剪貼項', async () => {
await app.client.waitUntilWindowLoaded();
await app.client.click("#copy-from-clipboard");
let clippings = await app.client.$$('.clippings-list-item');
return assert.equal(clippings.length, 1);
})
it('可以刪除剪切項', async () => {
await app.client.waitUntilWindowLoaded();
await app.client.click("#copy-from-clipboard") //先添加一項
.moveToObject(".clippings-list-item")//默認Remove按鈕是隱藏的,不能點擊,故先將鼠標指針移動到DOM元素上
.click(".remove-clipping");//刪除
let clippings = await app.client.$$('.clippings-list-item');
return assert.equal(clippings.length, 0);
})
獲取DOM
let clippings = await app.client.$$('.clippings-list-item'); //document.querySelectorAll
測試單擊交互操作
await app.client.click("#copy-from-clipboard");
在測試中移動鼠標
await app.client.click("#copy-from-clipboard") //先添加一項
.moveToObject(".clippings-list-item")//默認Remove按鈕是隱藏的,不能點擊,故先將鼠標指針移動到DOM元素上
.click(".remove-clipping");//刪除
使用 Sepctron控制Electron API
從剪貼板復制並顯示正確的文本
it('從剪貼板復制並顯示正確的文本', async () => {
let testText = 'Hello Word';
await app.client.waitUntilWindowLoaded();
await app.electron.clipboard.writeText(testText);
await app.client.click("#copy-from-clipboard");
let clippingText = await app.client.getText('.clipping-text');
return assert.equal(clippingText, testText);
})
寫剪貼板是否正確
it('寫剪貼板是否正確', async () => {
//從剪貼板復制文本
let testText = 'Hello Word';
await app.client.waitUntilWindowLoaded();
await app.electron.clipboard.writeText(testText);
await app.client.click("#copy-from-clipboard");
//模擬剪貼板有新的內容
await app.electron.clipboard.writeText('剪貼板新內容');
//寫剪貼板
await app.client.click('.copy-clipping');
let clippingText = await app.electron.clipboard.readText();
return assert.equal(clippingText, testText);
})
完整代碼
package.json
{
"name": "clipmaster-9000",
"version": "1.0.0",
"description": "A menubar application with a rich UI.",
"main": "app/main.js",
"scripts": {
"start": "electron .",
"test": "mocha"
},
"author": "weikai",
"license": "MIT",
"dependencies": {
"dexie": "^3.0.1",
"electron": "9.0.3",
"menubar": "^9.0.1",
"request": "^2.88.2"
},
"devDependencies": {
"mocha": "^8.0.1",
"spectron": "^11.0.0"
}
}
test/spec.js
const assert = require('assert'); //引入Nodde 內置的斷言庫
const path = require('path'); //引入Nodde 文集路徑輔助工具
const Application = require('spectron').Application; //引入Spectron的應用驅動程序
const electronPath = require('electron'); //已入electron,這讓我們可以訪問本地的Electron的開發版本
const app = new Application({
path: electronPath, //創建Spectron的Application對象,告訴他使用本地的Electron開發版本
// The following line tells spectron to look and use the main.js file
// and the package.json located 1 level above.
//應用自身的根目錄作為應用的起始點和當前項目package.json文件路徑
args: [path.join(__dirname, '..')]
});
//mocha:定義一組測試
describe('Clipmaster 9000', function () {
this.timeout(10000); //由於應用程序需要花費一些時間,因此增加Mocha的默認超時時間
beforeEach(() => {
return app.start(); //在每個應用之前啟動應用
});
afterEach(() => {
if (app && app.isRunning()) {
return app.stop(); //結束每個測試后停止應用
}
});
//定義一個測試(一個測試用例)
it('啟動一個窗口', async function () {
let count = await app.client.getWindowCount();
return assert.equal(count, 1);
})
it('窗口標題是否正確', async () => {
//waitUntilWindowLoaded等待窗口加載完html、css、js后獲取標題
let title = await app.client.waitUntilWindowLoaded().getTitle();
return assert.equal(title, 'Clipmaster 9000');
})
it('不要打開開發者工具', async () => {
let devToolsAreOpen = await app.client
.waitUntilWindowLoaded()
.browserWindow.isDevToolsOpened();
return assert.equal(devToolsAreOpen, false);
})
it('有一個按鈕,其文本為"Copy from Clipbard"', async () => {
//getText()是WebdriveIO提供,返回一個節點的文本內容的Promise對象
let buttonText = await app.client.getText("#copy-from-clipboard");
return assert.equal(buttonText, "Copy from Clipboard");
})
it('應用啟動是不存在剪貼項', async () => {
await app.client.waitUntilWindowLoaded();
let clippings = await app.client.$$('.clippings-list-item'); //document.querySelectorAll
return assert.equal(clippings.length, 0);
})
it('點擊"copy-from-clipboard"按鈕時,增加一項剪貼項', async () => {
await app.client.waitUntilWindowLoaded();
await app.client.click("#copy-from-clipboard");
let clippings = await app.client.$$('.clippings-list-item');
return assert.equal(clippings.length, 1);
})
it('可以刪除剪切項', async () => {
await app.client.waitUntilWindowLoaded();
await app.client.click("#copy-from-clipboard") //先添加一項
.moveToObject(".clippings-list-item")//默認Remove按鈕是隱藏的,不能點擊,故先將鼠標指針移動到DOM元素上
.click(".remove-clipping");//刪除
let clippings = await app.client.$$('.clippings-list-item');
return assert.equal(clippings.length, 0);
})
it('從剪貼板復制並顯示正確的文本', async () => {
let testText = 'Hello Word';
await app.client.waitUntilWindowLoaded();
await app.electron.clipboard.writeText(testText);
await app.client.click("#copy-from-clipboard");
let clippingText = await app.client.getText('.clipping-text');
return assert.equal(clippingText, testText);
})
it('寫剪貼板是否正確', async () => {
//從剪貼板復制文本
let testText = 'Hello Word';
await app.client.waitUntilWindowLoaded();
await app.electron.clipboard.writeText(testText);
await app.client.click("#copy-from-clipboard");
//模擬剪貼板有新的內容
await app.electron.clipboard.writeText('剪貼板新內容');
//寫剪貼板
await app.client.click('.copy-clipping');
let clippingText = await app.electron.clipboard.readText();
return assert.equal(clippingText, testText);
})
});
main.js
有兩個版本:有系統托盤版本和無系統托盤的版本
有系統托盤版本
const { menubar } = require('menubar');
const { globalShortcut, Menu } = require('electron');
const mb = menubar({
preloadWindow: true,
browserWindow: {
webPreferences: {
nodeIntegration: true
},
},
index: `file://${__dirname}/index.html`,
});
mb.on('ready', () => {
const secondaryMenu = Menu.buildFromTemplate([
{
label: 'Quit',
click() { mb.app.quit(); },
accelerator: 'CommandOrControl+Q'
},
]);
mb.tray.on('right-click', () => {
mb.tray.popUpContextMenu(secondaryMenu);
});
const createClipping = globalShortcut.register('CommandOrControl+!', () => {
mb.window.webContents.send('create-new-clipping');
});
const writeClipping = globalShortcut.register('CmdOrCtrl+Alt+@', () => {
mb.window.webContents.send('write-to-clipboard');
});
const publishClipping = globalShortcut.register('CmdOrCtrl+Alt+#', () => {
mb.window.webContents.send('publish-clipping');
});
if (!createClipping) { console.error('Registration failed', 'createClipping'); }
if (!writeClipping) { console.error('Registration failed', 'writeClipping'); }
if (!publishClipping) { console.error('Registration failed', 'publishClipping'); }
});
無系統托盤版本
const { app, BrowserWindow, globalShortcut, Menu } = require('electron');
let mainWindow = null; // #A
app.on('ready', () => {
mainWindow = createWindow();
const createClipping = globalShortcut.register('CommandOrControl+!', () => {
mainWindow.webContents.send('create-new-clipping');
});
const writeClipping = globalShortcut.register('CmdOrCtrl+Alt+@', () => {
mainWindow.webContents.send('write-to-clipboard');
});
const publishClipping = globalShortcut.register('CmdOrCtrl+Alt+#', () => {
mainWindow.webContents.send('publish-clipping');
});
if (!createClipping) { console.error('Registration failed', 'createClipping'); }
if (!writeClipping) { console.error('Registration failed', 'writeClipping'); }
if (!publishClipping) { console.error('Registration failed', 'publishClipping'); }
});
const createWindow = () => {
let newWindow = new BrowserWindow({
show: false, //#A.1首次創建窗口,先隱藏
webPreferences: {
nodeIntegration: true
}
});
//#A.2 需要長時間加載的頁面
newWindow.loadURL(`${__dirname}/index.html`); // #A
//#A.3:一次性時間監聽器,DOM就緒后再顯示窗口,避免在窗口中顯示白屏
newWindow.once('ready-to-show', () => {
newWindow.show();
//mainWindow.webContents.openDevTools();
});
newWindow.on('closed', () => {
newWindow = null;
})
return newWindow;
}
renderer.js
const { clipboard, ipcRenderer, shell } = require('electron');
const db = require('./dbdexie');
let baseUrl = 'https://api.github.com/gists';
const request = require('request').defaults({
url: baseUrl,
headers: { 'User-Agent': 'Clipmaster 9000' }
});
const clippingsList = document.getElementById('clippings-list');
const copyFromClipboardButton = document.getElementById('copy-from-clipboard');
ipcRenderer.on('create-new-clipping', () => {
addClippingToList();
new Notification('Clipping Added', {
body: `${clipboard.readText()}`
});
});
ipcRenderer.on('write-to-clipboard', () => {
const clipping = clippingsList.firstChild;
writeToClipboard(getClippingText(clipping));
new Notification('Clipping Copied', {
body: `${clipboard.readText()}`
});
});
ipcRenderer.on('publish-clipping', () => {
const clipping = clippingsList.firstChild;
publishClipping(getClippingText(clipping));
});
const initClippingElement = () => {
/*
db.clips
//.reverse() //按主鍵降序排列
.each(clip => {
let clippingElement = createClippingElement(clip.id, clip.value);
clippingsList.prepend(clippingElement);
});
*/
db.clips
.orderBy('id')
//.reverse() //按主鍵降序排列
.toArray((clips) => {
for (var clip of clips) {
let clippingElement = createClippingElement(clip.id, clip.value);
clippingsList.prepend(clippingElement);
}
});
}
const createClippingElement = (clipId, clippingText) => {
const clippingElement = document.createElement('article');
clippingElement.classList.add('clippings-list-item');
clippingElement.innerHTML = `
<div class="clipping-text" disabled="true"></div>
<div class="clipping-controls">
<button class="copy-clipping">→ Clipboard</button>
<button class="publish-clipping">Publish</button>
<button class="remove-clipping" data-clipId='${clipId}'>Remove</button>
</div>
`;
clippingElement.querySelector('.clipping-text').innerText = clippingText;
return clippingElement;
};
const addClippingToList = () => {
const clippingText = clipboard.readText();
db.clips.add({ value: clippingText, data: Date.now() })
.then(id => {
return db.clips.get(id);
}).then(item => {
const clippingElement = createClippingElement(item.id, item.value);
clippingsList.prepend(clippingElement);
}).catch(err => {
alert("Error: " + (err.stack || err));
});
};
copyFromClipboardButton.addEventListener('click', addClippingToList);
clippingsList.addEventListener('click', (event) => {
const hasClass = className => event.target.classList.contains(className);
let clipId = parseInt(event.target.getAttribute("data-clipId"));
const clippingListItem = getButtonParent(event);
if (hasClass('remove-clipping')) removeClipping(clipId, clippingListItem);
if (hasClass('copy-clipping')) writeToClipboard(getClippingText(clippingListItem));
if (hasClass('publish-clipping')) publishClipping(getClippingText(clippingListItem));
});
const removeClipping = (clipId, target) => {
db.clips.delete(clipId).then(result => {
//Promise that resolves successfully with an undefined result, no matter if a record was deleted or not.
//不管刪沒刪,都是返回:undefined
target.remove();
});
};
const writeToClipboard = (clippingText) => {
clipboard.writeText(clippingText);
};
const publishClipping = (clippingText) => {
request.post(baseUrl, toJSON(clippingText), (err, response, body) => {
if (err) {
return new Notification('Error Publishing Your Clipping', {
body: JSON.parse(err).message
});
}
const gistUrl = JSON.parse(body).documentation_url; // const gistUrl = JSON.parse(body).html_url;
const notification = new Notification('Your Clipping Has Been Published', {
body: `Click to open ${gistUrl} in your browser.`
});
notification.onclick = () => { shell.openExternal(gistUrl); };
clipboard.writeText(gistUrl);
});
};
const getButtonParent = ({ target }) => {
return target.parentNode.parentNode;
};
const getClippingText = (clippingListItem) => {
return clippingListItem.querySelector('.clipping-text').innerText;
};
const toJSON = (clippingText) => {
return {
body: JSON.stringify({
description: 'Created with Clipmaster 9000',
public: 'true',
files: {
'clipping.txt': { content: clippingText }
}
})
};
};
initClippingElement();
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Clipmaster 9000</title>
<link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
<div class="container">
<section class="controls">
<button id="copy-from-clipboard">Copy from Clipboard</button>
</section>
<section class="content">
<div id="clippings-list" class="clippings-list"></div>
</section>
</div>
<script>
require('./renderer.js');
</script>
</body>
</html>
style.css
html {
box-sizing: border-box;
}
html, body {
height: 100%;
width: 100%;
overflow: hidden;
font-size: 12px;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
}
body, input {
font: menu;
font-size: 12px;
}
body > div {
height: 100%;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
.container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
textarea, input, div, button { outline: none; }
.controls {
background-color: rgb(217, 241, 238);
padding: 1em;
top: 0;
position: fixed;
width: 100%;
}
.controls button {
background-color: rgb(181, 220, 216);
border: none;
padding: 0.5em 1em;
}
.controls button:hover {
background-color: rgb(156, 198, 192);
}
.controls button:active {
background-color: rgb(144, 182, 177);
}
.controls button:disabled {
background-color: rgb(196, 204, 202);
}
.content {
height: 100%;
}
.clippings-list {
margin-top: 65px;
padding: 0 10px;
}
.clippings-list-item {
border: 1px solid rgb(178, 193, 191);
box-shadow: 1px 1px 1px 1px rgba(205, 228, 224, 0.78);
padding: 0.5em;
margin-bottom: 1em;
}
.clipping-text {
background-color: rgb(228, 248, 245);
padding: 0.5em;
min-width: 100%;
max-height: 10em;
overflow: scroll;
}
.clipping-text::-webkit-scrollbar {
display: none;
}
.clipping-controls {
margin-top: 0.5em;
}
button {
background-color: rgb(181, 220, 216);
border: none;
font-size: 0.8em;
padding: 0.5em 1em;
}
button:hover {
background-color: rgb(156, 198, 192);
}
button:active {
background-color: rgb(144, 182, 177);
}
button:disabled {
background-color: rgb(196, 204, 202);
}
button.remove-clipping {
display: none;
float: right;
color: white;
background-color: rgb(208, 69, 55);
}
button.remove-clipping:hover {
background-color: rgb(208, 41, 29);
}
button.remove-clipping:active {
background-color: rgb(236, 0, 6);
}
button.remove-clipping:disabled {
background-color: rgb(152, 73, 64);
}
.clippings-list-item:hover button.remove-clipping {
display: inline-block;
}
dbdexie.js
const Dexie = require('dexie');
const db = new Dexie('clipsmarter_database');
db.version(1).stores({
clips: '++id, value, data'
});
module.exports = db;