chromium/ui/file_manager/integration_tests/file_manager/transfer.ts

// Copyright 2014 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 {ENTRIES, EntryType, getCaller, pending, repeatUntil, RootPath, sendTestMessage, TestEntryInfo} from '../test_util.js';

import {remoteCall} from './background.js';
import {DirectoryTreePageObject} from './page_objects/directory_tree.js';
import {BASIC_DRIVE_ENTRY_SET, BASIC_LOCAL_ENTRY_SET, OFFLINE_ENTRY_SET, SHARED_DRIVE_ENTRY_SET, SHARED_WITH_ME_ENTRY_SET} from './test_data.js';

interface TransferLocationOptions {
  volumeName: string;
  breadcrumbsPath: string;
  isTeamDrive?: boolean;
  initialEntries: TestEntryInfo[];
}

/**
 * Info for the source or destination of a transfer.
 */
class TransferLocationInfo {
  /**
   * The volume type (e.g. downloads, drive, drive_recent,
   * drive_shared_with_me, drive_offline) or team drive name.
   */
  volumeName: string;

  breadcrumbsPath: string;

  isTeamDrive: boolean;

  /** Expected initial contents in the volume. */
  initialEntries: TestEntryInfo[];

  constructor(opts: TransferLocationOptions) {
    this.volumeName = opts.volumeName;
    this.breadcrumbsPath = opts.breadcrumbsPath;
    this.isTeamDrive = opts.isTeamDrive ?? false;
    this.initialEntries = opts.initialEntries;
  }
}

interface TransferOptions {
  fileToTransfer: TestEntryInfo;
  source: TransferLocationInfo;
  destination: TransferLocationInfo;
  expectedDialogText?: string;
  expectedDialogOkButtonText?: string;
  isMove?: boolean;
  expectFailure?: boolean;
}

/**
 * Info for the transfer operation.
 */
class TransferInfo {
  /** The file to copy or move. Must be in the source location. */
  fileToTransfer: TestEntryInfo;

  /** The source location. */
  source: TransferLocationInfo;

  /** The destination location. */
  destination: TransferLocationInfo;

  /**
   * The expected content of the transfer dialog, or undefined if no dialog is
   * expected.
   */
  expectedDialogText?: string;

  /**
   * The expected label of the “ok” button on the dialog, if the dialog is
   * expected.
   */
  expectedDialogOkButtonText?: string;

  /**
   * True if this transfer is for a move operation, false for a copy operation.
   */
  isMove: boolean;

  /**
   * Whether the test is expected to fail, i.e. transferring to a folder without
   * correct permissions.
   */
  expectFailure: boolean;

  constructor(opts: TransferOptions) {
    this.fileToTransfer = opts.fileToTransfer;
    this.source = opts.source;
    this.destination = opts.destination;
    this.expectedDialogText = opts.expectedDialogText;
    this.expectedDialogOkButtonText = opts.expectedDialogOkButtonText;
    this.isMove = opts.isMove ?? false;
    this.expectFailure = opts.expectFailure ?? false;
  }
}

/**
 * Test function to copy from the specified source to the specified destination.
 * @param transferInfo Options for the transfer.
 */
