chromium/ui/file_manager/integration_tests/file_manager/directory_tree_context_menu.ts

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

import type {ElementObject} from '../prod/file_manager/shared_types.js';
import {addEntries, ENTRIES, EntryType, getCaller, pending, REPEAT_UNTIL_INTERVAL, repeatUntil, RootPath, sendTestMessage, TestEntryInfo, wait} from '../test_util.js';

import {remoteCall} from './background.js';
import {DirectoryTreePageObject} from './page_objects/directory_tree.js';
import {BASIC_DRIVE_ENTRY_SET, COMPLEX_DRIVE_ENTRY_SET, COMPUTERS_ENTRY_SET, SHARED_DRIVE_ENTRY_SET} from './test_data.js';

/**
 * Sets up for directory tree context menu test. In addition to normal setup,
 * we add destination directory.
 */
async function setupForDirectoryTreeContextMenuTest() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Add destination directory.
  await addEntries(['local'], [new TestEntryInfo({
                     type: EntryType.DIRECTORY,
                     targetPath: 'destination',
                     lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
                     nameText: 'destination',
                     sizeText: '--',
                     typeText: 'Folder',
                   })]);
  return appId;
}

const ITEMS_IN_DEST_DIR_BEFORE_PASTE = TestEntryInfo.getExpectedRows([]);

const ITEMS_IN_DEST_DIR_AFTER_PASTE =
    TestEntryInfo.getExpectedRows([new TestEntryInfo({
      type: EntryType.DIRECTORY,
      targetPath: 'photos',
      lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
      nameText: 'photos',
      sizeText: '--',
      typeText: 'Folder',
    })]);

/**
 * Clicks context menu item of id in directory tree.
 *
 * @param path Path of the tree item to trigger context menu.
 * @param id The context menu id.
 */
async function clickDirectoryTreeContextMenuItem(
    appId: string, path: string, id: string) {
  const contextMenu = '#directory-tree-context-menu:not([hidden])';

  const directoryTree = await DirectoryTreePageObject.create(appId);

  // Right click photos directory.
  await directoryTree.showContextMenuForItemByPath(path);

  // Check: context menu item |id| should be shown enabled.
  await remoteCall.waitForElement(
      appId, `${contextMenu} [command="#${id}"]:not([hidden]):not([disabled])`);

  // Click the menu item specified by |id|.
  await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId, [`${contextMenu} [command="#${id}"]`]);
}

/**
 * Navigates to destination directory and test paste operation to check
 * whether the paste operation is done correctly or not. This method does NOT
 * check source entry is deleted or not for cut operation.
 */
async function navigateToDestinationDirectoryAndTestPaste(appId: string) {
  // Navigates to destination directory.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/destination');

  // Confirm files before paste.
  await remoteCall.waitForFiles(
      appId, ITEMS_IN_DEST_DIR_BEFORE_PASTE, {ignoreLastModifiedTime: true});

  // Paste
  await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['body', 'v', true /* ctrl */, false, false]);

  // Confirm the photos directory is pasted correctly.
  await remoteCall.waitForFiles(
      appId, ITEMS_IN_DEST_DIR_AFTER_PASTE, {ignoreLastModifiedTime: true});
}

/**
 * Rename photos directory to specified name by using directory tree.
 * @param useKeyboardShortcut Set to true to use keyboard shortcut instead of
 *     mouse to trigger context menu.
 */
async function renamePhotosDirectoryTo(
    appId: string, newName: string,
    useKeyboardShortcut: boolean): Promise<void> {
  const directoryTree = await DirectoryTreePageObject.create(appId);
  if (useKeyboardShortcut) {
    await directoryTree.triggerRenameWithKeyboardByLabel('photos');
  } else {
    await clickDirectoryTreeContextMenuItem(
        appId, '/Downloads/photos', 'rename');
  }
  await directoryTree.renameItemByLabel('photos', newName);
}

/**
 * Renames directory and confirm current directory is moved to the renamed
 * directory.
 */
async function renameDirectoryFromDirectoryTreeSuccessCase(
    useKeyboardShortcut: boolean) {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await renamePhotosDirectoryTo(appId, 'New photos', useKeyboardShortcut);

  // Confirm that current directory has moved to new folder.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/My files/Downloads/New photos');
}

/**
 * Renames directory and confirms that an alert dialog is shown.
 */
async function renameDirectoryFromDirectoryTreeAndConfirmAlertDialog(
    newName: string) {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await renamePhotosDirectoryTo(appId, newName, false);
  // The folder name is not changed.
  await directoryTree.waitForChildItemByLabel('Downloads', 'photos');

  // Confirm that a dialog is shown.
  await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');
}

/**
 * Creates directory from directory tree.
 */
async function createDirectoryFromDirectoryTree(
    useKeyboardShortcut: boolean, changeCurrentDirectory: boolean) {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  if (changeCurrentDirectory) {
    await directoryTree.navigateToPath('/My files/Downloads/photos');
  } else {
    await directoryTree.expandTreeItemByLabel('Downloads');
  }
  if (useKeyboardShortcut) {
    await remoteCall.callRemoteTestUtil(
        'fakeKeyDown', appId, ['body', 'e', true /* ctrl */, false, false]);
  } else {
    await clickDirectoryTreeContextMenuItem(
        appId, '/Downloads/photos', 'new-folder');
  }
  await directoryTree.renameItemByLabel('New folder', 'test');
  await directoryTree.waitForItemByPath('/Downloads/photos/test');

  // Confirm that current directory is not changed at this timing.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId,
      changeCurrentDirectory ? '/My files/Downloads/photos' :
                               '/My files/Downloads');

  // Confirm that new directory is actually created by navigating to it.
  await directoryTree.navigateToPath('/My files/Downloads/photos/test');
}

/**
 * Checks all visible items in the context menu for directory tree.
 * @param breadcrumbsPath Path based on the entry labels like:
 *     /My files/Downloads/photos to item to be tested with context menu.
 * @param menuStates Mapping each command to it's enabled state.
 * @param rootsMenu True if the item uses #roots-context-menu instead of
 *     #directory-tree-context-menu
 * @param shortcutToPath For shortcuts it navigates to a different breadcrumbs
 *     path, like /My Drive/ShortcutName.
 */
async function checkContextMenu(
    appId: string, breadcrumbsPath: string,
    menuStates: Array<Array<string|boolean>>, rootsMenu?: boolean,
    shortcutToPath?: string) {
  // Navigate to the folder that will test the context menu.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  const query =
      await directoryTree.navigateToPath(breadcrumbsPath, shortcutToPath);

  // Selector for a both context menu used on directory tree, only one should
  // be visible at the time.
  const menuQuery = rootsMenu ?
      '#roots-context-menu:not([hidden]) cr-menu-item:not([hidden])' :
      '#directory-tree-context-menu:not([hidden]) cr-menu-item:not([hidden])';

  // Right click desired item in the directory tree.
  await remoteCall.waitForElement(appId, query);
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil(
          'fakeMouseRightClick', appId, [query]),
      'fakeMouseRightClick failed');

  // Wait for context menu to appear.
  await remoteCall.waitForElement(appId, menuQuery);

  function stateString(state?: boolean|string) {
    return state ? 'enabled' : 'disabled';
  }

  let msg;
  async function isCommandsEnabledAndOrdered() {
    // Grab all commands together and check they are in the expected order and
    // state.
    const actualItems = await remoteCall.queryElements(appId, [menuQuery]);
    let correctCommands = true;
    msg = '\nContext menu in the wrong order/state for: ' + breadcrumbsPath;
    for (let i = 0; i < Math.max(menuStates.length, actualItems.length); i++) {
      let expectedCommand = undefined;
      let expectedState = undefined;
      let actualCommand = undefined;
      let actualState = undefined;
      if (menuStates[i]) {
        expectedCommand = menuStates[i]![0];
        expectedState = menuStates[i]![1];
      }
      if (actualItems[i]) {
        actualCommand = actualItems[i]!.attributes['command'];
        actualState = actualItems[i]!.attributes['disabled'] ? false : true;
      }
      msg += '\n';
      if (expectedCommand !== actualCommand || expectedState !== actualState) {
        correctCommands = false;
      }
      msg += ` index: ${i}`;
      msg += `\n\t expected: ${expectedCommand} ${stateString(expectedState)}`;
      msg += `\n\t      got: ${actualCommand} ${stateString(actualState)}`;
    }
    return correctCommands;
  }

  // Check if commands are the way we expect, otherwise we try one more time.
  if (await isCommandsEnabledAndOrdered()) {
    return;
  }
  console.warn('Menus items did not match (trying again)...\n' + msg);
  await wait(REPEAT_UNTIL_INTERVAL);

  // Try the context menu one more time.
  await remoteCall.waitForElement(appId, query);
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil(
          'fakeMouseRightClick', appId, [query]),
      'fakeMouseRightClick failed');

  // Wait for context menu to appear.
  await remoteCall.waitForElement(appId, menuQuery);

  if (!await isCommandsEnabledAndOrdered()) {
    chrome.test.assertTrue(false, msg);
  }
}

/**
 * Tests copying a directory from directory tree with context menu.
 */
export async function dirCopyWithContextMenu() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'copy');
  await navigateToDestinationDirectoryAndTestPaste(appId);
}

/**
 * Tests copying a directory from directory tree with the keyboard shortcut.
 */
export async function dirCopyWithKeyboard() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');

  // Press Ctrl+C.
  await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['body', 'c', true /* ctrl */, false, false]);
  await navigateToDestinationDirectoryAndTestPaste(appId);
}

/**
 * Tests copying a directory without changing the current directory.
 */
export async function dirCopyWithoutChangingCurrent() {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'copy');
  await navigateToDestinationDirectoryAndTestPaste(appId);
}

/**
 * Tests cutting a directory with the context menu.
 */
export async function dirCutWithContextMenu() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'cut');
  await navigateToDestinationDirectoryAndTestPaste(appId);

  // Confirm that directory tree is updated.
  await directoryTree.waitForItemLostByPath('/Downloads/photos');
}

/**
 * Tests cutting a directory with the keyboard shortcut.
 */
export async function dirCutWithKeyboard() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');

  // Press Ctrl+X.
  await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['body', 'x', true /* ctrl */, false, false]);
  await navigateToDestinationDirectoryAndTestPaste(appId);

  // Confirm that directory tree is updated.
  await directoryTree.waitForItemLostByPath('/Downloads/photos');
}

/**
 * Tests cutting a directory without changing the current directory.
 */
export async function dirCutWithoutChangingCurrent() {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'cut');
  await navigateToDestinationDirectoryAndTestPaste(appId);
  await directoryTree.waitForItemLostByPath('/Downloads/photos');
}

/**
 * Tests pasting into folder with the context menu.
 */
export async function dirPasteWithContextMenu() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const destinationPath = '/Downloads/destination';

  // Copy photos directory as a test data.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['body', 'c', true /* ctrl */, false, false]);
  await directoryTree.navigateToPath(`/My files${destinationPath}`);

  // Confirm files before paste.
  await remoteCall.waitForFiles(
      appId, ITEMS_IN_DEST_DIR_BEFORE_PASTE, {ignoreLastModifiedTime: true});

  await clickDirectoryTreeContextMenuItem(
      appId, destinationPath, 'paste-into-folder');

  // Confirm the photos directory is pasted correctly.
  await remoteCall.waitForFiles(
      appId, ITEMS_IN_DEST_DIR_AFTER_PASTE, {ignoreLastModifiedTime: true});

  // Expand the directory tree.
  await directoryTree.expandTreeItemByPath(destinationPath);

  // Confirm the copied directory is added to the directory tree.
  await directoryTree.waitForItemByPath(`${destinationPath}/photos`);
}

/**
 * Tests pasting into a folder without changing the current directory.
 */
export async function dirPasteWithoutChangingCurrent() {
  const destinationPath = '/Downloads/destination';

  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');
  await directoryTree.focusTree();
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'copy');
  await clickDirectoryTreeContextMenuItem(
      appId, destinationPath, 'paste-into-folder');
  await directoryTree.waitForItemToHaveChildrenByLabel(
      'destination', /* hasChildren= */ true);
  await directoryTree.expandTreeItemByPath(destinationPath);

  // Confirm the copied directory is added to the directory tree.
  await directoryTree.waitForItemByPath(`${destinationPath}/photos`);
}

/**
 * Tests renaming a folder with the context menu.
 */
export async function dirRenameWithContextMenu() {
  return renameDirectoryFromDirectoryTreeSuccessCase(
      false /* do not use keyboard shortcut */);
}

/**
 * Tests that a child folder breadcrumbs is updated when renaming its parent
 * folder. crbug.com/885328.
 */
export async function dirRenameUpdateChildrenBreadcrumbs() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Add child-folder inside /photos/
  await addEntries(['local'], [new TestEntryInfo({
                     type: EntryType.DIRECTORY,
                     targetPath: 'photos/child-folder',
                     lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
                     nameText: 'child-folder',
                     sizeText: '--',
                     typeText: 'Folder',
                   })]);

  // Navigate to child folder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos/child-folder');

  // Rename parent folder.
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'rename');
  await directoryTree.renameItemByLabel('photos', 'photos-new');

  // Confirm that current directory is now My files or /Downloads, because it
  // can't find the previously selected folder /Downloads/photos/child-folder,
  // since its path/parent has been renamed.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/My files');

  // Navigate to child-folder using the new path.
  // |navigateWithDirectoryTree| already checks for breadcrumbs to
  // match the path.
  await directoryTree.navigateToPath(
      '/My files/Downloads/photos-new/child-folder');
}

/**
 * Tests renaming folder with the keyboard shortcut.
 */
export async function dirRenameWithKeyboard() {
  return renameDirectoryFromDirectoryTreeSuccessCase(
      true /* use keyboard shortcut */);
}

/**
 * Tests renaming folder without changing the current directory.
 */
export async function dirRenameWithoutChangingCurrent() {
  const appId = await setupForDirectoryTreeContextMenuTest();
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');
  await directoryTree.waitForItemByPath('/Downloads/photos');
  await renamePhotosDirectoryTo(
      appId, 'New photos', false /* Do not use keyboard shortcut. */);
  await directoryTree.waitForItemByPath('/Downloads/New photos');
}

/**
 * Tests renaming a folder to an empty string.
 */
export async function dirRenameToEmptyString() {
  const appId = await setupForDirectoryTreeContextMenuTest();

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');
  await renamePhotosDirectoryTo(appId, '', false);
  // The folder name is not changed.
  await directoryTree.waitForChildItemByLabel('Downloads', 'photos');

  // No dialog should be shown.
  await remoteCall.waitForElementLost(appId, '.cr-dialog-container.shown');
}

/**
 * Tests renaming folder an existing name.
 */
export async function dirRenameToExisting() {
  return renameDirectoryFromDirectoryTreeAndConfirmAlertDialog('destination');
}

/**
 * Tests renaming removable volume with the keyboard.
 */
export async function dirRenameRemovableWithKeyboard() {
  // Open Files app on local downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Mount a single partition NTFS USB volume: they can be renamed.
  await sendTestMessage({name: 'mountFakeUsb', filesystem: 'ntfs'});

  // Wait for the USB mount and click the USB volume.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByType('removable');

  // Check: the USB should be the currently focused directory tree item.
  await directoryTree.waitForFocusedItemByLabel('fake-usb');
  // Focus the directory tree.
  await directoryTree.focusTree();

  // Check: the USB volume is still the currently focused directory tree item.
  await directoryTree.waitForFocusedItemByLabel('fake-usb');

  // Rename the USB.
  await directoryTree.triggerRenameWithKeyboardByLabel('fake-usb');
  await directoryTree.renameItemByLabel('fake-usb', 'usb-was-renamed');
  await directoryTree.waitForItemByLabel('usb-was-renamed');
}

/**
 * Tests renaming removable volume with the context menu.
 */
export async function dirRenameRemovableWithContentMenu() {
  // Open Files app on local downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Mount a single partition NTFS USB volume: they can be renamed.
  await sendTestMessage({name: 'mountFakeUsb', filesystem: 'ntfs'});

  // Wait for the USB mount and click the USB volume.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByType('removable');

  // Check: the USB should be the currently focused directory tree item.
  await directoryTree.waitForFocusedItemByLabel('fake-usb');

  // Focus the directory tree.
  await directoryTree.focusTree();

  // Check: the USB should be the currently focused directory tree item.
  await directoryTree.waitForFocusedItemByLabel('fake-usb');

  // Right-click the USB volume.
  await directoryTree.showContextMenuForItemByLabel('fake-usb');

  // Check: a context menu with a 'rename' item should appear.
  const renameItem =
      'cr-menu-item[command="#rename"]:not([hidden]):not([disabled])';
  await remoteCall.waitForElement(appId, renameItem);

  // Click the context menu 'rename' item.
  await remoteCall.callRemoteTestUtil('fakeMouseClick', appId, [renameItem]);

  // Rename the USB.
  await directoryTree.renameItemByLabel('fake-usb', 'usb-was-renamed');
  await directoryTree.waitForItemByLabel('usb-was-renamed');
}

/**
 * Tests that opening context menu in the rename input won't commit the
 * renaming.
 */
