Тестирование React Проверка формы библиотеки onChange не работает должным образом

Я изучаю библиотеку тестирования React и пытаюсь понять, как проверять сообщения об ошибках с помощью onchange. Я даже упростил ее, поэтому форма отключена до тех пор, пока оба ввода не станут действительными. Он отлично работает во время ручного тестирования, но по какой-то причине внутри библиотеки тестирования React он просто не работает.

Либо мне не хватает чего-то базового, что не задокументировано, потому что это настолько очевидно, либо где-то есть ошибка.

Входные данные не проверяются должным образом ни во время события изменения, ни даже когда я нажимаю кнопку отправки.

Если форма явно недействительна, она сообщает, что она действительна.

У меня есть два входа:

Первый из них является обязательным и имеет минимальную длину 5, Второй вход имеет минимальную и максимальную длину 3, то есть ровно 3,

Вот код, показывающий различные тесты, которые должны легко пройти, но по какой-то странной причине провалиться:

Я пробовал как контролируемые, так и неконтролируемые формы и изо всех сил пытаюсь понять, почему это происходит. Я чувствую, что что-то находится прямо передо мной, но мне этого не хватает.

Я использую Vitest с шуткой и RTL.

Я попытался реализовать гиперформу на основе ответа ниже, но она все равно не работает:

import * as matchers from '@testing-library/jest-dom/vitest';
import {expect, afterAll} from 'vitest';
import { cleanup } from '@testing-library/react';
import Hyperform from 'hyperform';

expect.extend(matchers);

globalThis.HTMLInputElement.prototype.checkValidity = function () {
    return hyperform.checkValidity(this);
};

afterAll(() => {
    cleanup()
})

импортировать { defineConfig } из 'vite' реакция на импорт из @vitejs/plugin-react-swc

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/Tests/Rtl/Setup.js'
  }
})



export default function ErrorMessage(props) {
  return (
    <span data-testid = {props.testId} ref = {props.ref} style = {{color: 'red'}}>{props.text}</span>
  )
}


export default function ControlledForm() {

  console.info('Component form rendered!');

    const [inputOne, setInputOne] = useState({
        value: "",
        isValid: false,
        errorMessage: ""
    });

    const [inputTwo, setInputTwo] = useState({
      value: "",
      isValid: false,
      errorMessage: ""
    });

    const isValid = inputOne.isValid && inputTwo.isValid;

    console.info('Form isValid: ', isValid);

    function handleSubmit(e) {
      e.preventDefault();
      console.info('Form Submitted!', e.target.elements);
    }

    return (
      <div>
        <h3>Controlled Form</h3>
        <p>
          In this component, all state for inputs is in the top component!
        </p>
        <form 
          action = "" 
          method='POST' 
          onSubmit = {e => handleSubmit(e)}
          style = {{
            display: 'flex',
            flexDirection: 'column',
            gap: '1em'
          }}
        >
          <div>
            <input 
              className='practice-inputs'
              type = "text" 
              name = "inputOne" 
              placeholder='Input One:'
              value = {inputOne.value} 
              minLength = {5}
              maxLength = {7}
              required
              onChange = {(e) => {
                //Validate and check input on change here
                  console.info('Input 1 Validity on change: ', e.target.validity);
                  const isValid = e.target.checkValidity();
                  console.info('Is valid: ',isValid);
                  setInputOne({
                    value: e.target.value,
                    isValid: isValid,
                    errorMessage: (!isValid) ? 'Error happenned' : ''
                  })
              }}
            />
            <ErrorMessage testId='cErrorMessage1' text = {inputOne.errorMessage} />
          </div>
          
          <div>
            <input 
              className='practice-inputs'
              type = "text" 
              name = "inputTwo" 
              placeholder='Input Two: '
              value = {inputTwo.value} 
              minLength = {3}
              maxLength = {3}
              required
              onChange = {(e) => {
                //Validate and check input on change here
                  console.info('Input 2 Validity on change: ', e.target.validity);
                  setInputTwo({
                    value: e.target.value,
                    isValid: e.target.checkValidity(),
                    errorMessage: (!e.target.checkValidity()) ? 'Error happenned' : ''
                  })
              }}
            />
            <ErrorMessage testId='cErrorMessage2' text = {inputTwo.errorMessage} />
          </div>
          
          <SubmitButton disabled = {!isValid} text='Submit' />

        </form>
      </div>
    )
}

Тесты:

