chromium/ui/file_manager/file_manager/containers/cloud_panel_container_unittest.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {assertEquals, assertFalse} from 'chrome://webui-test/chai_assert.js';

import {waitUntil} from '../common/js/test_error_reporting.js';
import {updateBulkPinProgress} from '../state/ducks/bulk_pinning.js';
import {updateDriveConnectionStatus} from '../state/ducks/drive.js';
import {updatePreferences} from '../state/ducks/preferences.js';
import {waitDeepEquals} from '../state/for_tests.js';
import {getEmptyState, getStore} from '../state/store.js';
import {XfCloudPanel} from '../widgets/xf_cloud_panel.js';

import {type BulkPinProgress, BulkPinStage, CloudPanelContainer} from './cloud_panel_container.js';

/**
 * An instance of the cloud panel container.
 */
let container: CloudPanelContainer|null = null;

/**
 * An instance of the `<xf-cloud-panel>`.
 */
let panel: XfCloudPanel|null = null;

/**
 * A preferences object to initialize the store with that ensures bulk pinning
 * preference is enabled.
 */
const PREFERENCES = {
  driveEnabled: false,
  driveSyncEnabledOnMeteredNetwork: true,
  searchSuggestEnabled: false,
  use24hourClock: false,
  timezone: 'GMT+10',
  arcEnabled: false,
  arcRemovableMediaAccessEnabled: false,
  folderShortcuts: [],
  trashEnabled: false,
  officeFileMovedOneDrive: 0,
  officeFileMovedGoogleDrive: 0,
  driveFsBulkPinningAvailable: true,
  driveFsBulkPinningEnabled: true,
  localUserFilesAllowed: true,
  defaultLocation: chrome.fileManagerPrivate.DefaultLocation.MY_FILES,
  skyVaultMigrationDestination:
      chrome.fileManagerPrivate.CloudProvider.NOT_SPECIFIED,
};

export function setUp() {
  loadTimeData.overrideValues({'DRIVE_FS_BULK_PINNING': true});
  panel = document.createElement('xf-cloud-panel');
  document.body.appendChild(panel);
  container = new CloudPanelContainer(panel, /*test_=*/ true);
}

export function tearDown() {
  // Unsubscribe from the store.
  if (container) {
    getStore().unsubscribe(container);
  }
  document.body.innerHTML = window.trustedTypes!.emptyHTML;
}

/**
 * Tests that when bulk pinning is in a progress mode the cloud panel receives
 * that data as attributes.
 */
export async function testProgressAndItemsArePassedToElement() {
  // Initialize the store with bulk pinning pref enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 15% done with 24 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 150,
    filesToPin: 24,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 24,
  };

  // Dispatch an update to the store and wait for the panel to have the
  // attribute `items`.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  await waitUntil(() => panel!.hasAttribute('items'));

  // Assert the items and progress values are representative of the data in the
  // store.
  assertEquals(panel!.getAttribute('items'), '24');
  assertEquals(panel!.getAttribute('percentage'), '15');
}

/**
 * Tests that if somehow any invalid data makes its way into the store, it
 * doesn't propagate to the element.
 */
export async function testOutOfBoundsValuesDoNotUpdateProgress() {
  // Initialize the store with bulk pinning pref enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should contain invalid data.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 150,
    pinnedBytes: 1000,  // Greater than `bytesToPin`.
    filesToPin: -10,    // Negative number of files to pin.
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 24,
  };

  // Dispatch an update to the store and ensure the panel doesn't get
  // attributes.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));
}

/**
 * Tests that updates to the store unrelated to bulk pinning OR any duplicate
 * updates do not get passed onto the underlying element.
 */
export async function testOtherStoreUpdatesDontCauseThisContainerToUpdate() {
  // Initialize the store with bulk pinning pref enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Start using a basic syncing state.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 150,
    filesToPin: 24,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 24,
  };

  // Dispatch an update to the store and ensure the panel does get attributes.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '24');
  assertEquals(panel!.getAttribute('percentage'), '15');

  // Update the preferences with new values and ensure the `setAttribute` call
  // hasn't happened again and the attributes remain the same.
  store.dispatch(updatePreferences({...PREFERENCES, arcEnabled: true}));
  assertEquals(
      container!.updates, 1,
      'Preferences state change should not increment cloud panel updates');
  assertEquals(panel!.getAttribute('items'), '24');
  assertEquals(panel!.getAttribute('percentage'), '15');

  // Update the value for `pinnedBytes` and dispatch to the store then assert
  // more invocations have occurred and the attributes have updated.
  store.dispatch(updateBulkPinProgress({...bulkPinning, pinnedBytes: 300}));
  assertEquals(
      container!.updates, 2,
      'Bulk pin state change should increment updates to 2');
  assertEquals(panel!.getAttribute('items'), '24');
  assertEquals(panel!.getAttribute('percentage'), '30');

  // Update the bulk pin progress with the exact same data as before, this
  // should not cause another update and the attributes should not be changed.
  store.dispatch(updateBulkPinProgress({...bulkPinning, pinnedBytes: 300}));
  assertEquals(container!.updates, 2, 'Bulk pin state should not be changed');
  assertEquals(panel!.getAttribute('items'), '24');
  assertEquals(panel!.getAttribute('percentage'), '30');
}

