summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/queries/test_redirects.js
blob: 6b122e3180042ee2c1d79baf0f0d70dc1935aa92 (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
/* 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/. */

// Array of visits we will add to the database, will be populated later
// in the test.
var visits = [];

/**
 * Takes a sequence of query options, and compare query results obtained through
 * them with a custom filtered array of visits, based on the values we are
 * expecting from the query.
 *
 * @param  aSequence
 *         an array that contains query options in the form:
 *         [includeHidden, maxResults, sortingMode]
 */
function check_results_callback(aSequence) {
  // Sanity check: we should receive 3 parameters.
  Assert.equal(aSequence.length, 3);
  let includeHidden = aSequence[0];
  let maxResults = aSequence[1];
  let sortingMode = aSequence[2];
  info(" - - - ");
  info(
    "TESTING: includeHidden(" +
      includeHidden +
      ")," +
      " maxResults(" +
      maxResults +
      ")," +
      " sortingMode(" +
      sortingMode +
      ")."
  );

  function isHidden(aVisit) {
    return (
      aVisit.transType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
      aVisit.isRedirect
    );
  }

  // Build expectedData array.
  let expectedData = visits.filter(function (aVisit) {
    // Embed visits never appear in results.
    if (aVisit.transType == Ci.nsINavHistoryService.TRANSITION_EMBED) {
      return false;
    }

    if (!includeHidden && isHidden(aVisit)) {
      // If the page has any non-hidden visit, then it's visible.
      if (
        !visits.filter(function (refVisit) {
          return refVisit.uri == aVisit.uri && !isHidden(refVisit);
        }).length
      ) {
        return false;
      }
    }

    return true;
  });

  // Remove duplicates, since queries are RESULTS_AS_URI (unique pages).
  let seen = [];
  expectedData = expectedData.filter(function (aData) {
    if (seen.includes(aData.uri)) {
      return false;
    }
    seen.push(aData.uri);
    return true;
  });

  // Sort expectedData.
  function getFirstIndexFor(aEntry) {
    for (let i = 0; i < visits.length; i++) {
      if (visits[i].uri == aEntry.uri) {
        return i;
      }
    }
    return undefined;
  }
  function comparator(a, b) {
    if (sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING) {
      return b.lastVisit - a.lastVisit;
    }
    if (
      sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING
    ) {
      return b.visitCount - a.visitCount;
    }
    return getFirstIndexFor(a) - getFirstIndexFor(b);
  }
  expectedData.sort(comparator);

  // Crop results to maxResults if it's defined.
  if (maxResults) {
    expectedData = expectedData.slice(0, maxResults);
  }

  // Create a new query with required options.
  let query = PlacesUtils.history.getNewQuery();
  let options = PlacesUtils.history.getNewQueryOptions();
  options.includeHidden = includeHidden;
  options.sortingMode = sortingMode;
  if (maxResults) {
    options.maxResults = maxResults;
  }

  // Compare resultset with expectedData.
  let result = PlacesUtils.history.executeQuery(query, options);
  let root = result.root;
  root.containerOpen = true;
  compareArrayToResult(expectedData, root);
  root.containerOpen = false;
}

/**
 * Enumerates all the sequences of the cartesian product of the arrays contained
 * in aSequences.  Examples:
 *
 *   cartProd([[1, 2, 3], ["a", "b"]], callback);
 *   // callback is called 3 * 2 = 6 times with the following arrays:
 *   // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
 *
 *   cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
 *   // callback is called 1 * 3 * 2 = 6 times with the following arrays:
 *   // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
 *   // ["a", 3, "X"], ["a", 3, "Y"]
 *
 *   cartProd([[1], [2], [3], [4]], callback);
 *   // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
 *   // [1, 2, 3, 4]
 *
 *   cartProd([], callback);
 *   // callback is 0 times
 *
 *   cartProd([[1, 2, 3, 4]], callback);
 *   // callback is called 4 times with the following arrays:
 *   // [1], [2], [3], [4]
 *
 * @param  aSequences
 *         an array that contains an arbitrary number of arrays
 * @param  aCallback
 *         a function that is passed each sequence of the product as it's
 *         computed
 * @return the total number of sequences in the product
 */
function cartProd(aSequences, aCallback) {
  if (aSequences.length === 0) {
    return 0;
  }

  // For each sequence in aSequences, we maintain a pointer (an array index,
  // really) to the element we're currently enumerating in that sequence
  let seqEltPtrs = aSequences.map(() => 0);

  let numProds = 0;
  let done = false;
  while (!done) {
    numProds++;

    // prod = sequence in product we're currently enumerating
    let prod = [];
    for (let i = 0; i < aSequences.length; i++) {
      prod.push(aSequences[i][seqEltPtrs[i]]);
    }
    aCallback(prod);

    // The next sequence in the product differs from the current one by just a
    // single element.  Determine which element that is.  We advance the
    // "rightmost" element pointer to the "right" by one.  If we move past the
    // end of that pointer's sequence, reset the pointer to the first element
    // in its sequence and then try the sequence to the "left", and so on.

    // seqPtr = index of rightmost input sequence whose element pointer is not
    // past the end of the sequence
    let seqPtr = aSequences.length - 1;
    while (!done) {
      // Advance the rightmost element pointer.
      seqEltPtrs[seqPtr]++;

      // The rightmost element pointer is past the end of its sequence.
      if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
        seqEltPtrs[seqPtr] = 0;
        seqPtr--;

        // All element pointers are past the ends of their sequences.
        if (seqPtr < 0) {
          done = true;
        }
      } else {
        break;
      }
    }
  }
  return numProds;
}