describe('Controlled Form basic tests', () => {

    let inputOne; let inputTwo; let submitButton; let user;

    beforeEach(() => {
        render(<ControlledForm />)
        inputOne = screen.getByPlaceholderText(/Input One:/);
        inputTwo = screen.getByPlaceholderText(/Input Two:/);
        submitButton = screen.getByText(/submit/i);
    })

    it('Renders', () => {

    })

    it('Should be able to show an input by placeholder text', () => {
        expect(inputOne).toBeInTheDocument()
    })

    /**
     * Note, when looking for something that doesn't exist, I should use queryby
     */
    it('Should not be able to show inputs by an incorrect placeholder', () => {
        expect(screen.queryByPlaceholderText('placeholder that doesnt exist')).not.toBeInTheDocument()
    })

    /**
     * Here I am learning how to interact with inputs,
     * I need to wait for the type to finish, as it can take a bit of time to type the input,
     * Otherwise it would go to the next line without waiting and the input takes a bit of time
     * to be there
     */
    it('Just shows value of the input', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(inputOne).toHaveValue('abc');
    })

    /**
     * ok
     */
    it('Should have the error component in the document', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toBeInTheDocument();
    })


    //Okay
    it('Should have css style ?', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toHaveStyle('color: rgb(255,0,0)');    
    })

    //Okay
    it('Expect submit button to be in the document', async () => {
        expect(submitButton).toBeInTheDocument();
    })

    //Okay
    it('Its submit button should be disabled', () => {
        expect(submitButton).toBeDisabled();
    })

    /**
     * Why is this test failing ??
     */
    it('Expect submit button to be disabled when inputs are not valid', async () => {
        await userEvent.type(inputOne, 'a');
        await userEvent.type(inputTwo, 'a');
        expect(submitButton).toBeDisabled();
    })

    it('Should be valid', async () => {
        await userEvent.type(inputTwo, 'abc');
        expect(inputTwo).toBeValid()
    })

    //This is invalid but for some reason fails, because it's valid ?
    it('Should be valid', async () => {
        await userEvent.type(inputTwo, 'ab');
        expect(inputTwo).toBeInvalid()
    })



/**
     * Fails
     */
    it('Should be invalid', async () => {
        const user = userEvent.setup();
        await user.type(inputOne, 'abc');
        expect(inputOne).toBeInvalid();
    })

    /**
     * Fails
     * Error text does not have value,
     * But It clearly can be seen on browser
     */
    it('Should display error message', async () => {
        const user = userEvent.setup();
        await user.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toHaveValue(/error/i);
    })

Это идеально во время ручного теста:

Я также зарегистрировал вывод в консоли с помощью ручных тестов, и он работает отлично:

Я просмотрел документы здесь для сопоставителей: https://github.com/testing-library/jest-dom?tab=readme-ov-file#tobeinvalid

Там четко указано, возвращает ли проверка достоверность ложного значения, что оно и делает.

Вот мой ручной тест браузера, показывающий, что он явно работает:

Вот еще один неудачный тест, который получает пустые значения:

Здесь еще раз при ручном тесте видно сообщение об ошибке:

Здесь он отлично работает при ручном тестировании:

Я изо всех сил пытаюсь по-настоящему понять, как это работает, Поскольку при изменении конкретный компонент будет перерисовываться, Может быть, именно поэтому он не способен уловить новые ценности?

Я не вижу большого количества документации, объясняющей это, сейчас мне страшно задавать здесь вопросы. Буду рад любым советам.

Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Введение в CSS
Введение в CSS
CSS является неотъемлемой частью трех основных составляющих front-end веб-разработки.
Как выровнять Div по центру?
Как выровнять Div по центру?
Чтобы выровнять элемент <div>по горизонтали и вертикали с помощью CSS, можно использовать комбинацию свойств и значений CSS. Вот несколько методов,...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
Toor - Ангулярный шаблон для бронирования путешествий
Toor - Ангулярный шаблон для бронирования путешествий
Toor - Travel Booking Angular Template один из лучших Travel & Tour booking template in the world. 30+ валидированных HTML5 страниц, которые помогут...
2
0
343
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Нативный HTML5 HTMLInputElement.checkValidity() всегда будет возвращать true, если входное значение установлено JavaScript, а не взаимодействием с пользователем. См. связанный вопрос:

И стандартная спецификация HTML Установка требований к минимальной длине ввода: атрибут minlength:

Проверка ограничения: если элемент имеет минимально допустимую длину значения, его флаг «грязного значения» равен true, его значение было последний раз изменено пользователем (в отличие от изменения, внесенного сценарием), его значение не является пустой строкой и длина значения API элемента меньше минимально допустимой длины значения элемента, то элемент слишком короткий.

Вот почему тестовые примеры, связанные с проверкой ввода, терпят неудачу.

Одним из решений является использование Hyperform, который возьмет на себя проверку ввода:

Он включает в себя полную реализацию API проверки форм HTML5 в JavaScript, заменяет собственные методы браузера (если они вообще реализованы…) и обогащает ваш набор инструментов настраиваемыми событиями и перехватами.

Вы можете использовать пакет Hyperform только в целях тестирования. Вы по-прежнему можете использовать собственные методы проверки HTML5, такие как .checkValidity(), при выпуске веб-приложения.

jest.setup.js:

import hyperform from 'hyperform';

globalThis.HTMLInputElement.prototype.checkValidity = function () {
  return hyperform.checkValidity(this);
};

jest.config.js:

