Ошибка изменения состояния элемента

Меня зовут Дмитрий Макаренко, я Mobile QA Engineer в Badoo и Bumble: занимаюсь тестированием новой функциональности в наших приложениях вручную и покрытием её автотестами. 

За последние два года подход к автоматизации тестирования в нашей компании сильно изменился. Количество людей, активно вовлечённых в разработку тестов, увеличилось с десяти до 40 человек. А любая новая функциональность в приложениях теперь обязательно должна быть покрыта тестами до релиза. 

В таких условиях нам очень важно разрабатывать тесты настолько быстро, насколько это возможно. И делать их при этом стабильными — чтобы поддержка не отнимала у нас много времени. Мы решили поделиться практиками, которые помогают нам ускорять разработку тестов и повышать их стабильность. 

В подготовке текста мне помогал мой коллега Виктор Короневич: с этой темой мы вместе выступали на конференции Heisenbug. 

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

Напомню, что примеры из обеих частей в равной степени актуальны для тех, кто только начинает внедрять автоматизацию тестирования в своём проекте, и тех, кто уже активно ею занимается.

Спойлер

В конце статьи будет ссылка на тестовый проект со всеми практиками.

Практика 4. Верификация изменения состояния элементов

Пожалуй, эта практика одна из самых важных в мобильной автоматизации в принципе, потому что в приложениях очень редко встречаются статические элементы, которые сразу же отображаются на экране и всегда на нём доступны. Чаще мы сталкиваемся с тем, что загрузка элементов занимает какое-то время. 

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

Таким образом, прежде чем начать проверять элементы, нам необходимо дождаться их появления на экране. Естественно, эта проблема не нова и существуют стандартные решения. Например, в Selenium это различные типы методов wait, а в Calabash — метод wait_for. 

Зачем нам свой велосипед, или Почему мы отказались от стандартного решения

Когда мы начинали строить наш фреймворк автоматизации, мы использовали стандартный метод wait_for для ожидания появления или изменения состояния элементов. Но в какой-то момент столкнулись с проблемой. Иногда, раз в две-три недели, у нас зависали все тесты. Мы не могли понять, как и почему это происходит и что мы делаем не так.

После того как мы добавили логи и проанализировали их, оказалось, что эти зависания были связаны с реализацией метода wait_for, входящего в состав фреймворка Calabash. wait_for использует метод timeout модуля Ruby Timeout, который реализован на глобальном потоке. А тесты зависали, когда этот метод timeout использовался вложено в других методах: наших и фреймворка Calabash. 

Например, рассмотрим прокрутку страницы профиля до кнопки блокировки пользователя. 

def scroll_to_block_button
  wait_for(timeout: 30) do
    ui.scroll_down
    ui.wait_until_no_animation
    ui.element_displayed?(BLOCK_BUTTON)
  end
end

Мы видим, что используется метод wait_for. Происходит прокрутка экрана вниз, потом ожидание окончания анимации и проверка отображения кнопки блокировки. 

Рассмотрим реализацию метода wait_until_no_animation.

def wait_until_no_animation
  wait_for(timeout: 10) do
    !ui.any_element_animating?
  end
end

Метод wait_until_no_animation реализован так же с wait_for. Он ждёт, когда на экране закончится анимация. Получается, что wait_for, вызванный внутри wait_for, вызывает другие методы. Представьте себе, что вызовы wait_for также есть внутри методов Calabash. С увеличением цепочки wait_for внутри wait_for внутри wait_for риск зависания увеличивается. Поэтому мы решили отказаться от использования этого метода и придумать своё решение.

Требования к нему совпадали с требованиями к стандартным решениям. Нам нужен был метод, который бы повторял проверку до тех пор, пока не выполнится заданное условие либо пока не истечёт отведённое время. Если проверка не проходит успешно за отведённое время, наш метод должен выбрасывать ошибку.

Сначала мы создали модуль Poll с одним методом for, который повторял стандартный метод wait_for. Со временем собственная реализация позволила нам расширять функциональность модуля по мере того, как у нас появлялась такая необходимость. Мы добавили методы, ожидающие конкретные значения заданных условий. Например, Poll.for_true и Poll.for_false явно ожидают, что исполняемый код вернёт true либо false. В примерах ниже я покажу использование разных методов из модуля Poll.

Также мы добавили разные параметры методов. Рассмотрим подробнее параметр return_on_timeout. Его суть в том, что при использовании этого параметра наш метод Poll.for перестаёт выбрасывать ошибку, даже если заданное условие не выполняется, а просто возвращает результат выполнения проверки. 

Предвижу вопросы «Как это работает?» и «Зачем это нужно?». Начнём с первого. Если в методе Poll.for мы будем ждать, пока 2 станет больше, чем 3, то мы всегда будем получать ошибку по тайм-ауту. 

Poll.for { 2 > 3 }
> WaitError

Но если мы добавим наш параметр return_on_timeout и всё так же будем ждать, пока 2 станет больше, чем 3, то после окончания тайм-аута, 2 всё ещё не станет больше, чем 3, но наш тест не упадёт, а метод Poll.for вернёт результат этой проверки. 

Poll.for(return_on_timeout: true) { 2 > 3 }
> false

Зачем это нужно? Мы используем параметр return_on_timeout для верификации изменения состояния наших элементов. Но очень важно делать это аккуратно, так как он может скрывать реальные падения тестов. При некорректном использовании тесты будут продолжать выполняться, когда заданные условия не выполняются, в тех местах, где должны были бы выбросить ошибку. 

Варианты изменения состояния элементов

А теперь перейдём к самому интересному — поговорим о том, как проверять различные изменения состояния и какие изменения состояния вообще существуют. Познакомьтесь с нашим объектом тестирования — чёрным квадратом: 

Он умеет всего две вещи: появляться на экране и пропадать с экрана. 

Первый вариант изменения состояния называется «Должен появиться». Он происходит в том случае, когда состояние 1 – на экране нет нашего объекта тестирования, а состояние 2 – он должен появиться. 

Должен появиться

Должен появиться

Если он появляется, то проверка проходит успешно. 

Второй вариант изменения состояния называется «Должен пропасть». Происходит он тогда, когда в состоянии 1 отображается наш объект тестирования, а в состоянии 2 его быть не должно. 

Должен пропасть

Должен пропасть

Третий вариант не такой очевидный, как первые два, потому что в нём, по сути, мы проверяем неизменность состояния. Называется он «Не должен появиться». Это происходит, когда в состоянии 1 наш объект тестирования не отображается на экране и спустя какое-то время в состоянии 2 он всё ещё не должен появиться. 

Не должен появиться

Не должен появиться

Вы, наверное, догадались, какой вариант — четвёртый. Он называется «Не должен пропасть». Происходит это, когда в состоянии 1 объект отображается на экране, и спустя какое-то время в состоянии 2 он всё ещё находится там.

Не должен пропасть

Не должен пропасть

Реализация проверок разных вариантов

Мы зафиксировали все возможные варианты изменения состояния элементов. Как же их проверить? Разобьём реализацию на проверки первых двух вариантов и проверки третьего и четвёртого. 

В случае с первыми двумя вариантами всё довольно просто. Для проверки первого нам просто нужно подождать, пока элемент появится, используя наш метод Poll: 

# вариант "Должен появиться"
Poll.for_true { ui.elements_displayed?(locator) }

Для проверки второго — подождать, пока элемент пропадёт: 

# вариант "Должен пропасть"
Poll.for_false { ui.elements_displayed?(locator) }

Но в случае с третьим и четвёртым вариантами всё не так просто.

Рассмотрим вариант «Не должен появиться»: 

# вариант "Не должен появиться"
ui.wait_for_elements_not_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not appear")

Здесь мы, во-первых, фиксируем состояние отсутствия элемента на экране. 

Далее, используя Poll.for с параметром return_on_timeout, мы ждём появления элемента. При этом метод Poll.for не выбросит ошибку, а вернёт false, если элемент не появится. Значение, полученное из Poll.for, сохраняется в переменной actual_state.

После этого происходит проверка неизменности состояния элемента с использованием метода assert.

