chromium/ui/file_manager/integration_tests/file_manager/file_dialog.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 {TestEntryInfo} from '../test_util.js';
import {addEntries, ENTRIES, getCaller, openEntryChoosingWindow, pending, pollForChosenEntry, repeatUntil, sendBrowserTestCommand, sendTestMessage} from '../test_util.js';

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

/**
 * Sends a key event to an open file dialog, after selecting the file |name|
 * entry in the file list.
 *
 * @param name File name shown in the dialog.
 * @param key Key detail for fakeKeyDown event.
 * @param dialog ID of the file dialog window.
 * @return Promise to be fulfilled on success.
 */
async function sendOpenFileDialogKey(
    name: string, key: readonly[string, string, boolean, boolean, boolean],
    dialog: string): Promise<void> {
  await remoteCall.waitUntilSelected(dialog, name);
  await remoteCall.callRemoteTestUtil('fakeKeyDown', dialog, key);
}

/**
 * Clicks a button in the open file dialog, after selecting the file |name|
 * entry in the file list and checking that |button| exists.
 *
 * @param name File name shown in the dialog.
 * @param button Selector of the dialog button.
 * @param dialog ID of the file dialog window.
 * @return Promise to be fulfilled on success.
 */
async function clickOpenFileDialogButton(
    name: string, button: string, dialog: string): Promise<void> {
  await remoteCall.waitUntilSelected(dialog, name);
  await remoteCall.waitForElement(dialog, button);
  const event = [button, 'click'];
  await remoteCall.callRemoteTestUtil('fakeEvent', dialog, event);
}

/**
 * Sends an unload event to an open file dialog (after it is drawn) causing the
 * dialog to shut-down and close.
 *
 * @param dialog ID of the file dialog window.
 * @param element Element to query for drawing.
 * @return Promise to be fulfilled on success.
 */
async function unloadOpenFileDialog(
    dialog: string,
    element: string = '.button-panel button.ok'): Promise<void> {
  await remoteCall.waitForElement(dialog, element);
  await remoteCall.callRemoteTestUtil('unload', dialog, []);
  const errorCount =
      await remoteCall.callRemoteTestUtil('getErrorCount', dialog, []);
  chrome.test.assertEq(0, errorCount);
}

/**
 * Adds basic file entry sets for both 'local' and 'drive', and returns the
 * entry set of the given |volume|.
 *
 * @param volume Name of the volume.
 * @return Promise to resolve({Array<TestEntryInfo>}) on success, the Array
 *     being the basic file entry set of the |volume|.
 */
async function setUpFileEntrySet(volume: string): Promise<TestEntryInfo[]> {
  const localEntryPromise = addEntries(['local'], BASIC_LOCAL_ENTRY_SET);

  const driveEntries = [
    ENTRIES.hello,
    ENTRIES.pinned,
    ENTRIES.testCSEDocument,
    ENTRIES.testCSEFile,
    ENTRIES.testDocument,
    ENTRIES.docxFile,
  ];
  const driveEntryPromise = addEntries(['drive'], driveEntries);

  await Promise.all([localEntryPromise, driveEntryPromise]);
  if (volume === 'drive') {
    return driveEntries;
  }
  return BASIC_LOCAL_ENTRY_SET;
}

/**
 * Adds the basic file entry sets then opens the file dialog on the volume. Once
 * file |name| is shown, select it and click the Ok button.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to select in the dialog.
 * @param useBrowserOpen Whether to launch the dialog from the browser.
 * @return Promise to be fulfilled on success.
 */
async function openFileDialogClickOkButton(
    volume: string, name: string,
    useBrowserOpen: boolean = false): Promise<unknown> {
  const okButton = '.button-panel button.ok:enabled';
  await sendTestMessage(
      {name: 'expectFileTask', fileNames: [name], openType: 'open'});
  const closer = clickOpenFileDialogButton.bind(null, name, okButton);

  const entrySet = await setUpFileEntrySet(volume);
  const result = await remoteCall.openAndWaitForClosingDialog(
                     {type: 'openFile'}, volume, entrySet, closer,
                     useBrowserOpen) as Entry;
  // If the file is opened via the filesystem API, check the name matches.
  // Otherwise, the caller is responsible for verifying the returned URL.
  if (!useBrowserOpen) {
    chrome.test.assertEq(name, result.name);
  }

  return result;
}

