chromium/ui/file_manager/integration_tests/test_util.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 {CHOOSE_ENTRY_PROPERTY} from './file_manager/choose_entry_const.js';

interface TestMessageCommand {
  name: string;
  [key: string]: any;
}
/**
 * Sends a command to the controlling test harness, namely and usually, the
 * chrome FileManagerBrowserTest harness: it expects the command to contain the
 * 'name' of the command, and any required or optional arguments of the command,
 * e.g.,
 *
 *   await sendTestMessage({
 *     name: 'addEntries', // command with volume and entries arguments
 *     volume: volume,
 *     entries: entries
 *   });
 *
 * @param command Test command to send. The object is converted to a JSON string
 *     prior to sending.
 * @return Promise to be fulfilled with the value returned by the
 *     `chrome.test.sendMessage` callback.
 */
export async function sendTestMessage(command: TestMessageCommand):
    Promise<string> {
  if (typeof command.name === 'string') {
    return new Promise(
        fulfill => chrome.test.sendMessage(JSON.stringify(command), fulfill));
  }
  const error = 'sendTestMessage requires a command.name <string>';
  throw new Error(error);
}

/**
 * Waits (aka pauses, or sleeps) for the given time in milliseconds.
 * @param time Time in milliseconds.
 * @return Promise that will resolve after Time in milliseconds has elapsed.
 */
export function wait(time: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, time));
}

/** Interval milliseconds between checks of repeatUntil. */
export const REPEAT_UNTIL_INTERVAL = 200;

/** Interval milliseconds between log output of repeatUntil. */
export const LOG_INTERVAL = 3000;

/**
 * Returns caller's file, function and line/column number from the call stack.
 * @return String with the caller's file name and line/column number, as
 *     returned by exception stack trace. Example "at /a_file.js:1:1".
 */
export function getCaller(): string {
  const error = new Error('For extracting error.stack');
  const ignoreStackLines = 3;
  const lines = error.stack?.split('\n') ?? [];
  if (ignoreStackLines < lines.length) {
    const caller = lines[ignoreStackLines] ?? '';
    // Strip 'chrome-extension://oobinhbdbiehknkpbpejbbpdbkdjmoco' prefix.
    return caller.replace(/(chrome-extension:\/\/\w*)/gi, '').trim();
  }
  return '';
}


/**
 * Returns a pending marker. See also the repeatUntil function.
 * @param caller name of test function that originated the operation, it's the
 *     return of getCaller() function.
 * @param message Pending reason including %s, %d, or %j markers. %j format an
 *     object as JSON.
 * @param args Values to be assigined to %x markers.
 * @return Object which returns true for the expression: obj instanceof
 *     pending.
 */
export function pending(
    caller: string, message: string, ...args: any[]): PendingFunction {
  let index = 0;
  message = String(message);
  const formattedMessage = message.replace(/%[sdj]/g, (pattern) => {
    const arg = args[index++];
    switch (pattern) {
      case '%s':
        return String(arg);
      case '%d':
        return String(Number(arg));
      case '%j':
        return JSON.stringify(arg);
      default:
        return pattern;
    }
  });
  const pendingMarker = Object.create(pending.prototype);
  pendingMarker.message = caller + ': ' + formattedMessage;
  return pendingMarker;
}

type PendingFunction = typeof pending&{
  message: string,
};

/**
 * Waits until the checkFunction returns a value but a pending marker.
 * @param checkFunction Function to check a condition. It can return a pending
 *     marker created by a pending function.
 * @return Promise to be fulfilled with the return value of checkFunction when
 *     the checkFunction returns a value but a pending marker.
 */
export async function repeatUntil(checkFunction: (() => PendingFunction | any)):
    Promise<any> {
  let logTime = Date.now() + LOG_INTERVAL;
  while (true) {
    const result = await checkFunction();
    if (!(result instanceof pending)) {
      return result;
    }
    if (Date.now() > logTime) {
      console.warn((result as PendingFunction).message);
      logTime += LOG_INTERVAL;
    }
    await wait(REPEAT_UNTIL_INTERVAL);
  }
}

/**
 * Sends the test `command` to the browser test harness and awaits a string
 * result.
 * @param command Test command to send. Refer to sendTestMessage() above for the
 *     expected format of a test `command` object.
 * @param debug If truthy, log the result.
 */
export async function sendBrowserTestCommand(
    command: TestMessageCommand, debug: boolean = false): Promise<string> {
  const caller = getCaller();
  if (typeof command.name !== 'string') {
    chrome.test.fail('Invalid test command: ' + JSON.stringify(command));
  }
  const result = await repeatUntil(async () => {
    const tryAgain = pending(caller, 'Sent BrowserTest ' + command.name);
    try {
      const result = await sendTestMessage(command);
      if (typeof result !== 'string') {
        return tryAgain;
      }
      return result;
    } catch (error: any) {
      console.log(error.stack || error);
      return tryAgain;
    }
  });
  if (debug) {
    console.log('BrowserTest ' + command.name + ': ' + result);
  }
  return result;
}

/**
 * Get all the browser windows.
 * @param expectedInitialCount The number of windows expected before opening a
 *     new one.
 * @return Object returned from `chrome.windows.getAll()`.
 */
export async function getBrowserWindows(expectedInitialCount: number = 0):
    Promise<chrome.windows.Window[]> {
  const caller = getCaller();
  return repeatUntil(async () => {
    const result = await new Promise<chrome.windows.Window[]>((fulfill) => {
      chrome.windows.getAll({'populate': true}, fulfill);
    });
    if (result.length === expectedInitialCount) {
      return pending(caller, 'getBrowserWindows ' + result.length);
    }
    return result;
  });
}

