jsdom: Fix for TypeError: range(...).getBoundingClientRect is not a function

Basic info:

  • Node.js version: 12.16.3
  • jsdom version: 16.2.2

Minimal reproduction case

const { JSDOM } = require("jsdom");

const options = {
  runScripts: 'dangerously',
};

const dom = new JSDOM(`
<!DOCTYPE html>
<html>
<body>
  <p id="p0">Range: </p>
  <p id="p1">Bounds: </p>
  <p id="p2">Rects: </p>
  <script>
    let range = document.createRange();
    let p0 = document.getElementById("p0");
    p0.innerHTML += JSON.stringify(range);

    // prevent crash
    try {
      let bounds = range.getBoundingClientRect();
      let p1 = document.getElementById("p1");
      p1.innerHTML += JSON.stringify(bounds);
    }
    catch(e) {
      console.log(e.message);
    }
    
    // prevent crash
    try {
      let rects = range.getClientRects();
      let p2 = document.getElementById("p2");
      p2.innerHTML += JSON.stringify(rects);
    }
    catch(e) {
      console.log(e.message);
    }
  </script>
</body>
</html>
`, options);

console.log(dom.window.document.getElementById("p0").innerHTML);
console.log(dom.window.document.getElementById("p1").innerHTML);
console.log(dom.window.document.getElementById("p2").innerHTML);

Outputs:

range.getBoundingClientRect is not a function
range.getClientRects is not a function
Range: {}
Bounds:
Rects:

How does similar code behave in browsers?

Shouldn’t crash. Should instead show the output:

Range: {}
Bounds: {"x":0,"y":0,"width":0,"height":0,"top":0,"right":0,"bottom":0,"left":0}
Rects: {}

JS Bin: https://jsbin.com/keconidona/edit?html,output

Suggestions for a fix:

Based on my own tests, simply adding these functions to the Range class should already fix the problem:

getBoundingClientRect() {
  return {
    bottom: 0,
    height: 0,
    left: 0,
    right: 0,
    top: 0,
    width: 0
  };
}

getClientRects() {
  return [];
}

I used the same implementation defined here for consistency, although I just noticed this mock is missing the x, y and toJSON properties for getBoundingClientRect, and the item property for getClientRects – perhaps they should be updated as well.

I had to update both jsdom\lib\jsdom\living\range\Range-impl.js and jsdom\lib\jsdom\living\generated\Range.js to fix it in my node_modules. Not sure what else you’d need to change here in your source, but it should be an easy fix.

Workarounds:

If anyone else is facing this same problem, adding this to the top of your test file should prevent the crash while a fix isn’t available (thanks to @raspo for the simplified code):

document.createRange = () => {
  const range = new Range();

  range.getBoundingClientRect = jest.fn();

  range.getClientRects = () => {
    return {
      item: () => null,
      length: 0,
      [Symbol.iterator]: jest.fn()
    };
  };

  return range;
}

or this if you’re not using Jest:

document.createRange = () => {
  const range = new Range();

  range.getBoundingClientRect = () => {
    return {
      x: 0,
      y: 0,
      bottom: 0,
      height: 0,
      left: 0,
      right: 0,
      top: 0,
      width: 0,
      toJSON: () => {}
    };
  };

  range.getClientRects = () => {
    return {
      item: (index) => null,
      length: 0,
      *[Symbol.iterator](){}
    };
  };

  return range;
}

Additional info:

I actually came across this issue when trying to write automated tests for a React app that uses the CodeMirror editor. Using Jest and React Testing Library, the tests would crash whenever I called render() on my component, showing me the error TypeError: range(...).getBoundingClientRect is not a function.

I looked further into it, and found out it was because of a call to a range() function that then tries to access getBoundingClientRect() here, where this range() is either document.createRange() or document.body.createTextRange() as defined here.

Hope this helps 😃

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 41
  • Comments: 15 (1 by maintainers)

Commits related to this issue

Most upvoted comments

I just stumbled upon the same exact issue with a unit test for a component that uses CodeMirror. Thank you for the workaround @D-to-the-K!

Using jest, I ended up simplifying it down to this:

document.createRange = () => {
  const range = new Range();

  range.getBoundingClientRect = jest.fn();

  range.getClientRects = jest.fn(() => ({
    item: () => null,
    length: 0,
  }));

  return range;
};

I managed to resolve all the errors I was getting by putting this into setupTests.js:

Range.prototype.getBoundingClientRect = () => ({
  bottom: 0,
  height: 0,
  left: 0,
  right: 0,
  top: 0,
  width: 0,
});
Range.prototype.getClientRects = () => ({
  item: () => null,
  length: 0,
  [Symbol.iterator]: jest.fn(),
});

Hey, any update on this? Seems pretty clunky atm having to manually implement a mock document.createRange that contains getBoundingClientRect in our tests

With vitest:

import "@testing-library/jest-dom/vitest";

// https://github.com/jsdom/jsdom/issues/3002
Range.prototype.getBoundingClientRect = vi.fn();
Range.prototype.getClientRects = () => ({
  item: vi.fn(),
  length: 0,
  [Symbol.iterator]: vi.fn(),
});

Thanks guys, I too was unit testing an Angular component that uses ngx-codemirror, and your solution helped resolve the error.

Awesome, @Raspo! Glad I could help.

Following your suggestion, I ended up using this:

document.createRange = () => {
  const range = new Range();

  range.getBoundingClientRect = jest.fn();

  range.getClientRects = () => {
    return {
      item: () => null,
      length: 0,
      [Symbol.iterator]: jest.fn()
    };
  };

  return range;
}

Edit: I still needed the length, and TypeScript insisted on me having the Symbol.iterator there.