Для проверки варианта «Не должен пропасть» мы используем похожую логику, ожидая пропажи элемента с экрана вместо его появления: 

# вариант "Не должен пропасть"
ui.wait_for_elements_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { !ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not disappear")

Проверки этих четырёх вариантов изменения состояния актуальны для многих элементов мобильных приложений. А поскольку разработкой тестов у нас занимается много людей, всегда есть вероятность того, что кто-то забудет про некоторые варианты при создании новых проверок. Поэтому мы вынесли реализацию проверок всех вариантов изменения состояния в один метод:

def verify_dynamic_state(state:, timeout: 10, error_message:)
  options = {
    return_on_timeout: true,
    timeout:           timeout,
  }
  case state
    when 'should appear'
      actual_state = Poll.for(options) { yield }
      Assertions.assert_true(actual_state, error_message)
    when 'should disappear'
      actual_state = Poll.for(options) { !yield }
      Assertions.assert_true(actual_state, error_message)
    when 'should not appear'
      actual_state = Poll.for(options) { yield }
      Assertions.assert_false(actual_state, error_message)
    when 'should not disappear'
      actual_state = Poll.for(options) { !yield }
      Assertions.assert_false(actual_state, error_message)
    else
      raise("Undefined state: #{state}")
  end
end

yield – это код блока, переданного в данный метод. На примерах выше это был метод elements_displayed?. Но это может быть любой другой метод, результат выполнения которого отражает состояние необходимого нам элемента. Документация Ruby.

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

Выводы: 

  • важно не забывать про все четыре варианта изменения состояния при проверках UI-элементов;

  • полезно вынести эти проверки в общий метод. 

Мы рекомендуем использовать полную систему проверок всех вариантов изменения состояния. Что мы имеем в виду? Представьте, что когда элемент есть — это состояние true, а когда его нет — false. 

Состояние 1

Состояние 2

Должен появиться

FALSE

TRUE

Должен пропасть

TRUE

FALSE

Не должен появиться

FALSE

FALSE

Не должен пропасть

TRUE

TRUE

Мы строим матрицу всех комбинаций. При появлении нового состояния таблицу можно расширить и получить новые комбинации. 

Практика 5. Надёжная настройка предусловий тестов

Как можно догадаться из заголовка, задача этого раздела — разобраться, как настраивать предусловия перед началом выполнения теста.

Рассмотрим два примера. Первый – отключение сервиса локации на iOS в настройках. Второй – создание истории чата. 

В первом примере реализация метода отключения сервиса локации на iOS выглядит следующим образом: 

def switch_off_location_service
  ui.wait_for_elements_displayed(SWITCH)
  if ui.element_value(SWITCH) == ON
    ui.tap_element(SWITCH)
    ui.tap_element(TURN_OFF)
  end
end

Мы ждём, пока переключатель (элемент switch) появится на экране. Потом проверяем его состояние. Если оно не соответствует ожидаемому, мы его изменяем. 

После этого мы закрываем настройки и запускаем приложение. И иногда внезапно сталкиваемся с проблемой: почему-то сервис локации остаётся включённым. Как это получается? Мы же сделали всё, чтобы его отключить. Кажется, что это проблема работы системных настроек в iOS системе. При быстром выходе из настроек (а тест делает это моментально после нажатия на переключатель) их новое состояние не сохраняется. Но проблемы могут возникнуть и при настройке предусловий в нашем приложении.

Давайте обратимся ко второму примеру — созданию истории чата перед началом выполнения теста. Реализация метода выглядит следующим образом: 

def send_message(from:, to:, message:, count:)
  count.times do
    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
  end
end

Мы используем QAAPI для отправки сообщений по user_id. В цикле мы отправляем необходимое количество сообщений. 

После этого мы заходим в чат и производим необходимые проверки, связанные с загрузкой истории сообщений. Но иногда в чате отображаются не все сообщения. Иногда мы продолжаем получать их, уже находясь в чате, а не до его открытия. Связано это с тем, что сервер не может отправлять нужное количество сообщений моментально. В некоторых случаях доставка задерживается. 

Выделим общую проблему обоих примеров. Когда установка предусловий выполняется некорректно, тесты падают на последующих проверках, несмотря на то, что сами приложения работают как надо. Мы начинаем разбираться в причинах, а потом понимаем, что падение вызвано неправильной подготовкой предусловий. Такие ситуации расстраивают не только тестировщиков, но и разработчиков, которые анализируют прогоны тестов при разработке новой функциональности.

Как же решить эту проблему? Мы можем добавить гарантию выполнения действия в методы установки предусловий. 

Тогда наш метод отключения сервиса локации будет выглядеть следующим образом:

def ensure_location_services_switch_in_state_off
  ui.wait_for_elements_displayed(SWITCH)
  if ui.element_value(SWITCH) == ON
    ui.tap_element(SWITCH)
    ui.tap_element(TURN_OFF)
    Poll.for(timeout_message: 'Location Services should be disabled') do
      ui.element_value(SWITCH) == OFF
    end
  end
end

Используя метод Poll.for, мы убеждаемся, что состояние переключателя изменилось, прежде чем переходить к следующим действиям теста. Это позволяет избежать проблем, вызванных тем, что сервис локации время от времени был включён. 

Во втором примере нам снова помогут наши методы QAAPI. 

def send_message(from:, to:, message:, count:)
  actual_messages_count = QaApi.received_messages_count(to, from)
  expected_messages_count = actual_messages_count + count
  count.times do
    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
  end
  QaApi.wait_for_user_received_messages(from, to, expected_messages_count)
end

Перед отправкой сообщений мы получаем текущее количество сообщений в чате, а после — убеждаемся, что необходимое количество сообщений было отправлено. Только после этой проверки тест продолжает своё выполнение. Таким образом, когда мы открываем чат в приложении, мы видим все необходимые сообщения и можем выполнять нужные проверки.

Итак, мы рекомендуем всегда использовать гарантию выполнения действий в процессе установки предусловий в ваших тестах. Это позволит сэкономить время на исследовании падений в тех случаях, когда не срабатывает установка предусловий, потому что тест упадёт сразу же на этапе подготовки. 

В качестве общей рекомендации советуем добавлять в ваши тесты проверку выставления любых предустановок, когда выполняются асинхронные действия. 

Более подробно о проблемах, описанных в этом разделе, можно прочитать в статье Мартина Фаулера

Практика 6. Простые и сложные действия, или Независимость шагов в тестах

Простые действия

По выводам из предыдущего раздела может показаться, что нам стоит добавлять в тесты проверки выполнения всех действий. Но на деле это не так. В этом разделе мы поговорим о реализации шагов для разных действий и верификаций. Это очень общая формулировка, которая подходит практически для любого шага в наших тестах. Поэтому мы, как обычно, будем рассматривать конкретные примеры.

Начнём с теста поиска и отправки GIF-сообщений. 

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

When  primary_user opens Chat with chat_user

Потом открыть поле ввода GIF-сообщений: 

And   primary_user switches to GIF input source

Далее нам нужно ввести поисковый запрос для GIF-изображений, убедиться, что список обновился, отправить понравившееся изображение из списка и убедиться, что оно было отправлено.

And   primary_user searches for "bee" GIFs
And   primary_user sends 7th GIF in the list
Then  primary_user verifies that the selected GIF has been sent

Целиком сценарий выглядит так:

Scenario: Searching and sending GIF in Chat
  Given users with following parameters
    | role         | name |
    | primary_user | Dima |
    | chat_user    | Lera |
  And   primary_user logs in
  When  primary_user opens Chat with chat_user
  And   primary_user switches to GIF input source
  And   primary_user searches for "bee" GIFs
  And   primary_user sends 7th GIF in the list
  Then  primary_user verifies that the selected GIF has been sent

Обратим внимание на шаг, который отвечает за поиск гифки: 

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
  chat_page = Pages::ChatPage.new.await
  TestData.gif_list = chat_page.gif_list
  chat_page.search_for_gifs(keyword)
  Poll.for_true(timeout_message: 'Gif list is not updated') do
    (TestData.gif_list & chat_page.gif_list).empty?
  end
end

Здесь, как и почти во всех остальных шагах, мы делаем следующее:

  1. сначала ожидаем открытия нужной страницы (ChatPage); 

  2. потом сохраняем список всех доступных GIF-изображений; 

  3. далее вводим ключевое слово для поиска; 

  4. затем ждём изменения состояния — обновления списка (ведь мы говорили о том, что полезно добавлять в тесты проверку выполнения действий).

Кажется, что всё реализовано правильно. После завершения поиска мы убеждаемся, что список изображений обновился, и только после этого отправляем одно из них. Но у нас появится проблема, если мы, например, захотим написать тест, проверяющий, что после ввода идентичного поискового запроса список изображений не обновится. В этом случае нам придётся создавать отдельный шаг для ввода поискового запроса для GIF-изображений, который во многом будет дублировать уже имеющийся.

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

Как же нам этого избежать? Как вы, возможно, заметили, наш шаг поиска GIF-изображений на самом деле включал в себя три действия: 

  1. сохранение текущего списка;

  2. поиск;

  3. проверку обновления списка.

Решением проблемы переиспользования будет разделение этого шага на три простых и независимых. 

Первый шаг сохраняет текущий список изображений:

And(/^primary_user stores the current list of GIFs$/) do
  TestData.gif_list = Pages::ChatPage.new.await.gif_list
end

Второй шаг – поиск гифки — позволяет напечатать ключевое слово для поиска: 

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
  Pages::ChatPage.new.await.search_for_gifs(keyword)
end

На третьем шаге мы ждём обновления списка: 

And(/^primary_user verifies that list of GIFs is updated$/) do
  chat_page = Pages::ChatPage.new.await
  Poll.for_true(timeout_message: 'Gif list is not updated') do
    (TestData.gif_list & chat_page.gif_list).empty?
  end
end

В итоге наш первоначальный сценарий выглядит следующим образом:

Scenario: Searching and sending GIF in Chat
  Given users with following parameters
    | role         | name |
    | primary_user | Dima |
    | chat_user    | Lera |
  And   primary_user logs in
  When  primary_user opens Chat with chat_user
  And   primary_user switches to GIF input source
  And   primary_user stores the current list of GIFs
  And   primary_user searches for "bee" GIFs
  Then  primary_user verifies that list of GIFs is updated
  When  primary_user sends 7th GIF in the list
  Then  primary_user verifies that the selected GIF has been sent

Таким образом, мы можем использовать первые два шага даже в том случае, если в тесте список не обновляется. Это помогает нам экономить время на разработку, так как мы можем переиспользовать простые шаги в разных тестах. 

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

Сложные действия

Под сложными шагами мы подразумеваем те, которые включают в себя переходы между разными экранами приложения либо работу с изменением состояния какого-либо экрана. Рассмотрим такую ситуацию на примере голосования в мини-игре.

Мини-игра — это экран, на котором пользователю предлагаются профили других людей, которые ему написали. Можно либо отвечать на сообщения, либо пропускать этих пользователей. Действие пропуска назовём «Голосовать “нет”». 

Тестовый пользователь

Тестовый пользователь

Нам необходимо написать тест, который «проголосует “нет”» N раз, закроет экран игры, а потом откроет его снова и проверит, что пользователь находится на правильной позиции.

«Проголосовать “нет”» — простое действие. Но, если мы сделаем для него простой шаг, то для того чтобы проголосовать N раз, нам нужно будет использовать этот шаг столько же раз на уровне сценария. Читать такой сценарий неудобно. Поэтому есть смысл создать более сложный шаг с параметром «Количество голосов», который сможет проголосовать необходимое нам количество раз. 

Кажется, что реализовать такой шаг довольно легко. Необходимо всего лишь проголосовать на странице нужное количество раз. 

When(/^primary_user votes No in Messenger mini game (d+) times$/) do count
  page = Pages::MessengerMiniGamePage.new.await
  count.to_i.times do
    page.vote_no
  end
end

Но тут мы сталкиваемся с проблемой: иногда тест голосует слишком быстро. То есть он нажимает на кнопку до того, как обрабатывается предыдущее нажатие. В этом случае приложение не отправит новый голос на сервер. Наш шаг при этом выполнится успешно. А на следующем шаге, когда мы захотим убедиться, что пользователь находится на правильной позиции в игре, тест упадёт, потому что предыдущий шаг не выполнил свою задачу и сделал меньше голосов, чем было нужно.

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

When(/^primary_user votes No in Messenger mini game (d+) times$/) do count
  page = Pages::MessengerMiniGamePage.new.await
  count.to_i.times do
    progress_before = page.progress
    page.vote_no
    Poll.for_true do
      page.progress > progress_before
    end
  end
end

После выполнения каждого голоса мы добавим проверку изменения прогресса в мини-игре. Только после этого мы будем делать следующую попытку проголосовать. Так все голоса будут успевать обрабатываться, а мы избежим падений наших тестов.

Подведём итоги. Мы создаём независимые шаги для простых действий. Это позволяет легко их переиспользовать и создавать различные сценарии за более короткое время, чем если бы нам нужно было переписывать похожие друг на друга шаги. В шаги для сложных действий мы добавляем проверки того, что действие выполнено, перед переходом к следующим действиям. 

Обобщая эти рекомендации, советуем выделять независимые методы для простых действий в тестах и добавлять проверку предустановок в сложные действия. 

Практика 7. Верификация необязательных элементов

Под необязательными элементами мы понимаем такие элементы, которые могут либо отображаться, либо не отображаться на одном и том же экране в зависимости от каких-либо условий. Здесь мы рассмотрим пример диалогов о подтверждении действий пользователя, или алёртов (alerts).

Примеры диалоговых окон

Примеры диалоговых окон

Наверняка вы сталкивались с подобными диалогами в различных мобильных приложениях. В наших двух приложениях, например, их больше 70. Они появляются в разных местах в ответ на разные действия пользователей. Что же представляют собой необязательные элементы на них? 

Проанализируем скриншоты выше.

  • Скриншот 1: заголовок, описание и две кнопки.

  • Скриншот 2: заголовок, описание и одна кнопка.

  • Скриншот 3: описание и две кнопки.

Таким образом, необязательными элементами в этих диалогах являются заголовок и вторая кнопка. Локаторы у элементов на алёртах одинаковые — вне зависимости от того, в каком месте приложения или после какого действия пользователя они появляются. Поэтому мы хотим реализовать проверки для всех типов диалогов один раз на их базовой странице, а потом наследовать от неё каждый конкретный алёрт, чтобы не пришлось повторять этот код. Создание подобных проверок мы и рассмотрим в этом разделе. 

Начнём с того, как выглядит вызов метода для верификации каждого из диалогов:

class ClearAccountAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(title:        ClearAccount::TITLE,
                 description:  ClearAccount::MESSAGE,
                 first_button: ClearAccount::OK_BUTTON,
                 last_button:  ClearAccount::CANCEL_BUTTON)
  end
end
class WaitForAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(title:        WaitForTITLE,
                 description:  WaitForMESSAGE,
                 first_button: WaitForCLOSE_BUTTON)
  end
end
class SpecialOffersAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(description:  SpecialOffers::MESSAGE,
                 first_button: SpecialOffers::SURE_BUTTON,
                 last_button:  SpecialOffers::NO_THANKS_BUTTON)
  end
end

Во всех примерах мы вызываем метод verify_alert, передавая ему лексемы для проверки необходимых элементов. При этом, как вы можете заметить, WaitForAlert мы не передаём лексему для второй кнопки, так как её не должно быть, а SpecialOffersAlert — лексему для заголовка.

Рассмотрим реализацию метода verify_alert:

def verify_alert(title: nil, description:, first_button:, last_button: nil)
  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title
  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
 ui.wait_for_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON) if last_button
