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

2

5

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?

Maryleemarylin answered 4/6, 2019 at 1:19 Comment(0)
M
6

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.

Maryleemarylin answered 4/6, 2019 at 18:43 Comment(0)
C
2

Great answer above which was going to be my solution but since I'm using react-scripts I didn't really want to eject (testEnvironment is not supported config setting). So I had a look around in source code of react-scripts how I can potentially sneak in and override testEnvironment.

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

Line 124 looks quite interesting

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

So a brilliant idea came to my mind, no pun intended, to stick --env=jsdom-fourteen as command line arg. My CI command looks like this now

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

and it miraculously works :).

I also have setupTests.js file in src folder where I import jest-canvas-mock and jest-environment-jsdom-fourteen but before the --env hacky option the tests were spitting out the insertAdjacentElement mentioned above.

Obvs this is very hacky and it will break at some point but it's fine for now, hopefully Jest will start supporting JSDOM 14 soon.

Connective answered 9/8, 2019 at 8:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.