chromium/ui/file_manager/integration_tests/file_manager/trash.ts

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

import {DialogType} from '../prod/file_manager/shared_types.js';
import {addEntries, createNestedTestFolders, ENTRIES, RootPath, sendTestMessage} from '../test_util.js';

import {remoteCall} from './background.js';
import {DirectoryTreePageObject} from './page_objects/directory_tree.js';
import {BASIC_ANDROID_ENTRY_SET, BASIC_LOCAL_ENTRY_SET, DOWNLOADS_FAKE_TASKS, NESTED_ENTRY_SET} from './test_data.js';

/**
 * Clicks the enabled and visible delete button and ensures the move to trash
 * button is hidden.
 */
async function clickDeleteButton(appId: string) {
  await remoteCall.waitForElement(appId, '#move-to-trash[hidden]');
  await remoteCall.waitAndClickElement(
      appId, '#delete-button:not([hidden]):not([disabled])');
}

/**
 * Confirm the deletion happens and assert the dialog has the correct text.
 * @param okText expected OK text
 */
async function confirmPermanentDeletion(appId: string, okText: string) {
  // Check: the delete confirm dialog should appear.
  await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');

  // Check: the dialog 'Cancel' button should be focused by default.
  const dialogDefaultButton =
      await remoteCall.waitForElement(appId, '.cr-dialog-cancel:focus');
  chrome.test.assertEq('Cancel', dialogDefaultButton.text);

  // Click the delete confirm dialog 'Delete' button.
  const dialogDeleteButton =
      await remoteCall.waitAndClickElement(appId, '.cr-dialog-ok');
  chrome.test.assertEq(okText, dialogDeleteButton.text);

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Clicks the delete button and confirms the deletion.
 */
async function clickDeleteButtonAndConfirmDeletion(appId: string) {
  await clickDeleteButton(appId);
  await confirmPermanentDeletion(appId, 'Delete forever');
}

/**
 * Delete files in MyFiles and ensure they are moved to /.Trash.
 * Then delete items from /.Trash/files and /.Trash/info, then delete /.Trash.
 */
export async function trashMoveToTrash() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Enable hidden files to be shown.
  await remoteCall.showHiddenFiles(appId);

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

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /My files/Downloads/.Trash/info.
  await directoryTree.navigateToPath('/My files/Downloads/.Trash/info');

  // Select hello.txt.trashinfo.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt.trashinfo"]');

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt.trashinfo"]');

  // Navigate to /My files/Downloads.
  await directoryTree.navigateToPath('/My files/Downloads');

  // Select .Trash.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name=".Trash"]');

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(appId, '#file-list [file-name=".Trash"]');

  // Delete photos dir (no dialog),
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="photos"]');
  await remoteCall.clickTrashButton(appId);

  // Wait for photos to be removed, and .Trash to be recreated.
  await remoteCall.waitForElementLost(appId, '#file-list [file-name="photos"]');
  await remoteCall.waitForElement(appId, '#file-list [file-name=".Trash"]');
}

/**
 * Selects a file and a folder at the same time then deletes both.
 */
export async function trashMultipleEntries() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello, ENTRIES.photos], []);

  // Select all (both the file and the folder).
  const ctrlA = ['#file-list', 'a', true, false, false];
  await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, ctrlA);

  // Delete both entries.
  await remoteCall.clickTrashButton(appId);

  // Wait for both entries to be removed.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.waitForElementLost(appId, '#file-list [file-name="photos"]');
}

/**
 * Selects a non-empty folder and deletes it.
 */
export async function trashNonEmptyFolder() {
  // Build folder structure nested-folder0/nested-folder1.
  const entries = createNestedTestFolders(2);

  // Open files app to a Downloads folder containing nested-folder0.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // Select the folder.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="nested-folder0"]');

  // Delete the folder.
  await remoteCall.clickTrashButton(appId);

  // Wait for the folder to be removed.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="nested-folder0"]');
}

/**
 * Permanently delete files in Downloads.
 */
