使用 Jest 和 Enzyme @react-google-maps/api 进行 React 测试返回 TypeError:无法读取未定义的属性“maps”

xyz*_*242 3 reactjs jestjs enzyme

我正在尝试使用@react-google-maps/api包测试组件。我收到错误:TypeError: Cannot read property 'maps' of undefined

我的组件:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPersonId, updatePersonSettings, pushPersonMessage } from '../../actions/person';
import TemplatePage from '../templates/TemplatePage';
import Card from '../partials/Card';
import Msg from '../partials/Msg';
import { GoogleMap, Marker } from '@react-google-maps/api';
import markerPosition from'../../img/marker-position.png';
import PlacesAutocomplete, { geocodeByAddress, getLatLng } from 'react-places-autocomplete';
import PropTypes from 'prop-types';
import { DEFAULT_LAT, DEFAULT_LNG, DEFAULT_ZOOM } from '../../config/';


export class Settings extends Component {

  state = {
    lat: DEFAULT_LAT,
    lng: DEFAULT_LNG,
    zoom: DEFAULT_ZOOM,
    address: '',
    formSubmitted: false
  }

  componentDidMount () {
    const { lat, lng, zoom } = this.props.auth;
    this.setState({
      lat: lat !== undefined && lat !== null ? lat : DEFAULT_LAT,
      lng: lng !== undefined && lng !== null ? lng : DEFAULT_LNG,
      zoom: zoom !== undefined && zoom !== null ? zoom : DEFAULT_ZOOM
    });
    this.drawMarker();
  }

  handleOnSubmit = e => {
    e.preventDefault();
    const settings = {
      zoom: this.state.zoom,
      lat: this.state.lat,
      lng: this.state.lng
    }
    this.props.updatePersonSettings({ id: this.props.auth.person_id, settings })
  }

  handleChangeZoom = event => {
    this.setState({ zoom: parseInt(event.target.value )});
  }

  handleChangeAddress = (address) => {
    this.setState({ address });
  }

  handleSelect = (address) => {
    geocodeByAddress(address)
      .then(results => 
          getLatLng(results[0])
          .then(function(result) {
            this.setState({
              lat: result.lat,
              lng: result.lng,
            })
            this.drawMarker()
        }.bind(this))
      )
      .catch(error => console.error('Error', error));
  };

  handleMapClick = e => {
    this.setState({
      lat: e.latLng.lat(),
      lng: e.latLng.lng(),
    });
    this.drawMarker();
  }

  handleMapZoom = (zoom) => {
    console.log(zoom)
  }

  drawMarker = () => {
      return <Marker
      position={{
        lat: parseFloat(this.state.lat),
        lng: parseFloat(this.state.lng)
      }}
      icon={
        new window.google.maps.MarkerImage(
          markerPosition,
          null, /* size is determined at runtime */
          null, /* origin is 0,0 */
          null, /* anchor is bottom center of the scaled image */
          new window.google.maps.Size(48, 48)
        )
      }
    >
    </Marker>
  }

  get msg() {
    if(this.props.person !== '') {
      return <Msg msg={this.props.person} />
    }
    return null;
  }

