chromium/ui/file_manager/integration_tests/file_manager/drive_specific.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 {createTestFile, ENTRIES, EntryType, getCaller, getUserActionCount, pending, repeatUntil, RootPath, sendTestMessage, TestEntryInfo, waitForMediaApp} from '../test_util.js';

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

/**
 * Expected files shown in the search results for 'hello'
 */
const SEARCH_RESULTS_ENTRY_SET = [
  ENTRIES.hello,
];

/**
 * Expected text shown in the Enable Docs Offline dialog.
 */
const ENABLE_DOCS_OFFLINE_MESSAGE =
    'Enable Google Docs Offline to make Docs, Sheets and Slides ' +
    'available offline.';

/** The id attribute of the dismiss button in the educational banner. */
async function getDismissButtonId(appId: string) {
  return await remoteCall.isCrosComponents(appId) ? '#dismiss-button' :
                                                    '#dismiss-button-old';
}

/**
 * Opens the Enable Docs Offline dialog and waits for it to appear in the given
 * `appId` window.
 *
 */
async function openAndWaitForEnableDocsOfflineDialog(appId: string) {
  // Simulate Drive signalling Files App to open a dialog.
  await sendTestMessage({name: 'displayEnableDocsOfflineDialog'});

  // Check: the Enable Docs Offline dialog should appear.
  const dialogText = await remoteCall.waitForElement(
      appId, '.cr-dialog-container.shown .cr-dialog-text');
  chrome.test.assertEq(ENABLE_DOCS_OFFLINE_MESSAGE, dialogText.text);
}

/**
 * Waits for getLastDriveDialogResult to return the given |expectedResult|.
 */
async function waitForLastDriveDialogResult(expectedResult: string) {
  const caller = getCaller();
  await repeatUntil(async () => {
    const result = await sendTestMessage({name: 'getLastDriveDialogResult'});
    if (result === expectedResult) {
      return;
    }
    return pending(
        caller, 'Waiting for getLastDriveDialogResult: expected %s, actual %s',
        expectedResult, result);
  });
}

/**
 * Waits for a given notification to appear.
 * @param notificationId ID of notification to wait for.
 */
async function waitForNotification(notificationId: string) {
  const caller = getCaller();
  await repeatUntil(async () => {
    const idSet = await remoteCall.callRemoteTestUtil<Record<string, boolean>>(
        'getNotificationIDs', null, []);
    return !idSet[notificationId] ?
        pending(
            caller, 'Waiting for notification "%s" to appear.',
            notificationId) :
        null;
  });
}

/**
 * Tests opening the "Offline" on the sidebar navigation by clicking the icon,
 * and checks contents of the file list. Only the entries "available offline"
 * should be shown. "Available offline" entries are hosted documents and the
 * entries cached by DriveCache.
 */
export async function driveOpenSidebarOffline() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Click the icon of the Offline volume.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Offline');

  // Check: the file list should display the offline file set.
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(OFFLINE_ENTRY_SET));
}

/**
 * Tests opening the "Shared with me" on the sidebar navigation by clicking the
 * icon, and checks contents of the file list. Only the entries labeled with
 * "shared-with-me" should be shown.
 */
export async function driveOpenSidebarSharedWithMe() {
  // Open Files app on Drive containing "Shared with me" file entries.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET.concat([
        ENTRIES.sharedWithMeDirectory,
        ENTRIES.sharedWithMeDirectoryFile,
      ]));

  // Click the icon of the Shared With Me volume.
  // Use the icon for a click target.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Shared with me');

  // Wait until the breadcrumb path is updated.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Shared with me');

  // Verify the file list.
  await remoteCall.waitForFiles(
      appId,
      TestEntryInfo.getExpectedRows(
          SHARED_WITH_ME_ENTRY_SET.concat([ENTRIES.sharedWithMeDirectory])));

  // Navigate to the directory within Shared with me.
  chrome.test.assertFalse(!await remoteCall.callRemoteTestUtil(
      'openFile', appId, ['Shared Directory']));

  // Wait until the breadcrumb path is updated.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/Shared with me/Shared Directory');

  // Verify the file list.
  await remoteCall.waitForFiles(
      appId,
      TestEntryInfo.getExpectedRows([ENTRIES.sharedWithMeDirectoryFile]));
}

/**
 * Tests that pressing enter after typing a search shows all of
 * the results for that query.
 */
export async function drivePressEnterToSearch() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  remoteCall.typeSearchText(appId, 'hello');

  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeEvent', appId, ['#search-box cr-input', 'focus']));
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId,
      ['#search-box cr-input', 'Enter', false, false, false]));
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(SEARCH_RESULTS_ENTRY_SET));

  // Fetch A11y messages.
  const a11yMessages = await remoteCall.callRemoteTestUtil<string[]>(
      'getA11yAnnounces', appId, []);
  chrome.test.assertEq(1, a11yMessages.length, 'Missing a11y message');
  chrome.test.assertEq('Showing results for hello.', a11yMessages[0]);

  return appId;
}

/**
 * Tests that pressing the clear search button announces an a11y message and
 * shows all files/folders.
 */
export async function drivePressClearSearch() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET);

  // Start the search from a sub-folder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My Drive/photos');

  // Search the text.
  remoteCall.typeSearchText(appId, 'hello');

  // Wait for the result in the file list.
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(SEARCH_RESULTS_ENTRY_SET));

  // Click on the clear search button.
  await remoteCall.waitAndClickElement(appId, '#search-box cr-input .clear');

  // Wait for fil list to display all files.
  await remoteCall.waitForFiles(appId, []);

  // Check that a11y message for clearing the search term has been issued.
  const a11yMessages = await remoteCall.callRemoteTestUtil<string[]>(
      'getA11yAnnounces', appId, []);
  chrome.test.assertEq(2, a11yMessages.length, 'Missing a11y message');
  chrome.test.assertEq(
      'Search text cleared, showing all files and folders.', a11yMessages[1]);

  // The breadcrumbs should return back to the previous original folder.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(
      appId, '/My Drive/photos');
}

/**
 * Tests that pinning multiple files affects the pin action of individual
 * files.
 */
export async function drivePinMultiple() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

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

  // Open the context menu once the file is selected.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Check that the pin action is unticked, i.e. the action will pin the file.
  await remoteCall.waitForElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"]:not([checked])');

  // Additionally select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]', {shift: true});
  await remoteCall.waitForElement(appId, '[file-name="hello.txt"][selected]');

  // Open the context menu with both files selected.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Pin both files.
  await remoteCall.waitAndClickElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"]:not([checked])');

  // Wait for the toggle pinned async action to finish, so the next call to
  // display context menu is after the action has finished.
  await remoteCall.waitForElement(appId, '#file-context-menu[hidden]');

  // Wait for the pinned action to finish, it's flagged in the file list by
  // removing CSS class "dim-offline" and adding class "pinned".
  await remoteCall.waitForElementLost(
      appId, '#file-list .dim-offline[file-name="world.ogv"]');
  await remoteCall.waitForElement(
      appId, '#file-list .pinned[file-name="world.ogv"] xf-inline-status');

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

  // Wait for hello.txt to be unselected.
  await remoteCall.waitForElement(
      appId, '[file-name="hello.txt"]:not([selected])');

  // Open the context menu for world.ogv.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Check that the pin action is ticked, i.e. the action will unpin the file.
  await remoteCall.waitForElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"][checked]');
}

