| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * Extension ID of the Files app. |
| * @type {string} |
| * @const |
| */ |
| const FILE_MANAGER_EXTENSIONS_ID = 'hhaomjibdihmijegdhdafkllkbggdgoj'; |
| |
| const remoteCall = new RemoteCallFilesApp(FILE_MANAGER_EXTENSIONS_ID); |
| |
| /** |
| * Extension ID of Gallery. |
| * @type {string} |
| * @const |
| */ |
| const GALLERY_APP_ID = 'nlkncpkkdoccmpiclbokaimcnedabhhm'; |
| |
| const galleryApp = new RemoteCallGallery(GALLERY_APP_ID); |
| |
| /** |
| * Extension ID of Audio Player. |
| * @type {string} |
| * @const |
| */ |
| const AUDIO_PLAYER_APP_ID = 'cjbfomnbifhcdnihkgipgfcihmgjfhbf'; |
| |
| const audioPlayerApp = new RemoteCall(AUDIO_PLAYER_APP_ID); |
| |
| /** |
| * App ID of Video Player. |
| * @type {string} |
| * @const |
| */ |
| const VIDEO_PLAYER_APP_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko'; |
| |
| const videoPlayerApp = new RemoteCall(VIDEO_PLAYER_APP_ID); |
| |
| /** |
| * Basic entry set for the local volume. |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const BASIC_LOCAL_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.world, |
| ENTRIES.desktop, |
| ENTRIES.beautiful, |
| ENTRIES.photos |
| ]; |
| |
| /** |
| * Basic entry set for the drive volume that only includes read-write entries |
| * (no read-only or similar entries). |
| * |
| * TODO(hirono): Add a case for an entry cached by FileCache. For testing |
| * Drive, create more entries with Drive specific attributes. |
| * TODO(sashab): Merge items from COMPLEX_DRIVE_ENTRY_SET into here (so all |
| * tests run with read-only files) once crbug.com/850834 is fixed. |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const BASIC_DRIVE_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.world, |
| ENTRIES.desktop, |
| ENTRIES.beautiful, |
| ENTRIES.photos, |
| ENTRIES.unsupported, |
| ENTRIES.testDocument, |
| ENTRIES.testSharedDocument |
| ]; |
| |
| /** |
| * Basic entry set for the local crostini volume. |
| * @type {!Array<!TestEntryInfo>} |
| * @const |
| */ |
| const BASIC_CROSTINI_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.world, |
| ENTRIES.desktop, |
| ]; |
| |
| /** |
| * More complex entry set for Drive that includes entries with varying |
| * permissions (such as read-only entries). |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const COMPLEX_DRIVE_ENTRY_SET = [ |
| ENTRIES.hello, ENTRIES.photos, ENTRIES.readOnlyFolder, |
| ENTRIES.readOnlyDocument, ENTRIES.readOnlyStrictDocument, ENTRIES.readOnlyFile |
| ]; |
| |
| /** |
| * Nested entry set (directories inside each other). |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const NESTED_ENTRY_SET = [ |
| ENTRIES.directoryA, |
| ENTRIES.directoryB, |
| ENTRIES.directoryC |
| ]; |
| |
| /** |
| * Expected list of preset entries in fake test volumes. This should be in sync |
| * with FakeTestVolume::PrepareTestEntries in the test harness. |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const BASIC_FAKE_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.directoryA |
| ]; |
| |
| /** |
| * Expected files shown in "Recent". Directories (e.g. 'photos') are not in this |
| * list as they are not expected in "Recent". |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const RECENT_ENTRY_SET = [ |
| ENTRIES.desktop, |
| ENTRIES.beautiful, |
| ]; |
| |
| /** |
| * Expected files shown in "Offline", which should have the files |
| * "available offline". Google Documents, Google Spreadsheets, and the files |
| * cached locally are "available offline". |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const OFFLINE_ENTRY_SET = [ |
| ENTRIES.testDocument, |
| ENTRIES.testSharedDocument |
| ]; |
| |
| /** |
| * Expected files shown in "Shared with me", which should be the entries labeled |
| * with "shared-with-me". |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const SHARED_WITH_ME_ENTRY_SET = [ |
| ENTRIES.testSharedDocument |
| ]; |
| |
| /** |
| * Entry set for Drive that includes team drives of various permissions and |
| * nested files with various permissions. |
| * |
| * TODO(sashab): Add support for capabilities of Team Drive roots. |
| * |
| * @type {Array<TestEntryInfo>} |
| * @const |
| */ |
| const TEAM_DRIVE_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.teamDriveA, |
| ENTRIES.teamDriveAFile, |
| ENTRIES.teamDriveADirectory, |
| ENTRIES.teamDriveAHostedFile, |
| ENTRIES.teamDriveB, |
| ENTRIES.teamDriveBFile, |
| ]; |
| |
| /** |
| * Entry set for Drive that includes Computers, including nested computers with |
| * files and nested "USB and External Devices" with nested devices. |
| */ |
| let COMPUTERS_ENTRY_SET = [ |
| ENTRIES.hello, |
| ENTRIES.computerA, |
| ENTRIES.computerAFile, |
| ENTRIES.computerAdirectoryA, |
| ]; |
| |
| /** |
| * Opens a Files app's main window. |
| * |
| * TODO(mtomasz): Pass a volumeId or an enum value instead of full paths. |
| * |
| * @param {?string} initialRoot Root path to be used as a default current |
| * directory during initialization. Can be null, for no default path. |
| * @param {Object} appState App state to be passed with on opening the Files |
| * app. |
| * @return {Promise} Promise to be fulfilled after window creating. |
| */ |
| function openNewWindow(initialRoot, appState = {}) { |
| // TODO(mtomasz): Migrate from full paths to a pair of a volumeId and a |
| // relative path. To compose the URL communicate via messages with |
| // file_manager_browser_test.cc. |
| if (initialRoot) { |
| appState.currentDirectoryURL = 'filesystem:chrome-extension://' + |
| FILE_MANAGER_EXTENSIONS_ID + '/external' + initialRoot; |
| } |
| |
| return remoteCall.callRemoteTestUtil('openMainWindow', null, [appState]); |
| } |
| |
| /** |
| * Opens a file dialog and waits for closing it. |
| * |
| * @param {Object} dialogParams Dialog parameters to be passed to chrome. |
| * fileSystem.chooseEntry() API. |
| * @param {string} volumeName Volume name passed to the selectVolume remote |
| * funciton. |
| * @param {Array<TestEntryInfo>} expectedSet Expected set of the entries. |
| * @param {function(appId:string):Promise} closeDialog Function to close the |
| * dialog. |
| * @param {boolean} useBrowserOpen Whether to launch the select file dialog via |
| * a browser OpenFile() call. |
| * @return {Promise} Promise to be fulfilled with the result entry of the |
| * dialog. |
| */ |
| async function openAndWaitForClosingDialog( |
| dialogParams, volumeName, expectedSet, closeDialog, |
| useBrowserOpen = false) { |
| const caller = getCaller(); |
| let resultPromise; |
| if (useBrowserOpen) { |
| resultPromise = sendTestMessage({name: 'runSelectFileDialog'}); |
| } else { |
| resultPromise = new Promise(fulfill => { |
| chrome.fileSystem.chooseEntry(dialogParams, entry => { |
| fulfill(entry); |
| }); |
| chrome.test.assertTrue(!chrome.runtime.lastError, 'chooseEntry failed.'); |
| }); |
| } |
| |
| const appId = await remoteCall.waitForWindow('dialog#'); |
| await remoteCall.waitForElement(appId, '#file-list'); |
| await remoteCall.waitFor('isFileManagerLoaded', appId, true); |
| chrome.test.assertTrue( |
| await remoteCall.callRemoteTestUtil('selectVolume', appId, [volumeName]), |
| 'selectVolume failed'); |
| await remoteCall.waitForFiles( |
| appId, TestEntryInfo.getExpectedRows(expectedSet)); |
| await closeDialog(appId); |
| await repeatUntil(async () => { |
| const windows = await remoteCall.callRemoteTestUtil('getWindows', null, []); |
| if (windows[appId]) { |
| return pending(caller, 'Waiting for Window %s to hide.', appId); |
| } |
| }); |
| return resultPromise; |
| } |
| |
| /** |
| * Opens a Files app's main window and waits until it is initialized. Fills |
| * the window with initial files. Should be called for the first window only. |
| * |
| * TODO(mtomasz): Pass a volumeId or an enum value instead of full paths. |
| * |
| * @param {?string} initialRoot Root path to be used as a default current |
| * directory during initialization. Can be null, for no default path. |
| * @param {!Array<TestEntryInfo>>} initialLocalEntries List of initial |
| * entries to load in Google Drive (defaults to a basic entry set). |
| * @param {!Array<TestEntryInfo>>} initialDriveEntries List of initial |
| * entries to load in Google Drive (defaults to a basic entry set). |
| * @param {Object} appState App state to be passed with on opening the Files |
| * app. |
| * @return {Promise} Promise to be fulfilled with the window ID. |
| */ |
| async function setupAndWaitUntilReady( |
| initialRoot, initialLocalEntries = BASIC_LOCAL_ENTRY_SET, |
| initialDriveEntries = BASIC_DRIVE_ENTRY_SET, appState = {}) { |
| const localEntriesPromise = addEntries(['local'], initialLocalEntries); |
| const driveEntriesPromise = addEntries(['drive'], initialDriveEntries); |
| |
| const appId = await openNewWindow(initialRoot, appState); |
| await remoteCall.waitForElement(appId, '#detail-table'); |
| |
| // Wait until the elements are loaded in the table. |
| await Promise.all([ |
| remoteCall.waitForFileListChange(appId, 0), |
| localEntriesPromise, |
| driveEntriesPromise, |
| ]); |
| await remoteCall.waitFor('isFileManagerLoaded', appId, true); |
| return appId; |
| } |
| |
| /** |
| * Returns the name of the given file list entry. |
| * @param {Array<string>} file An entry in a file list. |
| * @return {string} Name of the file. |
| */ |
| function getFileName(fileListEntry) { |
| return fileListEntry[0]; |
| } |
| |
| /** |
| * Returns the size of the given file list entry. |
| * @param {Array<string>} An entry in a file list. |
| * @return {string} Size of the file. |
| */ |
| function getFileSize(fileListEntry) { |
| return fileListEntry[1]; |
| } |
| |
| /** |
| * Returns the type of the given file list entry. |
| * @param {Array<string>} An entry in a file list. |
| * @return {string} Type of the file. |
| */ |
| function getFileType(fileListEntry) { |
| return fileListEntry[2]; |
| } |
| |
| /** |
| * A value that when returned by an async test indicates that app errors should |
| * not be checked following completion of the test. |
| */ |
| const IGNORE_APP_ERRORS = Symbol('IGNORE_APP_ERRORS'); |
| |
| /** |
| * For async function tests, wait for the test to complete, check for app errors |
| * unless skipped, and report the results. |
| * @param {Promise} resultPromise A promise that resolves with the test result. |
| * @private |
| */ |
| async function awaitAsyncTestResult(resultPromise) { |
| chrome.test.assertTrue( |
| resultPromise instanceof Promise, 'test did not return a Promise'); |
| |
| // Hold a pending callback to ensure the test doesn't complete early. |
| const passCallback = chrome.test.callbackPass(); |
| |
| try { |
| const result = await resultPromise; |
| if (result !== IGNORE_APP_ERRORS) { |
| await checkIfNoErrorsOccuredOnApp(remoteCall); |
| } |
| } catch (error) { |
| // If the test has failed, ignore the exception and return. |
| if (error == 'chrome.test.failure') { |
| return; |
| } |
| |
| // Otherwise, report the exception as a test failure. chrome.test.fail() |
| // emits an exception; catch it to avoid spurious logging about an uncaught |
| // exception. |
| try { |
| chrome.test.fail(error.stack || error); |
| } catch (_) { |
| return; |
| } |
| } |
| |
| passCallback(); |
| } |
| |
| /** |
| * Namespace for test cases. |
| */ |
| const testcase = {}; |
| |
| /** |
| * When the FileManagerBrowserTest harness loads this test extension, request |
| * configuration and other details from that harness, including the test case |
| * name to run. Use the configuration/details to setup the test ennvironment, |
| * then run the test case using chrome.test.RunTests. |
| */ |
| window.addEventListener('load', () => { |
| const steps = [ |
| // Request the guest mode state. |
| () => { |
| sendBrowserTestCommand({name: 'isInGuestMode'}, steps.shift()); |
| }, |
| // Request the root entry paths. |
| mode => { |
| if (JSON.parse(mode) != chrome.extension.inIncognitoContext) { |
| return; |
| } |
| sendBrowserTestCommand({name: 'getRootPaths'}, steps.shift()); |
| }, |
| // Request the test case name. |
| paths => { |
| const roots = JSON.parse(paths); |
| RootPath.DOWNLOADS = roots.downloads; |
| RootPath.DOWNLOADS_PATH = roots.downloads_path; |
| RootPath.DRIVE = roots.drive; |
| RootPath.ANDROID_FILES = roots.android_files; |
| sendBrowserTestCommand({name: 'getTestName'}, steps.shift()); |
| }, |
| // Run the test case. |
| testCaseName => { |
| // Get the test function from testcase namespace testCaseName. |
| const test = testcase[testCaseName]; |
| // Verify test is an unnamed (aka 'anonymous') Function. |
| if (!(test instanceof Function) || test.name) { |
| chrome.test.fail('[' + testCaseName + '] not found.'); |
| return; |
| } |
| // Define the test case and its name for chrome.test logging. |
| test.generatedName = testCaseName; |
| const testCaseSymbol = Symbol(testCaseName); |
| const testCase = { |
| [testCaseSymbol]: () => { |
| return awaitAsyncTestResult(test()); |
| }, |
| }; |
| // Run the test. |
| chrome.test.runTests([testCase[testCaseSymbol]]); |
| } |
| ]; |
| steps.shift()(); |
| }); |
| |
| /** |
| * Creates a folder shortcut to |directoryName| using the context menu. Note the |
| * current directory must be a parent of the given |directoryName|. |
| * |
| * @param {string} appId Files app windowId. |
| * @param {string} directoryName Directory of shortcut to be created. |
| * @return {Promise} Promise fulfilled on success. |
| */ |
| async function createShortcut(appId, directoryName) { |
| chrome.test.assertTrue(await remoteCall.callRemoteTestUtil( |
| 'selectFile', appId, [directoryName])); |
| |
| await remoteCall.waitForElement(appId, ['.table-row[selected]']); |
| chrome.test.assertTrue(await remoteCall.callRemoteTestUtil( |
| 'fakeMouseRightClick', appId, ['.table-row[selected]'])); |
| |
| |
| await remoteCall.waitForElement(appId, '#file-context-menu:not([hidden])'); |
| await remoteCall.waitForElement( |
| appId, |
| '[command="#create-folder-shortcut"]:not([hidden]):not([disabled])'); |
| chrome.test.assertTrue(await remoteCall.callRemoteTestUtil( |
| 'fakeMouseClick', appId, |
| ['[command="#create-folder-shortcut"]:not([hidden]):not([disabled])'])); |
| |
| await remoteCall.waitForElement( |
| appId, `.tree-item[label="${directoryName}"]`); |
| } |
| |
| /** |
| * Expands a tree item by clicking on its expand icon. |
| * |
| * @param {string} appId Files app windowId. |
| * @param {string} treeItem Query to the tree item that should be expanded. |
| * @return {Promise} Promise fulfilled on success. |
| */ |
| async function expandTreeItem(appId, treeItem) { |
| const expandIcon = treeItem + '> .tree-row[has-children=true] > .expand-icon'; |
| await remoteCall.waitForElement(appId, expandIcon); |
| chrome.test.assertTrue(await remoteCall.callRemoteTestUtil( |
| 'fakeMouseClick', appId, [expandIcon])); |
| |
| const expandedSubtree = treeItem + '> .tree-children[expanded]'; |
| await remoteCall.waitForElement(appId, expandedSubtree); |
| } |