/**
 * Adds the given entries to the target volume(s).
 *
 * Note: passing 'local' as volume name will add entries to the "My
 * Files/Downloads", instead of "My files".
 *
 * @param volumeNames Names of target volumes.
 * @param entries List of entries to be added.
 * @return Promise to be fulfilled when the entries are added.
 */
export async function addEntries(
    volumeNames: string[], entries: TestEntryInfo[]): Promise<string[]> {
  if (volumeNames.length === 0) {
    return [];
  }
  const volumeResultPromises = volumeNames.map((volume) => {
    return sendTestMessage({
      name: 'addEntries',
      volume: volume,
      entries: entries,
    });
  });
  return Promise.all(volumeResultPromises);
}

export enum EntryType {
  FILE = 'file',
  DIRECTORY = 'directory',
  LINK = 'link',
  SHARED_DRIVE = 'team_drive',
  COMPUTER = 'Computer',
}

/** Enumeration that determines the shared status of entries. */
export enum SharedOption {
  // Not shared.
  NONE = 'none',

  // Shared but not visible in the 'Shared with me' view.
  SHARED = 'shared',

  // Shared and appears in the 'Shared With Me' view.
  SHARED_WITH_ME = 'sharedWithMe',

  // Not directly shared, but belongs to a folder that is shared with me.
  // Entries marked as indirectly shared do not have the 'shared' metadata
  // field, and thus cannot be located via search for shared items.
  INDIRECTLY_SHARED_WITH_ME = 'indirectlySharedWithMe',
}

export interface GetRootPathsResult {
  downloads: string;
  my_files: string;
  drive: string;
  android_files: string;
}

export const RootPath = {
  DOWNLOADS: '/must-be-filled-in-test-setup',
  MY_FILES: '/must-be-filled-in-test-setup',
  DRIVE: '/must-be-filled-in-test-setup',
  ANDROID_FILES: '/must-be-filled-in-test-setup',
};
Object.seal(RootPath);


/**
 * The capabilities (permissions) for the Test Entry. Structure should match
 * TestEntryCapabilities in file_manager_browsertest_base.cc. All capabilities
 * default to true if not specified.
 */
export interface TestEntryCapabilities {
  canCopy?: boolean;
  canDelete?: boolean;
  canRename?: boolean;
  canAddChildren?: boolean;
  canShare?: boolean;
}

/**
 * The folder features for the test entry. Structure should match
 * TestEntryFolderFeature in file_manager_browsertest_base.cc. All features
 * default to false is not specified.
 */
export interface TestEntryFolderFeature {
  isMachineRoot?: boolean;
  isArbitrarySyncFolder?: boolean;
  isExternalMedia?: boolean;
}

/**
 * Parameters to creat a Test Entry in the file manager. Structure should match
 * TestEntryInfo in file_manager_browsertest_base.cc.
 *
 * Field details:
 *
 * sourceFileName: Source file name that provides file contents (file location
 * relative to /chrome/test/data/chromeos/file_manager/).
 *
 * targetPath: Name of entry on the test file system. Used to determine the
 * actual name of the file.
 *
 * teamDriveName: Name of the team drive this entry is in. Defaults to a blank
 * string (no team drive). Team Drive names must be unique.
 *
 * computerName: Name of the computer this entry is in. Defaults to a blank
 * string (no computer). Computer names must be unique.
 *
 * lastModifiedTime: Last modified time as a text to be shown in the last
 * modified column.
 *
 * nameText: File name to be shown in the name column.
 *
 * sizeText: Size text to be shown in the size column.
 *
 * typeText: Type name to be shown in the type column.
 *
 * capabilities:  Capabilities of this file. Defaults to all capabilities
 * available (read-write access).
 *
 * folderFeature: Folder features of this file. Defaults to all features
 * disabled.
 *
 * pinned: Drive pinned status of this file. Defaults to false.
 *
 * availableOffline: Whether the file is available offline. Defaults to false.
 *
 * alternateUrl: File's Drive alternate URL. Defaults to an empty string.
 *
 * canPin: Whether the item can be pinned or not. Defaults to true.
 */
export interface TestEntryInfoOptions {
  type: EntryType;
  sourceFileName?: string;
  targetPath?: string;
  teamDriveName?: string;
  computerName?: string;
  mimeType?: string;
  sharedOption?: SharedOption;
  lastModifiedTime?: string;
  nameText?: string;
  sizeText?: string;
  typeText?: string;
  capabilities?: TestEntryCapabilities;
  folderFeature?: TestEntryFolderFeature;
  pinned?: boolean;
  dirty?: boolean;
  availableOffline?: boolean;
  alternateUrl?: string;
  canPin?: boolean;
  thumbnailFileName?: string;
}

/**
 * File system entry information for tests. Structure should match TestEntryInfo
 * in file_manager_browsertest_base.cc
 * TODO(sashab): Remove this, rename TestEntryInfoOptions to TestEntryInfo and
 * set the defaults in the record definition above.
 */
export class TestEntryInfo {
  type: EntryType;
  sourceFileName: string;
  targetPath: string;
  teamDriveName: string;
  computerName: string;
  mimeType: string;
  sharedOption: SharedOption;
  lastModifiedTime?: string;
  nameText: string;
  sizeText: string;
  typeText: string;
  capabilities?: TestEntryCapabilities;
  folderFeature?: TestEntryFolderFeature;
  pinned: boolean;
  dirty: boolean;
  availableOffline: boolean;
  alternateUrl: string;
  canPin: boolean;
  thumbnailFileName: string;

