chromium/ui/file_manager/integration_tests/file_manager/metadata.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 {MetadataStats} from '../prod/file_manager/shared_types.js';
import {addEntries, createTestFile, ENTRIES, EntryType, RootPath, TestEntryInfo} from '../test_util.js';

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

/**
 * Check if |value| equals the |desiredValue| within 1% margin of tolerance.
 * @param value The variable value.
 * @param desiredValue The desired value.
 */
function equal1PercentMargin(value: number, desiredValue: number): boolean {
  // floor and ceil to account to at least +/-1 unit.
  const minValue = Math.floor(desiredValue * 0.99);
  const maxValue = Math.ceil(desiredValue * 1.01);

  const result =
      value === desiredValue || (minValue <= value && value <= maxValue);
  if (!result) {
    console.log(
        'min value: ' + minValue + ' got value: ' + value +
        ' max value: ' + maxValue);
  }

  return result;
}

/**
 * Creates a test file, which can be inside another folder, however parent
 * folders have to be created by the caller.
 * @param path Folder path to be created,
 */
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',
  });
}

/**
 * Creates a Shared Drive.
 * @param name Shared Drive name.
 */
function createTestTeamDrive(name: string): TestEntryInfo {
  return new TestEntryInfo({
    teamDriveName: name,
    type: EntryType.SHARED_DRIVE,
    capabilities: {
      canCopy: true,
      canDelete: true,
      canRename: true,
      canAddChildren: true,
      canShare: true,
    },
  });
}

/**
 * Entries used by Drive and Downloads tests.
 */
const testEntries = [
  createTestFile('file1.txt'),
  createTestFile('file2.txt'),
  createTestFile('file3.txt'),
  createTestFile('file4.txt'),
  createTestFile('file5.txt'),
  createTestFile('file6.txt'),
  createTestFile('file7.txt'),
  createTestFile('file8.txt'),

  createTestFolder('photos1'),
  createTestFolder('photos1/folder1'),
  createTestFolder('photos1/folder2'),
  createTestFile('photos1/file1.txt'),

  createTestFolder('photos2'),
  createTestFolder('photos3'),
];

/**
 * Measures the number of metadata operations generated for:
 *  - Opening Files app in My Drive with 8 files and 3 folders.
 *  - Navigate to My Drive > photos1 > folder2, which is empty.
 */
export async function metadataDrive() {
  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, testEntries, testEntries);

  // Navigate 2 folders deep, because navigating in directory tree might
  // trigger further metadata fetches.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My Drive/photos1/folder1');

  // Fetch the metadata stats.
  const metadataStats = await remoteCall.callRemoteTestUtil<MetadataStats>(
      'getMetadataStats', appId, []);

  // Verify the number of metadata operations generated by the whole
  // navigation above.
  // If the asserts below fail, check if your change has increased the number
  // of metadata operations, because they impact the overall app performance.
  // Full fetch tally:
  //    8 files in My Drive
  // +  3 folders in My Drive.
  // +  1 My Drive root.
  // +  2 folders when expanding photos1
  // = 14
  chrome.test.assertEq(14, metadataStats.fullFetch);
  chrome.test.assertEq(12, metadataStats.fromCache);

  // Cleared 8 files + 3 folders when navigated out of My Drive and
  // clearing file list.
  chrome.test.assertEq(11, metadataStats.clearCacheCount);
  chrome.test.assertEq(0, metadataStats.clearAllCount);
  chrome.test.assertEq(0, metadataStats.invalidateCount);
}

/**
 * Measures the number of metadata operations generated for:
 *  - Opening Files app in Downloads with 8 files and 3 folders.
 *  - Navigate to Downloads > photos1 > folder1 which is empty.
 */
export async function metadataDownloads() {
  // Open Files app on Downloads.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DOWNLOADS, testEntries, testEntries);

  // Navigate 2 folders deep, because navigating in directory tree might
  // triggers further metadata fetches.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My files/Downloads/photos1/folder1');

  // Fetch the metadata stats.
  const metadataStats = await remoteCall.callRemoteTestUtil<MetadataStats>(
      'getMetadataStats', appId, []);

  // Verify the number of metadata operations generated by the whole
  // navigation above.
  // If the asserts below fail, check if your change has increased the number
  // of metadata operations, because they impact the overall app performance.
  // Full fetch tally:
  //    8 files in Downloads
  // +  3 folders in Downloads.
  // +  1 Downloads root.
  // +  1 read again photos1 when naviated to it.
  // +  8 files in Downloads again.
  // = 21
  chrome.test.assertEq(21, metadataStats.fullFetch);

  // 8 files and 3 folders in Downloads when expanding in the directory
  // tree.
  chrome.test.assertEq(0, metadataStats.fromCache);

  // Cleared 8 files + 3 folders when navigated out of Downloads and
  // clearing file list.
  chrome.test.assertEq(11, metadataStats.clearCacheCount);
  chrome.test.assertEq(0, metadataStats.clearAllCount);
  chrome.test.assertEq(0, metadataStats.invalidateCount);
}

/**
 * Measures the number of metadata operations generated for:
 *  - Opening Files app in My Drive with 51 folders.
 *  - Navigate to My Drive > folder1 which has 50 files.
 *
 * Using 50 files and 50 folders because in the Drive backend it has a
 * throttle for max of 20 concurrent operations.
 */
export async function metadataLargeDrive() {
  const entries = [createTestFolder('folder1')];

  const folder1ExpectedRows = [];

  for (let i = 0; i < 50; i++) {
    const testFile = createTestFile('folder1/file-' + i + '.txt');
    entries.push(testFile);
    folder1ExpectedRows.push(testFile.getExpectedRow());

    // Using sibling folders because navigateWithDirectoryTree expands their
    // parent directory tree might issue metadata requests the child folders.
    entries.push(createTestFile('sibling-folder-' + i));
  }

  // Open Files app on Drive.
  const appId =
      await remoteCall.setupAndWaitUntilReady(RootPath.DRIVE, entries, entries);
  console.log(' remoteCall.setupAndWaitUntilReady finished!');

  // Navigate only 1 folder deep,which is slightly different from
  // metadataDrive test.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/My Drive/folder1');

  // Wait for the metadata stats to reach the desired count.
  // File list component, doesn't display all files at once for performance
  // reasons. Since we can't check the modifiedTime for all files in file
  // list, which is a proxy for "all metadata requests have finished", we have
  // to wait until the metadata stats to have the expected count.
  // If the asserts below fail, check if your change has increased the number
  // of metadata operations, because they impact the overall app performance.
  const checkMetadata = (metadataStats: MetadataStats) => {
    // Full fetch tally:
    //    51 files in My Drive.
    // +  50 files in My Drive>folder1.
    // +   1 My Drive root.
    // +   1 read again folder1 when naviated to it.
    // = 103
    if (!equal1PercentMargin(metadataStats.fullFetch, 103)) {
      return false;
    }

    // 50 team drives cached, reading from file list when navigating to
    // /team_drives, then read cached when expanding directory tree.
    if (metadataStats.fromCache >= 70) {
      return false;
    }

    // Cleared 51 folders when navigated out of My Drive and clearing file
    // list.
    if (!equal1PercentMargin(metadataStats.clearCacheCount, 51)) {
      return false;
    }
    if (metadataStats.clearAllCount !== 0) {
      return false;
    }
    if (metadataStats.invalidateCount !== 0) {
      return false;
    }
    return true;
  };
  await remoteCall.waitFor('getMetadataStats', appId, checkMetadata as any);
}

/**
 * Measures the number of metadata operations generated for:
 *  - Opening Files app in My Drive, with 50 folders and 50 files.
 *  - Navigate to Shared Drives, with 50 team drives.
 *  - Expand Shared Drives to display the 50 team drives..
 */
export async function metadataTeamDrives() {
  const entries = [];
  const driveEntries = [];

  // Using 50 files and 50 folders because in the Drive backend it has a
  // throttle for max of 20 concurrent jobs.
  for (let i = 0; i < 50; i++) {
    entries.push(createTestFile('file-' + i + '.txt'));
    // Using sibling folders because navigateWithDirectoryTree expands their
    // parent and directory_tree.js issues some metadata requests for this
    // condition.
    entries.push(createTestFolder('sibling-folder-' + i));
    driveEntries.push(createTestTeamDrive('Team Drive ' + i));
  }

  // Downloads just some entries. Load some in Downloads and Downloads to
  // check that Downloads entries don't issue metadata calls when navigating
  // on Drive/Shared drives.
  const downloadsEntries = entries.slice(0, 7);

  // Open Files app on Drive.
  const appId = await remoteCall.setupAndWaitUntilReady(
      RootPath.DRIVE, downloadsEntries, entries.concat(driveEntries));

  // Navigate to Shared drives root.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.navigateToPath('/Shared drives');

  // Expand Shared Drives, because expanding might need metadata.
  await directoryTree.expandTreeItemByLabel('Shared drives');

  // Get all Shared Drives' children and check that we have 50 team drives.
  await directoryTree.waitForChildItemsCountByLabel('Shared drives', 50);

  // Fetch the metadata stats.
  const metadataStats = await remoteCall.callRemoteTestUtil<MetadataStats>(
      'getMetadataStats', appId, []);

  // Verify the number of metadata operations generated by the whole
  // navigation above.
  // If the asserts below fail, check if your change has increased the number
  // of metadata operations, because they impact the overall app performance.
  //
  // Full fetch tally:
  //    50 files in My Drive.
  // +  50 folders in My Drive.
  // +  50 team drives.
  // +   1 My Drive root.
  // +   1 Shared Drives root.
  // = 152
  chrome.test.assertEq(152, metadataStats.fullFetch);

  // No cache read here because metadata is retrieved from the store instead of
  // the metadata model when expanding directory tree.
  chrome.test.assertEq(0, metadataStats.fromCache);

  // Cleared 50 folders + 50 files when navigated out of My Drive and
  // clearing file list.
  chrome.test.assertEq(100, metadataStats.clearCacheCount);
  chrome.test.assertEq(0, metadataStats.clearAllCount);
  chrome.test.assertEq(0, metadataStats.invalidateCount);
}

/**
 *  Tests that fetching content metadata from a DocumentsProvider completes.
 */
export async function metadataDocumentsProvider() {
  // Add files to the DocumentsProvider volume.
  await addEntries(['documents_provider'], BASIC_LOCAL_ENTRY_SET);

  // Open Files app.
  const appId = await remoteCall.openNewWindow(RootPath.DOWNLOADS);

  // Wait for the DocumentsProvider volume to mount and click to open the
  // DocumentsProvider volume.
  const directoryTree = await DirectoryTreePageObject.create(appId);
  await directoryTree.selectItemByType('documents_provider');

  // Check: the DocumentsProvider files should appear in the file list.
  const files = TestEntryInfo.getExpectedRows(BASIC_LOCAL_ENTRY_SET);
  await remoteCall.waitForFiles(appId, files, {ignoreLastModifiedTime: true});

  // Select file hello.txt in the file list.
  await remoteCall.waitUntilSelected(appId, ENTRIES.hello.nameText);

  // Check that a request for content metadata completes.
  const result = await await remoteCall.callRemoteTestUtil<string[]>(
      'getContentMetadata', appId, [['mediaMimeType']]);

  // Check nothing in the result was returned.
  chrome.test.checkDeepEq([], result);
}