async function transferBetweenVolumes(transferInfo: TransferInfo) {
  let srcContents;
  if (transferInfo.source.isTeamDrive) {
    srcContents =
        TestEntryInfo.getExpectedRows(transferInfo.source.initialEntries.filter(
            entry => entry.type !== EntryType.SHARED_DRIVE &&
                entry.teamDriveName === transferInfo.source.volumeName));
  } else {
    srcContents =
        TestEntryInfo.getExpectedRows(transferInfo.source.initialEntries.filter(
            entry => entry.type !== EntryType.SHARED_DRIVE &&
                entry.teamDriveName === ''));
  }

  let dstContents;
  if (transferInfo.destination.isTeamDrive) {
    dstContents = TestEntryInfo.getExpectedRows(
        transferInfo.destination.initialEntries.filter(
            entry => entry.type !== EntryType.SHARED_DRIVE &&
                entry.teamDriveName === transferInfo.destination.volumeName));
  } else {
    dstContents = TestEntryInfo.getExpectedRows(
        transferInfo.destination.initialEntries.filter(
            entry => entry.type !== EntryType.SHARED_DRIVE &&
                entry.teamDriveName === ''));
  }

  const localFiles = BASIC_LOCAL_ENTRY_SET;
  const driveFiles = (transferInfo.source.isTeamDrive ||
                      transferInfo.destination.isTeamDrive) ?
      SHARED_DRIVE_ENTRY_SET :
      BASIC_DRIVE_ENTRY_SET.concat([
        ENTRIES.sharedDirectory,
        ENTRIES.sharedDirectoryFile,
        ENTRIES.docxFile,
      ]);

  // Open files app.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, localFiles, driveFiles);

  // Expand Drive root if either src or dst is within Drive.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  if (transferInfo.source.isTeamDrive || transferInfo.destination.isTeamDrive) {
    const myDriveContent = TestEntryInfo.getExpectedRows(driveFiles.filter(
        e => e.teamDriveName === '' && e.computerName === ''));
    // Select + expand + wait for its content.

    await directoryTree.navigateToPath('/My Drive');
    await remoteCall.waitForFiles(appId, myDriveContent);
  }

  // Select the source folder.
  await directoryTree.navigateToPath(transferInfo.source.breadcrumbsPath);

  // Wait for the expected files to appear in the file list.
  await remoteCall.waitForFiles(appId, srcContents);

  // Focus the file list.
  await remoteCall.callRemoteTestUtil(
      'focus', appId, ['#file-list:not([hidden])']);

  // Select the source file.
  await remoteCall.waitUntilSelected(
      appId, transferInfo.fileToTransfer.nameText);

  // Copy the file.
  const transferCommand = transferInfo.isMove ? 'cut' : 'copy';
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'execCommand', appId, [transferCommand]));

  // Select the destination folder.
  await directoryTree.navigateToPath(transferInfo.destination.breadcrumbsPath);

  // Wait for the expected files to appear in the file list.
  await remoteCall.waitForFiles(
      appId, dstContents, {ignoreFileSize: true, ignoreLastModifiedTime: true});
  // Paste the file.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']));

  // If we're expecting a confirmation dialog, confirm that it is shown.
  if (transferInfo.expectedDialogText !== undefined) {
    const {innerText} = await remoteCall.waitForElement(
        appId, '.cr-dialog-container.shown .cr-dialog-text');
    chrome.test.assertEq(transferInfo.expectedDialogText, innerText);

    // Check that the dialog contains required buttons.
    const okButton = await remoteCall.waitForElement(
        appId, '.cr-dialog-container.shown .cr-dialog-ok');
    chrome.test.assertEq(
        transferInfo.expectedDialogOkButtonText, okButton.innerText);
    const cancelButton = await remoteCall.waitForElement(
        appId, '.cr-dialog-container.shown .cr-dialog-cancel');
    chrome.test.assertEq('Cancel', cancelButton.innerText);

    // Press OK button.
    chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
        'fakeMouseClick', appId, ['button.cr-dialog-ok']));
  }

  // Wait for the file list to change, if the test is expected to pass.
  const dstContentsAfterPaste = dstContents.slice();
  const ignoreFileSize =
      transferInfo.source.volumeName === 'drive_shared_with_me' ||
      transferInfo.source.volumeName === 'drive_offline' ||
      transferInfo.destination.volumeName === 'drive_shared_with_me' ||
      transferInfo.destination.volumeName === 'drive_offline' ||
      transferInfo.destination.volumeName === 'my_files';

  // If we expected the transfer to succeed, add the pasted file to the list
  // of expected rows.
  if (!transferInfo.expectFailure &&
      transferInfo.source !== transferInfo.destination) {
    const pasteFile = transferInfo.fileToTransfer.getExpectedRow();
    // Check if we need to add (1) to the filename, in the case of a
    // duplicate file.
    for (let i = 0; i < dstContentsAfterPaste.length; i++) {
      if (dstContentsAfterPaste[i]![0] === pasteFile[0]) {
        // Replace the last '.' in filename with ' (1).'.
        // e.g. 'my.note.txt' -> 'my.note (1).txt'
        pasteFile[0] = pasteFile[0].replace(/\.(?=[^\.]+$)/, ' (1).');
        break;
      }
    }
    dstContentsAfterPaste.push(pasteFile);
  }

  // Check the last contents of file list.
  await remoteCall.waitForFiles(
      appId, dstContentsAfterPaste,
      {ignoreFileSize: ignoreFileSize, ignoreLastModifiedTime: true});

  return appId;
}

