如何正确模拟 DOM 以便我可以使用 Jest 测试使用 Xterm.js 的 Vue 应用程序?

How do I properly mock a DOM so that I can test a Vue app with Jest that uses Xterm.js?

我有一个呈现 Xterm.js 终端的 Vue 组件。

Terminal.vue

<template>
  <div id="terminal"></div>
</template>

<script>
import Vue from 'vue';
import { Terminal } from 'xterm/lib/public/Terminal';
import { ITerminalOptions, ITheme } from 'xterm';

export default Vue.extend({
  data() {
    return {};
  },
  mounted() {
    Terminal.applyAddon(fit);
    this.term = new Terminal(opts);
    this.term.open(document.getElementById('terminal'));    
  },
</script>

我想测试这个组件。

Terminal.test.js

import Terminal from 'components/Terminal'
import { mount } from '@vue/test-utils';

describe('test', ()=>{
  const wrapper = mount(App);
});

当我 运行 jest 这个测试文件时,我得到这个错误:

TypeError: Cannot set property 'globalCompositeOperation' of null

      45 |     this.term = new Terminal(opts);
    > 46 |     this.term.open(document.getElementById('terminal'));

深入研究堆栈跟踪,我发现它与 Xterm 的 ColorManager 有关。

  at new ColorManager (node_modules/xterm/src/renderer/ColorManager.ts:94:39)
  at new Renderer (node_modules/xterm/src/renderer/Renderer.ts:41:25)

如果我看他们的代码,我可以看到一个比较混乱的东西:

xterm.js/ColorManager.ts

  constructor(document: Document, public allowTransparency: boolean) {
    const canvas = document.createElement('canvas');
    canvas.width = 1;
    canvas.height = 1;
    const ctx = canvas.getContext('2d');
    // I would expect to see the "could not get rendering context"
    // error, as "ctx" shows up as "null" later, guessing from the
    // error that Jest caught
    if (!ctx) {
      throw new Error('Could not get rendering context');
    }
    this._ctx = ctx;
    // Somehow this._ctx is null here, but passed a boolean check earlier?
    this._ctx.globalCompositeOperation = 'copy';
    this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
    this.colors = {
      foreground: DEFAULT_FOREGROUND,
      background: DEFAULT_BACKGROUND,
      cursor: DEFAULT_CURSOR,
      cursorAccent: DEFAULT_CURSOR_ACCENT,
      selection: DEFAULT_SELECTION,
      ansi: DEFAULT_ANSI_COLORS.slice()
    };
  }

我不太清楚 canvas.getContext 显然是如何返回通过布尔检查(在 if(!ctx))的东西,但随后在同一变量上导致 cannot set globalCompositeOperation of null 错误。

我对如何成功进行模拟渲染并因此测试该组件感到非常困惑 - 在 xterm 自己的测试文件中,它们似乎使用 jsdom 创建了一个假的 DOM :

xterm.js/ColorManager.test.ts

  beforeEach(() => {
    dom = new jsdom.JSDOM('');
    window = dom.window;
    document = window.document;
    (<any>window).HTMLCanvasElement.prototype.getContext = () => ({
      createLinearGradient(): any {
        return null;
      },

      fillRect(): void { },

      getImageData(): any {
        return {data: [0, 0, 0, 0xFF]};
      }
    });
    cm = new ColorManager(document, false);
});

但我相信在幕后,vue-test-utils 也在使用 jsdom 创建假的 DOM。此外,文档指出 mount 函数同时附加和呈现 vue 组件。

Creates a Wrapper that contains the mounted and rendered Vue component.

https://vue-test-utils.vuejs.org/api/#mount

如何使用 Jest 成功模拟 DOM 以测试实现 Xterm.js 的 Vue 组件?

这有多种原因。

首先,正如我所怀疑的,Jest js 在底层使用了 jsdom。

Jsdom 不支持开箱即用的 canvas DOM api。首先,你需要jest-canvas-mock

npm install --save-dev jest-canvas-mock

然后,您需要将它添加到 jest 配置的 setupFiles 部分。我的在 package.json,所以我这样添加它:

package.json

{
  "jest": {
    "setupFiles": ["jest-canvas-mock"]
  }
}

然后,我收到关于 insertAdjacentElement DOM 元素方法的错误。具体来说,错误是:

[Vue warn]: Error in mounted hook: "TypeError: _this._terminal.element.insertAdjacentElement is not a function"

这是因为截至今天,jest 使用的 jsdom 版本是 11.12.0 :

npm ls jsdom

└─┬ jest@24.8.0
  └─┬ jest-cli@24.8.0
    └─┬ jest-config@24.8.0
      └─┬ jest-environment-jsdom@24.8.0
        └── jsdom@11.12.0 

通过, I discovered that at version 11.12.0, jsdom had not implemented insertAdjacentElement. However, a more recent version of jsdom implemented insertAdjacentElement back in July of 2018.

的帮助

Efforts to convince the jest team 使用更新版本的 jsdom 失败。他们不愿意放弃 node6 的兼容性,直到绝对的最后一秒(他们在四月份声称),或者根本不想再实现 jsdom,并且建议人们如果想要特征。

幸运的是,您可以手动设置 jest 使用的 jsdom 版本。

首先,安装 jest-environment-jsdom-fourteen 包。

npm install --save jest-environment-jsdom-fourteen

然后,你需要修改你的jest配置的testEnvironment 属性。所以,现在我的笑话配置看起来像:

package.json

  "jest": {
    "testEnvironment": "jest-environment-jsdom-fourteen",
    "setupFiles": ["jest-canvas-mock"]
  }

现在,我可以 运行 测试而不会出错。

上面的好答案将成为我的解决方案,但由于我正在使用 react-scripts 我真的不想弹出(不支持 testEnvironment 配置设置) .因此,我查看了 react-scripts 的源代码,了解如何潜入并覆盖 testEnvironment。

https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/scripts/test.js

第 124 行看起来很有趣

resolvedEnv = resolveJestDefaultEnvironment(`jest-environment-${env}`);

所以我想到了一个绝妙的主意,没有双关语,将 --env=jsdom-fourteen 作为命令行参数。我的 CI 命令现在看起来像这样

cross-env CI=true react-scripts test --coverage --env=jsdom-fourteen --testResultsProcessor=jest-teamcity-reporter

而且它奇迹般地有效:)。

我在导入 jest-canvas-mockjest-environment-jsdom 的 src 文件夹中也有 setupTests.js 文件-fourteen 但在 --env hacky 选项之前,测试吐出了​​上面提到的 insertAdjacentElement。

Obvs 这非常 hacky,它会在某些时候中断,但现在还好,希望 Jest 很快就会开始支持 JSDOM 14。