• 13.《Electron 跨平台开发实战》- chapter13 使用 Spectron 测试应用


    设置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:KzoneCodeLibelectronelectron-actionClipmaster-charp13-Sperctron> cnpm test
    
    > clipmaster-9000@1.0.0 test E:KzoneCodeLibelectronelectron-actionClipmaster-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:KzoneCodeLibelectronelectron-actionClipmaster-charp13-Sperctron> cnpm test 
    
    > clipmaster-9000@1.0.0 test E:KzoneCodeLibelectronelectron-actionClipmaster-charp13-Sperctron
    > mocha
    
      Clipmaster 9000
        √ 启动一个窗口
        √ 窗口标题是否正确
    
      2 passing (9s)
    
    • 测试不通过
      把断言改为
         return assert.equal(title, 'Clipmaster');
    
    PS E:KzoneCodeLibelectronelectron-actionClipmaster-charp13-Sperctron> cnpm test
    
    > clipmaster-9000@1.0.0 test E:KzoneCodeLibelectronelectron-actionClipmaster-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> (testspec.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">&rarr; 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%;
       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;
       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- 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;
    
  • 相关阅读:
    mysql check约束无效
    Illegal mix of collations for operation 'concat'
    执行automake时报错 error while making link: Operation not supported
    GCC 编译详解[转]
    gcc的选项
    关于MFLAGS与MAKEFLAGS
    gcc和g++的区别
    g++参数介绍
    gcc/g++基本命令简介
    semver语义化版本号
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/13168863.html
Copyright © 2020-2023  润新知