Merge fx-team to m-c.
|
@ -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 |
Двоичные данные
mobile/android/base/resources/drawable-hdpi/fxaccount_icon.png
До Ширина: | Высота: | Размер: 1.4 KiB После Ширина: | Высота: | Размер: 4.0 KiB |
Двоичные данные
mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png
До Ширина: | Высота: | Размер: 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 |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/fxaccount_icon.png
До Ширина: | Высота: | Размер: 901 B После Ширина: | Высота: | Размер: 2.4 KiB |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/fxaccount_mail.png
До Ширина: | Высота: | Размер: 1.3 KiB После Ширина: | Высота: | Размер: 4.8 KiB |
До Ширина: | Высота: | Размер: 421 B После Ширина: | Высота: | Размер: 603 B |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/sync_desktop.png
До Ширина: | Высота: | Размер: 206 B После Ширина: | Высота: | Размер: 1.1 KiB |
Двоичные данные
mobile/android/base/resources/drawable-mdpi/sync_mobile.png
До Ширина: | Высота: | Размер: 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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|