Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于 Vue 的 E2E test 环境搭建 #204

Open
felix-cao opened this issue Jun 21, 2021 · 3 comments
Open

基于 Vue 的 E2E test 环境搭建 #204

felix-cao opened this issue Jun 21, 2021 · 3 comments

Comments

@felix-cao
Copy link
Owner

felix-cao commented Jun 21, 2021

本文是基于一个2年前的 Vue 脚手架项目搭建 E2E 环境,实现思路是利用 Headless Chrome 来模拟用户的一切行为。

一、Headless Chrome

简单的在 window 系统中玩一下 Headless Chrome

cd 'C:\Program Files\Google\Chrome\Application'
# Dump DOM to the screen
chrome.exe --headless --disable-gpu --enable-logging --dump-dom https://www.baidu.com/
# Save the page as a PDF
chrome.exe --headless --disable-gpu --print-to-pdf=C:\Temp\output.pdf https://www.baidu.com/
# Screenshot the page
chrome.exe --headless --disable-gpu --screenshot=C:\Temp\screenshot.png  https://www.baidu.com/
# Set the window size
chrome.exe --headless --disable-gpu --screenshot=C:\Temp\screenshot.png --window-size=1280,1696 https://www.baidu.com/

二、安装依赖及 scripts 脚本

主要依赖 jest , puppeteer, babel, 在 package.json 中如下

"scripts": {
    "dev": "cross-env NODE_ENV=dev webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "build": "node build/build.js",
    "test": "jest --runInBand -c config/jest.config.js"
 },
// ......
"devDependencies": {
    "babel-core": "^6.22.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
     // ....... add the follow dependencies
    "babel-jest": "^23.6.0",
    "jest": "^26.0.1",
    "jest-dev-server": "^5.0.3",
    "jest-html-reporters": "^2.1.6",
    "jest-puppeteer": "^5.0.4",
    "webpack": "^3.6.0",
    "puppeteer": "^10.0.0",
}

.babelrc, 配置 babel

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    ["babel-preset-env", {"targets": {"node": "current"}}],
    "stage-2"
  ],
  "plugins": [
    "transform-vue-jsx", 
    "transform-runtime"
  ]
}

注意,这里的依赖是踩了坑后可以完整运行的。

  • 坑1:按照 jest 官方的 babel 教程 配置我当前项目会有错误,故单独安装,并将 babel-jest 版本调到 23.6.0
    image

  • 坑2:jest 调整到 26.0.2
    image

三、E2E test 配置

参考jest config我在 package.json"test": "jest --runInBand -c config/jest.config.js" 指定了 配置文件的位置
/config/jest.config.js

'use strict'
const path = require('path');
module.exports = {
  preset: "jest-puppeteer",
  globals: {
		baseUrl: 'http://localhost:8080',
    display: process.env.DISPLAY, // TOdel
		videosPath: path.resolve(__dirname, '../test/e2e/report'),  // TOdels
    // logpath: process.env.LOG_DIR || path.join( __dirname, 'log' ) // TOdel
	},
  maxConcurrency: 1,
  globalSetup: path.resolve(__dirname, './jest/setup.js'),
  globalTeardown: path.resolve(__dirname, './jest/teardown.js'),
  testEnvironment: path.resolve(__dirname, './jest/puppeteer_environment.js'),
  rootDir: path.resolve(__dirname, '../'),
  moduleFileExtensions: [ "js", ],
  testSequencer: path.resolve(__dirname, '../test/e2e/config/sequencer.js'),
  transform: { "^.+\\.[jt]sx?$": "<rootDir>/node_modules/babel-jest" },
  testMatch: [ '**/test/**/?(*.)+(spec|e2e).js' ],
  reporters: [
    "default",
    ["jest-html-reporters", {
      "publicPath": path.resolve(__dirname, '../test/e2e/report'),
      "filename": "html-report.html",
      "expand": true,
      "openReport": true
    }]
  ]
};

/config/jest/setup.js

const fs = require('fs');
const os = require('os');
const path = require('path');
const mkdirp = require('mkdirp');
const puppeteer = require('puppeteer');

const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');

module.exports = async function () {
  const browser = await puppeteer.launch({
    slowMo:500, // 输入延迟时间
    headless: false,
    devtools: false,
    defaultViewport: null,
    args: ['--window-size=1920,1080'],
  });
  global.__BROWSER_GLOBAL__ = browser;
  mkdirp.sync(DIR);
  fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint());
};