  render() {

    const { status } = this.props.person;
    const { lat, lng, zoom, address, formSubmitted } = this.state;

    return (
      <TemplatePage>

        { this.msg }
        <Card title='Settings' padding='large'>

          <form className="form" onSubmit={this.handleOnSubmit}>
            <div className="form-group">
              <label htmlFor="position">Default map position</label>
              <div className="google-map google-map__settings">

                  <GoogleMap
                    center={{ lat, lng }}
                    zoom={ zoom }
                    onClick={ e => this.handleMapClick(e) }
                    onZoomChanged={(e) => {
                      console.log('zoom changed')
                    }}
                  >
                    {this.drawMarker()}
                    <div className="map-constraints-container" />
                  </GoogleMap>
              </div>
            </div>
            <div className="form-group">
              <div className="map-constraints-slider"> 
                <label htmlFor="range">Default map zoom: {zoom}</label>
                <input 
                  type="range" 
                  id="zoom" 
                  value={ zoom } 
                  name="zoom" 
                  min="1" 
                  max="18" 
                  onChange={ this.handleChangeZoom }
                />
              </div>
            </div>

            <div className="form-group">
              <PlacesAutocomplete
                value={address}
                onChange={ this.handleChangeAddress }
                onSelect={ this.handleSelect }
              >
            {({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
              <div>
                <input
                  {...getInputProps({
                    placeholder: 'Search places...',
                    className: 'location-search-input',
                  })}
                />
                <div className="autocomplete-dropdown-container">
                  {loading && <div>Loading...</div>}
                  {suggestions.map(suggestion => {
                    const className = suggestion.active
                      ? 'suggestion-item--active'
                      : 'suggestion-item';
                    // inline style for demonstration purpose
                    const style = suggestion.active
                      ? { backgroundColor: '#fafafa', cursor: 'pointer' }
                      : { backgroundColor: '#ffffff', cursor: 'pointer' };
                    return (
                      <div
                        {...getSuggestionItemProps(suggestion, {
                          className,
                          style,
                        })}
                      >
                        <span>{suggestion.description}</span>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}
            </PlacesAutocomplete>
            </div>

            <div className="form-group">
              <input 
                type="submit" 
                value="Update settings" 
                className="btn btn--primary card__footer--btn-left"
                disabled={ formSubmitted && status === 'fetching' ? 'disabled' : null }
              />
            </div>
            </form>
        </Card>
      </TemplatePage>
    )
  }

}


Settings.defaultProps = {
  auth: {},
  person: {},
  fetchPersonId: () => Promise.resolve(),
  updatePersonsettings: () => Promise.resolve(),
  pushPersonMessage: () => Promise.resolve()
}


Settings.propTypes = {
  auth: PropTypes.object,
  person: PropTypes.object,
  fetchPersonId: PropTypes.func,
  updatePersonsettings: PropTypes.func,
  pushPersonMessage: PropTypes.func
};

export default connect(
  ({ auth, person }) => ({ auth, person }),
  { fetchPersonId, updatePersonSettings, pushPersonMessage }
)(Settings);

Run Code Online (Sandbox Code Playgroud)

我的测试:

import React from 'react';
import { shallow } from 'enzyme';
import { Settings } from '../../../components/pages/Settings';

test('should render settings page', () => {
  const wrapper = shallow(<Settings />);
  expect(wrapper).toMatchSnapshot();
});
Run Code Online (Sandbox Code Playgroud)

我读到,为了解决此类问题,最好模拟该包。在其他一些组件中,我使用 `` 包,我设法像这样模拟:

const zxcvbn = require.requireActual('zxcvbn');

export default (password = 'test') => {
  return zxcvbn(password);
}
Run Code Online (Sandbox Code Playgroud)

我将如何模拟@react-google-maps/api包并消除错误?这是一个好方法(模拟包)吗?或者可以通过其他方式解决这个问题吗?我将如何测试地图或标记是否已渲染?

小智 7

我知道这是一个老问题。但我管理这个的方法是通过模拟 google 对象。这只是模拟我所调用的内容,因此您需要添加您调用的任何谷歌常量和方法。

至于测试它是否被渲染,这一切都隐藏在谷歌地图 javascript api blob 中。您可以添加间谍以确保调用适当的函数。我认为不可能验证谷歌地图实际上正在渲染任何东西。

import ReactDOM from 'react-dom';

//ReactDOM.createPortal = jest.fn(node => node);

jest.mock('@react-google-maps/api', () => {
    const React = require('React');
    return {
        withGoogleMap: (Component) => Component,
        withScriptjs: (Component) => Component,
        Polyline: (props) => <div />,
        Marker: (props) => <div />,
        GoogleMap: (props) => (<div><div className="mock-google-maps" />{props.children}</div>),
    };
});

global.google = {
    maps: {
        LatLngBounds: () => ({
            extend: () => { },
        }),
        MapTypeId: {
            ROADMAP: 'rdmap',
            SATELLITE: 'stllte'
        },
        ControlPosition: {
            BOTTOM_CENTER: 'BC',
            BOTTOM_LEFT: 'BL',
            BOTTOM_RIGHT: 'BR',
            LEFT_BOTTOM: 'LB',
            LEFT_CENTER: 'LC',
            LEFT_TOP: 'LT',
            RIGHT_BOTTOM: 'RB',
            RIGHT_CENTER: 'RC',
            RIGHT_TOP: 'RT',
            TOP_CENTER: 'TC',
            TOP_LEFT: 'TL',
            TOP_RIGHT: 'TR',            
        },
        Size: function (w, h) {},
        Data: class {
            setStyle() {}
            addListener() {}
            setMap() {}
        }
    }
};
Run Code Online (Sandbox Code Playgroud)