mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 15:23:51 +00:00
Bug 1364096 - Autocomplete for network monitor flag values. r=ntim, r=jdescottes
This commit is contained in:
parent
d7cb1595c0
commit
199f0ac351
@ -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) => ({
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
DevToolsModules(
|
||||
'create-store.js',
|
||||
'filter-autocomplete-provider.js',
|
||||
'filter-predicates.js',
|
||||
'filter-text-utils.js',
|
||||
'format-utils.js',
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user