В этой статье я представлю пример использования Redux-Saga, JavaScript Generators и Redux путем создания функции опроса для веб-приложения. Запрос API опроса - это запрос AJAX к серверу с заданным интервалом времени для проверки наличия обновлений. Затем мы протестируем наш код с помощью Cypress.

Создайте функцию опроса с Redux Saga

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

Сервер будет иметь 3 ответа на наш запрос на опрос: started, succeeded или failed. В следующем коде показано, как написать запрос на опрос с помощью Redux Saga.

import { take, put, delay } from 'redux-saga/effects'
function* checkJobStatus() {
  let jobSucceeded = false;
  while (!jobSucceeded) {
    yield put({type: "POLLING_ACTION_REQUEST"});
    const pollingAction = yield take("POLLING_ACTION_RESPONSE");
    const pollingStatus = pollingAction.payload.response.status;
    switch (pollingStatus) {
      case POLLING_STATUS.SUCCEEDED:
        jobSucceeded = true;
        yield put({type: "HANDLE_POLLING_SUCCESS"});
        break;
      case POLLING_STATUS.FAILED:
        jobSucceeded = true;
        yield put({type: "HANDLE_POLLING_FAILURE"});
        break;
      default:
        break;
    }
    // delay the next polling request in 1 second
    yield call(delay, 1000);
  }
}

В приведенном выше примере мы выполняем запрос, используя эффект положить, который отправляет действие. Сразу после этого мы используем эффект Redux-Saga под названием take, который блокирует выполнение саги до тех пор, пока кто-то не отправит действие, указанное в качестве параметра. После отправки POLLING_ACTION_RESPONSE мы проверяем статус, возвращенный сервером. Если мы получаем статус завершен или не пройден, мы должны обработать их соответствующим образом, используя эффект положить. В противном случае мы выполняем еще один запрос на опрос к серверу и так далее, пока он не будет завершен.

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

Но что, если вы хотите обрабатывать больше, чем просто действовать в соответствии с ответом сервера на запрос опроса? Что, если вы хотите ограничить время опроса сервера, допустим, 1 минуту? Что, если вы также хотите разрешить пользователю отменить загрузку файла в середине обработки? Звучит немного сложнее, не правда ли?

С redux-saga это намного проще, чем вы думаете!

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

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

Посмотрим, как это делается с помощью redux-saga:

В следующем примере функция race Redux-Saga выполняется между четырьмя эффектами:

  1. Вызов нашей исходной функции checkJobStatus.
  2. CANCEL_POLLING действие, которое в конечном итоге может быть отправлено в магазин.
  3. POLLING_FAILED действие, которое в конечном итоге может быть отправлено в магазин.
  4. Призыв к задержке. delay - это служебная функция Redux-Saga, которая возвращает обещание, которое разрешается через X миллисекунд. Мы используем его, чтобы установить тайм-аут для гонки.
import { race, take, put, call, delay } from 'redux-saga/effects'
function* startPollingSaga(action) {
    // Race the following commands with a timeout of 1 minute
    const { response, failed, timeout } = yield race({
      response: call(checkJobStatus),
      cancel: take("CANCEL_POLLING"),
      failed: take("POLLING_FAILED"),
      timeout: call(delay, 60000)
    });
    // handle failure scenario
    if (failed) {
      yield put({type: "HANDLE_POLLING_FAILURE"});
    }
}

Если call(checkJobStatus) заканчивается первым, cancel, failed и timeout будут undefined. В нашем случае response также будет undefined, поскольку checkJobStatus не возвращает Promise, но самостоятельно обрабатывает ответ на опрос.

Если call(delay, 60000) разрешится первым, timeout будет результатом delay и cancel, failed и response будут undefined.

Если действие типа CANCEL_POLLING отправлено в Store до завершения checkJobStatus, response, failed и timeout будут undefined, а cancel получит значение отправленного действия.

Если действие типа POLLING_FAILED отправлено в магазин до завершения checkJobStatus, response, cancel и timeout будут undefined, а failed получит значение отправленного действия.

Примечание. В случае отправки действий POLLING_FAILED или CANCEL_POLLING эффект race автоматически отменяет checkJobStatus и delay, вызывая в нем ошибку отмены.

Тестирование Redux Saga с Cypress

Теперь, когда мы можем реализовать описанный выше сценарий, давайте узнаем, как мы можем легко его протестировать!

В этом примере я продемонстрирую решение с использованием Cypress.

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

describe('ui test', function() {
  it('should wait for processing to timeout', function() {
    // Overrides native global functions related to time allowing
    // them to be controlled synchronously before polling request
    cy.clock();
    cy.route('GET', 'upload file endpoint', uploadResponse)
      .as('fileUploaded');
    cy.route('GET', 'polling endpoint', pollingResponse)
      .as('pollingStarted');
    // Since this article is talking about file upload we are using
    // a custom command to imitate the file upload because it's not
    // built-in in cypress.
    cy.uploadFile('dropdown zone', 'file name');
     
    cy.wait('fileUploaded');
    cy.wait('pollingStarted');
    // Set the clock forward to cause a timeout
    cy.clock().then((clock) => {
      clock.tick(60000);
      clock.restore();
    });
    // Here you can verify that the desired ui behavior is as
    // expected  
    
  });
});

Итак, что у нас здесь?

Перед определением маршрутов и выполнением запроса на опрос мы хотим переопределить собственные глобальные функции, связанные со временем. Это позволит нам синхронно управлять собственными глобальными функциями. Для этой цели мы используем cy.clock();. Таким образом, мы можем решить позже установить часы вперед, чтобы мы могли вызвать тайм-аут.

После определения маршрутов запроса загрузки и опроса мы отправляем файл и ждем его загрузки и начала опроса.

Теперь мы можем перевести часы вперед:

cy.clock().then((clock) => {
 clock.tick(60000);
 clock.restore();
});

clock.tick(milliseconds)

Переместите часы на указанное число milliseconds. Будут вызваны все таймеры в пределах заданного диапазона времени.

clock.restore()

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

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

Итак, что мы узнали на данный момент? Мы узнали, что при использовании Redux-Saga очень легко управлять гонкой между несколькими действиями. Лично мне нравится то, что вы можете управлять всеми этими действиями в одном месте, что делает его интуитивно понятным и простым в обслуживании. Мы также узнали, что при использовании Cypress очень легко протестировать сценарий, в котором одно из ваших действий приводит к тайм-ауту. Мы видели, как «ждать» тайм-аута, который является асинхронным, синхронным образом.

Вот и все! Теперь ваша очередь попробовать!