1 // Copyright (c) 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.
8 * Extension ID of Files.app.
12 var FILE_MANAGER_EXTENSIONS_ID = 'hhaomjibdihmijegdhdafkllkbggdgoj';
15 * Calls a remote test util in Files.app's extension. See: test_util.js.
17 * @param {string} func Function name.
18 * @param {?string} appId Target window's App ID or null for functions
19 * not requiring a window.
20 * @param {Array.<*>} args Array of arguments.
21 * @param {function(*)=} opt_callback Callback handling the function's result.
22 * @return {Promise} Promise to be fulfilled with the result of the remote
25 function callRemoteTestUtil(func, appId, args, opt_callback) {
26 return new Promise(function(onFulfilled) {
27 chrome.runtime.sendMessage(
28 FILE_MANAGER_EXTENSIONS_ID, {
35 opt_callback.apply(null, arguments);
36 onFulfilled(arguments[0]);
42 * Returns promise to be fulfilled after the given milliseconds.
43 * @param {number} time Time in milliseconds.
46 return new Promise(function(callback) {
47 setTimeout(callback, time);
52 * Interval milliseconds between checks of repeatUntil.
56 var REPEAT_UNTIL_INTERVAL = 200;
59 * Interval milliseconds between log output of repeatUntil.
63 var LOG_INTERVAL = 3000;
66 * Returns a pending marker. See also the repeatUntil function.
67 * @param {string} message Pending reason including %s, %d, or %j markers. %j
68 * format an object as JSON.
69 * @param {Array.<*>} var_args Values to be assigined to %x markers.
70 * @return {Object} Object which returns true for the expression: obj instanceof
73 function pending(message, var_args) {
76 var formattedMessage = message.replace(/%[sdj]/g, function(pattern) {
77 var arg = args[index++];
79 case '%s': return String(arg);
80 case '%d': return Number(arg);
81 case '%j': return JSON.stringify(arg);
82 default: return pattern;
85 var pendingMarker = Object.create(pending.prototype);
86 pendingMarker.message = formattedMessage;
91 * Waits until the checkFunction returns a value but a pending marker.
92 * @param {function():*} checkFunction Function to check a condition. It can
93 * return a pending marker created by a pending function.
94 * @return {Promise} Promise to be fulfilled with the return value of
95 * checkFunction when the checkFunction reutrns a value but a pending
98 function repeatUntil(checkFunction) {
99 var logTime = Date.now() + LOG_INTERVAL;
100 var step = function() {
101 return checkFunction().then(function(result) {
102 if (result instanceof pending) {
103 if (Date.now() > logTime) {
104 console.log(result.message);
105 logTime += LOG_INTERVAL;
107 return wait(REPEAT_UNTIL_INTERVAL).then(step);
117 * Waits until a window having the given ID prefix appears.
118 * @param {string} windowIdPrefix ID prefix of the requested window.
119 * @return {Promise} promise Promise to be fulfilled with a found window's ID.
121 function waitForWindow(windowIdPrefix) {
122 return repeatUntil(function() {
123 return callRemoteTestUtil('getWindows', null, []).then(function(windows) {
124 for (var id in windows) {
125 if (id.indexOf(windowIdPrefix) === 0)
128 return pending('Window with the prefix %s is not found.', windowIdPrefix);
134 * Closes a window and waits until the window is closed.
136 * @param {string} windowId ID of the window to close.
137 * @return {Promise} promise Promise to be fulfilled with the result (true:
138 * success, false: failed).
140 function closeWindowAndWait(windowId) {
141 // Closes the window.
142 return callRemoteTestUtil('closeWindow', null, [windowId]).then(
144 // Returns false when the closing is failed.
148 return repeatUntil(function() {
149 return callRemoteTestUtil('getWindows', null, []).then(
151 for (var id in windows) {
152 if (id === windowId) {
153 // Window is still available. Continues waiting.
154 return pending('Window with the prefix %s is not found.',
158 // Window is not available. Closing is done successfully.
168 * Waits until the window turns to the given size.
169 * @param {string} windowId Target window ID.
170 * @param {number} width Requested width in pixels.
171 * @param {number} height Requested height in pixels.
173 function waitForWindowGeometry(windowId, width, height) {
174 return repeatUntil(function() {
175 return callRemoteTestUtil('getWindows', null, []).then(function(windows) {
176 if (!windows[windowId])
177 return pending('Window %s is not found.', windowId);
178 if (windows[windowId].innerWidth !== width ||
179 windows[windowId].innerHeight !== height) {
180 return pending('Expected window size is %j, but it is %j',
181 {width: width, height: height},
189 * Waits for the specified element appearing in the DOM.
190 * @param {string} windowId Target window ID.
191 * @param {string} query Query string for the element.
192 * @param {string=} opt_iframeQuery Query string for the iframe containing the
194 * @return {Promise} Promise to be fulfilled when the element appears.
196 function waitForElement(windowId, query, opt_iframeQuery) {
197 return repeatUntil(function() {
198 return callRemoteTestUtil(
201 [query, opt_iframeQuery]
202 ).then(function(elements) {
203 if (elements.length > 0)
207 'Element %s (maybe in iframe %s) is not found.',
215 * Waits for the specified element leaving from the DOM.
216 * @param {string} windowId Target window ID.
217 * @param {string} query Query string for the element.
218 * @param {string=} opt_iframeQuery Query string for the iframe containing the
220 * @return {Promise} Promise to be fulfilled when the element is lost.
222 function waitForElementLost(windowId, query, opt_iframeQuery) {
223 return repeatUntil(function() {
224 return callRemoteTestUtil(
227 [query, opt_iframeQuery]
228 ).then(function(elements) {
229 if (elements.length > 0)
230 return pending('Elements %j is still exists.', elements);
238 * Waits for the file list turns to the given contents.
239 * @param {string} windowId Target window ID.
240 * @param {Array.<Array.<string>>} expected Expected contents of file list.
241 * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options
242 * Options of the comparison. If orderCheck is true, it also compares the
243 * order of files. If ignoreLastModifiedTime is true, it compares the file
244 * without its last modified time.
245 * @return {Promise} Promise to be fulfilled when the file list turns to the
248 function waitForFiles(windowId, expected, opt_options) {
249 var options = opt_options || {};
250 return repeatUntil(function() {
251 return callRemoteTestUtil(
252 'getFileList', windowId, []).then(function(files) {
253 if (!options.orderCheck) {
257 for (var i = 0; i < Math.min(files.length, expected.length); i++) {
258 if (options.ignoreFileSize) {
262 if (options.ignoreLastModifiedTime) {
267 if (!chrome.test.checkDeepEq(expected, files)) {
268 return pending('waitForFiles: expected: %j actual %j.',
277 * Waits until the number of files in the file list is changed from the given
279 * TODO(hirono): Remove the function.
281 * @param {string} windowId Target window ID.
282 * @param {number} lengthBefore Number of items visible before.
283 * @return {Promise} Promise to be fulfilled with the contents of files.
285 function waitForFileListChange(windowId, lengthBefore) {
286 return repeatUntil(function() {
287 return callRemoteTestUtil(
288 'getFileList', windowId, []).then(function(files) {
290 var notReadyRows = files.filter(function(row) {
291 return row.filter(function(cell) { return cell == '...'; }).length;
293 if (notReadyRows.length === 0 &&
294 files.length !== lengthBefore &&
295 files.length !== 0) {
298 return pending('The number of file is %d. Not changed.', lengthBefore);
305 * Waits until the given taskId appears in the executed task list.
306 * @param {string} windowId Target window ID.
307 * @param {string} taskId Task ID to watch.
308 * @return {Promise} Promise to be fulfilled when the task appears in the
309 * executed task list.
311 function waitUntilTaskExecutes(windowId, taskId) {
312 return repeatUntil(function() {
313 return callRemoteTestUtil('getExecutedTasks', windowId, []).
314 then(function(executedTasks) {
315 if (executedTasks.indexOf(taskId) === -1)
316 return pending('Executed task is %j', executedTasks);
322 * Adds check of chrome.test to the end of the given promise.
323 * @param {Promise} promise Promise.
325 function testPromise(promise) {
326 promise.then(function() {
327 return new Promise(checkIfNoErrorsOccured);
328 }).then(chrome.test.callbackPass(function() {
329 // The callbacPass is necessary to avoid prematurely finishing tests.
330 // Don't put chrome.test.succeed() here to avoid doubled success log.
331 }), function(error) {
332 chrome.test.fail(error.stack || error);
337 * Sends a fake key down event.
338 * @param {string} windowId Window ID.
339 * @param {string} query Query for the target element.
340 * @param {string} keyIdentifer Key identifier.
341 * @param {boolean} ctrlKey Control key flag.
342 * @return {Promise} Promise to be fulfilled or rejected depending on the
345 function fakeKeyDown(windowId, query, keyIdentifer, ctrlKey) {
346 return new Promise(function(fulfill, reject) {
347 callRemoteTestUtil('fakeKeyDown',
349 [query, keyIdentifer, ctrlKey],
354 reject(new Error('Fail to fake key down.'));
360 * Executes a sequence of test steps.
363 function StepsRunner() {
366 * @type {Array.<function>}
373 * Creates a StepsRunner instance and runs the passed steps.
375 StepsRunner.run = function(steps) {
376 var stepsRunner = new StepsRunner();
377 stepsRunner.run_(steps);
380 StepsRunner.prototype = {
382 * @return {function} The next closure.
385 return this.steps_[0];
390 * Runs a sequence of the added test steps.
391 * @type {Array.<function>} List of the sequential steps.
393 StepsRunner.prototype.run_ = function(steps) {
394 this.steps_ = steps.slice(0);
396 // An extra step which acts as an empty callback for optional asynchronous
397 // calls in the last provided step.
398 this.steps_.push(function() {});
400 this.steps_ = this.steps_.map(function(f) {
401 return chrome.test.callbackPass(function() {
403 f.apply(this, arguments);
411 * Adds the givin entries to the target volume(s).
412 * @param {Array.<string>} volumeNames Names of target volumes.
413 * @param {Array.<TestEntryInfo>} entries List of entries to be added.
414 * @param {function(boolean)} callback Callback function to be passed the result
415 * of function. The argument is true on success.
417 function addEntries(volumeNames, entries, callback) {
418 if (volumeNames.length == 0) {
422 chrome.test.sendMessage(JSON.stringify({
424 volume: volumeNames.shift(),
426 }), chrome.test.callbackPass(function(result) {
427 if (result == "onEntryAdded")
428 addEntries(volumeNames, entries, callback);
438 var EntryType = Object.freeze({
440 DIRECTORY: 'directory'
447 var SharedOption = Object.freeze({
455 var RootPath = Object.seal({
456 DOWNLOADS: '/must-be-filled-in-test-setup',
457 DRIVE: '/must-be-filled-in-test-setup',
461 * File system entry information for tests.
463 * @param {EntryType} type Entry type.
464 * @param {string} sourceFileName Source file name that provides file contents.
465 * @param {string} targetName Name of entry on the test file system.
466 * @param {string} mimeType Mime type.
467 * @param {SharedOption} sharedOption Shared option.
468 * @param {string} lastModifiedTime Last modified time as a text to be shown in
469 * the last modified column.
470 * @param {string} nameText File name to be shown in the name column.
471 * @param {string} sizeText Size text to be shown in the size column.
472 * @param {string} typeText Type name to be shown in the type column.
475 function TestEntryInfo(type,
485 this.sourceFileName = sourceFileName || '';
486 this.targetPath = targetPath;
487 this.mimeType = mimeType || '';
488 this.sharedOption = sharedOption;
489 this.lastModifiedTime = lastModifiedTime;
490 this.nameText = nameText;
491 this.sizeText = sizeText;
492 this.typeText = typeText;
496 TestEntryInfo.getExpectedRows = function(entries) {
497 return entries.map(function(entry) { return entry.getExpectedRow(); });
501 * Obtains a expected row contents of the file in the file list.
503 TestEntryInfo.prototype.getExpectedRow = function() {
504 return [this.nameText, this.sizeText, this.typeText, this.lastModifiedTime];
508 * Filesystem entries used by the test cases.
509 * @type {Object.<string, TestEntryInfo>}
513 hello: new TestEntryInfo(
514 EntryType.FILE, 'text.txt', 'hello.txt',
515 'text/plain', SharedOption.NONE, 'Sep 4, 1998 12:34 PM',
516 'hello.txt', '51 bytes', 'Plain text'),
518 world: new TestEntryInfo(
519 EntryType.FILE, 'video.ogv', 'world.ogv',
520 'text/plain', SharedOption.NONE, 'Jul 4, 2012 10:35 AM',
521 'world.ogv', '59 KB', 'OGG video'),
523 unsupported: new TestEntryInfo(
524 EntryType.FILE, 'random.bin', 'unsupported.foo',
525 'application/x-foo', SharedOption.NONE, 'Jul 4, 2012 10:36 AM',
526 'unsupported.foo', '8 KB', 'FOO file'),
528 desktop: new TestEntryInfo(
529 EntryType.FILE, 'image.png', 'My Desktop Background.png',
530 'text/plain', SharedOption.NONE, 'Jan 18, 2038 1:02 AM',
531 'My Desktop Background.png', '272 bytes', 'PNG image'),
533 beautiful: new TestEntryInfo(
534 EntryType.FILE, 'music.ogg', 'Beautiful Song.ogg',
535 'text/plain', SharedOption.NONE, 'Nov 12, 2086 12:00 PM',
536 'Beautiful Song.ogg', '14 KB', 'OGG audio'),
538 photos: new TestEntryInfo(
539 EntryType.DIRECTORY, null, 'photos',
540 null, SharedOption.NONE, 'Jan 1, 1980 11:59 PM',
541 'photos', '--', 'Folder'),
543 testDocument: new TestEntryInfo(
544 EntryType.FILE, null, 'Test Document',
545 'application/vnd.google-apps.document',
546 SharedOption.NONE, 'Apr 10, 2013 4:20 PM',
547 'Test Document.gdoc', '--', 'Google document'),
549 testSharedDocument: new TestEntryInfo(
550 EntryType.FILE, null, 'Test Shared Document',
551 'application/vnd.google-apps.document',
552 SharedOption.SHARED, 'Mar 20, 2013 10:40 PM',
553 'Test Shared Document.gdoc', '--', 'Google document'),
555 newlyAdded: new TestEntryInfo(
556 EntryType.FILE, 'music.ogg', 'newly added file.ogg',
557 'audio/ogg', SharedOption.NONE, 'Sep 4, 1998 12:00 AM',
558 'newly added file.ogg', '14 KB', 'OGG audio'),
560 directoryA: new TestEntryInfo(
561 EntryType.DIRECTORY, null, 'A',
562 null, SharedOption.NONE, 'Jan 1, 2000 1:00 AM',
563 'A', '--', 'Folder'),
565 directoryB: new TestEntryInfo(
566 EntryType.DIRECTORY, null, 'A/B',
567 null, SharedOption.NONE, 'Jan 1, 2000 1:00 AM',
568 'B', '--', 'Folder'),
570 directoryC: new TestEntryInfo(
571 EntryType.DIRECTORY, null, 'A/B/C',
572 null, SharedOption.NONE, 'Jan 1, 2000 1:00 AM',
573 'C', '--', 'Folder'),
575 zipArchive: new TestEntryInfo(
576 EntryType.FILE, 'archive.zip', 'archive.zip',
577 'application/x-zip', SharedOption.NONE, 'Jan 1, 2014 1:00 AM',
578 'archive.zip', '533 bytes', 'Zip archive')
582 * Basic entry set for the local volume.
583 * @type {Array.<TestEntryInfo>}
586 var BASIC_LOCAL_ENTRY_SET = [
595 * Basic entry set for the drive volume.
597 * TODO(hirono): Add a case for an entry cached by FileCache. For testing
598 * Drive, create more entries with Drive specific attributes.
600 * @type {Array.<TestEntryInfo>}
603 var BASIC_DRIVE_ENTRY_SET = [
610 ENTRIES.testDocument,
611 ENTRIES.testSharedDocument
614 var NESTED_ENTRY_SET = [
621 * Expected files shown in "Recent". Directories (e.g. 'photos') are not in this
622 * list as they are not expected in "Recent".
624 * @type {Array.<TestEntryInfo>}
627 var RECENT_ENTRY_SET = [
633 ENTRIES.testDocument,
634 ENTRIES.testSharedDocument
638 * Expected files shown in "Offline", which should have the files
639 * "available offline". Google Documents, Google Spreadsheets, and the files
640 * cached locally are "available offline".
642 * @type {Array.<TestEntryInfo>}
645 var OFFLINE_ENTRY_SET = [
646 ENTRIES.testDocument,
647 ENTRIES.testSharedDocument
651 * Expected files shown in "Shared with me", which should be the entries labeled
652 * with "shared-with-me".
654 * @type {Array.<TestEntryInfo>}
657 var SHARED_WITH_ME_ENTRY_SET = [
658 ENTRIES.testSharedDocument
662 * Opens a Files.app's main window.
664 * TODO(mtomasz): Pass a volumeId or an enum value instead of full paths.
666 * @param {Object} appState App state to be passed with on opening Files.app.
668 * @param {?string} initialRoot Root path to be used as a default current
669 * directory during initialization. Can be null, for no default path.
670 * @param {function(string)} Callback with the app id.
671 * @return {Promise} Promise to be fulfilled after window creating.
673 function openNewWindow(appState, initialRoot, callback) {
676 // TODO(mtomasz): Migrate from full paths to a pair of a volumeId and a
677 // relative path. To compose the URL communicate via messages with
678 // file_manager_browser_test.cc.
679 var processedAppState = appState || {};
681 processedAppState.currentDirectoryURL =
682 'filesystem:chrome-extension://' + FILE_MANAGER_EXTENSIONS_ID +
683 '/external' + initialRoot;
686 return callRemoteTestUtil('openMainWindow',
693 * Opens a Files.app's main window and waits until it is initialized. Fills
694 * the window with initial files. Should be called for the first window only.
696 * TODO(hirono): Add parameters to specify the entry set to be prepared.
697 * TODO(mtomasz): Pass a volumeId or an enum value instead of full paths.
699 * @param {Object} appState App state to be passed with on opening Files.app.
701 * @param {?string} initialRoot Root path to be used as a default current
702 * directory during initialization. Can be null, for no default path.
703 * @param {function(string, Array.<Array.<string>>)} Callback with the app id
704 * and with the file list.
706 function setupAndWaitUntilReady(appState, initialRoot, callback) {
711 openNewWindow(appState, initialRoot, this.next);
715 addEntries(['local'], BASIC_LOCAL_ENTRY_SET, this.next);
718 chrome.test.assertTrue(success);
719 addEntries(['drive'], BASIC_DRIVE_ENTRY_SET, this.next);
722 chrome.test.assertTrue(success);
723 waitForElement(appId, '#detail-table').then(this.next);
726 waitForFileListChange(appId, 0).then(this.next);
729 callback(appId, fileList);
736 * Verifies if there are no Javascript errors in any of the app windows.
737 * @param {function()} Completion callback.
739 function checkIfNoErrorsOccured(callback) {
740 callRemoteTestUtil('getErrorCount', null, [], function(count) {
741 chrome.test.assertEq(0, count, 'The error count is not 0.');
747 * Returns the name of the given file list entry.
748 * @param {Array.<string>} file An entry in a file list.
749 * @return {string} Name of the file.
751 function getFileName(fileListEntry) {
752 return fileListEntry[0];
756 * Returns the size of the given file list entry.
757 * @param {Array.<string>} An entry in a file list.
758 * @return {string} Size of the file.
760 function getFileSize(fileListEntry) {
761 return fileListEntry[1];
765 * Returns the type of the given file list entry.
766 * @param {Array.<string>} An entry in a file list.
767 * @return {string} Type of the file.
769 function getFileType(fileListEntry) {
770 return fileListEntry[2];
774 * Namespace for test cases.
778 // Ensure the test cases are loaded.
779 window.addEventListener('load', function() {
781 // Check for the guest mode.
783 chrome.test.sendMessage(
784 JSON.stringify({name: 'isInGuestMode'}), steps.shift());
786 // Obtain the test case name.
788 if (JSON.parse(result) != chrome.extension.inIncognitoContext)
790 chrome.test.sendMessage(
791 JSON.stringify({name: 'getRootPaths'}), steps.shift());
793 // Obtain the root entry paths.
795 var roots = JSON.parse(result);
796 RootPath.DOWNLOADS = roots.downloads;
797 RootPath.DRIVE = roots.drive;
798 chrome.test.sendMessage(
799 JSON.stringify({name: 'getTestName'}), steps.shift());
801 // Run the test case.
802 function(testCaseName) {
803 if (!testcase[testCaseName]) {
804 chrome.test.runTests([function() {
805 chrome.test.fail(testCaseName + ' is not found.');
809 chrome.test.runTests([testcase[testCaseName]]);