|  | // Copyright 2014 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | 'use strict'; | 
|  |  | 
|  | /** | 
|  | * Sends a test message. | 
|  | * @param {Object} message Message to be sent. It is converted into JSON string | 
|  | *     before sending. | 
|  | * @return {Promise} Promise to be fulfilled with a returned value. | 
|  | */ | 
|  | function sendTestMessage(message) { | 
|  | return new Promise(function(fulfill) { | 
|  | chrome.test.sendMessage(JSON.stringify(message), fulfill); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns promise to be fulfilled after the given milliseconds. | 
|  | * @param {number} time Time in milliseconds. | 
|  | */ | 
|  | function wait(time) { | 
|  | return new Promise(function(callback) { | 
|  | setTimeout(callback, time); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Verifies if there are no Javascript errors in any of the app windows. | 
|  | * @param {function()} Completion callback. | 
|  | */ | 
|  | function checkIfNoErrorsOccuredOnApp(app, callback) { | 
|  | var countPromise = app.callRemoteTestUtil('getErrorCount', null, []); | 
|  | countPromise.then(function(count) { | 
|  | chrome.test.assertEq(0, count, 'The error count is not 0.'); | 
|  | callback(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Adds check of chrome.test to the end of the given promise. | 
|  | * @param {Promise} promise Promise. | 
|  | */ | 
|  | function testPromiseAndApps(promise, apps) { | 
|  | promise.then(function() { | 
|  | return Promise.all( | 
|  | apps.map(function(app) { | 
|  | return new Promise(checkIfNoErrorsOccuredOnApp.bind(null, app)); | 
|  | })); | 
|  | }).then(chrome.test.callbackPass(function() { | 
|  | // The callbacPass is necessary to avoid prematurely finishing tests. | 
|  | // Don't put chrome.test.succeed() here to avoid doubled success log. | 
|  | }), function(error) { | 
|  | chrome.test.fail(error.stack || error); | 
|  | }); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Interval milliseconds between checks of repeatUntil. | 
|  | * @type {number} | 
|  | * @const | 
|  | */ | 
|  | var REPEAT_UNTIL_INTERVAL = 200; | 
|  |  | 
|  | /** | 
|  | * Interval milliseconds between log output of repeatUntil. | 
|  | * @type {number} | 
|  | * @const | 
|  | */ | 
|  | var LOG_INTERVAL = 3000; | 
|  |  | 
|  | /** | 
|  | * Returns a pending marker. See also the repeatUntil function. | 
|  | * @param {string} message Pending reason including %s, %d, or %j markers. %j | 
|  | *     format an object as JSON. | 
|  | * @param {Array<*>} var_args Values to be assigined to %x markers. | 
|  | * @return {Object} Object which returns true for the expression: obj instanceof | 
|  | *     pending. | 
|  | */ | 
|  | function pending(message, var_args) { | 
|  | var index = 1; | 
|  | var args = arguments; | 
|  | var formattedMessage = message.replace(/%[sdj]/g, function(pattern) { | 
|  | var arg = args[index++]; | 
|  | switch(pattern) { | 
|  | case '%s': return String(arg); | 
|  | case '%d': return Number(arg); | 
|  | case '%j': return JSON.stringify(arg); | 
|  | default: return pattern; | 
|  | } | 
|  | }); | 
|  | var pendingMarker = Object.create(pending.prototype); | 
|  | pendingMarker.message = formattedMessage; | 
|  | return pendingMarker; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Waits until the checkFunction returns a value but a pending marker. | 
|  | * @param {function():*} checkFunction Function to check a condition. It can | 
|  | *     return a pending marker created by a pending function. | 
|  | * @return {Promise} Promise to be fulfilled with the return value of | 
|  | *     checkFunction when the checkFunction reutrns a value but a pending | 
|  | *     marker. | 
|  | */ | 
|  | function repeatUntil(checkFunction) { | 
|  | var logTime = Date.now() + LOG_INTERVAL; | 
|  | var step = function() { | 
|  | return Promise.resolve(checkFunction()).then(function(result) { | 
|  | if (result instanceof pending) { | 
|  | if (Date.now() > logTime) { | 
|  | console.warn(result.message); | 
|  | logTime += LOG_INTERVAL; | 
|  | } | 
|  | return wait(REPEAT_UNTIL_INTERVAL).then(step); | 
|  | } else { | 
|  | return result; | 
|  | } | 
|  | }); | 
|  | }; | 
|  | return step(); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Adds the givin entries to the target volume(s). | 
|  | * @param {Array<string>} volumeNames Names of target volumes. | 
|  | * @param {Array<TestEntryInfo>} entries List of entries to be added. | 
|  | * @param {function(boolean)=} opt_callback Callback function to be passed the | 
|  | *     result of function. The argument is true on success. | 
|  | * @return {Promise} Promise to be fulfilled when the entries are added. | 
|  | */ | 
|  | function addEntries(volumeNames, entries, opt_callback) { | 
|  | if (volumeNames.length == 0) { | 
|  | callback(true); | 
|  | return; | 
|  | } | 
|  | var volumeResultPromises = volumeNames.map(function(volume) { | 
|  | return sendTestMessage({ | 
|  | name: 'addEntries', | 
|  | volume: volume, | 
|  | entries: entries | 
|  | }); | 
|  | }); | 
|  | var resultPromise = Promise.all(volumeResultPromises); | 
|  | if (opt_callback) { | 
|  | resultPromise.then(opt_callback.bind(null, true), | 
|  | opt_callback.bind(null, false)); | 
|  | } | 
|  | return resultPromise; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * @enum {string} | 
|  | * @const | 
|  | */ | 
|  | var EntryType = Object.freeze({ | 
|  | FILE: 'file', | 
|  | DIRECTORY: 'directory' | 
|  | }); | 
|  |  | 
|  | /** | 
|  | * @enum {string} | 
|  | * @const | 
|  | */ | 
|  | var SharedOption = Object.freeze({ | 
|  | NONE: 'none', | 
|  | SHARED: 'shared' | 
|  | }); | 
|  |  | 
|  | /** | 
|  | * @enum {string} | 
|  | */ | 
|  | var RootPath = Object.seal({ | 
|  | DOWNLOADS: '/must-be-filled-in-test-setup', | 
|  | DRIVE: '/must-be-filled-in-test-setup', | 
|  | }); | 
|  |  | 
|  | /** | 
|  | * File system entry information for tests. | 
|  | * | 
|  | * @param {EntryType} type Entry type. | 
|  | * @param {string} sourceFileName Source file name that provides file contents. | 
|  | * @param {string} targetName Name of entry on the test file system. | 
|  | * @param {string} mimeType Mime type. | 
|  | * @param {SharedOption} sharedOption Shared option. | 
|  | * @param {string} lastModifiedTime Last modified time as a text to be shown in | 
|  | *     the last modified column. | 
|  | * @param {string} nameText File name to be shown in the name column. | 
|  | * @param {string} sizeText Size text to be shown in the size column. | 
|  | * @param {string} typeText Type name to be shown in the type column. | 
|  | * @constructor | 
|  | */ | 
|  | function TestEntryInfo(type, | 
|  | sourceFileName, | 
|  | targetPath, | 
|  | mimeType, | 
|  | sharedOption, | 
|  | lastModifiedTime, | 
|  | nameText, | 
|  | sizeText, | 
|  | typeText) { | 
|  | this.type = type; | 
|  | this.sourceFileName = sourceFileName || ''; | 
|  | this.targetPath = targetPath; | 
|  | this.mimeType = mimeType || ''; | 
|  | this.sharedOption = sharedOption; | 
|  | this.lastModifiedTime = lastModifiedTime; | 
|  | this.nameText = nameText; | 
|  | this.sizeText = sizeText; | 
|  | this.typeText = typeText; | 
|  | Object.freeze(this); | 
|  | }; | 
|  |  | 
|  | TestEntryInfo.getExpectedRows = function(entries) { | 
|  | return entries.map(function(entry) { return entry.getExpectedRow(); }); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Obtains a expected row contents of the file in the file list. | 
|  | */ | 
|  | TestEntryInfo.prototype.getExpectedRow = function() { | 
|  | return [this.nameText, this.sizeText, this.typeText, this.lastModifiedTime]; | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Filesystem entries used by the test cases. | 
|  | * @type {Object<TestEntryInfo>} | 
|  | * @const | 
|  | */ | 
|  | var ENTRIES = { | 
|  | hello: new TestEntryInfo( | 
|  | EntryType.FILE, 'text.txt', 'hello.txt', | 
|  | 'text/plain', SharedOption.NONE, 'Sep 4, 1998, 12:34 PM', | 
|  | 'hello.txt', '51 bytes', 'Plain text'), | 
|  |  | 
|  | world: new TestEntryInfo( | 
|  | EntryType.FILE, 'video.ogv', 'world.ogv', | 
|  | 'video/ogg', SharedOption.NONE, 'Jul 4, 2012, 10:35 AM', | 
|  | 'world.ogv', '59 KB', 'OGG video'), | 
|  |  | 
|  | unsupported: new TestEntryInfo( | 
|  | EntryType.FILE, 'random.bin', 'unsupported.foo', | 
|  | 'application/x-foo', SharedOption.NONE, 'Jul 4, 2012, 10:36 AM', | 
|  | 'unsupported.foo', '8 KB', 'FOO file'), | 
|  |  | 
|  | desktop: new TestEntryInfo( | 
|  | EntryType.FILE, 'image.png', 'My Desktop Background.png', | 
|  | 'image/png', SharedOption.NONE, 'Jan 18, 2038, 1:02 AM', | 
|  | 'My Desktop Background.png', '272 bytes', 'PNG image'), | 
|  |  | 
|  | // An image file without an extension, to confirm that file type detection | 
|  | // using mime types works fine. | 
|  | image2: new TestEntryInfo( | 
|  | EntryType.FILE, 'image2.png', 'image2', | 
|  | 'image/png', SharedOption.NONE, 'Jan 18, 2038, 1:02 AM', | 
|  | 'image2', '4 KB', 'PNG image'), | 
|  |  | 
|  | image3: new TestEntryInfo( | 
|  | EntryType.FILE, 'image3.jpg', 'image3.jpg', | 
|  | 'image/jpeg', SharedOption.NONE, 'Jan 18, 2038, 1:02 AM', | 
|  | 'image3.jpg', '3 KB', 'JPEG image'), | 
|  |  | 
|  | // An ogg file without a mime type, to confirm that file type detection using | 
|  | // file extensions works fine. | 
|  | beautiful: new TestEntryInfo( | 
|  | EntryType.FILE, 'music.ogg', 'Beautiful Song.ogg', | 
|  | null, SharedOption.NONE, 'Nov 12, 2086, 12:00 PM', | 
|  | 'Beautiful Song.ogg', '14 KB', 'OGG audio'), | 
|  |  | 
|  | photos: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'photos', | 
|  | null, SharedOption.NONE, 'Jan 1, 1980, 11:59 PM', | 
|  | 'photos', '--', 'Folder'), | 
|  |  | 
|  | testDocument: new TestEntryInfo( | 
|  | EntryType.FILE, null, 'Test Document', | 
|  | 'application/vnd.google-apps.document', | 
|  | SharedOption.NONE, 'Apr 10, 2013, 4:20 PM', | 
|  | 'Test Document.gdoc', '--', 'Google document'), | 
|  |  | 
|  | testSharedDocument: new TestEntryInfo( | 
|  | EntryType.FILE, null, 'Test Shared Document', | 
|  | 'application/vnd.google-apps.document', | 
|  | SharedOption.SHARED, 'Mar 20, 2013, 10:40 PM', | 
|  | 'Test Shared Document.gdoc', '--', 'Google document'), | 
|  |  | 
|  | newlyAdded: new TestEntryInfo( | 
|  | EntryType.FILE, 'music.ogg', 'newly added file.ogg', | 
|  | 'audio/ogg', SharedOption.NONE, 'Sep 4, 1998, 12:00 AM', | 
|  | 'newly added file.ogg', '14 KB', 'OGG audio'), | 
|  |  | 
|  | directoryA: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'A', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'A', '--', 'Folder'), | 
|  |  | 
|  | directoryB: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'A/B', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'B', '--', 'Folder'), | 
|  |  | 
|  | directoryC: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'A/B/C', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'C', '--', 'Folder'), | 
|  |  | 
|  | directoryD: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'D', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'D', '--', 'Folder'), | 
|  |  | 
|  | directoryE: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'D/E', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'E', '--', 'Folder'), | 
|  |  | 
|  | directoryF: new TestEntryInfo( | 
|  | EntryType.DIRECTORY, null, 'D/E/F', | 
|  | null, SharedOption.NONE, 'Jan 1, 2000, 1:00 AM', | 
|  | 'F', '--', 'Folder'), | 
|  |  | 
|  | zipArchive: new TestEntryInfo( | 
|  | EntryType.FILE, 'archive.zip', 'archive.zip', | 
|  | 'application/x-zip', SharedOption.NONE, 'Jan 1, 2014, 1:00 AM', | 
|  | 'archive.zip', '533 bytes', 'Zip archive'), | 
|  |  | 
|  | hiddenFile: new TestEntryInfo( | 
|  | EntryType.FILE, 'text.txt', '.hiddenfile.txt', | 
|  | 'text/plain', SharedOption.NONE, 'Sep 30, 2014, 3:30 PM', | 
|  | '.hiddenfile.txt', '51 bytes', 'Plain text') | 
|  | }; |