/** A list of transfer locations, for use with transferBetweenVolumes. */
const TRANSFER_LOCATIONS = {
  drive: new TransferLocationInfo({
    breadcrumbsPath: '/My Drive',
    volumeName: 'drive',
    initialEntries: BASIC_DRIVE_ENTRY_SET.concat([
      ENTRIES.sharedDirectory,
      ENTRIES.docxFile,
    ]),
  }),

  driveWithTeamDriveEntries: new TransferLocationInfo({
    breadcrumbsPath: '/My Drive',
    volumeName: 'drive',
    initialEntries: SHARED_DRIVE_ENTRY_SET,
  }),

  driveSharedDirectory: new TransferLocationInfo({
    breadcrumbsPath: '/My Drive/Shared',
    volumeName: 'drive',
    initialEntries: [ENTRIES.sharedDirectoryFile],
  }),

  downloads: new TransferLocationInfo({
    breadcrumbsPath: '/My files/Downloads',
    volumeName: 'downloads',
    initialEntries: BASIC_LOCAL_ENTRY_SET,
  }),

  sharedWithMe: new TransferLocationInfo({
    breadcrumbsPath: '/Shared with me',
    volumeName: 'drive_shared_with_me',
    initialEntries: SHARED_WITH_ME_ENTRY_SET.concat([
      ENTRIES.sharedDirectory,
      ENTRIES.sharedDirectoryFile,
    ]),
  }),

  driveOffline: new TransferLocationInfo({
    breadcrumbsPath: '/Offline',
    volumeName: 'drive_offline',
    initialEntries: OFFLINE_ENTRY_SET,
  }),

  driveTeamDriveA: new TransferLocationInfo({
    breadcrumbsPath: '/Shared drives/Team Drive A',
    volumeName: 'Team Drive A',
    isTeamDrive: true,
    initialEntries: SHARED_DRIVE_ENTRY_SET,
  }),

  driveTeamDriveB: new TransferLocationInfo({
    breadcrumbsPath: '/Shared drives/Team Drive B',
    volumeName: 'Team Drive B',
    isTeamDrive: true,
    initialEntries: SHARED_DRIVE_ENTRY_SET,
  }),

  my_files: new TransferLocationInfo({
    breadcrumbsPath: '/My files',
    volumeName: 'my_files',
    initialEntries: [
      new TestEntryInfo({
        type: EntryType.DIRECTORY,
        targetPath: 'Play files',
        nameText: 'Play files',
        lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
        sizeText: '--',
        typeText: 'Folder',
      }),
      new TestEntryInfo({
        type: EntryType.DIRECTORY,
        targetPath: 'Downloads',
        nameText: 'Downloads',
        lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
        sizeText: '--',
        typeText: 'Folder',
      }),
      new TestEntryInfo({
        type: EntryType.DIRECTORY,
        targetPath: 'Linux files',
        nameText: 'Linux files',
        lastModifiedTime: '...',
        sizeText: '--',
        typeText: 'Folder',
      }),
    ],
  }),
};
Object.freeze(TRANSFER_LOCATIONS);

/**
 * Tests copying from Drive to Downloads.
 */
export async function transferFromDriveToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.drive,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests copying an office file from Drive to Downloads.
 */
export async function transferOfficeFileFromDriveToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.docxFile,
    source: TRANSFER_LOCATIONS.drive,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests moving files from MyFiles/Downloads to MyFiles crbug.com/925175.
 */
export async function transferFromDownloadsToMyFilesMove() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.my_files,
    isMove: true,
  }));
}

/**
 * Tests copying files from MyFiles/Downloads to MyFiles crbug.com/925175.
 */
export async function transferFromDownloadsToMyFiles() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.my_files,
    isMove: false,
  }));
}

/**
 * Tests copying from Downloads to Drive.
 */
export async function transferFromDownloadsToDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.drive,
  }));
}

/**
 * Tests copying from Drive "Shared with me" to Downloads.
 */
export async function transferFromSharedWithMeToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.testSharedFile,
    source: TRANSFER_LOCATIONS.sharedWithMe,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests copying from Drive "Shared with me" to Drive.
 */
export async function transferFromSharedWithMeToDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.testSharedDocument,
    source: TRANSFER_LOCATIONS.sharedWithMe,
    destination: TRANSFER_LOCATIONS.drive,
  }));
}


/**
 * Tests copying from Downloads to a shared folder on Drive.
 */
export async function transferFromDownloadsToSharedFolder() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.driveSharedDirectory,
    expectedDialogText:
        'Copying this item will share it with everyone who can see the ' +
        'shared folder \'Shared\'.',
    expectedDialogOkButtonText: 'Copy',
  }));
}

/**
 * Tests moving from Downloads to a shared folder on Drive.
 */
export async function transferFromDownloadsToSharedFolderMove() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.driveSharedDirectory,
    expectedDialogText:
        'Moving this item will share it with everyone who can see the ' +
        'shared folder \'Shared\'.',
    expectedDialogOkButtonText: 'Move',
    isMove: true,
  }));
}

/**
 * Tests copying from a shared folder on Drive to Downloads.
 */
export async function transferFromSharedFolderToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.sharedDirectoryFile,
    source: TRANSFER_LOCATIONS.driveSharedDirectory,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests copying from Drive offline to Downloads.
 */
export async function transferFromOfflineToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.testSharedFile,
    source: TRANSFER_LOCATIONS.driveOffline,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests copying from Drive offline to Drive.
 */
export async function transferFromOfflineToDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.testDocument,
    source: TRANSFER_LOCATIONS.driveOffline,
    destination: TRANSFER_LOCATIONS.drive,
  }));
}

/**
 * Tests copying from a Team Drive to Drive.
 */
export async function transferFromTeamDriveToDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.teamDriveAFile,
    source: TRANSFER_LOCATIONS.driveTeamDriveA,
    destination: TRANSFER_LOCATIONS.driveWithTeamDriveEntries,
  }));
}

/**
 * Tests copying from Drive to a Team Drive.
 */
export async function transferFromDriveToTeamDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.driveWithTeamDriveEntries,
    destination: TRANSFER_LOCATIONS.driveTeamDriveA,
    expectedDialogText:
        'Members of \'Team Drive A\' will gain access to the copy of these ' +
        'items.',
    expectedDialogOkButtonText: 'Copy',
  }));
}

/**
 * Tests copying from a Team Drive to Downloads.
 */
export async function transferFromTeamDriveToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.teamDriveAFile,
    source: TRANSFER_LOCATIONS.driveTeamDriveA,
    destination: TRANSFER_LOCATIONS.downloads,
  }));
}

/**
 * Tests that a hosted file cannot be transferred from a Team Drive to a local
 * drive (e.g. Downloads). Hosted documents only make sense in the context of
 * Drive.
 */
export async function transferHostedFileFromTeamDriveToDownloads() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.teamDriveAHostedFile,
    source: TRANSFER_LOCATIONS.driveTeamDriveA,
    destination: TRANSFER_LOCATIONS.downloads,
    expectFailure: true,
  }));
}

/**
 * Tests copying from Downloads to a Team Drive.
 */
