Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-04-08 12:38:35 -04:00
commit 6a8f66fd96
60 changed files with 1230 additions and 769 deletions

View File

@ -102,10 +102,10 @@ pref("app.update.log", false);
pref("app.update.backgroundMaxErrors", 10);
// The aus update xml certificate checks for application update are disabled on
// Windows since the mar signature check which is currently only implemented on
// Windows is sufficient for preventing us from applying a mar that is not
// Windows and Mac OS X since the mar signature check are implemented on these
// platforms and is sufficient to prevent us from applying a mar that is not
// valid.
#ifdef XP_WIN
#if defined(XP_WIN) || defined(XP_MACOSX)
pref("app.update.cert.requireBuiltIn", false);
pref("app.update.cert.checkAttributes", false);
#else

View File

@ -568,11 +568,16 @@ let AboutReaderListener = {
}
},
updateReaderButton: function() {
if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) {
if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader ||
!(content.document instanceof content.HTMLDocument) ||
content.document.mozSyntheticDocument) {
return;
}
let isArticle = ReaderMode.isProbablyReaderable(content.document);
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: isArticle });
// Only send updates when there are articles; there's no point updating with
// |false| all the time.
if (ReaderMode.isProbablyReaderable(content.document)) {
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true });
}
},
};
AboutReaderListener.init();

View File

@ -54,7 +54,7 @@ If you install eslint and the react plugin globally:
You can also run it by hand in the browser/components/loop directory:
eslint -ext .js -ext .jsx .
eslint -ext .js -ext .jsx --ext .jsm .
Front-End Unit Tests
====================

View File

