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

This commit is contained in:
Ryan VanderMeulen 2014-12-18 17:27:16 -05:00
commit 30a5f38a23
40 changed files with 1357 additions and 249 deletions

View File

@ -353,7 +353,7 @@ input[type=button] {
}
@media not all and (max-resolution: 1dppx) {
#newtab-search-icon.magnifier {
#newtab-search-logo.magnifier {
background-image: url("chrome://browser/skin/magnifier@2x.png");
}
}

View File

@ -1040,6 +1040,8 @@
[currentEngine.name], 1);
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name")
.setAttribute("value", headerText);
document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine")
.engine = currentEngine;
// Update the 'Search for <keywords> with:" header.
let headerSearchText =
@ -1198,11 +1200,13 @@
return; // ignore right clicks.
let button = event.originalTarget;
if (button.localName != "button" || !button.engine)
let engine = button.engine || button.parentNode.engine;
if (!engine)
return;
let searchbar = document.getElementById("searchbar");
searchbar.handleSearchCommand(event, button.engine);
searchbar.handleSearchCommand(event, engine);
]]></handler>
<handler event="command"><![CDATA[

View File

@ -115,6 +115,10 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::ENABLE_INDEXED_DB,
// Shares an IndexedDB origin with about:loopconversation.
"loopconversation" },
{ "reader", "chrome://global/content/reader/aboutReader.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
};
static const int kRedirTotal = ArrayLength(kRedirMap);

View File

@ -115,6 +115,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "reader", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#if defined(XP_WIN)
{ NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
#elif defined(XP_MACOSX)

View File

@ -42,7 +42,11 @@ add_task(function() {
FullZoom.enlarge();
yield zoomChangePromise;
is(parseInt(zoomResetButton.label, 10), 110, "Zoom is changed to 110% for about:mozilla");
let attributeChangePromise = promiseAttributeMutation(zoomResetButton, "label", (v) => {
return parseInt(v, 10) == 100;
});
yield promiseTabLoadEvent(tab1, "about:home");
yield attributeChangePromise;
is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:home");
yield promiseTabHistoryNavigation(-1, function() {
return parseInt(zoomResetButton.label, 10) == 110;

View File

@ -461,6 +461,34 @@ function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) {
return deferred.promise;
}
/**
* Wait for an attribute on a node to change
*
* @param aNode Node on which the mutation is expected
* @param aAttribute The attribute we're interested in
* @param aFilterFn A function to check if the new value is what we want.
* @return {Promise} resolved when the requisite mutation shows up.
*/
function promiseAttributeMutation(aNode, aAttribute, aFilterFn) {
return new Promise((resolve, reject) => {
info("waiting for mutation of attribute '" + aAttribute + "'.");
let obs = new MutationObserver((mutations) => {
for (let mut of mutations) {
let attr = mut.attributeName;
let newValue = mut.target.getAttribute(attr);
if (aFilterFn(newValue)) {
ok(true, "mutation occurred: attribute '" + attr + "' changed to '" + newValue + "' from '" + mut.oldValue + "'.");
obs.disconnect();
resolve();
} else {
info("Ignoring mutation that produced value " + newValue + " because of filter.");
}
}
});
obs.observe(aNode, {attributeFilter: [aAttribute]});
});
}
function popupShown(aPopup) {
return promisePopupEvent(aPopup, "shown");
}

View File

@ -37,6 +37,8 @@
<script type="text/javascript" src="loop/shared/js/store.js"></script>
<script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
<script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>

View File

@ -730,6 +730,15 @@ loop.panel = (function(_, mozL10n) {
this.stopListening(this.props.store);
},
componentWillUpdate: function(nextProps, nextState) {
// If we've just created a room, close the panel - the store will open
// the room.
if (this.state.pendingCreation &&
!nextState.pendingCreation && !nextState.error) {
this.closeWindow();
}
},
_onStoreStateChanged: function() {
this.setState(this.props.store.getStoreState());
},
@ -747,8 +756,6 @@ loop.panel = (function(_, mozL10n) {
},
handleCreateButtonClick: function() {
this.closeWindow();
this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
nameTemplate: mozL10n.get("rooms_default_room_name_template"),
roomOwner: this.props.userDisplayName
@ -1003,7 +1010,8 @@ loop.panel = (function(_, mozL10n) {
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
mozLoop: navigator.mozLoop,
notifications: notifications
});
React.renderComponent(PanelView({

View File

@ -730,6 +730,15 @@ loop.panel = (function(_, mozL10n) {
this.stopListening(this.props.store);
},
componentWillUpdate: function(nextProps, nextState) {
// If we've just created a room, close the panel - the store will open
// the room.
if (this.state.pendingCreation &&
!nextState.pendingCreation && !nextState.error) {
this.closeWindow();
}
},
_onStoreStateChanged: function() {
this.setState(this.props.store.getStoreState());
},
@ -747,8 +756,6 @@ loop.panel = (function(_, mozL10n) {
},
handleCreateButtonClick: function() {
this.closeWindow();
this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
nameTemplate: mozL10n.get("rooms_default_room_name_template"),
roomOwner: this.props.userDisplayName
@ -1003,7 +1010,8 @@ loop.panel = (function(_, mozL10n) {
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
mozLoop: navigator.mozLoop,
notifications: notifications
});
React.renderComponent(<PanelView

View File

@ -29,6 +29,8 @@
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/store.js"></script>
<script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>

View File

@ -197,12 +197,23 @@ loop.shared.actions = (function() {
roomOwner: String
}),
/**
* When a room has been created.
* XXX: should move to some roomActions module - refs bug 1079284
*/
CreatedRoom: Action.define("createdRoom", {
roomToken: String
}),
/**
* Rooms creation error.
* XXX: should move to some roomActions module - refs bug 1079284
*/
CreateRoomError: Action.define("createRoomError", {
error: Error
// There's two types of error possible - one thrown by our code (and Error)
// and the other is an Object about the error codes from the server as
// returned by the Hawk request.
error: Object
}),
/**
@ -218,7 +229,10 @@ loop.shared.actions = (function() {
* XXX: should move to some roomActions module - refs bug 1079284
*/
DeleteRoomError: Action.define("deleteRoomError", {
error: Error
// There's two types of error possible - one thrown by our code (and Error)
// and the other is an Object about the error codes from the server as
// returned by the Hawk request.
error: Object
}),
/**

View File

@ -21,31 +21,7 @@ loop.store.ActiveRoomStore = (function() {
ROOM_FULL: 202
};
var ROOM_STATES = loop.store.ROOM_STATES = {
// The initial state of the room
INIT: "room-init",
// The store is gathering the room data
GATHER: "room-gather",
// The store has got the room data
READY: "room-ready",
// Obtaining media from the user
MEDIA_WAIT: "room-media-wait",
// The room is known to be joined on the loop-server
JOINED: "room-joined",
// The room is connected to the sdk server.
SESSION_CONNECTED: "room-session-connected",
// There are participants in the room.
HAS_PARTICIPANTS: "room-has-participants",
// There was an issue with the room
FAILED: "room-failed",
// The room is full
FULL: "room-full",
// The room conversation has ended
ENDED: "room-ended",
// The window is closing
CLOSING: "room-closing"
};
var ROOM_STATES = loop.store.ROOM_STATES;
/**
* Active room store.
*

View File

@ -0,0 +1,164 @@
/* 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/. */
/* global loop:true */
var loop = loop || {};
loop.store = loop.store || {};
loop.store.FxOSActiveRoomStore = (function() {
"use strict";
var sharedActions = loop.shared.actions;
var ROOM_STATES = loop.store.ROOM_STATES;
var FxOSActiveRoomStore = loop.store.createStore({
actions: [
"fetchServerData"
],
initialize: function(options) {
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._mozLoop = options.mozLoop;
},
/**
* Returns initial state data for this active room.
*/
getInitialStoreState: function() {
return {
roomState: ROOM_STATES.INIT,
audioMuted: false,
videoMuted: false,
failureReason: undefined
};
},
/**
* Registers the actions with the dispatcher that this store is interested
* in after the initial setup has been performed.
*/
_registerPostSetupActions: function() {
this.dispatcher.register(this, [
"joinRoom"
]);
},
/**
* Execute fetchServerData event action from the dispatcher. Although
* this is to fetch the server data - for rooms on the standalone client,
* we don't actually need to get any data. Therefore we just save the
* data that is given to us for when the user chooses to join the room.
*
* @param {sharedActions.FetchServerData} actionData
*/
fetchServerData: function(actionData) {
if (actionData.windowType !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
this._registerPostSetupActions();
this.setStoreState({
roomToken: actionData.token,
roomState: ROOM_STATES.READY
});
},
/**
* Handles the action to join to a room.
*/
joinRoom: function() {
// Reset the failure reason if necessary.
if (this.getStoreState().failureReason) {
this.setStoreState({failureReason: undefined});
}
this._setupOutgoingRoom(true);
},
/**
* Sets up an outgoing room. It will try launching the activity to let the
* FirefoxOS loop app handle the call. If the activity fails:
* - if installApp is true, then it'll try to install the FirefoxOS loop
* app.
* - if installApp is false, then it'll just log and error and fail.
*
* @param {boolean} installApp
*/
_setupOutgoingRoom: function(installApp) {
var request = new MozActivity({
name: "room-call",
data: {
type: "loop/rToken",
token: this.getStoreState("roomToken")
}
});
request.onsuccess = function() {};
request.onerror = (function(event) {
if (!installApp) {
// This really should not happen ever.
console.error(
"Unexpected activity launch error after the app has been installed");
return;
}
if (event.target.error.name !== "NO_PROVIDER") {
console.error ("Unexpected " + event.target.error.name);
return;
}
// We need to install the FxOS app.
this.setStoreState({
marketplaceSrc: loop.config.marketplaceUrl,
onMarketplaceMessage: this._onMarketplaceMessage.bind(this)
});
}).bind(this);
},
/**
* This method will handle events generated on the marketplace frame. It
* will launch the FirefoxOS loop app installation, and receive the result
* of the installation.
*
* @param {DOMEvent} event
*/
_onMarketplaceMessage: function(event) {
var message = event.data;
switch (message.name) {
case "loaded":
var marketplace = window.document.getElementById("marketplace");
// Once we have it loaded, we request the installation of the FxOS
// Loop client app. We will be receiving the result of this action
// via postMessage from the child iframe.
marketplace.contentWindow.postMessage({
"name": "install-package",
"data": {
"product": {
"name": loop.config.fxosApp.name,
"manifest_url": loop.config.fxosApp.manifestUrl,
"is_packaged": true
}
}
}, "*");
break;
case "install-package":
window.removeEventListener("message", this.onMarketplaceMessage);
if (message.error) {
console.error(message.error.error);
return;
}
// We installed the FxOS app, so we can continue with the call
// process.
this._setupOutgoingRoom(false);
break;
}
}
});
return FxOSActiveRoomStore;
})();

View File

@ -0,0 +1,33 @@
/* 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/. */
/* global loop:true */
var loop = loop || {};
loop.store = loop.store || {};
loop.store.ROOM_STATES = {
// The initial state of the room
INIT: "room-init",
// The store is gathering the room data
GATHER: "room-gather",
// The store has got the room data
READY: "room-ready",
// Obtaining media from the user
MEDIA_WAIT: "room-media-wait",
// The room is known to be joined on the loop-server
JOINED: "room-joined",
// The room is connected to the sdk server.
SESSION_CONNECTED: "room-session-connected",
// There are participants in the room.
HAS_PARTICIPANTS: "room-has-participants",
// There was an issue with the room
FAILED: "room-failed",
// The room is full
FULL: "room-full",
// The room conversation has ended
ENDED: "room-ended",
// The window is closing
CLOSING: "room-closing"
};

View File

@ -7,7 +7,7 @@
var loop = loop || {};
loop.store = loop.store || {};
(function() {
(function(mozL10n) {
"use strict";
/**
@ -67,6 +67,8 @@ loop.store = loop.store || {};
* - {mozLoop} mozLoop The MozLoop API object.
* - {ActiveRoomStore} activeRoomStore An optional substore for active room
* state.
* - {Notifications} notifications An optional notifications item that is
* required if create actions are to be used
*/
loop.store.RoomStore = loop.store.createStore({
/**
@ -89,6 +91,7 @@ loop.store = loop.store || {};
*/
actions: [
"createRoom",
"createdRoom",
"createRoomError",
"copyRoomUrl",
"deleteRoom",
@ -107,6 +110,7 @@ loop.store = loop.store || {};
throw new Error("Missing option mozLoop");
}
this._mozLoop = options.mozLoop;
this._notifications = options.notifications;
if (options.activeRoomStore) {
this.activeRoomStore = options.activeRoomStore;
@ -257,7 +261,10 @@ loop.store = loop.store || {};
* @param {sharedActions.CreateRoom} actionData The new room information.
*/
createRoom: function(actionData) {
this.setStoreState({pendingCreation: true});
this.setStoreState({
pendingCreation: true,
error: null,
});
var roomCreationData = {
roomName: this._generateNewRoomName(actionData.nameTemplate),
@ -266,19 +273,32 @@ loop.store = loop.store || {};
expiresIn: this.defaultExpiresIn
};
this._notifications.remove("create-room-error");
this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) {
this.setStoreState({pendingCreation: false});
if (err) {
this.dispatchAction(new sharedActions.CreateRoomError({error: err}));
return;
}
// Opens the newly created room
this.dispatchAction(new sharedActions.OpenRoom({
this.dispatchAction(new sharedActions.CreatedRoom({
roomToken: createdRoom.roomToken
}));
}.bind(this));
},
/**
* Executed when a room has been created
*/
createdRoom: function(actionData) {
this.setStoreState({pendingCreation: false});
// Opens the newly created room
this.dispatchAction(new sharedActions.OpenRoom({
roomToken: actionData.roomToken
}));
},
/**
* Executed when a room creation error occurs.
*
@ -289,6 +309,13 @@ loop.store = loop.store || {};
error: actionData.error,
pendingCreation: false
});
// XXX Needs a more descriptive error - bug 1109151.
this._notifications.set({
id: "create-room-error",
level: "error",
message: mozL10n.get("generic_failure_title")
});
},
/**
@ -406,4 +433,4 @@ loop.store = loop.store || {};
this.setStoreState({error: actionData.error});
}
});
})();
})(document.mozL10n || navigator.mozL10n);

View File

@ -66,22 +66,24 @@ browser.jar:
content/browser/loop/shared/img/telefonica@2x.png (content/shared/img/telefonica@2x.png)
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js)
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js)
content/browser/loop/shared/js/roomStates.js (content/shared/js/roomStates.js)
content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
# Shared libs
#ifdef DEBUG

View File

@ -83,6 +83,7 @@ config:
@echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js
@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
@echo "loop.config.fxosApp.rooms = true;" >> content/config.js
@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
@echo "loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';" >> content/config.js
@echo "loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';" >> content/config.js

View File

@ -98,12 +98,15 @@
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="shared/js/store.js"></script>
<script type="text/javascript" src="shared/js/roomStates.js"></script>
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/fxOSMarketplace.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>

View File

@ -0,0 +1,37 @@
/** @jsx React.DOM */
var loop = loop || {};
loop.fxOSMarketplaceViews = (function() {
"use strict";
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
* that allow us to request the installation of apps given their manifest
* URL. We will be embedding the content of this web page within an hidden
* iframe in case that we need to request the installation of the FxOS Loop
* client.
*/
var FxOSHiddenMarketplaceView = React.createClass({displayName: 'FxOSHiddenMarketplaceView',
render: function() {
return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true});
},
componentDidUpdate: function() {
// This happens only once when we change the 'src' property of the iframe.
if (this.props.onMarketplaceMessage) {
// The reason for listening on the global window instead of on the
// iframe content window is because the Marketplace is doing a
// window.top.postMessage.
// XXX Bug 1097703: This should be changed to an action when the old
// style URLs go away.
window.addEventListener("message", this.props.onMarketplaceMessage);
}
}
});
return {
FxOSHiddenMarketplaceView: FxOSHiddenMarketplaceView
};
})();

View File

@ -0,0 +1,37 @@
/** @jsx React.DOM */
var loop = loop || {};
loop.fxOSMarketplaceViews = (function() {
"use strict";
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
* that allow us to request the installation of apps given their manifest
* URL. We will be embedding the content of this web page within an hidden
* iframe in case that we need to request the installation of the FxOS Loop
* client.
*/
var FxOSHiddenMarketplaceView = React.createClass({
render: function() {
return <iframe id="marketplace" src={this.props.marketplaceSrc} hidden/>;
},
componentDidUpdate: function() {
// This happens only once when we change the 'src' property of the iframe.
if (this.props.onMarketplaceMessage) {
// The reason for listening on the global window instead of on the
// iframe content window is because the Marketplace is doing a
// window.top.postMessage.
// XXX Bug 1097703: This should be changed to an action when the old
// style URLs go away.
window.addEventListener("message", this.props.onMarketplaceMessage);
}
}
});
return {
FxOSHiddenMarketplaceView: FxOSHiddenMarketplaceView
};
})();

View File

@ -20,8 +20,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
propTypes: {
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
@ -189,8 +191,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
],
propTypes: {
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -379,6 +383,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
enableHangup: this._roomIsActive()})
)
),
loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView({
marketplaceSrc: this.state.marketplaceSrc,
onMarketplaceMessage: this.state.onMarketplaceMessage}),
StandaloneRoomFooter(null)
)
);

View File

@ -20,8 +20,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
var StandaloneRoomInfoArea = React.createClass({
propTypes: {
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
@ -189,8 +191,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
],
propTypes: {
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
@ -379,6 +383,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
enableHangup={this._roomIsActive()} />
</div>
</div>
<loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
marketplaceSrc={this.state.marketplaceSrc}
onMarketplaceMessage={this.state.onMarketplaceMessage} />
<StandaloneRoomFooter />
</div>
);

View File

@ -127,32 +127,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
* that allow us to request the installation of apps given their manifest
* URL. We will be embedding the content of this web page within an hidden
* iframe in case that we need to request the installation of the FxOS Loop
* client.
*/
var FxOSHiddenMarketplace = React.createClass({displayName: 'FxOSHiddenMarketplace',
render: function() {
return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true});
},
componentDidUpdate: function() {
// This happens only once when we change the 'src' property of the iframe.
if (this.props.onMarketplaceMessage) {
// The reason for listening on the global window instead of on the
// iframe content window is because the Marketplace is doing a
// window.top.postMessage.
window.addEventListener("message", this.props.onMarketplaceMessage);
}
}
});
var FxOSConversationModel = Backbone.Model.extend({
setupOutgoingCall: function() {
setupOutgoingCall: function(selectedCallType) {
if (selectedCallType) {
this.set("selectedCallType", selectedCallType);
}
// The FxOS Loop client exposes a "loop-call" activity. If we get the
// activity onerror callback it means that there is no "loop-call"
// activity handler available and so no FxOS Loop client installed.
@ -162,7 +141,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
type: "loop/token",
token: this.get("loopToken"),
callerId: this.get("callerId"),
callType: this.get("callType")
video: this.get("selectedCallType") === "audio-video"
}
});
@ -565,7 +544,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
dangerouslySetInnerHTML: {__html: tosHTML}})
),
FxOSHiddenMarketplace({
loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView({
marketplaceSrc: this.state.marketplaceSrc,
onMarketplaceMessage: this.state.onMarketplaceMessage}),
@ -959,8 +938,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
// XXX New types for flux style
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
},
@ -1036,14 +1017,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var conversation
if (helper.isFirefoxOS(navigator.userAgent)) {
conversation = new FxOSConversationModel();
} else {
conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
}
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
@ -1061,6 +1034,29 @@ loop.webapp = (function($, _, OT, mozL10n) {
dispatcher: dispatcher,
sdk: OT
});
var conversation;
var activeRoomStore;
if (helper.isFirefoxOS(navigator.userAgent)) {
if (loop.config.fxosApp) {
conversation = new FxOSConversationModel();
if (loop.config.fxosApp.rooms) {
activeRoomStore = new loop.store.FxOSActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop
});
}
}
}
conversation = conversation ||
new sharedModels.ConversationModel({}, {
sdk: OT
});
activeRoomStore = activeRoomStore ||
new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
var feedbackClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
@ -1075,10 +1071,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});

View File

@ -127,32 +127,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
* that allow us to request the installation of apps given their manifest
* URL. We will be embedding the content of this web page within an hidden
* iframe in case that we need to request the installation of the FxOS Loop
* client.
*/
var FxOSHiddenMarketplace = React.createClass({
render: function() {
return <iframe id="marketplace" src={this.props.marketplaceSrc} hidden/>;
},
componentDidUpdate: function() {
// This happens only once when we change the 'src' property of the iframe.
if (this.props.onMarketplaceMessage) {
// The reason for listening on the global window instead of on the
// iframe content window is because the Marketplace is doing a
// window.top.postMessage.
window.addEventListener("message", this.props.onMarketplaceMessage);
}
}
});
var FxOSConversationModel = Backbone.Model.extend({
setupOutgoingCall: function() {
setupOutgoingCall: function(selectedCallType) {
if (selectedCallType) {
this.set("selectedCallType", selectedCallType);
}
// The FxOS Loop client exposes a "loop-call" activity. If we get the
// activity onerror callback it means that there is no "loop-call"
// activity handler available and so no FxOS Loop client installed.
@ -162,7 +141,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
type: "loop/token",
token: this.get("loopToken"),
callerId: this.get("callerId"),
callType: this.get("callType")
video: this.get("selectedCallType") === "audio-video"
}
});
@ -565,7 +544,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
dangerouslySetInnerHTML={{__html: tosHTML}}></p>
</div>
<FxOSHiddenMarketplace
<loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
marketplaceSrc={this.state.marketplaceSrc}
onMarketplaceMessage= {this.state.onMarketplaceMessage} />
@ -959,8 +938,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
// XXX New types for flux style
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
loop.store.ActiveRoomStore).isRequired,
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
},
@ -1036,14 +1017,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
var conversation
if (helper.isFirefoxOS(navigator.userAgent)) {
conversation = new FxOSConversationModel();
} else {
conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
}
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
@ -1061,6 +1034,29 @@ loop.webapp = (function($, _, OT, mozL10n) {
dispatcher: dispatcher,
sdk: OT
});
var conversation;
var activeRoomStore;
if (helper.isFirefoxOS(navigator.userAgent)) {
if (loop.config.fxosApp) {
conversation = new FxOSConversationModel();
if (loop.config.fxosApp.rooms) {
activeRoomStore = new loop.store.FxOSActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop
});
}
}
}
conversation = conversation ||
new sharedModels.ConversationModel({}, {
sdk: OT
});
activeRoomStore = activeRoomStore ||
new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
var feedbackClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
@ -1075,10 +1071,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: helper,
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: feedbackClient
});

View File

@ -30,6 +30,7 @@ function getConfigFile(req, res) {
"loop.config.legalWebsiteUrl = 'https://www.mozilla.org/about/legal/terms/firefox-hello/';",
"loop.config.fxosApp = loop.config.fxosApp || {};",
"loop.config.fxosApp.name = 'Loop';",
"loop.config.fxosApp.rooms = true;",
"loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';",
"loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';",
"loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';",

View File

@ -52,6 +52,8 @@
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/roomStore.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>

View File

@ -946,11 +946,15 @@ describe("loop.panel", function() {
}));
});
it("should close the panel when 'Start a Conversation' is clicked",
it("should close the panel once a room is created and there is no error",
function() {
var view = createTestComponent();
TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
roomStore.setStoreState({pendingCreation: true});
sinon.assert.notCalled(fakeWindow.close);
roomStore.setStoreState({pendingCreation: false});
sinon.assert.calledOnce(fakeWindow.close);
});

View File

@ -0,0 +1,212 @@
/* global chai, loop */
var expect = chai.expect;
var sharedActions = loop.shared.actions;
describe("loop.store.FxOSActiveRoomStore", function () {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var sandbox;
var dispatcher;
var fakeMozLoop;
var store;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
fakeMozLoop = {
setLoopPref: sandbox.stub(),
rooms: {
join: sinon.stub()
}
};
store = new loop.store.FxOSActiveRoomStore(dispatcher, {
mozLoop: fakeMozLoop
});
});
afterEach(function() {
sandbox.restore();
});
describe("#FxOSActiveRoomStore - constructor", function() {
it("should throw an error if mozLoop is missing", function() {
expect(function() {
new loop.store.FxOSActiveRoomStore(dispatcher);
}).to.Throw(/mozLoop/);
});
});
describe("#FxOSActiveRoomStore - fetchServerData", function() {
it("should save the token", function() {
store.fetchServerData(new sharedActions.FetchServerData({
windowType: "room",
token: "fakeToken"
}));
expect(store.getStoreState().roomToken).eql("fakeToken");
});
it("should set the state to `READY`", function() {
store.fetchServerData(new sharedActions.FetchServerData({
windowType: "room",
token: "fakeToken"
}));
expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
});
});
describe("#FxOSActiveRoomStore - setupOutgoingRoom", function() {
var realMozActivity;
var _activityDetail;
var _onerror;
function fireError(errorName) {
_onerror({
target: {
error: {
name: errorName
}
}
});
}
before(function() {
realMozActivity = window.MozActivity;
window.MozActivity = function(activityDetail) {
_activityDetail = activityDetail;
return {
set onerror(cbk) {
_onerror = cbk;
}
};
};
});
after(function() {
window.MozActivity = realMozActivity;
});
beforeEach(function() {
sandbox.stub(console, "error");
_activityDetail = undefined;
_onerror = undefined;
});
afterEach(function() {
sandbox.restore();
});
it("should reset failureReason", function() {
store.setStoreState({failureReason: "Test"});
store.joinRoom();
expect(store.getStoreState().failureReason).eql(undefined);
});
it("should create an activity", function() {
store.setStoreState({
roomToken: "fakeToken",
token: "fakeToken"
});
expect(_activityDetail).to.not.exist;
store.joinRoom();
expect(_activityDetail).to.exist;
expect(_activityDetail).eql({
name: "room-call",
data: {
type: "loop/rToken",
token: "fakeToken"
}
});
});
it("should change the store state when the activity fail with a " +
"NO_PROVIDER error", function() {
loop.config = {
marketplaceUrl: "http://market/"
};
store._setupOutgoingRoom(true);
fireError("NO_PROVIDER");
expect(store.getStoreState().marketplaceSrc).eql(
loop.config.marketplaceUrl
);
});
it("should log an error when the activity fail with a error different " +
"from NO_PROVIDER", function() {
loop.config = {
marketplaceUrl: "http://market/"
};
store._setupOutgoingRoom(true);
fireError("whatever");
sinon.assert.calledOnce(console.error);
sinon.assert.calledWith(console.error, "Unexpected whatever");
});
it("should log an error and exist if an activity error is received when " +
"the parameter is false ", function() {
loop.config = {
marketplaceUrl: "http://market/"
};
store._setupOutgoingRoom(false);
fireError("whatever");
sinon.assert.calledOnce(console.error);
sinon.assert.calledWith(console.error,
"Unexpected activity launch error after the app has been installed");
});
});
describe("#FxOSActiveRoomStore - _onMarketplaceMessage", function() {
var setupOutgoingRoom;
beforeEach(function() {
sandbox.stub(console, "error");
setupOutgoingRoom = sandbox.stub(store, "_setupOutgoingRoom");
});
afterEach(function() {
setupOutgoingRoom.restore();
});
it("We should call trigger a FxOS outgoing call if we get " +
"install-package message without error", function() {
sinon.assert.notCalled(setupOutgoingRoom);
store._onMarketplaceMessage({
data: {
name: "install-package"
}
});
sinon.assert.calledOnce(setupOutgoingRoom);
});
it("We should log an error if we get install-package message with an " +
"error", function() {
sinon.assert.notCalled(setupOutgoingRoom);
store._onMarketplaceMessage({
data: {
name: "install-package",
error: { error: "whatever error" }
}
});
sinon.assert.notCalled(setupOutgoingRoom);
sinon.assert.calledOnce(console.error);
sinon.assert.calledWith(console.error, "whatever error");
});
});
});

View File

@ -50,6 +50,8 @@
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/roomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
@ -67,6 +69,7 @@
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="fxOSActiveRoomStore_test.js"></script>
<script src="conversationStore_test.js"></script>
<script src="feedbackStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>

View File

@ -64,7 +64,7 @@ describe("loop.store.RoomStore", function () {
});
describe("constructed", function() {
var fakeMozLoop, store;
var fakeMozLoop, fakeNotifications, store;
var defaultStoreState = {
error: undefined,
@ -86,7 +86,14 @@ describe("loop.store.RoomStore", function () {
on: sandbox.stub()
}
};
store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
fakeNotifications = {
set: sinon.stub(),
remove: sinon.stub()
};
store = new loop.store.RoomStore(dispatcher, {
mozLoop: fakeMozLoop,
notifications: fakeNotifications
});
store.setStoreState(defaultStoreState);
});
@ -153,7 +160,7 @@ describe("loop.store.RoomStore", function () {
expect(store.getStoreState().rooms).to.have.length.of(0);
});
})
});
});
describe("#findNextAvailableRoomNumber", function() {
@ -203,9 +210,20 @@ describe("loop.store.RoomStore", function () {
};
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
store.setStoreState({pendingCreation: false, rooms: []});
});
it("should clear any existing room errors", function() {
sandbox.stub(fakeMozLoop.rooms, "create");
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
sinon.assert.calledOnce(fakeNotifications.remove);
sinon.assert.calledWithExactly(fakeNotifications.remove,
"create-room-error");
});
it("should request creation of a new room", function() {
sandbox.stub(fakeMozLoop.rooms, "create");
@ -219,17 +237,6 @@ describe("loop.store.RoomStore", function () {
});
});
it("should store any creation encountered error", function() {
var err = new Error("fake");
sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
cb(err);
});
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
expect(store.getStoreState().error).eql(err);
});
it("should switch the pendingCreation state flag to true", function() {
sandbox.stub(fakeMozLoop.rooms, "create");
@ -238,33 +245,91 @@ describe("loop.store.RoomStore", function () {
expect(store.getStoreState().pendingCreation).eql(true);
});
it("should switch the pendingCreation state flag to false once the " +
"operation is done", function() {
sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
cb(null, {roomToken: "fakeToken"});
});
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
expect(store.getStoreState().pendingCreation).eql(false);
});
it("should dispatch an OpenRoom action once the operation is done",
it("should dispatch a CreatedRoom action once the operation is done",
function() {
var dispatch = sandbox.stub(dispatcher, "dispatch");
sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
cb(null, {roomToken: "fakeToken"});
});
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.OpenRoom({
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.CreatedRoom({
roomToken: "fakeToken"
}));
});
it("should dispatch a CreateRoomError action if the operation fails",
function() {
var err = new Error("fake");
sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
cb(err);
});
store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.CreateRoomError({
error: err
}));
});
});
describe("#createdRoom", function() {
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
});
it("should switch the pendingCreation state flag to false", function() {
store.setStoreState({pendingCreation:true});
store.createdRoom(new sharedActions.CreatedRoom({
roomToken: "fakeToken"
}));
expect(store.getStoreState().pendingCreation).eql(false);
});
it("should dispatch an OpenRoom action once the operation is done",
function() {
store.createdRoom(new sharedActions.CreatedRoom({
roomToken: "fakeToken"
}));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.OpenRoom({
roomToken: "fakeToken"
}));
});
});
describe("#createRoomError", function() {
it("should switch the pendingCreation state flag to false", function() {
store.setStoreState({pendingCreation:true});
store.createRoomError({
error: new Error("fake")
});
expect(store.getStoreState().pendingCreation).eql(false);
});
it("should set a notification", function() {
store.createRoomError({
error: new Error("fake")
});
sinon.assert.calledOnce(fakeNotifications.set);
sinon.assert.calledWithMatch(fakeNotifications.set, {
id: "create-room-error",
level: "error"
});
});
});
describe("#copyRoomUrl", function() {
it("should copy the room URL", function() {
var copyString = sandbox.stub(fakeMozLoop, "copyString");

View File

@ -48,6 +48,8 @@
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
@ -56,6 +58,7 @@
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/fxOSMarketplace.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->

View File

@ -332,6 +332,26 @@ describe("loop.standaloneRoomViews", function() {
.not.eql(null);
});
});
describe("Marketplace hidden iframe", function() {
it("should set src when the store state change",
function(done) {
var marketplace = view.getDOMNode().querySelector("#marketplace");
expect(marketplace.src).to.be.equal("");
activeRoomStore.setStoreState({
marketplaceSrc: "http://market/",
onMarketplaceMessage: function () {}
});
view.forceUpdate(function() {
expect(marketplace.src).to.be.equal("http://market/");
done();
});
});
});
});
});
});

