chromium/ui/file_manager/file_manager/containers/directory_tree_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 {assert} from 'chrome://resources/ash/common/assert.js';
import {assertArrayEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';

import {MockVolumeManager} from '../background/js/mock_volume_manager.js';
import {installMockChrome} from '../common/js/mock_chrome.js';
import type {MockFileSystem} from '../common/js/mock_entry.js';
import {waitUntil} from '../common/js/test_error_reporting.js';
import {waitForElementUpdate} from '../common/js/unittest_util.js';
import {VolumeType} from '../common/js/volume_manager_types.js';
import {addAndroidApps} from '../state/ducks/android_apps.js';
import {updateMaterializedViews} from '../state/ducks/materialized_views.js';
import {addVolume, removeVolume} from '../state/ducks/volumes.js';
import {createFakeVolumeMetadata, setUpFileManagerOnWindow, setupStore} from '../state/for_tests.js';
import {getStore} from '../state/store.js';
import type {XfTree} from '../widgets/xf_tree.js';

import {DirectoryTreeContainer} from './directory_tree_container.js';

/** The directory tree container instance */
let directoryTreeContainer: DirectoryTreeContainer;

/** To store all the directory changed listener. */
let directoryChangedListeners: Array<(event: any) => void>;

export function setUp() {
  // DirectoryModel and VolumeManager are required for DirectoryTreeContainer.
  setUpFileManagerOnWindow();
  // Mock file manager private API.
  directoryChangedListeners = [];
  const mockChrome = {
    fileManagerPrivate: {
      onDirectoryChanged: {
        addListener: (listener: (event: any) => void) => {
          directoryChangedListeners.push(listener);
        },
      },
    },
  };
  installMockChrome(mockChrome);
  // Initialize directory tree container.
  const {directoryModel} = window.fileManager;
  window.store = null;
  setupStore();
  directoryTreeContainer =
      new DirectoryTreeContainer(document.body, directoryModel);
}

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

/**
 * Returns the directory tree item labels.
 */
function getDirectoryTreeItemLabels(directoryTree: XfTree): string[] {
  const labels = [];
  for (const item of directoryTree.items) {
    labels.push(item.label);
  }
  return labels;
}

/**
 * Dispatch MyFiles and Drive volumes data to the store.
 */
async function addMyFilesAndDriveVolumes():
    Promise<{myFilesFs: MockFileSystem, driveFs: MockFileSystem}> {
  const {volumeManager} = window.fileManager;
  const store = getStore();
  // Prepare data for MyFiles.
  const downloadsVolumeInfo =
      volumeManager.getCurrentProfileVolumeInfo(VolumeType.DOWNLOADS)!;
  store.dispatch(addVolume(
      downloadsVolumeInfo, createFakeVolumeMetadata(downloadsVolumeInfo)));
  // Prepare data for Drive.
  const driveVolumeInfo =
      volumeManager.getCurrentProfileVolumeInfo(VolumeType.DRIVE)!;
  await driveVolumeInfo.resolveDisplayRoot();
  store.dispatch(
      addVolume(driveVolumeInfo, createFakeVolumeMetadata(driveVolumeInfo)));

  return {
    myFilesFs: downloadsVolumeInfo.fileSystem as MockFileSystem,
    driveFs: driveVolumeInfo.fileSystem as MockFileSystem,
  };
}

/**
 * Add MyFiles children and trigger file watcher change event.
 */
function addMyFilesChildren(parentEntry: Entry, childEntries: string[]) {
  const {volumeManager} = window.fileManager;
  const downloadsVolumeInfo =
      volumeManager.getCurrentProfileVolumeInfo(VolumeType.DOWNLOADS)!;
  const myFilesFs = downloadsVolumeInfo.fileSystem as MockFileSystem;
  myFilesFs.populate(childEntries);

  // Trigger file watcher event.
  const event = {
    entry: parentEntry,
    eventType: 'changed',
  };
  for (const listener of directoryChangedListeners) {
    listener(event);
  }
}


/**
 * Add Drive children and trigger file watcher change event.
 */
function addDriveChildren(parentEntry: Entry, childEntries: string[]) {
  const {volumeManager} = window.fileManager;
  const driveVolumeInfo =
      volumeManager.getCurrentProfileVolumeInfo(VolumeType.DRIVE)!;
  const driveFs = driveVolumeInfo.fileSystem as MockFileSystem;
  driveFs.populate(childEntries);

  // Trigger file watcher event.
  const event = {
    entry: parentEntry,
    eventType: 'changed',
  };
  for (const listener of directoryChangedListeners) {
    listener(event);
  }
}

/**
 * Add Android app navigation data to the store.
 */
function addAndroidAppToStore() {
  const store = getStore();
  store.dispatch(addAndroidApps({
    apps: [
      {
        name: 'App 1',
        packageName: 'com.test.app1',
        activityName: 'Activity1',
        iconSet: {icon16x16Url: 'url1', icon32x32Url: 'url2'},
      },
    ],
  }));
}

/**
 * Test case for typical creation of directory tree.
 * This test expects that the following tree is built.
 *
 * MyFiles
 * Google Drive
 * - My Drive
 * - Shared with me
 * - Offline
 */
export async function testCreateDirectoryTree() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  await waitUntil(() => {
    // Under the drive item, there exist 3 entries.
    return driveItem.items.length === 3;
  });
  // There exist 1 my drive entry and 2 fake entries under the drive item.
  assertEquals('My Drive', driveItem.items[0]!.label);
  assertEquals('Shared with me', driveItem.items[1]!.label);
  assertEquals('Offline', driveItem.items[2]!.label);
}

