Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

edges not rendered to JSDOM when testing with Jest and React Testing Library #716

Closed
kislakiruben opened this issue Nov 23, 2020 · 33 comments
Assignees
Labels
help wanted Extra attention is needed

Comments

@kislakiruben
Copy link

kislakiruben commented Nov 23, 2020

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} />);
};
@moklick
Copy link
Member

moklick commented Nov 26, 2020

Hey @kislakiruben

I could imagine that react flow is not finished with rendering. It seems that it only rendered the first tick. Maybe you could use waitFor from @testing-library/react?

@kislakiruben
Copy link
Author

yes, I tried to do that, but I got the same result. I'm wondering whether it has anything to do with how easy-peasy is initializing the state.

@MatiasCiccone
Copy link

I have the same issue. I'll try to create a sandbox for this

@MatiasCiccone
Copy link

This works for me locally but I'm not able to make it work in the sandbox. I may be missing something. Here is the link if anyone wants to give it a try https://codesandbox.io/s/blissful-spence-hnozn

@moklick
Copy link
Member

moklick commented Jan 20, 2021

Could you solve this @kislakiruben ?

@kislakiruben
Copy link
Author

Could you solve this @kislakiruben ?

Not really. I ditched writing unit tests for the graph implementaion.

@moklick
Copy link
Member

moklick commented Jan 20, 2021

All I needed to do is to mock the ResizeObserver to get this running. I used a fresh CRA and the latest version (8.4.1) of react flow.

@MatiasCiccone
Copy link

@moklick can you show how you mocked the ResizeObserver?

@moklick
Copy link
Member

moklick commented Jan 21, 2021

in setupTests.js:

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

global.ResizeObserver = ResizeObserver;

@moklick
Copy link
Member

moklick commented Jan 23, 2021

Could you check if it works for you with the current version @kislakiruben ?

@kislakiruben
Copy link
Author

@moklick it looks like it works with the latest version (8.5.0). we're using 8.0.0-next.4. thank you for fixing this!

@kislakiruben
Copy link
Author

I'm not sure, but I believe this commit fixed it: d6a410a

@moklick
Copy link
Member

moklick commented Jan 23, 2021

Ok, cool! Feel free to re-open this issue if you encounter a problem with react-flow + testing-library/react.

@moklick moklick closed this as completed Jan 23, 2021
@MatiasCiccone
Copy link

I just checked this with 8.7.0 and it is working.

@gfox1984
Copy link

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?

@m04r
Copy link

m04r commented May 12, 2021

i am having a similar issue.

when running tests, nodes are being rendered, but edges are not. i am using version 9.0.1 and ResizeObserver is being mocked as described above.

@zheller
Copy link

zheller commented Aug 22, 2021

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: jsdom/jsdom#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.

@Michael-Xie
Copy link

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

where would you put this code in?

When I put it into setupTests.js or the test file, it would give this warning:
console.error Warning: validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.

@gfox1984
Copy link

gfox1984 commented Sep 9, 2021

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.

@Michael-Xie
Copy link

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

where would you put this code in?

When I put it into setupTests.js or the test file, it would give this warning:
console.error Warning: validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.

@gfox1984 Thanks for providing the test file. It added coverage onto the CustomEdge component when I render the React Flow diagram.

As for my specific warning/error that I am seeing, I changed the <body> tag used <foreignObject> to div instead to resolve it. The <body> tag was used in the Edge with Button example, and am wondering if that needs to be updated to avoid this warning.

@bipin-u-babu
Copy link

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

@Bars92
Copy link

Bars92 commented Aug 29, 2022

Sadly I am also experiencing the same issue where edges are not visible in tests after applying #716 (comment). It does seem that the edges are been calculated but never rendered:

<svg
  class="react-flow__edges react-flow__container"
  height="1"
  style="z-index: 0;"
  width="1"
>
  <defs />
  <g /> <----
</svg>

using react v18.2, react-flow-renderer: v10.3.14 and @testing-library/react v13.3. Wasn't able to get the edges rendered, if anyone has an idea, otherwise I'll have to dig around more and debug.

@moklick
Copy link
Member

moklick commented Sep 5, 2022

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?