export async function trashPermanentlyDelete() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Send Shift+Delete to permanently delete, shows delete confirm dialog.
  const shiftDeleteKey = ['#quick-view', 'Delete', false, true, false];
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, shiftDeleteKey),
      'Pressing Shift+Delete failed.');

  // Confirm the permanent deletion of the "hello.txt" file.
  await confirmPermanentDeletion(appId, 'Delete forever');
}

/**
 * Files send to the Trash from ~/MyFiles should be able to be deleted once they
 * are in Trash.
 */
export async function trashDeleteFromTrashOriginallyFromMyFiles() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Cut the file "hello.txt" in preparation to move to ~/MyFiles.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('execCommand', appId, ['cut']));

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files');

  // Paste the file.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']));

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);
}

/**
 * Delete files then restore via progress center panel button 'Undo'.
 */
export async function trashRestoreFromToast() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Wait for the a success progress panel item to appear.
  await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item[status="success"]']);

  // Press the "Undo"" button on the success feedback panel.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId,
      [['#progress-panel', 'xf-panel-item', 'xf-button#primary-action']]));

  // Wait for file to reappear in list.
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Delete files then restore via Trash file context menu.
 */
export async function trashRestoreFromTrash() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Right-click the selected file to validate context menu.
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));
  await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');

  // Check that 'Restore from Trash' and 'Delete' are shown.
  const checkMenu = async (command: string) => {
    await remoteCall.waitForElement(
        appId,
        `#file-context-menu:not([hidden]) [command="${
            command}"]:not([hidden])`);
  };
  await checkMenu('#restore-from-trash');
  await checkMenu('#delete');

  // Restore item.
  await remoteCall.waitAndClickElement(appId, '#restore-from-trash-button');

  // Wait for completion of file restore.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /My files/Downloads and ensure the file is shown.
  await directoryTree.navigateToPath('/My files/Downloads');
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Delete files then restore via keyboard shortcut.
 */
export async function trashRestoreFromTrashShortcut() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Select file.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');

  // Press 'Delete' key.
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['#file-list', 'Delete', false, false, false]));

  // Wait for completion of file restore.
  await remoteCall.waitForElementLost(
      appId, '.tre-row input [file-name="hello.txt"]');

  // Navigate to /My files/Downloads and ensure the file is shown.
  await directoryTree.navigateToPath('/My files/Downloads');
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Delete files (move them into trash) then empty trash using the banner.
 */
export async function trashEmptyTrash() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  const fileNameSelector = '#file-list [file-name="hello.txt"]';
  const emptyTrashButtonSelector =
      ['trash-banner', 'cr-button[command="#empty-trash"]'];

  // Select hello.txt.
  await remoteCall.waitAndClickElement(appId, fileNameSelector);

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileNameSelector);

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitForElement(appId, fileNameSelector);
  // SimulateUiClick ensures that the focus gets updated, which resets
  // canExecute.
  await remoteCall.simulateUiClick(appId, fileNameSelector);

  // Click the empty trash button. SimulateUiClick ensures that the focus is set
  // to the empty trash button before bringing up the confirmation dialog.
  await remoteCall.waitForElement(appId, emptyTrashButtonSelector);
  await remoteCall.simulateUiClick(appId, emptyTrashButtonSelector);
  // Cancel delete.
  await remoteCall.waitAndClickElement(
      appId, '.files-confirm-dialog .cr-dialog-cancel');
  // Wait for the dialog to be hidden.
  await remoteCall.waitForElementLost(appId, '.cr-dialog-container.shown');
  // Check: the file is still present.
  await remoteCall.waitForElement(appId, fileNameSelector);
  // Empty trash and confirm delete (dialog shown).
  await remoteCall.waitForElement(appId, emptyTrashButtonSelector);
  await remoteCall.simulateUiClick(appId, emptyTrashButtonSelector);
  // Wait for the dialog to be shown again.
  await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');
  // Confirm the permanent deletion.
  await remoteCall.waitAndClickElement(
      appId, '.files-confirm-dialog .cr-dialog-ok');
  // Wait for the dialog to be hidden.
  await remoteCall.waitForElementLost(appId, '.cr-dialog-container.shown');

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(appId, fileNameSelector);
}