/config/jest/teardown.js

const os = require('os');
const path = require('path');
const rimraf = require('rimraf');

const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
module.exports = async function () {
  // close the browser instance
  setTimeout(() => {
     global.__BROWSER_GLOBAL__.close();
  }, 2000)
  rimraf.sync(DIR);
};

/config/jest/puppeteer_environment.js

const fs = require('fs');
const os = require('os');
const path = require('path');
const puppeteer = require('puppeteer');
const NodeEnvironment = require('jest-environment-node');
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');

class PuppeteerEnvironment extends NodeEnvironment {
  constructor(config) {
    super(config);
  }

  async setup() {
    await super.setup();
    const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8'); // get the wsEndpoint
    if (!wsEndpoint) {
      throw new Error('wsEndpoint not found');
    }

    this.global.__BROWSER__ = await puppeteer.connect({ // connect to puppeteer
      browserWSEndpoint: wsEndpoint,
    });

    this.global.page = await this.global.__BROWSER__.newPage();
    await this.global.page.setViewport({width: 1920, height: 1080});
  }

  async teardown() {
    await super.teardown();
  }

  getVmContext() {
    return super.getVmContext();
  }
}

module.exports = PuppeteerEnvironment;

`sequencer.js` 请参考 

四、指定文件顺序

只有先登录了,才能进行完整的 E2E test, 按照 stackoverflow 这里的介绍是实现不了的, 本文另辟捷径,在配置 maxConcurrency: 1 的情况下, 指定 E2E test 文件的执行顺序,从而达到目的。
即在 jest.config.js 中配置 testSequencer: path.resolve(__dirname, '../test/e2e/config/sequencer.js'),

/test/e2e/config/sequencer.js

const _ = require('lodash');
const path = require('path');
const orders = require('./orders');
const Sequencer = require('@jest/test-sequencer').default;

class CustomSequencer extends Sequencer {
  sort(tests) {
    let arrs = [];
    const newTests = _.filter(tests, item => !_.includes(item.path, 'example'));
    _.forEach(orders, pathVal => {
      const index = _.findIndex(newTests, { path: path.resolve(__dirname, `../${pathVal}`) });
      if(index >= 0) {
        arrs.push(newTests[index]);
        newTests.splice(index, 1);
      }
    })
    return _.concat(arrs, newTests);
  }
}

module.exports = CustomSequencer;

/test/e2e/config/orders.js

module.exports =  [ // 严格按照 orders 指定的顺序玩
  'login/index.e2e.js', // 取 e2e 下的文件,含e2e下的路径,如 loanCentral/tearsheet.e2e.js
]

五、 Example

以百度为例, /test/e2e/example/baidu.search.e2e.js

import * as _ from 'lodash';
const { PuppeteerScreenRecorder } = require('puppeteer-screen-recorder');

const timeout = 10000;

beforeAll(async () => {
  let page = global.page;
  await page.goto(`http://www.baidu.com`);
});

afterAll(async () => {
  await page.close();
});

describe( 'Login to THC', () => {
    it('should load error when type invalid username', async () => {
      await page.waitForSelector('input');
      await page.focus("#kw");
      await page.waitForTimeout(300);
      await page.keyboard.type('合肥高新区牛X', {delay: 300});
      await page.click('#su', {delay: 300});
      expect(1).toBe(1);
    }, timeout);
  }
);

上面的代码中, page.keyboard.type 是模拟键盘输入.

六、生产测试 report

yarn add -D jest-html-reporters

配置 jest.config.js

reporters: [ 
    ["jest-html-reporters", {
    "publicPath": path.resolve(__dirname, '../test/e2e/html-report'),
    "filename": "report.html",
    "expand": true,
    "openReport": true
    }]
]

Reference

至此,一个完整的 E2E Test 环境就搭建起来了。

@felix-cao
Copy link
Owner Author

录屏尝试

jest-puppeteer issueshttps://github.com/smooth-code/jest-puppeteer/issues/361,不可行

yarn add -D puppeteer-screen-recorder

实现代码:

import * as _ from 'lodash';
const { PuppeteerScreenRecorder } = require('puppeteer-screen-recorder');

const timeout = 10000;
let page;
let recorder;
beforeAll(async () => {
  page = await global.__BROWSER__.newPage();
  recorder = new PuppeteerScreenRecorder(page);
  await page.setViewport({width: 1920, height: 1080});
  await recorder.start(`${global.videosPath}/ceshi.mp4`);
  // await page.goto(`${global.Host}/tlink/login`);
  await page.goto(`http://www.baidu.com`);
});

