Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2015-10-28 16:41:39 -07:00
commit 5428a9a5a0
88 changed files with 1789 additions and 755 deletions

View File

@ -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() {

View File

@ -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;

View File

@ -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;
}

View File

@ -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,

View File

@ -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}

View File

@ -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");

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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");
},

View File

@ -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]

View File

@ -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);
});
};
}

View File

@ -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");
}
}

View File

@ -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");
}
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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;
});
}

View File

@ -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");
});

View File

@ -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);
}

View File

@ -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,
};
}

View File

@ -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({

View File

@ -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,
});
},

View File

@ -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,
},

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.");
}
/**

View File

@ -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: []
}]
};

View File

@ -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]

View File

@ -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();
});
}

View File

@ -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);
}
/**

View File

@ -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));

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View 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_

View File

@ -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 {

View File

@ -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);

View File

@ -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()) {

View File

@ -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);
}

View File

@ -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*

View File

@ -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);
}

View File

@ -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);
}

View File

@ -8,6 +8,7 @@ EXPORTS.mozilla += [
'AbstractTimelineMarker.h',
'AutoGlobalTimelineMarker.h',
'AutoTimelineMarker.h',
'CompositeTimelineMarker.h',
'ConsoleTimelineMarker.h',
'EventTimelineMarker.h',
'JavascriptTimelineMarker.h',

View File

@ -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");

View File

@ -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;

View File

@ -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

View 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");

View 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");

View File

@ -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

View File

@ -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);
}

View File

@ -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") \

View File

@ -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. */

View 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.

View File

@ -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

View 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.

View File

@ -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
}
};

View 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
});
});
}
};
};

View 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
};
};

View File

@ -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)

View File

@ -57,6 +57,8 @@ task:
extra:
build_product: '{{build_product}}'
build_name: '{{build_name}}'
build_type: '{{build_type}}'
index:
rank: {{pushlog_id}}
treeherder:

View File

@ -57,6 +57,8 @@ task:
extra:
build_product: 'b2g'
build_name: '{{build_name}}'
build_type: '{{build_type}}'
index:
rank: {{pushlog_id}}
treeherder:

View File

@ -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}}'

View File

@ -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
};
}

View File

@ -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;
}
};
}

View File

@ -6,7 +6,7 @@
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES.workers += [
EXTRA_JS_MODULES += [
'lz4.js',
'lz4_internal.js',
]

View File

@ -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() {

View 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));
}
});

View File

@ -8,3 +8,4 @@ support-files =
data/compression.lz
[test_lz4.js]
[test_lz4_sync.js]

View File

@ -31,6 +31,7 @@ DIRS += [
'find',
'gfx',
'jsdownloads',
'lz4',
'mediasniffer',
'microformats',
'osfile',
@ -56,7 +57,6 @@ DIRS += [
'urlformatter',
'viewconfig',
'workerloader',
'workerlz4',
'xulstore'
]

View File

@ -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;

View File

@ -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",

View File

@ -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/>

View File

@ -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) {

View File

@ -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 {

View File

@ -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));
}
}
}