Things I Learned (Cypress)

Add Context in Mochawesome Report🔗

• Cypress

mochawesome 是为 Mocha 提供的一个 Report 库,可以用于生成不错的 HTML 报告(见 npm)。库本身提供了一个 addContext 的 API,可以用于在运行 Test 的时候,存入额外的信息到 Context 中,最终在生成 HTML 报告的时候,将这部分 Context 信息写入对应的测试用例内。

参考代码如下:

const addContext = require('mochawesome/addContext');

describe('test suite', function () {
  it('unit test', function () {
    addContext(this, 'content');
    // or
    addContext(this, {
      title: 'title',
      value: 'value or object'
    });
  });
});

几点说明:

  1. 在 beforeEach 或 afterEach 的钩子内调用 addContext 也是允许的;
  2. 如果给定的第二个参数是 URL 或是一个图片的话,mochawesome 可以有相对应的展示;
  3. 记得 it 函数的第二个参数不要使用箭头函数,否则 this 的指向会有问题

然而,在 Cypress 中如果试图直接使用上述方法运行代码,会发现并不能成功。最终生成的报告内并没有对应的 context 信息。其原因在于,Cypress 在运行的过程中,原本被赋值的 context 属性被覆盖掉了,导致虽然进行了 addContext 的赋值,但是最终的结果中并没有保留这部分数据。

一个可行的解决方案是,在 test:after:run 事件中再进行赋值,保证结果生效。示例代码如下:

const addContext = require('mochawesome/addContext');

Cypress.Commands.add('addContext', (content) => {
  cy.once('test:after:run', test => {
    addContext({ test }, content);
  });
});

几点说明:

  1. 因为 addContext API 本质上就是往 test 对象上写 context 数据,而 Cypress 的 API 正好提供了 test 对象,因而第一个参数不需要传 this,直接将 test 以合适的方法传入就可以了;
  2. 上面的代码定义了一个 Cypress 的命令方便各个地方调用,类似的代码改成一个普通的函数也是可以的;
  3. 需要用 cy.once 保证这个代码只会被调用一次,这样其他的测试用例中不会有类似的数据被写入

Cypress Upload File🔗

• Cypress

Cypress 没有提供原生的上传文件支持,如果需要在 E2E 测试中进行文件上传的测试工作,最简单的方式就是自己写一个自定义的 Command。参考代码如下:

Cypress.Commands.add(
  'uploadFile',
  { prevSubject: true },
  (subject, fixtureFileName, mimeType = '') => {
    return cy.fixture(fixtureFileName, 'base64')
      .then(Cypress.Blob.base64StringToBlob)
      .then(blob => {
          const el = subject[0];
          const nameSegments = fixtureFileName.split('/');
          const name = nameSegments[nameSegments.length - 1];
          const testFile = new File([blob], name, { type: mimeType });
          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(testFile);

          const setter =
            Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files')
              .set;
          setter.call(el, dataTransfer.files);
        
          const event = new Event('change', { bubbles: true });
          el.dispatchEvent(event);
          return subject;
      });
  }
);

代码的解释如下:

cy.fixture(fixtureFileName, 'base64')

fixture 是 Cypress 提供的原生 API,可以读取 cypress/fixture 目录下的指定文件(文件名为 fixtureFileName)。上面这个 API 指定了文件需要以 Base64 的方式读取出来。

Cypress.Blob.base64StringToBlob 这一步顾名思义,就是将 Base64 字符串转化成对应的 Blob 类型。

剩下的代码,就是用 JavaScript 的方式模拟一个文件上传事件。其中,需要先将文件从 Blob 转换成 File(这里涉及到可能的 mime type 检查);然后,创建一个 DataTransfer 对象,把文件放进去,再赋值给 input(这里需要说明的是,React 组件会对 input 的属性做一层 proxy,因此直接使用 input.files = dataTransfer.files 这样的写法,调用的是 React 的方法而不是真正 DOM 的方法。按上面代码中的方法获取到真正的 setter,然后调用可以绕过去)。最后,在创建一个 Change 事件,传递给 input 组件,触发即可。

当然,简单起见,可以直接使用现成的库:cypress-file-upload。GitHub 地址见这里。


cypress reporter🔗

• Cypress

Cypress 默认提供了 spec reporter,在 CLI 运行的时候,会将结果输出到 stdout 中。同时,如果使用编程的方法直接调用 Cypress.run API,会以 Promise 的方式将运行的结果返回,程序可以从运行结果中,将主要的运行数据给读取出来。然而,不论是哪一种方案,都不能非常直观的将运行结果展示出来。以下介绍如何在 Cypress 中引入 Mochawesome reporter,用于生成直观的 HTML 报告。

需要事先说明的是,虽然 Cypress 是建立在 Mocha 的基础上,且 Mochawesome 是 Mocha 中非常流行的报告生成方案,但是直接使用 Mochawesome 在 Cypress 中生成报表还是有问题的。主要的原因在于,Cypress 调整了测试的行为,自 3.0 版本开始,每一个测试用例(spec)都是单独运行的。因此,原生的 Mochawesome 无法直接生成一个包含所有测试用例的完整报告。为此,需要借助一些额外的工具。

首先,在项目需要用到 mocha 和 mochawesome:

yarn add mocha mochawesome

另外需要两个额外的包,分别是 mochawesome-merge 和 mochawesome-report-generator。可以通过 yarn 或 npm 安装到工作目录中,也可以通过 npx 在需要的时候直接使用。这里,mochawesome-merge 将用于将所有的测试用例运行结果进行合并的,然后用 mochawesome-report-generator 包生成统一的完整报告。

接下来,修改 cypress.json 配置文件如下:

{
  "reporter": "mochawesome",
  "reporterOptions": {
    "reportDir": "cypress/results",
    "overwrite": false,
    "html": false,
    "json": true
  }
}

配置完成,再运行 Cypress,会在 cypress/results 目录下生成一批 JSON 文件(如 mochawesome.json,mochawesome_001.json,……)。

有了这批生成的 JSON 报告,就可以使用 mochawesome-merge 命令,将这些 JSON 文件打包成一个完整的 JSON 报告。CLI 命令如下:

npx mochawesome-merge --reportDir cypress/results > mochawesome.json

生成了完整的 JSON 文件之后,可以通过 mochawesome-report-generator 生成需要的 HTML 报告:

npx mochawesome-report-generator mochawesome.json

当然,如果需要以编程的方式来执行上面的生成报告过程,可以参考下面的代码:

const cypress = require('cypress');
const { merge } = require('mochawesome-merge');
const generator = require('mochawesome-report-generator');

async function generate() {
  await cypress.run(config)
  const report = await merge({ reportDir: 'cypress/results' });
  const htmlReports = await generator.create(report, {
    reportFilename: 'report.html',
    // cdn 的命令可以在生成 HTML 报告的时候不额外生成 JavaScript/CSS 文件
    // 这些静态文件会走 CDN (unpkg)
    // 这样,只需要保存一个 HTML 文件就可以了,方便存储
    cdn: true
  });
  // report 就是 HTML 报告文件生成的路径
  const [report] = htmlReports;
}

generate();

更多的参数使用可以参考项目的源代码。

需要注意的一点是,生成 Report 之前需要确认 cypress/results 目录是否是干净的空目录。如果目录中仍然包含上一次运行的结果,那么最终合并报告的时候,两次运行的结果会叠加在一起,最终导致报告中包含多次运行的内容。一般在 Docker 中运行的话不会有这个问题,但是在本地跑的时候需要注意清理工作。


Cypress No Internet🔗

• Cypress

在 Cypress 开发的过程中,因为其他操作导致内存吃紧,最终 Cypress 被操作系统杀掉。之后,再重启 Cypress,发现一直运行失败,所有 Test 全部都无法执行成功。

通过 cypress open 来打开 UI 并执行任意测试用例,发现浏览器直接返回 No Internet。浏览器给出的建议是:

  1. 检查系统是否联网
  2. 检查是否有 Proxy 配置

电脑本身可以正常上网,也没有手动进行任何 Proxy 配置,浏览器给出的建议并不能真正解决问题。

通过 ps -ef | grep Cypress 后发现,即使在 Cypress 没有运行的情况下,依然有运行中的进程:

执行 kill 命令杀死这些个进程。再重试 Cypress 就可以正常运作了。


Get Element by Content🔗

• Cypress

在 Cypress 中可以通过字符串来查找和定位元素,常用的命令包括 get,find 等。这里以 get 命令为例,在其文档可以看到一些用法的说明。

JavaScript 中支持的 selector 在 get 中都是可以使用的,除此之外,文档指出所有 jQuery 支持的也同样支持。(事实上,在 Cypress 注册命令的代码处可以,可以找到和 DOM 相关的代码,这部分代码中不难发现 jQuery 的影子)。

有了 jQuery 的强力支持,就可以写出复杂的选择条件。比如,选取含有某一文案的 HTML 组件。

在 jQuery 中,提供了 :contains 这个选择器(文档),可以找出所有包含某一指定字符串的所有元素。

于是,想要找出弹出层中的 Submit 按钮,就可以这么写:

Cypress.get('[role=dialog] button:contains("Submit")')

这里,使用了 [role=dialog] 来找到弹出层(dialog 相关的介绍可以看 MDN),然后再通过 button 找到按钮,最后用 :contains("Submit") 来找到 Submit 按钮。

当然,如果一个产品本身支持 i18n,那么 :contains 后面的部分就不好写了。一个可行的方案,是通过当前页面的语言,从一组文案中找到合适的文案,再调用 :contains 选择器。比如,写一个简单的 Cypress 命令,如下:

Cypress.Commands.add('getByText', (query, texts) =>
  cy.get('html').first().then(html => {
    const { lang } = html;
    return cy.get(`${query}:contains('${texts[lang]}')`);
  })
);

这里,通过 HTML 上的 lang 标记来确定当前页面所选用的语言(lang 的一些细节可以参考 MDN),然后再根据语言,从一组文案(即 texts 这个对象)中选取当前需要使用的文案。

命令的使用方法:

cy.getByText('[role=dialog] button', { en: 'Submit', zh: '提交' })
  .first()
  .click();