afterAll(async () => {
  await page.close();
});

describe( 'Login to THC', () => {
    it('should load error when type invalid username', async () => {
      await page.waitForSelector('input');
      await page.focus("#kw");
      await page.waitForTimeout(300);
      await page.keyboard.type('合肥九义软件公司', {delay: 300});
      await page.click('#su', {delay: 300});

      await recorder.stop();
      expect(1).toBe(1);
    }, timeout);
  }
);

结论:单个是成功的,但是对于整个项目是失败的,并且我们的项目中也没这个需求,因此,这个 feature 暂时不再深入研究下去

@felix-cao
Copy link
Owner Author

尝试让鼠标动起来

yarn add -D ghost-cursor

代码实现:

import { createCursor } from "ghost-cursor"
// .........
const cursor = createCursor(page)
await cursor.click(selector)

但效果不理想,鼠标没动起来,同时频繁引起 timeout 性能问题,不知道是什么原因,但 ghost-cursor 中展示的效果非常好。

@felix-cao
Copy link
Owner Author

felix-cao commented Jun 23, 2021

邮件通知

实现方法:使用 jest reporters 中的自定义reporters 配置来触发发送邮件动作,发送邮件功能使用 emailjs 插件

安装 emailjs

yarn add -D emailjs

/config/jest.config.js

    [`${path.resolve(__dirname, '../test/e2e/config')}/email-report.js`, {
      Smtp: {
        user: '491766244@qq.com',
        password: '1111111',
        host: 'smtp.qq.com',
        ssl: true,
      },
      To: {
        to: 'zfcao@thomasho.cn',
        cc: 'czf2008700@163.com, 491766244@qq.com',
      },
      ReportUrl: 'http://localhost:8081/',
      date,
    }]
  • ReportUrlroot 目录是 /test/e2e/report/, 需要在这里启一个web服务
  • to 发送测试报告到to指定邮箱,多个逗号分割
  • cc 发送测试报告抄送到指定的邮箱, 多个逗号分割
  • Smtp 是配置的邮箱服务器, 参考 emailjs options 部分

email-report.js

const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const dayjs = require("dayjs");
const email = require('emailjs');

class EmailReport {
  constructor(globalConfig, options) {
    this._globalConfig = globalConfig;
    this._options = options;
  }
  onRunStart() {
    const { publicPath, filename } = this._globalConfig.reporters[1][1]
    const reportFile = path.resolve(publicPath, filename);
    try {
      if (fs.existsSync(reportFile)) {
        fs.unlinkSync(reportFile)
      }
    } catch(err) {
      console.error(err)
    }
  }
  onRunComplete(contexts, results) { 
    this.sendEmail(results);
  }

  sendEmail(results) {
    const reportFile = path.resolve(__dirname, './template.html');
    let html = fs.readFileSync(reportFile, 'utf8');
    const client = new email.SMTPClient(this._options.Smtp);
    results.moreUrl = `${this._options.ReportUrl}${this._options.date}`;
    results.report_time = dayjs().format('MM/DD/YYYY HH:mm:ss');
    results.failedTestPer = (results.numFailedTests / results.numTotalTests * 100).toFixed(2);
    results.failedTestSuitesPer = (results.numFailedTestSuites / results.numTotalTestSuites * 100).toFixed(2);
    const arr = html.match(/{[\d_\w]+}/gm);

    _.forEach(arr, item => {
      const key = _.trim(item, '{}');
      html = _.replace(html, item, results[key]);
    })

    client.send(
      {
        from: this._options.Smtp.user,
        ...this._options.To,
        subject: `End to End test --- ${results.report_time}`,
        attachment: [
          {data: html, alternative: true }
        ]
      },
      (err, message) => {
        console.log(err || message);
      } 
    );
  }
}

module.exports = EmailReport;

一开始用的时 sendemail.js,代码如下, 发现其发送邮件不稳定,发送失败的几率比较高,因此不采纳这个方案

sendmail({
      from: 'no-reply@yourdomain.com',
      to: 'zfcao@thomasho.cn, czf2008700@163.com',
      subject: `E2E test Reporters ------ ${dayjs().format('MM/DD/YYYY HH:mm:ss')}`,
      html: content,
    }, function(err, reply) {
      console.log('err:-------------:', err && err.stack);
      console.dir('reply:-------------:', reply);
    });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant