chromium/ui/file_manager/file_manager/lib/actions_producer_unittest.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assertEquals, assertNotReached, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';

import {installMockChrome} from '../common/js/mock_chrome.js';
import {waitUntil} from '../common/js/test_error_reporting.js';

import {ConcurrentActionInvalidatedError} from './actions_producer.js';
import type {Action} from './base_store.js';
import {keepLatest, keyedKeepFirst, keyedKeepLatest} from './concurrency_models.js';
import {setupTestStore} from './for_tests.js';

export function setUp() {
  installMockChrome({});
}
/**
 * Helper to accumulate all produced actions from the ActionsProducer.
 */
async function consumeGenerator(gen: AsyncGenerator, producedResults: any[]) {
  while (true) {
    const {value, done} = await gen.next();
    if (value) {
      producedResults.push(value);
    }
    if (done) {
      break;
    }
  }
}

/**
 * Tests the keepLatest() concurrency model. Checks that the latest call
 * overtakes any previous call.
 */
export async function testKeepLatest() {
  const {actionsProducerSuccess} = setupTestStore();

  // keepLatest() wraps any ActionsGenerator.
  const action = keepLatest(actionsProducerSuccess);

  // Array to collect all the generated actions.
  const results: Action[] = [];

  // `first` here is a generator,  an ActionsProducer, wrapped in the
  // concurrency model `keepLatest`.
  const first = action('first-action');

  // Starts consuming the generated actions and can start another one in
  // parallel.
  const {value} = (await first.next());
  assertEquals(value!.type, '[test] step');
  assertEquals(value!.payload, '0 first-action');

  // A new call to the `action` will cause the previous `first` to be cancelled.
  const second = action('second-action');
  const secondValue = await second.next();
  assertEquals(secondValue.value!.type, '[test] step');
  assertEquals(secondValue.value!.payload, '0 second-action');
  results.push(secondValue.value!);

  // At this point `first` should be cancelled by the keepLatest().
  // However, the exception only shows up in the next iteration of the AP.
  try {
    await first.next();
    assertNotReached('`first` should have failed');
  } catch (error) {
    assertTrue(error instanceof ConcurrentActionInvalidatedError);
    // Check that the generator is in `done=true` state.
    const {value, done} = await first.next();
    assertEquals(value, undefined);
    assertEquals(done, true);
  }

  // Await for the second generator to be fully consumed.
  await consumeGenerator(second, results);

  // The second action generates 4 results: 1 at start, 2 from the args [2, 2]
  // and 1 for the final action.
  await waitUntil(
      () => results.filter(r => r.payload.includes('second-action')).length ===
          4);
}

/**
 * Tests the keyedKeepFirst() concurrency model. Checks that the first call
 * with the same key, doesn't start a new AP, and calls with a new key cancels
 * the previous APs and starts a new AP.
 */
export async function testKeyedKeepFirst() {
  const {actionsProducerSuccess} = setupTestStore();

  const action = keyedKeepFirst(actionsProducerSuccess, (payload: string) => {
    return `key-${payload}`;
  });

  // `first` here is a generator,  an ActionsProducer, wrapped in the
  // concurrency model `keyedKeepFirst`.
  const first = action('file-key-1');

  // Starts consuming the generated actions and can start another one in
  // parallel.
  const {value} = await first.next();
  assertEquals(value!.type, '[test] step');
  assertEquals(value!.payload, '0 file-key-1');

  // Make a second call with the same key, which finishes in the first iteration
  // without generating any action.
  const second = action('file-key-1');
  const secondValue = await second.next();
  assertEquals(secondValue!.value, undefined);
  assertEquals(secondValue!.done, true);

  // Start a new call with different key.
  // Array to collect all the generated actions.
  const results: Action[] = [];
  const third = action('another-key-2');
  const thirdValue = await third.next();
  assertEquals(thirdValue!.value!.type, '[test] step');
  assertEquals(thirdValue!.value!.payload, '0 another-key-2');
  results.push(thirdValue.value!);

  // At this point `first` should be cancelled by the keyedKeepFirst().
  // However, the exception only shows up in the next iteration of the AP.
  try {
    await first.next();
    assertNotReached('`first` should have failed');
  } catch (error) {
    assertTrue(error instanceof ConcurrentActionInvalidatedError);
    // Check that the generator is in `done=true` state.
    const {value, done} = await first.next();
    assertEquals(value, undefined);
    assertEquals(done, true);
  }

  // Await for the third generator to be fully consumed.
  await consumeGenerator(third, results);
  // The third action generates 4 results: 1 at start, 2 from the args [2,
  // 2] / and 1 for the final action.
  await waitUntil(
      () => results.filter(r => r.payload.includes('another-key-2')).length ===
          4);
}

/**
 * Tests the keyedKeepLatest() concurrency model. Checks that the first call
 * with the same key will cancel the pending AP with the same key, but shouldn't
 * affect the pending AP with different keys.
 */
export async function testKeyedKeepLatest() {
  const {actionsProducerSuccess} = setupTestStore();

  const action = keyedKeepLatest(actionsProducerSuccess, (payload: string) => {
    return `key-${payload}`;
  });

  // Array to collect all the generated actions.
  const results: Action[] = [];

  // `first` here is a generator, an ActionsProducer, wrapped in the
  // concurrency model `keyedKeepLatest`.
  const first = action('file-key-1');

  // Starts consuming the generated actions and can start another one in
  // parallel.
  const {value} = await first.next();
  assertEquals(value!.type, '[test] step');
  assertEquals(value!.payload, '0 file-key-1');

  // Make a second call with a different key, which shouldn't affect the first
  // action.
  const second = action('file-key-2');
  const secondValue = await second.next();
  assertEquals(secondValue.value!.type, '[test] step');
  assertEquals(secondValue.value!.payload, '0 file-key-2');
  results.push(secondValue.value!);

  // Start a new call with the same key as the first action, which cancels the
  // first action.
  const third = action('file-key-1');
  const thirdValue = await third.next();
  assertEquals(thirdValue.value!.type, '[test] step');
  assertEquals(thirdValue.value!.payload, '0 file-key-1');
  results.push(thirdValue.value!);

  // At this point `first` should be cancelled by the keyedKeepLatest().
  // However, the exception only shows up in the next iteration of the AP.
  try {
    await first.next();
    assertNotReached('`first` should have failed');
  } catch (error) {
    assertTrue(error instanceof ConcurrentActionInvalidatedError);
    // Check that the generator is in `done=true` state.
    const {value, done} = await first.next();
    assertEquals(value, undefined);
    assertEquals(done, true);
  }

  // Await for the second/third generator to be fully consumed.
  await Promise.all(
      [consumeGenerator(second, results), consumeGenerator(third, results)]);

  // The second/third action generates 4 results: 1 at start, 2 from the args
  // [2, 2] / and 1 for the final action.
  assertEquals(4, results.filter(r => r.payload.includes('file-key-1')).length);
  assertEquals(4, results.filter(r => r.payload.includes('file-key-2')).length);
}