chromium/ui/file_manager/integration_tests/file_manager/recents.ts

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

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

import {remoteCall} from './background.js';
import {DirectoryTreePageObject} from './page_objects/directory_tree.js';
import {BASIC_CROSTINI_ENTRY_SET, BASIC_DRIVE_ENTRY_SET, BASIC_LOCAL_ENTRY_SET, NESTED_ENTRY_SET, RECENT_ENTRY_SET} from './test_data.js';

// Mock files with recently modified dates, be aware the days passed in should
// be larger than 3 to prevent file list from showing "Today/Yesterday", which
// will break the waitForFiles() function.
// Test entry for a recently-modified video file.
const RECENTLY_MODIFIED_VIDEO =
    ENTRIES.world.cloneWithModifiedDate(getDateWithDayDiff(7));
const RECENTLY_MODIFIED_MOV_VIDEO =
    ENTRIES.movFile.cloneWithModifiedDate(getDateWithDayDiff(10));

// Test entry for a recently-modified document file.
const RECENTLY_MODIFIED_DOCUMENT =
    ENTRIES.docxFile.cloneWithModifiedDate(getDateWithDayDiff(12));

// Test entries for recent-modified android files.
const RECENT_MODIFIED_ANDROID_DOCUMENT =
    ENTRIES.documentsText.cloneWithModifiedDate(getDateWithDayDiff(15));
const RECENT_MODIFIED_ANDROID_IMAGE =
    ENTRIES.picturesImage.cloneWithModifiedDate(getDateWithDayDiff(20));
const RECENT_MODIFIED_ANDROID_AUDIO =
    ENTRIES.musicAudio.cloneWithModifiedDate(getDateWithDayDiff(21));
const RECENT_MODIFIED_ANDROID_VIDEO =
    ENTRIES.moviesVideo.cloneWithModifiedDate(getDateWithDayDiff(25));

// Special file used with provided volume. Due to the fact that we rely on
// ash::file_system_provider::FakeProvidedFileSystem we cannot clone it from
// existing entries. Among differences is the targetPath and sizeText that are
// set up differently.
const RECENT_PROVIDED_HELLO = new TestEntryInfo({
  type: EntryType.FILE,
  targetPath: '/recent-hello.txt',
  mimeType: 'text/plain',
  lastModifiedTime: getDateWithDayDiff(8),
  nameText: 'recent-hello.txt',
  sizeText: '6 bytes',
  typeText: 'Plain text',
});

/**
 * Enum for supported recent filter types.
 */
enum RecentFilterType {
  ALL = 'all',
  AUDIO = 'audio',
  IMAGE = 'image',
  VIDEO = 'video',
  DOCUMENT = 'document',
}

/**
 * Adds file entries to the Play Files folder and update media view root.
 */
async function addPlayFileEntries() {
  // We can't add file entries to Play Files ('android_files') directly,
  // because they won't be picked up by the fake ARC file system. Instead,
  // we need to add file entries to the corresponding media view root.
  await sendTestMessage({name: 'mountMediaView'});
  await addEntries(['media_view_audio'], [RECENT_MODIFIED_ANDROID_AUDIO]);
  await addEntries(['media_view_images'], [RECENT_MODIFIED_ANDROID_IMAGE]);
  await addEntries(['media_view_videos'], [RECENT_MODIFIED_ANDROID_VIDEO]);
  await addEntries(
      ['media_view_documents'], [RECENT_MODIFIED_ANDROID_DOCUMENT]);
}

/**
 * Navigates to Recent folder with specific type and verify the breadcrumb path.
 * @param appId Files app windowId.
 * @param type Recent file type.
 */
async function navigateToRecent(
    appId: string, type: RecentFilterType = RecentFilterType.ALL) {
  const breadcrumbMap = {
    [RecentFilterType.ALL]: '/Recent',
    [RecentFilterType.AUDIO]: '/Audio',
    [RecentFilterType.IMAGE]: '/Images',
    [RecentFilterType.VIDEO]: '/Videos',
    [RecentFilterType.DOCUMENT]: '/Documents',
  };

  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Recent');
  // "All" button is activated by default, no need to click.
  if (type !== RecentFilterType.ALL) {
    await remoteCall.waitAndClickElement(
        appId, [`[file-type-filter="${type}"]`]);
  }
  // Check the corresponding filter button is activated.
  await remoteCall.waitForElement(
      appId, [`[file-type-filter="${type}"].active`]);
  // Breadcrumb should always be "/Recents" if the flag is on.
  await verifyBreadcrumbsPath(appId, breadcrumbMap[RecentFilterType.ALL]);
}

/**
 * Verifies the current folder has the expected entries and checks the delete
 * button is hidden after selecting these files.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyCurrentEntries(
    appId: string, expectedEntries: TestEntryInfo[],
    trashButton: boolean = false) {
  // Verify Recents contains the expected files - those with an mtime in the
  // future.
  const files = TestEntryInfo.getExpectedRows(expectedEntries);
  await remoteCall.waitForFiles(appId, files);

  // Select all the files and check that the delete button isn't visible.
  const ctrlA = ['#file-list', 'a', true, false, false];
  await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, ctrlA);

  // Check: the file-list should be selected.
  await remoteCall.waitForElement(appId, '#file-list li[selected]');

  // Test that the delete button's visibility based on v2 flag.
  const buttonSelector =
      (trashButton) ? '#move-to-trash-button' : '#delete-button';
  const deleteButton = await remoteCall.waitForElement(appId, buttonSelector);
  chrome.test.assertFalse(
      deleteButton.hidden, `${buttonSelector} element should be visible`);
}

/**
 * Opens the Recent folder and checks the expected entries are showing there.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries, by default `RECENT_ENTRY_SET`
 *     is used.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyRecents(
    appId: string, expectedEntries: TestEntryInfo[] = RECENT_ENTRY_SET,
    trashButton: boolean = false) {
  await navigateToRecent(appId);
  await verifyCurrentEntries(appId, expectedEntries, trashButton);
}

/**
 * Opens the Recent Audio folder and checks the expected entries are showing
 * there.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyRecentAudio(
    appId: string, expectedEntries: TestEntryInfo[],
    trashButton: boolean = false) {
  await navigateToRecent(appId, RecentFilterType.AUDIO);
  await verifyCurrentEntries(appId, expectedEntries, trashButton);
}

/**
 * Opens the Recent Image folder and checks the expected entries are showing
 * there.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyRecentImages(
    appId: string, expectedEntries: TestEntryInfo[],
    trashButton: boolean = false) {
  await navigateToRecent(appId, RecentFilterType.IMAGE);
  await verifyCurrentEntries(appId, expectedEntries, trashButton);
}

/**
 * Opens the Recent Video folder and checks the expected entries are showing
 * there.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyRecentVideos(
    appId: string, expectedEntries: TestEntryInfo[],
    trashButton: boolean = false) {
  await navigateToRecent(appId, RecentFilterType.VIDEO);
  await verifyCurrentEntries(appId, expectedEntries, trashButton);
}

/**
 * Opens the Recent Document folder and checks the expected entries are showing
 * there.
 * @param appId Files app windowId.
 * @param expectedEntries Expected file entries.
 * @param trashButton If the file system doesn't support trash, a delete button
 *     will show instead of a trash button.
 */