@ -11,6 +11,7 @@ loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
@ -322,7 +323,8 @@ loop.conversationViews = (function(mozL10n) {
],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
cancelCall: React.PropTypes.func.isRequired,
failureReason: React.PropTypes.string
},
componentDidMount: function() {
@ -332,9 +334,18 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
this.setTitle(mozL10n.get("generic_failure_title"));
var errorString;
switch (this.props.failureReason) {
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
errorString = mozL10n.get("no_media_failure_message");
break;
default:
errorString = mozL10n.get("generic_failure_title");
}
return (
React.createElement("div", {className: "call-window"},
React.createElement("h2", null, mozL10n.get("generic_failure_title")),
React.createElement("h2", null, errorString),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-cancel",
@ -467,21 +478,22 @@ loop.conversationViews = (function(mozL10n) {
},
_getTitleMessage: function() {
var callStateReason =
this.getStoreState().callStateReason;
switch (this.getStoreState().callStateReason) {
case WEBSOCKET_REASONS.REJECT:
case WEBSOCKET_REASONS.BUSY:
case REST_ERRNOS.USER_UNAVAILABLE:
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
return mozL10n.get("generic_contact_unavailable_title");
} else {
return mozL10n.get("generic_failure_title");
return mozL10n.get("generic_contact_unavailable_title");
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
default:
return mozL10n.get("generic_failure_title");
}
},

View File

@ -11,6 +11,7 @@ loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
@ -322,7 +323,8 @@ loop.conversationViews = (function(mozL10n) {
],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
cancelCall: React.PropTypes.func.isRequired,
failureReason: React.PropTypes.string
},
componentDidMount: function() {
@ -332,9 +334,18 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
this.setTitle(mozL10n.get("generic_failure_title"));
var errorString;
switch (this.props.failureReason) {
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
errorString = mozL10n.get("no_media_failure_message");
break;
default:
errorString = mozL10n.get("generic_failure_title");
}
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
<h2>{errorString}</h2>
<div className="btn-group call-action-group">
<button className="btn btn-cancel"
@ -467,21 +478,22 @@ loop.conversationViews = (function(mozL10n) {
},
_getTitleMessage: function() {
var callStateReason =
this.getStoreState().callStateReason;
switch (this.getStoreState().callStateReason) {
case WEBSOCKET_REASONS.REJECT:
case WEBSOCKET_REASONS.BUSY:
case REST_ERRNOS.USER_UNAVAILABLE:
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
return mozL10n.get("generic_contact_unavailable_title");
} else {
return mozL10n.get("generic_failure_title");
return mozL10n.get("generic_contact_unavailable_title");
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
default:
return mozL10n.get("generic_failure_title");
}
},

View File

@ -251,7 +251,8 @@ loop.roomViews = (function(mozL10n) {
// FULL case should never happen on desktop.
return (
React.createElement(loop.conversationViews.GenericFailureView, {
cancelCall: this.closeWindow})
cancelCall: this.closeWindow,
failureReason: this.state.failureReason})
);
}
case ROOM_STATES.ENDED: {

View File

@ -251,7 +251,8 @@ loop.roomViews = (function(mozL10n) {
// FULL case should never happen on desktop.
return (
<loop.conversationViews.GenericFailureView
cancelCall={this.closeWindow} />
cancelCall={this.closeWindow}
failureReason={this.state.failureReason} />
);
}
case ROOM_STATES.ENDED: {

View File

@ -13,6 +13,7 @@ describe("loop.conversationViews", function () {
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
@ -450,6 +451,15 @@ describe("loop.conversationViews", function () {
{contactName: loop.conversationViews._getContactDisplayName(contact)});
});
it("should show 'no media' when the reason is FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA",
function () {
store.setStoreState({callStateReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA});
view = mountTestComponent({contact: contact});
sinon.assert.calledWithExactly(document.mozL10n.get, "no_media_failure_message");
});
it("should display a generic contact unavailable msg when the reason is" +
" WEBSOCKET_REASONS.BUSY and no display name is available", function() {
store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
@ -887,6 +897,11 @@ describe("loop.conversationViews", function () {
describe("GenericFailureView", function() {
var view, fakeAudio;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.GenericFailureView, props));
}
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
@ -895,14 +910,11 @@ describe("loop.conversationViews", function () {
};
navigator.mozLoop.doNotDisturb = false;
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.GenericFailureView, {
cancelCall: function() {}
}));
});
it("should play a failure sound, once", function() {
view = mountTestComponent({cancelCall: function() {}});
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
@ -911,7 +923,24 @@ describe("loop.conversationViews", function () {
});
it("should set the title to generic_failure_title", function() {
view = mountTestComponent({cancelCall: function() {}});
expect(fakeWindow.document.title).eql("generic_failure_title");
});
it("should show 'no media' for FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA reason", function() {
view = mountTestComponent({
cancelCall: function() {},
failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
});
expect(view.getDOMNode().querySelector("h2").textContent).eql("no_media_failure_message");
});
it("should show 'generic_failure_title' when no reason is specified", function() {
view = mountTestComponent({cancelCall: function() {}});
expect(view.getDOMNode().querySelector("h2").textContent).eql("generic_failure_title");
});
});
});

View File

@ -346,7 +346,7 @@ ReadingListImpl.prototype = {
let item = this._itemFromRecord(record);
this._callListeners("onItemAdded", item);
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("Reader:Added", item);
mm.broadcastAsyncMessage("Reader:Added", item.toJSON());
return item;
}),
@ -427,7 +427,7 @@ ReadingListImpl.prototype = {
}
this._invalidateIterators();
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("Reader:Removed", item);
mm.broadcastAsyncMessage("Reader:Removed", item.toJSON());
this._callListeners("onItemDeleted", item);
}),

View File

@ -21,6 +21,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
XPCOMUtils.defineLazyModuleGetter(this, "ServerClient",
"resource:///modules/readinglist/ServerClient.jsm");
// The maximum number of sub-requests per POST /batch supported by the server.
// See http://readinglist.readthedocs.org/en/latest/api/batch.html.
const BATCH_REQUEST_LIMIT = 25;
// The Last-Modified header of server responses is stored here.
const SERVER_LAST_MODIFIED_HEADER_PREF = "readinglist.sync.serverLastModified";
@ -180,8 +184,6 @@ SyncImpl.prototype = {
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "PATCH",
@ -189,7 +191,7 @@ SyncImpl.prototype = {
requests: requests,
},
};
let batchResponse = yield this._sendRequest(request);
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading changes", batchResponse);
return;
@ -244,8 +246,6 @@ SyncImpl.prototype = {
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "POST",
@ -254,7 +254,7 @@ SyncImpl.prototype = {
requests: requests,
},
};
let batchResponse = yield this._sendRequest(request);
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading new items", batchResponse);
return;
@ -308,8 +308,6 @@ SyncImpl.prototype = {
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "DELETE",
@ -317,7 +315,7 @@ SyncImpl.prototype = {
requests: requests,
},
};
let batchResponse = yield this._sendRequest(request);
let batchResponse = yield this._postBatch(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading deleted items", batchResponse);
return;
@ -504,6 +502,50 @@ SyncImpl.prototype = {
return response;
}),
/**
* The server limits the number of sub-requests in POST /batch'es to
* BATCH_REQUEST_LIMIT. This method takes an arbitrarily big batch request
* and breaks it apart into many individual batch requests in order to stay
* within the limit.
*
* @param bigRequest The same type of request object that _sendRequest takes.
* Since it's a POST /batch request, its `body` should have a
* `requests` property whose value is an array of sub-requests.
* `method` and `path` are automatically filled.
* @return Promise<response> Resolved when all requests complete with 200s, or
* when the first response that is not a 200 is received. In the
* first case, the resolved response is a combination of all the
* server responses, and response.body.responses contains the sub-
* responses for all the sub-requests in bigRequest. In the second
* case, the resolved response is the non-200 response straight from
* the server.
*/
_postBatch: Task.async(function* (bigRequest) {
log.debug("Sending batch requests");
let allSubResponses = [];
let remainingSubRequests = bigRequest.body.requests;
while (remainingSubRequests.length) {
let request = Object.assign({}, bigRequest);
request.method = "POST";
request.path = "/batch";
request.body.requests =
remainingSubRequests.splice(0, BATCH_REQUEST_LIMIT);
let response = yield this._sendRequest(request);
if (response.status != 200) {
return response;
}
allSubResponses = allSubResponses.concat(response.body.responses);
}
let bigResponse = {
status: 200,
body: {
responses: allSubResponses,
},
};
log.debug("All batch requests successfully sent");
return bigResponse;
}),
_handleUnexpectedResponse(contextMsgFragment, response) {
log.error(`Unexpected response ${contextMsgFragment}`, response);
},

View File

@ -40,6 +40,8 @@ let AnimationsPanel = {
this.startListeners();
yield this.createPlayerWidgets();
this.initialized.resolve();
this.emit(this.PANEL_INITIALIZED);

View File

@ -2,6 +2,7 @@
tags = devtools
subsuite = devtools
support-files =
doc_body_animation.html
doc_frame_script.js
doc_simple_animation.html
head.js
@ -12,6 +13,7 @@ support-files =
[browser_animation_participate_in_inspector_update.js]
[browser_animation_play_pause_button.js]
[browser_animation_playerFronts_are_refreshed.js]
[browser_animation_playerWidgets_appear_on_panel_init.js]
[browser_animation_playerWidgets_destroy.js]
[browser_animation_playerWidgets_disables_on_finished.js]
[browser_animation_playerWidgets_dont_show_time_after_duration.js]

View File

@ -0,0 +1,15 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that player widgets are displayed right when the animation panel is
// initialized, if the selected node (<body> by default) is animated.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_body_animation.html");
let {panel} = yield openAnimationInspector();
is(panel.playerWidgets.length, 1, "One animation player is displayed after init");
});

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
background-color: white;
color: black;
animation: change-background-color 3s infinite alternate;
}
@keyframes change-background-color {
to {
background-color: black;
color: white;
}
}
</style>
</head>
<body>
<h1>Animated body element</h1>
</body>
</html>

View File

@ -119,12 +119,12 @@ ToolSidebar.prototype = {
let tabs = this._tabbox.tabs;
// Create a toolbar and insert it first in the tabbox
let allTabsToolbar = this._panelDoc.createElementNS(XULNS, "toolbar");
this._tabbox.insertBefore(allTabsToolbar, tabs);
// Create a container and insert it first in the tabbox
let allTabsContainer = this._panelDoc.createElementNS(XULNS, "box");
this._tabbox.insertBefore(allTabsContainer, tabs);
// Move the tabs inside and make them flex
allTabsToolbar.appendChild(tabs);
allTabsContainer.appendChild(tabs);
tabs.setAttribute("flex", "1");
// Create the dropdown menu next to the tabs
@ -134,7 +134,7 @@ ToolSidebar.prototype = {
this._allTabsBtn.setAttribute("label", l10n("sidebar.showAllTabs.label"));
this._allTabsBtn.setAttribute("tooltiptext", l10n("sidebar.showAllTabs.tooltip"));
this._allTabsBtn.setAttribute("hidden", "true");
allTabsToolbar.appendChild(this._allTabsBtn);
allTabsContainer.appendChild(this._allTabsBtn);
let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
this._allTabsBtn.appendChild(menuPopup);
@ -162,7 +162,7 @@ ToolSidebar.prototype = {
// Moving back the tabs as a first child of the tabbox
this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
this._tabbox.querySelector("toolbar").remove();
this._tabbox.querySelector("box").remove();
this._allTabsBtn = null;
},

View File

@ -31,6 +31,7 @@ EXTRA_JS_MODULES.devtools += [
]
EXTRA_JS_MODULES.devtools.shared.profiler += [
'profiler/frame-utils.js',
'profiler/global.js',
'profiler/jit.js',
'profiler/tree-model.js',
@ -46,7 +47,6 @@ EXTRA_JS_MODULES.devtools.shared.timeline += [
EXTRA_JS_MODULES.devtools.shared += [
'autocomplete-popup.js',
'd3.js',
'devices.js',
'doorhanger.js',
'frame-script-utils.js',

View File

@ -0,0 +1,131 @@
/* 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 { Ci } = require("chrome");
const { extend } = require("sdk/util/object");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "CATEGORY_OTHER",
"devtools/shared/profiler/global", true);
// The cache used in the `nsIURL` function.
const gNSURLStore = new Map();
const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"];
const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"];
/**
* Parses the raw location of this function call to retrieve the actual
* function name, source url, host name, line and column.
*/
exports.parseLocation = function parseLocation (frame) {
// Parse the `location` for the function name, source url, line, column etc.
let lineAndColumn = frame.location.match(/((:\d+)*)\)?$/)[1];
let [, line, column] = lineAndColumn.split(":");
line = line || frame.line;
column = column || frame.column;
let firstParenIndex = frame.location.indexOf("(");
let lineAndColumnIndex = frame.location.indexOf(lineAndColumn);
let resource = frame.location.substring(firstParenIndex + 1, lineAndColumnIndex);
let url = resource.split(" -> ").pop();
let uri = nsIURL(url);
let functionName, fileName, hostName;
// If the URI digged out from the `location` is valid, this is a JS frame.
if (uri) {
functionName = frame.location.substring(0, firstParenIndex - 1);
fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/";
hostName = url.indexOf("jar:") == 0 ? "" : uri.host;
} else {
functionName = frame.location;
url = null;
}
return {
functionName: functionName,
fileName: fileName,
hostName: hostName,
url: url,
line: line,
column: column
};
},
/**
* Checks if the specified function represents a chrome or content frame.
*
* @param object frame
* The { category, location } properties of the frame.
* @return boolean
* True if a content frame, false if a chrome frame.
*/
exports.isContent = function isContent ({ category, location }) {
// Only C++ stack frames have associated category information.
return !!(!category &&
!CHROME_SCHEMES.find(e => location.contains(e)) &&
CONTENT_SCHEMES.find(e => location.contains(e)));
}
/**
* This filters out platform data frames in a sample. With latest performance
* tool in Fx40, when displaying only content, we still filter out all platform data,
* except we generalize platform data that are leaves. We do this because of two
* observations:
*
* 1. The leaf is where time is _actually_ being spent, so we _need_ to show it
* to developers in some way to give them accurate profiling data. We decide to
* split the platform into various category buckets and just show time spent in
* each bucket.
*
* 2. The calls leading to the leaf _aren't_ where we are spending time, but
* _do_ give the developer context for how they got to the leaf where they _are_
* spending time. For non-platform hackers, the non-leaf platform frames don't
* give any meaningful context, and so we can safely filter them out.
*
* Example transformations:
* Before: PlatformA -> PlatformB -> ContentA -> ContentB
* After: ContentA -> ContentB
*
* Before: PlatformA -> ContentA -> PlatformB -> PlatformC
* After: ContentA -> Category(PlatformC)
*/
exports.filterPlatformData = function filterPlatformData (frames) {
let result = [];
let last = frames.length - 1;
let frame;
for (let i = 0; i < frames.length; i++) {
frame = frames[i];
if (exports.isContent(frame)) {
result.push(frame);
} else if (last === i) {
// Extend here so we're not destructively editing
// the original profiler data. Set isMetaCategory `true`,
// and ensure we have a category set by default, because that's how
// the generalized frame nodes are organized.
result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame));
}
}
return result;
}
/**
* Helper for getting an nsIURL instance out of a string.
*/
function nsIURL(url) {
let cached = gNSURLStore.get(url);
if (cached) {
return cached;
}
let uri = null;
try {
uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
} catch(e) {
// The passed url string is invalid.
}
gNSURLStore.set(url, uri);
return uri;
}

View File

@ -4,9 +4,7 @@
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const {extend} = require("sdk/util/object");
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "L10N",
"devtools/shared/profiler/global", true);
loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
@ -17,15 +15,12 @@ loader.lazyRequireGetter(this, "CATEGORY_JIT",
"devtools/shared/profiler/global", true);
loader.lazyRequireGetter(this, "JITOptimizations",
"devtools/shared/profiler/jit", true);
loader.lazyRequireGetter(this, "CATEGORY_OTHER",
"devtools/shared/profiler/global", true);
const CHROME_SCHEMES = ["chrome://", "resource://", "jar:file://"];
const CONTENT_SCHEMES = ["http://", "https://", "file://", "app://"];
loader.lazyRequireGetter(this, "FrameUtils",
"devtools/shared/profiler/frame-utils");
exports.ThreadNode = ThreadNode;
exports.FrameNode = FrameNode;
exports.FrameNode.isContent = isContent;
exports.FrameNode.isContent = FrameUtils.isContent;
/**
* A call tree for a thread. This is essentially a linkage between all frames
@ -102,7 +97,7 @@ ThreadNode.prototype = {
// should be taken into consideration.
if (options.contentOnly) {
// The (root) node is not considered a content function, it'll be removed.
sampleFrames = filterPlatformData(sampleFrames);
sampleFrames = FrameUtils.filterPlatformData(sampleFrames);
} else {
// Remove the (root) node manually.
sampleFrames = sampleFrames.slice(1);
@ -253,42 +248,13 @@ FrameNode.prototype = {
// default to an "unknown" category otherwise.
let categoryData = CATEGORY_MAPPINGS[this.category] || {};
// Parse the `location` for the function name, source url, line, column etc.
let lineAndColumn = this.location.match(/((:\d+)*)\)?$/)[1];
let [, line, column] = lineAndColumn.split(":");
line = line || this.line;
column = column || this.column;
let parsedData = FrameUtils.parseLocation(this);
parsedData.nodeType = "Frame";
parsedData.categoryData = categoryData;
parsedData.isContent = FrameUtils.isContent(this);
parsedData.isMetaCategory = this.isMetaCategory;
let firstParenIndex = this.location.indexOf("(");
let lineAndColumnIndex = this.location.indexOf(lineAndColumn);
let resource = this.location.substring(firstParenIndex + 1, lineAndColumnIndex);
let url = resource.split(" -> ").pop();
let uri = nsIURL(url);
let functionName, fileName, hostName;
// If the URI digged out from the `location` is valid, this is a JS frame.
if (uri) {
functionName = this.location.substring(0, firstParenIndex - 1);
fileName = (uri.fileName + (uri.ref ? "#" + uri.ref : "")) || "/";
hostName = url.indexOf("jar:") == 0 ? "" : uri.host;
} else {
functionName = this.location;
url = null;
}
return this._data = {
nodeType: "Frame",
functionName: functionName,
fileName: fileName,
hostName: hostName,
url: url,
line: line,
column: column,
categoryData: categoryData,
isContent: !!isContent(this),
isMetaCategory: this.isMetaCategory
};
return this._data = parsedData;
},
/**
@ -310,83 +276,3 @@ FrameNode.prototype = {
return this._optimizations;
}
};
/**
* Checks if the specified function represents a chrome or content frame.
*
* @param object frame
* The { category, location } properties of the frame.
* @return boolean
* True if a content frame, false if a chrome frame.
*/
function isContent({ category, location }) {
// Only C++ stack frames have associated category information.
return !category &&
!CHROME_SCHEMES.find(e => location.contains(e)) &&
CONTENT_SCHEMES.find(e => location.contains(e));
}
/**
* Helper for getting an nsIURL instance out of a string.
*/
function nsIURL(url) {
let cached = gNSURLStore.get(url);
if (cached) {
return cached;
}
let uri = null;
try {
uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
} catch(e) {
// The passed url string is invalid.
}
gNSURLStore.set(url, uri);
return uri;
}
// The cache used in the `nsIURL` function.
let gNSURLStore = new Map();
/**
* This filters out platform data frames in a sample. With latest performance
* tool in Fx40, when displaying only content, we still filter out all platform data,
* except we generalize platform data that are leaves. We do this because of two
* observations:
*
* 1. The leaf is where time is _actually_ being spent, so we _need_ to show it
* to developers in some way to give them accurate profiling data. We decide to
* split the platform into various category buckets and just show time spent in
* each bucket.
*
* 2. The calls leading to the leaf _aren't_ where we are spending time, but
* _do_ give the developer context for how they got to the leaf where they _are_
* spending time. For non-platform hackers, the non-leaf platform frames don't
* give any meaningful context, and so we can safely filter them out.
*
* Example transformations:
* Before: PlatformA -> PlatformB -> ContentA -> ContentB
* After: ContentA -> ContentB
*
* Before: PlatformA -> ContentA -> PlatformB -> PlatformC
* After: ContentA -> Category(PlatformC)
*/
function filterPlatformData (frames) {
let result = [];
let last = frames.length - 1;
let frame;
for (let i = 0; i < frames.length; i++) {
frame = frames[i];
if (isContent(frame)) {
result.push(frame);
} else if (last === i) {
// Extend here so we're not destructively editing
// the original profiler data. Set isMetaCategory `true`,
// and ensure we have a category set by default, because that's how
// the generalized frame nodes are organized.
result.push(extend({ isMetaCategory: true, category: CATEGORY_OTHER }, frame));
}
}
return result;
}

View File

@ -31,6 +31,7 @@ support-files =
[browser_flame-graph-utils-03.js]
[browser_flame-graph-utils-04.js]
[browser_flame-graph-utils-05.js]
[browser_flame-graph-utils-06.js]
[browser_flame-graph-utils-hash.js]
[browser_graphs-01.js]
[browser_graphs-02.js]

View File

@ -0,0 +1,86 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the text displayed is the function name, file name and line number
// if applicable.
let {FlameGraphUtils, FLAME_GRAPH_BLOCK_HEIGHT} = devtools.require("devtools/shared/widgets/FlameGraph");
add_task(function*() {
yield promiseTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
});
function* performTest() {
let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA, {
flattenRecursion: true
});
ok(out, "Some data was outputted properly");
is(out.length, 10, "The outputted length is correct.");
info("Got flame graph data:\n" + out.toSource() + "\n");
for (let i = 0; i < out.length; i++) {
let found = out[i];
let expected = EXPECTED_OUTPUT[i];
is(found.blocks.length, expected.blocks.length,
"The correct number of blocks were found in this bucket.");
for (let j = 0; j < found.blocks.length; j++) {
is(found.blocks[j].x, expected.blocks[j].x,
"The expected block X position is correct for this frame.");
is(found.blocks[j].y, expected.blocks[j].y,
"The expected block Y position is correct for this frame.");
is(found.blocks[j].width, expected.blocks[j].width,
"The expected block width is correct for this frame.");
is(found.blocks[j].height, expected.blocks[j].height,
"The expected block height is correct for this frame.");
is(found.blocks[j].text, expected.blocks[j].text,
"The expected block text is correct for this frame.");
}
}
}
let TEST_DATA = [{
frames: [{
location: "A (http://path/to/file.js:10:5"
}, {
location: "B (http://path/to/file.js:100:5"
}],
time: 50,
}];
let EXPECTED_OUTPUT = [{
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: [{
srcData: {
startTime: 0,
rawLocation: "A (http://path/to/file.js:10:5)"
},
x: 0,
y: 0,
width: 50,
height: FLAME_GRAPH_BLOCK_HEIGHT,
text: "A (file.js:10)"
}]
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}, {
blocks: []
}];

View File

@ -9,6 +9,7 @@ const { Promise } = require("resource://gre/modules/Promise.jsm");
const { Task } = require("resource://gre/modules/Task.jsm");
const { getColor } = require("devtools/shared/theme");
const EventEmitter = require("devtools/toolkit/event-emitter");
const FrameUtils = require("devtools/shared/profiler/frame-utils");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
@ -1021,10 +1022,11 @@ let FlameGraphUtils = {
// If no frames are available, add a pseudo "idle" block in between.
if (options.showIdleBlocks && frames.length == 0) {
frames = [{ location: options.showIdleBlocks || "" }];
frames = [{ location: options.showIdleBlocks || "", idle: true }];
}
for (let { location } of frames) {
for (let frame of frames) {
let { location } = frame;
let prevFrame = prevFrames[frameIndex];
// Frames at the same location and the same depth will be reused.
@ -1045,7 +1047,7 @@ let FlameGraphUtils = {
y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
width: time - prevTime,
height: FLAME_GRAPH_BLOCK_HEIGHT,
text: location
text: this._formatLabel(frame)
});
}
@ -1115,6 +1117,30 @@ let FlameGraphUtils = {
}
return hash;
},
/**
* Takes a FrameNode and returns a string that should be displayed
* in its flame block.
*
* @param FrameNode frame
* @return string
*/
_formatLabel: function (frame) {
// If an idle block, just return the location which will just be "(idle)" text
// anyway.
if (frame.idle) {
return frame.location;
}
let { functionName, fileName, line } = FrameUtils.parseLocation(frame);
let label = functionName;
if (fileName) {
label += ` (${fileName}${line != null ? (":" + line) : ""})`;
}
return label;
}
};

View File

@ -65,6 +65,7 @@ skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
[browser_styleeditor_loading.js]
[browser_styleeditor_media_sidebar.js]
[browser_styleeditor_media_sidebar_sourcemaps.js]
[browser_styleeditor_navigate.js]
[browser_styleeditor_new.js]
[browser_styleeditor_nostyle.js]
[browser_styleeditor_pretty.js]

View File

@ -6,11 +6,7 @@
const TEST_URI = "http://example.com/browser/browser/devtools/styleeditor/" +
"test/browser_styleeditor_cmd_edit.html";
function test() {
return Task.spawn(spawnTest).then(finish, helpers.handleError);
}
function spawnTest() {
add_task(function* () {
let options = yield helpers.openTab(TEST_URI);
yield helpers.openToolbar(options);
@ -203,4 +199,4 @@ function spawnTest() {
yield helpers.closeToolbar(options);
yield helpers.closeTab(options);
}
});

View File

@ -9,8 +9,6 @@
const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
add_task(function() {
waitForExplicitFinish();
info("Opening netmonitor");
let tab = yield addTab("about:blank");
let target = TargetFactory.forTab(tab);

View File

@ -2,24 +2,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that hovering over a simple selector in the style-editor requests the
// highlighting of the corresponding nodes
waitForExplicitFinish();
const TEST_URL = "data:text/html;charset=utf8," +
"<style>div{color:red}</style><div>highlighter test</div>";
add_task(function*() {
let {UI} = yield addTabAndOpenStyleEditors(1, null, TEST_URL);
let editor = UI.editors[0];
let { ui } = yield openStyleEditorForURL(TEST_URL);
let editor = ui.editors[0];
// Mock the highlighter so we can locally assert that things happened
// correctly instead of accessing the highlighter elements

View File

@ -1,37 +1,36 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that style editor loads correctly.
const TESTCASE_URI = TEST_BASE_HTTP + "longload.html";
function test()
{
waitForExplicitFinish();
add_task(function* () {
// launch Style Editor right when the tab is created (before load)
// this checks that the Style Editor still launches correctly when it is opened
// *while* the page is still loading. The Style Editor should not signal that
// it is loaded until the accompanying content page is loaded.
let tabAdded = addTab(TESTCASE_URI);
let target = TargetFactory.forTab(gBrowser.selectedTab);
let styleEditorLoaded = gDevTools.showToolbox(target, "styleeditor");
addTabAndCheckOnStyleEditorAdded(function(panel) {
content.location = TESTCASE_URI;
}, testEditorAdded);
}
yield Promise.all([tabAdded, styleEditorLoaded]);
function testEditorAdded(event, editor)
{
let root = gPanelWindow.document.querySelector(".splitview-root");
let toolbox = gDevTools.getToolbox(target);
let panel = toolbox.getPanel("styleeditor");
let { panelWindow } = panel;
let root = panelWindow.document.querySelector(".splitview-root");
ok(!root.classList.contains("loading"),
"style editor root element does not have 'loading' class name anymore");
let button = gPanelWindow.document.querySelector(".style-editor-newButton");
let button = panelWindow.document.querySelector(".style-editor-newButton");
ok(!button.hasAttribute("disabled"),
"new style sheet button is enabled");
button = gPanelWindow.document.querySelector(".style-editor-importButton");
button = panelWindow.document.querySelector(".style-editor-importButton");
ok(!button.hasAttribute("disabled"),
"import button is enabled");
finish();
}
});

View File

@ -14,31 +14,31 @@ const NEW_RULE = "\n@media (max-width: 600px) { div { color: blue; } }";
waitForExplicitFinish();
add_task(function*() {
let {UI} = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI);
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
is(UI.editors.length, 2, "correct number of editors");
is(ui.editors.length, 2, "correct number of editors");
// Test first plain css editor
let plainEditor = UI.editors[0];
let plainEditor = ui.editors[0];
yield openEditor(plainEditor);
testPlainEditor(plainEditor);
// Test editor with @media rules
let mediaEditor = UI.editors[1];
let mediaEditor = ui.editors[1];
yield openEditor(mediaEditor);
testMediaEditor(mediaEditor);
// Test that sidebar hides when flipping pref
yield testShowHide(UI, mediaEditor);
yield testShowHide(ui, mediaEditor);
// Test adding a rule updates the list
yield testMediaRuleAdded(UI, mediaEditor);
yield testMediaRuleAdded(ui, mediaEditor);
// Test resizing and seeing @media matching state change
let originalWidth = window.outerWidth;
let originalHeight = window.outerHeight;
let onMatchesChange = listenForMediaChange(UI);
let onMatchesChange = listenForMediaChange(ui);
window.resizeTo(RESIZE, RESIZE);
yield onMatchesChange;

View File

@ -0,0 +1,37 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that selected sheet and cursor position is reset during navigation.
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
const NEW_URI = TEST_BASE_HTTPS + "media.html";
const LINE_NO = 5;
const COL_NO = 3;
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
loadCommonFrameScript();
is(ui.editors.length, 2, "Two sheets present after load.");
info("Selecting the second editor");
yield ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
info("Navigating to another page.");
executeInContent("devtools:test:navigate", { location: NEW_URI }, {}, false);
info("Waiting for sheets to be loaded after navigation.");
yield ui.once("stylesheets-reset");
info("Waiting for source editor to be ready.");
yield ui.editors[0].getSourceEditor();
is(ui.selectedEditor, ui.editors[0], "first editor is selected");
let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
is(line, 0, "first line is selected");
is(ch, 0, "first column is selected");
});

View File

@ -1,55 +1,44 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that new sheets can be added and edited.
const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
let TESTCASE_CSS_SOURCE = "body{background-color:red;";
let gOriginalHref;
let gUI;
waitForExplicitFinish();
const TESTCASE_CSS_SOURCE = "body{background-color:red;";
add_task(function*() {
let panel = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI);
gUI = panel.UI;
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
let editor = yield createNew();
let editor = yield createNew(ui, panel.panelWindow);
testInitialState(editor);
let originalHref = editor.styleSheet.href;
let waitForPropertyChange = onPropertyChange(editor);
yield typeInEditor(editor);
yield typeInEditor(editor, panel.panelWindow);
yield waitForPropertyChange;
testUpdated(editor);
gUI = null;
testUpdated(editor, originalHref);
});
function createNew() {
function createNew(ui, panelWindow) {
info("Creating a new stylesheet now");
let deferred = promise.defer();
gUI.once("editor-added", (ev, editor) => {
ui.once("editor-added", (ev, editor) => {
editor.getSourceEditor().then(deferred.resolve);
});
waitForFocus(function () {// create a new style sheet
let newButton = gPanelWindow.document.querySelector(".style-editor-newButton");
let newButton = panelWindow.document.querySelector(".style-editor-newButton");
ok(newButton, "'new' button exists");
EventUtils.synthesizeMouseAtCenter(newButton, {}, gPanelWindow);
}, gPanelWindow);
EventUtils.synthesizeMouseAtCenter(newButton, {}, panelWindow);
}, panelWindow);
return deferred.promise;
}
@ -71,7 +60,6 @@ function onPropertyChange(aEditor) {
function testInitialState(aEditor) {
info("Testing the initial state of the new editor");
gOriginalHref = aEditor.styleSheet.href;
let summary = aEditor.summary;
@ -90,22 +78,22 @@ function testInitialState(aEditor) {
"content's background color is initially white");
}
function typeInEditor(aEditor) {
function typeInEditor(aEditor, panelWindow) {
let deferred = promise.defer();
waitForFocus(function () {
for each (let c in TESTCASE_CSS_SOURCE) {
EventUtils.synthesizeKey(c, {}, gPanelWindow);
for (let c of TESTCASE_CSS_SOURCE) {
EventUtils.synthesizeKey(c, {}, panelWindow);
}
ok(aEditor.unsaved, "new editor has unsaved flag");
deferred.resolve();
}, gPanelWindow);
}, panelWindow);
return deferred.promise;
}
function testUpdated(aEditor) {
function testUpdated(aEditor, originalHref) {
info("Testing the state of the new editor after editing it");
is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
@ -115,6 +103,6 @@ function testUpdated(aEditor) {
is(parseInt(ruleCount), 1,
"new editor shows 1 rule after modification");
is(aEditor.styleSheet.href, gOriginalHref,
is(aEditor.styleSheet.href, originalHref,
"style sheet href did not change");
}

View File

@ -1,41 +1,28 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that 'no styles' indicator is shown if a page doesn't contain any style
// sheets.
const TESTCASE_URI = TEST_BASE_HTTP + "nostyle.html";
add_task(function* () {
let { panel } = yield openStyleEditorForURL(TESTCASE_URI);
let { panelWindow } = panel;
function test()
{
waitForExplicitFinish();
// launch Style Editor right when the tab is created (before load)
// this checks that the Style Editor still launches correctly when it is opened
// *while* the page is still loading. The Style Editor should not signal that
// it is loaded until the accompanying content page is loaded.
addTabAndCheckOnStyleEditorAdded(function(panel) {
panel.UI.once("stylesheets-reset", testDocumentLoad);
content.location = TESTCASE_URI;
}, () => {});
}
function testDocumentLoad(event)
{
let root = gPanelWindow.document.querySelector(".splitview-root");
let root = panelWindow.document.querySelector(".splitview-root");
ok(!root.classList.contains("loading"),
"style editor root element does not have 'loading' class name anymore");
ok(root.querySelector(".empty.placeholder"), "showing 'no style' indicator");
let button = gPanelWindow.document.querySelector(".style-editor-newButton");
let button = panelWindow.document.querySelector(".style-editor-newButton");
ok(!button.hasAttribute("disabled"),
"new style sheet button is enabled");
button = gPanelWindow.document.querySelector(".style-editor-importButton");
button = panelWindow.document.querySelector(".style-editor-importButton");
ok(!button.hasAttribute("disabled"),
"import button is enabled");
finish();
}
});

View File

@ -1,56 +1,51 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that minified sheets are automatically prettified but other are left
// untouched.
const TESTCASE_URI = TEST_BASE_HTTP + "minified.html";
let gUI;
const PRETTIFIED_SOURCE = "" +
"body\{\r?\n" + // body{
"\tbackground\:white;\r?\n" + // background:white;
"\}\r?\n" + // }
"\r?\n" + //
"div\{\r?\n" + // div{
"\tfont\-size\:4em;\r?\n" + // font-size:4em;
"\tcolor\:red\r?\n" + // color:red
"\}\r?\n" + // }
"\r?\n" + //
"span\{\r?\n" + // span{
"\tcolor\:green;\r?\n" // color:green;
"\}\r?\n"; // }
function test()
{
waitForExplicitFinish();
const ORIGINAL_SOURCE = "" +
"body \{ background\: red; \}\r?\n" + // body { background: red; }
"div \{\r?\n" + // div {
"font\-size\: 5em;\r?\n" + // font-size: 5em;
"color\: red\r?\n" + // color: red
"\}"; // }
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, editor => {
editor.getSourceEditor().then(function() {
testEditor(editor);
});
});
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
is(ui.editors.length, 2, "Two sheets present.");
content.location = TESTCASE_URI;
}
info("Testing minified style sheet.");
let editor = yield ui.editors[0].getSourceEditor();
let editorTestedCount = 0;
function testEditor(aEditor)
{
if (aEditor.styleSheet.styleSheetIndex == 0) {
let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n\r?\nspan\{\r?\n\tcolor\:green;\r?\n\}\r?\n";
let prettifiedSourceRE = new RegExp(prettifiedSource);
let prettifiedSourceRE = new RegExp(PRETTIFIED_SOURCE);
ok(prettifiedSourceRE.test(editor.sourceEditor.getText()),
"minified source has been prettified automatically");
ok(prettifiedSourceRE.test(aEditor.sourceEditor.getText()),
"minified source has been prettified automatically");
editorTestedCount++;
let summary = gUI.editors[1].summary;
EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
}
info("Selecting second, non-minified style sheet.");
yield ui.selectStyleSheet(ui.editors[1].styleSheet);
if (aEditor.styleSheet.styleSheetIndex == 1) {
let originalSource = "body \{ background\: red; \}\r?\ndiv \{\r?\nfont\-size\: 5em;\r?\ncolor\: red\r?\n\}";
let originalSourceRE = new RegExp(originalSource);
editor = ui.editors[1];
ok(originalSourceRE.test(aEditor.sourceEditor.getText()),
"non-minified source has been left untouched");
editorTestedCount++;
}
if (editorTestedCount == 2) {
gUI = null;
finish();
}
}
let originalSourceRE = new RegExp(ORIGINAL_SOURCE);
ok(originalSourceRE.test(editor.sourceEditor.getText()),
"non-minified source has been left untouched");
});

View File

@ -1,105 +1,38 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that selected sheet and cursor position persists during reload.
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
const NEW_URI = TEST_BASE_HTTPS + "media.html";
const LINE_NO = 5;
const COL_NO = 3;
let gContentWin;
let gUI;
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
loadCommonFrameScript();
function test()
{
waitForExplicitFinish();
is(ui.editors.length, 2, "Two sheets present after load.");
addTabAndOpenStyleEditors(2, function(panel) {
gContentWin = gBrowser.selectedBrowser.contentWindow.wrappedJSObject;
gUI = panel.UI;
gUI.editors[0].getSourceEditor().then(runTests);
});
info("Selecting the second editor");
yield ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
content.location = TESTCASE_URI;
}
info("Reloading page.");
executeInContent("devtools:test:reload", {}, {}, false /* no response */);
function runTests()
{
let count = 0;
gUI.on("editor-selected", function editorSelected(event, editor) {
if (editor.styleSheet != gUI.editors[1].styleSheet) {
return;
}
gUI.off("editor-selected", editorSelected);
editor.getSourceEditor().then(() => {
info("selected second editor, about to reload page");
reloadPage();
info("Waiting for sheets to be loaded after reload.");
yield ui.once("stylesheets-reset");
gUI.on("editor-added", function editorAdded(event, editor) {
if (++count == 2) {
info("all editors added after reload");
gUI.off("editor-added", editorAdded);
gUI.editors[1].getSourceEditor().then(testRemembered);
}
})
});
});
gUI.selectStyleSheet(gUI.editors[1].styleSheet, LINE_NO, COL_NO);
}
is(ui.editors.length, 2, "Two sheets present after reload.");
function testRemembered()
{
is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
info("Waiting for source editor to be ready.");
yield ui.editors[1].getSourceEditor();
let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
is(ui.selectedEditor, ui.editors[1], "second editor is selected after reload");
let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
is(line, LINE_NO, "correct line selected");
is(ch, COL_NO, "correct column selected");
testNewPage();
}
function testNewPage()
{
let count = 0;
gUI.on("editor-added", function editorAdded(event, editor) {
info("editor added here")
if (++count == 2) {
info("all editors added after navigating page");
gUI.off("editor-added", editorAdded);
gUI.editors[0].getSourceEditor().then(testNotRemembered);
}
})
info("navigating to a different page");
navigatePage();
}
function testNotRemembered()
{
is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
is(line, 0, "first line is selected");
is(ch, 0, "first column is selected");
gUI = null;
finish();
}
function reloadPage()
{
gContentWin.location.reload();
}
function navigatePage()
{
gContentWin.location = NEW_URI;
}
});

View File

@ -1,58 +1,26 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that StyleEditorUI.selectStyleSheet selects the correct sheet, line and
// column.
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
const NEW_URI = TEST_BASE_HTTPS + "media.html";
const LINE_NO = 5;
const COL_NO = 0;
let gContentWin;
let gUI;
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
let editor = ui.editors[1];
function test()
{
waitForExplicitFinish();
info("Selecting style sheet #1.");
yield ui.selectStyleSheet(editor.styleSheet.href, LINE_NO);
addTabAndOpenStyleEditors(2, function(panel) {
gContentWin = gBrowser.selectedBrowser.contentWindow.wrappedJSObject;
gUI = panel.UI;
gUI.editors[0].getSourceEditor().then(runTests);
});
is(ui.selectedEditor, ui.editors[1], "Second editor is selected.");
let {line, ch} = ui.selectedEditor.sourceEditor.getCursor();
content.location = TESTCASE_URI;
}
function runTests()
{
let count = 0;
// Make sure Editor doesn't go into an infinite loop when
// column isn't passed. See bug 941018.
gUI.on("editor-selected", function editorSelected(event, editor) {
if (editor.styleSheet != gUI.editors[1].styleSheet) {
return;
}
gUI.off("editor-selected", editorSelected);
editor.getSourceEditor().then(() => {
is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
is(line, LINE_NO, "correct line selected");
is(ch, COL_NO, "correct column selected");
gUI = null;
finish();
});
});
gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO);
}
is(line, LINE_NO, "correct line selected");
is(ch, COL_NO, "correct column selected");
});

View File

@ -1,82 +1,67 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that the style sheet list can be navigated with keyboard.
const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
let gUI;
add_task(function* () {
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
function test()
{
waitForExplicitFinish();
info("Waiting for source editor to load.");
yield ui.editors[0].getSourceEditor();
addTabAndOpenStyleEditors(4, runTests);
let selected = ui.once("editor-selected");
content.location = TESTCASE_URI;
}
info("Testing keyboard navigation on the sheet list.");
testKeyboardNavigation(ui.editors[0], panel);
function runTests(panel)
{
gUI = panel.UI;
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[2].getSourceEditor().then(onEditor2Attach);
}
info("Waiting for editor #2 to be selected due to keyboard navigation.");
yield selected;
ok(ui.editors[2].sourceEditor.hasFocus(), "Editor #2 has focus.");
});
function getStylesheetNameLinkFor(aEditor)
{
return aEditor.summary.querySelector(".stylesheet-name");
}
function onEditor0Attach(aEditor)
function testKeyboardNavigation(aEditor, panel)
{
let panelWindow = panel.panelWindow;
let ui = panel.UI;
waitForFocus(function () {
let summary = aEditor.summary;
EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
EventUtils.synthesizeMouseAtCenter(summary, {}, panelWindow);
let item = getStylesheetNameLinkFor(gUI.editors[0]);
is(gPanelWindow.document.activeElement, item,
let item = getStylesheetNameLinkFor(ui.editors[0]);
is(panelWindow.document.activeElement, item,
"editor 0 item is the active element");
EventUtils.synthesizeKey("VK_DOWN", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[1]);
is(gPanelWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
item = getStylesheetNameLinkFor(ui.editors[1]);
is(panelWindow.document.activeElement, item,
"editor 1 item is the active element");
EventUtils.synthesizeKey("VK_HOME", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[0]);
is(gPanelWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_HOME", {}, panelWindow);
item = getStylesheetNameLinkFor(ui.editors[0]);
is(panelWindow.document.activeElement, item,
"fist editor item is the active element");
EventUtils.synthesizeKey("VK_END", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[3]);
is(gPanelWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_END", {}, panelWindow);
item = getStylesheetNameLinkFor(ui.editors[3]);
is(panelWindow.document.activeElement, item,
"last editor item is the active element");
EventUtils.synthesizeKey("VK_UP", {}, gPanelWindow);
item = getStylesheetNameLinkFor(gUI.editors[2]);
is(gPanelWindow.document.activeElement, item,
EventUtils.synthesizeKey("VK_UP", {}, panelWindow);
item = getStylesheetNameLinkFor(ui.editors[2]);
is(panelWindow.document.activeElement, item,
"editor 2 item is the active element");
EventUtils.synthesizeKey("VK_RETURN", {}, gPanelWindow);
EventUtils.synthesizeKey("VK_RETURN", {}, panelWindow);
// this will attach and give focus editor 2
}, gPanelWindow);
}
function onEditor2Attach(aEditor)
{
// Wait for the focus to be set.
executeSoon(function () {
ok(aEditor.sourceEditor.hasFocus(),
"editor 2 has focus");
gUI = null;
finish();
});
}, panelWindow);
}

View File

@ -4,16 +4,14 @@
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
waitForExplicitFinish();
const NEW_RULE = "body { background-color: purple; }";
add_task(function*() {
let {UI} = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI);
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
is(UI.editors.length, 2, "correct number of editors");
is(ui.editors.length, 2, "correct number of editors");
let editor = UI.editors[0];
let editor = ui.editors[0];
yield openEditor(editor);
// Set text twice in a row

View File

@ -179,13 +179,22 @@ let UI = {
UI.updateCommands();
UI.updateProjectButton();
UI.openProject();
UI.autoStartProject();
yield UI.autoStartProject();
UI.autoOpenToolbox();
UI.saveLastSelectedProject();
projectList.update();
});
return;
case "project-stopped":
case "project-started":
this.updateCommands();
projectList.update();
UI.autoOpenToolbox();
break;
case "project-stopped":
UI.destroyToolbox();
this.updateCommands();
projectList.update();
break;
case "runtime-global-actors":
this.updateCommands();
projectList.update();
@ -657,7 +666,7 @@ let UI = {
}, console.error);
},
autoStartProject: function() {
autoStartProject: Task.async(function*() {
let project = AppManager.selectedProject;
if (!project) {
@ -669,15 +678,27 @@ let UI = {
return; // For something that is not an editable app, we're done.
}
Task.spawn(function() {
// Do not force opening apps that are already running, as they may have
// some activity being opened and don't want to dismiss them.
if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) {
yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app");
}
yield UI.createToolbox();
});
},
// Do not force opening apps that are already running, as they may have
// some activity being opened and don't want to dismiss them.
if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) {
yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app");
}
}),
autoOpenToolbox: Task.async(function*() {
let project = AppManager.selectedProject;
if (!project) {
return;
}
if (!(project.type == "runtimeApp" ||
project.type == "mainProcess" ||
project.type == "tab")) {
return; // For something that is not an editable app, we're done.
}
yield UI.createToolbox();
}),
importAndSelectApp: Task.async(function* (source) {
let isPackaged = !!source.path;
@ -1015,6 +1036,7 @@ let UI = {
let panel = document.querySelector("#deck").selectedPanel;
let nbox = document.querySelector("#notificationbox");
if (panel && panel.id == "deck-panel-details" &&
AppManager.selectedProject &&
AppManager.selectedProject.type != "packaged" &&
this.toolboxIframe) {
nbox.setAttribute("toolboxfullscreen", "true");

View File

@ -249,6 +249,7 @@ cancel_button=Cancel
cannot_start_call_session_not_ready=Can't start call, session is not ready.
network_disconnected=The network connection terminated abruptly.
connection_error_see_console_notification=Call failed; see console for details.
no_media_failure_message=No camera or microphone found.
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
## parts between {{..}} because these will be replaced with links with the labels

View File

@ -201,7 +201,7 @@ let AboutHome = {
// Trigger a search through nsISearchEngine.getSubmission()
let submission = engine.getSubmission(data.searchTerms, null, "homepage");
let where = data.useNewTab ? "tab" : "current";
let where = data.useNewTab ? "tabshifted" : "current";
window.openUILinkIn(submission.uri.spec, where, false,
submission.postData);

View File

@ -226,9 +226,6 @@ this.ContentSearch = {
// method on it will throw.
return Promise.resolve();
}
if (data.useNewTab) {
browser.getTabBrowser().selectedTab = newTab;
}
let win = browser.ownerDocument.defaultView;
win.BrowserSearch.recordSearchInHealthReport(engine, data.whence,
data.selection || null);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -6,13 +6,10 @@
package org.mozilla.gecko;
import java.util.HashSet;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.prompts.PromptInput;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.AnchoredPopup;
@ -21,7 +18,6 @@ import org.mozilla.gecko.widget.DoorHanger;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import org.mozilla.gecko.widget.DoorhangerConfig;
public class DoorHangerPopup extends AnchoredPopup
@ -109,15 +105,16 @@ public class DoorHangerPopup extends AnchoredPopup
private DoorhangerConfig makeConfigFromJSON(JSONObject json) throws JSONException {
final int tabId = json.getInt("tabID");
final String id = json.getString("value");
final DoorhangerConfig config = new DoorhangerConfig(tabId, id);
final String typeString = json.optString("category");
final boolean isLogin = DoorHanger.Type.LOGIN.toString().equals(typeString);
final DoorHanger.Type doorhangerType = isLogin ? DoorHanger.Type.LOGIN : DoorHanger.Type.DEFAULT;
final DoorhangerConfig config = new DoorhangerConfig(tabId, id, doorhangerType, this);
config.setMessage(json.getString("message"));
config.setButtons(json.getJSONArray("buttons"));
config.appendButtonsFromJSON(json.getJSONArray("buttons"));
config.setOptions(json.getJSONObject("options"));
final String typeString = json.optString("category");
if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
config.setType(DoorHanger.Type.LOGIN);
}
return config;
}
@ -181,18 +178,6 @@ public class DoorHangerPopup extends AnchoredPopup
final DoorHanger newDoorHanger = DoorHanger.Get(mContext, config);
final JSONArray buttons = config.getButtons();
for (int i = 0; i < buttons.length(); i++) {
try {
JSONObject buttonObject = buttons.getJSONObject(i);
String label = buttonObject.getString("label");
String tag = String.valueOf(buttonObject.getInt("callback"));
newDoorHanger.addButton(label, tag, this);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating doorhanger button", e);
}
}
mDoorHangers.add(newDoorHanger);
mContent.addView(newDoorHanger);
@ -206,32 +191,10 @@ public class DoorHangerPopup extends AnchoredPopup
* DoorHanger.OnButtonClickListener implementation
*/
@Override
public void onButtonClick(DoorHanger dh, String tag) {
JSONObject response = new JSONObject();
try {
response.put("callback", tag);
CheckBox checkBox = dh.getCheckBox();
// If the checkbox is being used, pass its value
if (checkBox != null) {
response.put("checked", checkBox.isChecked());
}
List<PromptInput> doorHangerInputs = dh.getInputs();
if (doorHangerInputs != null) {
JSONObject inputs = new JSONObject();
for (PromptInput input : doorHangerInputs) {
inputs.put(input.getId(), input.getValue());
}
response.put("inputs", inputs);
}
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating onClick response", e);
}
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString());
GeckoAppShell.sendEventToGecko(e);
removeDoorHanger(dh);
removeDoorHanger(doorhanger);
updatePopup();
}

View File

@ -381,6 +381,7 @@ size. -->
<!ENTITY doorhanger_login_edit_username_hint "Username">
<!ENTITY doorhanger_login_edit_password_hint "Password">
<!ENTITY doorhanger_login_edit_toggle "Show password">
<!ENTITY doorhanger_login_edit_toast_error "Failed to save login">
<!ENTITY pref_titlebar_mode "Title bar">
<!ENTITY pref_titlebar_mode_title "Show page title">

View File

@ -39,6 +39,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
android:textColor="@color/link_blue"
android:paddingBottom="@dimen/doorhanger_section_padding_large"
android:visibility="gone"/>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/doorhanger_padding"
android:orientation="vertical">
<EditText android:id="@+id/username_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:hint="@string/doorhanger_login_edit_username_hint"/>
<EditText android:id="@+id/password_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:password="true"
android:hint="@string/doorhanger_login_edit_password_hint"/>
<CheckBox android:id="@+id/checkbox_toggle_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/doorhanger_login_edit_toggle"
android:paddingTop="@dimen/doorhanger_padding"/>
</LinearLayout>

View File

@ -345,6 +345,7 @@
<string name="doorhanger_login_edit_username_hint">&doorhanger_login_edit_username_hint;</string>
<string name="doorhanger_login_edit_password_hint">&doorhanger_login_edit_password_hint;</string>
<string name="doorhanger_login_edit_toggle">&doorhanger_login_edit_toggle;</string>
<string name="doorhanger_login_edit_toast_error">&doorhanger_login_edit_toast_error;</string>
<string name="pref_titlebar_mode">&pref_titlebar_mode;</string>
<string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string>

View File

@ -17,7 +17,6 @@ import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.DoorHanger;
import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
@ -34,6 +33,8 @@ import org.mozilla.gecko.widget.DoorhangerConfig;
* an arrow panel popup hanging from the lock icon in the browser toolbar.
*/
public class SiteIdentityPopup extends AnchoredPopup {
public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING };
private static final String LOGTAG = "GeckoSiteIdentityPopup";
private static final String MIXED_CONTENT_SUPPORT_URL =
@ -142,7 +143,7 @@ public class SiteIdentityPopup extends AnchoredPopup {
// Remove any existing mixed content notification.
removeMixedContentNotification();
final DoorhangerConfig config = new DoorhangerConfig();
final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.MIXED_CONTENT, mButtonClickListener);
int icon;
if (blocked) {
icon = R.drawable.shield_enabled_doorhanger;
@ -154,11 +155,11 @@ public class SiteIdentityPopup extends AnchoredPopup {
}
config.setLink(mContext.getString(R.string.learn_more), MIXED_CONTENT_SUPPORT_URL, "\n\n");
config.setType(DoorHanger.Type.SITE);
addNotificationButtons(config, blocked);
mMixedContentNotification = DoorHanger.Get(mContext, config);
mMixedContentNotification.setIcon(icon);
addNotificationButtons(mMixedContentNotification, blocked);
mContent.addView(mMixedContentNotification);
mDivider.setVisibility(View.VISIBLE);
@ -175,7 +176,7 @@ public class SiteIdentityPopup extends AnchoredPopup {
// Remove any existing tracking content notification.
removeTrackingContentNotification();
final DoorhangerConfig config = new DoorhangerConfig();
final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mButtonClickListener);
int icon;
if (blocked) {
@ -189,12 +190,12 @@ public class SiteIdentityPopup extends AnchoredPopup {
}
config.setLink(mContext.getString(R.string.learn_more), TRACKING_CONTENT_SUPPORT_URL, "\n\n");
config.setType(DoorHanger.Type.SITE);
addNotificationButtons(config, blocked);
mTrackingContentNotification = DoorHanger.Get(mContext, config);
mTrackingContentNotification.setIcon(icon);
addNotificationButtons(mTrackingContentNotification, blocked);
mContent.addView(mTrackingContentNotification);
mDivider.setVisibility(View.VISIBLE);
@ -207,13 +208,12 @@ public class SiteIdentityPopup extends AnchoredPopup {
}
}
private void addNotificationButtons(DoorHanger dh, boolean blocked) {
// TODO: Add support for buttons in DoorHangerConfig.
private void addNotificationButtons(DoorhangerConfig config, boolean blocked) {
if (blocked) {
dh.addButton(mContext.getString(R.string.disable_protection), "disable", mButtonClickListener);
dh.addButton(mContext.getString(R.string.keep_blocking), "keepBlocking", mButtonClickListener);
config.appendButton(mContext.getString(R.string.disable_protection), ButtonType.DISABLE.ordinal());
config.appendButton(mContext.getString(R.string.keep_blocking), ButtonType.KEEP_BLOCKING.ordinal());
} else {
dh.addButton(mContext.getString(R.string.enable_protection), "enable", mButtonClickListener);
config.appendButton(mContext.getString(R.string.enable_protection), ButtonType.ENABLE.ordinal());
}
}
@ -290,18 +290,9 @@ public class SiteIdentityPopup extends AnchoredPopup {
private class PopupButtonListener implements OnButtonClickListener {
@Override
public void onButtonClick(DoorHanger dh, String tag) {
try {
JSONObject data = new JSONObject();
data.put("allowContent", tag.equals("disable"));
data.put("contentType", (dh == mMixedContentNotification ? "mixed" : "tracking"));
GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", data.toString());
GeckoAppShell.sendEventToGecko(e);
} catch (JSONException e) {
Log.e(LOGTAG, "Exception creating message to enable/disable content blocking", e);
}
public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", response.toString());
GeckoAppShell.sendEventToGecko(e);
dismiss();
}
}

View File

@ -5,6 +5,9 @@
package org.mozilla.gecko.widget;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.Button;
import org.mozilla.gecko.R;
import org.mozilla.gecko.prompts.PromptInput;
@ -13,11 +16,11 @@ import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import org.mozilla.gecko.toolbar.SiteIdentityPopup;
import java.util.ArrayList;
import java.util.List;
@ -25,23 +28,16 @@ import java.util.List;
public class DefaultDoorHanger extends DoorHanger {
private static final String LOGTAG = "GeckoDefaultDoorHanger";
private final Resources mResources;
private static int sSpinnerTextColor = -1;
private List<PromptInput> mInputs;
private CheckBox mCheckBox;
public DefaultDoorHanger(Context context, DoorhangerConfig config) {
this(context, config, Type.DEFAULT);
}
public DefaultDoorHanger(Context context, DoorhangerConfig config, Type type) {
super(context, config, type);
mResources = getResources();
if (sSpinnerTextColor == -1) {
sSpinnerTextColor = getResources().getColor(R.color.text_color_primary_disable_only);
sSpinnerTextColor = mResources.getColor(R.color.text_color_primary_disable_only);
}
loadConfig(config);
}
@ -62,15 +58,15 @@ public class DefaultDoorHanger extends DoorHanger {
if (link != null) {
addLink(link.label, link.url, link.delimiter);
}
setButtons(config);
}
@Override
public List<PromptInput> getInputs() {
private List<PromptInput> getInputs() {
return mInputs;
}
@Override
public CheckBox getCheckBox() {
private CheckBox getCheckBox() {
return mCheckBox;
}
@ -115,6 +111,54 @@ public class DefaultDoorHanger extends DoorHanger {
}
}
@Override
protected Button createButtonInstance(final String text, final int id) {
final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
button.setText(text);
button.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
final JSONObject response = new JSONObject();
try {
// TODO: Bug 1149359 - Split this into each Doorhanger Type class.
switch (mType) {
case MIXED_CONTENT:
response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
response.put("contentType", ("mixed"));
break;
case TRACKING:
response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal()));
response.put("contentType", ("tracking"));
break;
default:
response.put("callback", id);
CheckBox checkBox = getCheckBox();
// If the checkbox is being used, pass its value
if (checkBox != null) {
response.put("checked", checkBox.isChecked());
}
List<PromptInput> doorHangerInputs = getInputs();
if (doorHangerInputs != null) {
JSONObject inputs = new JSONObject();
for (PromptInput input : doorHangerInputs) {
inputs.put(input.getId(), input.getValue());
}
response.put("inputs", inputs);
}
}
mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating onClick response", e);
}
}
});
return button;
}
private void styleInput(PromptInput input, View view) {
if (input instanceof PromptInput.MenulistInput) {
styleDropdownInputs(input, view);

View File

@ -6,49 +6,47 @@
package org.mozilla.gecko.widget;
import android.content.Context;
import android.content.res.Resources;
import android.text.Html;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.prompts.PromptInput;
import java.util.List;
public abstract class DoorHanger extends LinearLayout {
public static DoorHanger Get(Context context, DoorhangerConfig config) {
final Type type = config.getType();
if (type != null) {
switch (type) {
case LOGIN:
return new LoginDoorHanger(context, config);
case SITE:
return new DefaultDoorHanger(context, config, type);
}
switch (type) {
case LOGIN:
return new LoginDoorHanger(context, config);
case TRACKING:
case MIXED_CONTENT:
return new DefaultDoorHanger(context, config, type);
}
return new DefaultDoorHanger(context, config);
return new DefaultDoorHanger(context, config, type);
}
public static enum Type { DEFAULT, LOGIN, SITE }
public static enum Type { DEFAULT, LOGIN, TRACKING, MIXED_CONTENT}
public interface OnButtonClickListener {
public void onButtonClick(DoorHanger dh, String tag);
public void onButtonClick(JSONObject response, DoorHanger doorhanger);
}
private static final LayoutParams sButtonParams;
protected static final LayoutParams sButtonParams;
static {
sButtonParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f);
}
@ -58,7 +56,8 @@ public abstract class DoorHanger extends LinearLayout {
// Divider between doorhangers.
private final View mDivider;
private final LinearLayout mButtonsContainer;
protected final LinearLayout mButtonsContainer;
protected final OnButtonClickListener mOnButtonClickListener;
// The tab this doorhanger is associated with.
private final int mTabId;
@ -66,10 +65,13 @@ public abstract class DoorHanger extends LinearLayout {
// DoorHanger identifier.
private final String mIdentifier;
protected final Type mType;
private final ImageView mIcon;
private final TextView mMessage;
protected Context mContext;
protected final Context mContext;
protected final Resources mResources;
protected int mDividerColor;
@ -80,6 +82,7 @@ public abstract class DoorHanger extends LinearLayout {
protected DoorHanger(Context context, DoorhangerConfig config, Type type) {
super(context);
mContext = context;
mResources = context.getResources();
mTabId = config.getTabId();
mIdentifier = config.getId();
@ -96,16 +99,22 @@ public abstract class DoorHanger extends LinearLayout {
mDivider = findViewById(R.id.divider_doorhanger);
mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
mMessage = (TextView) findViewById(R.id.doorhanger_message);
if (type == Type.SITE) {
// TODO: Bug 1149359 - split this into DoorHanger subclasses.
if (type == Type.TRACKING || type == Type.MIXED_CONTENT) {
mMessage.setTextAppearance(getContext(), R.style.TextAppearance_DoorHanger_Small);
}
mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons);
mDividerColor = getResources().getColor(R.color.divider_light);
mType = type;
mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons);
mOnButtonClickListener = config.getButtonClickListener();
mDividerColor = mResources.getColor(R.color.divider_light);
setOrientation(VERTICAL);
}
abstract protected void loadConfig(DoorhangerConfig config);
protected abstract void loadConfig(DoorhangerConfig config);
protected void setOptions(final JSONObject options) {
final int persistence = options.optInt("persistence");
@ -119,6 +128,21 @@ public abstract class DoorHanger extends LinearLayout {
if (timeout > 0) {
mTimeout = timeout;
}
}
protected void setButtons(DoorhangerConfig config) {
final JSONArray buttons = config.getButtons();
final OnButtonClickListener listener = config.getButtonClickListener();
for (int i = 0; i < buttons.length(); i++) {
try {
final JSONObject buttonObject = buttons.getJSONObject(i);
final String label = buttonObject.getString("label");
final int callbackId = buttonObject.getInt("callback");
addButtonToLayout(label, callbackId);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating doorhanger button", e);
}
}
}
public int getTabId() {
@ -166,18 +190,13 @@ public abstract class DoorHanger extends LinearLayout {
mMessage.setMovementMethod(LinkMovementMethod.getInstance());
}
public void addButton(final String text, final String tag, final OnButtonClickListener listener) {
final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
button.setText(text);
button.setTag(tag);
button.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
listener.onButtonClick(DoorHanger.this, tag);
}
});
/**
* Creates and adds a button into the DoorHanger.
* @param text Button text
* @param id Identifier associated with the button
*/
private void addButtonToLayout(String text, int id) {
final Button button = createButtonInstance(text, id);
if (mButtonsContainer.getChildCount() == 0) {
// If this is the first button we're adding, make the choices layout visible.
mButtonsContainer.setVisibility(View.VISIBLE);
@ -195,6 +214,8 @@ public abstract class DoorHanger extends LinearLayout {
mButtonsContainer.addView(button, sButtonParams);
}
protected abstract Button createButtonInstance(String text, int id);
/*
* Checks with persistence and timeout options to see if it's okay to remove a doorhanger.
*
@ -222,14 +243,4 @@ public abstract class DoorHanger extends LinearLayout {
return true;
}
// TODO: remove and expose through instance Button Handler.
public List<PromptInput> getInputs() {
return null;
}
public CheckBox getCheckBox() {
return null;
}
}

View File

@ -5,7 +5,9 @@
package org.mozilla.gecko.widget;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.widget.DoorHanger.Type;
@ -24,23 +26,29 @@ public class DoorhangerConfig {
}
}
private static final String LOGTAG = "DoorhangerConfig";
private final int tabId;
private final String id;
private DoorHanger.Type type;
private final DoorHanger.OnButtonClickListener buttonClickListener;
private final DoorHanger.Type type;
private String message;
private JSONObject options;
private Link link;
private JSONArray buttons;
private JSONArray buttons = new JSONArray();
public DoorhangerConfig() {
public DoorhangerConfig(Type type, DoorHanger.OnButtonClickListener listener) {
// XXX: This should only be used by SiteIdentityPopup doorhangers which
// don't need tab or id references, until bug 1141904 unifies doorhangers.
this(-1, null);
this(-1, null, type, listener);
}
public DoorhangerConfig(int tabId, String id) {
public DoorhangerConfig(int tabId, String id, DoorHanger.Type type, DoorHanger.OnButtonClickListener buttonClickListener) {
this.tabId = tabId;
this.id = id;
this.type = type;
this.buttonClickListener = buttonClickListener;
}
public int getTabId() {
@ -51,10 +59,6 @@ public class DoorhangerConfig {
return id;
}
public void setType(Type type) {
this.type = type;
}
public Type getType() {
return type;
}
@ -75,8 +79,33 @@ public class DoorhangerConfig {
return options;
}
public void setButtons(JSONArray buttons) {
this.buttons = buttons;
/**
* Add buttons from JSON to the Config object.
* @param buttons JSONArray of JSONObjects of the form { label: <label>, callback: <callback_id> }
*/
public void appendButtonsFromJSON(JSONArray buttons) {
try {
for (int i = 0; i < buttons.length(); i++) {
this.buttons.put(buttons.get(i));
}
} catch (JSONException e) {
Log.e(LOGTAG, "Error parsing buttons from JSON", e);
}
}
public void appendButton(String label, int callbackId) {
final JSONObject button = new JSONObject();
try {
button.put("label", label);
button.put("callback", callbackId);
this.buttons.put(button);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating button", e);
}
}
public DoorHanger.OnButtonClickListener getButtonClickListener() {
return this.buttonClickListener;
}
public JSONArray getButtons() {

View File

@ -5,12 +5,22 @@
package org.mozilla.gecko.widget;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import ch.boye.httpclientandroidlib.util.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
@ -20,9 +30,11 @@ import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
public class LoginDoorHanger extends DoorHanger {
private static final String LOGTAG = "LoginDoorHanger";
private enum ActionType { EDIT };
final TextView mTitle;
final TextView mLogin;
private final TextView mTitle;
private final TextView mLogin;
private int mCallbackID;
public LoginDoorHanger(Context context, DoorhangerConfig config) {
super(context, config, Type.LOGIN);
@ -37,7 +49,7 @@ public class LoginDoorHanger extends DoorHanger {
protected void loadConfig(DoorhangerConfig config) {
setOptions(config.getOptions());
setMessage(config.getMessage());
setButtons(config);
}
@Override
@ -51,7 +63,7 @@ public class LoginDoorHanger extends DoorHanger {
final String text = titleObj.getString("text");
mTitle.setText(text);
} catch (JSONException e) {
Log.e(LOGTAG, "Error loading title from options JSON");
Log.e(LOGTAG, "Error loading title from options JSON", e);
}
final String resource = titleObj.optString("resource");
@ -60,20 +72,132 @@ public class LoginDoorHanger extends DoorHanger {
@Override
public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
if (favicon != null) {
mTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(mContext.getResources(), favicon), null, null, null);
mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
mTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(mResources, favicon), null, null, null);
mTitle.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
}
}
});
}
}
final String subtext = options.optString("subtext");
if (!TextUtils.isEmpty(subtext)) {
mLogin.setText(subtext);
mLogin.setVisibility(View.VISIBLE);
} else {
final JSONObject actionText = options.optJSONObject("actionText");
addActionText(actionText);
}
@Override
protected Button createButtonInstance(final String text, final int id) {
// HACK: Confirm button will the the rightmost/last button added. Bug 1147064 should add differentiation of the two.
mCallbackID = id;
final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null);
button.setText(text);
button.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
final JSONObject response = new JSONObject();
try {
response.put("callback", id);
} catch (JSONException e) {
Log.e(LOGTAG, "Error making doorhanger response message", e);
}
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
}
});
return button;
}
/**
* Add sub-text to the doorhanger and add the click action.
*
* If the parsing the action from the JSON throws, the text is left visible, but there is no
* click action.
* @param actionTextObj JSONObject containing blob for making an action.
*/
private void addActionText(JSONObject actionTextObj) {
if (actionTextObj == null) {
mLogin.setVisibility(View.GONE);
return;
}
boolean hasUsername = true;
String text = actionTextObj.optString("text");
if (TextUtils.isEmpty(text)) {
hasUsername = false;
text = mResources.getString(R.string.doorhanger_login_no_username);
}
mLogin.setText(text);
mLogin.setVisibility(View.VISIBLE);
// Make action.
try {
final JSONObject bundle = actionTextObj.getJSONObject("bundle");
final ActionType type = ActionType.valueOf(actionTextObj.getString("type"));
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
switch (type) {
case EDIT:
builder.setTitle(mResources.getString(R.string.doorhanger_login_edit_title));
final View view = LayoutInflater.from(mContext).inflate(R.layout.login_edit_dialog, null);
final EditText username = (EditText) view.findViewById(R.id.username_edit);
username.setText(bundle.getString("username"));
final EditText password = (EditText) view.findViewById(R.id.password_edit);
password.setText(bundle.getString("password"));
final CheckBox passwordCheckbox = (CheckBox) view.findViewById(R.id.checkbox_toggle_password);
passwordCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
password.setTransformationMethod(null);
} else {
password.setTransformationMethod(PasswordTransformationMethod.getInstance());
}
}
});
builder.setView(view);
builder.setPositiveButton(R.string.button_remember, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
JSONObject response = new JSONObject();
try {
response.put("callback", mCallbackID);
final JSONObject inputs = new JSONObject();
inputs.put("username", username.getText());
inputs.put("password", password.getText());
response.put("inputs", inputs);
} catch (JSONException e) {
Log.e(LOGTAG, "Error creating doorhanger reply message");
response = null;
Toast.makeText(mContext, mResources.getString(R.string.doorhanger_login_edit_toast_error), Toast.LENGTH_SHORT).show();
}
mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this);
}
});
builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
}
final Dialog dialog = builder.create();
mLogin.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dialog.show();
}
});
} catch (JSONException e) {
// Log an error, but leave the text visible if there was a username.
Log.e(LOGTAG, "Error fetching actionText from JSON", e);
if (!hasUsername) {
mLogin.setVisibility(View.GONE);
}
}
}
}