export async function dirContextMenuForRenameInput() {
  // Open Files app on local downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.photos], []);

  // Navigate to the photos folder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos');

  // Start renaming the photos folder.
  await clickDirectoryTreeContextMenuItem(appId, '/Downloads/photos', 'rename');

  // Rename the item without committing the rename.
  await directoryTree.inputNewNameForItemByLabel('photos', 'NEW NAME');

  // Right click to show the context menu.
  await directoryTree.showContextMenuForRenameInputByLabel('photos');

  // Context menu must be visible.
  const contextMenu = '#text-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenu);

  // Dismiss the context menu.
  const escKey = [contextMenu, 'Escape', false, false, false];
  await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, escKey);

  // Check: The rename input should be still be visible and with the same
  // content.
  const inputElement = await directoryTree.waitForRenameInputByLabel('photos');
  chrome.test.assertEq('NEW NAME', inputElement.value);

  // Check: The rename input should be the focused element.
  const focusedElement =
      await remoteCall.callRemoteTestUtil('getActiveElement', appId, []);
  chrome.test.assertEq(inputElement, focusedElement);
}

/**
 * Tests creating a folder with the context menu.
 */
export async function dirCreateWithContextMenu() {
  return createDirectoryFromDirectoryTree(
      false /* do not use keyboard shortcut */,
      true /* change current directory */);
}

/**
 * Tests creating a folder with the keyboard shortcut.
 */
export async function dirCreateWithKeyboard() {
  return createDirectoryFromDirectoryTree(
      true /* use keyboard shortcut */, true /* change current directory */);
}

/**
 * Tests creating folder without changing the current directory.
 */
export async function dirCreateWithoutChangingCurrent() {
  return createDirectoryFromDirectoryTree(
      false /* Do not use keyboard shortcut */,
      false /* Do not change current directory */);
}

/**
 * Tests the creation of new folders from the directory tree from the context
 * menu. Creates the new folders in random order to ensure directory tree
 * sorting does not break folder renaming. crbug.com/1004717
 */
export async function dirCreateMultipleFolders() {
  const caller = getCaller();

  // Open Files app on local downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello], []);
  const directoryTree = await DirectoryTreePageObject.create(appId);

  const createNewFolder = async (name: string) => {
    // Ctrl+E to create a new folder in downloads.
    await directoryTree.focusTree();
    await directoryTree.selectItemByLabel('Downloads');
    await remoteCall.fakeKeyDown(appId, 'body', 'e', true, false, false);

    // Rename folder.
    await directoryTree.renameItemByLabel('New folder', name);
  };

  const checkDownloadsSubFolders = async (expectedLabels: string[]) => {
    const directoryItems =
        await directoryTree.getChildItemsByParentLabel('Downloads');
    const directoryItemsLabels =
        directoryItems.map(child => directoryTree.getItemLabel(child));

    // Check downloads subfolders are created in sorted order.
    const equalLength = expectedLabels.length === directoryItemsLabels.length;
    for (let i = 0; i < expectedLabels.length; i++) {
      if (!equalLength || expectedLabels[i] !== directoryItemsLabels[i]) {
        return pending(
            caller,
            'Waiting for downloads subfolders to be created in sorted order');
      }
    }
    return undefined;
  };

  // The folders in sorted order would be 111, aaa. Create these
  // folders in random order. crbug.com/1004717
  const names = ['aaa', '111'];
  while (names.length) {
    const index = Math.floor(Math.random() * names.length);
    const [deletedName] = names.splice(index, 1);
    await createNewFolder(deletedName!);
  }

  // Check: the new folders should have been created in the right order.
  await repeatUntil(async () => {
    return checkDownloadsSubFolders(['111', 'aaa']);
  });
}

/**
 * Tests context menu for Recent root, currently it doesn't show context menu.
 */
export async function dirContextMenuRecent() {
  // Open Files app on Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Focus the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.focusTree();

  // Select Recent root.
  await directoryTree.selectItemByLabel('Recent');

  // Wait it to navigate to it.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Recent');

  // Right click Recent root.
  await directoryTree.showContextMenuForItemByLabel('Recent');

  // Check that both menus are still hidden.
  await remoteCall.waitForElement(appId, '#roots-context-menu[hidden]');
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu[hidden]');
}

/**
 * Tests context menu for a ZIP root inside it.
 */
export async function dirContextMenuZip() {
  await sendTestMessage({
    name: 'expectFileTask',
    fileNames: [ENTRIES.zipArchive.targetPath],
    openType: 'launch',
  });

  const zipMenus = [
    ['#unmount', true],
    ['#share-with-linux', true],
  ];

  // Open Files app on Downloads containing a ZIP file.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.zipArchive], []);

  // Select the ZIP file.
  await remoteCall.waitUntilSelected(appId, ENTRIES.zipArchive.nameText);

  // Press the Enter key to mount the ZIP file.
  const key = ['#file-list', 'Enter', false, false, false];
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, key),
      'fakeKeyDown failed');

  // Check the context menu is on desired state.
  await checkContextMenu(appId, '/archive.zip', zipMenus, true /* rootMenu */);

  // checkContextMenu leaves the context menu open, so just click on the eject
  // menu item.
  await remoteCall.waitAndClickElement(
      appId, '#roots-context-menu [command="#unmount"]:not([disabled])');

  // Ensure the archive has been removed.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemLostByLabel(ENTRIES.zipArchive.nameText);
}

/**
 * Tests context menu on the Eject button of a ZIP root.
 */
export async function dirContextMenuZipEject() {
  await sendTestMessage({
    name: 'expectFileTask',
    fileNames: [ENTRIES.zipArchive.targetPath],
    openType: 'launch',
  });

  // Open Files app on Downloads containing a zip file.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.zipArchive], []);

  // Select the ZIP file.
  await remoteCall.waitUntilSelected(appId, ENTRIES.zipArchive.nameText);

  // Press the Enter key to mount the ZIP file.
  const key = ['#file-list', 'Enter', false, false, false];
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, key),
      'fakeKeyDown failed');

  const directoryTree = await DirectoryTreePageObject.create(appId);

  // Focus on the eject button and right click the eject button.
  await directoryTree.showContextMenuForEjectButtonByLabel(
      ENTRIES.zipArchive.nameText);

  // Wait for, and click the eject menu item.
  await remoteCall.waitAndClickElement(
      appId,
      '#roots-context-menu:not([hidden]) ' +
          '[command="#unmount"]:not([disabled])');

  // Ensure the archive has been removed.
  await directoryTree.waitForItemLostByLabel(ENTRIES.zipArchive.nameText);
}