/**
 * Test case for creating tree with Team Drives.
 * This test expects that the following tree is built.
 *
 * MyFiles
 * Google Drive
 * - My Drive
 * - Team Drives (only if there is a child team drive).
 * - Shared with me
 * - Offline
 */
export async function testCreateDirectoryTreeWithTeamDrive() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();
  // Add Team Drives.
  addDriveChildren(driveFs.entries['/team_drives']!, ['/team_drives/a/']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  // Expand Drive before checking Team drives.
  driveItem.expanded = true;
  await waitUntil(() => {
    // Under the drive item, there exist 4 entries.
    return driveItem.items.length === 4;
  });
  // There exist 1 my drive entry and 3 fake entries under the drive item.
  assertEquals('My Drive', driveItem.items[0]!.label);
  assertEquals('Shared drives', driveItem.items[1]!.label);
  assertEquals('Shared with me', driveItem.items[2]!.label);
  assertEquals('Offline', driveItem.items[3]!.label);
}

/**
 * Test case for creating tree with empty Team Drives.
 * The Team Drives subtree should be removed if the user has no team drives.
 */
export async function testCreateDirectoryTreeWithEmptyTeamDrive() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  // Expand Drive before checking Team drives.
  driveItem.expanded = true;
  await waitUntil(() => {
    // Root entries under Drive volume is generated, Team Drives isn't
    // included because it has no child.
    // See testCreateDirectoryTreeWithTeamDrive for detail.
    return driveItem.items.length === 3;
  });
  let teamDrivesItemFound = false;
  for (let i = 0; i < driveItem.items.length; i++) {
    if (driveItem.items[i]!.label === 'Shared drives') {
      teamDrivesItemFound = true;
      break;
    }
  }
  assertFalse(teamDrivesItemFound, 'Team Drives should NOT be generated');
}

/**
 * Test case for creating tree with Computers.
 * This test expects that the following tree is built.
 *
 * MyFiles
 * Google Drive
 * - My Drive
 * - Computers (only if there is a child computer).
 * - Shared with me
 * - Offline
 */
export async function testCreateDirectoryTreeWithComputers() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();
  // Add Computers.
  addDriveChildren(driveFs.entries['/Computers']!, ['/Computers/My Laptop/']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  // Expand Drive before checking Computers.
  driveItem.expanded = true;
  await waitUntil(() => {
    // Under the drive item, there exist 4 entries.
    return driveItem.items.length === 4;
  });
  // There exist 1 my drive entry and 3 fake entries under the drive item.
  assertEquals('My Drive', driveItem.items[0]!.label);
  assertEquals('Computers', driveItem.items[1]!.label);
  assertEquals('Shared with me', driveItem.items[2]!.label);
  assertEquals('Offline', driveItem.items[3]!.label);
}

/**
 * Test case for creating tree with empty Computers.
 * The Computers subtree should be removed if the user has no computers.
 */