/**
 * Tests that pinning hosted files without the required extensions is disabled,
 * and that it does not affect multiple selections with non-hosted files.
 */
export async function drivePinHosted() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Select Test Document.gdoc.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="Test Document.gdoc"]');
  await remoteCall.waitForElement(
      appId, '[file-name="Test Document.gdoc"][selected]');

  // Open the context menu once the file is selected.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Check that the pin action is disabled and unticked.
  await remoteCall.waitForElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"][disabled]:not([checked])');

  // Additionally select hello.txt.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]', {shift: true});
  await remoteCall.waitForElement(appId, '[file-name="hello.txt"][selected]');

  // Open the context menu with both files selected.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // The pin action should be enabled to pin only hello.txt, so select it.
  await remoteCall.waitAndClickElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"]:not([checked]):not([disabled])');

  // Wait for the toggle pinned async action to finish, so the next call to
  // display context menu is after the action has finished.
  await remoteCall.waitForElement(appId, '#file-context-menu[hidden]');

  // Wait for the pinned action to finish, it's flagged in the file list by
  // removing CSS class "dim-offline" and adding class "pinned".
  await remoteCall.waitForElementLost(
      appId, '#file-list .dim-offline[file-name="hello.txt"]');
  await remoteCall.waitForElement(
      appId, '#file-list .pinned[file-name="hello.txt"] xf-inline-status');

  // Test Document.gdoc should not be pinned however.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Test Document.gdoc"]:not(.pinned)');


  // Open the context menu with both files selected.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Check that the pin action is ticked, i.e. the action will unpin the file.
  await remoteCall.waitForElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"][checked]:not([disabled])');
}

/**
 * Tests pinning a file to a mobile network.
 * TODO(b/296960734): Fix this test once the notification has been fixed.
 */
export async function drivePinFileMobileNetwork() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);
  const caller = getCaller();
  await sendTestMessage({name: 'useCellularNetwork'});
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  await repeatUntil(() => {
    return navigator.connection.type !== 'cellular' ?
        pending(caller, 'Network state is not changed to cellular.') :
        null;
  });
  await remoteCall.waitForElement(appId, ['.table-row[selected]']);
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseRightClick', appId, ['.table-row[selected]']));

  // Wait for the menu to appear and click on toggle pinned.
  await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])');
  await remoteCall.waitAndClickElement(
      appId, '[command="#toggle-pinned"]:not([hidden]):not([disabled])');

  // Wait for the toggle pinned async action to finish, so the next call to
  // display context menu is after the action has finished.
  await remoteCall.waitForElement(appId, '#file-context-menu[hidden]');

  // Wait for the pinned action to finish, it's flagged in the file list by
  // removing CSS class "dim-offline".
  await remoteCall.waitForElementLost(
      appId, '#file-list .dim-offline[file-name="hello.txt"]');


  // Open context menu again.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeEvent', appId, ['#file-list', 'contextmenu']));

  // Check: File is pinned.
  await remoteCall.waitForElement(appId, '[command="#toggle-pinned"][checked]');
  await remoteCall.waitForElement(
      appId, '#file-list .pinned[file-name="hello.txt"] xf-inline-status');
  await waitForNotification('disabled-mobile-sync');
  await sendTestMessage({
    name: 'clickNotificationButton',
    extensionId: FILE_MANAGER_EXTENSIONS_ID,
    notificationId: 'disabled-mobile-sync',
    index: 0,
  });
  await repeatUntil(async () => {
    const preferences =
        await remoteCall
            .callRemoteTestUtil<chrome.fileManagerPrivate.Preferences>(
                'getPreferences', null, []);
    return !preferences.driveSyncEnabledOnMeteredNetwork ?
        pending(caller, 'Drive sync is still disabled.') :
        null;
  });
}

/**
 * Tests that the pinned toggle in the toolbar updates on pinned state changes
 * within fake entries.
 */
export async function drivePinToggleUpdatesInFakeEntries() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Navigate to the Offline fake entry.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Offline');

  // Bring up the context menu for test.txt.
  await remoteCall.waitAndRightClick(
      appId, '#file-list [file-name="test.txt"]');

  // The pinned toggle should update to be checked.
  await remoteCall.waitForElementJelly(
      appId, '#pinned-toggle-jelly[selected]', '#pinned-toggle[checked]');

  // Unpin the file.
  await remoteCall.waitAndClickElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"][checked]');

  // The pinned toggle should change to be unchecked.
  await remoteCall.waitForElementJelly(
      appId, '#pinned-toggle-jelly:not([selected])',
      '#pinned-toggle:not([checked])');

  // Navigate to the Shared with me fake entry.
  await directoryTree.navigateToPath('/Shared with me');

  // Bring up the context menu for test.txt.
  await remoteCall.waitAndRightClick(
      appId, '#file-list [file-name="test.txt"]');

  // The pinned toggle should remain unchecked.
  await remoteCall.waitForElementJelly(
      appId, '#pinned-toggle-jelly:not([selected])',
      '#pinned-toggle:not([checked])');

  // Pin the file.
  await remoteCall.waitAndClickElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"]:not([checked])');

  // The pinned toggle should change to be checked.
  await remoteCall.waitForElementJelly(
      appId, '#pinned-toggle-jelly[selected]', '#pinned-toggle[checked]');
}

/**
 * Tests that pressing Ctrl+A (select all files) from the search box doesn't
 * put the Files App into check-select mode (crbug.com/849253).
 */
export async function drivePressCtrlAFromSearch() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Focus the search box.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId, ['#search-button']));

  // Wait for the search box to be visible.
  await remoteCall.waitForElement(
      appId, ['#search-box cr-input:not([hidden])']);

  // Press Ctrl+A inside the search box.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeKeyDown', appId, ['#search-box cr-input', 'A', true, false, false]));

  // Check we didn't enter check-select mode.
  await remoteCall.waitForElement(appId, ['body:not(.check-select)']);
}

/**
 * Verify that "Available Offline" is not available from the gear menu for a
 * drive file.
 */
export async function driveAvailableOfflineGearMenu() {
  const pinnedMenuQuery = '#file-context-menu:not([hidden]) ' +
      'cr-menu-item[command="#toggle-pinned"]:not([disabled])';

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

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

  // Wait for the entry to be selected.
  await remoteCall.waitForElement(appId, '.table-row[selected]');

  // Click on the icon of the file to check select it
  await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId,
      ['#file-list .table-row[selected] .detail-checkmark']);

  // Ensure gear button is available
  await remoteCall.waitForElement(appId, '#selection-menu-button');

  // Click on gear menu and ensure "Available Offline" is not shown.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId, ['#selection-menu-button']));

  // Check that "Available Offline" is not shown in the menu. This element is
  // hidden via a display:none css rule, so check that.
  const e = await remoteCall.waitForElementStyles(
      appId, pinnedMenuQuery, ['display']);
  chrome.test.assertEq('none', e.styles?.['display']);
}