export async function transferFromDownloadsToTeamDrive() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.driveTeamDriveA,
    expectedDialogText:
        'Members of \'Team Drive A\' will gain access to the copy of these ' +
        'items.',
    expectedDialogOkButtonText: 'Copy',
  }));
}

/**
 * Tests copying between Team Drives.
 */
export async function transferBetweenTeamDrives() {
  return transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.teamDriveBFile,
    source: TRANSFER_LOCATIONS.driveTeamDriveB,
    destination: TRANSFER_LOCATIONS.driveTeamDriveA,
    expectedDialogText:
        'Members of \'Team Drive A\' will gain access to the copy of these ' +
        'items.',
    expectedDialogOkButtonText: 'Copy',
  }));
}

/**
 * Tests that moving a file to its current location is a no-op.
 */
export async function transferFromDownloadsToDownloads() {
  const appId = await transferBetweenVolumes(new TransferInfo({
    fileToTransfer: ENTRIES.hello,
    source: TRANSFER_LOCATIONS.downloads,
    destination: TRANSFER_LOCATIONS.downloads,
    isMove: true,
  }));

  // Check: No feedback panel items.
  const panelItems =
      await remoteCall.queryElements(appId, ['#progress-panel', '#panel']);

  chrome.test.assertEq(0, panelItems.length);
}

/**
 * Tests that the root html element .drag-drop-active class appears when drag
 * drop operations are active, and is removed when the operations complete.
 */
export async function transferDragDropActiveLeave() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .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('My files');

  // Check: the html element should not have drag-drop-active class.
  const htmlDragDropActive = ['html.drag-drop-active'];
  await remoteCall.waitForElementLost(appId, htmlDragDropActive);

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

  // Check: the html element should have drag-drop-active class.
  await remoteCall.waitForElementsCount(appId, htmlDragDropActive, 1);

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

  // Check: the html element should not have drag-drop-active class.
  await remoteCall.waitForElementLost(appId, htmlDragDropActive);
}

/**
 * Tests that the root html element .drag-drop-active class appears when drag
 * drop operations are active, and is removed when the operations complete.
 */
export async function transferDragDropActiveDrop() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // Expand Downloads to display "photos" folder in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .entry-name`;

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

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

  // Check: the html element should not have drag-drop-active class.
  const htmlDragDropActive = ['html.drag-drop-active'];
  await remoteCall.waitForElementLost(appId, htmlDragDropActive);

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

  // Check: the html element should have drag-drop-active class.
  await remoteCall.waitForElementsCount(appId, htmlDragDropActive, 1);

  // Send a drop event to the target to end drag-drop operations.
  await finishDrop('#file-list', /* dragLeave= */ false);

  // Check: the html element should not have drag-drop-active class.
  await remoteCall.waitForElementLost(appId, htmlDragDropActive);
}

/**
 * Tests that dragging a file over a directory tree item that can accept the
 * drop changes the class of that tree item to 'accepts'.
 */
export async function transferDragDropTreeItemAccepts() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // 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 li[file-name="${ENTRIES.photos.nameText}"] .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('My files');

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

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

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

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

  // Check: the target should not have accepts class and should not have denies
  // class.
  await directoryTree.waitForItemToFinishDropByLabel('My files');
}

/**
 * Tests that dragging a file over a directory tree item that cannot accept
 * the drop changes the class of that tree item to 'denies'.
 */
export async function transferDragDropTreeItemDenies() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .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('Recent');

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

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

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

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

  // Check: the target should not have denies class and should not have accepts
  // class.
  await directoryTree.waitForItemToFinishDropByLabel('Recent');
}

/**
 * Tests that dragging a file over an EntryList directory tree item (here a
 * partitioned USB drive) does not raise an error.
 */