/**
 * Clicks the OK button in the provided dialog, expecting the provided `name` to
 * be passed into the `OnFilesImpl()` observer in the C++ test harness.
 *
 * @param appId App window Id.
 * @param name The (single) filename passed to the EXPECT_CALL when verifying
 *     the mocked OnFilesOpenedImpl().
 * @param openType Type of the dialog ('open' or 'saveAs').
 */
async function clickOkButtonExpectName(
    appId: string, name: string, openType: string) {
  await sendTestMessage({name: 'expectFileTask', fileNames: [name], openType});

  const okButton = '.button-panel button.ok:enabled';
  await remoteCall.waitAndClickElement(appId, okButton);
}

/**
 * Adds the basic file entry sets then opens the save file dialog on the volume.
 * Once file |name| is shown, select it and click the Ok button, again clicking
 * Ok in the confirmation dialog.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to select in the dialog.
 * @return Promise to be fulfilled on success.
 */
async function saveFileDialogClickOkButton(
    volume: string, name: string): Promise<void> {
  const caller = getCaller();

  const closer = async (appId: string) => {
    await remoteCall.waitUntilSelected(appId, name);
    await repeatUntil(async () => {
      const element =
          await remoteCall.waitForElement(appId, '#filename-input-textbox');
      if (element.value !== name) {
        return pending(caller, 'Text field not updated');
      }
      return;
    });

    await clickOkButtonExpectName(appId, name, 'saveAs');

    const confirmOkButton = '.files-confirm-dialog .cr-dialog-ok';
    await remoteCall.waitForElement(appId, confirmOkButton);
    await remoteCall.callRemoteTestUtil(
        'fakeEvent', appId, [confirmOkButton, 'click']);
  };

  const entrySet = await setUpFileEntrySet(volume);
  const result =
      await remoteCall.openAndWaitForClosingDialog(
          {type: 'saveFile'}, volume, entrySet, closer, false) as Entry;
  chrome.test.assertEq(name, result.name);
}

/**
 * Adds the basic file entry sets then opens the file dialog on the volume. Once
 * file |name| is shown, select it and verify that the Ok button is disabled.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to select in the dialog where the OK button should be
 *     disabled
 * @param enabledName File name to select where the OK button should be enabled,
 *     used to ensure that switching to |name| results in the OK button becoming
 *     disabled.
 * @param type The dialog type to open.
 * @return Promise to be fulfilled on success.
 */
async function openFileDialogExpectOkButtonDisabled(
    volume: string, name: string, enabledName: string,
    type: 'openFile'|'saveFile' = 'openFile'): Promise<void> {
  const okButton = '.button-panel button.ok:enabled';
  const disabledOkButton = '.button-panel button.ok:disabled';
  const cancelButton = '.button-panel button.cancel';
  const closer = async (dialog: string) => {
    await remoteCall.waitUntilSelected(dialog, enabledName);
    await remoteCall.waitForElement(dialog, okButton);
    await remoteCall.waitUntilSelected(dialog, name);
    await remoteCall.waitForElement(dialog, disabledOkButton);
    clickOpenFileDialogButton(name, cancelButton, dialog);
  };

  const entrySet = await setUpFileEntrySet(volume);
  chrome.test.assertEq(
      undefined,
      await remoteCall.openAndWaitForClosingDialog(
          {type}, volume, entrySet, closer));
}

/**
 * Adds the basic file entry sets then opens the file dialog on the volume. Once
 * file |name| is shown, verifies that it's dimmed according to added classes.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to check for being dimmed in the dialog.
 * @return Promise to be fulfilled on success.
 */
