remote_call.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. // Copyright 2014 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import {ElementObject} from './element_object.js';
  5. import {KeyModifiers} from './key_modifiers.js';
  6. import {getCaller, pending, repeatUntil, sendTestMessage} from './test_util.js';
  7. import {VolumeManagerCommonVolumeType} from './volume_manager_common_volume_type.js';
  8. /**
  9. * When step by step tests are enabled, turns on automatic step() calls. Note
  10. * that if step() is defined at the time of this call, invoke it to start the
  11. * test auto-stepping ball rolling.
  12. */
  13. window.autoStep = () => {
  14. window.autostep = window.autostep || false;
  15. if (!window.autostep) {
  16. window.autostep = true;
  17. }
  18. if (window.autostep && typeof window.step === 'function') {
  19. window.step();
  20. }
  21. };
  22. /**
  23. * This error type is thrown by executeJsInPreviewTagSwa_ if the script to
  24. * execute in the untrusted context produces an error.
  25. */
  26. export class ExecuteScriptError extends Error {
  27. constructor(message) {
  28. super(message);
  29. this.name = 'ExecuteScriptError';
  30. }
  31. }
  32. /**
  33. * Class to manipulate the window in the remote extension.
  34. */
  35. export class RemoteCall {
  36. /**
  37. * @param {string} origin ID of the app to be manipulated.
  38. */
  39. constructor(origin) {
  40. this.origin_ = origin;
  41. /**
  42. * Tristate holding the cached result of isStepByStepEnabled_().
  43. * @type {?boolean}
  44. */
  45. this.cachedStepByStepEnabled_ = null;
  46. }
  47. /**
  48. * Checks whether step by step tests are enabled or not.
  49. * @private
  50. * @return {!Promise<boolean>}
  51. */
  52. async isStepByStepEnabled_() {
  53. if (this.cachedStepByStepEnabled_ === null) {
  54. this.cachedStepByStepEnabled_ = await new Promise((fulfill) => {
  55. chrome.commandLinePrivate.hasSwitch(
  56. 'enable-file-manager-step-by-step-tests', fulfill);
  57. });
  58. }
  59. return this.cachedStepByStepEnabled_;
  60. }
  61. /**
  62. * Sends a test |message| to the test code running in the File Manager.
  63. * @param {!Object} message
  64. * @return {!Promise<*>} A promise which when fulfilled returns the
  65. * result of executing test code with the given message.
  66. */
  67. sendMessage(message) {
  68. return new Promise((fulfill) => {
  69. chrome.runtime.sendMessage(this.origin_, message, {}, fulfill);
  70. });
  71. }
  72. /**
  73. * Calls a remote test util in the Files app's extension. See:
  74. * registerRemoteTestUtils in test_util_base.js.
  75. *
  76. * @param {string} func Function name.
  77. * @param {?string} appId App window Id or null for functions not requiring a
  78. * window.
  79. * @param {?Array<*>=} args Array of arguments.
  80. * @param {function(*)=} opt_callback Callback handling the function's result.
  81. * @return {!Promise} Promise to be fulfilled with the result of the remote
  82. * utility.
  83. */
  84. async callRemoteTestUtil(func, appId, args, opt_callback) {
  85. const stepByStep = await this.isStepByStepEnabled_();
  86. let finishCurrentStep;
  87. if (stepByStep) {
  88. while (window.currentStep) {
  89. await window.currentStep;
  90. }
  91. window.currentStep = new Promise(resolve => {
  92. finishCurrentStep = () => {
  93. window.currentStep = null;
  94. resolve();
  95. };
  96. });
  97. console.info('Executing: ' + func + ' on ' + appId + ' with args: ');
  98. console.info(args);
  99. if (window.autostep !== true) {
  100. await new Promise((onFulfilled) => {
  101. console.info('Type step() to continue...');
  102. /** @type {?function()} */
  103. window.step = function() {
  104. window.step = null;
  105. onFulfilled();
  106. };
  107. });
  108. } else {
  109. console.info('Auto calling step() ...');
  110. }
  111. }
  112. const response = await this.sendMessage({func, appId, args});
  113. if (stepByStep) {
  114. console.info('Returned value:');
  115. console.info(JSON.stringify(response));
  116. finishCurrentStep();
  117. }
  118. if (opt_callback) {
  119. opt_callback(response);
  120. }
  121. return response;
  122. }
  123. /**
  124. * Waits until a window having the given ID prefix appears.
  125. * @param {string} windowIdPrefix ID prefix of the requested window.
  126. * @return {!Promise<string>} promise Promise to be fulfilled with a found
  127. * window's ID.
  128. */
  129. waitForWindow(windowIdPrefix) {
  130. const caller = getCaller();
  131. const windowIdRegex = new RegExp(windowIdPrefix);
  132. return repeatUntil(async () => {
  133. const windows = await this.callRemoteTestUtil('getWindows', null, []);
  134. for (const id in windows) {
  135. if (id.indexOf(windowIdPrefix) === 0 || windowIdRegex.test(id)) {
  136. return id;
  137. }
  138. }
  139. return pending(
  140. caller, 'Window with the prefix %s is not found.', windowIdPrefix);
  141. });
  142. }
  143. /**
  144. * Closes a window and waits until the window is closed.
  145. *
  146. * @param {string} appId App window Id.
  147. * @return {Promise} promise Promise to be fulfilled with the result (true:
  148. * success, false: failed).
  149. */
  150. async closeWindowAndWait(appId) {
  151. const caller = getCaller();
  152. // Closes the window.
  153. if (!await this.callRemoteTestUtil('closeWindow', null, [appId])) {
  154. // Returns false when the closing is failed.
  155. return false;
  156. }
  157. return repeatUntil(async () => {
  158. const windows = await this.callRemoteTestUtil('getWindows', null, []);
  159. for (const id in windows) {
  160. if (id === appId) {
  161. // Window is still available. Continues waiting.
  162. return pending(
  163. caller, 'Window with the prefix %s is not found.', appId);
  164. }
  165. }
  166. // Window is not available. Closing is done successfully.
  167. return true;
  168. });
  169. }
  170. /**
  171. * Waits until the window turns to the given size.
  172. * @param {string} appId App window Id.
  173. * @param {number} width Requested width in pixels.
  174. * @param {number} height Requested height in pixels.
  175. */
  176. waitForWindowGeometry(appId, width, height) {
  177. const caller = getCaller();
  178. return repeatUntil(async () => {
  179. const windows = await this.callRemoteTestUtil('getWindows', null, []);
  180. if (!windows[appId]) {
  181. return pending(caller, 'Window %s is not found.', appId);
  182. }
  183. if (windows[appId].outerWidth !== width ||
  184. windows[appId].outerHeight !== height) {
  185. return pending(
  186. caller, 'Expected window size is %j, but it is %j',
  187. {width: width, height: height}, windows[appId]);
  188. }
  189. });
  190. }
  191. /**
  192. * Waits for the specified element appearing in the DOM.
  193. * @param {string} appId App window Id.
  194. * @param {string|!Array<string>} query Query to specify the element.
  195. * If query is an array, |query[0]| specifies the first
  196. * element(s), |query[1]| specifies elements inside the shadow DOM of
  197. * the first element, and so on.
  198. * @return {Promise<ElementObject>} Promise to be fulfilled when the element
  199. * appears.
  200. */
  201. waitForElement(appId, query) {
  202. return this.waitForElementStyles(appId, query, []);
  203. }
  204. /**
  205. * Waits for the specified element appearing in the DOM.
  206. * @param {string} appId App window Id.
  207. * @param {string|!Array<string>} query Query to specify the element.
  208. * If query is an array, |query[0]| specifies the first
  209. * element(s), |query[1]| specifies elements inside the shadow DOM of
  210. * the first element, and so on.
  211. * @param {!Array<string>} styleNames List of CSS property name to be
  212. * obtained. NOTE: Causes element style re-calculation.
  213. * @return {Promise<ElementObject>} Promise to be fulfilled when the element
  214. * appears.
  215. */
  216. waitForElementStyles(appId, query, styleNames) {
  217. const caller = getCaller();
  218. return repeatUntil(async () => {
  219. const elements = await this.callRemoteTestUtil(
  220. 'deepQueryAllElements', appId, [query, styleNames]);
  221. if (elements && elements.length > 0) {
  222. return /** @type {ElementObject} */ (elements[0]);
  223. }
  224. return pending(caller, 'Element %s is not found.', query);
  225. });
  226. }
  227. /**
  228. * Waits for a remote test function to return a specific result.
  229. *
  230. * @param {string} funcName Name of remote test function to be executed.
  231. * @param {?string} appId App window Id.
  232. * @param {function(Object):boolean|boolean|Object} expectedResult An value to
  233. * be checked against the return value of |funcName| or a callback that
  234. * receives the return value of |funcName| and returns true if the result
  235. * is the expected value.
  236. * @param {?Array<*>=} args Arguments to be provided to |funcName| when
  237. * executing it.
  238. * @return {Promise} Promise to be fulfilled when the |expectedResult| is
  239. * returned from |funcName| execution.
  240. */
  241. waitFor(funcName, appId, expectedResult, args) {
  242. const caller = getCaller();
  243. args = args || [];
  244. return repeatUntil(async () => {
  245. const result = await this.callRemoteTestUtil(funcName, appId, args);
  246. if (typeof expectedResult === 'function' && expectedResult(result)) {
  247. return result;
  248. }
  249. if (expectedResult === result) {
  250. return result;
  251. }
  252. const msg = 'waitFor: Waiting for ' +
  253. `${funcName} to return ${expectedResult}, ` +
  254. `but got ${JSON.stringify(result)}.`;
  255. return pending(caller, msg);
  256. });
  257. }
  258. /**
  259. * Waits for the specified element leaving from the DOM.
  260. * @param {string} appId App window Id.
  261. * @param {string|!Array<string>} query Query to specify the element.
  262. * If query is an array, |query[0]| specifies the first
  263. * element(s), |query[1]| specifies elements inside the shadow DOM of
  264. * the first element, and so on.
  265. * @return {Promise} Promise to be fulfilled when the element is lost.
  266. */
  267. waitForElementLost(appId, query) {
  268. const caller = getCaller();
  269. return repeatUntil(async () => {
  270. const elements =
  271. await this.callRemoteTestUtil('deepQueryAllElements', appId, [query]);
  272. if (elements.length > 0) {
  273. return pending(caller, 'Elements %j still exists.', elements);
  274. }
  275. return true;
  276. });
  277. }
  278. /**
  279. * Wait for the |query| to match |count| elements.
  280. *
  281. * @param {string} appId App window Id.
  282. * @param {string|!Array<string>} query Query to specify the element.
  283. * If query is an array, |query[0]| specifies the first
  284. * element(s), |query[1]| specifies elements inside the shadow DOM of
  285. * the first element, and so on.
  286. * @param {number} count The expected element match count.
  287. * @return {Promise} Promise to be fulfilled on success.
  288. */
  289. waitForElementsCount(appId, query, count) {
  290. const caller = getCaller();
  291. return repeatUntil(async () => {
  292. const expect = `Waiting for [${query}] to match ${count} elements`;
  293. const result =
  294. await this.callRemoteTestUtil('countElements', appId, [query, count]);
  295. return !result ? pending(caller, expect) : true;
  296. });
  297. }
  298. /**
  299. * Sends a fake key down event.
  300. * @param {string} appId App window Id.
  301. * @param {string|!Array<string>} query Query to specify the element.
  302. * If query is an array, |query[0]| specifies the first
  303. * element(s), |query[1]| specifies elements inside the shadow DOM of
  304. * the first element, and so on.
  305. * @param {string} key DOM UI Events Key value.
  306. * @param {boolean} ctrlKey Control key flag.
  307. * @param {boolean} shiftKey Shift key flag.
  308. * @param {boolean} altKey Alt key flag.
  309. * @return {Promise} Promise to be fulfilled or rejected depending on the
  310. * result.
  311. */
  312. async fakeKeyDown(appId, query, key, ctrlKey, shiftKey, altKey) {
  313. const result = await this.callRemoteTestUtil(
  314. 'fakeKeyDown', appId, [query, key, ctrlKey, shiftKey, altKey]);
  315. if (result) {
  316. return true;
  317. } else {
  318. throw new Error('Fail to fake key down.');
  319. }
  320. }
  321. /**
  322. * Sets the given input text on the element identified by the query.
  323. * @param {string} appId App window ID.
  324. * @param {string|!Array<string>} selector The query selector to locate
  325. * the element
  326. * @param {string} text The text to be set on the element.
  327. */
  328. async inputText(appId, selector, text) {
  329. chrome.test.assertTrue(
  330. await this.callRemoteTestUtil('inputText', appId, [selector, text]));
  331. }
  332. /**
  333. * Gets file entries just under the volume.
  334. *
  335. * @param {VolumeManagerCommonVolumeType} volumeType Volume type.
  336. * @param {Array<string>} names File name list.
  337. * @return {Promise} Promise to be fulfilled with file entries or rejected
  338. * depending on the result.
  339. */
  340. getFilesUnderVolume(volumeType, names) {
  341. return this.callRemoteTestUtil(
  342. 'getFilesUnderVolume', null, [volumeType, names]);
  343. }
  344. /**
  345. * Waits for a single file.
  346. * @param {VolumeManagerCommonVolumeType} volumeType Volume type.
  347. * @param {string} name File name.
  348. * @return {!Promise} Promise to be fulfilled when the file had found.
  349. */
  350. waitForAFile(volumeType, name) {
  351. const caller = getCaller();
  352. return repeatUntil(async () => {
  353. if ((await this.getFilesUnderVolume(volumeType, [name])).length === 1) {
  354. return true;
  355. }
  356. return pending(caller, '"' + name + '" is not found.');
  357. });
  358. }
  359. /**
  360. * Shorthand for clicking an element.
  361. * @param {string} appId App window Id.
  362. * @param {string|!Array<string>} query Query to specify the element.
  363. * If query is an array, |query[0]| specifies the first
  364. * element(s), |query[1]| specifies elements inside the shadow DOM of
  365. * the first element, and so on.
  366. * @param {KeyModifiers=} opt_keyModifiers Object
  367. * @return {Promise} Promise to be fulfilled with the clicked element.
  368. */
  369. async waitAndClickElement(appId, query, opt_keyModifiers) {
  370. const element = await this.waitForElement(appId, query);
  371. const result = await this.callRemoteTestUtil(
  372. 'fakeMouseClick', appId, [query, opt_keyModifiers]);
  373. chrome.test.assertTrue(result, 'mouse click failed.');
  374. return element;
  375. }
  376. /**
  377. * Shorthand for right-clicking an element.
  378. * @param {string} appId App window Id.
  379. * @param {string|!Array<string>} query Query to specify the element.
  380. * If query is an array, |query[0]| specifies the first
  381. * element(s), |query[1]| specifies elements inside the shadow DOM of
  382. * the first element, and so on.
  383. * @param {KeyModifiers=} opt_keyModifiers Object
  384. * @return {Promise} Promise to be fulfilled with the clicked element.
  385. */
  386. async waitAndRightClick(appId, query, opt_keyModifiers) {
  387. const element = await this.waitForElement(appId, query);
  388. const result = await this.callRemoteTestUtil(
  389. 'fakeMouseRightClick', appId, [query, opt_keyModifiers]);
  390. chrome.test.assertTrue(result, 'mouse right-click failed.');
  391. return element;
  392. }
  393. /**
  394. * Shorthand for focusing an element.
  395. * @param {string} appId App window Id.
  396. * @param {!Array<string>} query Query to specify the element to be focused.
  397. * @return {Promise} Promise to be fulfilled with the focused element.
  398. */
  399. async focus(appId, query) {
  400. const element = await this.waitForElement(appId, query);
  401. const result = await this.callRemoteTestUtil('focus', appId, query);
  402. chrome.test.assertTrue(result, 'focus failed.');
  403. return element;
  404. }
  405. /**
  406. * Simulate Click in the UI in the middle of the element.
  407. * @param{string} appId App window ID contains the element. NOTE: The click is
  408. * simulated on most recent window in the window system.
  409. * @param {string|!Array<string>} query Query to the element to be clicked.
  410. * @return {!Promise} A promise fulfilled after the click event.
  411. */
  412. async simulateUiClick(appId, query) {
  413. const element = /* @type {!Object} */ (
  414. await this.waitForElementStyles(appId, query, ['display']));
  415. chrome.test.assertTrue(!!element, 'element for simulateUiClick not found');
  416. // Find the middle of the element.
  417. const x =
  418. Math.floor(element['renderedLeft'] + (element['renderedWidth'] / 2));
  419. const y =
  420. Math.floor(element['renderedTop'] + (element['renderedHeight'] / 2));
  421. return sendTestMessage(
  422. {appId, name: 'simulateClick', 'clickX': x, 'clickY': y});
  423. }
  424. }
  425. /**
  426. * Class to manipulate the window in the remote extension.
  427. */
  428. export class RemoteCallFilesApp extends RemoteCall {
  429. /**
  430. * @return {boolean} Returns whether the code is running in SWA mode.
  431. */
  432. isSwaMode() {
  433. return this.origin_.startsWith('chrome://');
  434. }
  435. /**
  436. * Sends a test |message| to the test code running in the File Manager.
  437. * @param {!Object} message
  438. * @return {!Promise<*>}
  439. * @override
  440. */
  441. sendMessage(message) {
  442. if (!this.isSwaMode()) {
  443. return super.sendMessage(message);
  444. }
  445. const command = {
  446. name: 'callSwaTestMessageListener',
  447. appId: message.appId,
  448. data: JSON.stringify(message),
  449. };
  450. return new Promise((fulfill) => {
  451. chrome.test.sendMessage(JSON.stringify(command), (response) => {
  452. if (response === '"@undefined@"') {
  453. fulfill(undefined);
  454. } else {
  455. try {
  456. fulfill(response == '' ? true : JSON.parse(response));
  457. } catch (e) {
  458. console.error(`Failed to parse "${response}" due to ${e}`);
  459. fulfill(false);
  460. }
  461. }
  462. });
  463. });
  464. }
  465. /** @override */
  466. async waitForWindow(windowIdPrefix) {
  467. if (!this.isSwaMode()) {
  468. return super.waitForWindow(windowIdPrefix);
  469. }
  470. return this.waitForSwaWindow();
  471. }
  472. async getWindows() {
  473. if (!this.isSwaMode()) {
  474. return this.callRemoteTestUtil('getWindows', null, []);
  475. }
  476. return JSON.parse(
  477. await sendTestMessage({name: 'getWindowsSWA', isSWA: true}));
  478. }
  479. /**
  480. * Wait for a SWA window to be open.
  481. * @return {!Promise<string>}
  482. */
  483. async waitForSwaWindow() {
  484. const caller = getCaller();
  485. const appId = await repeatUntil(async () => {
  486. const ret = await sendTestMessage({name: 'findSwaWindow'});
  487. if (ret === 'none') {
  488. return pending(caller, 'Wait for SWA window');
  489. }
  490. return ret;
  491. });
  492. return appId;
  493. }
  494. /**
  495. * Executes a script in the context of a <preview-tag> element contained in
  496. * the window.
  497. * For SWA: It's the first chrome-untrusted://file-manager <iframe>.
  498. * For legacy: It's the first elements based on the `query`.
  499. * Responds with its output.
  500. *
  501. * @param {string} appId App window Id.
  502. * @param {!Array<string>} query Query to the <preview-tag> element (this is
  503. * ignored for SWA).
  504. * @param {string} statement Javascript statement to be executed within the
  505. * <preview-tag>.
  506. * @return {!Promise<*>} resolved with the return value of the `statement`.
  507. */
  508. async executeJsInPreviewTag(appId, query, statement) {
  509. if (this.isSwaMode()) {
  510. return this.executeJsInPreviewTagSwa_(statement);
  511. }
  512. return this.callRemoteTestUtil(
  513. 'deepExecuteScriptInWebView', appId, [query, statement]);
  514. }
  515. /**
  516. * Inject javascript statemenent in the first chrome-untrusted://file-manager
  517. * page found and respond with its output.
  518. * @private
  519. * @param {string} statement
  520. * @return {!Promise}
  521. */
  522. async executeJsInPreviewTagSwa_(statement) {
  523. const script = `try {
  524. let result = ${statement};
  525. result = result === undefined ? '@undefined@' : [result];
  526. window.domAutomationController.send(JSON.stringify(result));
  527. } catch (error) {
  528. const errorInfo = {'@error@': error.message, '@stack@': error.stack};
  529. window.domAutomationController.send(JSON.stringify(errorInfo));
  530. }`;
  531. const command = {
  532. name: 'executeScriptInChromeUntrusted',
  533. data: script,
  534. };
  535. const response = await sendTestMessage(command);
  536. if (response === '"@undefined@"') {
  537. return undefined;
  538. }
  539. const output = JSON.parse(response);
  540. if ('@error@' in output) {
  541. console.error(output['@error@']);
  542. console.error('Original StackTrace:\n' + output['@stack@']);
  543. throw new ExecuteScriptError(
  544. 'Error executing JS in Preview: ' + output['@error@']);
  545. } else {
  546. return output;
  547. }
  548. }
  549. /**
  550. * Waits until the expected URL shows in the last opened browser tab.
  551. * @param {string} expectedUrl
  552. * @return {!Promise} Promise to be fulfilled when the expected URL is shown
  553. * in a browser window.
  554. */
  555. async waitForLastOpenedBrowserTabUrl(expectedUrl) {
  556. const caller = getCaller();
  557. return repeatUntil(async () => {
  558. const command = {name: 'getLastActiveTabURL'};
  559. const activeBrowserTabURL = await sendTestMessage(command);
  560. if (activeBrowserTabURL !== expectedUrl) {
  561. return pending(
  562. caller, 'waitForActiveBrowserTabUrl: expected %j actual %j.',
  563. expectedUrl, activeBrowserTabURL);
  564. }
  565. });
  566. }
  567. /**
  568. * Returns whether an window exists with the expected URL.
  569. * @param {string} expectedUrl
  570. * @return {!Promise<boolean>} Promise resolved with true or false depending
  571. * on whether such window exists.
  572. */
  573. async windowUrlExists(expectedUrl) {
  574. const command = {name: 'expectWindowURL', expectedUrl: expectedUrl};
  575. const windowExists = await sendTestMessage(command);
  576. return windowExists == 'true';
  577. }
  578. /**
  579. * Waits for the file list turns to the given contents.
  580. * @param {string} appId App window Id.
  581. * @param {Array<Array<string>>} expected Expected contents of file list.
  582. * @param {{orderCheck:(?boolean|undefined), ignoreFileSize:
  583. * (?boolean|undefined), ignoreLastModifiedTime:(?boolean|undefined)}=}
  584. * opt_options Options of the comparison. If orderCheck is true, it also
  585. * compares the order of files. If ignoreLastModifiedTime is true, it
  586. * compares the file without its last modified time.
  587. * @return {Promise} Promise to be fulfilled when the file list turns to the
  588. * given contents.
  589. */
  590. waitForFiles(appId, expected, opt_options) {
  591. const options = opt_options || {};
  592. const caller = getCaller();
  593. return repeatUntil(async () => {
  594. const files = await this.callRemoteTestUtil('getFileList', appId, []);
  595. if (!options.orderCheck) {
  596. files.sort();
  597. expected.sort();
  598. }
  599. for (let i = 0; i < Math.min(files.length, expected.length); i++) {
  600. // Change the value received from the UI to match when comparing.
  601. if (options.ignoreFileSize) {
  602. files[i][1] = expected[i][1];
  603. }
  604. if (options.ignoreLastModifiedTime) {
  605. if (expected[i].length < 4) {
  606. // expected sometimes doesn't include the modified time at all, so
  607. // just remove from the data from UI.
  608. files[i].splice(3, 1);
  609. } else {
  610. files[i][3] = expected[i][3];
  611. }
  612. }
  613. }
  614. if (!chrome.test.checkDeepEq(expected, files)) {
  615. return pending(
  616. caller, 'waitForFiles: expected: %j actual %j.', expected, files);
  617. }
  618. });
  619. }
  620. /**
  621. * Waits until the number of files in the file list is changed from the given
  622. * number.
  623. * TODO(hirono): Remove the function.
  624. *
  625. * @param {string} appId App window Id.
  626. * @param {number} lengthBefore Number of items visible before.
  627. * @return {Promise} Promise to be fulfilled with the contents of files.
  628. */
  629. waitForFileListChange(appId, lengthBefore) {
  630. const caller = getCaller();
  631. return repeatUntil(async () => {
  632. const files = await this.callRemoteTestUtil('getFileList', appId, []);
  633. files.sort();
  634. const notReadyRows =
  635. files.filter((row) => row.filter((cell) => cell == '...').length);
  636. if (notReadyRows.length === 0 && files.length !== lengthBefore &&
  637. files.length !== 0) {
  638. return files;
  639. } else {
  640. return pending(
  641. caller, 'The number of file is %d. Not changed.', lengthBefore);
  642. }
  643. });
  644. }
  645. /**
  646. * Waits until the given taskId appears in the executed task list.
  647. * @param {string} appId App window Id.
  648. * @param {!chrome.fileManagerPrivate.FileTaskDescriptor} descriptor Task to
  649. * watch.
  650. * @param {Array<Object>=} opt_replyArgs arguments to reply to executed task.
  651. * @return {Promise} Promise to be fulfilled when the task appears in the
  652. * executed task list.
  653. */
  654. waitUntilTaskExecutes(appId, descriptor, opt_replyArgs) {
  655. const caller = getCaller();
  656. return repeatUntil(async () => {
  657. if (!await this.callRemoteTestUtil(
  658. 'taskWasExecuted', appId, [descriptor])) {
  659. const executedTasks =
  660. (await this.callRemoteTestUtil('getExecutedTasks', appId, []))
  661. .map(
  662. ({appId, taskType, actionId}) =>
  663. `${appId}|${taskType}|${actionId}`);
  664. return pending(caller, 'Executed task is %j', executedTasks);
  665. }
  666. if (opt_replyArgs) {
  667. await this.callRemoteTestUtil(
  668. 'replyExecutedTask', appId, [descriptor, opt_replyArgs]);
  669. }
  670. });
  671. }
  672. /**
  673. * Check if the next tabforcus'd element has the given ID or not.
  674. * @param {string} appId App window Id.
  675. * @param {string} elementId String of 'id' attribute which the next
  676. * tabfocus'd element should have.
  677. * @return {Promise} Promise to be fulfilled with the result.
  678. */
  679. async checkNextTabFocus(appId, elementId) {
  680. const result = await sendTestMessage({name: 'dispatchTabKey'});
  681. chrome.test.assertEq(
  682. result, 'tabKeyDispatched', 'Tab key dispatch failure');
  683. const caller = getCaller();
  684. return repeatUntil(async () => {
  685. let element =
  686. await this.callRemoteTestUtil('getActiveElement', appId, []);
  687. if (element && element.attributes['id'] === elementId) {
  688. return true;
  689. }
  690. // Try to check the shadow root.
  691. element =
  692. await this.callRemoteTestUtil('deepGetActiveElement', appId, []);
  693. if (element && element.attributes['id'] === elementId) {
  694. return true;
  695. }
  696. return pending(
  697. caller,
  698. 'Waiting for active element with id: "' + elementId +
  699. '", but current is: "' + element.attributes['id'] + '"');
  700. });
  701. }
  702. /**
  703. * Returns a promise that repeatedly checks for a file with the given
  704. * name to be selected in the app window with the given ID. Typical
  705. * use
  706. *
  707. * await remoteCall.waitUntilSelected('file#0', 'hello.txt');
  708. * ... // either the test timed out or hello.txt is currently selected.
  709. *
  710. * @param {string} appId App window Id.
  711. * @param {string} fileName the name of the file to be selected.
  712. * @return {Promise<boolean>} Promise that indicates if selection was
  713. * successful.
  714. */
  715. waitUntilSelected(appId, fileName) {
  716. const caller = getCaller();
  717. return repeatUntil(async () => {
  718. const selected =
  719. await this.callRemoteTestUtil('selectFile', appId, [fileName]);
  720. if (!selected) {
  721. return pending(caller, `File ${fileName} not yet selected`);
  722. }
  723. });
  724. }
  725. /**
  726. * Waits until the current directory is changed.
  727. * @param {string} appId App window Id.
  728. * @param {string} expectedPath Path to be changed to.
  729. * @return {Promise} Promise to be fulfilled when the current directory is
  730. * changed to expectedPath.
  731. */
  732. waitUntilCurrentDirectoryIsChanged(appId, expectedPath) {
  733. const caller = getCaller();
  734. return repeatUntil(async () => {
  735. const path =
  736. await this.callRemoteTestUtil('getBreadcrumbPath', appId, []);
  737. if (path !== expectedPath) {
  738. return pending(
  739. caller, 'Expected path is %s got %s', expectedPath, path);
  740. }
  741. });
  742. }
  743. /**
  744. * Expands tree item.
  745. * @param {string} appId App window Id.
  746. * @param {string} query Query to the <tree-item> element.
  747. */
  748. async expandTreeItemInDirectoryTree(appId, query) {
  749. await this.waitForElement(appId, query);
  750. const elements = await this.callRemoteTestUtil(
  751. 'queryAllElements', appId, [`${query}[expanded]`]);
  752. // If it's already expanded just set the focus on directory tree.
  753. if (elements.length > 0) {
  754. return this.callRemoteTestUtil('focus', appId, ['#directory-tree']);
  755. }
  756. // We must wait until <tree-item> has attribute [has-children=true]
  757. // otherwise it won't expand. We must also to account for the case
  758. // :not([expanded]) to ensure it has NOT been expanded by some async
  759. // operation since the [expanded] checks above.
  760. const expandIcon =
  761. query + ':not([expanded]) > .tree-row[has-children=true] .expand-icon';
  762. await this.waitAndClickElement(appId, expandIcon);
  763. // Wait for the expansion to finish.
  764. await this.waitForElement(appId, query + '[expanded]');
  765. // Force the focus on directory tree.
  766. await this.callRemoteTestUtil('focus', appId, ['#directory-tree']);
  767. }
  768. /**
  769. * Expands directory tree for specified path.
  770. */
  771. expandDirectoryTreeFor(appId, path, volumeType = 'downloads') {
  772. return this.expandDirectoryTreeForInternal_(
  773. appId, path.split('/'), 0, volumeType);
  774. }
  775. /**
  776. * Internal function for expanding directory tree for specified path.
  777. */
  778. async expandDirectoryTreeForInternal_(appId, components, index, volumeType) {
  779. if (index >= components.length - 1) {
  780. return;
  781. }
  782. // First time we should expand the root/volume first.
  783. if (index === 0) {
  784. await this.expandVolumeInDirectoryTree(appId, volumeType);
  785. return this.expandDirectoryTreeForInternal_(
  786. appId, components, index + 1, volumeType);
  787. }
  788. const path = '/' + components.slice(1, index + 1).join('/');
  789. await this.expandTreeItemInDirectoryTree(
  790. appId, `[full-path-for-testing="${path}"]`);
  791. await this.expandDirectoryTreeForInternal_(
  792. appId, components, index + 1, volumeType);
  793. }
  794. /**
  795. * Expands download volume in directory tree.
  796. */
  797. expandDownloadVolumeInDirectoryTree(appId) {
  798. return this.expandVolumeInDirectoryTree(appId, 'downloads');
  799. }
  800. /**
  801. * Expands download volume in directory tree.
  802. */
  803. expandVolumeInDirectoryTree(appId, volumeType) {
  804. return this.expandTreeItemInDirectoryTree(
  805. appId, `[volume-type-for-testing="${volumeType}"]`);
  806. }
  807. /**
  808. * Navigates to specified directory on the specified volume by using directory
  809. * tree.
  810. * DEPRECATED: Use background.js:navigateWithDirectoryTree instead
  811. * crbug.com/996626.
  812. */
  813. async navigateWithDirectoryTree(
  814. appId, path, rootLabel, volumeType = 'downloads') {
  815. await this.expandDirectoryTreeFor(appId, path, volumeType);
  816. // Select target path.
  817. await this.callRemoteTestUtil(
  818. 'fakeMouseClick', appId, [`[full-path-for-testing="${path}"]`]);
  819. // Entries within Drive starts with /root/ but it isn't displayed in the
  820. // breadcrubms used by waitUntilCurrentDirectoryIsChanged.
  821. path = path.replace(/^\/root/, '')
  822. .replace(/^\/team_drives/, '')
  823. .replace(/^\/Computers/, '');
  824. // TODO(lucmult): Remove this once MyFilesVolume is rolled out.
  825. // Remove /Downloads duplication when MyFilesVolume is enabled.
  826. if (volumeType == 'downloads' && path.startsWith('/Downloads') &&
  827. rootLabel.endsWith('/Downloads')) {
  828. rootLabel = rootLabel.replace('/Downloads', '');
  829. }
  830. // Wait until the Files app is navigated to the path.
  831. return this.waitUntilCurrentDirectoryIsChanged(
  832. appId, `/${rootLabel}${path}`);
  833. }
  834. /**
  835. * Wait until the expected number of volumes is mounted.
  836. * @param {number} expectedVolumesCount Expected number of mounted volumes.
  837. * @return {Promise} promise Promise to be fulfilled.
  838. */
  839. async waitForVolumesCount(expectedVolumesCount) {
  840. const caller = getCaller();
  841. return repeatUntil(async () => {
  842. const volumesCount = await sendTestMessage({name: 'getVolumesCount'});
  843. if (volumesCount === expectedVolumesCount.toString()) {
  844. return;
  845. }
  846. const msg =
  847. 'Expected number of mounted volumes: ' + expectedVolumesCount +
  848. '. Actual: ' + volumesCount;
  849. return pending(caller, msg);
  850. });
  851. }
  852. /**
  853. * Isolates the specified banner to test. The banner is still checked against
  854. * it's filters, but is now the top priority banner.
  855. * @param {string} appId App window Id
  856. * @param {string} bannerTagName Banner tag name in lowercase to isolate.
  857. */
  858. async isolateBannerForTesting(appId, bannerTagName) {
  859. await this.waitFor('isFileManagerLoaded', appId, true);
  860. chrome.test.assertTrue(await this.callRemoteTestUtil(
  861. 'isolateBannerForTesting', appId, [bannerTagName]));
  862. }
  863. /**
  864. * Disables banners from attaching to the DOM.
  865. * @param {string} appId App window Id
  866. */
  867. async disableBannersForTesting(appId) {
  868. await this.waitFor('isFileManagerLoaded', appId, true);
  869. chrome.test.assertTrue(
  870. await this.callRemoteTestUtil('disableBannersForTesting', appId, []));
  871. }
  872. /**
  873. * Sends text to the search box in the Files app.
  874. * @param {string} appId App window Id
  875. * @param {string} text The text to type in the search box.
  876. */
  877. async typeSearchText(appId, text) {
  878. const searchBoxInput = ['#search-box cr-input'];
  879. // Focus the search box.
  880. await this.waitAndClickElement(appId, '#search-button');
  881. // Input the text.
  882. await this.inputText(appId, searchBoxInput, text);
  883. // Notify the element of the input.
  884. chrome.test.assertTrue(await this.callRemoteTestUtil(
  885. 'fakeEvent', appId, ['#search-box cr-input', 'input']));
  886. }
  887. /**
  888. * Waits for the search box auto complete list to appear.
  889. * @param {string} appId
  890. * @return {!Promise<!Array<string>>} Array of the names in the auto complete
  891. * list.
  892. */
  893. async waitForSearchAutoComplete(appId) {
  894. // Wait for the list to appear.
  895. await this.waitForElement(appId, '#autocomplete-list li');
  896. // Return the result.
  897. const elements = await this.callRemoteTestUtil(
  898. 'deepQueryAllElements', appId, ['#autocomplete-list li']);
  899. return elements.map((element) => element.text);
  900. }
  901. }