зеркало из https://github.com/mozilla/gecko-dev.git
Bug 692424 - GCLI and DOMTemplate could use a Promise implementation, r=msucan
This commit is contained in:
Родитель
f7e490ff24
Коммит
1fec61a409
|
@ -12,15 +12,17 @@
|
|||
# for the specific language governing rights and limitations under the
|
||||
# License.
|
||||
#
|
||||
# The Original Code is HUDService code.
|
||||
# The Original Code is the Style Inspector.
|
||||
#
|
||||
# The Initial Developer of the Original Code is Mozilla Corporation.
|
||||
#
|
||||
# Portions created by the Initial Developer are Copyright (C) 2010
|
||||
# The Initial Developer of the Original Code is
|
||||
# The Mozilla Foundation.
|
||||
#
|
||||
# Portions created by the Initial Developer are Copyright (C) 2011
|
||||
# the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Mike Ratcliffe <mratcliffe@mozilla.com> (Original author)
|
||||
# Mike Ratcliffe <mratcliffe@mozilla.com> (Original author)
|
||||
# Joe Walker <jwalker@mozilla.com>
|
||||
#
|
||||
# Alternatively, the contents of this file may be used under the terms of
|
||||
# either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
|
@ -44,12 +46,11 @@ VPATH = @srcdir@
|
|||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
ifdef ENABLE_TESTS
|
||||
ifneq (mobile,$(MOZ_BUILD_APP))
|
||||
# DIRS += test # no tests yet
|
||||
endif
|
||||
DIRS += test
|
||||
endif
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
libs::
|
||||
$(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools
|
||||
$(NSINSTALL) $(srcdir)/Promise.jsm $(FINAL_TARGET)/modules/devtools
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright 2009-2011 Mozilla Foundation and contributors
|
||||
* Licensed under the New BSD license. See LICENSE.txt or:
|
||||
* http://opensource.org/licenses/BSD-3-Clause
|
||||
*/
|
||||
|
||||
|
||||
var EXPORTED_SYMBOLS = [ "Promise" ];
|
||||
|
||||
/**
|
||||
* Create an unfulfilled promise
|
||||
* @constructor
|
||||
*/
|
||||
function Promise() {
|
||||
this._status = Promise.PENDING;
|
||||
this._value = undefined;
|
||||
this._onSuccessHandlers = [];
|
||||
this._onErrorHandlers = [];
|
||||
|
||||
// Debugging help
|
||||
this._id = Promise._nextId++;
|
||||
Promise._outstanding[this._id] = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* We give promises and ID so we can track which are outstanding
|
||||
*/
|
||||
Promise._nextId = 0;
|
||||
|
||||
/**
|
||||
* Outstanding promises. Handy list for debugging only
|
||||
*/
|
||||
Promise._outstanding = [];
|
||||
|
||||
/**
|
||||
* Recently resolved promises. Also for debugging only
|
||||
*/
|
||||
Promise._recent = [];
|
||||
|
||||
/**
|
||||
* A promise can be in one of 2 states.
|
||||
* The ERROR and SUCCESS states are terminal, the PENDING state is the only
|
||||
* start state.
|
||||
*/
|
||||
Promise.ERROR = -1;
|
||||
Promise.PENDING = 0;
|
||||
Promise.SUCCESS = 1;
|
||||
|
||||
/**
|
||||
* Yeay for RTTI
|
||||
*/
|
||||
Promise.prototype.isPromise = true;
|
||||
|
||||
/**
|
||||
* Have we either been resolve()ed or reject()ed?
|
||||
*/
|
||||
Promise.prototype.isComplete = function() {
|
||||
return this._status != Promise.PENDING;
|
||||
};
|
||||
|
||||
/**
|
||||
* Have we resolve()ed?
|
||||
*/
|
||||
Promise.prototype.isResolved = function() {
|
||||
return this._status == Promise.SUCCESS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Have we reject()ed?
|
||||
*/
|
||||
Promise.prototype.isRejected = function() {
|
||||
return this._status == Promise.ERROR;
|
||||
};
|
||||
|
||||
/**
|
||||
* Take the specified action of fulfillment of a promise, and (optionally)
|
||||
* a different action on promise rejection
|
||||
*/
|
||||
Promise.prototype.then = function(onSuccess, onError) {
|
||||
if (typeof onSuccess === 'function') {
|
||||
if (this._status === Promise.SUCCESS) {
|
||||
onSuccess.call(null, this._value);
|
||||
}
|
||||
else if (this._status === Promise.PENDING) {
|
||||
this._onSuccessHandlers.push(onSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof onError === 'function') {
|
||||
if (this._status === Promise.ERROR) {
|
||||
onError.call(null, this._value);
|
||||
}
|
||||
else if (this._status === Promise.PENDING) {
|
||||
this._onErrorHandlers.push(onError);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Like then() except that rather than returning <tt>this</tt> we return
|
||||
* a promise which resolves when the original promise resolves
|
||||
*/
|
||||
Promise.prototype.chainPromise = function(onSuccess) {
|
||||
var chain = new Promise();
|
||||
chain._chainedFrom = this;
|
||||
this.then(function(data) {
|
||||
try {
|
||||
chain.resolve(onSuccess(data));
|
||||
}
|
||||
catch (ex) {
|
||||
chain.reject(ex);
|
||||
}
|
||||
}, function(ex) {
|
||||
chain.reject(ex);
|
||||
});
|
||||
return chain;
|
||||
};
|
||||
|
||||
/**
|
||||
* Supply the fulfillment of a promise
|
||||
*/
|
||||
Promise.prototype.resolve = function(data) {
|
||||
return this._complete(this._onSuccessHandlers,
|
||||
Promise.SUCCESS, data, 'resolve');
|
||||
};
|
||||
|
||||
/**
|
||||
* Renege on a promise
|
||||
*/
|
||||
Promise.prototype.reject = function(data) {
|
||||
return this._complete(this._onErrorHandlers, Promise.ERROR, data, 'reject');
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal method to be called on resolve() or reject()
|
||||
* @private
|
||||
*/
|
||||
Promise.prototype._complete = function(list, status, data, name) {
|
||||
// Complain if we've already been completed
|
||||
if (this._status != Promise.PENDING) {
|
||||
if (typeof 'console' === 'object') {
|
||||
console.error('Promise complete. Attempted ' + name + '() with ', data);
|
||||
console.error('Prev status = ', this._status, ', value = ', this._value);
|
||||
}
|
||||
throw new Error('Promise already complete');
|
||||
}
|
||||
|
||||
this._status = status;
|
||||
this._value = data;
|
||||
|
||||
// Call all the handlers, and then delete them
|
||||
list.forEach(function(handler) {
|
||||
handler.call(null, this._value);
|
||||
}, this);
|
||||
delete this._onSuccessHandlers;
|
||||
delete this._onErrorHandlers;
|
||||
|
||||
// Remove the given {promise} from the _outstanding list, and add it to the
|
||||
// _recent list, pruning more than 20 recent promises from that list
|
||||
delete Promise._outstanding[this._id];
|
||||
// The original code includes this very useful debugging aid, however there
|
||||
// is concern that it will create a memory leak, so we leave it out here.
|
||||
/*
|
||||
Promise._recent.push(this);
|
||||
while (Promise._recent.length > 20) {
|
||||
Promise._recent.shift();
|
||||
}
|
||||
*/
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an array of promises and returns a promise that that is fulfilled once
|
||||
* all the promises in the array are fulfilled
|
||||
* @param promiseList The array of promises
|
||||
* @return the promise that is fulfilled when all the array is fulfilled
|
||||
*/
|
||||
Promise.group = function(promiseList) {
|
||||
if (!Array.isArray(promiseList)) {
|
||||
promiseList = Array.prototype.slice.call(arguments);
|
||||
}
|
||||
|
||||
// If the original array has nothing in it, return now to avoid waiting
|
||||
if (promiseList.length === 0) {
|
||||
return new Promise().resolve([]);
|
||||
}
|
||||
|
||||
var groupPromise = new Promise();
|
||||
var results = [];
|
||||
var fulfilled = 0;
|
||||
|
||||
var onSuccessFactory = function(index) {
|
||||
return function(data) {
|
||||
results[index] = data;
|
||||
fulfilled++;
|
||||
// If the group has already failed, silently drop extra results
|
||||
if (groupPromise._status !== Promise.ERROR) {
|
||||
if (fulfilled === promiseList.length) {
|
||||
groupPromise.resolve(results);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
promiseList.forEach(function(promise, index) {
|
||||
var onSuccess = onSuccessFactory(index);
|
||||
var onError = groupPromise.reject.bind(groupPromise);
|
||||
promise.then(onSuccess, onError);
|
||||
});
|
||||
|
||||
return groupPromise;
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
#
|
||||
# ***** BEGIN LICENSE BLOCK *****
|
||||
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public License Version
|
||||
# 1.1 (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
# http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
# for the specific language governing rights and limitations under the
|
||||
# License.
|
||||
#
|
||||
# The Original Code is GCLI.
|
||||
#
|
||||
# The Initial Developer of the Original Code is
|
||||
# The Mozilla Foundation.
|
||||
#
|
||||
# Portions created by the Initial Developer are Copyright (C) 2011
|
||||
# the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Joe Walker <jwalker@mozilla.com> (Original author)
|
||||
#
|
||||
# Alternatively, the contents of this file may be used under the terms of
|
||||
# either of the GNU General Public License Version 2 or later (the "GPL"),
|
||||
# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
# in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
# of those above. If you wish to allow use of your version of this file only
|
||||
# under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
# use your version of this file under the terms of the MPL, indicate your
|
||||
# decision by deleting the provisions above and replace them with the notice
|
||||
# and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
# the provisions above, a recipient may use your version of this file under
|
||||
# the terms of any one of the MPL, the GPL or the LGPL.
|
||||
#
|
||||
# ***** END LICENSE BLOCK *****
|
||||
|
||||
DEPTH = ../../../..
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
relativesrcdir = browser/devtools/shared/test
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
||||
_BROWSER_TEST_FILES = \
|
||||
browser_promise_basic.js \
|
||||
head.js \
|
||||
$(NULL)
|
||||
|
||||
_BROWSER_TEST_PAGES = \
|
||||
browser_promise_basic.html \
|
||||
$(NULL)
|
||||
|
||||
libs:: $(_BROWSER_TEST_FILES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
|
||||
|
||||
libs:: $(_BROWSER_TEST_PAGES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Promise Tests</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,212 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that our Promise implementation works properly
|
||||
|
||||
Cu.import("resource:///modules/devtools/Promise.jsm");
|
||||
|
||||
function test() {
|
||||
addTab("about:blank", function() {
|
||||
info("Starting Promise Tests");
|
||||
testBasic();
|
||||
});
|
||||
}
|
||||
|
||||
var postResolution;
|
||||
|
||||
function testBasic() {
|
||||
postResolution = new Promise();
|
||||
ok(postResolution.isPromise, "We have a promise");
|
||||
ok(!postResolution.isComplete(), "Promise is initially incomplete");
|
||||
ok(!postResolution.isResolved(), "Promise is initially unresolved");
|
||||
ok(!postResolution.isRejected(), "Promise is initially unrejected");
|
||||
|
||||
// Test resolve() *after* then() in the same context
|
||||
var reply = postResolution.then(testPostResolution, fail)
|
||||
.resolve("postResolution");
|
||||
is(reply, postResolution, "return this; working ok");
|
||||
}
|
||||
|
||||
var preResolution;
|
||||
|
||||
function testPostResolution(data) {
|
||||
is(data, "postResolution", "data is postResolution");
|
||||
ok(postResolution.isComplete(), "postResolution Promise is complete");
|
||||
ok(postResolution.isResolved(), "postResolution Promise is resolved");
|
||||
ok(!postResolution.isRejected(), "postResolution Promise is unrejected");
|
||||
|
||||
try {
|
||||
info("Expected double resolve error");
|
||||
postResolution.resolve("double resolve");
|
||||
ok(false, "double resolve");
|
||||
}
|
||||
catch (ex) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Test resolve() *before* then() in the same context
|
||||
preResolution = new Promise();
|
||||
var reply = preResolution.resolve("preResolution")
|
||||
.then(testPreResolution, fail);
|
||||
is(reply, preResolution, "return this; working ok");
|
||||
}
|
||||
|
||||
var laterResolution;
|
||||
|
||||
function testPreResolution(data) {
|
||||
is(data, "preResolution", "data is preResolution");
|
||||
ok(preResolution.isComplete(), "preResolution Promise is complete");
|
||||
ok(preResolution.isResolved(), "preResolution Promise is resolved");
|
||||
ok(!preResolution.isRejected(), "preResolution Promise is unrejected");
|
||||
|
||||
// Test resolve() *after* then() in a later context
|
||||
laterResolution = new Promise();
|
||||
laterResolution.then(testLaterResolution, fail);
|
||||
executeSoon(function() {
|
||||
laterResolution.resolve("laterResolution");
|
||||
});
|
||||
}
|
||||
|
||||
var laterRejection;
|
||||
|
||||
function testLaterResolution(data) {
|
||||
is(data, "laterResolution", "data is laterResolution");
|
||||
ok(laterResolution.isComplete(), "laterResolution Promise is complete");
|
||||
ok(laterResolution.isResolved(), "laterResolution Promise is resolved");
|
||||
ok(!laterResolution.isRejected(), "laterResolution Promise is unrejected");
|
||||
|
||||
// Test reject() *after* then() in a later context
|
||||
laterRejection = new Promise().then(fail, testLaterRejection);
|
||||
executeSoon(function() {
|
||||
laterRejection.reject("laterRejection");
|
||||
});
|
||||
}
|
||||
|
||||
function testLaterRejection(data) {
|
||||
is(data, "laterRejection", "data is laterRejection");
|
||||
ok(laterRejection.isComplete(), "laterRejection Promise is complete");
|
||||
ok(!laterRejection.isResolved(), "laterRejection Promise is unresolved");
|
||||
ok(laterRejection.isRejected(), "laterRejection Promise is rejected");
|
||||
|
||||
// Test chaining
|
||||
var orig = new Promise();
|
||||
orig.chainPromise(function(data) {
|
||||
is(data, "origData", "data is origData");
|
||||
return data.replace(/orig/, "new");
|
||||
}).then(function(data) {
|
||||
is(data, "newData", "data is newData");
|
||||
testChain();
|
||||
});
|
||||
orig.resolve("origData");
|
||||
}
|
||||
|
||||
var member1;
|
||||
var member2;
|
||||
var member3;
|
||||
var laterGroup;
|
||||
|
||||
function testChain() {
|
||||
// Test an empty group
|
||||
var empty1 = Promise.group();
|
||||
ok(empty1.isComplete(), "empty1 Promise is complete");
|
||||
ok(empty1.isResolved(), "empty1 Promise is resolved");
|
||||
ok(!empty1.isRejected(), "empty1 Promise is unrejected");
|
||||
|
||||
// Test a group with no members
|
||||
var empty2 = Promise.group([]);
|
||||
ok(empty2.isComplete(), "empty2 Promise is complete");
|
||||
ok(empty2.isResolved(), "empty2 Promise is resolved");
|
||||
ok(!empty2.isRejected(), "empty2 Promise is unrejected");
|
||||
|
||||
// Test grouping using resolve() in a later context
|
||||
member1 = new Promise();
|
||||
member2 = new Promise();
|
||||
member3 = new Promise();
|
||||
laterGroup = Promise.group(member1, member2, member3);
|
||||
laterGroup.then(testLaterGroup, fail);
|
||||
|
||||
member1.then(function(data) {
|
||||
is(data, "member1", "member1 is member1");
|
||||
executeSoon(function() {
|
||||
member2.resolve("member2");
|
||||
});
|
||||
}, fail);
|
||||
member2.then(function(data) {
|
||||
is(data, "member2", "member2 is member2");
|
||||
executeSoon(function() {
|
||||
member3.resolve("member3");
|
||||
});
|
||||
}, fail);
|
||||
member3.then(function(data) {
|
||||
is(data, "member3", "member3 is member3");
|
||||
// The group should now fire
|
||||
}, fail);
|
||||
executeSoon(function() {
|
||||
member1.resolve("member1");
|
||||
});
|
||||
}
|
||||
|
||||
var tidyGroup;
|
||||
|
||||
function testLaterGroup(data) {
|
||||
is(data[0], "member1", "member1 is member1");
|
||||
is(data[1], "member2", "member2 is member2");
|
||||
is(data[2], "member3", "member3 is member3");
|
||||
is(data.length, 3, "data.length is right");
|
||||
ok(laterGroup.isComplete(), "laterGroup Promise is complete");
|
||||
ok(laterGroup.isResolved(), "laterGroup Promise is resolved");
|
||||
ok(!laterGroup.isRejected(), "laterGroup Promise is unrejected");
|
||||
|
||||
// Test grouping resolve() *before* then() in the same context
|
||||
tidyGroup = Promise.group([
|
||||
postResolution, preResolution, laterResolution,
|
||||
member1, member2, member3, laterGroup
|
||||
]);
|
||||
tidyGroup.then(testTidyGroup, fail);
|
||||
}
|
||||
|
||||
var failGroup;
|
||||
|
||||
function testTidyGroup(data) {
|
||||
is(data[0], "postResolution", "postResolution is postResolution");
|
||||
is(data[1], "preResolution", "preResolution is preResolution");
|
||||
is(data[2], "laterResolution", "laterResolution is laterResolution");
|
||||
is(data[3], "member1", "member1 is member1");
|
||||
is(data[6][1], "member2", "laterGroup is laterGroup");
|
||||
is(data.length, 7, "data.length is right");
|
||||
ok(tidyGroup.isComplete(), "tidyGroup Promise is complete");
|
||||
ok(tidyGroup.isResolved(), "tidyGroup Promise is resolved");
|
||||
ok(!tidyGroup.isRejected(), "tidyGroup Promise is unrejected");
|
||||
|
||||
// Test grouping resolve() *before* then() in the same context
|
||||
failGroup = Promise.group(postResolution, laterRejection);
|
||||
failGroup.then(fail, testFailGroup);
|
||||
}
|
||||
|
||||
function testFailGroup(data) {
|
||||
is(data, "laterRejection", "laterRejection is laterRejection");
|
||||
|
||||
postResolution = undefined;
|
||||
preResolution = undefined;
|
||||
laterResolution = undefined;
|
||||
member1 = undefined;
|
||||
member2 = undefined;
|
||||
member3 = undefined;
|
||||
laterGroup = undefined;
|
||||
laterRejection = undefined;
|
||||
|
||||
finished();
|
||||
}
|
||||
|
||||
function fail() {
|
||||
gBrowser.removeCurrentTab();
|
||||
info("Failed Promise Tests");
|
||||
ok(false, "fail called");
|
||||
finish();
|
||||
}
|
||||
|
||||
function finished() {
|
||||
gBrowser.removeCurrentTab();
|
||||
info("Finishing Promise Tests");
|
||||
finish();
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/* ***** BEGIN LICENSE BLOCK *****
|
||||
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||
*
|
||||
* The contents of this file are subject to the Mozilla Public License Version
|
||||
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
* http://www.mozilla.org/MPL/
|
||||
*
|
||||
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing rights and limitations under the
|
||||
* License.
|
||||
*
|
||||
* The Original Code is DevTools test code.
|
||||
*
|
||||
* The Initial Developer of the Original Code is
|
||||
* The Mozilla Foundation.
|
||||
*
|
||||
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||
* the Initial Developer. All Rights Reserved.
|
||||
*
|
||||
* Contributor(s):
|
||||
* Joe Walker <jwalker@mozilla.com> (Original author)
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||
* of those above. If you wish to allow use of your version of this file only
|
||||
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||
* use your version of this file under the terms of the MPL, indicate your
|
||||
* decision by deleting the provisions above and replace them with the notice
|
||||
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||
* the provisions above, a recipient may use your version of this file under
|
||||
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
let tab;
|
||||
let browser;
|
||||
|
||||
function addTab(aURL, aCallback)
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
||||
function onTabLoad() {
|
||||
browser.removeEventListener("load", onTabLoad, true);
|
||||
aCallback();
|
||||
}
|
||||
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
content.location = aURL;
|
||||
|
||||
tab = gBrowser.selectedTab;
|
||||
browser = gBrowser.getBrowserForTab(tab);
|
||||
|
||||
browser.addEventListener("load", onTabLoad, true);
|
||||
}
|
||||
|
||||
registerCleanupFunction(function tearDown() {
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
tab = undefined;
|
||||
browser = undefined;
|
||||
});
|
Загрузка…
Ссылка в новой задаче