84

I'm trying to write tests for my web components projects in jest. I already use babel with es2015 preset. I'm facing an issue while loading the js file. I have followed a piece of code where document object has a currentScript object. But in test context it is null. So I was thinking of mocking same. But jest.fn() is not really help in same. How can I handle this issue?

Piece of code where jest is failing.

var currentScriptElement = document._currentScript || document.currentScript;
var importDoc = currentScriptElement.ownerDocument;

Test case I have written. component.test.js

import * as Component from './sample-component.js';

describe('component test', function() {
  it('check instance', function() {
    console.log(Component);
    expect(Component).toBeDefined();
  });
});

Following is the error thrown by jest

Test suite failed to run

    TypeError: Cannot read property 'ownerDocument' of null

      at src/components/sample-component/sample-component.js:4:39

Update: As per suggestion from Andreas Köberle, I have added some global vars and tried to mock like following

__DEV__.document.currentScript = document._currentScript = {
  ownerDocument: ''
};
__DEV__.window = {
  document: __DEV__.document
}
__DEV__.document.registerElement = jest.fn();

import * as Component from './arc-sample-component.js';

describe('component test', function() {
  it('check instance', function() {
    console.log(Component);
    expect(Component).toBeDefined();
  });
});

But no luck

Update: I have tried above code without __dev__. Also by setting document as global.

Sébastien Temprado
  • 1,413
  • 4
  • 18
  • 29
thecodejack
  • 12,689
  • 10
  • 44
  • 59

9 Answers9

56

Similar to what others have said, but instead of trying to mock the DOM yourself, just use JSDOM:

// __mocks__/client.js

import { JSDOM } from "jsdom"
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window

Then in your jest config:

    "setupFiles": [
      "./__mocks__/client.js"
    ],
Marius Butuc
  • 17,781
  • 22
  • 77
  • 111
Patrick Lee Scott
  • 8,217
  • 3
  • 36
  • 42
25

I have resolved this using setUpFiles property in jest. This will execute after jsdom and before each test which is perfect for me.

Set setupFiles, in Jest config, e.g.:

"setupFiles": ["<rootDir>/browserMock.js"]


// browserMock.js
Object.defineProperty(document, 'currentScript', {
  value: document.createElement('script'),
});

Ideal situation would be loading webcomponents.js to polyfill the jsdom.

thecodejack
  • 12,689
  • 10
  • 44
  • 59
19

I have been struggling with mocking document for a project I am on. I am calling document.querySelector() inside a React component and need to make sure it is working right. Ultimately this is what worked for me:

it('should test something', () => {
  const spyFunc = jest.fn();
  Object.defineProperty(global.document, 'querySelector', { value: spyFunc });
  <run some test>
  expect(spyFunc).toHaveBeenCalled()
});
sam
  • 1,005
  • 1
  • 11
  • 24
strausd
  • 401
  • 3
  • 11
7

If like me you're looking to mock document to undefined (e.g. for server side / client side tests) I was able to use object.defineProperty inside my test suites without having to use setupFiles

Example:

beforeAll(() => {
  Object.defineProperty(global, 'document', {});
})
tctc91
  • 1,343
  • 2
  • 21
  • 41
  • I tried to mock `document.referrer` and `document.URL` by providing them in the object...no luck. The properties still showed up as their usual values in test runner. – theUtherSide May 18 '18 at 22:41
  • @theUtherSide I've noticed this solution working in some versions of Node and not others. Maybe install NVM and have a play with different versions – tctc91 May 19 '18 at 13:38
  • Thanks for the tip! I'll play with Node version and post some findings! I really like this strategy better than using the Jest config. I work on a big team, and it's better to keep our unit tests isolated and, of course atomic! – theUtherSide May 19 '18 at 15:12
  • Confirming -- The app where I tried this is using Node v8.11.1 and Jest 21.2.1 – theUtherSide May 21 '18 at 20:43
  • 1
    `TypeError: Cannot redefine property: document` => ` at JSDOMEnvironment.teardown (node_modules/jest-environment-jsdom/build/index.js:192:14)` – Dimitri Kopriwa Feb 15 '20 at 07:03
6

If you need to define test values for properties, there is a slightly more granular approach. Each property needs to be defined individually, and it's also necessary to make the properties writeable:

Object.defineProperty(window.document, 'URL', {
  writable: true,
  value: 'someurl'
});

See: https://github.com/facebook/jest/issues/890

This worked for me using Jest 21.2.1 and Node v8.11.1

theUtherSide
  • 3,338
  • 4
  • 36
  • 35
6

This is the structure in my project named super-project inside the folder super-project:


  • super-project
    • config
      • __mocks__
        • dom.js
    • src
      • user.js
    • tests
      • user.test.js
    • jest.config.js
    • package.json

You need to setup Jest to use a mock in your tests:

dom.js:

import { JSDOM } from "jsdom"
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window

user.js:

export function create() {
  return document.createElement('table');  
}

user.test.js:

import { create } from "../src/user";

test('create table', () => {
  expect(create().outerHTML).toBe('<table></table>');
});

jest.config.js:

module.exports = {
  setupFiles: ["./config/__mocks__/dom.js"],
};

References:

You need to create a manual mock:
https://jestjs.io/docs/en/manual-mocks.html

Manipulating DOM object:
https://jestjs.io/docs/en/tutorial-jquery

Jest Configuration:
https://jestjs.io/docs/en/configuration

danilo
  • 7,680
  • 7
  • 43
  • 46
3

I could resolve this same issue using global scope module on nodejs, setting document with mock of document, in my case, getElementsByClassName:

// My simple mock file
export default {
    getElementsByClassName: () => {
        return [{
            className: 'welcome'
        }]
    }
};

// Your test file
import document from './name.component.mock.js';
global.document = {
    getElementsByClassName: document.getElementsByClassName
};
Renato B.
  • 31
  • 3
2

I found another solution. Let's say inside of your component you want to get a reference to an element in the DOM by className (document.getElementsByClassName). You could do the following:

let wrapper
beforeEach(() => {
    wrapper = mount(<YourComponent/>)
    jest.spyOn(document, 'getElementsByClassName').mockImplementation(() => 
        [wrapper.find('.some-class').getDOMNode()]
    )
})

This way you are manually setting the return value of getElementsByClassName equal to the reference of .some-class. It might be necessary to rerender the component by calling wrapper.setProps({}).

Hope this helps some of you!

Marnix.hoh
  • 1,556
  • 1
  • 15
  • 26
-2

Hope this helps

const wrapper = document.createElement('div');
const render = shallow(<MockComponent{...props} />);
document.getElementById = jest.fn((id) => {
      wrapper.innerHTML = render.find(`#${id}`).html();
      return wrapper;
    });
vnxyz
  • 386
  • 3
  • 10
  • 1
    This answer assumes the Component is a React Component. I don't see anything that indicates this. It looks like a plain javascript web component to me. – Greg Malcolm Mar 04 '20 at 18:23