@moklick moklick reopened this Sep 5, 2022
@moklick moklick self-assigned this Sep 5, 2022
@moklick moklick added the help wanted Extra attention is needed label Sep 5, 2022
@moklick moklick changed the title Nodes and edges not rendered to JSDOM when testing with Jest and React Testing Library edges not rendered to JSDOM when testing with Jest and React Testing Library Sep 5, 2022
@bcakmakoglu
Copy link
Contributor

@moklick Maybe it needs to be mocked in the jest environment? Seems like documentation on this particular API is ... non-existent :(

I just found this gist that seems somewhat relevant.

@moklick
Copy link
Member

moklick commented Sep 5, 2022

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
Copy link

gfox1984 commented Sep 5, 2022

Hey, sorry, I'm late to the party. I haven't updated reactflow in a while, so yes it's possible that there's more that needs to be mocked now. I guess that window.DOMMatrixReadOnly needs a polyfill.

I haven't tried it, but I see that there is one available here: https://github.com/thednp/dommatrix. Then you should be able to do something like this:

import DOMMatrix from 'dommatrix';
...

beforeAll(() => {
  ...  
  window.DOMMatrixReadOnly = DOMMatrix;
});

@moklick I'm not sure it's a good idea to start adding fallbacks to the code just for Jest, although I agree that it would be convenient and that it's not really specific to Jest either (you could see it as a general fallback for exotic browsers). But maybe a better approach would be to create an interface for all the DOM-specific operations that are not available in the testing environment, then add an optional prop to the ReactFlow Provider to be able pass an implementation for it (= dependency injection). If not provided, you'd use your default implementation based on the actual DOMMatrixReadOnly, ResizeObserver, etc. In Jest, we'd just use our own (or you could also provide a package (or export some utilities) to help with that.)

@moklick
Copy link
Member

moklick commented Sep 5, 2022

@gfox1984 I agree! Would be better to mock things from within setupTests. Now that I am working on it, do you know how to mock offsetWidth and offsetHeight of an element (that's another issue I just encountered)? I can't find anything about it.

@gfox1984
Copy link

gfox1984 commented Sep 5, 2022

@gfox1984 I agree! Would be better to mock things from within setupTests. Now that I am working on it, do you know how to mock offsetWidth and offsetHeight of an element (that's another issue I just encountered)? I can't find anything about it.

You should be able to do this:

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

@moklick
Copy link
Member

moklick commented Sep 14, 2022

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/

@moklick moklick closed this as completed Sep 14, 2022
@Bars92
Copy link

Bars92 commented Sep 29, 2022

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

@seunggs
Copy link

seunggs commented Oct 23, 2022

@Bars92 Are you able to use user-events to simulate a click event on the node? I can't seem to get it to work.

@avtblspd
Copy link

avtblspd commented Jan 31, 2023

@moklick

I'm using storybook and creating storyshot for jest to test using addon-storyshots
I had import it manually inside story-snapshot.test.js
https://reactflow.dev/docs/guides/testing/

global.DOMMatrixReadOnly = DOMMatrixReadOnly;

and RAN "yarn jest --collect-coverage"

// story-snapshots.test.js
import path from "path";
import initStoryshots, { snapshotWithOptions } from "@storybook/addon-storyshots";
import ReactDOM from "react-dom";
import { ResizeObserver } from "./packages/tumbler-react-flows/src/ResizeObserver";
import { DOMMatrixReadOnly } from "./packages/tumbler-react-flows/src/DOMMatrixReadOnly";

const options = {
  createNodeMock: () => {
    return {
      offsetHeight: 150,
      offsetLeft: 0,
      offsetTop: 0,
      offsetWidth: 300,
      addEventListener: () => {},
      removeEventListener: () => {},
      style: {},
      getBoundingClientRect: () => ({
        top: 0,
        bottom: 10,
        right: 100,
        left: 50,
        width: 300
      }),
      scrollIntoView: () => {},
      getElementsByTagName: () => []
    };
  }
};

// multiSnapshotWithOptions is saving snapshots with /packages as root...
const test = ({ stories2snapsConverter, context, story, renderTree }) => {
  let snapshotFileName = stories2snapsConverter.getSnapshotFileName(context);
  if (!path.isAbsolute(snapshotFileName)) {
    snapshotFileName = path.join(path.dirname(__filename), snapshotFileName);
  }
  return snapshotWithOptions(options)({ story, context, renderTree, snapshotFileName });
};

initStoryshots({ test });

beforeAll(() => {
  ReactDOM.createPortal = jest.fn(element => {
    return element;
  });

  window.getComputedStyle = jest.fn(() => {
    return {
      getPropertyValue: () => 200
    };
  });

  window.scroll = jest.fn(() => {});

  // Setup ResizeObserver and offset* properties
  // see: https://github.com/wbkd/react-flow/issues/716

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

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

afterEach(() => {
  ReactDOM.createPortal.mockClear();
  window.getComputedStyle.mockClear();
});

Error: TypeError: P.current.closest is not a function

image

It seems like its breaking here:
https://github.com/wbkd/react-flow/blob/893ee87838ed66ccc7d1db45f68319ec9fe042f2/packages/core/src/container/ZoomPane/index.tsx

Line 105: domNode: zoomPane.current.closest('.react-flow') as HTMLDivElement

My Storyshot

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots FlowEmptyCanvas Flow Empty Canvas 1`] = `
<div
  className="dndflow"
>
  <div
    className="dndflow"
  >
    <div
      className="reactflow-wrapper"
    >
      <div
        className="react-flow"
        data-testid="rf__wrapper"
        onDragOver={[Function]}
        onDrop={[Function]}
        style={
          Object {
            "height": "100%",
            "overflow": "hidden",
            "position": "relative",
            "width": "100%",
            "zIndex": 0,
          }
        }
      >
        <div
          className="react-flow__renderer"
          style={
            Object {
              "height": "100%",
              "left": 0,
              "position": "absolute",
              "top": 0,
              "width": "100%",
            }
          }
        >
          <div
            className="react-flow__pane"
            onClick={[Function]}
            onContextMenu={[Function]}
            onWheel={[Function]}
            style={
              Object {
                "height": "100%",
                "left": 0,
                "position": "absolute",
                "top": 0,
                "width": "100%",
              }
            }
          >
            <div
              className="react-flow__viewport react-flow__container"
              style={
                Object {
                  "transform": "translate(0px,0px) scale(1)",
                }
              }
            >
              <div
                className="react-flow__edgelabel-renderer"
              />
              <div
                className="react-flow__nodes"
                style={
                  Object {
                    "height": "100%",
                    "left": 0,
                    "position": "absolute",
                    "top": 0,
                    "width": "100%",
                  }
                }
              />
            </div>
          </div>
        </div>
        <div
          className="react-flow__panel react-flow__attribution bottom right"
          data-message="Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev"
          style={
            Object {
              "pointerEvents": "all",
            }
          }
        >
          <a
            aria-label="React Flow attribution"
            href="https://reactflow.dev"
            rel="noopener noreferrer"
            target="_blank"
          >
            React Flow
          </a>
        </div>
        <div
          id="react-flow__node-desc-1"
          style={
            Object {
              "display": "none",
            }
          }
        >
          Press enter or space to select a node.
          You can then use the arrow keys to move the node around.
           Press delete to remove it and escape to cancel.
           
        </div>
        <div
          id="react-flow__edge-desc-1"
          style={
            Object {
              "display": "none",
            }
          }
        >
          Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
        </div>
        <div
          aria-atomic="true"
          aria-live="assertive"
          id="react-flow__aria-live-1"
          style={
            Object {
              "border": 0,
              "clip": "rect(0px, 0px, 0px, 0px)",
              "clipPath": "inset(100%)",
              "height": 1,
              "margin": -1,
              "overflow": "hidden",
              "padding": 0,
              "position": "absolute",
              "width": 1,
            }
          }
        />
      </div>
    </div>
  </div>
  <aside
    className="base"
    data-testid="tumbler-aside"
  >
    <div
      className="description"
    >
      You can drag these nodes to the pane on the right.
    </div>
    <div
      className="dndnode input"
      draggable={true}
      onDragStart={[Function]}
    >
      Input Node
    </div>
    <div
      className="dndnode"
      draggable={true}
      onDragStart={[Function]}
    >
      Default Node
    </div>
    <div
      className="dndnode output"
      draggable={true}
      onDragStart={[Function]}
    >
      Output Node
    </div>
  </aside>
</div>
`;

@avtblspd
Copy link

avtblspd commented Feb 1, 2023

@moklick did you got chance to look at the issue ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests