// 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} volumeNames Names of target volumes. * @param {Array} 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} * @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') };