1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/tests/queries/test_async.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,366 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim:set ts=2 sw=2 sts=2 et: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +let tests = [ 1.11 + { 1.12 + desc: "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " + 1.13 + "close container with a single child", 1.14 + 1.15 + loading: function (node, newState, oldState) { 1.16 + this.checkStateChanged("loading", 1); 1.17 + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); 1.18 + }, 1.19 + 1.20 + opened: function (node, newState, oldState) { 1.21 + this.checkStateChanged("opened", 1); 1.22 + this.checkState("loading", 1); 1.23 + this.checkArgs("opened", node, oldState, node.STATE_LOADING); 1.24 + 1.25 + print("Checking node children"); 1.26 + compareArrayToResult(this.data, node); 1.27 + 1.28 + print("Closing container"); 1.29 + node.containerOpen = false; 1.30 + }, 1.31 + 1.32 + closed: function (node, newState, oldState) { 1.33 + this.checkStateChanged("closed", 1); 1.34 + this.checkState("opened", 1); 1.35 + this.checkArgs("closed", node, oldState, node.STATE_OPENED); 1.36 + this.success(); 1.37 + } 1.38 + }, 1.39 + 1.40 + { 1.41 + desc: "nsNavHistoryFolderResultNode: After async open and no changes, " + 1.42 + "second open should be synchronous", 1.43 + 1.44 + loading: function (node, newState, oldState) { 1.45 + this.checkStateChanged("loading", 1); 1.46 + this.checkState("closed", 0); 1.47 + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); 1.48 + }, 1.49 + 1.50 + opened: function (node, newState, oldState) { 1.51 + let cnt = this.checkStateChanged("opened", 1, 2); 1.52 + let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED; 1.53 + this.checkArgs("opened", node, oldState, expectOldState); 1.54 + 1.55 + print("Checking node children"); 1.56 + compareArrayToResult(this.data, node); 1.57 + 1.58 + print("Closing container"); 1.59 + node.containerOpen = false; 1.60 + }, 1.61 + 1.62 + closed: function (node, newState, oldState) { 1.63 + let cnt = this.checkStateChanged("closed", 1, 2); 1.64 + this.checkArgs("closed", node, oldState, node.STATE_OPENED); 1.65 + 1.66 + switch (cnt) { 1.67 + case 1: 1.68 + node.containerOpen = true; 1.69 + break; 1.70 + case 2: 1.71 + this.success(); 1.72 + break; 1.73 + } 1.74 + } 1.75 + }, 1.76 + 1.77 + { 1.78 + desc: "nsNavHistoryFolderResultNode: After closing container in " + 1.79 + "loading(), opened() should not be called", 1.80 + 1.81 + loading: function (node, newState, oldState) { 1.82 + this.checkStateChanged("loading", 1); 1.83 + this.checkArgs("loading", node, oldState, node.STATE_CLOSED); 1.84 + print("Closing container"); 1.85 + node.containerOpen = false; 1.86 + }, 1.87 + 1.88 + opened: function (node, newState, oldState) { 1.89 + do_throw("opened should not be called"); 1.90 + }, 1.91 + 1.92 + closed: function (node, newState, oldState) { 1.93 + this.checkStateChanged("closed", 1); 1.94 + this.checkState("loading", 1); 1.95 + this.checkArgs("closed", node, oldState, node.STATE_LOADING); 1.96 + this.success(); 1.97 + } 1.98 + } 1.99 +]; 1.100 + 1.101 + 1.102 +/** 1.103 + * Instances of this class become the prototypes of the test objects above. 1.104 + * Each test can therefore use the methods of this class, or they can override 1.105 + * them if they want. To run a test, call setup() and then run(). 1.106 + */ 1.107 +function Test() { 1.108 + // This maps a state name to the number of times it's been observed. 1.109 + this.stateCounts = {}; 1.110 + // Promise object resolved when the next test can be run. 1.111 + this.deferNextTest = Promise.defer(); 1.112 +} 1.113 + 1.114 +Test.prototype = { 1.115 + /** 1.116 + * Call this when an observer observes a container state change to sanity 1.117 + * check the arguments. 1.118 + * 1.119 + * @param aNewState 1.120 + * The name of the new state. Used only for printing out helpful info. 1.121 + * @param aNode 1.122 + * The node argument passed to containerStateChanged. 1.123 + * @param aOldState 1.124 + * The old state argument passed to containerStateChanged. 1.125 + * @param aExpectOldState 1.126 + * The expected old state. 1.127 + */ 1.128 + checkArgs: function (aNewState, aNode, aOldState, aExpectOldState) { 1.129 + print("Node passed on " + aNewState + " should be result.root"); 1.130 + do_check_eq(this.result.root, aNode); 1.131 + print("Old state passed on " + aNewState + " should be " + aExpectOldState); 1.132 + 1.133 + // aOldState comes from xpconnect and will therefore be defined. It may be 1.134 + // zero, though, so use strict equality just to make sure aExpectOldState is 1.135 + // also defined. 1.136 + do_check_true(aOldState === aExpectOldState); 1.137 + }, 1.138 + 1.139 + /** 1.140 + * Call this when an observer observes a container state change. It registers 1.141 + * the state change and ensures that it has been observed the given number 1.142 + * of times. See checkState for parameter explanations. 1.143 + * 1.144 + * @return The number of times aState has been observed, including the new 1.145 + * observation. 1.146 + */ 1.147 + checkStateChanged: function (aState, aExpectedMin, aExpectedMax) { 1.148 + print(aState + " state change observed"); 1.149 + if (!this.stateCounts.hasOwnProperty(aState)) 1.150 + this.stateCounts[aState] = 0; 1.151 + this.stateCounts[aState]++; 1.152 + return this.checkState(aState, aExpectedMin, aExpectedMax); 1.153 + }, 1.154 + 1.155 + /** 1.156 + * Ensures that the state has been observed the given number of times. 1.157 + * 1.158 + * @param aState 1.159 + * The name of the state. 1.160 + * @param aExpectedMin 1.161 + * The state must have been observed at least this number of times. 1.162 + * @param aExpectedMax 1.163 + * The state must have been observed at most this number of times. 1.164 + * This parameter is optional. If undefined, it's set to 1.165 + * aExpectedMin. 1.166 + * @return The number of times aState has been observed, including the new 1.167 + * observation. 1.168 + */ 1.169 + checkState: function (aState, aExpectedMin, aExpectedMax) { 1.170 + let cnt = this.stateCounts[aState] || 0; 1.171 + if (aExpectedMax === undefined) 1.172 + aExpectedMax = aExpectedMin; 1.173 + if (aExpectedMin === aExpectedMax) { 1.174 + print(aState + " should be observed only " + aExpectedMin + 1.175 + " times (actual = " + cnt + ")"); 1.176 + } 1.177 + else { 1.178 + print(aState + " should be observed at least " + aExpectedMin + 1.179 + " times and at most " + aExpectedMax + " times (actual = " + 1.180 + cnt + ")"); 1.181 + } 1.182 + do_check_true(cnt >= aExpectedMin && cnt <= aExpectedMax); 1.183 + return cnt; 1.184 + }, 1.185 + 1.186 + /** 1.187 + * Asynchronously opens the root of the test's result. 1.188 + */ 1.189 + openContainer: function () { 1.190 + // Set up the result observer. It delegates to this object's callbacks and 1.191 + // wraps them in a try-catch so that errors don't get eaten. 1.192 + this.observer = let (self = this) { 1.193 + containerStateChanged: function (container, oldState, newState) { 1.194 + print("New state passed to containerStateChanged() should equal the " + 1.195 + "container's current state"); 1.196 + do_check_eq(newState, container.state); 1.197 + 1.198 + try { 1.199 + switch (newState) { 1.200 + case Ci.nsINavHistoryContainerResultNode.STATE_LOADING: 1.201 + self.loading(container, newState, oldState); 1.202 + break; 1.203 + case Ci.nsINavHistoryContainerResultNode.STATE_OPENED: 1.204 + self.opened(container, newState, oldState); 1.205 + break; 1.206 + case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED: 1.207 + self.closed(container, newState, oldState); 1.208 + break; 1.209 + default: 1.210 + do_throw("Unexpected new state! " + newState); 1.211 + } 1.212 + } 1.213 + catch (err) { 1.214 + do_throw(err); 1.215 + } 1.216 + }, 1.217 + }; 1.218 + this.result.addObserver(this.observer, false); 1.219 + 1.220 + print("Opening container"); 1.221 + this.result.root.containerOpen = true; 1.222 + }, 1.223 + 1.224 + /** 1.225 + * Starts the test and returns a promise resolved when the test completes. 1.226 + */ 1.227 + run: function () { 1.228 + this.openContainer(); 1.229 + return this.deferNextTest.promise; 1.230 + }, 1.231 + 1.232 + /** 1.233 + * This must be called before run(). It adds a bookmark and sets up the 1.234 + * test's result. Override if need be. 1.235 + */ 1.236 + setup: function () { 1.237 + // Populate the database with different types of bookmark items. 1.238 + this.data = DataHelper.makeDataArray([ 1.239 + { type: "bookmark" }, 1.240 + { type: "separator" }, 1.241 + { type: "folder" }, 1.242 + { type: "bookmark", uri: "place:terms=foo" } 1.243 + ]); 1.244 + yield task_populateDB(this.data); 1.245 + 1.246 + // Make a query. 1.247 + this.query = PlacesUtils.history.getNewQuery(); 1.248 + this.query.setFolders([DataHelper.defaults.bookmark.parent], 1); 1.249 + this.opts = PlacesUtils.history.getNewQueryOptions(); 1.250 + this.opts.asyncEnabled = true; 1.251 + this.result = PlacesUtils.history.executeQuery(this.query, this.opts); 1.252 + }, 1.253 + 1.254 + /** 1.255 + * Call this when the test has succeeded. It cleans up resources and starts 1.256 + * the next test. 1.257 + */ 1.258 + success: function () { 1.259 + this.result.removeObserver(this.observer); 1.260 + 1.261 + // Resolve the promise object that indicates that the next test can be run. 1.262 + this.deferNextTest.resolve(); 1.263 + } 1.264 +}; 1.265 + 1.266 +/** 1.267 + * This makes it a little bit easier to use the functions of head_queries.js. 1.268 + */ 1.269 +let DataHelper = { 1.270 + defaults: { 1.271 + bookmark: { 1.272 + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, 1.273 + uri: "http://example.com/", 1.274 + title: "test bookmark" 1.275 + }, 1.276 + 1.277 + folder: { 1.278 + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, 1.279 + title: "test folder" 1.280 + }, 1.281 + 1.282 + separator: { 1.283 + parent: PlacesUtils.bookmarks.unfiledBookmarksFolder 1.284 + } 1.285 + }, 1.286 + 1.287 + /** 1.288 + * Converts an array of simple bookmark item descriptions to the more verbose 1.289 + * format required by task_populateDB() in head_queries.js. 1.290 + * 1.291 + * @param aData 1.292 + * An array of objects, each of which describes a bookmark item. 1.293 + * @return An array of objects suitable for passing to populateDB(). 1.294 + */ 1.295 + makeDataArray: function DH_makeDataArray(aData) { 1.296 + return let (self = this) aData.map(function (dat) { 1.297 + let type = dat.type; 1.298 + dat = self._makeDataWithDefaults(dat, self.defaults[type]); 1.299 + switch (type) { 1.300 + case "bookmark": 1.301 + return { 1.302 + isBookmark: true, 1.303 + uri: dat.uri, 1.304 + parentFolder: dat.parent, 1.305 + index: PlacesUtils.bookmarks.DEFAULT_INDEX, 1.306 + title: dat.title, 1.307 + isInQuery: true 1.308 + }; 1.309 + case "separator": 1.310 + return { 1.311 + isSeparator: true, 1.312 + parentFolder: dat.parent, 1.313 + index: PlacesUtils.bookmarks.DEFAULT_INDEX, 1.314 + isInQuery: true 1.315 + }; 1.316 + case "folder": 1.317 + return { 1.318 + isFolder: true, 1.319 + readOnly: false, 1.320 + parentFolder: dat.parent, 1.321 + index: PlacesUtils.bookmarks.DEFAULT_INDEX, 1.322 + title: dat.title, 1.323 + isInQuery: true 1.324 + }; 1.325 + default: 1.326 + do_throw("Unknown data type when populating DB: " + type); 1.327 + } 1.328 + }); 1.329 + }, 1.330 + 1.331 + /** 1.332 + * Returns a copy of aData, except that any properties that are undefined but 1.333 + * defined in aDefaults are set to the corresponding values in aDefaults. 1.334 + * 1.335 + * @param aData 1.336 + * An object describing a bookmark item. 1.337 + * @param aDefaults 1.338 + * An object describing the default bookmark item. 1.339 + * @return A copy of aData with defaults values set. 1.340 + */ 1.341 + _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) { 1.342 + let dat = {}; 1.343 + for (let [prop, val] in Iterator(aDefaults)) { 1.344 + dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val; 1.345 + } 1.346 + return dat; 1.347 + } 1.348 +}; 1.349 + 1.350 +function run_test() 1.351 +{ 1.352 + run_next_test(); 1.353 +} 1.354 + 1.355 +add_task(function test_async() 1.356 +{ 1.357 + for (let [, test] in Iterator(tests)) { 1.358 + remove_all_bookmarks(); 1.359 + 1.360 + test.__proto__ = new Test(); 1.361 + yield test.setup(); 1.362 + 1.363 + print("------ Running test: " + test.desc); 1.364 + yield test.run(); 1.365 + } 1.366 + 1.367 + remove_all_bookmarks(); 1.368 + print("All tests done, exiting"); 1.369 +});