michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let tests = [ michael@0: { michael@0: desc: "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " + michael@0: "close container with a single child", michael@0: michael@0: loading: function (node, newState, oldState) { michael@0: this.checkStateChanged("loading", 1); michael@0: this.checkArgs("loading", node, oldState, node.STATE_CLOSED); michael@0: }, michael@0: michael@0: opened: function (node, newState, oldState) { michael@0: this.checkStateChanged("opened", 1); michael@0: this.checkState("loading", 1); michael@0: this.checkArgs("opened", node, oldState, node.STATE_LOADING); michael@0: michael@0: print("Checking node children"); michael@0: compareArrayToResult(this.data, node); michael@0: michael@0: print("Closing container"); michael@0: node.containerOpen = false; michael@0: }, michael@0: michael@0: closed: function (node, newState, oldState) { michael@0: this.checkStateChanged("closed", 1); michael@0: this.checkState("opened", 1); michael@0: this.checkArgs("closed", node, oldState, node.STATE_OPENED); michael@0: this.success(); michael@0: } michael@0: }, michael@0: michael@0: { michael@0: desc: "nsNavHistoryFolderResultNode: After async open and no changes, " + michael@0: "second open should be synchronous", michael@0: michael@0: loading: function (node, newState, oldState) { michael@0: this.checkStateChanged("loading", 1); michael@0: this.checkState("closed", 0); michael@0: this.checkArgs("loading", node, oldState, node.STATE_CLOSED); michael@0: }, michael@0: michael@0: opened: function (node, newState, oldState) { michael@0: let cnt = this.checkStateChanged("opened", 1, 2); michael@0: let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED; michael@0: this.checkArgs("opened", node, oldState, expectOldState); michael@0: michael@0: print("Checking node children"); michael@0: compareArrayToResult(this.data, node); michael@0: michael@0: print("Closing container"); michael@0: node.containerOpen = false; michael@0: }, michael@0: michael@0: closed: function (node, newState, oldState) { michael@0: let cnt = this.checkStateChanged("closed", 1, 2); michael@0: this.checkArgs("closed", node, oldState, node.STATE_OPENED); michael@0: michael@0: switch (cnt) { michael@0: case 1: michael@0: node.containerOpen = true; michael@0: break; michael@0: case 2: michael@0: this.success(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: { michael@0: desc: "nsNavHistoryFolderResultNode: After closing container in " + michael@0: "loading(), opened() should not be called", michael@0: michael@0: loading: function (node, newState, oldState) { michael@0: this.checkStateChanged("loading", 1); michael@0: this.checkArgs("loading", node, oldState, node.STATE_CLOSED); michael@0: print("Closing container"); michael@0: node.containerOpen = false; michael@0: }, michael@0: michael@0: opened: function (node, newState, oldState) { michael@0: do_throw("opened should not be called"); michael@0: }, michael@0: michael@0: closed: function (node, newState, oldState) { michael@0: this.checkStateChanged("closed", 1); michael@0: this.checkState("loading", 1); michael@0: this.checkArgs("closed", node, oldState, node.STATE_LOADING); michael@0: this.success(); michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: michael@0: /** michael@0: * Instances of this class become the prototypes of the test objects above. michael@0: * Each test can therefore use the methods of this class, or they can override michael@0: * them if they want. To run a test, call setup() and then run(). michael@0: */ michael@0: function Test() { michael@0: // This maps a state name to the number of times it's been observed. michael@0: this.stateCounts = {}; michael@0: // Promise object resolved when the next test can be run. michael@0: this.deferNextTest = Promise.defer(); michael@0: } michael@0: michael@0: Test.prototype = { michael@0: /** michael@0: * Call this when an observer observes a container state change to sanity michael@0: * check the arguments. michael@0: * michael@0: * @param aNewState michael@0: * The name of the new state. Used only for printing out helpful info. michael@0: * @param aNode michael@0: * The node argument passed to containerStateChanged. michael@0: * @param aOldState michael@0: * The old state argument passed to containerStateChanged. michael@0: * @param aExpectOldState michael@0: * The expected old state. michael@0: */ michael@0: checkArgs: function (aNewState, aNode, aOldState, aExpectOldState) { michael@0: print("Node passed on " + aNewState + " should be result.root"); michael@0: do_check_eq(this.result.root, aNode); michael@0: print("Old state passed on " + aNewState + " should be " + aExpectOldState); michael@0: michael@0: // aOldState comes from xpconnect and will therefore be defined. It may be michael@0: // zero, though, so use strict equality just to make sure aExpectOldState is michael@0: // also defined. michael@0: do_check_true(aOldState === aExpectOldState); michael@0: }, michael@0: michael@0: /** michael@0: * Call this when an observer observes a container state change. It registers michael@0: * the state change and ensures that it has been observed the given number michael@0: * of times. See checkState for parameter explanations. michael@0: * michael@0: * @return The number of times aState has been observed, including the new michael@0: * observation. michael@0: */ michael@0: checkStateChanged: function (aState, aExpectedMin, aExpectedMax) { michael@0: print(aState + " state change observed"); michael@0: if (!this.stateCounts.hasOwnProperty(aState)) michael@0: this.stateCounts[aState] = 0; michael@0: this.stateCounts[aState]++; michael@0: return this.checkState(aState, aExpectedMin, aExpectedMax); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the state has been observed the given number of times. michael@0: * michael@0: * @param aState michael@0: * The name of the state. michael@0: * @param aExpectedMin michael@0: * The state must have been observed at least this number of times. michael@0: * @param aExpectedMax michael@0: * The state must have been observed at most this number of times. michael@0: * This parameter is optional. If undefined, it's set to michael@0: * aExpectedMin. michael@0: * @return The number of times aState has been observed, including the new michael@0: * observation. michael@0: */ michael@0: checkState: function (aState, aExpectedMin, aExpectedMax) { michael@0: let cnt = this.stateCounts[aState] || 0; michael@0: if (aExpectedMax === undefined) michael@0: aExpectedMax = aExpectedMin; michael@0: if (aExpectedMin === aExpectedMax) { michael@0: print(aState + " should be observed only " + aExpectedMin + michael@0: " times (actual = " + cnt + ")"); michael@0: } michael@0: else { michael@0: print(aState + " should be observed at least " + aExpectedMin + michael@0: " times and at most " + aExpectedMax + " times (actual = " + michael@0: cnt + ")"); michael@0: } michael@0: do_check_true(cnt >= aExpectedMin && cnt <= aExpectedMax); michael@0: return cnt; michael@0: }, michael@0: michael@0: /** michael@0: * Asynchronously opens the root of the test's result. michael@0: */ michael@0: openContainer: function () { michael@0: // Set up the result observer. It delegates to this object's callbacks and michael@0: // wraps them in a try-catch so that errors don't get eaten. michael@0: this.observer = let (self = this) { michael@0: containerStateChanged: function (container, oldState, newState) { michael@0: print("New state passed to containerStateChanged() should equal the " + michael@0: "container's current state"); michael@0: do_check_eq(newState, container.state); michael@0: michael@0: try { michael@0: switch (newState) { michael@0: case Ci.nsINavHistoryContainerResultNode.STATE_LOADING: michael@0: self.loading(container, newState, oldState); michael@0: break; michael@0: case Ci.nsINavHistoryContainerResultNode.STATE_OPENED: michael@0: self.opened(container, newState, oldState); michael@0: break; michael@0: case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED: michael@0: self.closed(container, newState, oldState); michael@0: break; michael@0: default: michael@0: do_throw("Unexpected new state! " + newState); michael@0: } michael@0: } michael@0: catch (err) { michael@0: do_throw(err); michael@0: } michael@0: }, michael@0: }; michael@0: this.result.addObserver(this.observer, false); michael@0: michael@0: print("Opening container"); michael@0: this.result.root.containerOpen = true; michael@0: }, michael@0: michael@0: /** michael@0: * Starts the test and returns a promise resolved when the test completes. michael@0: */ michael@0: run: function () { michael@0: this.openContainer(); michael@0: return this.deferNextTest.promise; michael@0: }, michael@0: michael@0: /** michael@0: * This must be called before run(). It adds a bookmark and sets up the michael@0: * test's result. Override if need be. michael@0: */ michael@0: setup: function () { michael@0: // Populate the database with different types of bookmark items. michael@0: this.data = DataHelper.makeDataArray([ michael@0: { type: "bookmark" }, michael@0: { type: "separator" }, michael@0: { type: "folder" }, michael@0: { type: "bookmark", uri: "place:terms=foo" } michael@0: ]); michael@0: yield task_populateDB(this.data); michael@0: michael@0: // Make a query. michael@0: this.query = PlacesUtils.history.getNewQuery(); michael@0: this.query.setFolders([DataHelper.defaults.bookmark.parent], 1); michael@0: this.opts = PlacesUtils.history.getNewQueryOptions(); michael@0: this.opts.asyncEnabled = true; michael@0: this.result = PlacesUtils.history.executeQuery(this.query, this.opts); michael@0: }, michael@0: michael@0: /** michael@0: * Call this when the test has succeeded. It cleans up resources and starts michael@0: * the next test. michael@0: */ michael@0: success: function () { michael@0: this.result.removeObserver(this.observer); michael@0: michael@0: // Resolve the promise object that indicates that the next test can be run. michael@0: this.deferNextTest.resolve(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * This makes it a little bit easier to use the functions of head_queries.js. michael@0: */ michael@0: let DataHelper = { michael@0: defaults: { michael@0: bookmark: { michael@0: parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, michael@0: uri: "http://example.com/", michael@0: title: "test bookmark" michael@0: }, michael@0: michael@0: folder: { michael@0: parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, michael@0: title: "test folder" michael@0: }, michael@0: michael@0: separator: { michael@0: parent: PlacesUtils.bookmarks.unfiledBookmarksFolder michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Converts an array of simple bookmark item descriptions to the more verbose michael@0: * format required by task_populateDB() in head_queries.js. michael@0: * michael@0: * @param aData michael@0: * An array of objects, each of which describes a bookmark item. michael@0: * @return An array of objects suitable for passing to populateDB(). michael@0: */ michael@0: makeDataArray: function DH_makeDataArray(aData) { michael@0: return let (self = this) aData.map(function (dat) { michael@0: let type = dat.type; michael@0: dat = self._makeDataWithDefaults(dat, self.defaults[type]); michael@0: switch (type) { michael@0: case "bookmark": michael@0: return { michael@0: isBookmark: true, michael@0: uri: dat.uri, michael@0: parentFolder: dat.parent, michael@0: index: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: title: dat.title, michael@0: isInQuery: true michael@0: }; michael@0: case "separator": michael@0: return { michael@0: isSeparator: true, michael@0: parentFolder: dat.parent, michael@0: index: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: isInQuery: true michael@0: }; michael@0: case "folder": michael@0: return { michael@0: isFolder: true, michael@0: readOnly: false, michael@0: parentFolder: dat.parent, michael@0: index: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: title: dat.title, michael@0: isInQuery: true michael@0: }; michael@0: default: michael@0: do_throw("Unknown data type when populating DB: " + type); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Returns a copy of aData, except that any properties that are undefined but michael@0: * defined in aDefaults are set to the corresponding values in aDefaults. michael@0: * michael@0: * @param aData michael@0: * An object describing a bookmark item. michael@0: * @param aDefaults michael@0: * An object describing the default bookmark item. michael@0: * @return A copy of aData with defaults values set. michael@0: */ michael@0: _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) { michael@0: let dat = {}; michael@0: for (let [prop, val] in Iterator(aDefaults)) { michael@0: dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val; michael@0: } michael@0: return dat; michael@0: } michael@0: }; michael@0: michael@0: function run_test() michael@0: { michael@0: run_next_test(); michael@0: } michael@0: michael@0: add_task(function test_async() michael@0: { michael@0: for (let [, test] in Iterator(tests)) { michael@0: remove_all_bookmarks(); michael@0: michael@0: test.__proto__ = new Test(); michael@0: yield test.setup(); michael@0: michael@0: print("------ Running test: " + test.desc); michael@0: yield test.run(); michael@0: } michael@0: michael@0: remove_all_bookmarks(); michael@0: print("All tests done, exiting"); michael@0: });