Bug 993272 - Uplift Add-on SDK to Firefox

This commit is contained in:
Erik Vold 2014-04-14 22:56:11 -07:00
parent 557b8e0b3e
commit 7b8ad73f76
24 changed files with 440 additions and 51 deletions

View File

@ -1,10 +1,11 @@
/* 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";
var self = require("sdk/self");
var panels = require("sdk/panel");
var widgets = require("sdk/widget");
var { Panel } = require("sdk/panel");
var { ToggleButton } = require("sdk/ui");
function replaceMom(html) {
return html.replace("World", "Mom");
@ -21,20 +22,18 @@ exports.main = function(options, callbacks) {
helloHTML = replaceMom(helloHTML);
// ... and then create a panel that displays it.
var myPanel = panels.Panel({
contentURL: "data:text/html," + helloHTML
var myPanel = Panel({
contentURL: "data:text/html," + helloHTML,
onHide: handleHide
});
// Load the URL of the sample image.
var iconURL = self.data.url("mom.png");
// Create a widget that displays the image. We'll attach the panel to it.
// When you click the widget, the panel will pop up.
widgets.Widget({
var button = ToggleButton({
id: "test-widget",
label: "Mom",
contentURL: iconURL,
panel: myPanel
icon: './mom.png',
onChange: handleChange
});
// If you run cfx with --static-args='{"quitWhenDone":true}' this program
@ -42,3 +41,13 @@ exports.main = function(options, callbacks) {
if (options.staticArgs.quitWhenDone)
callbacks.quit();
}
function handleChange(state) {
if (state.checked) {
myPanel.show({ position: button });
}
}
function handleHide() {
button.state('window', { checked: false });
}

View File

@ -3,9 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Disable tests below for now.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
/*
var m = require("main");
var self = require("sdk/self");
@ -26,4 +23,3 @@ exports.testID = function(test) {
test.assertEqual(self.data.url("sample.html"),
"resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html");
};
*/

View File

@ -1,26 +1,34 @@
/* 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";
var data = require("sdk/self").data;
var { data } = require("sdk/self");
var { ToggleButton } = require("sdk/ui");
var base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
"AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
"N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
"bWRR9AAAAABJRU5ErkJggg%3D%3D";
var reddit_panel = require("sdk/panel").Panel({
width: 240,
height: 320,
contentURL: "http://www.reddit.com/.mobile?keep_extension=True",
contentScriptFile: [data.url("jquery-1.4.4.min.js"),
data.url("panel.js")]
data.url("panel.js")],
onHide: handleHide
});
reddit_panel.port.on("click", function(url) {
require("sdk/tabs").open(url);
});
require("sdk/widget").Widget({
let button = ToggleButton({
id: "open-reddit-btn",
label: "Reddit",
contentURL: "http://www.reddit.com/static/favicon.ico",
panel: reddit_panel
icon: base64png,
onChange: handleChange
});
exports.main = function(options, callbacks) {
@ -29,3 +37,13 @@ exports.main = function(options, callbacks) {
if (options.staticArgs.quitWhenDone)
callbacks.quit();
};
function handleChange(state) {
if (state.checked) {
reddit_panel.show({ position: button });
}
}
function handleHide() {
button.state('window', { checked: false });
}

View File

@ -3,9 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Disable tests below for now.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
/*
var m = require("main");
var self = require("sdk/self");
@ -23,4 +20,3 @@ exports.testMain = function(test) {
exports.testData = function(test) {
test.assert(self.data.load("panel.js").length > 0);
};
*/

View File

@ -107,7 +107,9 @@ const ContentWorker = Object.freeze({
error: pipe.emit.bind(null, "console", "error"),
debug: pipe.emit.bind(null, "console", "debug"),
exception: pipe.emit.bind(null, "console", "exception"),
trace: pipe.emit.bind(null, "console", "trace")
trace: pipe.emit.bind(null, "console", "trace"),
time: pipe.emit.bind(null, "console", "time"),
timeEnd: pipe.emit.bind(null, "console", "timeEnd")
});
},

View File

