Bug 968621 - Uplift Add-on SDK to Firefox

This commit is contained in:
Erik Vold 2014-02-06 09:02:01 -08:00
parent 848fc8acc5
commit 04f31de6b2
12 changed files with 168 additions and 69 deletions

View File

@ -3,7 +3,6 @@
- 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/. -->
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
@ -17,7 +16,7 @@
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>21.0</em:minVersion>
<em:minVersion>26.0</em:minVersion>
<em:maxVersion>29.0a1</em:maxVersion>
</Description>
</em:targetApplication>

View File

@ -34,8 +34,15 @@ const { events: stateEvents } = require('../state/events');
const { events: viewEvents } = require('./view/events');
const events = require('../../event/utils');
const { id: addonID } = require('../../self');
const { identify } = require('../id');
const buttons = new Map();
const toWidgetId = id =>
('action-button--' + addonID.toLowerCase()+ '-' + id).
replace(/[^a-z0-9_-]/g, '');
const ActionButton = Class({
extends: EventTarget,
implements: [
@ -48,32 +55,37 @@ const ActionButton = Class({
disabled: false
}, buttonContract(options));
let id = toWidgetId(options.id);
register(this, state);
// Setup listeners.
setListeners(this, options);
buttons.set(options.id, this);
buttons.set(id, this);
view.create(state);
view.create(merge({}, state, { id: id }));
},
dispose: function dispose() {
buttons.delete(this.id);
let id = toWidgetId(this.id);
buttons.delete(id);
off(this);
view.dispose(this.id);
view.dispose(id);
unregister(this);
},
get id() this.state().id,
click: function click() { view.click(this.id) }
click: function click() { view.click(toWidgetId(this.id)) }
});
exports.ActionButton = ActionButton;
identify.define(ActionButton, ({id}) => toWidgetId(id));
let actionButtonStateEvents = events.filter(stateEvents,
e => e.target instanceof ActionButton);
@ -95,7 +107,7 @@ on(updateEvents, 'data', ({target: id, window}) => {
});
on(actionButtonStateEvents, 'data', ({target, window, state}) => {
let { id } = target;
let id = toWidgetId(target.id);
view.setIcon(id, window, state.icon);
view.setLabel(id, window, state.label);
view.setDisabled(id, window, state.disabled);

View File

@ -34,8 +34,15 @@ const { events: stateEvents } = require('../state/events');
const { events: viewEvents } = require('./view/events');
const events = require('../../event/utils');
const { id: addonID } = require('../../self');
const { identify } = require('../id');
const buttons = new Map();
const toWidgetId = id =>
('toggle-button--' + addonID.toLowerCase()+ '-' + id).
replace(/[^a-z0-9_-]/g, '');
const ToggleButton = Class({
extends: EventTarget,
implements: [
@ -49,32 +56,37 @@ const ToggleButton = Class({
checked: false
}, toggleButtonContract(options));
let id = toWidgetId(options.id);
register(this, state);
// Setup listeners.
setListeners(this, options);
buttons.set(options.id, this);
buttons.set(id, this);
view.create(merge({ type: 'checkbox' }, state));
view.create(merge({ type: 'checkbox' }, state, { id: id }));
},
dispose: function dispose() {
buttons.delete(this.id);
let id = toWidgetId(this.id);
buttons.delete(id);
off(this);
view.dispose(this.id);
view.dispose(id);
unregister(this);
},
get id() this.state().id,
click: function click() view.click(this.id)
click: function click() view.click(toWidgetId(this.id))
});
exports.ToggleButton = ToggleButton;
identify.define(ToggleButton, ({id}) => toWidgetId(id));
let toggleButtonStateEvents = events.filter(stateEvents,
e => e.target instanceof ToggleButton);
@ -85,7 +97,8 @@ let clickEvents = events.filter(toggleButtonViewEvents, e => e.type === 'click')
let updateEvents = events.filter(toggleButtonViewEvents, e => e.type === 'update');
on(toggleButtonStateEvents, 'data', ({target, window, state}) => {
let { id } = target;
let id = toWidgetId(target.id);
view.setIcon(id, window, state.icon);
view.setLabel(id, window, state.label);
view.setDisabled(id, window, state.disabled);

View File

@ -13,9 +13,7 @@ module.metadata = {
const { Cu } = require('chrome');
const { on, off, emit } = require('../../event/core');
const { id: addonID, data } = require('sdk/self');
const buttonPrefix =
'button--' + addonID.toLowerCase().replace(/[^a-z0-9-_]/g, '');
const { data } = require('sdk/self');
const { isObject } = require('../../lang/type');
@ -28,9 +26,6 @@ const { events: viewEvents } = require('./view/events');
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const toWidgetID = id => buttonPrefix + '-' + id;
const toButtonID = id => id.substr(buttonPrefix.length + 1);
const views = new Map();
const customizedWindows = new WeakMap();
@ -47,14 +42,14 @@ const buttonListener = {
customizedWindows.delete(window);
for (let [id, ] of views) {
let placement = CustomizableUI.getPlacementOfWidget(toWidgetID(id));
let placement = CustomizableUI.getPlacementOfWidget(id);
if (placement)
emit(viewEvents, 'data', { type: 'update', target: id, window: window });
}
},
onWidgetAfterDOMChange: (node, nextNode, container) => {
let id = toButtonID(node.id);
let { id } = node;
let view = views.get(id);
let window = node.ownerDocument.defaultView;
@ -73,11 +68,11 @@ require('../../system/unload').when( _ =>
function getNode(id, window) {
return !views.has(id) || ignoreWindow(window)
? null
: CustomizableUI.getWidget(toWidgetID(id)).forWindow(window).node
: CustomizableUI.getWidget(id).forWindow(window).node
};
function isInToolbar(id) {
let placement = CustomizableUI.getPlacementOfWidget(toWidgetID(id));
let placement = CustomizableUI.getPlacementOfWidget(id);
return placement && CustomizableUI.getAreaType(placement.area) === 'toolbar';
}
@ -120,7 +115,7 @@ function create(options) {
throw new Error('The ID "' + id + '" seems already used.');
CustomizableUI.createWidget({
id: toWidgetID(id),
id: id,
type: 'custom',
removable: true,
defaultArea: AREA_NAVBAR,
@ -131,7 +126,7 @@ function create(options) {
let node = document.createElementNS(XUL_NS, 'toolbarbutton');
let image = getImage(icon, false, window.devicePixelRatio);
let image = getImage(icon, true, window.devicePixelRatio);
if (ignoreWindow(window))
node.style.display = 'none';
@ -170,7 +165,7 @@ function dispose(id) {
if (!views.has(id)) return;
views.delete(id);
CustomizableUI.destroyWidget(toWidgetID(id));
CustomizableUI.destroyWidget(id);
}
exports.dispose = dispose;

View File

@ -8,7 +8,6 @@ DEFAULT_COMMON_PREFS = {
# sets this preference)
'browser.dom.window.dump.enabled': True,
# warn about possibly incorrect code
'javascript.options.strict': True,
'javascript.options.showInConsole': True,
# Allow remote connections to the debugger

View File

@ -106,12 +106,24 @@ if not mswindows:
class Popen(subprocess.Popen):
kill_called = False
if mswindows:
def _execute_child(self, args, executable, preexec_fn, close_fds,
cwd, env, universal_newlines, startupinfo,
creationflags, shell,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite):
def _execute_child(self, *args_tuple):
# workaround for bug 958609
if sys.hexversion < 0x02070600: # prior to 2.7.6
(args, executable, preexec_fn, close_fds,
cwd, env, universal_newlines, startupinfo,
creationflags, shell,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite) = args_tuple
to_close = set()
else: # 2.7.6 and later
(args, executable, preexec_fn, close_fds,
cwd, env, universal_newlines, startupinfo,
creationflags, shell, to_close,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite) = args_tuple
if not isinstance(args, types.StringTypes):
args = subprocess.list2cmdline(args)

View File

@ -1,7 +1,6 @@
/* 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/. */
'use strict';
module.metadata = {
@ -11,7 +10,7 @@ module.metadata = {
};
const { isTabOpen, activateTab, openTab,
closeTab, getURI } = require('sdk/tabs/utils');
closeTab, getTabURL, getWindowHoldingTab } = require('sdk/tabs/utils');
const windows = require('sdk/deprecated/window-utils');
const { LoaderWithHookedConsole } = require('sdk/test/loader');
const { setTimeout } = require('sdk/timers');
@ -19,10 +18,27 @@ const { is } = require('sdk/system/xul-app');
const tabs = require('sdk/tabs');
const isAustralis = "gCustomizeMode" in windows.activeBrowserWindow;
const { set: setPref } = require("sdk/preferences/service");
const { defer } = require('sdk/core/promise');
const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
let uri = require('sdk/self').data.url('index.html');
function closeTabPromise(tab) {
let { promise, resolve } = defer();
let url = getTabURL(tab);
tabs.on('close', function onCloseTab(t) {
if (t.url == url) {
tabs.removeListener('close', onCloseTab);
setTimeout(_ => resolve(tab))
}
});
closeTab(tab);
return promise;
}
function isChromeVisible(window) {
let x = window.document.documentElement.getAttribute('disablechrome')
return x !== 'true';
@ -61,11 +77,12 @@ exports['test that add-on page has no chrome'] = function(assert, done) {
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTab(tab);
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
closeTabPromise(tab).then(function() {
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
}).then(null, assert.fail);
});
};
@ -86,11 +103,12 @@ exports['test that add-on page with hash has no chrome'] = function(assert, done
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTab(tab);
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
closeTabPromise(tab).then(function() {
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
}).then(null, assert.fail);
});
};
@ -111,11 +129,12 @@ exports['test that add-on page with querystring has no chrome'] = function(asser
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTab(tab);
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
closeTabPromise(tab).then(function() {
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
}).then(null, assert.fail);
});
};
@ -136,11 +155,12 @@ exports['test that add-on page with hash and querystring has no chrome'] = funct
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTab(tab);
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
closeTabPromise(tab).then(function() {
assert.ok(isChromeVisible(window), 'chrome is visible again');
loader.unload();
assert.ok(!isTabOpen(tab), 'add-on page tab is closed on unload');
done();
}).then(null, assert.fail);
});
};
@ -158,9 +178,10 @@ exports['test that malformed uri is not an addon-page'] = function(assert, done)
assert.ok(isChromeVisible(window), 'chrome is visible for malformed uri');
closeTab(tab);
loader.unload();
done();
closeTabPromise(tab).then(function() {
loader.unload();
done();
}).then(null, assert.fail);
});
};

View File

@ -25,6 +25,40 @@ exports.testOptionsType = function(assert, done) {
});
}
exports.testButton = function(assert, done) {
tabs.open({
url: 'about:addons',
onReady: function(tab) {
sp.once('sayHello', function() {
assert.pass('The button was pressed!');
tab.close(done)
});
tab.attach({
contentScriptWhen: 'end',
contentScript: 'function onLoad() {\n' +
'unsafeWindow.removeEventListener("load", onLoad, false);\n' +
'AddonManager.getAddonByID("' + self.id + '", function(aAddon) {\n' +
'unsafeWindow.gViewController.viewObjects.detail.node.addEventListener("ViewChanged", function whenViewChanges() {\n' +
'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' +
'setTimeout(function() {\n' + // TODO: figure out why this is necessary..
'unsafeWindow.document.querySelector("button[label=\'Click me!\']").click()\n' +
'}, 250);\n' +
'}, false);\n' +
'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' +
'});\n' +
'}\n' +
// Wait for the load event ?
'if (document.readyState == "complete") {\n' +
'onLoad()\n' +
'} else {\n' +
'unsafeWindow.addEventListener("load", onLoad, false);\n' +
'}\n',
});
}
});
}
if (app.is('Firefox')) {
exports.testAOM = function(assert, done) {
tabs.open({
@ -41,7 +75,8 @@ if (app.is('Firefox')) {
'self.postMessage({\n' +
'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[title=\'some-title\']")),\n' +
'myInteger: getAttributes(unsafeWindow.document.querySelector("setting[title=\'my-int\']")),\n' +
'myHiddenInt: getAttributes(unsafeWindow.document.querySelector("setting[title=\'hidden-int\']"))\n' +
'myHiddenInt: getAttributes(unsafeWindow.document.querySelector("setting[title=\'hidden-int\']")),\n' +
'sayHello: getAttributes(unsafeWindow.document.querySelector("button[label=\'Click me!\']"))\n' +
'});\n' +
'}, 250);\n' +
'}, false);\n' +
@ -53,7 +88,8 @@ if (app.is('Firefox')) {
'pref: ele.getAttribute("pref"),\n' +
'type: ele.getAttribute("type"),\n' +
'title: ele.getAttribute("title"),\n' +
'desc: ele.getAttribute("desc")\n' +
'desc: ele.getAttribute("desc"),\n' +
'"data-jetpack-id": ele.getAttribute(\'data-jetpack-id\')\n' +
'}\n' +
'}\n' +
'}\n' +
@ -69,12 +105,14 @@ if (app.is('Firefox')) {
assert.equal(msg.somePreference.pref, 'extensions.'+self.id+'.somePreference', 'somePreference path is correct');
assert.equal(msg.somePreference.title, 'some-title', 'somePreference title is correct');
assert.equal(msg.somePreference.desc, 'Some short description for the preference', 'somePreference description is correct');
assert.equal(msg.somePreference['data-jetpack-id'], self.id, 'data-jetpack-id attribute value is correct');
// test myInteger
assert.equal(msg.myInteger.type, 'integer', 'myInteger is a int');
assert.equal(msg.myInteger.pref, 'extensions.'+self.id+'.myInteger', 'extensions.test-simple-prefs.myInteger');
assert.equal(msg.myInteger.title, 'my-int', 'myInteger title is correct');
assert.equal(msg.myInteger.desc, 'How many of them we have.', 'myInteger desc is correct');
assert.equal(msg.myInteger['data-jetpack-id'], self.id, 'data-jetpack-id attribute value is correct');
// test myHiddenInt
assert.equal(msg.myHiddenInt.type, undefined, 'myHiddenInt was not displayed');
@ -82,6 +120,9 @@ if (app.is('Firefox')) {
assert.equal(msg.myHiddenInt.title, undefined, 'myHiddenInt was not displayed');
assert.equal(msg.myHiddenInt.desc, undefined, 'myHiddenInt was not displayed');
// test sayHello
assert.equal(msg.sayHello['data-jetpack-id'], self.id, 'data-jetpack-id attribute value is correct');
tab.close(done);
}
});

View File

@ -19,5 +19,10 @@
"hidden": true,
"value": 5,
"title": "hidden-int"
}]
}, {
"name": "sayHello",
"type": "control",
"label": "Click me!",
"title": "hello"
}]
}