  /**
   * @param options Parameters to create the TestEntryInfo.
   */
  constructor(options: TestEntryInfoOptions) {
    this.type = options.type;
    this.sourceFileName = options.sourceFileName || '';
    this.thumbnailFileName = options.thumbnailFileName || '';
    this.targetPath = options.targetPath || '';
    this.teamDriveName = options.teamDriveName || '';
    this.computerName = options.computerName || '';
    this.mimeType = options.mimeType || '';
    this.sharedOption = options.sharedOption || SharedOption.NONE;
    this.lastModifiedTime = options.lastModifiedTime;
    this.nameText = options.nameText || '';
    this.sizeText = options.sizeText || '';
    this.typeText = options.typeText || '';
    this.capabilities = options.capabilities;
    this.folderFeature = options.folderFeature;
    this.pinned = !!options.pinned;
    this.dirty = !!options.dirty;
    this.availableOffline = !!options.availableOffline;
    this.alternateUrl = options.alternateUrl || '';
    this.canPin = options.canPin !== undefined ? !!options.canPin : true;
    Object.freeze(this);
  }

  /**
   * Obtains the expected row contents for each file.
   */
  static getExpectedRows(entries: TestEntryInfo[]): string[][] {
    return entries.map((entry) => {
      return entry.getExpectedRow();
    });
  }

  /**
   * Obtains a expected row contents of the file in the file list.
   */
  getExpectedRow(): [string, string, string, string] {
    return [
      this.nameText,
      this.sizeText,
      this.typeText,
      this.lastModifiedTime ?? '',
    ];
  }

  /**
   * Returns a new entry with modified attributes specified in the
   * `newOptions` object.
   * @param newOptions  The options to be modified.
   */
  cloneWith(newOptions: Object): TestEntryInfo {
    return new TestEntryInfo(Object.assign({}, this, newOptions));
  }

  /**
   * Clone the existing TestEntryInfo object to a new TestEntryInfo object but
   * with modified lastModifiedTime field. This is especially useful for
   * constructing TestEntryInfo for Recents view.
   *
   * @param newDate the new modified date time
   */
  cloneWithModifiedDate(newDate: string): TestEntryInfo {
    return this.cloneWith({lastModifiedTime: newDate});
  }

  /**
   * Clone the existing TestEntryInfo object to a new TestEntryInfo object but
   * with modified targetPath field. This is especially useful for testing
   * rename functionality.
   *
   * @param newName the new modified name
   */
  cloneWithNewName(newName: string): TestEntryInfo {
    return this.cloneWith({
      targetPath: newName,
      nameText: newName,
    });
  }
}

/**
 * Filesystem entries used by the test cases.
 * TODO(sashab): Rename 'nameText', 'sizeText' and 'typeText' to
 * 'expectedNameText', 'expectedSizeText' and 'expectedTypeText' to reflect that
 * they are the expected values for those columns in the file manager.
 */