/**
 * Delete files (move them into trash) then empty trash using shortcut.
 */
export async function trashEmptyTrashShortcut() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Press Ctrl+Shift+Delete key.
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId,
      ['#file-list', 'Delete', /*ctrl=*/ true, /*shift=*/ true, false]));

  // Confirm dialog.
  await remoteCall.waitAndClickElement(
      appId, '.files-confirm-dialog .cr-dialog-ok');

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Delete files (move them into trash) then permanently delete.
 */
export async function trashDeleteFromTrash() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);
}

/**
 * Delete files (move them into trash) then permanently delete.
 */
export async function trashDeleteFromTrashOriginallyFromDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.hello]);

  // Select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Confirm the permanent deletion of the file does not say 'forever'.
  await clickDeleteButton(appId);
  await confirmPermanentDeletion(appId, 'Delete');
}

/**
 * When selecting items whilst in the trash root, no files tasks should be
 * available.
 */
export async function trashNoTasksInTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  await remoteCall.callRemoteTestUtil(
      'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);

  // Select hello.txt and make sure tasks are visible.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.waitForElement(appId, '#tasks:not([hidden])');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown and the tasks button is
  // hidden.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.waitForElement(appId, '#tasks[hidden]');
}

/**
 * Double clicking on a file while in Trash shows a disallowed alert dialog.
 */
export async function trashDoubleClickOnFileInTrashRootShowsDialog() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  await remoteCall.callRemoteTestUtil(
      'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);

  // Select hello.txt and make sure a default task is executed when double
  // clicking.
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeMouseDoubleClick', appId, ['#file-list [file-name="hello.txt"]']));
  await remoteCall.waitUntilTaskExecutes(
      appId, DOWNLOADS_FAKE_TASKS[0]!.descriptor, ['hello.txt']);

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.waitForElement(appId, '#tasks[hidden]');

  // Double-click the file and ensure an alert dialog is displayed.
  await remoteCall.callRemoteTestUtil(
      'fakeMouseDoubleClick', appId, ['#file-list [file-name="hello.txt"]']);
  await remoteCall.waitForElement(appId, '.files-confirm-dialog');
}

/**
 * Pressing Enter on a file while in Trash shows a disallowed confirm dialog
 * with a restore button that performs restoration on the file.
 */
export async function
trashPressingEnterOnFileInTrashRootShowsDialogWithRestoreButton() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  await remoteCall.callRemoteTestUtil(
      'overrideTasks', appId, [DOWNLOADS_FAKE_TASKS]);

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to /Trash and ensure the file is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.waitForElement(appId, '#tasks[hidden]');

  // Press "Enter" on the file and ensure an alert dialog is displayed.
  const enterKey = ['#file-list', 'Enter', false, false, false] as const;
  await remoteCall.fakeKeyDown(appId, ...enterKey);
  await remoteCall.waitForElement(appId, '.files-confirm-dialog');

  // Click the "Restore" button on the error message and ensure it restores the
  // file back to the Downloads directory.
  await remoteCall.waitAndClickElement(appId, '.cr-dialog-ok');
  await directoryTree.navigateToPath('/My files/Downloads');
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Double clicking on a file while in Trash shows a disallowed alert dialog.
 */
export async function trashTraversingFolderShowsDisallowedDialog() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select the Photos folder and trash the whole thing.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="photos"]');
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, '#file-list [file-name="photos"]');

  // Navigate to /Trash and ensure the "photos" folder is shown.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="photos"]');

  // Double-click the folder and ensure an alert dialog is displayed.
  await remoteCall.callRemoteTestUtil(
      'fakeMouseDoubleClick', appId, ['#file-list [file-name="photos"]']);
  await remoteCall.waitForElement(appId, '.files-confirm-dialog');

  // Dismiss the alert dialog.
  await remoteCall.waitAndClickElement(appId, '.cr-dialog-cancel');

  // Select the element and press enter, outside of Trash this would navigate to
  // the folder but in Trash it should show a confirm dialog.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="photos"]');
  const enterKey = ['#file-list', 'Enter', false, false, false] as const;
  await remoteCall.fakeKeyDown(appId, ...enterKey);
  await remoteCall.waitForElement(appId, '.files-confirm-dialog');
}

