|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim:set ts=2 sw=2 sts=2 et: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 let tests = [ |
|
8 { |
|
9 desc: "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " + |
|
10 "close container with a single child", |
|
11 |
|
12 loading: function (node, newState, oldState) { |
|
13 this.checkStateChanged("loading", 1); |
|
14 this.checkArgs("loading", node, oldState, node.STATE_CLOSED); |
|
15 }, |
|
16 |
|
17 opened: function (node, newState, oldState) { |
|
18 this.checkStateChanged("opened", 1); |
|
19 this.checkState("loading", 1); |
|
20 this.checkArgs("opened", node, oldState, node.STATE_LOADING); |
|
21 |
|
22 print("Checking node children"); |
|
23 compareArrayToResult(this.data, node); |
|
24 |
|
25 print("Closing container"); |
|
26 node.containerOpen = false; |
|
27 }, |
|
28 |
|
29 closed: function (node, newState, oldState) { |
|
30 this.checkStateChanged("closed", 1); |
|
31 this.checkState("opened", 1); |
|
32 this.checkArgs("closed", node, oldState, node.STATE_OPENED); |
|
33 this.success(); |
|
34 } |
|
35 }, |
|
36 |
|
37 { |
|
38 desc: "nsNavHistoryFolderResultNode: After async open and no changes, " + |
|
39 "second open should be synchronous", |
|
40 |
|
41 loading: function (node, newState, oldState) { |
|
42 this.checkStateChanged("loading", 1); |
|
43 this.checkState("closed", 0); |
|
44 this.checkArgs("loading", node, oldState, node.STATE_CLOSED); |
|
45 }, |
|
46 |
|
47 opened: function (node, newState, oldState) { |
|
48 let cnt = this.checkStateChanged("opened", 1, 2); |
|
49 let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED; |
|
50 this.checkArgs("opened", node, oldState, expectOldState); |
|
51 |
|
52 print("Checking node children"); |
|
53 compareArrayToResult(this.data, node); |
|
54 |
|
55 print("Closing container"); |
|
56 node.containerOpen = false; |
|
57 }, |
|
58 |
|
59 closed: function (node, newState, oldState) { |
|
60 let cnt = this.checkStateChanged("closed", 1, 2); |
|
61 this.checkArgs("closed", node, oldState, node.STATE_OPENED); |
|
62 |
|
63 switch (cnt) { |
|
64 case 1: |
|
65 node.containerOpen = true; |
|
66 break; |
|
67 case 2: |
|
68 this.success(); |
|
69 break; |
|
70 } |
|
71 } |
|
72 }, |
|
73 |
|
74 { |
|
75 desc: "nsNavHistoryFolderResultNode: After closing container in " + |
|
76 "loading(), opened() should not be called", |
|
77 |
|
78 loading: function (node, newState, oldState) { |
|
79 this.checkStateChanged("loading", 1); |
|
80 this.checkArgs("loading", node, oldState, node.STATE_CLOSED); |
|
81 print("Closing container"); |
|
82 node.containerOpen = false; |
|
83 }, |
|
84 |
|
85 opened: function (node, newState, oldState) { |
|
86 do_throw("opened should not be called"); |
|
87 }, |
|
88 |
|
89 closed: function (node, newState, oldState) { |
|
90 this.checkStateChanged("closed", 1); |
|
91 this.checkState("loading", 1); |
|
92 this.checkArgs("closed", node, oldState, node.STATE_LOADING); |
|
93 this.success(); |
|
94 } |
|
95 } |
|
96 ]; |
|
97 |
|
98 |
|
99 /** |
|
100 * Instances of this class become the prototypes of the test objects above. |
|
101 * Each test can therefore use the methods of this class, or they can override |
|
102 * them if they want. To run a test, call setup() and then run(). |
|
103 */ |
|
104 function Test() { |
|
105 // This maps a state name to the number of times it's been observed. |
|
106 this.stateCounts = {}; |
|
107 // Promise object resolved when the next test can be run. |
|
108 this.deferNextTest = Promise.defer(); |
|
109 } |
|
110 |
|
111 Test.prototype = { |
|
112 /** |
|
113 * Call this when an observer observes a container state change to sanity |
|
114 * check the arguments. |
|
115 * |
|
116 * @param aNewState |
|
117 * The name of the new state. Used only for printing out helpful info. |
|
118 * @param aNode |
|
119 * The node argument passed to containerStateChanged. |
|
120 * @param aOldState |
|
121 * The old state argument passed to containerStateChanged. |
|
122 * @param aExpectOldState |
|
123 * The expected old state. |
|
124 */ |
|
125 checkArgs: function (aNewState, aNode, aOldState, aExpectOldState) { |
|
126 print("Node passed on " + aNewState + " should be result.root"); |
|
127 do_check_eq(this.result.root, aNode); |
|
128 print("Old state passed on " + aNewState + " should be " + aExpectOldState); |
|
129 |
|
130 // aOldState comes from xpconnect and will therefore be defined. It may be |
|
131 // zero, though, so use strict equality just to make sure aExpectOldState is |
|
132 // also defined. |
|
133 do_check_true(aOldState === aExpectOldState); |
|
134 }, |
|
135 |
|
136 /** |
|
137 * Call this when an observer observes a container state change. It registers |
|
138 * the state change and ensures that it has been observed the given number |
|
139 * of times. See checkState for parameter explanations. |
|
140 * |
|
141 * @return The number of times aState has been observed, including the new |
|
142 * observation. |
|
143 */ |
|
144 checkStateChanged: function (aState, aExpectedMin, aExpectedMax) { |
|
145 print(aState + " state change observed"); |
|
146 if (!this.stateCounts.hasOwnProperty(aState)) |
|
147 this.stateCounts[aState] = 0; |
|
148 this.stateCounts[aState]++; |
|
149 return this.checkState(aState, aExpectedMin, aExpectedMax); |
|
150 }, |
|
151 |
|
152 /** |
|
153 * Ensures that the state has been observed the given number of times. |
|
154 * |
|
155 * @param aState |
|
156 * The name of the state. |
|
157 * @param aExpectedMin |
|
158 * The state must have been observed at least this number of times. |
|
159 * @param aExpectedMax |
|
160 * The state must have been observed at most this number of times. |
|
161 * This parameter is optional. If undefined, it's set to |
|
162 * aExpectedMin. |
|
163 * @return The number of times aState has been observed, including the new |
|
164 * observation. |
|
165 */ |
|
166 checkState: function (aState, aExpectedMin, aExpectedMax) { |
|
167 let cnt = this.stateCounts[aState] || 0; |
|
168 if (aExpectedMax === undefined) |
|
169 aExpectedMax = aExpectedMin; |
|
170 if (aExpectedMin === aExpectedMax) { |
|
171 print(aState + " should be observed only " + aExpectedMin + |
|
172 " times (actual = " + cnt + ")"); |
|
173 } |
|
174 else { |
|
175 print(aState + " should be observed at least " + aExpectedMin + |
|
176 " times and at most " + aExpectedMax + " times (actual = " + |
|
177 cnt + ")"); |
|
178 } |
|
179 do_check_true(cnt >= aExpectedMin && cnt <= aExpectedMax); |
|
180 return cnt; |
|
181 }, |
|
182 |
|
183 /** |
|
184 * Asynchronously opens the root of the test's result. |
|
185 */ |
|
186 openContainer: function () { |
|
187 // Set up the result observer. It delegates to this object's callbacks and |
|
188 // wraps them in a try-catch so that errors don't get eaten. |
|
189 this.observer = let (self = this) { |
|
190 containerStateChanged: function (container, oldState, newState) { |
|
191 print("New state passed to containerStateChanged() should equal the " + |
|
192 "container's current state"); |
|
193 do_check_eq(newState, container.state); |
|
194 |
|
195 try { |
|
196 switch (newState) { |
|
197 case Ci.nsINavHistoryContainerResultNode.STATE_LOADING: |
|
198 self.loading(container, newState, oldState); |
|
199 break; |
|
200 case Ci.nsINavHistoryContainerResultNode.STATE_OPENED: |
|
201 self.opened(container, newState, oldState); |
|
202 break; |
|
203 case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED: |
|
204 self.closed(container, newState, oldState); |
|
205 break; |
|
206 default: |
|
207 do_throw("Unexpected new state! " + newState); |
|
208 } |
|
209 } |
|
210 catch (err) { |
|
211 do_throw(err); |
|
212 } |
|
213 }, |
|
214 }; |
|
215 this.result.addObserver(this.observer, false); |
|
216 |
|
217 print("Opening container"); |
|
218 this.result.root.containerOpen = true; |
|
219 }, |
|
220 |
|
221 /** |
|
222 * Starts the test and returns a promise resolved when the test completes. |
|
223 */ |
|
224 run: function () { |
|
225 this.openContainer(); |
|
226 return this.deferNextTest.promise; |
|
227 }, |
|
228 |
|
229 /** |
|
230 * This must be called before run(). It adds a bookmark and sets up the |
|
231 * test's result. Override if need be. |
|
232 */ |
|
233 setup: function () { |
|
234 // Populate the database with different types of bookmark items. |
|
235 this.data = DataHelper.makeDataArray([ |
|
236 { type: "bookmark" }, |
|
237 { type: "separator" }, |
|
238 { type: "folder" }, |
|
239 { type: "bookmark", uri: "place:terms=foo" } |
|
240 ]); |
|
241 yield task_populateDB(this.data); |
|
242 |
|
243 // Make a query. |
|
244 this.query = PlacesUtils.history.getNewQuery(); |
|
245 this.query.setFolders([DataHelper.defaults.bookmark.parent], 1); |
|
246 this.opts = PlacesUtils.history.getNewQueryOptions(); |
|
247 this.opts.asyncEnabled = true; |
|
248 this.result = PlacesUtils.history.executeQuery(this.query, this.opts); |
|
249 }, |
|
250 |
|
251 /** |
|
252 * Call this when the test has succeeded. It cleans up resources and starts |
|
253 * the next test. |
|
254 */ |
|
255 success: function () { |
|
256 this.result.removeObserver(this.observer); |
|
257 |
|
258 // Resolve the promise object that indicates that the next test can be run. |
|
259 this.deferNextTest.resolve(); |
|
260 } |
|
261 }; |
|
262 |
|
263 /** |
|
264 * This makes it a little bit easier to use the functions of head_queries.js. |
|
265 */ |
|
266 let DataHelper = { |
|
267 defaults: { |
|
268 bookmark: { |
|
269 parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, |
|
270 uri: "http://example.com/", |
|
271 title: "test bookmark" |
|
272 }, |
|
273 |
|
274 folder: { |
|
275 parent: PlacesUtils.bookmarks.unfiledBookmarksFolder, |
|
276 title: "test folder" |
|
277 }, |
|
278 |
|
279 separator: { |
|
280 parent: PlacesUtils.bookmarks.unfiledBookmarksFolder |
|
281 } |
|
282 }, |
|
283 |
|
284 /** |
|
285 * Converts an array of simple bookmark item descriptions to the more verbose |
|
286 * format required by task_populateDB() in head_queries.js. |
|
287 * |
|
288 * @param aData |
|
289 * An array of objects, each of which describes a bookmark item. |
|
290 * @return An array of objects suitable for passing to populateDB(). |
|
291 */ |
|
292 makeDataArray: function DH_makeDataArray(aData) { |
|
293 return let (self = this) aData.map(function (dat) { |
|
294 let type = dat.type; |
|
295 dat = self._makeDataWithDefaults(dat, self.defaults[type]); |
|
296 switch (type) { |
|
297 case "bookmark": |
|
298 return { |
|
299 isBookmark: true, |
|
300 uri: dat.uri, |
|
301 parentFolder: dat.parent, |
|
302 index: PlacesUtils.bookmarks.DEFAULT_INDEX, |
|
303 title: dat.title, |
|
304 isInQuery: true |
|
305 }; |
|
306 case "separator": |
|
307 return { |
|
308 isSeparator: true, |
|
309 parentFolder: dat.parent, |
|
310 index: PlacesUtils.bookmarks.DEFAULT_INDEX, |
|
311 isInQuery: true |
|
312 }; |
|
313 case "folder": |
|
314 return { |
|
315 isFolder: true, |
|
316 readOnly: false, |
|
317 parentFolder: dat.parent, |
|
318 index: PlacesUtils.bookmarks.DEFAULT_INDEX, |
|
319 title: dat.title, |
|
320 isInQuery: true |
|
321 }; |
|
322 default: |
|
323 do_throw("Unknown data type when populating DB: " + type); |
|
324 } |
|
325 }); |
|
326 }, |
|
327 |
|
328 /** |
|
329 * Returns a copy of aData, except that any properties that are undefined but |
|
330 * defined in aDefaults are set to the corresponding values in aDefaults. |
|
331 * |
|
332 * @param aData |
|
333 * An object describing a bookmark item. |
|
334 * @param aDefaults |
|
335 * An object describing the default bookmark item. |
|
336 * @return A copy of aData with defaults values set. |
|
337 */ |
|
338 _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) { |
|
339 let dat = {}; |
|
340 for (let [prop, val] in Iterator(aDefaults)) { |
|
341 dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val; |
|
342 } |
|
343 return dat; |
|
344 } |
|
345 }; |
|
346 |
|
347 function run_test() |
|
348 { |
|
349 run_next_test(); |
|
350 } |
|
351 |
|
352 add_task(function test_async() |
|
353 { |
|
354 for (let [, test] in Iterator(tests)) { |
|
355 remove_all_bookmarks(); |
|
356 |
|
357 test.__proto__ = new Test(); |
|
358 yield test.setup(); |
|
359 |
|
360 print("------ Running test: " + test.desc); |
|
361 yield test.run(); |
|
362 } |
|
363 |
|
364 remove_all_bookmarks(); |
|
365 print("All tests done, exiting"); |
|
366 }); |