end

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

В чём же проблема этого подхода? В том, что мы пропускаем проверку того, что необязательный элемент не отображается тогда, когда он не должен отображаться. Это может привести к комичным ситуациям. Например, может появиться такой алёрт:

Пользователь не понимает, что выбрать: обе кнопки как будто бы закрывают диалог. Это похоже на критический баг. Даже если приложение не падает, это нужно исправить. А тесты нам нужно изменить так, чтобы они выявляли подобные проблемы.

Для этого в тестах мы меняем проверку 

ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title

на

if title.nil?
  Assertions.assert_false(ui.elements_displayed?(ALERT_TITLE), "Alert title should not be displayed")
else
  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE)
end

Мы изменили условие if и добавили проверку второго состояния. Если мы не передаём лексему для необязательного элемента, значит, этого элемента не должно быть на экране, что мы и проверяем. Если же в title есть какой-то текст, мы понимаем, что элемент с этим текстом должен быть, и проверяем его. Мы решили выделить эту логику в общий метод, который назвали wait_for_optional_element_text. Этот метод мы можем применять не только для диалогов из этого примера, но и для любых других экранов приложения, на которых есть необязательные элементы. Видим, что if-условие из примера выше полностью находится внутри нового метода:

def wait_for_optional_element_text(expected_lexeme:, locator:)
  GuardChecks.not_nil(locator, 'Locator should be specified')
  if expected_lexeme.nil?
    Assertions.assert_false(elements_displayed?(locator), "Element with locator #{locator} should not be displayed")
  else
    wait_for_element_text(expected_lexeme: expected_lexeme, locator: locator)
  end