/**
 * Tests that dragging an accepted file over Trash shows that it accepts the
 * action and performs a trash operation (move a move).
 */
export async function trashDragDropRootAcceptsEntries() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // The drag has to start in the file list column "name" text, otherwise it
  // starts a drag-selection instead of a drag operation.
  const source = '#file-list [file-name="hello.txt"] .entry-name';

  // Select the source file.
  await remoteCall.waitAndClickElement(appId, source);

  // Wait for the directory tree target.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemByLabel('Trash');

  // Drag the source and hover it over the target.
  const finishDrop = await directoryTree.dragFilesToItemByLabel(
      source, 'Trash', /* skipDrop= */ true);

  // Check: drag hovering should navigate the file list.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Trash');

  // Check: the target should have accepts class and should not have denies
  // class.
  await directoryTree.waitForItemToAcceptDropByLabel('Trash');

  // Send a dragdrop event to the target to start a trash operation.
  await finishDrop('#file-list', /* dragLeave= */ false);

  // The Trash root should not have either accepts nor denies after stopping the
  // dragdrop event.
  await directoryTree.waitForItemToFinishDropByLabel('Trash');
}

/**
 * Tests that dragging a file from a location that Trash is not enabled (Android
 * files in this case) shows it is denied.
 */
export async function trashDragDropFromDisallowedRootsFails() {
  // Open Files app on Play Files.
  await addEntries(['android_files'], BASIC_ANDROID_ENTRY_SET);
  const appId = await remoteCall.openNewWindow(RootPath.ANDROID_FILES);

  // Wait for the file list to appear.
  await remoteCall.waitForElement(appId, '#file-list');

  // Set the source of the drag event to the name of the file.
  const source = `#file-list li[file-name="${
      BASIC_ANDROID_ENTRY_SET[0]!.nameText}"] .entry-name`;

  // Select the source file.
  await remoteCall.waitAndClickElement(appId, source);

  // Wait for the directory tree target to be visible.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemByLabel('Trash');

  // Drag the source and hover it over the target.
  const finishDrop = await directoryTree.dragFilesToItemByLabel(
      source, 'Trash', /* skipDrop= */ true);

  // Wait for the directory to change to the Trash root.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Trash');

  // The Trash root in the directory tree shouldn't have the accepts class and
  // should have a denies class.
  await directoryTree.waitForItemToDenyDropByLabel('Trash');

  // Send a dragleave event to the target to stop the drag event.
  await finishDrop('#file-list', /* dropLeave= */ true);

  // The Trash root should not have either accepts nor denies after stopping the
  // dragdrop event.
  await directoryTree.waitForItemToFinishDropByLabel('Trash');
}

/**
 * Tests that dragging and dropping on the Trash root actually trashes the item
 * and it appears in Trash after drop completed.
 */
export async function trashDragDropRootPerformsTrashAction() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // The drag has to start in the file list column "name" text, otherwise it
  // starts a drag-selection instead of a drag operation.
  const source = '#file-list [file-name="hello.txt"] .entry-name';

  // Select the source file.
  await remoteCall.waitAndClickElement(appId, source);

  // Wait for the directory tree target.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemByLabel('Trash');

  // Send a dragdrop event to the target to start a trash operation.
  await directoryTree.dragFilesToItemByLabel(
      source, 'Trash', /* skipDrop= */ false);

  // Wait for element to disappear from the "Downloads" view, this indicates it
  // should be in trash.
  await remoteCall.waitForElementLost(appId, source);

  // Navigate to the Trash root.
  await directoryTree.navigateToPath('/Trash');

  // Wait for the element to appear in the Trash.
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Tests that dragging an entry that is non-modifiable (Downloads in this case)
 * should not be allowed despite residing in a trashable location.
 */