/**
 * Verify that "Available Offline" is not available from the gear menu for a
 * drive directory.
 */
export async function driveAvailableOfflineDirectoryGearMenu() {
  const pinnedMenuQuery = '#file-context-menu:not([hidden]) ' +
      'cr-menu-item[command="#toggle-pinned"]:not([disabled])';

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

  // Select a file.
  await remoteCall.waitUntilSelected(appId, 'photos');

  // Wait for the entry to be selected.
  await remoteCall.waitForElement(appId, '.table-row[selected]');

  // Click on the icon of the file to check select it
  await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId,
      ['#file-list .table-row[selected] .detail-checkmark']);

  // Ensure gear button is available
  await remoteCall.waitForElement(appId, '#selection-menu-button');

  // Click on gear menu and ensure "Available Offline" is not shown.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseClick', appId, ['#selection-menu-button']));

  // Check that "Available Offline" is not shown in the menu. This element is
  // hidden via a display:none css rule, so check that.
  const e = await remoteCall.waitForElementStyles(
      appId, pinnedMenuQuery, ['display']);
  chrome.test.assertEq('none', e.styles?.['display']);
}

/**
 * Verify that the "Available Offline" toggle in the action bar appears and
 * changes according to the selection.
 */
export async function driveAvailableOfflineActionBar() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  // Check the "Available Offline" toggle is currently hidden as no file is
  // currently selected.
  await remoteCall.waitForElement(
      appId, '#action-bar #pinned-toggle-wrapper[hidden]');

  // Select a hosted file.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="Test Document.gdoc"]');

  // Wait for the entry to be selected.
  await remoteCall.waitForElement(appId, '.table-row[selected]');

  // Check the "Available Offline" toggle is shown in the action bar, but
  // disabled.
  await remoteCall.waitForElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly[disabled]:not([selected])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle[disabled]:not([checked])');

  // Now select a non-hosted file.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="hello.txt"]');

  // Check the "Available Offline" toggle is now enabled, and pin the file.
  remoteCall.waitAndClickElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly:not([disabled]):not([selected])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle:not([disabled]):not([checked])');

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


  // Hover cursor over the pinned icon.
  chrome.test.assertTrue(await remoteCall.callRemoteTestUtil(
      'fakeMouseOver', appId,
      ['#file-list .pinned[file-name="hello.txt"] xf-inline-status']));

  // Verify the correct tooltip is displayed.
  const tooltip = await remoteCall.waitForElement(
      appId, ['files-tooltip[visible=true]', '#label']);
  chrome.test.assertEq('Available offline', tooltip.text);

  // Check the "Available Offline" toggle is enabled and checked.
  await remoteCall.waitForElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly[selected]:not([disabled])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle[checked]:not([disabled])');

  // Select another file that is not pinned.
  await remoteCall.waitAndClickElement(
      appId, '#file-list [file-name="world.ogv"]');

  // Check the "Available Offline" toggle is enabled and unchecked.
  await remoteCall.waitForElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly:not([disabled]):not([selected])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle:not([disabled]):not([checked])');

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

  // Check the "Available Offline" toggle is enabled and checked.
  await remoteCall.waitForElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly[selected]:not([disabled])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle[checked]:not([disabled])');

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

  // Check the "Available Offline" toggle is still available in the action bar.
  await remoteCall.waitForElementJelly(
      appId,
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle-jelly[selected]:not([disabled])',
      '#action-bar #pinned-toggle-wrapper:not([hidden]) ' +
          '#pinned-toggle[checked]:not([disabled])');
}

/**
 * Tests following links to folders.
 */
export async function driveLinkToDirectory() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET.concat([
        ENTRIES.directoryA,
        ENTRIES.directoryB,
        ENTRIES.directoryC,
        ENTRIES.deeplyBuriedSmallJpeg,
        ENTRIES.linkGtoB,
        ENTRIES.linkHtoFile,
        ENTRIES.linkTtoTransitiveDirectory,
      ]));

  // Select the link
  await remoteCall.waitUntilSelected(appId, 'G');
  await remoteCall.waitForElement(appId, '.table-row[selected]');

  // Ensure the "G" directory has the shortcut class applied.
  await remoteCall.waitForElement(appId, '#file-list [file-name="G"].shortcut');

  // Open the link
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['G']));

  // Check the contents of current directory.
  await remoteCall.waitForFiles(appId, [ENTRIES.directoryC.getExpectedRow()]);
}

/**
 * Tests opening files through folder links.
 */
export async function driveLinkOpenFileThroughLinkedDirectory() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET.concat([
        ENTRIES.directoryA,
        ENTRIES.directoryB,
        ENTRIES.directoryC,
        ENTRIES.deeplyBuriedSmallJpeg,
        ENTRIES.linkGtoB,
        ENTRIES.linkHtoFile,
        ENTRIES.linkTtoTransitiveDirectory,
      ]));

  // Navigate through link.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['G']));
  await remoteCall.waitForFiles(appId, [ENTRIES.directoryC.getExpectedRow()]);
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['C']));
  await remoteCall.waitForFiles(
      appId, [ENTRIES.deeplyBuriedSmallJpeg.getExpectedRow()]);

  await sendTestMessage(
      {name: 'expectFileTask', fileNames: ['deep.jpg'], openType: 'launch'});
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['deep.jpg']));

  // The MediaApp window should open for the image.
  await waitForMediaApp();
}

/**
 * Tests opening files through transitive links.
 */
export async function driveLinkOpenFileThroughTransitiveLink() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET.concat([
        ENTRIES.directoryA,
        ENTRIES.directoryB,
        ENTRIES.directoryC,
        ENTRIES.deeplyBuriedSmallJpeg,
        ENTRIES.linkGtoB,
        ENTRIES.linkHtoFile,
        ENTRIES.linkTtoTransitiveDirectory,
      ]));

  // Navigate through link.
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['T']));
  await remoteCall.waitForFiles(
      appId, [ENTRIES.deeplyBuriedSmallJpeg.getExpectedRow()]);

  await sendTestMessage(
      {name: 'expectFileTask', fileNames: ['deep.jpg'], openType: 'launch'});
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('openFile', appId, ['deep.jpg']));

  // The MediaApp window should open for the image.
  await waitForMediaApp();
}

/**
 * Tests that the welcome banner appears when a Drive volume is opened.
 */
