使用 React Hook 客户端测试 Apollo 查询

Posted

技术标签:

【中文标题】使用 React Hook 客户端测试 Apollo 查询【英文标题】:Testing Apollo Query with React Hook Client 【发布时间】:2020-04-11 15:01:10 【问题描述】:

我正在尝试使用 jest 为这个组件编写测试

import  useState, useRef  from 'react';
import PropTypes from 'prop-types';
import  connect  from 'react-redux';
import  Query  from 'react-apollo';

import  updateYourDetails  from 'universal/domain/health/yourDetails/yourDetailsActions';
import Input from 'universal/components/input/input';
import InputNumber from 'universal/components/input/inputNumber/inputNumber';
import AsyncButton from 'universal/components/asyncButton/asyncButton';
import ErrorMessage from 'universal/components/errorMessage/errorMessage';
import Link from 'universal/components/link/link';
import analytics from 'universal/utils/analytics/analytics';
import  isChatAvailable  from 'universal/logic/chatLogic';
import  validators  from 'universal/utils/validation';
import  localTimezone, getWeekdays  from 'universal/utils/date';
import 
  CALL_ME_BACK_LOADING_MSG,
  CALL_ME_BACK_LABELS_SCHEDULE_TIME,
  CALL_ME_BACK_LABELS_SELECTED_DATE,
  CALL_ME_BACK_ERROR_MSG,
  CALL_ME_BACK_TEST_PARENT_WEEKDAY,
  CALL_ME_BACK_TEST_CHILD_WEEKDAY,
 from 'universal/constants/callMeBack';

import CallCenterAvailibility from './CallCenterAvailibility';
import SelectWrapper from './SelectWrapper';
import SelectOption from './SelectOption';
import styles from './callMeBackLightBox.css';
import  CALL_ME_BACK_QUERY  from './callMeBackQuery';
import postData from './postData';

