summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/queries/test_async.js
blob: 32a5c1a69128ec25258025eb52cde33cd12f9d26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var tests = [
  {
    desc:
      "nsNavHistoryFolderResultNode: Basic test, asynchronously open and " +
      "close container with a single child",

    loading(node, newState, oldState) {
      this.checkStateChanged("loading", 1);
      this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
    },

    opened(node, newState, oldState) {
      this.checkStateChanged("opened", 1);
      this.checkState("loading", 1);
      this.checkArgs("opened", node, oldState, node.STATE_LOADING);

      print("Checking node children");
      compareArrayToResult(this.data, node);

      print("Closing container");
      node.containerOpen = false;
    },

    closed(node, newState, oldState) {
      this.checkStateChanged("closed", 1);
      this.checkState("opened", 1);
      this.checkArgs("closed", node, oldState, node.STATE_OPENED);
      this.success();
    },
  },

  {
    desc:
      "nsNavHistoryFolderResultNode: After async open and no changes, " +
      "second open should be synchronous",

    loading(node, newState, oldState) {
      this.checkStateChanged("loading", 1);
      this.checkState("closed", 0);
      this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
    },

    opened(node, newState, oldState) {
      let cnt = this.checkStateChanged("opened", 1, 2);
      let expectOldState = cnt === 1 ? node.STATE_LOADING : node.STATE_CLOSED;
      this.checkArgs("opened", node, oldState, expectOldState);

      print("Checking node children");
      compareArrayToResult(this.data, node);

      print("Closing container");
      node.containerOpen = false;
    },

    closed(node, newState, oldState) {
      let cnt = this.checkStateChanged("closed", 1, 2);
      this.checkArgs("closed", node, oldState, node.STATE_OPENED);

      switch (cnt) {
        case 1:
          node.containerOpen = true;
          break;
        case 2:
          this.success();
          break;
      }
    },
  },

  {
    desc:
      "nsNavHistoryFolderResultNode: After closing container in " +
      "loading(), opened() should not be called",

    loading(node, newState, oldState) {
      this.checkStateChanged("loading", 1);
      this.checkArgs("loading", node, oldState, node.STATE_CLOSED);
      print("Closing container");
      node.containerOpen = false;
    },

    opened() {
      do_throw("opened should not be called");
    },

    closed(node, newState, oldState) {
      this.checkStateChanged("closed", 1);
      this.checkState("loading", 1);
      this.checkArgs("closed", node, oldState, node.STATE_LOADING);
      this.success();
    },
  },
];

/**
 * Instances of this class become the prototypes of the test objects above.
 * Each test can therefore use the methods of this class, or they can override
 * them if they want.  To run a test, call setup() and then run().
 */
function Test() {
  // This maps a state name to the number of times it's been observed.
  this.stateCounts = {};
  // Promise object resolved when the next test can be run.
  this.deferNextTest = Promise.withResolvers();
}

Test.prototype = {
  /**
   * Call this when an observer observes a container state change to sanity
   * check the arguments.
   *
   * @param aNewState
   *        The name of the new state.  Used only for printing out helpful info.
   * @param aNode
   *        The node argument passed to containerStateChanged.
   * @param aOldState
   *        The old state argument passed to containerStateChanged.
   * @param aExpectOldState
   *        The expected old state.
   */
  checkArgs(aNewState, aNode, aOldState, aExpectOldState) {
    print("Node passed on " + aNewState + " should be result.root");
    Assert.equal(this.result.root, aNode);
    print("Old state passed on " + aNewState + " should be " + aExpectOldState);

    // aOldState comes from xpconnect and will therefore be defined.  It may be
    // zero, though, so use strict equality just to make sure aExpectOldState is
    // also defined.
    Assert.ok(aOldState === aExpectOldState);
  },

  /**
   * Call this when an observer observes a container state change.  It registers
   * the state change and ensures that it has been observed the given number
   * of times.  See checkState for parameter explanations.
   *
   * @return The number of times aState has been observed, including the new
   *         observation.
   */
  checkStateChanged(aState, aExpectedMin, aExpectedMax) {
    print(aState + " state change observed");
    if (!this.stateCounts.hasOwnProperty(aState)) {
      this.stateCounts[aState] = 0;
    }
    this.stateCounts[aState]++;
    return this.checkState(aState, aExpectedMin, aExpectedMax);
  },

  /**
   * Ensures that the state has been observed the given number of times.
   *
   * @param  aState
   *         The name of the state.
   * @param  aExpectedMin
   *         The state must have been observed at least this number of times.
   * @param  aExpectedMax
   *         The state must have been observed at most this number of times.
   *         This parameter is optional.  If undefined, it's set to
   *         aExpectedMin.
   * @return The number of times aState has been observed, including the new
   *         observation.
   */
  checkState(aState, aExpectedMin, aExpectedMax) {
    let cnt = this.stateCounts[aState] || 0;
    if (aExpectedMax === undefined) {
      aExpectedMax = aExpectedMin;
    }
    if (aExpectedMin === aExpectedMax) {
      print(
        aState +
          " should be observed only " +
          aExpectedMin +
          " times (actual = " +
          cnt +
          ")"
      );
    } else {
      print(
        aState +
          " should be observed at least " +
          aExpectedMin +
          " times and at most " +
          aExpectedMax +
          " times (actual = " +
          cnt +
          ")"
      );
    }
    Assert.ok(cnt >= aExpectedMin && cnt <= aExpectedMax);
    return cnt;
  },

  /**
   * Asynchronously opens the root of the test's result.
   */
  openContainer() {
    // Set up the result observer.  It delegates to this object's callbacks and
    // wraps them in a try-catch so that errors don't get eaten.
    let self = this;
    this.observer = {
      containerStateChanged(container, oldState, newState) {
        print(
          "New state passed to containerStateChanged() should equal the " +
            "container's current state"
        );
        Assert.equal(newState, container.state);

        try {
          switch (newState) {
            case Ci.nsINavHistoryContainerResultNode.STATE_LOADING:
              self.loading(container, newState, oldState);
              break;
            case Ci.nsINavHistoryContainerResultNode.STATE_OPENED:
              self.opened(container, newState, oldState);
              break;
            case Ci.nsINavHistoryContainerResultNode.STATE_CLOSED:
              self.closed(container, newState, oldState);
              break;
            default:
              do_throw("Unexpected new state! " + newState);
          }
        } catch (err) {
          do_throw(err);
        }
      },
    };
    this.result.addObserver(this.observer);

    print("Opening container");
    this.result.root.containerOpen = true;
  },

  /**
   * Starts the test and returns a promise resolved when the test completes.
   */
  run() {
    this.openContainer();
    return this.deferNextTest.promise;
  },

  /**
   * This must be called before run().  It adds a bookmark and sets up the
   * test's result.  Override if need be.
   */
  async setup() {
    // Populate the database with different types of bookmark items.
    this.data = DataHelper.makeDataArray([
      { type: "bookmark" },
      { type: "separator" },
      { type: "folder" },
      { type: "bookmark", uri: "place:terms=foo" },
    ]);
    await task_populateDB(this.data);

    // Make a query.
    this.query = PlacesUtils.history.getNewQuery();
    this.query.setParents([DataHelper.defaults.bookmark.parentGuid]);
    this.opts = PlacesUtils.history.getNewQueryOptions();
    this.opts.asyncEnabled = true;
    this.result = PlacesUtils.history.executeQuery(this.query, this.opts);
  },

  /**
   * Call this when the test has succeeded.  It cleans up resources and starts
   * the next test.
   */
  success() {
    this.result.removeObserver(this.observer);

    // Resolve the promise object that indicates that the next test can be run.
    this.deferNextTest.resolve();
  },
};

/**
 * This makes it a little bit easier to use the functions of head_queries.js.
 */
var DataHelper = {
  defaults: {
    bookmark: {
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      uri: "http://example.com/",
      title: "test bookmark",
    },

    folder: {
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
      title: "test folder",
    },

    separator: {
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
  },

  /**
   * Converts an array of simple bookmark item descriptions to the more verbose
   * format required by task_populateDB() in head_queries.js.
   *
   * @param  aData
   *         An array of objects, each of which describes a bookmark item.
   * @return An array of objects suitable for passing to populateDB().
   */
  makeDataArray: function DH_makeDataArray(aData) {
    let self = this;
    return aData.map(function (dat) {
      let type = dat.type;
      dat = self._makeDataWithDefaults(dat, self.defaults[type]);
      switch (type) {
        case "bookmark":
          return {
            isBookmark: true,
            uri: dat.uri,
            parentGuid: dat.parentGuid,
            index: PlacesUtils.bookmarks.DEFAULT_INDEX,
            title: dat.title,
            isInQuery: true,
          };
        case "separator":
          return {
            isSeparator: true,
            parentGuid: dat.parentGuid,
            index: PlacesUtils.bookmarks.DEFAULT_INDEX,
            isInQuery: true,
          };
        case "folder":
          return {
            isFolder: true,
            parentGuid: dat.parentGuid,
            index: PlacesUtils.bookmarks.DEFAULT_INDEX,
            title: dat.title,
            isInQuery: true,
          };
        default:
          do_throw("Unknown data type when populating DB: " + type);
          return undefined;
      }
    });
  },

  /**
   * Returns a copy of aData, except that any properties that are undefined but
   * defined in aDefaults are set to the corresponding values in aDefaults.
   *
   * @param  aData
   *         An object describing a bookmark item.
   * @param  aDefaults
   *         An object describing the default bookmark item.
   * @return A copy of aData with defaults values set.
   */
  _makeDataWithDefaults: function DH__makeDataWithDefaults(aData, aDefaults) {
    let dat = {};
    for (let [prop, val] of Object.entries(aDefaults)) {
      dat[prop] = aData.hasOwnProperty(prop) ? aData[prop] : val;
    }
    return dat;
  },
};

add_task(async function test_async() {
  for (let test of tests) {
    await PlacesUtils.bookmarks.eraseEverything();

    Object.setPrototypeOf(test, new Test());
    await test.setup();

    print("------ Running test: " + test.desc);
    await test.run();
  }

  await PlacesUtils.bookmarks.eraseEverything();
  print("All tests done, exiting");
});