--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -29,22 +29,23 @@ function updateRequest(id, data, batch)
type: UPDATE_REQUEST,
id,
data,
meta: { batch },
};
}
/**
- * Clone the currently selected request, set the "isCustom" attribute.
+ * Clone the request with the given id, set the "isCustom" attribute.
* Used by the "Edit and Resend" feature.
*/
-function cloneSelectedRequest() {
+function cloneSelectedRequest(id) {
return {
- type: CLONE_SELECTED_REQUEST
+ type: CLONE_SELECTED_REQUEST,
+ id
};
}
/**
* Send a new HTTP request using the data in the custom request form.
*/
function sendCustomRequest(connector) {
return (dispatch, getState) => {
--- a/devtools/client/netmonitor/src/components/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/HeadersPanel.js
@@ -162,16 +162,17 @@ class HeadersPanel extends Component {
);
}
render() {
const {
openLink,
cloneSelectedRequest,
request: {
+ id,
fromCache,
fromServiceWorker,
httpVersion,
method,
remoteAddress,
remotePort,
requestHeaders,
requestHeadersFromUploadStream: uploadHeaders,
@@ -224,17 +225,17 @@ class HeadersPanel extends Component {
StatusCode({ item }),
statusCodeDocURL ? MDNLink({
url: statusCodeDocURL,
}) : span({
className: "headers-summary learn-more-link",
}),
button({
className: "devtools-button edit-and-resend-button",
- onClick: cloneSelectedRequest,
+ onClick: () => cloneSelectedRequest(id),
}, EDIT_AND_RESEND),
button({
"aria-pressed": this.state.rawHeadersOpened,
className: toggleRawHeadersClassList.join(" "),
onClick: this.toggleRawHeaders,
}, RAW_HEADERS),
)
);
--- a/devtools/client/netmonitor/src/components/NetworkDetailsPanel.js
+++ b/devtools/client/netmonitor/src/components/NetworkDetailsPanel.js
@@ -73,14 +73,14 @@ NetworkDetailsPanel.propTypes = {
openLink: PropTypes.func,
};
module.exports = connect(
(state) => ({
activeTabId: state.ui.detailsPanelSelectedTab,
request: getSelectedRequest(state),
}),
- (dispatch) => ({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ (dispatch, props) => ({
+ cloneSelectedRequest: (id) => dispatch(Actions.cloneSelectedRequest(id)),
selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
toggleNetworkDetails: () => dispatch(Actions.toggleNetworkDetails()),
}),
)(NetworkDetailsPanel);
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -10,16 +10,17 @@ const PropTypes = require("devtools/clie
const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
const Actions = require("../actions/index");
const { formDataURI } = require("../utils/request-utils");
const {
getDisplayedRequests,
getSelectedRequest,
+ getRequestFromTarget,
getWaterfallScale,
} = require("../selectors/index");
loader.lazyGetter(this, "setImageTooltip", function() {
return require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper")
.setImageTooltip;
});
loader.lazyGetter(this, "getImageDimensions", function() {
@@ -151,25 +152,17 @@ class RequestListContent extends Compone
*
* @param Node target
* The element node currently being hovered.
* @param object tooltip
* The current tooltip instance.
* @return {Promise}
*/
async onHover(target, tooltip) {
- const itemEl = target.closest(".request-list-item");
- if (!itemEl) {
- return false;
- }
- const itemId = itemEl.dataset.id;
- if (!itemId) {
- return false;
- }
- const requestItem = this.props.displayedRequests.find(r => r.id == itemId);
+ const requestItem = getRequestFromTarget(target, this.props.displayedRequests);
if (!requestItem) {
return false;
}
if (!target.closest(".requests-list-file")) {
return false;
}
@@ -182,17 +175,17 @@ class RequestListContent extends Compone
.requestData(requestItem.id, "responseContent");
const { encoding, text } = responseContent.content;
const src = formDataURI(mimeType, encoding, text);
const maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
const { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
const options = { maxDim, naturalWidth, naturalHeight };
setImageTooltip(tooltip, tooltip.doc, src, options);
- return itemEl.querySelector(".requests-list-file");
+ return true;
}
/**
* Scroll listener for the requests menu view.
*/
onScroll() {
this.tooltip.hide();
}
@@ -232,28 +225,32 @@ class RequestListContent extends Compone
evt.preventDefault();
evt.stopPropagation();
this.props.onSelectDelta(delta);
}
}
onContextMenu(evt) {
evt.preventDefault();
- const { selectedRequest, displayedRequests } = this.props;
+
+ const { displayedRequests } = this.props;
+ const selectedRequest = getRequestFromTarget(evt.target, displayedRequests);
if (!this.contextMenu) {
const { connector, cloneSelectedRequest, openStatistics } = this.props;
+
this.contextMenu = new RequestListContextMenu({
connector,
cloneSelectedRequest,
openStatistics,
});
}
this.contextMenu.open(evt, selectedRequest, displayedRequests);
+ return true;
}
/**
* If selection has just changed (by keyboard navigation), don't keep the list
* scrolled to bottom, but allow scrolling up with the selection.
*/
onFocusedNodeChange() {
this.shouldScrollBottom = false;
@@ -291,17 +288,17 @@ class RequestListContent extends Compone
connector,
columns,
item,
index,
isSelected: item.id === (selectedRequest && selectedRequest.id),
key: item.id,
onContextMenu: this.onContextMenu,
onFocusedNodeChange: this.onFocusedNodeChange,
- onMouseDown: () => onItemMouseDown(item.id),
+ onMouseDown: (evt) => onItemMouseDown(evt, item.id),
onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
onWaterfallMouseDown: () => onWaterfallMouseDown(),
requestFilterTypes,
}))
)
)
)
@@ -317,27 +314,33 @@ module.exports = connect(
networkDetailsHeight: state.ui.networkDetailsHeight,
displayedRequests: getDisplayedRequests(state),
firstRequestStartedMillis: state.requests.firstStartedMillis,
selectedRequest: getSelectedRequest(state),
scale: getWaterfallScale(state),
requestFilterTypes: state.filters.requestFilterTypes,
}),
(dispatch, props) => ({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ cloneSelectedRequest: (id) => dispatch(Actions.cloneSelectedRequest(id)),
openStatistics: (open) => dispatch(Actions.openStatistics(props.connector, open)),
/**
* A handler that opens the stack trace tab when a stack trace is available
*/
onCauseBadgeMouseDown: (cause) => {
if (cause.stacktrace && cause.stacktrace.length > 0) {
dispatch(Actions.selectDetailsPanelTab("stack-trace"));
}
},
- onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
+ onItemMouseDown: (evt, id) => {
+ if (evt.button === 0) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ dispatch(Actions.selectRequest(id));
+ }
+ },
/**
* A handler that opens the security tab in the details view if secure or
* broken security indicator is clicked.
*/
onSecurityIconMouseDown: (securityState) => {
if (securityState && securityState !== "insecure") {
dispatch(Actions.selectDetailsPanelTab("security"));
}
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -114,23 +114,19 @@ function requestsReducer(state = Request
return {
...state,
selectedId: action.id,
};
}
// Clone selected request for re-send.
case CLONE_SELECTED_REQUEST: {
- const { requests, selectedId } = state;
+ const { requests } = state;
- if (!selectedId) {
- return state;
- }
-
- const clonedRequest = requests.get(selectedId);
+ const clonedRequest = requests.get(action.id);
if (!clonedRequest) {
return state;
}
const newRequest = {
id: clonedRequest.id + "-clone",
method: clonedRequest.method,
url: clonedRequest.url,
--- a/devtools/client/netmonitor/src/reducers/ui.js
+++ b/devtools/client/netmonitor/src/reducers/ui.js
@@ -12,16 +12,17 @@ const {
ENABLE_PERSISTENT_LOGS,
DISABLE_BROWSER_CACHE,
OPEN_STATISTICS,
REMOVE_SELECTED_CUSTOM_REQUEST,
RESET_COLUMNS,
RESPONSE_HEADERS,
SELECT_DETAILS_PANEL_TAB,
SEND_CUSTOM_REQUEST,
+ CLONE_SELECTED_REQUEST,
SELECT_REQUEST,
TOGGLE_COLUMN,
WATERFALL_RESIZE,
PANELS,
} = require("../constants");
const cols = {
status: true,
@@ -156,16 +157,17 @@ function ui(state = UI(), action) {
case RESET_COLUMNS:
return resetColumns(state);
case REMOVE_SELECTED_CUSTOM_REQUEST:
case SEND_CUSTOM_REQUEST:
return openNetworkDetails(state, { open: false });
case SELECT_DETAILS_PANEL_TAB:
return setDetailsPanelTab(state, action);
case SELECT_REQUEST:
+ case CLONE_SELECTED_REQUEST:
return openNetworkDetails(state, { open: true });
case TOGGLE_COLUMN:
return toggleColumn(state, action);
case WATERFALL_RESIZE:
return resizeWaterfall(state, action);
default:
return state;
}
--- a/devtools/client/netmonitor/src/selectors/requests.js
+++ b/devtools/client/netmonitor/src/selectors/requests.js
@@ -131,16 +131,30 @@ const getSelectedRequest = createSelecto
const isSelectedRequestVisible = createSelector(
state => state.requests,
getDisplayedRequests,
({ selectedId }, displayedRequests) =>
displayedRequests.some(r => r.id === selectedId)
);
+function getRequestFromTarget(target, requests) {
+ const element = target.closest(".request-list-item");
+ if (!element) {
+ return null;
+ }
+
+ const id = element.dataset.id;
+ if (!id) {
+ return null;
+ }
+
+ return requests.find(r => r.id === id);
+}
+
function getRequestById(state, id) {
return state.requests.requests.get(id);
}
function getDisplayedRequestById(state, id) {
return getDisplayedRequests(state).find(r => r.id === id);
}
@@ -152,14 +166,15 @@ function getRecordingState(state) {
return state.requests.recording;
}
module.exports = {
getDisplayedRequestById,
getDisplayedRequests,
getDisplayedRequestsSummary,
getRecordingState,
+ getRequestFromTarget,
getRequestById,
getSelectedRequest,
getSortedRequests,
getTypeFilteredRequests,
isSelectedRequestVisible,
};
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -175,17 +175,17 @@ class RequestListContextMenu {
visible: copySubmenu.slice(10, 14).some((subMenu) => subMenu.visible),
});
menu.push({
id: "request-list-context-resend",
label: L10N.getStr("netmonitor.context.editAndResend"),
accesskey: L10N.getStr("netmonitor.context.editAndResend.accesskey"),
visible: !!(selectedRequest && !isCustom),
- click: cloneSelectedRequest,
+ click: () => cloneSelectedRequest(id),
});
menu.push({
type: "separator",
visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
});
menu.push({
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -35,16 +35,17 @@ support-files =
html_single-get-page.html
html_send-beacon.html
html_sorting-test-page.html
html_statistics-test-page.html
html_status-codes-test-page.html
html_api-calls-test-page.html
html_copy-as-curl.html
html_curl-utils.html
+ html_open-context-menu.html
html_open-request-in-tab.html
sjs_content-type-test-server.sjs
sjs_cors-test-server.sjs
sjs_https-redirect-test-server.sjs
sjs_hsts-test-server.sjs
sjs_json-test-server.sjs
sjs_method-test-server.sjs
sjs_set-cookie-same-site.sjs
@@ -133,16 +134,17 @@ subsuite = clipboard
[browser_net_json-long.js]
[browser_net_json-malformed.js]
[browser_net_json-nogrip.js]
[browser_net_json_custom_mime.js]
[browser_net_json_text_mime.js]
[browser_net_jsonp.js]
[browser_net_large-response.js]
[browser_net_leak_on_tab_close.js]
+[browser_net_open_context_menu.js]
[browser_net_open_in_debugger.js]
[browser_net_open_in_style_editor.js]
[browser_net_open_request_in_tab.js]
[browser_net_pane-collapse.js]
[browser_net_pane-network-details.js]
[browser_net_pane-toggle.js]
[browser_net_pause.js]
skip-if = (verify && debug && (os == 'win'))
--- a/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
+++ b/devtools/client/netmonitor/test/browser_net_copy_as_curl.js
@@ -123,19 +123,18 @@ add_task(async function() {
});
await waitRequest;
}
async function testClipboardContent(expectedResult) {
const { document } = monitor.panelWin;
const items = document.querySelectorAll(".request-list-item");
- EventUtils.sendMouseEvent({ type: "mousedown" }, items[items.length - 1]);
- EventUtils.sendMouseEvent({ type: "contextmenu" },
- document.querySelectorAll(".request-list-item")[0]);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, items[0]);
+ EventUtils.sendMouseEvent({ type: "contextmenu" }, items[items.length - 1]);
/* Ensure that the copy as cURL option is always visible */
const copyUrlParamsNode = monitor.panelWin.parent.document
.querySelector("#request-list-context-copy-as-curl");
is(!!copyUrlParamsNode, true,
"The \"Copy as cURL\" context menu item should not be hidden.");
await waitForClipboardPromise(function setup() {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_open_context_menu.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the right click on request item
+ */
+
+add_task(async function() {
+ const { tab, monitor } = await initNetMonitor(OPEN_CONTEXT_MENU_URL);
+ info("Starting test...");
+
+ const { document, store, windowRequire } = monitor.panelWin;
+ const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+
+ store.dispatch(Actions.batchEnable(false));
+
+ const wait = waitForNetworkEvents(monitor, 1);
+ tab.linkedBrowser.reload();
+ await wait;
+
+ const items = document.querySelectorAll(".request-list-item");
+ EventUtils.sendMouseEvent({ type: "contextmenu" }, items[0]);
+
+ is(!!document.querySelector(".network-details-panel"), false,
+ "The network details panel should be hidden after contextmenu event fired.");
+});
+
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -31,18 +31,17 @@ add_task(async function() {
const origItemId = getSortedRequests(store.getState()).get(0).id;
store.dispatch(Actions.selectRequest(origItemId));
await waitForRequestData(store, ["requestHeaders", "requestPostData"], origItemId);
let origItem = getSortedRequests(store.getState()).get(0);
// add a new custom request cloned from selected request
-
- store.dispatch(Actions.cloneSelectedRequest());
+ store.dispatch(Actions.cloneSelectedRequest(origItemId));
await testCustomForm(origItem);
let customItem = getSelectedRequest(store.getState());
testCustomItem(customItem, origItem);
// edit the custom request
await editCustomForm();
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -51,17 +51,17 @@ add_task(async function() {
await waitUntil(() => {
item = getRequestById(store.getState(), item.id);
return item.requestHeaders && item.responseHeaders;
});
const { size } = getSortedRequests(store.getState());
info("Cloning the selected request into a custom clone");
- store.dispatch(Actions.cloneSelectedRequest());
+ store.dispatch(Actions.cloneSelectedRequest(item.id));
info("Sending the cloned request (without change)");
store.dispatch(Actions.sendCustomRequest(connector));
await waitUntil(() => getSortedRequests(store.getState()).size === size + 1);
}
info("Waiting for both resent requests");
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -67,16 +67,17 @@ const INFINITE_GET_URL = EXAMPLE_URL + "
const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
const SINGLE_GET_URL = EXAMPLE_URL + "html_single-get-page.html";
const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.html";
const SEND_BEACON_URL = EXAMPLE_URL + "html_send-beacon.html";
const CORS_URL = EXAMPLE_URL + "html_cors-test-page.html";
const PAUSE_URL = EXAMPLE_URL + "html_pause-test-page.html";
+const OPEN_CONTEXT_MENU_URL = EXAMPLE_URL + "html_open-context-menu.html";
const OPEN_REQUEST_IN_TAB_URL = EXAMPLE_URL + "html_open-request-in-tab.html";
const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
const SIMPLE_UNSORTED_COOKIES_SJS = EXAMPLE_URL + "sjs_simple-unsorted-cookies-test-server.sjs";
const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
const HTTPS_CONTENT_TYPE_SJS = HTTPS_EXAMPLE_URL + "sjs_content-type-test-server.sjs";
const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_open-context-menu.html
@@ -0,0 +1,17 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Monitor test page</title>
+ </head>
+
+ <body>
+ <p>Open request context menu</p>
+ </body>
+</html>