export async function driveWelcomeBanner() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  await remoteCall.isolateBannerForTesting(appId, 'drive-welcome-banner');
  const driveWelcomeBannerQuery = '#banners > drive-welcome-banner';
  const driveWelcomeBannerDismissButtonQuery = [
    '#banners > drive-welcome-banner',
    'educational-banner',
    await getDismissButtonId(appId),
  ];

  // Open the Drive volume in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Google Drive');

  // Check: the Drive welcome banner should appear.
  await remoteCall.waitForElement(appId, driveWelcomeBannerQuery);

  // Close the Drive welcome banner.
  await remoteCall.waitAndClickElement(
      appId, driveWelcomeBannerDismissButtonQuery);

  await remoteCall.waitForElement(
      appId, '#banners > drive-welcome-banner[hidden]');
}

/**
 * Tests that the Drive offline info banner appears when a Drive volume is
 * opened.
 */
export async function driveOfflineInfoBanner() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  await remoteCall.isolateBannerForTesting(
      appId, 'drive-offline-pinning-banner');
  const driveOfflineBannerShownQuery =
      '#banners > drive-offline-pinning-banner:not([hidden])';
  const driveOfflineBannerHiddenQuery =
      '#banners > drive-offline-pinning-banner[hidden]';
  const driveOfflineLearnMoreLinkQuery =
      ['#banners > drive-offline-pinning-banner', '[slot="extra-button"]'];

  // Check: the Drive Offline info banner should appear.
  await remoteCall.waitForElement(appId, driveOfflineBannerShownQuery);

  // Click on the 'Learn more' button.
  await remoteCall.waitAndClickElement(appId, driveOfflineLearnMoreLinkQuery);

  // Check: the Drive offline info banner should disappear.
  await remoteCall.waitForElement(appId, driveOfflineBannerHiddenQuery);

  // Navigate to a different directory within Drive.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My Drive/photos');

  // Check: the Drive offline info banner should stay hidden.
  await remoteCall.waitForElement(appId, driveOfflineBannerHiddenQuery);
}

/**
 * Tests that the encryption badge appears next to the CSE file when a Drive
 *  volume is opened.
 */
export async function driveEncryptionBadge() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.hello, ENTRIES.testCSEFile]);

  // Check: encrypted file has a badge.
  const encrypted = await remoteCall.waitForElementStyles(
      appId, '#file-list [file-name="test-encrypted.txt"] .encrypted-icon',
      ['display', 'visibility']);
  chrome.test.assertNe('none', encrypted.styles?.['display']);
  chrome.test.assertEq('visible', encrypted.styles?.['visibility']);

  // Check: the badge is included in accessibility labels.
  const row = await remoteCall.waitForElementStyles(
      appId, '#file-list [file-name="test-encrypted.txt"]',
      ['aria-labelledby', 'display', 'visibility']);
  const ariaLabelledBy = row.attributes['aria-labelledby']!;
  const encryptedBadgeId = ariaLabelledBy.split(/ +/).filter(
      (id) => id.indexOf('encrypted') !== -1)[0];
  chrome.test.assertTrue(
      encryptedBadgeId !== undefined,
      'no encrypted label found in aria for the encrypted file');
  const encryptedBadgeElements =
      await remoteCall.queryElements(appId, `#${encryptedBadgeId}`);
  chrome.test.assertEq(
      1, encryptedBadgeElements.length,
      'no referenced encrypted label element found');

  // Check: non-encrypted file doesn't have a badge.
  const plain = await remoteCall.queryElements(
      appId, ['#file-list [file-name="hello.txt"] .encrypted-icon']);
  chrome.test.assertEq(0, plain.length);
}

/**
 * Tests that the inline sync status "in progress" icon is displayed in "My
 * Drive" as the file starts syncing then disappears as it finishes syncing
 * (i.e., the file reaches 100% progress).
 */
export async function driveInlineSyncStatusSingleFileProgressEvents() {
  const toBeUploaded = new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video.ogv',
    thumbnailFileName: 'image.png',
    targetPath: 'toBeUploaded.ogv',
    mimeType: 'video/ogg',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'toBeUploaded.ogv',
    sizeText: '56 KB',
    typeText: 'OGG video',
    availableOffline: true,
  });

  // Open Files app on Drive and copy over entry to be uploaded.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [toBeUploaded]);

  // Fake the file starting to sync.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${toBeUploaded.targetPath}`,
    progress: 50,
  });

  // Verify this data reaches the UI as a progress value of 50%.
  const inlineStatus = await remoteCall.waitForElement(
      appId, 'xf-inline-status[sync-status=in_progress]');

  chrome.test.assertEq(Number(inlineStatus.attributes['progress']), 0.5);

  // Fake the file finishing syncing.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${toBeUploaded.targetPath}`,
    progress: 100,
  });

  // Verify the "sync in progress" icon is no longer displayed.
  await remoteCall.waitForElementLost(
      appId, 'xf-inline-status[sync-status=in_progress]');
}

/**
 * Tests that the inline sync status icons are displayed in Drive on parent
 * folders containing entries and that child entries' statuses are aggregated
 * respecting the order of precedence (failed > in progress > completed).
 */
export async function driveInlineSyncStatusParentFolderProgressEvents() {
  const parentDir = new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'some_folder',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: 'some_folder',
    sizeText: '--',
    typeText: 'Folder',
  });

  const toBeUploaded = new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video.ogv',
    thumbnailFileName: 'image.png',
    targetPath: 'some_folder/toBeUploaded.ogv',
    mimeType: 'video/ogg',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'toBeUploaded.ogv',
    sizeText: '56 KB',
    typeText: 'OGG video',
    availableOffline: true,
  });

  const toFailUploading = new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video.ogv',
    thumbnailFileName: 'image.png',
    targetPath: 'some_folder/toFailUploading.ogv',
    mimeType: 'video/ogg',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'toFailUploading.ogv',
    sizeText: '56 KB',
    typeText: 'OGG video',
    availableOffline: true,
  });

  // Open Files app on Drive and copy over entry to be uploaded.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [parentDir, toBeUploaded, toFailUploading]);

  // Fake syncing both files to Drive.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${toBeUploaded.targetPath}`,
    progress: 50,
  });
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${toFailUploading.targetPath}`,
    progress: 50,
  });
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${parentDir.targetPath}`,
    progress: 50,
  });
  // States:
  // some_folder - syncing in progress
  // some_folder/toBeUploaded - syncing in progress
  // some_folder/toFailUploading - syncing in progress

  const syncInProgressQuery = 'xf-inline-status[sync-status=in_progress]';
  const syncQueuedQuery = 'xf-inline-status[sync-status=queued]';

  // Verify the "sync in progress" icon is displayed in the parent "some_folder"
  // folder.
  await remoteCall.waitForElement(appId, syncInProgressQuery);

  // Go inside the some_folder folder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My Drive/some_folder');

  // Fake toFailUploading.ogv failing to sync to Drive.
  await sendTestMessage({
    name: 'setDriveSyncError',
    path: `/root/${toFailUploading.targetPath}`,
  });
  // States:
  // some_folder - syncing in progress
  // some_folder/toBeUploaded - syncing in progress
  // some_folder/toFailUploading - syncing failed (when file fail to sync, their
  // status changes back to "queued")

  // Verify the "sync queued" icon is displayed.
  // (failed > in progress)
  await remoteCall.waitForElement(appId, syncQueuedQuery);

  // Fake root/some_folder/world.ogv finishing syncing.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${toBeUploaded.targetPath}`,
    progress: 100,
  });
  // States:
  // toBeUploaded - syncing completed
  // toFailUploading - syncing failed

  // Verify the "sync queued" icon is still displayed in the parent folder.
  // (failed > completed)
  await remoteCall.waitForElement(appId, syncQueuedQuery);
}

