Repository: mui/material-ui (TypeScript)
Date: 2019-05-25 16:26:02+00:00
Task type: Bug Fix

Issue:
Jest + react-test-renderer snapshot testing fails with ButtonBase
Hi - I just upgraded to v4 and those test failures are really puzzling me.
It seems they all related with react-test-renderer, and most of them is caused by `ButtonBase` I believe. For some reason some tests are passing while similar tests are constantly failing. 

It seems some tests are only failing only when `disabled` is set to true. However I have some other tests that does not involve using `disabled` which might be a different issue elsewhere.

I have the latest version of react, jest, and react-test-renderer. I'm not sure which package is actually causing the issue but since the error message comes from MUI I'm reporting it here.

Error message:

```
    Error: Uncaught [Error: Material-UI: expected an Element but found null. Please check your console for additional warnings and try fixing those. If the error persists please file an issue.]
```

The exact same code works perfectly fine in the browser without reporting any errors or warnings in the console. 


- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔

Tests should pass. 

## Current Behavior 😯

Tests fail but the component works perfectly fine without any warnings in the browser.

## Steps to Reproduce 🕹

Link to test repo: https://github.com/pallymore/mui-v4-test

Codesandbox.io reports a different error - I think it's either their environment or I didn't set this up correctly, but the code (in the test file) is very similar to what I have in the repo above. 

https://codesandbox.io/s/trusting-shaw-1h9mt

Steps: 

1. write snapshot tests with MUI v4's ButtonBase component and set disabled to true.

```
it('renders ButtonBase without crashing', () => {
  const tree = renderer.create(<ButtonBase>ButtonBase</ButtonBase>).toJSON();
  expect(tree).toMatchSnapshot();
});
```

2. run tests, see error message:

```
    Error: Uncaught [Error: Material-UI: expected an Element but found null. Please check your console for additional warnings and try fixing those. If the error persists please file an issue.]
```

3. same component usage is perfectly fine in a browser.

## Context 🔦

Trying to make my tests pass. 

## Your Environment 🌎
| Tech         | Version |
|--------------|---------|
| Material-UI  | v4.0.0  |
| React        |v16.8.6|
| react-test-renderer|v16.8.6|
| react-scripts|v3.0.1|



Issue discussion:
Workaround explained in https://github.com/mui-org/material-ui/issues/15598#issuecomment-490963667. I'll leave this open because I might have an idea how to improve this situation.
@pallymore You can reproduce the same problem with:
```jsx
import React from 'react';
import renderer from 'react-test-renderer';
import { ButtonBase } from '@material-ui/core';

it('1', () => {
  const tree = renderer.create(<ButtonBase>ButtonBase</ButtonBase>);
  expect(tree).toMatchSnapshot();
});

it('2', () => {
  const tree = renderer.create(<ButtonBase>ButtonBase</ButtonBase>);
  expect(tree).toMatchSnapshot();
});
```

I don't think that the `disabled` prop is involved in the problem. 

Solution patch:
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.js b/packages/material-ui/src/ButtonBase/ButtonBase.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -110,20 +110,7 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
   if (disabled && focusVisible) {
     setFocusVisible(false);
   }
-  const getOwnerDocument = React.useCallback(() => {
-    const button = getButtonNode();
-    if (button == null) {
-      throw new Error(
-        [
-          `Material-UI: expected an Element but found ${button}.`,
-          'Please check your console for additional warnings and try fixing those.',
-          'If the error persists please file an issue.',
-        ].join(' '),
-      );
-    }
-    return button.ownerDocument;
-  }, []);
-  const { isFocusVisible, onBlurVisible } = useIsFocusVisible(getOwnerDocument);
+  const { isFocusVisible, onBlurVisible, ref: focusVisibleRef } = useIsFocusVisible();
 
   React.useImperativeHandle(
     action,
@@ -281,7 +268,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
   }
 
   const handleUserRef = useForkRef(buttonRefProp, ref);