export async function testCreateDirectoryTreeWithEmptyComputers() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  // Expand Drive before checking Computers.
  driveItem.expanded = true;
  await waitUntil(() => {
    // Root entries under Drive volume is generated, Computers isn't
    // included because it has no child.
    // See testCreateDirectoryTreeWithComputers for detail.
    return driveItem.items.length === 3;
  });
  let computersItemFound = false;
  for (let i = 0; i < driveItem.items.length; i++) {
    if (driveItem.items[i]!.label === 'Computers') {
      computersItemFound = true;
      break;
    }
  }
  assertFalse(computersItemFound, 'Computers should NOT be generated');
}

/**
 * Test case for creating tree with Team Drives & Computers.
 * This test expects that the following tree is built.
 *
 * MyFiles
 * Google Drive
 * - My Drive
 * - Team Drives
 * - Computers
 * - Shared with me
 * - Offline
 */
export async function testCreateDirectoryTreeWithTeamDrivesAndComputers() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();
  // Add Team drives.
  addDriveChildren(driveFs.entries['/team_drives']!, ['/team_drives/a/']);
  // Add Computers.
  addDriveChildren(driveFs.entries['/Computers']!, ['/Computers/My Laptop/']);
  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  assertEquals('Downloads', myFilesItem.label);
  assertEquals('Google Drive', driveItem.label);

  // Expand Drive before checking Team drives and Computers.
  driveItem.expanded = true;
  await waitUntil(() => {
    // Under the drive item, there exist 5 entries.
    return driveItem.items.length === 5;
  });
  // There exist 1 my drive entry and 4 fake entries under the drive item.
  assertEquals('My Drive', driveItem.items[0]!.label);
  assertEquals('Shared drives', driveItem.items[1]!.label);
  assertEquals('Computers', driveItem.items[2]!.label);
  assertEquals('Shared with me', driveItem.items[3]!.label);
  assertEquals('Offline', driveItem.items[4]!.label);
}

/**
 * Test case for setting separator property.
 *
 * 'separator' property is used to display a line divider between
 * "sections" in the directory tree.
 *
 * This test expects that the following tree is built.
 *
 * MyFiles
 * Google Drive
 * Android app 1
 */
export async function testSeparatorInNavigationSections() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();
  // Add Android apps.
  addAndroidAppToStore();

  // At top level, MyFiles, Drive and Android app should be listed.
  await waitUntil(() => directoryTree.items.length === 3);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;
  const androidAppItem = directoryTree.items[2]!;

  // First element should not have separator property, to not display a
  // division line in the first section.
  // Downloads:
  assertFalse(myFilesItem.separator);

  // Drive should have separator, because it's a new section but not the
  // first section.
  assertTrue(driveItem.separator);

  // Android app should have separator, because it's a new section but not the
  // first section.
  assertTrue(androidAppItem.separator);
}

/**
 * Test case for directory update with volume changes.
 *
 * Mounts/unmounts removable and archive volumes, and checks these volumes come
 * up to/disappear from the list correctly.
 */
export async function testDirectoryTreeUpdateWithVolumeChanges() {
  const directoryTree = directoryTreeContainer.tree;
  const store = getStore();

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // There are 2 volumes, MyFiles and Drive, at first.
  await waitUntil(() => directoryTree.items.length === 2);
  assertArrayEquals(
      [
        'Downloads',
        'Google Drive',
      ],
      getDirectoryTreeItemLabels(directoryTree));

  // Mounts a removable volume.
  const {volumeManager} = window.fileManager;
  const removableVolumeInfo = MockVolumeManager.createMockVolumeInfo(
      VolumeType.REMOVABLE, 'removable', 'Removable 1');
  volumeManager.volumeInfoList.add(removableVolumeInfo);
  store.dispatch(addVolume(
      removableVolumeInfo, createFakeVolumeMetadata(removableVolumeInfo)));

  // Asserts that a removable directory is added after the update.
  await waitUntil(() => directoryTree.items.length === 3);
  assertArrayEquals(
      [
        'Downloads',
        'Google Drive',
        'Removable 1',
      ],
      getDirectoryTreeItemLabels(directoryTree));

  // Mounts an archive volume.
  const archiveVolumeInfo = MockVolumeManager.createMockVolumeInfo(
      VolumeType.ARCHIVE, 'archive', 'Archive 1');
  volumeManager.volumeInfoList.add(archiveVolumeInfo);
  store.dispatch(addVolume(
      archiveVolumeInfo, createFakeVolumeMetadata(archiveVolumeInfo)));

  // Asserts that an archive directory is added before the removable
  await waitUntil(() => directoryTree.items.length === 4);
  assertArrayEquals(
      [
        'Downloads',
        'Google Drive',
        'Removable 1',
        'Archive 1',
      ],
      getDirectoryTreeItemLabels(directoryTree));

  // Deletes an archive directory.
  store.dispatch(removeVolume(archiveVolumeInfo.volumeId));

  // Asserts that an archive directory is deleted.
  await waitUntil(() => directoryTree.items.length === 3);
  assertArrayEquals(
      [
        'Downloads',
        'Google Drive',
        'Removable 1',
      ],
      getDirectoryTreeItemLabels(directoryTree));
}