/**
 * Tests context menu for Shortcut roots.
 */
export async function dirContextMenuShortcut() {
  const menus = [
    ['#rename', false],
    ['#unpin-folder', true],
    ['#share-with-linux', true],
  ];
  const entry = ENTRIES.directoryD;
  const entryName = entry.nameText;

  // Open Files app on Drive.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], [entry]);

  // Create a shortcut to directory D.
  await remoteCall.createShortcut(appId, entryName);

  // Check the context menu is on desired state.
  await checkContextMenu(
      appId, `/${entryName}`, menus, true /* rootMenu */,
      `/My Drive/${entryName}`);
}

/**
 * Tests context menu for MyFiles, Downloads and sub-folder.
 */
export async function dirContextMenuMyFilesWithPaste() {
  const myFilesMenus = [
    ['#share-with-linux', true],
    ['#new-folder', true],
  ];
  const downloadsMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#move-to-trash', false],
    ['#new-folder', true],
  ];
  const photosTwoMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#move-to-trash', true],
    ['#new-folder', true],
  ];

  const photosTwo = new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'photosTwo',
    lastModifiedTime: 'Jan 1, 1990, 11:59 PM',
    nameText: 'photosTwo',
    sizeText: '--',
    typeText: 'Folder',
  });

  const photosT = new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'photosT',
    mimeType: 'text/plain',
    lastModifiedTime: 'Jan 1, 1993, 11:59 PM',
    nameText: 'photosT',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  });

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS,
      [ENTRIES.beautiful, ENTRIES.photos, ENTRIES.hello, photosTwo, photosT],
      []);

  {
    // Select and copy photos directory into the clipboard to test
    // paste-into-folder command.
    await remoteCall.waitUntilSelected(appId, ENTRIES.photos.nameText);
    chrome.test.assertTrue(
        !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
        'execCommand failed');

    const photosMenus = [
      ['#cut', true],
      ['#copy', true],
      ['#paste-into-folder', false],
      ['#share-with-linux', true],
      ['#rename', true],
      ['#move-to-trash', true],
      ['#new-folder', true],
    ];

    // Check the context menu is on desired state for MyFiles.
    await checkContextMenu(
        appId, '/My files', myFilesMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads.
    await checkContextMenu(
        appId, '/My files/Downloads', downloadsMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads>photos.
    await checkContextMenu(
        appId, '/My files/Downloads/photos', photosMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads>photosTwo.
    // This used to be a breaking case as photos is a substring of photosTwo,
    // and we would treat photosTwo as a descendant of photos.
    // See crbug.com/1032436.
    await checkContextMenu(
        appId, '/My files/Downloads/photosTwo', photosTwoMenus,
        false /* rootMenu */);
  }

  {
    const directoryTree = await DirectoryTreePageObject.create(appId);
    await directoryTree.navigateToPath('/My files/Downloads');
    // Select and copy photosT file into the clipboard to test
    // paste-into-folder command.
    await remoteCall.waitUntilSelected(appId, 'photosT');
    chrome.test.assertTrue(
        !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
        'execCommand failed');

    const photosMenus = [
      ['#cut', true],
      ['#copy', true],
      ['#paste-into-folder', true],
      ['#share-with-linux', true],
      ['#rename', true],
      ['#move-to-trash', true],
      ['#new-folder', true],
    ];

    // Check the context menu is on desired state for MyFiles.
    await checkContextMenu(
        appId, '/My files', myFilesMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads.
    await checkContextMenu(
        appId, '/My files/Downloads', downloadsMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads>photos.
    await checkContextMenu(
        appId, '/My files/Downloads/photos', photosMenus, false /* rootMenu */);

    // Check the context menu for MyFiles>Downloads>photosTwo.
    await checkContextMenu(
        appId, '/My files/Downloads/photosTwo', photosTwoMenus,
        false /* rootMenu */);
  }
}

/**
 * Tests context menu for MyFiles, Downloads and sub-folder.
 */
export async function dirContextMenuMyFiles() {
  const myFilesMenus = [
    ['#share-with-linux', true],
    ['#new-folder', true],
  ];
  const downloadsMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#move-to-trash', false],
    ['#new-folder', true],
  ];
  const photosMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#move-to-trash', true],
    ['#new-folder', true],
  ];

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.photos], []);

  // Check the context menu is on desired state for MyFiles.
  await checkContextMenu(
      appId, '/My files', myFilesMenus, false /* rootMenu */);

  // Check the context menu for MyFiles>Downloads.
  await checkContextMenu(
      appId, '/My files/Downloads', downloadsMenus, false /* rootMenu */);

  // Check the context menu for MyFiles>Downloads>photos.
  await checkContextMenu(
      appId, '/My files/Downloads/photos', photosMenus, false /* rootMenu */);

  // Right click Linux files (FakeEntry).
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.showContextMenuForItemByLabel('Linux files');

  // Wait a few milliseconds to give menu a chance to display.
  await wait(REPEAT_UNTIL_INTERVAL);

  // Fetch all visible cr-menu's.
  const elements =
      await remoteCall.queryElements(appId, ['cr-menu:not([hidden])']);

  // Check: No context menus should be visible for FakeEntry.
  chrome.test.assertEq(0, elements.length);
}

/**
 * Tests context menu for Crostini real root and a folder inside it.
 */
