michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Tests the sanitize dialog (a.k.a. the clear recent history dialog). michael@0: * See bug 480169. michael@0: * michael@0: * The purpose of this test is not to fully flex the sanitize timespan code; michael@0: * browser/base/content/test/general/browser_sanitize-timespans.js does that. This michael@0: * test checks the UI of the dialog and makes sure it's correctly connected to michael@0: * the sanitize timespan code. michael@0: * michael@0: * Some of this code, especially the history creation parts, was taken from michael@0: * browser/base/content/test/general/browser_sanitize-timespans.js. michael@0: */ michael@0: michael@0: Cc["@mozilla.org/moz/jssubscript-loader;1"]. michael@0: getService(Ci.mozIJSSubScriptLoader). michael@0: loadSubScript("chrome://browser/content/sanitize.js"); michael@0: michael@0: const dm = Cc["@mozilla.org/download-manager;1"]. michael@0: getService(Ci.nsIDownloadManager); michael@0: const formhist = Cc["@mozilla.org/satchel/form-history;1"]. michael@0: getService(Ci.nsIFormHistory2); michael@0: michael@0: // Add tests here. Each is a function that's called by doNextTest(). michael@0: var gAllTests = [ michael@0: michael@0: /** michael@0: * Moves the grippy around, makes sure it works OK. michael@0: */ michael@0: function () { michael@0: // Add history (within the past hour) to get some rows in the tree. michael@0: let uris = []; michael@0: let places = []; michael@0: let pURI; michael@0: for (let i = 0; i < 30; i++) { michael@0: pURI = makeURI("http://" + i + "-minutes-ago.com/"); michael@0: places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); michael@0: uris.push(pURI); michael@0: } michael@0: michael@0: addVisits(places, function() { michael@0: // Open the dialog and do our tests. michael@0: openWindow(function (aWin) { michael@0: let wh = new WindowHelper(aWin); michael@0: wh.selectDuration(Sanitizer.TIMESPAN_HOUR); michael@0: wh.checkGrippy("Grippy should be at last row after selecting HOUR " + michael@0: "duration", michael@0: wh.getRowCount() - 1); michael@0: michael@0: // Move the grippy around. michael@0: let row = wh.getGrippyRow(); michael@0: while (row !== 0) { michael@0: row--; michael@0: wh.moveGrippyBy(-1); michael@0: wh.checkGrippy("Grippy should be moved up one row", row); michael@0: } michael@0: wh.moveGrippyBy(-1); michael@0: wh.checkGrippy("Grippy should remain at first row after trying to move " + michael@0: "it up", michael@0: 0); michael@0: while (row !== wh.getRowCount() - 1) { michael@0: row++; michael@0: wh.moveGrippyBy(1); michael@0: wh.checkGrippy("Grippy should be moved down one row", row); michael@0: } michael@0: wh.moveGrippyBy(1); michael@0: wh.checkGrippy("Grippy should remain at last row after trying to move " + michael@0: "it down", michael@0: wh.getRowCount() - 1); michael@0: michael@0: // Cancel the dialog, make sure history visits are not cleared. michael@0: wh.checkPrefCheckbox("history", false); michael@0: michael@0: wh.cancelDialog(); michael@0: yield promiseHistoryClearedState(uris, false); michael@0: michael@0: // OK, done, cleanup after ourselves. michael@0: blankSlate(); michael@0: yield promiseHistoryClearedState(uris, true); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the combined history-downloads checkbox clears both history michael@0: * visits and downloads when checked; the dialog respects simple timespan. michael@0: */ michael@0: function () { michael@0: // Add history (within the past hour). michael@0: let uris = []; michael@0: let places = []; michael@0: let pURI; michael@0: for (let i = 0; i < 30; i++) { michael@0: pURI = makeURI("http://" + i + "-minutes-ago.com/"); michael@0: places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); michael@0: uris.push(pURI); michael@0: } michael@0: // Add history (over an hour ago). michael@0: let olderURIs = []; michael@0: for (let i = 0; i < 5; i++) { michael@0: pURI = makeURI("http://" + (60 + i) + "-minutes-ago.com/"); michael@0: places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(60 + i)}); michael@0: olderURIs.push(pURI); michael@0: } michael@0: michael@0: addVisits(places, function() { michael@0: // Add downloads (within the past hour). michael@0: let downloadIDs = []; michael@0: for (let i = 0; i < 5; i++) { michael@0: downloadIDs.push(addDownloadWithMinutesAgo(i)); michael@0: } michael@0: // Add downloads (over an hour ago). michael@0: let olderDownloadIDs = []; michael@0: for (let i = 0; i < 5; i++) { michael@0: olderDownloadIDs.push(addDownloadWithMinutesAgo(61 + i)); michael@0: } michael@0: let totalHistoryVisits = uris.length + olderURIs.length; michael@0: michael@0: // Open the dialog and do our tests. michael@0: openWindow(function (aWin) { michael@0: let wh = new WindowHelper(aWin); michael@0: wh.selectDuration(Sanitizer.TIMESPAN_HOUR); michael@0: wh.checkGrippy("Grippy should be at proper row after selecting HOUR " + michael@0: "duration", michael@0: uris.length); michael@0: michael@0: // Accept the dialog, make sure history visits and downloads within one michael@0: // hour are cleared. michael@0: wh.checkPrefCheckbox("history", true); michael@0: wh.acceptDialog(); michael@0: yield promiseHistoryClearedState(uris, true); michael@0: ensureDownloadsClearedState(downloadIDs, true); michael@0: michael@0: // Make sure visits and downloads > 1 hour still exist. michael@0: yield promiseHistoryClearedState(olderURIs, false); michael@0: ensureDownloadsClearedState(olderDownloadIDs, false); michael@0: michael@0: // OK, done, cleanup after ourselves. michael@0: blankSlate(); michael@0: yield promiseHistoryClearedState(olderURIs, true); michael@0: ensureDownloadsClearedState(olderDownloadIDs, true); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the combined history-downloads checkbox removes neither michael@0: * history visits nor downloads when not checked. michael@0: */ michael@0: function () { michael@0: // Add history, downloads, form entries (within the past hour). michael@0: let uris = []; michael@0: let places = []; michael@0: let pURI; michael@0: for (let i = 0; i < 5; i++) { michael@0: pURI = makeURI("http://" + i + "-minutes-ago.com/"); michael@0: places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); michael@0: uris.push(pURI); michael@0: } michael@0: michael@0: addVisits(places, function() { michael@0: let downloadIDs = []; michael@0: for (let i = 0; i < 5; i++) { michael@0: downloadIDs.push(addDownloadWithMinutesAgo(i)); michael@0: } michael@0: let formEntries = []; michael@0: for (let i = 0; i < 5; i++) { michael@0: formEntries.push(addFormEntryWithMinutesAgo(i)); michael@0: } michael@0: michael@0: // Open the dialog and do our tests. michael@0: openWindow(function (aWin) { michael@0: let wh = new WindowHelper(aWin); michael@0: wh.selectDuration(Sanitizer.TIMESPAN_HOUR); michael@0: wh.checkGrippy("Grippy should be at last row after selecting HOUR " + michael@0: "duration", michael@0: wh.getRowCount() - 1); michael@0: michael@0: // Remove only form entries, leave history (including downloads). michael@0: wh.checkPrefCheckbox("history", false); michael@0: wh.checkPrefCheckbox("formdata", true); michael@0: wh.acceptDialog(); michael@0: michael@0: // Of the three only form entries should be cleared. michael@0: yield promiseHistoryClearedState(uris, false); michael@0: ensureDownloadsClearedState(downloadIDs, false); michael@0: ensureFormEntriesClearedState(formEntries, true); michael@0: michael@0: // OK, done, cleanup after ourselves. michael@0: blankSlate(); michael@0: yield promiseHistoryClearedState(uris, true); michael@0: ensureDownloadsClearedState(downloadIDs, true); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the "Everything" duration option works. michael@0: */ michael@0: function () { michael@0: // Add history. michael@0: let uris = []; michael@0: let places = []; michael@0: let pURI; michael@0: // within past hour, within past two hours, within past four hours and michael@0: // outside past four hours michael@0: [10, 70, 130, 250].forEach(function(aValue) { michael@0: pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); michael@0: places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); michael@0: uris.push(pURI); michael@0: }); michael@0: addVisits(places, function() { michael@0: michael@0: // Open the dialog and do our tests. michael@0: openWindow(function (aWin) { michael@0: let wh = new WindowHelper(aWin); michael@0: wh.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); michael@0: wh.checkPrefCheckbox("history", true); michael@0: wh.acceptDialog(); michael@0: yield promiseHistoryClearedState(uris, true); michael@0: }); michael@0: }); michael@0: } michael@0: ]; michael@0: michael@0: // Used as the download database ID for a new download. Incremented for each michael@0: // new download. See addDownloadWithMinutesAgo(). michael@0: var gDownloadId = 5555551; michael@0: michael@0: // Index in gAllTests of the test currently being run. Incremented for each michael@0: // test run. See doNextTest(). michael@0: var gCurrTest = 0; michael@0: michael@0: var now_uSec = Date.now() * 1000; michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: /** michael@0: * This wraps the dialog and provides some convenience methods for interacting michael@0: * with it. michael@0: * michael@0: * A warning: Before you call any function that uses the tree (or any function michael@0: * that calls a function that uses the tree), you must set a non-everything michael@0: * duration by calling selectDuration(). The dialog does not initialize the michael@0: * tree if it does not yet need to be shown. michael@0: * michael@0: * @param aWin michael@0: * The dialog's nsIDOMWindow michael@0: */ michael@0: function WindowHelper(aWin) { michael@0: this.win = aWin; michael@0: } michael@0: michael@0: WindowHelper.prototype = { michael@0: /** michael@0: * "Presses" the dialog's OK button. michael@0: */ michael@0: acceptDialog: function () { michael@0: is(this.win.document.documentElement.getButton("accept").disabled, false, michael@0: "Dialog's OK button should not be disabled"); michael@0: this.win.document.documentElement.acceptDialog(); michael@0: }, michael@0: michael@0: /** michael@0: * "Presses" the dialog's Cancel button. michael@0: */ michael@0: cancelDialog: function () { michael@0: this.win.document.documentElement.cancelDialog(); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the grippy row is in the right place, tree selection is OK, michael@0: * and that the grippy's visible. michael@0: * michael@0: * @param aMsg michael@0: * Passed to is() when checking grippy location michael@0: * @param aExpectedRow michael@0: * The row that the grippy should be at michael@0: */ michael@0: checkGrippy: function (aMsg, aExpectedRow) { michael@0: is(this.getGrippyRow(), aExpectedRow, aMsg); michael@0: this.checkTreeSelection(); michael@0: this.ensureGrippyIsVisible(); michael@0: }, michael@0: michael@0: /** michael@0: * (Un)checks a history scope checkbox (browser & download history, michael@0: * form history, etc.). michael@0: * michael@0: * @param aPrefName michael@0: * The final portion of the checkbox's privacy.cpd.* preference name michael@0: * @param aCheckState michael@0: * True if the checkbox should be checked, false otherwise michael@0: */ michael@0: checkPrefCheckbox: function (aPrefName, aCheckState) { michael@0: var pref = "privacy.cpd." + aPrefName; michael@0: var cb = this.win.document.querySelectorAll( michael@0: "#itemList > [preference='" + pref + "']"); michael@0: is(cb.length, 1, "found checkbox for " + pref + " preference"); michael@0: if (cb[0].checked != aCheckState) michael@0: cb[0].click(); michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the tree selection is appropriate to the grippy row. (A michael@0: * single, contiguous selection should exist from the first row all the way michael@0: * to the grippy.) michael@0: */ michael@0: checkTreeSelection: function () { michael@0: let grippyRow = this.getGrippyRow(); michael@0: let sel = this.getTree().view.selection; michael@0: if (grippyRow === 0) { michael@0: is(sel.getRangeCount(), 0, michael@0: "Grippy row is 0, so no tree selection should exist"); michael@0: } michael@0: else { michael@0: is(sel.getRangeCount(), 1, michael@0: "Grippy row > 0, so only one tree selection range should exist"); michael@0: let min = {}; michael@0: let max = {}; michael@0: sel.getRangeAt(0, min, max); michael@0: is(min.value, 0, "Tree selection should start at first row"); michael@0: is(max.value, grippyRow - 1, michael@0: "Tree selection should end at row before grippy"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The grippy should always be visible when it's moved directly. This method michael@0: * ensures that. michael@0: */ michael@0: ensureGrippyIsVisible: function () { michael@0: let tbo = this.getTree().treeBoxObject; michael@0: let firstVis = tbo.getFirstVisibleRow(); michael@0: let lastVis = tbo.getLastVisibleRow(); michael@0: let grippyRow = this.getGrippyRow(); michael@0: ok(firstVis <= grippyRow && grippyRow <= lastVis, michael@0: "Grippy row should be visible; this inequality should be true: " + michael@0: firstVis + " <= " + grippyRow + " <= " + lastVis); michael@0: }, michael@0: michael@0: /** michael@0: * @return The dialog's duration dropdown michael@0: */ michael@0: getDurationDropdown: function () { michael@0: return this.win.document.getElementById("sanitizeDurationChoice"); michael@0: }, michael@0: michael@0: /** michael@0: * @return The grippy row index michael@0: */ michael@0: getGrippyRow: function () { michael@0: return this.win.gContiguousSelectionTreeHelper.getGrippyRow(); michael@0: }, michael@0: michael@0: /** michael@0: * @return The tree's row count (includes the grippy row) michael@0: */ michael@0: getRowCount: function () { michael@0: return this.getTree().view.rowCount; michael@0: }, michael@0: michael@0: /** michael@0: * @return The tree michael@0: */ michael@0: getTree: function () { michael@0: return this.win.gContiguousSelectionTreeHelper.tree; michael@0: }, michael@0: michael@0: /** michael@0: * @return True if the "Everything" warning panel is visible (as opposed to michael@0: * the tree) michael@0: */ michael@0: isWarningPanelVisible: function () { michael@0: return this.win.document.getElementById("durationDeck").selectedIndex == 1; michael@0: }, michael@0: michael@0: /** michael@0: * @return True if the tree is visible (as opposed to the warning panel) michael@0: */ michael@0: isTreeVisible: function () { michael@0: return this.win.document.getElementById("durationDeck").selectedIndex == 0; michael@0: }, michael@0: michael@0: /** michael@0: * Moves the grippy one row at a time in the direction and magnitude specified. michael@0: * If aDelta < 0, moves the grippy up; if aDelta > 0, moves it down. michael@0: * michael@0: * @param aDelta michael@0: * The amount and direction to move michael@0: */ michael@0: moveGrippyBy: function (aDelta) { michael@0: if (aDelta === 0) michael@0: return; michael@0: let key = aDelta < 0 ? "UP" : "DOWN"; michael@0: let abs = Math.abs(aDelta); michael@0: let treechildren = this.getTree().treeBoxObject.treeBody; michael@0: treechildren.focus(); michael@0: for (let i = 0; i < abs; i++) { michael@0: EventUtils.sendKey(key); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Selects a duration in the duration dropdown. michael@0: * michael@0: * @param aDurVal michael@0: * One of the Sanitizer.TIMESPAN_* values michael@0: */ michael@0: selectDuration: function (aDurVal) { michael@0: this.getDurationDropdown().value = aDurVal; michael@0: if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) { michael@0: is(this.isTreeVisible(), false, michael@0: "Tree should not be visible for TIMESPAN_EVERYTHING"); michael@0: is(this.isWarningPanelVisible(), true, michael@0: "Warning panel should be visible for TIMESPAN_EVERYTHING"); michael@0: } michael@0: else { michael@0: is(this.isTreeVisible(), true, michael@0: "Tree should be visible for non-TIMESPAN_EVERYTHING"); michael@0: is(this.isWarningPanelVisible(), false, michael@0: "Warning panel should not be visible for non-TIMESPAN_EVERYTHING"); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Adds a download to history. michael@0: * michael@0: * @param aMinutesAgo michael@0: * The download will be downloaded this many minutes ago michael@0: */ michael@0: function addDownloadWithMinutesAgo(aMinutesAgo) { michael@0: let name = "fakefile-" + aMinutesAgo + "-minutes-ago"; michael@0: let data = { michael@0: id: gDownloadId, michael@0: name: name, michael@0: source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", michael@0: target: name, michael@0: startTime: now_uSec - (aMinutesAgo * 60 * 1000000), michael@0: endTime: now_uSec - ((aMinutesAgo + 1) *60 * 1000000), michael@0: state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, michael@0: currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, michael@0: guid: "a1bcD23eF4g5" michael@0: }; michael@0: michael@0: let db = dm.DBConnection; michael@0: let stmt = db.createStatement( michael@0: "INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " + michael@0: "state, currBytes, maxBytes, preferredAction, autoResume, guid) " + michael@0: "VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " + michael@0: ":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)"); michael@0: try { michael@0: for (let prop in data) { michael@0: stmt.params[prop] = data[prop]; michael@0: } michael@0: stmt.execute(); michael@0: } michael@0: finally { michael@0: stmt.reset(); michael@0: } michael@0: michael@0: is(downloadExists(gDownloadId), true, michael@0: "Sanity check: download " + gDownloadId + michael@0: " should exist after creating it"); michael@0: michael@0: return gDownloadId++; michael@0: } michael@0: michael@0: /** michael@0: * Adds a form entry to history. michael@0: * michael@0: * @param aMinutesAgo michael@0: * The entry will be added this many minutes ago michael@0: */ michael@0: function addFormEntryWithMinutesAgo(aMinutesAgo) { michael@0: let name = aMinutesAgo + "-minutes-ago"; michael@0: formhist.addEntry(name, "dummy"); michael@0: michael@0: // Artifically age the entry to the proper vintage. michael@0: let db = formhist.DBConnection; michael@0: let timestamp = now_uSec - (aMinutesAgo * 60 * 1000000); michael@0: db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " + michael@0: timestamp + " WHERE fieldname = '" + name + "'"); michael@0: michael@0: is(formhist.nameExists(name), true, michael@0: "Sanity check: form entry " + name + " should exist after creating it"); michael@0: return name; michael@0: } michael@0: michael@0: /** michael@0: * Removes all history visits, downloads, and form entries. michael@0: */ michael@0: function blankSlate() { michael@0: PlacesUtils.bhistory.removeAllPages(); michael@0: dm.cleanUp(); michael@0: formhist.removeAllEntries(); michael@0: } michael@0: michael@0: /** michael@0: * Checks to see if the download with the specified ID exists. michael@0: * michael@0: * @param aID michael@0: * The ID of the download to check michael@0: * @return True if the download exists, false otherwise michael@0: */ michael@0: function downloadExists(aID) michael@0: { michael@0: let db = dm.DBConnection; michael@0: let stmt = db.createStatement( michael@0: "SELECT * " + michael@0: "FROM moz_downloads " + michael@0: "WHERE id = :id" michael@0: ); michael@0: stmt.params.id = aID; michael@0: let rows = stmt.executeStep(); michael@0: stmt.finalize(); michael@0: return !!rows; michael@0: } michael@0: michael@0: /** michael@0: * Runs the next test in the gAllTests array. If all tests have been run, michael@0: * finishes the entire suite. michael@0: */ michael@0: function doNextTest() { michael@0: if (gAllTests.length <= gCurrTest) { michael@0: blankSlate(); michael@0: waitForAsyncUpdates(finish); michael@0: } michael@0: else { michael@0: let ct = gCurrTest; michael@0: gCurrTest++; michael@0: gAllTests[ct](); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Ensures that the specified downloads are either cleared or not. michael@0: * michael@0: * @param aDownloadIDs michael@0: * Array of download database IDs michael@0: * @param aShouldBeCleared michael@0: * True if each download should be cleared, false otherwise michael@0: */ michael@0: function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) { michael@0: let niceStr = aShouldBeCleared ? "no longer" : "still"; michael@0: aDownloadIDs.forEach(function (id) { michael@0: is(downloadExists(id), !aShouldBeCleared, michael@0: "download " + id + " should " + niceStr + " exist"); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Ensures that the specified form entries are either cleared or not. michael@0: * michael@0: * @param aFormEntries michael@0: * Array of form entry names michael@0: * @param aShouldBeCleared michael@0: * True if each form entry should be cleared, false otherwise michael@0: */ michael@0: function ensureFormEntriesClearedState(aFormEntries, aShouldBeCleared) { michael@0: let niceStr = aShouldBeCleared ? "no longer" : "still"; michael@0: aFormEntries.forEach(function (entry) { michael@0: is(formhist.nameExists(entry), !aShouldBeCleared, michael@0: "form entry " + entry + " should " + niceStr + " exist"); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Opens the sanitize dialog and runs a callback once it's finished loading. michael@0: * michael@0: * @param aOnloadCallback michael@0: * A function that will be called once the dialog has loaded michael@0: */ michael@0: function openWindow(aOnloadCallback) { michael@0: function windowObserver(aSubject, aTopic, aData) { michael@0: if (aTopic != "domwindowopened") michael@0: return; michael@0: michael@0: Services.ww.unregisterNotification(windowObserver); michael@0: let win = aSubject.QueryInterface(Ci.nsIDOMWindow); michael@0: win.addEventListener("load", function onload(event) { michael@0: win.removeEventListener("load", onload, false); michael@0: executeSoon(function () { michael@0: // Some exceptions that reach here don't reach the test harness, but michael@0: // ok()/is() do... michael@0: try { michael@0: Task.spawn(function() { michael@0: aOnloadCallback(win); michael@0: }).then(function() { michael@0: waitForAsyncUpdates(doNextTest); michael@0: }); michael@0: } michael@0: catch (exc) { michael@0: win.close(); michael@0: ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); michael@0: finish(); michael@0: } michael@0: }); michael@0: }, false); michael@0: } michael@0: Services.ww.registerNotification(windowObserver); michael@0: Services.ww.openWindow(null, michael@0: "chrome://browser/content/sanitize.xul", michael@0: "Sanitize", michael@0: "chrome,titlebar,dialog,centerscreen,modal", michael@0: null); michael@0: } michael@0: michael@0: /** michael@0: * Creates a visit time. michael@0: * michael@0: * @param aMinutesAgo michael@0: * The visit will be visited this many minutes ago michael@0: */ michael@0: function visitTimeForMinutesAgo(aMinutesAgo) { michael@0: return now_uSec - (aMinutesAgo * 60 * 1000000); michael@0: } michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: function test() { michael@0: blankSlate(); michael@0: waitForExplicitFinish(); michael@0: // Kick off all the tests in the gAllTests array. michael@0: waitForAsyncUpdates(doNextTest); michael@0: }