Búsqueda de sitios web

reactjs - Usando falsos de sinon en los callbacks de componentes de React al hacer pruebas


Estoy encontrando algo que no puedo explicar. Tengo la siguiente prueba:

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';

describe('ReplicationForm component', () => {
  it('calls saveUrl on submit with new url', () => {
    const saveUrlFake = sinon.fake();
    const otherFake = (url: string) => console.log(url);

    render(<ReplicationForm saveUrl={otherFake} />, { wrapper: TestApp });
    (screen.getByLabelText('Hostname') as HTMLInputElement).value = 'https://example.com';
    (screen.getByLabelText('Database') as HTMLInputElement).value = 'poi';
    (screen.getByLabelText('Username') as HTMLInputElement).value = 'shaw';
    (screen.getByLabelText('Password') as HTMLInputElement).value = 'IMissYouR00t';
    userEvent.click(screen.getByRole('button', { name: 'Submit' }));

    console.log(saveUrlFake.callCount);
    expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:[email /poi');
  });
});

Cuando uso otherFake, la función de devolución de llamada se llama correctamente y la salida deseada se imprime en la consola. Desafortunadamente, si uso saveUrlFake, nunca se llama. El saveUrlFake.callCount es 0 y no hay ningún argumento para verificar.

Extrañamente, cuando yo llamo al saveUrlFake yo mismo con saveUrlFake('https://shaw:[email /poi') la prueba funciona. Mi pregunta es, ¿por qué mi falso de sinon no es llamado por el componente de React?

resumen

El componente completo en cuestión:

import styled from '@emotion/styled';
import { Button, TextField, withTheme } from '@material-ui/core';
import React from 'react';
import { useForm } from 'react-hook-form';
import { FormattedMessage, useIntl } from 'react-intl';

const StyledForm = withTheme(
  styled.form`
    display: flex;
    flex-direction: column;

    & > *:not(div:first-of-type) {
      margin-top: ${({ theme }) => theme.spacing(2)}px;
    }
  `
);

type FormModel = {
  origin: string;
  database: string;
  username: string;
  password: string;
};

type ReplicationFormProps = {
  url?: string;
  saveUrl: (url: string) => void;
};

export default function ReplicationForm({ url, saveUrl }: ReplicationFormProps) {
  const intl = useIntl();
  const getDefaultValuesFromURL = (inputUrl: string): FormModel => {
    const { origin, username, password, pathname } = new URL(inputUrl);
    return {
      origin,
      username,
      password,
      database: pathname.replaceAll('/', '')
    };
  };
  const defaultValues: FormModel = url
    ? getDefaultValuesFromURL(url)
    : {
        origin: '',
        database: '',
        username: '',
        password: ''
      };
  const { handleSubmit, formState, register } = useForm<FormModel>({ defaultValues });

  const onSubmit = (data: FormModel) => {
    const newUrl = new URL(data.origin);
    newUrl.username = data.username;
    newUrl.password = data.password;
    newUrl.pathname = `/${data.database}`;

    saveUrl(newUrl.toString());
  };

  return (
    <StyledForm onSubmit={handleSubmit(onSubmit)}>
      <TextField
        id="replication-form-origin"
        name="origin"
        inputRef={register}
        label={intl.formatMessage({ id: 'label.hostname', defaultMessage: 'Hostname' })}
        fullWidth
      />
      <TextField
        id="replication-form-database"
        name="database"
        inputRef={register}
        label={intl.formatMessage({ id: 'label.database', defaultMessage: 'Database' })}
        fullWidth
      />
      <TextField
        id="replication-form-username"
        name="username"
        inputRef={register}
        label={intl.formatMessage({ id: 'label.username', defaultMessage: 'Username' })}
        fullWidth
      />
      <TextField
        id="replication-form-password"
        name="password"
        type="password"
        inputRef={register}
        label={intl.formatMessage({ id: 'label.password', defaultMessage: 'Password' })}
        fullWidth
      />

      <Button type="submit" color="primary" disabled={formState.isDirty}>
        <FormattedMessage id="action.submit" defaultMessage="Submit" />
      </Button>
    </StyledForm>
  );
}

Respuesta #1

Parece que la función de devolución de llamada se llama de manera asincrónica, por lo que necesita ser esperada. La biblioteca de pruebas ofrece waitFor(), que ayuda en tales situaciones.

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';

describe('ReplicationForm component', () => {
  it('calls saveUrl on submit with new url', async () => {
    const saveUrlFake = sinon.fake();

    render(<ReplicationForm saveUrl={saveUrlFake} />, { wrapper: TestApp });
    userEvent.type(screen.getByLabelText('Hostname *') as HTMLInputElement, 'https://example.com');
    userEvent.type(screen.getByLabelText('Database *'), 'poi');
    userEvent.type(screen.getByLabelText('Username *'), 'shaw');
    userEvent.type(screen.getByLabelText('Password *'), 'IMissYouR00t');
    userEvent.click(screen.getByRole('button', { name: 'Submit' }));

    await waitFor(() => expect(saveUrlFake.called).to.be.true);
    expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:[email /poi');
  });
});