How to set the width of a React component during test?

喜你入骨 提交于 2020-06-10 08:02:46

问题


I'm trying to test a slider component.

This slider component can be variable in width. When you click on the "track" of the slider it should change the value and trigger an onChange callback. The value is a based on where you click on the track. If you click the halfway point when the min value is 100 and the max value is 200, then it should report a value of 150.

The problem I'm running into is that when I render the component using ReactTest.renderIntoDocument the component doesn't have any width, so it can't calculate a new value when you click on it.

Here is the component Slider.js

import React, {PropTypes} from 'react';
import ReactDOM from 'react-dom';
import { noop } from 'lodash';
import style from './style.scss';

export default class Slider extends React.Component {
  render() {
    return (
      <div
        className='Slider'
        onClick={this.handleClick.bind(this)}
        {...this.props}
      >
        <div
          className='handle'
          style={{left: `${this.calculateLeft()}%`}}>
        </div>
        <div className='track'></div>
      </div>
    );
  }

  handleClick(e) {
    let node = ReactDOM.findDOMNode(this);
    let {clientX, clientY} = e;
    let {offsetLeft, offsetWidth, clientWidth} = node;
    let xPercent = (clientX - offsetLeft) / offsetWidth;
    console.log(offsetLeft, offsetWidth, clientWidth, xPercent);
    this.props.onChange(normalize(xPercent, this.props.min, this.props.max));
  }

  calculateLeft() {
    let numerator = this.props.value - this.props.min;
    let denominator = this.props.max - this.props.min;
    return numerator / denominator * 100;
  }
}

// Proptypes
// ----------------------------------------------------------------------------
Slider.propTypes = {
  // Callback for when the value changes.
  onChange: PropTypes.func,
  // The value for when the slider is at 0%
  min: PropTypes.number,
  // The value for when the slider is at 100%
  max: PropTypes.number,
  // The starting value
  value: validateValue,
}

Slider.defaultProps = {
  onChange: noop,
  min: 0,
  max: 100,
}

// Custom Validation
// ----------------------------------------------------------------------------
function validateValue(props, propName, componentName) {
  let value = props[propName];

  if (typeof(value) !== 'number') {
    return new Error(`value must be a number, got ${typeof(value)}`);
  }

  if (value > props.max || value < props.min) {
    return new Error(
      `value: ${value} must be between max: ${props.max}
      and min: ${props.min}`
    );
  }
}

// Helpers
// ---------------------------------------------------------------------------

function normalize(floatValue, min, max) {
  let range = max - min;
  let normalizedValue = floatValue * range + min;
  // cleverly restrict the value be between the min and max
  return [min, normalizedValue, max].sort()[1];
}

Stylesheet (style.scss):

.Slider {
  position: relative;
  display: block;
  width: 100px;

  .track {
    height: 4px;
    background: #666;
    border-radius: 2px;
  }

  .handle {
    width: 12px;
    height: 12px;
    background: #fff;
    border-radius: 10px;
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    transition: left 100ms linear;
  }
}

Here is my test:

import Slider from './Slider';
import React from 'react';
import {
  renderIntoDocument,
  findRenderedDOMComponentWithClass,
  findRenderedDOMComponentWithTag,
  Simulate
} from 'react-addons-test-utils';

describe('Slider', function() {

  describe('click', function() {
    it('triggers the onChange callback', function() {
      const onChange = sinon.spy();
      const component = renderIntoDocument(
        <Slider
          style={{width: 100, height: 40}}
          min={100}
          max={200}
          value={150}
          onChange={onChange}
        />
      );

      const track = findRenderedDOMComponentWithClass(component, 'track');

      Simulate.click(track, {clientY: 0, clientX: 10})
      expect(onChange).to.have.been.calledWith(110);
    });
  });
});

Test output

LOG LOG: 0, 0, 0, Infinity
click
  ✗ triggers the onChange callback
AssertionError: expected onChange to have been called with arguments 10
    onChange(200)

    at /components/Slider/test.js:99 < webpack:///src/components/Slider/test.js:55:6

Those log statements are from the handleClick() function in the component.