export async function dirContextMenuCrostini() {
  const linuxMenus = [
    ['#new-folder', true],
  ];
  const folderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', true],
    ['#delete', true],
    ['#new-folder', true],
  ];

  // Add a crostini folder.
  await addEntries(['crostini'], [ENTRIES.photos]);

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  // Select Crostini, because the first right click doesn't show any context
  // menu, just actually mounts crostini converting the tree item from fake to
  // real root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectPlaceholderItemByType('crostini');

  // Wait for the real root to appear.
  await directoryTree.waitForItemByType('crostini');

  // Check the context menu for Linux files.
  await checkContextMenu(
      appId, '/My files/Linux files', linuxMenus, false /* rootMenu */);

  // Check the context menu for a folder in Linux files.
  await checkContextMenu(
      appId, '/My files/Linux files/photos', folderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for ARC++/Play files root and a folder inside it.
 */
export async function dirContextMenuPlayFiles() {
  const playFilesMenus = [
    ['#new-folder', false],
  ];
  const folderMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#delete', false],
    ['#new-folder', true],
  ];

  // Add an Android folder.
  await addEntries(['android_files'], [ENTRIES.directoryDocuments]);

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  // Check the context menu for Play files.
  await checkContextMenu(
      appId, '/My files/Play files', playFilesMenus, false /* rootMenu */);

  // Check the context menu for a folder in Play files.
  await checkContextMenu(
      appId, '/My files/Play files/Documents', folderMenus,
      false /* rootMenu */);
}

/**
 * Tests context menu for USB root (single and multiple partitions).
 */
export async function dirContextMenuUsbs() {
  const ext4UsbMenus = [
    ['#unmount', true],
    ['#format', true],
    ['#rename', false],
    ['#share-with-linux', true],
  ];
  const ntfsUsbMenus = [
    ['#unmount', true],
    ['#format', true],
    ['#rename', true],
    ['#share-with-linux', true],
  ];
  const partitionsRootMenus = [
    ['#unmount', true],
    ['#format', false],
  ];
  const partition1Menus = [
    ['#share-with-linux', true],
    ['#format', true],
    ['#rename', false],
    ['#new-folder', true],
  ];
  const folderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#delete', true],
    ['#new-folder', true],
  ];
  const ext4DeviceMenus = [
    ['#unmount', true],
    ['#erase-device', true],
  ];
  const ext4PartitionMenus = [
    ['#share-with-linux', true],
    ['#format', true],
    ['#rename', false],
    ['#new-folder', true],
  ];
  const ntfsDeviceMenus = [
    ['#unmount', true],
    ['#erase-device', true],
  ];
  const ntfsPartitionMenus = [
    ['#share-with-linux', true],
    ['#format', true],
    ['#rename', true],
    ['#new-folder', true],
  ];
  const deviceMenus = [
    ['#unmount', true],
    ['#erase-device', true],
  ];

  // Mount removable volumes.
  await sendTestMessage({name: 'mountUsbWithPartitions'});
  await sendTestMessage({name: 'mountFakeUsb', filesystem: 'ext4'});

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  if (await remoteCall.isSinglePartitionFormat(appId)) {
    // Check the context menu for single partition drive.
    await checkContextMenu(
        appId, '/FAKEUSB', ext4DeviceMenus, true /* rootMenu */);

    // Check the context menu for single partition ext4 USB.
    await checkContextMenu(
        appId, '/FAKEUSB/fake-usb', ext4PartitionMenus, false /* rootMenu */);

    // Check the context menu for a folder inside a single USB partition.
    await checkContextMenu(
        appId, '/FAKEUSB/fake-usb/A', folderMenus, false /* rootMenu */);

    // Check the context menu for multiple partitions USB (root).
    await checkContextMenu(
        appId, '/Drive Label', deviceMenus, true /* rootMenu */);

    // Check the context menu for multiple partitions USB (actual partition).
    await checkContextMenu(
        appId, '/Drive Label/partition-1', partition1Menus,
        false /* rootMenu */);

    // Check the context menu for a folder inside a partition1.
    await checkContextMenu(
        appId, '/Drive Label/partition-1/A', folderMenus, false /* rootMenu */);

    // Remount the single partition ext4 USB as NTFS
    await sendTestMessage({name: 'unmountUsb'});
    await sendTestMessage({name: 'mountFakeUsb', filesystem: 'ntfs'});

    // Check the context menu for a single partition NTFS USB.
    await checkContextMenu(
        appId, '/FAKEUSB', ntfsDeviceMenus, true /* rootMenu */);

    // Check the context menu for a single partition NTFS USB.
    await checkContextMenu(
        appId, '/FAKEUSB/fake-usb', ntfsPartitionMenus, false /* rootMenu */);
  } else {
    // Check the context menu for single partition ext4 USB.
    await checkContextMenu(
        appId, '/fake-usb', ext4UsbMenus, true /* rootMenu */);

    // Check the context menu for a folder inside a single USB partition.
    await checkContextMenu(
        appId, '/fake-usb/A', folderMenus, false /* rootMenu */);

    // Check the context menu for multiple partitions USB (root).
    await checkContextMenu(
        appId, '/Drive Label', partitionsRootMenus, true /* rootMenu */);

    // Check the context menu for multiple partitions USB (actual partition).
    await checkContextMenu(
        appId, '/Drive Label/partition-1', partition1Menus,
        false /* rootMenu */);

    // Check the context menu for a folder inside a partition1.
    await checkContextMenu(
        appId, '/Drive Label/partition-1/A', folderMenus, false /* rootMenu */);

    // Remount the single partition ext4 USB as NTFS
    await sendTestMessage({name: 'unmountUsb'});
    await sendTestMessage({name: 'mountFakeUsb', filesystem: 'ntfs'});

    // Check the context menu for a single partition NTFS USB.
    await checkContextMenu(
        appId, '/fake-usb', ntfsUsbMenus, true /* rootMenu */);
  }
}

/**
 * Tests context menu for USB root with DCIM folder.
 */