async function verifyRecentDocuments(
    appId: string, expectedEntries: TestEntryInfo[],
    trashButton: boolean = false) {
  await navigateToRecent(appId, RecentFilterType.DOCUMENT);
  await verifyCurrentEntries(appId, expectedEntries, trashButton);
}

/**
 * Verifies the breadcrumb has the expected path.
 * @param appId Files app windowId.
 * @param expectedPath Expected breadcrumb path.
 */
async function verifyBreadcrumbsPath(appId: string, expectedPath: string) {
  await remoteCall.waitUntilCurrentDirectoryIsChanged(appId, expectedPath);
}

/**
 * Selects a file and right click to show the context menu, then click the
 * specified context menu item.
 * @param appId Files app windowId.
 * @param fileName Name of the file to right click.
 * @param commandId The command id for the context menu item.
 */
async function rightClickContextMenu(
    appId: string, fileName: string, commandId: string) {
  // Select the item.
  await remoteCall.waitUntilSelected(appId, fileName);

  // Right-click the selected file.
  await remoteCall.waitAndRightClick(appId, '.table-row[selected]');

  // Click the context menu item with the command id.
  const contextMenuItem = '#file-context-menu:not([hidden]) ' +
      `[command="#${commandId}"]:not([hidden]):not([disabled])`;
  await remoteCall.waitAndClickElement(appId, contextMenuItem);
}

/**
 * Opens given file's containing folder by choosing "Go to file location"
 * context menu item.
 * @param appId Files app windowId.
 * @param fileName Name of the file to open containing folder.
 */
async function goToFileLocation(appId: string, fileName: string) {
  await rightClickContextMenu(appId, fileName, 'go-to-file-location');
}

/**
 * Deletes a given file by choosing "Delete" context menu item.
 * @param appId Files app windowId.
 * @param fileName Name of the file to delete.
 * @param confirmDeletion If the file system doesn't support trash, need to
 *     confirm the deletion.
 */
async function deleteFile(
    appId: string, fileName: string, confirmDeletion: boolean = false) {
  const command = (confirmDeletion) ? 'delete' : 'move-to-trash';
  await rightClickContextMenu(appId, fileName, command);
  if (confirmDeletion) {
    // Click "Delete" on the Delete confirm dialog.
    await remoteCall.waitAndClickElement(
        appId, '.files-confirm-dialog button.cr-dialog-ok');
  }
}

/**
 * Renames a given file by choosing "Rename" context menu item.
 * @param appId Files app windowId.
 * @param fileName Name of the file to rename.
 * @param newName The new file name.
 */
async function renameFile(appId: string, fileName: string, newName: string) {
  const textInput = '#file-list .table-row[renaming] input.rename';
  await rightClickContextMenu(appId, fileName, 'rename');
  // Wait for the rename input field.
  await remoteCall.waitForElement(appId, textInput);
  // Input the new name.
  await remoteCall.inputText(appId, textInput, newName);
  const inputElement = await remoteCall.waitForElement(appId, textInput);
  chrome.test.assertEq(newName, inputElement.value);
  // Press Enter to commit renaming.
  const keyDown = [textInput, 'Enter', false, false, false];
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('fakeKeyDown', appId, keyDown));
  // Wait until renaming is complete.
  const renamingItem = '#file-list .table-row[renaming]';
  await remoteCall.waitForElementLost(appId, renamingItem);
}

/**
 * Cuts a given file by choosing "Cut" context menu item and paste the file to
 * the new folder.
 * @param appId Files app windowId.
 * @param fileName Name of the file to cut.
 * @param newFolder Full breadcrumb path for the new folder to paste.
 */
async function cutFileAndPasteTo(
    appId: string, fileName: string, newFolder: string) {
  await rightClickContextMenu(appId, fileName, 'cut');
  // Go to the new folder to paste.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath(newFolder);
  chrome.test.assertTrue(
      await remoteCall.callRemoteTestUtil('execCommand', appId, ['paste']));
  // Wait for the operation to be completed.
  const caller = getCaller();
  await repeatUntil(async () => {
    const element = await remoteCall.waitForElement(
        appId, ['#progress-panel', 'xf-panel-item']);
    const expectedPrimaryText =
        `Moving ${fileName} to ${newFolder.split('/').pop()}`;
    const expectedSecondaryText = 'Complete';
    const actualPrimaryText = element.attributes['primary-text'];
    const actualSecondaryText = element.attributes['secondary-text'];

    if (expectedPrimaryText === actualPrimaryText &&
        expectedSecondaryText === actualSecondaryText) {
      return;
    }

    return pending(
        caller,
        `Expected feedback panel msg: "${expectedPrimaryText} - ${
            expectedSecondaryText}", got "${actualPrimaryText} - ${
            actualSecondaryText}"`);
  });
}

/**
 * Waits for the empty folder element to show and assert the content to match
 * the expected message.
 * @param appId Files app windowId.
 * @param expectedMessage The expected empty folder message
 */
async function waitForEmptyFolderMessage(
    appId: string, expectedMessage: string) {
  const caller = getCaller();
  // Use repeatUntil() here because when we switch between different filters,
  // the message changes but the element itself will always show there.
  await repeatUntil(async () => {
    const emptyMessage = await remoteCall.waitForElement(
        appId, '#empty-folder:not(.hidden) > .label');
    if (emptyMessage.text === expectedMessage) {
      return;
    }

    return pending(
        caller,
        `Expected empty folder message: "${expectedMessage}", got "${
            emptyMessage.text}"`);
  });
}