/**
 * Tests that the Enable Docs Offline dialog appears in the Files App.
 */
export async function driveEnableDocsOfflineDialog() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  // Open the Enable Docs Offline dialog.
  await openAndWaitForEnableDocsOfflineDialog(appId);

  // Click on the ok button.
  await remoteCall.waitAndClickElement(
      appId, '.cr-dialog-container.shown .cr-dialog-ok');

  // Check: the last dialog result should be 1 (accept).
  await waitForLastDriveDialogResult('1');

  // Open the Enable Docs Offline dialog.
  await openAndWaitForEnableDocsOfflineDialog(appId);

  // Click on the cancel button.
  await remoteCall.waitAndClickElement(
      appId, '.cr-dialog-container.shown .cr-dialog-cancel');

  // Check: the last dialog result should be 2 (reject).
  await waitForLastDriveDialogResult('2');

  // Open the Enable Docs Offline dialog.
  await openAndWaitForEnableDocsOfflineDialog(appId);

  // Check: the last dialog result should be 3 (dismiss).
  await waitForLastDriveDialogResult('3');
}

/**
 * Tests that the Enable Docs Offline dialog launches a Chrome notification if
 * there are no Files App windows open.
 */
export async function driveEnableDocsOfflineDialogWithoutWindow() {
  // Wait for the background page to listen to events from the browser.
  await remoteCall.callRemoteTestUtil('waitForBackgroundReady', null, []);

  // Simulate Drive signalling Files App to open a dialog.
  await sendTestMessage({name: 'displayEnableDocsOfflineDialog'});

  // Check: the Enable Docs Offline notification should appear.
  await waitForNotification('enable-docs-offline');

  // Click on the ok button.
  await sendTestMessage({
    name: 'clickNotificationButton',
    extensionId: FILE_MANAGER_EXTENSIONS_ID,
    notificationId: 'enable-docs-offline',
    index: 1,
  });

  // Check: the last dialog result should be 1 (accept).
  await waitForLastDriveDialogResult('1');

  // Simulate Drive signalling Files App to open a dialog.
  await sendTestMessage({name: 'displayEnableDocsOfflineDialog'});

  // Check: the Enable Docs Offline notification should appear.
  await waitForNotification('enable-docs-offline');

  // Click on the cancel button.
  await sendTestMessage({
    name: 'clickNotificationButton',
    extensionId: FILE_MANAGER_EXTENSIONS_ID,
    notificationId: 'enable-docs-offline',
    index: 0,
  });

  // Check: the last dialog result should be 2 (reject).
  await waitForLastDriveDialogResult('2');

  // Simulate Drive signalling Files App to open a dialog.
  await sendTestMessage({name: 'displayEnableDocsOfflineDialog'});

  // Check: the Enable Docs Offline notification should appear.
  await waitForNotification('enable-docs-offline');

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

  // Check: the Enable Docs Offline dialog should appear in Files app.
  const dialogText = await remoteCall.waitForElement(
      appId, '.cr-dialog-container.shown .cr-dialog-text');
  chrome.test.assertEq(ENABLE_DOCS_OFFLINE_MESSAGE, dialogText.text);

  // Check: the Enable Docs Offline notification should disappear.
  const caller = getCaller();
  await repeatUntil(async () => {
    const idSet = await remoteCall.callRemoteTestUtil<Record<string, boolean>>(
        'getNotificationIDs', null, []);
    return idSet['enable-docs-offline'] ?
        pending(
            caller, 'Waiting for Drive confirm notification to disappear.') :
        null;
  });
}

/**
 * Tests that the Enable Docs Offline dialog appears in the focused window if
 * there are more than one Files App windows open.
 */
export async function driveEnableDocsOfflineDialogMultipleWindows() {
  // Open two Files app windows on Drive, and the second one should be focused.
  const appId1 = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);
  const appId2 = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  // Open the Enable Docs Offline dialog and check that it appears in the second
  // window.
  await openAndWaitForEnableDocsOfflineDialog(appId2);

  // Check: there should be no dialog shown in the first window.
  await remoteCall.waitForElementLost(appId1, '.cr-dialog-container.shown');

  // Click on the ok button in the second window.
  await remoteCall.waitAndClickElement(
      appId2, '.cr-dialog-container.shown .cr-dialog-ok');

  // Check: the last dialog result should be 1 (accept).
  await waitForLastDriveDialogResult('1');
}

/**
 * Tests that the Enable Docs Offline dialog disappears when Drive is unmounted.
 */
export async function driveEnableDocsOfflineDialogDisappearsOnUnmount() {
  // Open Files app on Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);

  // Open the Enable Docs Offline dialog.
  await openAndWaitForEnableDocsOfflineDialog(appId);

  // Unmount Drive.
  await sendTestMessage({name: 'unmountDrive'});

  // Check: the Enable Docs Offline dialog should disappear.
  await remoteCall.waitForElementLost(appId, '.cr-dialog-container.shown');
}

/**
 * Tests that when deleting a file on Google Drive the dialog has no mention of
 * permanent deletion (as the files aren't pemanently deleted but go to Google
 * Drive trash instead).
 */
export async function driveDeleteDialogDoesntMentionPermanentDelete() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, []);

  // Wait for the "hello.txt" file to appear.
  const helloTxtSelector = '#file-list [file-name="photos"]';
  await remoteCall.waitAndClickElement(appId, helloTxtSelector);

  // Ensure the move-to-trash command is hidden and disabled on Google Drive and
  // then click the enabled delete button
  await remoteCall.waitForElement(appId, '#move-to-trash[hidden][disabled]');
  await remoteCall.waitAndClickElement(
      appId, '#delete-button:not([hidden]):not([disabled])');

  // 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);

  // Check: the dialog has no mention in the text of "permanent".
  const dialogText = await remoteCall.waitForElement(appId, '.cr-dialog-text');
  chrome.test.assertFalse(
      (dialogText.text ?? '').toLowerCase().includes('permanent'));

  // The dialog 'Delete' button should be only contain the text "Delete".
  const dialogDeleteButton =
      await remoteCall.waitAndClickElement(appId, '.cr-dialog-ok');
  chrome.test.assertEq('Delete', dialogDeleteButton.text);

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

/**
 * Tests that Google One offer banner appears if a user navigates to Drive
 * volume.
 */