export const ENTRIES = {
  hello: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'hello.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'hello.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  dirty: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'dirty.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'dirty.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    dirty: true,
  }),

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

  webm: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'world.webm',
    targetPath: 'world.webm',
    mimeType: 'video/webm',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'world.webm',
    sizeText: '17 KB',
    typeText: 'WebM video',
  }),

  video: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video_long.ogv',
    targetPath: 'video_long.ogv',
    mimeType: 'video/ogg',
    lastModifiedTime: 'Jan 14, 2019, 16:01 PM',
    nameText: 'video_long.ogv',
    sizeText: '166 KB',
    typeText: 'OGG video',
  }),

  subtitle: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video.vtt',
    targetPath: 'world.vtt',
    mimeType: 'text/vtt',
    lastModifiedTime: 'Feb 7, 2019, 15:03 PM',
    nameText: 'world.vtt',
    sizeText: '46 bytes',
    typeText: 'VTT text',
  }),

  unsupported: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'random.bin',
    targetPath: 'unsupported.foo',
    mimeType: 'application/x-foo',
    lastModifiedTime: 'Jul 4, 2012, 10:36 AM',
    nameText: 'unsupported.foo',
    sizeText: '8 KB',
    typeText: 'FOO file',
  }),

  desktop: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image.png',
    thumbnailFileName: 'image.png',
    targetPath: 'My Desktop Background.png',
    mimeType: 'image/png',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'My Desktop Background.png',
    sizeText: '272 bytes',
    typeText: 'PNG image',
  }),

  image2: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image2.png',
    // No file extension.
    targetPath: 'image2',
    mimeType: 'image/png',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'image2',
    sizeText: '4 KB',
    typeText: 'PNG image',
  }),

  image3: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image3.jpg',
    targetPath: 'image3.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'image3.jpg',
    sizeText: '3 KB',
    typeText: 'JPEG image',
  }),

  smallJpeg: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'small.jpg',
    targetPath: 'small.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'small.jpg',
    sizeText: '1 KB',
    typeText: 'JPEG image',
  }),

  // Used to differentiate between .jpg and .jpeg handling.
  sampleJpeg: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'small.jpg',
    targetPath: 'sample.jpeg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'sample.jpeg',
    sizeText: '1 KB',
    typeText: 'JPEG image',
  }),

  brokenJpeg: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'broken.jpg',
    targetPath: 'broken.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'broken.jpg',
    sizeText: '1 byte',
    typeText: 'JPEG image',
  }),

  exifImage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'exif.jpg',
    // No mime type.
    targetPath: 'exif.jpg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'exif.jpg',
    sizeText: '31 KB',
    typeText: 'JPEG image',
  }),

  webpImage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image.webp',
    // No mime type.
    targetPath: 'image.webp',
    lastModifiedTime: 'Jan 19, 2021, 1:10 PM',
    nameText: 'image.webp',
    sizeText: '5 KB',
    typeText: 'WebP image',
  }),

  rawImage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'raw.orf',
    // No mime type.
    targetPath: 'raw.orf',
    lastModifiedTime: 'May 20, 2019, 10:10 AM',
    nameText: 'raw.orf',
    sizeText: '214 KB',
    typeText: 'ORF image',
  }),

  nefImage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'raw.nef',
    // No mime type.
    targetPath: 'raw.nef',
    lastModifiedTime: 'May 9, 2015, 11:16 PM',
    nameText: 'raw.nef',
    sizeText: '92 KB',
    typeText: 'NEF image',
  }),

  beautiful: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'music.ogg',
    // No mime type.
    targetPath: 'Beautiful Song.ogg',
    lastModifiedTime: 'Nov 12, 2086, 12:00 PM',
    nameText: 'Beautiful Song.ogg',
    sizeText: '14 KB',
    typeText: 'OGG audio',
  }),

  movFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'video.mov',
    targetPath: 'mac.mov',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'mac.mov',
    sizeText: '875 bytes',
    typeText: 'QuickTime video',
  }),

  docxFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.docx',
    targetPath: 'word.docx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.wordprocessingml.document',
    lastModifiedTime: 'Jul 4, 2038, 10:35 AM',
    nameText: 'word.docx',
    sizeText: '9 KB',
    typeText: 'Word document',
  }),

  photos: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'photos',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: 'photos',
    sizeText: '--',
    typeText: 'Folder',
  }),

  testCSEDocument: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'Test Encrypted Document',
    mimeType: 'application/vnd.google-gsuite.encrypted; ' +
        'content="application/vnd.google-apps.document"',
    lastModifiedTime: 'Apr 10, 2013, 4:20 PM',
    nameText: 'Test Encrypted Document.gdoc',
    sizeText: '--',
    typeText: 'Google document',
  }),

  testCSEFile: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'test-encrypted.txt',
    mimeType: 'application/vnd.google-gsuite.encrypted; content="text/plain"',
    lastModifiedTime: 'Apr 10, 2013, 4:20 PM',
    nameText: 'test-encrypted.txt',
    sizeText: '--',
    typeText: 'Plain text',
  }),

  // The directory itself is not encrypted, but will contain encrypted entries
  // like testCSEFileInDirectory
  testCSEDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'encrypted_files',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: 'encrypted_files',
    sizeText: '--',
    typeText: 'Folder',
  }),

  testCSEFileInDirectory: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'encrypted_files/test.txt',
    mimeType: 'application/vnd.google-gsuite.encrypted; content="text/plain"',
    lastModifiedTime: 'Apr 10, 2013, 4:20 PM',
    nameText: 'test.txt',
    sizeText: '--',
    typeText: 'Plain text',
  }),

  testDocument: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'Test Document',
    mimeType: 'application/vnd.google-apps.document',
    lastModifiedTime: 'Apr 10, 2013, 4:20 PM',
    nameText: 'Test Document.gdoc',
    sizeText: '--',
    typeText: 'Google document',
  }),

  testSharedDocument: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'Test Shared Document',
    mimeType: 'application/vnd.google-apps.document',
    sharedOption: SharedOption.SHARED,
    lastModifiedTime: 'Mar 20, 2013, 10:40 PM',
    nameText: 'Test Shared Document.gdoc',
    sizeText: '--',
    typeText: 'Google document',
  }),

  testSharedFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'test.txt',
    mimeType: 'text/plain',
    sharedOption: SharedOption.SHARED,
    lastModifiedTime: 'Mar 20, 2012, 11:40 PM',
    nameText: 'test.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    pinned: true,
  }),

  sharedDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Shared',
    sharedOption: SharedOption.SHARED,
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Shared',
    sizeText: '--',
    typeText: 'Folder',
  }),

  sharedDirectoryFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'Shared/file.txt',
    mimeType: 'text/plain',
    sharedOption: SharedOption.SHARED,
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'file.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  newlyAdded: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'music.ogg',
    targetPath: 'newly added file.ogg',
    mimeType: 'audio/ogg',
    lastModifiedTime: 'Sep 4, 1998, 12:00 AM',
    nameText: 'newly added file.ogg',
    sizeText: '14 KB',
    typeText: 'OGG audio',
  }),

  tallText: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'tall.txt',
    targetPath: 'tall.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'tall.txt',
    sizeText: '546 bytes',
    typeText: 'Plain text',
  }),

  plainText: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'plaintext',
    // No mime type, no file extension.
    targetPath: 'plaintext',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'plaintext',
    sizeText: '32 bytes',
    typeText: 'Plain text',
  }),

  utf8Text: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'utf8.txt',
    targetPath: 'utf8.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'utf8.txt',
    sizeText: '191 bytes',
    typeText: 'Plain text',
  }),

  mHtml: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'page.mhtml',
    targetPath: 'page.mhtml',
    mimeType: 'multipart/related',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'page.mhtml',
    sizeText: '421 bytes',
    typeText: 'HTML document',
  }),

  tallHtml: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'tall.html',
    targetPath: 'tall.html',
    mimeType: 'text/html',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'tall.html',
    sizeText: '589 bytes',
    typeText: 'HTML document',
  }),

  tallPdf: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'tall.pdf',
    targetPath: 'tall.pdf',
    mimeType: 'application/pdf',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'tall.pdf',
    sizeText: '15 KB',
    typeText: 'PDF document',
  }),

  popupPdf: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'popup.pdf',
    targetPath: 'popup.pdf',
    mimeType: 'application/pdf',
    lastModifiedTime: 'Jul 4, 2000, 10:42 AM',
    nameText: 'popup.pdf',
    sizeText: '538 bytes',
    typeText: 'PDF document',
  }),

  imgPdf: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'img.pdf',
    targetPath: 'imgpdf',
    mimeType: 'application/pdf',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'imgpdf',
    sizeText: '1608 bytes',
    typeText: 'PDF document',
  }),

  smallDocx: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.docx',
    targetPath: 'text.docx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.wordprocessingml.document',
    lastModifiedTime: 'Jan 4, 2019, 10:57 AM',
    nameText: 'text.docx',
    sizeText: '8.7 KB',
    typeText: 'Office document',
    alternateUrl: 'https://drive.google.com/open?id=smalldocxid&usp=drive_fs',
  }),

  smallDocxHosted: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.docx',
    targetPath: 'synced.docx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.wordprocessingml.document',
    lastModifiedTime: 'Jan 4, 2019, 10:57 AM',
    nameText: 'synced.docx',
    sizeText: '8.7 KB',
    typeText: 'Office document',
    alternateUrl: 'https://docs.google.com/document/d/smalldocxid' +
        '?rtpof=true&usp=drive_fs',
  }),

  smallDocxPinned: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.docx',
    targetPath: 'pinned.docx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.wordprocessingml.document',
    lastModifiedTime: 'Jan 4, 2019, 10:57 AM',
    nameText: 'pinned.docx',
    sizeText: '8.7 KB',
    typeText: 'Office document',
    pinned: true,
    alternateUrl: 'https://docs.google.com/document/d/pinneddocxid' +
        '?rtpof=true&usp=drive_fs',
  }),

  smallXlsxPinned: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'sheet.xlsx',
    targetPath: 'pinned.xlsx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.spreadsheetml.sheet',
    lastModifiedTime: 'Jan 10, 2020, 11:58 PM',
    nameText: 'pinned.xlsx',
    sizeText: '5.7 KB',
    typeText: 'Office spreadsheet',
    pinned: true,
    alternateUrl: 'https://docs.google.com/document/d/pinnedxlsxid' +
        '?rtpof=true&usp=drive_fs',
  }),

  smallPptxPinned: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'presentation.pptx',
    targetPath: 'pinned.pptx',
    mimeType: 'application/vnd.openxmlformats-officedocument' +
        '.presentationml.presentation',
    lastModifiedTime: 'Jan 14, 2020, 10:15 AM',
    nameText: 'pinned.pptx',
    sizeText: '35.2 KB',
    typeText: 'Office document',
    pinned: true,
    alternateUrl: 'https://docs.google.com/document/d/pinnedpptxid' +
        '?rtpof=true&usp=drive_fs',
  }),

  pinned: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'pinned.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'pinned.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    pinned: true,
  }),

  directoryA: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'A',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'A',
    sizeText: '--',
    typeText: 'Folder',
  }),

  directoryB: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'A/B',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'B',
    sizeText: '--',
    typeText: 'Folder',
  }),

  directoryC: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'A/B/C',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'C',
    sizeText: '--',
    typeText: 'Folder',
  }),

  directoryD: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'D',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'D',
    sizeText: '--',
    typeText: 'Folder',
  }),

  directoryE: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'D/E',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'E',
    sizeText: '--',
    typeText: 'Folder',
  }),

  directoryF: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'D/E/F',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'F',
    sizeText: '--',
    typeText: 'Folder',
  }),

  dotTrash: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: '.Trash',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: '.Trash',
    sizeText: '--',
    typeText: 'Folder',
  }),

  deeplyBuriedSmallJpeg: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'A/B/C/deep.jpg',
    sourceFileName: 'small.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'deep.jpg',
    sizeText: '886 bytes',
    typeText: 'JPEG image',
  }),

  linkGtoB: new TestEntryInfo({
    type: EntryType.LINK,
    targetPath: 'G',
    sourceFileName: 'A/B',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'G',
    sizeText: '--',
    typeText: 'Folder',
  }),

  linkHtoFile: new TestEntryInfo({
    type: EntryType.LINK,
    targetPath: 'H.jpg',
    sourceFileName: 'A/B/C/deep.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'H.jpg',
    sizeText: '886 bytes',
    typeText: 'JPEG image',
  }),

  linkTtoTransitiveDirectory: new TestEntryInfo({
    type: EntryType.LINK,
    targetPath: 'T',
    sourceFileName: 'G/C',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'T',
    sizeText: '--',
    typeText: 'Folder',
  }),

  zipArchive: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'archive.zip',
    targetPath: 'archive.zip',
    mimeType: 'application/x-zip',
    lastModifiedTime: 'Jan 1, 2014, 1:00 AM',
    nameText: 'archive.zip',
    sizeText: '743 bytes',
    typeText: 'ZIP archive',
  }),

  zipSJISArchive: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'sjis.zip',
    targetPath: 'sjis.zip',
    mimeType: 'application/x-zip',
    lastModifiedTime: 'Apr 6, 2022, 1:00 AM',
    nameText: 'sjis.zip',
    sizeText: '479 bytes',
    typeText: 'ZIP archive',
  }),

  zipExtArchive: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'tera.zip',
    targetPath: 'tera.zip',
    mimeType: 'application/x-zip',
    lastModifiedTime: 'Apr 6, 2022, 1:00 AM',
    nameText: 'tera.zip',
    sizeText: '250 bytes',
    typeText: 'ZIP archive',
  }),

  debPackage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'package.deb',
    targetPath: 'package.deb',
    mimeType: 'application/vnd.debian.binary-package',
    lastModifiedTime: 'Jan 1, 2014, 1:00 AM',
    nameText: 'package.deb',
    sizeText: '724 bytes',
    typeText: 'DEB file',
  }),

  tiniFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'archive.tar.gz',
    targetPath: 'test.tini',
    mimeType: 'application/gzip',
    lastModifiedTime: 'Jan 1, 2014, 1:00 AM',
    nameText: 'test.tini',
    sizeText: '439 bytes',
    typeText: 'Crostini image file',
  }),

  hiddenFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: '.hiddenfile.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 30, 2014, 3:30 PM',
    nameText: '.hiddenfile.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  // Team-drive entries.
  teamDriveA: new TestEntryInfo({
    type: EntryType.SHARED_DRIVE,
    teamDriveName: 'Team Drive A',
    capabilities: {
      canCopy: true,
      canDelete: true,
      canRename: true,
      canAddChildren: true,
      canShare: true,
    },
  }),

  teamDriveAFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'teamDriveAFile.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'teamDriveAFile.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    teamDriveName: 'Team Drive A',
    capabilities: {
      canCopy: true,
      canDelete: true,
      canRename: true,
      canAddChildren: false,
      canShare: true,
    },
  }),

  teamDriveADirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'teamDriveADirectory',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'teamDriveADirectory',
    sizeText: '--',
    typeText: 'Folder',
    teamDriveName: 'Team Drive A',
    capabilities: {
      canCopy: true,
      canDelete: true,
      canRename: true,
      canAddChildren: true,
      canShare: false,
    },
  }),

  teamDriveAHostedFile: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'teamDriveAHostedDoc',
    mimeType: 'application/vnd.google-apps.document',
    lastModifiedTime: 'Apr 10, 2013, 4:20 PM',
    nameText: 'teamDriveAHostedDoc.gdoc',
    sizeText: '--',
    typeText: 'Google document',
    teamDriveName: 'Team Drive A',
  }),

  teamDriveB: new TestEntryInfo({
    type: EntryType.SHARED_DRIVE,
    teamDriveName: 'Team Drive B',
    capabilities: {
      canCopy: true,
      canDelete: false,
      canRename: false,
      canAddChildren: false,
      canShare: true,
    },
  }),

  teamDriveBFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'teamDriveBFile.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'teamDriveBFile.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    teamDriveName: 'Team Drive B',
    capabilities: {
      canCopy: true,
      canDelete: false,
      canRename: false,
      canAddChildren: false,
      canShare: true,
    },
  }),

  teamDriveBDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'teamDriveBDirectory',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'teamDriveBDirectory',
    sizeText: '--',
    typeText: 'Folder',
    teamDriveName: 'Team Drive B',
    capabilities: {
      canCopy: true,
      canDelete: false,
      canRename: false,
      canAddChildren: false,
      canShare: true,
    },
  }),

  // Computer entries.
  computerA: new TestEntryInfo({
    type: EntryType.COMPUTER,
    computerName: 'Computer A',
    folderFeature: {
      isMachineRoot: true,
    },
  }),

  computerAFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'computerAFile.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'computerAFile.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    computerName: 'Computer A',
    capabilities: {
      canCopy: true,
      canDelete: true,
      canRename: true,
      canAddChildren: false,
      canShare: true,
    },
  }),

  computerAdirectoryA: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'A',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    computerName: 'Computer A',
    nameText: 'A',
    sizeText: '--',
    typeText: 'Folder',
  }),

  // Read-only and write-restricted entries.
  // TODO(sashab): Generate all combinations of capabilities inside the test, to
  // ensure maximum coverage.

  // A folder that can't be renamed or deleted or have children added, but can
  // be copied and shared.
  readOnlyFolder: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Read-Only Folder',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Read-Only Folder',
    sizeText: '--',
    typeText: 'Folder',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  // A google doc file that can't be renamed or deleted, but can be copied and
  // shared.
  readOnlyDocument: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'Read-Only Doc',
    mimeType: 'application/vnd.google-apps.document',
    lastModifiedTime: 'Mar 20, 2013, 10:40 PM',
    nameText: 'Read-Only Doc.gdoc',
    sizeText: '--',
    typeText: 'Google document',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  // A google doc file that can't be renamed, deleted, copied or shared.
  readOnlyStrictDocument: new TestEntryInfo({
    type: EntryType.FILE,
    targetPath: 'Read-Only (Strict) Doc',
    mimeType: 'application/vnd.google-apps.document',
    lastModifiedTime: 'Mar 20, 2013, 10:40 PM',
    nameText: 'Read-Only (Strict) Doc.gdoc',
    sizeText: '--',
    typeText: 'Google document',
    capabilities: {
      canCopy: false,
      canAddChildren: false,
      canRename: false,
      canDelete: false,
      canShare: false,
    },
  }),

  // A regular file that can't be renamed or deleted, but can be copied and
  // shared.
  readOnlyFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image4.jpg',
    targetPath: 'Read-Only File.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2038, 1:02 AM',
    nameText: 'Read-Only File.jpg',
    sizeText: '9 KB',
    typeText: 'JPEG image',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  // A ZIP file that can't be renamed or deleted, but can be copied and
  // shared.
  readOnlyZipFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'archive.zip',
    targetPath: 'archive.zip',
    mimeType: 'application/x-zip',
    lastModifiedTime: 'Jan 1, 2014, 1:00 AM',
    nameText: 'archive.zip',
    sharedOption: SharedOption.SHARED,
    sizeText: '743 bytes',
    typeText: 'ZIP archive',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  // A regular file that can't be renamed, but can be deleted.
  deletableFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'Deletable File.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'Deletable File.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: false,
      canDelete: true,
    },
  }),

  // A regular file that can't be deleted, but can be renamed.
  renamableFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'Renamable File.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'Renamable File.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    capabilities: {
      canCopy: true,
      canAddChildren: false,
      canRename: true,
      canDelete: false,
    },
  }),

  // Default Android directories.
  directoryDocuments: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Documents',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Documents',
    sizeText: '--',
    typeText: 'Folder',
    capabilities: {
      canCopy: false,
      canAddChildren: true,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  directoryMovies: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Movies',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Movies',
    sizeText: '--',
    typeText: 'Folder',
    capabilities: {
      canCopy: false,
      canAddChildren: true,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  directoryMusic: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Music',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Music',
    sizeText: '--',
    typeText: 'Folder',
    capabilities: {
      canCopy: false,
      canAddChildren: true,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  directoryPictures: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Pictures',
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Pictures',
    sizeText: '--',
    typeText: 'Folder',
    capabilities: {
      canCopy: false,
      canAddChildren: true,
      canRename: false,
      canDelete: false,
      canShare: true,
    },
  }),

  // Android test files.
  documentsText: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'Documents/android.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'android.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  moviesVideo: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'world.webm',
    targetPath: 'Movies/android.webm',
    mimeType: 'video/webm',
    lastModifiedTime: 'Jul 4, 2012, 10:35 AM',
    nameText: 'android.webm',
    sizeText: '17 KB',
    typeText: 'WebM video',
  }),

  musicAudio: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'music.ogg',
    targetPath: 'Music/android.ogg',
    mimeType: 'audio/ogg',
    lastModifiedTime: 'Sep 4, 1998, 12:00 AM',
    nameText: 'android.ogg',
    sizeText: '14 KB',
    typeText: 'OGG audio',
  }),

  picturesImage: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'image3.jpg',
    targetPath: 'Pictures/android.jpg',
    mimeType: 'image/jpeg',
    lastModifiedTime: 'Jan 18, 2012, 1:02 AM',
    nameText: 'android.jpg',
    sizeText: '3 KB',
    typeText: 'JPEG image',
  }),

  neverSync: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'never-sync.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'never-sync.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  sharedWithMeDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'Shared Directory',
    sharedOption: SharedOption.SHARED_WITH_ME,
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'Shared Directory',
    sizeText: '--',
    typeText: 'Folder',
  }),

  sharedWithMeDirectoryFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'Shared Directory/file.txt',
    mimeType: 'text/plain',
    sharedOption: SharedOption.INDIRECTLY_SHARED_WITH_ME,
    lastModifiedTime: 'Jan 1, 2000, 1:00 AM',
    nameText: 'file.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  crdownload: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'hello.crdownload',
    mimeType: 'application/octet-stream',
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    nameText: 'hello.crdownload',
    sizeText: '51 bytes',
    typeText: 'CRDOWNLOAD file',
  }),

  pluginVm: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: 'PvmDefault',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: 'Windows Files',
    sizeText: '--',
    typeText: 'Folder',
  }),

  invalidLastModifiedDate: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'invalidLastModifiedDate.txt',
    mimeType: 'text/plain',
    nameText: 'invalidLastModifiedDate.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
  }),

  trashRootDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: '.Trash',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: '.Trash',
    sizeText: '--',
    typeText: 'Folder',
  }),

  trashInfoDirectory: new TestEntryInfo({
    type: EntryType.DIRECTORY,
    targetPath: '.Trash/info',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    nameText: 'info',
    sizeText: '--',
    typeText: 'Folder',
  }),

  oldTrashInfoFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'old_file.trashinfo',
    targetPath: '.Trash/info/hello.txt.trashinfo',
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    mimeType: 'text/plan',
    nameText: 'hello.txt.trashinfo',
    sizeText: '64 bytes',
    typeText: 'TRASHINFO',
  }),

  cantPinFile: new TestEntryInfo({
    type: EntryType.FILE,
    sourceFileName: 'text.txt',
    targetPath: 'text.txt',
    mimeType: 'text/plain',
    lastModifiedTime: 'Mar 20, 2012, 11:40 PM',
    nameText: 'text.txt',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    canPin: false,
  }),
};


/**
 * Creates a test file, which can be inside folders, however parent folders
 * have to be created by the caller using |createTestFolder|.
 * @param path File path to be created,
 */
export function createTestFile(path: string): TestEntryInfo {
  const name = path.split('/').pop();
  return new TestEntryInfo({
    targetPath: path,
    nameText: name,
    type: EntryType.FILE,
    lastModifiedTime: 'Sep 4, 1998, 12:34 PM',
    sizeText: '51 bytes',
    typeText: 'Plain text',
    sourceFileName: 'text.txt',
    mimeType: 'text/plain',
  });
}

/**
 * Creates a folder test entry from a folder |path|.
 * @param path The folder path.
 */
export function createTestFolder(path: string): TestEntryInfo {
  const name = path.split('/').pop();
  return new TestEntryInfo({
    targetPath: path,
    nameText: name,
    type: EntryType.DIRECTORY,
    lastModifiedTime: 'Jan 1, 1980, 11:59 PM',
    sizeText: '--',
    typeText: 'Folder',
  });
}

/**
 * Returns an array of nested folder test entries, where |depth| controls
 * the nesting. For example, a |depth| of 4 will return:
 *
 *   [0]: nested-folder0
 *   [1]: nested-folder0/nested-folder1
 *   [2]: nested-folder0/nested-folder1/nested-folder2
 *   [3]: nested-folder0/nested-folder1/nested-folder2/nested-folder3
 *
 * @param depth The nesting depth.
 */
export function createNestedTestFolders(depth: number): TestEntryInfo[] {
  const nestedFolderTestEntries = [];

  for (let path = 'nested-folder0', i = 0; i < depth; ++i) {
    nestedFolderTestEntries.push(createTestFolder(path));
    path += `/nested-folder${i + 1}`;
  }

  return nestedFolderTestEntries;
}

/**
 * Returns the count for |value| for the histogram |name|.
 * @param name The histogram to be queried.
 * @param value The value within that histogram to query.
 * @return A promise fulfilled with the count.
 */
export async function getHistogramCount(
    name: string, value: number): Promise<number> {
  const result = await sendTestMessage({
    'name': 'getHistogramCount',
    'histogramName': name,
    'value': value,
  });
  return JSON.parse(result);
}

/**
 * Returns the sum for for the histogram |name|.
 * @param name The histogram to be queried.
 * @return A promise fulfilled with the sum.
 */
export async function getHistogramSum(name: string): Promise<number> {
  const result = await sendTestMessage({
    'name': 'getHistogramSum',
    'histogramName': name,
  });
  return parseInt(JSON.parse(result), 10);
}

/**
 * Checks the expected total count for the histogram |name|.
 * @param name The histogram to be queried.
 * @param count The expected sample count.
 */
export async function expectHistogramTotalCount(name: string, count: number) {
  await sendTestMessage({
    'name': 'expectHistogramTotalCount',
    'histogramName': name,
    'count': count,
  });
}

/**
 * Returns the count for the user action |name|.
 * @param name The user action to be queried.
 * @return A promise fulfilled with the count.
 */
export async function getUserActionCount(name: string): Promise<number> {
  const result = await sendTestMessage({
    'name': 'getUserActionCount',
    'userActionName': name,
  });
  return JSON.parse(result);
}

/**
 * Returns a date time string with diff days. This can be used as the
 * lastModifiedTime field of TestEntryInfo object, which is useful to construct
 * a recent file.
 * @param diffDays how many days in diff
 */
export function getDateWithDayDiff(diffDays: number): string {
  const nowDate = new Date();
  nowDate.setDate(nowDate.getDate() - diffDays);
  // Format: "May 2, 2021, 11:25 AM"
  return formatDate(nowDate);
}

/**
 * Formats the date to be able to compare to Files app date.
 */
export function formatDate(date: Date) {
  return sanitizeDate(date.toLocaleString('default', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
    hour12: true,
    hour: 'numeric',
    minute: 'numeric',
  }));
}

/**
 * Sanitizes the formatted date. Replaces unusual space with normal space.
 * @param strDate the date already in the string format.
 */
export function sanitizeDate(strDate: string): string {
  return strDate.replace('\u202f', ' ');
}

/**
 * Opens a foreground window that makes a call to
 * chrome.fileSystem.chooseEntry. This is due to the fact that this API
 * shouldn't be called in the background page (see crbug.com/736930).
 *
 * @return Promise fulfilled when a foreground window opens.
 */
export async function openEntryChoosingWindow(
    params: chrome.fileSystem.ChooseEntryOptions):
    Promise<chrome.windows.Window> {
  const json = JSON.stringify(params);
  const url = 'file_manager/choose_entry.html?' +
      new URLSearchParams({value: json}).toString();
  return new Promise((resolve, reject) => {
    chrome.windows.create({url, height: 600, width: 400}, (win) => {
      if (chrome.runtime.lastError) {
        reject(chrome.runtime.lastError);
      } else {
        resolve(win);
      }
    });
  });
}

/**
 * Companion function to openEntryChoosingWindow function. This function waits
 * until entry selected in a dialog shown by chooseEntry() is set.
 * @return the entry set by the dialog shown via chooseEntry().
 */
export async function pollForChosenEntry(caller: string):
    Promise<null|(Entry | Entry[])> {
  await repeatUntil(() => {
    if (window[CHOOSE_ENTRY_PROPERTY] !== undefined) {
      return;
    }
    return pending(caller, 'Waiting for chooseEntry() result');
  });
  return window[CHOOSE_ENTRY_PROPERTY]!;
}

/** Waits until the MediaApp/Backlight shows up. */
export async function waitForMediaApp() {
  // The MediaApp window should open for the file.
  const caller = getCaller();
  const mediaAppAppId = 'jhdjimmaggjajfjphpljagpgkidjilnj';
  await repeatUntil(async () => {
    const result = await sendTestMessage({
      name: 'hasSwaStarted',
      swaAppId: mediaAppAppId,
    });

    if (result === 'true') {
      return;
    }
    return pending(caller, 'Waiting for MediaApp to open');
  });
}