/**
 * Tests that file entries populated in the Downloads folder recently will be
 * displayed in Recent folder.
 */
export async function recentsDownloads() {
  // Populate downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Verifies file list in Recents.
  await verifyRecents(
      appId, /*expectedEntries=*/ undefined, /*trashButton=*/ true);

  // Tests that selecting "Go to file location" for a file navigates to
  // Downloads since the file in Recents is from Downloads.
  await goToFileLocation(appId, ENTRIES.desktop.nameText);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(BASIC_LOCAL_ENTRY_SET));
  await verifyBreadcrumbsPath(appId, '/My files/Downloads');
}

/**
 * Tests that file entries populated in My Drive folder recently will be
 * displayed in Recent folder.
 */
export async function recentsDrive() {
  // Populate drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, [], BASIC_DRIVE_ENTRY_SET);

  // Verifies file list in Recents.
  await verifyRecents(appId);

  // Tests that selecting "Go to file location" for a file navigates to
  // My Drive since the file in Recents is from Google Drive.
  await goToFileLocation(appId, ENTRIES.desktop.nameText);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(BASIC_DRIVE_ENTRY_SET));
  await verifyBreadcrumbsPath(appId, '/My Drive');
}

/**
 * Tests that file entries populated in Play Files folder recently will be
 * displayed in Recent folder.
 */
export async function recentsPlayFiles() {
  // Populate Play Files.
  await addPlayFileEntries();
  const appId = await remoteCall.openNewWindow(RootPath.ANDROID_FILES, {});
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);

  // Verifies file list in Recents. Audio files from Play Files folder are
  // not supported in Recents.
  await verifyRecents(appId, [
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
}

/**
 * Tests what happens if listing play files is interspersed with plain listing
 * of another directory.
 */
export async function recentsSearchPlayFilesShowDownloads() {
  // Populate Play Files.
  await addPlayFileEntries();
  const appId = await remoteCall.openNewWindow(RootPath.ANDROID_FILES, {});
  await remoteCall.waitFor('isFileManagerLoaded', appId, true);
  // Verify that the Recent view is correct.
  await verifyRecents(appId, [
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);

  const directoryTree = await DirectoryTreePageObject.create(appId);
  // Rapidly switch between listing Downloads and accessing them via the Recent
  // view. We leave Downloads empty to make switching faster. The choice of 10
  // switches is somewhat arbitrary. The main thing we are testing is that
  // searches triggered by switching to Recent, even if not finished, do not
  // cause a crash.
  for (let i = 0; i < 10; i++) {
    await directoryTree.selectItemByLabel('Recent');
    await directoryTree.selectItemByLabel('Downloads');
  }
}

/**
 * Tests that file entries populated in the My Files folder recently will be
 * displayed in the Recent folder.
 */
export async function recentsMyFiles() {
  // Populate My Files.
  addEntries(['my_files'], [ENTRIES.beautiful, ENTRIES.photos]);

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

  // Verify file list in Recents.
  await verifyRecents(appId, [ENTRIES.beautiful], /*trashButton=*/ true);
}

/**
 * Tests that file entries populated in Crostini folder recently won't be
 * displayed in Recent folder when Crostini has not been mounted.
 */
export async function recentsCrostiniNotMounted() {
  // Add entries to crostini volume, but do not mount.
  // The crostini entries should not show up in recents.
  await addEntries(['crostini'], BASIC_CROSTINI_ENTRY_SET);

  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.photos], []);
  await verifyRecents(appId, [ENTRIES.beautiful], /*trashButton=*/ true);
}

/**
 * Tests that file entries populated in Downloads folder and Crostini folder
 * recently will be displayed in Recent folder when Crostini has been mounted.
 */
export async function recentsCrostiniMounted() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.photos], []);
  // Mount crostini and both downloads and crostini entries will be in recents.
  await remoteCall.mountCrostini(appId);
  await verifyRecents(appId);
}

/**
 * Tests that file entries populated in Downloads folder and My Drive folder
 * recently will be displayed in Recent folder.
 */
export async function recentsDownloadsAndDrive() {
  // Populate both downloads and drive with disjoint sets of files.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.hello, ENTRIES.photos],
      [ENTRIES.desktop, ENTRIES.world, ENTRIES.testDocument]);
  await verifyRecents(appId);
}

/**
 * Tests that file entries populated in Downloads, Drive and Play Files folder
 * recently will be displayed in Recent folder.
 */
export async function recentsDownloadsAndDriveAndPlayFiles() {
  // Populate downloads, drive and play files.
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.hello, ENTRIES.photos],
      [ENTRIES.desktop, ENTRIES.world, ENTRIES.testDocument]);

  await verifyRecents(appId, RECENT_ENTRY_SET.concat([
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]));
}

/**
 * Tests that the same file entries populated in Downloads folder and My Drive
 * folder recently will be displayed in Recent folder twice when the file
 * entries are the same.
 */
export async function recentsDownloadsAndDriveWithOverlap() {
  // Populate both downloads and drive with overlapping sets of files.
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);
  await verifyRecents(appId, RECENT_ENTRY_SET.concat(RECENT_ENTRY_SET));
}


/**
 * Tests that the nested file entries populated in Downloads folder recently
 * will be displayed in Recent folder.
 */
export async function recentsNested() {
  // Populate downloads with nested folder structure. |desktop| is added to
  // ensure Recents has different files to Downloads/A/B/C
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS,
      NESTED_ENTRY_SET.concat([ENTRIES.deeplyBuriedSmallJpeg]), []);

  // Verifies file list in Recents.
  await verifyRecents(
      appId, [ENTRIES.deeplyBuriedSmallJpeg], /*trashButton=*/ true);

  // Tests that selecting "Go to file location" for a file navigates to
  // Downloads/A/B/C since the file in Recents is from Downloads/A/B/C.
  await goToFileLocation(appId, ENTRIES.deeplyBuriedSmallJpeg.nameText);
  await remoteCall.waitForElement(appId, `[scan-completed="C"]`);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([ENTRIES.deeplyBuriedSmallJpeg]));
  await verifyBreadcrumbsPath(appId, '/My files/Downloads/A/B/C');

  // Check: The directory should be highlighted in the directory tree.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.waitForSelectedItemByLabel('C');
  await directoryTree.waitForFocusableItemByLabel('C');
}

/**
 * Tests that the audio file entries populated in Downloads folder recently
 * will be displayed in Recent Audio folder.
 */