/**
 * Test case for directory tree with disabled Android files.
 *
 * If some of the volumes under MyFiles are disabled, this should be reflected
 * in the directory model as well: the children shouldn't be loaded, and
 * clicking on the item or the expand icon shouldn't do anything.
 */
export async function testDirectoryTreeWithAndroidDisabled() {
  const store = getStore();
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  await waitUntil(() => directoryTree.items.length === 2);

  // Create Android 'Play files' volume and set as disabled.
  const {volumeManager} = window.fileManager;
  const androidVolumeInfo = MockVolumeManager.createMockVolumeInfo(
      VolumeType.ANDROID_FILES, 'android_files:droid', 'Android files');
  volumeManager.volumeInfoList.add(androidVolumeInfo);
  volumeManager.isDisabled = (volumeType) => {
    return volumeType === VolumeType.ANDROID_FILES;
  };
  store.dispatch(addVolume(
      androidVolumeInfo, createFakeVolumeMetadata(androidVolumeInfo)));

  // Asserts that MyFiles should have 1 child: Android files.
  const myFilesItem = directoryTree.items[0]!;
  await waitUntil(() => myFilesItem.items.length === 1);
  const androidItem = myFilesItem.items[0]!;
  assertEquals('Android files', androidItem.label);
  assertTrue(androidItem.disabled);

  // Clicking on the item shouldn't select it.
  androidItem.click();
  await waitForElementUpdate(androidItem);
  assertFalse(androidItem.selected);

  // The item shouldn't be expanded.
  assertFalse(androidItem.expanded);
  // Clicking on the expand icon shouldn't expand.
  const expandIcon =
      androidItem.shadowRoot!.querySelector<HTMLSpanElement>('.expand-icon')!;
  expandIcon.click();
  await waitForElementUpdate(androidItem);
  assertFalse(androidItem.expanded);
}

/**
 * Test case for directory tree with disabled removable volume.
 *
 * If removable volumes are disabled, they should be allowed to mount/unmount,
 * but cannot be selected or expanded.
 */
export async function testDirectoryTreeWithRemovableDisabled() {
  const store = getStore();
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  await waitUntil(() => directoryTree.items.length === 2);

  // Create removable volume and set as disabled.
  const {volumeManager} = window.fileManager;
  const removableVolumeInfo = MockVolumeManager.createMockVolumeInfo(
      VolumeType.REMOVABLE, 'removable', 'Removable');
  volumeManager.volumeInfoList.add(removableVolumeInfo);
  volumeManager.isDisabled = (volumeType) => {
    return volumeType === VolumeType.REMOVABLE;
  };
  store.dispatch(addVolume(
      removableVolumeInfo, createFakeVolumeMetadata(removableVolumeInfo)));

  // Asserts that a removable directory is added after the update.
  await waitUntil(() => directoryTree.items.length === 3);
  const removableItem = directoryTree.items[2]!;
  assertEquals('Removable', removableItem.label);
  assertTrue(removableItem.disabled);

  // Clicking on the item shouldn't select it.
  removableItem.click();
  await waitForElementUpdate(removableItem);
  assertFalse(removableItem.selected);

  // The item shouldn't be expanded.
  assertFalse(removableItem.expanded);
  // Clicking on the expand icon shouldn't expand.
  const expandIcon =
      removableItem.shadowRoot!.querySelector<HTMLSpanElement>('.expand-icon')!;
  expandIcon.click();
  await waitForElementUpdate(removableItem);
  assertFalse(removableItem.expanded);

  // Unmount the removable and assert that it's removed from the directory.
  store.dispatch(removeVolume(removableVolumeInfo.volumeId));
  await waitUntil(() => directoryTree.items.length === 2);
}

