| // 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') |
| }; |