--- a/devtools/client/netmonitor/src/components/CustomRequestPanel.js
+++ b/devtools/client/netmonitor/src/components/CustomRequestPanel.js
@@ -1,14 +1,15 @@
/* 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 { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { L10N } = require("../utils/l10n");
const Actions = require("../actions/index");
const { getSelectedRequest } = require("../selectors/index");
const {
getUrlQuery,
@@ -25,229 +26,265 @@ const {
const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
-function CustomRequestPanel({
- removeSelectedCustomRequest,
- request = {},
- sendCustomRequest,
- updateRequest,
-}) {
- let {
- method,
- customQueryValue,
- requestHeaders,
- requestPostData,
- url,
- } = request;
-
- let headers = "";
- if (requestHeaders) {
- headers = requestHeaders.customHeadersValue ?
- requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+/*
+ * Custom request panel component
+ * A network request editor which simply provide edit and resend interface
+ * for netowrk development.
+ */
+class CustomRequestPanel extends Component {
+ static get propTypes() {
+ return {
+ connector: PropTypes.object.isRequired,
+ removeSelectedCustomRequest: PropTypes.func.isRequired,
+ request: PropTypes.object,
+ sendCustomRequest: PropTypes.func.isRequired,
+ updateRequest: PropTypes.func.isRequired,
+ };
}
- let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
- let params = customQueryValue;
- if (!params) {
- params = queryArray ?
- queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
- }
- let postData = requestPostData && requestPostData.postData.text ?
- requestPostData.postData.text : "";
- return (
- div({ className: "custom-request-panel" },
- div({ className: "tabpanel-summary-container custom-request" },
- div({ className: "custom-request-label custom-header" },
- CUSTOM_NEW_REQUEST
- ),
- button({
- className: "devtools-button",
- id: "custom-request-send-button",
- onClick: sendCustomRequest,
- },
- CUSTOM_SEND
- ),
- button({
- className: "devtools-button",
- id: "custom-request-close-button",
- onClick: removeSelectedCustomRequest,
- },
- CUSTOM_CANCEL
- ),
- ),
- div({
- className: "tabpanel-summary-container custom-method-and-url",
- id: "custom-method-and-url",
- },
- input({
- className: "custom-method-value",
- id: "custom-method-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- value: method || "GET",
- }),
- input({
- className: "custom-url-value",
- id: "custom-url-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- value: url || "http://",
- }),
- ),
- // Hide query field when there is no params
- params ? div({
- className: "tabpanel-summary-container custom-section",
- id: "custom-query",
- },
- div({ className: "custom-request-label" }, CUSTOM_QUERY),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-query-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 4,
- value: params,
- wrap: "off",
- })
- ) : null,
- div({
- id: "custom-headers",
- className: "tabpanel-summary-container custom-section",
- },
- div({ className: "custom-request-label" }, CUSTOM_HEADERS),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-headers-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 8,
- value: headers,
- wrap: "off",
- })
- ),
- div({
- id: "custom-postdata",
- className: "tabpanel-summary-container custom-section",
- },
- div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
- textarea({
- className: "tabpanel-summary-input",
- id: "custom-postdata-value",
- onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
- rows: 6,
- value: postData,
- wrap: "off",
- })
- ),
- )
- );
-}
+ componentDidMount() {
+ this.maybeFetchPostData(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.maybeFetchPostData(nextProps);
+ }
-CustomRequestPanel.displayName = "CustomRequestPanel";
-
-CustomRequestPanel.propTypes = {
- connector: PropTypes.object.isRequired,
- removeSelectedCustomRequest: PropTypes.func.isRequired,
- request: PropTypes.object,
- sendCustomRequest: PropTypes.func.isRequired,
- updateRequest: PropTypes.func.isRequired,
-};
-
-/**
- * Parse a text representation of a name[divider]value list with
- * the given name regex and divider character.
- *
- * @param {string} text - Text of list
- * @return {array} array of headers info {name, value}
- */
-function parseRequestText(text, namereg, divider) {
- let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
- let pairs = [];
-
- for (let line of text.split("\n")) {
- let matches = regex.exec(line);
- if (matches) {
- let [, name, value] = matches;
- pairs.push({ name, value });
+ /**
+ * When switching to another request, lazily fetch request post data
+ * from the backend. The panel will first be empty and then display the content.
+ */
+ maybeFetchPostData(props) {
+ if (props.request.requestPostDataAvailable &&
+ (!props.request.requestPostData ||
+ !props.request.requestPostData.postData.text)) {
+ // This method will set `props.request.requestPostData`
+ // asynchronously and force another render.
+ props.connector.requestData(props.request.id, "requestPostData");
}
}
- return pairs;
-}
+
+ /**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param {string} text - Text of list
+ * @return {array} array of headers info {name, value}
+ */
+ parseRequestText(text, namereg, divider) {
+ let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
+ let pairs = [];
+
+ for (let line of text.split("\n")) {
+ let matches = regex.exec(line);
+ if (matches) {
+ let [, name, value] = matches;
+ pairs.push({ name, value });
+ }
+ }
+ return pairs;
+ }
+
+ /**
+ * Update Custom Request Fields
+ *
+ * @param {Object} evt click event
+ * @param {Object} request current request
+ * @param {updateRequest} updateRequest action
+ */
+ updateCustomRequestFields(evt, request, updateRequest) {
+ const val = evt.target.value;
+ let data;
-/**
- * Update Custom Request Fields
- *
- * @param {Object} evt click event
- * @param {Object} request current request
- * @param {updateRequest} updateRequest action
- */
-function updateCustomRequestFields(evt, request, updateRequest) {
- const val = evt.target.value;
- let data;
- switch (evt.target.id) {
- case "custom-headers-value":
- let customHeadersValue = val || "";
- // Parse text representation of multiple HTTP headers
- let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
- // Remove temp customHeadersValue while query string is parsable
- if (customHeadersValue === "" ||
+ switch (evt.target.id) {
+ case "custom-headers-value":
+ let customHeadersValue = val || "";
+ // Parse text representation of multiple HTTP headers
+ let headersArray = this.parseRequestText(customHeadersValue, "\\S+?", ":");
+ // Remove temp customHeadersValue while query string is parsable
+ if (customHeadersValue === "" ||
headersArray.length === customHeadersValue.split("\n").length) {
- customHeadersValue = null;
- }
- data = {
- requestHeaders: {
- customHeadersValue,
- headers: headersArray,
+ customHeadersValue = null;
+ }
+ data = {
+ requestHeaders: {
+ customHeadersValue,
+ headers: headersArray,
+ },
+ };
+ break;
+ case "custom-method-value":
+ data = { method: val.trim() };
+ break;
+ case "custom-postdata-value":
+ data = {
+ requestPostData: {
+ postData: { text: val },
+ }
+ };
+ break;
+ case "custom-query-value":
+ let customQueryValue = val || "";
+ // Parse readable text list of a query string
+ let queryArray = customQueryValue ?
+ this.parseRequestText(customQueryValue, ".+?", "=") : [];
+ // Write out a list of query params into a query string
+ let queryString = queryArray.map(
+ ({ name, value }) => name + "=" + value).join("&");
+ let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
+ request.url.split("?")[0];
+ // Remove temp customQueryValue while query string is parsable
+ if (customQueryValue === "" ||
+ queryArray.length === customQueryValue.split("\n").length) {
+ customQueryValue = null;
+ }
+ data = {
+ customQueryValue,
+ url,
+ };
+ break;
+ case "custom-url-value":
+ data = {
+ customQueryValue: null,
+ url: val
+ };
+ break;
+ default:
+ break;
+ }
+ if (data) {
+ // All updateRequest batch mode should be disabled to make UI editing in sync
+ updateRequest(request.id, data, false);
+ }
+ }
+
+ render() {
+ let {
+ removeSelectedCustomRequest,
+ request = {},
+ sendCustomRequest,
+ updateRequest,
+ } = this.props;
+ let {
+ method,
+ customQueryValue,
+ requestHeaders,
+ requestPostData,
+ url,
+ } = request;
+
+ let headers = "";
+ if (requestHeaders) {
+ headers = requestHeaders.customHeadersValue ?
+ requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+ }
+ let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+ let params = customQueryValue;
+ if (!params) {
+ params = queryArray ?
+ queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
+ }
+ let postData = requestPostData && requestPostData.postData.text ?
+ requestPostData.postData.text : "";
+
+ return (
+ div({ className: "custom-request-panel" },
+ div({ className: "tabpanel-summary-container custom-request" },
+ div({ className: "custom-request-label custom-header" },
+ CUSTOM_NEW_REQUEST,
+ ),
+ button({
+ className: "devtools-button",
+ id: "custom-request-send-button",
+ onClick: sendCustomRequest,
+ },
+ CUSTOM_SEND,
+ ),
+ button({
+ className: "devtools-button",
+ id: "custom-request-close-button",
+ onClick: removeSelectedCustomRequest,
+ },
+ CUSTOM_CANCEL,
+ ),
+ ),
+ div({
+ className: "tabpanel-summary-container custom-method-and-url",
+ id: "custom-method-and-url",
},
- };
- break;
- case "custom-method-value":
- data = { method: val.trim() };
- break;
- case "custom-postdata-value":
- data = {
- requestPostData: {
- postData: { text: val },
- }
- };
- break;
- case "custom-query-value":
- let customQueryValue = val || "";
- // Parse readable text list of a query string
- let queryArray = customQueryValue ?
- parseRequestText(customQueryValue, ".+?", "=") : [];
- // Write out a list of query params into a query string
- let queryString = queryArray.map(
- ({ name, value }) => name + "=" + value).join("&");
- let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
- request.url.split("?")[0];
- // Remove temp customQueryValue while query string is parsable
- if (customQueryValue === "" ||
- queryArray.length === customQueryValue.split("\n").length) {
- customQueryValue = null;
- }
- data = {
- customQueryValue,
- url,
- };
- break;
- case "custom-url-value":
- data = {
- customQueryValue: null,
- url: val
- };
- break;
- default:
- break;
- }
- if (data) {
- // All updateRequest batch mode should be disabled to make UI editing in sync
- updateRequest(request.id, data, false);
+ input({
+ className: "custom-method-value",
+ id: "custom-method-value",
+ onChange: (evt) =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ value: method || "GET",
+ }),
+ input({
+ className: "custom-url-value",
+ id: "custom-url-value",
+ onChange: (evt) =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ value: url || "http://",
+ }),
+ ),
+ // Hide query field when there is no params
+ params ? div({
+ className: "tabpanel-summary-container custom-section",
+ id: "custom-query",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_QUERY),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-query-value",
+ onChange: (evt) =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 4,
+ value: params,
+ wrap: "off",
+ }),
+ ) : null,
+ div({
+ id: "custom-headers",
+ className: "tabpanel-summary-container custom-section",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_HEADERS),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-headers-value",
+ onChange: (evt) =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 8,
+ value: headers,
+ wrap: "off",
+ }),
+ ),
+ div({
+ id: "custom-postdata",
+ className: "tabpanel-summary-container custom-section",
+ },
+ div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
+ textarea({
+ className: "tabpanel-summary-input",
+ id: "custom-postdata-value",
+ onChange: (evt) =>
+ this.updateCustomRequestFields(evt, request, updateRequest),
+ rows: 6,
+ value: postData,
+ wrap: "off",
+ }),
+ ),
+ )
+ );
}
}
module.exports = connect(
(state) => ({ request: getSelectedRequest(state) }),
(dispatch, props) => ({
removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
sendCustomRequest: () => dispatch(Actions.sendCustomRequest(props.connector)),
--- a/devtools/client/netmonitor/src/components/HeadersPanel.js
+++ b/devtools/client/netmonitor/src/components/HeadersPanel.js
@@ -44,16 +44,17 @@ const SUMMARY_VERSION = L10N.getStr("net
/*
* Headers panel component
* Lists basic information about the request
*/
class HeadersPanel extends Component {
static get propTypes() {
return {
+ connector: PropTypes.object.isRequired,
cloneSelectedRequest: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
renderValue: PropTypes.func,
openLink: PropTypes.func,
};
}
constructor(props) {
@@ -62,16 +63,40 @@ class HeadersPanel extends Component {
this.state = {
rawHeadersOpened: false,
};
this.getProperties = this.getProperties.bind(this);
this.toggleRawHeaders = this.toggleRawHeaders.bind(this);
this.renderSummary = this.renderSummary.bind(this);
this.renderValue = this.renderValue.bind(this);
+ this.maybeFetchPostData = this.maybeFetchPostData.bind(this);
+ }
+
+ componentDidMount() {
+ this.maybeFetchPostData(this.props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.maybeFetchPostData(nextProps);
+ }
+
+ /**
+ * When switching to another request, lazily fetch request post data
+ * from the backend. The panel will first be empty and then display the content.
+ * Fetching post data is used for updating requestHeadersFromUploadStream section,
+ */
+ maybeFetchPostData(props) {
+ if (props.request.requestPostDataAvailable &&
+ (!props.request.requestPostData ||
+ !props.request.requestPostData.postData.text)) {
+ // This method will set `props.request.requestPostData`
+ // asynchronously and force another render.
+ props.connector.requestData(props.request.id, "requestPostData");
+ }
}
getProperties(headers, title) {
if (headers && headers.headers.length) {
let headerKey = `${title} (${getFormattedSize(headers.headersSize, 3)})`;
let propertiesResult = {
[headerKey]:
headers.headers.reduce((acc, { name, value }) =>
--- a/devtools/client/netmonitor/src/components/ParamsPanel.js
+++ b/devtools/client/netmonitor/src/components/ParamsPanel.js
@@ -41,56 +41,100 @@ class ParamsPanel extends Component {
return {
connector: PropTypes.object.isRequired,
openLink: PropTypes.func,
request: PropTypes.object.isRequired,
updateRequest: PropTypes.func.isRequired,
};
}
+ constructor(props) {
+ super(props);
+ }
+
componentDidMount() {
+ this.maybeFetchPostData(this.props);
updateFormDataSections(this.props);
}
componentWillReceiveProps(nextProps) {
+ this.maybeFetchPostData(nextProps);
updateFormDataSections(nextProps);
}
+ /**
+ * When switching to another request, lazily fetch request post data
+ * from the backend. The panel will first be empty and then display the content.
+ */
+ maybeFetchPostData(props) {
+ if (props.request.requestPostDataAvailable &&
+ (!props.request.requestPostData ||
+ !props.request.requestPostData.postData.text)) {
+ // This method will set `props.request.requestPostData`
+ // asynchronously and force another render.
+ props.connector.requestData(props.request.id, "requestPostData");
+ }
+ }
+
+ /**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ * This function also deal with duplicate key case
+ * (for multiple selection and query params with same keys)
+ *
+ * @param {Object[]} arr - key-value pair array like query or form params
+ * @returns {Object} Rep compatible object
+ */
+ getProperties(arr) {
+ return sortObjectKeys(arr.reduce((map, obj) => {
+ let value = map[obj.name];
+ if (value) {
+ if (typeof value !== "object") {
+ map[obj.name] = [value];
+ }
+ map[obj.name].push(obj.value);
+ } else {
+ map[obj.name] = obj.value;
+ }
+ return map;
+ }, {}));
+ }
+
render() {
let {
openLink,
request
} = this.props;
let {
formDataSections,
mimeType,
requestPostData,
url,
} = request;
let postData = requestPostData ? requestPostData.postData.text : null;
let query = getUrlQuery(url);
- if (!formDataSections && !postData && !query) {
+ if ((!formDataSections || formDataSections.length === 0) && !postData && !query) {
return div({ className: "empty-notice" },
PARAMS_EMPTY_TEXT
);
}
let object = {};
let json;
// Query String section
if (query) {
- object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
+ object[PARAMS_QUERY_STRING] = this.getProperties(parseQueryString(query));
}
// Form Data section
if (formDataSections && formDataSections.length > 0) {
let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
- object[PARAMS_FORM_DATA] = getProperties(parseFormData(sections));
+ object[PARAMS_FORM_DATA] = this.getProperties(parseFormData(sections));
}
// Request payload section
if (formDataSections && formDataSections.length === 0 && postData) {
try {
json = JSON.parse(postData);
} catch (error) {
// Continue regardless of parsing error
@@ -118,37 +162,13 @@ class ParamsPanel extends Component {
sectionNames: SECTION_NAMES,
openLink,
})
)
);
}
}
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- * This function also deal with duplicate key case
- * (for multiple selection and query params with same keys)
- *
- * @param {Object[]} arr - key-value pair array like query or form params
- * @returns {Object} Rep compatible object
- */
-function getProperties(arr) {
- return sortObjectKeys(arr.reduce((map, obj) => {
- let value = map[obj.name];
- if (value) {
- if (typeof value !== "object") {
- map[obj.name] = [value];
- }
- map[obj.name].push(obj.value);
- } else {
- map[obj.name] = obj.value;
- }
- return map;
- }, {}));
-}
-
module.exports = connect(null,
(dispatch) => ({
updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
}),
)(ParamsPanel);
--- a/devtools/client/netmonitor/src/components/RequestListContent.js
+++ b/devtools/client/netmonitor/src/components/RequestListContent.js
@@ -8,16 +8,17 @@ const { Component, createFactory } = req
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
const Actions = require("../actions/index");
const { setTooltipImageContent } = require("../request-list-tooltip");
const {
getDisplayedRequests,
+ getSelectedRequest,
getWaterfallScale,
} = require("../selectors/index");
// Components
const RequestListHeader = createFactory(require("./RequestListHeader"));
const RequestListItem = createFactory(require("./RequestListItem"));
const RequestListContextMenu = require("../request-list-context-menu");
@@ -31,59 +32,57 @@ const MAX_SCROLL_HEIGHT = 2147483647;
/**
* Renders the actual contents of the request list.
*/
class RequestListContent extends Component {
static get propTypes() {
return {
connector: PropTypes.object.isRequired,
columns: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
+ cloneSelectedRequest: PropTypes.func.isRequired,
displayedRequests: PropTypes.array.isRequired,
firstRequestStartedMillis: PropTypes.number.isRequired,
fromCache: PropTypes.bool,
onCauseBadgeMouseDown: PropTypes.func.isRequired,
onItemMouseDown: PropTypes.func.isRequired,
onSecurityIconMouseDown: PropTypes.func.isRequired,
onSelectDelta: PropTypes.func.isRequired,
onWaterfallMouseDown: PropTypes.func.isRequired,
+ openStatistics: PropTypes.func.isRequired,
scale: PropTypes.number,
- selectedRequestId: PropTypes.string,
+ selectedRequest: PropTypes.object,
};
}
constructor(props) {
super(props);
this.isScrolledToBottom = this.isScrolledToBottom.bind(this);
this.onHover = this.onHover.bind(this);
this.onScroll = this.onScroll.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.onFocusedNodeChange = this.onFocusedNodeChange.bind(this);
}
componentWillMount() {
- const { dispatch, connector } = this.props;
+ const { connector, cloneSelectedRequest, openStatistics } = this.props;
this.contextMenu = new RequestListContextMenu({
- cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
- getTabTarget: connector.getTabTarget,
- getLongString: connector.getLongString,
- openStatistics: (open) => dispatch(Actions.openStatistics(connector, open)),
- requestData: connector.requestData,
+ connector,
+ cloneSelectedRequest,
+ openStatistics,
});
this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
}
componentDidMount() {
// Install event handler for displaying a tooltip
this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
interactive: true
});
-
// Install event handler to hide the tooltip on scroll
this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
}
componentWillUpdate(nextProps) {
// Check if the list is scrolled to bottom before the UI update.
// The scroll is ever needed only if new rows are added to the list.
const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
@@ -215,37 +214,37 @@ class RequestListContent extends Compone
columns,
displayedRequests,
firstRequestStartedMillis,
onCauseBadgeMouseDown,
onItemMouseDown,
onSecurityIconMouseDown,
onWaterfallMouseDown,
scale,
- selectedRequestId,
+ selectedRequest,
} = this.props;
return (
- div({ className: "requests-list-wrapper"},
- div({ className: "requests-list-table"},
+ div({ className: "requests-list-wrapper" },
+ div({ className: "requests-list-table" },
div({
ref: "contentEl",
className: "requests-list-contents",
tabIndex: 0,
onKeyDown: this.onKeyDown,
- style: {"--timings-scale": scale, "--timings-rev-scale": 1 / scale}
+ style: { "--timings-scale": scale, "--timings-rev-scale": 1 / scale }
},
RequestListHeader(),
displayedRequests.map((item, index) => RequestListItem({
firstRequestStartedMillis,
fromCache: item.status === "304" || item.fromCache,
columns,
item,
index,
- isSelected: item.id === selectedRequestId,
+ isSelected: item.id === (selectedRequest && selectedRequest.id),
key: item.id,
onContextMenu: this.onContextMenu,
onFocusedNodeChange: this.onFocusedNodeChange,
onMouseDown: () => onItemMouseDown(item.id),
onCauseBadgeMouseDown: () => onCauseBadgeMouseDown(item.cause),
onSecurityIconMouseDown: () => onSecurityIconMouseDown(item.securityState),
onWaterfallMouseDown: () => onWaterfallMouseDown(),
}))
@@ -256,21 +255,22 @@ class RequestListContent extends Compone
}
}
module.exports = connect(
(state) => ({
columns: state.ui.columns,
displayedRequests: getDisplayedRequests(state),
firstRequestStartedMillis: state.requests.firstStartedMillis,
- selectedRequestId: state.requests.selectedId,
+ selectedRequest: getSelectedRequest(state),
scale: getWaterfallScale(state),
}),
- (dispatch) => ({
- dispatch,
+ (dispatch, props) => ({
+ cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+ 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"));
}
},
--- a/devtools/client/netmonitor/src/components/ResponsePanel.js
+++ b/devtools/client/netmonitor/src/components/ResponsePanel.js
@@ -51,27 +51,20 @@ class ResponsePanel extends Component {
height: 0,
},
};
this.updateImageDimemsions = this.updateImageDimemsions.bind(this);
this.isJSON = this.isJSON.bind(this);
}
- /**
- * `componentDidMount` is called when opening the ResponsePanel for the first time
- */
componentDidMount() {
this.maybeFetchResponseContent(this.props);
}
- /**
- * `componentWillReceiveProps` is the only method called when switching between two
- * requests while the response panel is displayed.
- */
componentWillReceiveProps(nextProps) {
this.maybeFetchResponseContent(nextProps);
}
/**
* When switching to another request, lazily fetch response content
* from the backend. The Response Panel will first be empty and then
* display the content.
--- a/devtools/client/netmonitor/src/components/SecurityPanel.js
+++ b/devtools/client/netmonitor/src/components/SecurityPanel.js
@@ -54,27 +54,20 @@ class SecurityPanel extends Component {
static get propTypes() {
return {
connector: PropTypes.object.isRequired,
openLink: PropTypes.func,
request: PropTypes.object.isRequired,
};
}
- /**
- * `componentDidMount` is called when opening the SecurityPanel for the first time
- */
componentDidMount() {
this.maybeFetchSecurityInfo(this.props);
}
- /**
- * `componentWillReceiveProps` is the only method called when switching between two
- * requests while the security panel is displayed.
- */
componentWillReceiveProps(nextProps) {
this.maybeFetchSecurityInfo(nextProps);
}
/**
* When switching to another request, lazily fetch securityInfo
* from the backend. The Security Panel will first be empty and then
* display the content.
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -1,17 +1,16 @@
/* 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 { createFactory } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
const { L10N } = require("../utils/l10n");
const { PANELS } = require("../constants");
// Components
const Tabbar = createFactory(require("devtools/client/shared/components/tabs/TabBar"));
const TabPanel = createFactory(require("devtools/client/shared/components/tabs/Tabs").TabPanel);
const CookiesPanel = createFactory(require("./CookiesPanel"));
const HeadersPanel = createFactory(require("./HeadersPanel"));
@@ -53,17 +52,22 @@ function TabboxPanel({
onSelect: selectTab,
renderOnlySelected: true,
showAllTabsMenu: true,
},
TabPanel({
id: PANELS.HEADERS,
title: HEADERS_TITLE,
},
- HeadersPanel({ request, cloneSelectedRequest, openLink }),
+ HeadersPanel({
+ cloneSelectedRequest,
+ connector,
+ openLink,
+ request,
+ }),
),
TabPanel({
id: PANELS.COOKIES,
title: COOKIES_TITLE,
},
CookiesPanel({ request, openLink }),
),
TabPanel({
@@ -113,9 +117,9 @@ TabboxPanel.propTypes = {
cloneSelectedRequest: PropTypes.func,
connector: PropTypes.object.isRequired,
openLink: PropTypes.func,
request: PropTypes.object,
selectTab: PropTypes.func.isRequired,
sourceMapService: PropTypes.object,
};
-module.exports = connect()(TabboxPanel);
+module.exports = TabboxPanel;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -30,16 +30,17 @@ class FirefoxDataProvider {
this.rdpRequestMap = new Map();
// Map[key string => Promise] used by `requestData` to prevent requesting the same
// request data twice.
this.lazyRequestData = new Map();
// Fetching data from the backend
this.getLongString = this.getLongString.bind(this);
+ this.getRequestFromQueue = this.getRequestFromQueue.bind(this);
// Event handlers
this.onNetworkEvent = this.onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
}
/**
* Add a new network request to application state.
@@ -255,28 +256,25 @@ class FirefoxDataProvider {
isRequestPayloadReady(id) {
let record = this.rdpRequestMap.get(id);
if (!record) {
return false;
}
let { payload } = this.getRequestFromQueue(id);
- // The payload is ready when all values in the record are true. (i.e. all data
- // received, but the lazy one. responseContent is the only one for now).
+ // The payload is ready when all values in the record are true.
// Note that we never fetch response header/cookies for request with security issues.
// Bug 1404917 should simplify this heuristic by making all these field be lazily
// fetched, only on-demand.
- return record.requestHeaders &&
- record.requestCookies &&
- record.eventTimings &&
+ return record.requestHeaders && record.requestCookies && record.eventTimings &&
(
- (record.responseHeaders && record.responseCookies) ||
- payload.securityState == "broken" ||
- (payload.responseContentAvailable && !payload.status)
+ (record.responseHeaders && record.responseCookies) ||
+ payload.securityState === "broken" ||
+ (!payload.status && payload.responseContentAvailable)
);
}
/**
* Merge upcoming networkEventUpdate payload into existing one.
*
* @param {string} id request id
* @param {object} payload request data payload
@@ -379,23 +377,27 @@ class FirefoxDataProvider {
// that started during the pause and we missed its `networkEvent`.
if (!this.rdpRequestMap.has(actor)) {
return;
}
switch (updateType) {
case "requestHeaders":
case "requestCookies":
- case "requestPostData":
case "responseHeaders":
case "responseCookies":
this.requestPayloadData(actor, updateType);
break;
- // (Be careful, securityState can be undefined, for example for WebSocket requests)
- // Also note that service worker don't have security info set.
+ case "requestPostData":
+ this.updateRequest(actor, {
+ // This field helps knowing when/if requestPostData property is available
+ // and can be requested via `requestData`
+ requestPostDataAvailable: true
+ });
+ break;
case "securityInfo":
this.updateRequest(actor, { securityState: networkInfo.securityInfo });
break;
case "responseStart":
this.updateRequest(actor, {
httpVersion: networkInfo.response.httpVersion,
remoteAddress: networkInfo.response.remoteAddress,
remotePort: networkInfo.response.remotePort,
@@ -406,17 +408,16 @@ class FirefoxDataProvider {
emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
});
break;
case "responseContent":
this.updateRequest(actor, {
contentSize: networkInfo.response.bodySize,
transferredSize: networkInfo.response.transferredSize,
mimeType: networkInfo.response.content.mimeType,
-
// This field helps knowing when/if responseContent property is available
// and can be requested via `requestData`
responseContentAvailable: true,
});
break;
case "eventTimings":
this.pushRequestToQueue(actor, { totalTime: networkInfo.totalTime });
this.requestPayloadData(actor, updateType);
@@ -466,19 +467,18 @@ class FirefoxDataProvider {
// or, everytime requestData is called for fetching data lazily.
if (this.isRequestPayloadReady(actor)) {
let payloadFromQueue = this.getRequestFromQueue(actor).payload;
// Clean up
this.cleanUpQueue(actor);
this.rdpRequestMap.delete(actor);
- let { updateRequest } = this.actions;
- if (updateRequest) {
- await updateRequest(actor, payloadFromQueue, true);
+ if (this.actions.updateRequest) {
+ await this.actions.updateRequest(actor, payloadFromQueue, true);
}
// This event is fired only once per request, once all the properties are fetched
// from `onNetworkEventUpdate`. There should be no more RDP requests after this.
emit(EVENTS.PAYLOAD_READY, actor);
}
}
@@ -493,34 +493,36 @@ class FirefoxDataProvider {
* @param {string} actor actor id (used as request id)
* @param {string} method identifier of the data we want to fetch
*
* @return {Promise} return a promise resolved when data is received.
*/
requestData(actor, method) {
// Key string used in `lazyRequestData`. We use this Map to prevent requesting
// the same data twice at the same time.
- let key = actor + "-" + method;
+ let key = `${actor}-${method}`;
let promise = this.lazyRequestData.get(key);
// If a request is pending, reuse it.
if (promise) {
return promise;
}
// Fetch the data
promise = this._requestData(actor, method);
this.lazyRequestData.set(key, promise);
promise.then(async () => {
// Remove the request from the cache, any new call to requestData will fetch the
// data again.
this.lazyRequestData.delete(key, promise);
- let payloadFromQueue = this.getRequestFromQueue(actor).payload;
- let { updateRequest } = this.actions;
- if (updateRequest) {
- await updateRequest(actor, payloadFromQueue, true);
+ if (this.actions.updateRequest) {
+ await this.actions.updateRequest(
+ actor,
+ this.getRequestFromQueue(actor).payload,
+ true,
+ );
}
});
return promise;
}
/**
* Internal helper used to request HTTP details from the backend.
*
@@ -543,24 +545,35 @@ class FirefoxDataProvider {
let updatingEventName = `UPDATING_${method.replace(/([A-Z])/g, "_$1").toUpperCase()}`;
// Emit event that tell we just start fetching some data
emit(EVENTS[updatingEventName], actor);
let response = await new Promise((resolve, reject) => {
// Do a RDP request to fetch data from the actor.
if (typeof this.webConsoleClient[clientMethodName] === "function") {
- this.webConsoleClient[clientMethodName](actor, (res) => {
+ // Make sure we fetch the real actor data instead of cloned actor
+ // e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
+ this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
+ if (res.error) {
+ console.error(res.error);
+ }
resolve(res);
});
} else {
reject(new Error(`Error: No such client method '${clientMethodName}'!`));
}
});
+ // Restore clone actor id
+ if (actor.includes("-clone")) {
+ // Because response's properties are read-only, we create a new response
+ response = { ...response, from: `${response.from}-clone` };
+ }
+
// Call data processing method.
return this[callbackMethodName](response);
}
/**
* Handles additional information received for a "requestHeaders" packet.
*
* @param {object} response the message received from the server.
@@ -586,22 +599,22 @@ class FirefoxDataProvider {
});
}
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param {object} response the message received from the server.
*/
- onRequestPostData(response) {
- return this.updateRequest(response.from, {
+ async onRequestPostData(response) {
+ let payload = await this.updateRequest(response.from, {
requestPostData: response
- }).then(() => {
- emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
});
+ emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+ return payload;
}
/**
* Handles additional information received for a "securityInfo" packet.
*
* @param {object} response the message received from the server.
*/
onSecurityInfo(response) {
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -119,16 +119,17 @@ const UPDATE_PROPS = [
"totalTime",
"eventTimings",
"headersSize",
"customQueryValue",
"requestHeaders",
"requestHeadersFromUploadStream",
"requestCookies",
"requestPostData",
+ "requestPostDataAvailable",
"responseHeaders",
"responseCookies",
"responseContent",
"responseContentAvailable",
"formDataSections",
"stacktrace",
];
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -115,17 +115,17 @@ HarBuilder.prototype = {
buildEntry: async function (log, file) {
let page = this.getPage(log, file);
let entry = {};
entry.pageref = page.id;
entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
entry.time = file.endedMillis - file.startedMillis;
- entry.request = this.buildRequest(file);
+ entry.request = await this.buildRequest(file);
entry.response = await this.buildResponse(file);
entry.cache = this.buildCache(file);
entry.timings = file.eventTimings ? file.eventTimings.timings : {};
if (file.remoteAddress) {
entry.serverIPAddress = file.remoteAddress;
}
@@ -147,43 +147,33 @@ HarBuilder.prototype = {
let timings = {
onContentLoad: -1,
onLoad: -1
};
return timings;
},
- buildRequest: function (file) {
+ buildRequest: async function (file) {
let request = {
bodySize: 0
};
request.method = file.method;
request.url = file.url;
request.httpVersion = file.httpVersion || "";
-
request.headers = this.buildHeaders(file.requestHeaders);
request.headers = this.appendHeadersPostData(request.headers, file);
request.cookies = this.buildCookies(file.requestCookies);
-
request.queryString = parseQueryString(getUrlQuery(file.url)) || [];
-
- if (file.requestPostData) {
- request.postData = this.buildPostData(file);
- }
-
request.headersSize = file.requestHeaders.headersSize;
+ request.postData = await this.buildPostData(file);
- // Set request body size, but make sure the body is fetched
- // from the backend.
- if (file.requestPostData) {
- this.fetchData(file.requestPostData.postData.text).then(value => {
- request.bodySize = value.length;
- });
+ if (request.postData && request.postData.text) {
+ request.bodySize = request.postData.text.length;
}
return request;
},
/**
* Fetch all header values from the backend (if necessary) and
* build the result HAR structure.
@@ -238,57 +228,67 @@ HarBuilder.prototype = {
value: value
});
});
});
return result;
},
- buildPostData: function (file) {
+ buildPostData: async function (file) {
+ // When using HarAutomation, HarCollector will automatically fetch requestPostData,
+ // but when we use it from netmonitor, FirefoxDataProvider should fetch it itself
+ // lazily, via requestData.
+ let requestPostData = file.requestPostData;
+ let requestHeaders = file.requestHeaders;
+ let requestHeadersFromUploadStream;
+
+ if (!requestPostData && this._options.requestData) {
+ let payload = await this._options.requestData(file.id, "requestPostData");
+ requestPostData = payload.requestPostData;
+ requestHeadersFromUploadStream = payload.requestHeadersFromUploadStream;
+ }
+
+ if (!requestPostData.postData.text) {
+ return undefined;
+ }
+
let postData = {
- mimeType: findValue(file.requestHeaders.headers, "content-type"),
+ mimeType: findValue(requestHeaders.headers, "content-type"),
params: [],
- text: ""
+ text: requestPostData.postData.text,
};
- if (!file.requestPostData) {
- return postData;
- }
-
- if (file.requestPostData.postDataDiscarded) {
+ if (requestPostData.postDataDiscarded) {
postData.comment = L10N.getStr("har.requestBodyNotIncluded");
return postData;
}
- // Load request body from the backend.
- this.fetchData(file.requestPostData.postData.text).then(postDataText => {
- postData.text = postDataText;
-
- // If we are dealing with URL encoded body, parse parameters.
- let { headers } = file.requestHeaders;
- if (CurlUtils.isUrlEncodedRequest({ headers, postDataText })) {
- postData.mimeType = "application/x-www-form-urlencoded";
+ // If we are dealing with URL encoded body, parse parameters.
+ if (CurlUtils.isUrlEncodedRequest({
+ headers: requestHeaders.headers,
+ postDataText: postData.text,
+ })) {
+ postData.mimeType = "application/x-www-form-urlencoded";
- // Extract form parameters and produce nice HAR array.
- getFormDataSections(
- file.requestHeaders,
- file.requestHeadersFromUploadStream,
- file.requestPostData,
- this._options.getString,
- ).then(formDataSections => {
- formDataSections.forEach(section => {
- let paramsArray = parseQueryString(section);
- if (paramsArray) {
- postData.params = [...postData.params, ...paramsArray];
- }
- });
- });
- }
- });
+ // Extract form parameters and produce nice HAR array.
+ let formDataSections = await getFormDataSections(
+ requestHeaders,
+ requestHeadersFromUploadStream,
+ requestPostData,
+ this._options.getString,
+ );
+
+ formDataSections.forEach((section) => {
+ let paramsArray = parseQueryString(section);
+ if (paramsArray) {
+ postData.params = [...postData.params, ...paramsArray];
+ }
+ });
+ }
return postData;
},
buildResponse: async function (file) {
let response = {
status: 0
};
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_copy_all_as_har.js
@@ -14,28 +14,28 @@ add_task(function* () {
let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
info("Starting test... ");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let RequestListContextMenu = windowRequire(
"devtools/client/netmonitor/src/request-list-context-menu");
- let { getLongString, getTabTarget, requestData } = connector;
+ let { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
yield wait;
- let contextMenu = new RequestListContextMenu({
- getTabTarget, getLongString, requestData });
+ let contextMenu = new RequestListContextMenu({ connector });
- yield contextMenu.copyAllAsHar();
+ yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
let jsonString = SpecialPowers.getClipboardData("text/unicode");
let har = JSON.parse(jsonString);
// Check out HAR log
isnot(har.log, null, "The HAR log must exist");
is(har.log.creator.name, "Firefox", "The creator field must be set");
is(har.log.browser.name, "Firefox", "The browser field must be set");
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data.js
@@ -11,31 +11,31 @@ add_task(function* () {
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... ");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let RequestListContextMenu = windowRequire(
"devtools/client/netmonitor/src/request-list-context-menu");
- let { getLongString, getTabTarget, requestData } = connector;
+ let { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
// Execute one POST request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 0, 1);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.executeTest();
});
yield wait;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({
- getTabTarget, getLongString, requestData });
- let jsonString = yield contextMenu.copyAllAsHar();
+ let contextMenu = new RequestListContextMenu({ connector });
+ let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_post_data_on_get.js
@@ -11,37 +11,37 @@ add_task(function* () {
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... ");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let RequestListContextMenu = windowRequire(
"devtools/client/netmonitor/src/request-list-context-menu");
- let { getLongString, getTabTarget, requestData } = connector;
+ let { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
// Execute one GET request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 1);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.executeTest3();
});
yield wait;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({
- getTabTarget, getLongString, requestData });
- let jsonString = yield contextMenu.copyAllAsHar();
+ let contextMenu = new RequestListContextMenu({ connector });
+ let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
- is(entry.request.postData, undefined,
- "Check post data is not present");
+
+ is(entry.request.postData, undefined, "Check post data is not present");
// Clean up
return teardown(monitor);
});
--- a/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
+++ b/devtools/client/netmonitor/src/har/test/browser_net_har_throttle_upload.js
@@ -15,17 +15,18 @@ function* throttleUploadTest(actuallyThr
HAR_EXAMPLE_URL + "html_har_post-data-test-page.html");
info("Starting test... (actuallyThrottle = " + actuallyThrottle + ")");
let { connector, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let RequestListContextMenu = windowRequire(
"devtools/client/netmonitor/src/request-list-context-menu");
- let { getLongString, getTabTarget, setPreferences, requestData } = connector;
+ let { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
const size = 4096;
const uploadSize = actuallyThrottle ? size / 3 : 0;
const request = {
"NetworkMonitor.throttleData": {
@@ -35,32 +36,31 @@ function* throttleUploadTest(actuallyThr
downloadBPSMax: 200000,
uploadBPSMean: uploadSize,
uploadBPSMax: uploadSize,
},
};
info("sending throttle request");
yield new Promise((resolve) => {
- setPreferences(request, (response) => {
+ connector.setPreferences(request, (response) => {
resolve(response);
});
});
// Execute one POST request on the page and wait till its done.
let wait = waitForNetworkEvents(monitor, 0, 1);
yield ContentTask.spawn(tab.linkedBrowser, { size }, function* (args) {
content.wrappedJSObject.executeTest2(args.size);
});
yield wait;
// Copy HAR into the clipboard (asynchronous).
- let contextMenu = new RequestListContextMenu({
- getTabTarget, getLongString, requestData });
- let jsonString = yield contextMenu.copyAllAsHar();
+ let contextMenu = new RequestListContextMenu({ connector });
+ let jsonString = yield contextMenu.copyAllAsHar(getSortedRequests(store.getState()));
let har = JSON.parse(jsonString);
// Check out the HAR log.
isnot(har.log, null, "The HAR log must exist");
is(har.log.pages.length, 1, "There must be one page");
is(har.log.entries.length, 1, "There must be one request");
let entry = har.log.entries[0];
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -129,16 +129,17 @@ function requestsReducer(state = Request
let newRequest = {
id: clonedRequest.id + "-clone",
method: clonedRequest.method,
url: clonedRequest.url,
urlDetails: clonedRequest.urlDetails,
requestHeaders: clonedRequest.requestHeaders,
requestPostData: clonedRequest.requestPostData,
+ requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
isCustom: true
};
return {
...state,
requests: mapSet(requests, newRequest.id, newRequest),
selectedId: newRequest.id,
};
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -4,408 +4,391 @@
"use strict";
const Services = require("Services");
const { Curl } = require("devtools/client/shared/curl");
const { gDevTools } = require("devtools/client/framework/devtools");
const { saveAs } = require("devtools/client/shared/file-saver");
const { copyString } = require("devtools/shared/platform/clipboard");
+const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
const { HarExporter } = require("./har/har-exporter");
const {
getSelectedRequest,
getSortedRequests,
} = require("./selectors/index");
const { L10N } = require("./utils/l10n");
-const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
const {
+ formDataURI,
getUrlQuery,
+ getUrlBaseName,
parseQueryString,
- getUrlBaseName,
- formDataURI,
} = require("./utils/request-utils");
-function RequestListContextMenu({
- cloneSelectedRequest,
- getLongString,
- getTabTarget,
- openStatistics,
- requestData,
-}) {
- this.cloneSelectedRequest = cloneSelectedRequest;
- this.getLongString = getLongString;
- this.getTabTarget = getTabTarget;
- this.openStatistics = openStatistics;
- this.requestData = requestData;
-}
+class RequestListContextMenu {
+ constructor(props) {
+ this.props = props;
+ }
-RequestListContextMenu.prototype = {
- get selectedRequest() {
+ open(event) {
// FIXME: Bug 1336382 - Implement RequestListContextMenu React component
- // Remove window.store
- return getSelectedRequest(window.store.getState());
- },
+ // Remove window.store.getState()
+ let selectedRequest = getSelectedRequest(window.store.getState());
+ let sortedRequests = getSortedRequests(window.store.getState());
- get sortedRequests() {
- // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
- // Remove window.store
- return getSortedRequests(window.store.getState());
- },
-
- /**
- * Handle the context menu opening. Hide items if no request is selected.
- * Since visible attribute only accept boolean value but the method call may
- * return undefined, we use !! to force convert any object to boolean
- */
- open(event = {}) {
- let selectedRequest = this.selectedRequest;
let menu = [];
let copySubmenu = [];
+ let {
+ id,
+ isCustom,
+ method,
+ mimeType,
+ httpVersion,
+ requestHeaders,
+ requestPostDataAvailable,
+ responseHeaders,
+ responseContentAvailable,
+ url,
+ } = selectedRequest || {};
+ let {
+ cloneSelectedRequest,
+ openStatistics,
+ } = this.props;
copySubmenu.push({
id: "request-list-context-copy-url",
label: L10N.getStr("netmonitor.context.copyUrl"),
accesskey: L10N.getStr("netmonitor.context.copyUrl.accesskey"),
visible: !!selectedRequest,
- click: () => this.copyUrl(),
+ click: () => this.copyUrl(url),
});
copySubmenu.push({
id: "request-list-context-copy-url-params",
label: L10N.getStr("netmonitor.context.copyUrlParams"),
accesskey: L10N.getStr("netmonitor.context.copyUrlParams.accesskey"),
- visible: !!(selectedRequest && getUrlQuery(selectedRequest.url)),
- click: () => this.copyUrlParams(),
+ visible: !!(selectedRequest && getUrlQuery(url)),
+ click: () => this.copyUrlParams(url),
});
copySubmenu.push({
id: "request-list-context-copy-post-data",
label: L10N.getStr("netmonitor.context.copyPostData"),
accesskey: L10N.getStr("netmonitor.context.copyPostData.accesskey"),
- visible: !!(selectedRequest && selectedRequest.requestPostData),
- click: () => this.copyPostData(),
+ visible: !!(selectedRequest && requestPostDataAvailable),
+ click: () => this.copyPostData(id),
});
copySubmenu.push({
id: "request-list-context-copy-as-curl",
label: L10N.getStr("netmonitor.context.copyAsCurl"),
accesskey: L10N.getStr("netmonitor.context.copyAsCurl.accesskey"),
visible: !!selectedRequest,
- click: () => this.copyAsCurl(),
+ click: () => this.copyAsCurl(id, url, method, requestHeaders, httpVersion),
});
copySubmenu.push({
type: "separator",
- visible: !!selectedRequest,
+ visible: copySubmenu.slice(0, 4).some((subMenu) => subMenu.visible),
});
copySubmenu.push({
id: "request-list-context-copy-request-headers",
label: L10N.getStr("netmonitor.context.copyRequestHeaders"),
accesskey: L10N.getStr("netmonitor.context.copyRequestHeaders.accesskey"),
- visible: !!(selectedRequest && selectedRequest.requestHeaders),
- click: () => this.copyRequestHeaders(),
+ visible: !!(selectedRequest && requestHeaders && requestHeaders.rawHeaders),
+ click: () => this.copyRequestHeaders(requestHeaders.rawHeaders.trim()),
});
copySubmenu.push({
id: "response-list-context-copy-response-headers",
label: L10N.getStr("netmonitor.context.copyResponseHeaders"),
accesskey: L10N.getStr("netmonitor.context.copyResponseHeaders.accesskey"),
- visible: !!(selectedRequest && selectedRequest.responseHeaders),
- click: () => this.copyResponseHeaders(),
+ visible: !!(selectedRequest && responseHeaders && responseHeaders.rawHeaders),
+ click: () => this.copyResponseHeaders(responseHeaders.rawHeaders.trim()),
});
copySubmenu.push({
id: "request-list-context-copy-response",
label: L10N.getStr("netmonitor.context.copyResponse"),
accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
- visible: !!(selectedRequest && selectedRequest.responseContentAvailable),
- click: () => this.copyResponse(),
+ visible: !!(selectedRequest && responseContentAvailable),
+ click: () => this.copyResponse(id),
});
copySubmenu.push({
id: "request-list-context-copy-image-as-data-uri",
label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
- visible: !!(selectedRequest &&
- selectedRequest.mimeType &&
- selectedRequest.mimeType.includes("image/")),
- click: () => this.copyImageAsDataUri(),
+ visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+ click: () => this.copyImageAsDataUri(id, mimeType),
});
copySubmenu.push({
type: "separator",
- visible: !!selectedRequest,
+ visible: copySubmenu.slice(5, 9).some((subMenu) => subMenu.visible),
});
copySubmenu.push({
id: "request-list-context-copy-all-as-har",
label: L10N.getStr("netmonitor.context.copyAllAsHar"),
accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"),
- visible: this.sortedRequests.size > 0,
- click: () => this.copyAllAsHar(),
+ visible: sortedRequests.size > 0,
+ click: () => this.copyAllAsHar(sortedRequests),
});
menu.push({
label: L10N.getStr("netmonitor.context.copy"),
accesskey: L10N.getStr("netmonitor.context.copy.accesskey"),
visible: !!selectedRequest,
submenu: copySubmenu,
});
menu.push({
id: "request-list-context-save-all-as-har",
label: L10N.getStr("netmonitor.context.saveAllAsHar"),
accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"),
- visible: this.sortedRequests.size > 0,
- click: () => this.saveAllAsHar(),
+ visible: sortedRequests.size > 0,
+ click: () => this.saveAllAsHar(sortedRequests),
});
menu.push({
id: "request-list-context-save-image-as",
label: L10N.getStr("netmonitor.context.saveImageAs"),
accesskey: L10N.getStr("netmonitor.context.saveImageAs.accesskey"),
- visible: !!(selectedRequest &&
- selectedRequest.mimeType &&
- selectedRequest.mimeType.includes("image/")),
- click: () => this.saveImageAs(),
+ visible: !!(selectedRequest && mimeType && mimeType.includes("image/")),
+ click: () => this.saveImageAs(id, url),
});
menu.push({
type: "separator",
- visible: !!(selectedRequest && !selectedRequest.isCustom),
+ 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 && !selectedRequest.isCustom),
- click: this.cloneSelectedRequest,
+ visible: !!(selectedRequest && !isCustom),
+ click: cloneSelectedRequest,
});
menu.push({
type: "separator",
- visible: !!selectedRequest,
+ visible: copySubmenu.slice(15, 16).some((subMenu) => subMenu.visible),
});
menu.push({
id: "request-list-context-newtab",
label: L10N.getStr("netmonitor.context.newTab"),
accesskey: L10N.getStr("netmonitor.context.newTab.accesskey"),
visible: !!selectedRequest,
- click: () => this.openRequestInTab()
+ click: () => this.openRequestInTab(url),
});
menu.push({
id: "request-list-context-open-in-debugger",
label: L10N.getStr("netmonitor.context.openInDebugger"),
accesskey: L10N.getStr("netmonitor.context.openInDebugger.accesskey"),
- visible: !!(selectedRequest &&
- selectedRequest.mimeType &&
- selectedRequest.mimeType.includes("javascript")),
- click: () => this.openInDebugger()
+ visible: !!(selectedRequest && mimeType && mimeType.includes("javascript")),
+ click: () => this.openInDebugger(url),
});
menu.push({
id: "request-list-context-open-in-style-editor",
label: L10N.getStr("netmonitor.context.openInStyleEditor"),
accesskey: L10N.getStr("netmonitor.context.openInStyleEditor.accesskey"),
visible: !!(selectedRequest &&
- Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
- selectedRequest.mimeType &&
- selectedRequest.mimeType.includes("css")),
- click: () => this.openInStyleEditor()
+ Services.prefs.getBoolPref("devtools.styleeditor.enabled") &&
+ mimeType && mimeType.includes("css")),
+ click: () => this.openInStyleEditor(url),
});
menu.push({
id: "request-list-context-perf",
label: L10N.getStr("netmonitor.context.perfTools"),
accesskey: L10N.getStr("netmonitor.context.perfTools.accesskey"),
- visible: this.sortedRequests.size > 0,
- click: () => this.openStatistics(true)
+ visible: sortedRequests.size > 0,
+ click: () => openStatistics(true),
});
- return showMenu(event, menu);
- },
+ showMenu(event, menu);
+ }
/**
* Opens selected item in a new tab.
*/
- openRequestInTab() {
+ openRequestInTab(url) {
let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
- win.openUILinkIn(this.selectedRequest.url, "tab", { relatedToCurrent: true });
- },
+ win.openUILinkIn(url, "tab", { relatedToCurrent: true });
+ }
/**
* Opens selected item in the debugger
*/
- openInDebugger() {
- let toolbox = gDevTools.getToolbox(this.getTabTarget());
- toolbox.viewSourceInDebugger(this.selectedRequest.url, 0);
- },
+ openInDebugger(url) {
+ let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+ toolbox.viewSourceInDebugger(url, 0);
+ }
/**
* Opens selected item in the style editor
*/
- openInStyleEditor() {
- let toolbox = gDevTools.getToolbox(this.getTabTarget());
- toolbox.viewSourceInStyleEditor(this.selectedRequest.url, 0);
- },
+ openInStyleEditor(url) {
+ let toolbox = gDevTools.getToolbox(this.props.connector.getTabTarget());
+ toolbox.viewSourceInStyleEditor(url, 0);
+ }
/**
* Copy the request url from the currently selected item.
*/
- copyUrl() {
- copyString(this.selectedRequest.url);
- },
+ copyUrl(url) {
+ copyString(url);
+ }
/**
* Copy the request url query string parameters from the currently
* selected item.
*/
- copyUrlParams() {
- let params = getUrlQuery(this.selectedRequest.url).split("&");
+ copyUrlParams(url) {
+ let params = getUrlQuery(url).split("&");
copyString(params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"));
- },
+ }
/**
* Copy the request form data parameters (or raw payload) from
* the currently selected item.
*/
- copyPostData() {
- let { formDataSections, requestPostData } = this.selectedRequest;
+ async copyPostData(id) {
+ // FIXME: Bug 1336382 - Implement RequestListContextMenu React component
+ // Remove window.store.getState()
+ let { formDataSections } = getSelectedRequest(window.store.getState());
let params = [];
-
// Try to extract any form data parameters.
formDataSections.forEach(section => {
let paramsArray = parseQueryString(section);
if (paramsArray) {
params = [...params, ...paramsArray];
}
});
let string = params
.map(param => param.name + (param.value ? "=" + param.value : ""))
.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
// Fall back to raw payload.
if (!string) {
+ let { requestPostData } = await this.props.connector
+ .requestData(id, "requestPostData");
string = requestPostData.postData.text;
if (Services.appinfo.OS !== "WINNT") {
string = string.replace(/\r/g, "");
}
}
copyString(string);
- },
+ }
/**
* Copy a cURL command from the currently selected item.
*/
- copyAsCurl() {
- let selected = this.selectedRequest;
+ async copyAsCurl(id, url, method, requestHeaders, httpVersion) {
+ let { requestPostData } = await this.props.connector
+ .requestData(id, "requestPostData");
// Create a sanitized object for the Curl command generator.
let data = {
- url: selected.url,
- method: selected.method,
- headers: selected.requestHeaders.headers,
- httpVersion: selected.httpVersion,
- postDataText: selected.requestPostData && selected.requestPostData.postData.text,
+ url,
+ method,
+ headers: requestHeaders.headers,
+ httpVersion: httpVersion,
+ postDataText: requestPostData ? requestPostData.postData.text : "",
};
copyString(Curl.generateCommand(data));
- },
+ }
/**
* Copy the raw request headers from the currently selected item.
*/
- copyRequestHeaders() {
- let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
+ copyRequestHeaders(rawHeaders) {
if (Services.appinfo.OS !== "WINNT") {
rawHeaders = rawHeaders.replace(/\r/g, "");
}
copyString(rawHeaders);
- },
+ }
/**
* Copy the raw response headers from the currently selected item.
*/
- copyResponseHeaders() {
- let rawHeaders = this.selectedRequest.responseHeaders.rawHeaders.trim();
+ copyResponseHeaders(rawHeaders) {
if (Services.appinfo.OS !== "WINNT") {
rawHeaders = rawHeaders.replace(/\r/g, "");
}
copyString(rawHeaders);
- },
+ }
/**
* Copy image as data uri.
*/
- async copyImageAsDataUri() {
- let responseContent = await this.requestData(this.selectedRequest.id,
- "responseContent");
- let { mimeType } = this.selectedRequest;
+ async copyImageAsDataUri(id, mimeType) {
+ let responseContent = await this.props.connector.requestData(id, "responseContent");
let { encoding, text } = responseContent.content;
- let src = formDataURI(mimeType, encoding, text);
- copyString(src);
- },
+ copyString(formDataURI(mimeType, encoding, text));
+ }
/**
* Save image as.
*/
- async saveImageAs() {
- let responseContent = await this.requestData(this.selectedRequest.id,
- "responseContent");
+ async saveImageAs(id, url) {
+ let responseContent = await this.props.connector.requestData(id, "responseContent");
let { encoding, text } = responseContent.content;
- let fileName = getUrlBaseName(this.selectedRequest.url);
+ let fileName = getUrlBaseName(url);
let data;
if (encoding === "base64") {
let decoded = atob(text);
data = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; ++i) {
data[i] = decoded.charCodeAt(i);
}
} else {
data = text;
}
saveAs(new Blob([data]), fileName, document);
- },
+ }
/**
* Copy response data as a string.
*/
- async copyResponse() {
- let responseContent = await this.requestData(this.selectedRequest.id,
- "responseContent");
+ async copyResponse(id) {
+ let responseContent = await this.props.connector.requestData(id, "responseContent");
copyString(responseContent.content.text);
- },
+ }
/**
* Copy HAR from the network panel content to the clipboard.
*/
- copyAllAsHar() {
- return HarExporter.copy(this.getDefaultHarOptions());
- },
+ copyAllAsHar(sortedRequests) {
+ return HarExporter.copy(this.getDefaultHarOptions(sortedRequests));
+ }
/**
* Save HAR from the network panel content to a file.
*/
- saveAllAsHar() {
- // FIXME: This will not work in launchpad
+ saveAllAsHar(sortedRequests) {
+ // This will not work in launchpad
// document.execCommand(‘cut’/‘copy’) was denied because it was not called from
// inside a short running user-generated event handler.
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
- return HarExporter.save(this.getDefaultHarOptions());
- },
+ return HarExporter.save(this.getDefaultHarOptions(sortedRequests));
+ }
- getDefaultHarOptions() {
- let form = this.getTabTarget().form;
- let title = form.title || form.url;
+ getDefaultHarOptions(sortedRequests) {
+ let { getLongString, getTabTarget, requestData } = this.props.connector;
+ let { form: { title, url } } = getTabTarget();
return {
- requestData: this.requestData,
- getString: this.getLongString,
- items: this.sortedRequests,
- title: title
+ getString: getLongString,
+ items: sortedRequests,
+ requestData,
+ title: title || url,
};
}
-};
+}
module.exports = RequestListContextMenu;
--- a/devtools/client/netmonitor/test/browser_net_curl-utils.js
+++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js
@@ -13,49 +13,52 @@ add_task(function* () {
let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL);
info("Starting test... ");
let { store, windowRequire, connector } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let {
getSortedRequests,
} = windowRequire("devtools/client/netmonitor/src/selectors/index");
- let { getLongString } = connector;
+ let {
+ getLongString,
+ requestData,
+ } = connector;
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, 1, 3);
yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) {
content.wrappedJSObject.performRequests(url);
});
yield wait;
let requests = {
get: getSortedRequests(store.getState()).get(0),
post: getSortedRequests(store.getState()).get(1),
multipart: getSortedRequests(store.getState()).get(2),
multipartForm: getSortedRequests(store.getState()).get(3),
};
- let data = yield createCurlData(requests.get, getLongString);
+ let data = yield createCurlData(requests.get, getLongString, requestData);
testFindHeader(data);
- data = yield createCurlData(requests.post, getLongString);
+ data = yield createCurlData(requests.post, getLongString, requestData);
testIsUrlEncodedRequest(data);
testWritePostDataTextParams(data);
testWriteEmptyPostDataTextParams(data);
testDataArgumentOnGeneratedCommand(data);
- data = yield createCurlData(requests.multipart, getLongString);
+ data = yield createCurlData(requests.multipart, getLongString, requestData);
testIsMultipartRequest(data);
testGetMultipartBoundary(data);
testMultiPartHeaders(data);
testRemoveBinaryDataFromMultipartText(data);
- data = yield createCurlData(requests.multipartForm, getLongString);
+ data = yield createCurlData(requests.multipartForm, getLongString, requestData);
testMultiPartHeaders(data);
testGetHeadersFromMultipartText({
postDataText: "Content-Type: text/plain\r\n\r\n",
});
if (Services.appinfo.OS != "WINNT") {
testEscapeStringPosix();
@@ -226,17 +229,17 @@ function testEscapeStringWin() {
"Backslashes should be escaped.");
let newLines = "line1\r\nline2\r\nline3";
is(CurlUtils.escapeStringWin(newLines),
'"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
"Newlines should be escaped.");
}
-function* createCurlData(selected, getLongString) {
+function* createCurlData(selected, getLongString, requestData) {
let { url, method, httpVersion } = selected;
// Create a sanitized object for the Curl command generator.
let data = {
url,
method,
headers: [],
httpVersion,
@@ -244,16 +247,17 @@ function* createCurlData(selected, getLo
};
// Fetch header values.
for (let { name, value } of selected.requestHeaders.headers) {
let text = yield getLongString(value);
data.headers.push({ name: name, value: text });
}
+ let { requestPostData } = yield requestData(selected.id, "requestPostData");
// Fetch the request payload.
- if (selected.requestPostData) {
- let postData = selected.requestPostData.postData.text;
+ if (requestPostData) {
+ let postData = requestPostData.postData.text;
data.postDataText = yield getLongString(postData);
}
return data;
}
--- a/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_debugger.js
@@ -7,33 +7,34 @@
* Test the 'Open in debugger' feature
*/
add_task(function* () {
let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
info("Starting test... ");
let { document, store, windowRequire } = monitor.panelWin;
-
+ let contextMenuDoc = monitor.panelWin.parent.document;
// Avoid async processing
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.performRequests();
});
yield wait;
+ wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-debugger");
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll(".request-list-item")[2]);
EventUtils.sendMouseEvent({ type: "contextmenu" },
document.querySelectorAll(".request-list-item")[2]);
+ yield wait;
let onDebuggerReady = toolbox.once("jsdebugger-ready");
- monitor.panelWin.parent.document
- .querySelector("#request-list-context-open-in-debugger").click();
+ contextMenuDoc.querySelector("#request-list-context-open-in-debugger").click();
yield onDebuggerReady;
ok(true, "Debugger has been open");
yield teardown(monitor);
});
--- a/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
+++ b/devtools/client/netmonitor/test/browser_net_open_in_style_editor.js
@@ -7,31 +7,33 @@
* Test the 'Open in debugger' feature
*/
add_task(function* () {
let { tab, monitor, toolbox} = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
info("Starting test... ");
let { document, store, windowRequire } = monitor.panelWin;
-
+ let contextMenuDoc = monitor.panelWin.parent.document;
// Avoid async processing
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.performRequests();
});
yield wait;
+ wait = waitForDOM(contextMenuDoc, "#request-list-context-open-in-style-editor");
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll(".request-list-item")[1]);
EventUtils.sendMouseEvent({ type: "contextmenu" },
document.querySelectorAll(".request-list-item")[1]);
+ yield wait;
let onStyleEditorReady = toolbox.once("styleeditor-ready");
monitor.panelWin.parent.document
.querySelector("#request-list-context-open-in-style-editor").click();
yield onStyleEditorReady;
ok(true, "Style Editor has been open");
--- a/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
+++ b/devtools/client/netmonitor/test/browser_net_open_request_in_tab.js
@@ -7,30 +7,33 @@
* Tests if Open in new tab works.
*/
add_task(function* () {
let { tab, monitor } = yield initNetMonitor(CUSTOM_GET_URL);
info("Starting test...");
let { document, store, windowRequire } = monitor.panelWin;
+ let contextMenuDoc = monitor.panelWin.parent.document;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
let wait = waitForNetworkEvents(monitor, 1);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.performRequests(1);
});
yield wait;
+ wait = waitForDOM(contextMenuDoc, "#request-list-context-newtab");
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll(".request-list-item")[0]);
EventUtils.sendMouseEvent({ type: "contextmenu" },
document.querySelectorAll(".request-list-item")[0]);
+ yield wait;
let onTabOpen = once(gBrowser.tabContainer, "TabOpen", false);
monitor.panelWin.parent.document
.querySelector("#request-list-context-newtab").click();
yield onTabOpen;
ok(true, "A new tab has been opened");
--- a/devtools/client/netmonitor/test/browser_net_post-data-03.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-03.js
@@ -21,25 +21,24 @@ add_task(function* () {
let wait = waitForNetworkEvents(monitor, 0, 1);
yield ContentTask.spawn(tab.linkedBrowser, {}, function* () {
content.wrappedJSObject.performRequests();
});
yield wait;
// Wait for all tree view updated by react
- wait = waitForDOM(document, "#headers-panel");
+ wait = waitForDOM(document, "#headers-panel .tree-section .treeLabel", 3);
EventUtils.sendMouseEvent({ type: "click" },
document.querySelector(".network-details-panel-toggle"));
EventUtils.sendMouseEvent({ type: "click" },
document.querySelector("#headers-tab"));
yield wait;
let tabpanel = document.querySelector("#headers-panel");
-
is(tabpanel.querySelectorAll(".tree-section .treeLabel").length, 3,
"There should be 3 header sections displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".tree-section .treeLabel")[2].textContent,
L10N.getStr("requestHeadersFromUpload") + " (" +
L10N.getFormatStr("networkMenu.sizeB", 74) + ")",
"The request headers from upload section doesn't have the correct title.");
--- a/devtools/client/netmonitor/test/browser_net_raw_headers.js
+++ b/devtools/client/netmonitor/test/browser_net_raw_headers.js
@@ -25,19 +25,21 @@ add_task(function* () {
});
yield wait;
wait = waitForDOM(document, ".headers-overview");
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll(".request-list-item")[0]);
yield wait;
+ let onRequestPostData = monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_POST_DATA);
wait = waitForDOM(document, ".raw-headers-container textarea", 2);
EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
yield wait;
+ yield onRequestPostData;
testRawHeaderButtonStyle(true);
testShowRawHeaders(getSortedRequests(store.getState()).get(0));
EventUtils.sendMouseEvent({ type: "click" }, getRawHeadersButton());
testRawHeaderButtonStyle(false);
--- a/devtools/client/netmonitor/test/browser_net_resend.js
+++ b/devtools/client/netmonitor/test/browser_net_resend.js
@@ -12,16 +12,17 @@ const ADD_HEADER = "Test-header: true";
const ADD_UA_HEADER = "User-Agent: Custom-Agent";
const ADD_POSTDATA = "&t3=t4";
add_task(function* () {
let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
info("Starting test... ");
let { document, store, windowRequire, connector } = monitor.panelWin;
+ let { requestData } = connector;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
let {
getSelectedRequest,
getSortedRequests,
} = windowRequire("devtools/client/netmonitor/src/selectors/index");
store.dispatch(Actions.batchEnable(false));
@@ -31,36 +32,38 @@ add_task(function* () {
});
yield wait;
let origItem = getSortedRequests(store.getState()).get(0);
store.dispatch(Actions.selectRequest(origItem.id));
// add a new custom request cloned from selected request
+
store.dispatch(Actions.cloneSelectedRequest());
-
testCustomForm(origItem);
let customItem = getSelectedRequest(store.getState());
testCustomItem(customItem, origItem);
// edit the custom request
yield editCustomForm();
+
// FIXME: reread the customItem, it's been replaced by a new object (immutable!)
customItem = getSelectedRequest(store.getState());
testCustomItemChanged(customItem, origItem);
// send the new request
wait = waitForNetworkEvents(monitor, 0, 1);
store.dispatch(Actions.sendCustomRequest(connector));
yield wait;
let sentItem = getSelectedRequest(store.getState());
- testSentRequest(sentItem, origItem);
+
+ yield testSentRequest(sentItem, origItem);
// Ensure the UI shows the new request, selected, and that the detail panel was closed.
is(getSortedRequests(store.getState()).length, 3, "There are 3 requests shown");
is(document.querySelector(".request-list-item.selected").getAttribute("data-id"),
sentItem.id, "The sent request is selected");
is(document.querySelector(".network-details-panel"), null,
"The detail panel is hidden");
@@ -137,37 +140,43 @@ add_task(function* () {
type(ADD_UA_HEADER);
let postData = document.getElementById("custom-postdata-value");
let postFocus = once(postData, "focus", false);
postData.setSelectionRange(postData.value.length, postData.value.length);
postData.focus();
yield postFocus;
- // add to POST data
+ // add to POST data once textarea has updated
+ yield waitUntil(() => postData.textContent !== "");
type(ADD_POSTDATA);
}
/*
* Make sure newly created event matches expected request
*/
- function testSentRequest(data, origData) {
+ function* testSentRequest(data, origData) {
is(data.method, origData.method, "correct method in sent request");
is(data.url, origData.url + "&" + ADD_QUERY, "correct url in sent request");
let { headers } = data.requestHeaders;
let hasHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_HEADER);
ok(hasHeader, "new header added to sent request");
let hasUAHeader = headers.some(h => `${h.name}: ${h.value}` == ADD_UA_HEADER);
ok(hasUAHeader, "User-Agent header added to sent request");
- is(data.requestPostData.postData.text,
- origData.requestPostData.postData.text + ADD_POSTDATA,
- "post data added to sent request");
+ let { requestPostData: clonedRequestPostData } = yield requestData(data.id,
+ "requestPostData");
+ let { requestPostData: origRequestPostData } = yield requestData(origData.id,
+ "requestPostData");
+
+ is(clonedRequestPostData.postData.text,
+ origRequestPostData.postData.text + ADD_POSTDATA,
+ "post data added to sent request");
}
function type(string) {
for (let ch of string) {
EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
}
}
});
--- a/devtools/client/netmonitor/test/browser_net_resend_cors.js
+++ b/devtools/client/netmonitor/test/browser_net_resend_cors.js
@@ -58,20 +58,21 @@ add_task(function* () {
// Check the resent requests
for (let i = 0; i < ITEMS.length; i++) {
let item = ITEMS[i];
is(item.method, METHODS[i], `The ${item.method} request has the right method`);
is(item.url, requestUrl, `The ${item.method} request has the right URL`);
is(item.status, 200, `The ${item.method} response has the right status`);
if (item.method === "POST") {
- // Force fetching response content
+ // Force fetching lazy load data
let responseContent = yield connector.requestData(item.id, "responseContent");
+ let { requestPostData } = yield connector.requestData(item.id, "requestPostData");
- is(item.requestPostData.postData.text, "post-data",
+ is(requestPostData.postData.text, "post-data",
"The POST request has the right POST data");
// eslint-disable-next-line mozilla/no-cpows-in-tests
is(responseContent.content.text, "Access-Control-Allow-Origin: *",
"The POST response has the right content");
}
}
info("Finishing the test");
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -298,37 +298,32 @@ function teardown(monitor) {
}
function waitForNetworkEvents(monitor, getRequests, postRequests = 0) {
return new Promise((resolve) => {
let panel = monitor.panelWin;
let { getNetworkRequest } = panel.connector;
let progress = {};
let genericEvents = 0;
- let postEvents = 0;
let payloadReady = 0;
let awaitedEventsToListeners = [
["UPDATING_REQUEST_HEADERS", onGenericEvent],
["RECEIVED_REQUEST_HEADERS", onGenericEvent],
["UPDATING_REQUEST_COOKIES", onGenericEvent],
["RECEIVED_REQUEST_COOKIES", onGenericEvent],
- ["UPDATING_REQUEST_POST_DATA", onPostEvent],
- ["RECEIVED_REQUEST_POST_DATA", onPostEvent],
["UPDATING_RESPONSE_HEADERS", onGenericEvent],
["RECEIVED_RESPONSE_HEADERS", onGenericEvent],
["UPDATING_RESPONSE_COOKIES", onGenericEvent],
["RECEIVED_RESPONSE_COOKIES", onGenericEvent],
["UPDATING_EVENT_TIMINGS", onGenericEvent],
["RECEIVED_EVENT_TIMINGS", onGenericEvent],
["PAYLOAD_READY", onPayloadReady]
];
let expectedGenericEvents = awaitedEventsToListeners
.filter(([, listener]) => listener == onGenericEvent).length;
- let expectedPostEvents = awaitedEventsToListeners
- .filter(([, listener]) => listener == onPostEvent).length;
function initProgressForURL(url) {
if (progress[url]) {
return;
}
progress[url] = {};
awaitedEventsToListeners.forEach(function ([e]) {
progress[url][e] = 0;
@@ -346,27 +341,16 @@ function waitForNetworkEvents(monitor, g
// Must have been related to reloading document to disable cache.
// Ignore the event.
return;
}
genericEvents++;
maybeResolve(event, actor, networkInfo);
}
- function onPostEvent(event, actor) {
- let networkInfo = getNetworkRequest(actor);
- if (!networkInfo) {
- // Must have been related to reloading document to disable cache.
- // Ignore the event.
- return;
- }
- postEvents++;
- maybeResolve(event, actor, networkInfo);
- }
-
function onPayloadReady(event, actor) {
let networkInfo = getNetworkRequest(actor);
if (!networkInfo) {
// Must have been related to reloading document to disable cache.
// Ignore the event.
return;
}
@@ -374,31 +358,29 @@ function waitForNetworkEvents(monitor, g
maybeResolve(event, actor, networkInfo);
}
function maybeResolve(event, actor, networkInfo) {
info("> Network events progress: " +
"Payload: " + payloadReady + "/" + (getRequests + postRequests) + ", " +
"Generic: " + genericEvents + "/" +
((getRequests + postRequests) * expectedGenericEvents) + ", " +
- "Post: " + postEvents + "/" + (postRequests * expectedPostEvents) + ", " +
"got " + event + " for " + actor);
let url = networkInfo.request.url;
updateProgressForURL(url, event);
// Uncomment this to get a detailed progress logging (when debugging a test)
// info("> Current state: " + JSON.stringify(progress, null, 2));
// There are `expectedGenericEvents` updates which need to be fired for a request
// to be considered finished. The "requestPostData" packet isn't fired for non-POST
// requests.
if (payloadReady >= (getRequests + postRequests) &&
- genericEvents >= (getRequests + postRequests) * expectedGenericEvents &&
- postEvents >= postRequests * expectedPostEvents) {
+ genericEvents >= (getRequests + postRequests) * expectedGenericEvents) {
awaitedEventsToListeners.forEach(([e, l]) => panel.off(EVENTS[e], l));
executeSoon(resolve);
}
}
awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
});
}