/**
 * Tests that when there are no bytes to be pinned, the percente should be
 * updated to be 100% as any new user who logs in with no new changes will have
 * no bytes to pin on initialization.
 */
export async function testZeroBytesToPinShouldShowAllFilesSynced() {
  // Initialize the store with bulk pinning pref enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 0% done with 0 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 0,
    pinnedBytes: 0,
    filesToPin: 0,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 0,
  };

  // Dispatch an update to the store and wait for the panel to have the
  // attribute `items`.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  await waitUntil(() => panel!.hasAttribute('items'));

  // Assert the items and progress values are representative of the data in the
  // store.
  assertEquals(panel!.getAttribute('items'), '0');
  assertEquals(panel!.getAttribute('percentage'), '100');
}

/**
 * Tests that when a click event is emitted, the correct subpage in settings is
 * opened.
 */
export async function testWhenSettingsClickEventEmittedSettingsSubpageOpened() {
  // Mock the fileManagerPrivate API.
  let pageRequested: string|null = null;
  chrome.fileManagerPrivate.openSettingsSubpage = (page: string) => {
    pageRequested = page;
  };

  // Dispatch an event from the cloud panel indicating the drive settings was
  // clicked.
  panel!.dispatchEvent(
      new CustomEvent(XfCloudPanel.events.DRIVE_SETTINGS_CLICKED, {
        bubbles: true,
        composed: true,
      }));

  // Wait until the page has called the private API and assert it's the correct
  // page.
  await waitUntil(() => pageRequested !== null);
  assertEquals(pageRequested, 'googleDrive');
}

/**
 * Tests that the element doesn't receive updates when the preference is
 * disabled, after enabling the preference updates should propagate through.
 */
export async function
testInProgressStateDoesNotUpdateThePanelWhenPrefDisabled() {
  // Initialize the store with bulk pinning disabled.
  const store = getStore();
  store.init({
    ...getEmptyState(),
    preferences: {...PREFERENCES, driveFsBulkPinningEnabled: false},
  });

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 100,
    filesToPin: 10,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 10,
  };

  // Dispatch an update to the store, wait for the store to update before
  // asserting that the panel doesn't get attributes due to the pref not being
  // enabled.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  await waitDeepEquals(
      store, BulkPinStage.SYNCING,
      (state) => state.bulkPinning && state.bulkPinning!.stage);
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));
  assertEquals(container!.updates, 0);

  // Enable the bulk pinning preference and wait for it to update the cloud
  // panel attributes.
  store.dispatch(updatePreferences({...PREFERENCES}));
  await waitUntil(() => container!.updates === 1);
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '10');
}

/**
 * Tests that updating the syncing stage to offline adds the type attribute and
 * going back to syncing (i.e. back online) removes the type attribute.
 */
export async function
testPausedStateAddsTypeAttributeAndSyncingRemovesAttribute() {
  // Initialize the store with bulk pinning enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 100,
    filesToPin: 10,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 10,
  };

  // Dispatch an update to the store and ensure the panel does get attributes.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '10');

  // Pausing the bulk pinning operation does not update the attributes except
  // changing the type attribute to offline.
  store.dispatch(updateBulkPinProgress(
      {...bulkPinning, pinnedBytes: 200, stage: BulkPinStage.PAUSED_OFFLINE}));
  assertEquals(
      container!.updates, 2,
      'Bulk pin state stage should increment updates to 2');
  assertEquals(panel!.getAttribute('type'), 'offline');
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));
  store.dispatch(updateBulkPinProgress({
    ...bulkPinning,
    pinnedBytes: 200,
    stage: BulkPinStage.PAUSED_BATTERY_SAVER,
  }));
  assertEquals(
      container!.updates, 3,
      'Bulk pin state stage should increment updates to 3');
  assertEquals(panel!.getAttribute('type'), 'battery_saver');
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));

  // Switching back into `SYNCING` with new pinned bytes removes the type
  // attribute and updates the attributes.
  store.dispatch(updateBulkPinProgress({...bulkPinning, pinnedBytes: 300}));
  assertEquals(
      container!.updates, 4,
      'Bulk pin state stage should increment updates to 4');
  assertFalse(panel!.hasAttribute('type'));
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '30');
}