export async function recentAudioDownloads() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  // ENTRIES.beautiful is recently-modified and has .ogg file extension.
  await verifyRecentAudio(appId, [ENTRIES.beautiful], /*trashButton=*/ true);
}

/**
 * Tests that if the audio file entries without MIME type are being populated in
 * both Downloads folder and My Drive folder, only the ones from Downloads
 * folder will be displayed in Recent Audio folder.
 */
export async function recentAudioDownloadsAndDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, BASIC_DRIVE_ENTRY_SET);
  // TODO(b:267515423): Fix MIME type for Entries.beautiful.
  // ENTRIES.beautiful in BASIC_DRIVE_ENTRY_SET does not have mime type.
  // The implementation of drivefs used in tests accepts files if the
  // MIME type cannot be determined, erring on the side of acceptance.
  await verifyRecentAudio(appId, [ENTRIES.beautiful, ENTRIES.beautiful]);

  // Tests that selecting "Go to file location" for the file navigates to
  // Downloads since the same file from My Drive doesn't appear in Recent
  // Audio folder.
  await goToFileLocation(appId, ENTRIES.beautiful.nameText);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows(BASIC_LOCAL_ENTRY_SET));
  await verifyBreadcrumbsPath(appId, '/My files/Downloads');
}

/**
 * Tests that the audio file entries populated in Downloads, Drive and Play
 * Files folder recently will be displayed in Recent Audio folder.
 */
export async function recentAudioDownloadsAndDriveAndPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, BASIC_DRIVE_ENTRY_SET);
  // TODO(b:267515423): Fix MIME type for Entries.beautiful.
  // ENTRIES.beautiful in BASIC_DRIVE_ENTRY_SET does not have mime type.
  // The implementation of drivefs used in tests accepts files if the
  // MIME type cannot be determined, erring on the side of acceptance.
  // Play Files recents doesn't support audio root, so audio file in Play
  // Files won't be included.
  await verifyRecentAudio(appId, [ENTRIES.beautiful, ENTRIES.beautiful]);
}

/**
 * Tests that the image file entries populated in Downloads folder recently will
 * be displayed in Recents Image folder.
 */
export async function recentImagesDownloads() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  // ENTRIES.desktop is recently-modified and has .png file extension.
  await verifyRecentImages(appId, [ENTRIES.desktop], /*trashButton=*/ true);
}

/**
 * Tests that if the image file entries with MIME type are being populated in
 * both Downloads folder and My Drive folder, the file entries will be displayed
 * in Recent Audio folder regardless of whether it's from Downloads or My Drive.
 */
export async function recentImagesDownloadsAndDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);
  // TODO(b:267515423): Fix MIME type for Entries.beautiful.
  // ENTRIES.desktop has 'image/png' mime type, too. Both the file in Downloads
  // and the file in Drive should be shown in Images.
  await verifyRecentImages(appId, [
    ENTRIES.beautiful,
    ENTRIES.desktop,
    ENTRIES.desktop,
  ]);
}

/**
 * Tests that the image file entries populated in Downloads, Drive and Play
 */
export async function recentImagesDownloadsAndDriveAndPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);
  await verifyRecentImages(appId, [
    ENTRIES.beautiful,
    ENTRIES.desktop,
    ENTRIES.desktop,
    RECENT_MODIFIED_ANDROID_IMAGE,
  ]);
}

/**
 * Tests that the video file entries populated in Downloads folder recently will
 * be displayed in Recent Videos folder.
 */
export async function recentVideosDownloads() {
  // RECENTLY_MODIFIED_VIDEO is recently-modified and has .ogv file extension.
  // It should be shown in Videos.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS,
      BASIC_LOCAL_ENTRY_SET.concat(
          [RECENTLY_MODIFIED_VIDEO, RECENTLY_MODIFIED_MOV_VIDEO]),
      []);
  await verifyRecentVideos(
      appId, [RECENTLY_MODIFIED_VIDEO, RECENTLY_MODIFIED_MOV_VIDEO],
      /*trashButton=*/ true);
}

/**
 * Tests that if the video file entries with MIME type are being populated in
 * both Downloads folder and My Drive folder, the file entries will be displayed
 * in Recent Video folder regardless of whether it's from Downloads or My Drive.
 */
export async function recentVideosDownloadsAndDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS,
      BASIC_LOCAL_ENTRY_SET.concat([RECENTLY_MODIFIED_VIDEO]),
      BASIC_DRIVE_ENTRY_SET.concat([RECENTLY_MODIFIED_VIDEO]));
  // RECENTLY_MODIFIED_VIDEO has video mime type (video/ogg) too, so the file
  // from Drive should be shown too.
  // The implementation of drivefs used in tests accepts files if the MIME type
  // cannot be determined, erring on the side of acceptance, hence
  // ENTRIES.beautiful presence in this group.
  await verifyRecentVideos(appId, [
    ENTRIES.beautiful,
    RECENTLY_MODIFIED_VIDEO,
    RECENTLY_MODIFIED_VIDEO,
  ]);
}

/**
 * Tests that the video file entries populated in Downloads, Drive and Play
 * Files folder recently will be displayed in Recent Image folder.
 */
export async function recentVideosDownloadsAndDriveAndPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS,
      BASIC_LOCAL_ENTRY_SET.concat([RECENTLY_MODIFIED_VIDEO]),
      BASIC_DRIVE_ENTRY_SET.concat([RECENTLY_MODIFIED_VIDEO]));
  // The implementation of drivefs used in tests accepts files if the MIME type
  // cannot be determined, erring on the side of acceptance, hence
  // ENTRIES.beautiful presence in this group.
  await verifyRecentVideos(appId, [
    ENTRIES.beautiful,
    RECENTLY_MODIFIED_VIDEO,
    RECENTLY_MODIFIED_VIDEO,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
}

/**
 * Tests that the document file entries populated in Downloads folder recently
 * will be displayed in Recent Document folder.
 */
export async function recentDocumentsDownloads() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [RECENTLY_MODIFIED_DOCUMENT], []);
  await verifyRecentDocuments(
      appId, [RECENTLY_MODIFIED_DOCUMENT], /*trashButton=*/ true);
}

/**
 * Tests that if the video file entries with MIME type are being populated in
 * both Downloads folder and My Drive folder, the file entries will be displayed
 * in Recent Document folder regardless of whether it's from Downloads or My
 * Drive.
 */
export async function recentDocumentsDownloadsAndDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [RECENTLY_MODIFIED_DOCUMENT],
      [RECENTLY_MODIFIED_DOCUMENT, RECENTLY_MODIFIED_VIDEO]);
  // RECENTLY_MODIFIED_DOCUMENT exists in both local and drive folder, the
  // file will appear twice in the result. RECENTLY_MODIFIED_VIDEO won't
  // be included because it's not a Document.
  await verifyRecentDocuments(
      appId, [RECENTLY_MODIFIED_DOCUMENT, RECENTLY_MODIFIED_DOCUMENT]);
}

/**
 * Tests that the document file entries populated in Downloads, Drive and Play
 * Files folder recently will be displayed in Recent Document folder.
 */
export async function recentDocumentsDownloadsAndDriveAndPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [RECENTLY_MODIFIED_DOCUMENT],
      [RECENTLY_MODIFIED_DOCUMENT]);
  await verifyRecentDocuments(appId, [
    RECENTLY_MODIFIED_DOCUMENT,
    RECENTLY_MODIFIED_DOCUMENT,
    RECENT_MODIFIED_ANDROID_DOCUMENT,
  ]);
}

/**
 * Tests if an active filter button is clicked again, it will become inactive
 * and the "All" filter button will become active and focus.
 */
export async function recentsFilterResetToAll() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  await navigateToRecent(appId, RecentFilterType.AUDIO);
  // Clicks the active "Audio" filter button.
  await remoteCall.waitAndClickElement(
      appId, ['[file-type-filter="audio"].active']);
  // Verifies the "All" button is focus and all recent files are shown.
  await remoteCall.waitForElement(appId, ['[file-type-filter="all"].active']);
  const focusedElement =
      await remoteCall.callRemoteTestUtil<ElementObject|null>(
          'getActiveElement', appId, []);
  chrome.test.assertEq('all', focusedElement?.attributes['file-type-filter']);
  await verifyCurrentEntries(appId, RECENT_ENTRY_SET, /*trashButton=*/ true);
}

/**
 * Tests if directory changes to a non-Recents folder, the sorting should be
 * reset to the original one (the one before entering Recents) and the group
 * heading should be hidden.
 */
export async function recentsSortingResetAfterChangingDirectory() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);

  // Change the sorting to "Size".
  await remoteCall.waitAndClickElement(appId, '#sort-button');
  await remoteCall.waitAndClickElement(
      appId, '#sort-menu #sort-menu-sort-by-size');
  await remoteCall.waitForElement(
      appId, '.table-header-label.size #sort-direction-button');
  // Navigate to Recents and click the "Audio" filter button.
  await navigateToRecent(appId, RecentFilterType.AUDIO);
  // Check the sorting is changed to "Date modified" and group heading is shown.
  await remoteCall.waitForElement(
      appId, '.table-header-label.modificationTime #sort-direction-button');
  await remoteCall.waitForElement(
      appId, '.group-heading.group-by-modificationTime');
  // Navigate back to Downloads folder.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByLabel('Downloads');
  // Check the sorting resets back to "Size" and group heading is hidden.
  await remoteCall.waitForElement(
      appId, '.table-header-label.size #sort-direction-button');
  await remoteCall.waitForElementLost(appId, '.group-heading');
}

/**
 * Tests when we switch the active filter button between All and others, the
 * correct a11y messages will be announced.
 */
export async function recentsA11yMessages() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  await navigateToRecent(appId, RecentFilterType.IMAGE);
  // Checks "images filter on" a11y message is announced.
  let a11yMessages = await remoteCall.callRemoteTestUtil<string[]>(
      'getA11yAnnounces', appId, []);
  chrome.test.assertEq(
      'Images filter is on.', a11yMessages[a11yMessages.length - 1]);

  // Clicks the "Videos" filter button to activate it.
  await remoteCall.waitAndClickElement(appId, ['[file-type-filter="video"]']);
  await remoteCall.waitForElement(appId, ['[file-type-filter="video"].active']);
  // Checks "video filter on" a11y message is announced.
  a11yMessages =
      await remoteCall.callRemoteTestUtil('getA11yAnnounces', appId, []);
  chrome.test.assertEq(
      'Images filter is off. Videos filter is on.',
      a11yMessages[a11yMessages.length - 1]);

  // Clicks the active "Videos" filter button again.
  await remoteCall.waitAndClickElement(
      appId, ['[file-type-filter="video"].active']);
  await remoteCall.waitForElement(appId, ['[file-type-filter="all"].active']);
  // Checks "filter reset" a11y message is announced.
  a11yMessages =
      await remoteCall.callRemoteTestUtil('getA11yAnnounces', appId, []);
  chrome.test.assertEq(
      'Videos filter is off. Filter is reset.',
      a11yMessages[a11yMessages.length - 1]);
}

/**
 * Tests the read only flag on Recents view should be hidden.
 */
export async function recentsReadOnlyHidden() {
  const appId = await remoteCall.setupAndWaitUntilReady(RootPath.DOWNLOADS);
  await navigateToRecent(appId);
  const readOnlyIndicator =
      await remoteCall.waitForElement(appId, ['#read-only-indicator']);
  chrome.test.assertTrue(
      readOnlyIndicator.hidden, 'Read only indicator should be hidden');
}

/**
 * Tests delete operation can be performed in Recents view on files from
 * Downloads, Drive and Play Files.
 */
