| // 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 {DirectoryTreePageObject} from './file_manager/page_objects/directory_tree.js'; |
| import {BASIC_CROSTINI_ENTRY_SET, BASIC_DRIVE_ENTRY_SET, BASIC_LOCAL_ENTRY_SET} from './file_manager/test_data.js'; |
| import type {ElementObject, FilesAppState, KeyModifiers, VolumeType} from './prod/file_manager/shared_types.js'; |
| import {addEntries, getCaller, openEntryChoosingWindow, pending, pollForChosenEntry, repeatUntil, sendTestMessage, TestEntryInfo} from './test_util.js'; |
| |
| export type MenuObject = ElementObject&{ |
| items?: ElementObject[], |
| }; |
| /** |
| * When step by step tests are enabled, turns on automatic step() calls. Note |
| * that if step() is defined at the time of this call, invoke it to start the |
| * test auto-stepping ball rolling. |
| */ |
| window.autoStep = () => { |
| window.autostep = window.autostep || false; |
| if (!window.autostep) { |
| window.autostep = true; |
| } |
| if (window.autostep && typeof window.step === 'function') { |
| window.step(); |
| } |
| }; |
| |
| /** |
| * This error type is thrown by executeJsInPreviewTagSwa_ if the script to |
| * execute in the untrusted context produces an error. |
| */ |
| export class ExecuteScriptError extends Error { |
| constructor(message: string) { |
| super(message); |
| this.name = 'ExecuteScriptError'; |
| } |
| } |
| |
| /** |
| * Class to manipulate the window in the remote extension. |
| */ |
| export class RemoteCall { |
| /** |
| * Tristate holding the cached result of isStepByStepEnabled_(). |
| */ |
| private cachedStepByStepEnabled_: boolean|null = null; |
| |
| /** |
| * @param origin ID of the app to be manipulated. |
| */ |
| constructor(protected origin_: string) {} |
| |
| /** |
| * Checks whether step by step tests are enabled or not. |
| */ |
| private async isStepByStepEnabled_(): Promise<boolean> { |
| if (this.cachedStepByStepEnabled_ === null) { |
| this.cachedStepByStepEnabled_ = await new Promise( |
| fulfill => chrome.commandLinePrivate.hasSwitch( |
| 'enable-file-manager-step-by-step-tests', fulfill)); |
| } |
| return this.cachedStepByStepEnabled_!; |
| } |
| |
| /** |
| * Sends a test `message` to the test code running in the File Manager. |
| * @return A promise which when fulfilled returns the result of executing test |
| * code with the given message. |
| */ |
| sendMessage(message: Object): Promise<unknown> { |
| return new Promise((fulfill) => { |
| chrome.runtime.sendMessage(this.origin_, message, {}, fulfill); |
| }); |
| } |
| |
| /** |
| * Calls a remote test util in the Files app's extension. See: |
| * registerRemoteTestUtils in test_util_base.js. |
| * |
| * @param func Function name. |
| * @param appId App window Id or null for functions not requiring a window. |
| * @param args Array of arguments. |
| * @param callback Callback handling the function's result. |
| * @return Promise to be fulfilled with the result of the remote utility. |
| */ |
| async callRemoteTestUtil<T>( |
| func: string, appId: null|string, args?: null|readonly unknown[], |
| callback?: (r: T) => void): Promise<T> { |
| const stepByStep = await this.isStepByStepEnabled_(); |
| let finishCurrentStep; |
| if (stepByStep) { |
| while (window.currentStep) { |
| await window.currentStep; |
| } |
| window.currentStep = new Promise(resolve => { |
| finishCurrentStep = () => { |
| console.groupEnd(); |
| window.currentStep = null; |
| resolve(); |
| }; |
| }); |
| console.group('Executing: ' + func + ' on ' + appId + ' with args: '); |
| console.info(args); |
| if (window.autostep !== true) { |
| await new Promise<void>((onFulfilled) => { |
| console.info('Type step() to continue...'); |
| window.step = function() { |
| window.step = null; |
| onFulfilled(); |
| }; |
| }); |
| } else { |
| console.info('Auto calling step() ...'); |
| } |
| } |
| const response = await this.sendMessage({func, appId, args}); |
| |
| if (stepByStep) { |
| console.info('Returned value:'); |
| console.info(JSON.stringify(response)); |
| finishCurrentStep!(); |
| } |
| if (callback) { |
| callback(response as T); |
| } |
| return response as T; |
| } |
| |
| /** |
| * Waits for a SWA window to be open. |
| * @param debug Whether to debug the findSwaWindow. |
| */ |
| async waitForWindow(debug: boolean = false): Promise<string> { |
| const caller = getCaller(); |
| const appId = await repeatUntil(async () => { |
| const msg = { |
| name: 'findSwaWindow', |
| debug: debug ?? undefined, |
| }; |
| const ret = await sendTestMessage(msg); |
| if (ret === 'none') { |
| return pending(caller, 'Wait for SWA window'); |
| } |
| return ret; |
| }); |
| |
| return appId; |
| } |
| |
| /** |
| * Waits for the dialog window and waits for it to fully load. |
| * @return dialog's id. |
| */ |
| async waitForDialog(): Promise<string> { |
| const dialog = await this.waitForWindow(); |
| |
| // Wait for Files app to finish loading. |
| await this.waitFor('isFileManagerLoaded', dialog, true); |
| |
| return dialog; |
| } |
| |
| /** |
| * Waits for the specified element appearing in the DOM. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @return Promise to be fulfilled when the element appears. |
| */ |
| async waitForElement(appId: string, query: string|string[]): |
| Promise<ElementObject> { |
| return this.waitForElementStyles(appId, query, []); |
| } |
| |
| /** |
| * Waits for the specified element appearing in the DOM. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @param styleNames List of CSS property name to be obtained. NOTE: Causes |
| * element style re-calculation. |
| * @return Promise to be fulfilled when the element appears. |
| */ |
| async waitForElementStyles( |
| appId: string, query: string|string[], |
| styleNames: string[]): Promise<ElementObject> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const elements = await this.callRemoteTestUtil<ElementObject[]>( |
| 'deepQueryAllElements', appId, [query, styleNames]); |
| if (elements && elements.length > 0) { |
| return elements[0]; |
| } |
| return pending(caller, 'Element %s is not found.', query); |
| }); |
| } |
| |
| /** |
| * Waits for a remote test function to return a specific result. |
| * |
| * @param funcName Name of remote test function to be executed. |
| * @param appId App window Id. |
| * @param expectedResult An value to be checked against the return value of |
| * `funcName` or a callback that receives the return value of `funcName` |
| * and returns true if the result is the expected value. |
| * @param args Arguments to be provided to `funcName` when executing it. |
| * @return Promise to be fulfilled when the `expectedResult` is returned from |
| * `funcName` execution. |
| */ |
| waitFor( |
| funcName: string, appId: null|string, |
| expectedResult: boolean|((r: unknown) => boolean), |
| args?: unknown[]|null): Promise<void> { |
| const caller = getCaller(); |
| args = args || []; |
| return repeatUntil(async () => { |
| const result = await this.callRemoteTestUtil(funcName, appId, args); |
| if (typeof expectedResult === 'function' && expectedResult(result)) { |
| return result; |
| } |
| if (expectedResult === result) { |
| return result; |
| } |
| const msg = 'waitFor: Waiting for ' + |
| `${funcName} to return ${expectedResult}, ` + |
| `but got ${JSON.stringify(result)}.`; |
| return pending(caller, msg); |
| }); |
| } |
| |
| /** |
| * Waits for the specified element leaving from the DOM. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @return Promise to be fulfilled when the element is lost. |
| */ |
| waitForElementLost(appId: string, query: string|string[]): Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const elements = await this.queryElements(appId, query); |
| if (elements.length > 0) { |
| return pending(caller, 'Elements %j still exists.', elements); |
| } |
| return true; |
| }); |
| } |
| |
| async queryElements( |
| appId: string, query: string|string[], |
| styleNames?: string[]): Promise<ElementObject[]> { |
| const args: Array<string|string[]> = [query]; |
| if (styleNames) { |
| args.push(styleNames); |
| } |
| return this.callRemoteTestUtil<ElementObject[]>( |
| 'deepQueryAllElements', appId, args); |
| } |
| |
| /** |
| * Waits for the `query` to match `count` elements. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @param count The expected element match count. |
| * @return Promise to be fulfilled on success. |
| */ |
| async waitForElementsCount( |
| appId: string, query: string|string[], count: number): Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const expect = `Waiting for [${query}] to match ${count} elements`; |
| const result = |
| await this.callRemoteTestUtil('countElements', appId, [query, count]); |
| return !result ? pending(caller, expect) : true; |
| }); |
| } |
| |
| /** |
| * Sends a fake key down event. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, |query[0]| specifies the first |
| * element(s), |query[1]| specifies elements inside the shadow DOM of |
| * the first element, and so on. |
| * @param key DOM UI Events Key value. |
| * @param ctrlKey Control key flag. |
| * @param shiftKey Shift key flag. |
| * @param altKey Alt key flag. |
| * @return Promise to be fulfilled or rejected depending on |
| * the result. |
| */ |
| async fakeKeyDown( |
| appId: string, query: string|string[], key: string, ctrlKey: boolean, |
| shiftKey: boolean, altKey: boolean): Promise<boolean> { |
| const result = await this.callRemoteTestUtil( |
| 'fakeKeyDown', appId, [query, key, ctrlKey, shiftKey, altKey]); |
| if (result) { |
| return true; |
| } |
| throw new Error('Fail to fake key down.'); |
| } |
| |
| /** |
| * Sets the given input text on the element identified by the query. |
| * @param appId App window ID. |
| * @param selector The query selector to locate the element |
| * @param text The text to be set on the element. |
| */ |
| async inputText(appId: string, selector: string|string[], text: string) { |
| chrome.test.assertTrue( |
| await this.callRemoteTestUtil('inputText', appId, [selector, text])); |
| } |
| |
| /** |
| * Gets file entries just under the volume. |
| * @param volumeType Volume type. |
| * @param names File name list. |
| * @return Promise to be fulfilled with file urls for entries or rejected |
| * depending on the result. |
| */ |
| getFilesUnderVolume(volumeType: VolumeType, names: string[]): |
| Promise<string[]> { |
| return this.callRemoteTestUtil<string[]>( |
| 'getFilesUnderVolume', null, [volumeType, names]); |
| } |
| |
| /** |
| * Waits for a single file. |
| * @param volumeType Volume type. |
| * @param name File name. |
| * @return Promise to be fulfilled when the file had found. |
| */ |
| waitForFile(volumeType: VolumeType, name: string): Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| if ((await this.getFilesUnderVolume(volumeType, [name])).length === 1) { |
| return true; |
| } |
| return pending(caller, `"${name}" is not found.`); |
| }); |
| } |
| |
| /** |
| * Shorthand for clicking an element. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @return Promise to be fulfilled with the clicked element. |
| */ |
| async waitAndClickElement( |
| appId: string, query: string|string[], |
| keyModifiers?: KeyModifiers): Promise<ElementObject> { |
| const element = await this.waitForElement(appId, query); |
| const result = await this.callRemoteTestUtil<boolean>( |
| 'fakeMouseClick', appId, [query, keyModifiers]); |
| chrome.test.assertTrue(result, 'mouse click failed.'); |
| return element; |
| } |
| |
| /** |
| * Shorthand for right-clicking an element. |
| * @param appId App window Id. |
| * @param query Query to specify the element. |
| * If query is an array, `query[0]` specifies the first element(s), |
| * `query[1]` specifies elements inside the shadow DOM of the first element, |
| * and so on. |
| * @return Promise to be fulfilled with the clicked element. |
| */ |
| async waitAndRightClick( |
| appId: string, query: string|string[], |
| keyModifiers?: KeyModifiers): Promise<ElementObject> { |
| const element = await this.waitForElement(appId, query); |
| const result = await this.callRemoteTestUtil<boolean>( |
| 'fakeMouseRightClick', appId, [query, keyModifiers]); |
| chrome.test.assertTrue(result, 'mouse right-click failed.'); |
| return element; |
| } |
| |
| /** |
| * Shorthand for focusing an element. |
| * @param appId App window Id. |
| * @param query Query to specify the element to be focused. |
| * @return Promise to be fulfilled with the focused element. |
| */ |
| async focus(appId: string, query: string[]): Promise<null|ElementObject> { |
| const element = await this.waitForElement(appId, query); |
| const result = |
| await this.callRemoteTestUtil<boolean>('focus', appId, query); |
| chrome.test.assertTrue(result, 'focus failed.'); |
| return element; |
| } |
| |
| /** |
| * Simulates Click in the UI in the middle of the element. |
| * @param appId App window ID contains the element. NOTE: The click is |
| * simulated on most recent window in the window system. |
| * @param query Query to the element to be clicked. |
| * @param leftClick If true, simulate left click. Otherwise simulate right |
| * click. |
| * @return A promise fulfilled after the click event. |
| */ |
| async simulateUiClick( |
| appId: string, query: string|string[], |
| leftClick: boolean = true): Promise<unknown> { |
| const element = await this.waitForElementStyles(appId, query, ['display']); |
| chrome.test.assertTrue(!!element, 'element for simulateUiClick not found'); |
| |
| // Find the middle of the element. |
| const left = element.renderedLeft ?? 0; |
| const top = element.renderedTop ?? 0; |
| const width = element.renderedWidth ?? 0; |
| const height = element.renderedHeight ?? 0; |
| const x = Math.floor(left + (width / 2)); |
| const y = Math.floor(top + (height / 2)); |
| |
| return sendTestMessage( |
| {appId, name: 'simulateClick', 'clickX': x, 'clickY': y, leftClick}); |
| } |
| |
| /** |
| * Simulates Right Click in blank/empty space of the file list element. |
| * @param appId App window ID contains the element. NOTE: The click is |
| * simulated on most recent window in the window system. |
| * @return A promise fulfilled after the click event. |
| */ |
| async rightClickFileListBlankSpace(appId: string): Promise<void> { |
| await this.simulateUiClick( |
| appId, '#file-list .spacer.signals-overscroll', false); |
| } |
| |
| /** |
| * Selects the option given by the index in the menu given by the type. This |
| * only works in V2 version of the search. |
| * @param appId The ID that identifies the files app. |
| * @param type The search option type (location, recency, type). |
| * @param index The index of the button. |
| * @return A promise that resolves to true if click was successful and false |
| * otherwise. |
| */ |
| selectSearchOption(appId: string, type: string, index: number): |
| Promise<boolean> { |
| return this.callRemoteTestUtil('fakeMouseClick', appId, [ |
| [ |
| 'xf-search-options', |
| `xf-select#${type}-selector`, |
| `cr-action-menu cr-button:nth-of-type(${index})`, |
| ], |
| ]); |
| } |
| } |
| |
| /** |
| * Class to manipulate the window in the remote extension. |
| */ |
| export class RemoteCallFilesApp extends RemoteCall { |
| /** |
| * Sends a test `message` to the test code running in the File Manager. |
| */ |
| override sendMessage(message: {appId: string}): Promise<unknown> { |
| const command = { |
| name: 'callSwaTestMessageListener', |
| appId: message.appId, |
| data: JSON.stringify(message), |
| }; |
| |
| return new Promise((fulfill) => { |
| chrome.test.sendMessage(JSON.stringify(command), (response) => { |
| if (response === '"@undefined@"') { |
| fulfill(undefined); |
| } else { |
| try { |
| fulfill(response === '' ? true : JSON.parse(response)); |
| } catch (e) { |
| console.error(`Failed to parse "${response}" due to ${e}`); |
| fulfill(false); |
| } |
| } |
| }); |
| }); |
| } |
| |
| async getWindows() { |
| return JSON.parse(await sendTestMessage({name: 'getWindows'}) as string); |
| } |
| |
| /** |
| * Executes a script in the context of a <preview-tag> element contained in |
| * the window. |
| * For SWA: It's the first chrome-untrusted://file-manager <iframe>. |
| * For legacy: It's the first elements based on the `query`. |
| * Responds with its output. |
| * |
| * @param appId App window Id. |
| * @param query Query to the <preview-tag> element (this is ignored for SWA). |
| * @param statement Javascript statement to be executed within the |
| * <preview-tag>. |
| * @return resolved with the return value of the `statement`. |
| */ |
| async executeJsInPreviewTag<T>( |
| _appId: string, _query: string[], |
| statement: string): Promise<T|undefined> { |
| return this.executeJsInPreviewTagSwa_(statement); |
| } |
| |
| /** |
| * Injects javascript statemenent in the first chrome-untrusted://file-manager |
| * page found and respond with its output. |
| */ |
| private async executeJsInPreviewTagSwa_<T>(statement: string): |
| Promise<T|undefined> { |
| const script = `try { |
| let result = ${statement}; |
| result = result === undefined ? '@undefined@' : [result]; |
| window.domAutomationController.send(JSON.stringify(result)); |
| } catch (error) { |
| const errorInfo = {'@error@': error.message, '@stack@': error.stack}; |
| window.domAutomationController.send(JSON.stringify(errorInfo)); |
| }`; |
| |
| const command = { |
| name: 'executeScriptInChromeUntrusted', |
| data: script, |
| }; |
| |
| const response = await sendTestMessage(command) as string; |
| if (response === '"@undefined@"') { |
| return undefined; |
| } |
| const output = JSON.parse(response); |
| if ('@error@' in output) { |
| console.error(output['@error@']); |
| console.error('Original StackTrace:\n' + output['@stack@']); |
| throw new ExecuteScriptError( |
| 'Error executing JS in Preview: ' + output['@error@']); |
| } else { |
| return output; |
| } |
| } |
| |
| /** |
| * Waits until the expected URL shows in the last opened browser tab. |
| * @return Promise to be fulfilled when the expected URL is shown in a browser |
| * window. |
| */ |
| async waitForLastOpenedBrowserTabUrl(expectedUrl: string): Promise<string> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const command = {name: 'getLastActiveTabURL'}; |
| const activeBrowserTabURL = await sendTestMessage(command); |
| if (activeBrowserTabURL !== expectedUrl) { |
| return pending( |
| caller, 'waitForActiveBrowserTabUrl: expected %j actual %j.', |
| expectedUrl, activeBrowserTabURL); |
| } |
| return undefined; |
| }); |
| } |
| |
| /** |
| * Returns whether a window exists with the expected origin. |
| * @return Promise resolved with true or false depending on whether such |
| * window exists. |
| */ |
| async windowOriginExists(expectedOrigin: string): Promise<boolean> { |
| const command = {name: 'expectWindowOrigin', expectedOrigin}; |
| const windowExists = await sendTestMessage(command); |
| return windowExists === 'true'; |
| } |
| |
| /** |
| * Waits for the file list turns to the given contents. |
| * @param appId App window Id. |
| * @param expected Expected contents of file list. |
| * @param options Options of the comparison. If orderCheck is true, it also |
| * compares the order of files. If ignoreLastModifiedTime is true, it |
| * compares the file without its last modified time. |
| * @return Promise to be fulfilled when the file list turns to the given |
| * contents. |
| */ |
| waitForFiles(appId: string, expected: string[][], options?: { |
| orderCheck?: boolean, |
| ignoreFileSize?: boolean, |
| ignoreLastModifiedTime?: boolean, |
| }): Promise<void> { |
| options = options || {}; |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const files = |
| await this.callRemoteTestUtil<string[][]>('getFileList', appId, []); |
| if (!options!.orderCheck) { |
| files.sort(); |
| expected.sort(); |
| } |
| for (let i = 0; i < Math.min(files.length, expected.length); i++) { |
| // Change the value received from the UI to match when comparing. |
| if (options!.ignoreFileSize) { |
| files[i]![1] = expected[i]![1]!; |
| } |
| if (options!.ignoreLastModifiedTime) { |
| if (expected[i]!.length < 4) { |
| // Expected sometimes doesn't include the modified time at all, so |
| // just remove from the data from UI. |
| files[i]!.splice(3, 1); |
| } else { |
| files[i]![3]! = expected[i]![3]!; |
| } |
| } |
| } |
| if (!chrome.test.checkDeepEq(expected, files)) { |
| return pending( |
| caller, 'waitForFiles: expected: %j actual %j.', expected, files); |
| } |
| return undefined; |
| }); |
| } |
| |
| /** |
| * Waits until the number of files in the file list is changed from the |
| * given number. |
| * @param appId App window Id. |
| * @param lengthBefore Number of items visible before. |
| * @return Promise to be fulfilled with the contents of files. |
| */ |
| waitForFileListChange(appId: string, lengthBefore: number): Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const files = |
| await this.callRemoteTestUtil<string[][]>('getFileList', appId, []); |
| files.sort(); |
| |
| const notReadyRows = |
| files.filter((row) => row.filter(cell => cell === '...').length); |
| |
| if (notReadyRows.length === 0 && files.length !== lengthBefore && |
| files.length !== 0) { |
| return files; |
| } else { |
| return pending( |
| caller, 'The number of file is %d. Not changed.', lengthBefore); |
| } |
| }); |
| } |
| |
| /** |
| * Waits until the given taskId appears in the executed task list. |
| * @param appId App window Id. |
| * @param descriptor Task to watch. |
| * @param fileNames Name of files that should have been passed to the |
| * executeTasks(). |
| * @param replyArgs arguments to reply to executed task. |
| * @return Promise to be fulfilled when the task appears in the executed task |
| * list. |
| */ |
| waitUntilTaskExecutes( |
| appId: string, descriptor: chrome.fileManagerPrivate.FileTaskDescriptor, |
| fileNames: string[], replyArgs?: Object[]): Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| if (!await this.callRemoteTestUtil( |
| 'taskWasExecuted', appId, [descriptor, fileNames])) { |
| const tasks = await this.callRemoteTestUtil<Array<{ |
| descriptor: chrome.fileManagerPrivate.FileTaskDescriptor, |
| fileNames: string[], |
| }>>('getExecutedTasks', appId, []); |
| const executedTasks = tasks.map((task) => { |
| const {appId, taskType, actionId} = task.descriptor; |
| const executedFileNames = task.fileNames; |
| return `${appId}|${taskType}|${actionId} for ${ |
| JSON.stringify(executedFileNames)}`; |
| }); |
| return pending(caller, 'Executed task is %j', executedTasks); |
| } |
| if (replyArgs) { |
| await this.callRemoteTestUtil( |
| 'replyExecutedTask', appId, [descriptor, replyArgs]); |
| } |
| return undefined; |
| }); |
| } |
| |
| /** |
| * Checks if the next tabforcus'd element has the given ID or not. |
| * @param appId App window Id. |
| * @param elementId String of `id` attribute which the next tabfocus'd element |
| * should have. |
| */ |
| async checkNextTabFocus(appId: string, elementId: string): Promise<boolean> { |
| const result = await sendTestMessage({name: 'dispatchTabKey'}); |
| chrome.test.assertEq( |
| result, 'tabKeyDispatched', 'Tab key dispatch failure'); |
| |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const element = await this.callRemoteTestUtil<ElementObject|null>( |
| 'getActiveElement', appId, []); |
| // TODO(b/285977941): Remove this special handling. |
| // For new directory tree implementation, directory tree itself |
| // ("#directory-tree") is not focusable, the underlying tree item will be |
| // focused, for directory tree related focus check, the `elementId` format |
| // will be "directory-tree#<tree item label>", so we need to check the |
| // label here for the new tree. |
| if (elementId.startsWith('directory-tree#')) { |
| const treeItemLabel = elementId.split('#')[1]; |
| // For new tree. |
| if (element && element.attributes['label'] === treeItemLabel) { |
| return true; |
| } |
| // For old tree. |
| if (element && element.attributes['id'] === 'directory-tree') { |
| return true; |
| } |
| } |
| if (element && element.attributes['id'] === elementId) { |
| return true; |
| } |
| // Try to check the shadow root. |
| const activeElements = await this.callRemoteTestUtil<ElementObject[]>( |
| 'deepGetActivePath', appId, []); |
| const matches = |
| activeElements.filter(el => el.attributes['id'] === elementId); |
| if (matches.length === 1) { |
| return true; |
| } |
| if (matches.length > 1) { |
| console.error(`Found ${ |
| matches.length} active elements with the same id: ${elementId}`); |
| } |
| |
| return pending( |
| caller, |
| `Waiting for active element with id: "${ |
| elementId}", but current is: "${element!.attributes['id']}"`); |
| }); |
| } |
| |
| /** |
| * Returns a promise that repeatedly checks for a file with the given name to |
| * be selected in the app window with the given ID. Typical use |
| * |
| * await remoteCall.waitUntilSelected('file#0', 'hello.txt'); |
| * ... // either the test timed out or hello.txt is currently selected. |
| * |
| * @param appId App window Id. |
| * @param fileName the name of the file to be selected. |
| * @return Promise that indicates if selection was successful. |
| */ |
| async waitUntilSelected(appId: string, fileName: string): Promise<boolean> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const selected = |
| await this.callRemoteTestUtil('selectFile', appId, [fileName]); |
| if (!selected) { |
| return pending(caller, `File ${fileName} not yet selected`); |
| } |
| return undefined; |
| }); |
| } |
| |
| /** |
| * Waits until the current directory is changed. |
| * @param appId App window Id. |
| * @param expectedPath Path to be changed to. |
| * @return Promise to be fulfilled when the current directory is changed to |
| * expectedPath. |
| */ |
| async waitUntilCurrentDirectoryIsChanged(appId: string, expectedPath: string): |
| Promise<void> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const path = |
| await this.callRemoteTestUtil('getBreadcrumbPath', appId, []); |
| if (path !== expectedPath) { |
| return pending( |
| caller, 'Expected path is %s got %s', expectedPath, path); |
| } |
| return undefined; |
| }); |
| } |
| |
| /** |
| * Waits until the expected number of volumes is mounted. |
| * @param expectedVolumesCount Expected number of mounted volumes. |
| * @return promise Promise to be fulfilled. |
| */ |
| async waitForVolumesCount(expectedVolumesCount: number): Promise<unknown> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const volumesCount = await sendTestMessage({name: 'getVolumesCount'}); |
| if (volumesCount === expectedVolumesCount.toString()) { |
| return; |
| } |
| const msg = |
| 'Expected number of mounted volumes: ' + expectedVolumesCount + |
| '. Actual: ' + volumesCount; |
| return pending(caller, msg); |
| }); |
| } |
| |
| /** |
| * Isolates the specified banner to test. The banner is still checked against |
| * it's filters, but is now the top priority banner. |
| * @param appId App window Id |
| * @param bannerTagName Banner tag name in lowercase to isolate. |
| */ |
| async isolateBannerForTesting(appId: string, bannerTagName: string) { |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| chrome.test.assertTrue(await this.callRemoteTestUtil( |
| 'isolateBannerForTesting', appId, [bannerTagName])); |
| } |
| |
| /** |
| * Disables banners from attaching to the DOM. |
| * @param appId App window Id |
| */ |
| async disableBannersForTesting(appId: string) { |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| chrome.test.assertTrue( |
| await this.callRemoteTestUtil('disableBannersForTesting', appId, [])); |
| } |
| |
| /** |
| * Sends text to the search box in the Files app. |
| * @param appId App window Id |
| * @param text The text to type in the search box. |
| */ |
| async typeSearchText(appId: string, text: string) { |
| const searchBoxInput = ['#search-box cr-input']; |
| |
| // Focus the search box. |
| await this.waitAndClickElement(appId, '#search-button'); |
| |
| // Wait for search to fully open. |
| await this.waitForElementLost(appId, '#search-wrapper[collapsed]'); |
| |
| // Input the text. |
| await this.inputText(appId, searchBoxInput, text); |
| |
| // Notify the element of the input. |
| chrome.test.assertTrue(await this.callRemoteTestUtil( |
| 'fakeEvent', appId, ['#search-box cr-input', 'input'])); |
| } |
| |
| /** |
| * Waits for the search box auto complete list to appear. |
| * @return Array of the names in the auto complete list. |
| */ |
| async waitForSearchAutoComplete(appId: string): Promise<string[]> { |
| // Wait for the list to appear. |
| await this.waitForElement(appId, '#autocomplete-list li'); |
| |
| // Return the result. |
| const elements = await this.queryElements(appId, ['#autocomplete-list li']); |
| return elements.map(element => element.text ?? ''); |
| } |
| |
| /** |
| * Disables nudges from expiring for testing. |
| * @param appId App window Id |
| */ |
| async disableNudgeExpiry(appId: string) { |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| chrome.test.assertTrue( |
| await this.callRemoteTestUtil('disableNudgeExpiry', appId, [])); |
| } |
| |
| /** |
| * Selects the file and displays the context menu for the file. |
| * @return resolved when the context menu is visible. |
| */ |
| async showContextMenuFor(appId: string, fileName: string): Promise<void> { |
| // Select the file. |
| await this.waitUntilSelected(appId, fileName); |
| |
| // Right-click to display the context menu. |
| await this.waitAndRightClick(appId, '.table-row[selected]'); |
| |
| // Wait for the context menu to appear. |
| await this.waitForElement(appId, '#file-context-menu:not([hidden])'); |
| |
| // Wait for the tasks to be fully fetched. |
| await this.waitForElement(appId, '#tasks[get-tasks-completed]'); |
| } |
| |
| /** |
| * @param appId App window Id. |
| */ |
| async dismissMenu(appId: string): Promise<void> { |
| await this.fakeKeyDown(appId, 'body', 'Escape', false, false, false); |
| } |
| |
| /** |
| * Returns the menu as `ElementObject` and its menu-items (including |
| * separators) in the `items` property. |
| * @param appId App window Id. |
| * @param menu The name of the menu. |
| * @return Promise to be fulfilled with the menu. |
| */ |
| async getMenu(appId: string, menu: string|string[]): |
| Promise<undefined|MenuObject> { |
| let menuId = ''; |
| // TODO: Implement for other menus. |
| if (menu === 'context-menu') { |
| menuId = '#file-context-menu'; |
| } else if (menu === 'tasks') { |
| menuId = '#tasks-menu'; |
| } |
| |
| if (!menuId) { |
| console.error(`Invalid menu '${menu}'`); |
| return; |
| } |
| |
| // Get the top level menu element. |
| const menuElement: MenuObject = await this.waitForElement(appId, menuId); |
| // Query all the menu items. |
| menuElement.items = await this.queryElements(appId, `${menuId} > *`); |
| return menuElement; |
| } |
| |
| /** |
| * Displays the "tasks" menu from the "OPEN" button dropdown. |
| * The caller code has to prepare the selection to have multiple tasks. |
| * @param appId App window Id. |
| */ |
| async expandOpenDropdown(appId: string) { |
| // Wait the OPEN button to have multiple tasks. |
| await this.waitAndClickElement(appId, '#tasks[multiple]'); |
| } |
| |
| /** |
| * Checks if an item is pinned on drive or not. |
| * @param appId app window ID. |
| * @param path Path from the drive mount point, e.g. /root/test.txt |
| * @param status Pinned status to expect drive item to be. |
| */ |
| async expectDriveItemPinnedStatus( |
| appId: string, path: string, status: boolean) { |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| chrome.test.assertEq( |
| await sendTestMessage({ |
| appId, |
| name: 'isItemPinned', |
| path, |
| }), |
| String(status)); |
| } |
| |
| /** |
| * Sends a delete event via the `OnFilesChanged` drivefs delegate method. |
| * @param appId app window ID. |
| * @param path Path from the drive mount point, e.g. /root/test.txt |
| */ |
| async sendDriveCloudDeleteEvent(appId: string, path: string) { |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| await sendTestMessage({ |
| appId, |
| name: 'sendDriveCloudDeleteEvent', |
| path, |
| }); |
| } |
| |
| /** |
| * Whether the Jellybean UI is enabled. |
| * @param appId app window ID |
| */ |
| async isCrosComponents(appId: string): Promise<boolean> { |
| return await sendTestMessage({ |
| appId, |
| name: 'isCrosComponents', |
| }) === 'true'; |
| } |
| |
| /** |
| * Waits for the nudge with the given text to be visible. |
| * @param appId app window ID. |
| * @param expectedText Text that should be displayed in the Nudge. |
| */ |
| async waitNudge(appId: string, expectedText: string): Promise<boolean> { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const nudgeDot = await this.waitForElementStyles( |
| appId, ['xf-nudge', '#dot'], ['left']); |
| if ((nudgeDot.renderedLeft ?? 0) < 0) { |
| return pending(caller, 'Wait nudge to appear'); |
| } |
| |
| const actualText = |
| await this.waitForElement(appId, ['xf-nudge', '#text']); |
| console.log(actualText); |
| chrome.test.assertEq(actualText.text, expectedText); |
| |
| return true; |
| }); |
| } |
| |
| /** |
| * Waits for the <xf-cloud-panel> element to be visible on the DOM. |
| * @param appId app window ID |
| */ |
| async waitForCloudPanelVisible(appId: string) { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const styles = await this.waitForElementStyles( |
| appId, ['xf-cloud-panel', 'cr-action-menu', 'dialog'], ['left']); |
| |
| if ((styles.renderedHeight ?? 0) > 0 && (styles.renderedWidth ?? 0) > 0 && |
| (styles.renderedTop ?? 0) > 0 && (styles.renderedLeft ?? 0) > 0) { |
| return true; |
| } |
| |
| return pending(caller, `Waiting for xf-cloud-panel to appear.`); |
| }); |
| } |
| |
| /** |
| * Waits for the underlying bulk pinning manager to enter the specified stage. |
| * @param want The stage the bulk pinning is expected to be in. This is a |
| * string relating to the stage defined in the `PinningManager`. |
| */ |
| async waitForBulkPinningStage(want: string) { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const currentStage = await sendTestMessage({name: 'getBulkPinningStage'}); |
| if (currentStage === want) { |
| return true; |
| } |
| return pending(caller, `Still waiting for syncing stage: ${want}`); |
| }); |
| } |
| |
| /** |
| * Waits until the pin manager has the expected required space. |
| */ |
| async waitForBulkPinningRequiredSpace(want: number) { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const actualRequiredSpace = |
| await sendTestMessage({name: 'getBulkPinningRequiredSpace'}); |
| const parsedSpace = parseInt(actualRequiredSpace, 10); |
| if (parsedSpace === want) { |
| return true; |
| } |
| return pending(caller, `Still waiting for required space to be ${want}`); |
| }); |
| } |
| |
| /** |
| * Waits until the cloud panel has the specified item and percentage |
| * attributes defined, if the `timeoutSeconds` is supplied it will only wait |
| * for the specified time before timing out. |
| * @param appId app window ID |
| * @param items The items expected on the cloud panel. |
| * @param percentage The percentage integer expected on the cloud panel. |
| * @param timeoutSeconds Whether to timeout when verifying the panel |
| * attributes. |
| */ |
| async waitForCloudPanelState( |
| appId: string, items: number, percentage: number, |
| timeoutSeconds: number = 10) { |
| const futureDate = new Date(); |
| futureDate.setSeconds(futureDate.getSeconds() + timeoutSeconds); |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| chrome.test.assertTrue( |
| new Date() < futureDate, |
| `Timed out waiting for items=${items} and percentage=${ |
| percentage} to appear on xf-cloud-panel`); |
| const cloudPanel = await this.queryElements( |
| appId, |
| [`xf-cloud-panel[percentage="${percentage}"][items="${items}"]`]); |
| if (cloudPanel && cloudPanel.length === 1) { |
| return true; |
| } |
| return pending( |
| caller, |
| `Still waiting for xf-cloud-panel to have items=${ |
| items} and percentage=${percentage}`); |
| }); |
| } |
| |
| /** |
| * Waits for the feedback panel to show an item with the provided messages. |
| * @param appId app window ID |
| * @param expectedPrimaryMessageRegex The expected primary-text of the item. |
| * @param expectedSecondaryMessageRegex The expected secondary-text of the |
| * item. |
| */ |
| async waitForFeedbackPanelItem( |
| appId: string, expectedPrimaryMessageRegex: RegExp, |
| expectedSecondaryMessageRegex: RegExp) { |
| const caller = getCaller(); |
| return repeatUntil(async () => { |
| const element = await this.waitForElement( |
| appId, ['#progress-panel', 'xf-panel-item']); |
| |
| const actualPrimaryText = element.attributes['primary-text'] ?? ''; |
| const actualSecondaryText = element.attributes['secondary-text'] ?? ''; |
| |
| if (expectedPrimaryMessageRegex.test(actualPrimaryText) && |
| expectedSecondaryMessageRegex.test(actualSecondaryText)) { |
| return; |
| } |
| return pending( |
| caller, |
| `Expected feedback panel item with primary-text regex:"${ |
| expectedPrimaryMessageRegex}" and secondary-text regex:"${ |
| expectedSecondaryMessageRegex}", got item with primary-text "${ |
| actualPrimaryText}" and secondary-text "${actualSecondaryText}"`); |
| }); |
| } |
| |
| /** |
| * Clicks the enabled and visible move to trash button and ensures the delete |
| * button is hidden. |
| */ |
| async clickTrashButton(appId: string) { |
| await this.waitForElement(appId, '#delete-button[hidden]'); |
| await this.waitAndClickElement( |
| appId, '#move-to-trash-button:not([hidden]):not([disabled])'); |
| } |
| |
| /** Fakes the response from spaced when it retrieves the free space. */ |
| async setSpacedFreeSpace(freeSpace: bigint) { |
| console.log(freeSpace); |
| await sendTestMessage( |
| {name: 'setSpacedFreeSpace', freeSpace: String(freeSpace)}); |
| } |
| |
| /** |
| * Waits for the specified element appearing in the DOM. `query_jelly` or |
| * `query_old` are used depending on the state of the migration to |
| * cros_components. |
| * @param appId App window Id. |
| * @param queryJelly Used when cros_components are used. See `waitForElement` |
| * for details. |
| * @param queryOld Used when cros_components are not used. See |
| * `waitForElement` for details. |
| */ |
| async waitForElementJelly( |
| appId: string, queryJelly: string|string[], |
| queryOld: string|string[]): Promise<ElementObject> { |
| const isJellybean = await this.isCrosComponents(appId); |
| return this.waitForElement(appId, isJellybean ? queryJelly : queryOld); |
| } |
| |
| /** |
| * Shorthand for clicking the appropriate element, depending the state of |
| * the Jellybean experiment. |
| * @param appId App window Id. |
| * @param queryJelly The query when using cros_components. See |
| * `waitAndClickElement` for details. |
| * @param queryOld The query when not using cros_components. See |
| * `waitAndClickElement` for details. |
| */ |
| async waitAndClickElementJelly( |
| appId: string, queryJelly: string|string[], queryOld: string|string[], |
| keyModifiers?: KeyModifiers): Promise<ElementObject> { |
| const isJellybean = await this.isCrosComponents(appId); |
| return await this.waitAndClickElement( |
| appId, isJellybean ? queryJelly : queryOld, keyModifiers); |
| } |
| |
| /** Sets the pooled storage quota on Drive volume. */ |
| async setPooledStorageQuotaUsage( |
| usedUserBytes: number, totalUserBytes: number, |
| organizationLimitExceeded: boolean) { |
| return sendTestMessage({ |
| name: 'setPooledStorageQuotaUsage', |
| usedUserBytes, |
| totalUserBytes, |
| organizationLimitExceeded, |
| }); |
| } |
| |
| /** |
| * Opens a Files app's main window. |
| * @param initialRoot Root path to be used as a default current directory |
| * during initialization. Can be null, for no default path. |
| * @param appState App state to be passed with on opening the Files app. |
| * @return Promise to be fulfilled after window creating. |
| */ |
| async openNewWindow(initialRoot: null|string, appState?: null|FilesAppState): |
| Promise<string> { |
| appState = appState ?? {}; |
| if (initialRoot) { |
| const tail = `external${initialRoot}`; |
| appState.currentDirectoryURL = `filesystem:${this.origin_}/${tail}`; |
| } |
| |
| const launchDir = appState ? appState.currentDirectoryURL : undefined; |
| const type = appState ? appState.type : undefined; |
| const volumeFilter = appState ? appState.volumeFilter : undefined; |
| const searchQuery = appState ? appState.searchQuery : undefined; |
| const appId = await sendTestMessage({ |
| name: 'launchFileManager', |
| launchDir, |
| type, |
| volumeFilter, |
| searchQuery, |
| }); |
| |
| return appId; |
| } |
| |
| /** |
| * Opens a file dialog and waits for closing it. |
| * @param dialogParams Dialog parameters to be passed to |
| * openEntryChoosingWindow() function. |
| * @param volumeType Volume icon type passed to the directory page object's |
| * selectItemByType function. |
| * @param expectedSet Expected set of the entries. |
| * @param closeDialog Function to close the dialog. |
| * @param useBrowserOpen Whether to launch the select file dialog via a |
| * browser OpenFile() call. |
| * @param debug Whether to debug the waitForWindow(). |
| * @return Promise to be fulfilled with the result entry of the dialog. |
| */ |
| async openAndWaitForClosingDialog( |
| dialogParams: chrome.fileSystem.ChooseEntryOptions, volumeType: string, |
| expectedSet: TestEntryInfo[], closeDialog: (a: string) => Promise<void>, |
| useBrowserOpen: boolean = false, |
| debug: boolean = false): Promise<unknown> { |
| const caller = getCaller(); |
| let resultPromise; |
| if (useBrowserOpen) { |
| await sendTestMessage({name: 'runSelectFileDialog'}); |
| resultPromise = async () => { |
| return await sendTestMessage( |
| {name: 'waitForSelectFileDialogNavigation'}); |
| }; |
| } else { |
| await openEntryChoosingWindow(dialogParams); |
| resultPromise = () => { |
| return pollForChosenEntry(caller); |
| }; |
| } |
| |
| const appId = await this.waitForWindow(debug); |
| await this.waitForElement(appId, '#file-list'); |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| const directoryTree = await DirectoryTreePageObject.create(appId); |
| await directoryTree.selectItemByType(volumeType); |
| await this.waitForFiles(appId, TestEntryInfo.getExpectedRows(expectedSet)); |
| await closeDialog(appId); |
| await repeatUntil(async () => { |
| const windows = await this.getWindows(); |
| if (windows[appId] !== appId) { |
| return; |
| } |
| return pending(caller, 'Waiting for Window %s to hide.', appId); |
| }); |
| return await 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. |
| * @param initialRoot Root path to be used as a default current directory |
| * during initialization. Can be null, for no default path. |
| * @param initialLocalEntries List of initial entries to load in Downloads |
| * (defaults to a basic entry set). |
| * @param initialDriveEntries List of initial entries to load in Google Drive |
| * (defaults to a basic entry set). |
| * @param appState App state to be passed with on opening the Files app. |
| * @return Promise to be fulfilled with the window ID. |
| */ |
| async setupAndWaitUntilReady( |
| initialRoot: null|string, |
| initialLocalEntries: TestEntryInfo[] = BASIC_LOCAL_ENTRY_SET, |
| initialDriveEntries: TestEntryInfo[] = BASIC_DRIVE_ENTRY_SET, |
| appState?: FilesAppState): Promise<string> { |
| const localEntriesPromise = addEntries(['local'], initialLocalEntries); |
| const driveEntriesPromise = addEntries(['drive'], initialDriveEntries); |
| |
| const appId = await this.openNewWindow(initialRoot, appState); |
| await this.waitForElement(appId, '#detail-table'); |
| |
| // Wait until the elements are loaded in the table. |
| await Promise.all([ |
| this.waitForFileListChange(appId, 0), |
| localEntriesPromise, |
| driveEntriesPromise, |
| ]); |
| await this.waitFor('isFileManagerLoaded', appId, true); |
| return appId; |
| } |
| |
| /** |
| * Creates a folder shortcut to |directoryName| using the context menu. Note |
| * the current directory must be a parent of the given |directoryName|. |
| * |
| * @param appId Files app windowId. |
| * @param directoryName Directory of shortcut to be created. |
| * @return Promise fulfilled on success. |
| */ |
| async createShortcut(appId: string, directoryName: string): Promise<void> { |
| await this.waitUntilSelected(appId, directoryName); |
| |
| await this.waitForElement(appId, ['.table-row[selected]']); |
| chrome.test.assertTrue(await this.callRemoteTestUtil( |
| 'fakeMouseRightClick', appId, ['.table-row[selected]'])); |
| |
| await this.waitForElement(appId, '#file-context-menu:not([hidden])'); |
| await this.waitForElement( |
| appId, '[command="#pin-folder"]:not([hidden]):not([disabled])'); |
| chrome.test.assertTrue(await this.callRemoteTestUtil( |
| 'fakeMouseClick', appId, |
| ['[command="#pin-folder"]:not([hidden]):not([disabled])'])); |
| |
| const directoryTree = await DirectoryTreePageObject.create(appId); |
| await directoryTree.waitForShortcutItemByLabel(directoryName); |
| } |
| |
| /** |
| * Mounts crostini volume by clicking on the fake crostini root. |
| * @param appId Files app windowId. |
| * @param initialEntries List of initial entries to load in Crostini (defaults |
| * to a basic entry set). |
| */ |
| async mountCrostini( |
| appId: string, |
| initialEntries: TestEntryInfo[] = BASIC_CROSTINI_ENTRY_SET) { |
| const directoryTree = await DirectoryTreePageObject.create(appId); |
| |
| // Add entries to crostini volume, but do not mount. |
| await addEntries(['crostini'], initialEntries); |
| |
| // Linux files fake root is shown. |
| await directoryTree.waitForPlaceholderItemByType('crostini'); |
| |
| // Mount crostini, and ensure real root and files are shown. |
| await directoryTree.selectPlaceholderItemByType('crostini'); |
| await directoryTree.waitForItemByType('crostini'); |
| const files = TestEntryInfo.getExpectedRows(initialEntries); |
| await this.waitForFiles(appId, files); |
| } |
| |
| /** |
| * Registers a GuestOS, mounts the volume, and populates it with tbe specified |
| * entries. |
| * @param appId Files app windowId. |
| * @param initialEntries List of initial entries to load in the volume. |
| */ |
| async mountGuestOs(appId: string, initialEntries: TestEntryInfo[]) { |
| await sendTestMessage({ |
| name: 'registerMountableGuest', |
| displayName: 'Bluejohn', |
| canMount: true, |
| vmType: 'bruschetta', |
| }); |
| const directoryTree = await DirectoryTreePageObject.create(appId); |
| |
| // Wait for the GuestOS fake root then click it. |
| await directoryTree.selectPlaceholderItemByType('bruschetta'); |
| |
| // Wait for the volume to get mounted. |
| await directoryTree.waitForItemByType('bruschetta'); |
| |
| // Add entries to GuestOS volume |
| await addEntries(['guest_os_0'], initialEntries); |
| |
| // Ensure real root and files are shown. |
| const files = TestEntryInfo.getExpectedRows(initialEntries); |
| await this.waitForFiles(appId, files); |
| } |
| |
| /** |
| * Returns true if the SinglePartitionFormat flag is on. |
| * @param appId Files app windowId. |
| */ |
| async isSinglePartitionFormat(appId: string) { |
| const dialog = |
| await this.waitForElement(appId, ['files-format-dialog', 'cr-dialog']); |
| const flag = dialog.attributes['single-partition-format'] || ''; |
| return !!flag; |
| } |
| |
| /** |
| * Shows hidden files to facilitate tests again the .Trash directory. |
| */ |
| async showHiddenFiles(appId: string, check: boolean = true) { |
| // Open the gear menu by clicking the gear button. |
| chrome.test.assertTrue(await this.callRemoteTestUtil( |
| 'fakeMouseClick', appId, ['#gear-button'])); |
| |
| // Wait for menu to not be hidden. |
| await this.waitForElement(appId, '#gear-menu:not([hidden])'); |
| |
| // Wait for menu item to appear. |
| await this.waitForElement( |
| appId, |
| `#gear-menu-toggle-hidden-files:not([disabled])${ |
| check ? ':not([checked])' : '[checked]'}`); |
| |
| // Click the menu item. |
| await this.callRemoteTestUtil( |
| 'fakeMouseClick', appId, ['#gear-menu-toggle-hidden-files']); |
| } |
| } |