/**
 * Test a disabled drive volume.
 *
 * If drive is disabled, this should be reflected in the directory model as
 * well: the children shouldn't be loaded, and clicking on the item or the
 * expand icon shouldn't do anything.
 */
export async function testDirectoryTreeWithDriveDisabled() {
  const directoryTree = directoryTreeContainer.tree;

  // Disable Drive volume before adding it.
  const {volumeManager} = window.fileManager;
  volumeManager.isDisabled = (volumeType) => {
    return volumeType === VolumeType.DRIVE;
  };

  // Add MyFiles and Drive to the store.
  const {myFilesFs, driveFs} = await addMyFilesAndDriveVolumes();
  // Add sub folders for them.
  addMyFilesChildren(myFilesFs.entries['/']!, ['/folder1/']);
  addDriveChildren(driveFs.entries['/root']!, ['/root/folder1/']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  await waitUntil(() => {
    // Under the drive item, there exist 0 entries. In MyFiles should
    // exist 1 entry folder1.
    return driveItem.items.length === 0 && myFilesItem.items.length === 1;
  });

  assertEquals('Google Drive', driveItem.label);
  assertTrue(driveItem.disabled);

  // Clicking on the item shouldn't select it.
  driveItem.click();
  await waitForElementUpdate(driveItem);
  assertFalse(driveItem.selected);

  // The item shouldn't be expanded.
  const treeItemElement = driveItem.shadowRoot!.querySelector('li')!;
  const expandIconElement =
      driveItem.shadowRoot!.querySelector<HTMLSpanElement>('.expand-icon')!;
  let isExpanded = treeItemElement.getAttribute('aria-expanded') || 'false';
  assertEquals('false', isExpanded);
  // Clicking on the expand icon shouldn't expand.
  expandIconElement.click();
  await waitForElementUpdate(driveItem);
  isExpanded = treeItemElement.getAttribute('aria-expanded') || 'false';
  assertEquals('false', isExpanded);
}

/**
 * Test adding the first team drive for a user.
 * Team Drives subtree should be shown after the change notification is
 * delivered.
 */
export async function testAddFirstTeamDrive() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const driveItem = directoryTree.items[1]!;

  // Drive should include My Drive, Shared with me, Offline.
  await waitUntil(() => {
    return driveItem.items.length === 3;
  });

  // Add one folder to the Team drives.
  addDriveChildren(driveFs.entries['/team_drives']!, ['/team_drives/a/']);
  // Expand Drive before checking Team drives.
  driveItem.expanded = true;
  await waitUntil(() => {
    return driveItem.items.length === 4 &&
        driveItem.items[1]!.label === 'Shared drives';
  });
}

/**
 * Test removing the last team drive for a user.
 * Team Drives subtree should be removed after the change notification is
 * delivered.
 */
export async function testRemoveLastTeamDrive() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();
  // Add one entry under Team Drives.
  addDriveChildren(driveFs.entries['/team_drives']!, ['/team_drives/a/']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const driveItem = directoryTree.items[1]!;

  // Expand Drive before checking Team drives .
  driveItem.expanded = true;
  // Drive should include My Drive, Team drives, Shared with me, Offline.
  await waitUntil(() => {
    return driveItem.items.length === 4;
  });

  // Remove the only child from Team drives.
  await new Promise<void>(resolve => {
    driveFs.entries['/team_drives/a']!.remove(resolve);
  });

  const event = {
    entry: driveFs.entries['/team_drives'],
    eventType: 'changed',
  };
  for (const listener of directoryChangedListeners) {
    listener(event);
  }
  // Wait Team drives grand root to disappear.
  await waitUntil(() => {
    for (let i = 0; i < driveItem.items.length; i++) {
      if (driveItem.items[i]!.label === 'Shared drives') {
        return false;
      }
    }
    return true;
  });
}

/**
 * Test adding the first computer for a user.
 * Computers subtree should be shown after the change notification is
 * delivered.
 */
export async function testAddFirstComputer() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const driveItem = directoryTree.items[1]!;

  // Drive should include My Drive, Shared with me, Offline.
  await waitUntil(() => {
    return driveItem.items.length === 3;
  });

  // Test that we initially do not have a Computers item under Drive, and that
  // adding a filesystem "/Computers/My Laptop" results in the Computers item
  // being displayed under Drive.
  addDriveChildren(driveFs.entries['/Computers']!, ['/Computers/My Laptop/']);
  // Expand Drive before checking Computers.
  driveItem.expanded = true;
  await waitUntil(() => {
    return driveItem.items.length === 4 &&
        driveItem.items[1]!.label === 'Computers';
  });
}