View File

@ -1169,8 +1169,7 @@ describe("loop.webapp", function() {
before(function() {
model = new loop.webapp.FxOSConversationModel({
loopToken: "fakeToken",
callerId: "callerId",
callType: "callType"
callerId: "callerId"
});
realMozActivity = window.MozActivity;
@ -1214,13 +1213,44 @@ describe("loop.webapp", function() {
beforeEach(function() {
trigger = sandbox.stub(model, "trigger");
_activityProps = undefined;
});
afterEach(function() {
trigger.restore();
});
it("Activity properties", function() {
it("Activity properties with video call", function() {
expect(_activityProps).to.not.exist;
model.setupOutgoingCall("audio-video");
expect(_activityProps).to.exist;
expect(_activityProps).eql({
name: "loop-call",
data: {
type: "loop/token",
token: "fakeToken",
callerId: "callerId",
video: true
}
});
});
it("Activity properties with audio call", function() {
expect(_activityProps).to.not.exist;
model.setupOutgoingCall("audio");
expect(_activityProps).to.exist;
expect(_activityProps).eql({
name: "loop-call",
data: {
type: "loop/token",
token: "fakeToken",
callerId: "callerId",
video: false
}
});
});
it("Activity properties by default", function() {
expect(_activityProps).to.not.exist;
model.setupOutgoingCall();
expect(_activityProps).to.exist;
@ -1230,7 +1260,7 @@ describe("loop.webapp", function() {
type: "loop/token",
token: "fakeToken",
callerId: "callerId",
callType: "callType"
video: false
}
});
});

View File

@ -44,12 +44,15 @@
<script src="../content/shared/js/store.js"></script>
<script src="../content/shared/js/roomStore.js"></script>
<script src="../content/shared/js/conversationStore.js"></script>
<script src="../content/shared/js/roomStates.js"></script>
<script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../content/shared/js/activeRoomStore.js"></script>
<script src="../content/shared/js/feedbackStore.js"></script>
<script src="../content/shared/js/feedbackViews.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../content/js/fxOSMarketplace.js"></script>
<script src="../content/js/webapp.js"></script>
<script src="../content/js/standaloneRoomViews.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>

View File

@ -867,6 +867,11 @@ inDOMUtils::CssPropertyIsValid(const nsAString& aPropertyName,
return NS_OK;
}
if (propertyID == eCSSPropertyExtra_variable) {
*_retval = true;
return NS_OK;
}
// Get a parser, parse the property.
nsCSSParser parser;
*_retval = parser.IsValueValidForProperty(propertyID, aPropertyValue);

View File

@ -74,6 +74,11 @@
property: "content",
value: "\"hello\"",
expected: true
},
{
property: "color",
value: "var(--some-kind-of-green)",
expected: true
}
];

View File

@ -349,6 +349,8 @@ var BrowserApp = {
#endif
#ifdef NIGHTLY_BUILD
WebcompatReporter.init();
Telemetry.addData("TRACKING_PROTECTION_ENABLED",
Services.prefs.getBoolPref("privacy.trackingprotection.enabled"));
#endif
} catch(ex) { console.log(ex); }
}, false);
@ -6788,15 +6790,18 @@ var IdentityHandler = {
getTrackingMode: function getTrackingMode(aState) {
if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 2);
return this.TRACKING_MODE_CONTENT_BLOCKED;
}
// Only show an indicator for loaded tracking content if the pref to block it is enabled
if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) &&
Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 1);
return this.TRACKING_MODE_CONTENT_LOADED;
}
Telemetry.addData("TRACKING_PROTECTION_SHIELD", 0);
return this.TRACKING_MODE_UNKNOWN;
},