export async function trashDragDropNonModifiableEntriesCantBeTrashed() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

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

  // Use Downloads entry as the drag source. Although this is technically a
  // folder and resides on a trashable location "My files", it is a special
  // entry so we should disallow this from being acceptable as a drop target on
  // Trash.
  const source = '#file-list [file-name="Downloads"] .entry-name';

  // Select the source file.
  await remoteCall.waitAndClickElement(appId, source);

  // Wait for the directory tree target.
  await directoryTree.waitForItemByLabel('Trash');

  // Send a dragdrop event to the target to try Trash the downloads folder.
  await directoryTree.dragFilesToItemByLabel(
      source, 'Trash', /* skipDrop= */ false);

  // Navigate to Trash to ensure Downloads wasn't sent there.
  await directoryTree.navigateToPath('/Trash');

  // Ensure the Downloads entry doesn't exist in Trash.
  await remoteCall.waitForElement(appId, `[scan-completed="Trash"]`);
  await remoteCall.waitForFiles(appId, []);
}

/**
 * Tests the Trash root is not visible when opening Files app as a select file
 * dialog.
 */
export async function trashDontShowTrashRootOnSelectFileDialog() {
  // Open Files app on Downloads as a select file dialog.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, [],
      {type: DialogType.SELECT_OPEN_FILE});

  // Navigate to the My files directory to ensure the directory tree has fully
  // loaded and wait for My files to finish scanning.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files');
  await remoteCall.waitForElement(appId, `[scan-completed="My files"]`);

  // Ensure the Trash root entry is not visible on the page.
  await directoryTree.waitForItemLostByLabel('Trash');
}

/**
 * Tests the Trash root is not visible when Files app is used as a select file
 * dialog within Android applications.
 */
export async function trashDontShowTrashRootWhenOpeningAsAndroidFilePicker() {
  // Open Files app on Downloads as an Android file picker.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, [],
      {volumeFilter: ['media-store-files-only']});

  // Navigate to the My files directory to ensure the directory tree has fully
  // loaded and wait for My files to finish scanning.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files');
  await remoteCall.waitForElement(appId, `[scan-completed="My files"]`);

  // Ensure the Trash root entry is not visible on the page.
  await directoryTree.waitForItemLostByLabel('Trash');
}

/**
 * Tests that a trashed file with a deletion date >30 days gets permanently
 * removed.
 */
export async function trashEnsureOldEntriesArePeriodicallyRemoved() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello], []);
  const fileNameSelector = '#file-list [file-name="hello.txt"]';

  // Select hello.txt and make sure a default task is executed when double
  // clicking.
  await remoteCall.waitAndClickElement(appId, fileNameSelector);
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileNameSelector);

  // Navigate to /Trash and ensure the file is there and has not been deleted,
  // the deletion date is well within the periodic deletion boundaries.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitForElement(appId, fileNameSelector);

  // Navigate away from /Trash (to /My files) as the periodic removal will only
  // be kicked off on initial directory scan.
  await directoryTree.navigateToPath('/My files');

  // Overwrite the existing .trashinfo file with an older one that is outside
  // the 30 day window and should trigger periodic removal.
  await addEntries(['local'], [
    ENTRIES.trashRootDirectory,
    ENTRIES.trashInfoDirectory,
    ENTRIES.oldTrashInfoFile,
  ]);

  // Navigate to /Trash and ensure the file has been removed.
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitForElement(appId, `[scan-completed="Trash"]`);
  await remoteCall.waitForElementLost(appId, fileNameSelector);

  // Expect no feedback panel element to appear as the IOTask was kicked off
  // with notifications disabled.
  await remoteCall.waitForElementLost(
      appId, ['#progress-panel', 'xf-panel-item']);
}

/**
 * Tests that dragging and dropping out of the Trash root restore files to the
 * location that was requested (i.e. the drop target).
 */
export async function trashDragDropOutOfTrashPerformsRestoration() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt and send it to the Trash.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Wait for the element to appear in the Trash.
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');

  // Send a dragdrop event that emulates dragging the hello.txt onto the "My
  // files" root in the directory tree.
  await directoryTree.dragFilesToItemByLabel(
      '#file-list [file-name="hello.txt"] .entry-name', 'My files',
      /* skipDrop= */ false);

  // Wait for the element to disappear from the file list.
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // A feedback panel item should be created to indicate the restoring function.
  await remoteCall.waitForElement(appId, ['#progress-panel', 'xf-panel-item']);

  // Navigate to the "My files" root and ensure the file exists there now.
  await directoryTree.navigateToPath('/My files');
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Tests that the "Moving to trash" visual signal that is shown whilst a trash
 * operation is in progress, does not contain the "Undo" button.
 */