export async function driveGoogleOneOfferBannerEnabled() {
  const userActionShown = 'FileBrowser.GoogleOneOffer.Shown';
  const userActionGetPerk = 'FileBrowser.GoogleOneOffer.GetPerk';
  const userActionDismiss = 'FileBrowser.GoogleOneOffer.Dismiss';

  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Visibility of a banner is controlled with hidden attribute once it gets
  // attached to the DOM.
  await remoteCall.waitForElement(
      appId, 'google-one-offer-banner:not([hidden])');
  chrome.test.assertEq(1, await getUserActionCount(userActionShown));

  // extra-button (get perk button) is provided by google-one-offer-banner.
  await remoteCall.waitAndClickElement(
      appId, ['google-one-offer-banner', '[slot="extra-button"]']);
  chrome.test.assertEq(1, await getUserActionCount(userActionGetPerk));
  // Check: dismiss event should not be recorded for dismiss caused by the get
  // perk button click.
  chrome.test.assertEq(0, await getUserActionCount(userActionDismiss));
  await remoteCall.waitForLastOpenedBrowserTabUrl(
      'https://www.google.com/chromebook/perks/?id=google.one.2019');
  await remoteCall.waitForElement(appId, 'google-one-offer-banner[hidden]');
  // Check: If Google One offer banner is shown, Drive welcome banner should not
  // be shown. Holding space welcome banner is the next one after the Drive
  // welcome banner.
  await remoteCall.waitForElement(
      appId, 'holding-space-welcome-banner:not([hidden])');
}

/**
 * Tests that Google One offer banner does not appear if the flag is off, which
 * is the default.
 */
export async function driveGoogleOneOfferBannerDisabled() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // If Google One offer banner is not shown, Drive welcome banner should be
  // shown. We cannot test google-one-offer-banner[hidden] here as it should not
  // be in the DOM tree.
  await remoteCall.waitForElement(appId, 'drive-welcome-banner:not([hidden])');
}

/**
 * Test that Google One offer banner can get dismissed with a click of Dismiss
 * button.
 */
export async function driveGoogleOneOfferBannerDismiss() {
  const userActionDismiss = 'FileBrowser.GoogleOneOffer.Dismiss';

  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Visibility of a banner is controlled with hidden attribute once it gets
  // attached to the DOM.
  await remoteCall.waitForElement(
      appId, 'google-one-offer-banner:not([hidden])');

  // dismiss-button is provided by educational-banner.
  await remoteCall.waitAndClickElement(appId, [
    'google-one-offer-banner',
    'educational-banner',
    await getDismissButtonId(appId),
  ]);
  chrome.test.assertEq(1, await getUserActionCount(userActionDismiss));
  await remoteCall.waitForElement(appId, 'google-one-offer-banner[hidden]');
}

/**
 * Tests that when bulk pinning is enabled, the "Available offline" toggle
 * should not be visible. When the preference is updated, the toggle should
 * reappear.
 */
export async function
drivePinToggleIsDisabledAndHiddenWhenBulkPinningEnabled() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.hello]);


  const toggleId = await remoteCall.isCrosComponents(appId) ?
      'pinned-toggle-jelly' :
      'pinned-toggle';

  // Bring up the context menu for test.txt.
  await remoteCall.waitAndRightClick(
      appId, '#file-list [file-name="hello.txt"]');

  // The pinned toggle should be visible along with the command.
  await remoteCall.waitForElement(
      appId,
      `#pinned-toggle-wrapper:not([hidden]) #${toggleId}:not([disabled])`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"]:not([hidden][disabled])');

  // Mock the free space returned by spaced to be 4 GB and enable the bulk
  // pinning preference
  await remoteCall.setSpacedFreeSpace(4n << 30n);
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});

  // Wait for both the pinned toggle and the pinned command to become hidden and
  // disabled.
  await remoteCall.waitForElement(
      appId, `#pinned-toggle-wrapper[hidden] #${toggleId}[disabled]`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"][hidden][disabled]');

  // Disable the bulk pinning preference and wait for the pinned toggle and
  // command to become visible and available.
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: false});
  await remoteCall.waitForElement(
      appId,
      `#pinned-toggle-wrapper:not([hidden]) #${toggleId}:not([disabled])`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"]:not([hidden][disabled])');
}

/**
 * Tests that "Shared with me" which is outside "My drive" retains the pinned
 * property and it is not updated when bulk pinning is enabled.
 */
export async function driveFoldersRetainPinnedPropertyWhenBulkPinningEnabled() {
  // Open Files app on Drive containing "Shared with me" file entries.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.hello, ENTRIES.sharedWithMeDirectory]);

  // Enable the bulk pinning preference first.
  await remoteCall.setSpacedFreeSpace(4n << 30n);
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
  await remoteCall.waitForBulkPinningStage('Syncing');

  // Navigate to the shared with me directory and assert that the pinned
  // property is not set on the directory.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Shared with me');
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Shared Directory"]:not(.pinned)');

  // Disable the bulk pinning preference.
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: false});
  await remoteCall.waitForBulkPinningStage('Stopped');

  // Pin the "Shared Directory" folder in Shared with me and wait for the pinned
  // class to be updated.
  await remoteCall.showContextMenuFor(appId, 'Shared Directory');
  await remoteCall.waitAndClickElement(
      appId,
      '#file-context-menu:not([hidden]) ' +
          '[command="#toggle-pinned"]:not([checked])');
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Shared Directory"].pinned');

  // Enable and disable bulk pinning and ensure the pinned attribute is not
  // removed.
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
  await remoteCall.waitForBulkPinningStage('Syncing');
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Shared Directory"].pinned');
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: false});
  await remoteCall.waitForBulkPinningStage('Stopped');
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Shared Directory"].pinned');
}

/**
 * Tests that when bulk pinning is enabled, the "Available offline" toggle
 * should still be visible in the Shared with me section.
 */
export async function
drivePinToggleIsEnabledInSharedWithMeWhenBulkPinningEnabled() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], [
    ENTRIES.hello,
    ENTRIES.sharedWithMeDirectory,
    ENTRIES.sharedWithMeDirectoryFile,
  ]);

  const toggleId = await remoteCall.isCrosComponents(appId) ?
      'pinned-toggle-jelly' :
      'pinned-toggle';

  // Click the Shared with me volume, it has no children so navigating using the
  // directory tree doesn't work.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Shared with me');

  // Wait until the breadcrumb path is updated.
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, '/Shared with me');

  // Bring up the context menu for Shared Directory.
  await remoteCall.waitAndRightClick(
      appId, '#file-list [file-name="Shared Directory"]');

  // The pinned toggle should be visible along with the command.
  await remoteCall.waitForElement(
      appId,
      `#pinned-toggle-wrapper:not([hidden]) #${toggleId}:not([disabled])`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"]:not([hidden][disabled])');

  // Mock the free space returned by spaced to be 4 GB and enable the bulk
  // pinning preference.
  await remoteCall.setSpacedFreeSpace(4n << 30n);
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
  await remoteCall.waitForBulkPinningStage('Syncing');

  // After bulk pinning is enabled and in the syncing stage, the toggle should
  // still be visible and enabled.
  await remoteCall.waitForElement(
      appId,
      `#pinned-toggle-wrapper:not([hidden]) #${toggleId}:not([disabled])`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"]:not([hidden][disabled])');

  // Disable the bulk pinning preference wait for it to reflect in the pin state
  // and ensure the pinned toggle has not changed.
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: false});
  await remoteCall.waitForBulkPinningStage('Stopped');
  await remoteCall.waitForElement(
      appId,
      `#pinned-toggle-wrapper:not([hidden]) #${toggleId}:not([disabled])`);
  await remoteCall.waitForElement(
      appId, '[command="#toggle-pinned"]:not([hidden][disabled])');
}