async function openFileDialogExpectEntryDimmed(
    volume: string, name: string): Promise<void> {
  const type = 'openFile';
  const cancelButton = '.button-panel button.cancel';
  const fileEntry = `#file-list [file-name="${name}"]`;
  const closer = async (dialog: string) => {
    const element = await remoteCall.waitForElement(dialog, fileEntry);
    let dimmed = false;
    for (const className of element.attributes['class']!.split(' ')) {
      if (className === 'dim-offline') {
        // The 'dim-offline' class dims an element only if the connection
        // status is 'OFFLINE', which is not something this test is verifying.
        continue;
      }
      if (className.startsWith('dim')) {
        dimmed = true;
        break;
      }
    }
    chrome.test.assertTrue(dimmed, 'The file entry should be dimmed');
    clickOpenFileDialogButton(name, cancelButton, dialog);
  };

  const entrySet = await setUpFileEntrySet(volume);
  chrome.test.assertEq(
      undefined,
      await remoteCall.openAndWaitForClosingDialog(
          {type}, volume, entrySet, closer));
}

/**
 * Adds the basic file entry sets then opens the file dialog on the volume. Once
 * file |name| is shown, select it and click the Cancel button.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to select in the dialog.
 * @return Promise to be fulfilled on success.
 */
async function openFileDialogClickCancelButton(
    volume: string, name: string): Promise<void> {
  const cancelButton = '.button-panel button.cancel';
  const closer = clickOpenFileDialogButton.bind(null, name, cancelButton);

  const entrySet = await setUpFileEntrySet(volume);
  chrome.test.assertEq(
      undefined,
      await remoteCall.openAndWaitForClosingDialog(
          {type: 'openFile'}, volume, entrySet, closer));
}

/**
 * Adds the basic file entry sets then opens the file dialog on the volume. Once
 * file |name| is shown, select it and send an Escape key.
 *
 * @param volume Volume name for  remoteCall.openAndWaitForClosingDialog.
 * @param name File name to select in the dialog.
 * @return Promise to be fulfilled on success.
 */
async function openFileDialogSendEscapeKey(
    volume: string, name: string): Promise<void> {
  const escapeKey = ['#file-list', 'Escape', false, false, false] as const;
  const closer = sendOpenFileDialogKey.bind(null, name, escapeKey);

  const entrySet = await setUpFileEntrySet(volume);
  chrome.test.assertEq(
      undefined,
      await remoteCall.openAndWaitForClosingDialog(
          {type: 'openFile'}, volume, entrySet, closer));
}

/**
 * Tests for display:none status of feedback panels in Files app.
 *
 * @param type Type of dialog to open.
 */
async function checkFeedbackDisplayHidden(type: 'openFile'|'saveFile') {
  // Open dialog of the specified 'type'.
  await openEntryChoosingWindow({type});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);
  // Check the display style of the feedback panels container.
  const element = await remoteCall.waitForElementStyles(
      appId, ['.files-feedback-panels'], ['display']);
  // Check that CSS display style is 'none'.
  chrome.test.assertTrue(element.styles!['display'] === 'none');
}

/**
 * Test file present in Downloads.
 */
function getTestFileName(): string {
  // Type TestEntryInfo's targetPath can be undefined, but the first item
  // from BASIC_LOCAL_ENTRY_SET has value, we need to do type casting here.
  return BASIC_LOCAL_ENTRY_SET[0]!.targetPath;
}

/**
 * Tests opening file dialog on Downloads and closing it with Ok button.
 */
export async function openFileDialogDownloads() {
  return openFileDialogClickOkButton('downloads', getTestFileName());
}

/**
 * Tests opening file dialog sets aria-multiselect true on grid and list.
 */
export async function openFileDialogAriaMultipleSelect() {
  // Open File dialog.
  await openEntryChoosingWindow({type: 'openFile'});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Check: <list> has aria-multiselect set to true.
  const list = 'list#file-list[aria-multiselectable=true]';
  await remoteCall.waitForElement(appId, list);

  // Check: <grid> has aria-multiselect set to true.
  const grid = 'grid#file-list[aria-multiselectable=true]';
  await remoteCall.waitForElement(appId, grid);
}

/**
 * Tests opening save file dialog sets aria-multiselect false on grid and list.
 */