export async function transferDragAndHoverTreeItemEntryList() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Mount a partitioned USB.
  await sendTestMessage({name: 'mountUsbWithPartitions'});

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .entry-name`;

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

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

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

/**
 * Tests that dragging a file over a FakeEntry directory tree item (here a
 * USB drive) does not raise an error.
 */
export async function transferDragAndHoverTreeItemFakeEntry() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Mount a USB.
  await sendTestMessage({name: 'mountFakeUsb'});

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .entry-name`;

  // Wait for the directory tree target.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  if (await remoteCall.isSinglePartitionFormat(appId)) {
    await directoryTree.waitForItemByLabel('FAKEUSB');
    await directoryTree.expandTreeItemByLabel('FAKEUSB');
  }
  await directoryTree.waitForItemByLabel('fake-usb');

  // Drag the source and hover it over the target.
  await directoryTree.dragFilesToItemByLabel(
      source, 'fake-usb', /* skipDrop= */ true);

  let navigationPath = '/fake-usb';
  if (await remoteCall.isSinglePartitionFormat(appId)) {
    navigationPath = '/FAKEUSB/fake-usb';
  }
  // Check: drag hovering should navigate the file list.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, navigationPath);
}

/**
 * Tests that dragging a file list item selects its file list row.
 */
export async function transferDragFileListItemSelects() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // The drag has to start in the file list column "name" text, otherwise it
  // starts a drag-selection instead of a drag operation.
  const listItem = `#file-list li[file-name="${ENTRIES.hello.nameText}"]`;
  const source = listItem + ' .entry-name';

  // Wait for the source.
  await remoteCall.waitForElement(appId, source);

  // Wait for the target.
  const target = listItem + ' .detail-icon';
  await remoteCall.waitForElement(appId, target);

  // Check: the file list row should not be selected
  await remoteCall.waitForElement(appId, listItem + ':not([selected])');

  // Drag the source and hover it over the target.
  const skipDrop = true;
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil(
          'fakeDragAndDrop', appId, [source, target, skipDrop]),
      'fakeDragAndDrop failed');

  // Check: the file list row should be selected.
  await remoteCall.waitForElement(appId, listItem + '[selected]');
}

/**
 * Tests that dropping a file on a directory tree item (folder) copies the
 * file to that folder.
 */
export async function transferDragAndDrop() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // Expand Downloads to display "photos" folder in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .entry-name`;

  // Wait for the source.
  await remoteCall.waitForElement(appId, source);

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

  // Drag the source and drop it on the target.
  await directoryTree.dragFilesToItemByLabel(
      source, 'photos', /* skipDrop= */ false);

  // Navigate the file list to the target.
  await directoryTree.selectItemByLabel('photos');

  // Wait for navigation to finish.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/My files/Downloads/photos');

  // Check: the dropped file should appear in the file list.
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([ENTRIES.hello]),
      {ignoreLastModifiedTime: true});
}

/**
 * Tests that dropping a folder on a directory tree item (folder) copies the
 * folder and also updates the directory tree.
 */
export async function transferDragAndDropFolder() {
  // Note that directoryB is a child of directoryA.
  const entries = [ENTRIES.directoryA, ENTRIES.directoryB, ENTRIES.directoryD];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // Expand Downloads to display folder "D" in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');

  // 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 li[file-name="${ENTRIES.directoryA.nameText}"] .entry-name`;

  // Wait for the source.
  await remoteCall.waitForElement(appId, source);

  // Wait for the directory tree target.
  await directoryTree.waitForItemByLabel(ENTRIES.directoryD.nameText);

  // Drag the source and drop it on the target.
  await directoryTree.dragFilesToItemByLabel(
      source, ENTRIES.directoryD.nameText, /* skipDrop= */ false);

  // Check: the dropped folder "A" should appear in the directory tree under
  // the target folder "D" and be expandable (as folder "A" contains "B").
  // signature, so it must be accessed with ['directoryA'].
  await directoryTree.expandTreeItemByLabel(ENTRIES.directoryA.nameText);
}

/*
 * Tests that dragging a file over a directory tree item (folder) navigates
 * the file list to that folder.
 */
export async function transferDragAndHover() {
  const entries = [ENTRIES.hello, ENTRIES.photos];

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, entries, []);

  // Expand Downloads to display "photos" folder in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.expandTreeItemByLabel('Downloads');

  // 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 li[file-name="${ENTRIES.hello.nameText}"] .entry-name`;

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

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

  // Check: drag hovering should navigate the file list.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/My files/Downloads/photos');
}