View File

@ -254,6 +254,11 @@ this.Download.prototype = {
* Indicates whether, at this time, there is any partially downloaded data
* that can be used when restarting a failed or canceled download.
*
* Even if the download has partial data on disk, hasPartialData will be false
* if that data cannot be used to restart the download. In order to determine
* if a part file is being used which contains partial data the
* Download.target.partFilePath should be checked.
*
* This property is relevant while the download is in progress, and also if it
* failed or has been canceled. If the download has been completed
* successfully, this property is always false.
@ -263,6 +268,13 @@ this.Download.prototype = {
*/
hasPartialData: false,
/**
* Indicates whether, at this time, there is any data that has been blocked.
* Since reputation blocking takes place after the download has fully
* completed a value of true also indicates 100% of the data is present.
*/
hasBlockedData: false,
/**
* This can be set to a function that is called after other properties change.
*/
@ -353,11 +365,18 @@ this.Download.prototype = {
message: "Cannot start after finalization."}));
}
if (this.error && this.error.becauseBlockedByReputationCheck) {
return Promise.reject(new DownloadError({
message: "Cannot start after being blocked " +
"by a reputation check."}));
}
// Initialize all the status properties for a new or restarted download.
this.stopped = false;
this.canceled = false;
this.error = null;
this.hasProgress = false;
this.hasBlockedData = false;
this.progress = 0;
this.totalBytes = 0;
this.currentBytes = 0;
@ -448,20 +467,16 @@ this.Download.prototype = {
yield this.saver.execute(DS_setProgressBytes.bind(this),
DS_setProperties.bind(this));
// Check for application reputation, which requires the entire file to
// be downloaded. After that, check for the last time if the download
// has been canceled. Both cases require the target file to be deleted,
// thus we process both in the same block of code.
if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) ||
this._promiseCanceled) {
// Check for the last time if the download has been canceled.
if (this._promiseCanceled) {
try {
yield OS.File.remove(this.target.path);
} catch (ex) {
Cu.reportError(ex);
}
// If this is actually a cancellation, this exception will be changed
// in the catch block below.
throw new DownloadError({ becauseBlockedByReputationCheck: true });
// Cancellation exceptions will be changed in the catch block below.
throw new DownloadError();
}
// Update the status properties for a successful download.
@ -513,24 +528,7 @@ this.Download.prototype = {
this.speed = 0;
this._notifyChange();
if (this.succeeded) {
yield DownloadIntegration.downloadDone(this);
this._deferSucceeded.resolve();
if (this.launchWhenSucceeded) {
this.launch().then(null, Cu.reportError);
// Always schedule files to be deleted at the end of the private browsing
// mode, regardless of the value of the pref.
if (this.source.isPrivate) {
gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
new FileUtils.File(this.target.path));
} else if (Services.prefs.getBoolPref(
"browser.helperApps.deleteTempFileOnExit")) {
gExternalAppLauncher.deleteTemporaryFileOnExit(
new FileUtils.File(this.target.path));
}
}
yield this._succeed();
}
}
}
@ -541,6 +539,133 @@ this.Download.prototype = {
return currentAttempt;
},
/**
* Perform the actions necessary when a Download succeeds.
*
* @return {Promise}
* @resolves When the steps to take after success have completed.
* @rejects JavaScript exception if any of the operations failed.
*/
_succeed: Task.async(function* () {
yield DownloadIntegration.downloadDone(this);
this._deferSucceeded.resolve();
if (this.launchWhenSucceeded) {
this.launch().then(null, Cu.reportError);
// Always schedule files to be deleted at the end of the private browsing
// mode, regardless of the value of the pref.
if (this.source.isPrivate) {
gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
new FileUtils.File(this.target.path));
} else if (Services.prefs.getBoolPref(
"browser.helperApps.deleteTempFileOnExit")) {
gExternalAppLauncher.deleteTemporaryFileOnExit(
new FileUtils.File(this.target.path));
}
}
}),
/**
* When a request to unblock the download is received, contains a promise
* that will be resolved when the unblock request is completed. This property
* will then continue to hold the promise indefinitely.
*/
_promiseUnblock: null,
/**
* When a request to confirm the block of the download is received, contains
* a promise that will be resolved when cleaning up the download has
* completed. This property will then continue to hold the promise
* indefinitely.
*/
_promiseConfirmBlock: null,
/**
* Unblocks a download which had been blocked by reputation.
*
* The file will be moved out of quarantine and the download will be
* marked as succeeded.
*
* @return {Promise}
* @resolves When the Download has been unblocked and succeeded.
* @rejects JavaScript exception if any of the operations failed.
*/
unblock: function() {
if (this._promiseUnblock) {
return this._promiseUnblock;
}
if (this._promiseConfirmBlock) {
return Promise.reject(new Error(
"Download block has been confirmed, cannot unblock."));
}
if (!this.hasBlockedData) {
return Promise.reject(new Error(
"unblock may only be called on Downloads with blocked data."));
}
this._promiseUnblock = Task.spawn(function* () {
try {
yield OS.File.move(this.target.partFilePath, this.target.path);
} catch (ex) {
yield this.refresh();
this._promiseUnblock = null;
throw ex;
}
this.succeeded = true;
this.hasBlockedData = false;
this._notifyChange();
yield this._succeed();
}.bind(this));
return this._promiseUnblock;
},
/**
* Confirms that a blocked download should be cleaned up.
*
* If a download was blocked but retained on disk this method can be used
* to remove the file.
*
* @return {Promise}
* @resolves When the Download's data has been removed.
* @rejects JavaScript exception if any of the operations failed.
*/
confirmBlock: function() {
if (this._promiseConfirmBlock) {
return this._promiseConfirmBlock;
}
if (this._promiseUnblock) {
return Promise.reject(new Error(
"Download is being unblocked, cannot confirmBlock."));
}
if (!this.hasBlockedData) {
return Promise.reject(new Error(
"confirmBlock may only be called on Downloads with blocked data."));
}
this._promiseConfirmBlock = Task.spawn(function* () {
try {
yield OS.File.remove(this.target.partFilePath);
} catch (ex) {
yield this.refresh();
this._promiseConfirmBlock = null;
throw ex;
}
this.hasBlockedData = false;
this._notifyChange();
}.bind(this));
return this._promiseConfirmBlock;
},
/*
* Launches the file after download has completed. This can open
* the file with the default application for the target MIME type
@ -772,21 +897,34 @@ this.Download.prototype = {
}
// Update the current progress from disk if we retained partial data.
if (this.hasPartialData && this.target.partFilePath) {
let stat = yield OS.File.stat(this.target.partFilePath);
if ((this.hasPartialData || this.hasBlockedData) &&
this.target.partFilePath) {
// Ignore the result if the state has changed meanwhile.
if (!this.stopped || this._finalized) {
return;
try {
let stat = yield OS.File.stat(this.target.partFilePath);
// Ignore the result if the state has changed meanwhile.
if (!this.stopped || this._finalized) {
return;
}
// Update the bytes transferred and the related progress properties.
this.currentBytes = stat.size;
if (this.totalBytes > 0) {
this.hasProgress = true;
this.progress = Math.floor(this.currentBytes /
this.totalBytes * 100);
}
} catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
// Ignore the result if the state has changed meanwhile.
if (!this.stopped || this._finalized) {
return;
}
this.hasBlockedData = false;
this.hasPartialData = false;
}
// Update the bytes transferred and the related progress properties.
this.currentBytes = stat.size;
if (this.totalBytes > 0) {
this.hasProgress = true;
this.progress = Math.floor(this.currentBytes /
this.totalBytes * 100);
}
this._notifyChange();
}
}.bind(this)).then(null, Cu.reportError);
@ -984,6 +1122,7 @@ const kPlainSerializableDownloadProperties = [
"canceled",
"totalBytes",
"hasPartialData",
"hasBlockedData",
"tryToKeepPartialData",
"launcherPath",
"launchWhenSucceeded",
@ -1818,11 +1957,6 @@ this.DownloadCopySaver.prototype = {
// background file saver that the operation can finish. If the
// data transfer failed, the saver has been already stopped.
if (Components.isSuccessCode(aStatusCode)) {
if (partFilePath) {
// Move to the final target if we were using a part file.
backgroundFileSaver.setTarget(
new FileUtils.File(targetPath), false);
}
backgroundFileSaver.finish(Cr.NS_OK);
}
}
@ -1855,6 +1989,8 @@ this.DownloadCopySaver.prototype = {
// We will wait on this promise in case no error occurred while setting
// up the chain of objects for the download.
yield deferSaveComplete.promise;
yield this._checkReputationAndMove();
} catch (ex) {
// Ensure we always remove the placeholder for the final target file on
// failure, independently of which code path failed. In some cases, the
@ -1876,6 +2012,47 @@ this.DownloadCopySaver.prototype = {
}.bind(this));
},
/**
* Perform the reputation check and cleanup the downloaded data if required.
* If the download passes the reputation check and is using a part file we
* will move it to the target path since reputation checking is the final
* step in the saver.
*
* @return {Promise}
* @resolves When the reputation check and cleanup is complete.
* @rejects DownloadError if the download should be blocked.
*/
_checkReputationAndMove: Task.async(function* () {
let download = this.download;
let targetPath = this.download.target.path;
let partFilePath = this.download.target.partFilePath;
if (yield DownloadIntegration.shouldBlockForReputationCheck(download)) {
download.progress = 100;
download.hasPartialData = false;
// We will remove the potentially dangerous file if instructed by
// DownloadIntegration. We will always remove the file when the
// download did not use a partial file path, meaning it
// currently has its final filename.
if (!DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
try {
yield OS.File.remove(partFilePath || targetPath);
} catch (ex) {
Cu.reportError(ex);
}
} else {
download.hasBlockedData = true;
}
throw new DownloadError({ becauseBlockedByReputationCheck: true });
}
if (partFilePath) {
yield OS.File.move(partFilePath, targetPath);
}
}),
/**
* Implements "DownloadSaver.cancel".
*/
@ -2171,13 +2348,12 @@ this.DownloadLegacySaver.prototype = {
// to its final target path when the download succeeds. In this case,
// an empty ".part" file is created even if no data was received from
// the source.
if (this.download.target.partFilePath) {
yield OS.File.move(this.download.target.partFilePath,
this.download.target.path);
} else {
// The download implementation may not have created the target file if
// no data was received from the source. In this case, ensure that an
// empty file is created as expected.
//
// When no ".part" file path is provided the download implementation may
// not have created the target file (if no data was received from the
// source). In this case, ensure that an empty file is created as
// expected.
if (!this.download.target.partFilePath) {
try {
// This atomic operation is more efficient than an existence check.
let file = yield OS.File.open(this.download.target.path,
@ -2185,6 +2361,9 @@ this.DownloadLegacySaver.prototype = {
yield file.close();
} catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { }
}
yield this._checkReputationAndMove();
} catch (ex) {
// Ensure we always remove the final target file on failure,
// independently of which code path failed. In some cases, the
@ -2217,6 +2396,10 @@ this.DownloadLegacySaver.prototype = {
}.bind(this));
},
_checkReputationAndMove: function () {
return DownloadCopySaver.prototype._checkReputationAndMove.call(this);
},
/**
* Implements "DownloadSaver.cancel".
*/

View File

@ -152,6 +152,7 @@ this.DownloadIntegration = {
dontCheckApplicationReputation: true,
#endif
shouldBlockInTestForApplicationReputation: false,
shouldKeepBlockedDataInTest: false,
dontOpenFileAndFolder: false,
downloadDoneCalled: false,
_deferTestOpenFile: null,
@ -174,6 +175,30 @@ this.DownloadIntegration = {
return (this._testMode = mode);
},
/**
* Returns whether data for blocked downloads should be kept on disk.
* Implementations which support unblocking downloads may return true to
* keep the blocked download on disk until its fate is decided.
*
* If a download is blocked and the partial data is kept the Download's
* 'hasBlockedData' property will be true. In this state Download.unblock()
* or Download.confirmBlock() may be used to either unblock the download or
* remove the downloaded data respectively.
*
* Even if shouldKeepBlockedData returns true, if the download did not use a
* partFile the blocked data will be removed - preventing the complete
* download from existing on disk with its final filename.
*
* @return boolean True if data should be kept.
*/
shouldKeepBlockedData: function() {
if (this.shouldBlockInTestForApplicationReputation) {
return this.shouldKeepBlockedDataInTest;
}
return false;
},
/**
* Performs initialization of the list of persistent downloads, before its
* first use by the host application. This function may be called only once

View File

@ -1547,42 +1547,231 @@ add_task(function test_getSha256Hash()
}
});
/**
* Checks that application reputation blocks the download and the target file
* does not exist.
* Create a download which will be reputation blocked.
*
* @param options
* {
* keepPartialData: bool,
* keepBlockedData: bool,
* }
* @return {Promise}
* @resolves The reputation blocked download.
* @rejects JavaScript exception.
*/
add_task(function test_blocked_applicationReputation()
{
let promiseBlockedDownload = Task.async(function* (options) {
function cleanup() {
DownloadIntegration.shouldBlockInTestForApplicationReputation = false;
DownloadIntegration.shouldKeepBlockedDataInTest = false;
}
do_register_cleanup(cleanup);
let {keepPartialData, keepBlockedData} = options;
DownloadIntegration.shouldBlockInTestForApplicationReputation = true;
DownloadIntegration.shouldKeepBlockedDataInTest = keepBlockedData;
let download;
try {
if (!gUseLegacySaver) {
// When testing DownloadCopySaver, we want to check that the promise
// returned by the "start" method is rejected.
if (keepPartialData) {
download = yield promiseStartDownload_tryToKeepPartialData();
continueResponses();
} else if (gUseLegacySaver) {
download = yield promiseStartLegacyDownload();
} else {
download = yield promiseNewDownload();
yield download.start();
} else {
// When testing DownloadLegacySaver, we cannot be sure whether we are
// testing the promise returned by the "start" method or we are testing
// the "error" property checked by promiseDownloadStopped. This happens
// because we don't have control over when the download is started.
download = yield promiseStartLegacyDownload();
yield promiseDownloadStopped(download);
do_throw("The download should have blocked.");
}
yield promiseDownloadStopped(download);
do_throw("The download should have blocked.");
} catch (ex if ex instanceof Downloads.Error && ex.becauseBlocked) {
do_check_true(ex.becauseBlockedByReputationCheck);
do_check_true(download.error.becauseBlockedByReputationCheck);
}
do_check_true(download.stopped);
do_check_false(download.succeeded);
do_check_false(yield OS.File.exists(download.target.path));
cleanup();
return download;
});
/**
* Checks that application reputation blocks the download and the target file
* does not exist.
*/
add_task(function test_blocked_applicationReputation()
{
let download = yield promiseBlockedDownload({
keepPartialData: false,
keepBlockedData: false,
});
// Now that the download is blocked, the target file should not exist.
do_check_false(yield OS.File.exists(download.target.path));
cleanup();
// There should also be no blocked data in this case
do_check_false(download.hasBlockedData);
});
/**
* Checks that application reputation blocks the download but maintains the
* blocked data, which will be deleted when the block is confirmed.
*/
add_task(function test_blocked_applicationReputation_confirmBlock()
{
let download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
do_check_true(download.hasBlockedData);
do_check_true(yield OS.File.exists(download.target.partFilePath));
yield download.confirmBlock();
// After confirming the block the download should be in a failed state and
// have no downloaded data left on disk.
do_check_true(download.stopped);
do_check_false(download.succeeded);
do_check_false(download.hasBlockedData);
do_check_false(yield OS.File.exists(download.target.partFilePath));
do_check_false(yield OS.File.exists(download.target.path));
});
/**
* Checks that application reputation blocks the download but maintains the
* blocked data, which will be used to complete the download when unblocking.
*/
add_task(function test_blocked_applicationReputation_unblock()
{
let download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
do_check_true(download.hasBlockedData);
do_check_true(yield OS.File.exists(download.target.partFilePath));
yield download.unblock();
// After unblocking the download should have succeeded and be
// present at the final path.
do_check_true(download.stopped);
do_check_true(download.succeeded);
do_check_false(download.hasBlockedData);
do_check_false(yield OS.File.exists(download.target.partFilePath));
do_check_true(yield OS.File.exists(download.target.path));
// The only indication the download was previously blocked is the
// existence of the error, so we make sure it's still set.
do_check_true(download.error instanceof Downloads.Error);
do_check_true(download.error.becauseBlocked);
do_check_true(download.error.becauseBlockedByReputationCheck);
});
/**
* Check that calling cancel on a blocked download will not cause errors
*/
add_task(function test_blocked_applicationReputation_cancel()
{
let download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
// This call should succeed on a blocked download.
yield download.cancel();
// Calling cancel should not have changed the current state, the download
// should still be blocked.
do_check_true(download.error.becauseBlockedByReputationCheck);
do_check_true(download.stopped);
do_check_false(download.succeeded);
do_check_true(download.hasBlockedData);
});
/**
* Checks that unblock and confirmBlock cannot race on a blocked download
*/
add_task(function test_blocked_applicationReputation_decisionRace()
{
let download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
let unblockPromise = download.unblock();
let confirmBlockPromise = download.confirmBlock();
yield confirmBlockPromise.then(() => {
do_throw("confirmBlock should have failed.");
}, () => {});
yield unblockPromise;
// After unblocking the download should have succeeded and be
// present at the final path.
do_check_true(download.stopped);
do_check_true(download.succeeded);
do_check_false(download.hasBlockedData);
do_check_false(yield OS.File.exists(download.target.partFilePath));
do_check_true(yield OS.File.exists(download.target.path));
download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
confirmBlockPromise = download.confirmBlock();
unblockPromise = download.unblock();
yield unblockPromise.then(() => {
do_throw("unblock should have failed.");
}, () => {});
yield confirmBlockPromise;
// After confirming the block the download should be in a failed state and
// have no downloaded data left on disk.
do_check_true(download.stopped);
do_check_false(download.succeeded);
do_check_false(download.hasBlockedData);
do_check_false(yield OS.File.exists(download.target.partFilePath));
do_check_false(yield OS.File.exists(download.target.path));
});
/**
* Checks that unblocking a blocked download fails if the blocked data has been
* removed.
*/
add_task(function test_blocked_applicationReputation_unblock()
{
let download = yield promiseBlockedDownload({
keepPartialData: true,
keepBlockedData: true,
});
do_check_true(download.hasBlockedData);
do_check_true(yield OS.File.exists(download.target.partFilePath));
// Remove the blocked data without telling the download.
yield OS.File.remove(download.target.partFilePath);
let unblockPromise = download.unblock();
yield unblockPromise.then(() => {
do_throw("unblock should have failed.");
}, () => {});
// Even though unblocking failed the download state should have been updated
// to reflect the lack of blocked data.
do_check_false(download.hasBlockedData);
do_check_true(download.stopped);
do_check_false(download.succeeded);
});
/**