export async function saveFileDialogAriaSingleSelect() {
  // Open Save as dialog.
  await openEntryChoosingWindow({type: 'saveFile'});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Check: <list> has aria-multiselect set to false.
  const list = 'list#file-list[aria-multiselectable=false]';
  await remoteCall.waitForElement(appId, list);

  // Check: <grid> has aria-multiselect set to false.
  const grid = 'grid#file-list[aria-multiselectable=false]';
  await remoteCall.waitForElement(appId, grid);
}

/**
 * Tests opening save file dialog on Downloads and closing it with Ok button.
 */
export async function saveFileDialogDownloads() {
  return saveFileDialogClickOkButton('downloads', getTestFileName());
}

/**
 * Tests opening save file dialog on Downloads and using New Folder button.
 */
export async function saveFileDialogDownloadsNewFolderButton() {
  // Open Save as dialog.
  await openEntryChoosingWindow({type: 'saveFile'});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Check: New Folder button should be enabled and click on it.
  const query = '#new-folder-button:not([disabled])';
  await remoteCall.waitAndClickElement(appId, query);

  // Wait for the new folder with input to appear, assume the rest of the
  // process works (covered by other tests).
  const textInput = '#file-list .table-row[renaming] input.rename';
  await remoteCall.waitForElement(appId, textInput);
}

/**
 * Tests opening file dialog on Downloads and closing it with Cancel button.
 */
export async function openFileDialogCancelDownloads() {
  return openFileDialogClickCancelButton('downloads', getTestFileName());
}

/**
 * Tests opening file dialog on Downloads and closing it with ESC key.
 */
export async function openFileDialogEscapeDownloads() {
  return openFileDialogSendEscapeKey('downloads', getTestFileName());
}

/**
 * Tests the feedback panels are hidden when using an open file dialog.
 */
export async function openFileDialogPanelsDisabled() {
  return checkFeedbackDisplayHidden('openFile');
}

/**
 * Tests the feedback panels are hidden when using a save file dialog.
 */
export async function saveFileDialogPanelsDisabled() {
  return checkFeedbackDisplayHidden('saveFile');
}

/**
 * Test file present in Drive only.
 */
const TEST_DRIVE_FILE = ENTRIES.hello.targetPath;

/**
 * Test file present in Drive only.
 */
const TEST_DRIVE_PINNED_FILE = ENTRIES.pinned.targetPath;

/**
 * Tests opening file dialog on Drive and closing it with Ok button.
 */
export async function openFileDialogDrive() {
  return openFileDialogClickOkButton('drive', TEST_DRIVE_FILE);
}

/**
 * Tests save file dialog on Drive and closing it with Ok button.
 */
export async function saveFileDialogDrive() {
  return saveFileDialogClickOkButton('drive', TEST_DRIVE_FILE);
}

/**
 * Tests that an unpinned file cannot be selected in file open dialogs while
 * offline.
 */
export async function openFileDialogDriveOffline() {
  return openFileDialogExpectOkButtonDisabled(
      'drive', TEST_DRIVE_FILE, TEST_DRIVE_PINNED_FILE);
}

/**
 * Tests that an unpinned file cannot be selected in save file dialogs while
 * offline.
 */
export async function saveFileDialogDriveOffline() {
  return openFileDialogExpectOkButtonDisabled(
      'drive', TEST_DRIVE_FILE, TEST_DRIVE_PINNED_FILE, 'saveFile');
}

/**
 * Tests opening file dialog on Drive and closing it with Ok button.
 */
export async function openFileDialogDriveOfflinePinned() {
  return openFileDialogClickOkButton('drive', TEST_DRIVE_PINNED_FILE);
}

/**
 * Tests save file dialog on Drive and closing it with Ok button.
 */
export async function saveFileDialogDriveOfflinePinned() {
  return saveFileDialogClickOkButton('drive', TEST_DRIVE_PINNED_FILE);
}

/**
 * Tests opening a file from Drive in the browser, ensuring it correctly opens
 * the file URL.
 */
export async function openFileDialogDriveFromBrowser() {
  const url = new URL(
      await openFileDialogClickOkButton('drive', TEST_DRIVE_FILE, true) as
      string);

  chrome.test.assertEq(url.protocol, 'file:');
  chrome.test.assertTrue(
      url.pathname.endsWith(`/root/${TEST_DRIVE_FILE}`), url.pathname);
}