export async function dirContextMenuUsbDcim() {
  const usbMenus = [
    ['#unmount', true],
    ['#format', true],
    ['#rename', false],
    ['#share-with-linux', true],
  ];
  const dcimFolderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#delete', true],
    ['#new-folder', true],
  ];
  const deviceUsbMenus = [
    ['#share-with-linux', true],
    ['#format', true],
    ['#rename', false],
    ['#new-folder', true],
  ];

  // Mount removable volumes.
  await sendTestMessage({name: 'mountFakeUsbDcim'});

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  if (await remoteCall.isSinglePartitionFormat(appId)) {
    // Check the context menu for single partition USB.
    await checkContextMenu(
        appId, '/FAKEUSB/fake-usb', deviceUsbMenus, false /* rootMenu */);

    // Check the context menu for the DCIM folder inside USB.
    await checkContextMenu(
        appId, '/FAKEUSB/fake-usb/DCIM', dcimFolderMenus, false /* rootMenu */);
  } else {
    // Check the context menu for single partition USB.
    await checkContextMenu(appId, '/fake-usb', usbMenus, true /* rootMenu */);

    // Check the context menu for the DCIM folder inside USB.
    await checkContextMenu(
        appId, '/fake-usb/DCIM', dcimFolderMenus, false /* rootMenu */);
  }
}

/*
 * Tests context menu for Mtp root and a folder inside it.
 */
export async function dirContextMenuMtp() {
  const folderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', true],
    ['#delete', true],
    ['#new-folder', true],
  ];

  // Mount fake MTP volume.
  await sendTestMessage({name: 'mountFakeMtp'});

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  // Select Recent root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemToHaveChildrenByLabel(
      'fake-mtp', /* hasChildren= */ true);
  await directoryTree.selectItemByLabel('fake-mtp');

  // Right click on MTP root.
  await directoryTree.showContextMenuForItemByLabel('fake-mtp');

  // Check that both menus are still hidden, since there is not context menu
  // for MTP root.
  await remoteCall.waitForElement(appId, '#roots-context-menu[hidden]');
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu[hidden]');

  // Check the context menu for a folder inside a MTP.
  await checkContextMenu(
      appId, '/fake-mtp/A', folderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for FSP root and a folder inside it.
 */
export async function dirContextMenuFsp() {
  const fspMenus = [
    ['#unmount', true],
  ];
  const folderMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', false],
    ['#delete', false],
    ['#new-folder', false],
  ];

  // Install a FSP.
  const manifest = 'manifest_source_file.json';
  await sendTestMessage({name: 'launchProviderExtension', manifest: manifest});

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  // Check the context menu for FSP root.
  await checkContextMenu(appId, '/Test (1)', fspMenus, true /* rootMenu */);

  // Check the context menu for a folder inside a FSP.
  await checkContextMenu(
      appId, '/Test (1)/folder', folderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for DocumentsProvider root and a folder inside it.
 */
export async function dirContextMenuDocumentsProvider() {
  const folderMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', false],
    ['#delete', false],
    ['#new-folder', false],
  ];

  // Add a DocumentsProvider folder.
  await addEntries(['documents_provider'], [ENTRIES.readOnlyFolder]);

  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);

  // Wait for DocumentsProvider to appear.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemToHaveChildrenByLabel(
      'DocumentsProvider', /* hasChildren= */ true);

  // Select DocumentsProvider root.
  await directoryTree.selectItemByLabel('DocumentsProvider');

  // Wait it to navigate to it.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/DocumentsProvider');

  // Right click DocumentsProvider root.
  await directoryTree.showContextMenuForItemByLabel('DocumentsProvider');

  // Check that both menus are still hidden, because DocumentsProvider root
  // doesn't show any context menu.
  await remoteCall.waitForElement(appId, '#roots-context-menu[hidden]');
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu[hidden]');

  // Check the context menu for a folder inside a DocumentsProvider.
  await checkContextMenu(
      appId, '/DocumentsProvider/Read-Only Folder', folderMenus,
      false /* rootMenu */);
}

/**
 * Tests context menu for My Drive, read-only and read-write folder inside it.
 */
export async function dirContextMenuMyDrive() {
  const myDriveMenus = [
    ['#share-with-linux', true],
    ['#new-folder', true],
  ];
  const readOnlyFolderMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', false],
    ['#pin-folder', true],
    ['#delete', false],
    ['#new-folder', false],
  ];
  const readWriteFolderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#pin-folder', true],
    ['#delete', true],
    ['#new-folder', true],
  ];

  // Open Files App on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], COMPLEX_DRIVE_ENTRY_SET);

  // Select and copy hello.txt into the clipboard to test paste-into-folder
  // command.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
      'execCommand failed');

  // Check that Google Drive is expanded.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemToExpandByLabel('Google Drive');

  // Check the context menu for My Drive root.
  await checkContextMenu(
      appId, '/My Drive', myDriveMenus, false /* rootMenu */);

  // Check the context menu for read-only folder.
  await checkContextMenu(
      appId, '/My Drive/Read-Only Folder', readOnlyFolderMenus,
      false /* rootMenu */);

  // Check the context menu for read+write folder.
  await checkContextMenu(
      appId, '/My Drive/photos', readWriteFolderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for Shared drives grand-root, a read+write shared drive
 * root, a folder inside it, a read-only shared drive and a folder inside
 * it.
 */
export async function dirContextMenuSharedDrive() {
  const sharedDriveGrandRootMenus = [
    ['#share-with-linux', true],
  ];
  const readWriteSharedDriveRootMenus = [
    ['#cut', false],
    ['#copy', false],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#rename', false],
    ['#delete', true],
    ['#new-folder', true],
  ];
  const readWriteFolderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#rename', true],
    ['#pin-folder', true],
    ['#delete', true],
    ['#new-folder', true],
  ];
  const readOnlySharedDriveRootMenus = [
    ['#cut', false],
    ['#copy', false],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', false],
    ['#delete', false],
    ['#new-folder', false],
  ];
  const readOnlyFolderMenus = [
    ['#cut', false],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#share-with-linux', true],
    ['#rename', false],
    ['#pin-folder', true],
    ['#delete', false],
    ['#new-folder', false],
  ];

  // Open Files App on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], SHARED_DRIVE_ENTRY_SET);

  // Select and copy hello.txt into the clipboard to test paste-into-folder
  // command.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
      'execCommand failed');

  // Check that Google Drive is expanded.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemToExpandByLabel('Google Drive');

  // Check the context menu for Shared drives grand root.
  await checkContextMenu(
      appId, '/Shared drives', sharedDriveGrandRootMenus, false /* rootMenu */);

  // Check the context menu for a read+write shared drive root.
  await checkContextMenu(
      appId, '/Shared drives/Team Drive A', readWriteSharedDriveRootMenus,
      false /* rootMenu */);

  // Check the context menu for read+write folder.
  await checkContextMenu(
      appId, '/Shared drives/Team Drive A/teamDriveADirectory',
      readWriteFolderMenus, false /* rootMenu */);

  // Check the context menu for a read-only shared drive root.
  await checkContextMenu(
      appId, '/Shared drives/Team Drive B', readOnlySharedDriveRootMenus,
      false /* rootMenu */);

  // Check the context menu for read+write folder.
  await checkContextMenu(
      appId, '/Shared drives/Team Drive B/teamDriveBDirectory',
      readOnlyFolderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for Google Drive/Shared with me root, currently it
 * doesn't show context menu.
 */
export async function dirContextMenuSharedWithMe() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET);

  // Focus the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.focusTree();

  // Select Shared with me root.
  await directoryTree.selectItemByLabel('Shared with me');

  // Wait it to navigate to it.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Shared with me');

  // Right click Shared with me root.
  await directoryTree.showContextMenuForItemByLabel('Shared with me');

  // Check that both menus are still hidden.
  await remoteCall.waitForElement(appId, '#roots-context-menu[hidden]');
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu[hidden]');
}

