xyflow: edges not rendered to JSDOM when testing with Jest and React Testing Library

Hi!

I’m having some issue on making the nodes and edges to show up when I’m rendering the graph to JSDOM with Jest and React Testing Library. I have the following set up:

//App.js
import ReactFlow from "react-flow-renderer";

const App = () => (
  <ReactFlow
    elements={[
      {
        data: { label: "a" },
        id: "b",
        position: { x: 10, y: 10 },
      },
      {
        data: { label: "b" },
        id: "a",
        position: { x: 10, y: 80 },
      },
      {
        id: "edge-a-b",
        source: "a",
        target: "b",
      },
    ]}
  />
);

And then I try to test it:

//App.test.js
import { render } from "@testing-library/react";

import App from "./App";

it("should render", async () => {
  const { debug, getByText } = render(<App />);

  debug(); 
  expect(getByText("a")).toBeInTheDocument();
});

The test fails because the node is not rendered. If I do a debug, I can see that the wrapper elements for the graph are being rendered, but no nodes or edges.

<body>
  <div>
    <div
      class="react-flow"
    >
      <div
        class="react-flow__renderer react-flow__zoompane"
      >
        <div
          class="react-flow__nodes"
          style="transform: translate(0px,0px) scale(1);"
        />
        <svg
          class="react-flow__edges"
          height="500"
          width="500"
        >
          <defs>
            <marker
              class="react-flow__arrowhead"
              id="react-flow__arrowclosed"
              markerHeight="12.5"
              markerWidth="12.5"
              orient="auto"
              refX="0"
              refY="0"
              viewBox="-10 -10 20 20"
            >
              <polyline
                fill="#b1b1b7"
                points="-5,-4 0,0 -5,4 -5,-4"
                stroke="#b1b1b7"
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="1"
              />
            </marker>
            <marker
              class="react-flow__arrowhead"
              id="react-flow__arrow"
              markerHeight="12.5"
              markerWidth="12.5"
              orient="auto"
              refX="0"
              refY="0"
              viewBox="-10 -10 20 20"
            >
              <polyline
                fill="none"
                points="-5,-4 0,0 -5,4"
                stroke="#b1b1b7"
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="1.5"
              />
            </marker>
          </defs>
          <g
            transform="translate(0,0) scale(1)"
          />
        </svg>
        <div
          class="react-flow__pane"
        />
      </div>
    </div>
  </div>
</body>

I wanted to set up a codesandbox, but I couldn’t get to polyfill ResizeObserver.

Any ideas on what might be going on and how to ‘fix’ it?

EDIT: if I pass a callback to onLoad, I can see the elements in there:

import ReactFlow from "react-flow-renderer";

const App = () => {
  const onLoad = (instance) => {
    console.log(instance.getElements());
  };

  return (<ReactFlow elements={[...]} onLoad={onLoad} />);
};

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 33 (11 by maintainers)

Most upvoted comments

I put it in the test file itself (eg: MyComponent.test.tsx) inside beforeAll. Below is my setup:

beforeAll(() => {
  // Setup ResizeObserver and offset* properties
  // see: https://github.com/wbkd/react-flow/issues/716

  window.ResizeObserver =
    window.ResizeObserver ||
    jest.fn().mockImplementation(() => ({
      disconnect: jest.fn(),
      observe: jest.fn(),
      unobserve: jest.fn(),
    }));

  Object.defineProperties(window.HTMLElement.prototype, {
    offsetHeight: {
      get() {
        return parseFloat(this.style.height) || 1;
      },
    },
    offsetWidth: {
      get() {
        return parseFloat(this.style.width) || 1;
      },
    },
  });

  (window.SVGElement as any).prototype.getBBox = () => ({x:0, y:0, width: 0, height: 0});
});

@moklick I think it would be useful to add an example to the site. Most people use Jest + Testing Library in their application, and setting up tests is a bit challenging.

Hey everyone,

I looked into this but I haven’t found a good solution yet. There are two problems:

  1. ReactFlow measures nodes and updates the dimensions via ResizeObserver. The ResizeObserver doesn’t seem to work while running tests. This is fixable. I could update the nodes immediately while we are in a testing environment.
  2. When I apply the fix mentioned above, I get another error: "[TypeError: window.DOMMatrixReadOnly is not a constructor" I don’t understand why this is not supported by jest or why I can’t find anything about it. It’s a dom library that is supoorted by all browsers https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrixReadOnly

Can anyone help here?

Nice ! Thank you @moklick, I applied the changes above and both nodes and edges are now fully visible in my jest tests 👍

Hey there,

Ran into this bug. I’m pretty sure I found the root problem in react-testing library. First, like folks have noted you need to make sure to mock the RenderObserver for things to really just work to begin with. After that, nodes will render but edges don’t. Edges aren’t being rendered because in edge initialization it bails out if the source node __rf.width isn’t set: https://github.com/wbkd/react-flow/blob/main/src/container/EdgeRenderer/index.tsx#L90

This width is set by getting node.offsetWidth here: https://github.com/wbkd/react-flow/blob/41db69e06c22d278657ba810198f371119b4972b/src/utils/index.ts#L14.

React-testing-library uses jsdom under the hood, which by design doesn’t implement layout, but this is effectively the same problem people are discussing here: https://github.com/jsdom/jsdom/issues/135, and can pretty much be solved by putting this in your code:

Object.defineProperties(window.HTMLElement.prototype, {
  offsetHeight: {
    get() { return parseFloat(this.style.height) || 1; },
  },
  offsetWidth: {
    get() { return parseFloat(this.style.width) || 1; },
  },
});

To stub out the methods to ensure node width isn’t falsy, and then you get edges rendered.

I’m not really sure if there is much that needs to be fixed in this library, since I don’t think it makes sense for it to be aware of the fact that it’t not actually being rendered… but I think it might be solved by an example.

Hey, sorry I’m a bit confused here. What exactly is working? I’m running version 9.3.2 (the latest), and I still have the original problem that no edges or nodes are rendered in the tests.

I tried adding:

import { ResizeObserver } from '@juggle/resize-observer';
global.ResizeObserver = ResizeObserver;

which fixes this error:

console.error Error: Uncaught [ReferenceError: ResizeObserver is not defined]

But still no nodes and edges are rendered.

Notes:

  1. I still have the following warning console.warn The React Flow parent container needs a width and a height to render the graph., despite wrapping ReactFlow into <div style={{ width: 500, height: 500 }}>
  2. I tried setting the following option onlyRenderVisibleElements={false}, but it did not make a difference

@MatiasCiccone @kislakiruben can you confirm you are able to access the elements within react flow in your tests?

Good news. I’ve found a better solution. You can get rid of the @juggle/resize-observer dependency and don’t need to pass a fake width and height to your nodes in order to test them. The trick is to create a ResizeObserver class that works slighty different than the original one.

Updated setupTests.ts:

// To make sure that the tests are working, it's important that you are using
// this implementation of ResizeObserver and DOMMatrixReadOnly 
class ResizeObserver {
  callback: globalThis.ResizeObserverCallback;

  constructor(callback: globalThis.ResizeObserverCallback) {
    this.callback = callback;
  }

  observe(target: Element) {
    this.callback([{ target } as globalThis.ResizeObserverEntry], this);
  }

  unobserve() {}

  disconnect() {}
}

global.ResizeObserver = ResizeObserver;

class DOMMatrixReadOnly {
  m22: number;
  constructor(transform: string) {
    const scale = transform?.match(/scale\(([1-9.])\)/)?.[1];
    this.m22 = scale !== undefined ? +scale : 1;
  }
}
// @ts-ignore
global.DOMMatrixReadOnly = DOMMatrixReadOnly;

Object.defineProperties(global.HTMLElement.prototype, {
  offsetHeight: {
    get() {
      return parseFloat(this.style.height) || 1;
    },
  },
  offsetWidth: {
    get() {
      return parseFloat(this.style.width) || 1;
    },
  },
});

(global.SVGElement as any).prototype.getBBox = () => ({
  x: 0,
  y: 0,
  width: 0,
  height: 0,
});

export {};

I’ll updated the docs, too: https://reactflow.dev/docs/guides/testing/

Thanks @bcakmakoglu ! I have an idea… I will just do something like this:

export function getScaleFromElement(element: HTMLDivElement): number {
  if (window.DOMMatrixReadOnly) {
    const style = window.getComputedStyle(element);
    const { m22 } = new window.DOMMatrixReadOnly(style.transform);
    return m22;
  }

  const scale = element.style?.transform?.match(/scale\(([1-9.])\)/)?.[1];
  return scale !== undefined ? +scale : 1;
}

@gfox1984, @Zachary Heller: I could still see this issue in the latest version, edges are not rendering in test. Do you have any solution?

in setupTests.js:

import { ResizeObserver } from '@juggle/resize-observer';

global.ResizeObserver = ResizeObserver;