/**
 * Tests opening a hosted doc in the browser, ensuring it correctly navigates to
 * the doc's URL.
 */
export async function openFileDialogDriveHostedDoc() {
  chrome.test.assertEq(
      await openFileDialogClickOkButton(
          'drive', ENTRIES.testDocument.nameText, true),
      'https://document_alternate_link/Test%20Document');
}

/**
 * Tests opening a hosted doc in the browser, ensuring it correctly navigates to
 * the doc's URL.
 */
export async function openFileDialogDriveEncryptedFile() {
  chrome.test.assertEq(
      await openFileDialogClickOkButton(
          'drive', ENTRIES.testCSEFile.nameText, true),
      'https://file_alternate_link/test-encrypted.txt');
}

/**
 * Tests that selecting a hosted doc from a dialog requiring a real file is
 * disabled.
 */
export async function openFileDialogDriveHostedNeedsFile() {
  return openFileDialogExpectOkButtonDisabled(
      'drive', ENTRIES.testDocument.nameText, TEST_DRIVE_FILE);
}

/**
 * Tests that selecting a hosted doc from a dialog requiring a real file is
 * disabled.
 */
export async function saveFileDialogDriveHostedNeedsFile() {
  return openFileDialogExpectOkButtonDisabled(
      'drive', ENTRIES.testDocument.nameText, TEST_DRIVE_FILE, 'saveFile');
}

/**
 * Test that an encrypted (via CSE) file will be marked as grey in a dialog
 * requiring a read file.
 */
export async function openFileDialogDriveCSEGrey() {
  return openFileDialogExpectEntryDimmed('drive', ENTRIES.testCSEFile.nameText);
}

/**
 * Tests that selecting an encrypted (via CSE) file from a dialog requiring a
 * real file is disabled.
 */
export async function openFileDialogDriveCSENeedsFile() {
  return openFileDialogExpectOkButtonDisabled(
      'drive', ENTRIES.testCSEFile.nameText, TEST_DRIVE_FILE);
}

/**
 * Tests opening file dialog on Drive and selecting an office file.
 */
export async function openFileDialogDriveOfficeFile() {
  return openFileDialogClickOkButton('drive', ENTRIES.docxFile.nameText);
}

/**
 * Tests opening file dialog on Drive and selecting multiple files including an
 * office file.
 */
export async function openMultiFileDialogDriveOfficeFile() {
  await setUpFileEntrySet('drive');
  await openEntryChoosingWindow({type: 'openFile', acceptsMultiple: true});
  const appId = await remoteCall.waitForDialog();

  // Wait for initial load to finish.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

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

  // Sort the file names so we can compare the array directly with the entries
  // returned from pollForChosenEntry() without worrying about
  // order.
  const selectFileNames = [
    ENTRIES.hello.nameText,
    ENTRIES.docxFile.nameText,
  ].sort();

  // Select both files with the dialog.
  await remoteCall.waitAndClickElement(
      appId, `#file-list [file-name="${selectFileNames[0]}"]`);
  await remoteCall.waitAndClickElement(
      appId, `#file-list [file-name="${selectFileNames[1]}"]`, {ctrl: true});
  await sendTestMessage(
      {name: 'expectFileTask', fileNames: selectFileNames, openType: 'open'});
  const okButton = '.button-panel button.ok:enabled';
  await remoteCall.waitAndClickElement(appId, okButton);

  const chosenEntries = ((await pollForChosenEntry(getCaller())) as Entry[])
                            .map(entry => entry.name)
                            .sort();
  chrome.test.assertEq(selectFileNames, chosenEntries);
}

/**
 * Tests opening file dialog on Drive and closing it with Cancel button.
 */
export async function openFileDialogCancelDrive() {
  return openFileDialogClickCancelButton('drive', TEST_DRIVE_FILE);
}

/**
 * Tests opening file dialog on Drive and closing it with ESC key.
 */
export async function openFileDialogEscapeDrive() {
  return openFileDialogSendEscapeKey('drive', TEST_DRIVE_FILE);
}