export const CallMeForm = props => 
  const initSelectedDate = getWeekdays()
    .splice(0, 1)
    .reduce(acc => ( ...acc ));

  const  onSubmissionComplete, className, variant  = props;
  const [hasSuccessfullySubmitted, setHasSuccessfullySubmitted] = useState(false);
  const [apiStatus, setApiStatus] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [cellNumber, setCallNumber] = useState(props.cellNumber || '');
  const [customerFirstName, setCustomerFirstName] = useState(props.customerFirstName || '');
  const [number, setNumber] = useState(props.Number || '');
  const [selectedDate, setSelectedDate] = useState(initSelectedDate || '');
  const [scheduledTime, setScheduledTime] = useState('');

  const weekdays = getWeekdays() || [];
  const timezone = localTimezone || '';
  const requestReceived = apiStatus === 'CALLBACK_ALREADY_EXIST';

  const cellNumberInput = useRef(null);
  const customerFirstNameInput = useRef(null);

  const getQuery = () => (
    <Query query=CALL_ME_BACK_QUERY variables= weekday: selectedDate.weekday >
      ( data, error, loading ) => 
        if (loading)
          return (
            <SelectWrapper disabled labelTitle=CALL_ME_BACK_LABELS_SCHEDULE_TIME name="scheduledTime">
              <SelectOption label=CALL_ME_BACK_LOADING_MSG />
            </SelectWrapper>
          );
        if (error) return <ErrorMessage hasError errorMessage=<p>CALL_ME_BACK_ERROR_MSG</p> />;
        return (
          <CallCenterAvailibility
            selectedDate=selectedDate
            callCenterBusinessHour=data.callCenterBusinessHour
            onChange=val => setScheduledTime(val)
          />
        );
      
    </Query>
  );

  const getPostSubmitMessage = (firstName: string, type: string) => 
    const messages = 
      callCentreClosed: `a`,
      requestReceived: `b`,
      default: `c`,
    ;
    return `Thanks $firstName, $messages[type] || messages.default`;
  ;

  const validate = () => 
    const inputs = [customerFirstNameInput, cellNumberInput];
    const firstInvalidIndex = inputs.map(input => input.current.validate()).indexOf(false);
    const isValid = firstInvalidIndex === -1;

    return isValid;
  ;

  const onSubmitForm = event => 
    event.preventDefault();
    onSubmit();
  ;

  const onSubmit = async () => 
    if (variant === '0' && !validate()) 
      return;
    

    analytics.track(analytics.events.callMeBack.callMeBackSubmit, 
      trackingSource: 'Call Me Form',
    );

    setIsLoading(true);

    const srDescription = '';
    const response = await postData(
      cellNumber,
      customerFirstName,
      number,
      scheduledTime,
      timezone,
      srDescription,
    );
    const  status  = response;

    const updatedSubmissionFlag = status === 'CALLBACK_ALREADY_EXIST' || status === 'CALLBACK_ADDED_SUCCESSFULLY';

    // NOTE: add a slight delay for better UX
    setTimeout(() => 
      setApiStatus(apiStatus);
      setIsLoading(false);
      setHasSuccessfullySubmitted(updatedSubmissionFlag);
    , 400);

    // Update Redux store
    updateYourDetails(
      mobile: cellNumber,
      firstName: customerFirstName,
    );

    if (onSubmissionComplete) 
      onSubmissionComplete();
    
  ;

  if (hasSuccessfullySubmitted) 
    return (
      <p aria-live="polite" role="status">
        getPostSubmitMessage(
          customerFirstName,
          (!requestReceived && !isChatAvailable() && 'callCentreClosed') || (requestReceived && 'requestReceived')
        )
      </p>
    );
  

  return (
    <form onSubmit=onSubmitForm className=className>
      variant !== '1' && (
        <>
          <label htmlFor="customerFirstName" className=styles.inputLabel>
            First name
          </label>
          <Input
            className=styles.input
            initialValue=customerFirstName
            isMandatory
            maxLength=20
            name="customerFirstName"
            onChange=val => setCustomerFirstName(val)
            ref=customerFirstNameInput
            value=customerFirstName
            ...validators.plainCharacters
          />
        </>
      )
      variant !== '1' && (
        <>
          <label htmlFor="cellNumber" className=styles.inputLabel>
            Mobile number
          </label>
          <Input
            className=styles.input
            initialValue=cellNumber
            isMandatory
            maxLength=10
            name="cellNumber"
            onChange=val => setCallNumber(val)
            ref=cellNumberInput
            type="tel"
            value=cellNumber
            ...validators.tel
          />
        </>
      )
      variant !== '1' && (
        <>
          ' '
          <label htmlFor="number" className=styles.inputLabel>
            Qantas Frequent Flyer number (optional)
          </label>
          <InputNumber
            className=styles.input
            disabled=Boolean(props.number)
            initialValue=number
            name="number"
            onChange=val => setNumber(val)
            value=number
          />
        </>
      )
      weekdays && (
        <>
          <SelectWrapper
            testId=`$CALL_ME_BACK_TEST_PARENT_WEEKDAY`
            labelTitle=CALL_ME_BACK_LABELS_SELECTED_DATE
            name="selectedDate"
            onChange=val =>
              setSelectedDate(
                ...weekdays.filter(( value ) => value === val).reduce(acc => ( ...acc )),
              )
            
            tabIndex=0
          >
            weekdays.map(( value, label , i) => (
              <SelectOption
                testId=`$CALL_ME_BACK_TEST_CHILD_WEEKDAY-$i`
                key=value
                label=label
                value=value
              />
            ))
          </SelectWrapper>
          getQuery()
        </>
      )
      <AsyncButton className=styles.submitButton onClick=onSubmit isLoading=isLoading>
        Call me
      </AsyncButton>
      <ErrorMessage
        hasError=(apiStatus >= 400 && apiStatus < 600) || apiStatus === 'Failed to fetch'
        errorMessage=
          <p>
            There was an error submitting your request to call you back. Please try again or call us at' '
            <Link href="tel:134960">13 49 60</Link>.
          </p>
        
      />
    </form>
  );
;

CallMeForm.propTypes = 
  cellNumber: PropTypes.string,
  customerFirstName: PropTypes.string,
  number: PropTypes.string,

  onSubmissionComplete: PropTypes.func,
  className: PropTypes.string,
  variant: PropTypes.string,
;

const mapStateToProps = state => 
  const  frequentFlyer, yourDetails  = state;

  return 
    cellNumber: yourDetails.mobile,
    customerFirstName: yourDetails.firstName,
    number: frequentFlyer.memberNumber,
  ;
;

export default connect(mapStateToProps)(CallMeForm);