/**
 * Populate the visits array and add visits to the database.
 * We will generate visit-chains like:
 *   visit -> redirect_temp -> redirect_perm
 */
add_task(async function test_add_visits_to_database() {
  await PlacesUtils.bookmarks.eraseEverything();

  // We don't really bother on this, but we need a time to add visits.
  let timeInMicroseconds = Date.now() * 1000;
  let visitCount = 1;

  // Array of all possible transition types we could be redirected from.
  let t = [
    Ci.nsINavHistoryService.TRANSITION_LINK,
    Ci.nsINavHistoryService.TRANSITION_TYPED,
    Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
    // Embed visits are not added to the database and we don't want redirects
    // to them, thus just avoid addition.
    // Ci.nsINavHistoryService.TRANSITION_EMBED,
    Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
    // Would make hard sorting by visit date because last_visit_date is actually
    // calculated excluding download transitions, but the query includes
    // downloads.
    // Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
  ];

  function newTimeInMicroseconds() {
    timeInMicroseconds = timeInMicroseconds - 1000;
    return timeInMicroseconds;
  }

  // we add a visit for each of the above transition types.
  t.forEach(transition =>
    visits.push({
      isVisit: true,
      transType: transition,
      uri: "http://" + transition + ".example.com/",
      title: transition + "-example",
      isRedirect: true,
      lastVisit: newTimeInMicroseconds(),
      visitCount:
        transition == Ci.nsINavHistoryService.TRANSITION_EMBED ||
        transition == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK
          ? 0
          : visitCount++,
      isInQuery: true,
    })
  );

  // Add a REDIRECT_TEMPORARY layer of visits for each of the above visits.
  t.forEach(transition =>
    visits.push({
      isVisit: true,
      transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
      uri: "http://" + transition + ".redirect.temp.example.com/",
      title: transition + "-redirect-temp-example",
      lastVisit: newTimeInMicroseconds(),
      isRedirect: true,
      referrer: "http://" + transition + ".example.com/",
      visitCount: visitCount++,
      isInQuery: true,
    })
  );

  // Add a REDIRECT_PERMANENT layer of visits for each of the above redirects.
  t.forEach(transition =>
    visits.push({
      isVisit: true,
      transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
      uri: "http://" + transition + ".redirect.perm.example.com/",
      title: transition + "-redirect-perm-example",
      lastVisit: newTimeInMicroseconds(),
      isRedirect: true,
      referrer: "http://" + transition + ".redirect.temp.example.com/",
      visitCount: visitCount++,
      isInQuery: true,
    })
  );

  // Add a REDIRECT_PERMANENT layer of visits that loop to the first visit.
  // These entries should not change visitCount or lastVisit, otherwise
  // guessing an order would be a nightmare.
  function getLastValue(aURI, aProperty) {
    for (let i = 0; i < visits.length; i++) {
      if (visits[i].uri == aURI) {
        return visits[i][aProperty];
      }
    }
    do_throw("Unknown uri.");
    return null;
  }
  t.forEach(transition =>
    visits.push({
      isVisit: true,
      transType: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
      uri: "http://" + transition + ".example.com/",
      title: getLastValue("http://" + transition + ".example.com/", "title"),
      lastVisit: getLastValue(
        "http://" + transition + ".example.com/",
        "lastVisit"
      ),
      isRedirect: true,
      referrer: "http://" + transition + ".redirect.perm.example.com/",
      visitCount: getLastValue(
        "http://" + transition + ".example.com/",
        "visitCount"
      ),
      isInQuery: true,
    })
  );

  // Add an unvisited bookmark in the database, it should never appear.
  visits.push({
    isBookmark: true,
    uri: "http://unvisited.bookmark.com/",
    parentGuid: PlacesUtils.bookmarks.menuGuid,
    index: PlacesUtils.bookmarks.DEFAULT_INDEX,
    title: "Unvisited Bookmark",
    isInQuery: false,
  });

  // Put visits in the database.
  await task_populateDB(visits);
});

add_task(async function test_redirects() {
  // Frecency and hidden are updated asynchronously, wait for them.
  await PlacesTestUtils.promiseAsyncUpdates();

  // This array will be used by cartProd to generate a matrix of all possible
  // combinations.
  let includeHidden_options = [true, false];
  let maxResults_options = [5, 10, 20, null];
  // These sortingMode are choosen to toggle using special queries for history
  // menu.
  let sorting_options = [
    Ci.nsINavHistoryQueryOptions.SORT_BY_NONE,
    Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
    Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
  ];
  // Will execute check_results_callback() for each generated combination.
  cartProd(
    [includeHidden_options, maxResults_options, sorting_options],
    check_results_callback
  );

  await PlacesUtils.bookmarks.eraseEverything();

  await PlacesUtils.history.clear();
});