export async function recentsAllowDeletion() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], [ENTRIES.desktop]);
  await navigateToRecent(appId);
  const files = TestEntryInfo.getExpectedRows([
    ENTRIES.beautiful,
    ENTRIES.desktop,
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  await remoteCall.waitForFiles(appId, files);

  // Delete a file originated from Downloads.
  await deleteFile(appId, ENTRIES.beautiful.nameText);
  const files1 = TestEntryInfo.getExpectedRows([
    ENTRIES.desktop,
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  await remoteCall.waitForFiles(appId, files1);

  // Delete a file originated from Drive.
  await deleteFile(appId, ENTRIES.desktop.nameText, /*confirmDeletion=*/ true);
  const files2 = TestEntryInfo.getExpectedRows([
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  await remoteCall.waitForFiles(appId, files2);

  // Delete a file originated from Play Files.
  await deleteFile(
      appId, RECENT_MODIFIED_ANDROID_IMAGE.nameText, /*confirmDeletion=*/ true);
  const files3 = TestEntryInfo.getExpectedRows(
      [RECENT_MODIFIED_ANDROID_DOCUMENT, RECENT_MODIFIED_ANDROID_VIDEO]);
  await remoteCall.waitForFiles(appId, files3);
}

/**
 * Tests delete operation can be performed in Recents view with multiple files
 * from different sources including Downloads, Drive and Play Files.
 */
export async function recentsAllowMultipleFilesDeletion() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], [ENTRIES.desktop]);
  await navigateToRecent(appId);
  const files = TestEntryInfo.getExpectedRows([
    ENTRIES.beautiful,
    ENTRIES.desktop,
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  await remoteCall.waitForFiles(appId, files);

  // Select all files from the gear menu.
  await remoteCall.waitAndClickElement(appId, '#gear-button');
  const selectAllMenu = '#gear-menu:not([hidden]) ' +
      `[command="#select-all"]:not([hidden]):not([disabled])`;
  await remoteCall.waitAndClickElement(appId, selectAllMenu);
  await remoteCall.waitForElement(appId, '.table-row[selected]');
  // Wait for the files selection label.
  const caller = getCaller();
  await repeatUntil(async () => {
    const element =
        await remoteCall.waitForElement(appId, '#files-selected-label');
    const expectedLabel = '5 files selected';

    if (element.text === expectedLabel) {
      return;
    }

    return pending(
        caller,
        `Expected files selection label: "${expectedLabel}", got "${
            element.text}"`);
  });
  // Delete all selected files via action bar.
  await remoteCall.waitAndClickElement(appId, '#delete-button');
  // Click okay on the confirm dialog.
  await remoteCall.waitAndClickElement(
      appId, '.files-confirm-dialog button.cr-dialog-ok');

  // Check all files should be deleted.
  await remoteCall.waitForFiles(appId, []);
}

/**
 * Tests rename operation can be performed in Recents view on files from
 * Downloads, Drive.
 */
export async function recentsAllowRename() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], [ENTRIES.desktop]);
  await navigateToRecent(appId);
  const files =
      TestEntryInfo.getExpectedRows([ENTRIES.beautiful, ENTRIES.desktop]);
  await remoteCall.waitForFiles(appId, files);

  // Rename a file originated from Downloads.
  const newBeautiful = ENTRIES.beautiful.cloneWithNewName('new-beautiful.ogg');
  await renameFile(appId, ENTRIES.beautiful.nameText, newBeautiful.nameText);
  const files1 = TestEntryInfo.getExpectedRows([
    newBeautiful,
    ENTRIES.desktop,
  ]);
  await remoteCall.waitForFiles(appId, files1);

  // Rename a file originated from Drive.
  const newDesktop = ENTRIES.desktop.cloneWithNewName('new-desktop.png');
  await renameFile(appId, ENTRIES.desktop.nameText, newDesktop.nameText);
  const files2 = TestEntryInfo.getExpectedRows([
    newDesktop,
    newBeautiful,
  ]);
  await remoteCall.waitForFiles(appId, files2);
}

/**
 * Tests rename operation is not allowed in Recents view for files from Play
 * files.
 */
export async function recentsNoRenameForPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);
  await navigateToRecent(appId);
  const files = TestEntryInfo.getExpectedRows([
    ENTRIES.beautiful,
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  await remoteCall.waitForFiles(appId, files);

  // Select the item.
  await remoteCall.waitUntilSelected(
      appId, RECENT_MODIFIED_ANDROID_DOCUMENT.nameText);

  // Right-click the selected file.
  await remoteCall.waitAndRightClick(appId, '.table-row[selected]');

  // Checks the rename menu should be disabled.
  const renameMenu = '#file-context-menu:not([hidden]) ' +
      '[command="#rename"][disabled]:not([hidden])';
  await remoteCall.waitForElement(appId, renameMenu);
}

/**
 * Tests cut operation can be performed in Recents view on files from Downloads.
 */
export async function recentsAllowCutForDownloads() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, ENTRIES.directoryA], []);
  const files = [ENTRIES.beautiful.getExpectedRow()];
  const newFolderBreadcrumb =
      `/My files/Downloads/${ENTRIES.directoryA.nameText}`;

  // Cut/Paste a file originated from Downloads.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  await cutFileAndPasteTo(
      appId, ENTRIES.beautiful.nameText, newFolderBreadcrumb);
  // The file being cut should appear in the new directory.
  await remoteCall.waitForFiles(appId, files);
  // Recents view still have the full file list because the file being cut just
  // moves to a new directory, but it still belongs to Recent.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  // Use "go to location" to validate the file in Recents after cut is
  // collected from the new folder.
  await goToFileLocation(appId, ENTRIES.beautiful.nameText);
  await remoteCall.waitForFiles(appId, files);
  await verifyBreadcrumbsPath(appId, newFolderBreadcrumb);
}

/**
 * Tests cut operation can be performed in Recents view on files from Drive.
 */
export async function recentsAllowCutForDrive() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.directoryA], [ENTRIES.desktop]);
  const files = TestEntryInfo.getExpectedRows([ENTRIES.desktop]);
  const newFolderBreadcrumb =
      `/My files/Downloads/${ENTRIES.directoryA.nameText}`;

  // Cut/Paste a file originated from Drive.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  await cutFileAndPasteTo(appId, ENTRIES.desktop.nameText, newFolderBreadcrumb);
  // The file being cut should appear in the new directory.
  await remoteCall.waitForFiles(appId, files);
  // Recents view still have the full file list because the file being cut just
  // moves to a new directory, but it still belongs to Recent.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  // Use "go to location" to validate the file in Recents after cut is
  // collected from the new folder.
  await goToFileLocation(appId, ENTRIES.desktop.nameText);
  await remoteCall.waitForFiles(appId, files);
  await verifyBreadcrumbsPath(appId, newFolderBreadcrumb);
}

/**
 * Tests cut operation can be performed in Recents view on files from Play
 * Files.
 */
