This commit is contained in:
Ryan VanderMeulen 2014-02-12 08:33:21 -05:00
Родитель 325b72dad4 9e384f54b5
Коммит e91712ff4f
92 изменённых файлов: 3500 добавлений и 2127 удалений

Просмотреть файл

@ -20,7 +20,7 @@
<command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" />
<command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" />
<command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/>
<command id="cmd_newNavigatorTab" oncommand="BrowserOpenNewTabOrWindow(event);"/>
<command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/>
<command id="Browser:SavePage" oncommand="saveDocument(window.content.document);"/>

Просмотреть файл

@ -202,14 +202,9 @@ var ctrlTab = {
list = list.filter(function (tab) !tab.closing);
if (this.recentlyUsedLimit != 0) {
let recentlyUsedTabs = [];
for (let tab of this._recentlyUsedTabs) {
if (!tab.hidden && !tab.closing) {
recentlyUsedTabs.push(tab);
if (this.recentlyUsedLimit > 0 && recentlyUsedTabs.length >= this.recentlyUsedLimit)
break;
}
}
let recentlyUsedTabs = this._recentlyUsedTabs;
if (this.recentlyUsedLimit > 0)
recentlyUsedTabs = this._recentlyUsedTabs.slice(0, this.recentlyUsedLimit);
for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) {
list.splice(list.indexOf(recentlyUsedTabs[i]), 1);
list.unshift(recentlyUsedTabs[i]);

Просмотреть файл

@ -7203,3 +7203,11 @@ let BrowserChromeTest = {
this._cb = cb;
}
};
function BrowserOpenNewTabOrWindow(event) {
if (event.shiftKey) {
OpenBrowserWindow();
} else {
BrowserOpenTab();
}
}

Просмотреть файл

@ -120,7 +120,6 @@
oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
<toolbarseparator/>
<toolbaritem id="panelMenu_bookmarksMenu"
flex="1"
orient="vertical"
smoothscroll="false"
onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
@ -145,7 +144,7 @@
<panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
<label value="&helpMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-helpItems"/>
<vbox id="PanelUI-helpItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-developer" flex="1">

Просмотреть файл

@ -303,6 +303,7 @@ const PanelUI = {
let multiView = document.createElement("panelmultiview");
tempPanel.appendChild(multiView);
multiView.setAttribute("mainViewIsSubView", "true");
multiView.setMainView(viewNode);
viewNode.classList.add("cui-widget-panelview");
CustomizableUI.addPanelCloseListeners(tempPanel);

Просмотреть файл

@ -66,6 +66,8 @@
<property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
<property name="_mainView" readonly="true"
onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
<property name="showingSubViewAsMainView" readonly="true"
onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
<property name="ignoreMutations">
<getter>
@ -322,8 +324,13 @@
<method name="_syncContainerWithMainView">
<body><![CDATA[
if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
this._viewContainer.style.height =
this._mainView.scrollHeight + "px";
let height;
if (this.showingSubViewAsMainView) {
height = this._heightOfSubview(this._mainView);
} else {
height = this._mainView.scrollHeight;
}
this._viewContainer.style.height = height + "px";
}
]]></body>
</method>

Просмотреть файл

@ -3139,7 +3139,7 @@ function WidgetSingleWrapper(aWidget, aNode) {
this.__defineGetter__("anchor", function() {
let anchorId;
// First check for an anchor for the area:
let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
if (placement) {
anchorId = gAreas.get(placement.area).get("anchor");
}

Просмотреть файл

@ -854,6 +854,7 @@ CustomizeMode.prototype = {
this.persistCurrentSets(true);
this._updateResetButton();
this._updateEmptyPaletteNotice();
this._showPanelCustomizationPlaceholders();
this.resetting = false;
}.bind(this)).then(null, ERROR);
@ -955,8 +956,10 @@ CustomizeMode.prototype = {
_onUIChange: function() {
this._changed = true;
this._updateResetButton();
this._updateEmptyPaletteNotice();
if (!this.resetting) {
this._updateResetButton();
this._updateEmptyPaletteNotice();
}
this.dispatchToolboxEvent("customizationchange");
},

Просмотреть файл

@ -29,6 +29,9 @@ const EVENTS = {
SOURCE_SHOWN: "Debugger:EditorSourceShown",
SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
// When the editor has shown a source and set the line / column position
EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
// When scopes, variables, properties and watch expressions are fetched and
// displayed in the variables view.
FETCHED_SCOPES: "Debugger:FetchedScopes",
@ -1300,7 +1303,7 @@ SourceScripts.prototype = {
deferred.promise.pretty = wantPretty;
this._cache.set(aSource.url, deferred.promise);
const afterToggle = ({ error, message, source: text }) => {
const afterToggle = ({ error, message, source: text, contentType }) => {
if (error) {
// Revert the rejected promise from the cache, so that the original
// source's text may be shown when the source is selected.
@ -1308,7 +1311,7 @@ SourceScripts.prototype = {
deferred.reject([aSource, message || error]);
return;
}
deferred.resolve([aSource, text]);
deferred.resolve([aSource, text, contentType]);
};
if (wantPretty) {
@ -1360,14 +1363,15 @@ SourceScripts.prototype = {
}
// Get the source text from the active thread.
this.activeThread.source(aSource).source(({ error, message, source: text }) => {
this.activeThread.source(aSource)
.source(({ error, message, source: text, contentType }) => {
if (aOnTimeout) {
window.clearTimeout(fetchTimeout);
}
if (error) {
deferred.reject([aSource, message || error]);
} else {
deferred.resolve([aSource, text]);
deferred.resolve([aSource, text, contentType]);
}
});
@ -1406,13 +1410,13 @@ SourceScripts.prototype = {
}
/* Called if fetching a source finishes successfully. */
function onFetch([aSource, aText]) {
function onFetch([aSource, aText, aContentType]) {
// If fetching the source has previously timed out, discard it this time.
if (!pending.has(aSource.url)) {
return;
}
pending.delete(aSource.url);
fetched.push([aSource.url, aText]);
fetched.push([aSource.url, aText, aContentType]);
maybeFinish();
}

Просмотреть файл

@ -388,7 +388,8 @@ let DebuggerView = {
this._setEditorText(L10N.getStr("loadingText"));
this._editorSource = { url: aSource.url, promise: deferred.promise };
DebuggerController.SourceScripts.getText(aSource).then(([, aText]) => {
DebuggerController.SourceScripts.getText(aSource)
.then(([, aText, aContentType]) => {
// Avoid setting an unexpected source. This may happen when switching
// very fast between sources that haven't been fetched yet.
if (this._editorSource.url != aSource.url) {
@ -396,7 +397,7 @@ let DebuggerView = {
}
this._setEditorText(aText);
this._setEditorMode(aSource.url, aSource.contentType, aText);
this._setEditorMode(aSource.url, aContentType, aText);
// Synchronize any other components with the currently displayed source.
DebuggerView.Sources.selectedValue = aSource.url;
@ -406,7 +407,7 @@ let DebuggerView = {
// Resolve and notify that a source file was shown.
window.emit(EVENTS.SOURCE_SHOWN, aSource);
deferred.resolve([aSource, aText]);
deferred.resolve([aSource, aText, aContentType]);
},
([, aError]) => {
let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError);
@ -466,10 +467,14 @@ let DebuggerView = {
// Make sure the requested source client is shown in the editor, then
// update the source editor's caret position and debug location.
return this._setEditorSource(sourceForm, aFlags).then(() => {
return this._setEditorSource(sourceForm, aFlags)
.then(([,, aContentType]) => {
// Record the contentType learned from fetching
sourceForm.contentType = aContentType;
// Line numbers in the source editor should start from 1. If invalid
// or not specified, then don't do anything.
if (aLine < 1) {
window.emit(EVENTS.EDITOR_LOCATION_SET);
return;
}
if (aFlags.charOffset) {
@ -485,6 +490,7 @@ let DebuggerView = {
if (!aFlags.noDebug) {
this.editor.setDebugLocation(aLine - 1);
}
window.emit(EVENTS.EDITOR_LOCATION_SET);
}).then(null, console.error);
},

Просмотреть файл

@ -28,6 +28,8 @@ support-files =
code_ugly-5.js
code_ugly-6.js
code_ugly-7.js
code_ugly-8
code_ugly-8^headers^
doc_auto-pretty-print-01.html
doc_auto-pretty-print-02.html
doc_binary_search.html
@ -56,6 +58,7 @@ support-files =
doc_pause-exceptions.html
doc_pretty-print.html
doc_pretty-print-2.html
doc_pretty-print-3.html
doc_random-javascript.html
doc_recursion-stack.html
doc_scope-variable.html
@ -161,6 +164,7 @@ support-files =
[browser_dbg_pretty-print-10.js]
[browser_dbg_pretty-print-11.js]
[browser_dbg_pretty-print-12.js]
[browser_dbg_pretty-print-13.js]
[browser_dbg_progress-listener-bug.js]
[browser_dbg_reload-preferred-script-01.js]
[browser_dbg_reload-preferred-script-02.js]

Просмотреть файл

@ -0,0 +1,87 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that clicking the pretty print button prettifies the source, even
* when the source URL does not end in ".js", but the content type is
* JavaScript.
*/
const TAB_URL = EXAMPLE_URL + "doc_pretty-print-3.html";
let gTab, gDebuggee, gPanel, gDebugger;
let gEditor, gSources;
function test() {
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
gTab = aTab;
gDebuggee = aDebuggee;
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gEditor = gDebugger.DebuggerView.editor;
gSources = gDebugger.DebuggerView.Sources;
promise.all([waitForSourceShown(gPanel, "code_ugly-8"),
waitForEditorLocationSet(gPanel)])
.then(testSourceIsUgly)
.then(() => {
const finished = waitForSourceShown(gPanel, "code_ugly-8");
clickPrettyPrintButton();
testProgressBarShown();
return finished;
})
.then(testSourceIsPretty)
.then(testEditorShown)
.then(testSourceIsStillPretty)
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
});
});
}
function testSourceIsUgly() {
ok(!gEditor.getText().contains("\n "),
"The source shouldn't be pretty printed yet.");
}
function clickPrettyPrintButton() {
gDebugger.document.getElementById("pretty-print").click();
}
function testProgressBarShown() {
const deck = gDebugger.document.getElementById("editor-deck");
is(deck.selectedIndex, 2, "The progress bar should be shown");
}
function testSourceIsPretty() {
ok(gEditor.getText().contains("\n "),
"The source should be pretty printed.")
}
function testEditorShown() {
const deck = gDebugger.document.getElementById("editor-deck");
is(deck.selectedIndex, 0, "The editor should be shown");
}
function testSourceIsStillPretty() {
const deferred = promise.defer();
const { source } = gSources.selectedItem.attachment;
gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => {
ok(text.contains("\n "),
"Subsequent calls to getText return the pretty printed source.");
deferred.resolve();
});
return deferred.promise;
}
registerCleanupFunction(function() {
gTab = null;
gDebuggee = null;
gPanel = null;
gDebugger = null;
gEditor = null;
gSources = null;
});

Просмотреть файл

@ -0,0 +1,3 @@
function foo() { var a=1; var b=2; bar(a, b); }
function bar(c, d) { debugger; }
foo();

Просмотреть файл

@ -0,0 +1 @@
Content-Type: application/javascript

Просмотреть файл

@ -0,0 +1,8 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<title>Debugger Pretty Printing Test Page</title>
</head>
<script src="code_ugly-8"></script>

Просмотреть файл

@ -236,6 +236,11 @@ function waitForSourceShown(aPanel, aUrl) {
});
}
function waitForEditorLocationSet(aPanel) {
return waitForDebuggerEvents(aPanel,
aPanel.panelWin.EVENTS.EDITOR_LOCATION_SET);
}
function ensureSourceIs(aPanel, aUrl, aWaitFlag = false) {
if (aPanel.panelWin.DebuggerView.Sources.selectedValue.contains(aUrl)) {
ok(true, "Expected source is shown: " + aUrl);

Просмотреть файл

@ -205,7 +205,8 @@ StyleEditorUI.prototype = {
// remember saved file locations
for (let editor of this.editors) {
if (editor.savedFile) {
this.savedLocations[editor.styleSheet.href] = editor.savedFile;
let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
this.savedLocations[identifier] = editor.savedFile;
}
}
@ -257,7 +258,8 @@ StyleEditorUI.prototype = {
*/
_addStyleSheetEditor: function(styleSheet, file, isNew) {
// recall location of saved file for this sheet after page reload
let savedFile = this.savedLocations[styleSheet.href];
let identifier = this.getStyleSheetIdentifier(styleSheet);
let savedFile = this.savedLocations[identifier];
if (savedFile && !file) {
file = savedFile;
}
@ -526,6 +528,18 @@ StyleEditorUI.prototype = {
return deferred.promise;
},
/**
* Returns an identifier for the given style sheet.
*
* @param {StyleSheet} aStyleSheet
* The style sheet to be identified.
*/
getStyleSheetIdentifier: function (aStyleSheet) {
// Identify inline style sheets by their host page URI and index at the page.
return aStyleSheet.href ? aStyleSheet.href :
"inline-" + aStyleSheet.styleSheetIndex + "-at-" + aStyleSheet.nodeHref;
},
/**
* selects a stylesheet and optionally moves the cursor to a selected line
*

Просмотреть файл

@ -7,6 +7,8 @@ support-files =
import.css
import.html
import2.css
inline-1.html
inline-2.html
longload.html
media-small.css
media.html
@ -37,6 +39,7 @@ support-files =
[browser_styleeditor_import.js]
[browser_styleeditor_import_rule.js]
[browser_styleeditor_init.js]
[browser_styleeditor_inline_friendly_names.js]
[browser_styleeditor_loading.js]
[browser_styleeditor_new.js]
[browser_styleeditor_nostyle.js]

Просмотреть файл

@ -0,0 +1,150 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let gUI;
const FIRST_TEST_PAGE = TEST_BASE + "inline-1.html"
const SECOND_TEST_PAGE = TEST_BASE + "inline-2.html"
const SAVE_PATH = "test.css";
function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
// First test that identifiers are correcly generated. If not other tests
// are likely to fail.
testIndentifierGeneration();
waitForEditors(2)
.then(saveFirstInlineStyleSheet)
.then(testFriendlyNamesAfterSave)
.then(reloadPage)
.then(testFriendlyNamesAfterSave)
.then(navigateToAnotherPage)
.then(testFriendlyNamesAfterNavigation)
.then(finishTests);
});
content.location = FIRST_TEST_PAGE;
}
function testIndentifierGeneration() {
let fakeStyleSheetFile = {
"href": "http://example.com/test.css",
"nodeHref": "http://example.com/",
"styleSheetIndex": 1
}
let fakeInlineStyleSheet = {
"href": null,
"nodeHref": "http://example.com/",
"styleSheetIndex": 2
}
is(gUI.getStyleSheetIdentifier(fakeStyleSheetFile), "http://example.com/test.css",
"URI is the identifier of style sheet file.");
is(gUI.getStyleSheetIdentifier(fakeInlineStyleSheet), "inline-2-at-http://example.com/",
"Inline style sheets are identified by their page and position at that page.");
}
function saveFirstInlineStyleSheet() {
let deferred = promise.defer();
let editor = gUI.editors[0];
let destFile = FileUtils.getFile("ProfD", [SAVE_PATH]);
editor.saveToFile(destFile, function (file) {
ok(file, "File was correctly saved.");
deferred.resolve();
});
return deferred.promise;
}
function testFriendlyNamesAfterSave() {
let firstEditor = gUI.editors[0];
let secondEditor = gUI.editors[1];
// The friendly name of first sheet should've been remembered, the second should
// not be the same (bug 969900).
is(firstEditor.friendlyName, SAVE_PATH,
"Friendly name is correct for the saved inline style sheet.");
isnot(secondEditor.friendlyName, SAVE_PATH,
"Friendly name is for the second inline style sheet is not the same as first.");
return promise.resolve(null);
}
function reloadPage() {
info("Reloading page.");
content.location.reload();
return waitForEditors(2);
}
function navigateToAnotherPage() {
info("Navigating to another page.");
let deferred = promise.defer();
gBrowser.removeCurrentTab();
gUI = null;
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
waitForEditors(2).then(deferred.resolve);
});
content.location = SECOND_TEST_PAGE;
return deferred.promise;
}
function testFriendlyNamesAfterNavigation() {
let firstEditor = gUI.editors[0];
let secondEditor = gUI.editors[1];
// Inline style sheets shouldn't have the name of previously saved file as the
// page is different.
isnot(firstEditor.friendlyName, SAVE_PATH,
"The first editor doesn't have the save path as a friendly name.");
isnot(secondEditor.friendlyName, SAVE_PATH,
"The second editor doesn't have the save path as a friendly name.");
return promise.resolve(null);
}
function finishTests() {
gUI = null;
finish();
}
/**
* Waits for all editors to be added.
*
* @param {int} aNumberOfEditors
* The number of editors to wait until proceeding.
*
* Returns a promise that's resolved once all editors are added.
*/
function waitForEditors(aNumberOfEditors) {
let deferred = promise.defer();
let count = 0;
info("Waiting for " + aNumberOfEditors + " editors to be added");
gUI.on("editor-added", function editorAdded(event, editor) {
if (++count == aNumberOfEditors) {
info("All editors added. Resolving promise.");
gUI.off("editor-added", editorAdded);
gUI.editors[0].getSourceEditor().then(deferred.resolve);
}
else {
info ("Editor " + count + " of " + aNumberOfEditors + " added.");
}
});
return deferred.promise;
}

Просмотреть файл

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>Inline test page #1</title>
<style type="text/css">
.second {
font-size:2em;
}
</style>
<style type="text/css">
.first {
font-size:3em;
}
</style>
</head>
<body class="first">
Inline test page #1
</body>
</html>

Просмотреть файл

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>Inline test page #2</title>
<style type="text/css">
.second {
font-size:2em;
}
</style>
<style type="text/css">
.first {
font-size:3em;
}
</style>
</head>
<body class="second">
Inline test page #2
</body>
</html>

Просмотреть файл

@ -1,59 +1,71 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* ***** BEGIN LICENSE BLOCK *****
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*
* Contributor(s):
* Mihai Șucan <mihai.sucan@gmail.com>
*
* ***** END LICENSE BLOCK ***** */
/* 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/. */
function consoleOpened(HUD) {
HUD.jsterm.clearOutput();
// Test that the console output scrolls to JS eval results when there are many
// messages displayed. See bug 601352.
let longMessage = "";
for (let i = 0; i < 50; i++) {
longMessage += "LongNonwrappingMessage";
}
function test() {
Task.spawn(runner).then(finishTest);
for (let i = 0; i < 50; i++) {
content.console.log("test message " + i);
}
function* runner() {
let {tab} = yield loadTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
let hud = yield openConsole(tab);
hud.jsterm.clearOutput();
content.console.log(longMessage);
let longMessage = "";
for (let i = 0; i < 50; i++) {
longMessage += "LongNonwrappingMessage";
}
for (let i = 0; i < 50; i++) {
content.console.log("test message " + i);
}
for (let i = 0; i < 50; i++) {
content.console.log("test1 message " + i);
}
HUD.jsterm.execute("1+1", performTest);
content.console.log(longMessage);
function performTest(node) {
let scrollNode = HUD.outputNode.parentNode;
for (let i = 0; i < 50; i++) {
content.console.log("test2 message " + i);
}
yield waitForMessages({
webconsole: hud,
messages: [{
text: "test1 message 0",
}, {
text: "test1 message 49",
}, {
text: "LongNonwrappingMessage",
}, {
text: "test2 message 0",
}, {
text: "test2 message 49",
}],
});
let nodeDeferred = promise.defer();
hud.jsterm.execute("1+1", (node) => { nodeDeferred.resolve(node); });
let node = yield nodeDeferred.promise;
let scrollNode = hud.outputNode.parentNode;
let rectNode = node.getBoundingClientRect();
let rectOutput = scrollNode.getBoundingClientRect();
console.debug("rectNode", rectNode, "rectOutput", rectOutput);
console.log("scrollNode scrollHeight", scrollNode.scrollHeight, "scrollTop", scrollNode.scrollTop, "clientHeight", scrollNode.clientHeight);
isnot(scrollNode.scrollTop, 0, "scroll location is not at the top");
// Visible scroll viewport.
let height = scrollNode.scrollHeight - scrollNode.scrollTop;
// The bounding client rect .top/left coordinates are relative to the
// console iframe.
// Top position of the last message node, relative to the outputNode.
let top = rectNode.top + scrollNode.scrollTop;
let bottom = top + node.clientHeight;
info("output height " + height + " node top " + top + " node bottom " + bottom + " node height " + node.clientHeight);
// Visible scroll viewport.
let height = rectOutput.height;
// Top and bottom coordinates of the last message node, relative to the outputNode.
let top = rectNode.top - rectOutput.top;
let bottom = top + rectNode.height;
info("node top " + top + " node bottom " + bottom + " node clientHeight " + node.clientHeight);
ok(top >= 0 && bottom <= height, "last message is visible");
finishTest();
};
}
}
function test() {
addTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
browser.addEventListener("load", function tabLoad(aEvent) {
browser.removeEventListener(aEvent.type, tabLoad, true);
openConsole(null, consoleOpened);
}, true);
}

Просмотреть файл

@ -196,7 +196,7 @@
if (event.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
if (typeof SelectionHelperUI != 'undefined') {
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
event.clientX, event.clientY);
event.clientX, event.clientY, this);
} else {
// If we don't have access to SelectionHelperUI then we are using this
// binding for browser content (e.g. about:config)
@ -295,7 +295,7 @@
if (event.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
if (typeof SelectionHelperUI != 'undefined') {
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
event.clientX, event.clientY);
event.clientX, event.clientY, this);
} else {
// If we don't have access to SelectionHelperUI then we are using this
// binding for browser content (e.g. about:config)

Просмотреть файл

@ -292,6 +292,12 @@
if (aShouldDismiss)
ContextUI.dismissTabs();
if (!InputSourceHelper.isPrecise && this.textLength) {
let inputRectangle = this.inputField.getBoundingClientRect();
SelectionHelperUI.attachEditSession(ChromeSelectionHandler,
inputRectangle.left, inputRectangle.top, this);
}
]]>
</body>
</method>
@ -448,6 +454,15 @@
<handler event="click" phase="capturing">
<![CDATA[
// workaround for bug 925457: taping browser chrome resets last tap
// co-ordinates to 'undefined' so that we know not to shift the
// browser when the keyboard is up in SelectionHandler's
// _calcNewContentPosition().
Browser.selectedTab.browser.messageManager.sendAsyncMessage(
"Browser:ResetLastPos", {
xPos: null,
yPos: null
});
this.beginEditing(true);
]]>
</handler>

Просмотреть файл

@ -482,8 +482,7 @@ Desktop browser's sync prefs.
autocompletepopup="urlbar-autocomplete"
completeselectedindex="true"
placeholder="&urlbar.emptytext;"
tabscrolling="true"
onclick="SelectionHelperUI.urlbarTextboxClick(this);"/>
tabscrolling="true" />
<toolbarbutton id="go-button"
class="urlbar-button"

Просмотреть файл

@ -33,7 +33,7 @@ var ChromeSelectionHandler = {
_onSelectionAttach: function _onSelectionAttach(aJson) {
this._domWinUtils = Util.getWindowUtils(window);
this._contentWindow = window;
this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
this._targetElement = aJson.target;
this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
if (!this._targetIsEditable) {
this._onFail("not an editable?", this._targetElement);

Просмотреть файл

@ -435,10 +435,12 @@ var SelectionHelperUI = {
*
* Attaches to existing selection and begins editing.
*
* @param aMsgTarget - Browser or chrome message target
* @param aX, aY - Browser relative client coordinates.
* @param aMsgTarget - Browser or chrome message target.
* @param aX Tap browser relative client X coordinate.
* @param aY Tap browser relative client Y coordinate.
* @param aTarget Actual tap target (optional).
*/
attachEditSession: function attachEditSession(aMsgTarget, aX, aY) {
attachEditSession: function attachEditSession(aMsgTarget, aX, aY, aTarget) {
if (!aMsgTarget || this.isActive)
return;
this._init(aMsgTarget);
@ -448,6 +450,7 @@ var SelectionHelperUI = {
// back with information on the current selection. SelectionAttach
// takes client coordinates.
this._sendAsyncMessage("Browser:SelectionAttach", {
target: aTarget,
xPos: aX,
yPos: aY
});
@ -464,11 +467,12 @@ var SelectionHelperUI = {
* Once the user starts a drag, the caret marker is hidden, and
* the start and end markers take over.
*
* @param aMsgTarget - Browser or chrome message target
* @param aX, aY - Browser relative client coordinates of the tap
* that initiated the session.
* @param aMsgTarget - Browser or chrome message target.
* @param aX Tap browser relative client X coordinate.
* @param aY Tap browser relative client Y coordinate.
* @param aTarget Actual tap target (optional).
*/
attachToCaret: function attachToCaret(aMsgTarget, aX, aY) {
attachToCaret: function attachToCaret(aMsgTarget, aX, aY, aTarget) {
if (!this.isActive) {
this._init(aMsgTarget);
this._setupDebugOptions();
@ -476,9 +480,14 @@ var SelectionHelperUI = {
this._hideMonocles();
}
this._lastPoint = { xPos: aX, yPos: aY };
this._lastCaretAttachment = {
target: aTarget,
xPos: aX,
yPos: aY
};
this._sendAsyncMessage("Browser:CaretAttach", {
target: aTarget,
xPos: aX,
yPos: aY
});
@ -516,38 +525,14 @@ var SelectionHelperUI = {
});
},
/*
* Event handler on the navbar text input. Called from navbar bindings
* when focus is applied to the edit.
*/
urlbarTextboxClick: function(aEdit) {
// workaround for bug 925457: taping browser chrome resets last tap
// co-ordinates to 'undefined' so that we know not to shift the browser
// when the keyboard is up in SelectionHandler's _calcNewContentPosition().
Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
xPos: null,
yPos: null
});
if (InputSourceHelper.isPrecise || !aEdit.textLength) {
return;
}
// Enable selection when there's text in the control
let innerRect = aEdit.inputField.getBoundingClientRect();
this.attachEditSession(ChromeSelectionHandler,
innerRect.left,
innerRect.top);
},
/*
* Click handler for chrome pages loaded into the browser (about:config).
* Called from the text input bindings via the attach_edit_session_to_content
* observer.
*/
chromeTextboxClick: function (aEvent) {
this.attachEditSession(Browser.selectedTab.browser,
aEvent.clientX, aEvent.clientY);
this.attachEditSession(Browser.selectedTab.browser, aEvent.clientX,
aEvent.clientY, aEvent.target);
},
/*
@ -875,12 +860,13 @@ var SelectionHelperUI = {
/*
* Handles taps that move the current caret around in text edits,
* clear active selection and focus when neccessary, or change
* modes. Only active afer SelectionHandlerUI is initialized.
* clear active selection and focus when necessary, or change
* modes. Only active after SelectionHandlerUI is initialized.
*/
_onClick: function(aEvent) {
if (this.layerMode == kChromeLayer && this._targetIsEditable) {
this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY);
this.attachToCaret(this._msgTarget, aEvent.clientX, aEvent.clientY,
aEvent.target);
}
},
@ -909,7 +895,8 @@ var SelectionHelperUI = {
*/
_onDeckOffsetChanged: function _onDeckOffsetChanged(aEvent) {
// Update the monocle position and display
this.attachToCaret(null, this._lastPoint.xPos, this._lastPoint.yPos);
this.attachToCaret(null, this._lastCaretAttachment.xPos,
this._lastCaretAttachment.yPos, this._lastCaretAttachment.target);
},
/*

Просмотреть файл

@ -107,7 +107,8 @@ gTests.push({
let autocompletePopup = document.getElementById("urlbar-autocomplete-scroll");
yield waitForEvent(autocompletePopup, "transitionend");
SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x, editCoords.y);
SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x,
editCoords.y, edit);
ok(SelectionHelperUI.isSelectionUIVisible, "selection enabled");
let selection = edit.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
@ -136,7 +137,8 @@ gTests.push({
edit.value = "wikipedia.org";
edit.select();
let editCoords = logicalCoordsForElement(edit);
SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x, editCoords.y);
SelectionHelperUI.attachEditSession(ChromeSelectionHandler, editCoords.x,
editCoords.y, edit);
edit.blur();
ok(!SelectionHelperUI.isSelectionUIVisible, "selection no longer enabled");
clearSelection(edit);
@ -228,6 +230,26 @@ gTests.push({
}
});
gTests.push({
desc: "Bug 957646 - Selection monocles sometimes don't display when tapping" +
" text ion the nav bar.",
run: function() {
yield showNavBar();
let edit = document.getElementById("urlbar-edit");
edit.value = "about:mozilla";
let editRectangle = edit.getBoundingClientRect();
// Tap outside the input but close enough for fluffing to take effect.
sendTap(window, editRectangle.left + 50, editRectangle.top - 2);
yield waitForCondition(function () {
return SelectionHelperUI.isSelectionUIVisible;
});
}
});
function test() {
if (!isLandscapeMode()) {
todo(false, "browser_selection_tests need landscape mode to run.");

Просмотреть файл

@ -26,3 +26,41 @@ gTests.push({
ok(simpleMeasurements.UITelemetry["metro-ui"]["window-height"], "window-height measurement was captured");
}
});
gTests.push({
desc: "Test tab count telemetry",
run: function() {
// Wait for Session Manager to be initialized.
yield waitForCondition(() => window.__SSID);
Services.obs.notifyObservers(null, "reset-telemetry-vars", null);
yield waitForCondition(function () {
let simpleMeasurements = getTelemetryPayload().simpleMeasurements;
return simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"] == 1;
});
let simpleMeasurements = getTelemetryPayload().simpleMeasurements;
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 1);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 1);
let tab2 = Browser.addTab("about:mozilla");
simpleMeasurements = getTelemetryPayload().simpleMeasurements;
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 2);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 2);
let tab3 = Browser.addTab("about:config");
simpleMeasurements = getTelemetryPayload().simpleMeasurements;
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 3);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
Browser.closeTab(tab2, { forceClose: true } );
simpleMeasurements = getTelemetryPayload().simpleMeasurements;
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 2);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
Browser.closeTab(tab3, { forceClose: true } );
simpleMeasurements = getTelemetryPayload().simpleMeasurements;
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 1);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
}
});

Просмотреть файл

@ -24,6 +24,9 @@ XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
return NetUtil;
});
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
"resource://gre/modules/UITelemetry.jsm");
// -----------------------------------------------------------------------
// Session Store
// -----------------------------------------------------------------------
@ -52,6 +55,9 @@ SessionStore.prototype = {
_maxTabsUndo: 1,
_shouldRestore: false,
// Tab telemetry variables
_maxTabsOpen: 1,
init: function ss_init() {
// Get file references
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
@ -63,6 +69,13 @@ SessionStore.prototype = {
this._loadState = STATE_STOPPED;
try {
UITelemetry.addSimpleMeasureFunction("metro-tabs",
this._getTabStats.bind(this));
} catch (ex) {
// swallow exception that occurs if metro-tabs measure is already set up
}
try {
let shutdownWasUnclean = false;
@ -184,6 +197,13 @@ SessionStore.prototype = {
})
},
_getTabStats: function() {
return {
currTabCount: this._currTabCount,
maxTabCount: this._maxTabsOpen
};
},
observe: function ss_observe(aSubject, aTopic, aData) {
let self = this;
let observerService = Services.obs;
@ -197,6 +217,7 @@ SessionStore.prototype = {
observerService.addObserver(this, "quit-application-requested", true);
observerService.addObserver(this, "quit-application-granted", true);
observerService.addObserver(this, "quit-application", true);
observerService.addObserver(this, "reset-telemetry-vars", true);
break;
case "final-ui-startup":
observerService.removeObserver(this, "final-ui-startup");
@ -264,6 +285,7 @@ SessionStore.prototype = {
observerService.removeObserver(this, "quit-application-requested");
observerService.removeObserver(this, "quit-application-granted");
observerService.removeObserver(this, "quit-application");
observerService.removeObserver(this, "reset-telemetry-vars");
// If a save has been queued, kill the timer and save state now
if (this._saveTimer) {
@ -295,13 +317,24 @@ SessionStore.prototype = {
this._saveTimer = null;
this.saveState();
break;
case "reset-telemetry-vars":
// Used in mochitests only.
this._maxTabsOpen = 1;
}
},
updateTabTelemetryVars: function(window) {
this._currTabCount = window.Browser.tabs.length;
if (this._currTabCount > this._maxTabsOpen) {
this._maxTabsOpen = this._currTabCount;
}
},
handleEvent: function ss_handleEvent(aEvent) {
let window = aEvent.currentTarget.ownerDocument.defaultView;
switch (aEvent.type) {
case "TabOpen":
this.updateTabTelemetryVars(window);
case "TabClose": {
let browser = aEvent.originalTarget.linkedBrowser;
if (aEvent.type == "TabOpen") {
@ -313,6 +346,9 @@ SessionStore.prototype = {
}
break;
}
case "TabRemove":
this.updateTabTelemetryVars(window);
break;
case "TabSelect": {
let browser = aEvent.originalTarget.linkedBrowser;
this.onTabSelect(window, browser);
@ -361,6 +397,7 @@ SessionStore.prototype = {
let tabContainer = aWindow.document.getElementById("tabs");
tabContainer.addEventListener("TabOpen", this, true);
tabContainer.addEventListener("TabClose", this, true);
tabContainer.addEventListener("TabRemove", this, true);
tabContainer.addEventListener("TabSelect", this, true);
},
@ -372,6 +409,7 @@ SessionStore.prototype = {
let tabContainer = aWindow.document.getElementById("tabs");
tabContainer.removeEventListener("TabOpen", this, true);
tabContainer.removeEventListener("TabClose", this, true);
tabContainer.removeEventListener("TabRemove", this, true);
tabContainer.removeEventListener("TabSelect", this, true);
if (this._loadState == STATE_RUNNING) {

Просмотреть файл

@ -640,10 +640,12 @@ tabmodalprompt:not([promptType="promptUserAndPass"]) .infoContainer {
.meta {
background-color: @panel_light_color@;
/* bug 969354
background-image: url("chrome://browser/skin/images/firefox-watermark.png");
background-repeat: no-repeat;
background-position: center center;
background-attachment: fixed;
*/
}
/* needs to observe the viewstate */

Просмотреть файл

@ -560,6 +560,11 @@ menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
-moz-appearance: none;
}
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1[open="true"],
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:hover:active {
padding: 3px;
}
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-icon {

Просмотреть файл

@ -41,7 +41,8 @@ body {
}
.property-name {
width: 50%;
/* -12px is so the expander triangle isn't pushed up above the property */
width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -49,8 +50,7 @@ body {
}
.property-value {
width: 50%;
max-width: 100%;
width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -75,7 +75,8 @@ body {
width: 200px;
}
.property-value {
width: auto;
/* -212px is accounting for the 200px property-name and the 12px triangle */
width: calc(100% - 212px);
}
}

Просмотреть файл

@ -22,6 +22,8 @@
#downloads-button[cui-areatype="toolbar"] > #downloads-indicator-anchor > #downloads-indicator-icon {
background: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"),
0, 198, 18, 180) center no-repeat;
min-width: 18px;
min-height: 18px;
}
#downloads-button[cui-areatype="toolbar"][attention] > #downloads-indicator-anchor > #downloads-indicator-icon {

Просмотреть файл

@ -2573,24 +2573,13 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png);
}
.tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]),
.tabs-newtab-button:hover {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-middle@2x.png),
url(chrome://browser/skin/tabbrowser/tab-background-end@2x.png);
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-middle:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-middle@2x.png);
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start@2x.png);
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-end@2x.png);
}
.tab-background-middle[selected=true] {
background-image: url(chrome://browser/skin/tabbrowser/tab-active-middle@2x.png),
@fgTabTexture@,
@ -4125,6 +4114,10 @@ window > chatbox {
margin-bottom: 0px;
}
#main-window:not([tabsintitlebar]):-moz-lwtheme > #titlebar {
margin-bottom: 5px;
}
#main-window[tabsintitlebar]:-moz-lwtheme > #titlebar > #titlebar-content {
margin-top: 11px;
margin-bottom: 11px;

Просмотреть файл

@ -59,7 +59,8 @@ body {
}
.property-name {
width: 50%;
/* -12px is so the expander triangle isn't pushed up above the property */
width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -67,8 +68,7 @@ body {
}
.property-value {
width: 50%;
max-width: 100%;
width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -93,7 +93,8 @@ body {
width: 200px;
}
.property-value {
width: auto;
/* -212px is accounting for the 200px property-name and the 12px triangle */
width: calc(100% - 212px);
}
}

Просмотреть файл

@ -494,15 +494,15 @@ box.requests-menu-status {
/* Footer */
#requests-menu-footer {
border-top: solid 1px hsla(210,5%,5%,.3);
}
.theme-dark #requests-menu-footer {
border-top: 1px solid @table_itemDarkStartBorder@;
box-shadow: 0 1px 0 @table_itemDarkEndBorder@ inset;
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
}
.theme-light #requests-menu-footer {
border-top: 1px solid @table_itemLightStartBorder@;
box-shadow: 0 1px 0 @table_itemLightEndBorder@ inset;
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
}
@ -530,14 +530,14 @@ box.requests-menu-status {
.theme-dark .requests-menu-footer-spacer:not(:first-child),
.theme-dark .requests-menu-footer-button:not(:first-child) {
-moz-border-start: 1px solid @table_itemDarkStartBorder@;
box-shadow: -1px 0 0 @table_itemDarkEndBorder@;
-moz-border-start: 1px solid @table_itemDarkEndBorder@;
box-shadow: -1px 0 0 @table_itemDarkStartBorder@;
}
.theme-light .requests-menu-footer-spacer:not(:first-child),
.theme-light .requests-menu-footer-button:not(:first-child) {
-moz-border-start: 1px solid @table_itemLightStartBorder@;
box-shadow: -1px 0 0 @table_itemLightEndBorder@;
-moz-border-start: 1px solid @table_itemLightEndBorder@;
box-shadow: -1px 0 0 @table_itemLightStartBorder@;
}
.requests-menu-footer-button {
@ -546,18 +546,25 @@ box.requests-menu-status {
}
.requests-menu-footer-button:hover {
background: rgba(0,0,0,0.20);
background: rgba(0,0,0,0.10);
}
.requests-menu-footer-button:hover:active {
background: rgba(0,0,0,0.35);
.theme-dark .requests-menu-footer-button:hover:active {
background-color: rgba(29,79,115,0.4); /* Select Highlight Blue at 40% opacity */
}
.requests-menu-footer-button:not(:active)[checked] {
background-color: rgba(0,0,0,0.25);
background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3));
background-size: 100% 1px;
background-repeat: no-repeat;
.theme-light .requests-menu-footer-button:hover:active {
background-color: rgba(76,158,217,0.4); /* Select Highlight Blue at 40% opacity */
}
.theme-dark .requests-menu-footer-button:not(:active)[checked] {
background-color: rgba(29,79,115,1); /* Select Highlight Blue */
color: rgba(245,247,250,1); /* Light foreground text */
}
.theme-light .requests-menu-footer-button:not(:active)[checked] {
background-color: rgba(76,158,217,1); /* Select Highlight Blue */
color: rgba(245,247,250,1); /* Light foreground text */
}
.requests-menu-footer-label {

Просмотреть файл

@ -393,6 +393,10 @@
background-image: linear-gradient(transparent, transparent), @smallSeparatorDark@;
}
.theme-dark .devtools-sidebar-tabs > tabs > tab:hover {
background-image: linear-gradient(hsla(206,37%,4%,.2), hsla(206,37%,4%,.2)), @smallSeparatorDark@;
}
.theme-dark .devtools-sidebar-tabs > tabs > tab:hover:active {
background-image: linear-gradient(hsla(206,37%,4%,.4), hsla(206,37%,4%,.4)), @smallSeparatorDark@;
}

Просмотреть файл

@ -224,6 +224,7 @@
/* End selected tab */
/* new tab button border and gradient on hover */
.tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]),
.tabs-newtab-button:hover {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png),
url(chrome://browser/skin/tabbrowser/tab-background-middle.png),
@ -233,27 +234,6 @@
background-size: @tabCurveWidth@ 100%, calc(100% - (2 * @tabCurveWidth@)) 100%, @tabCurveWidth@ 100%;
}
/* normal tab border and gradient on hover */
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-middle:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-middle.png);
background-repeat: repeat-x;
background-size: auto 100%;
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]),
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
background-repeat: no-repeat;
background-size: 100% 100%;
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-start:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png);
}
.tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-background-end:not([selected=true]) {
background-image: url(chrome://browser/skin/tabbrowser/tab-background-end.png);
}
/* Tab pointer-events */
.tabbrowser-tab {
pointer-events: none;

Просмотреть файл

@ -59,7 +59,8 @@ body {
}
.property-name {
width: 50%;
/* -12px is so the expander triangle isn't pushed up above the property */
width: calc(100% - 12px);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -67,8 +68,7 @@ body {
}
.property-value {
width: 50%;
max-width: 100%;
width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -93,7 +93,8 @@ body {
width: 200px;
}
.property-value {
width: auto;
/* -212px is accounting for the 200px property-name and the 12px triangle */
width: calc(100% - 212px);
}
}

Просмотреть файл

@ -161,14 +161,15 @@ namespace {
class StorageNotifierRunnable : public nsRunnable
{
public:
StorageNotifierRunnable(nsISupports* aSubject)
: mSubject(aSubject)
StorageNotifierRunnable(nsISupports* aSubject, const char16_t* aType)
: mSubject(aSubject), mType(aType)
{ }
NS_DECL_NSIRUNNABLE
private:
nsCOMPtr<nsISupports> mSubject;
const char16_t* mType;
};
NS_IMETHODIMP
@ -177,7 +178,7 @@ StorageNotifierRunnable::Run()
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
observerService->NotifyObservers(mSubject, "dom-storage2-changed", nullptr);
observerService->NotifyObservers(mSubject, "dom-storage2-changed", mType);
}
return NS_OK;
}
@ -207,7 +208,11 @@ DOMStorage::BroadcastChangeNotification(const nsSubstring& aKey,
return;
}
nsRefPtr<StorageNotifierRunnable> r = new StorageNotifierRunnable(event);
nsRefPtr<StorageNotifierRunnable> r =
new StorageNotifierRunnable(event,
GetType() == LocalStorage
? MOZ_UTF16("localStorage")
: MOZ_UTF16("sessionStorage"));
NS_DispatchToMainThread(r);
}

Просмотреть файл

@ -13,9 +13,13 @@
#include "unistd.h"
#include "dirent.h"
#include "sys/stat.h"
#if !defined(ANDROID)
#if defined(ANDROID)
#include <sys/vfs.h>
#define statvfs statfs
#else
#include "sys/statvfs.h"
#include <spawn.h>
#endif // !defined(ANDROID)
#endif // defined(ANDROID)
#endif // defined(XP_UNIX)
#if defined(XP_LINUX)
@ -526,6 +530,9 @@ static const dom::ConstantSpec gLibcProperties[] =
// The size of |time_t|.
{ "OSFILE_SIZEOF_TIME_T", INT_TO_JSVAL(sizeof (time_t)) },
// The size of |fsblkcnt_t|.
{ "OSFILE_SIZEOF_FSBLKCNT_T", INT_TO_JSVAL(sizeof (fsblkcnt_t)) },
#if !defined(ANDROID)
// The size of |posix_spawn_file_actions_t|.
{ "OSFILE_SIZEOF_POSIX_SPAWN_FILE_ACTIONS_T", INT_TO_JSVAL(sizeof (posix_spawn_file_actions_t)) },
@ -585,6 +592,13 @@ static const dom::ConstantSpec gLibcProperties[] =
{ "OSFILE_OFFSETOF_STAT_ST_BIRTHTIME", INT_TO_JSVAL(offsetof (struct stat, st_birthtime)) },
#endif // defined(_DARWIN_FEATURE_64_BIT_INODE)
// Defining |statvfs|
{ "OSFILE_SIZEOF_STATVFS", INT_TO_JSVAL(sizeof (struct statvfs)) },
{ "OSFILE_OFFSETOF_STATVFS_F_BSIZE", INT_TO_JSVAL(offsetof (struct statvfs, f_bsize)) },
{ "OSFILE_OFFSETOF_STATVFS_F_BAVAIL", INT_TO_JSVAL(offsetof (struct statvfs, f_bavail)) },
#endif // defined(XP_UNIX)

Просмотреть файл

@ -12,5 +12,6 @@ support-files =
[test_storageLocalStorageEventCheckNoPropagation.html]
[test_storageLocalStorageEventCheckPropagation.html]
[test_storageNotifications.html]
[test_storageSessionStorageEventCheckNoPropagation.html]
[test_storageSessionStorageEventCheckPropagation.html]

Просмотреть файл

@ -0,0 +1,127 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>sessionStorage basic test</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script type="application/javascript;version=1.7">
var expectedTypes = [
"localStorage",
"localStorage",
"sessionStorage",
"localStorage",
"sessionStorage",
"sessionStorage",
"localStorage",
"sessionStorage",
"localStorage",
"sessionStorage",
"localStorage",
"sessionStorage",
"sessionStorage",
"localStorage",
"sessionStorage",
"localStorage",
];
var tests = Tests();
function setup() {
sessionStorage.clear();
SimpleTest.executeSoon(function() {
tests.next();
});
}
function Tests()
{
// Initially check the both storages are empty
is(sessionStorage.length, 0, "Session storage is empty [1]");
is(localStorage.length, 0, "Local storage is empty [1]");
var onStorageChanged = {
observe: function(subject, topic, type) {
if (topic == "dom-storage2-changed") {
ok(expectedTypes.length > 0, "Not more then expected events encountered");
is(type, expectedTypes.shift(), "Expected type of the storage notificaiton");
tests.next();
}
}
}
// Listen for dom-storage2-changed notification
SpecialPowers.Services.obs.addObserver(onStorageChanged,
"dom-storage2-changed", false);
// add an empty-value key
localStorage.setItem("empty", "");
yield undefined;
localStorage.setItem("empty", "value-1");
yield undefined;
sessionStorage.setItem("empty", "");
yield undefined;
localStorage.removeItem("empty");
yield undefined;
sessionStorage.setItem("empty", "value-1");
yield undefined;
sessionStorage.removeItem("empty");
yield undefined;
localStorage.setItem("key1", "value-1");
yield undefined;
sessionStorage.setItem("key2", "value-2");
yield undefined;
localStorage.setItem("key1", "value-1-2");
yield undefined;
sessionStorage.setItem("key2", "value-2-2");
yield undefined;
localStorage.setItem("key3", "value-3");
yield undefined;
sessionStorage.setItem("key4", "value-4");
yield undefined;
sessionStorage.removeItem("key4");
yield undefined;
localStorage.setItem("key4", "value-4");
yield undefined;
sessionStorage.clear();
yield undefined;
localStorage.clear();
yield undefined;
SimpleTest.executeSoon(function () {
SpecialPowers.Services.obs.removeObserver(onStorageChanged,
"dom-storage2-changed", false);
is(expectedTypes.length, 0, "received the correct number of events");
sessionStorage.clear();
localStorage.clear();
tests = null;
SimpleTest.finish();
});
}
SimpleTest.waitForExplicitFinish();
</script>
</head>
<body onload="setup();">
</body>
</html>

Просмотреть файл

@ -817,7 +817,7 @@ pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
// These prefs require a restart to take effect.
pref("browser.snippets.enabled", true);
pref("browser.snippets.enabled", false);
pref("browser.snippets.syncPromo.enabled", false);
#ifdef MOZ_ANDROID_SYNTHAPKS

Просмотреть файл

@ -101,6 +101,8 @@ public class BrowserContract {
public static final class Favicons implements CommonColumns, DateSyncColumns {
private Favicons() {}
public static final String TABLE_NAME = "favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "favicons");
public static final String URL = "url";
@ -112,6 +114,8 @@ public class BrowserContract {
public static final class Thumbnails implements CommonColumns {
private Thumbnails() {}
public static final String TABLE_NAME = "thumbnails";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "thumbnails");
public static final String URL = "url";
@ -122,6 +126,10 @@ public class BrowserContract {
public static final class Bookmarks implements CommonColumns, URLColumns, FaviconColumns, SyncColumns {
private Bookmarks() {}
public static final String TABLE_NAME = "bookmarks";
public static final String VIEW_WITH_FAVICONS = "bookmarks_with_favicons";
public static final int FIXED_ROOT_ID = 0;
public static final int FAKE_DESKTOP_FOLDER_ID = -1;
public static final int FIXED_READING_LIST_ID = -2;
@ -162,6 +170,11 @@ public class BrowserContract {
@RobocopTarget
public static final class History implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns, SyncColumns {
private History() {}
public static final String TABLE_NAME = "history";
public static final String VIEW_WITH_FAVICONS = "history_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
public static final Uri CONTENT_OLD_URI = Uri.withAppendedPath(AUTHORITY_URI, "history/old");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
@ -172,6 +185,11 @@ public class BrowserContract {
@RobocopTarget
public static final class Combined implements CommonColumns, URLColumns, HistoryColumns, FaviconColumns {
private Combined() {}
public static final String VIEW_NAME = "combined";
public static final String VIEW_WITH_FAVICONS = "combined_with_favicons";
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
public static final int DISPLAY_NORMAL = 0;
@ -305,4 +323,47 @@ public class BrowserContract {
public static final String IMAGE_URL = "image_url";
public static final String CREATED = "created";
}
/*
* Contains names and schema definitions for tables and views
* no longer being used by current ContentProviders. These values are used
* to make incremental updates to the schema during a database upgrade. Will be
* removed with bug 947018.
*/
static final class Obsolete {
public static final String TABLE_IMAGES = "images";
public static final String VIEW_BOOKMARKS_WITH_IMAGES = "bookmarks_with_images";
public static final String VIEW_HISTORY_WITH_IMAGES = "history_with_images";
public static final String VIEW_COMBINED_WITH_IMAGES = "combined_with_images";
public static final class Images implements CommonColumns, SyncColumns {
private Images() {}
public static final String URL = "url_key";
public static final String FAVICON_URL = "favicon_url";
public static final String FAVICON = "favicon";
public static final String THUMBNAIL = "thumbnail";
public static final String _ID = "_id";
public static final String GUID = "guid";
public static final String DATE_CREATED = "created";
public static final String DATE_MODIFIED = "modified";
public static final String IS_DELETED = "deleted";
}
public static final class Combined {
private Combined() {}
public static final String THUMBNAIL = "thumbnail";
}
static final String TABLE_BOOKMARKS_JOIN_IMAGES = Bookmarks.TABLE_NAME + " LEFT OUTER JOIN " +
Obsolete.TABLE_IMAGES + " ON " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " +
DBUtils.qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
static final String TABLE_HISTORY_JOIN_IMAGES = History.TABLE_NAME + " LEFT OUTER JOIN " +
Obsolete.TABLE_IMAGES + " ON " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, History.URL) + " = " +
DBUtils.qualifyColumn(Obsolete.TABLE_IMAGES, Obsolete.Images.URL);
static final String FAVICON_DB = "favicon_urls.db";
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -81,4 +81,19 @@ public class DBUtils {
Log.d(LOGTAG, "Failed to unlock database");
GeckoAppShell.listOfOpenFiles();
}
/**
* Verifies that 0-byte arrays aren't added as favicon or thumbnail data.
* @param values ContentValues of query
* @param columnName Name of data column to verify
*/
public static void stripEmptyByteArray(ContentValues values, String columnName) {
if (values.containsKey(columnName)) {
byte[] data = values.getAsByteArray(columnName);
if (data == null || data.length == 0) {
Log.w(LOGTAG, "Tried to insert an empty or non-byte-array image. Ignoring.");
values.putNull(columnName);
}
}
}
}

Просмотреть файл

@ -0,0 +1,277 @@
/* 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/. */
package org.mozilla.gecko.db;
import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
/*
* Abstract class containing methods needed to make a SQLite-based content provider with a
* database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
* updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
*/
public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
protected Context mContext;
protected PerProfileDatabases<T> mDatabases;
/*
* Returns the name of the database file. Used to get a path
* to the DB file.
*
* @return name of the database file
*/
abstract protected String getDatabaseName();
/*
* Creates and returns an instance of a DB helper. Given a
* context and a path to the DB file
*
* @param context to use to create the database helper
* @param databasePath path to the DB file
* @return instance of the database helper
*/
abstract protected T createDatabaseHelper(Context context, String databasePath);
/*
* Inserts an item into the database within a DB transaction.
*
* @param uri query URI
* @param values column values to be inserted
* @return a URI for the newly inserted item
*/
abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
/*
* Deletes items from the database within a DB transaction.
*
* @param uri query URI
* @param selection An optional filter to match rows to update.
* @param selectionArgs arguments for the selection
* @return number of rows impacted by the deletion
*/
abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
/*
* Updates the database within a DB transaction.
*
* @param uri Query URI
* @param values A set of column_name/value pairs to add to the database.
* @param selection An optional filter to match rows to update.
* @param selectionArgs Arguments for the selection
* @return number of rows impacted by the update
*/
abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/*
* Fetches a readable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used.
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a readable SQLiteDatabase
*/
protected SQLiteDatabase getReadableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
}
/*
* Fetches a writeable database based on the profile indicated in the
* passed URI. If the URI does not contain a profile param, the default profile
* is used
*
* @param uri content URI optionally indicating the profile of the user
* @return instance of a writeable SQLiteDatabase
*/
protected SQLiteDatabase getWritableDatabase(Uri uri) {
String profile = null;
if (uri != null) {
profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
}
return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
}
@Override
public boolean onCreate() {
synchronized (this) {
mContext = getContext();
mDatabases = new PerProfileDatabases<T>(
getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
@Override
public T makeDatabaseHelper(Context context, String databasePath) {
return createDatabaseHelper(context, databasePath);
}
});
}
return true;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning delete transaction: " + uri);
db.beginTransaction();
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful delete transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
deleted = deleteInTransaction(uri, selection, selectionArgs);
}
if (deleted > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
try {
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning insert transaction: " + uri);
db.beginTransaction();
try {
result = insertInTransaction(uri, values);
db.setTransactionSuccessful();
trace("Successful insert transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
result = insertInTransaction(uri, values);
}
} catch (SQLException sqle) {
Log.e(LOGTAG, "exception in DB operation", sqle);
} catch (UnsupportedOperationException uoe) {
Log.e(LOGTAG, "don't know how to perform that insert", uoe);
}
if (result != null) {
getContext().getContentResolver().notifyChange(uri, null);
}
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
trace("Calling update on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
if (Build.VERSION.SDK_INT >= 11) {
trace("Beginning update transaction: " + uri);
db.beginTransaction();
try {
updated = updateInTransaction(uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
trace("Successful update transaction: " + uri);
} finally {
db.endTransaction();
}
} else {
updated = updateInTransaction(uri, values, selection, selectionArgs);
}
if (updated > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return updated;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
if (values == null) {
return 0;
}
int numValues = values.length;
int successes = 0;
final boolean hasTransactions = Build.VERSION.SDK_INT >= 11;
final SQLiteDatabase db = getWritableDatabase(uri);
if (hasTransactions) {
db.beginTransaction();
}
try {
for (int i = 0; i < numValues; i++) {
insertInTransaction(uri, values[i]);
successes++;
}
if (hasTransactions) {
trace("Flushing DB bulkinsert...");
db.setTransactionSuccessful();
}
} finally {
if (hasTransactions) {
db.endTransaction();
}
}
if (successes > 0) {
mContext.getContentResolver().notifyChange(uri, null);
}
return successes;
}
protected boolean isTest(Uri uri) {
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
// Calculate these once, at initialization. isLoggable is too expensive to
// have in-line in each log call.
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
}

Просмотреть файл

@ -265,6 +265,10 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
Logger.error(LOG_TAG, "Failed to get token.", e);
callback.handleError(null, e);
}
@Override
public void handleBackoff(int backoffSeconds) {
}
});
}

Просмотреть файл

@ -112,6 +112,7 @@ gbjar.sources += [
'CustomEditText.java',
'DataReportingNotification.java',
'db/BrowserContract.java',
'db/BrowserDatabaseHelper.java',
'db/BrowserDB.java',
'db/BrowserProvider.java',
'db/DBUtils.java',
@ -122,6 +123,7 @@ gbjar.sources += [
'db/PerProfileDatabases.java',
'db/SQLiteBridgeContentProvider.java',
'db/TabsProvider.java',
'db/TransactionalProvider.java',
'Distribution.java',
'DoorHangerPopup.java',
'EditBookmarkDialog.java',

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 601 B

После

Ширина:  |  Высота:  |  Размер: 2.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 283 B

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 283 B

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.4 KiB

После

Ширина:  |  Высота:  |  Размер: 4.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.6 KiB

После

Ширина:  |  Высота:  |  Размер: 9.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.0 KiB

После

Ширина:  |  Высота:  |  Размер: 3.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 569 B

После

Ширина:  |  Высота:  |  Размер: 3.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 213 B

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 213 B

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 901 B

После

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.3 KiB

После

Ширина:  |  Высота:  |  Размер: 4.8 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 421 B

После

Ширина:  |  Высота:  |  Размер: 603 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 206 B

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 213 B

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Просмотреть файл

@ -49,6 +49,20 @@ public class SyncResponse {
return this.getStatusCode() == 200;
}
/**
* Fetch the content type of the HTTP response body.
*
* @return a <code>Header</code> instance, or <code>null</code> if there was
* no body or no valid Content-Type.
*/
public Header getContentType() {
HttpEntity entity = this.response.getEntity();
if (entity == null) {
return null;
}
return entity.getContentType();
}
private String body = null;
public String body() throws IllegalStateException, IOException {
if (body != null) {
@ -145,6 +159,14 @@ public class SyncResponse {
}
}
/**
* @return A number of seconds, or -1 if the 'X-Backoff' header was not
* present.
*/
public int backoffInSeconds() throws NumberFormatException {
return this.getIntegerHeader("x-backoff");
}
/**
* @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not
* present.
@ -154,14 +176,21 @@ public class SyncResponse {
}
/**
* @return A number of milliseconds, or -1 if neither the 'Retry-After' or
* 'X-Weave-Backoff' header was present.
* Extract a number of seconds, or -1 if none of the specified headers were present.
*
* @param includeRetryAfter
* if <code>true</code>, the Retry-After header is excluded. This is
* useful for processing non-error responses where a Retry-After
* header would be unexpected.
* @return the maximum of the three possible backoff headers, in seconds.
*/
public long totalBackoffInMilliseconds() {
public int totalBackoffInSeconds(boolean includeRetryAfter) {
int retryAfterInSeconds = -1;
try {
retryAfterInSeconds = retryAfterInSeconds();
} catch (NumberFormatException e) {
if (includeRetryAfter) {
try {
retryAfterInSeconds = retryAfterInSeconds();
} catch (NumberFormatException e) {
}
}
int weaveBackoffInSeconds = -1;
@ -170,7 +199,26 @@ public class SyncResponse {
} catch (NumberFormatException e) {
}
long totalBackoff = (long) Math.max(retryAfterInSeconds, weaveBackoffInSeconds);
int backoffInSeconds = -1;
try {
backoffInSeconds = backoffInSeconds();
} catch (NumberFormatException e) {
}
int totalBackoff = Math.max(retryAfterInSeconds, Math.max(backoffInSeconds, weaveBackoffInSeconds));
if (totalBackoff < 0) {
return -1;
} else {
return totalBackoff;
}
}
/**
* @return A number of milliseconds, or -1 if neither the 'Retry-After',
* 'X-Backoff', or 'X-Weave-Backoff' header were present.
*/
public long totalBackoffInMilliseconds() {
long totalBackoff = totalBackoffInSeconds(true);
if (totalBackoff < 0) {
return -1;
} else {

Просмотреть файл

@ -25,6 +25,7 @@ import org.mozilla.gecko.sync.ThreadPool;
* * Headers:
* * Retry-After
* * X-Weave-Backoff
* * X-Backoff
* * X-Weave-Records?
* * ...
* * Timeouts

Просмотреть файл

@ -1,5 +1,7 @@
package org.mozilla.gecko.tests;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.Telemetry;
import android.util.Log;
@ -12,8 +14,13 @@ public class testUITelemetry extends JavascriptTest {
@Override
public void testJavascript() throws Exception {
blockForGeckoReady();
Log.i("GeckoTest", "Adding telemetry events.");
// We can't run these tests unless telemetry is turned on --
// the events will be dropped on the floor.
Log.i("GeckoTest", "Enabling telemetry.");
PrefsHelper.setPref(AppConstants.TELEMETRY_PREF_NAME, true);
Log.i("GeckoTest", "Adding telemetry events.");
try {
Telemetry.sendUIEvent("enone", "method0");
Telemetry.startUISession("foo");

Просмотреть файл

@ -3,7 +3,11 @@
* 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/. */
Components.utils.import("resource://gre/modules/Services.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
function do_check_array_eq(a1, a2) {
do_check_eq(a1.length, a2.length);
@ -12,15 +16,27 @@ function do_check_array_eq(a1, a2) {
}
}
add_test(function test_telemetry_events() {
let bridge = Components.classes["@mozilla.org/android/bridge;1"]
.getService(Components.interfaces.nsIAndroidBridge);
function getObserver() {
let bridge = Cc["@mozilla.org/android/bridge;1"]
.getService(Ci.nsIAndroidBridge);
let obsXPCOM = bridge.browserApp.getUITelemetryObserver();
do_check_true(!!obsXPCOM);
return obsXPCOM.wrappedJSObject;
}
let obs = obsXPCOM.wrappedJSObject;
/**
* The following event test will fail if telemetry isn't enabled. The Java-side
* part of this test should have turned it on; fail if it didn't work.
*/
add_test(function test_enabled() {
let obs = getObserver();
do_check_true(!!obs);
do_check_true(obs.enabled);
run_next_test();
});
add_test(function test_telemetry_events() {
let obs = getObserver();
let measurements = obs.getUIMeasurements();
let expected = [

Просмотреть файл

@ -29,6 +29,7 @@ import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRe
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@ -93,6 +94,28 @@ public class TokenServerClient {
});
}
/**
* Notify the delegate that some kind of backoff header (X-Backoff,
* X-Weave-Backoff, Retry-After) was received and should be acted upon.
*
* This method is non-terminal, and will be followed by a separate
* <code>invoke*</code> call.
*
* @param delegate
* the delegate to inform.
* @param backoffSeconds
* the number of seconds for which the system should wait before
* making another token server request to this server.
*/
protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleBackoff(backoffSeconds);
}
});
}
protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
executor.execute(new Runnable() {
@Override
@ -102,17 +125,21 @@ public class TokenServerClient {
});
}
public TokenServerToken processResponse(HttpResponse response)
throws TokenServerException {
SyncResponse res = new SyncResponse(response);
public TokenServerToken processResponse(SyncResponse res) throws TokenServerException {
int statusCode = res.getStatusCode();
Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
// Responses should *always* be JSON, even in the case of 4xx and 5xx
// errors. If we don't see JSON, the server is likely very unhappy.
String contentType = response.getEntity().getContentType().getValue();
if (!contentType.equals("application/json") && !contentType.startsWith("application/json;")) {
final Header contentType = res.getContentType();
if (contentType == null) {
throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
}
final String type = contentType.getValue();
if (!type.equals("application/json") &&
!type.startsWith("application/json;")) {
Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
contentType + ". Misconfigured server?");
throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
@ -230,10 +257,21 @@ public class TokenServerClient {
@Override
public void handleHttpResponse(HttpResponse response) {
// Skew.
SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
skewHandler.updateSkew(response, System.currentTimeMillis());
// Extract backoff regardless of whether this was an error response, and
// Retry-After for 503 responses. The error will be handled elsewhere.)
SyncResponse res = new SyncResponse(response);
final boolean includeRetryAfter = res.getStatusCode() == 503;
int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter);
if (backoffInSeconds > -1) {
client.notifyBackoff(delegate, backoffInSeconds);
}
try {
TokenServerToken token = client.processResponse(response);
TokenServerToken token = client.processResponse(res);
client.invokeHandleSuccess(delegate, token);
} catch (TokenServerException e) {
client.invokeHandleFailure(delegate, e);

Просмотреть файл

@ -4,9 +4,13 @@
package org.mozilla.gecko.tokenserver;
public interface TokenServerClientDelegate {
void handleSuccess(TokenServerToken token);
void handleFailure(TokenServerException e);
void handleError(Exception e);
/**
* Might be called multiple times, in addition to the other terminating handler methods.
*/
void handleBackoff(int backoffSeconds);
}

Просмотреть файл

@ -173,11 +173,16 @@ function uninstallFakePAC() {
Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
}
// We want to ensure the legacy provider is used for most of these tests; the
// tests that know how to deal with the Firefox Accounts identity hack things
// to ensure that still works.
// We want to ensure the legacy provider is used for most of these tests,
// including after a service.startOver. The tests that know how to deal with
// the Firefox Accounts identity hack things to ensure that still works.
function setDefaultIdentityConfig() {
Cu.import("resource://gre/modules/Services.jsm");
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", true);
do_register_cleanup(function() {
Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
Services.prefs.clearUserPref("services.sync-testing.startOverKeepIdentity");
});
}
setDefaultIdentityConfig();

Просмотреть файл

@ -106,10 +106,10 @@ WeaveService.prototype = {
fxAccountsEnabled = !prefs.prefHasUserValue("username");
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", fxAccountsEnabled);
}
// Currently we don't support toggling this pref after initialization, so
// inject the pref value as a regular boolean.
delete this.fxAccountsEnabled;
return this.fxAccountsEnabled = fxAccountsEnabled;
// Currently we don't support toggling this pref after initialization -
// except when sync is reset - but this 1 exception is enough that we can't
// cache the value.
return fxAccountsEnabled;
},
observe: function (subject, topic, data) {

Просмотреть файл

@ -237,8 +237,7 @@ this.BrowserIDManager.prototype = {
* username is derived from account.
*
* Changing the account name has the side-effect of wiping out stored
* credentials. Keep in mind that persistCredentials() will need to be called
* to flush the changes to disk.
* credentials.
*
* Set this value to null to clear out identity information.
*/

Просмотреть файл

@ -40,18 +40,30 @@ PasswordEngine.prototype = {
SyncEngine.prototype._syncFinish.call(this);
// Delete the weave credentials from the server once
if (!Svc.Prefs.get("deletePwd", false)) {
if (!Svc.Prefs.get("deletePwdFxA", false)) {
try {
let ids = Services.logins.findLogins({}, PWDMGR_HOST, "", "")
.map(function(info) {
return info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid;
});
let coll = new Collection(this.engineURL, null, this.service);
coll.ids = ids;
let ret = coll.delete();
this._log.debug("Delete result: " + ret);
Svc.Prefs.set("deletePwd", true);
let ids = [];
for (let host of Utils.getSyncCredentialsHosts()) {
for (let info of Services.logins.findLogins({}, host, "", "")) {
ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid);
}
}
if (ids.length) {
let coll = new Collection(this.engineURL, null, this.service);
coll.ids = ids;
let ret = coll.delete();
this._log.debug("Delete result: " + ret);
if (!ret.success && ret.status != 400) {
// A non-400 failure means try again next time.
return;
}
} else {
this._log.debug("Didn't find any passwords to delete");
}
// If there were no ids to delete, or we succeeded, or got a 400,
// record success.
Svc.Prefs.set("deletePwdFxA", true);
Svc.Prefs.reset("deletePwd"); // The old prefname we previously used.
}
catch(ex) {
this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex));
@ -150,8 +162,9 @@ PasswordStore.prototype = {
for (let i = 0; i < logins.length; i++) {
// Skip over Weave password/passphrase entries
let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo);
if (metaInfo.hostname == PWDMGR_HOST)
if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) {
continue;
}
items[metaInfo.guid] = metaInfo;
}
@ -288,7 +301,7 @@ PasswordTracker.prototype = {
case "removeLogin":
// Skip over Weave password/passphrase changes.
subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
if (subject.hostname == PWDMGR_HOST) {
if (Utils.getSyncCredentialsHosts().has(subject.hostname)) {
break;
}

Просмотреть файл

@ -448,9 +448,11 @@ IdentityManager.prototype = {
* Deletes Sync credentials from the password manager.
*/
deleteSyncCredentials: function deleteSyncCredentials() {
let logins = Services.logins.findLogins({}, PWDMGR_HOST, "", "");
for each (let login in logins) {
Services.logins.removeLogin(login);
for (let host of Utils.getSyncCredentialsHosts()) {
let logins = Services.logins.findLogins({}, host, "", "");
for each (let login in logins) {
Services.logins.removeLogin(login);
}
}
// Wait until after store is updated in case it fails.

Просмотреть файл

@ -889,7 +889,32 @@ Sync11Service.prototype = {
this.identity.deleteSyncCredentials();
Svc.Obs.notify("weave:service:start-over:finish");
// If necessary, reset the identity manager, then re-initialize it so the
// FxA manager is used. This is configurable via a pref - mainly for tests.
let keepIdentity = false;
try {
keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
} catch (_) { /* no such pref */ }
if (keepIdentity) {
Svc.Obs.notify("weave:service:start-over:finish");
return;
}
this.identity.username = "";
Services.prefs.clearUserPref("services.sync.fxaccounts.enabled");
this.status.__authManager = null;
this.identity = Status._authManager;
this._clusterManager = this.identity.createClusterManager(this);
// Tell the new identity manager to initialize itself
this.identity.initialize().then(() => {
Svc.Obs.notify("weave:service:start-over:finish");
}).then(null, err => {
this._log.error("startOver failed to re-initialize the identity manager: " + err);
// Still send the observer notification so the current state is
// reflected in the UI.
Svc.Obs.notify("weave:service:start-over:finish");
});
},
persistLogin: function persistLogin() {

Просмотреть файл

@ -601,6 +601,38 @@ this.Utils = {
return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL),
statusInterval);
},
/**
* Return a set of hostnames (including the protocol) which may have
* credentials for sync itself stored in the login manager.
*
* In general, these hosts will not have their passwords synced, will be
* reset when we drop sync credentials, etc.
*/
getSyncCredentialsHosts: function() {
// This is somewhat expensive and the result static, so we cache the result.
if (this._syncCredentialsHosts) {
return this._syncCredentialsHosts;
}
let result = new Set();
// the legacy sync host.
result.add(PWDMGR_HOST);
// The FxA hosts - these almost certainly all have the same hostname, but
// better safe than sorry...
for (let prefName of ["identity.fxaccounts.remote.force_auth.uri",
"identity.fxaccounts.remote.uri",
"identity.fxaccounts.settings.uri"]) {
let prefVal;
try {
prefVal = Services.prefs.getCharPref(prefName);
} catch (_) {
continue;
}
let uri = Services.io.newURI(prefVal, null, null);
result.add(uri.prePath);
}
return this._syncCredentialsHosts = result;
},
};
XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() {

Просмотреть файл

@ -0,0 +1,61 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://services-sync/service.js");
function run_test() {
initTestLogging("Trace");
run_next_test();
}
add_task(function* test_startover() {
let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity", true);
Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
yield configureIdentity({username: "johndoe"});
// The pref that forces FxA identities should not be set.
do_check_false(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
// And the boolean flag on the xpcom service should reflect this.
let xps = Cc["@mozilla.org/weave/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
do_check_false(xps.fxAccountsEnabled);
// we expect the "legacy" provider (but can't instanceof that, as BrowserIDManager
// extends it)
do_check_false(Service.identity instanceof BrowserIDManager);
Service.login();
// We should have a cluster URL
do_check_true(Service.clusterURL.length > 0);
// remember some stuff so we can reset it after.
let oldIdentidy = Service.identity;
let oldClusterManager = Service._clusterManager;
let deferred = Promise.defer();
Services.obs.addObserver(function observeStartOverFinished() {
Services.obs.removeObserver(observeStartOverFinished, "weave:service:start-over:finish");
deferred.resolve();
}, "weave:service:start-over:finish", false);
Service.startOver();
yield deferred; // wait for the observer to fire.
// should have reset the pref that indicates if FxA is enabled.
do_check_true(Services.prefs.getBoolPref("services.sync.fxaccounts.enabled"));
// the xpcom service should agree FxA is enabled.
do_check_true(xps.fxAccountsEnabled);
// should have swapped identities.
do_check_true(Service.identity instanceof BrowserIDManager);
// should have clobbered the cluster URL
do_check_eq(Service.clusterURL, "");
// reset the world.
Service.identity = oldIdentity = Service.identity;
Service._clusterManager = Service._clusterManager;
Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", false);
Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue);
});

Просмотреть файл

@ -121,6 +121,9 @@ skip-if = os == "android"
[test_syncscheduler.js]
[test_upgrade_old_sync_key.js]
# Firefox Accounts specific tests
[test_fxa_startOver.js]
# Finally, we test each engine.
[test_addons_engine.js]
run-sequentially = Hardcoded port in static files.

Просмотреть файл

@ -784,6 +784,21 @@ File.move = function move(sourcePath, destPath, options) {
Type.path.toMsg(destPath), options], [sourcePath, destPath]);
};
/**
* Gets the number of bytes available on disk to the current user.
*
* @param {string} Platform-specific path to a directory on the disk to
* query for free available bytes.
*
* @return {number} The number of bytes available for the current user.
* @throws {OS.File.Error} In case of any error.
*/
File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
return Scheduler.post("getAvailableFreeSpace",
[Type.path.toMsg(sourcePath)], sourcePath
).then(Type.uint64_t.fromMsg);
};
/**
* Remove an empty directory.
*

Просмотреть файл

@ -324,6 +324,10 @@ const EXCEPTION_NAMES = {
return File.move(Type.path.fromMsg(sourcePath),
Type.path.fromMsg(destPath), options);
},
getAvailableFreeSpace: function getAvailableFreeSpace(sourcePath) {
return Type.uint64_t.toMsg(
File.getAvailableFreeSpace(Type.path.fromMsg(sourcePath)));
},
makeDir: function makeDir(path, options) {
return File.makeDir(Type.path.fromMsg(path), options);
},

Просмотреть файл

@ -218,6 +218,24 @@
ctypes.ArrayType(Type.timeval.implementation, 2));
}
// Types fsblkcnt_t and fsfilcnt_t, used by structure |statvfs|
Type.fsblkcnt_t =
Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName("fsblkcnt_t");
// Structure |statvfs|
// Use an hollow structure
{
let statvfs = new SharedAll.HollowStructure("statvfs",
Const.OSFILE_SIZEOF_STATVFS);
statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BSIZE,
"f_bsize", Type.unsigned_long.implementation);
statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL,
"f_bavail", Type.fsblkcnt_t.implementation);
Type.statvfs = statvfs.getType();
}
// Declare libc functions as functions of |OS.Unix.File|
// Finalizer-related functions
@ -506,6 +524,12 @@
/*len*/ Type.size_t,
/*flags*/ Type.unsigned_int); // Linux/Android-specific
libc.declareLazyFFI(SysFile, "statvfs",
"statvfs", ctypes.default_abi,
/*return*/ Type.negativeone_or_nothing,
/*path*/ Type.path,
/*buf*/ Type.statvfs.out_ptr);
libc.declareLazyFFI(SysFile, "symlink",
"symlink", ctypes.default_abi,
/*return*/ Type.negativeone_or_nothing,

Просмотреть файл

@ -351,6 +351,27 @@
}
};
/**
* Gets the number of bytes available on disk to the current user.
*
* @param {string} sourcePath Platform-specific path to a directory on
* the disk to query for free available bytes.
*
* @return {number} The number of bytes available for the current user.
* @throws {OS.File.Error} In case of any error.
*/
File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) {
let fileSystemInfo = new Type.statvfs.implementation();
let fileSystemInfoPtr = fileSystemInfo.address();
throw_on_negative("statvfs", UnixFile.statvfs(sourcePath, fileSystemInfoPtr));
let bytes = new Type.uint64_t.implementation(
fileSystemInfo.f_bsize * fileSystemInfo.f_bavail);
return bytes.value;
};
/**
* Default mode for opening directories.
*/

Просмотреть файл

@ -296,6 +296,14 @@
/*buf*/ Type.out_path
);
libc.declareLazyFFI(SysFile, "GetDiskFreeSpaceEx",
"GetDiskFreeSpaceExW", ctypes.winapi_abi,
/*return*/ Type.zero_or_nothing,
/*directoryName*/ Type.path,
/*freeBytesForUser*/ Type.uint64_t.out_ptr,
/*totalBytesForUser*/ Type.uint64_t.out_ptr,
/*freeTotalBytesOnDrive*/ Type.uint64_t.out_ptr);
libc.declareLazyFFI(SysFile, "GetFileInformationByHandle",
"GetFileInformationByHandle", ctypes.winapi_abi,
/*return*/ Type.zero_or_nothing,

Просмотреть файл

@ -571,6 +571,26 @@
}
};
/**
* Gets the number of bytes available on disk to the current user.
*
* @param {string} sourcePath Platform-specific path to a directory on
* the disk to query for free available bytes.
*
* @return {number} The number of bytes available for the current user.
* @throws {OS.File.Error} In case of any error.
*/
File.getAvailableFreeSpace = function Win_getAvailableFreeSpace(sourcePath) {
let freeBytesAvailableToUser = new Type.uint64_t.implementation(0);
let freeBytesAvailableToUserPtr = freeBytesAvailableToUser.address();
throw_on_zero("getAvailableFreeSpace",
WinFile.GetDiskFreeSpaceEx(sourcePath, freeBytesAvailableToUserPtr, null, null)
);
return freeBytesAvailableToUser.value;
};
/**
* A global value used to receive data during time conversions.
*/

Просмотреть файл

@ -0,0 +1,38 @@
/* 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";
Components.utils.import("resource://gre/modules/osfile.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
do_register_cleanup(function() {
Services.prefs.setBoolPref("toolkit.osfile.log", false);
});
function run_test() {
Services.prefs.setBoolPref("toolkit.osfile.log", true);
run_next_test();
}
/**
* Test OS.File.getAvailableFreeSpace
*/
add_task(function() {
// Set up profile. We will use profile path to query for available free
// space.
do_get_profile();
let dir = OS.Constants.Path.profileDir;
// Sanity checking for the test
do_check_true((yield OS.File.exists(dir)));
// Query for available bytes for user
let availableBytes = yield OS.File.getAvailableFreeSpace(dir);
do_check_true(!!availableBytes);
do_check_true(availableBytes > 0);
});

Просмотреть файл

@ -2,6 +2,7 @@
head =
tail =
[test_available_free_space.js]
[test_osfile_closed.js]
[test_path.js]
[test_osfile_async.js]

Просмотреть файл

@ -59,6 +59,12 @@ function startClient(transport, onDone) {
gClient.addListener("appUninstall", function (aState, aType, aPacket) {
sendAsyncMessage("uninstalled-event", { manifestURL: aType.manifestURL });
});
addMessageListener("appActorRequest", request => {
webappActorRequest(request, response => {
sendAsyncMessage("appActorResponse", response);
});
});
}
function webappActorRequest(request, onResponse) {

Просмотреть файл

@ -62,9 +62,16 @@ SimpleTest.waitForExplicitFinish();
var installTestApp, mm;
const PACKAGED_APP_ID = "test-app-id";
const PACKAGED_APP_ORIGIN = "app://" + PACKAGED_APP_ID;
const PACKAGED_APP_MANIFEST = PACKAGED_APP_ORIGIN + "/manifest.webapp";
const CERTIFIED_APP_ID = "test-certified-id";
const CERTIFIED_APP_ORIGIN = "app://" + CERTIFIED_APP_ID;
const CERTIFIED_APP_MANIFEST = CERTIFIED_APP_ORIGIN + "/manifest.webapp";
var steps = [
function() {
ok(true, "Start setting up");
info("== SETUP ==");
// Set up
launchableValue = SpecialPowers.setAllAppsLaunchable(true);
SpecialPowers.addPermission("webapps-manage", true, document);
@ -117,13 +124,12 @@ var steps = [
SpecialPowers.autoConfirmAppInstall(next);
},
function() {
ok(true, "== TEST == Install packaged app");
let appId = "test-app-id";
info("== TEST == Install packaged app");
let url = SimpleTest.getTestFileURL("data/app.zip");
installTestApp(url, appId,
installTestApp(url, PACKAGED_APP_ID,
function (aResponse, aApp) {
ok(true, "Installed");
is(aResponse.appId, appId, "Got same app id");
is(aResponse.appId, PACKAGED_APP_ID, "Got same app id");
if ("error" in aResponse) {
ok(false, "Error: " + aResponse.error);
}
@ -137,13 +143,12 @@ var steps = [
);
},
function () {
ok(true, "== TEST == Reinstall packaged app");
let appId = "test-app-id";
info("== TEST == Reinstall packaged app");
let url = SimpleTest.getTestFileURL("data/app-updated.zip");
installTestApp(url, appId,
installTestApp(url, PACKAGED_APP_ID,
function (aResponse, aApp) {
ok(true, "Reinstalled");
is(aResponse.appId, appId, "Got same app id");
is(aResponse.appId, PACKAGED_APP_ID, "Got same app id");
if ("error" in aResponse) {
ok(false, "Error: " + aResponse.error);
}
@ -157,13 +162,12 @@ var steps = [
);
},
function() {
ok(true, "== TEST == Install certified app");
let appId = "test-certified-id";
info("== TEST == Install certified app");
let url = SimpleTest.getTestFileURL("data/app-certified.zip");
installTestApp(url, appId,
installTestApp(url, CERTIFIED_APP_ID,
function (aResponse, aApp) {
ok(true, "Installed");
is(aResponse.appId, appId, "Got same app id");
is(aResponse.appId, CERTIFIED_APP_ID, "Got same app id");
if ("error" in aResponse) {
ok(false, "Error: " + aResponse.error);
}
@ -176,15 +180,137 @@ var steps = [
}
);
},
function() {
info("== TEST == Get all apps");
getAll(false);
},
function() {
info("== TEST == Get packaged app");
getApp({
id: PACKAGED_APP_ID,
manifestURL: PACKAGED_APP_MANIFEST
}, true);
},
function() {
info("== TEST == Get certified app");
getApp({
id: CERTIFIED_APP_ID,
manifestURL: CERTIFIED_APP_MANIFEST
}, false);
},
function() {
info("== SETUP == Enable certified app access");
SpecialPowers.pushPrefEnv({
"set": [["devtools.debugger.forbid-certified-apps", false]]
}, next);
},
function() {
info("== TEST == Get all apps (CERTIFIED ENABLED)");
getAll(true);
},
function() {
info("== TEST == Get packaged app (CERTIFIED ENABLED)");
getApp({
id: PACKAGED_APP_ID,
manifestURL: PACKAGED_APP_MANIFEST
}, true);
},
function() {
info("== TEST == Get certified app (CERTIFIED ENABLED)");
getApp({
id: CERTIFIED_APP_ID,
manifestURL: CERTIFIED_APP_MANIFEST
}, true);
},
function() {
info("== SETUP == Disable certified app access");
SpecialPowers.popPrefEnv(next);
},
function() {
info("== TEST == Uninstall packaged app");
uninstall(PACKAGED_APP_MANIFEST);
},
function() {
info("== TEST == Uninstall certified app");
uninstall(CERTIFIED_APP_MANIFEST);
},
function() {
ok(true, "all done!\n");
mm.sendAsyncMessage("cleanup");
SpecialPowers.popPrefEnv(finish);
SpecialPowers.flushPrefEnv(finish);
}
];
addLoadEvent(start);
function getAll(expectCertified) {
mm.addMessageListener("appActorResponse", function onResponse(response) {
mm.removeMessageListener("appActorResponse", onResponse);
ok("apps" in response, "Apps found in getAll reply");
let apps = response.apps;
let packagedApp, certifiedApp;
for (let app of apps) {
switch (app.id) {
case PACKAGED_APP_ID:
packagedApp = app;
break;
case CERTIFIED_APP_ID:
certifiedApp = app;
break;
}
}
ok(packagedApp, "Packaged app found via getAll");
is(!!certifiedApp, expectCertified, "Certified app matches expectation");
next();
});
mm.sendAsyncMessage("appActorRequest", {
type: "getAll"
});
}
function getApp(appInfo, expected) {
mm.addMessageListener("appActorResponse", function onResponse(response) {
mm.removeMessageListener("appActorResponse", onResponse);
is("app" in response, expected, "App existence matches expectation");
is("error" in response, !expected, "Error existence matches expectation");
if (!expected) {
is(response.error, "forbidden", "Error message is correct");
next();
return;
}
let app = response.app;
for (let key in appInfo) {
is(app[key], appInfo[key], "Value for " + key + " matches");
}
next();
});
mm.sendAsyncMessage("appActorRequest", {
type: "getApp",
manifestURL: appInfo.manifestURL
});
}
function uninstall(manifestURL) {
mm.addMessageListener("appActorResponse", function onResponse(response) {
mm.removeMessageListener("appActorResponse", onResponse);
ok(!("error" in response), "App uninstalled successfully");
next();
});
mm.sendAsyncMessage("appActorRequest", {
type: "uninstall",
manifestURL: manifestURL
});
}
</script>
</pre>
</body>

Просмотреть файл

@ -2460,7 +2460,14 @@ SourceActor.prototype = {
// XXX bug 865252: Don't load from the cache if this is a source mapped
// source because we can't guarantee that the cache has the most up to date
// content for this source like we can if it isn't source mapped.
return fetch(this._url, { loadFromCache: !this._sourceMap });
let sourceFetched = fetch(this._url, { loadFromCache: !this._sourceMap });
// Record the contentType we just learned during fetching
sourceFetched.then(({ contentType }) => {
this._contentType = contentType;
});
return sourceFetched;
},
/**