-  const handleRef = useForkRef(handleUserRef, buttonRef);
+  const handleOwnRef = useForkRef(focusVisibleRef, buttonRef);
+  const handleRef = useForkRef(handleUserRef, handleOwnRef);
 
   return (
     <ComponentProp
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -113,12 +113,6 @@ function Tooltip(props) {
   const enterTimer = React.useRef();
   const leaveTimer = React.useRef();
   const touchTimer = React.useRef();
-  // can be removed once we drop support for non ref forwarding class components
-  const handleOwnRef = React.useCallback(instance => {
-    // #StrictMode ready
-    setChildNode(ReactDOM.findDOMNode(instance));
-  }, []);
-  const handleRef = useForkRef(children.ref, handleOwnRef);
 
   React.useEffect(() => {
     warning(
@@ -205,13 +199,7 @@ function Tooltip(props) {
     }
   };
 
-  const getOwnerDocument = React.useCallback(() => {
-    if (childNode == null) {
-      return null;
-    }
-    return childNode.ownerDocument;
-  }, [childNode]);
-  const { isFocusVisible, onBlurVisible } = useIsFocusVisible(getOwnerDocument);
+  const { isFocusVisible, onBlurVisible, ref: focusVisibleRef } = useIsFocusVisible();
   const [childIsFocusVisible, setChildIsFocusVisible] = React.useState(false);
   function handleBlur() {
     if (childIsFocusVisible) {
@@ -310,6 +298,16 @@ function Tooltip(props) {
     }, leaveTouchDelay);
   };
 
+  // can be removed once we drop support for non ref forwarding class components
+  const handleOwnRef = useForkRef(
+    React.useCallback(instance => {
+      // #StrictMode ready
+      setChildNode(ReactDOM.findDOMNode(instance));
+    }, []),
+    focusVisibleRef,
+  );
+  const handleRef = useForkRef(children.ref, handleOwnRef);
+
   let open = isControlled ? openProp : openState;
 
   // There is no point in displaying an empty tooltip.
diff --git a/packages/material-ui/src/utils/focusVisible.js b/packages/material-ui/src/utils/focusVisible.js
--- a/packages/material-ui/src/utils/focusVisible.js
+++ b/packages/material-ui/src/utils/focusVisible.js
@@ -1,5 +1,6 @@
 // based on https://github.com/WICG/focus-visible/blob/v4.1.5/src/focus-visible.js
 import React from 'react';
+import ReactDOM from 'react-dom';
 
 let hadKeyboardEvent = true;
 let hadFocusVisibleRecently = false;
@@ -122,13 +123,13 @@ export function handleBlurVisible() {
   }, 100);
 }
 
-export function useIsFocusVisible(getOwnerDocument) {
-  React.useEffect(() => {
-    const ownerDocument = getOwnerDocument();
-    if (ownerDocument != null) {
-      prepare(ownerDocument);
+export function useIsFocusVisible() {
+  const ref = React.useCallback(instance => {
+    const node = ReactDOM.findDOMNode(instance);
+    if (node != null) {
+      prepare(node.ownerDocument);
     }
-  }, [getOwnerDocument]);
+  }, []);
 
-  return { isFocusVisible, onBlurVisible: handleBlurVisible };
+  return { isFocusVisible, onBlurVisible: handleBlurVisible, ref };
 }


Test patch:
diff --git a/packages/material-ui/src/ButtonBase/ButtonBase.test.js b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
--- a/packages/material-ui/src/ButtonBase/ButtonBase.test.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.test.js
@@ -637,7 +637,7 @@ describe('<ButtonBase />', () => {
       PropTypes.resetWarningCache();
     });
 
-    it('throws with additional warnings on invalid `component` prop', () => {
+    it('warns on invalid `component` prop', () => {
       // Only run the test on node. On the browser the thrown error is not caught
       if (!/jsdom/.test(window.navigator.userAgent)) {
         return;
@@ -648,19 +648,12 @@ describe('<ButtonBase />', () => {
       }
 
       // cant match the error message here because flakiness with mocha watchmode
-      assert.throws(() => mount(<ButtonBase component={Component} />));
+      mount(<ButtonBase component={Component} />);
 
       assert.include(
         consoleErrorMock.args()[0][0],
         'Invalid prop `component` supplied to `ForwardRef(ButtonBase)`. Expected an element type that can hold a ref',
       );
-      // first mount includes React warning that isn't logged on subsequent calls
-      // in watchmode because it's cached
-      const customErrorIndex = consoleErrorMock.callCount() === 3 ? 1 : 2;
-      assert.include(
-        consoleErrorMock.args()[customErrorIndex][0],
-        'Error: Material-UI: expected an Element but found null. Please check your console for additional warnings and try fixing those.',
-      );
     });
   });
 });
diff --git a/packages/material-ui/src/utils/focusVisible.test.js b/packages/material-ui/src/utils/focusVisible.test.js
--- a/packages/material-ui/src/utils/focusVisible.test.js
+++ b/packages/material-ui/src/utils/focusVisible.test.js
@@ -17,17 +17,9 @@ function simulatePointerDevice() {
 }
 
 const SimpleButton = React.forwardRef(function SimpleButton(props, ref) {
-  const [element, setElement] = React.useState(null);
-  const getOwnerDocument = React.useCallback(() => {
-    if (element === null) {
-      return null;
-    }
-
-    return element.ownerDocument;
-  }, [element]);
-  const { isFocusVisible, onBlurVisible } = useIsFocusVisible(getOwnerDocument);
+  const { isFocusVisible, onBlurVisible, ref: focusVisibleRef } = useIsFocusVisible();
 
-  const handleRef = useForkRef(setElement, ref);
+  const handleRef = useForkRef(focusVisibleRef, ref);
 
   const [focusVisible, setFocusVisible] = React.useState(false);