end

Реализация метода verify_alert тоже изменилась: 

def verify_alert(title: nil, description:, first_button:, last_button: nil)
  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
  ui.wait_for_optional_element_text(expected_lexeme: title, locator: ALERT_TITLE)
  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
  ui.wait_for_optional_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON)
end

Мы всё так же ожидаем появления обязательных элементов на экране. После этого проверяем, что их текст совпадает с переданными в метод лексемами. В случае же с необязательными элементами теперь мы используем метод wait_for_optional_element_text, который помогает убедиться, что необязательный элемент не отображается тогда, когда не должен отображаться.  

Подводя итоги, хотим обратить ваше внимание на то, что не бывает необязательных проверок, — бывают необязательные элементы, все состояния которых нужно обязательно проверять. А выделение проверки состояний необязательных элементов в общий метод позволяет нам легко переиспользовать его для разных экранов приложения.

Резюме: мы советуем использовать полную систему проверок всех состояний нужных элементов, а также выделять общие методы для однотипных действий. 

Вы можете заметить, что подобные рекомендации уже звучали в этой статье. Фокус в том, что различались примеры их применения. 

Общие рекомендации 

Выделим основные рекомендации по автоматизации тестирования мобильных приложений из семи практик, которые мы описали:

  • так как проверки – это то, ради чего мы пишем тесты, всегда используйте полную систему проверок; 

  • не забывайте добавлять проверку предустановки для асинхронных действий; 

  • выделяйте общие методы для переиспользования однотипного кода — как в шагах, так и в методах на страницах;

  • делайте объект тестирования простым; 

  • выделяйте независимые методы для простых действий в тестах. 

Возможно, эти советы кому-то покажутся очевидными. Но мы хотим обратить ваше внимание на то, что применять их можно (и нужно) в разных ситуациях. Если вы хотите дополнить список другими полезными рекомендациями, добро пожаловать в комментарии! 

Бонус

Мы подготовили тестовый проект, в котором отразили все практики, описанные в статье. Переходите по ссылке, изучайте и применяйте: 

Mobile Automation Sample Project

Сегодня мы поговорим об основных, часто встречающихся ошибках, которые часто допускают разработчики при работе с состоянием (state) при разработке React-приложения.

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

Поскольку мы все учимся, это также означает, что мы все также склонны к ошибкам. Хорошо. Цель — стать лучше и стать лучше. Если ты ошибаешься и учишься на ней, у тебя все отлично! Но если вы не научились чему-то новому и продолжаете повторять одни и те же ошибки, ну … тогда, похоже, вы застаётесь в своей карьере.