我的测试文件如下

    import  render, cleanup  from '@testing-library/react';
import  MockedProvider  from 'react-apollo/test-utils';
import  shallow  from 'enzyme';
import MockDate from 'mockdate';
import  isChatAvailable  from 'universal/logic/chatLogic';


import  CALL_ME_BACK_QUERY  from './callMeBackQuery';
import  CallMeForm  from './CallMeForm';
import postData from './postData';

jest.mock('universal/components/input/input', () => 'Input');
jest.mock('universal/components/asyncButton/asyncButton', () => 'AsyncButton');
jest.mock('universal/components/errorMessage/errorMessage', () => 'ErrorMessage');
jest.mock('universal/logic/chatLogic');
jest.mock('./postData');

describe('CallMeForm', () => 
  let output;

  beforeEach(() => 
    jest.resetModules();
    jest.resetAllMocks();
    const mockQueryData = [
      
        client:,
        request: 
          query: CALL_ME_BACK_QUERY,
          variables:  weekday: '' ,
        ,
        result: 
          data: 
            callCenterBusinessHour: 
              timeStartHour: 9,
              timeStartMinute: 0,
              timeEndHour: 5,
              timeEndMinute: 0,
              closed: false,
            ,
          ,
        ,
      ,
    ];


    const  container  = render(<MockedProvider mocks=mockQueryData addTypename=false><CallMeForm /></MockedProvider>);
    output = container;
  );

  afterEach(cleanup);

  it('renders correctly', () => 
    expect(output).toMatchSnapshot();
  );
);

我不断收到错误:TypeError: this.state.client.stop is not a function

我还删除了 &lt;MockedProvider&gt; 包装器,我得到另一个错误 Invariant Violation: could not find "client" in the context or pass in as a prop。将根组件包装在一个 中,或者将一个 ApolloClient 实例传递给 通过道具。

有谁知道我为什么会收到这个错误以及如何解决这个问题?

【问题讨论】:

【参考方案1】:

我没有解决办法,但我有一些信息。

首先,我在这里遇到了同样的错误,使用@testing-library/react 渲染。

然后我尝试使用 ReactDOM 进行渲染,如下所示:

// inside the it() call with async function
const container = document.createElement("div");
ReactDOM.render(
    < MockedProvider ...props>
        <MyComponent />
    </MockedProvider>,
    container
);

await wait(0);
expect(container).toMatchSnapshot();

并且还尝试使用 Enzyme 进行渲染,像这样:

// inside the it() call, with async function too
const wrapper = mount(
    <MockedProvider ...props>
        <MyComponent />
    </MemoryRouter>
);

await wait(0);
expect(wrapper.html()).toMatchSnapshot();

ReactDOM 和 Enzyme 方法都运行良好。 关于我们得到的错误,我想可能与@testing-library/react有关=/

我没有尝试使用react-test-renderer 进行渲染,也许它也可以。

嗯,这就是我得到的……也许它对你有所帮助。

Ps.:关于waait:https://www.apollographql.com/docs/react/development-testing/testing/#testing-final-state


2020 年 2 月 5 日编辑:

基于https://github.com/apollographql/react-apollo/pull/2165#issuecomment-478865830,我找到了该解决方案(它看起来很丑但有效¯\_(ツ)_/¯):

<MockedProvider ...props>
    <ApolloConsumer>
        client => 
            client.stop = jest.fn();
            return <MyComponent />;
        
    </ApolloConsumer>
</MockedProvider>

【讨论】:

【参考方案2】:

我遇到了同样的问题并且能够解决它。我缺少对等依赖。

您的 package.json 未显示,所以我不确定您的问题是否与我的相同,但我能够通过安装“apollo-client”解决问题。

我正在为我的客户端使用 AWS Appsync,因此没有安装 apollo-client。

【讨论】:

以上是关于使用 React Hook 客户端测试 Apollo 查询的主要内容,如果未能解决你的问题,请参考以下文章

如何测试使用自定义 TypeScript React Hook 的组件?

如何测试使用自定义TypeScript React Hook的组件?

我应该如何测试使用 Typescript 进行 api 调用的 React Hook “useEffect”?

react-hook-form submit 没有从 jest 测试中获取 changeText

react Hook

react native apollo client useQuery invalid hook call error