/**
 * Test removing the last computer for a user.
 * Computers subtree should be removed after the change notification is
 * delivered.
 */
export async function testRemoveLastComputer() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {driveFs} = await addMyFilesAndDriveVolumes();
  // Add one entry under Computers.
  addDriveChildren(driveFs.entries['/Computers']!, ['/Computers/My Laptop/']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const driveItem = directoryTree.items[1]!;

  // Expand Drive before checking Computers.
  driveItem.expanded = true;
  // Drive should include My Drive, Computers, Shared with me, Offline.
  await waitUntil(() => {
    return driveItem.items.length === 4;
  });

  // Check that removing the local computer "My Laptop" results in the entire
  // "Computers" element being removed, as it has no children.
  await new Promise<void>(resolve => {
    driveFs.entries['/Computers/My Laptop']!.remove(resolve);
  });

  const event = {
    entry: driveFs.entries['/Computers'],
    eventType: 'changed',
  };
  for (const listener of directoryChangedListeners) {
    listener(event);
  }
  // Wait Computers grand root to disappear.
  await waitUntil(() => {
    for (let i = 0; i < driveItem.items.length; i++) {
      if (driveItem.items[i]!.label === 'Computers') {
        return false;
      }
    }
    return true;
  });
}

/**
 * Test adding FSP and SMB.
 * Sub directories should be fetched for FSP, but not for the SMB.
 */
export async function testAddProviderAndSMB() {
  const store = getStore();
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  const volumeManager = window.fileManager.volumeManager as MockVolumeManager;
  // Add a volume representing a non-Smb provider to the mock filesystem.
  const nonSmbProviderVolumeInfo =
      volumeManager.createVolumeInfo(VolumeType.PROVIDED, 'fsp', 'FSP_LABEL');
  volumeManager.volumeInfoList.add(nonSmbProviderVolumeInfo);
  // Add a sub directory to the non-Smb provider.
  const providerFs =
      assert(volumeManager.volumeInfoList.item(2).fileSystem) as MockFileSystem;
  providerFs.populate(['/fsp_child/']);
  store.dispatch(addVolume(
      nonSmbProviderVolumeInfo,
      createFakeVolumeMetadata(nonSmbProviderVolumeInfo)));

  // Add a volume representing an smbfs share to the mock filesystem.
  const smbShareVolumeInfo =
      volumeManager.createVolumeInfo(VolumeType.SMB, 'smbfs', 'SMBFS_LABEL');
  volumeManager.volumeInfoList.add(smbShareVolumeInfo);
  // Add a sub directory to the smbfs.
  const smbFs =
      assert(volumeManager.volumeInfoList.item(3).fileSystem) as MockFileSystem;
  smbFs.populate(['/smbfs_child/']);
  store.dispatch(addVolume(
      smbShareVolumeInfo, createFakeVolumeMetadata(smbShareVolumeInfo)));

  // At top level, MyFiles and Drive, 2 volumes should be listed.
  await waitUntil(() => directoryTree.items.length === 4);

  assertEquals('Downloads', directoryTree.items[0]!.label);
  assertEquals('Google Drive', directoryTree.items[1]!.label);
  assertEquals('SMBFS_LABEL', directoryTree.items[2]!.label);
  assertEquals('FSP_LABEL', directoryTree.items[3]!.label);

  const smbFsItem = directoryTree.items[2]!;
  const smbFsKey = smbFsItem.dataset['navigationKey']!;
  // At first rendering, SMB is marked as mayHaveChildren without any scanning.
  // So it shouldn't have any children in the store.
  assertTrue(smbFsItem.mayHaveChildren);
  assertEquals(0, smbFsItem.items.length);
  assertEquals(0, store.getState().allEntries[smbFsKey]!.children.length);

  const providerItem = directoryTree.items[3]!;
  // Expand it before checking children items.
  providerItem.expanded = true;
  await waitUntil(() => {
    // Under providerItem there should be 1 entry, 'non_smb_child'.
    return providerItem.items.length === 1;
  });
  assertEquals('fsp_child', providerItem.items[0]!.label);
  // Ensure there are no entries under smbItem.
  assertEquals(0, smbFsItem.items.length);

  // After clicking on the SMB, we still don't scan for children, because it
  // already has the canExpand=true.
  smbFsItem.click();
  await waitUntil(() => {
    return smbFsItem.selected &&
        store.getState().allEntries[smbFsKey]!.children.length === 0;
  });

  // Expand SMB volume.
  smbFsItem.expanded = true;
  await waitUntil(() => {
    // Wait until the SMB share item has been updated with its sub
    // directories.
    return smbFsItem.items.length === 1 &&
        // Wait until the SMB file data has children.
        store.getState().allEntries[smbFsKey]!.children.length > 0;
  });
  assertEquals('smbfs_child', smbFsItem.items[0]!.label);
}