/**
 * Tests that updating the syncing stage to not enough space adds the type
 * attribute and goes back to syncing (i.e. user has started it again) removes
 * the type attribute.
 */
export async function
testNotEnoughSpaceStateAddsTypeAttributeAndSyncingRemovesAttribute() {
  // Initialize the store with bulk pinning enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 100,
    filesToPin: 10,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 10,
  };

  // Dispatch an update to the store and ensure the panel does get attributes.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '10');

  // Entering into a not enough space state ensures the type is updated and the
  // items and percentage attributes are removed.
  store.dispatch(updateBulkPinProgress({
    ...bulkPinning,
    pinnedBytes: 200,
    stage: BulkPinStage.NOT_ENOUGH_SPACE,
  }));
  assertEquals(
      container!.updates, 2,
      'Bulk pin state stage should increment updates to 2');
  assertEquals(panel!.getAttribute('type'), 'not_enough_space');
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));

  // Switching back into `SYNCING` with new pinned bytes removes the type
  // attribute and updates the attributes.
  store.dispatch(updateBulkPinProgress({...bulkPinning, pinnedBytes: 300}));
  assertEquals(
      container!.updates, 3,
      'Bulk pin state stage should increment updates to 3');
  assertFalse(panel!.hasAttribute('type'));
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '30');
}

/**
 * Test that any existing properties are removed when moving to the listing
 * files stage.
 */
export async function testExistingPropertiesAreRemovedOnSubsequentSyncds() {
  // Initialize the store with bulk pinning enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 1000,
    pinnedBytes: 100,
    filesToPin: 10,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 10,
  };

  // Dispatch an update to the store and ensure the panel does get attributes.
  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '10');
  assertEquals(panel!.getAttribute('percentage'), '10');

  // Dispatch an update to the store to move back to the listing files stage,
  // this should clear the percentage attribute.
  store.dispatch(updateBulkPinProgress({
    ...bulkPinning,
    stage: BulkPinStage.LISTING_FILES,
    pinnedBytes: 0,
  }));
  assertEquals(
      container!.updates, 2,
      'Bulk pin state change should increment updates to 2');
  assertEquals(panel!.getAttribute('items'), '10');
  assertFalse(panel!.hasAttribute('percentage'));
}

/**
 * Tests that if the user has any files to pin but no bytes (i.e. has ONLY
 * 0-byte files) the percentage is also attached (a pre-requisite to show the
 * File sync is on page).
 */
export async function testNoBytesToPinButHasFilesAddsPercentage() {
  // Initialize the store with bulk pinning enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 0,
    pinnedBytes: 0,
    filesToPin: 1,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 1,
  };

  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '1');
  assertEquals(panel!.getAttribute('seconds'), '0');
  assertEquals(panel!.getAttribute('percentage'), '100');
}

/**
 * Tests that a metered network update to the store passes the state down to the
 * cloud panel.
 */
export async function testMeteredNetworkState() {
  // Initialize the store with bulk pinning enabled.
  const store = getStore();
  store.init({...getEmptyState(), preferences: PREFERENCES});

  // Setup a syncing state that should be 10% done with 10 items.
  const bulkPinning: BulkPinProgress = {
    stage: BulkPinStage.SYNCING,
    freeSpaceBytes: 0,
    requiredSpaceBytes: 0,
    bytesToPin: 0,
    pinnedBytes: 0,
    filesToPin: 1,
    remainingSeconds: 0,
    shouldPin: true,
    emptiedQueue: false,
    listedFiles: 1,
  };

  store.dispatch(updateBulkPinProgress(bulkPinning));
  assertEquals(
      container!.updates, 1,
      'Bulk pin state change should increment updates to 1');
  assertEquals(panel!.getAttribute('items'), '1');
  assertEquals(panel!.getAttribute('seconds'), '0');
  assertEquals(panel!.getAttribute('percentage'), '100');

  // Entering into a not enough space state ensures the type is updated and the
  // items and percentage attributes are removed.
  store.dispatch(updateDriveConnectionStatus({
    type: chrome.fileManagerPrivate.DriveConnectionStateType.METERED,
  }));
  assertEquals(
      container!.updates, 2,
      'Bulk pin state stage should increment updates to 2');
  assertEquals(panel!.getAttribute('type'), 'metered_network');
  assertFalse(panel!.hasAttribute('items'));
  assertFalse(panel!.hasAttribute('percentage'));
}