Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
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/. */
7 /**
8 * Tests the sanitize dialog (a.k.a. the clear recent history dialog).
9 * See bug 480169.
10 *
11 * The purpose of this test is not to fully flex the sanitize timespan code;
12 * browser/base/content/test/general/browser_sanitize-timespans.js does that. This
13 * test checks the UI of the dialog and makes sure it's correctly connected to
14 * the sanitize timespan code.
15 *
16 * Some of this code, especially the history creation parts, was taken from
17 * browser/base/content/test/general/browser_sanitize-timespans.js.
18 */
20 Cc["@mozilla.org/moz/jssubscript-loader;1"].
21 getService(Ci.mozIJSSubScriptLoader).
22 loadSubScript("chrome://browser/content/sanitize.js");
24 const dm = Cc["@mozilla.org/download-manager;1"].
25 getService(Ci.nsIDownloadManager);
26 const formhist = Cc["@mozilla.org/satchel/form-history;1"].
27 getService(Ci.nsIFormHistory2);
29 // Add tests here. Each is a function that's called by doNextTest().
30 var gAllTests = [
32 /**
33 * Moves the grippy around, makes sure it works OK.
34 */
35 function () {
36 // Add history (within the past hour) to get some rows in the tree.
37 let uris = [];
38 let places = [];
39 let pURI;
40 for (let i = 0; i < 30; i++) {
41 pURI = makeURI("http://" + i + "-minutes-ago.com/");
42 places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
43 uris.push(pURI);
44 }
46 addVisits(places, function() {
47 // Open the dialog and do our tests.
48 openWindow(function (aWin) {
49 let wh = new WindowHelper(aWin);
50 wh.selectDuration(Sanitizer.TIMESPAN_HOUR);
51 wh.checkGrippy("Grippy should be at last row after selecting HOUR " +
52 "duration",
53 wh.getRowCount() - 1);
55 // Move the grippy around.
56 let row = wh.getGrippyRow();
57 while (row !== 0) {
58 row--;
59 wh.moveGrippyBy(-1);
60 wh.checkGrippy("Grippy should be moved up one row", row);
61 }
62 wh.moveGrippyBy(-1);
63 wh.checkGrippy("Grippy should remain at first row after trying to move " +
64 "it up",
65 0);
66 while (row !== wh.getRowCount() - 1) {
67 row++;
68 wh.moveGrippyBy(1);
69 wh.checkGrippy("Grippy should be moved down one row", row);
70 }
71 wh.moveGrippyBy(1);
72 wh.checkGrippy("Grippy should remain at last row after trying to move " +
73 "it down",
74 wh.getRowCount() - 1);
76 // Cancel the dialog, make sure history visits are not cleared.
77 wh.checkPrefCheckbox("history", false);
79 wh.cancelDialog();
80 yield promiseHistoryClearedState(uris, false);
82 // OK, done, cleanup after ourselves.
83 blankSlate();
84 yield promiseHistoryClearedState(uris, true);
85 });
86 });
87 },
89 /**
90 * Ensures that the combined history-downloads checkbox clears both history
91 * visits and downloads when checked; the dialog respects simple timespan.
92 */
93 function () {
94 // Add history (within the past hour).
95 let uris = [];
96 let places = [];
97 let pURI;
98 for (let i = 0; i < 30; i++) {
99 pURI = makeURI("http://" + i + "-minutes-ago.com/");
100 places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
101 uris.push(pURI);
102 }
103 // Add history (over an hour ago).
104 let olderURIs = [];
105 for (let i = 0; i < 5; i++) {
106 pURI = makeURI("http://" + (60 + i) + "-minutes-ago.com/");
107 places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(60 + i)});
108 olderURIs.push(pURI);
109 }
111 addVisits(places, function() {
112 // Add downloads (within the past hour).
113 let downloadIDs = [];
114 for (let i = 0; i < 5; i++) {
115 downloadIDs.push(addDownloadWithMinutesAgo(i));
116 }
117 // Add downloads (over an hour ago).
118 let olderDownloadIDs = [];
119 for (let i = 0; i < 5; i++) {
120 olderDownloadIDs.push(addDownloadWithMinutesAgo(61 + i));
121 }
122 let totalHistoryVisits = uris.length + olderURIs.length;
124 // Open the dialog and do our tests.
125 openWindow(function (aWin) {
126 let wh = new WindowHelper(aWin);
127 wh.selectDuration(Sanitizer.TIMESPAN_HOUR);
128 wh.checkGrippy("Grippy should be at proper row after selecting HOUR " +
129 "duration",
130 uris.length);
132 // Accept the dialog, make sure history visits and downloads within one
133 // hour are cleared.
134 wh.checkPrefCheckbox("history", true);
135 wh.acceptDialog();
136 yield promiseHistoryClearedState(uris, true);
137 ensureDownloadsClearedState(downloadIDs, true);
139 // Make sure visits and downloads > 1 hour still exist.
140 yield promiseHistoryClearedState(olderURIs, false);
141 ensureDownloadsClearedState(olderDownloadIDs, false);
143 // OK, done, cleanup after ourselves.
144 blankSlate();
145 yield promiseHistoryClearedState(olderURIs, true);
146 ensureDownloadsClearedState(olderDownloadIDs, true);
147 });
148 });
149 },
151 /**
152 * Ensures that the combined history-downloads checkbox removes neither
153 * history visits nor downloads when not checked.
154 */
155 function () {
156 // Add history, downloads, form entries (within the past hour).
157 let uris = [];
158 let places = [];
159 let pURI;
160 for (let i = 0; i < 5; i++) {
161 pURI = makeURI("http://" + i + "-minutes-ago.com/");
162 places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
163 uris.push(pURI);
164 }
166 addVisits(places, function() {
167 let downloadIDs = [];
168 for (let i = 0; i < 5; i++) {
169 downloadIDs.push(addDownloadWithMinutesAgo(i));
170 }
171 let formEntries = [];
172 for (let i = 0; i < 5; i++) {
173 formEntries.push(addFormEntryWithMinutesAgo(i));
174 }
176 // Open the dialog and do our tests.
177 openWindow(function (aWin) {
178 let wh = new WindowHelper(aWin);
179 wh.selectDuration(Sanitizer.TIMESPAN_HOUR);
180 wh.checkGrippy("Grippy should be at last row after selecting HOUR " +
181 "duration",
182 wh.getRowCount() - 1);
184 // Remove only form entries, leave history (including downloads).
185 wh.checkPrefCheckbox("history", false);
186 wh.checkPrefCheckbox("formdata", true);
187 wh.acceptDialog();
189 // Of the three only form entries should be cleared.
190 yield promiseHistoryClearedState(uris, false);
191 ensureDownloadsClearedState(downloadIDs, false);
192 ensureFormEntriesClearedState(formEntries, true);
194 // OK, done, cleanup after ourselves.
195 blankSlate();
196 yield promiseHistoryClearedState(uris, true);
197 ensureDownloadsClearedState(downloadIDs, true);
198 });
199 });
200 },
202 /**
203 * Ensures that the "Everything" duration option works.
204 */
205 function () {
206 // Add history.
207 let uris = [];
208 let places = [];
209 let pURI;
210 // within past hour, within past two hours, within past four hours and
211 // outside past four hours
212 [10, 70, 130, 250].forEach(function(aValue) {
213 pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
214 places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
215 uris.push(pURI);
216 });
217 addVisits(places, function() {
219 // Open the dialog and do our tests.
220 openWindow(function (aWin) {
221 let wh = new WindowHelper(aWin);
222 wh.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
223 wh.checkPrefCheckbox("history", true);
224 wh.acceptDialog();
225 yield promiseHistoryClearedState(uris, true);
226 });
227 });
228 }
229 ];
231 // Used as the download database ID for a new download. Incremented for each
232 // new download. See addDownloadWithMinutesAgo().
233 var gDownloadId = 5555551;
235 // Index in gAllTests of the test currently being run. Incremented for each
236 // test run. See doNextTest().
237 var gCurrTest = 0;
239 var now_uSec = Date.now() * 1000;
241 ///////////////////////////////////////////////////////////////////////////////
243 /**
244 * This wraps the dialog and provides some convenience methods for interacting
245 * with it.
246 *
247 * A warning: Before you call any function that uses the tree (or any function
248 * that calls a function that uses the tree), you must set a non-everything
249 * duration by calling selectDuration(). The dialog does not initialize the
250 * tree if it does not yet need to be shown.
251 *
252 * @param aWin
253 * The dialog's nsIDOMWindow
254 */
255 function WindowHelper(aWin) {
256 this.win = aWin;
257 }
259 WindowHelper.prototype = {
260 /**
261 * "Presses" the dialog's OK button.
262 */
263 acceptDialog: function () {
264 is(this.win.document.documentElement.getButton("accept").disabled, false,
265 "Dialog's OK button should not be disabled");
266 this.win.document.documentElement.acceptDialog();
267 },
269 /**
270 * "Presses" the dialog's Cancel button.
271 */
272 cancelDialog: function () {
273 this.win.document.documentElement.cancelDialog();
274 },
276 /**
277 * Ensures that the grippy row is in the right place, tree selection is OK,
278 * and that the grippy's visible.
279 *
280 * @param aMsg
281 * Passed to is() when checking grippy location
282 * @param aExpectedRow
283 * The row that the grippy should be at
284 */
285 checkGrippy: function (aMsg, aExpectedRow) {
286 is(this.getGrippyRow(), aExpectedRow, aMsg);
287 this.checkTreeSelection();
288 this.ensureGrippyIsVisible();
289 },
291 /**
292 * (Un)checks a history scope checkbox (browser & download history,
293 * form history, etc.).
294 *
295 * @param aPrefName
296 * The final portion of the checkbox's privacy.cpd.* preference name
297 * @param aCheckState
298 * True if the checkbox should be checked, false otherwise
299 */
300 checkPrefCheckbox: function (aPrefName, aCheckState) {
301 var pref = "privacy.cpd." + aPrefName;
302 var cb = this.win.document.querySelectorAll(
303 "#itemList > [preference='" + pref + "']");
304 is(cb.length, 1, "found checkbox for " + pref + " preference");
305 if (cb[0].checked != aCheckState)
306 cb[0].click();
307 },
309 /**
310 * Ensures that the tree selection is appropriate to the grippy row. (A
311 * single, contiguous selection should exist from the first row all the way
312 * to the grippy.)
313 */
314 checkTreeSelection: function () {
315 let grippyRow = this.getGrippyRow();
316 let sel = this.getTree().view.selection;
317 if (grippyRow === 0) {
318 is(sel.getRangeCount(), 0,
319 "Grippy row is 0, so no tree selection should exist");
320 }
321 else {
322 is(sel.getRangeCount(), 1,
323 "Grippy row > 0, so only one tree selection range should exist");
324 let min = {};
325 let max = {};
326 sel.getRangeAt(0, min, max);
327 is(min.value, 0, "Tree selection should start at first row");
328 is(max.value, grippyRow - 1,
329 "Tree selection should end at row before grippy");
330 }
331 },
333 /**
334 * The grippy should always be visible when it's moved directly. This method
335 * ensures that.
336 */
337 ensureGrippyIsVisible: function () {
338 let tbo = this.getTree().treeBoxObject;
339 let firstVis = tbo.getFirstVisibleRow();
340 let lastVis = tbo.getLastVisibleRow();
341 let grippyRow = this.getGrippyRow();
342 ok(firstVis <= grippyRow && grippyRow <= lastVis,
343 "Grippy row should be visible; this inequality should be true: " +
344 firstVis + " <= " + grippyRow + " <= " + lastVis);
345 },
347 /**
348 * @return The dialog's duration dropdown
349 */
350 getDurationDropdown: function () {
351 return this.win.document.getElementById("sanitizeDurationChoice");
352 },
354 /**
355 * @return The grippy row index
356 */
357 getGrippyRow: function () {
358 return this.win.gContiguousSelectionTreeHelper.getGrippyRow();
359 },
361 /**
362 * @return The tree's row count (includes the grippy row)
363 */
364 getRowCount: function () {
365 return this.getTree().view.rowCount;
366 },
368 /**
369 * @return The tree
370 */
371 getTree: function () {
372 return this.win.gContiguousSelectionTreeHelper.tree;
373 },
375 /**
376 * @return True if the "Everything" warning panel is visible (as opposed to
377 * the tree)
378 */
379 isWarningPanelVisible: function () {
380 return this.win.document.getElementById("durationDeck").selectedIndex == 1;
381 },
383 /**
384 * @return True if the tree is visible (as opposed to the warning panel)
385 */
386 isTreeVisible: function () {
387 return this.win.document.getElementById("durationDeck").selectedIndex == 0;
388 },
390 /**
391 * Moves the grippy one row at a time in the direction and magnitude specified.
392 * If aDelta < 0, moves the grippy up; if aDelta > 0, moves it down.
393 *
394 * @param aDelta
395 * The amount and direction to move
396 */
397 moveGrippyBy: function (aDelta) {
398 if (aDelta === 0)
399 return;
400 let key = aDelta < 0 ? "UP" : "DOWN";
401 let abs = Math.abs(aDelta);
402 let treechildren = this.getTree().treeBoxObject.treeBody;
403 treechildren.focus();
404 for (let i = 0; i < abs; i++) {
405 EventUtils.sendKey(key);
406 }
407 },
409 /**
410 * Selects a duration in the duration dropdown.
411 *
412 * @param aDurVal
413 * One of the Sanitizer.TIMESPAN_* values
414 */
415 selectDuration: function (aDurVal) {
416 this.getDurationDropdown().value = aDurVal;
417 if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) {
418 is(this.isTreeVisible(), false,
419 "Tree should not be visible for TIMESPAN_EVERYTHING");
420 is(this.isWarningPanelVisible(), true,
421 "Warning panel should be visible for TIMESPAN_EVERYTHING");
422 }
423 else {
424 is(this.isTreeVisible(), true,
425 "Tree should be visible for non-TIMESPAN_EVERYTHING");
426 is(this.isWarningPanelVisible(), false,
427 "Warning panel should not be visible for non-TIMESPAN_EVERYTHING");
428 }
429 }
430 };
432 /**
433 * Adds a download to history.
434 *
435 * @param aMinutesAgo
436 * The download will be downloaded this many minutes ago
437 */
438 function addDownloadWithMinutesAgo(aMinutesAgo) {
439 let name = "fakefile-" + aMinutesAgo + "-minutes-ago";
440 let data = {
441 id: gDownloadId,
442 name: name,
443 source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
444 target: name,
445 startTime: now_uSec - (aMinutesAgo * 60 * 1000000),
446 endTime: now_uSec - ((aMinutesAgo + 1) *60 * 1000000),
447 state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
448 currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
449 guid: "a1bcD23eF4g5"
450 };
452 let db = dm.DBConnection;
453 let stmt = db.createStatement(
454 "INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " +
455 "state, currBytes, maxBytes, preferredAction, autoResume, guid) " +
456 "VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " +
457 ":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)");
458 try {
459 for (let prop in data) {
460 stmt.params[prop] = data[prop];
461 }
462 stmt.execute();
463 }
464 finally {
465 stmt.reset();
466 }
468 is(downloadExists(gDownloadId), true,
469 "Sanity check: download " + gDownloadId +
470 " should exist after creating it");
472 return gDownloadId++;
473 }
475 /**
476 * Adds a form entry to history.
477 *
478 * @param aMinutesAgo
479 * The entry will be added this many minutes ago
480 */
481 function addFormEntryWithMinutesAgo(aMinutesAgo) {
482 let name = aMinutesAgo + "-minutes-ago";
483 formhist.addEntry(name, "dummy");
485 // Artifically age the entry to the proper vintage.
486 let db = formhist.DBConnection;
487 let timestamp = now_uSec - (aMinutesAgo * 60 * 1000000);
488 db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " +
489 timestamp + " WHERE fieldname = '" + name + "'");
491 is(formhist.nameExists(name), true,
492 "Sanity check: form entry " + name + " should exist after creating it");
493 return name;
494 }
496 /**
497 * Removes all history visits, downloads, and form entries.
498 */
499 function blankSlate() {
500 PlacesUtils.bhistory.removeAllPages();
501 dm.cleanUp();
502 formhist.removeAllEntries();
503 }
505 /**
506 * Checks to see if the download with the specified ID exists.
507 *
508 * @param aID
509 * The ID of the download to check
510 * @return True if the download exists, false otherwise
511 */
512 function downloadExists(aID)
513 {
514 let db = dm.DBConnection;
515 let stmt = db.createStatement(
516 "SELECT * " +
517 "FROM moz_downloads " +
518 "WHERE id = :id"
519 );
520 stmt.params.id = aID;
521 let rows = stmt.executeStep();
522 stmt.finalize();
523 return !!rows;
524 }
526 /**
527 * Runs the next test in the gAllTests array. If all tests have been run,
528 * finishes the entire suite.
529 */
530 function doNextTest() {
531 if (gAllTests.length <= gCurrTest) {
532 blankSlate();
533 waitForAsyncUpdates(finish);
534 }
535 else {
536 let ct = gCurrTest;
537 gCurrTest++;
538 gAllTests[ct]();
539 }
540 }
542 /**
543 * Ensures that the specified downloads are either cleared or not.
544 *
545 * @param aDownloadIDs
546 * Array of download database IDs
547 * @param aShouldBeCleared
548 * True if each download should be cleared, false otherwise
549 */
550 function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
551 let niceStr = aShouldBeCleared ? "no longer" : "still";
552 aDownloadIDs.forEach(function (id) {
553 is(downloadExists(id), !aShouldBeCleared,
554 "download " + id + " should " + niceStr + " exist");
555 });
556 }
558 /**
559 * Ensures that the specified form entries are either cleared or not.
560 *
561 * @param aFormEntries
562 * Array of form entry names
563 * @param aShouldBeCleared
564 * True if each form entry should be cleared, false otherwise
565 */
566 function ensureFormEntriesClearedState(aFormEntries, aShouldBeCleared) {
567 let niceStr = aShouldBeCleared ? "no longer" : "still";
568 aFormEntries.forEach(function (entry) {
569 is(formhist.nameExists(entry), !aShouldBeCleared,
570 "form entry " + entry + " should " + niceStr + " exist");
571 });
572 }
574 /**
575 * Opens the sanitize dialog and runs a callback once it's finished loading.
576 *
577 * @param aOnloadCallback
578 * A function that will be called once the dialog has loaded
579 */
580 function openWindow(aOnloadCallback) {
581 function windowObserver(aSubject, aTopic, aData) {
582 if (aTopic != "domwindowopened")
583 return;
585 Services.ww.unregisterNotification(windowObserver);
586 let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
587 win.addEventListener("load", function onload(event) {
588 win.removeEventListener("load", onload, false);
589 executeSoon(function () {
590 // Some exceptions that reach here don't reach the test harness, but
591 // ok()/is() do...
592 try {
593 Task.spawn(function() {
594 aOnloadCallback(win);
595 }).then(function() {
596 waitForAsyncUpdates(doNextTest);
597 });
598 }
599 catch (exc) {
600 win.close();
601 ok(false, "Unexpected exception: " + exc + "\n" + exc.stack);
602 finish();
603 }
604 });
605 }, false);
606 }
607 Services.ww.registerNotification(windowObserver);
608 Services.ww.openWindow(null,
609 "chrome://browser/content/sanitize.xul",
610 "Sanitize",
611 "chrome,titlebar,dialog,centerscreen,modal",
612 null);
613 }
615 /**
616 * Creates a visit time.
617 *
618 * @param aMinutesAgo
619 * The visit will be visited this many minutes ago
620 */
621 function visitTimeForMinutesAgo(aMinutesAgo) {
622 return now_uSec - (aMinutesAgo * 60 * 1000000);
623 }
625 ///////////////////////////////////////////////////////////////////////////////
627 function test() {
628 blankSlate();
629 waitForExplicitFinish();
630 // Kick off all the tests in the gAllTests array.
631 waitForAsyncUpdates(doNextTest);
632 }