0

My test of a react component looks like this (based off this article):

// MyComponent.test.js


import { mount } from 'enzyme';
import MyComponent from './MyComponent.jsx';

describe('<MyComponent />', () => {
  let props;
  let state;
  let mountedComponent;

  // The problematic part to be changed
  const component = () => {
    if (!mountedComponent) {
      // This enzyme mount is actually much more complex, 
      // as I'm wrapping all sorts of contexts and globals around it
      // which is why I want to take this function outside,
      // and use it as boilerplate in every test
      mountedComponent = mount(<MyComponent {...props} />);
    }
    return mountedComponent;
  };

  beforeEach(() => {
    props = {};
    state = {};
    mountedComponent = undefined;
  });

  it('Works', () => {
    state = { val: true };
    component().setState(state,
      () => expect(component().state('val')).to.equal(true),
    );
  });
});

This works well, the component() function properly returns the same mountedComponent if called multiple times in the same it, as the current value of mountedComponent is preserved between calls, and only resets beforeEach test.

Now if I extract the component() function outside this test to another file:

// getMountedComponent.js

const getMountedComponent = (AnyComponent, props, mountedComponent) => {

  if (!mountedComponent) {
    // Appears not to properly reassign mountedComponent
    mountedComponent = mount(<AnyComponent {...props} />);
  }
  return mountedComponent;
};

And replace the component() function with this:

// MyComponent.test.js

// Cleaner problematic part
const component = () => getMountedComponent(MyComponent, props, mountedComponent);

Then this this test fails, because component() returns a fresh component the second time, with state = null.

It appears to be a scope issue, but I can't wrap my head around this?

Florian Bienefelt
  • 1,448
  • 5
  • 15
  • 28

2 Answers2

1

The problem is that your getMountedComponent function accepts mountedComponent argument - actually it creates new mountedComponent variable inside this function so it overrides variable with the same name defined in describle block. So every time you call getMountedComponent it creates new local variable and as a result you never assign any value to mountedComponent variable defined in describe scope. To fix it you can cache component on function itself (functions are first-class objects in JS) insetad of using external variable:

function getMountedComponent(MyComponent, props) {

  if (!getMountedComponent.mountedComponent) {
    // Appears not to properly reassign mountedComponent
    getMountedComponent.mountedComponent = mount(<MyComponent {...props} />);
  }
  return getMountedComponent.mountedComponent;
};

To clear function cache just use this:

delete getMountedComponent.mountedComponent;
Bartek Fryzowicz
  • 6,464
  • 18
  • 27
  • The reason I'm pulling the function out of the describe function, is because it is actually a bit more complex than what I explained. Which means I want the `getMountedComponent` to live in another file. – Florian Bienefelt May 05 '17 at 17:10
  • Beautiful ! It worked :) I immediately used this to write a function: `getMountedComponent.reset = () => { delete getMountedComponent.mountedComponent; }` – Florian Bienefelt May 05 '17 at 17:36
  • Great :) Yes, adding a method to clear cache is very good idea :) – Bartek Fryzowicz May 05 '17 at 17:41
0

Javascript by reference vs. by value

Javascript is always pass by value, but when a variable refers to an object (including arrays), the "value" is a reference to the object.

When you've taken the getMountedComponent function outside, you are no longer setting the mountedComponent variable in your describe function and therefore leaves it undefined, so it will create a new copy of the mountedComponent everytime.

const getMountedComponent = (MyComponent, props, mountedComponent) => {

  if (!mountedComponent) {
    // You are not changing the "mountedComponent" defined in your "describe" here
    // because "mountedComponent" is pass by value
    mountedComponent = mount(<MyComponent {...props} />);
  }
  return mountedComponent;
};

describe('<MyComponent />', () => {
  let props;
  let state;
  let mountedComponent; // This stays undefined
  // ...
}
Community
  • 1
  • 1
Nelson Yeung
  • 3,262
  • 3
  • 19
  • 29
  • The reason I use `mountedComponent`, is to be able to reset it in `beforeEach`, because I actually have lots of tests running here. Agreed, for a single test it isn't useful. Since mountedComponent ends up referring to a React component, which is an object, I was expecting it to be passed by reference if you call the function again, is that incorrect? – Florian Bienefelt May 05 '17 at 17:13
  • @FlorianBienefelt I see, I'll remove that part of the answer. Initially, your variable is not an object, though, so it'll stay undefined. Even with an object, you can only modify its properties, so you can't just assign it as some new component. – Nelson Yeung May 05 '17 at 17:26
  • @FlorianBienefelt Ignore the "Initially, your variable is not an object, though, so it'll stay undefined.". Even with an object, you are passing the value of the reference to the function. Hence, if you do `mountedComponent = foo`, you are not changing what's inside that reference but just replaced the value of the reference. It's a bit hard to explain...I hope I made a bit of sense. – Nelson Yeung May 05 '17 at 17:34