The width is zero so the denominator ends up being zero when calculating xPercent, which causes it to be Infinity. This causes it to just use the max value of 200.

TLDR

How do I make the component have width when rendering it during a test?


回答1:


I've been fighting the same problem myself today - I'm building a component that will scale its text size based on the size of the element. Because renderIntoDocument places your component inside a detached DOM node, it isn't possible to calculate offsetWidth, clientWidth, etc.

Are you testing in a browser or node.js? (EDIT: I see you tagged the question PhantomJS so I'm guessing browser!) If you're in a browser you may be able to render the component into the DOM for real:

React.render(<Slider />, document.body);

If you're worried about test isolation, you can create an IFrame to render the component into, and clean that up afterwards:

beforeEach(function() {
    this.iframe = document.createElement('iframe');
    document.body.appendChild(this.iframe);
});

React.render(<Slider />, this.iframe.contentDocument.body);

afterEach(function() {
    document.body.removeChild(this.iframe);
});

Then call this.iframe.contentDocument.body.querySelectorAll('.track') to get the HTML Element and run your assertions against it (This is a plain HTML element, not a React component, so use the standard APIs to query it).




回答2:


Here is an example. React 0.14 warns about rendering document into body. Like what Matt said, we will need to append 'div' in the iframe to prevent such errors.

describe('html tooltip utility class', function() {

let iframe;
let div;

beforeEach(() => {
    iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    div = document.createElement('div');
});


it('returns true if text overflows', () => {
    // jshint ignore:start
    let style = {
        width: 5
    };
    let data = 'hello this is a long text.';
    iframe.contentDocument.body.appendChild(div);
    ReactDOM.render(<div style={style}>{data}</div>, div);

    // jshint ignore:end
    let textNode = div.querySelectorAll('div')[0];

    expect(HTMLTooltip.showTooltip(textNode)).to.be.true;
});

afterEach(() => {
    document.body.removeChild(iframe);
});
});



回答3:


Have you tried to set the dimension for the actual dom node first before testing it? I use Enzyme and what I normally do is create a dummy element node, attached it to the body, then mount the Component to that element. If I need to setup width and height of the element inside the component, i will just update its real DOM node width and height through vanilla javascript. I'm posting my code example below, hope it will help.

Component code that needs to be tested

getMaskComponentContent() {
    const { clientWidth, clientHeight } = this.button;
    const size = clientWidth + clientHeight;
    const lineGap = 15;
    let lines = [];

    for (let i = lineGap; i < size; i += lineGap) {
        lines.push(<line key={i} className='lrn-line1' x1='0' y1={i} x2={i} y2='0'/>);
    }

    return (
        <svg className='lrn-mask' xmlns='http://www.w3.org/2000/svg'>
            <rect x='0' y='0' width='100%' height='100%' fill='transparent'/>
            {lines}
        </svg>
    );
}

Unit-test with enzyme

let wrapper, mountElement;

function setup(props = {}, mountOptions) {
    const providerWrapper = enzyme.mount(<MaskableElement {...props}/>, mountOptions);

    wrapper = providerWrapper.find('MaskableElement');
}

beforeEach(function () {
    // Create dummy element
    mountElement = document.createElement('DIV');
    document.body.appendChild(mountElement);
});

afterEach(function () {
    mountElement.remove();
});

it('the masking svg should contain multiple line elements based on the width and height of the main button', function () {
    // First we setup the component without maskId
    setup({
        maskIds: []
    }, {
        attachTo: mountElement
    });

    const button = wrapper.find('button');
    const node = button.node;

    // then we set size to the component
    node.style.width = '300px';
    node.style.height = '30px';

    // stimulate click event to apply the mask
    button.simulate('click');

    const svg = button.find('svg');

    // 330 is the total clientWidth + clientHeight, 15 is the gap b/w lines
    const expectedNumberOfLines = (330 / 15) - 1; 

    expect(svg.find('line').length).toEqual(expectedNumberOfLines);
});


来源:https://stackoverflow.com/questions/33325893/how-to-set-the-width-of-a-react-component-during-test

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!