Bug 1364096 - Autocomplete for network monitor flag values. r=ntim, r=jdescottes

This commit is contained in:
Ruturaj K. Vartak 2017-07-28 03:11:00 -04:00
parent d7cb1595c0
commit 199f0ac351
7 changed files with 276 additions and 61 deletions

View File

@ -17,10 +17,11 @@ const { FILTER_SEARCH_DELAY } = require("../constants");
const {
getDisplayedRequestsSummary,
getRequestFilterTypes,
getTypeFilteredRequests,
isNetworkDetailsToggleButtonDisabled,
} = require("../selectors/index");
const { autocompleteProvider } = require("../utils/filter-text-utils");
const { autocompleteProvider } = require("../utils/filter-autocomplete-provider");
const { L10N } = require("../utils/l10n");
// Components
@ -54,6 +55,7 @@ const Toolbar = createClass({
toggleBrowserCache: PropTypes.func.isRequired,
browserCacheDisabled: PropTypes.bool.isRequired,
toggleRequestFilterType: PropTypes.func.isRequired,
filteredRequests: PropTypes.object.isRequired,
},
toggleRequestFilterType(evt) {
@ -73,6 +75,7 @@ const Toolbar = createClass({
toggleNetworkDetails,
toggleBrowserCache,
browserCacheDisabled,
filteredRequests,
} = this.props;
let toggleButtonClassName = [
@ -132,7 +135,8 @@ const Toolbar = createClass({
placeholder: SEARCH_PLACE_HOLDER,
type: "filter",
onChange: setRequestFilterText,
autocompleteProvider,
autocompleteProvider: filter =>
autocompleteProvider(filter, filteredRequests),
}),
button({
className: toggleButtonClassName.join(" "),
@ -168,6 +172,7 @@ module.exports = connect(
networkDetailsOpen: state.ui.networkDetailsOpen,
browserCacheDisabled: state.ui.browserCacheDisabled,
requestFilterTypes: getRequestFilterTypes(state),
filteredRequests: getTypeFilteredRequests(state),
summary: getDisplayedRequestsSummary(state),
}),
(dispatch) => ({

View File

@ -45,6 +45,16 @@ const getFilterFn = createSelector(
}
);
const getTypeFilterFn = createSelector(
state => state.filters,
filters => r => {
const matchesType = filters.requestFilterTypes.some((enabled, filter) => {
return enabled && Filters[filter] && Filters[filter](r);
});
return matchesType;
}
);
const getSortFn = createSelector(
state => state.requests.requests,
state => state.sort,
@ -69,6 +79,12 @@ const getDisplayedRequests = createSelector(
.filter(filterFn).sort(sortFn).toList()
);
const getTypeFilteredRequests = createSelector(
state => state.requests.requests,
getTypeFilterFn,
(requests, filterFn) => requests.valueSeq().filter(filterFn).toList()
);
const getDisplayedRequestsSummary = createSelector(
getDisplayedRequests,
state => state.requests.lastEndedMillis - state.requests.firstStartedMillis,
@ -118,4 +134,5 @@ module.exports = {
getRequestById,
getSelectedRequest,
getSortedRequests,
getTypeFilteredRequests,
};

View File

@ -0,0 +1,177 @@
/* 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 { FILTER_FLAGS } = require("../constants");
/*
* Generates a value for the given filter
* ie. if flag = status-code, will generate "200" from the given request item.
* For flags related to cookies, it might generate an array based on the request
* ie. ["cookie-name-1", "cookie-name-2", ...]
*
* @param {string} flag - flag specified in filter, ie. "status-code"
* @param {object} request - Network request item
* @return {string|Array} - The output is a string or an array based on the request
*/
function getAutocompleteValuesForFlag(flag, request) {
let values = [];
let { responseCookies = { cookies: [] } } = request;
responseCookies = responseCookies.cookies || responseCookies;
switch (flag) {
case "status-code":
// Sometimes status comes as Number
values.push(String(request.status));
break;
case "scheme":
values.push(request.urlDetails.scheme);
break;
case "domain":
values.push(request.urlDetails.host);
break;
case "remote-ip":
values.push(request.remoteAddress);
break;
case "cause":
values.push(request.cause.type);
break;
case "mime-type":
values.push(request.mimeType);
break;
case "set-cookie-name":
values = responseCookies.map(c => c.name);
break;
case "set-cookie-value":
values = responseCookies.map(c => c.value);
break;
case "set-cookie-domain":
values = responseCookies.map(c => c.hasOwnProperty("domain") ?
c.domain : request.urlDetails.host);
break;
case "is":
values = ["cached", "from-cache", "running"];
break;
case "has-response-header":
// Some requests not having responseHeaders..?
values = request.responseHeaders &&
request.responseHeaders.headers.map(h => h.name);
break;
case "protocol":
values.push(request.httpVersion);
break;
case "method":
default:
values.push(request[flag]);
}
return values;
}
/*
* For a given lastToken passed ie. "is:", returns an array of populated flag
* values for consumption in autocompleteProvider
* ie. ["is:cached", "is:running", "is:from-cache"]
*
* @param {string} lastToken - lastToken parsed from filter input, ie "is:"
* @param {object} requests - List of requests from which values are generated
* @return {Array} - array of autocomplete values
*/
function getLastTokenFlagValues(lastToken, requests) {
// The last token must be a string like "method:GET" or "method:", Any token
// without a ":" cant be used to parse out flag values
if (!lastToken.includes(":")) {
return [];
}
// Parse out possible flag from lastToken
let [flag, typedFlagValue] = lastToken.split(":");
let isNegativeFlag = false;
// Check if flag is used with negative match
if (flag.startsWith("-")) {
flag = flag.slice(1);
isNegativeFlag = true;
}
// Flag is some random string, return
if (!FILTER_FLAGS.includes(flag)) {
return [];
}
let values = [];
for (let request of requests) {
values.push(...getAutocompleteValuesForFlag(flag, request));
}
values = [...new Set(values)];
return values
.filter(value => {
if (typedFlagValue) {
let lowerTyped = typedFlagValue.toLowerCase(),
lowerValue = value.toLowerCase();
return lowerValue.includes(lowerTyped) && lowerValue !== lowerTyped;
}
return typeof value !== "undefined" && value !== "" && value !== "undefined";
})
.sort()
.map(value => isNegativeFlag ? `-${flag}:${value}` : `${flag}:${value}`);
}
/**
* Generates an autocomplete list for the search-box for network monitor
*
* It expects an entire string of the searchbox ie "is:cached pr".
* The string is then tokenized into "is:cached" and "pr"
*
* @param {string} filter - The entire search string of the search box
* @param {object} requests - Iteratable object of requests displayed
* @return {Array} - The output is an array of objects as below
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
* `value` is used to update the search-box input box for given item
* `displayValue` is used to render the autocomplete list
*/
function autocompleteProvider(filter, requests) {
if (!filter) {
return [];
}
let negativeAutocompleteList = FILTER_FLAGS.map((item) => `-${item}`);
let baseList = [...FILTER_FLAGS, ...negativeAutocompleteList]
.map((item) => `${item}:`);
// The last token is used to filter the base autocomplete list
let tokens = filter.split(/\s+/g);
let lastToken = tokens[tokens.length - 1];
let previousTokens = tokens.slice(0, tokens.length - 1);
// Autocomplete list is not generated for empty lastToken
if (!lastToken) {
return [];
}
let autocompleteList;
let availableValues = getLastTokenFlagValues(lastToken, requests);
if (availableValues.length > 0) {
autocompleteList = availableValues;
} else {
autocompleteList = baseList
.filter((item) => {
return item.toLowerCase().startsWith(lastToken.toLowerCase())
&& item.toLowerCase() !== lastToken.toLowerCase();
});
}
return autocompleteList
.sort()
.map(item => ({
value: [...previousTokens, item].join(" "),
displayValue: item,
}));
}
module.exports = {
autocompleteProvider,
};

View File

@ -103,6 +103,11 @@ function processFlagFilter(type, value) {
}
function isFlagFilterMatch(item, { type, value, negative }) {
// Ensures when filter token is exactly a flag ie. "remote-ip:", all values are shown
if (value.length < 1) {
return true;
}
let match = true;
let { responseCookies = { cookies: [] } } = item;
responseCookies = responseCookies.cookies || responseCookies;
@ -185,7 +190,7 @@ function isFlagFilterMatch(item, { type, value, negative }) {
let host = item.urlDetails.host;
let i = responseCookies.findIndex(c => {
let domain = c.hasOwnProperty("domain") ? c.domain : host;
return domain === value;
return domain.includes(value);
});
match = i > -1;
} else {
@ -193,10 +198,12 @@ function isFlagFilterMatch(item, { type, value, negative }) {
}
break;
case "set-cookie-name":
match = responseCookies.findIndex(c => c.name.toLowerCase() === value) > -1;
match = responseCookies.findIndex(c =>
c.name.toLowerCase().includes(value)) > -1;
break;
case "set-cookie-value":
match = responseCookies.findIndex(c => c.value.toLowerCase() === value) > -1;
match = responseCookies.findIndex(c =>
c.value.toLowerCase().includes(value)) > -1;
break;
}
if (negative) {
@ -242,50 +249,6 @@ function isFreetextMatch(item, text) {
return match;
}
/**
* Generates an autocomplete list for the search-box for network monitor
*
* It expects an entire string of the searchbox ie "is:cached pr".
* The string is then tokenized into "is:cached" and "pr"
*
* @param {string} filter - The entire search string of the search box
* @return {Array} - The output is an array of objects as below
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
* `value` is used to update the search-box input box for given item
* `displayValue` is used to render the autocomplete list
*/
function autocompleteProvider(filter) {
if (!filter) {
return [];
}
let negativeAutocompleteList = FILTER_FLAGS.map((item) => `-${item}`);
let baseList = [...FILTER_FLAGS, ...negativeAutocompleteList]
.map((item) => `${item}:`);
// The last token is used to filter the base autocomplete list
let tokens = filter.split(/\s+/g);
let lastToken = tokens[tokens.length - 1];
let previousTokens = tokens.slice(0, tokens.length - 1);
// Autocomplete list is not generated for empty lastToken
if (!lastToken) {
return [];
}
return baseList
.filter((item) => {
return item.toLowerCase().startsWith(lastToken.toLowerCase())
&& item.toLowerCase() !== lastToken.toLowerCase();
})
.sort()
.map(item => ({
value: [...previousTokens, item].join(" "),
displayValue: item,
}));
}
module.exports = {
isFreetextMatch,
autocompleteProvider,
};

View File

@ -5,6 +5,7 @@
DevToolsModules(
'create-store.js',
'filter-autocomplete-provider.js',
'filter-predicates.js',
'filter-text-utils.js',
'format-utils.js',

View File

@ -3,6 +3,23 @@
"use strict";
/**
* Test autocomplete based on filtering flags and requests
*/
const REQUESTS = [
{ url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" },
{ url: "sjs_content-type-test-server.sjs?fmt=html&res=undefined&text=Sample" +
"&cookies=1" },
{ url: "sjs_content-type-test-server.sjs?fmt=css&text=sample" },
{ url: "sjs_content-type-test-server.sjs?fmt=js&text=sample" },
{ url: "sjs_content-type-test-server.sjs?fmt=font" },
{ url: "sjs_content-type-test-server.sjs?fmt=image" },
{ url: "sjs_content-type-test-server.sjs?fmt=audio" },
{ url: "sjs_content-type-test-server.sjs?fmt=video" },
{ url: "sjs_content-type-test-server.sjs?fmt=gzip" },
{ url: "sjs_status-codes-test-server.sjs?sts=304" },
];
function testAutocompleteContents(expected, document) {
expected.forEach(function (item, i) {
is(
@ -19,16 +36,27 @@ function testAutocompleteContents(expected, document) {
add_task(async function () {
let { monitor } = await initNetMonitor(FILTERING_URL);
let { document, window } = monitor.panelWin;
let { document, store, windowRequire } = monitor.panelWin;
let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
info("Starting test... ");
// Let the requests load completely before the autocomplete tests begin
// as autocomplete values also rely on the network requests.
let waitNetwork = waitForNetworkEvents(monitor, REQUESTS.length);
loadCommonFrameScript();
await performRequestsInContent(REQUESTS);
await waitNetwork;
EventUtils.synthesizeMouseAtCenter(
document.querySelector(".devtools-filterinput"), {}, window);
// Empty Mouse click should keep autocomplete hidden
ok(!document.querySelector(".devtools-autocomplete-popup"),
"Autocomplete Popup Created");
"Autocomplete Popup still hidden");
document.querySelector(".devtools-filterinput").focus();
// Typing a char should invoke a autocomplete
EventUtils.synthesizeKey("s", {});
ok(document.querySelector(".devtools-autocomplete-popup"),
@ -46,25 +74,42 @@ add_task(async function () {
testAutocompleteContents(["scheme:"], document);
EventUtils.synthesizeKey("VK_TAB", {});
// Tab selection should hide autocomplete
ok(!document.querySelector(".devtools-autocomplete-popup"),
"Autocomplete Popup Hidden");
ok(document.querySelector(".devtools-autocomplete-popup"),
"Autocomplete Popup alive with content values");
testAutocompleteContents(["scheme:http"], document);
EventUtils.synthesizeKey("VK_RETURN", {});
is(document.querySelector(".devtools-filterinput").value,
"scheme:", "Value correctly set after TAB");
"scheme:http", "Value correctly set after Enter");
ok(!document.querySelector(".devtools-autocomplete-popup"),
"Autocomplete Popup hidden after keyboard Enter key");
// Space separated tokens
EventUtils.synthesizeKey("https ", {});
// Adding just a space should keep popup hidden
ok(!document.querySelector(".devtools-autocomplete-popup"),
"Autocomplete Popup still hidden");
// The last token where autocomplete is availabe shall generate the popup
EventUtils.synthesizeKey("p", {});
EventUtils.synthesizeKey(" p", {});
testAutocompleteContents(["protocol:"], document);
// The new value of the text box should be previousTokens + latest value selected
// First return selects "protocol:"
EventUtils.synthesizeKey("VK_RETURN", {});
// Second return selects "protocol:HTTP/1.1"
EventUtils.synthesizeKey("VK_RETURN", {});
is(document.querySelector(".devtools-filterinput").value,
"scheme:https protocol:", "Tokenized click generates correct value in input box");
"scheme:http protocol:HTTP/1.1",
"Tokenized click generates correct value in input box");
// Explicitly type in `flag:` renders autocomplete with values
EventUtils.synthesizeKey(" status-code:", {});
testAutocompleteContents(["status-code:200", "status-code:304"], document);
// Typing the exact value closes autocomplete
EventUtils.synthesizeKey("304", {});
ok(!document.querySelector(".devtools-autocomplete-popup"),
"Typing the exact value closes autocomplete");
// Check if mime-type has been correctly parsed out and values also get autocomplete
EventUtils.synthesizeKey(" mime-type:au", {});
testAutocompleteContents(["mime-type:audio/ogg"], document);
// The negative filter flags
EventUtils.synthesizeKey(" -", {});
@ -89,5 +134,9 @@ add_task(async function () {
"-transferred:",
], document);
// Autocomplete for negative filtering
EventUtils.synthesizeKey("is:", {});
testAutocompleteContents(["-is:cached", "-is:from-cache", "-is:running"], document);
await teardown(monitor);
});

View File

@ -83,6 +83,9 @@ html|button, html|select {
border-radius: 4px;
padding: 1px 0;
cursor: default;
text-overflow: ellipsis;
white-space: pre;
overflow: hidden;
}
.devtools-autocomplete-listbox .autocomplete-item > .initial-value,