/** Test aria-expanded attribute for directory tree item. */
export async function testAriaExpanded() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  const {myFilesFs, driveFs} = await addMyFilesAndDriveVolumes();
  // Add sub folders for them.
  addMyFilesChildren(myFilesFs.entries['/']!, ['/folder1/']);
  addDriveChildren(driveFs.entries['/root']!, ['/root/folder1']);

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  await waitUntil(() => {
    // Under the drive item, there exist 3 entries. In MyFiles should
    // exist 1 entry folder1.
    return driveItem.items.length === 3 && myFilesItem.items.length === 1;
  });

  // MyFiles will be expanded by default.
  assertTrue(myFilesItem.expanded);
  const treeItemElement = myFilesItem.shadowRoot!.querySelector('li')!;
  const expandIconElement =
      myFilesItem.shadowRoot!.querySelector<HTMLSpanElement>('.expand-icon')!;
  const ariaExpanded = treeItemElement.getAttribute('aria-expanded');
  assertTrue(ariaExpanded === 'true');
  // .tree-children should have role="group" otherwise Chromevox doesn't
  // speak the depth level properly.
  const treeChildrenElement =
      myFilesItem.shadowRoot!.querySelector<HTMLUListElement>('.tree-children')!
      ;
  assertEquals('group', treeChildrenElement.getAttribute('role'));
  // Click to collapse MyFiles.
  expandIconElement.click();
  await waitForElementUpdate(myFilesItem);
  assertFalse(myFilesItem.expanded);

  // After clicking on expand-icon, aria-expanded should be set to false.
  assertEquals('false', treeItemElement.getAttribute('aria-expanded'));
}

/** Test aria-description attribute for selected directory tree item. */
export async function testAriaDescription() {
  const directoryTree = directoryTreeContainer.tree;

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  const myFilesItem = directoryTree.items[0]!;
  const driveItem = directoryTree.items[1]!;

  // Select MyFiles and the aria-description should have value.
  const ariaDescription = 'Current directory';
  myFilesItem.selected = true;
  await waitForElementUpdate(myFilesItem);
  assertEquals(ariaDescription, myFilesItem.getAttribute('aria-description'));

  // Select MyDrive.
  driveItem.selected = true;
  await waitForElementUpdate(driveItem);

  // Now the aria-description on Drive should have value.
  assertEquals(ariaDescription, driveItem.getAttribute('aria-description'));
  assertFalse(myFilesItem.hasAttribute('aria-description'));
}

/**
 * Test adding a materialized view causes it to display in the tree.
 */
export async function testAddMaterializedView() {
  const directoryTree = directoryTreeContainer.tree;
  const store = getStore();

  // Add MyFiles and Drive to the store.
  await addMyFilesAndDriveVolumes();

  // At top level, MyFiles and Drive should be listed.
  await waitUntil(() => directoryTree.items.length === 2);
  assertEquals(directoryTree.items[0]!.label, 'Downloads');
  assertEquals(directoryTree.items[1]!.label, 'Google Drive');

  // Add a view.
  store.dispatch(updateMaterializedViews(
      {materializedViews: [{viewId: 1, name: 'test view'}]}));

  // The container should refresh the navigation roots and display the new
  // materialized view.
  await waitUntil(() => directoryTree.items.length === 3);

  assertEquals(directoryTree.items[0]!.label, 'test view');
  assertEquals(directoryTree.items[1]!.label, 'Downloads');
  assertEquals(directoryTree.items[2]!.label, 'Google Drive');

  // Remove view should remove from the tree.
  store.dispatch(updateMaterializedViews({materializedViews: []}));
  await waitUntil(() => directoryTree.items.length === 2);
  assertEquals(directoryTree.items[0]!.label, 'Downloads');
  assertEquals(directoryTree.items[1]!.label, 'Google Drive');
}