export async function recentsAllowCutForPlayFiles() {
  await addPlayFileEntries();
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.directoryA], []);
  const files = TestEntryInfo.getExpectedRows([
    RECENT_MODIFIED_ANDROID_DOCUMENT,
    RECENT_MODIFIED_ANDROID_IMAGE,
    RECENT_MODIFIED_ANDROID_VIDEO,
  ]);
  const newFolderBreadcrumb =
      `/My files/Downloads/${ENTRIES.directoryA.nameText}`;

  // Cut/Paste a file originated from Play Files.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  await cutFileAndPasteTo(
      appId, RECENT_MODIFIED_ANDROID_IMAGE.nameText, newFolderBreadcrumb);
  // The file being cut should appear in the new directory.
  const filesInNewDir =
      TestEntryInfo.getExpectedRows([RECENT_MODIFIED_ANDROID_IMAGE]);
  await remoteCall.waitForFiles(appId, filesInNewDir);
  // Recents view still have the full file list because the file being cut just
  // moves to a new directory, but it still belongs to Recent.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(appId, files);
  // Use "go to location" to validate the file in Recents after cut is
  // collected from the new folder.
  await goToFileLocation(appId, RECENT_MODIFIED_ANDROID_IMAGE.nameText);
  await remoteCall.waitForFiles(appId, filesInNewDir);
  await verifyBreadcrumbsPath(appId, newFolderBreadcrumb);
}

/**
 * Tests the time-period group heading can be displayed in Recents.
 */
export async function recentsTimePeriodHeadings() {
  const todayFile = ENTRIES.hello.cloneWithModifiedDate(getDateWithDayDiff(0));
  const yesterdayFile =
      ENTRIES.desktop.cloneWithModifiedDate(getDateWithDayDiff(1));
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [todayFile, yesterdayFile], []);
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([todayFile, yesterdayFile]), {
        // Ignore last modified time because it will show Today/Yesterday
        // instead of the actual date.
        ignoreLastModifiedTime: true,
      });
  // Check headings in list view mode.
  await remoteCall.waitForElementsCount(appId, ['.group-heading'], 2);
  const groupHeadings =
      await remoteCall.queryElements(appId, ['.group-heading']);
  const fileItems =
      await remoteCall.queryElements(appId, ['.group-heading + .table-row']);
  chrome.test.assertEq(2, fileItems.length);

  chrome.test.assertEq('Today', groupHeadings[0]?.text);
  chrome.test.assertEq(
      todayFile.nameText, fileItems[0]?.attributes['file-name']);
  chrome.test.assertEq('Yesterday', groupHeadings[1]?.text);
  chrome.test.assertEq(
      yesterdayFile.nameText, fileItems[1]?.attributes['file-name']);

  // Switch to grid view.
  await remoteCall.waitAndClickElement(appId, '#view-button');
  await remoteCall.waitForElementsCount(appId, ['.grid-title'], 2);
  // Check headings in grid view mode.
  const groupTitles = await remoteCall.queryElements(appId, ['.grid-title']);
  const gridItems =
      await remoteCall.queryElements(appId, ['.grid-title + .thumbnail-item']);
  chrome.test.assertEq(2, gridItems.length);

  chrome.test.assertEq('Today', groupTitles[0]?.text);
  chrome.test.assertEq(
      todayFile.nameText, gridItems[0]?.attributes['file-name']);
  chrome.test.assertEq('Yesterday', groupTitles[1]?.text);
  chrome.test.assertEq(
      yesterdayFile.nameText, gridItems[1]?.attributes['file-name']);
}

/**
 * Tests message will show in Recents for empty folder.
 */
export async function recentsEmptyFolderMessage() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.directoryA], []);
  await navigateToRecent(appId);
  // All filter is on by default.
  await waitForEmptyFolderMessage(appId, 'No recent files');
  // Activates to audio filter.
  await remoteCall.waitAndClickElement(appId, [`[file-type-filter="audio"]`]);
  await waitForEmptyFolderMessage(appId, 'No recent audio files');
  // Activates to documents filter.
  await remoteCall.waitAndClickElement(
      appId, [`[file-type-filter="document"]`]);
  await waitForEmptyFolderMessage(appId, 'No recent documents');
  // Activates to images filter.
  await remoteCall.waitAndClickElement(appId, [`[file-type-filter="image"]`]);
  await waitForEmptyFolderMessage(appId, 'No recent images');
  // Activates to videos filter.
  await remoteCall.waitAndClickElement(appId, [`[file-type-filter="video"]`]);
  await waitForEmptyFolderMessage(appId, 'No recent videos');
}


/**
 * Tests message will show in Recents after the last file is deleted.
 */
export async function recentsEmptyFolderMessageAfterDeletion() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful], []);
  await navigateToRecent(appId);
  const files = TestEntryInfo.getExpectedRows([ENTRIES.beautiful]);
  await remoteCall.waitForFiles(appId, files);
  await deleteFile(appId, ENTRIES.beautiful.nameText);
  await waitForEmptyFolderMessage(appId, 'No recent files');
}

/**
 * Construct a file with modified date as 1am today in a specific timezone.
 * @param timezone the timezone string
 */
function prepareFileFor1AMToday(timezone: string): TestEntryInfo {
  const nowDate = new Date();
  nowDate.setHours(1, 0, 0, 0);
  // Format: "May 2, 2021, 11:25 AM GMT+1000"
  const modifiedDate = sanitizeDate(nowDate.toLocaleString('default', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
    hour12: true,
    hour: 'numeric',
    minute: 'numeric',
    timeZone: timezone,
    timeZoneName: 'longOffset',
  }));
  return ENTRIES.beautiful.cloneWithModifiedDate(modifiedDate);
}

/**
 * Tests the group heading and modified date column in the list view will
 * change once the timezone changes.
 */