/**
 * Tests opening file dialog, then closing it with an 'unload' event.
 */
export async function openFileDialogUnload() {
  await openEntryChoosingWindow({type: 'openFile'});
  const dialog = await remoteCall.waitForDialog();
  await unloadOpenFileDialog(dialog);
}

/**
 * Tests that the open file dialog's filetype filter does not default to all
 * types.
 */
export async function openFileDialogDefaultFilter() {
  const params = {
    type: 'openFile' as const,
    accepts: [{extensions: ['jpg']}],
    acceptsAllTypes: true,
  };
  await openEntryChoosingWindow(params);
  const dialog = await remoteCall.waitForDialog();

  // Check: 'JPEG image' should be selected.
  const selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option:checked');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
}

/**
 * Tests that the save file dialog's filetype filter defaults to all types.
 */
export async function saveFileDialogDefaultFilter() {
  const params = {
    type: 'saveFile' as const,
    accepts: [{extensions: ['jpg']}],
    acceptsAllTypes: true,
  };
  await openEntryChoosingWindow(params);
  const dialog = await remoteCall.waitForDialog();

  // Check: 'All files' should be selected.
  const selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option:checked');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);
}

/**
 * Tests that the save file dialog's filetype filter can be navigated using the
 * keyboard.
 */