/**
 * Tests that files that can't be pinned should have the correct CSS class
 * applied to them. When they go back to being able to be pinned (e.g. from Docs
 * offline coming back online) then ensure the inline icon is updated.
 */
export async function
driveCantPinItemsShouldHaveClassNameAndGetUpdatedWhenCanPin() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.cantPinFile]);

  // Ensure the `cant_pin.txt` file has the cant-pin class.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="text.txt"].cant-pin');

  // Update the file metadata to ensure the file can now be pinned.
  await sendTestMessage(
      {name: 'setCanPin', path: '/root/text.txt', canPin: true});

  // Wait for the `.cant-pin` class to be removed.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="text.txt"]:not(.cant-pin)');
}

/**
 * Tests that items that are cached outside of their virtual list get their
 * inline sync status updated when they get attached back to the DOM.
 */
export async function driveItemsOutOfViewportShouldUpdateTheirSyncStatus() {
  const entries = [];
  const emptyFile = createTestFile('text.txt');
  for (let i = 0; i < 50; ++i) {
    entries.push(emptyFile.cloneWithNewName(`File ${i}`));
  }

  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], entries);

  // Sort the table by the `name` column.
  await remoteCall.waitAndClickElement(
      appId, ['.table-header-cell:nth-of-type(1)']);

  // Update the second file's metadata to "not pinnable" and wait for the
  // corresponding icon to be displayed.
  const secondFileName = entries[1]!.nameText;
  await sendTestMessage(
      {name: 'setCanPin', path: `/root/${secondFileName}`, canPin: false});
  await remoteCall.waitForElement(
      appId, `#file-list [file-name="${secondFileName}"].cant-pin`);

  // Wait for the first entry to appear in the file list.
  const firstFileName = entries[0]!.nameText;
  await remoteCall.waitForElement(
      appId, `#file-list [file-name="${firstFileName}"]`);

  // Scroll to the bottom of the virtual list which ensures the first element
  // should be removed from the DOM and cached.
  await remoteCall.callRemoteTestUtil(
      'setScrollTop', appId, ['#file-list', 10000]);

  await remoteCall.waitForElementLost(
      appId, `#file-list [file-name="${firstFileName}"]`);
  const lastFileName = entries[entries.length - 1]!.nameText;
  await remoteCall.waitForElement(
      appId, `#file-list [file-name="${lastFileName}"]`);

  // Send a file sync progress for the first file name.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${firstFileName}`,
    progress: 50,
  });

  // Send a file sync progress event for the last file name to use as a marker
  // to know when the first file has made it to 50% pie progress.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${lastFileName}`,
    progress: 50,
  });

  const inlineSyncSelector = (fileName: string) => `#file-list [file-name="${
      fileName}"] xf-inline-status[sync-status=in_progress]`;

  // Wait for the progress to appear on the last file and assert it received the
  // correct progress value.
  let lastFileInlineStatus =
      await remoteCall.waitForElement(appId, inlineSyncSelector(lastFileName));
  chrome.test.assertEq(
      Number(lastFileInlineStatus.attributes['progress']), 0.5);

  // Send a "completed" sync progress event for the second last file and wait
  // for its effect.
  const secondLastFileName = entries[entries.length - 2]!.nameText;
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${secondLastFileName}`,
    progress: 100,
  });

  // Scroll back up to the first element.
  await remoteCall.callRemoteTestUtil('setScrollTop', appId, ['#file-list', 0]);

  // Assert that the first element has the 50% progress as the event was sent
  // before the last file event was sent.
  const firstFileInlineStatus =
      await remoteCall.waitForElement(appId, inlineSyncSelector(firstFileName));
  chrome.test.assertEq(
      Number(firstFileInlineStatus.attributes['progress']), 0.5);

  // Ensure the second file is still displayed as "not pinnable".
  await remoteCall.waitForElement(
      appId, `#file-list [file-name="${secondFileName}"].cant-pin`);

  // Switch to grid view.
  await remoteCall.waitAndClickElement(appId, '#view-button');

  // Wait for the first entry to appear in the file grid.
  await remoteCall.waitForElement(
      appId, `grid#file-list [file-name="${firstFileName}"]`);

  // Scroll back down to the last element.
  await remoteCall.callRemoteTestUtil(
      'setScrollTop', appId, ['grid#file-list', 10000]);

  // Assert that the last element still has 50% progress.
  lastFileInlineStatus =
      await remoteCall.waitForElement(appId, inlineSyncSelector(lastFileName));
  chrome.test.assertEq(
      Number(lastFileInlineStatus.attributes['progress']), 0.5);
}

/**
 * Tests that when bulk pinning is enabled the queued state is shown for all
 * files that the PinningManager is tracking but has not yet pinned.
 */
export async function driveAllItemsShouldBeQueuedIfTrackedByPinningManager() {
  // Stop the PinningManager from pinning files.
  await sendTestMessage({name: 'setBulkPinningShouldPinFiles', enabled: false});

  // Add a single empty file and load Files app up at the Drive root.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.hello]);

  // Enable bulk pinning functionality.
  await remoteCall.setSpacedFreeSpace(4n << 30n);
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});

  // Wait for bulk pinning to enter the syncing stage.
  await remoteCall.waitForBulkPinningStage('Syncing');

  // The file should have a queued despite never getting set to pinned.
  await remoteCall.waitForElement(
      appId,
      '#file-list [file-name="hello.txt"] xf-inline-status[sync-status=queued]');

  // Disable bulk pinning and ensure the sync status gets removed (i.e. returns
  // to not found).
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: false});
  await remoteCall.waitForElement(
      appId,
      '#file-list [file-name="hello.txt"] xf-inline-status[sync-status=not_found]');

  // Ensure the pin manager pins files then re-enable the bulk pinning
  // preferece. The hello file should be pinned now.
  await sendTestMessage({name: 'setBulkPinningShouldPinFiles', enabled: true});
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
  await remoteCall.waitForElement(
      appId,
      '#file-list [file-name="hello.txt"].pinned xf-inline-status[sync-status=not_found]');
}

/**
 * Tests that items that have the `dirty` metadata flag set to true have their
 * sync_status property returned as "QUEUED".
 */