export async function recentsRespondToTimezoneChangeForListView() {
  // Set timezone to Brisbane (GMT+10).
  await sendTestMessage({name: 'setTimezone', timezone: 'Australia/Brisbane'});
  const testFile = prepareFileFor1AMToday('Australia/Brisbane');
  const isEarlierThan2AM = (new Date()).getHours() < 2;

  // Open Files app and go to Recent tab.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [testFile], []);
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([testFile]), {
        // Ignore last modified time because it will show Today/Yesterday
        // instead of the actual date.
        ignoreLastModifiedTime: true,
      });
  // Check date modified column.
  const filesBefore =
      await remoteCall.callRemoteTestUtil<string[][]>('getFileList', appId, []);
  chrome.test.assertEq(filesBefore[0]![3], 'Today 1:00 AM');
  // Check group heading.
  const groupHeadingBefore =
      await remoteCall.waitForElement(appId, ['.group-heading']);
  chrome.test.assertEq('Today', groupHeadingBefore.text);

  // Set timezone to Perth (GMT+8).
  await sendTestMessage({name: 'setTimezone', timezone: 'Australia/Perth'});

  // If the OS time before timezone change is earlier than 2am, then after
  // timezone change the current date will move to a day before, the OS and
  // date and modification date changes at the same time, so it will be "today".
  // For example:
  // before timezone change: os time 1:30am file modification time: today 1am
  // move by -2 hours: os time 11:30pm file modification time: today 11pm
  const targetDate = isEarlierThan2AM ? 'Today' : 'Yesterday';
  const targetTime = '11:00 PM';

  // Check date modified column.
  const caller = getCaller();
  await repeatUntil(async () => {
    const filesAfter = await remoteCall.callRemoteTestUtil<string[][]>(
        'getFileList', appId, []);
    // We need to assert the exact time here, so the timezones before/after
    // should not involve daylight savings.
    if (filesAfter[0]![3] === `${targetDate} ${targetTime}`) {
      return;
    }

    return pending(
        caller,
        `Expected modified date to be "${targetDate} ${targetTime}", got "${
            filesAfter[0]![3]}"`);
  });

  // Check group heading.
  const groupHeadingAfter =
      await remoteCall.waitForElement(appId, ['.group-heading']);
  chrome.test.assertEq(targetDate, groupHeadingAfter.text);
}

/**
 * Tests the group heading in the grid view will change once the timezone
 * changes.
 */
export async function recentsRespondToTimezoneChangeForGridView() {
  // Set timezone to Brisbane (GMT+10).
  await sendTestMessage({name: 'setTimezone', timezone: 'Australia/Brisbane'});
  const testFile = prepareFileFor1AMToday('Australia/Brisbane');
  const isEarlierThan2AM = (new Date()).getHours() < 2;

  // Open Files app and go to Recent tab.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [testFile], []);
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([testFile]), {
        // Ignore last modified time because it will show Today/Yesterday
        // instead of the actual date.
        ignoreLastModifiedTime: true,
      });
  // Switch to grid view.
  await remoteCall.waitAndClickElement(appId, '#view-button');
  // Check group heading.
  const groupHeadingBefore =
      await remoteCall.waitForElement(appId, ['.grid-title']);
  chrome.test.assertEq('Today', groupHeadingBefore.text);

  // Set timezone to Perth (GMT+8).
  await sendTestMessage({name: 'setTimezone', timezone: 'Australia/Perth'});

  const targetDate = isEarlierThan2AM ? 'Today' : 'Yesterday';

  // Check group heading.
  const caller = getCaller();
  await repeatUntil(async () => {
    const groupHeadingAfter =
        await remoteCall.waitForElement(appId, ['.grid-title']);
    if (groupHeadingAfter.text === targetDate) {
      return;
    }

    return pending(
        caller,
        `Expected group heading to be "${targetDate}", got "${
            groupHeadingAfter.text}"`);
  });
}

/**
 * Tests the search term will be respected when switching between different
 * filter buttons.
 */
export async function recentsRespectSearchWhenSwitchingFilter() {
  // tall.txt
  const txtFile1 =
      ENTRIES.tallText.cloneWithModifiedDate(getDateWithDayDiff(4));
  // utf8.txt
  const txtFile2 =
      ENTRIES.utf8Text.cloneWithModifiedDate(getDateWithDayDiff(5));
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, [ENTRIES.beautiful, txtFile1, txtFile2], []);
  // Before search, 3 files shows in the Recent tab.
  await navigateToRecent(appId);
  const files =
      TestEntryInfo.getExpectedRows([ENTRIES.beautiful, txtFile1, txtFile2]);
  await remoteCall.waitForFiles(appId, files);

  // Search term "tall".
  await remoteCall.typeSearchText(appId, 'tall');

  // Check only tall.txt should show.
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([txtFile1]));

  // Switch to "Document" filter. Since search is active, use search options.
  chrome.test.assertTrue(
      !!await remoteCall.selectSearchOption(appId, 'type', 3),
      'Failed to click "Documents" type selector');

  // Check there is still only tall.txt in the file list (no utf8.txt).
  await remoteCall.waitForFiles(
      appId, TestEntryInfo.getExpectedRows([txtFile1]));
}

/**
 * Checks that Recents folder shows files from file system provider.
 */
export async function recentFileSystemProviderFiles() {
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
  // Add 4 levels of folders to the provided file system. We wish to test that
  // recently modified files appear in the Recent view, but also use this test
  // to document the current limit of nesting enforced by the recent view for
  // provided files.
  const testFolders = [
    new TestEntryInfo({
      type: EntryType.DIRECTORY,
      targetPath: '/Level1',
      mimeType: 'text/plain',
      lastModifiedTime: 'Fri, 25 Apr 2014 01:47:53',
      nameText: 'Level1',
      sizeText: '',
      typeText: '',
    }),
    new TestEntryInfo({
      type: EntryType.DIRECTORY,
      targetPath: '/Level1/Level2',
      mimeType: 'text/plain',
      lastModifiedTime: 'Fri, 25 Apr 2014 01:47:53',
      nameText: 'Level2',
      sizeText: '',
      typeText: '',
    }),
    new TestEntryInfo({
      type: EntryType.DIRECTORY,
      targetPath: '/Level1/Level2/Level3',
      mimeType: 'text/plain',
      lastModifiedTime: 'Fri, 25 Apr 2014 01:47:53',
      nameText: 'Level3',
      sizeText: '',
      typeText: '',
    }),
  ];
  const testEntries = [
    RECENT_PROVIDED_HELLO,
    RECENT_PROVIDED_HELLO.cloneWith({
      targetPath: '/Level1/recent-hello1.txt',
      nameText: 'recent-hello1.txt',
    }),
    RECENT_PROVIDED_HELLO.cloneWith({
      targetPath: '/Level1/Level2/recent-hello2.txt',
      nameText: 'recent-hello2.txt',
    }),
    RECENT_PROVIDED_HELLO.cloneWith({
      targetPath: '/Level1/Level2/Level3/recent-hello3.txt',
      nameText: 'recent-hello3.txt',
    }),
  ];
  await addEntries(['provided'], testFolders.concat(testEntries));

  // Expect that regardless of the depth of folder nesting, all recently
  // modified files are present.
  await navigateToRecent(appId);
  await remoteCall.waitForFiles(
      appId,
      TestEntryInfo.getExpectedRows(RECENT_ENTRY_SET.concat(testEntries)));
}