module.exports = {
  testEnvironment: 'jsdom',
  setupFiles: ['<rootDir>/jest.setup.js'],
};

Для демонстрации я упростил ваш код:

index.tsx:

import React, { useState } from 'react';

const ErrorMessage = (props) => (
  <span data-testid = {props.testId} style = {{ color: 'red' }}>
    {props.text}
  </span>
);

export default function ControlledForm() {
  const [inputOne, setInputOne] = useState({ value: '', isValid: false, errorMessage: '' });
  const [inputTwo, setInputTwo] = useState({ value: '', isValid: false, errorMessage: '' });
  const isValid = inputOne.isValid && inputTwo.isValid;

  return (
    <form action = "" method = "POST" style = {{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
      <div>
        <input
          className = "practice-inputs"
          type = "text"
          name = "inputOne"
          placeholder = "Input One:"
          value = {inputOne.value}
          minLength = {5}
          maxLength = {7}
          required
          onChange = {(e) => {
            const isValid = e.target.checkValidity();
            setInputOne({
              value: e.target.value,
              isValid: isValid,
              errorMessage: !isValid ? 'Error happenned' : '',
            });
          }}
        />
        <ErrorMessage testId = "cErrorMessage1" text = {inputOne.errorMessage} />
      </div>

      <div>
        <input
          className = "practice-inputs"
          type = "text"
          name = "inputTwo"
          placeholder = "Input Two: "
          value = {inputTwo.value}
          minLength = {3}
          maxLength = {3}
          required
          onChange = {(e) => {
            setInputTwo({
              value: e.target.value,
              isValid: e.target.checkValidity(),
              errorMessage: !e.target.checkValidity() ? 'Error happenned' : '',
            });
          }}
        />
        <ErrorMessage testId = "cErrorMessage2" text = {inputTwo.errorMessage} />
      </div>

      <button disabled = {!isValid} type = "submit">
        Submit
      </button>
    </form>
  );
}

index.test.tsx:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import React from 'react';
import ControlledForm from '.';

describe('Controlled Form basic tests', () => {
  let inputOne: HTMLInputElement;
  let inputTwo: HTMLInputElement;
  let submitButton: HTMLButtonElement;

  beforeEach(() => {
    render(<ControlledForm />);
    inputOne = screen.getByPlaceholderText(/Input One:/);
    inputTwo = screen.getByPlaceholderText(/Input Two:/);
    submitButton = screen.getByText(/submit/i);
  });

  it('Expect submit button to be disabled when inputs are not valid', async () => {
    await userEvent.type(inputOne, 'a');
    await userEvent.type(inputTwo, 'a');
    expect(submitButton).toBeDisabled();
  });

  it('Should be valid', async () => {
    await userEvent.type(inputTwo, 'abc');
    expect(inputTwo).toBeValid();
  });

  it('Should be valid', async () => {
    await userEvent.type(inputTwo, 'ab');
    expect(inputTwo).toBeInvalid();
  });

  it('Should be invalid', async () => {
    const user = userEvent.setup();
    await user.type(inputOne, 'abc');
    expect(inputOne).toBeInvalid();
  });

  it('Should display error message', async () => {
    const user = userEvent.setup();
    await user.type(inputOne, 'abc');
    expect(screen.getByTestId('cErrorMessage1')).toHaveTextContent('Error happenned');
  });
});

Результат испытаний:

 PASS  stackoverflow/78199219/index.test.tsx
  Controlled Form basic tests
    √ Expect submit button to be disabled when inputs are not valid (204 ms)                                                                                                                                                                                 
    √ Should be valid (108 ms)                                                                                                                                                                                                                               
    √ Should be valid (94 ms)                                                                                                                                                                                                                                
    √ Should be invalid (94 ms)                                                                                                                                                                                                                              
    √ Should display error message (95 ms)                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                             
Test Suites: 1 passed, 1 total                                                                                                                                                                                                                               
Tests:       5 passed, 5 total                                                                                                                                                                                                                               
Snapshots:   0 total
Time:        1.888 s, estimated 2 s
Ran all test suites related to changed files.

Кроме того, вам следует использовать .toHaveTextContent() для проверки element.textContent, а не .toHaveValue()

Спасибо за это, у меня выходной, но проверю это завтра. Я обнаружил, что псевдонимы CSS :valid, :invalid работают нормально. Вообще говоря, если бы я хотел протестировать входные данные, единственным вариантом было бы использовать HyperForm. ? Что делать, если я не хочу использовать эту дополнительную зависимость? Есть ли обходной путь или что-то еще, что я мог бы проверить?

theMyth 27.03.2024 08:30

Мне удалось заставить его работать, спасибо, я принял ответ: есть ли обходной путь без гиперформы? Я использовал его как зависимость от разработки. Но поскольку это настолько распространенный аспект разработки на стороне клиента, я думал, что среды тестирования позаботятся об этом «из коробки».

theMyth 03.04.2024 15:43

Другие вопросы по теме