View File

@ -260,20 +260,22 @@ exports.testTabLocation = function(assert, done) {
// TEST: tab.close()
exports.testTabClose = function(assert, done) {
let url = "data:text/html;charset=utf-8,foo";
let testName = "testTabClose";
let url = "data:text/html;charset=utf-8," + testName;
assert.notEqual(tabs.activeTab.url, url, "tab is not the active tab");
tabs.on('ready', function onReady(tab) {
tabs.removeListener('ready', onReady);
tabs.once('ready', function onReady(tab) {
assert.equal(tabs.activeTab.url, tab.url, "tab is now the active tab");
assert.equal(url, tab.url, "tab url is the test url");
let secondOnCloseCalled = false;
// Bug 699450: Multiple calls to tab.close should not throw
tab.close(function() secondOnCloseCalled = true);
try {
tab.close(function () {
assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
assert.ok(secondOnCloseCalled,
"The immediate second call to tab.close gots its callback fired");
"The immediate second call to tab.close happened");
assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
done();
@ -282,7 +284,6 @@ exports.testTabClose = function(assert, done) {
catch(e) {
assert.fail("second call to tab.close() thrown an exception: " + e);
}
assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab");
});
tabs.open(url);

View File

@ -21,7 +21,7 @@ function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
const { AREA_NAVBAR } = CustomizableUI;
let widgets = CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR).
filter((id) => id.startsWith('button--') && id.endsWith(buttonId));
filter((id) => id.startsWith('action-button--') && id.endsWith(buttonId));
if (widgets.length === 0)
throw new Error('Widget with id `' + id +'` not found.');
@ -594,6 +594,7 @@ exports['test button click'] = function(assert, done) {
then(done, assert.fail);
});
}).then(null, assert.fail);
}
exports['test button icon set'] = function(assert) {

View File

@ -21,7 +21,7 @@ function getWidget(buttonId, window = getMostRecentBrowserWindow()) {
const { AREA_NAVBAR } = CustomizableUI;
let widgets = CustomizableUI.getWidgetIdsInArea(AREA_NAVBAR).
filter((id) => id.startsWith('button--') && id.endsWith(buttonId));
filter((id) => id.startsWith('toggle-button--') && id.endsWith(buttonId));
if (widgets.length === 0)
throw new Error('Widget with id `' + id +'` not found.');