export async function driveDirtyItemsShouldBeDisplayedAsQueued() {
  // Add a single test file with the dirty metadata set to "true" and load Files
  // app up at the Drive root.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], [ENTRIES.dirty]);

  // The file should be displayed as "queued" despite it not having received any
  // progress events yet because dirty=true.
  await remoteCall.waitForElement(
      appId,
      '#file-list [file-name="dirty.txt"] xf-inline-status[sync-status=queued]');

  // Fake the file starting to sync.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${ENTRIES.dirty.targetPath}`,
    progress: 50,
  });

  // Verify that the sync_state transitions to "in_progress".
  await remoteCall.waitForElement(
      appId,
      '#file-list [file-name="dirty.txt"] xf-inline-status[sync-status=in_progress]');
}

/**
 * Tests that the Drive bulk pinning banner is disabled (i.e. doesn't appear
 * between the Drive welcome banner but before the Holding space banner).
 */
export async function driveBulkPinningBannerDisabled() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Visibility of a banner is controlled with hidden attribute once it gets
  // attached to the DOM.
  await remoteCall.waitForElement(appId, 'drive-welcome-banner:not([hidden])');

  // extra-button (get perk button) is provided by google-one-offer-banner.
  await remoteCall.waitAndClickElement(appId, [
    'drive-welcome-banner',
    'educational-banner',
    await getDismissButtonId(appId),
  ]);

  await remoteCall.waitForElement(appId, 'drive-welcome-banner[hidden]');
  // Check: If Google One offer banner is shown, Drive welcome banner should not
  // be shown. Holding space welcome banner is the next one after the Drive
  // welcome banner.
  await remoteCall.waitForElement(
      appId, 'holding-space-welcome-banner:not([hidden])');
}

/**
 * Tests that the Drive bulk pinning banner is enabled (i.e. it appears directly
 * after the Drive welcome banner).
 */
export async function driveBulkPinningBannerEnabled() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE);

  // Visibility of a banner is controlled with hidden attribute once it gets
  // attached to the DOM.
  await remoteCall.waitForElement(appId, 'drive-welcome-banner:not([hidden])');

  // extra-button (get perk button) is provided by google-one-offer-banner.
  await remoteCall.waitAndClickElement(appId, [
    'drive-welcome-banner',
    'educational-banner',
    await getDismissButtonId(appId),
  ]);

  await remoteCall.waitForElement(appId, 'drive-welcome-banner[hidden]');
  // Check: If Google One offer banner is shown, Drive welcome banner should not
  // be shown. Holding space welcome banner is the next one after the Drive
  // welcome banner.
  await remoteCall.waitForElement(
      appId, 'drive-bulk-pinning-banner:not([hidden])');
}

/*
 * Checks that we cannot open Google Doc without network connection.
 */
export async function openDriveDocWhenOffline() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], [
    ENTRIES.testDocument,
    ENTRIES.hello,
  ]);

  // Setup the open-with task for drive.
  const fakeOpenWith = new FakeTask(
      true, {appId: 'id', taskType: 'drive', actionId: 'open-with'},
      'DummyOpenWith');
  await remoteCall.callRemoteTestUtil('overrideTasks', appId, [
    [fakeOpenWith],
  ]);

  // Start bulk pinning.
  await remoteCall.setSpacedFreeSpace(4n << 30n);
  await sendTestMessage({name: 'setBulkPinningEnabledPref', enabled: true});
  await remoteCall.waitForBulkPinningStage('Syncing');

  // Wait for the hello.txt file to be pinned.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="hello.txt"].pinned');
  // Check that the gdoc file is not pinned.
  await remoteCall.waitForElement(
      appId, '#file-list [file-name="Test Document.gdoc"]:not(.pinned)');

  // Turn off all services.
  await sendTestMessage({name: 'setDeviceOffline'});

  // Check that hello.txt opens on double click without network.
  await remoteCall.waitUntilSelected(appId, 'hello.txt');
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeMouseDoubleClick', appId,
      ['#file-list li.table-row[selected] .filename-label span']));
  await remoteCall.waitUntilTaskExecutes(
      appId, fakeOpenWith.descriptor, ['hello.txt']);

  // Check that Test Document.gdoc does not open on double click. We do not
  // check that the task was NOT executed. Instead we check that the "You
  // are offline" dialog was shown.
  await remoteCall.waitUntilSelected(appId, 'Test Document.gdoc');
  chrome.test.assertTrue(!!await remoteCall.callRemoteTestUtil(
      'fakeMouseDoubleClick', appId,
      ['#file-list li.table-row[selected] .filename-label span']));
  await remoteCall.waitForElement(
      appId, '.files-alert-dialog[aria-label="You are offline"]');
}

/*
 * Verifies that once a file completes syncing, its syncing status
 * indicator displays as "completed" and is dismissed about 300ms
 * later.
 */
export async function completedSyncStatusDismissesAfter300Ms() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], [
    ENTRIES.hello,
  ]);

  const timeBeforeCompletion = Date.now();

  // Fake the file finishing syncing.
  await sendTestMessage({
    name: 'setDriveSyncProgress',
    path: `/root/${ENTRIES.hello.targetPath}`,
    progress: 100,
  });

  const completedQuery = '#file-list xf-inline-status[sync-status=completed]';

  // Verify the "sync completed" icon is displayed.
  await remoteCall.waitForElement(appId, completedQuery);

  // Verify the completed state is eventually dismissed.
  await remoteCall.waitForElementLost(appId, completedQuery);

  // Verify that at least 300ms have passed since the syncing completed.
  chrome.test.assertTrue(Date.now() - timeBeforeCompletion >= 300);
}

/**
 * Tests that when the organization limit has exceeded (not the user storage)
 * the out of organization space banner appears.
 */
export async function driveOutOfOrganizationSpaceBanner() {
  await remoteCall.setPooledStorageQuotaUsage(
      1 * 1024 * 1024, 2 * 1024 * 1024, true);

  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [ENTRIES.hello]);

  await remoteCall.waitForElement(
      appId, 'drive-out-of-organization-space-banner');
}

/**
 * Tests that copy operation of a directory will start, but a error message will
 * appear when encrypted files within that directory were skipped.
 */
export async function copyDirectoryWithEncryptedFile() {
  const dir = ENTRIES.testCSEDirectory;
  const file = ENTRIES.testCSEFileInDirectory;
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, [], [dir, file]);
  await sendTestMessage({name: 'mockDriveReadFailure', path: file.targetPath});

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

  await remoteCall.waitForFiles(appId, [dir.getExpectedRow()]);
  await remoteCall.waitUntilSelected(appId, dir.nameText);

  await remoteCall.callRemoteTestUtil('execCommand', appId, ['copy']);
  await directoryTree.navigateToPath('/My files/Downloads');
  await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']);

  const panelType = 3;  // panelTypeError from PanelItem
  const panel = await remoteCall.waitForElement(
      appId, ['#progress-panel', `xf-panel-item[panel-type="${panelType}"]`]);

  chrome.test.assertEq(
      ENTRIES.testCSEFileInDirectory.nameText +
          ' could not be copied because it is encrypted.',
      panel.attributes['primary-text']);
}