В этом духе, вот три распространенных ошибки, которые я часто вижу во время обзоров кода, которые младшие разработчики делают при работе с состоянием компонента React. Мы рассмотрим каждую ошибку и затем обсудим, как ее исправить.

  • Изменение состояния напрямую.

  • При изменении состояния компонента важно, чтобы вы возвращали новую копию состояния с изменениями, а не изменяли текущее состояние напрямую. Если вы неправильно измените состояние компонента, алгоритм сравнения React не поймает изменение, и ваш компонент не будет обновляться должным образом. Давайте посмотрим на пример.

    Скажем, у вас есть состояние, которое выглядит так:

    this.state = {
      colors: ['red', 'green', 'blue']
    }
    

    И теперь вы хотите добавить цвет «yellow» в этот массив. Может быть заманчиво сделать это:

    this.state.colors.push('yellow')
    

    Или даже это:

    this.state.colors = [...this.state.colors, 'yellow']
    

    Но оба эти подхода неверны! При обновлении состояния в компоненте класса вам всегда нужно использовать метод setState, и вы всегда должны быть осторожны, чтобы не изменять объекты. Вот правильный способ добавить элемент в массив:

    this.setState(prevState => ({ colors: [...prevState.colors, 'yellow'] }))
    

    И это приводит нас прямо к ошибке номер два.

  • Установка состояния, которое опирается на предыдущее состояние без использования функции

  • Есть два способа использовать метод setState.

    Первый способ — предоставить объект в качестве аргумента.

    Второй способ — предоставить функцию в качестве аргумента.

    Итак, когда вы хотите использовать один поверх другого?

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

    this.setState({ isDisabled: !this.state.isDisabled })
    

    Итак, что с этим не так? Проблема заключается в том, что обновления состояния React могут быть пакетными, а это означает, что в одном цикле обновления может происходить несколько обновлений состояния. Если ваши обновления должны были быть пакетными, и у вас было несколько обновлений до включенного / отключенного состояния, конечный результат может быть не тем, что вы ожидаете.

    Более правильным способом обновления состояния здесь было бы предоставление функции предыдущего состояния в качестве аргумента:

    this.setState(prevState => ({ isDisabled: !prevState.isDisabled }))
    

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

    То же самое верно для чего-то вроде увеличения счетчика.

    Не делайте этого!!!:

    this.setState({ counterValue: this.state.counterValue + 1 })
    

    Делайте правильно!!!:

    this.setState(prevState => ({ counterValue: prevState.counterValue + 1 }))
    

    Ключевым моментом здесь является то, что если ваше новое состояние зависит от значения старого состояния, вы всегда должны использовать функцию в качестве аргумента. Если вы устанавливаете значение, которое не зависит от значения старого состояния, то вы можете использовать объект в качестве аргумента.

  • Не забывайте, что setState является асинхронным

  • Наконец, важно помнить, что setState является асинхронным методом. В качестве примера, давайте представим, что у нас есть компонент с состоянием, которое выглядит так:

    this.state = { name: 'John' }
    

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

    this.setState({ name: 'Matt' })
    console.log(this.state.name)
    

    Вы можете подумать, что вывод будет 'Matt' на консоль, но это не так! Это будет — «Джон»!

    Причина этого в том, что опять же setState является асинхронным. Это означает, что он собирается запустить обновление состояния, когда доберется до строки, которая вызывает setState, но код ниже будет продолжать выполняться, поскольку асинхронный код не является блокирующим.

    Если у вас есть код, который необходимо запустить после обновления состояния, React позволяет предоставить функцию обратного вызова, которая запускается после завершения обновления.

    Правильный способ записать текущее состояние после обновления:

    this.setState({ name: 'Matt' }, () => console.log(this.state.name))
    

    Намного лучше! Теперь вывод в консоль будет 'Matt', как и ожидалось.

    Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.

    This answer is to provide enough information to not change/mutate the state directly in React.

    React follows Unidirectional Data Flow. Meaning, the data flow inside react should and will be expected to be in a circular path.

    React’s Data flow without flux

    enter image description here

    To make React work like this, developers made React similar to functional programming. The rule of thumb of functional programming is immutability. Let me explain it loud and clear.

    How does the unidirectional flow works?

    • states are a data store which contains the data of a component.
    • The view of a component renders based on the state.
    • When the view needs to change something on the screen, that value should be supplied from the store.
    • To make this happen, React provides setState() function which takes in an object of new states and does a compare and merge(similar to object.assign()) over the previous state and adds the new state to the state data store.
    • Whenever the data in the state store changes, react will trigger an re-render with the new state which the view consumes and shows it on the screen.

    This cycle will continue throughout the component’s lifetime.

    If you see the above steps, it clearly shows a lot of things are happening behind when you change the state. So, when you mutate the state directly and call setState() with an empty object. The previous state will be polluted with your mutation. Due to which, the shallow compare and merge of two states will be disturbed or won’t happen, because you’ll have only one state now. This will disrupt all the React’s Lifecycle Methods.

    As a result, your app will behave abnormal or even crash. Most of the times, it won’t affect your app because all the apps which we use for testing this are pretty small.

    And another downside of mutation of Objects and Arrays in JavaScript is, when you assign an object or an array, you’re just making a reference of that object or that array. When you mutate them, all the reference to that object or that array will be affected. React handles this in a intelligent way in the background and simply give us an API to make it work.

    Most common errors done when handling states in React

    // original state
    this.state = {
      a: [1,2,3,4,5]
    }
    
    // changing the state in react
    // need to add '6' in the array
    
    // bad approach
    const b = this.state.a.push(6)
    this.setState({
      a: b
    }) 
    

    In the above example, this.state.a.push(6) will mutate the state directly. Assigning it to another variable and calling setState is same as what’s shown below. As we mutated the state anyway, there’s no point assigning it to another variable and calling setState with that variable.

    // same as 
    this.state.a.push(6)
    this.setState({})
    

    Many people do this. This is so wrong. This breaks the beauty of React and is bad programming practice.

    So, what’s the best way to handle states in React? Let me explain.

    When you need to change ‘something’ in the existing state, first get a copy of that ‘something’ from the current state.

    // original state
    this.state = {
      a: [1,2,3,4,5]
    }
    
    // changing the state in react
    // need to add '6' in the array
    
    // create a copy of this.state.a
    // you can use ES6's destructuring or loadash's _.clone()
    const currentStateCopy = [...this.state.a]
    

    Now, mutating currentStateCopy won’t mutate the original state. Do operations over currentStateCopy and set it as the new state using setState().

    currentStateCopy.push(6)
    this.setState({
      a: currentStateCopy
    })
    

    This is beautiful, right?

    By doing this, all the references of this.state.a won’t get affected until we use setState. This gives you control over your code and this’ll help you write elegant test and make you confident about the performance of the code in production.

    To answer your question,

    Why can’t I directly modify a component’s state?


    Well, you can. But, you need to face the following consequences.

    1. When you scale, you’ll be writing unmanageable code.
    2. You’ll lose control of state across components.
    3. Instead of using React, you’ll be writing custom codes over React.

    Immutability is not a necessity because JavaScript is single threaded, but it’s a good to follow practices which will help you in the long run.

    PS. I’ve written about 10000 lines of mutable React JS code. If it breaks now, I don’t know where to look into because all the values are mutated somewhere. When I realized this, I started writing immutable code. Trust me! That’s the best thing you can do it to a product or an app.

    Сегодня мы поговорим об основных, часто встречающихся ошибках, которые часто допускают разработчики при работе с состоянием (state) при разработке React-приложения.

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

    Поскольку мы все учимся, это также означает, что мы все также склонны к ошибкам. Хорошо. Цель — стать лучше и стать лучше. Если ты ошибаешься и учишься на ней, у тебя все отлично! Но если вы не научились чему-то новому и продолжаете повторять одни и те же ошибки, ну … тогда, похоже, вы застаётесь в своей карьере.

    В этом духе, вот три распространенных ошибки, которые я часто вижу во время обзоров кода, которые младшие разработчики делают при работе с состоянием компонента React. Мы рассмотрим каждую ошибку и затем обсудим, как ее исправить.

  • Изменение состояния напрямую.

  • При изменении состояния компонента важно, чтобы вы возвращали новую копию состояния с изменениями, а не изменяли текущее состояние напрямую. Если вы неправильно измените состояние компонента, алгоритм сравнения React не поймает изменение, и ваш компонент не будет обновляться должным образом. Давайте посмотрим на пример.

    Скажем, у вас есть состояние, которое выглядит так:

    this.state = {
      colors: ['red', 'green', 'blue']
    }
    

    И теперь вы хотите добавить цвет «yellow» в этот массив. Может быть заманчиво сделать это:

    this.state.colors.push('yellow')
    

    Или даже это:

    this.state.colors = [...this.state.colors, 'yellow']
    

    Но оба эти подхода неверны! При обновлении состояния в компоненте класса вам всегда нужно использовать метод setState, и вы всегда должны быть осторожны, чтобы не изменять объекты. Вот правильный способ добавить элемент в массив:

    this.setState(prevState => ({ colors: [...prevState.colors, 'yellow'] }))
    

    И это приводит нас прямо к ошибке номер два.

  • Установка состояния, которое опирается на предыдущее состояние без использования функции

  • Есть два способа использовать метод setState.

    Первый способ — предоставить объект в качестве аргумента.

    Второй способ — предоставить функцию в качестве аргумента.

    Итак, когда вы хотите использовать один поверх другого?

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

    this.setState({ isDisabled: !this.state.isDisabled })
    

    Итак, что с этим не так? Проблема заключается в том, что обновления состояния React могут быть пакетными, а это означает, что в одном цикле обновления может происходить несколько обновлений состояния. Если ваши обновления должны были быть пакетными, и у вас было несколько обновлений до включенного / отключенного состояния, конечный результат может быть не тем, что вы ожидаете.

    Более правильным способом обновления состояния здесь было бы предоставление функции предыдущего состояния в качестве аргумента:

    this.setState(prevState => ({ isDisabled: !prevState.isDisabled }))
    

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

    То же самое верно для чего-то вроде увеличения счетчика.

    Не делайте этого!!!:

    this.setState({ counterValue: this.state.counterValue + 1 })
    

    Делайте правильно!!!:

    this.setState(prevState => ({ counterValue: prevState.counterValue + 1 }))
    

    Ключевым моментом здесь является то, что если ваше новое состояние зависит от значения старого состояния, вы всегда должны использовать функцию в качестве аргумента. Если вы устанавливаете значение, которое не зависит от значения старого состояния, то вы можете использовать объект в качестве аргумента.

  • Не забывайте, что setState является асинхронным

  • Наконец, важно помнить, что setState является асинхронным методом. В качестве примера, давайте представим, что у нас есть компонент с состоянием, которое выглядит так:

    this.state = { name: 'John' }
    

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

    this.setState({ name: 'Matt' })
    console.log(this.state.name)
    

    Вы можете подумать, что вывод будет 'Matt' на консоль, но это не так! Это будет — «Джон»!

    Причина этого в том, что опять же setState является асинхронным. Это означает, что он собирается запустить обновление состояния, когда доберется до строки, которая вызывает setState, но код ниже будет продолжать выполняться, поскольку асинхронный код не является блокирующим.

    Если у вас есть код, который необходимо запустить после обновления состояния, React позволяет предоставить функцию обратного вызова, которая запускается после завершения обновления.

    Правильный способ записать текущее состояние после обновления:

    this.setState({ name: 'Matt' }, () => console.log(this.state.name))
    

    Намного лучше! Теперь вывод в консоль будет 'Matt', как и ожидалось.

    Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.

    Здравствуйте, не понимаю, почему не изменяется состояние флажков при клике, самое интересное, если заменить todo.completed на todo.text и изменить текст, то состояние будет изменено, также если добавить console.log(«Clicked», id), то оно будет выводить слово Clicked и id флажка. Буду очень рад если поможете разобраться.
    Вот код родительского компонента:

    import React, { Component } from 'react'
    import TodoItem from "./TodoItem"
    import todosData from "./todosData"
    
    class App extends Component  {
      
        constructor() {
            super()
            this.state = {
                todos: todosData
            }
            this.handleChange = this.handleChange.bind(this)
        }
        
        handleChange(id) {
          this.setState(prevState => {
              const updatedTodos = prevState.todos.map(todo => {
                  if (todo.id === id) {
                      todo.completed = !todo.completed
                  }
                  return todo
              })
              return {
                  todos: updatedTodos
              }
          })
      }
        render() {
          const todoItems = this.state.todos.map(item => <TodoItem key={item.id} item={item} handleChange={this.handleChange}/>)        
            return (
                <div className="todo-list">
                    {todoItems}
                </div>
            )    
        }
    }
    
    export default App

    Вот код дочернего компонента:

    import React from "react"
    
    function TodoItem(props) {
        return (
            <div className="todo-item">
                <input 
                    type="checkbox" 
                    checked={props.item.completed} 
                    onChange={() => props.handleChange(props.item.id)}
                />
                <p>{props.item.text}</p>
            </div>
        )
    }
    
    export default TodoItem

    Вот код JSON-файла откуда всё берётся:

    const todosData = [
        {
            id: 1,
            text: "Take out the trash",
            completed: true
        },
        {
            id: 2,
            text: "Grocery shopping",
            completed: false
        },
        {
            id: 3,
            text: "Clean gecko tank",
            completed: false
        },
        {
            id: 4,
            text: "Mow lawn",
            completed: true
        },
        {
            id: 5,
            text: "Catch up on Arrested Development",
            completed: false
        }
    ]
    
    export default todosData

    5 месяцев назад·7 мин. на чтение

    Поскольку React становится все более и более популярным, все больше и больше React разработчиков сталкиваются с различными проблемами в процессе разработки.
    В этой статье, мы обобщим некоторые распространенные ошибки в разработке React приложений, чтобы помочь вам их избежать.
    Если вы только начинаете использовать React, рекомендуется внимательно ознакомиться с этой статьей. Если вы уже используете React для разработки проектов, также рекомендуется проверить и заполнить пробелы.

    Прочитав эту статью, вы узнаете, как избежать эти 11 ошибок React:

    • При рендеринге списка не используется key
    • Изменение значения состояния прямым присваиванием
    • Привязка значения состояния непосредственно к свойству value инпута
    • Использование состояния сразу после выполнения setState
    • Появление бесконечного цикла при использовании useState + useEffect
    • Отсутствие очистки побочных эффектов в useEffect
    • Неправильное использование логических операторов
    • Тип пропсов компонента не типизирован
    • Передача строк в качестве значений компонентам
    • Имя компонента не начинается с заглавной буквы
    • Неверная привязка события к элементу

    Ошибка: При рендеринге списка не используется key

    Проблема
    Когда мы впервые изучали React, мы отображали список следующим образом:

    const items = [
      { id: 1, value: 'item1' },
      { id: 2, value: 'item2' },
      { id: 3, value: 'item3' },
      { id: 4, value: 'item4' },
      { id: 5, value: 'item5' }
    ];
    
    const listItems = items.map((item) => {
      return <li>{item.value}</li>
    });

    После рендеринга консоль выдаст предупреждение, что для элементов списка необходимо указать ключ.
    Решение
    Вам просто нужно последовать этой подсказке и добавить key к каждому элементу:

    const items = [
      { id: 1, value: ‘item1’ },
      { id: 2, value: ‘item2’ },
      { id: 3, value: ‘item3’ },
      { id: 4, value: ‘item4’ },
      { id: 5, value: ‘item5’ }
    ];
    
    const listItems = items.map((item) => {
      return <li key={item.id}>{item.value}</li>
    });


    key помогает React определить, какие элементы были изменены, например, добавлены или удалены. Поэтому нам нужно установить уникальное значение ключа для каждого элемента в массиве.
    Для значения ключа лучше всего установить уникальное значение. В приведенном выше примере используется id. Можно использовать индекс массива, но такой подход не рекомендуется.
    Уникальный ключ помогает React следить за изменениями списка — какой элемент удалился или переместился.

    Ошибка: Изменение значения состояния прямым присваиванием

    Проблема
    В React нельзя назначать состояние и изменять напрямую, иначе это вызовет проблемы.

    // классовый компонент
    
    handleChange = () => {
       this.state.name = "John";
    };

    В этот момент будет выдано предупреждение не изменять состояние напрямую, а использовать setState().
    Решение
    Классовые компоненты могут быть изменены с помощью setState(), а функциональные компоненты могут быть изменены с помощью useState():

    // Классовые компоненты: используйте setState()
    this.setState({ name: "John" });
    
    // Функциональные компоненты:используйте useState()
    const [name, setName] = useState("");
    setName("John");

    Ошибка: Привязка значения состояния непосредственно к свойству value инпута

    Проблема
    Когда мы напрямую привязываем значение состояния к свойству value инпута, мы обнаружим, что независимо от того, что мы вводим в поле ввода, содержимое поля ввода не изменится.

    export default function App() {
      const [count, setCount] = useState(0);
      return <input type="text" value={count} />;
    }

    Это связано с тем, что мы используем переменную состояния в качестве значения по умолчанию для присвоения значения <input>, а состояние в функциональном компоненте может быть изменено только функцией set*, возвращаемым useState. Таким образом, решение также очень простое, просто используйте функцию set* при изменении. Подробнее о том как работать с инпутом в React можно прочитать в этой статье.
    Решение
    Просто привяжите событие onChange к <input> и измените его, вызвав setCount:

    export default function App() {
      const [count, setCount] = useState(0);
      const handleChange= (event) => setCount(event.target.value);
    
      return <input type="text" value={count} onChange={handleChange} />;
    }

    Ошибка: Использование состояния сразу после выполнения setState

    Проблема
    Когда мы изменяем данные через setState() и сразу же хотим получить новые данные, возникнет ситуация, что возвращаются старые данные:

    // Классовые компоненты
    
    // инициализация состояния
    this.state = { name: "John" };
    
    // обновление состояния
    this.setState({ name: "Hello, John!" });
    console.log(this.state.name); // => John

    Это связано с тем, что setState() является асинхронным. Когда setState() выполняется, реальная операция обновления будет помещена в асинхронную очередь для выполнения, а код, который будет выполняться следующим (т.е. console.log в примере), выполняется синхронно, поэтому выводимое в консоль состояние не является последним значением.
    Решение
    Просто передайте последующую операцию, которая будет выполняться как функция, в качестве второго параметра setState(), эта функция обратного вызова будет выполнена после завершения обновления.

    this.setState({ name: "Hello, John!" }, () => {
      console.log(this.state.name); // => Hello, John!
    });

    Теперь обновленное значение выводится правильно.

    Ошибка: Появление бесконечного цикла при использовании useState + useEffect

    Проблема
    Когда мы напрямую вызываем метод set*(), возвращаемый useState() внутри useEffect(), и не устанавливаем второй параметр в useEffect(), мы столкнемся с бесконечным циклом:

    export default function App() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        setCount(count + 1);
      });
      return <div className="App">{count}</div>;
    }

    После этого можно увидеть, что данные на странице обновляются, и функция useEffect() вызывается бесконечно, входя в состояние бесконечного цикла.
    Решение
    Это распространенная проблема неправильного использования useEffect(). useEffect() можно рассматривать как комбинацию трех функций жизненного цикла: componentDidMount, componentDidUpdate и componentWillUnmount в классовых компонентах. useEffect(effect, deps) принимает 2 аргумента:

    • effect функция, которая должна выполниться (побочный эффект)
    • deps массив зависимостей

    При изменении массива deps выполняется функция эффекта. Чтобы изменить метод, вам нужно всего лишь передать [] в качестве второго аргумента useEffect() :

    export default function App() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        setCount(count + 1);
      }, []);
    
      return <div className="App">{count}</div>;
    }


    Приведем 4 случая использования useEffect:

    • Если второй параметр не передан: при обновлении любого состояния будет запущена функция эффекта useEffect.
    useEffect(() => {
      setCount(count + 1);
    });
    • Если второй параметр — это пустой массив: функция эффекта useEffect срабатывает только при монтировании и размонтировании.
    useEffect(() => {
      setCount(count + 1);
    }, []);
    • Если второй параметр представляет собой массив с одним значением: функция эффекта useEffect будет запускаться только при изменении значения.
    useEffect(() => {
      setCount(count + 1);
    }, [name]);
    • Если второй параметр представляет собой массив c несколькими значениями: функция эффекта useEffect будет запускаться при изменении хотя бы одного из значений из списка зависимостей.
    useEffect(() => {
      setCount(count + 1);
    }, [name, age]);

    Ошибка: Отсутствие очистки побочных эффектов в useEffect

    Проблема
    В классовых компонентах мы используем метод жизненного цикла componentWillUnmount() для очистки некоторых побочных эффектов, таких как таймеры, слушатели событий и т. д.
    Решение
    Из функции эффекта useEffect() может быть возвращена функция очистки, которая аналогична роли метода жизненного цикла componentWillUnmount():

    useEffect(() => {
      // ...
      return () => clearInterval(id);
    }, [name, age]);

    Ошибка: Неправильное использование логических операторов

    Проблема
    В синтаксисе JSX/TSX мы часто используем логические значения для управления отображаемыми элементами, и во многих случаях мы используем оператор && для обработки этой логики:

    const count = 0;
    const Comp = () => count && <h1>Chris1993</h1>;

    Мы думаем, что в это время страница будет отображать пустой контент, но на самом деле на ней отобразится 0.
    Решение
    Причина в том, что ложное выражение приводит к тому, что элементы после && пропускаются, и будет возвращено значение ложного выражения. Поэтому нужно стараться написать условие оценки как можно более полным, не полагаясь на истинное и ложное логическое значение JavaScript для сравнения:

    const count = 0;
    const Comp = () => count > 0 && <h1>Chris1993</h1>;

    Теперь страница будет отображать пустой контент, как и ожидается.

    Ошибка: Типы просов компонента не типизированы

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

    const UserInfo = (props) => {
      return (
        <div>
          {props.name} : {props.age}
        </div>
      );
    };

    Решение

    • Определить типы пропсов компонента, используя TypeScript.
    // Классовые компоненты
    interface AppProps {
      value: string;
    }
    interface AppState {
      count: number;
    }
    class App extends React.Component<AppProps, AppStore> {
      // ...
    }
    
    // Функциональные компоненты
    interface AppProps {
      value?: string;
    }
    const App: React.FC<AppProps> = ({ value = "", children }) => {
      //...
    };
    • Без использования TypeScript типы пропсов могут быть определены с помощью propTypes.
    const UserInfo = (props) => {
      return (
        <div>
          {props.name} : {props.age}
        </div>
      );
    };
    UserInfo.propTypes = {
      name: PropTypes.string.isRequired,
      age: PropTypes.number.isRequired,
    };

    Ошибка: Передача строк в качестве значений компонентам

    Проблема
    Так как React имеет шаблонный синтаксис, очень похожий на HTML, часто бывает так, что числа передаются напрямую компонентам в пропсы, что приводит к неожиданному результату:

    <MyComp count="99"></MyComp>

    Сравнение props.count === 99 в компоненте MyComp вернет false.
    Решение
    Правильный способ должен заключаться в использовании фигурных скобок для передачи пропсов:

    <MyComp count={99}></MyComp>

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

    <MyComp count={"99"}></MyComp>

    Ошибка: Имя компонента не начинается с заглавной буквы

    Проблема
    Начинающие разработчики часто забывают называть свои компоненты с заглавной буквы. Компоненты, начинающиеся со строчной буквы в JSX/TSX, компилируются в элементы HTML, такие как <div /> для тегов HTML.

    class myComponent extends React.component {}

    Решение
    Просто измените первую букву на заглавную:

    class MyComponent extends React.component {}

    Ошибка: Неверная привязка события к элементу в классовых компонентах

    Проблема

    import { Component } from "react";
    
    export default class HelloComponent extends Component {
      constructor() {
        super();
        this.state = {
          name: "John",
        };
      }
      update() {
        this.setState({ name: "Hello John!" });
      }
    
      render() {
        return (
          <div>
            <button onClick={this.update}>update</button>
          </div>
        );
      }
    }

    При нажатии на кнопку update консоль сообщит об ошибке, что невозможно прочитать свойства undefined (чтение setState)
    Решение
    Это происходит потому, что this не привязан к тому контексту, который мы ожидаем. Есть несколько решений:

    • Привязать контекст в конструкторе при помощи метода bind
    constructor() {
      super();
      this.state = {
        name: "John"
      };
      this.update = this.update.bind(this);
    }
    • Использовать стрелочные функции
    update = () => {
      this.setState({ name: "Hello John!" });
    };
    • Привязать прямо в функции рендеринга
    <button onClick={this.update.bind(this)}>update</button>
    • Использовать стрелочные функции в функции рендеринга (не рекомендуется, т.к. это создает новую функцию каждый раз при рендеринге компонента, что влияет на производительность)
    <button onClick={() => this.update()}>update</button>

    4 месяца назад·6 мин. на чтение

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

    Технологии

    Поскольку эта статья будет носить характер мнения. Сделаем несколько предположений о том, какие технологии будут использоваться в проекте:

    • Приложение — React (Hooks)
    • Глобальное управление состоянием — Redux, Redux Toolkit
    • Маршрутизация — React Router
    • Стили — CSS, Sass, Styled Components
    • Тестирование — Jest, React Testing Library

    Нет четкого мнения по поводу стилей, идеальны ли Styled Components, CSS-модули или пользовательский набор для Sass, поэтому можно использовать любой подходящий вариант.
    Также предполагается, что тесты находятся рядом с кодом, а не в папке tests на верхнем уровне.
    Все, что здесь написано, может быть применимо, если вы используете обычный Redux, а не Redux Toolkit. Можно настроить ваш Redux как feature slices.
    Также будет удобно визуализировать созданные компоненты с помощью, например, Storybook. Также покажем, как проект будет выглядеть с этими файлами, если вы решите использовать его в своем проекте.
    Для примера будем использовать приложение “Библиотека”, который имеет страницу со списком книг, страницу со списком авторов и систему аутентификации.

    Структура каталога

    Структура каталогов верхнего уровня будет выглядеть следующим образом:

    • assets — глобальные статические файлы, такие как изображения, svg, логотип компании и т.д.
    • components — глобальные общие/повторно используемые компоненты, такие как макеты (обертки), компоненты форм, кнопки
    • services — JavaScript модули
    • store — глобальное хранилище Redux
    • utils — утилиты, помощники, константы и т.п.
    • views — можно также назвать pages, здесь будет содержаться большая часть приложения.
      Лучше сохранять привычные соглашения, где это возможно, поэтому src содержит все, index.js является точкой входа, а App.js устанавливает аутентификацию и маршрутизацию.
    .
    └── /src
        ├── /assets
        ├── /components
        ├── /services
        ├── /store
        ├── /utils
        ├── /views
        ├── index.js
        └── App.js

    Также в проекте могут присутствовать некоторые дополнительные папки, которые у вас могут быть, такие как types, если это проект TypeScript, middlewares (промежуточное программное обеспечение), если необходимо, возможно, контекст для работы с Context API и т.д.

    Псевдонимы (алиасы)

    Полезно настроить систему на использование псевдонимов, чтобы все, что находится в папке компонентов, можно было импортировать как @components, изображения как @assets и т. д. Если у вас Webpack, это делается через конфигурацию resolve.

    module.exports = {
      resolve: {
        extensions: ['js', 'ts'],
        alias: {
          '@': path.resolve(__dirname, 'src'),
          '@assets': path.resolve(__dirname, 'src/assets'),
          '@components': path.resolve(__dirname, 'src/components'),
          // ...etc
        },
      },
    }

    Это просто облегчает импорт из любого места в проекте и перемещение файлов без изменения импорта, и вы никогда не получите что-то вроде ../../../../../../../components/.

    Компоненты

    В папке components компоненты сгруппированы по типам — формы, таблицы, кнопки, макеты и т.д. Специфика зависит от конкретного приложения.
    В данном примере предполагается, что вы либо создаете собственную систему форм, либо создаете привязку к существующей системе форм (например, комбинируя Formik и Material UI). В этом случае вы создадите папку для каждого компонента (TextField, Select, Radio, Dropdown и т.д.), а внутри будет файл для самого компонента, стили, тесты и Storybook, если он используется.

    • Component.js — собственно компонент React
    • Component.styles.js — файл стилей для компонента
    • Component.test.js — тесты
    • Component.stories.js — файл Storybook.

    Это имеет гораздо больше смысла, чем иметь одну папку, содержащую файлы для всех компонентов, одну папку, содержащую все тесты, одну папку, содержащую все файлы Storybook и т.д. Все связанное группируется вместе и легко находится.

    .
    └── /src
        └── /components
            ├── /forms
            │   ├── /TextField
            │   │   ├── TextField.js
            │   │   ├── TextField.styles.js
            │   │   ├── TextField.test.js
            │   │   └── TextField.stories.js
            │   ├── /Select
            │   │   ├── Select.js
            │   │   ├── Select.styles.js
            │   │   ├── Select.test.js
            │   │   └── Select.stories.js
            │   └── index.js
            ├── /routing
            │   └── /PrivateRoute
            │       ├── /PrivateRoute.js
            │       └── /PrivateRoute.test.js
            └── /layout
                └── /navigation
                    └── /NavBar
                        ├── NavBar.js
                        ├── NavBar.styles.js
                        ├── NavBar.test.js
                        └── NavBar.stories.js


    Обратите внимание, что в каталоге components/forms есть файл index.js. Часто справедливо советуют избегать использования файлов index.js, поскольку они не являются явными, но в данном случае это имеет смысл — в конечном итоге он будет индексом всех форм и будет выглядеть примерно так:

    // src/components/forms/index.js
    
    import { TextField } from './TextField/TextField'
    import { Select } from './Select/Select'
    import { Radio } from './Radio/Radio'
    
    export { TextField, Select, Radio }

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

    import { TextField, Select, Radio } from '@components/forms'

    Иногда лучше использовать этот подход, чем создавать index.js внутри каждой папки внутри forms, так что теперь у вас есть только один index.js, который фактически индексирует всю директорию, в отличие от десяти файлов index.js только для того, чтобы упростить импорт для каждого отдельного файла.

    Сервисы

    Каталог services менее важен, чем компоненты, но если вы создаете простой модуль JavaScript, который используется остальной частью приложения, он может быть полезен. Обычный пример — модуль LocalStorage, который может выглядеть следующим образом:

    .
    └── /src
        └── /services
            ├── /LocalStorage
            │   ├── LocalStorage.service.js
            │   └── LocalStorage.test.js
            └── index.js
    

    Пример сервиса:

    // src/services/LocalStorage/LocalStorage.service.js
    export const LocalStorage = {
      get(key) {},
      set(key, value) {},
      remove(key) {},
      clear() {},
    }
    import { LocalStorage } from '@services'
    
    LocalStorage.get('foo')

    Хранилище (стор, store)

    Глобальное хранилище данных будет содержаться в директории store — в данном случае Redux. Каждая функциональность будет иметь свою папку, в которой будет содержаться слайс Redux Toolkit, а также экшены и тесты. Эта настройка также может быть использована с обычным Redux, вы просто создадите файл .reducers.js и .actions.js вместо slice. Если вы используете саги, это может быть .saga.js вместо .actions.js для действий Redux Thunk.

    .
    └── /src
        ├── /store
        │   ├── /authentication
        │   │   ├── /authentication.slice.js
        │   │   ├── /authentication.actions.js
        │   │   └── /authentication.test.js
        │   ├── /authors
        │   │   ├── /authors.slice.js
        │   │   ├── /authors.actions.js
        │   │   └── /authors.test.js
        │   └── /books
        │       ├── /books.slice.js
        │       ├── /books.actions.js
        │       └── /books.test.js
        ├── rootReducer.js
        └── index.js

    Вы также можете добавить что-то вроде ui секции стора для обработки модальных окон, всплывающих уведомлений, переключения боковой панели и других глобальных состояний пользовательского интерфейса, что, лучше, чем иметь const [isOpen, setIsOpen] = useState(false) повсюду.
    В rootReducer вы импортируете все свои фрагменты и объединяете их с помощью combineReducers, а в index.js настраиваете магазин.

    Утилиты (Utils)

    Нужна ли вашему проекту папка utils — решать вам. Но обычно существуют некоторые глобальные функции, такие как валидация и конвертеры, которые могут легко использоваться в различных разделах приложения. Если вы упорядочите их — не будете иметь один файл helpers.js, содержащий тысячи функций, — она может стать полезным дополнением к организации вашего проекта.

    .
    └── src
        └── /utils
            ├── /constants
            │   └── countries.constants.js
            └── /helpers
                ├── validation.helpers.js
                ├── currency.helpers.js
                └── array.helpers.js

    Опять же, папка utils может содержать все, что вы захотите, и что, по вашему мнению, имеет смысл держать на глобальном уровне. Если вам не нравятся «многоуровневые» имена файлов, вы можете просто назвать его validation.js. Однако, это облегчает навигацию по именам файлов при поиске в IDE.

    Views

    Здесь находится основная часть вашего приложения: в каталоге views. Любая страница в вашем приложении — это «представление» (view). В этом небольшом примере представления довольно хорошо согласуются со стором Redux, но не обязательно, что стор и представления будут полностью совпадать, поэтому они разделены. Кроме того, книги могут тянуть за собой авторов, и так далее.
    Все, что находится внутри представления, является сущностью, которая, скорее всего, будет использоваться только в этом конкретном представлении — BookForm, который будет использоваться только на маршруте /books, и AuthorBlurb, который будет использоваться только на маршруте /authors. Это могут быть специфические формы, модальные окна, кнопки, любые компоненты, которые не будут глобальными.
    Преимущество хранения всего в домене вместо объединения всех страниц в компоненты/страницы заключается в том, что это позволяет легко взглянуть на структуру приложения и узнать, сколько в нем представлений верхнего уровня, и понять, где находится все, что используется только этим представлением. Если есть вложенные маршруты, вы всегда можете добавить папку вложенных представлений в основной маршрут.

    .
    └── /src
        └── /views
            ├── /Authors
            │   ├── /AuthorsPage
            │   │   ├── AuthorsPage.js
            │   │   └── AuthorsPage.test.js
            │   └── /AuthorBlurb
            │       ├── /AuthorBlurb.js
            │       └── /AuthorBlurb.test.js
            ├── /Books
            │   ├── /BooksPage
            │   │   ├── BooksPage.js
            │   │   └── BooksPage.test.js
            │   └── /BookForm
            │       ├── /BookForm.js
            │       └── /BookForm.test.js
            └── /Login
                ├── LoginPage
                │   ├── LoginPage.styles.js
                │   ├── LoginPage.js
                │   └── LoginPage.test.js
                └── LoginForm
                    ├── LoginForm.js
                    └── LoginForm.test.js

    Хранение всего в папках может показаться раздражающим, если вы никогда не создавали свой проект таким образом — вы всегда можете сделать его более плоским или перенести тесты в свой собственный каталог, имитирующий остальную часть приложения.

    Итоги

    Эта структура приложения и организация React, которая хорошо масштабируется для большого enterprise приложения, удобна для тестирования и стилизации, а также сохраняет все вместе в функционально ориентированном виде. Она более вложенная, чем традиционная структура, в которой все находится в компонентах и контейнерах.
    Легко взглянуть на эту систему и понять, что нужно для вашего приложения и куда идти, чтобы поработать над конкретным разделом или компонентом, который влияет на приложение глобально.

  • Ошибка изменения параметров рабочего сервера кластера
  • Ошибка извлечения проверьте файлы андроид что значит на телефоне
  • Ошибка извлечения проверьте файлы андроид что делать
  • Ошибка извлечения ошибка распаковки архива
  • Ошибка извлечения исо образа