export async function trashRestorationDialogInProgressDoesntShowUndo() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Tell the progress center to never finish the operation which leaves the in
  // progress visual signal visible.
  await remoteCall.callRemoteTestUtil(
      'progressCenterNeverNotifyCompleted', appId, []);

  // Select hello.txt and send it to the Trash.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // A feedback panel item should be created to indicate the item is being sent
  // to the trash with only a secondary action.
  const cancelButton = await remoteCall.waitForElement(
      appId,
      ['#progress-panel', 'xf-panel-item', 'xf-button#secondary-action']);
  await remoteCall.waitForElementLost(
      appId, ['#progress-panel', 'xf-panel-item', 'xf-button#primary-action']);

  // Ensure the secondary action is of the category cancel.
  chrome.test.assertEq(cancelButton.attributes['data-category'], 'cancel');
}

/**
 * Tests that the `TrashEnabled` preference adds and removes the trash root
 * from the directory tree.
 */
export async function trashTogglingTrashEnabledPrefUpdatesDirectoryTree() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt and send it to the Trash, this file should not be removed
  // in between enabling and disabling the feature.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(
      appId, '#file-list [file-name="hello.txt"]');

  // Wait for Trash root to be visible.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForItemByLabel('Trash');

  // Disable trash.
  await sendTestMessage({name: 'setTrashEnabled', enabled: false});

  // Wait for the Trash root to disappear.
  await directoryTree.waitForItemLostByLabel('Trash');

  // Ensure the delete button shows up instead of the move to trash button.
  await remoteCall.waitUntilSelected(appId, 'world.ogv');
  await clickDeleteButton(appId);
  await remoteCall.waitForElement(appId, '.cr-dialog-container.shown');

  // Cancel the dialog.
  await remoteCall.waitAndClickElement(appId, '.cr-dialog-cancel');

  // Enable trash.
  await sendTestMessage({name: 'setTrashEnabled', enabled: true});

  // Wait for the Trash root to appear again.
  await directoryTree.waitForItemByLabel('Trash');

  // Navigate to the "Trash" root and ensure the file exists there now.
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitForElement(appId, '#file-list [file-name="hello.txt"]');
}

/**
 * Tests that the `TrashEnabled` preference adds and removes the trash root
 * from the directory tree and when navigated on the Trash root, removal
 * navigates the user back to My files.
 */
export async function trashTogglingTrashEnabledNavigatesAwayFromTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Disable trash.
  await sendTestMessage({name: 'setTrashEnabled', enabled: false});

  // Wait for the Trash root to disappear.
  await directoryTree.waitForItemLostByLabel('Trash');

  // Ensure the new root is now at My files.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/My files');
}

/**
 * Verify that files that have their parents trashed show an alert dialog to
 * indicate that restoration is not possible.
 */
export async function trashCantRestoreWhenParentDoesntExist() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, NESTED_ENTRY_SET, []);

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

  // Ensure the "B" directory exists within "A".
  await remoteCall.waitForFiles(appId, [ENTRIES.directoryB.getExpectedRow()]);

  // Select the "B" directory.
  await remoteCall.waitAndClickElement(appId, '#file-list [file-name="B"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, '#file-list [file-name="B"]');

  // Navigate to /My files/Downloads and click the "A" directory.
  await directoryTree.navigateToPath('/My files/Downloads');
  await remoteCall.waitAndClickElement(appId, '#file-list [file-name="A"]');

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, '#file-list [file-name="A"]');

  // Navigate to Trash and click the "B" directory of which the parent "A"
  // directory has been removed.
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitAndClickElement(appId, '#file-list [file-name="B"]');

  // Right-click the selected file to validate context menu.
  await remoteCall.waitAndRightClick(appId, '.table-row[selected]');
  await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');

  // Restore item and expect the alert dialog is shown as the parent has been
  // removed.
  await remoteCall.waitAndClickElement(appId, '#restore-from-trash-button');
  await remoteCall.waitForElement(appId, '.files-alert-dialog');
}