/**
 * Tests dropping a file originated from the browser.
 */
export async function transferDropBrowserFile() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.hello], []);

  // Send drop event on current directory.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil(
          'fakeDropBrowserFile', appId,
          ['browserfile', 'content', 'text/plain', '#file-list']),
      'fakeDropBrowserFile failed');

  // File should be created.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="browserfile"]');
}

/**
 * Tests that copying a deleted file shows an error.
 */
export async function transferDeletedFile() {
  const entry = ENTRIES.hello;

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);

  // Select the file.
  await remoteCall.waitUntilSelected(appId, entry.nameText);

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

  // Delete the file.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'deleteFile', appId, [entry.nameText]));

  // Wait for completion of file deletion.
  await remoteCall.waitForElementLost(
      appId, `#file-list [file-name="${entry.nameText}"]`);

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

  // Check that the error appears in the feedback panel.
  let element: ElementObject|null = null;
  const caller = getCaller();
  await repeatUntil(async () => {
    element = await remoteCall.waitForElement(
        appId, ['#progress-panel', 'xf-panel-item']);
    const expectedMsg = `Whoops, ${entry.nameText} no longer exists.`;
    const actualMsg = element.attributes['primary-text'];

    if (actualMsg === expectedMsg) {
      return;
    }

    return pending(
        caller,
        `Expected feedback panel msg: "${expectedMsg}", got "${actualMsg}"`);
  });

  // Check that only one line of text is shown.
  chrome.test.assertFalse(!!element!.attributes['secondary-text']);
}

/**
 * Tests that transfer source/destination persists if app window is re-opened.
 */
export async function transferInfoIsRemembered() {
  const entry = ENTRIES.hello;

  // Open files app.
  let appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);

  // Select the file.
  await remoteCall.waitUntilSelected(appId, entry.nameText);

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

  // Tell the background page to never finish the file copy.
  await remoteCall.callRemoteTestUtil(
      'progressCenterNeverNotifyCompleted', appId, []);

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

  // The feedback panel should appear: record the feedback panel text.
  let panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);
  const primaryText = panel.attributes['primary-text'];
  const secondaryText = panel.attributes['secondary-text'];

  // Open a Files app window again.
  appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Check the feedback panel text is remembered.
  panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);
  chrome.test.assertEq(primaryText, panel.attributes['primary-text']);
  chrome.test.assertEq(secondaryText, panel.attributes['secondary-text']);
}

/**
 * Tests that destination text line shows name for USB targets.
 */
export async function transferToUsbHasDestinationText() {
  const entry = ENTRIES.hello;

  // Open files app.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);

  // Mount a USB volume.
  await sendTestMessage({name: 'mountFakeUsbEmpty'});

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

  // Select the file.
  await remoteCall.waitUntilSelected(appId, entry.nameText);

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

  let navigationPath = '/fake-usb';
  if (await remoteCall.isSinglePartitionFormat(appId)) {
    navigationPath = '/FAKEUSB/fake-usb';
  }
  // Select USB volume.
  await directoryTree.navigateToPath(navigationPath);

  // Tell the background page to never finish the file copy.
  await remoteCall.callRemoteTestUtil(
      'progressCenterNeverNotifyCompleted', appId, []);

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

  // Check the feedback panel destination message contains the target device.
  const panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);

  chrome.test.assertTrue(
      panel.attributes['primary-text']!.includes('to fake-usb'),
      'Feedback panel does not contain device name.');
}

/**
 * Tests that dismissing an error notification on the foreground
 * page is propagated to the background page.
 */
