mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to central, a=merge
This commit is contained in:
commit
5428a9a5a0
@ -1566,6 +1566,8 @@ var gBrowserInit = {
|
||||
|
||||
TabletModeUpdater.uninit();
|
||||
|
||||
gTabletModePageCounter.finish();
|
||||
|
||||
BrowserOnClick.uninit();
|
||||
|
||||
DevEdition.uninit();
|
||||
@ -4443,6 +4445,7 @@ var XULBrowserWindow = {
|
||||
BookmarkingUI.onLocationChange();
|
||||
SocialUI.updateState(location);
|
||||
UITour.onLocationChange(location);
|
||||
gTabletModePageCounter.inc();
|
||||
}
|
||||
|
||||
// Utility functions for disabling find
|
||||
@ -5440,6 +5443,29 @@ var TabletModeUpdater = {
|
||||
},
|
||||
};
|
||||
|
||||
var gTabletModePageCounter = {
|
||||
inc() {
|
||||
if (!AppConstants.isPlatformAndVersionAtLeast("win", "10.0")) {
|
||||
this.inc = () => {};
|
||||
return;
|
||||
}
|
||||
this.inc = this._realInc;
|
||||
this.inc();
|
||||
},
|
||||
|
||||
_desktopCount: 0,
|
||||
_tabletCount: 0,
|
||||
_realInc() {
|
||||
let inTabletMode = document.documentElement.hasAttribute("tabletmode");
|
||||
this[inTabletMode ? "_tabletCount" : "_desktopCount"]++;
|
||||
},
|
||||
|
||||
finish() {
|
||||
Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD").add("tablet", this._tabletCount);
|
||||
Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD").add("desktop", this._desktopCount);
|
||||
},
|
||||
};
|
||||
|
||||
#ifdef CAN_DRAW_IN_TITLEBAR
|
||||
function updateTitlebarDisplay() {
|
||||
|
||||
|
@ -60,7 +60,6 @@ function CustomizeMode(aWindow) {
|
||||
// to the user when in customizing mode.
|
||||
this.visiblePalette = this.document.getElementById(kPaletteId);
|
||||
this.paletteEmptyNotice = this.document.getElementById("customization-empty");
|
||||
this.paletteSpacer = this.document.getElementById("customization-spacer");
|
||||
this.tipPanel = this.document.getElementById("customization-tipPanel");
|
||||
if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
|
||||
let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
|
||||
@ -287,7 +286,6 @@ CustomizeMode.prototype = {
|
||||
this.visiblePalette.clientTop;
|
||||
this.visiblePalette.setAttribute("showing", "true");
|
||||
}, 0);
|
||||
this.paletteSpacer.hidden = true;
|
||||
this._updateEmptyPaletteNotice();
|
||||
|
||||
this.swatchForTheme(document);
|
||||
@ -366,7 +364,6 @@ CustomizeMode.prototype = {
|
||||
let documentElement = document.documentElement;
|
||||
|
||||
// Hide the palette before starting the transition for increased perf.
|
||||
this.paletteSpacer.hidden = false;
|
||||
this.visiblePalette.hidden = true;
|
||||
this.visiblePalette.removeAttribute("showing");
|
||||
this.paletteEmptyNotice.hidden = true;
|
||||
|
@ -254,15 +254,13 @@ body {
|
||||
/* See .room-entry-context-item for the margin/size reductions.
|
||||
* An extra 40px to make space for the call button and chevron. */
|
||||
width: calc(100% - 1rem - 56px);
|
||||
|
||||
}
|
||||
|
||||
.room-list > .room-entry.room-active > h2 {
|
||||
.room-list > .room-entry.room-active:not(.room-opened) > h2 {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.room-list > .room-entry:hover {
|
||||
.room-list > .room-entry:not(.room-opened):hover {
|
||||
background: #dbf7ff;
|
||||
}
|
||||
|
||||
@ -417,7 +415,7 @@ html[dir="rtl"] .room-entry-context-actions > .dropdown-menu {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.room-entry:hover .room-entry-context-item {
|
||||
.room-entry:not(.room-opened):hover .room-entry-context-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -362,10 +362,14 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
/**
|
||||
* Room list entry.
|
||||
*
|
||||
* Active Room means there are participants in the room.
|
||||
* Opened Room means the user is in the room.
|
||||
*/
|
||||
var RoomEntry = React.createClass({displayName: "RoomEntry",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isOpenedRoom: React.PropTypes.bool.isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
|
||||
},
|
||||
@ -418,7 +422,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
render: function() {
|
||||
var roomClasses = React.addons.classSet({
|
||||
"room-entry": true,
|
||||
"room-active": this._isActive()
|
||||
"room-active": this._isActive(),
|
||||
"room-opened": this.props.isOpenedRoom
|
||||
});
|
||||
|
||||
var roomTitle = this.props.room.decryptedContext.roomName ||
|
||||
@ -427,8 +432,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: roomClasses,
|
||||
onClick: this.handleClickEntry,
|
||||
onMouseLeave: this._handleMouseOut,
|
||||
onClick: this.props.isOpenedRoom ? null : this.handleClickEntry,
|
||||
onMouseLeave: this.props.isOpenedRoom ? null : this._handleMouseOut,
|
||||
ref: "roomEntry"},
|
||||
React.createElement("h2", null,
|
||||
roomTitle
|
||||
@ -436,15 +441,17 @@ loop.panel = (function(_, mozL10n) {
|
||||
React.createElement(RoomEntryContextItem, {
|
||||
mozLoop: this.props.mozLoop,
|
||||
roomUrls: this.props.room.decryptedContext.urls}),
|
||||
React.createElement(RoomEntryContextButtons, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
eventPosY: this.state.eventPosY,
|
||||
handleClickEntry: this.handleClickEntry,
|
||||
handleContextChevronClick: this.handleContextChevronClick,
|
||||
ref: "contextActions",
|
||||
room: this.props.room,
|
||||
showMenu: this.state.showMenu,
|
||||
toggleDropdownMenu: this.toggleDropdownMenu})
|
||||
this.props.isOpenedRoom ? null :
|
||||
React.createElement(RoomEntryContextButtons, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
eventPosY: this.state.eventPosY,
|
||||
handleClickEntry: this.handleClickEntry,
|
||||
handleContextChevronClick: this.handleContextChevronClick,
|
||||
ref: "contextActions",
|
||||
room: this.props.room,
|
||||
showMenu: this.state.showMenu,
|
||||
toggleDropdownMenu: this.toggleDropdownMenu})
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -719,12 +726,20 @@ loop.panel = (function(_, mozL10n) {
|
||||
return (
|
||||
React.createElement("div", {className: "rooms"},
|
||||
this._renderNewRoomButton(),
|
||||
React.createElement("h1", null, mozL10n.get("rooms_list_recent_conversations")),
|
||||
React.createElement("h1", null, mozL10n.get(this.state.openedRoom === null ?
|
||||
"rooms_list_recently_browsed" :
|
||||
"rooms_list_currently_browsing")),
|
||||
React.createElement("div", {className: "room-list"},
|
||||
this.state.rooms.map(function(room, i) {
|
||||
if (this.state.openedRoom !== null &&
|
||||
room.roomToken !== this.state.openedRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement(RoomEntry, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
isOpenedRoom: room.roomToken === this.state.openedRoom,
|
||||
key: room.roomToken,
|
||||
mozLoop: this.props.mozLoop,
|
||||
room: room})
|
||||
@ -927,8 +942,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
clearOnDocumentHidden: true,
|
||||
notifications: this.props.notifications}),
|
||||
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
|
||||
mozLoop: this.props.mozLoop,
|
||||
store: this.props.roomStore}),
|
||||
mozLoop: this.props.mozLoop,
|
||||
store: this.props.roomStore}),
|
||||
React.createElement("div", {className: "footer"},
|
||||
React.createElement("div", {className: "user-details"},
|
||||
React.createElement(AccountLink, {fxAEnabled: this.props.mozLoop.fxAEnabled,
|
||||
|
@ -362,10 +362,14 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
/**
|
||||
* Room list entry.
|
||||
*
|
||||
* Active Room means there are participants in the room.
|
||||
* Opened Room means the user is in the room.
|
||||
*/
|
||||
var RoomEntry = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isOpenedRoom: React.PropTypes.bool.isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
|
||||
},
|
||||
@ -418,7 +422,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
render: function() {
|
||||
var roomClasses = React.addons.classSet({
|
||||
"room-entry": true,
|
||||
"room-active": this._isActive()
|
||||
"room-active": this._isActive(),
|
||||
"room-opened": this.props.isOpenedRoom
|
||||
});
|
||||
|
||||
var roomTitle = this.props.room.decryptedContext.roomName ||
|
||||
@ -427,8 +432,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
|
||||
return (
|
||||
<div className={roomClasses}
|
||||
onClick={this.handleClickEntry}
|
||||
onMouseLeave={this._handleMouseOut}
|
||||
onClick={this.props.isOpenedRoom ? null : this.handleClickEntry}
|
||||
onMouseLeave={this.props.isOpenedRoom ? null : this._handleMouseOut}
|
||||
ref="roomEntry">
|
||||
<h2>
|
||||
{roomTitle}
|
||||
@ -436,15 +441,17 @@ loop.panel = (function(_, mozL10n) {
|
||||
<RoomEntryContextItem
|
||||
mozLoop={this.props.mozLoop}
|
||||
roomUrls={this.props.room.decryptedContext.urls} />
|
||||
<RoomEntryContextButtons
|
||||
dispatcher={this.props.dispatcher}
|
||||
eventPosY={this.state.eventPosY}
|
||||
handleClickEntry={this.handleClickEntry}
|
||||
handleContextChevronClick={this.handleContextChevronClick}
|
||||
ref="contextActions"
|
||||
room={this.props.room}
|
||||
showMenu={this.state.showMenu}
|
||||
toggleDropdownMenu={this.toggleDropdownMenu} />
|
||||
{this.props.isOpenedRoom ? null :
|
||||
<RoomEntryContextButtons
|
||||
dispatcher={this.props.dispatcher}
|
||||
eventPosY={this.state.eventPosY}
|
||||
handleClickEntry={this.handleClickEntry}
|
||||
handleContextChevronClick={this.handleContextChevronClick}
|
||||
ref="contextActions"
|
||||
room={this.props.room}
|
||||
showMenu={this.state.showMenu}
|
||||
toggleDropdownMenu={this.toggleDropdownMenu} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -719,12 +726,20 @@ loop.panel = (function(_, mozL10n) {
|
||||
return (
|
||||
<div className="rooms">
|
||||
{this._renderNewRoomButton()}
|
||||
<h1>{mozL10n.get("rooms_list_recent_conversations")}</h1>
|
||||
<h1>{mozL10n.get(this.state.openedRoom === null ?
|
||||
"rooms_list_recently_browsed" :
|
||||
"rooms_list_currently_browsing")}</h1>
|
||||
<div className="room-list">{
|
||||
this.state.rooms.map(function(room, i) {
|
||||
if (this.state.openedRoom !== null &&
|
||||
room.roomToken !== this.state.openedRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomEntry
|
||||
dispatcher={this.props.dispatcher}
|
||||
isOpenedRoom={room.roomToken === this.state.openedRoom}
|
||||
key={room.roomToken}
|
||||
mozLoop={this.props.mozLoop}
|
||||
room={room} />
|
||||
@ -927,8 +942,8 @@ loop.panel = (function(_, mozL10n) {
|
||||
clearOnDocumentHidden={true}
|
||||
notifications={this.props.notifications} />
|
||||
<RoomList dispatcher={this.props.dispatcher}
|
||||
mozLoop={this.props.mozLoop}
|
||||
store={this.props.roomStore} />
|
||||
mozLoop={this.props.mozLoop}
|
||||
store={this.props.roomStore} />
|
||||
<div className="footer">
|
||||
<div className="user-details">
|
||||
<AccountLink fxAEnabled={this.props.mozLoop.fxAEnabled}
|
||||
|
@ -13,6 +13,7 @@ describe("loop.panel", function() {
|
||||
var sandbox, notifications;
|
||||
var fakeXHR, fakeWindow, fakeMozLoop, fakeEvent;
|
||||
var requests = [];
|
||||
var roomData, roomData2, roomList, roomName;
|
||||
var mozL10nGetSpy;
|
||||
|
||||
beforeEach(function() {
|
||||
@ -73,6 +74,45 @@ describe("loop.panel", function() {
|
||||
userProfile: null
|
||||
};
|
||||
|
||||
roomName = "First Room Name";
|
||||
roomData = {
|
||||
roomToken: "QzBbvGmIZWU",
|
||||
roomUrl: "http://sample/QzBbvGmIZWU",
|
||||
decryptedContext: {
|
||||
roomName: roomName
|
||||
},
|
||||
maxSize: 2,
|
||||
participants: [{
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}],
|
||||
ctime: 1405517418
|
||||
};
|
||||
|
||||
roomData2 = {
|
||||
roomToken: "QzBbvlmIZWU",
|
||||
roomUrl: "http://sample/QzBbvlmIZWU",
|
||||
decryptedContext: {
|
||||
roomName: "Second Room Name"
|
||||
},
|
||||
maxSize: 2,
|
||||
participants: [{
|
||||
displayName: "Bill",
|
||||
account: "bill@example.com",
|
||||
roomConnectionId: "2a1737a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Bob",
|
||||
roomConnectionId: "781f212b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}],
|
||||
ctime: 1405517417
|
||||
};
|
||||
|
||||
roomList = [new loop.store.Room(roomData), new loop.store.Room(roomData2)];
|
||||
|
||||
document.mozL10n.initialize(navigator.mozLoop);
|
||||
sandbox.stub(document.mozL10n, "get").returns("Fake title");
|
||||
});
|
||||
@ -538,25 +578,10 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
describe("loop.panel.RoomEntry", function() {
|
||||
var dispatcher, roomData;
|
||||
var dispatcher;
|
||||
|
||||
beforeEach(function() {
|
||||
dispatcher = new loop.Dispatcher();
|
||||
roomData = {
|
||||
roomToken: "QzBbvGmIZWU",
|
||||
roomUrl: "http://sample/QzBbvGmIZWU",
|
||||
decryptedContext: {
|
||||
roomName: "Second Room Name"
|
||||
},
|
||||
maxSize: 2,
|
||||
participants: [
|
||||
{ displayName: "Alexis", account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
|
||||
{ displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
|
||||
],
|
||||
ctime: 1405517418
|
||||
};
|
||||
});
|
||||
|
||||
function mountRoomEntry(props) {
|
||||
@ -576,7 +601,10 @@ describe("loop.panel", function() {
|
||||
// the actions we are triggering.
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
view = mountRoomEntry({ room: new loop.store.Room(roomData) });
|
||||
view = mountRoomEntry({
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
});
|
||||
|
||||
// XXX Current version of React cannot use TestUtils.Simulate, please
|
||||
@ -618,6 +646,7 @@ describe("loop.panel", function() {
|
||||
|
||||
roomEntry = mountRoomEntry({
|
||||
deleteRoom: sandbox.stub(),
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
});
|
||||
@ -648,6 +677,18 @@ describe("loop.panel", function() {
|
||||
|
||||
sinon.assert.calledOnce(fakeWindow.close);
|
||||
});
|
||||
|
||||
it("should not dispatch an OpenRoom action when button is clicked if room is already opened", function() {
|
||||
roomEntry = mountRoomEntry({
|
||||
deleteRoom: sandbox.stub(),
|
||||
isOpenedRoom: true,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
|
||||
TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
|
||||
|
||||
sinon.assert.notCalled(dispatcher.dispatch);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -656,6 +697,7 @@ describe("loop.panel", function() {
|
||||
|
||||
function mountEntryForContext() {
|
||||
return mountRoomEntry({
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
}
|
||||
@ -716,6 +758,7 @@ describe("loop.panel", function() {
|
||||
|
||||
roomEntry = mountRoomEntry({
|
||||
dispatcher: dispatcher,
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
roomEntryNode = roomEntry.getDOMNode();
|
||||
@ -727,6 +770,7 @@ describe("loop.panel", function() {
|
||||
it("should update room name", function() {
|
||||
var roomEntry = mountRoomEntry({
|
||||
dispatcher: dispatcher,
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
var updatedRoom = new loop.store.Room(_.extend({}, roomData, {
|
||||
@ -749,6 +793,7 @@ describe("loop.panel", function() {
|
||||
beforeEach(function() {
|
||||
roomEntry = mountRoomEntry({
|
||||
dispatcher: dispatcher,
|
||||
isOpenedRoom: false,
|
||||
room: new loop.store.Room(roomData)
|
||||
});
|
||||
});
|
||||
@ -808,7 +853,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
describe("loop.panel.RoomList", function() {
|
||||
var roomStore, dispatcher, fakeEmail, dispatch, roomData;
|
||||
var roomStore, dispatcher, fakeEmail, dispatch;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeEmail = "fakeEmail@example.com";
|
||||
@ -817,6 +862,7 @@ describe("loop.panel", function() {
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
roomStore.setStoreState({
|
||||
openedRoom: null,
|
||||
pendingCreation: false,
|
||||
pendingInitialRetrieval: false,
|
||||
rooms: [],
|
||||
@ -824,24 +870,6 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
dispatch = sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
roomData = {
|
||||
roomToken: "QzBbvGmIZWU",
|
||||
roomUrl: "http://sample/QzBbvGmIZWU",
|
||||
decryptedContext: {
|
||||
roomName: "Second Room Name"
|
||||
},
|
||||
maxSize: 2,
|
||||
participants: [{
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}],
|
||||
ctime: 1405517418
|
||||
};
|
||||
});
|
||||
|
||||
function createTestComponent() {
|
||||
@ -896,6 +924,27 @@ describe("loop.panel", function() {
|
||||
|
||||
expect(view.getDOMNode().querySelectorAll(".room-list-loading").length).to.eql(1);
|
||||
});
|
||||
|
||||
it("should show multiple rooms in list with no opened room", function() {
|
||||
roomStore.setStoreState({ rooms: roomList });
|
||||
|
||||
var view = createTestComponent();
|
||||
|
||||
var node = view.getDOMNode();
|
||||
expect(node.querySelectorAll(".room-opened").length).to.eql(0);
|
||||
expect(node.querySelectorAll(".room-entry").length).to.eql(2);
|
||||
});
|
||||
|
||||
it("should only show the opened room you're in when you're in a room", function() {
|
||||
roomStore.setStoreState({ rooms: roomList, openedRoom: roomList[0].roomToken });
|
||||
|
||||
var view = createTestComponent();
|
||||
|
||||
var node = view.getDOMNode();
|
||||
expect(node.querySelectorAll(".room-opened").length).to.eql(1);
|
||||
expect(node.querySelectorAll(".room-entry").length).to.eql(1);
|
||||
expect(node.querySelectorAll(".room-opened h2")[0].textContent).to.equal(roomName);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.NewRoomView", function() {
|
||||
@ -1074,7 +1123,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
describe("RoomEntryContextButtons", function() {
|
||||
var view, dispatcher, roomData;
|
||||
var view, dispatcher;
|
||||
|
||||
function createTestComponent(extraProps) {
|
||||
var props = _.extend({
|
||||
@ -1091,24 +1140,6 @@ describe("loop.panel", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
roomData = {
|
||||
roomToken: "QzBbvGmIZWU",
|
||||
roomUrl: "http://sample/QzBbvGmIZWU",
|
||||
decryptedContext: {
|
||||
roomName: "Second Room Name"
|
||||
},
|
||||
maxSize: 2,
|
||||
participants: [{
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}],
|
||||
ctime: 1405517418
|
||||
};
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
|
@ -524,12 +524,6 @@ BrowserGlue.prototype = {
|
||||
case "autocomplete-did-enter-text":
|
||||
this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
|
||||
break;
|
||||
case "tablet-mode-change":
|
||||
if (data == "tablet-mode") {
|
||||
Services.telemetry.getHistogramById("FX_TABLET_MODE_USED_DURING_SESSION")
|
||||
.add(1);
|
||||
}
|
||||
break;
|
||||
case "test-initialize-sanitizer":
|
||||
this._sanitizer.onStartup();
|
||||
break;
|
||||
@ -640,7 +634,6 @@ BrowserGlue.prototype = {
|
||||
os.addObserver(this, "flash-plugin-hang", false);
|
||||
os.addObserver(this, "xpi-signature-changed", false);
|
||||
os.addObserver(this, "autocomplete-did-enter-text", false);
|
||||
os.addObserver(this, "tablet-mode-change", false);
|
||||
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
|
||||
ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
|
||||
@ -699,7 +692,6 @@ BrowserGlue.prototype = {
|
||||
os.removeObserver(this, "flash-plugin-hang");
|
||||
os.removeObserver(this, "xpi-signature-changed");
|
||||
os.removeObserver(this, "autocomplete-did-enter-text");
|
||||
os.removeObserver(this, "tablet-mode-change");
|
||||
},
|
||||
|
||||
_onAppDefaults: function BG__onAppDefaults() {
|
||||
@ -1080,13 +1072,6 @@ BrowserGlue.prototype = {
|
||||
let scaling = aWindow.devicePixelRatio * 100;
|
||||
Services.telemetry.getHistogramById(SCALING_PROBE_NAME).add(scaling);
|
||||
}
|
||||
|
||||
#ifdef XP_WIN
|
||||
if (WindowsUIUtils.inTabletMode) {
|
||||
Services.telemetry.getHistogramById("FX_TABLET_MODE_USED_DURING_SESSION")
|
||||
.add(1);
|
||||
}
|
||||
#endif
|
||||
},
|
||||
|
||||
// the first browser window has finished initializing
|
||||
|
@ -183,9 +183,12 @@ tour_label=Tour
|
||||
## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
|
||||
rooms_default_room_name_template=Conversation {{conversationLabel}}
|
||||
rooms_leave_button_label=Leave
|
||||
## LOCALIZATION NOTE (rooms_list_recent_conversations): String is in all caps
|
||||
## LOCALIZATION NOTE (rooms_list_recently_browsed): String is in all caps
|
||||
## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
|
||||
rooms_list_recent_conversations=RECENT CONVERSATIONS
|
||||
rooms_list_recently_browsed=RECENTLY BROWSED
|
||||
## LOCALIZATION NOTE (rooms_list_currently_browsing): String is in all caps
|
||||
## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
|
||||
rooms_list_currently_browsing=CURRENTLY BROWSING
|
||||
rooms_change_failed_label=Conversation cannot be updated
|
||||
rooms_panel_title=Choose a conversation or start a new one
|
||||
rooms_room_full_label=There are already two people in this conversation.
|
||||
|
@ -85,7 +85,11 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
||||
}
|
||||
|
||||
.search-panel-current-engine {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-panel-tree {
|
||||
border-top: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.search-panel-header {
|
||||
@ -95,10 +99,6 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
||||
color: MenuText;
|
||||
}
|
||||
|
||||
.search-panel-tree[collapsed=true] + .search-panel-header {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.search-panel-header > label {
|
||||
margin-top: 2px !important;
|
||||
margin-bottom: 1px !important;
|
||||
|
@ -110,7 +110,11 @@
|
||||
}
|
||||
|
||||
.search-panel-current-engine {
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-panel-tree {
|
||||
border-top: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.search-panel-header {
|
||||
@ -123,10 +127,6 @@
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.search-panel-tree[collapsed=true] + .search-panel-header {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.search-panel-header > label {
|
||||
margin-top: 2px !important;
|
||||
margin-bottom: 2px !important;
|
||||
|
@ -42,8 +42,10 @@
|
||||
<use id="expand-active" xlink:href="#expand-shape"/>
|
||||
<use id="expand-disabled" xlink:href="#expand-shape"/>
|
||||
<use id="expand-hover" xlink:href="#expand-shape"/>
|
||||
<use id="expand-white" xlink:href="#expand-shape"/>
|
||||
<use id="minimize" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-active" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-disabled" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-hover" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-white" xlink:href="#minimize-shape"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
@ -105,6 +105,14 @@ chatbar > chatbox > .chat-titlebar > .chat-swap-button {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
chatbox[src^="about:loopconversation#"] .chat-minimize-button {
|
||||
list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-white");
|
||||
}
|
||||
|
||||
chatbox[src^="about:loopconversation#"] .chat-swap-button {
|
||||
list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-white");
|
||||
}
|
||||
|
||||
.chat-loop-hangup {
|
||||
list-style-image: url("chrome://browser/skin/social/chat-icons.svg#exit-white");
|
||||
background-color: #d13f1a;
|
||||
@ -129,6 +137,10 @@ chatbar > chatbox > .chat-titlebar > .chat-swap-button {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
chatbox[src^="about:loopconversation#"] .chat-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-titlebar {
|
||||
height: 26px;
|
||||
min-height: 26px;
|
||||
@ -147,6 +159,11 @@ chatbar > chatbox > .chat-titlebar > .chat-swap-button {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
chatbox[src^="about:loopconversation#"] > .chat-titlebar {
|
||||
background-color: #00a9dc;
|
||||
border-color: #00a9dc;
|
||||
}
|
||||
|
||||
.chat-titlebar > .notification-anchor-icon {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
|
@ -118,7 +118,11 @@
|
||||
}
|
||||
|
||||
.search-panel-current-engine {
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-panel-tree {
|
||||
border-top: 1px solid #ccc !important;
|
||||
}
|
||||
|
||||
.search-panel-header {
|
||||
@ -130,10 +134,6 @@
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.search-panel-tree[collapsed=true] + .search-panel-header {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.search-panel-header > label {
|
||||
margin-top: 2px !important;
|
||||
margin-bottom: 1px !important;
|
||||
|
@ -26,9 +26,11 @@
|
||||
// devtools coding style.
|
||||
|
||||
// Rules from the mozilla plugin
|
||||
"mozilla/balanced-listeners": 2,
|
||||
"mozilla/components-imports": 1,
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
"mozilla/mark-test-function-used": 1,
|
||||
"mozilla/no-aArgs": 1,
|
||||
"mozilla/var-only-at-top-level": 1,
|
||||
|
||||
// Disallow using variables outside the blocks they are defined (especially
|
||||
|
@ -77,26 +77,30 @@ body.dragging .tag-line {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
z-index: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Indicates a tag-line in the markup-view as being an active drop target by
|
||||
* drawing a horizontal line where the dragged element would be inserted if
|
||||
* dropped here */
|
||||
.tag-line.drop-target::before, .tag-line.drag-target::before {
|
||||
.tag-line.drop-target::before,
|
||||
.tag-line.drag-target::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
/* Offset these by 1000px to make sure they cover the full width of the view */
|
||||
padding-left: 1000px;
|
||||
left: -1000px;
|
||||
}
|
||||
|
||||
.tag-line.drag-target::before {
|
||||
border-top: 2px dashed var(--theme-contrast-background);
|
||||
border-top: 2px solid var(--theme-content-color2);
|
||||
}
|
||||
|
||||
.tag-line.drop-target::before {
|
||||
border-top: 2px dashed var(--theme-content-color1);
|
||||
border-top: 2px solid var(--theme-contrast-background);
|
||||
}
|
||||
|
||||
/* In case the indicator is put on the closing .tag-line, the indentation level
|
||||
|
@ -3,6 +3,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const {Cc, Cu, Ci} = require("chrome");
|
||||
|
||||
@ -16,11 +17,11 @@ const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
|
||||
const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
|
||||
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
|
||||
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
|
||||
const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
|
||||
const AUTOCOMPLETE_POPUP_PANEL_ID = "markupview_autoCompletePopup";
|
||||
|
||||
const {UndoStack} = require("devtools/client/shared/undo");
|
||||
const {editableField, InplaceEditor} = require("devtools/client/shared/inplace-editor");
|
||||
const {gDevTools} = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
|
||||
const {HTMLEditor} = require("devtools/client/markupview/html-editor");
|
||||
const promise = require("promise");
|
||||
const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
|
||||
@ -37,7 +38,7 @@ Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
loader.lazyGetter(this, "DOMParser", function() {
|
||||
return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
|
||||
return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
|
||||
});
|
||||
loader.lazyGetter(this, "AutocompletePopup", () => {
|
||||
return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
|
||||
@ -75,7 +76,7 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
|
||||
|
||||
try {
|
||||
this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
|
||||
} catch(ex) {
|
||||
} catch (ex) {
|
||||
this.maxChildren = DEFAULT_MAX_CHILDREN;
|
||||
}
|
||||
|
||||
@ -94,34 +95,34 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
|
||||
|
||||
this._containers = new Map();
|
||||
|
||||
this._boundMutationObserver = this._mutationObserver.bind(this);
|
||||
this.walker.on("mutations", this._boundMutationObserver);
|
||||
|
||||
this._boundOnDisplayChange = this._onDisplayChange.bind(this);
|
||||
this.walker.on("display-change", this._boundOnDisplayChange);
|
||||
|
||||
// Binding functions that need to be called in scope.
|
||||
this._mutationObserver = this._mutationObserver.bind(this);
|
||||
this._onDisplayChange = this._onDisplayChange.bind(this);
|
||||
this._onMouseClick = this._onMouseClick.bind(this);
|
||||
|
||||
this._onMouseUp = this._onMouseUp.bind(this);
|
||||
this.doc.body.addEventListener("mouseup", this._onMouseUp);
|
||||
|
||||
this._boundOnNewSelection = this._onNewSelection.bind(this);
|
||||
this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
|
||||
this._onNewSelection();
|
||||
|
||||
this._boundKeyDown = this._onKeyDown.bind(this);
|
||||
this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
|
||||
|
||||
this._onNewSelection = this._onNewSelection.bind(this);
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._onCopy = this._onCopy.bind(this);
|
||||
this._frame.contentWindow.addEventListener("copy", this._onCopy);
|
||||
this._onFocus = this._onFocus.bind(this);
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
|
||||
|
||||
this._boundFocus = this._onFocus.bind(this);
|
||||
this._frame.addEventListener("focus", this._boundFocus, false);
|
||||
|
||||
this._makeTooltipPersistent = this._makeTooltipPersistent.bind(this);
|
||||
// Listening to various events.
|
||||
this._elt.addEventListener("click", this._onMouseClick, false);
|
||||
this._elt.addEventListener("mousemove", this._onMouseMove, false);
|
||||
this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
|
||||
this.doc.body.addEventListener("mouseup", this._onMouseUp);
|
||||
this.win.addEventListener("keydown", this._onKeyDown, false);
|
||||
this.win.addEventListener("copy", this._onCopy);
|
||||
this._frame.addEventListener("focus", this._onFocus, false);
|
||||
this.walker.on("mutations", this._mutationObserver);
|
||||
this.walker.on("display-change", this._onDisplayChange);
|
||||
this._inspector.selection.on("new-node-front", this._onNewSelection);
|
||||
this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
|
||||
|
||||
this._onNewSelection();
|
||||
this._initTooltips();
|
||||
this._initHighlighter();
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
@ -133,36 +134,12 @@ MarkupView.prototype = {
|
||||
* How long does a node flash when it mutates (in ms).
|
||||
*/
|
||||
CONTAINER_FLASHING_DURATION: 500,
|
||||
/**
|
||||
* How long do you have to hold the mouse down before a drag
|
||||
* starts (in ms).
|
||||
*/
|
||||
GRAB_DELAY: 400,
|
||||
|
||||
_selectedContainer: null,
|
||||
|
||||
_initTooltips: function() {
|
||||
this.tooltip = new Tooltip(this._inspector.panelDoc);
|
||||
this._makeTooltipPersistent(false);
|
||||
|
||||
this._elt.addEventListener("click", this._onMouseClick, false);
|
||||
},
|
||||
|
||||
_initHighlighter: function() {
|
||||
// Show the box model on markup-view mousemove
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._elt.addEventListener("mousemove", this._onMouseMove, false);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
|
||||
|
||||
// Show markup-containers as hovered on toolbox "picker-node-hovered" event
|
||||
// which happens when the "pick" button is pressed
|
||||
this._onToolboxPickerHover = (event, nodeFront) => {
|
||||
this.showNode(nodeFront).then(() => {
|
||||
this._showContainerAsHovered(nodeFront);
|
||||
});
|
||||
};
|
||||
this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
|
||||
},
|
||||
|
||||
_makeTooltipPersistent: function(state) {
|
||||
@ -174,52 +151,26 @@ MarkupView.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_onToolboxPickerHover: function(event, nodeFront) {
|
||||
this.showNode(nodeFront).then(() => {
|
||||
this._showContainerAsHovered(nodeFront);
|
||||
}, e => console.error(e));
|
||||
},
|
||||
|
||||
isDragging: false,
|
||||
|
||||
_onMouseMove: function(event) {
|
||||
if (this.isDragging) {
|
||||
event.preventDefault();
|
||||
this._dragStartEl = event.target;
|
||||
|
||||
let docEl = this.doc.documentElement;
|
||||
|
||||
if (this._scrollInterval) {
|
||||
clearInterval(this._scrollInterval);
|
||||
}
|
||||
|
||||
// Auto-scroll when the mouse approaches top/bottom edge
|
||||
let distanceFromBottom = docEl.clientHeight - event.pageY + this.win.scrollY,
|
||||
distanceFromTop = event.pageY - this.win.scrollY;
|
||||
|
||||
if (distanceFromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
||||
// Map our distance from 0-50 to 5-15 range so the speed is kept
|
||||
// in a range not too fast, not too slow
|
||||
let speed = map(distanceFromBottom, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
||||
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
||||
// Here, we use minus because the value of speed - 15 is always negative
|
||||
// and it makes the speed relative to the distance between mouse and edge
|
||||
// the closer to the edge, the faster
|
||||
this._scrollInterval = setInterval(() => {
|
||||
docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (distanceFromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
||||
// refer to bottom edge's comments for more info
|
||||
let speed = map(distanceFromTop, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
||||
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
||||
|
||||
this._scrollInterval = setInterval(() => {
|
||||
docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let target = event.target;
|
||||
|
||||
// Search target for a markupContainer reference, if not found, walk up
|
||||
// Auto-scroll if we're dragging.
|
||||
if (this.isDragging) {
|
||||
event.preventDefault();
|
||||
this._autoScroll(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the current container as hovered and highlight it.
|
||||
// This requires finding the current MarkupContainer (walking up the DOM).
|
||||
while (!target.container) {
|
||||
if (target.tagName.toLowerCase() === "body") {
|
||||
return;
|
||||
@ -238,6 +189,47 @@ MarkupView.prototype = {
|
||||
this._showContainerAsHovered(container.node);
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed on each mouse-move while a node is being dragged in the view.
|
||||
* Auto-scrolls the view to reveal nodes below the fold to drop the dragged
|
||||
* node in.
|
||||
*/
|
||||
_autoScroll: function(event) {
|
||||
let docEl = this.doc.documentElement;
|
||||
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
|
||||
// Auto-scroll when the mouse approaches top/bottom edge.
|
||||
let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
|
||||
let fromTop = event.pageY - this.win.scrollY;
|
||||
|
||||
if (fromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
||||
// Map our distance from 0-50 to 5-15 range so the speed is kept in a
|
||||
// range not too fast, not too slow.
|
||||
let speed = map(
|
||||
fromBottom,
|
||||
0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
||||
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
||||
|
||||
this._autoScrollInterval = setInterval(() => {
|
||||
docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (fromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
|
||||
let speed = map(
|
||||
fromTop,
|
||||
0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
|
||||
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
|
||||
|
||||
this._autoScrollInterval = setInterval(() => {
|
||||
docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
|
||||
_onMouseClick: function(event) {
|
||||
// From the target passed here, let's find the parent MarkupContainer
|
||||
// and ask it if the tooltip should be shown
|
||||
@ -261,8 +253,8 @@ MarkupView.prototype = {
|
||||
_onMouseUp: function() {
|
||||
this.indicateDropTarget(null);
|
||||
this.indicateDragTarget(null);
|
||||
if (this._scrollInterval) {
|
||||
clearInterval(this._scrollInterval);
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
},
|
||||
|
||||
@ -280,13 +272,11 @@ MarkupView.prototype = {
|
||||
|
||||
this.indicateDropTarget(null);
|
||||
this.indicateDragTarget(null);
|
||||
if (this._scrollInterval) {
|
||||
clearInterval(this._scrollInterval);
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
_hoveredNode: null,
|
||||
|
||||
/**
|
||||
@ -307,10 +297,12 @@ MarkupView.prototype = {
|
||||
},
|
||||
|
||||
_onMouseLeave: function() {
|
||||
if (this._scrollInterval) {
|
||||
clearInterval(this._scrollInterval);
|
||||
if (this._autoScrollInterval) {
|
||||
clearInterval(this._autoScrollInterval);
|
||||
}
|
||||
if (this.isDragging) {
|
||||
return;
|
||||
}
|
||||
if (this.isDragging) return;
|
||||
|
||||
this._hideBoxModel(true);
|
||||
if (this._hoveredNode) {
|
||||
@ -457,7 +449,6 @@ MarkupView.prototype = {
|
||||
*/
|
||||
_onNewSelection: function() {
|
||||
let selection = this._inspector.selection;
|
||||
let reason = selection.reason;
|
||||
|
||||
this.htmlEditor.hide();
|
||||
if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
|
||||
@ -1237,7 +1228,7 @@ MarkupView.prototype = {
|
||||
this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
|
||||
// Need to focus the <html> element instead of the frame / window
|
||||
// in order to give keyboard focus back to doc (from editor).
|
||||
this._frame.contentDocument.documentElement.focus();
|
||||
this.doc.documentElement.focus();
|
||||
|
||||
if (aCommit) {
|
||||
this.updateNodeOuterHTML(aNode, aValue, oldValue);
|
||||
@ -1527,10 +1518,7 @@ MarkupView.prototype = {
|
||||
|
||||
this._clearBriefBoxModelTimer();
|
||||
|
||||
this._elt.removeEventListener("click", this._onMouseClick, false);
|
||||
|
||||
this._hoveredNode = null;
|
||||
this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
|
||||
|
||||
this.htmlEditor.destroy();
|
||||
this.htmlEditor = null;
|
||||
@ -1541,46 +1529,21 @@ MarkupView.prototype = {
|
||||
this.popup.destroy();
|
||||
this.popup = null;
|
||||
|
||||
this._frame.removeEventListener("focus", this._boundFocus, false);
|
||||
this._boundFocus = null;
|
||||
|
||||
if (this._boundUpdatePreview) {
|
||||
this._frame.contentWindow.removeEventListener("scroll",
|
||||
this._boundUpdatePreview, true);
|
||||
this._boundUpdatePreview = null;
|
||||
}
|
||||
|
||||
if (this._boundResizePreview) {
|
||||
this._frame.contentWindow.removeEventListener("resize",
|
||||
this._boundResizePreview, true);
|
||||
this._frame.contentWindow.removeEventListener("overflow",
|
||||
this._boundResizePreview, true);
|
||||
this._frame.contentWindow.removeEventListener("underflow",
|
||||
this._boundResizePreview, true);
|
||||
this._boundResizePreview = null;
|
||||
}
|
||||
|
||||
this._frame.contentWindow.removeEventListener("keydown",
|
||||
this._boundKeyDown, false);
|
||||
this._boundKeyDown = null;
|
||||
|
||||
this._frame.contentWindow.removeEventListener("copy", this._onCopy);
|
||||
this._onCopy = null;
|
||||
|
||||
this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
|
||||
this._boundOnNewSelection = null;
|
||||
|
||||
this.walker.off("mutations", this._boundMutationObserver);
|
||||
this._boundMutationObserver = null;
|
||||
|
||||
this.walker.off("display-change", this._boundOnDisplayChange);
|
||||
this._boundOnDisplayChange = null;
|
||||
|
||||
this._elt.removeEventListener("click", this._onMouseClick, false);
|
||||
this._elt.removeEventListener("mousemove", this._onMouseMove, false);
|
||||
this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
|
||||
this.doc.body.removeEventListener("mouseup", this._onMouseUp);
|
||||
this.win.removeEventListener("keydown", this._onKeyDown, false);
|
||||
this.win.removeEventListener("copy", this._onCopy);
|
||||
this._frame.removeEventListener("focus", this._onFocus, false);
|
||||
this.walker.off("mutations", this._mutationObserver);
|
||||
this.walker.off("display-change", this._onDisplayChange);
|
||||
this._inspector.selection.off("new-node-front", this._onNewSelection);
|
||||
this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
|
||||
|
||||
this._elt = null;
|
||||
|
||||
for (let [key, container] of this._containers) {
|
||||
for (let [, container] of this._containers) {
|
||||
container.destroy();
|
||||
}
|
||||
this._containers = null;
|
||||
@ -1597,6 +1560,18 @@ MarkupView.prototype = {
|
||||
return this._destroyer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the closest element with class tag-line. These are used to indicate
|
||||
* drag and drop targets.
|
||||
* @param {DOMNode} el
|
||||
* @return {DOMNode}
|
||||
*/
|
||||
findClosestDragDropTarget: function(el) {
|
||||
return el.classList.contains("tag-line")
|
||||
? el
|
||||
: el.querySelector(".tag-line") || el.closest(".tag-line");
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes an element as it's only argument and marks the element
|
||||
* as the drop target
|
||||
@ -1606,14 +1581,15 @@ MarkupView.prototype = {
|
||||
this._lastDropTarget.classList.remove("drop-target");
|
||||
}
|
||||
|
||||
if (!el) return;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = el.classList.contains("tag-line") ?
|
||||
el : el.querySelector(".tag-line") || el.closest(".tag-line");
|
||||
if (!target) return;
|
||||
|
||||
target.classList.add("drop-target");
|
||||
this._lastDropTarget = target;
|
||||
let target = this.findClosestDragDropTarget(el);
|
||||
if (target) {
|
||||
target.classList.add("drop-target");
|
||||
this._lastDropTarget = target;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1624,19 +1600,20 @@ MarkupView.prototype = {
|
||||
this._lastDragTarget.classList.remove("drag-target");
|
||||
}
|
||||
|
||||
if (!el) return;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = el.classList.contains("tag-line") ?
|
||||
el : el.querySelector(".tag-line") || el.closest(".tag-line");
|
||||
|
||||
if (!target) return;
|
||||
|
||||
target.classList.add("drag-target");
|
||||
this._lastDragTarget = target;
|
||||
let target = this.findClosestDragDropTarget(el);
|
||||
if (target) {
|
||||
target.classList.add("drag-target");
|
||||
this._lastDragTarget = target;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to get the nodes required to modify the markup after dragging the element (parent/nextSibling)
|
||||
* Used to get the nodes required to modify the markup after dragging the
|
||||
* element (parent/nextSibling).
|
||||
*/
|
||||
get dropTargetNodes() {
|
||||
let target = this._lastDropTarget;
|
||||
@ -1686,7 +1663,6 @@ MarkupView.prototype = {
|
||||
function MarkupContainer() { }
|
||||
|
||||
MarkupContainer.prototype = {
|
||||
|
||||
/*
|
||||
* Initialize the MarkupContainer. Should be called while one
|
||||
* of the other contain classes is instantiated.
|
||||
@ -1743,9 +1719,9 @@ MarkupContainer.prototype = {
|
||||
let isCanvas = tagName === "canvas";
|
||||
|
||||
return isImage || isCanvas;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1868,7 +1844,6 @@ MarkupContainer.prototype = {
|
||||
return this.elt.parentNode ? this.elt.parentNode.container : null;
|
||||
},
|
||||
|
||||
_isMouseDown: false,
|
||||
_isDragging: false,
|
||||
_dragStartY: 0,
|
||||
|
||||
@ -1892,25 +1867,30 @@ MarkupContainer.prototype = {
|
||||
/**
|
||||
* Check if element is draggable
|
||||
*/
|
||||
isDraggable: function(target) {
|
||||
return this._isMouseDown &&
|
||||
this.markup._dragStartEl === target &&
|
||||
!this.node.isPseudoElement &&
|
||||
isDraggable: function() {
|
||||
let tagName = this.node.tagName.toLowerCase();
|
||||
|
||||
return !this.node.isPseudoElement &&
|
||||
!this.node.isAnonymous &&
|
||||
!this.node.isDocumentElement &&
|
||||
tagName !== "body" &&
|
||||
tagName !== "head" &&
|
||||
this.win.getSelection().isCollapsed &&
|
||||
this.node.parentNode().tagName !== null;
|
||||
},
|
||||
|
||||
_onMouseDown: function(event) {
|
||||
let target = event.target;
|
||||
let {target, button, metaKey, ctrlKey} = event;
|
||||
let isLeftClick = button === 0;
|
||||
let isMiddleClick = button === 1;
|
||||
let isMetaClick = isLeftClick && (metaKey || ctrlKey);
|
||||
|
||||
// The "show more nodes" button (already has its onclick).
|
||||
// The "show more nodes" button already has its onclick, so early return.
|
||||
if (target.nodeName === "button") {
|
||||
return;
|
||||
}
|
||||
|
||||
// target is the MarkupContainer itself.
|
||||
this._isMouseDown = true;
|
||||
this.hovered = false;
|
||||
this.markup.navigate(this);
|
||||
event.stopPropagation();
|
||||
@ -1924,9 +1904,7 @@ MarkupContainer.prototype = {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
let isMiddleClick = event.button === 1;
|
||||
let isMetaClick = event.button === 0 && (event.metaKey || event.ctrlKey);
|
||||
|
||||
// Follow attribute links if middle or meta click.
|
||||
if (isMiddleClick || isMetaClick) {
|
||||
let link = target.dataset.link;
|
||||
let type = target.dataset.type;
|
||||
@ -1934,62 +1912,61 @@ MarkupContainer.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start dragging the container after a delay.
|
||||
this.markup._dragStartEl = target;
|
||||
setTimeout(() => {
|
||||
// Make sure the mouse is still down and on target.
|
||||
if (!this.isDraggable(target)) {
|
||||
return;
|
||||
}
|
||||
this.isDragging = true;
|
||||
|
||||
// Start node drag & drop (if the mouse moved, see _onMouseMove).
|
||||
if (isLeftClick && this.isDraggable()) {
|
||||
this._isPreDragging = true;
|
||||
this._dragStartY = event.pageY;
|
||||
this.markup.indicateDropTarget(this.elt);
|
||||
|
||||
// If this is the last child, use the closing <div.tag-line> of parent as indicator
|
||||
this.markup.indicateDragTarget(this.elt.nextElementSibling ||
|
||||
this.markup.getContainer(this.node.parentNode()).closeTagLine);
|
||||
}, this.markup.GRAB_DELAY);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On mouse up, stop dragging.
|
||||
*/
|
||||
_onMouseUp: Task.async(function*() {
|
||||
this._isMouseDown = false;
|
||||
this._isPreDragging = false;
|
||||
|
||||
if (!this.isDragging) {
|
||||
return;
|
||||
if (this.isDragging) {
|
||||
this.cancelDragging();
|
||||
|
||||
let dropTargetNodes = this.markup.dropTargetNodes;
|
||||
|
||||
if (!dropTargetNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
|
||||
dropTargetNodes.nextSibling);
|
||||
this.markup.emit("drop-completed");
|
||||
}
|
||||
|
||||
this.cancelDragging();
|
||||
|
||||
let dropTargetNodes = this.markup.dropTargetNodes;
|
||||
|
||||
if (!dropTargetNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
|
||||
dropTargetNodes.nextSibling);
|
||||
this.markup.emit("drop-completed");
|
||||
}),
|
||||
|
||||
/**
|
||||
* On mouse move, move the dragged element if any and indicate the drop target.
|
||||
* On mouse move, move the dragged element and indicate the drop target.
|
||||
*/
|
||||
_onMouseMove: function(event) {
|
||||
if (!this.isDragging) {
|
||||
return;
|
||||
// If this is the first move after mousedown, only start dragging after the
|
||||
// mouse has travelled a few pixels and then indicate the start position.
|
||||
let initialDiff = Math.abs(event.pageY - this._dragStartY);
|
||||
if (this._isPreDragging && initialDiff >= DRAG_DROP_MIN_INITIAL_DISTANCE) {
|
||||
this._isPreDragging = false;
|
||||
this.isDragging = true;
|
||||
|
||||
// If this is the last child, use the closing <div.tag-line> of parent as
|
||||
// indicator.
|
||||
let position = this.elt.nextElementSibling ||
|
||||
this.markup.getContainer(this.node.parentNode())
|
||||
.closeTagLine;
|
||||
this.markup.indicateDragTarget(position);
|
||||
}
|
||||
|
||||
let diff = event.pageY - this._dragStartY;
|
||||
this.elt.style.top = diff + "px";
|
||||
if (this.isDragging) {
|
||||
let diff = event.pageY - this._dragStartY;
|
||||
this.elt.style.top = diff + "px";
|
||||
|
||||
let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
|
||||
event.pageY - this.win.scrollY);
|
||||
|
||||
this.markup.indicateDropTarget(el);
|
||||
let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
|
||||
event.pageY - this.win.scrollY);
|
||||
this.markup.indicateDropTarget(el);
|
||||
}
|
||||
},
|
||||
|
||||
cancelDragging: function() {
|
||||
@ -1997,7 +1974,7 @@ MarkupContainer.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isMouseDown = false;
|
||||
this._isPreDragging = false;
|
||||
this.isDragging = false;
|
||||
this.elt.style.removeProperty("top");
|
||||
},
|
||||
|
@ -46,12 +46,11 @@ skip-if = e10s # scratchpad.xul is not loading in e10s window
|
||||
[browser_markupview_copy_image_data.js]
|
||||
[browser_markupview_css_completion_style_attribute.js]
|
||||
[browser_markupview_dragdrop_autoscroll.js]
|
||||
[browser_markupview_dragdrop_distance.js]
|
||||
[browser_markupview_dragdrop_dragRootNode.js]
|
||||
[browser_markupview_dragdrop_escapeKeyPress.js]
|
||||
[browser_markupview_dragdrop_invalidNodes.js]
|
||||
[browser_markupview_dragdrop_isDragging.js]
|
||||
[browser_markupview_dragdrop_reorder.js]
|
||||
[browser_markupview_dragdrop_textSelection.js]
|
||||
[browser_markupview_events.js]
|
||||
skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
|
||||
[browser_markupview_events_form.js]
|
||||
|
@ -4,62 +4,64 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test: Dragging nodes near top/bottom edges of inspector
|
||||
// should auto-scroll
|
||||
// Test that dragging a node near the top or bottom edge of the markup-view
|
||||
// auto-scrolls the view.
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop_autoscroll.html";
|
||||
const GRAB_DELAY = 400;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
let markup = inspector.markup;
|
||||
let viewHeight = markup.doc.documentElement.clientHeight;
|
||||
|
||||
let container = yield getContainerForSelector("#first", inspector);
|
||||
let rect = container.elt.getBoundingClientRect();
|
||||
info("Pretend the markup-view is dragging");
|
||||
markup.isDragging = true;
|
||||
|
||||
info("Simulating mouseDown on #first");
|
||||
container._onMouseDown({
|
||||
target: container.tagLine,
|
||||
pageX: 10,
|
||||
pageY: rect.top,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
|
||||
let onScrolled = waitForViewScroll(markup);
|
||||
|
||||
markup._onMouseMove({
|
||||
preventDefault: () => {},
|
||||
target: markup.doc.body,
|
||||
pageY: viewHeight
|
||||
});
|
||||
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
let bottomScrollPos = yield onScrolled;
|
||||
ok(bottomScrollPos > 0, "The view was scrolled down");
|
||||
|
||||
let clientHeight = markup.doc.documentElement.clientHeight;
|
||||
info("Simulating mouseMove on #first with pageY: " + clientHeight);
|
||||
info("Simulate a mousemove at the top and expect more scrolling");
|
||||
onScrolled = waitForViewScroll(markup);
|
||||
|
||||
let ev = {
|
||||
target: container.tagLine,
|
||||
pageX: 10,
|
||||
pageY: clientHeight,
|
||||
preventDefault: function() {}
|
||||
};
|
||||
markup._onMouseMove({
|
||||
preventDefault: () => {},
|
||||
target: markup.doc.body,
|
||||
pageY: 0
|
||||
});
|
||||
|
||||
info("Listening on scroll event");
|
||||
let scroll = onScroll(markup.win);
|
||||
let topScrollPos = yield onScrolled;
|
||||
ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
|
||||
is(topScrollPos, 0, "The view was scrolled up to the top");
|
||||
|
||||
markup._onMouseMove(ev);
|
||||
|
||||
yield scroll;
|
||||
|
||||
let dropCompleted = once(markup, "drop-completed");
|
||||
|
||||
container._onMouseUp(ev);
|
||||
markup._onMouseUp(ev);
|
||||
|
||||
yield dropCompleted;
|
||||
|
||||
ok("Scroll event fired");
|
||||
info("Simulate a mouseup to stop dragging");
|
||||
markup._onMouseUp();
|
||||
});
|
||||
|
||||
function onScroll(win) {
|
||||
return new Promise((resolve, reject) => {
|
||||
win.onscroll = function(e) {
|
||||
resolve(e);
|
||||
}
|
||||
function waitForViewScroll(markup) {
|
||||
let el = markup.doc.documentElement;
|
||||
let startPos = el.scrollTop;
|
||||
|
||||
return new Promise(resolve => {
|
||||
let isDone = () => {
|
||||
if (el.scrollTop === startPos) {
|
||||
resolve(el.scrollTop);
|
||||
} else {
|
||||
startPos = el.scrollTop;
|
||||
// Continue checking every 50ms.
|
||||
setTimeout(isDone, 50);
|
||||
}
|
||||
};
|
||||
|
||||
// Start checking if the view scrolled after a while.
|
||||
setTimeout(isDone, 50);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that nodes don't start dragging before the mouse has moved by at least
|
||||
// the minimum vertical distance defined in markup-view.js by
|
||||
// DRAG_DROP_MIN_INITIAL_DISTANCE.
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const TEST_NODE = "#test";
|
||||
|
||||
// Keep this in sync with DRAG_DROP_MIN_INITIAL_DISTANCE in markup-view.js
|
||||
const MIN_DISTANCE = 10;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
info("Drag the test node by half of the minimum distance");
|
||||
yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE / 2);
|
||||
yield checkIsDragging(inspector, TEST_NODE, false);
|
||||
|
||||
info("Drag the test node by exactly the minimum distance");
|
||||
yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE);
|
||||
yield checkIsDragging(inspector, TEST_NODE, true);
|
||||
inspector.markup.cancelDragging();
|
||||
|
||||
info("Drag the test node by more than the minimum distance");
|
||||
yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * 2);
|
||||
yield checkIsDragging(inspector, TEST_NODE, true);
|
||||
inspector.markup.cancelDragging();
|
||||
|
||||
info("Drag the test node by minus the minimum distance");
|
||||
yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * -1);
|
||||
yield checkIsDragging(inspector, TEST_NODE, true);
|
||||
inspector.markup.cancelDragging();
|
||||
});
|
||||
|
||||
function* checkIsDragging(inspector, selector, isDragging) {
|
||||
let container = yield getContainerForSelector(selector, inspector);
|
||||
if (isDragging) {
|
||||
ok(container.isDragging, "The container is being dragged");
|
||||
ok(inspector.markup.isDragging, "And the markup-view knows it");
|
||||
} else {
|
||||
ok(!container.isDragging, "The container hasn't been marked as dragging");
|
||||
ok(!inspector.markup.isDragging, "And the markup-view either");
|
||||
}
|
||||
}
|
@ -4,27 +4,19 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test if html root node is draggable
|
||||
// Test that the root node isn't draggable (as well as head and body).
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 400;
|
||||
const TEST_DATA = ["html", "head", "body"];
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
let el = yield getContainerForSelector("html", inspector);
|
||||
let rect = el.tagLine.getBoundingClientRect();
|
||||
for (let selector of TEST_DATA) {
|
||||
info("Try to drag/drop node " + selector);
|
||||
yield simulateNodeDrag(inspector, selector);
|
||||
|
||||
info("Simulating mouseDown on html root node");
|
||||
el._onMouseDown({
|
||||
target: el.tagLine,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
|
||||
info("Waiting for a little bit more than the markup-view grab delay");
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
is(el.isDragging, false, "isDragging is false");
|
||||
let container = yield getContainerForSelector(selector, inspector);
|
||||
ok(!container.isDragging, "The container hasn't been marked as dragging");
|
||||
}
|
||||
});
|
||||
|
@ -4,31 +4,30 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test whether ESCAPE keypress cancels dragging of an element
|
||||
// Test whether ESCAPE keypress cancels dragging of an element.
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 400;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
let {markup} = inspector;
|
||||
|
||||
let el = yield getContainerForSelector("#test", inspector);
|
||||
let rect = el.tagLine.getBoundingClientRect();
|
||||
info("Get a test container");
|
||||
let container = yield getContainerForSelector("#test", inspector);
|
||||
|
||||
info("Simulating mouseDown on #test");
|
||||
el._onMouseDown({
|
||||
target: el.tagLine,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
info("Simulate a drag/drop on this container");
|
||||
yield simulateNodeDrag(inspector, "#test");
|
||||
|
||||
info("Waiting for a little bit more than the markup-view grab delay");
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
ok(el.isDragging, "isDragging true after mouseDown");
|
||||
ok(container.isDragging && markup.isDragging,
|
||||
"The container is being dragged");
|
||||
ok(markup.doc.body.classList.contains("dragging"),
|
||||
"The dragging css class was added");
|
||||
|
||||
info("Simulating ESCAPE keypress");
|
||||
info("Simulate ESCAPE keypress");
|
||||
EventUtils.sendKey("escape", inspector.panelWin);
|
||||
is(el.isDragging, false, "isDragging false after ESCAPE keypress");
|
||||
|
||||
ok(!container.isDragging && !markup.isDragging,
|
||||
"The dragging has stopped");
|
||||
ok(!markup.doc.body.classList.contains("dragging"),
|
||||
"The dragging css class was removed");
|
||||
});
|
||||
|
@ -4,55 +4,42 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test: pseudo-elements and anonymous nodes should not be draggable
|
||||
// Check that pseudo-elements and anonymous nodes are not draggable.
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 400;
|
||||
|
||||
add_task(function*() {
|
||||
Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
|
||||
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
info("Expanding #test");
|
||||
info("Expanding nodes below #test");
|
||||
let parentFront = yield getNodeFront("#test", inspector);
|
||||
yield inspector.markup.expandNode(parentFront);
|
||||
yield waitForMultipleChildrenUpdates(inspector);
|
||||
|
||||
info("Getting the ::before pseudo element");
|
||||
let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
|
||||
let beforePseudo = parentContainer.elt.children[1].firstChild.container;
|
||||
|
||||
parentContainer.elt.scrollIntoView(true);
|
||||
|
||||
info("Simulating mouseDown on #test::before");
|
||||
beforePseudo._onMouseDown({
|
||||
target: beforePseudo.tagLine,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
info("Simulate dragging the ::before pseudo element");
|
||||
yield simulateNodeDrag(inspector, beforePseudo);
|
||||
|
||||
info("Waiting " + (GRAB_DELAY + 1) + "ms")
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
is(beforePseudo.isDragging, false, "[pseudo-element] isDragging is false after GRAB_DELAY has passed");
|
||||
ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging");
|
||||
|
||||
info("Expanding nodes below #anonymousParent");
|
||||
let inputFront = yield getNodeFront("#anonymousParent", inspector);
|
||||
|
||||
yield inspector.markup.expandNode(inputFront);
|
||||
yield waitForMultipleChildrenUpdates(inspector);
|
||||
|
||||
info("Getting the anonymous node");
|
||||
let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
|
||||
let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
|
||||
|
||||
inputContainer.elt.scrollIntoView(true);
|
||||
|
||||
info("Simulating mouseDown on input#anonymousParent div");
|
||||
anonymousDiv._onMouseDown({
|
||||
target: anonymousDiv.tagLine,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
info("Simulate dragging the anonymous node");
|
||||
yield simulateNodeDrag(inspector, anonymousDiv);
|
||||
|
||||
info("Waiting " + (GRAB_DELAY + 1) + "ms")
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
is(anonymousDiv.isDragging, false, "[anonymous element] isDragging is false after GRAB_DELAY has passed");
|
||||
ok(!anonymousDiv.isDragging, "anonymous node isn't dragging");
|
||||
});
|
||||
|
@ -1,65 +0,0 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test drag mode's delay, it shouldn't enable dragging before
|
||||
// GRAB_DELAY = 400 has passed
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 400;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
|
||||
let el = yield getContainerForSelector("#test", inspector);
|
||||
let rect = el.tagLine.getBoundingClientRect();
|
||||
|
||||
info("Simulating mouseDown on #test");
|
||||
el._onMouseDown({
|
||||
target: el.tagLine,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
|
||||
ok(!el.isDragging, "isDragging should not be set to true immediately");
|
||||
|
||||
info("Waiting for 10ms");
|
||||
yield wait(10);
|
||||
ok(!el.isDragging, "isDragging should not be set to true after a brief wait");
|
||||
|
||||
info("Waiting " + (GRAB_DELAY + 1) + "ms");
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
ok(el.isDragging, "isDragging true after GRAB_DELAY has passed");
|
||||
|
||||
let dropCompleted = once(inspector.markup, "drop-completed");
|
||||
|
||||
info("Simulating mouseUp on #test");
|
||||
el._onMouseUp({
|
||||
target: el.tagLine,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y
|
||||
});
|
||||
|
||||
yield dropCompleted;
|
||||
|
||||
ok(!el.isDragging, "isDragging false after mouseUp");
|
||||
|
||||
info("Simulating middle click on #test");
|
||||
el._onMouseDown({
|
||||
target: el.tagLine,
|
||||
button: 1,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
ok(!el.isDragging, "isDragging should not be set to true immediately");
|
||||
|
||||
info("Waiting " + (GRAB_DELAY + 1) + "ms");
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
ok(!el.isDragging, "isDragging never starts after middle click after mouseUp");
|
||||
});
|
@ -4,135 +4,104 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test different kinds of drag and drop node re-ordering
|
||||
// Test different kinds of drag and drop node re-ordering.
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 5;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
inspector.markup.GRAB_DELAY = GRAB_DELAY;
|
||||
let ids;
|
||||
|
||||
info("Expanding #test");
|
||||
info("Expand #test node");
|
||||
let parentFront = yield getNodeFront("#test", inspector);
|
||||
let parent = yield getNode("#test");
|
||||
let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
|
||||
|
||||
yield inspector.markup.expandNode(parentFront);
|
||||
yield waitForMultipleChildrenUpdates(inspector);
|
||||
|
||||
info("Scroll #test into view");
|
||||
let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
|
||||
parentContainer.elt.scrollIntoView(true);
|
||||
|
||||
info("Testing putting an element back in it's original place");
|
||||
info("Test putting an element back at its original place");
|
||||
yield dragElementToOriginalLocation("#firstChild", inspector);
|
||||
is(parent.children[0].id, "firstChild", "#firstChild is still the first child of #test");
|
||||
is(parent.children[1].id, "middleChild", "#middleChild is still the second child of #test");
|
||||
ids = yield getChildrenIDsOf(parentFront, inspector);
|
||||
is(ids[0], "firstChild",
|
||||
"#firstChild is still the first child of #test");
|
||||
is(ids[1], "middleChild",
|
||||
"#middleChild is still the second child of #test");
|
||||
|
||||
info("Testing switching elements inside their parent");
|
||||
yield moveElementDown("#firstChild", "#middleChild", inspector);
|
||||
|
||||
is(parent.children[0].id, "middleChild", "#firstChild is now the second child of #test");
|
||||
is(parent.children[1].id, "firstChild", "#middleChild is now the first child of #test");
|
||||
ids = yield getChildrenIDsOf(parentFront, inspector);
|
||||
is(ids[0], "middleChild",
|
||||
"#firstChild is now the second child of #test");
|
||||
is(ids[1], "firstChild",
|
||||
"#middleChild is now the first child of #test");
|
||||
|
||||
info("Testing switching elements with a last child");
|
||||
yield moveElementDown("#firstChild", "#lastChild", inspector);
|
||||
|
||||
is(parent.children[1].id, "lastChild", "#lastChild is now the second child of #test");
|
||||
is(parent.children[2].id, "firstChild", "#firstChild is now the last child of #test");
|
||||
ids = yield getChildrenIDsOf(parentFront, inspector);
|
||||
is(ids[1], "lastChild",
|
||||
"#lastChild is now the second child of #test");
|
||||
is(ids[2], "firstChild",
|
||||
"#firstChild is now the last child of #test");
|
||||
|
||||
info("Testing appending element to a parent");
|
||||
yield moveElementDown("#before", "#test", inspector);
|
||||
|
||||
is(parent.children.length, 4, "New element appended to #test");
|
||||
is(parent.children[0].id, "before", "New element is appended at the right place (currently first child)");
|
||||
ids = yield getChildrenIDsOf(parentFront, inspector);
|
||||
is(ids.length, 4,
|
||||
"New element appended to #test");
|
||||
is(ids[0], "before",
|
||||
"New element is appended at the right place (currently first child)");
|
||||
|
||||
info("Testing moving element to after it's parent");
|
||||
yield moveElementDown("#firstChild", "#test", inspector);
|
||||
|
||||
is(parent.children.length, 3, "#firstChild is no longer #test's child");
|
||||
is(parent.nextElementSibling.id, "firstChild", "#firstChild is now #test's nextElementSibling");
|
||||
ids = yield getChildrenIDsOf(parentFront, inspector);
|
||||
is(ids.length, 3,
|
||||
"#firstChild is no longer #test's child");
|
||||
let siblingFront = yield inspector.walker.nextSibling(parentFront);
|
||||
is(siblingFront.id, "firstChild",
|
||||
"#firstChild is now #test's nextElementSibling");
|
||||
});
|
||||
|
||||
function* dragContainer(selector, targetOffset, inspector) {
|
||||
info("Dragging the markup-container for node " + selector);
|
||||
|
||||
let container = yield getContainerForSelector(selector, inspector);
|
||||
|
||||
let updated = inspector.once("inspector-updated");
|
||||
|
||||
let rect = {
|
||||
x: container.tagLine.offsetLeft,
|
||||
y: container.tagLine.offsetTop
|
||||
};
|
||||
|
||||
info("Simulating mouseDown on " + selector);
|
||||
container._onMouseDown({
|
||||
target: container.tagLine,
|
||||
pageX: rect.x,
|
||||
pageY: rect.y,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
|
||||
let targetX = rect.x + targetOffset.x,
|
||||
targetY = rect.y + targetOffset.y;
|
||||
|
||||
setTimeout(() => {
|
||||
info("Simulating mouseMove on " + selector +
|
||||
" with pageX: " + targetX + " pageY: " + targetY);
|
||||
container._onMouseMove({
|
||||
target: container.tagLine,
|
||||
pageX: targetX,
|
||||
pageY: targetY
|
||||
});
|
||||
|
||||
info("Simulating mouseUp on " + selector +
|
||||
" with pageX: " + targetX + " pageY: " + targetY);
|
||||
container._onMouseUp({
|
||||
target: container.tagLine,
|
||||
pageX: targetX,
|
||||
pageY: targetY
|
||||
});
|
||||
|
||||
container.markup._onMouseUp();
|
||||
}, GRAB_DELAY+1);
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
function* dragElementToOriginalLocation(selector, inspector) {
|
||||
let el = yield getContainerForSelector(selector, inspector);
|
||||
let height = el.tagLine.getBoundingClientRect().height;
|
||||
|
||||
info("Picking up and putting back down " + selector);
|
||||
|
||||
function onMutation() {
|
||||
ok(false, "Mutation received from dragging a node back to its location");
|
||||
}
|
||||
inspector.on("markupmutation", onMutation);
|
||||
yield dragContainer(selector, {x: 0, y: 0}, inspector);
|
||||
yield simulateNodeDragAndDrop(inspector, selector, 0, 0);
|
||||
|
||||
// Wait a bit to make sure the event never fires.
|
||||
// This doesn't need to catch *all* cases, since the mutation
|
||||
// will cause failure later in the test when it checks element ordering.
|
||||
yield new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
yield wait(500);
|
||||
inspector.off("markupmutation", onMutation);
|
||||
}
|
||||
|
||||
function* moveElementDown(selector, next, inspector) {
|
||||
info("Switching " + selector + " with " + next);
|
||||
|
||||
let container = yield getContainerForSelector(next, inspector);
|
||||
let height = container.tagLine.getBoundingClientRect().height;
|
||||
|
||||
let onMutated = inspector.once("markupmutation");
|
||||
let uiUpdate = inspector.once("inspector-updated");
|
||||
|
||||
let el = yield getContainerForSelector(next, inspector);
|
||||
let height = el.tagLine.getBoundingClientRect().height;
|
||||
|
||||
info("Switching " + selector + ' with ' + next);
|
||||
|
||||
yield dragContainer(selector, {x: 0, y: Math.round(height) + 2}, inspector);
|
||||
yield simulateNodeDragAndDrop(inspector, selector, 0, Math.round(height) + 2);
|
||||
|
||||
let mutations = yield onMutated;
|
||||
is(mutations.length, 2, "2 mutations");
|
||||
yield uiUpdate;
|
||||
};
|
||||
|
||||
is(mutations.length, 2, "2 mutations were received");
|
||||
}
|
||||
|
||||
function* getChildrenIDsOf(parentFront, {walker}) {
|
||||
let {nodes} = yield walker.children(parentFront);
|
||||
// Filter out non-element nodes since children also returns pseudo-elements.
|
||||
return nodes.filter(node => {
|
||||
return !node.isPseudoElement;
|
||||
}).map(node => {
|
||||
return node.id;
|
||||
});
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test: Nodes should not be draggable if there is a text selected
|
||||
// (trying to move selected text around shouldn't trigger node drag and drop)
|
||||
|
||||
const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
|
||||
const GRAB_DELAY = 400;
|
||||
|
||||
add_task(function*() {
|
||||
let {inspector} = yield addTab(TEST_URL).then(openInspector);
|
||||
let markup = inspector.markup;
|
||||
|
||||
info("Expanding span#before");
|
||||
let spanFront = yield getNodeFront("#before", inspector);
|
||||
let spanContainer = yield getContainerForNodeFront(spanFront, inspector);
|
||||
let span = yield getNode("#before");
|
||||
|
||||
yield inspector.markup.expandNode(spanFront);
|
||||
yield waitForMultipleChildrenUpdates(inspector);
|
||||
|
||||
spanContainer.elt.scrollIntoView(true);
|
||||
|
||||
info("Selecting #before's text content");
|
||||
|
||||
let textContent = spanContainer.elt.children[1].firstChild.container;
|
||||
|
||||
let selectRange = markup.doc.createRange();
|
||||
selectRange.selectNode(textContent.editor.elt.querySelector('[tabindex]'));
|
||||
markup.doc.getSelection().addRange(selectRange);
|
||||
|
||||
info("Simulating mouseDown on #before");
|
||||
|
||||
spanContainer._onMouseDown({
|
||||
pageX: 0,
|
||||
pageY: 0,
|
||||
target: spanContainer.tagLine,
|
||||
stopPropagation: function() {},
|
||||
preventDefault: function() {}
|
||||
});
|
||||
|
||||
yield wait(GRAB_DELAY + 1);
|
||||
|
||||
is(spanContainer.isDragging, false, "isDragging should be false if there is a text selected");
|
||||
});
|
@ -776,3 +776,73 @@ function unregisterActor(registrar, front) {
|
||||
return registrar.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate dragging a MarkupContainer by calling its mousedown and mousemove
|
||||
* handlers.
|
||||
* @param {InspectorPanel} inspector The current inspector-panel instance.
|
||||
* @param {String|MarkupContainer} selector The selector to identify the node or
|
||||
* the MarkupContainer for this node.
|
||||
* @param {Number} xOffset Optional x offset to drag by.
|
||||
* @param {Number} yOffset Optional y offset to drag by.
|
||||
*/
|
||||
function* simulateNodeDrag(inspector, selector, xOffset = 10, yOffset = 10) {
|
||||
let container = typeof selector === "string"
|
||||
? yield getContainerForSelector(selector, inspector)
|
||||
: selector;
|
||||
let rect = container.tagLine.getBoundingClientRect();
|
||||
let scrollX = inspector.markup.doc.documentElement.scrollLeft;
|
||||
let scrollY = inspector.markup.doc.documentElement.scrollTop;
|
||||
|
||||
info("Simulate mouseDown on element " + selector);
|
||||
container._onMouseDown({
|
||||
target: container.tagLine,
|
||||
button: 0,
|
||||
pageX: scrollX + rect.x,
|
||||
pageY: scrollY + rect.y,
|
||||
stopPropagation: () => {},
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
// _onMouseDown selects the node, so make sure to wait for the
|
||||
// inspector-updated event if the current selection was different.
|
||||
if (inspector.selection.nodeFront !== container.node) {
|
||||
yield inspector.once("inspector-updated");
|
||||
}
|
||||
|
||||
info("Simulate mouseMove on element " + selector);
|
||||
container._onMouseMove({
|
||||
pageX: scrollX + rect.x + xOffset,
|
||||
pageY: scrollY + rect.y + yOffset
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate dropping a MarkupContainer by calling its mouseup handler. This is
|
||||
* meant to be called after simulateNodeDrag has been called.
|
||||
* @param {InspectorPanel} inspector The current inspector-panel instance.
|
||||
* @param {String|MarkupContainer} selector The selector to identify the node or
|
||||
* the MarkupContainer for this node.
|
||||
*/
|
||||
function* simulateNodeDrop(inspector, selector) {
|
||||
info("Simulate mouseUp on element " + selector);
|
||||
let container = typeof selector === "string"
|
||||
? yield getContainerForSelector(selector, inspector)
|
||||
: selector;
|
||||
container._onMouseUp();
|
||||
inspector.markup._onMouseUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate drag'n'dropping a MarkupContainer by calling its mousedown,
|
||||
* mousemove and mouseup handlers.
|
||||
* @param {InspectorPanel} inspector The current inspector-panel instance.
|
||||
* @param {String|MarkupContainer} selector The selector to identify the node or
|
||||
* the MarkupContainer for this node.
|
||||
* @param {Number} xOffset Optional x offset to drag by.
|
||||
* @param {Number} yOffset Optional y offset to drag by.
|
||||
*/
|
||||
function* simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) {
|
||||
yield simulateNodeDrag(inspector, selector, xOffset, yOffset);
|
||||
yield simulateNodeDrop(inspector, selector);
|
||||
}
|
||||
|
@ -47,6 +47,9 @@ function createTreeProperties (census) {
|
||||
getRoots: () => census.children,
|
||||
getKey: node => node.id,
|
||||
itemHeight: HEAP_TREE_ROW_HEIGHT,
|
||||
// Because we never add or remove children when viewing the same census, we
|
||||
// can always reuse a cached traversal if one is available.
|
||||
reuseCachedTraversal: traversal => true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,13 @@ const Toolbar = module.exports = createClass({
|
||||
DOM.div({ className: "devtools-toolbar" }, [
|
||||
DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
|
||||
|
||||
DOM.select({
|
||||
className: `select-breakdown`,
|
||||
onChange: e => onBreakdownChange(e.target.value),
|
||||
}, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName))),
|
||||
DOM.label({},
|
||||
"Breakdown by ",
|
||||
DOM.select({
|
||||
className: `select-breakdown`,
|
||||
onChange: e => onBreakdownChange(e.target.value),
|
||||
}, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName)))
|
||||
),
|
||||
|
||||
DOM.label({}, [
|
||||
DOM.input({
|
||||
|
@ -100,7 +100,7 @@ const TreeNode = createFactory(createClass({
|
||||
|
||||
/**
|
||||
* A generic tree component. See propTypes for the public API.
|
||||
*
|
||||
*
|
||||
* @see `devtools/client/memory/components/test/mochitest/head.js` for usage
|
||||
* @see `devtools/client/memory/components/heap.js` for usage
|
||||
*/
|
||||
@ -130,7 +130,11 @@ const Tree = module.exports = createClass({
|
||||
// A predicate function to filter out unwanted items from the tree.
|
||||
filter: PropTypes.func,
|
||||
// The depth to which we should automatically expand new items.
|
||||
autoExpandDepth: PropTypes.number
|
||||
autoExpandDepth: PropTypes.number,
|
||||
// A predicate that returns true if the last DFS traversal that was cached
|
||||
// can be reused, false otherwise. The predicate function is passed the
|
||||
// cached traversal as an array of nodes.
|
||||
reuseCachedTraversal: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
@ -139,7 +143,8 @@ const Tree = module.exports = createClass({
|
||||
expanded: new Set(),
|
||||
seen: new Set(),
|
||||
focused: undefined,
|
||||
autoExpandDepth: AUTO_EXPAND_DEPTH
|
||||
autoExpandDepth: AUTO_EXPAND_DEPTH,
|
||||
reuseCachedTraversal: null,
|
||||
};
|
||||
},
|
||||
|
||||
@ -149,7 +154,8 @@ const Tree = module.exports = createClass({
|
||||
height: window.innerHeight,
|
||||
expanded: new Set(),
|
||||
seen: new Set(),
|
||||
focused: undefined
|
||||
focused: undefined,
|
||||
cachedTraversal: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
@ -273,10 +279,23 @@ const Tree = module.exports = createClass({
|
||||
* Perform a pre-order depth-first search over the whole forest.
|
||||
*/
|
||||
_dfsFromRoots(maxDepth = Infinity) {
|
||||
const cached = this.state.cachedTraversal;
|
||||
if (cached
|
||||
&& maxDepth === Infinity
|
||||
&& this.props.reuseCachedTraversal
|
||||
&& this.props.reuseCachedTraversal(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const traversal = [];
|
||||
for (let root of this.props.getRoots()) {
|
||||
this._dfs(root, maxDepth, traversal);
|
||||
}
|
||||
|
||||
if (this.props.reuseCachedTraversal) {
|
||||
this.state.cachedTraversal = traversal;
|
||||
}
|
||||
|
||||
return traversal;
|
||||
},
|
||||
|
||||
@ -296,7 +315,8 @@ const Tree = module.exports = createClass({
|
||||
}
|
||||
|
||||
this.setState({
|
||||
expanded: this.state.expanded
|
||||
expanded: this.state.expanded,
|
||||
cachedTraversal: null,
|
||||
});
|
||||
},
|
||||
|
||||
@ -308,7 +328,8 @@ const Tree = module.exports = createClass({
|
||||
_onCollapse(item) {
|
||||
this.state.expanded.delete(item);
|
||||
this.setState({
|
||||
expanded: this.state.expanded
|
||||
expanded: this.state.expanded,
|
||||
cachedTraversal: null,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -49,14 +49,14 @@ const breakdowns = exports.breakdowns = {
|
||||
breakdown: {
|
||||
by: "coarseType",
|
||||
objects: ALLOCATION_STACK,
|
||||
strings: ALLOCATION_STACK,
|
||||
strings: COUNT,
|
||||
scripts: INTERNAL_TYPE,
|
||||
other: INTERNAL_TYPE,
|
||||
}
|
||||
},
|
||||
|
||||
allocationStack: {
|
||||
displayName: "Allocation Site",
|
||||
displayName: "Allocation Stack",
|
||||
breakdown: ALLOCATION_STACK,
|
||||
},
|
||||
|
||||
|
@ -7,6 +7,15 @@
|
||||
* DOMString name
|
||||
* object? stack
|
||||
* object? endStack
|
||||
* unsigned short processType;
|
||||
* boolean isOffMainThread;
|
||||
|
||||
The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h,
|
||||
specifying if this marker originates in a content, chrome, plugin etc. process.
|
||||
Additionally, markers may be created from any thread on those processes, and
|
||||
`isOffMainThread` highights whether or not they're from the main thread. The most
|
||||
common type of marker is probably going to be from a GeckoProcessType_Content's
|
||||
main thread when debugging content.
|
||||
|
||||
## DOMEvent
|
||||
|
||||
|
@ -32,7 +32,11 @@ function createParentNode (marker) {
|
||||
* @param array filter
|
||||
*/
|
||||
function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
|
||||
let { getCurrentParentNode, pushNode, popParentNode } = createParentNodeFactory(rootNode);
|
||||
let {
|
||||
getCurrentParentNode,
|
||||
pushNode,
|
||||
popParentNode
|
||||
} = createParentNodeFactory(rootNode);
|
||||
|
||||
for (let i = 0, len = markersList.length; i < len; i++) {
|
||||
let curr = markersList[i];
|
||||
@ -48,7 +52,7 @@ function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
|
||||
let nestable = "nestable" in blueprint ? blueprint.nestable : true;
|
||||
let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
|
||||
|
||||
let finalized = null;
|
||||
let finalized = false;
|
||||
|
||||
// If this marker is collapsible, turn it into a parent marker.
|
||||
// If there are no children within it later, it will be turned
|
||||
@ -57,9 +61,14 @@ function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
|
||||
curr = createParentNode(curr);
|
||||
}
|
||||
|
||||
// If not nestible, just push it inside the root node,
|
||||
// like console.time/timeEnd.
|
||||
if (!nestable) {
|
||||
// If not nestible, just push it inside the root node. Additionally,
|
||||
// markers originating outside the main thread are considered to be
|
||||
// "never collapsible", to avoid confusion.
|
||||
// A beter solution would be to collapse every marker with its siblings
|
||||
// from the same thread, but that would require a thread id attached
|
||||
// to all markers, which is potentially expensive and rather useless at
|
||||
// the moment, since we don't really have that many OTMT markers.
|
||||
if (!nestable || curr.isOffMainThread) {
|
||||
pushNode(rootNode, curr);
|
||||
continue;
|
||||
}
|
||||
@ -68,9 +77,13 @@ function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
|
||||
// recursively upwards if this marker is outside their ranges and nestable.
|
||||
while (!finalized && parentNode) {
|
||||
// If this marker is eclipsed by the current parent marker,
|
||||
// make it a child of the current parent and stop
|
||||
// going upwards.
|
||||
if (nestable && curr.end <= parentNode.end) {
|
||||
// make it a child of the current parent and stop going upwards.
|
||||
// If the markers aren't from the same process, attach them to the root
|
||||
// node as well. Every process has its own main thread.
|
||||
if (nestable &&
|
||||
curr.start >= parentNode.start &&
|
||||
curr.end <= parentNode.end &&
|
||||
curr.processType == parentNode.processType) {
|
||||
pushNode(parentNode, curr);
|
||||
finalized = true;
|
||||
break;
|
||||
@ -112,6 +125,7 @@ function createParentNodeFactory (root) {
|
||||
}
|
||||
|
||||
let lastParent = parentMarkers.pop();
|
||||
|
||||
// If this finished parent marker doesn't have an end time,
|
||||
// so probably a synthesized marker, use the last marker's end time.
|
||||
if (lastParent.end == void 0) {
|
||||
@ -119,7 +133,7 @@ function createParentNodeFactory (root) {
|
||||
}
|
||||
|
||||
// If no children were ever pushed into this parent node,
|
||||
// remove it's submarkers so it behaves like a non collapsible
|
||||
// remove its submarkers so it behaves like a non collapsible
|
||||
// node.
|
||||
if (!lastParent.submarkers.length) {
|
||||
delete lastParent.submarkers;
|
||||
@ -131,7 +145,9 @@ function createParentNodeFactory (root) {
|
||||
/**
|
||||
* Returns the most recent parent node.
|
||||
*/
|
||||
getCurrentParentNode: () => parentMarkers.length ? parentMarkers[parentMarkers.length - 1] : null,
|
||||
getCurrentParentNode: () => parentMarkers.length
|
||||
? parentMarkers[parentMarkers.length - 1]
|
||||
: null,
|
||||
|
||||
/**
|
||||
* Push this marker into the most recent parent node.
|
||||
|
@ -110,6 +110,7 @@ MarkerView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
|
||||
_displaySelf: function(document, arrowNode) {
|
||||
let targetNode = document.createElement("hbox");
|
||||
targetNode.className = "waterfall-tree-item";
|
||||
targetNode.setAttribute("otmt", this.marker.isOffMainThread);
|
||||
|
||||
if (this == this.root) {
|
||||
// Bounds are needed for properly positioning and scaling markers in
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
function* spawnTest() {
|
||||
let { panel } = yield initPerformance(WORKER_URL);
|
||||
let { PerformanceController } = panel.panelWin;
|
||||
let { $$, $, PerformanceController } = panel.panelWin;
|
||||
|
||||
loadFrameScripts();
|
||||
|
||||
@ -27,26 +27,43 @@ function* spawnTest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
testWorkerMarker(markers.find(m => m.name == "Worker"));
|
||||
testWorkerMarkerData(markers.find(m => m.name == "Worker"));
|
||||
return true;
|
||||
});
|
||||
|
||||
yield stopRecording(panel);
|
||||
ok(true, "Recording has ended.");
|
||||
|
||||
for (let node of $$(".waterfall-marker-name[value=Worker")) {
|
||||
testWorkerMarkerUI(node.parentNode.parentNode);
|
||||
}
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function testWorkerMarker(marker) {
|
||||
function testWorkerMarkerData(marker) {
|
||||
ok(true, "Found a worker marker.");
|
||||
|
||||
ok("start" in marker,
|
||||
"The start time is specified in the worker marker.");
|
||||
ok("end" in marker,
|
||||
"The end time is specified in the worker marker.");
|
||||
|
||||
ok("workerOperation" in marker,
|
||||
"The worker operation is specified in the worker marker.");
|
||||
|
||||
ok("processType" in marker,
|
||||
"The process type is specified in the worker marker.");
|
||||
ok("isOffMainThread" in marker,
|
||||
"The thread origin is specified in the worker marker.");
|
||||
}
|
||||
|
||||
function testWorkerMarkerUI(node) {
|
||||
is(node.className, "waterfall-tree-item",
|
||||
"The marker node has the correct class name.");
|
||||
ok(node.hasAttribute("otmt"),
|
||||
"The marker node specifies if it is off the main thread or not.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,163 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests if the waterfall collapsing logic works properly
|
||||
* when dealing with OTMT markers.
|
||||
*/
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test() {
|
||||
const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
|
||||
|
||||
let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
|
||||
|
||||
WaterfallUtils.collapseMarkersIntoNode({
|
||||
rootNode: rootMarkerNode,
|
||||
markersList: gTestMarkers
|
||||
});
|
||||
|
||||
compare(rootMarkerNode, gExpectedOutput);
|
||||
|
||||
function compare (marker, expected) {
|
||||
for (let prop in expected) {
|
||||
if (prop === "submarkers") {
|
||||
for (let i = 0; i < expected.submarkers.length; i++) {
|
||||
compare(marker.submarkers[i], expected.submarkers[i]);
|
||||
}
|
||||
} else if (prop !== "uid") {
|
||||
equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const gTestMarkers = [
|
||||
{ start: 1, end: 4, name: "A1-mt", processType: 1, isOffMainThread: false },
|
||||
// This should collapse only under A1-mt
|
||||
{ start: 2, end: 3, name: "B1", processType: 1, isOffMainThread: false },
|
||||
// This should never collapse.
|
||||
{ start: 2, end: 3, name: "C1", processType: 1, isOffMainThread: true },
|
||||
|
||||
{ start: 5, end: 8, name: "A1-otmt", processType: 1, isOffMainThread: true },
|
||||
// This should collapse only under A1-mt
|
||||
{ start: 6, end: 7, name: "B2", processType: 1, isOffMainThread: false },
|
||||
// This should never collapse.
|
||||
{ start: 6, end: 7, name: "C2", processType: 1, isOffMainThread: true },
|
||||
|
||||
{ start: 9, end: 12, name: "A2-mt", processType: 2, isOffMainThread: false },
|
||||
// This should collapse only under A2-mt
|
||||
{ start: 10, end: 11, name: "D1", processType: 2, isOffMainThread: false },
|
||||
// This should never collapse.
|
||||
{ start: 10, end: 11, name: "E1", processType: 2, isOffMainThread: true },
|
||||
|
||||
{ start: 13, end: 16, name: "A2-otmt", processType: 2, isOffMainThread: true },
|
||||
// This should collapse only under A2-mt
|
||||
{ start: 14, end: 15, name: "D2", processType: 2, isOffMainThread: false },
|
||||
// This should never collapse.
|
||||
{ start: 14, end: 15, name: "E2", processType: 2, isOffMainThread: true },
|
||||
|
||||
// This should not collapse, because there's no parent in this process.
|
||||
{ start: 14, end: 15, name: "F", processType: 3, isOffMainThread: false },
|
||||
|
||||
// This should never collapse.
|
||||
{ start: 14, end: 15, name: "G", processType: 3, isOffMainThread: true },
|
||||
];
|
||||
|
||||
const gExpectedOutput = {
|
||||
name: "(root)",
|
||||
submarkers: [{
|
||||
start: 1,
|
||||
end: 4,
|
||||
name: "A1-mt",
|
||||
processType: 1,
|
||||
isOffMainThread: false,
|
||||
submarkers: [{
|
||||
start: 2,
|
||||
end: 3,
|
||||
name: "B1",
|
||||
processType: 1,
|
||||
isOffMainThread: false
|
||||
}]
|
||||
}, {
|
||||
start: 2,
|
||||
end: 3,
|
||||
name: "C1",
|
||||
processType: 1,
|
||||
isOffMainThread: true
|
||||
}, {
|
||||
start: 5,
|
||||
end: 8,
|
||||
name: "A1-otmt",
|
||||
processType: 1,
|
||||
isOffMainThread: true,
|
||||
submarkers: [{
|
||||
start: 6,
|
||||
end: 7,
|
||||
name: "B2",
|
||||
processType: 1,
|
||||
isOffMainThread: false
|
||||
}]
|
||||
}, {
|
||||
start: 6,
|
||||
end: 7,
|
||||
name: "C2",
|
||||
processType: 1,
|
||||
isOffMainThread: true
|
||||
}, {
|
||||
start: 9,
|
||||
end: 12,
|
||||
name: "A2-mt",
|
||||
processType: 2,
|
||||
isOffMainThread: false,
|
||||
submarkers: [{
|
||||
start: 10,
|
||||
end: 11,
|
||||
name: "D1",
|
||||
processType: 2,
|
||||
isOffMainThread: false
|
||||
}]
|
||||
}, {
|
||||
start: 10,
|
||||
end: 11,
|
||||
name: "E1",
|
||||
processType: 2,
|
||||
isOffMainThread: true
|
||||
}, {
|
||||
start: 13,
|
||||
end: 16,
|
||||
name: "A2-otmt",
|
||||
processType: 2,
|
||||
isOffMainThread: true,
|
||||
submarkers: [{
|
||||
start: 14,
|
||||
end: 15,
|
||||
name: "D2",
|
||||
processType: 2,
|
||||
isOffMainThread: false
|
||||
}]
|
||||
}, {
|
||||
start: 14,
|
||||
end: 15,
|
||||
name: "E2",
|
||||
processType: 2,
|
||||
isOffMainThread: true
|
||||
}, {
|
||||
start: 14,
|
||||
end: 15,
|
||||
name: "F",
|
||||
processType: 3,
|
||||
isOffMainThread: false,
|
||||
submarkers: []
|
||||
}, {
|
||||
start: 14,
|
||||
end: 15,
|
||||
name: "G",
|
||||
processType: 3,
|
||||
isOffMainThread: true,
|
||||
submarkers: []
|
||||
}]
|
||||
};
|
@ -33,3 +33,4 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
|
||||
[test_waterfall-utils-collapse-02.js]
|
||||
[test_waterfall-utils-collapse-03.js]
|
||||
[test_waterfall-utils-collapse-04.js]
|
||||
[test_waterfall-utils-collapse-05.js]
|
||||
|
@ -1158,5 +1158,7 @@ function waitForStyleEditor(toolbox, href) {
|
||||
function reloadPage(inspector) {
|
||||
let onNewRoot = inspector.once("new-root");
|
||||
content.location.reload();
|
||||
return onNewRoot.then(inspector.markup._waitForChildren);
|
||||
return onNewRoot.then(() => {
|
||||
inspector.markup._waitForChildren();
|
||||
});
|
||||
}
|
||||
|
@ -505,6 +505,17 @@
|
||||
-moz-margin-end: -14px;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTMT markers
|
||||
*/
|
||||
|
||||
.waterfall-tree-item[otmt=true] .waterfall-marker-bullet,
|
||||
.waterfall-tree-item[otmt=true] .waterfall-marker-bar {
|
||||
background-color: transparent;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker details view
|
||||
*/
|
||||
@ -552,43 +563,53 @@
|
||||
menuitem.marker-color-graphs-full-red:before,
|
||||
.marker-color-graphs-full-red {
|
||||
background-color: var(--theme-graphs-full-red);
|
||||
border-color: var(--theme-graphs-full-red);
|
||||
}
|
||||
menuitem.marker-color-graphs-full-blue:before,
|
||||
.marker-color-graphs-full-blue {
|
||||
background-color: var(--theme-graphs-full-blue);
|
||||
border-color: var(--theme-graphs-full-blue);
|
||||
}
|
||||
|
||||
menuitem.marker-color-graphs-green:before,
|
||||
.marker-color-graphs-green {
|
||||
background-color: var(--theme-graphs-green);
|
||||
border-color: var(--theme-graphs-green);
|
||||
}
|
||||
menuitem.marker-color-graphs-blue:before,
|
||||
.marker-color-graphs-blue {
|
||||
background-color: var(--theme-graphs-blue);
|
||||
border-color: var(--theme-graphs-blue);
|
||||
}
|
||||
menuitem.marker-color-graphs-bluegrey:before,
|
||||
.marker-color-graphs-bluegrey {
|
||||
background-color: var(--theme-graphs-bluegrey);
|
||||
border-color: var(--theme-graphs-bluegrey);
|
||||
}
|
||||
menuitem.marker-color-graphs-purple:before,
|
||||
.marker-color-graphs-purple {
|
||||
background-color: var(--theme-graphs-purple);
|
||||
border-color: var(--theme-graphs-purple);
|
||||
}
|
||||
menuitem.marker-color-graphs-yellow:before,
|
||||
.marker-color-graphs-yellow {
|
||||
background-color: var(--theme-graphs-yellow);
|
||||
border-color: var(--theme-graphs-yellow);
|
||||
}
|
||||
menuitem.marker-color-graphs-orange:before,
|
||||
.marker-color-graphs-orange {
|
||||
background-color: var(--theme-graphs-orange);
|
||||
border-color: var(--theme-graphs-orange);
|
||||
}
|
||||
menuitem.marker-color-graphs-red:before,
|
||||
.marker-color-graphs-red {
|
||||
background-color: var(--theme-graphs-red);
|
||||
border-color: var(--theme-graphs-red);
|
||||
}
|
||||
menuitem.marker-color-graphs-grey:before,
|
||||
.marker-color-graphs-grey{
|
||||
background-color: var(--theme-graphs-grey);
|
||||
border-color: var(--theme-graphs-grey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -814,6 +814,17 @@ ThreadActor.prototype = {
|
||||
return function () {
|
||||
// onStep is called with 'this' set to the current frame.
|
||||
|
||||
// Only allow stepping stops at entry points for the line, when
|
||||
// the stepping occurs in a single frame. The "same frame"
|
||||
// check makes it so a sequence of steps can step out of a frame
|
||||
// and into subsequent calls in the outer frame. E.g., if there
|
||||
// is a call "a(b())" and the user steps into b, then this
|
||||
// condition makes it possible to step out of b and into a.
|
||||
if (this === startFrame &&
|
||||
!this.script.getOffsetLocation(this.offset).isEntryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const generatedLocation = thread.sources.getFrameLocation(this);
|
||||
const newLocation = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
|
||||
generatedLocation));
|
||||
|
@ -1975,6 +1975,10 @@ RemoteBrowserTabActor.prototype = {
|
||||
if (this._form) {
|
||||
let deferred = promise.defer();
|
||||
let onFormUpdate = msg => {
|
||||
// There may be more than just one childtab.js up and running
|
||||
if (this._form.actor != msg.json.actor) {
|
||||
return;
|
||||
}
|
||||
this._mm.removeMessageListener("debug:form", onFormUpdate);
|
||||
this._form = msg.json;
|
||||
deferred.resolve(this);
|
||||
|
@ -211,8 +211,7 @@ SrcdirProvider.prototype = {
|
||||
let entries = [];
|
||||
let lines = data.split(/\n/);
|
||||
let preprocessed = /^\s*\*/;
|
||||
let contentEntry =
|
||||
new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)");
|
||||
let contentEntry = /^\s+content\/(\S+)\s+\((\S+)\)/;
|
||||
for (let line of lines) {
|
||||
if (preprocessed.test(line)) {
|
||||
dump("Unable to override preprocessed file: " + line + "\n");
|
||||
@ -220,12 +219,12 @@ SrcdirProvider.prototype = {
|
||||
}
|
||||
let match = contentEntry.exec(line);
|
||||
if (match) {
|
||||
let pathComponents = match[3].split("/");
|
||||
let pathComponents = match[2].split("/");
|
||||
pathComponents.unshift(clientDir);
|
||||
let path = OS.Path.join.apply(OS.Path, pathComponents);
|
||||
let uri = this.fileURI(path);
|
||||
let entry = "override chrome://" + match[1] +
|
||||
"/content/" + match[2] + "\t" + uri;
|
||||
let chromeURI = "chrome://devtools/content/" + match[1];
|
||||
let entry = "override " + chromeURI + "\t" + uri;
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,10 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "AbstractTimelineMarker.h"
|
||||
|
||||
#include "mozilla/TimeStamp.h"
|
||||
#include "MainThreadUtils.h"
|
||||
#include "nsAppRunner.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
@ -13,6 +16,8 @@ AbstractTimelineMarker::AbstractTimelineMarker(const char* aName,
|
||||
MarkerTracingType aTracingType)
|
||||
: mName(aName)
|
||||
, mTracingType(aTracingType)
|
||||
, mProcessType(XRE_GetProcessType())
|
||||
, mIsOffMainThread(!NS_IsMainThread())
|
||||
{
|
||||
MOZ_COUNT_CTOR(AbstractTimelineMarker);
|
||||
SetCurrentTime();
|
||||
@ -23,6 +28,8 @@ AbstractTimelineMarker::AbstractTimelineMarker(const char* aName,
|
||||
MarkerTracingType aTracingType)
|
||||
: mName(aName)
|
||||
, mTracingType(aTracingType)
|
||||
, mProcessType(XRE_GetProcessType())
|
||||
, mIsOffMainThread(!NS_IsMainThread())
|
||||
{
|
||||
MOZ_COUNT_CTOR(AbstractTimelineMarker);
|
||||
SetCustomTime(aTime);
|
||||
@ -68,4 +75,16 @@ AbstractTimelineMarker::SetCustomTime(DOMHighResTimeStamp aTime)
|
||||
mTime = aTime;
|
||||
}
|
||||
|
||||
void
|
||||
AbstractTimelineMarker::SetProcessType(GeckoProcessType aProcessType)
|
||||
{
|
||||
mProcessType = aProcessType;
|
||||
}
|
||||
|
||||
void
|
||||
AbstractTimelineMarker::SetOffMainThread(bool aIsOffMainThread)
|
||||
{
|
||||
mIsOffMainThread = aIsOffMainThread;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "TimelineMarkerEnums.h" // for MarkerTracingType
|
||||
#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
|
||||
#include "nsXULAppAPI.h" // for GeckoProcessType
|
||||
#include "mozilla/UniquePtr.h"
|
||||
|
||||
struct JSContext;
|
||||
@ -48,15 +49,23 @@ public:
|
||||
DOMHighResTimeStamp GetTime() const { return mTime; }
|
||||
MarkerTracingType GetTracingType() const { return mTracingType; }
|
||||
|
||||
const uint8_t GetProcessType() const { return mProcessType; };
|
||||
const bool IsOffMainThread() const { return mIsOffMainThread; };
|
||||
|
||||
private:
|
||||
const char* mName;
|
||||
DOMHighResTimeStamp mTime;
|
||||
MarkerTracingType mTracingType;
|
||||
|
||||
uint8_t mProcessType; // @see `enum GeckoProcessType`.
|
||||
bool mIsOffMainThread;
|
||||
|
||||
protected:
|
||||
void SetCurrentTime();
|
||||
void SetCustomTime(const TimeStamp& aTime);
|
||||
void SetCustomTime(DOMHighResTimeStamp aTime);
|
||||
void SetProcessType(GeckoProcessType aProcessType);
|
||||
void SetOffMainThread(bool aIsOffMainThread);
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
33
docshell/base/timeline/CompositeTimelineMarker.h
Normal file
33
docshell/base/timeline/CompositeTimelineMarker.h
Normal file
@ -0,0 +1,33 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_CompositeTimelineMarker_h_
|
||||
#define mozilla_CompositeTimelineMarker_h_
|
||||
|
||||
#include "TimelineMarker.h"
|
||||
#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
class CompositeTimelineMarker : public TimelineMarker
|
||||
{
|
||||
public:
|
||||
explicit CompositeTimelineMarker(const TimeStamp& aTime,
|
||||
MarkerTracingType aTracingType)
|
||||
: TimelineMarker("Composite", aTime, aTracingType)
|
||||
{
|
||||
// Even though these markers end up being created on the main thread in the
|
||||
// content or chrome processes, they actually trace down code in the
|
||||
// compositor parent process. All the information for creating these markers
|
||||
// is sent along via IPC to an nsView when a composite finishes.
|
||||
// Mark this as 'off the main thread' to style it differently in frontends.
|
||||
SetOffMainThread(true);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_CompositeTimelineMarker_h_
|
@ -40,6 +40,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
if (GetTracingType() == MarkerTracingType::START) {
|
||||
aMarker.mCauseName.Construct(mCause);
|
||||
} else {
|
||||
|
@ -25,6 +25,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
if (GetTracingType() == MarkerTracingType::START) {
|
||||
aMarker.mType.Construct(mType);
|
||||
aMarker.mEventPhase.Construct(mPhase);
|
||||
|
@ -31,6 +31,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
aMarker.mCauseName.Construct(mCause);
|
||||
|
||||
if (!mFunctionName.IsEmpty() || !mFileName.IsEmpty()) {
|
||||
|
@ -26,6 +26,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
if (GetTracingType() == MarkerTracingType::START) {
|
||||
aMarker.mRestyleHint.Construct(mRestyleHint);
|
||||
}
|
||||
|
@ -28,7 +28,10 @@ TimelineMarker::TimelineMarker(const char* aName,
|
||||
void
|
||||
TimelineMarker::AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker)
|
||||
{
|
||||
// Nothing to do here for plain markers.
|
||||
if (GetTracingType() == MarkerTracingType::START) {
|
||||
aMarker.mProcessType.Construct(GetProcessType());
|
||||
aMarker.mIsOffMainThread.Construct(IsOffMainThread());
|
||||
}
|
||||
}
|
||||
|
||||
JSObject*
|
||||
|
@ -22,6 +22,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
if (!mCause.IsEmpty()) {
|
||||
aMarker.mCauseName.Construct(mCause);
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ public:
|
||||
|
||||
virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
|
||||
{
|
||||
TimelineMarker::AddDetails(aCx, aMarker);
|
||||
|
||||
if (GetTracingType() == MarkerTracingType::START) {
|
||||
aMarker.mWorkerOperation.Construct(mOperationType);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ EXPORTS.mozilla += [
|
||||
'AbstractTimelineMarker.h',
|
||||
'AutoGlobalTimelineMarker.h',
|
||||
'AutoTimelineMarker.h',
|
||||
'CompositeTimelineMarker.h',
|
||||
'ConsoleTimelineMarker.h',
|
||||
'EventTimelineMarker.h',
|
||||
'JavascriptTimelineMarker.h',
|
||||
|
@ -11,6 +11,12 @@ function rectangleContains(rect, x, y, width, height) {
|
||||
rect.height >= height;
|
||||
}
|
||||
|
||||
function sanitizeMarkers(list) {
|
||||
// Worker markers are currently gathered from all docshells, which may
|
||||
// interfere with this test.
|
||||
return list.filter(e => e.name != "Worker")
|
||||
}
|
||||
|
||||
var TESTS = [{
|
||||
desc: "Changing the width of the test element",
|
||||
searchFor: "Paint",
|
||||
@ -19,6 +25,7 @@ var TESTS = [{
|
||||
div.setAttribute("class", "resize-change-color");
|
||||
},
|
||||
check: function(markers) {
|
||||
markers = sanitizeMarkers(markers);
|
||||
ok(markers.length > 0, "markers were returned");
|
||||
console.log(markers);
|
||||
info(JSON.stringify(markers.filter(m => m.name == "Paint")));
|
||||
@ -40,6 +47,7 @@ var TESTS = [{
|
||||
div.setAttribute("class", "change-color");
|
||||
},
|
||||
check: function(markers) {
|
||||
markers = sanitizeMarkers(markers);
|
||||
ok(markers.length > 0, "markers were returned");
|
||||
ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
|
||||
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
|
||||
@ -59,6 +67,7 @@ var TESTS = [{
|
||||
div.setAttribute("class", "change-color add-class");
|
||||
},
|
||||
check: function(markers) {
|
||||
markers = sanitizeMarkers(markers);
|
||||
ok(markers.length > 0, "markers were returned");
|
||||
ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
|
||||
ok(!markers.some(m => m.name == "Paint"), "markers doesn't include Paint");
|
||||
@ -84,6 +93,7 @@ var TESTS = [{
|
||||
}, 100);
|
||||
},
|
||||
check: function(markers) {
|
||||
markers = sanitizeMarkers(markers);
|
||||
is(markers.length, 2, "Got 2 markers");
|
||||
is(markers[0].name, "ConsoleTime", "Got first ConsoleTime marker");
|
||||
is(markers[0].causeName, "FOO", "Got ConsoleTime FOO detail");
|
||||
@ -105,7 +115,7 @@ var TESTS = [{
|
||||
content.console.timeStamp(undefined);
|
||||
},
|
||||
check: function (markers) {
|
||||
markers = markers.filter(e => e.name != "Worker");
|
||||
markers = sanitizeMarkers(markers);
|
||||
is(markers.length, 4, "Got 4 markers");
|
||||
is(markers[0].name, "TimeStamp", "Got Timestamp marker");
|
||||
is(markers[0].causeName, "paper", "Got Timestamp label value");
|
||||
|
@ -38,6 +38,9 @@ dictionary ProfileTimelineMarker {
|
||||
DOMHighResTimeStamp end = 0;
|
||||
object? stack = null;
|
||||
|
||||
unsigned short processType;
|
||||
boolean isOffMainThread;
|
||||
|
||||
/* For ConsoleTime, Timestamp and Javascript markers. */
|
||||
DOMString causeName;
|
||||
|
||||
|
@ -218,6 +218,9 @@ methods of other kinds of objects.
|
||||
|
||||
* columnNumber: the column number for which offset is an entry point
|
||||
|
||||
* isEntryPoint: true if the offset is a column entry point, as
|
||||
would be reported by getAllColumnOffsets(); otherwise false.
|
||||
|
||||
`getOffsetsCoverage()`:
|
||||
: Return `null` or an array which contains informations about the coverage of
|
||||
all opcodes. The elements of the array are objects, each of which describes
|
||||
|
36
js/src/jit-test/tests/debug/Frame-onStep-11.js
Normal file
36
js/src/jit-test/tests/debug/Frame-onStep-11.js
Normal file
@ -0,0 +1,36 @@
|
||||
// Stepping out of a finally should not appear to
|
||||
// step backward to some earlier statement.
|
||||
|
||||
var g = newGlobal();
|
||||
g.eval(`function f() {
|
||||
debugger; // +0
|
||||
var x = 0; // +1
|
||||
try { // +2
|
||||
x = 1; // +3
|
||||
throw 'something'; // +4
|
||||
} catch (e) { // +5
|
||||
x = 2; // +6
|
||||
} finally { // +7
|
||||
x = 3; // +8
|
||||
} // +9
|
||||
x = 4; // +10
|
||||
}`); // +11
|
||||
|
||||
var dbg = Debugger(g);
|
||||
|
||||
let foundLines = '';
|
||||
|
||||
dbg.onDebuggerStatement = function(frame) {
|
||||
let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber;
|
||||
frame.onStep = function() {
|
||||
// Only record a stop when the offset is an entry point.
|
||||
let foundLine = this.script.getOffsetLocation(this.offset).lineNumber;
|
||||
if (foundLine != debugLine && this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) {
|
||||
foundLines += "," + (foundLine - debugLine);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
g.f();
|
||||
|
||||
assertEq(foundLines, ",1,2,3,4,6,7,8,10");
|
129
js/src/jit-test/tests/debug/Frame-onStep-12.js
Normal file
129
js/src/jit-test/tests/debug/Frame-onStep-12.js
Normal file
@ -0,0 +1,129 @@
|
||||
// Because our script source notes record only those bytecode offsets
|
||||
// at which source positions change, the default behavior in the
|
||||
// absence of a source note is to attribute a bytecode instruction to
|
||||
// the same source location as the preceding instruction. When control
|
||||
// flows from the preceding bytecode to the one we're emitting, that's
|
||||
// usually plausible. But successors in the bytecode stream are not
|
||||
// necessarily successors in the control flow graph. If the preceding
|
||||
// bytecode was a back edge of a loop, or the jump at the end of a
|
||||
// 'then' clause, its source position can be completely unrelated to
|
||||
// that of its successor.
|
||||
|
||||
// We try to avoid showing such nonsense offsets to the user by
|
||||
// requiring breakpoints and single-stepping to stop only at a line's
|
||||
// entry points, as reported by Debugger.Script.prototype.getLineOffsets;
|
||||
// and then ensuring that those entry points are all offsets mentioned
|
||||
// explicitly in the source notes, and hence deliberately attributed
|
||||
// to the given bytecode.
|
||||
|
||||
// This bit of JavaScript compiles to bytecode ending in a branch
|
||||
// instruction whose source position is the body of an unreachable
|
||||
// loop. The first instruction of the bytecode we emit following it
|
||||
// will inherit this nonsense position, if we have not explicitly
|
||||
// emitted a source note for said instruction.
|
||||
|
||||
// This test steps across such code and verifies that control never
|
||||
// appears to enter the unreachable loop.
|
||||
|
||||
var bitOfCode = `debugger; // +0
|
||||
if(false) { // +1
|
||||
for(var b=0; b<0; b++) { // +2
|
||||
c = 2 // +3
|
||||
} // +4
|
||||
}`; // +5
|
||||
|
||||
var g = newGlobal();
|
||||
var dbg = Debugger(g);
|
||||
|
||||
g.eval("function nothing() { }\n");
|
||||
|
||||
var log = '';
|
||||
dbg.onDebuggerStatement = function(frame) {
|
||||
let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber;
|
||||
frame.onStep = function() {
|
||||
let foundLine = this.script.getOffsetLocation(this.offset).lineNumber;
|
||||
if (this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) {
|
||||
log += (foundLine - debugLine).toString(16);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function testOne(name, body, expected) {
|
||||
print(name);
|
||||
log = '';
|
||||
g.eval(`function ${name} () { ${body} }`);
|
||||
g.eval(`${name}();`);
|
||||
assertEq(log, expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Test the instructions at the end of a "try".
|
||||
testOne("testTryFinally",
|
||||
`try {
|
||||
${bitOfCode}
|
||||
} finally { // +6
|
||||
} // +7
|
||||
nothing(); // +8
|
||||
`, "168");
|
||||
|
||||
// The same but without a finally clause.
|
||||
testOne("testTryCatch",
|
||||
`try {
|
||||
${bitOfCode}
|
||||
} catch (e) { // +6
|
||||
} // +7
|
||||
nothing(); // +8
|
||||
`, "18");
|
||||
|
||||
// Test the instructions at the end of a "catch".
|
||||
testOne("testCatchFinally",
|
||||
`try {
|
||||
throw new TypeError();
|
||||
} catch (e) {
|
||||
${bitOfCode}
|
||||
} finally { // +6
|
||||
} // +7
|
||||
nothing(); // +8
|
||||
`, "168");
|
||||
|
||||
// The same but without a finally clause. This relies on a
|
||||
// SpiderMonkey extension, because otherwise there's no way to see
|
||||
// extra instructions at the end of a catch.
|
||||
testOne("testCatch",
|
||||
`try {
|
||||
throw new TypeError();
|
||||
} catch (e if e instanceof TypeError) {
|
||||
${bitOfCode}
|
||||
} catch (e) { // +6
|
||||
} // +7
|
||||
nothing(); // +8
|
||||
`, "18");
|
||||
|
||||
// Test the instruction at the end of a "finally" clause.
|
||||
testOne("testFinally",
|
||||
`try {
|
||||
} finally {
|
||||
${bitOfCode}
|
||||
} // +6
|
||||
nothing(); // +7
|
||||
`, "17");
|
||||
|
||||
// Test the instruction at the end of a "then" clause.
|
||||
testOne("testThen",
|
||||
`if (1 === 1) {
|
||||
${bitOfCode}
|
||||
} else { // +6
|
||||
} // +7
|
||||
nothing(); // +8
|
||||
`, "18");
|
||||
|
||||
// Test the instructions leaving a switch block.
|
||||
testOne("testSwitch",
|
||||
`var x = 5;
|
||||
switch (x) {
|
||||
case 5:
|
||||
${bitOfCode}
|
||||
} // +6
|
||||
nothing(); // +7
|
||||
`, "17");
|
@ -14,9 +14,10 @@ Debugger(global).onDebuggerStatement = function (frame) {
|
||||
};
|
||||
|
||||
global.log = "";
|
||||
global.eval("function ppppp() { return 1; }");
|
||||
// 1 2 3 4
|
||||
// 0123456789012345678901234567890123456789012345678
|
||||
global.eval("function f(){ 1 && print(print()) && new Error() } debugger;");
|
||||
global.eval("function f(){ 1 && ppppp(ppppp()) && new Error() } debugger;");
|
||||
global.f();
|
||||
|
||||
// 14 - Enter the function body
|
||||
|
@ -2,18 +2,28 @@
|
||||
|
||||
var global = newGlobal();
|
||||
Debugger(global).onDebuggerStatement = function (frame) {
|
||||
var script = frame.eval("f").return.script;
|
||||
var script = frame.script;
|
||||
var byOffset = [];
|
||||
script.getAllColumnOffsets().forEach(function (entry) {
|
||||
var {lineNumber, columnNumber, offset} = entry;
|
||||
var location = script.getOffsetLocation(offset);
|
||||
assertEq(location.lineNumber, lineNumber);
|
||||
assertEq(location.columnNumber, columnNumber);
|
||||
byOffset[offset] = {lineNumber, columnNumber};
|
||||
});
|
||||
|
||||
frame.onStep = function() {
|
||||
var offset = frame.offset;
|
||||
var location = script.getOffsetLocation(offset);
|
||||
if (location.isEntryPoint) {
|
||||
assertEq(location.lineNumber, byOffset[offset].lineNumber);
|
||||
assertEq(location.columnNumber, byOffset[offset].columnNumber);
|
||||
} else {
|
||||
assertEq(byOffset[offset], undefined);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function test(body) {
|
||||
print("Test: " + body);
|
||||
global.eval(`function f(n) { ${body} } debugger;`);
|
||||
global.eval(`function f(n) { debugger; ${body} }`);
|
||||
global.f(3);
|
||||
}
|
||||
|
||||
|
@ -124,6 +124,7 @@
|
||||
macro(int8, int8, "int8") \
|
||||
macro(int16, int16, "int16") \
|
||||
macro(int32, int32, "int32") \
|
||||
macro(isEntryPoint, isEntryPoint, "isEntryPoint") \
|
||||
macro(isExtensible, isExtensible, "isExtensible") \
|
||||
macro(iteratorIntrinsic, iteratorIntrinsic, "__iterator__") \
|
||||
macro(join, join, "join") \
|
||||
|
@ -4856,53 +4856,71 @@ class BytecodeRangeWithPosition : private BytecodeRange
|
||||
|
||||
BytecodeRangeWithPosition(JSContext* cx, JSScript* script)
|
||||
: BytecodeRange(cx, script), lineno(script->lineno()), column(0),
|
||||
sn(script->notes()), snpc(script->code())
|
||||
sn(script->notes()), snpc(script->code()), isEntryPoint(false)
|
||||
{
|
||||
if (!SN_IS_TERMINATOR(sn))
|
||||
snpc += SN_DELTA(sn);
|
||||
updatePosition();
|
||||
while (frontPC() != script->main())
|
||||
popFront();
|
||||
isEntryPoint = true;
|
||||
}
|
||||
|
||||
void popFront() {
|
||||
BytecodeRange::popFront();
|
||||
if (!empty())
|
||||
if (empty())
|
||||
isEntryPoint = false;
|
||||
else
|
||||
updatePosition();
|
||||
}
|
||||
|
||||
size_t frontLineNumber() const { return lineno; }
|
||||
size_t frontColumnNumber() const { return column; }
|
||||
|
||||
// Entry points are restricted to bytecode offsets that have an
|
||||
// explicit mention in the line table. This restriction avoids a
|
||||
// number of failing cases caused by some instructions not having
|
||||
// sensible (to the user) line numbers, and it is one way to
|
||||
// implement the idea that the bytecode emitter should tell the
|
||||
// debugger exactly which offsets represent "interesting" (to the
|
||||
// user) places to stop.
|
||||
bool frontIsEntryPoint() const { return isEntryPoint; }
|
||||
|
||||
private:
|
||||
void updatePosition() {
|
||||
/*
|
||||
* Determine the current line number by reading all source notes up to
|
||||
* and including the current offset.
|
||||
*/
|
||||
jsbytecode *lastLinePC = nullptr;
|
||||
while (!SN_IS_TERMINATOR(sn) && snpc <= frontPC()) {
|
||||
SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
|
||||
if (type == SRC_COLSPAN) {
|
||||
ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, 0));
|
||||
MOZ_ASSERT(ptrdiff_t(column) + colspan >= 0);
|
||||
column += colspan;
|
||||
lastLinePC = snpc;
|
||||
} else if (type == SRC_SETLINE) {
|
||||
lineno = size_t(GetSrcNoteOffset(sn, 0));
|
||||
column = 0;
|
||||
lastLinePC = snpc;
|
||||
} else if (type == SRC_NEWLINE) {
|
||||
lineno++;
|
||||
column = 0;
|
||||
lastLinePC = snpc;
|
||||
}
|
||||
|
||||
sn = SN_NEXT(sn);
|
||||
snpc += SN_DELTA(sn);
|
||||
}
|
||||
isEntryPoint = lastLinePC == frontPC();
|
||||
}
|
||||
|
||||
size_t lineno;
|
||||
size_t column;
|
||||
jssrcnote* sn;
|
||||
jsbytecode* snpc;
|
||||
bool isEntryPoint;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -5083,6 +5101,10 @@ DebuggerScript_getOffsetLocation(JSContext* cx, unsigned argc, Value* vp)
|
||||
if (!ScriptOffset(cx, script, args[0], &offset))
|
||||
return false;
|
||||
|
||||
FlowGraphSummary flowData(cx);
|
||||
if (!flowData.populate(cx, script))
|
||||
return false;
|
||||
|
||||
RootedPlainObject result(cx, NewBuiltinClassInstance<PlainObject>(cx));
|
||||
if (!result)
|
||||
return false;
|
||||
@ -5100,6 +5122,15 @@ DebuggerScript_getOffsetLocation(JSContext* cx, unsigned argc, Value* vp)
|
||||
if (!DefineProperty(cx, result, cx->names().columnNumber, value))
|
||||
return false;
|
||||
|
||||
// The same entry point test that is used by getAllColumnOffsets.
|
||||
bool isEntryPoint = (r.frontIsEntryPoint() &&
|
||||
!flowData[offset].hasNoEdges() &&
|
||||
(flowData[offset].lineno() != r.frontLineNumber() ||
|
||||
flowData[offset].column() != r.frontColumnNumber()));
|
||||
value.setBoolean(isEntryPoint);
|
||||
if (!DefineProperty(cx, result, cx->names().isEntryPoint, value))
|
||||
return false;
|
||||
|
||||
args.rval().setObject(*result);
|
||||
return true;
|
||||
}
|
||||
@ -5122,6 +5153,9 @@ DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc, Value* vp)
|
||||
if (!result)
|
||||
return false;
|
||||
for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
|
||||
if (!r.frontIsEntryPoint())
|
||||
continue;
|
||||
|
||||
size_t offset = r.frontOffset();
|
||||
size_t lineno = r.frontLineNumber();
|
||||
|
||||
@ -5195,7 +5229,8 @@ DebuggerScript_getAllColumnOffsets(JSContext* cx, unsigned argc, Value* vp)
|
||||
size_t offset = r.frontOffset();
|
||||
|
||||
/* Make a note, if the current instruction is an entry point for the current position. */
|
||||
if (!flowData[offset].hasNoEdges() &&
|
||||
if (r.frontIsEntryPoint() &&
|
||||
!flowData[offset].hasNoEdges() &&
|
||||
(flowData[offset].lineno() != lineno ||
|
||||
flowData[offset].column() != column)) {
|
||||
RootedPlainObject entry(cx, NewBuiltinClassInstance<PlainObject>(cx));
|
||||
@ -5259,6 +5294,9 @@ DebuggerScript_getLineOffsets(JSContext* cx, unsigned argc, Value* vp)
|
||||
if (!result)
|
||||
return false;
|
||||
for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
|
||||
if (!r.frontIsEntryPoint())
|
||||
continue;
|
||||
|
||||
size_t offset = r.frontOffset();
|
||||
|
||||
/* If the op at offset is an entry point, append offset to result. */
|
||||
|
11
testing/eslint-plugin-mozilla/docs/balanced-listeners.rst
Normal file
11
testing/eslint-plugin-mozilla/docs/balanced-listeners.rst
Normal file
@ -0,0 +1,11 @@
|
||||
.. _balanced-listeners:
|
||||
|
||||
==================
|
||||
balanced-listeners
|
||||
==================
|
||||
|
||||
Rule Details
|
||||
------------
|
||||
|
||||
Checks that for every occurences of 'addEventListener' or 'on' there is an
|
||||
occurence of 'removeEventListener' or 'off' with the same event name.
|
@ -4,6 +4,9 @@
|
||||
Mozilla ESLint Plugin
|
||||
=====================
|
||||
|
||||
``balanced-listeners`` checks that every addEventListener has a
|
||||
removeEventListener (and does the same for on/off).
|
||||
|
||||
``components-imports`` adds the filename of imported files e.g.
|
||||
``Cu.import("some/path/Blah.jsm")`` adds Blah to the global scope.
|
||||
|
||||
@ -13,6 +16,8 @@ should be imported by head.js (as far as we can correctly resolve the path).
|
||||
``mark-test-function-used`` simply marks test (the test method) as used. This
|
||||
avoids ESLint telling us that the function is never called.
|
||||
|
||||
``no-aArgs`` prevents using the hungarian notation in function arguments.
|
||||
|
||||
``var-only-at-top-level`` Marks all var declarations that are not at the top
|
||||
level invalid.
|
||||
|
||||
@ -31,6 +36,7 @@ level invalid.
|
||||
Example configuration::
|
||||
|
||||
"rules": {
|
||||
"mozilla/balanced-listeners": 2,
|
||||
"mozilla/components-imports": 1,
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
"mozilla/mark-test-function-used": 1,
|
||||
@ -40,7 +46,9 @@ Example configuration::
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
balanced-listeners
|
||||
components-imports
|
||||
import-headjs-globals
|
||||
mark-test-function-used
|
||||
no-aArgs
|
||||
var-only-at-top-level
|
||||
|
12
testing/eslint-plugin-mozilla/docs/no-aArgs.rst
Normal file
12
testing/eslint-plugin-mozilla/docs/no-aArgs.rst
Normal file
@ -0,0 +1,12 @@
|
||||
.. _no-aArgs:
|
||||
|
||||
========
|
||||
no-aArgs
|
||||
========
|
||||
|
||||
Rule Details
|
||||
------------
|
||||
|
||||
Checks that function argument names don't start with lowercase 'a' followed by a
|
||||
capital letter. This is to prevent the use of Hungarian notation whereby the
|
||||
first letter is a prefix that indicates the type or intended use of a variable.
|
@ -13,15 +13,19 @@
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"balanced-listeners": require("../lib/rules/balanced-listeners"),
|
||||
"components-imports": require("../lib/rules/components-imports"),
|
||||
"import-headjs-globals": require("../lib/rules/import-headjs-globals"),
|
||||
"mark-test-function-used": require("../lib/rules/mark-test-function-used"),
|
||||
"no-aArgs": require("../lib/rules/no-aArgs"),
|
||||
"var-only-at-top-level": require("../lib/rules/var-only-at-top-level")
|
||||
},
|
||||
rulesConfig: {
|
||||
"balanced-listeners": 0,
|
||||
"components-imports": 0,
|
||||
"import-headjs-globals": 0,
|
||||
"mark-test-function-used": 0,
|
||||
"no-aArgs": 0,
|
||||
"var-only-at-top-level": 0
|
||||
}
|
||||
};
|
||||
|
107
testing/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
Normal file
107
testing/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @fileoverview Check that there's a removeEventListener for each
|
||||
* addEventListener and an off for each on.
|
||||
* Note that for now, this rule is rather simple in that it only checks that
|
||||
* for each event name there is both an add and remove listener. It doesn't
|
||||
* check that these are called on the right objects or with the same callback.
|
||||
*
|
||||
* 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";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(context) {
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
var DICTIONARY = {
|
||||
"addEventListener": "removeEventListener",
|
||||
"on": "off"
|
||||
};
|
||||
// Invert this dictionary to make it easy later.
|
||||
var INVERTED_DICTIONARY = {};
|
||||
for (var i in DICTIONARY) {
|
||||
INVERTED_DICTIONARY[DICTIONARY[i]] = i;
|
||||
}
|
||||
|
||||
// Collect the add/remove listeners in these 2 arrays.
|
||||
var addedListeners = [];
|
||||
var removedListeners = [];
|
||||
|
||||
function addAddedListener(node) {
|
||||
addedListeners.push({
|
||||
functionName: node.callee.property.name,
|
||||
type: node.arguments[0].value,
|
||||
node: node.callee.property,
|
||||
useCapture: node.arguments[2] ? node.arguments[2].value : null
|
||||
});
|
||||
}
|
||||
|
||||
function addRemovedListener(node) {
|
||||
removedListeners.push({
|
||||
functionName: node.callee.property.name,
|
||||
type: node.arguments[0].value,
|
||||
useCapture: node.arguments[2] ? node.arguments[2].value : null
|
||||
});
|
||||
}
|
||||
|
||||
function getUnbalancedListeners() {
|
||||
var unbalanced = [];
|
||||
|
||||
for (var i = 0; i < addedListeners.length; i ++) {
|
||||
if (!hasRemovedListener(addedListeners[i])) {
|
||||
unbalanced.push(addedListeners[i]);
|
||||
}
|
||||
}
|
||||
addedListeners = removedListeners = [];
|
||||
|
||||
return unbalanced;
|
||||
}
|
||||
|
||||
function hasRemovedListener(addedListener) {
|
||||
for (var i = 0; i < removedListeners.length; i ++) {
|
||||
var listener = removedListeners[i];
|
||||
if (DICTIONARY[addedListener.functionName] === listener.functionName &&
|
||||
addedListener.type === listener.type &&
|
||||
addedListener.useCapture === listener.useCapture) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
CallExpression: function(node) {
|
||||
if (node.callee.type === "MemberExpression") {
|
||||
var listenerMethodName = node.callee.property.name;
|
||||
|
||||
if (DICTIONARY.hasOwnProperty(listenerMethodName)) {
|
||||
addAddedListener(node);
|
||||
} else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) {
|
||||
addRemovedListener(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"Program:exit": function() {
|
||||
getUnbalancedListeners().forEach(function(listener) {
|
||||
context.report(listener.node,
|
||||
"No corresponding '{{functionName}}({{type}})' was found.", {
|
||||
functionName: DICTIONARY[listener.functionName],
|
||||
type: listener.type
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
50
testing/eslint-plugin-mozilla/lib/rules/no-aArgs.js
Normal file
50
testing/eslint-plugin-mozilla/lib/rules/no-aArgs.js
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @fileoverview warns against using hungarian notation in function arguments
|
||||
* (i.e. aArg).
|
||||
*
|
||||
* 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";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(context) {
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
function isPrefixed(name) {
|
||||
return name.length >= 2 && /^a[A-Z]/.test(name);
|
||||
}
|
||||
|
||||
function deHungarianize(name) {
|
||||
return name.substring(1, 2).toLowerCase() +
|
||||
name.substring(2, name.length);
|
||||
}
|
||||
|
||||
function checkFunction(node) {
|
||||
for (var i = 0; i < node.params.length; i ++) {
|
||||
var param = node.params[i];
|
||||
if (param.name && isPrefixed(param.name)) {
|
||||
context.report(param, "Parameter '{{name}}' uses Hungarian Notation, consider using '{{suggestion}}' instead.", {
|
||||
name: param.name,
|
||||
suggestion: deHungarianize(param.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"FunctionDeclaration": checkFunction,
|
||||
"ArrowFunctionExpression": checkFunction,
|
||||
"FunctionExpression": checkFunction
|
||||
};
|
||||
};
|
@ -110,7 +110,7 @@ def decorate_task_treeherder_routes(task, suffix):
|
||||
for env in treeheder_env:
|
||||
task['routes'].append('{}.{}'.format(TREEHERDER_ROUTES[env], suffix))
|
||||
|
||||
def decorate_task_json_routes(build, task, json_routes, parameters):
|
||||
def decorate_task_json_routes(task, json_routes, parameters):
|
||||
"""
|
||||
Decorate the given task with routes.json routes.
|
||||
|
||||
@ -118,15 +118,9 @@ def decorate_task_json_routes(build, task, json_routes, parameters):
|
||||
:param json_routes: the list of routes to use from routes.json
|
||||
:param parameters: dictionary of parameters to use in route templates
|
||||
"""
|
||||
fmt = parameters.copy()
|
||||
fmt.update({
|
||||
'build_product': task['extra']['build_product'],
|
||||
'build_name': build['build_name'],
|
||||
'build_type': build['build_type'],
|
||||
})
|
||||
routes = task.get('routes', [])
|
||||
for route in json_routes:
|
||||
routes.append(route.format(**fmt))
|
||||
routes.append(route.format(**parameters))
|
||||
|
||||
task['routes'] = routes
|
||||
|
||||
@ -436,6 +430,14 @@ class Graph(object):
|
||||
build_parameters = dict(parameters)
|
||||
build_parameters['build_slugid'] = slugid()
|
||||
build_task = templates.load(build['task'], build_parameters)
|
||||
|
||||
# Copy build_* attributes to expose them to post-build tasks
|
||||
# as well as json routes and tests
|
||||
task_extra = build_task['task']['extra']
|
||||
build_parameters['build_name'] = task_extra['build_name']
|
||||
build_parameters['build_type'] = task_extra['build_type']
|
||||
build_parameters['build_product'] = task_extra['build_product']
|
||||
|
||||
set_interactive_task(build_task, interactive)
|
||||
|
||||
# try builds don't use cache
|
||||
@ -445,8 +447,7 @@ class Graph(object):
|
||||
if params['revision_hash']:
|
||||
decorate_task_treeherder_routes(build_task['task'],
|
||||
treeherder_route)
|
||||
decorate_task_json_routes(build,
|
||||
build_task['task'],
|
||||
decorate_task_json_routes(build_task['task'],
|
||||
json_routes,
|
||||
build_parameters)
|
||||
|
||||
|
@ -57,6 +57,8 @@ task:
|
||||
|
||||
extra:
|
||||
build_product: '{{build_product}}'
|
||||
build_name: '{{build_name}}'
|
||||
build_type: '{{build_type}}'
|
||||
index:
|
||||
rank: {{pushlog_id}}
|
||||
treeherder:
|
||||
|
@ -57,6 +57,8 @@ task:
|
||||
|
||||
extra:
|
||||
build_product: 'b2g'
|
||||
build_name: '{{build_name}}'
|
||||
build_type: '{{build_type}}'
|
||||
index:
|
||||
rank: {{pushlog_id}}
|
||||
treeherder:
|
||||
|
@ -17,6 +17,9 @@ task:
|
||||
provisionerId: aws-provisioner-v1
|
||||
schedulerId: task-graph-scheduler
|
||||
|
||||
routes:
|
||||
- 'index.gecko.v1.{{project}}.latest.simulator.{{build_type}}'
|
||||
|
||||
scopes:
|
||||
- 'docker-worker:cache:tc-vcs'
|
||||
- 'docker-worker:image:{{#docker_image}}builder{{/docker_image}}'
|
||||
|
@ -4,15 +4,25 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var SharedAll;
|
||||
var Primitives;
|
||||
if (typeof Components != "undefined") {
|
||||
throw new Error("This file is meant to be loaded in a worker");
|
||||
}
|
||||
if (!module || !exports) {
|
||||
throw new Error("Please load this module with require()");
|
||||
}
|
||||
let Cu = Components.utils;
|
||||
SharedAll = {};
|
||||
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
|
||||
Cu.import("resource://gre/modules/lz4_internal.js");
|
||||
Cu.import("resource://gre/modules/ctypes.jsm");
|
||||
|
||||
const SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
|
||||
const Internals = require("resource://gre/modules/workers/lz4_internal.js");
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"Lz4"
|
||||
];
|
||||
this.exports = {};
|
||||
} else if (typeof module != "undefined" && typeof require != "undefined") {
|
||||
SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
|
||||
Primitives = require("resource://gre/modules/lz4_internal.js");
|
||||
} else {
|
||||
throw new Error("Please load this module with Component.utils.import or with require()");
|
||||
}
|
||||
|
||||
const MAGIC_NUMBER = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); // "mozLz4a\0"
|
||||
|
||||
@ -76,12 +86,12 @@ function compressFileContent(array, options = {}) {
|
||||
} else {
|
||||
throw new TypeError("compressFileContent requires a size");
|
||||
}
|
||||
let maxCompressedSize = Internals.maxCompressedSize(inputBytes);
|
||||
let maxCompressedSize = Primitives.maxCompressedSize(inputBytes);
|
||||
let outputArray = new Uint8Array(HEADER_SIZE + maxCompressedSize);
|
||||
|
||||
// Compress to output array
|
||||
let payload = new Uint8Array(outputArray.buffer, outputArray.byteOffset + HEADER_SIZE);
|
||||
let compressedSize = Internals.compress(array, inputBytes, payload);
|
||||
let compressedSize = Primitives.compress(array, inputBytes, payload);
|
||||
|
||||
// Add headers
|
||||
outputArray.set(MAGIC_NUMBER);
|
||||
@ -125,12 +135,19 @@ function decompressFileContent(array, options = {}) {
|
||||
let decompressedBytes = (new SharedAll.Type.size_t.implementation(0));
|
||||
|
||||
// Decompress
|
||||
let success = Internals.decompress(inputData, bytes - HEADER_SIZE,
|
||||
outputBuffer, outputBuffer.byteLength,
|
||||
decompressedBytes.address());
|
||||
let success = Primitives.decompress(inputData, bytes - HEADER_SIZE,
|
||||
outputBuffer, outputBuffer.byteLength,
|
||||
decompressedBytes.address());
|
||||
if (!success) {
|
||||
throw new LZError("decompress", "becauseLZInvalidContent", "Invalid content:Decompression stopped at " + decompressedBytes.value);
|
||||
}
|
||||
return new Uint8Array(outputBuffer.buffer, outputBuffer.byteOffset, decompressedBytes.value);
|
||||
}
|
||||
exports.decompressFileContent = decompressFileContent;
|
||||
|
||||
if (typeof Components != "undefined") {
|
||||
this.Lz4 = {
|
||||
compressFileContent: compressFileContent,
|
||||
decompressFileContent: decompressFileContent
|
||||
};
|
||||
}
|
@ -4,19 +4,28 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var Primitives = {};
|
||||
|
||||
var SharedAll;
|
||||
if (typeof Components != "undefined") {
|
||||
throw new Error("This file is meant to be loaded in a worker");
|
||||
}
|
||||
if (!module || !exports) {
|
||||
throw new Error("Please load this module with require()");
|
||||
let Cu = Components.utils;
|
||||
SharedAll = {};
|
||||
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"Primitives"
|
||||
];
|
||||
this.Primitives = Primitives;
|
||||
this.exports = {};
|
||||
} else if (typeof module != "undefined" && typeof require != "undefined") {
|
||||
SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
|
||||
} else {
|
||||
throw new Error("Please load this module with Component.utils.import or with require()");
|
||||
}
|
||||
|
||||
var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
|
||||
var libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul);
|
||||
var Type = SharedAll.Type;
|
||||
|
||||
var Primitives = {};
|
||||
|
||||
libxul.declareLazyFFI(Primitives, "compress",
|
||||
"workerlz4_compress",
|
||||
null,
|
||||
@ -44,14 +53,16 @@ libxul.declareLazyFFI(Primitives, "maxCompressedSize",
|
||||
/*inputSize*/ Type.size_t
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
get compress() {
|
||||
return Primitives.compress;
|
||||
},
|
||||
get decompress() {
|
||||
return Primitives.decompress;
|
||||
},
|
||||
get maxCompressedSize() {
|
||||
return Primitives.maxCompressedSize;
|
||||
}
|
||||
};
|
||||
if (typeof module != "undefined") {
|
||||
module.exports = {
|
||||
get compress() {
|
||||
return Primitives.compress;
|
||||
},
|
||||
get decompress() {
|
||||
return Primitives.decompress;
|
||||
},
|
||||
get maxCompressedSize() {
|
||||
return Primitives.maxCompressedSize;
|
||||
}
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
|
||||
|
||||
EXTRA_JS_MODULES.workers += [
|
||||
EXTRA_JS_MODULES += [
|
||||
'lz4.js',
|
||||
'lz4_internal.js',
|
||||
]
|
@ -44,8 +44,8 @@ self.onmessage = function() {
|
||||
var Lz4;
|
||||
var Internals;
|
||||
function test_import() {
|
||||
Lz4 = require("resource://gre/modules/workers/lz4.js");
|
||||
Internals = require("resource://gre/modules/workers/lz4_internal.js");
|
||||
Lz4 = require("resource://gre/modules/lz4.js");
|
||||
Internals = require("resource://gre/modules/lz4_internal.js");
|
||||
}
|
||||
|
||||
function test_bound() {
|
41
toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js
Normal file
41
toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const Cu = Components.utils;
|
||||
Cu.import("resource://gre/modules/lz4.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function compare_arrays(a, b) {
|
||||
return Array.prototype.join.call(a) == Array.prototype.join.call(a);
|
||||
}
|
||||
|
||||
add_task(function() {
|
||||
let path = OS.Path.join("data", "compression.lz");
|
||||
let data = yield OS.File.read(path);
|
||||
let decompressed = Lz4.decompressFileContent(data);
|
||||
let text = (new TextDecoder()).decode(decompressed);
|
||||
do_check_eq(text, "Hello, lz4");
|
||||
});
|
||||
|
||||
add_task(function() {
|
||||
for (let length of [0, 1, 1024]) {
|
||||
let array = new Uint8Array(length);
|
||||
for (let i = 0; i < length; ++i) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
|
||||
let compressed = Lz4.compressFileContent(array);
|
||||
do_print("Compressed " + array.byteLength + " bytes into " +
|
||||
compressed.byteLength);
|
||||
|
||||
let decompressed = Lz4.decompressFileContent(compressed);
|
||||
do_print("Decompressed " + compressed.byteLength + " bytes into " +
|
||||
decompressed.byteLength);
|
||||
|
||||
do_check_true(compare_arrays(array, decompressed));
|
||||
}
|
||||
});
|
@ -8,3 +8,4 @@ support-files =
|
||||
data/compression.lz
|
||||
|
||||
[test_lz4.js]
|
||||
[test_lz4_sync.js]
|
@ -31,6 +31,7 @@ DIRS += [
|
||||
'find',
|
||||
'gfx',
|
||||
'jsdownloads',
|
||||
'lz4',
|
||||
'mediasniffer',
|
||||
'microformats',
|
||||
'osfile',
|
||||
@ -56,7 +57,6 @@ DIRS += [
|
||||
'urlformatter',
|
||||
'viewconfig',
|
||||
'workerloader',
|
||||
'workerlz4',
|
||||
'xulstore'
|
||||
]
|
||||
|
||||
|
@ -18,7 +18,7 @@ var SharedAll =
|
||||
require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
|
||||
var Path = require("resource://gre/modules/osfile/ospath.jsm");
|
||||
var Lz4 =
|
||||
require("resource://gre/modules/workers/lz4.js");
|
||||
require("resource://gre/modules/lz4.js");
|
||||
var LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
|
||||
var clone = SharedAll.clone;
|
||||
|
||||
|
@ -4352,10 +4352,13 @@
|
||||
"extended_statistics_ok": true,
|
||||
"description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
|
||||
},
|
||||
"FX_TABLET_MODE_USED_DURING_SESSION": {
|
||||
"expires_in_version": "46",
|
||||
"kind": "count",
|
||||
"description": "Windows 10+ only: The number of times tablet-mode is used during a session"
|
||||
"FX_TABLETMODE_PAGE_LOAD": {
|
||||
"expires_in_version": "47",
|
||||
"kind": "exponential",
|
||||
"high": 100000,
|
||||
"n_buckets": 30,
|
||||
"keyed": true,
|
||||
"description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
|
||||
},
|
||||
"FX_TOUCH_USED": {
|
||||
"expires_in_version": "46",
|
||||
@ -5705,13 +5708,33 @@
|
||||
"kind": "boolean",
|
||||
"description": "Count the number of times the user clicked 'allow' on the hidden-plugin infobar."
|
||||
},
|
||||
"POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS": {
|
||||
"expires_in_version": "40",
|
||||
"kind": "linear",
|
||||
"low": 25,
|
||||
"high": "80 * 25",
|
||||
"n_buckets": "80 + 1",
|
||||
"description": "The time (in milliseconds) after showing a PopupNotification that the mainAction was first triggered"
|
||||
"POPUP_NOTIFICATION_STATS": {
|
||||
"alert_emails": ["firefox-dev@mozilla.org"],
|
||||
"expires_in_version": "48",
|
||||
"kind": "enumerated",
|
||||
"keyed": true,
|
||||
"n_values": 40,
|
||||
"description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
|
||||
},
|
||||
"POPUP_NOTIFICATION_MAIN_ACTION_MS": {
|
||||
"alert_emails": ["firefox-dev@mozilla.org"],
|
||||
"expires_in_version": "48",
|
||||
"kind": "exponential",
|
||||
"keyed": true,
|
||||
"low": 100,
|
||||
"high": 600000,
|
||||
"n_buckets": 40,
|
||||
"description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
|
||||
},
|
||||
"POPUP_NOTIFICATION_DISMISSAL_MS": {
|
||||
"alert_emails": ["firefox-dev@mozilla.org"],
|
||||
"expires_in_version": "48",
|
||||
"kind": "exponential",
|
||||
"keyed": true,
|
||||
"low": 200,
|
||||
"high": 20000,
|
||||
"n_buckets": 50,
|
||||
"description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
|
||||
},
|
||||
"DEVTOOLS_DEBUGGER_RDP_LOCAL_RELOAD_MS": {
|
||||
"expires_in_version": "never",
|
||||
|
@ -492,7 +492,7 @@
|
||||
</xul:hbox>
|
||||
<children includes="popupnotificationcontent"/>
|
||||
<xul:label class="text-link popup-notification-learnmore-link"
|
||||
xbl:inherits="href=learnmoreurl">&learnMore;</xul:label>
|
||||
xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
|
||||
<xul:spacer flex="1"/>
|
||||
<xul:hbox class="popup-notification-button-container"
|
||||
pack="end" align="center">
|
||||
@ -500,7 +500,7 @@
|
||||
<xul:button anonid="button"
|
||||
class="popup-notification-menubutton"
|
||||
type="menu-button"
|
||||
xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
|
||||
xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey">
|
||||
<xul:menupopup anonid="menupopup"
|
||||
xbl:inherits="oncommand=menucommand">
|
||||
<children/>
|
||||
|
@ -7,6 +7,7 @@ this.EXPORTED_SYMBOLS = ["PopupNotifications"];
|
||||
var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const NOTIFICATION_EVENT_DISMISSED = "dismissed";
|
||||
@ -21,6 +22,21 @@ const ICON_ANCHOR_ATTRIBUTE = "popupnotificationanchor";
|
||||
|
||||
const PREF_SECURITY_DELAY = "security.notification_enable_delay";
|
||||
|
||||
// Enumerated values for the POPUP_NOTIFICATION_STATS telemetry histogram.
|
||||
const TELEMETRY_STAT_OFFERED = 0;
|
||||
const TELEMETRY_STAT_ACTION_1 = 1;
|
||||
const TELEMETRY_STAT_ACTION_2 = 2;
|
||||
const TELEMETRY_STAT_ACTION_3 = 3;
|
||||
const TELEMETRY_STAT_ACTION_LAST = 4;
|
||||
const TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE = 5;
|
||||
const TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE = 6;
|
||||
const TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON = 7;
|
||||
const TELEMETRY_STAT_DISMISSAL_NOT_NOW = 8;
|
||||
const TELEMETRY_STAT_OPEN_SUBMENU = 10;
|
||||
const TELEMETRY_STAT_LEARN_MORE = 11;
|
||||
|
||||
const TELEMETRY_STAT_REOPENED_OFFSET = 20;
|
||||
|
||||
var popupNotificationsMap = new WeakMap();
|
||||
var gNotificationParents = new WeakMap;
|
||||
|
||||
@ -54,6 +70,13 @@ function Notification(id, message, anchorID, mainAction, secondaryActions,
|
||||
this.browser = browser;
|
||||
this.owner = owner;
|
||||
this.options = options || {};
|
||||
|
||||
this._dismissed = false;
|
||||
this.wasDismissed = false;
|
||||
this.recordedTelemetryStats = new Set();
|
||||
this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
|
||||
this.browser.ownerDocument.defaultView);
|
||||
this.timeCreated = this.owner.window.performance.now();
|
||||
}
|
||||
|
||||
Notification.prototype = {
|
||||
@ -68,6 +91,20 @@ Notification.prototype = {
|
||||
options: null,
|
||||
timeShown: null,
|
||||
|
||||
/**
|
||||
* Indicates whether the notification is currently dismissed.
|
||||
*/
|
||||
set dismissed(value) {
|
||||
this._dismissed = value;
|
||||
if (value) {
|
||||
// Keep the dismissal into account when recording telemetry.
|
||||
this.wasDismissed = true;
|
||||
}
|
||||
},
|
||||
get dismissed() {
|
||||
return this._dismissed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the notification and updates the popup accordingly if needed.
|
||||
*/
|
||||
@ -95,7 +132,45 @@ Notification.prototype = {
|
||||
|
||||
reshow: function() {
|
||||
this.owner._reshowNotifications(this.anchorElement, this.browser);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a value to the specified histogram, that must be keyed by ID.
|
||||
*/
|
||||
_recordTelemetry(histogramId, value) {
|
||||
if (this.isPrivate) {
|
||||
// The reason why we don't record telemetry in private windows is because
|
||||
// the available actions can be different from regular mode. The main
|
||||
// difference is that all of the persistent permission options like
|
||||
// "Always remember" aren't there, so they really need to be handled
|
||||
// separately to avoid skewing results. For notifications with the same
|
||||
// choices, there would be no reason not to record in private windows as
|
||||
// well, but it's just simpler to use the same check for everything.
|
||||
return;
|
||||
}
|
||||
let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
|
||||
histogram.add("(all)", value);
|
||||
histogram.add(this.id, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an enumerated value to the POPUP_NOTIFICATION_STATS histogram,
|
||||
* ensuring that it is recorded at most once for each distinct Notification.
|
||||
*
|
||||
* Statistics for reopened notifications are recorded in separate buckets.
|
||||
*
|
||||
* @param value
|
||||
* One of the TELEMETRY_STAT_ constants.
|
||||
*/
|
||||
_recordTelemetryStat(value) {
|
||||
if (this.wasDismissed) {
|
||||
value += TELEMETRY_STAT_REOPENED_OFFSET;
|
||||
}
|
||||
if (!this.recordedTelemetryStats.has(value)) {
|
||||
this.recordedTelemetryStats.add(value);
|
||||
this._recordTelemetry("POPUP_NOTIFICATION_STATS", value);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@ -416,6 +491,12 @@ PopupNotifications.prototype = {
|
||||
case "activate":
|
||||
case "TabSelect":
|
||||
let self = this;
|
||||
// This is where we could detect if the panel is dismissed if the page
|
||||
// was switched. Unfortunately, the user usually has clicked elsewhere
|
||||
// at this point so this value only gets recorded for programmatic
|
||||
// reasons, like the "Learn More" link being clicked and resulting in a
|
||||
// tab switch.
|
||||
this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE;
|
||||
// setTimeout(..., 0) needed, otherwise openPopup from "activate" event
|
||||
// handler results in the popup being hidden again for some reason...
|
||||
this.window.setTimeout(function () {
|
||||
@ -465,7 +546,11 @@ PopupNotifications.prototype = {
|
||||
/**
|
||||
* Dismisses the notification without removing it.
|
||||
*/
|
||||
_dismiss: function PopupNotifications_dismiss() {
|
||||
_dismiss: function PopupNotifications_dismiss(telemetryReason) {
|
||||
if (telemetryReason) {
|
||||
this.nextDismissReason = telemetryReason;
|
||||
}
|
||||
|
||||
let browser = this.panel.firstChild &&
|
||||
this.panel.firstChild.notification.browser;
|
||||
this.panel.hidePopup();
|
||||
@ -546,17 +631,21 @@ PopupNotifications.prototype = {
|
||||
popupnotification.setAttribute("label", n.message);
|
||||
popupnotification.setAttribute("id", popupnotificationID);
|
||||
popupnotification.setAttribute("popupid", n.id);
|
||||
popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
|
||||
popupnotification.setAttribute("closebuttoncommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON});`);
|
||||
if (n.mainAction) {
|
||||
popupnotification.setAttribute("buttonlabel", n.mainAction.label);
|
||||
popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
|
||||
popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
|
||||
popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonEvent(event, 'buttoncommand');");
|
||||
popupnotification.setAttribute("buttonpopupshown", "PopupNotifications._onButtonEvent(event, 'buttonpopupshown');");
|
||||
popupnotification.setAttribute("learnmoreclick", "PopupNotifications._onButtonEvent(event, 'learnmoreclick');");
|
||||
popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
|
||||
popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
|
||||
popupnotification.setAttribute("closeitemcommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_NOT_NOW});event.stopPropagation();`);
|
||||
} else {
|
||||
popupnotification.removeAttribute("buttonlabel");
|
||||
popupnotification.removeAttribute("buttonaccesskey");
|
||||
popupnotification.removeAttribute("buttoncommand");
|
||||
popupnotification.removeAttribute("buttonpopupshown");
|
||||
popupnotification.removeAttribute("learnmoreclick");
|
||||
popupnotification.removeAttribute("menucommand");
|
||||
popupnotification.removeAttribute("closeitemcommand");
|
||||
}
|
||||
@ -588,6 +677,8 @@ PopupNotifications.prototype = {
|
||||
popupnotification.notification = n;
|
||||
|
||||
if (n.secondaryActions) {
|
||||
let telemetryStatId = TELEMETRY_STAT_ACTION_2;
|
||||
|
||||
n.secondaryActions.forEach(function (a) {
|
||||
let item = doc.createElementNS(XUL_NS, "menuitem");
|
||||
item.setAttribute("label", a.label);
|
||||
@ -596,6 +687,13 @@ PopupNotifications.prototype = {
|
||||
item.action = a;
|
||||
|
||||
popupnotification.appendChild(item);
|
||||
|
||||
// We can only record a limited number of actions in telemetry. If
|
||||
// there are more, the latest are all recorded in the last bucket.
|
||||
item.action.telemetryStatId = telemetryStatId;
|
||||
if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
|
||||
telemetryStatId++;
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (n.options.hideNotNow) {
|
||||
@ -658,9 +756,18 @@ PopupNotifications.prototype = {
|
||||
// click-to-play plugins, so copy the popupid and use css.
|
||||
this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
|
||||
notificationsToShow.forEach(function (n) {
|
||||
// Record that the notification was actually displayed on screen.
|
||||
// Notifications that were opened a second time or that were originally
|
||||
// shown with "options.dismissed" will be recorded in a separate bucket.
|
||||
n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
|
||||
// Remember the time the notification was shown for the security delay.
|
||||
n.timeShown = this.window.performance.now();
|
||||
}, this);
|
||||
|
||||
// Unless the panel closing is triggered by a specific known code path,
|
||||
// the next reason will be that the user clicked elsewhere.
|
||||
this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE;
|
||||
|
||||
this.panel.openPopup(anchorElement, "bottomcenter topleft");
|
||||
notificationsToShow.forEach(function (n) {
|
||||
this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
|
||||
@ -979,6 +1086,16 @@ PopupNotifications.prototype = {
|
||||
if (notifications.indexOf(notificationObj) == -1)
|
||||
return;
|
||||
|
||||
// Record the time of the first notification dismissal if the main action
|
||||
// was not triggered in the meantime.
|
||||
let timeSinceShown = this.window.performance.now() - notificationObj.timeShown;
|
||||
if (!notificationObj.wasDismissed &&
|
||||
!notificationObj.recordedTelemetryMainAction) {
|
||||
notificationObj._recordTelemetry("POPUP_NOTIFICATION_DISMISSAL_MS",
|
||||
timeSinceShown);
|
||||
}
|
||||
notificationObj._recordTelemetryStat(this.nextDismissReason);
|
||||
|
||||
// Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
|
||||
// if the notification is removed.
|
||||
if (notificationObj.options.removeOnDismissal) {
|
||||
@ -990,7 +1107,7 @@ PopupNotifications.prototype = {
|
||||
}, this);
|
||||
},
|
||||
|
||||
_onButtonCommand: function PopupNotifications_onButtonCommand(event) {
|
||||
_onButtonEvent(event, type) {
|
||||
// Need to find the associated notification object, which is a bit tricky
|
||||
// since it isn't associated with the button directly - this is kind of
|
||||
// gross and very dependent on the structure of the popupnotification
|
||||
@ -1002,27 +1119,42 @@ PopupNotifications.prototype = {
|
||||
notificationEl = parent;
|
||||
|
||||
if (!notificationEl)
|
||||
throw "PopupNotifications_onButtonCommand: couldn't find notification element";
|
||||
throw "PopupNotifications._onButtonEvent: couldn't find notification element";
|
||||
|
||||
if (!notificationEl.notification)
|
||||
throw "PopupNotifications_onButtonCommand: couldn't find notification";
|
||||
throw "PopupNotifications._onButtonEvent: couldn't find notification";
|
||||
|
||||
let notification = notificationEl.notification;
|
||||
let timeSinceShown = this.window.performance.now() - notification.timeShown;
|
||||
|
||||
// Only report the first time mainAction is triggered and remember that this occurred.
|
||||
if (!notification.timeMainActionFirstTriggered) {
|
||||
notification.timeMainActionFirstTriggered = timeSinceShown;
|
||||
Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
|
||||
add(timeSinceShown);
|
||||
if (type == "buttonpopupshown") {
|
||||
notification._recordTelemetryStat(TELEMETRY_STAT_OPEN_SUBMENU);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "learnmoreclick") {
|
||||
notification._recordTelemetryStat(TELEMETRY_STAT_LEARN_MORE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the total timing of the main action since the notification was
|
||||
// created, even if the notification was dismissed in the meantime.
|
||||
let timeSinceCreated = this.window.performance.now() - notification.timeCreated;
|
||||
if (!notification.recordedTelemetryMainAction) {
|
||||
notification.recordedTelemetryMainAction = true;
|
||||
notification._recordTelemetry("POPUP_NOTIFICATION_MAIN_ACTION_MS",
|
||||
timeSinceCreated);
|
||||
}
|
||||
|
||||
let timeSinceShown = this.window.performance.now() - notification.timeShown;
|
||||
if (timeSinceShown < this.buttonDelay) {
|
||||
Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
|
||||
Services.console.logStringMessage("PopupNotifications._onButtonEvent: " +
|
||||
"Button click happened before the security delay: " +
|
||||
timeSinceShown + "ms");
|
||||
return;
|
||||
}
|
||||
|
||||
notification._recordTelemetryStat(TELEMETRY_STAT_ACTION_1);
|
||||
|
||||
try {
|
||||
notification.mainAction.callback.call();
|
||||
} catch(error) {
|
||||
@ -1044,6 +1176,9 @@ PopupNotifications.prototype = {
|
||||
throw "menucommand target has no associated action/notification";
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
target.notification._recordTelemetryStat(target.action.telemetryStatId);
|
||||
|
||||
try {
|
||||
target.action.callback.call();
|
||||
} catch(error) {
|
||||
|
@ -217,7 +217,7 @@ int NS_main(int argc, NS_tchar **argv)
|
||||
}
|
||||
|
||||
if (!NS_tstrcmp(argv[1], NS_T("check-signature"))) {
|
||||
#ifdef XP_WIN
|
||||
#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE)
|
||||
if (ERROR_SUCCESS == VerifyCertificateTrustForFile(argv[2])) {
|
||||
return 0;
|
||||
} else {
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "nsIWidgetListener.h"
|
||||
#include "nsContentUtils.h" // for nsAutoScriptBlocker
|
||||
#include "mozilla/TimelineConsumers.h"
|
||||
#include "mozilla/CompositeTimelineMarker.h"
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
@ -1098,9 +1099,9 @@ nsView::DidCompositeWindow(const TimeStamp& aCompositeStart,
|
||||
|
||||
if (timelines && timelines->HasConsumer(docShell)) {
|
||||
timelines->AddMarkerForDocShell(docShell,
|
||||
"Composite", aCompositeStart, MarkerTracingType::START);
|
||||
MakeUnique<CompositeTimelineMarker>(aCompositeStart, MarkerTracingType::START));
|
||||
timelines->AddMarkerForDocShell(docShell,
|
||||
"Composite", aCompositeEnd, MarkerTracingType::END);
|
||||
MakeUnique<CompositeTimelineMarker>(aCompositeEnd, MarkerTracingType::END));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user