/**
 * Verify that infeasible actions within Trash root are disabled and hidden when
 * right clicking a file. Verify that feasible actions are enabled.
 */
export async function
trashInfeasibleActionsForFileDisabledAndHiddenInTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello], []);

  const fileSelector = '#file-list [file-name="hello.txt"]';

  // Select hello.txt and send it to the Trash.
  await remoteCall.waitAndClickElement(appId, fileSelector);
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Wait for the element to appear in the Trash and right click it to get
  // access to the context menu.
  await remoteCall.waitAndRightClick(appId, fileSelector);

  const contextMenuSelector = '#file-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenuSelector);

  // Ensure each infeasible action is disabled and hidden.
  const infeasibleActions = [
    'rename',
    'new-folder',
    'paste',
    'copy',
    'zip-selection',
    'set-wallpaper',
    'invoke-sharesheet',
  ];
  for (const action of infeasibleActions) {
    await remoteCall.waitForElement(
        appId,
        contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
  }

  // Ensure each feasible action not disabled and not hidden.
  const feasibleActions = [
    'cut',
    'get-info',
    'delete',
    'restore-from-trash',
  ];
  for (const action of feasibleActions) {
    await remoteCall.waitForElement(
        appId,
        contextMenuSelector + ' [command="#' + action +
            '"]:not([disabled]):not([hidden])');
  }
}

/**
 * Verify that infeasible actions within Trash root are disabled and hidden when
 * right clicking a folder. Verify that feasible actions are enabled.
 */
export async function
trashInfeasibleActionsForFolderDisabledAndHiddenInTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello, ENTRIES.directoryA], []);

  // Select and copy hello.txt into the clipboard to check the Paste Into Folder
  // action.
  await remoteCall.waitUntilSelected(appId, ENTRIES.hello.nameText);
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
      'execCommand failed');

  const fileSelector = '#file-list [file-name="A"]';

  // Select folder A and send it to the Trash.
  await remoteCall.waitAndClickElement(appId, fileSelector);
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Wait for the element to appear in the Trash and right click it to get
  // access to the context menu.
  await remoteCall.waitAndRightClick(appId, fileSelector);

  const contextMenuSelector = '#file-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenuSelector);

  // Ensure each infeasible action is disabled and hidden.
  const infeasibleActions = [
    'rename',
    'new-folder',
    'copy',
    'zip-selection',
    'paste-into-folder',
  ];
  for (const action of infeasibleActions) {
    await remoteCall.waitForElement(
        appId,
        contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
  }

  // Ensure each feasible action not disabled and not hidden.
  const feasibleActions = [
    'cut',
    'get-info',
    'delete',
    'restore-from-trash',
  ];
  for (const action of feasibleActions) {
    await remoteCall.waitForElement(
        appId,
        contextMenuSelector + ' [command="#' + action +
            '"]:not([disabled]):not([hidden])');
  }
}

/**
 * Verify that Extract All within Trash root is disabled and hidden when right
 * clicking a zip file.
 */
export async function trashExtractAllForZipHiddenAndDisabledInTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.zipArchive], []);

  const fileSelector = '#file-list [file-name="archive.zip"]';

  // Select tera.zip and send it to the Trash.
  await remoteCall.waitAndClickElement(appId, fileSelector);
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Wait for the element to appear in the Trash and right click it to get
  // access to the context menu.
  await remoteCall.waitAndRightClick(appId, fileSelector);

  const contextMenuSelector = '#file-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenuSelector);
  // Ensure extract all action is disabled and hidden.
  await remoteCall.waitForElement(
      appId,
      contextMenuSelector + ' [command="#extract-all"][disabled][hidden]');
}

/**
 * Verify that infeasible actions within Trash root are disabled and hidden when
 * right clicking a blank space. Verify that Cut is disabled but not hidden.
 */
export async function trashAllActionsDisabledForBlankSpaceInTrashRoot() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello], []);

  // Select and copy hello.txt into the clipboard to check the Paste action.
  await remoteCall.waitUntilSelected(appId, ENTRIES.hello.nameText);
  chrome.test.assertTrue(
      !!await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']),
      'execCommand failed');

  // Navigate to the Trash root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Trash');

  // Click blank space.
  await remoteCall.rightClickFileListBlankSpace(appId);

  // Ensure the context menu is not hidden.
  const contextMenuSelector = '#file-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, contextMenuSelector);
  // Ensure each infeasible action is disabled and hidden.
  const infeasibleActions = [
    'paste',
    'new-folder',
    'copy',
  ];
  for (const action of infeasibleActions) {
    await remoteCall.waitForElement(
        appId,
        contextMenuSelector + ' [command="#' + action + '"][disabled][hidden]');
  }

  // Ensure Cut is disabled and not hidden.
  await remoteCall.waitForElement(
      appId, contextMenuSelector + ' [command="#cut"][disabled]:not([hidden])');
}

export async function trashStaleTrashInfoFilesAreRemovedAfterOneHour() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  const fileSelector = '#file-list [file-name="hello.txt"]';
  const trashInfoSelector = '#file-list [file-name="hello.txt.trashinfo"]';

  // Select hello.txt and send it to the Trash.
  await remoteCall.waitAndClickElement(appId, fileSelector);
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Enable hidden files to be shown.
  await remoteCall.showHiddenFiles(appId);

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

  // Select hello.txt.
  await remoteCall.waitAndClickElement(appId, fileSelector);

  // Delete selected item.
  await clickDeleteButtonAndConfirmDeletion(appId);
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Navigate to /My files/Downloads/.Trash/info and ensure the .trashinfo file
  // is still there.
  await directoryTree.navigateToPath('/My files/Downloads/.Trash/info');
  await remoteCall.waitForElement(appId, trashInfoSelector);

  // Update the modification date for the .trashinfo file.
  chrome.test.assertEq(
      await sendTestMessage({
        name: 'updateModificationDate',
        localPath: 'Downloads/.Trash/info/hello.txt.trashinfo',
        modificationDate: ((new Date()).getTime() - 120 * 60 * 1000),
      }),
      'true');

  // Navigate to the Trash directory which should kick off the removal of the
  // stale .trashinfo file.
  await directoryTree.navigateToPath('/Trash');
  await remoteCall.waitForElementLost(appId, fileSelector);

  // Navigate back to the .Trash/info directory and ensure the .trashinfo file
  // has been removed.
  await directoryTree.navigateToPath('/My files/Downloads/.Trash/info');
  await remoteCall.waitForElementLost(appId, trashInfoSelector);
}

/**
 * If the current directory is .Trash or sub folders of .Trash (e.g.
 * files/info), after disable "show hidden files" menu, the current directory
 * should navigate back to MyFiles.
 */
export async function trashTogglingHiddenFilesNavigatesAwayFromTrash() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Select hello.txt.
  const helloFileSelector = '#file-list [file-name="hello.txt"]';
  await remoteCall.waitAndClickElement(appId, helloFileSelector);

  // Delete item and wait for it to be removed (no dialog).
  await remoteCall.clickTrashButton(appId);
  await remoteCall.waitForElementLost(appId, helloFileSelector);

  // Enable hidden files to be shown.
  await remoteCall.showHiddenFiles(appId);

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

  // Uncheck "show hidden files".
  await remoteCall.showHiddenFiles(appId, /* check= */ false);

  // Expect current directory goes back to MyFiles.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/My files');
  await directoryTree.waitForItemLostByLabel('.Trash');

  // Show hidden files again.
  await remoteCall.showHiddenFiles(appId);

  // Navigate to /My files/Downloads/.Trash/files this time.
  await directoryTree.navigateToPath('/My files/Downloads/.Trash/files');

  // Uncheck "show hidden files".
  await remoteCall.showHiddenFiles(appId, /* check= */ false);

  // Expect current directory goes back to MyFiles.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/My files');
  await directoryTree.waitForItemLostByLabel('.Trash');
}