export async function transferDismissedErrorIsRemembered() {
  const entry = ENTRIES.hello;

  // Open Files app on Downloads.
  let appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);

  // Select a file to copy.
  await remoteCall.waitUntilSelected(appId, entry.nameText);

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

  // Force all file copy operations to trigger an error.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'forceErrorsOnFileOperations', appId, [true]));

  // Select Downloads.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Downloads');

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

  // Check: an error feedback panel with failure status should appear.
  let errorPanel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);
  // If we've grabbed a reference to a progress panel, it will disappear
  // quickly and be replaced by the error panel, so loop and wait for it.
  while (errorPanel.attributes['indicator'] === 'progress') {
    chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
        'requestAnimationFrame', appId, []));
    errorPanel = await remoteCall.waitForElement(
        appId, ['#progress-panel', 'xf-panel-item']);
  }
  chrome.test.assertEq('failure', errorPanel.attributes['status']);

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

  // Open a Files app window again.
  appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS, [entry], []);

  // Turn off the error generation for file operations.
  chrome.test.assertFalse(await remoteCall.callRemoteTestUtil(
      'forceErrorsOnFileOperations', appId, [false]));

  // Tell the background page to never finish the file copy.
  await remoteCall.callRemoteTestUtil(
      'progressCenterNeverNotifyCompleted', appId, []);

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

  // Check: the first feedback panel item should be a progress panel.
  // If the error persisted then we'd see a summary panel here.
  const progressPanel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);
  chrome.test.assertEq('progress', progressPanel.attributes['indicator']);
}

/**
 * Tests no remaining time displayed for not supported operations like format.
 */
export async function transferNotSupportedOperationHasNoRemainingTimeText() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Show a |format| progress panel.
  await remoteCall.callRemoteTestUtil('sendProgressItem', null, [
    'item-id-1',
    /* ProgressItemType.FORMAT */ 'format',
    /* ProgressItemState.PROGRESSING */ 'progressing',
    'Formatting',
  ]);

  // Check the progress panel is open.
  let panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);

  // Check no remaining time shown for 'format' panel type.
  chrome.test.assertEq('', panel.attributes['secondary-text']);

  // Show a |format| error panel.
  await remoteCall.callRemoteTestUtil('sendProgressItem', null, [
    'item-id-2',
    /* ProgressItemType.FORMAT */ 'format',
    /* ProgressItemState.ERROR */ 'error',
    'Failed',
  ]);

  // Check the progress panel is open.
  panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item#item-id-2']);

  // Check no remaining time shown for 'format' error panel type.
  chrome.test.assertEq('', panel.attributes['secondary-text']);
}

/**
 * Tests updating same panel keeps same message.
 * Use case: crbug/1137229
 */
export async function transferUpdateSamePanelItem() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Show a |format| error in feedback panel.
  await remoteCall.callRemoteTestUtil('sendProgressItem', null, [
    'item-id',
    /* ProgressItemType.FORMAT */ 'format',
    /* ProgressItemState.ERROR */ 'error',
    'Failed',
  ]);

  // Check the error panel is open.
  let panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);

  // Dispatch another |format| feedback panel with the same id and panel type.
  await remoteCall.callRemoteTestUtil('sendProgressItem', null, [
    'item-id',
    /* ProgressItemType.FORMAT */ 'format',
    /* ProgressItemState.ERROR */ 'error',
    'Failed new message',
  ]);

  // Check the progress panel is open.
  panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);

  // Check secondary text is still empty for the error panel.
  chrome.test.assertEq('', panel.attributes['secondary-text']);
}

/**
 * Tests prepraring message shown when the remaining time is zero.
 */
export async function transferShowPreparingMessageForZeroRemainingTime() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Show a |copy| progress in feedback panel.
  await remoteCall.callRemoteTestUtil('sendProgressItem', null, [
    'item-id',
    /* ProgressItemType.COPY */ 'copy',
    /* ProgressItemState.PROGRESSING */ 'progressing',
    'Copying File1.txt to Downloads',
    /* remainingTime*/ 0,
  ]);

  // Check the error panel is open.
  const panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', 'xf-panel-item']);

  // Check secondary text is preparing message.
  chrome.test.assertEq('Preparing', panel.attributes['secondary-text']);
}