@ -31,6 +31,9 @@ let detachFrom = method("detatchFrom");
exports.detachFrom = detachFrom;
function attach(modification, target) {
if (!modification)
return;
let window = getTargetWindow(target);
attachTo(modification, window);
@ -42,6 +45,9 @@ function attach(modification, target) {
exports.attach = attach;
function detach(modification, target) {
if (!modification)
return;
if (target) {
let window = getTargetWindow(target);
detachFrom(modification, window);

View File

@ -67,3 +67,8 @@ function load(sandbox, uri) {
}
}
exports.load = load;
/**
* Forces the given `sandbox` to be freed immediately.
*/
exports.nuke = Cu.nukeSandbox

View File

@ -201,6 +201,10 @@ function createWorker (mod, window) {
contentScript: mod.contentScript,
contentScriptFile: mod.contentScriptFile,
contentScriptOptions: mod.contentScriptOptions,
// Bug 980468: Syntax errors from scripts can happen before the worker
// can set up an error handler. They are per-mod rather than per-worker
// so are best handled at the mod level.
onError: (e) => emit(mod, 'error', e)
});
workers.set(mod, worker);
pipe(worker, mod);

View File

@ -18,7 +18,7 @@ const { isPrivateBrowsingSupported } = require('./self');
const { isWindowPBSupported } = require('./private-browsing/utils');
const { Class } = require("./core/heritage");
const { merge } = require("./util/object");
const { WorkerHost, detach, attach, destroy } = require("./content/utils");
const { WorkerHost } = require("./content/utils");
const { Worker } = require("./content/worker");
const { Disposable } = require("./core/disposable");
const { WeakReference } = require('./core/reference');
@ -34,6 +34,8 @@ const { getNodeView, getActiveView } = require("./view/core");
const { isNil, isObject, isNumber } = require("./lang/type");
const { getAttachEventType } = require("./content/utils");
const { number, boolean, object } = require('./deprecated/api-utils');
const { Style } = require("./stylesheet/style");
const { attach, detach } = require("./content/mod");
let isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
some(value => isNumber(value) && !isNaN(value));
@ -63,7 +65,16 @@ let displayContract = contract({
position: position
});
let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
let panelContract = contract(merge({
// contentStyle* / contentScript* are sharing the same validation constraints,
// so they can be mostly reused, except for the messages.
contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
msg: 'The `contentStyle` option must be a string or an array of strings.'
}),
contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
})
}, displayContract.rules, loaderContract.rules));
function isDisposed(panel) !views.has(panel);
@ -72,12 +83,13 @@ let panels = new WeakMap();
let models = new WeakMap();
let views = new WeakMap();
let workers = new WeakMap();
let styles = new WeakMap();
function viewFor(panel) views.get(panel)
function modelFor(panel) models.get(panel)
function panelFor(view) panels.get(view)
function workerFor(panel) workers.get(panel)
const viewFor = (panel) => views.get(panel);
const modelFor = (panel) => models.get(panel);
const panelFor = (view) => panels.get(view);
const workerFor = (panel) => workers.get(panel);
const styleFor = (panel) => styles.get(panel);
// Utility function takes `panel` instance and makes sure it will be
// automatically hidden as soon as other panel is shown.
@ -125,6 +137,12 @@ const Panel = Class({
}, panelContract(options));
models.set(this, model);
if (model.contentStyle || model.contentStyleFile) {
styles.set(this, Style({
uri: model.contentStyleFile,
source: model.contentStyle
}));
}
// Setup view
let view = domPanel.make();
@ -148,7 +166,8 @@ const Panel = Class({
this.hide();
off(this);
destroy(workerFor(this));
workerFor(this).destroy();
detach(styleFor(this));
domPanel.dispose(viewFor(this));
@ -177,7 +196,7 @@ const Panel = Class({
domPanel.setURL(viewFor(this), model.contentURL);
// Detach worker so that messages send will be queued until it's
// reatached once panel content is ready.
detach(workerFor(this));
workerFor(this).detach();
},
/* Public API: Panel.isShowing */
@ -262,12 +281,25 @@ let hides = filter(panelEvents, ({type}) => type === "popuphidden");
let ready = filter(panelEvents, ({type, target}) =>
getAttachEventType(modelFor(panelFor(target))) === type);
// Styles should be always added as soon as possible, and doesn't makes them
// depends on `contentScriptWhen`
let start = filter(panelEvents, ({type}) => type === "document-element-inserted");
// Forward panel show / hide events to panel's own event listeners.
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));
attach(worker, domPanel.getContentDocument(target).defaultView);
on(ready, "data", ({target}) => {
let panel = panelFor(target);
let window = domPanel.getContentDocument(target).defaultView;
workerFor(panel).attach(window);
});
on(start, "data", ({target}) => {
let panel = panelFor(target);
let window = domPanel.getContentDocument(target).defaultView;
attach(styleFor(panel), window);
});

View File

@ -109,17 +109,21 @@ exports.pathFor = function pathFor(id) {
*/
exports.platform = runtime.OS.toLowerCase();
const [, architecture, compiler] = runtime.XPCOMABI ?
runtime.XPCOMABI.match(/^([^-]*)-(.*)$/) :
[, null, null];
/**
* What processor architecture you're running on:
* `'arm', 'ia32', or 'x64'`.
*/
exports.architecture = runtime.XPCOMABI.split('_')[0];
exports.architecture = architecture;
/**
* What compiler used for build:
* `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...`
*/
exports.compiler = runtime.XPCOMABI.split('_')[1];
exports.compiler = compiler;
/**
* The application's build ID/date, for example "2004051604".

View File

@ -8,12 +8,13 @@ module.metadata = {
'stability': 'unstable'
};
const { Cc, Ci } = require('chrome');
const { Cc, Ci, Cu } = require('chrome');
const { Unknown } = require('../platform/xpcom');
const { Class } = require('../core/heritage');
const { ns } = require('../core/namespace');
const { addObserver, removeObserver, notifyObservers } =
Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
const unloadSubject = require('@loader/unload');
const Subject = Class({
extends: Unknown,
@ -94,6 +95,10 @@ function on(type, listener, strong) {
let observer = Observer(listener);
observers[type] = observer;
addObserver(observer, type, weak);
// WeakRef gymnastics to remove all alive observers on unload
let ref = Cu.getWeakReference(observer);
weakRefs.set(observer, ref);
stillAlive.set(ref, type);
}
}
exports.on = on;
@ -120,6 +125,31 @@ function off(type, listener) {
let observer = observers[type];
delete observers[type];
removeObserver(observer, type);
stillAlive.delete(weakRefs.get(observer));
}
}
exports.off = off;
// must use WeakMap to keep reference to all the WeakRefs (!), see bug 986115
let weakRefs = new WeakMap();
// and we're out of beta, we're releasing on time!
let stillAlive = new Map();
on('sdk:loader:destroy', function onunload({ subject, data: reason }) {
// using logic from ./unload, to avoid a circular module reference
if (subject.wrappedJSObject === unloadSubject) {
off('sdk:loader:destroy', onunload);
// don't bother
if (reason === 'shutdown')
return;
stillAlive.forEach( (type, ref) => {
let observer = ref.get();
if (observer)
removeObserver(observer, type);
})
}
// a strong reference
}, true);

View File

@ -62,9 +62,11 @@ exports.LoaderWithHookedConsole = function (module, callback) {
error: hook.bind("error"),
debug: hook.bind("debug"),
exception: hook.bind("exception"),
time: hook.bind("time"),
timeEnd: hook.bind("timeEnd"),
__exposedProps__: {
log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
exception: "rw"
exception: "rw", time: "rw", timeEnd: "rw"
}
}
}),
@ -105,9 +107,11 @@ exports.LoaderWithFilteredConsole = function (module, callback) {
error: hook.bind("error"),
debug: hook.bind("debug"),
exception: hook.bind("exception"),
time: hook.bind("time"),
timeEnd: hook.bind("timeEnd"),
__exposedProps__: {
log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
exception: "rw"
exception: "rw", time: "rw", timeEnd: "rw"
}
}
});

View File

@ -8,6 +8,7 @@ const { isLocalURL } = require('../../url');
const { isNil, isObject, isString } = require('../../lang/type');
const { required, either, string, boolean, object } = require('../../deprecated/api-utils');
const { merge } = require('../../util/object');
const { freeze } = Object;
function isIconSet(icons) {
return Object.keys(icons).
@ -16,6 +17,7 @@ function isIconSet(icons) {
let iconSet = {
is: either(object, string),
map: v => isObject(v) ? freeze(merge({}, v)) : v,
ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
msg: 'The option "icon" must be a local URL or an object with ' +
'numeric keys / local URL values pair.'

View File

@ -253,9 +253,11 @@ const Sidebar = Class({
remove(sidebars, this);
// stop tracking windows
internals.tracker.unload();
internals.tracker = null;
if (internals.tracker) {
internals.tracker.unload();
}
internals.tracker = null;
internals.windowNS = null;
views.delete(this);

View File

@ -382,8 +382,8 @@ exports["test:ensure console.xxx works in cs"] = WorkerTest(
let calls = [];
function onMessage(type, msg) {
assert.equal(type, msg,
"console.xxx(\"xxx\"), i.e. message is equal to the " +
"console method name we are calling");
"console.xxx(\"xxx\"), i.e. message is equal to the " +
"console method name we are calling");
calls.push(msg);
}
@ -391,19 +391,23 @@ exports["test:ensure console.xxx works in cs"] = WorkerTest(
let worker = loader.require("sdk/content/worker").Worker({
window: browser.contentWindow,
contentScript: "new " + function WorkerScope() {
console.time("time");
console.log("log");
console.info("info");
console.warn("warn");
console.error("error");
console.debug("debug");
console.exception("exception");
console.timeEnd("timeEnd");
self.postMessage();
},
onMessage: function() {
// Ensure that console methods are called in the same execution order
const EXPECTED_CALLS = ["time", "log", "info", "warn", "error",
"debug", "exception", "timeEnd"];
assert.equal(JSON.stringify(calls),
JSON.stringify(["log", "info", "warn", "error", "debug", "exception"]),
"console has been called successfully, in the expected order");
JSON.stringify(EXPECTED_CALLS),
"console methods have been called successfully, in expected order");
done();
}
});

View File

@ -957,7 +957,7 @@ exports.testPageModCss = function(assert, done) {
'data:text/html;charset=utf-8,<div style="background: silver">css test</div>', [{
include: ["*", "data:*"],
contentStyle: "div { height: 100px; }",
contentStyleFile: data.url("pagemod-css-include-file.css")
contentStyleFile: data.url("css-include-file.css")
}],
function(win, done) {
let div = win.document.querySelector("div");
@ -1531,4 +1531,32 @@ exports.testDetachOnUnload = function(assert, done) {
})
}
exports.testSyntaxErrorInContentScript = function(assert, done) {
const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript";
let hitError = null;
let attached = false;
testPageMod(assert, done, url, [{
include: url,
contentScript: 'console.log(23',
onAttach: function() {
attached = true;
},
onError: function(e) {
hitError = e;
}
}],
function(win, done) {
assert.ok(attached, "The worker was attached.");
assert.notStrictEqual(hitError, null, "The syntax error was reported.");
if (hitError)
assert.equal(hitError.name, "SyntaxError", "The error thrown should be a SyntaxError");
done();
}
);
};
require('sdk/test').run(exports);

View File

@ -25,6 +25,8 @@ const { URL } = require('sdk/url');
const fixtures = require('./fixtures')
const SVG_URL = fixtures.url('mofo_logo.SVG');
const CSS_URL = fixtures.url('css-include-file.css');
const Isolate = fn => '(' + fn + ')()';
function ignorePassingDOMNodeWarning(type, message) {
@ -974,6 +976,88 @@ exports['test panel can be constructed without any arguments'] = function (asser
assert.ok(true, "Creating a panel with no arguments does not throw");
};
exports['test panel CSS'] = function(assert, done) {
const loader = Loader(module);
const { Panel } = loader.require('sdk/panel');
const { getActiveView } = loader.require('sdk/view/core');
const getContentWindow = panel =>
getActiveView(panel).querySelector('iframe').contentWindow;
let panel = Panel({
contentURL: 'data:text/html;charset=utf-8,' +
'<div style="background: silver">css test</div>',
contentStyle: 'div { height: 100px; }',
contentStyleFile: CSS_URL,
onShow: () => {
ready(getContentWindow(panel)).then(({ document }) => {
let div = document.querySelector('div');
assert.equal(div.clientHeight, 100, 'Panel contentStyle worked');
assert.equal(div.offsetHeight, 120, 'Panel contentStyleFile worked');
loader.unload();
done();
}).then(null, assert.fail);
}
});
panel.show();
};
exports['test panel CSS list'] = function(assert, done) {
const loader = Loader(module);
const { Panel } = loader.require('sdk/panel');
const { getActiveView } = loader.require('sdk/view/core');
const getContentWindow = panel =>
getActiveView(panel).querySelector('iframe').contentWindow;
let panel = Panel({
contentURL: 'data:text/html;charset=utf-8,' +
'<div style="width:320px; max-width: 480px!important">css test</div>',
contentStyleFile: [
// Highlight evaluation order in this list
"data:text/css;charset=utf-8,div { border: 1px solid black; }",
"data:text/css;charset=utf-8,div { border: 10px solid black; }",
// Highlight evaluation order between contentStylesheet & contentStylesheetFile
"data:text/css;charset=utf-8s,div { height: 1000px; }",
// Highlight precedence between the author and user style sheet
"data:text/css;charset=utf-8,div { width: 200px; max-width: 640px!important}",
],
contentStyle: [
"div { height: 10px; }",
"div { height: 100px; }"
],
onShow: () => {
ready(getContentWindow(panel)).then(({ window, document }) => {
let div = document.querySelector('div');
let style = window.getComputedStyle(div);
assert.equal(div.clientHeight, 100,
'Panel contentStyle list is evaluated after contentStyleFile');
assert.equal(div.offsetHeight, 120,
'Panel contentStyleFile list works');
assert.equal(style.width, '320px',
'add-on author/page author stylesheet precedence works');
assert.equal(style.maxWidth, '480px',
'add-on author/page author stylesheet !important precedence works');
loader.unload();
done();
}).then(null, assert.fail);
}
});
panel.show();
};
if (isWindowPBSupported) {
exports.testGetWindow = function(assert, done) {
let activeWindow = getMostRecentBrowserWindow();

View File

@ -2,7 +2,7 @@
* 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/. */
const { sandbox, load, evaluate } = require('sdk/loader/sandbox');
const { sandbox, load, evaluate, nuke } = require('sdk/loader/sandbox');
const xulApp = require("sdk/system/xul-app");
const fixturesURI = module.uri.split('test-sandbox.js')[0] + 'fixtures/';
@ -137,4 +137,30 @@ exports['test metadata'] = function(assert) {
let self = require('sdk/self');
}
exports['test nuke sandbox'] = function(assert) {
let fixture = sandbox('http://example.com');
fixture.foo = 'foo';
let ref = evaluate(fixture, 'let a = {bar: "bar"}; a');
nuke(fixture);
assert.ok(Cu.isDeadWrapper(fixture), 'sandbox should be dead');
assert.throws(
() => fixture.foo,
/can't access dead object/,
'property of nuked sandbox should not be accessible'
);
assert.ok(Cu.isDeadWrapper(ref), 'ref to object from sandbox should be dead');
assert.throws(
() => ref.bar,
/can't access dead object/,
'object from nuked sandbox should not be alive'
);
}
require('test').run(exports);

View File

@ -119,6 +119,30 @@ exports["test listeners are GC-ed"] = function(assert, done) {
});
};
exports["test alive listeners are removed on unload"] = function(assert) {
let receivedFromWeak = [];
let receivedFromStrong = [];
let loader = Loader(module);
let events = loader.require('sdk/system/events');
let type = 'test-alive-listeners-are-removed';
const handler = (event) => receivedFromStrong.push(event);
const weakHandler = (event) => receivedFromWeak.push(event);
events.on(type, handler, true);
events.on(type, weakHandler);
events.emit(type, { data: 1 });
assert.equal(receivedFromStrong.length, 1, "strong listener invoked");
assert.equal(receivedFromWeak.length, 1, "weak listener invoked");
loader.unload();
events.emit(type, { data: 2 });
assert.equal(receivedFromWeak.length, 1, "weak listener was removed");
assert.equal(receivedFromStrong.length, 1, "strong listener was removed");
};
exports["test handle nsIObserverService notifications"] = function(assert) {
let ios = Cc['@mozilla.org/network/io-service;1']
.getService(Ci.nsIIOService);

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var runtime = require("sdk/system/runtime");
const runtime = require("sdk/system/runtime");
exports["test system runtime"] = function(assert) {
assert.equal(typeof(runtime.inSafeMode), "boolean",
@ -14,7 +14,7 @@ exports["test system runtime"] = function(assert) {
"runtime.processType is a number");
assert.equal(typeof(runtime.widgetToolkit), "string",
"runtime.widgetToolkit is string");
var XPCOMABI = typeof(runtime.XPCOMABI);
const XPCOMABI = runtime.XPCOMABI;
assert.ok(XPCOMABI === null || typeof(XPCOMABI) === "string",
"runtime.XPCOMABI is string or null if not supported by platform");
};

View File

@ -0,0 +1,37 @@
/* 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 runtime = require("sdk/system/runtime");
const system = require("sdk/system");
exports["test system architecture and compiler"] = function(assert) {
if (system.architecture !== null) {
assert.equal(
runtime.XPCOMABI.indexOf(system.architecture), 0,
"system.architecture is starting substring of runtime.XPCOMABI"
);
}
if (system.compiler !== null) {
assert.equal(
runtime.XPCOMABI.indexOf(system.compiler),
runtime.XPCOMABI.length - system.compiler.length,
"system.compiler is trailing substring of runtime.XPCOMABI"
);
}
assert.ok(
system.architecture === null || typeof(system.architecture) === "string",
"system.architecture is string or null if not supported by platform"
);
assert.ok(
system.compiler === null || typeof(system.compiler) === "string",
"system.compiler is string or null if not supported by platform"
);
};
require("test").run(exports);

View File

@ -835,6 +835,44 @@ exports['test button state are snapshot'] = function(assert) {
loader.unload();
}
exports['test button icon object is a snapshot'] = function(assert) {
let loader = Loader(module);
let { ActionButton } = loader.require('sdk/ui');
let icon = {
'16': './foo.png'
};
let button = ActionButton({
id: 'my-button-17',
label: 'my button',
icon: icon
});
assert.deepEqual(button.icon, icon,
'button.icon has the same properties of the object set in the constructor');
assert.notEqual(button.icon, icon,
'button.icon is not the same object of the object set in the constructor');
assert.throws(
() => button.icon[16] = './bar.png',
/16 is read-only/,
'properties of button.icon are ready-only'
);
let newIcon = {'16': './bar.png'};
button.icon = newIcon;
assert.deepEqual(button.icon, newIcon,
'button.icon has the same properties of the object set');
assert.notEqual(button.icon, newIcon,
'button.icon is not the same object of the object set');
loader.unload();
}
exports['test button after destroy'] = function(assert) {
let loader = Loader(module);
let { ActionButton } = loader.require('sdk/ui');

View File

@ -844,6 +844,44 @@ exports['test button state are snapshot'] = function(assert) {
loader.unload();
}
exports['test button icon object is a snapshot'] = function(assert) {
let loader = Loader(module);
let { ToggleButton } = loader.require('sdk/ui');
let icon = {
'16': './foo.png'
};
let button = ToggleButton({
id: 'my-button-17',
label: 'my button',
icon: icon
});
assert.deepEqual(button.icon, icon,
'button.icon has the same properties of the object set in the constructor');
assert.notEqual(button.icon, icon,
'button.icon is not the same object of the object set in the constructor');
assert.throws(
() => button.icon[16] = './bar.png',
/16 is read-only/,
'properties of button.icon are ready-only'
);
let newIcon = {'16': './bar.png'};
button.icon = newIcon;
assert.deepEqual(button.icon, newIcon,
'button.icon has the same properties of the object set');
assert.notEqual(button.icon, newIcon,
'button.icon is not the same object of the object set');
loader.unload();
}
exports['test button after destroy'] = function(assert) {
let loader = Loader(module);
let { ToggleButton } = loader.require('sdk/ui');