/**
 * Tests context menu for Google Drive/Offline root, currently it doesn't show
 * context menu.
 */
export async function dirContextMenuOffline() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET);

  // Focus the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.focusTree();

  // Select Shared with me root.
  await directoryTree.selectItemByLabel('Offline');

  // Wait it to navigate to it.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Offline');

  // Right click Shared with me root.
  await directoryTree.showContextMenuForItemByLabel('Offline');

  // Check that both menus are still hidden.
  await remoteCall.waitForElement(appId, '#roots-context-menu[hidden]');
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu[hidden]');
}

/**
 * Tests context menu for Google Drive/Computer grand-root, a computer root, a
 * folder inside it.
 */
export async function dirContextMenuComputers() {
  const computersGrandRootMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', false],
    ['#delete', false],
    ['#new-folder', false],
  ];
  const computerRootMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', false],
    ['#rename', false],
    ['#delete', false],
    ['#new-folder', false],
  ];
  const folderMenus = [
    ['#cut', true],
    ['#copy', true],
    ['#paste-into-folder', true],
    ['#share-with-linux', true],
    ['#rename', false],
    ['#pin-folder', true],
    ['#delete', true],
    ['#new-folder', true],
  ];

  // Open Files App on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], COMPUTERS_ENTRY_SET);

  // Select and copy hello.txt into the clipboard to test paste-into-folder
  // command.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
      'execCommand failed');

  // Check that Google Drive is expanded.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemToExpandByLabel('Google Drive');

  // Check the context menu for Computers grand root.
  await checkContextMenu(
      appId, '/Computers', computersGrandRootMenus, false /* rootMenu */);

  // Check the context menu for a computer root.
  await checkContextMenu(
      appId, '/Computers/Computer A', computerRootMenus, false /* rootMenu */);

  // Check the context menu for a folder inside a computer.
  await checkContextMenu(
      appId, '/Computers/Computer A/A', folderMenus, false /* rootMenu */);
}

/**
 * Tests context menu for Trash root.
 */
export async function dirContextMenuTrash() {
  const trashMenu = [
    ['#empty-trash', true],
  ];

  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.photos], []);

  // Check the context menu for Trash.
  await checkContextMenu(appId, '/Trash', trashMenu, /*rootMenu=*/ false);
}

/**
 * Tests that context menu in directory tree gets the focus, so ChromeVox can
 * announce it.
 */
export async function dirContextMenuFocus() {
  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.photos], []);

  // Wait for /My files/Downloads to appear in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemByLabel('Downloads');

  // Right-click the /My files/Downloads tree row.
  await directoryTree.showContextMenuForItemByLabel('Downloads');

  // Wait for the context menu to appear.
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu:not([hidden])');

  // Wait for the menu item to get focus.
  await remoteCall.waitForElement(
      appId, '#directory-tree-context-menu cr-menu-item:focus');

  // Check currently focused element.
  const focusedElement =
      await remoteCall.callRemoteTestUtil<ElementObject|null>(
          'getActiveElement', appId, []);
  chrome.test.assertEq('menuitem', focusedElement?.attributes['role']);
}

/**
 * Test that the directory tree context menu can be opened by keyboard
 * navigation.
 */
export async function dirContextMenuKeyboardNavigation() {
  // Open Files app on local Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.photos], []);

  // Navigate to /My files/Downloads which will focus the Downloads tree item.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads');

  // Send a contextmenu event to the directory tree. Downloads is initially
  // focused so the subitem context menu will appear.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeEvent', appId, [directoryTree.rootSelector, 'contextmenu']));

  // Wait for context menu to appear.
  const contextMenuQuery =
      '#directory-tree-context-menu:not([hidden]) cr-menu-item:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenuQuery);

  // Dismiss the context menu.
  await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, [contextMenuQuery, 'Escape', false, false, false]);
  await remoteCall.waitForElementLost(appId, contextMenuQuery);

  // Move the focus up on the directory tree to "My files" via keyboard
  // navigation.
  await directoryTree.focusPreviousItem();

  // Send a contextmenu event to the directory tree.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeEvent', appId, [directoryTree.rootSelector, 'contextmenu']));

  // Wait for the "New folder" to appear in the context menu and then press it.
  // This is to verify that the folder created is done in the correct location
  // (i.e. "My files").
  const newFolderQuery = contextMenuQuery + '[command="#new-folder"]';
  await remoteCall.waitForElement(appId, newFolderQuery);
  await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId, [newFolderQuery]);

  // Ensure it's possible to navigate to the newly created folder.
  await directoryTree.navigateToPath('/My files/New folder');
}