View File

@ -2232,7 +2232,11 @@ var NativeWindow = {
* { text: <title>,
* resource: <resource_url> }
*
* subtext: A string to appear below the doorhanger message.
* actionText: An object that specifies a clickable string, a type of action,
* and a bundle blob for the consumer to create a click action.
* { text: <text>,
* type: <type>,
* bundle: <blob-object> }
*
* @param aCategory
* Doorhanger type to display (e.g., LOGIN)

View File

@ -148,11 +148,11 @@ LoginManagerPrompter.prototype = {
* String message to be displayed in the doorhanger
* @param aButtons
* Buttons to display with the doorhanger
* @param aSubtext
* String to be displayed below the aBody message
* @param aActionText
* Object with text to be displayed as clickable, along with a bundle to create an action
*
*/
_showLoginNotification : function (aName, aTitle, aBody, aButtons, aSubtext) {
_showLoginNotification : function (aName, aTitle, aBody, aButtons, aActionText) {
this.log("Adding new " + aName + " notification bar");
let notifyWin = this._window.top;
let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
@ -171,7 +171,7 @@ LoginManagerPrompter.prototype = {
persistWhileVisible: true,
timeout: Date.now() + 10000,
title: aTitle,
subtext: aSubtext
actionText: aActionText
}
var nativeWindow = this._getNativeWindow();
@ -194,11 +194,16 @@ LoginManagerPrompter.prototype = {
let displayHost = this._getShortDisplayHost(aLogin.hostname);
let title = { text: displayHost, resource: aLogin.hostname };
let subtext = null;
if (aLogin.username) {
subtext = this._sanitizeUsername(aLogin.username);
}
let username = aLogin.username ? this._sanitizeUsername(aLogin.username) : "";
let actionText = {
text: username,
type: "EDIT",
bundle: { username: username,
password: aLogin.password }
};
// The callbacks in |buttons| have a closure to access the variables
// in scope here; set one to |this._pwmgr| so we can get back to pwmgr
// without a getService() call.
@ -215,14 +220,19 @@ LoginManagerPrompter.prototype = {
},
{
label: this._getLocalizedString("rememberButton"),
callback: function() {
callback: function(checked, response) {
if (response) {
aLogin.username = response["username"] || aLogin.username;
aLogin.password = response["password"] || aLogin.password;
}
pwmgr.addLogin(aLogin);
promptHistogram.add(PROMPT_ADD);
}
}
];
this._showLoginNotification("password-save", title, notificationText, buttons, subtext);
this._showLoginNotification("password-save", title, notificationText, buttons, actionText);
},
/*

View File

@ -74,7 +74,7 @@ body {
display: none;
text-align: center;
width: 100%;
font-size: 0.9rem;
font-size: 0.9em;
}
.header {
@ -101,7 +101,7 @@ body {
}
.header > h1 {
font-size: 1.33rem;
font-size: 1.33em;
font-weight: 700;
line-height: 1.1em;
width: 100%;
@ -146,16 +146,16 @@ body {
/* This covers caption, domain, and credits
texts in the reader UI */
.content .wp-caption-text,
.content figcaption,
#moz-reader-content .wp-caption-text,
#moz-reader-content figcaption,
.header > .domain,
.header > .credits {
font-size: 0.9rem;
font-size: 0.9em;
}
.content {
#moz-reader-content {
display: none;
font-size: 1rem;
font-size: 1em;
}
.content a {
@ -175,7 +175,7 @@ body {
height: auto !important;
}
.content p {
#moz-reader-content p {
line-height: 1.4em !important;
margin: 0px !important;
margin-bottom: 20px !important;
@ -258,8 +258,8 @@ body {
border-left-color: #777777 !important;
}
.content ul,
.content ol {
#moz-reader-content ul,
#moz-reader-content ol {
margin: 0px !important;
margin-bottom: 20px !important;
padding: 0px !important;

View File

@ -48,7 +48,7 @@ let AboutReader = function(mm, win, articlePromise) {
this._domainElementRef = Cu.getWeakReference(doc.getElementById("reader-domain"));
this._titleElementRef = Cu.getWeakReference(doc.getElementById("reader-title"));
this._creditsElementRef = Cu.getWeakReference(doc.getElementById("reader-credits"));
this._contentElementRef = Cu.getWeakReference(doc.getElementById("reader-content"));
this._contentElementRef = Cu.getWeakReference(doc.getElementById("moz-reader-content"));
this._toolbarElementRef = Cu.getWeakReference(doc.getElementById("reader-toolbar"));
this._messageElementRef = Cu.getWeakReference(doc.getElementById("reader-message"));
@ -354,13 +354,13 @@ AboutReader.prototype = {
},
_setFontSize: function Reader_setFontSize(newFontSize) {
let htmlClasses = this._doc.documentElement.classList;
let containerClasses = this._doc.getElementById("container").classList;
if (this._fontSize > 0)
htmlClasses.remove("font-size" + this._fontSize);
containerClasses.remove("font-size" + this._fontSize);
this._fontSize = newFontSize;
htmlClasses.add("font-size" + this._fontSize);
containerClasses.add("font-size" + this._fontSize);
this._mm.sendAsyncMessage("Reader:SetIntPref", {
name: "reader.font_size",

View File

@ -18,6 +18,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm
XPCOMUtils.defineLazyModuleGetter(this, "ReaderWorker", "resource://gre/modules/reader/ReaderWorker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyGetter(this, "Readability", function() {
let scope = {};
Services.scriptloader.loadSubScript("resource://gre/modules/reader/Readability.js", scope);
return scope["Readability"];
});
this.ReaderMode = {
// Version of the cache schema.
CACHE_VERSION: 1,
@ -78,36 +84,7 @@ this.ReaderMode = {
return false;
}
let REGEXPS = {
unlikelyCandidates: /combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter/i,
okMaybeItsACandidate: /and|article|body|column|main|shadow/i,
};
let nodes = doc.getElementsByTagName("p");
if (nodes.length < 5) {
return false;
}
let possibleParagraphs = 0;
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
let matchString = node.className + " " + node.id;
if (REGEXPS.unlikelyCandidates.test(matchString) &&
!REGEXPS.okMaybeItsACandidate.test(matchString)) {
continue;
}
if (node.textContent.trim().length < 100) {
continue;
}
possibleParagraphs++;
if (possibleParagraphs >= 5) {
return true;
}
}
return false;
return new Readability(uri, doc).isProbablyReaderable();
},
/**

View File

@ -19,7 +19,7 @@
<div id="reader-credits" class="credits"></div>
</div>
<div id="reader-content" class="content">
<div id="moz-reader-content" class="content">
</div>
<div id="reader-message" class="message">

View File

@ -77,6 +77,8 @@
<li><a href="about:license#chromium">Chromium License</a></li>
<li><a href="about:license#codemirror">CodeMirror License</a></li>
<li><a href="about:license#cubic-bezier">cubic-bezier License</a></li>
<li><a href="about:license#d3">D3 License</a></li>
<li><a href="about:license#dagre-d3">Dagre-D3 License</a></li>
<li><a href="about:license#dtoa">dtoa License</a></li>
<li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
<li><a href="about:license#edl">Eclipse Distribution License</a></li>
@ -1161,13 +1163,13 @@ DAMAGES.
<p>Some versions of this product contains code from the following LGPLed libraries:</p>
<ul>
<li><a
<li><a
href="https://addons.mozilla.org/en-US/firefox/addon/görans-hemmasnickrade-ordli/">Swedish dictionary</a>
</ul>
<pre>Copyright &copy; 2007 Free Software Foundation, Inc.
&lt;<a href="http://fsf.org/">http://fsf.org/</a>&gt;
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.</pre>
@ -1368,7 +1370,7 @@ executables of your software products.
<h1><a id="adobecmap"></a>Adobe CMap License</h1>
<p>This license applies to files in the directory
<p>This license applies to files in the directory
<span class="path">browser/extensions/pdfjs/content/web/cmaps/</span>.</p>
<pre>
@ -1386,12 +1388,12 @@ disclaimer.
Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
provided with the distribution.
Neither the name of Adobe Systems Incorporated nor the names
of its contributors may be used to endorse or promote
products derived from this software without specific prior
written permission.
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
@ -2056,6 +2058,73 @@ DEALINGS IN THE SOFTWARE.
</pre>
<hr>
<h1><a id="d3"></a>D3 License</h1>
<p>This license applies to the file
<span class="path">browser/devtools/shared/d3.js</span>.
</p>
<pre>
Copyright (c) 2014, Michael Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name Michael Bostock may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</pre>
<hr>
<h1><a id="dagre-d3"></a>Dagre-D3 License</h1>
<p>This license applies to the file
<span class="path">browser/devtools/webaudioeditor/lib/dagre-d3.js</span>.
</p>
<pre>
Copyright (c) 2013 Chris Pettitt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
</pre>
<hr>
<h1><a id="dtoa"></a>dtoa License</h1>
@ -4685,4 +4754,3 @@ following terms:
</body>
</html>

View File

@ -2,6 +2,32 @@
* 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/. */
/**
* Test log warnings that happen before the test has started
* "Couldn't get the user appdata directory. Crash events may not be produced."
* in nsExceptionHandler.cpp (possibly bug 619104)
*
* Test log warnings that happen after the test has finished
* "OOPDeinit() without successful OOPInit()" in nsExceptionHandler.cpp
* (bug 619104)
* "XPCOM objects created/destroyed from static ctor/dtor" in nsTraceRefcnt.cpp
* (possibly bug 457479)
*
* Other warnings printed to the test logs
* "site security information will not be persisted" in
* nsSiteSecurityService.cpp and the error in nsSystemInfo.cpp preceding this
* error are due to not having a profile when running some of the xpcshell
* tests. Since most xpcshell tests also log these errors these tests don't
* call do_get_profile unless necessary for the test.
* The "This method is lossy. Use GetCanonicalPath !" warning on Windows in
* nsLocalFileWin.cpp is from the call to GetNSSProfilePath in
* nsNSSComponent.cpp due to it using GetNativeCanonicalPath.
* "!mMainThread" in nsThreadManager.cpp are due to using timers and it might be
* possible to fix some or all of these in the test itself.
* "NS_FAILED(rv)" in nsThreadUtils.cpp are due to using timers and it might be
* possible to fix some or all of these in the test itself.
*/
'use strict';
const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
@ -782,6 +808,17 @@ function setupTestCommon() {
// it is defined as a function.
adjustGeneralPaths();
// This prevents a warning about not being able to find the greprefs.js file
// from being logged.
let grePrefsFile = getGREDir();
if (!grePrefsFile.exists()) {
grePrefsFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
}
grePrefsFile.append("greprefs.js");
if (!grePrefsFile.exists()) {
grePrefsFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
}
// Remove the updates directory on Windows and Mac OS X which is located
// outside of the application directory after the call to adjustGeneralPaths
// has set it up. Since the test hasn't ran yet and the directory shouldn't
@ -1581,8 +1618,8 @@ function shouldRunServiceTest(aFirstTest) {
// in which case we should fail the test if the updater binary is signed so
// the build system can be fixed by adding the registry key.
if (IS_AUTHENTICODE_CHECK_ENABLED) {
Assert.ok(isBinSigned, "the updater.exe binary should not be signed " +
"when the test registry key doesn't exist (if not, build " +
Assert.ok(!isBinSigned, "the updater.exe binary should not be signed " +
"when the test registry key doesn't exist (if it is, build " +
"system configuration bug?)");
}

View File

@ -93,7 +93,7 @@ body.loaded {
display: none;
text-align: center;
width: 100%;
font-size: 0.9rem;
font-size: 0.9em;
}
/* Header */
@ -104,8 +104,8 @@ body.loaded {
}
.domain {
font-size: 0.9rem;
line-height: 1.33rem;
font-size: 0.9em;
line-height: 1.48em;
padding-bottom: 4px;
font-family: "Fira Sans", Helvetica, Arial, sans-serif;
text-decoration: none;
@ -123,16 +123,16 @@ body.loaded {
}
.header > h1 {
font-size: 1.33rem;
line-height: 1.66rem;
font-size: 1.33em;
line-height: 1.25em;
width: 100%;
margin: 30px 0;
padding: 0;
}
.header > .credits {
font-size: 0.9rem;
line-height: 1.33rem;
font-size: 0.9em;
line-height: 1.48em;
margin: 0 0 30px 0;
padding: 0;
font-style: italic;
@ -140,10 +140,10 @@ body.loaded {
/* Content */
.content {
#moz-reader-content {
display: none;
font-size: 1rem;
line-height: 1.6rem;
font-size: 1em;
line-height: 1.6em;
}
.content h1,
@ -152,19 +152,19 @@ body.loaded {
font-weight: bold;
}
.content h1 {
font-size: 1.33rem;
line-height: 1.66rem;
#moz-reader-content h1 {
font-size: 1.33em;
line-height: 1.25em;
}
.content h2 {
font-size: 1.1rem;
line-height: 1.66rem;
#moz-reader-content h2 {
font-size: 1.1em;
line-height: 1.51em;
}
.content h3 {
font-size: 1rem;
line-height: 1.66rem;
#moz-reader-content h3 {
font-size: 1em;
line-height: 1.66em;
}
.content a {
@ -208,11 +208,11 @@ body.loaded {
margin-right: auto;
}
.content .caption,
.content .wp-caption-text,
.content figcaption {
font-size: 0.9rem;
line-height: 1.33rem;
#moz-reader-content .caption,
#moz-reader-content .wp-caption-text,
#moz-reader-content figcaption {
font-size: 0.9em;
line-height: 1.48em;
font-style: italic;
}