Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a Vue component that renders an Xterm.js terminal.

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>

I would like to test this component.

Terminal.test.js

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

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

When I run jest on this test file, I get this error:

TypeError: Cannot set property 'globalCompositeOperation' of null

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

Digging into the stack trace, I can see it has something to do with Xterm's 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)

If I look at their code, I can see a relatively confusing thing:

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()
    };
  }

I'm not quite clear on how canvas.getContext apparently returned something that passed the boolean check (at if(!ctx)) but then later caused a cannot set globalCompositeOperation of null error on that same variable.

I'm very confused about how I can successfully go about mock-rendering and thus testing this component - in xterm's own testing files, they appear to be creating a fake DOM using jsdom:

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);
});

But I believe that under the hood, vue-test-utils is also creating a fake DOM using jsdom. Furthermore, the documentation indicates that the mount function both attaches and renders the vue component.

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

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

How can I successfully mock a DOM in such a way that I can test a Vue component that implements Xterm.js, using Jest?

like image 516
Caleb Jay Avatar asked Jan 27 '23 05:01

Caleb Jay


1 Answers

There are multiple reasons for this.

First of all, Jest js uses jsdom under the hood, as I suspected.

Jsdom doesn't support the canvas DOM api out of the box. First of all, you need jest-canvas-mock.

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

Then, you need to add it to the setupFiles portion of your jest config. Mine was in package.json, so I added it like so:

package.json

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

Then, I was getting errors about the insertAdjacentElement DOM element method. Specifically, the error was:

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

This is because the version of jsdom used by jest is, as of today, 11.12.0 :

npm ls jsdom

└─┬ [email protected]
  └─┬ [email protected]
    └─┬ [email protected]
      └─┬ [email protected]
        └── [email protected] 

Through the help of stackoverflow, 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 to use a more up to date version of jsdom have failed. They are unwilling to let go of node6 compatibility until the absolute last second (they claimed back in April), or alternatively don't want to implement jsdom at all anymore, and are recommending people fork their own versions of the repo if they want the feature.

Luckily, you can manually set which version of jsdom jest uses.

First, install the jest-environment-jsdom-fourteen package.

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

Then, you need to modify the testEnvironment property of your jest config. So, now my jest config looks like:

package.json

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

Now, I can run tests without errors.

like image 158
Caleb Jay Avatar answered Jan 29 '23 07:01

Caleb Jay