Bug 982379 - Uplift Add-on SDK to Firefox

This commit is contained in:
Erik Vold 2014-03-12 17:20:56 -07:00
parent 35f2b81da7
commit c3fd8f5dcd
23 changed files with 484 additions and 186 deletions

View File

@ -0,0 +1,40 @@
/* 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";
const { Cu } = require("chrome");
// Because Firefox Holly, we still need to check if `CustomizableUI` is
// available. Once Australis will officially land, we can safely remove it.
// See Bug 959142
try {
Cu.import("resource:///modules/CustomizableUI.jsm", {});
}
catch (e) {
throw Error("Unsupported Application: The module" + module.id +
" does not support this application.");
}
const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
const { receive } = require("../event/utils");
const { InputPort } = require("./system");
const { object} = require("../util/sequence");
const { getOuterId } = require("../window/utils");
const Input = function() {};
Input.prototype = Object.create(InputPort.prototype);
Input.prototype.onCustomizeStart = function (window) {
receive(this, object([getOuterId(window), true]));
}
Input.prototype.onCustomizeEnd = function (window) {
receive(this, object([getOuterId(window), null]));
}
Input.prototype.addListener = input => CustomizableUI.addListener(input);
Input.prototype.removeListener = input => CustomizableUI.removeListener(input);
exports.CustomizationInput = Input;

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cr } = require("chrome");
const { Cc, Ci, Cr, Cu } = require("chrome");
const { Input, start, stop, end, receive, outputs } = require("../event/utils");
const { once, off } = require("../event/core");
const { id: addonID } = require("../self");
@ -14,6 +14,7 @@ const { addObserver, removeObserver } = Cc['@mozilla.org/observer-service;1'].
const addonUnloadTopic = "sdk:loader:destroy";
const isXrayWrapper = Cu.isXrayWrapper;
// In the past SDK used to double-wrap notifications dispatched, which
// made them awkward to use outside of SDK. At present they no longer
// do that, although we still supported for legacy reasons.
@ -48,23 +49,29 @@ InputPort.prototype.constructor = InputPort;
// When port is started (which is when it's subgraph get's
// first subscriber) actual observer is registered.
InputPort.start = input => {
addObserver(input, input.topic, false);
input.addListener(input);
// Also register add-on unload observer to end this signal
// when that happens.
addObserver(input, addonUnloadTopic, false);
};
InputPort.prototype[start] = InputPort.start;
InputPort.addListener = input => addObserver(input, input.topic, false);
InputPort.prototype.addListener = InputPort.addListener;
// When port is stopped (which is when it's subgraph has no
// no subcribers left) an actual observer unregistered.
// Note that port stopped once it ends as well (which is when
// add-on is unloaded).
InputPort.stop = input => {
removeObserver(input, input.topic);
input.removeListener(input);
removeObserver(input, addonUnloadTopic);
};
InputPort.prototype[stop] = InputPort.stop;
InputPort.removeListener = input => removeObserver(input, input.topic);
InputPort.prototype.removeListener = InputPort.removeListener;
// `InputPort` also implements `nsIObserver` interface and
// `nsISupportsWeakReference` interfaces as it's going to be used as such.
InputPort.prototype.QueryInterface = function(iid) {
@ -80,9 +87,11 @@ InputPort.prototype.QueryInterface = function(iid) {
InputPort.prototype.observe = function(subject, topic, data) {
// Unwrap message from the subject. SDK used to have it's own version of
// wrappedJSObjects which take precedence, if subject has `wrappedJSObject`
// use it as message. Otherwise use subject as is.
// and it's not an XrayWrapper use it as message. Otherwise use subject as
// is.
const message = subject === null ? null :
isLegacyWrapper(subject) ? unwrapLegacy(subject) :
isXrayWrapper(subject) ? subject :
subject.wrappedJSObject ? subject.wrappedJSObject :
subject;

View File

@ -13,7 +13,6 @@ module.metadata = {
};
const { Ci } = require("chrome");
const { validateOptions: valid } = require('./deprecated/api-utils');
const { setTimeout } = require('./timers');
const { isPrivateBrowsingSupported } = require('./self');
const { isWindowPBSupported } = require('./private-browsing/utils');
@ -31,11 +30,14 @@ const { events } = require("./panel/events");
const systemEvents = require("./system/events");
const { filter, pipe, stripListeners } = require("./event/utils");
const { getNodeView, getActiveView } = require("./view/core");
const { isNil, isObject } = require("./lang/type");
const { isNil, isObject, isNumber } = require("./lang/type");
const { getAttachEventType } = require("./content/utils");
const { number, boolean, object } = require('./deprecated/api-utils');
let number = { is: ['number', 'undefined', 'null'] };
let boolean = { is: ['boolean', 'undefined', 'null'] };
let isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
some(value => isNumber(value) && !isNaN(value));
let isSDKObj = obj => obj instanceof Class;
let rectContract = contract({
top: number,
@ -44,16 +46,20 @@ let rectContract = contract({
left: number
});
let rect = {
is: ['object', 'undefined', 'null'],
map: function(v) isNil(v) || !isObject(v) ? v : rectContract(v)
let position = {
is: object,
map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v),
ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)),
msg: 'The option "position" must be a SDK object registered as anchor; ' +
'or an object with one or more of the following keys set to numeric ' +
'values: top, right, bottom, left.'
}
let displayContract = contract({
width: number,
height: number,
focus: boolean,
position: rect
position: position
});
let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
@ -176,7 +182,7 @@ const Panel = Class({
get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
/* Public API: Panel.show */
show: function show(options, anchor) {
show: function show(options={}, anchor) {
if (options instanceof Ci.nsIDOMElement) {
[anchor, options] = [options, null];
}
@ -191,7 +197,7 @@ const Panel = Class({
let model = modelFor(this);
let view = viewFor(this);
let anchorView = getNodeView(anchor);
let anchorView = getNodeView(anchor || options.position);
options = merge({
position: model.position,
@ -239,24 +245,25 @@ exports.Panel = Panel;
getActiveView.define(Panel, viewFor);
// Filter panel events to only panels that are create by this module.
let panelEvents = filter(events, function({target}) panelFor(target));
let panelEvents = filter(events, ({target}) => panelFor(target));
// Panel events emitted after panel has being shown.
let shows = filter(panelEvents, function({type}) type === "popupshown");
let shows = filter(panelEvents, ({type}) => type === "popupshown");
// Panel events emitted after panel became hidden.
let hides = filter(panelEvents, function({type}) type === "popuphidden");
let hides = filter(panelEvents, ({type}) => type === "popuphidden");
// Panel events emitted after content inside panel is ready. For different
// panels ready may mean different state based on `contentScriptWhen` attribute.
// Weather given event represents readyness is detected by `getAttachEventType`
// helper function.
let ready = filter(panelEvents, function({type, target})
let ready = filter(panelEvents, ({type, target}) =>
getAttachEventType(modelFor(panelFor(target))) === type);
// Forward panel show / hide events to panel's own event listeners.
on(shows, "data", function({target}) emit(panelFor(target), "show"));
on(hides, "data", function({target}) emit(panelFor(target), "hide"));
on(shows, "data", ({target}) => emit(panelFor(target), "show"));
on(hides, "data", ({target}) => emit(panelFor(target), "hide"));
on(ready, "data", function({target}) {
let worker = workerFor(panelFor(target));

View File

@ -23,6 +23,8 @@ const events = require("../system/events");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) {
position = position || {};
let x, y;
let hasTop = !isNil(position.top);
@ -127,14 +129,32 @@ function display(panel, options, anchor) {
({x, y, width, height}) = calculateRegion(options, viewportRect);
}
else {
// The XUL Panel has an arrow, so the margin needs to be reset
// to the default value.
panel.style.margin = "";
let { CustomizableUI, window } = anchor.ownerDocument.defaultView;
// In Australis, widgets may be positioned in an overflow panel or the
// menu panel.
// In such cases clicking this widget will hide the overflow/menu panel,
// and the widget's panel will show instead.
if (CustomizableUI) {
let node = anchor;
({anchor}) = CustomizableUI.getWidget(anchor.id).forWindow(window);
// if `node` is not the `anchor` itself, it means the widget is
// positioned in a panel, therefore we have to hide it before show
// the widget's panel in the same anchor
if (node !== anchor)
CustomizableUI.hidePanelForNode(anchor);
}
width = width || defaultWidth;
height = height || defaultHeight;
// Open the popup by the anchor.
let rect = anchor.getBoundingClientRect();
let window = anchor.ownerDocument.defaultView;
let zoom = getScreenPixelsPerCSSPixel(window);
let screenX = rect.left + window.mozInnerScreenX * zoom;
let screenY = rect.top + window.mozInnerScreenY * zoom;

View File

@ -12,7 +12,6 @@ const { Cc, Ci, CC } = require('chrome');
const options = require('@loader/options');
const file = require('./io/file');
const runtime = require("./system/runtime");
var cfxArgs = require("@test/options");
const appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
@ -70,13 +69,12 @@ exports.exit = function exit(code) {
stream.write(status, status.length);
stream.flush();
stream.close();
if (cfxArgs.parseable) {
console.log('wrote to resultFile');
}
}
forcedExit = true;
appStartup.quit(E_FORCE);
if (code == 0) {
forcedExit = true;
}
appStartup.quit(code ? E_ATTEMPT : E_FORCE);
};
// Adapter for nodejs's stdout & stderr:

View File

@ -7,7 +7,6 @@ module.metadata = {
"stability": "experimental"
};
var { setTimeout } = require("../timers");
var { exit, stdout } = require("../system");
var cfxArgs = require("@test/options");
@ -20,16 +19,9 @@ function runTests(findAndRunTests) {
stdout.write(tests.passed + " of " + total + " tests passed.\n");
if (tests.failed == 0) {
if (tests.passed === 0) {
if (tests.passed === 0)
stdout.write("No tests were run\n");
}
setTimeout(function() {
if (cfxArgs.parseable) {
console.log('calling exit(0)');
}
exit(0);
}, 0);
exit(0);
} else {
if (cfxArgs.verbose || cfxArgs.parseable)
printFailedTests(tests, stdout.write);

View File

@ -26,6 +26,7 @@ const { merge } = require('../../util/object');
const { Disposable } = require('../../core/disposable');
const { on, off, emit, setListeners } = require('../../event/core');
const { EventTarget } = require('../../event/target');
const { getNodeView } = require('../../view/core');
const view = require('./view');
const { buttonContract, stateContract } = require('./contract');
@ -89,6 +90,10 @@ exports.ActionButton = ActionButton;
identify.define(ActionButton, ({id}) => toWidgetId(id));
getNodeView.define(ActionButton, button =>
view.nodeFor(toWidgetId(button.id))
);
let actionButtonStateEvents = events.filter(stateEvents,
e => e.target instanceof ActionButton);

View File

@ -26,6 +26,7 @@ const { merge } = require('../../util/object');
const { Disposable } = require('../../core/disposable');
const { on, off, emit, setListeners } = require('../../event/core');
const { EventTarget } = require('../../event/target');
const { getNodeView } = require('../../view/core');
const view = require('./view');
const { toggleButtonContract, toggleStateContract } = require('./contract');
@ -90,6 +91,10 @@ exports.ToggleButton = ToggleButton;
identify.define(ToggleButton, ({id}) => toWidgetId(id));
getNodeView.define(ToggleButton, button =>
view.nodeFor(toWidgetId(button.id))
);
let toggleButtonStateEvents = events.filter(stateEvents,
e => e.target instanceof ToggleButton);

View File

@ -108,6 +108,11 @@ function getImage(icon, isInToolbar, pixelRatio) {
return image;
}
function nodeFor(id, window=getMostRecentBrowserWindow()) {
return customizedWindows.has(window) ? null : getNode(id, window);
};
exports.nodeFor = nodeFor;
function create(options) {
let { id, label, icon, type } = options;
@ -183,7 +188,7 @@ function setIcon(id, window, icon) {
exports.setIcon = setIcon;
function setLabel(id, window, label) {
let node = customizedWindows.has(window) ? null : getNode(id, window);
let node = nodeFor(id, window);
if (node) {
node.setAttribute('label', label);
@ -193,7 +198,7 @@ function setLabel(id, window, label) {
exports.setLabel = setLabel;
function setDisabled(id, window, disabled) {
let node = customizedWindows.has(window) ? null : getNode(id, window);
let node = nodeFor(id, window);
if (node)
node.disabled = disabled;
@ -201,7 +206,7 @@ function setDisabled(id, window, disabled) {
exports.setDisabled = setDisabled;
function setChecked(id, window, checked) {
let node = customizedWindows.has(window) ? null : getNode(id, window);
let node = nodeFor(id, window);
if (node)
node.checked = checked;
@ -209,8 +214,7 @@ function setChecked(id, window, checked) {
exports.setChecked = setChecked;
function click(id) {
let window = getMostRecentBrowserWindow();
let node = customizedWindows.has(window) ? null : getNode(id, window);
let node = nodeFor(id);
if (node)
node.click();

View File

@ -32,17 +32,17 @@ const mailbox = new OutputPort({ id: "frame-mailbox" });
const input = Frames;
const urlToId = url =>
const makeID = url =>
("frame-" + addonID + "-" + url).
split("/").join("-").
split(".").join("-").
replace(/[^A-Za-z0-9_\-]/g, "");
const validate = contract({
id: {
name: {
is: ["string", "undefined"],
ok: x => /^[a-z][a-z0-9-_]+$/i.test(x),
msg: "The `option.id` must be a valid alphanumeric string (hyphens and " +
msg: "The `option.name` must be a valid alphanumeric string (hyphens and " +
"underscores are allowed) starting with letter."
},
url: {
@ -88,7 +88,7 @@ const Frame = Class({
implements: [Disposable, Source],
initialize: function(params={}) {
const options = validate(params);
const id = options.id || urlToId(options.url);
const id = makeID(options.name || options.url);
if (frames.has(id))
throw Error("Frame with this id already exists: " + id);

View File

@ -19,7 +19,7 @@ const { events: browserEvents } = require('../browser/events');
const { events: tabEvents } = require('../tab/events');
const { events: stateEvents } = require('./state/events');
const { windows, isInteractive, getMostRecentBrowserWindow } = require('../window/utils');
const { windows, isInteractive, getFocusedBrowser } = require('../window/utils');
const { getActiveTab, getOwnerWindow } = require('../tabs/utils');
const { ignoreWindow } = require('../private-browsing/utils');
@ -47,7 +47,7 @@ const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWind
const isEnumerable = window => !ignoreWindow(window);
const browsers = _ =>
windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
const getMostRecentTab = _ => getActiveTab(getMostRecentBrowserWindow());
const getMostRecentTab = _ => getActiveTab(getFocusedBrowser());
function getStateFor(component, target) {
if (!isRegistered(component))
@ -172,7 +172,7 @@ exports.properties = properties;
function state(contract) {
return {
state: function state(target, state) {
let nativeTarget = target === 'window' ? getMostRecentBrowserWindow()
let nativeTarget = target === 'window' ? getFocusedBrowser()
: target === 'tab' ? getMostRecentTab()
: viewFor(target);

View File

@ -16,11 +16,13 @@ const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/u
const { InputPort } = require("../../input/system");
const { OutputPort } = require("../../output/system");
const { Interactive } = require("../../input/browser");
const { CustomizationInput } = require("../../input/customizable-ui");
const { pairs, map, isEmpty, object,
each, keys, values } = require("../../util/sequence");
const { curry, flip } = require("../../lang/functional");
const { patch, diff } = require("diffpatcher/index");
const prefs = require("../../preferences/service");
const { getByOuterId } = require("../../window/utils");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
@ -38,8 +40,9 @@ const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
// date.
const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
new InputPort({ id: "toolbar-change" })]));
const State = lift((toolbars, windows) => ({windows: windows, toolbars: toolbars}),
Toolbars, Interactive);
const State = lift((toolbars, windows, customizable) =>
({windows: windows, toolbars: toolbars, customizable: customizable}),
Toolbars, Interactive, new CustomizationInput());
// Shared event handler that makes `event.target.parent` collapsed.
// Used as toolbar's close buttons click handler.
@ -88,12 +91,17 @@ const addView = curry((options, {document}) => {
view.setAttribute("collapsed", options.collapsed);
view.setAttribute("toolbarname", options.title);
view.setAttribute("pack", "end");
view.setAttribute("defaultset", options.items.join(","));
view.setAttribute("customizable", true);
view.setAttribute("style", "max-height: 40px;");
view.setAttribute("customizable", "false");
view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
view.setAttribute("mode", "icons");
view.setAttribute("iconsize", "small");
view.setAttribute("context", "toolbar-context-menu");
view.setAttribute("class", "toolbar-primary chromeclass-toolbar");
let label = document.createElementNS(XUL_NS, "label");
label.setAttribute("value", options.title);
label.setAttribute("collapsed", "true");
view.appendChild(label);
let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
closeButton.setAttribute("id", "close-" + options.id);
@ -103,6 +111,24 @@ const addView = curry((options, {document}) => {
view.appendChild(closeButton);
// In order to have a close button not costumizable, aligned on the right,
// leaving the customizable capabilities of Australis, we need to create
// a toolbar inside a toolbar.
// This is should be a temporary hack, we should have a proper XBL for toolbar
// instead. See:
// https://bugzilla.mozilla.org/show_bug.cgi?id=982005
let toolbar = document.createElementNS(XUL_NS, "toolbar");
toolbar.setAttribute("id", "inner-" + options.id);
toolbar.setAttribute("defaultset", options.items.join(","));
toolbar.setAttribute("customizable", "true");
toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden");
toolbar.setAttribute("mode", "icons");
toolbar.setAttribute("iconsize", "small");
toolbar.setAttribute("context", "toolbar-context-menu");
toolbar.setAttribute("flex", "1");
view.insertBefore(toolbar, closeButton);
const observer = new document.defaultView.MutationObserver(attributesChanged);
observer.observe(view, { attributes: true,
attributeFilter: ["collapsed", "toolbarname"] });
@ -117,23 +143,34 @@ const removeView = curry((id, {document}) => {
if (view) view.remove();
});
const updateView = curry((id, {title, collapsed}, {document}) => {
const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
const view = document.getElementById(id);
if (view && title)
if (!view)
return;
if (title)
view.setAttribute("toolbarname", title);
if (view && collapsed !== void(0))
if (collapsed !== void(0))
view.setAttribute("collapsed", Boolean(collapsed));
if (isCustomizing !== void(0)) {
view.querySelector("label").collapsed = !isCustomizing;
view.querySelector("toolbar").style.visibility = isCustomizing
? "hidden" : "visible";
}
});
const viewUpdate = curry(flip(updateView));
// Utility function used to register toolbar into CustomizableUI.
const registerToolbar = state => {
// If it's first additon register toolbar as customizableUI component.
CustomizableUI.registerArea(state.id, {
CustomizableUI.registerArea("inner-" + state.id, {
type: CustomizableUI.TYPE_TOOLBAR,
legacy: true,
defaultPlacements: [...state.items, "close-" + state.id]
defaultPlacements: [...state.items]
});
};
// Utility function used to unregister toolbar from the CustomizableUI.
@ -148,7 +185,7 @@ const reactor = new Reactor({
// we unregister toolbar and remove it from each window
// it was added to.
if (update === null) {
unregisterToolbar(id);
unregisterToolbar("inner-" + id);
each(removeView(id), values(past.windows));
send(output, object([id, null]));
@ -190,10 +227,16 @@ const reactor = new Reactor({
if (window)
each(viewAdd(window), values(past.toolbars));
}, values(delta.windows));
each(([id, isCustomizing]) => {
each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
keys(present.toolbars));
}, pairs(delta.customizable))
},
onEnd: state => {
each(id => {
unregisterToolbar(id);
unregisterToolbar("inner-" + id);
each(removeView(id), values(state.windows));
}, keys(state.toolbars));
}

View File

@ -444,28 +444,10 @@ const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
// Special case for click events: if the widget doesn't have a click
// handler, but it does have a panel, display the panel.
if ("click" == type && !this._listeners("click").length && this.panel) {
// In Australis, widgets may be positioned in an overflow panel or the
// menu panel.
// In such cases clicking this widget will hide the overflow/menu panel,
// and the widget's panel will show instead.
let anchor = domNode;
let { CustomizableUI, window } = domNode.ownerDocument.defaultView;
if (CustomizableUI) {
({anchor}) = CustomizableUI.getWidget(domNode.id).forWindow(window);
// if `anchor` is not the `domNode` itself, it means the widget is
// positioned in a panel, therefore we have to hide it before show
// the widget's panel in the same anchor
if (anchor !== domNode)
CustomizableUI.hidePanelForNode(domNode);
}
// This kind of ugly workaround, instead we should implement
// `getNodeView` for the `Widget` class itself, but that's kind of
// hard without cleaning things up.
this.panel.show(null, getNodeView.implement({}, () => anchor));
this.panel.show(null, getNodeView.implement({}, () => domNode));
}
},
@ -795,8 +777,10 @@ WidgetChrome.prototype._createNode = function WC__createNode() {
// Initial population of a widget's content.
WidgetChrome.prototype.fill = function WC_fill() {
let { node, _doc: document } = this;
// Create element
var iframe = this._doc.createElement("iframe");
let iframe = document.createElement("iframe");
iframe.setAttribute("type", "content");
iframe.setAttribute("transparent", "transparent");
iframe.style.overflow = "hidden";
@ -809,14 +793,23 @@ WidgetChrome.prototype.fill = function WC_fill() {
// Do this early, because things like contentWindow are null
// until the node is attached to a document.
this.node.appendChild(iframe);
node.appendChild(iframe);
var label = this._doc.createElement("label");
let label = document.createElement("label");
label.setAttribute("value", this._widget.label);
label.className = "toolbarbutton-text";
label.setAttribute("crop", "right");
label.setAttribute("flex", "1");
this.node.appendChild(label);
node.appendChild(label);
// This toolbarbutton is created to provide a more consistent user experience
// during customization, see:
// https://bugzilla.mozilla.org/show_bug.cgi?id=959640
let button = document.createElement("toolbarbutton");
button.setAttribute("label", this._widget.label);
button.setAttribute("crop", "right");
button.className = "toolbarbutton-1 chromeclass-toolbar-additional";
node.appendChild(button);
// add event handlers
this.addEventHandlers();

View File

@ -19,6 +19,8 @@ const WM = Cc['@mozilla.org/appshell/window-mediator;1'].
getService(Ci.nsIWindowMediator);
const io = Cc['@mozilla.org/network/io-service;1'].
getService(Ci.nsIIOService);
const FM = Cc["@mozilla.org/focus-manager;1"].
getService(Ci.nsIFocusManager);
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
@ -356,6 +358,18 @@ function getFocusedWindow() {
}
exports.getFocusedWindow = getFocusedWindow;
/**
* Returns the focused browser window if any, or the most recent one.
* Opening new window, updates most recent window, but focus window
* changes later; so most recent window and focused window are not always
* the same.
*/
function getFocusedBrowser() {
let window = FM.activeWindow;
return isBrowser(window) ? window : getMostRecentBrowserWindow()
}
exports.getFocusedBrowser = getFocusedBrowser;
/**
* Returns the focused element in the most recent focused window
*/

View File

@ -543,11 +543,9 @@ def initializer(env_root, args, out=sys.stdout, err=sys.stderr):
if existing:
print >>err, 'This command must be run in an empty directory.'
return {"result":1}
for d in ['lib','data','test','doc']:
for d in ['lib','data','test']:
os.mkdir(os.path.join(path,d))
print >>out, '*', d, 'directory created'
open(os.path.join(path,'README.md'),'w').write('')
print >>out, '* README.md written'
jid = create_jid()
print >>out, '* generated jID automatically:', jid
open(os.path.join(path,'package.json'),'w').write(PACKAGE_JSON % {'name':addon.lower(),
@ -558,8 +556,6 @@ def initializer(env_root, args, out=sys.stdout, err=sys.stderr):
print >>out, '* test/test-main.js written'
open(os.path.join(path,'lib','main.js'),'w').write('')
print >>out, '* lib/main.js written'
open(os.path.join(path,'doc','main.md'),'w').write('')
print >>out, '* doc/main.md written'
if len(args) == 1:
print >>out, '\nYour sample add-on is now ready.'
print >>out, 'Do "cfx test" to test it and "cfx run" to try it. Have fun!'

View File

@ -736,7 +736,6 @@ def run_app(harness_root_dir, manifest_rdf, harness_options,
if os.path.exists(resultfile):
result = open(resultfile).read()
if result:
sys.stderr.write("resultfile contained " + "'" + result + "'\n")
if result in ['OK', 'FAIL']:
done = True
else:
@ -755,9 +754,7 @@ def run_app(harness_root_dir, manifest_rdf, harness_options,
else:
runner.wait(10)
finally:
sys.stderr.write("Done.\n")
outf.close()
sys.stderr.write("Clean the profile.\n")
if profile:
profile.cleanup()

View File

@ -3,18 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.metadata = {
engines: {
'Firefox': '*'
}
};
const { isTabOpen, activateTab, openTab,
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');
const { is } = require('sdk/system/xul-app');
const app = require("sdk/system/xul-app");
const tabs = require('sdk/tabs');
const isAustralis = "gCustomizeMode" in windows.activeBrowserWindow;
const { set: setPref } = require("sdk/preferences/service");
@ -44,6 +38,10 @@ function isChromeVisible(window) {
return x !== 'true';
}
// Once Bug 903018 is resolved, just move the application testing to
// module.metadata.engines
if (app.is('Firefox')) {
exports['test add-on page deprecation message'] = function(assert) {
let { loader, messages } = LoaderWithHookedConsole(module);
loader.require('sdk/addon-page');
@ -74,7 +72,7 @@ exports['test that add-on page has no chrome'] = function(assert, done) {
setTimeout(function() {
activateTab(tab);
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
assert.equal(isChromeVisible(window), app.is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTabPromise(tab).then(function() {
@ -100,7 +98,7 @@ exports['test that add-on page with hash has no chrome'] = function(assert, done
setTimeout(function() {
activateTab(tab);
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
assert.equal(isChromeVisible(window), app.is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTabPromise(tab).then(function() {
@ -126,7 +124,7 @@ exports['test that add-on page with querystring has no chrome'] = function(asser
setTimeout(function() {
activateTab(tab);
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
assert.equal(isChromeVisible(window), app.is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTabPromise(tab).then(function() {
@ -152,7 +150,7 @@ exports['test that add-on page with hash and querystring has no chrome'] = funct
setTimeout(function() {
activateTab(tab);
assert.equal(isChromeVisible(window), is('Fennec') || isAustralis,
assert.equal(isChromeVisible(window), app.is('Fennec') || isAustralis,
'chrome is not visible for addon page');
closeTabPromise(tab).then(function() {
@ -185,4 +183,8 @@ exports['test that malformed uri is not an addon-page'] = function(assert, done)
});
};
} else {
exports['test unsupported'] = (assert) => assert.pass('This application is unsupported.');
}
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -4,29 +4,36 @@
"use strict";
const { exec } = require("sdk/system/child_process");
const { platform, pathFor } = require("sdk/system");
const PROFILE_DIR = pathFor("ProfD");
const isWindows = platform.toLowerCase().indexOf("win") === 0;
/**
* Ensures using child_process and underlying subprocess.jsm
* works within an addon
*/
exports["test child_process in an addon"] = (assert, done) => {
exec(isWindows ? "DIR /A-D" : "ls -al", {
cwd: PROFILE_DIR
}, (err, stdout, stderr) => {
assert.ok(!err, "no errors");
assert.equal(stderr, "", "stderr is empty");
assert.ok(/extensions\.ini/.test(stdout), "stdout output of `ls -al` finds files");
if (isWindows)
assert.ok(!/<DIR>/.test(stdout), "passing args works");
else
assert.ok(/d(r[-|w][-|x]){3}/.test(stdout), "passing args works");
done();
});
};
const { exec } = require("sdk/system/child_process");
const { platform, pathFor } = require("sdk/system");
const PROFILE_DIR = pathFor("ProfD");
const isWindows = platform.toLowerCase().indexOf("win") === 0;
const app = require("sdk/system/xul-app");
// Once Bug 903018 is resolved, just move the application testing to
// module.metadata.engines
if (app.is("Firefox")) {
exports["test child_process in an addon"] = (assert, done) => {
exec(isWindows ? "DIR /A-D" : "ls -al", {
cwd: PROFILE_DIR
}, (err, stdout, stderr) => {
assert.ok(!err, "no errors");
assert.equal(stderr, "", "stderr is empty");
assert.ok(/extensions\.ini/.test(stdout), "stdout output of `ls -al` finds files");
if (isWindows)
assert.ok(!/<DIR>/.test(stdout), "passing args works");
else
assert.ok(/d(r[-|w][-|x]){3}/.test(stdout), "passing args works");
done();
});
};
} else {
exports["test unsupported"] = (assert) => assert.pass("This application is unsupported.");
}
require("sdk/test/runner").runTestsFromModule(module);

View File

@ -4,21 +4,24 @@
'use strict';
module.metadata = {
'engines': {
'Firefox': '*'
}
};
const { safeMerge: merge } = require('sdk/util/object');
const app = require("sdk/system/xul-app");
merge(module.exports,
require('./tests/test-places-bookmarks'),
require('./tests/test-places-events'),
require('./tests/test-places-favicon'),
require('./tests/test-places-history'),
require('./tests/test-places-host'),
require('./tests/test-places-utils')
);
// Once Bug 903018 is resolved, just move the application testing to
// module.metadata.engines
if (app.is('Firefox')) {
merge(module.exports,
require('./tests/test-places-bookmarks'),
require('./tests/test-places-events'),
require('./tests/test-places-favicon'),
require('./tests/test-places-history'),
require('./tests/test-places-host'),
require('./tests/test-places-utils')
);
} else {
exports['test unsupported'] = (assert) => {
assert.pass('This application is unsupported.');
};
}
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -12,46 +12,54 @@ const { preferencesBranch } = require('sdk/self');
const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {});
exports.testAOMLocalization = function(assert, done) {
tabs.open({
url: 'about:addons',
onReady: function(tab) {
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..
'self.postMessage({\n' +
'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[data-jetpack-id=\'' + self.id + '\']"))\n' +
'});\n' +
'}, 250);\n' +
'}, false);\n' +
'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' +
'});\n' +
'function getAttributes(ele) {\n' +
'if (!ele) return {};\n' +
'return {\n' +
'title: ele.getAttribute("title")\n' +
// Once Bug 903018 is resolved, just move the application testing to
// module.metadata.engines
//
// This should work in Fennec, needs to be refactored to work, via bug 979645
if (app.is('Firefox')) {
exports.testAOMLocalization = function(assert, done) {
tabs.open({
url: 'about:addons',
onReady: function(tab) {
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..
'self.postMessage({\n' +
'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[data-jetpack-id=\'' + self.id + '\']"))\n' +
'});\n' +
'}, 250);\n' +
'}, false);\n' +
'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' +
'});\n' +
'function getAttributes(ele) {\n' +
'if (!ele) return {};\n' +
'return {\n' +
'title: ele.getAttribute("title")\n' +
'}\n' +
'}\n' +
'}\n' +
'}\n' +
// Wait for the load event ?
'if (document.readyState == "complete") {\n' +
'onLoad()\n' +
'} else {\n' +
'unsafeWindow.addEventListener("load", onLoad, false);\n' +
'}\n',
onMessage: function(msg) {
// test somePreference
assert.equal(msg.somePreference.title, 'A', 'somePreference title is correct');
tab.close(done);
}
});
}
});
// Wait for the load event ?
'if (document.readyState == "complete") {\n' +
'onLoad()\n' +
'} else {\n' +
'unsafeWindow.addEventListener("load", onLoad, false);\n' +
'}\n',
onMessage: function(msg) {
// test somePreference
assert.equal(msg.somePreference.title, 'A', 'somePreference title is correct');
tab.close(done);
}
});
}
});
}
} else {
exports['test unsupported'] = (assert) => assert.pass('This test is unsupported.');
}
require('sdk/test/runner').runTestsFromModule(module);

View File

@ -46,9 +46,9 @@ exports["test frame API"] = function*(assert) {
/The `options.url`/,
"options.url must be local url");
assert.throws(() => new Frame({url: url, id: "4you" }),
/The `option.id` must be a valid/,
"can only take valid ID's");
assert.throws(() => new Frame({url: url, name: "4you" }),
/The `option.name` must be a valid/,
"can only take valid names");
const f1 = new Frame({ url: url });
@ -64,8 +64,8 @@ exports["test frame API"] = function*(assert) {
"can't have two identical frames");
const f2 = new Frame({ id: "frame-2", url: url });
assert.pass("can create frame with same url but diff id");
const f2 = new Frame({ name: "frame-2", url: url });
assert.pass("can create frame with same url but diff name");
const p2 = wait(f2, "register");
yield p1;
@ -145,7 +145,7 @@ exports["test host to content messaging"] = function*(assert) {
event.source.postMessage("pong!", event.origin);
});
} + "</script>";
const f1 = new Frame({ id: "mailbox", url: url });
const f1 = new Frame({ name: "mailbox", url: url });
const t1 = new Toolbar({ title: "mailbox", items: [f1] });
const e1 = yield wait(f1, "ready");
@ -169,7 +169,7 @@ exports["test content to host messaging"] = function*(assert) {
window.parent.postMessage("ping!", "*");
} + "</script>";
const f1 = new Frame({ id: "inbox", url: url });
const f1 = new Frame({ name: "inbox", url: url });
const t1 = new Toolbar({ title: "inbox", items: [f1] });
const e1 = yield wait(f1, "message");
@ -197,7 +197,7 @@ exports["test direct messaging"] = function*(assert) {
} + "</script>";
const w1 = getMostRecentBrowserWindow();
const f1 = new Frame({ url: url, id: "mail-cluster" });
const f1 = new Frame({ url: url, name: "mail-cluster" });
const t1 = new Toolbar({ title: "claster", items: [f1] });
yield wait(f1, "ready");

View File

@ -1020,6 +1020,44 @@ exports['test button click do not messing up states'] = function(assert) {
loader.unload();
}
exports['test buttons can have anchored panels'] = function(assert, done) {
let loader = Loader(module);
let { ToggleButton } = loader.require('sdk/ui');
let { Panel } = loader.require('sdk/panel');
let { identify } = loader.require('sdk/ui/id');
let { getActiveView } = loader.require('sdk/view/core');
let button = ToggleButton({
id: 'my-button-22',
label: 'my button',
icon: './icon.png',
onChange: ({checked}) => checked && panel.show({position: button})
});
let panel = Panel();
panel.once('show', () => {
let { document } = getMostRecentBrowserWindow();
let buttonNode = document.getElementById(identify(button));
let panelNode = getActiveView(panel);
assert.ok(button.state('window').checked,
'button is checked');
assert.equal(panelNode.getAttribute('type'), 'arrow',
'the panel is a arrow type');
assert.strictEqual(buttonNode, panelNode.anchorNode,
'the panel is anchored properly to the button');
loader.unload();
done();
});
button.click();
}
// If the module doesn't support the app we're being run in, require() will
// throw. In that case, remove all tests above from exports, and add one dummy
// test that passes.

View File

@ -12,11 +12,12 @@ module.metadata = {
const { Toolbar } = require("sdk/ui/toolbar");
const { Loader } = require("sdk/test/loader");
const { identify } = require("sdk/ui/id");
const { getMostRecentBrowserWindow, open } = require("sdk/window/utils");
const { getMostRecentBrowserWindow, open, getOuterId } = require("sdk/window/utils");
const { ready, close } = require("sdk/window/helpers");
const { defer } = require("sdk/core/promise");
const { send } = require("sdk/event/utils");
const { send, stop, Reactor } = require("sdk/event/utils");
const { object } = require("sdk/util/sequence");
const { CustomizationInput } = require("sdk/input/customizable-ui");
const { OutputPort } = require("sdk/output/system");
const output = new OutputPort({ id: "toolbar-change" });
@ -357,4 +358,120 @@ exports["test title change"] = function*(assert) {
yield close(w2);
};
exports["test toolbar is not customizable"] = function*(assert, done) {
const { window, document, gCustomizeMode } = getMostRecentBrowserWindow();
const outerId = getOuterId(window);
const input = new CustomizationInput();
const customized = defer();
const customizedEnd = defer();
new Reactor({ onStep: value => {
if (value[outerId] === true)
customized.resolve();
if (value[outerId] === null)
customizedEnd.resolve();
}}).run(input);
const toolbar = new Toolbar({ title: "foo" });
yield wait(toolbar, "attach");
let view = document.getElementById(toolbar.id);
let label = view.querySelector("label");
let inner = view.querySelector("toolbar");
assert.equal(view.getAttribute("customizable"), "false",
"The outer toolbar is not customizable.");
assert.ok(label.collapsed,
"The label is not displayed.")
assert.equal(inner.getAttribute("customizable"), "true",
"The inner toolbar is customizable.");
assert.equal(window.getComputedStyle(inner).visibility, "visible",
"The inner toolbar is visible.");
// Enter in customization mode
gCustomizeMode.toggle();
yield customized.promise;
assert.equal(view.getAttribute("customizable"), "false",
"The outer toolbar is not customizable.");
assert.equal(label.collapsed, false,
"The label is displayed.")
assert.equal(inner.getAttribute("customizable"), "true",
"The inner toolbar is customizable.");
assert.equal(window.getComputedStyle(inner).visibility, "hidden",
"The inner toolbar is hidden.");
// Exit from customization mode
gCustomizeMode.toggle();
yield customizedEnd.promise;
assert.equal(view.getAttribute("customizable"), "false",
"The outer toolbar is not customizable.");
assert.ok(label.collapsed,
"The label is not displayed.")
assert.equal(inner.getAttribute("customizable"), "true",
"The inner toolbar is customizable.");
assert.equal(window.getComputedStyle(inner).visibility, "visible",
"The inner toolbar is visible.");
toolbar.destroy();
};
exports["test button are attached to toolbar"] = function*(assert) {
const { document } = getMostRecentBrowserWindow();
const { ActionButton, ToggleButton } = require("sdk/ui");
const { identify } = require("sdk/ui/id");
let action = ActionButton({
id: "btn-1",
label: "action",
icon: "./placeholder.png"
});
let toggle = ToggleButton({
id: "btn-2",
label: "toggle",
icon: "./placeholder.png"
});
const toolbar = new Toolbar({
title: "foo",
items: [action, toggle]
});
yield wait(toolbar, "attach");
let actionNode = document.getElementById(identify(action));
let toggleNode = document.getElementById(identify(toggle));
assert.notEqual(actionNode, null,
"action button exists in the document");
assert.notEqual(actionNode, null,
"action button exists in the document");
assert.notEqual(toggleNode, null,
"toggle button exists in the document");
assert.equal(actionNode.nextElementSibling, toggleNode,
"action button is placed before toggle button");
assert.equal(actionNode.parentNode.parentNode.id, toolbar.id,
"buttons are placed in the correct toolbar");
toolbar.destroy();
};
require("sdk/test").run(exports);