export async function saveFileDialogDefaultFilterKeyNavigation() {
  const params = {
    type: 'saveFile' as const,
    accepts: [{extensions: ['jpg']}],
    acceptsAllTypes: true,
  };
  await openEntryChoosingWindow(params);
  const dialog = await remoteCall.waitForDialog();

  // Check: 'All files' should be selected.
  let selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: up key causes 'JPEG image' to  be selected.
  const selectControl = 'div.file-type';
  const arrowUpKey = ['ArrowUp', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowUpKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);

  // Check: down key causes 'All files' to be selected.
  const arrowDownKey = ['ArrowDown', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowDownKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: another down key doesn't wrap to the top selection.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowDownKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: left key acts like up when control is closed.
  const arrowLeftKey = ['ArrowLeft', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowLeftKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);

  // Check: right key acts like down when control is closed.
  const arrowRightKey = ['ArrowRight', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowRightKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: Enter key expands the select control.
  const enterKey = ['Enter', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...enterKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: second Enter key collapses the select control.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...enterKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: space key expands the select control.
  const spaceKey = [' ', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: second space key collapses the select control.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: Escape key collapses the select control.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');
  const escapeKey = ['Escape', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...escapeKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: tab key collapses the select control.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');
  const tabKey = ['Tab', false, false, false] as const;
  await remoteCall.fakeKeyDown(dialog, selectControl, ...tabKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');

  // Check: tab key collapsing remembers changed selection.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowUpKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
  await remoteCall.fakeKeyDown(dialog, selectControl, ...tabKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);

  // Check: Escape key collapsing remembers changed selection.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowDownKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);
  await remoteCall.fakeKeyDown(dialog, selectControl, ...escapeKey);
  await remoteCall.waitForElementLost(
      dialog, '.file-type div.options[expanded=expanded]');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: left arrow does nothing with control expanded.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...spaceKey);
  await remoteCall.waitForElement(
      dialog, '.file-type div.options[expanded=expanded]');
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowLeftKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('0', selectedFilter.value);
  chrome.test.assertEq('All files', selectedFilter.text);

  // Check: right arrow does nothing with control expanded.
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowUpKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
  await remoteCall.fakeKeyDown(dialog, selectControl, ...arrowRightKey);
  selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option.selected');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
}

/**
 * Tests that filtering works with { acceptsAllTypes: false } and a single
 * filter. Regression test for https://crbug.com/1097448.
 */
export async function saveFileDialogSingleFilterNoAcceptAll() {
  const params = {
    type: 'saveFile' as const,
    accepts: [{extensions: ['jpg']}],
    acceptsAllTypes: false,
  };
  await openEntryChoosingWindow(params);
  const dialog = await remoteCall.waitForDialog();

  // Check: 'JPEG image' should be selected.
  const selectedFilter =
      await remoteCall.waitForElement(dialog, '.file-type option:checked');
  chrome.test.assertEq('1', selectedFilter.value);
  chrome.test.assertEq('JPEG image', selectedFilter.text);
}

/**
 * Opens a "Save As" dialog and clicks OK. Helper for the
 * saveFileDialogExtension* tests.
 *
 * @param extraParams Extra options to pass to chooseEntry().
 * @param expectName Name for the 'expectFileTask' mock expectation.
 * @return The name of the entry from chooseEntry().
 */
async function showSaveAndConfirmExpecting(
    extraParams: chrome.fileSystem.ChooseEntryOptions,
    expectName: string): Promise<string> {
  const caller = getCaller();

  const params = {
    type: 'saveFile' as const,
    accepts: [{extensions: ['jpg']}],
  };
  await openEntryChoosingWindow(Object.assign(params, extraParams));
  const dialog = await remoteCall.waitForDialog();

  // Ensure the input field is ready.
  await remoteCall.waitForElement(dialog, '#filename-input-textbox');

  await clickOkButtonExpectName(dialog, expectName, 'saveAs');
  const entry = await pollForChosenEntry(caller) as Entry;
  return entry.name;
}

/**
 * Tests that a file extension is not automatically added upon confirmation
 * whilst the "All Files" filter is selected on the "Save As" dialog. Note the
 * saveFileDialogDefaultFilter test above verifies that 'All Files' is actually
 * the default in this setup.
 */
export async function saveFileDialogExtensionNotAddedWithNoFilter() {
  // Note these tests use the suggestedName field as a robust way to simulate a
  // user typing into the input field.
  const extraParams = {acceptsAllTypes: true, suggestedName: 'test'};
  const name = await showSaveAndConfirmExpecting(extraParams, 'test');
  chrome.test.assertEq('test', name);
}

/**
 * With no "All Files" option, the JPEG filter should be applied by default, and
 * a ".jpg" extension automatically added on confirm.
 */
export async function saveFileDialogExtensionAddedWithJpegFilter() {
  const extraParams = {acceptsAllTypes: false, suggestedName: 'test'};
  const name = await showSaveAndConfirmExpecting(extraParams, 'test.jpg');
  chrome.test.assertEq('test.jpg', name);
}

/**
 * An extension should only be added if the user didn't provide one, even if it
 * doesn't match the current filter for JPEG files (i.e. /\.(jpg)$/i).
 */
export async function saveFileDialogExtensionNotAddedWhenProvided() {
  const extraParams = {acceptsAllTypes: false, suggestedName: 'foo.png'};
  const name = await showSaveAndConfirmExpecting(extraParams, 'foo.png');
  chrome.test.assertEq('foo.png', name);
}

/**
 * Tests that context menu on File List for file picker dialog.
 * File picker dialog displays fewer menu options than full Files app. For
 * example copy/paste commands are disabled. Right-click on a file/folder should
 * show context menu, whereas right-clicking on the blank parts of file list
 * should NOT display the context menu.
 *
 * crbug.com/917975 crbug.com/983507.
 */
export async function openFileDialogFileListShowContextMenu() {
  // Make sure the file picker will open to Downloads.
  sendBrowserTestCommand({name: 'setLastDownloadDir'});

  // Add entries to Downloads.
  await addEntries(['local'], BASIC_LOCAL_ENTRY_SET);

  // Open file picker dialog.
  await openEntryChoosingWindow({type: 'openFile'});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Wait for files to be displayed.
  const expectedRows = [
    ['Play files', '--', 'Folder'],
    ['Downloads', '--', 'Folder'],
    ['Linux files', '--', 'Folder'],
  ];
  await remoteCall.waitForFiles(
      appId, expectedRows, {ignoreLastModifiedTime: true});

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

  // Right-click "photos" folder to show context menu.
  await remoteCall.waitAndRightClick(appId, '#file-list [file-name="photos"]');

  // Wait until the context menu appears.
  const menuVisible = '#file-context-menu:not([hidden])';
  await remoteCall.waitForElement(appId, menuVisible);

  // Dismiss context menu.
  const escKey = ['Escape', false, false, false] as const;
  await remoteCall.fakeKeyDown(appId, menuVisible, ...escKey);
  await remoteCall.waitForElementLost(appId, menuVisible);

  // Right-click inside of #file-list (in an empty space).
  await remoteCall.rightClickFileListBlankSpace(appId);

  // Check that context menu is NOT displayed because there is no visible menu
  // items.
  await remoteCall.waitForElement(appId, '#file-context-menu[hidden]');
}

/**
 * Tests that select all is disabled in the gear menu for an open file dialog.
 */
export async function openFileDialogSelectAllDisabled() {
  // Open file picker dialog.
  await openEntryChoosingWindow({type: 'openFile'});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Wait for the gear menu button to appear and click it.
  await remoteCall.waitAndClickElement(appId, '#gear-button');

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

  // Check: #select-all command is shown, but disabled.
  await remoteCall.waitForElement(
      appId,
      '#gear-menu ' +
          'cr-menu-item[command="#select-all"][disabled]:not([hidden])');
}

/**
 * Tests that select all is enabled in the gear menu for an open multiple files
 * dialog. crbug.com/937251
 */
export async function openMultiFileDialogSelectAllEnabled() {
  // Make sure the file picker will open to Downloads.
  sendBrowserTestCommand({name: 'setLastDownloadDir'});

  // Open file picker dialog with support for selecting multiple files.
  await openEntryChoosingWindow({type: 'openFile', acceptsMultiple: true});
  const appId = await remoteCall.waitForDialog();

  // Wait to finish initial load.
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Wait for the gear menu button to appear and click it.
  await remoteCall.waitAndClickElement(appId, '#gear-button');

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

  // Check: #select-all command is shown, but enabled.
  await remoteCall.waitForElement(
      appId,
      '#gear-menu ' +
          'cr-menu-item[command="#select-all"]:not([disabled]):not([hidden])');
}

/**
 * Tests open file dialog on a GuestOS volume. Check that the placeholder is
 * shown in the dialog and that clicking on it mounts the volume. We don't
 * bother actually opening a file since once it's mounted it works like any
 * other local FUSE volume.
 */
export async function openFileDialogGuestOs() {
  // Register a fake GuestOs guest.
  await sendTestMessage({
    name: 'registerMountableGuest',
    displayName: 'Bluejohn',
    canMount: true,
    vmType: 'bruschetta',
  });

  // Open the open file dialog.
  await openEntryChoosingWindow({type: 'openFile'});

  // Wait for the dialog to be fully loaded.
  const appId = await remoteCall.waitForWindow();
  await remoteCall.waitForElement(appId, '#file-list');
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Click the Guest OS placeholder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectPlaceholderItemByType('bruschetta');

  // Wait for the directory scanning to finish to guarantee the FileWatcher call
  // is finished.
  await remoteCall.waitForElement(
      appId, `#list-container[scan-completed="Bluejohn"]`);

  // Wait for the actual volume to appear.
  await directoryTree.waitForItemByType('bruschetta');
}

/**
 * Tests save file dialog on a GuestOS volume. Check that the placeholder is
 * shown in the dialog and that clicking on it mounts the volume. We don't
 * bother actually saving a file since once it's mounted it works like any other
 * local FUSE volume.
 */
export async function saveFileDialogGuestOs() {
  // Register a fake GuestOs guest.
  await sendTestMessage({
    name: 'registerMountableGuest',
    displayName: 'Bluejohn',
    canMount: true,
    vmType: 'bruschetta',
  });

  // Open the save file dialog.
  await openEntryChoosingWindow({type: 'saveFile'});

  // Wait for the dialog to be fully loaded.
  const appId = await remoteCall.waitForWindow();
  await remoteCall.waitForElement(appId, '#file-list');
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Click the Guest OS placeholder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectPlaceholderItemByType('bruschetta');

  // Wait for the directory scanning to finish to guarantee the FileWatcher call
  // is finished.
  await remoteCall.waitForElement(
      appId, `#list-container[scan-completed="Bluejohn"]`);

  // Wait for the actual volume to appear.
  await directoryTree.waitForItemByType('bruschetta');
}