mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central a=merge
This commit is contained in:
commit
6bb48ca9de
@ -194,9 +194,9 @@ const Frame = Class({
|
||||
dispose: function() {
|
||||
emit(this, 'detach', this);
|
||||
ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
|
||||
ns(this).messageManager = null;
|
||||
|
||||
frameMap.delete(ns(this).messageManager);
|
||||
ns(this).messageManager = null;
|
||||
},
|
||||
|
||||
// Returns the browser or iframe element this frame displays in
|
||||
|
@ -318,15 +318,8 @@ var RemoteTabViewer = {
|
||||
}
|
||||
}
|
||||
|
||||
// if Clients hasn't synced yet this session, we need to sync it as well.
|
||||
if (Weave.Service.clientsEngine.lastSync == 0) {
|
||||
Weave.Service.clientsEngine.sync();
|
||||
}
|
||||
|
||||
// Force a sync only for the tabs engine
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
engine.lastModified = null;
|
||||
engine.sync();
|
||||
// Ask Sync to just do the tabs engine if it can.
|
||||
Weave.Service.sync(["tabs"]);
|
||||
Services.prefs.setIntPref("services.sync.lastTabFetch",
|
||||
Math.floor(Date.now() / 1000));
|
||||
|
||||
|
@ -505,6 +505,7 @@ tags = mcb
|
||||
skip-if = e10s # Bug 1100687 - test directly manipulates content (content.document.getElementById)
|
||||
[browser_bug1045809.js]
|
||||
tags = mcb
|
||||
[browser_bug1225194-remotetab.js]
|
||||
[browser_e10s_switchbrowser.js]
|
||||
[browser_e10s_about_process.js]
|
||||
[browser_e10s_chrome_process.js]
|
||||
|
@ -0,0 +1,18 @@
|
||||
add_task(function* test_remotetab_opens() {
|
||||
const url = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
|
||||
yield BrowserTestUtils.withNewTab({url: "about:robots", gBrowser}, function* () {
|
||||
// Set the urlbar to include the moz-action
|
||||
gURLBar.value = "moz-action:remotetab," + JSON.stringify({ url });
|
||||
// Focus the urlbar so we can press enter
|
||||
gURLBar.focus();
|
||||
|
||||
// The URL is going to open in the current tab as it is currently about:blank
|
||||
let promiseTabLoaded = promiseTabLoadEvent(gBrowser.selectedTab);
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
|
||||
yield promiseTabLoaded;
|
||||
|
||||
Assert.equal(gBrowser.selectedTab.linkedBrowser.currentURI.spec, url, "correct URL loaded");
|
||||
});
|
||||
});
|
@ -4,6 +4,8 @@
|
||||
* 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/. */
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
const TEST_URL_BASES = [
|
||||
"http://example.org/browser/browser/base/content/test/general/dummy_page.html#tabmatch",
|
||||
"http://example.org/browser/browser/base/content/test/general/moz.png#tabmatch"
|
||||
|
@ -356,6 +356,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
gBrowser.removeTab(prevTab);
|
||||
return;
|
||||
}
|
||||
} else if (action.type == "remotetab") {
|
||||
url = action.params.url;
|
||||
} else if (action.type == "keyword") {
|
||||
url = action.params.url;
|
||||
} else if (action.type == "searchengine") {
|
||||
|
@ -65,7 +65,10 @@ global.IconDetails = {
|
||||
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
||||
extension.principal, url,
|
||||
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
||||
} catch (e if !context) {
|
||||
} catch (e) {
|
||||
if (context) {
|
||||
throw e;
|
||||
}
|
||||
// If there's no context, it's because we're handling this
|
||||
// as a manifest directive. Log a warning rather than
|
||||
// raising an error, but don't accept the URL in any case.
|
||||
@ -172,7 +175,10 @@ global.openPanel = (node, popupURL, extension) => {
|
||||
GlobalManager.injectInDocShell(browser.docShell, extension, context);
|
||||
browser.setAttribute("src", context.uri.spec);
|
||||
|
||||
let contentLoadListener = () => {
|
||||
let contentLoadListener = event => {
|
||||
if (event.target != browser.contentDocument) {
|
||||
return;
|
||||
}
|
||||
browser.removeEventListener("load", contentLoadListener, true);
|
||||
|
||||
let contentViewer = browser.docShell.contentViewer;
|
||||
@ -343,7 +349,7 @@ global.TabManager = {
|
||||
if (!window.gBrowser) {
|
||||
return [];
|
||||
}
|
||||
return [ for (tab of window.gBrowser.tabs) this.convert(extension, tab) ];
|
||||
return Array.map(window.gBrowser.tabs, tab => this.convert(extension, tab));
|
||||
},
|
||||
};
|
||||
|
||||
@ -459,8 +465,8 @@ global.WindowListManager = {
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
event.currentTarget.removeEventListener(event.type, this);
|
||||
let window = event.target.defaultView;
|
||||
window.removeEventListener("load", this);
|
||||
if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
|
||||
return;
|
||||
}
|
||||
|
@ -694,8 +694,8 @@ pref("ui.scrolling.overscroll_snap_limit", -1);
|
||||
pref("ui.scrolling.min_scrollable_distance", -1);
|
||||
// The axis lock mode for panning behaviour - set between standard, free and sticky
|
||||
pref("ui.scrolling.axis_lock_mode", "standard");
|
||||
// Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
||||
pref("ui.scrolling.negate_wheel_scrollY", true);
|
||||
// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
||||
pref("ui.scrolling.negate_wheel_scroll", true);
|
||||
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
|
||||
// auto-detect based on reported hardware values
|
||||
pref("ui.scrolling.gamepad_dead_zone", 115);
|
||||
|
@ -689,8 +689,8 @@ pref("ui.scrolling.overscroll_snap_limit", -1);
|
||||
pref("ui.scrolling.min_scrollable_distance", -1);
|
||||
// The axis lock mode for panning behaviour - set between standard, free and sticky
|
||||
pref("ui.scrolling.axis_lock_mode", "standard");
|
||||
// Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
||||
pref("ui.scrolling.negate_wheel_scrollY", true);
|
||||
// Negate scroll, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
|
||||
pref("ui.scrolling.negate_wheel_scroll", true);
|
||||
// Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
|
||||
// auto-detect based on reported hardware values
|
||||
pref("ui.scrolling.gamepad_dead_zone", 115);
|
||||
|
@ -1180,7 +1180,7 @@ public class GeckoAppShell
|
||||
context.getResources().getString(R.string.share_title));
|
||||
}
|
||||
|
||||
final Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
|
||||
Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
|
||||
if (!TextUtils.isEmpty(mimeType)) {
|
||||
Intent intent = getIntentForActionString(action);
|
||||
intent.setDataAndType(uri, mimeType);
|
||||
@ -1226,9 +1226,9 @@ public class GeckoAppShell
|
||||
return intent;
|
||||
}
|
||||
|
||||
// Have a special handling for SMS, as the message body
|
||||
// is not extracted from the URI automatically.
|
||||
if (!"sms".equals(scheme) && !"smsto".equals(scheme)) {
|
||||
// Have a special handling for SMS based schemes, as the query parameters
|
||||
// are not extracted from the URI automatically.
|
||||
if (!"sms".equals(scheme) && !"smsto".equals(scheme) && !"mms".equals(scheme) && !"mmsto".equals(scheme)) {
|
||||
return intent;
|
||||
}
|
||||
|
||||
@ -1237,27 +1237,44 @@ public class GeckoAppShell
|
||||
return intent;
|
||||
}
|
||||
|
||||
final String[] fields = query.split("&");
|
||||
boolean foundBody = false;
|
||||
String resultQuery = "";
|
||||
for (String field : fields) {
|
||||
if (foundBody || !field.startsWith("body=")) {
|
||||
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found the first body param. Put it into the intent.
|
||||
final String body = Uri.decode(field.substring(5));
|
||||
intent.putExtra("sms_body", body);
|
||||
foundBody = true;
|
||||
// It is common to see sms*/mms* uris on the web without '//', it is W3C standard not to have the slashes,
|
||||
// but android's Uri builder & Uri require the slashes and will interpret those without as malformed.
|
||||
String currentUri = uri.toString();
|
||||
String correctlyFormattedDataURIScheme = scheme + "://";
|
||||
if (!currentUri.contains(correctlyFormattedDataURIScheme)) {
|
||||
uri = Uri.parse(currentUri.replaceFirst(scheme + ":", correctlyFormattedDataURIScheme));
|
||||
}
|
||||
|
||||
if (!foundBody) {
|
||||
final String[] fields = query.split("&");
|
||||
boolean shouldUpdateIntent = false;
|
||||
String resultQuery = "";
|
||||
for (String field : fields) {
|
||||
if (field.startsWith("body=")) {
|
||||
final String body = Uri.decode(field.substring(5));
|
||||
intent.putExtra("sms_body", body);
|
||||
shouldUpdateIntent = true;
|
||||
} else if (field.startsWith("subject=")) {
|
||||
final String subject = Uri.decode(field.substring(8));
|
||||
intent.putExtra("subject", subject);
|
||||
shouldUpdateIntent = true;
|
||||
} else if (field.startsWith("cc=")) {
|
||||
final String ccNumber = Uri.decode(field.substring(3));
|
||||
String phoneNumber = uri.getAuthority();
|
||||
if (phoneNumber != null) {
|
||||
uri = uri.buildUpon().encodedAuthority(phoneNumber + ";" + ccNumber).build();
|
||||
}
|
||||
shouldUpdateIntent = true;
|
||||
} else {
|
||||
resultQuery = resultQuery.concat(resultQuery.length() > 0 ? "&" + field : field);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdateIntent) {
|
||||
// No need to rewrite the URI, then.
|
||||
return intent;
|
||||
}
|
||||
|
||||
// Form a new URI without the body field in the query part, and
|
||||
// Form a new URI without the extracted fields in the query part, and
|
||||
// push that into the new Intent.
|
||||
final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
|
||||
final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
|
||||
|
@ -22,6 +22,7 @@ import org.mozilla.gecko.util.ThreadUtils;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
@ -120,14 +121,16 @@ class JavaPanZoomController
|
||||
private AxisLockMode mMode;
|
||||
/* Whether or not to wait for a double-tap before dispatching a single-tap */
|
||||
private boolean mWaitForDoubleTap;
|
||||
/* Used to change the scrollY direction */
|
||||
private boolean mNegateWheelScrollY;
|
||||
/* Used to change the scroll direction */
|
||||
private boolean mNegateWheelScroll;
|
||||
/* Whether the current event has been default-prevented. */
|
||||
private boolean mDefaultPrevented;
|
||||
/* Whether longpress events are enabled, or suppressed by robocop tests. */
|
||||
private boolean isLongpressEnabled;
|
||||
/* Whether longpress detection should be ignored */
|
||||
private boolean mIgnoreLongPress;
|
||||
/* Pointer scrolling delta, scaled by the preferred list item height which matches Android platform behavior */
|
||||
private float mPointerScrollFactor;
|
||||
|
||||
// Handler to be notified when overscroll occurs
|
||||
private Overscroll mOverscroll;
|
||||
@ -153,7 +156,7 @@ class JavaPanZoomController
|
||||
mMode = AxisLockMode.STANDARD;
|
||||
|
||||
String[] prefs = { "ui.scrolling.axis_lock_mode",
|
||||
"ui.scrolling.negate_wheel_scrollY",
|
||||
"ui.scrolling.negate_wheel_scroll",
|
||||
"ui.scrolling.gamepad_dead_zone" };
|
||||
PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
|
||||
@Override public void prefValue(String pref, String value) {
|
||||
@ -175,8 +178,8 @@ class JavaPanZoomController
|
||||
}
|
||||
|
||||
@Override public void prefValue(String pref, boolean value) {
|
||||
if (pref.equals("ui.scrolling.negate_wheel_scrollY")) {
|
||||
mNegateWheelScrollY = value;
|
||||
if (pref.equals("ui.scrolling.negate_wheel_scroll")) {
|
||||
mNegateWheelScroll = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,6 +191,13 @@ class JavaPanZoomController
|
||||
});
|
||||
|
||||
Axis.initPrefs();
|
||||
|
||||
TypedValue outValue = new TypedValue();
|
||||
if (view.getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
|
||||
mPointerScrollFactor = outValue.getDimension(view.getContext().getResources().getDisplayMetrics());
|
||||
} else {
|
||||
mPointerScrollFactor = MAX_SCROLL;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -577,10 +587,11 @@ class JavaPanZoomController
|
||||
if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) {
|
||||
float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
|
||||
float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
if (mNegateWheelScrollY) {
|
||||
if (mNegateWheelScroll) {
|
||||
scrollX *= -1.0;
|
||||
scrollY *= -1.0;
|
||||
}
|
||||
scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL);
|
||||
scrollBy(scrollX * mPointerScrollFactor, scrollY * mPointerScrollFactor);
|
||||
bounce();
|
||||
return true;
|
||||
}
|
||||
|
@ -34,9 +34,7 @@ var modules = {
|
||||
},
|
||||
|
||||
rights: {
|
||||
uri: AppConstants.MOZ_OFFICIAL_BRANDING ?
|
||||
"chrome://browser/content/aboutRights.xhtml" :
|
||||
"chrome://global/content/aboutRights.xhtml",
|
||||
uri: "chrome://browser/content/aboutRights.xhtml",
|
||||
privileged: false
|
||||
},
|
||||
blocked: {
|
||||
|
@ -1268,7 +1268,7 @@ Sync11Service.prototype = {
|
||||
return reason;
|
||||
},
|
||||
|
||||
sync: function sync() {
|
||||
sync: function sync(engineNamesToSync) {
|
||||
if (!this.enabled) {
|
||||
this._log.debug("Not syncing as Sync is disabled.");
|
||||
return;
|
||||
@ -1288,14 +1288,14 @@ Sync11Service.prototype = {
|
||||
else {
|
||||
this._log.trace("In sync: no need to login.");
|
||||
}
|
||||
return this._lockedSync.apply(this, arguments);
|
||||
return this._lockedSync(engineNamesToSync);
|
||||
})();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync up engines with the server.
|
||||
*/
|
||||
_lockedSync: function _lockedSync() {
|
||||
_lockedSync: function _lockedSync(engineNamesToSync) {
|
||||
return this._lock("service.js: sync",
|
||||
this._notify("sync", "", function onNotify() {
|
||||
|
||||
@ -1306,7 +1306,7 @@ Sync11Service.prototype = {
|
||||
let cb = Async.makeSpinningCallback();
|
||||
synchronizer.onComplete = cb;
|
||||
|
||||
synchronizer.sync();
|
||||
synchronizer.sync(engineNamesToSync);
|
||||
// wait() throws if the first argument is truthy, which is exactly what
|
||||
// we want.
|
||||
let result = cb.wait();
|
||||
|
@ -31,7 +31,7 @@ this.EngineSynchronizer = function EngineSynchronizer(service) {
|
||||
}
|
||||
|
||||
EngineSynchronizer.prototype = {
|
||||
sync: function sync() {
|
||||
sync: function sync(engineNamesToSync) {
|
||||
if (!this.onComplete) {
|
||||
throw new Error("onComplete handler not installed.");
|
||||
}
|
||||
@ -96,6 +96,9 @@ EngineSynchronizer.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only honor the "hint" of what engines to Sync if this isn't
|
||||
// a first sync.
|
||||
let allowEnginesHint = false;
|
||||
// Wipe data in the desired direction if necessary
|
||||
switch (Svc.Prefs.get("firstSync")) {
|
||||
case "resetClient":
|
||||
@ -107,6 +110,9 @@ EngineSynchronizer.prototype = {
|
||||
case "wipeRemote":
|
||||
this.service.wipeRemote(engineManager.enabledEngineNames);
|
||||
break;
|
||||
default:
|
||||
allowEnginesHint = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.service.clientsEngine.localCommands) {
|
||||
@ -143,8 +149,17 @@ EngineSynchronizer.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the engines to sync has been specified, we sync in the order specified.
|
||||
let enginesToSync;
|
||||
if (allowEnginesHint && engineNamesToSync) {
|
||||
this._log.info("Syncing specified engines", engineNamesToSync);
|
||||
enginesToSync = engineManager.get(engineNamesToSync).filter(e => e.enabled);
|
||||
} else {
|
||||
this._log.info("Syncing all enabled engines.");
|
||||
enginesToSync = engineManager.getEnabled();
|
||||
}
|
||||
try {
|
||||
for (let engine of engineManager.getEnabled()) {
|
||||
for (let engine of enginesToSync) {
|
||||
// If there's any problems with syncing the engine, report the failure
|
||||
if (!(this._syncEngine(engine)) || this.service.status.enforceBackoff) {
|
||||
this._log.info("Aborting sync for failure in " + engine.name);
|
||||
|
159
services/sync/tests/unit/test_service_sync_specified.js
Normal file
159
services/sync/tests/unit/test_service_sync_specified.js
Normal file
@ -0,0 +1,159 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://services-sync/constants.js");
|
||||
Cu.import("resource://services-sync/engines.js");
|
||||
Cu.import("resource://services-sync/engines/clients.js");
|
||||
Cu.import("resource://services-sync/record.js");
|
||||
Cu.import("resource://services-sync/service.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://testing-common/services/sync/utils.js");
|
||||
|
||||
initTestLogging();
|
||||
Service.engineManager.clear();
|
||||
|
||||
let syncedEngines = []
|
||||
|
||||
function SteamEngine() {
|
||||
SyncEngine.call(this, "Steam", Service);
|
||||
}
|
||||
SteamEngine.prototype = {
|
||||
__proto__: SyncEngine.prototype,
|
||||
_sync: function _sync() {
|
||||
syncedEngines.push(this.name);
|
||||
}
|
||||
};
|
||||
Service.engineManager.register(SteamEngine);
|
||||
|
||||
function StirlingEngine() {
|
||||
SyncEngine.call(this, "Stirling", Service);
|
||||
}
|
||||
StirlingEngine.prototype = {
|
||||
__proto__: SteamEngine.prototype,
|
||||
_sync: function _sync() {
|
||||
syncedEngines.push(this.name);
|
||||
}
|
||||
};
|
||||
Service.engineManager.register(StirlingEngine);
|
||||
|
||||
// Tracking info/collections.
|
||||
var collectionsHelper = track_collections_helper();
|
||||
var upd = collectionsHelper.with_updated_collection;
|
||||
|
||||
function sync_httpd_setup(handlers) {
|
||||
|
||||
handlers["/1.1/johndoe/info/collections"] = collectionsHelper.handler;
|
||||
delete collectionsHelper.collections.crypto;
|
||||
delete collectionsHelper.collections.meta;
|
||||
|
||||
let cr = new ServerWBO("keys");
|
||||
handlers["/1.1/johndoe/storage/crypto/keys"] =
|
||||
upd("crypto", cr.handler());
|
||||
|
||||
let cl = new ServerCollection();
|
||||
handlers["/1.1/johndoe/storage/clients"] =
|
||||
upd("clients", cl.handler());
|
||||
|
||||
return httpd_setup(handlers);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
syncedEngines = [];
|
||||
let engine = Service.engineManager.get("steam");
|
||||
engine.enabled = true;
|
||||
engine.syncPriority = 1;
|
||||
|
||||
engine = Service.engineManager.get("stirling");
|
||||
engine.enabled = true;
|
||||
engine.syncPriority = 2;
|
||||
|
||||
let server = sync_httpd_setup({
|
||||
"/1.1/johndoe/storage/meta/global": new ServerWBO("global", {}).handler(),
|
||||
});
|
||||
new SyncTestingInfrastructure(server, "johndoe", "ilovejane",
|
||||
"abcdeabcdeabcdeabcdeabcdea");
|
||||
return server;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
|
||||
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_noEngines() {
|
||||
_("Test: An empty array of engines to sync does nothing.");
|
||||
let server = setUp();
|
||||
|
||||
try {
|
||||
_("Sync with no engines specified.");
|
||||
Service.sync([]);
|
||||
deepEqual(syncedEngines, [], "no engines were synced");
|
||||
|
||||
} finally {
|
||||
Service.startOver();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_oneEngine() {
|
||||
_("Test: Only one engine is synced.");
|
||||
let server = setUp();
|
||||
|
||||
try {
|
||||
|
||||
_("Sync with 1 engine specified.");
|
||||
Service.sync(["steam"]);
|
||||
deepEqual(syncedEngines, ["steam"])
|
||||
|
||||
} finally {
|
||||
Service.startOver();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_bothEnginesSpecified() {
|
||||
_("Test: All engines are synced when specified in the correct order (1).");
|
||||
let server = setUp();
|
||||
|
||||
try {
|
||||
_("Sync with both engines specified.");
|
||||
Service.sync(["steam", "stirling"]);
|
||||
deepEqual(syncedEngines, ["steam", "stirling"])
|
||||
|
||||
} finally {
|
||||
Service.startOver();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_bothEnginesSpecified() {
|
||||
_("Test: All engines are synced when specified in the correct order (2).");
|
||||
let server = setUp();
|
||||
|
||||
try {
|
||||
_("Sync with both engines specified.");
|
||||
Service.sync(["stirling", "steam"]);
|
||||
deepEqual(syncedEngines, ["stirling", "steam"])
|
||||
|
||||
} finally {
|
||||
Service.startOver();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_bothEnginesDefault() {
|
||||
_("Test: All engines are synced when nothing is specified.");
|
||||
let server = setUp();
|
||||
|
||||
try {
|
||||
Service.sync();
|
||||
deepEqual(syncedEngines, ["steam", "stirling"])
|
||||
|
||||
} finally {
|
||||
Service.startOver();
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
});
|
@ -93,6 +93,7 @@ skip-if = os == "mac" || os == "linux"
|
||||
[test_service_sync_remoteSetup.js]
|
||||
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
|
||||
skip-if = os == "android"
|
||||
[test_service_sync_specified.js]
|
||||
[test_service_sync_updateEnabledEngines.js]
|
||||
# Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
|
||||
skip-if = os == "android"
|
||||
|
@ -6,6 +6,40 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
|
||||
var testDone = new Promise(resolve => { testResolve = resolve; });
|
||||
|
||||
var messageHandler = new Map();
|
||||
var messageAwaiter = new Map();
|
||||
|
||||
var messageQueue = [];
|
||||
|
||||
SimpleTest.registerCleanupFunction(() => {
|
||||
if (messageQueue.length) {
|
||||
SimpleTest.is(messageQueue.length, 0, "message queue is empty");
|
||||
}
|
||||
if (messageAwaiter.size) {
|
||||
SimpleTest.is(messageAwaiter.size, 0, "no tasks awaiting on messages");
|
||||
}
|
||||
});
|
||||
|
||||
function checkMessages() {
|
||||
while (messageQueue.length) {
|
||||
let [msg, ...args] = messageQueue[0];
|
||||
|
||||
let listener = messageAwaiter.get(msg);
|
||||
if (!listener) {
|
||||
break;
|
||||
}
|
||||
|
||||
messageQueue.shift();
|
||||
messageAwaiter.delete(msg);
|
||||
|
||||
listener.resolve(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function checkDuplicateListeners(msg) {
|
||||
if (messageHandler.has(msg) || messageAwaiter.has(msg)) {
|
||||
throw new Error("only one message handler allowed");
|
||||
}
|
||||
}
|
||||
|
||||
function testHandler(kind, pass, msg, ...args) {
|
||||
if (kind == "test-eq") {
|
||||
@ -29,11 +63,13 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
|
||||
|
||||
testMessage(msg, ...args) {
|
||||
var handler = messageHandler.get(msg);
|
||||
if (!handler) {
|
||||
return;
|
||||
if (handler) {
|
||||
handler(...args);
|
||||
} else {
|
||||
messageQueue.push([msg, ...args]);
|
||||
checkMessages();
|
||||
}
|
||||
|
||||
handler(...args);
|
||||
},
|
||||
};
|
||||
|
||||
@ -41,21 +77,15 @@ ExtensionTestUtils.loadExtension = function(ext, id = null)
|
||||
|
||||
extension.awaitMessage = (msg) => {
|
||||
return new Promise(resolve => {
|
||||
if (messageHandler.has(msg)) {
|
||||
throw new Error("only one message handler allowed");
|
||||
}
|
||||
checkDuplicateListeners(msg);
|
||||
|
||||
messageHandler.set(msg, (...args) => {
|
||||
messageHandler.delete(msg);
|
||||
resolve(...args);
|
||||
});
|
||||
messageAwaiter.set(msg, {resolve});
|
||||
checkMessages();
|
||||
});
|
||||
};
|
||||
|
||||
extension.onMessage = (msg, callback) => {
|
||||
if (messageHandler.has(msg)) {
|
||||
throw new Error("only one message handler allowed");
|
||||
}
|
||||
checkDuplicateListeners(msg);
|
||||
messageHandler.set(msg, callback);
|
||||
};
|
||||
|
||||
|
@ -66,6 +66,7 @@ var {
|
||||
injectAPI,
|
||||
extend,
|
||||
flushJarCache,
|
||||
instanceOf,
|
||||
} = ExtensionUtils;
|
||||
|
||||
const LOGGER_ID_BASE = "addons.webextension.";
|
||||
@ -322,6 +323,9 @@ var GlobalManager = {
|
||||
|
||||
let eventHandler = docShell.chromeEventHandler;
|
||||
let listener = event => {
|
||||
if (event.target != docShell.contentViewer.DOMDocument) {
|
||||
return;
|
||||
}
|
||||
eventHandler.removeEventListener("unload", listener);
|
||||
context.unload();
|
||||
};
|
||||
@ -343,10 +347,10 @@ this.ExtensionData = function(rootURI)
|
||||
|
||||
this.manifest = null;
|
||||
this.id = null;
|
||||
// Map(locale-name -> message-map)
|
||||
// Map(locale-name -> Map(message-key -> localized-strings))
|
||||
//
|
||||
// Contains a key for each loaded locale, each of which is a
|
||||
// JSON-compatible object with a property for each message
|
||||
// in that locale.
|
||||
// Map of message keys to their localized strings.
|
||||
this.localeMessages = new Map();
|
||||
this.selectedLocale = null;
|
||||
this._promiseLocales = null;
|
||||
@ -372,41 +376,36 @@ ExtensionData.prototype = {
|
||||
},
|
||||
|
||||
// https://developer.chrome.com/extensions/i18n
|
||||
localizeMessage(message, substitutions, locale = this.selectedLocale) {
|
||||
let messages = {};
|
||||
if (this.localeMessages.has(locale)) {
|
||||
messages = this.localeMessages.get(locale);
|
||||
}
|
||||
localizeMessage(message, substitutions = [], locale = this.selectedLocale, defaultValue = "??") {
|
||||
let locales = new Set([locale, this.defaultLocale]
|
||||
.filter(locale => this.localeMessages.has(locale)));
|
||||
|
||||
if (message in messages) {
|
||||
let str = messages[message].message;
|
||||
// Message names are case-insensitive, so normalize them to lower-case.
|
||||
message = message.toLowerCase();
|
||||
for (let locale of locales) {
|
||||
let messages = this.localeMessages.get(locale);
|
||||
if (messages.has(message)) {
|
||||
let str = messages.get(message)
|
||||
|
||||
if (!substitutions) {
|
||||
substitutions = [];
|
||||
} else if (!Array.isArray(substitutions)) {
|
||||
substitutions = [substitutions];
|
||||
}
|
||||
|
||||
// https://developer.chrome.com/extensions/i18n-messages
|
||||
// |str| may contain substrings of the form $1 or $PLACEHOLDER$.
|
||||
// In the former case, we replace $n with substitutions[n - 1].
|
||||
// In the latter case, we consult the placeholders array.
|
||||
// The placeholder may itself use $n to refer to substitutions.
|
||||
let replacer = (matched, name) => {
|
||||
if (name.length == 1 && name[0] >= '1' && name[0] <= '9') {
|
||||
return substitutions[parseInt(name) - 1];
|
||||
} else {
|
||||
let content = messages[message].placeholders[name].content;
|
||||
if (content[0] == '$') {
|
||||
return replacer(matched, content[1]);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
if (!Array.isArray(substitutions)) {
|
||||
substitutions = [substitutions];
|
||||
}
|
||||
};
|
||||
return str.replace(/\$([A-Za-z_@]+)\$/, replacer)
|
||||
.replace(/\$([0-9]+)/, replacer)
|
||||
.replace(/\$\$/, "$");
|
||||
|
||||
let replacer = (matched, index, dollarSigns) => {
|
||||
if (index) {
|
||||
// This is not quite Chrome-compatible. Chrome consumes any number
|
||||
// of digits following the $, but only accepts 9 substitutions. We
|
||||
// accept any number of substitutions.
|
||||
index = parseInt(index) - 1;
|
||||
return index in substitutions ? substitutions[index] : "";
|
||||
} else {
|
||||
// For any series of contiguous `$`s, the first is dropped, and
|
||||
// the rest remain in the output string.
|
||||
return dollarSigns;
|
||||
}
|
||||
};
|
||||
return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for certain pre-defined messages.
|
||||
@ -425,7 +424,7 @@ ExtensionData.prototype = {
|
||||
}
|
||||
|
||||
Cu.reportError(`Unknown localization message ${message}`);
|
||||
return "??";
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
// Localize a string, replacing all |__MSG_(.*)__| tokens with the
|
||||
@ -440,7 +439,7 @@ ExtensionData.prototype = {
|
||||
}
|
||||
|
||||
return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
|
||||
return this.localizeMessage(message, [], locale);
|
||||
return this.localizeMessage(message, [], locale, matched);
|
||||
});
|
||||
},
|
||||
|
||||
@ -565,6 +564,61 @@ ExtensionData.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
// Validates the contents of a locale JSON file, normalizes the
|
||||
// messages into a Map of message key -> localized string pairs.
|
||||
processLocale(locale, messages) {
|
||||
let result = new Map();
|
||||
|
||||
// Chrome does not document the semantics of its localization
|
||||
// system very well. It handles replacements by pre-processing
|
||||
// messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
|
||||
// replacements. Later, it processes the resulting string for
|
||||
// |$[0-9]| replacements.
|
||||
//
|
||||
// Again, it does not document this, but it accepts any number
|
||||
// of sequential |$|s, and replaces them with that number minus
|
||||
// 1. It also accepts |$| followed by any number of sequential
|
||||
// digits, but refuses to process a localized string which
|
||||
// provides more than 9 substitutions.
|
||||
if (!instanceOf(messages, "Object")) {
|
||||
this.packagingError(`Invalid locale data for ${locale}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (let key of Object.keys(messages)) {
|
||||
let msg = messages[key];
|
||||
|
||||
if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
|
||||
this.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Substitutions are case-insensitive, so normalize all of their names
|
||||
// to lower-case.
|
||||
let placeholders = new Map();
|
||||
if (instanceOf(msg.placeholders, "Object")) {
|
||||
for (let key of Object.keys(msg.placeholders)) {
|
||||
placeholders.set(key.toLowerCase(), msg.placeholders[key]);
|
||||
}
|
||||
}
|
||||
|
||||
let replacer = (match, name) => {
|
||||
let replacement = placeholders.get(name.toLowerCase());
|
||||
if (instanceOf(replacement, "Object") && "content" in replacement) {
|
||||
return replacement.content;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
|
||||
|
||||
// Message names are also case-insensitive, so normalize them to lower-case.
|
||||
result.set(key.toLowerCase(), value);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Reads the locale file for the given Gecko-compatible locale code, and
|
||||
// stores its parsed contents in |this.localeMessages.get(locale)|.
|
||||
readLocaleFile: Task.async(function* (locale) {
|
||||
@ -572,9 +626,10 @@ ExtensionData.prototype = {
|
||||
let dir = locales.get(locale);
|
||||
let file = `_locales/${dir}/messages.json`;
|
||||
|
||||
let messages = {};
|
||||
let messages = new Map();
|
||||
try {
|
||||
messages = yield this.readJSON(file);
|
||||
messages = this.processLocale(locale, messages);
|
||||
} catch (e) {
|
||||
this.packagingError(`Loading locale file ${file}: ${e}`);
|
||||
}
|
||||
@ -638,16 +693,25 @@ ExtensionData.prototype = {
|
||||
// default locale if no locale code is given, and sets it as the currently
|
||||
// selected locale on success.
|
||||
//
|
||||
// Pre-loads the default locale for fallback message processing, regardless
|
||||
// of the locale specified.
|
||||
//
|
||||
// If no locales are unavailable, resolves to |null|.
|
||||
initLocale: Task.async(function* (locale = this.defaultLocale) {
|
||||
if (locale == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let localeData = yield this.readLocaleFile(locale);
|
||||
let promises = [this.readLocaleFile(locale)];
|
||||
|
||||
let { defaultLocale } = this;
|
||||
if (locale != defaultLocale && !this.localeMessages.has(defaultLocale)) {
|
||||
promises.push(this.readLocaleFile(defaultLocale));
|
||||
}
|
||||
|
||||
let results = yield Promise.all(promises);
|
||||
this.selectedLocale = locale;
|
||||
return localeData;
|
||||
return results[0];
|
||||
}),
|
||||
};
|
||||
|
||||
@ -777,7 +841,7 @@ this.Extension.generate = function(id, data)
|
||||
files[bgScript] = data.background;
|
||||
}
|
||||
|
||||
provide(files, ["manifest.json"], JSON.stringify(manifest));
|
||||
provide(files, ["manifest.json"], manifest);
|
||||
|
||||
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
|
||||
let zipW = new ZipWriter();
|
||||
@ -807,6 +871,8 @@ this.Extension.generate = function(id, data)
|
||||
let script = files[filename];
|
||||
if (typeof(script) == "function") {
|
||||
script = "(" + script.toString() + ")()";
|
||||
} else if (typeof(script) == "object") {
|
||||
script = JSON.stringify(script);
|
||||
}
|
||||
|
||||
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
|
||||
@ -940,7 +1006,7 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
||||
if (locale === undefined) {
|
||||
let locales = yield this.promiseLocales();
|
||||
|
||||
let localeList = Object.keys(locales).map(locale => {
|
||||
let localeList = Array.from(locales.keys(), locale => {
|
||||
return { name: locale, locales: [locale] };
|
||||
});
|
||||
|
||||
@ -971,7 +1037,7 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
||||
|
||||
return this.runManifest(this.manifest);
|
||||
}).catch(e => {
|
||||
dump(`Extension error: ${e} ${e.filename}:${e.lineNumber}\n`);
|
||||
dump(`Extension error: ${e} ${e.filename || e.fileName}:${e.lineNumber}\n`);
|
||||
Cu.reportError(e);
|
||||
throw e;
|
||||
});
|
||||
|
@ -330,7 +330,13 @@ var DocumentManager = {
|
||||
},
|
||||
|
||||
handleEvent: function(event) {
|
||||
let window = event.target.defaultView;
|
||||
let window = event.currentTarget;
|
||||
if (event.target != window.document) {
|
||||
// We use capturing listeners so we have precedence over content script
|
||||
// listeners, but only care about events targeted to the element we're
|
||||
// listening on.
|
||||
return;
|
||||
}
|
||||
window.removeEventListener(event.type, this, true);
|
||||
|
||||
// Need to check if we're still on the right page? Greasemonkey does this.
|
||||
@ -437,7 +443,7 @@ function BrowserExtensionContent(data)
|
||||
this.id = data.id;
|
||||
this.uuid = data.uuid;
|
||||
this.data = data;
|
||||
this.scripts = [ for (scriptData of data.content_scripts) new Script(scriptData) ];
|
||||
this.scripts = data.content_scripts.map(scriptData => new Script(scriptData));
|
||||
this.webAccessibleResources = data.webAccessibleResources;
|
||||
this.whiteListedHosts = data.whiteListedHosts;
|
||||
|
||||
|
@ -64,6 +64,12 @@ function runSafe(context, f, ...args)
|
||||
return runSafeWithoutClone(f, ...args);
|
||||
}
|
||||
|
||||
// Return true if the given value is an instance of the given
|
||||
// native type.
|
||||
function instanceOf(value, type) {
|
||||
return {}.toString.call(value) == `[object ${type}]`;
|
||||
}
|
||||
|
||||
// Extend the object |obj| with the property descriptors of each object in
|
||||
// |args|.
|
||||
function extend(obj, ...args) {
|
||||
@ -634,4 +640,5 @@ this.ExtensionUtils = {
|
||||
Messenger,
|
||||
extend,
|
||||
flushJarCache,
|
||||
instanceOf,
|
||||
};
|
||||
|
@ -121,7 +121,7 @@ extensions.registerAPI((extension, context) => {
|
||||
|
||||
getAll: function(callback) {
|
||||
let alarms = alarmsMap.get(extension);
|
||||
result = [ for (alarm of alarms) alarm.data ];
|
||||
result = alarms.map(alarm => alarm.data);
|
||||
runSafe(context, callback, result);
|
||||
},
|
||||
|
||||
|
@ -53,7 +53,11 @@ BackgroundPage.prototype = {
|
||||
|
||||
// TODO: Right now we run onStartup after the background page
|
||||
// finishes. See if this is what Chrome does.
|
||||
window.windowRoot.addEventListener("load", () => {
|
||||
let loadListener = event => {
|
||||
if (event.target != window.document) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.removeEventListener("load", loadListener, true);
|
||||
if (this.scripts) {
|
||||
let doc = window.document;
|
||||
for (let script of this.scripts) {
|
||||
@ -74,7 +78,8 @@ BackgroundPage.prototype = {
|
||||
if (this.extension.onStartup) {
|
||||
this.extension.onStartup();
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
window.windowRoot.addEventListener("load", loadListener, true);
|
||||
},
|
||||
|
||||
shutdown() {
|
||||
|
@ -121,7 +121,7 @@ extensions.registerPrivilegedAPI("notifications", (extension, context) => {
|
||||
|
||||
getAll: function(callback) {
|
||||
let notifications = notificationsMap.get(extension);
|
||||
notifications = [ for (notification of notifications) notification.id ];
|
||||
notifications = notifications.map(notification => notification.id);
|
||||
runSafe(context, callback, notifications);
|
||||
},
|
||||
|
||||
|
@ -35,5 +35,7 @@ support-files =
|
||||
[test_ext_bookmarks.html]
|
||||
[test_ext_alarms.html]
|
||||
[test_ext_background_window_properties.html]
|
||||
[test_ext_background_sub_windows.html]
|
||||
[test_ext_jsversion.html]
|
||||
skip-if = e10s # Uses a console monitor which doesn't work from a content process. The code being tested doesn't run in a tab content process in any case.
|
||||
[test_ext_i18n.html]
|
||||
|
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for sub-frames of WebExtension background pages</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
|
||||
<script type="text/javascript" src="head.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="application/javascript;version=1.8">
|
||||
|
||||
add_task(function* testBackgroundWindow() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
background: "new " + function() {
|
||||
browser.test.log("background script executed");
|
||||
|
||||
browser.test.sendMessage("background-script-load");
|
||||
|
||||
let img = document.createElement("img");
|
||||
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
document.body.appendChild(img);
|
||||
|
||||
img.onload = () => {
|
||||
browser.test.log("image loaded");
|
||||
|
||||
let iframe = document.createElement("iframe");
|
||||
iframe.src = "about:blank?1";
|
||||
|
||||
iframe.onload = () => {
|
||||
browser.test.log("iframe loaded");
|
||||
setTimeout(() => {
|
||||
browser.test.notifyPass("background sub-window test done");
|
||||
}, 0);
|
||||
};
|
||||
document.body.appendChild(iframe);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
info("extension loaded");
|
||||
|
||||
let loadCount = 0;
|
||||
extension.onMessage("background-script-load", () => {
|
||||
loadCount++;
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
info("startup complete loaded");
|
||||
|
||||
yield extension.awaitFinish("background sub-window test done")
|
||||
|
||||
is(loadCount, 1, "background script loaded only once");
|
||||
|
||||
yield extension.unload();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
121
toolkit/components/extensions/test/mochitest/test_ext_i18n.html
Normal file
121
toolkit/components/extensions/test/mochitest/test_ext_i18n.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for WebExtension localization APIs</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
|
||||
<script type="text/javascript" src="head.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="application/javascript;version=1.8">
|
||||
|
||||
add_task(function* test_i18n() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"default_locale": "jp",
|
||||
},
|
||||
|
||||
files: {
|
||||
"_locales/en_US/messages.json": {
|
||||
"foo": {
|
||||
"message": "Foo.",
|
||||
"description": "foo",
|
||||
},
|
||||
|
||||
"basic_substitutions": {
|
||||
"message": "'$0' '$14' '$1' '$5' '$$$$$' '$$'.",
|
||||
"description": "foo",
|
||||
},
|
||||
|
||||
"Named_placeholder_substitutions": {
|
||||
"message": "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo",
|
||||
"description": "foo",
|
||||
"placeholders": {
|
||||
"foO": {
|
||||
"content": "_foo_ $1 _bar_",
|
||||
"description": "foo",
|
||||
},
|
||||
|
||||
"bad name": {
|
||||
"content": "Nope.",
|
||||
"description": "bad name",
|
||||
},
|
||||
|
||||
"bad_value": "Nope.",
|
||||
|
||||
"bad_content_value": {
|
||||
"content": ["Accepted, but shouldn't break."],
|
||||
"description": "bad value",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"broken_placeholders": {
|
||||
"message": "$broken$",
|
||||
"description": "broken placeholders",
|
||||
"placeholders": "foo.",
|
||||
},
|
||||
},
|
||||
|
||||
"_locales/jp/messages.json": {
|
||||
"foo": {
|
||||
"message": "(foo)",
|
||||
"description": "foo",
|
||||
},
|
||||
|
||||
"bar": {
|
||||
"message": "(bar)",
|
||||
"description": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
background: "new " + function() {
|
||||
let _ = browser.i18n.getMessage.bind(browser.i18n);
|
||||
|
||||
browser.test.assertEq("Foo.", _("Foo"), "Simple message in selected locale.");
|
||||
|
||||
browser.test.assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");
|
||||
|
||||
let substitutions = [];
|
||||
substitutions[4] = "5";
|
||||
substitutions[13] = "14";
|
||||
|
||||
browser.test.assertEq("'$0' '14' '' '5' '$$$$' '$'.",
|
||||
_("basic_substitutions", substitutions),
|
||||
"Basic numeric substitutions");
|
||||
|
||||
browser.test.assertEq("'$0' '' 'just a string' '' '$$$$' '$'.",
|
||||
_("basic_substitutions", "just a string"),
|
||||
"Basic numeric substitutions, with non-array value");
|
||||
|
||||
let values = _("named_placeholder_substitutions", ["(subst $1 $2)", "(2 $1 $2)"]).split("\n");
|
||||
|
||||
browser.test.assertEq("_foo_ (subst $1 $2) _bar_", values[0], "Named and numeric substitution");
|
||||
|
||||
browser.test.assertEq("(2 $1 $2)", values[1], "Numeric substitution amid named placeholders");
|
||||
|
||||
browser.test.assertEq("$bad name$", values[2], "Named placeholder with invalid key");
|
||||
|
||||
browser.test.assertEq("", values[3], "Named placeholder with an invalid value");
|
||||
|
||||
browser.test.assertEq("Accepted, but shouldn't break.", values[4], "Named placeholder with a strange content value");
|
||||
|
||||
browser.test.assertEq("$foo", values[5], "Non-placeholder token that should be ignored");
|
||||
|
||||
browser.test.notifyPass("l10n");
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
yield extension.awaitFinish("l10n");
|
||||
yield extension.unload();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1104,7 +1104,7 @@ var Histogram = {
|
||||
+ " ".repeat(Math.max(0, labelPadTo - String(label).length)) + label // Right-aligned label
|
||||
+ " |" + "#".repeat(Math.round(MAX_BAR_CHARS * barValue / maxBarValue)) // Bar
|
||||
+ " " + value // Value
|
||||
+ " " + Math.round(100 * value / aHgram.sum) + "%"; // Percentage
|
||||
+ " " + Math.round(100 * value / aHgram.sample_count) + "%"; // Percentage
|
||||
|
||||
// Construct the HTML labels + bars
|
||||
let belowEm = Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
|
||||
|
Loading…
Reference in New Issue
Block a user