/** * This file (head.js) is injected into all other test contexts within * this directory, allowing one to utilize the functions here in said * tests without referencing head.js explicitly.
*/
// The below file (shared-head.js) handles imports, constants, and // utility functions, and is loaded into this context.
Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", this
);
// All tests are asynchronous.
waitForExplicitFinish();
const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); // To enable logging for try runs, just set the pref to true.
Services.prefs.setBoolPref("devtools.debugger.log", false);
// Uncomment this pref to dump all devtools emitted events to the console. // Services.prefs.setBoolPref("devtools.dump.emit", true);
// Always reset some prefs to their original values after the test finishes. const gDefaultFilters = Services.prefs.getCharPref( "devtools.netmonitor.filters"
);
// Reveal many columns for test
Services.prefs.setCharPref( "devtools.netmonitor.visibleColumns", '["initiator","contentSize","cookies","domain","duration",' + '"endTime","file","url","latency","method","protocol",' + '"remoteip","responseTime","scheme","setCookies",' + '"startTime","status","transferred","type","waterfall"]'
);
async function disableCacheAndReload(toolbox, waitForLoad) { // Disable the cache for any toolbox that it is opened from this point on.
Services.prefs.setBoolPref("devtools.cache.disabled", true);
// If the page which is reloaded is not found, this will likely cause // reloadTopLevelTarget to not return so let not wait for it. if (waitForLoad) {
await toolbox.commands.targetCommand.reloadTopLevelTarget();
} else {
toolbox.commands.targetCommand.reloadTopLevelTarget();
}
}
/** * Wait for 2 markers during document load.
*/ function waitForTimelineMarkers(monitor) { returnnew Promise(resolve => { const markers = [];
function handleTimelineEvent(marker) {
info(`Got marker: ${marker.name}`);
markers.push(marker); if (markers.length == 2) {
monitor.panelWin.api.off(
TEST_EVENTS.TIMELINE_EVENT,
handleTimelineEvent
);
info("Got two timeline markers, done waiting");
resolve(markers);
}
}
// Start collecting all networkEventUpdate event when panel is opened. // removeTab() should be called once all corresponded RECEIVED_* events finished. function startNetworkEventUpdateObserver(panelWin) {
updatingTypes.forEach(type =>
panelWin.api.on(type, actor => { const key = actor + "-" + updatedTypes[updatingTypes.indexOf(type)];
finishedQueue[key] = finishedQueue[key] ? finishedQueue[key] + 1 : 1;
})
);
async function waitForAllNetworkUpdateEvents() { function checkNetworkEventUpdateState() { for (const key in finishedQueue) { if (finishedQueue[key] > 0) { returnfalse;
}
} returntrue;
}
info("Wait for completion of all NetworkUpdateEvents packets...");
await waitUntil(() => checkNetworkEventUpdateState());
finishedQueue = {};
}
/** * Clears the network requests in the UI * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element.
*/
async function clearNetworkEvents(monitor) { const { store, windowRequire } = monitor.panelWin; const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
await waitForAllNetworkUpdateEvents();
info("Clearing the network requests in the UI");
store.dispatch(Actions.clearRequests({ isExplicitClear: true }));
}
function teardown(monitor, privateWindow) {
info("Destroying the specified network monitor.");
return (async function () { const tab = monitor.commands.descriptorFront.localTab;
/** * Wait for the request(s) to be fully notified to the frontend. * * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element. * @param {Number} getRequests * The number of request to wait for * @param {Object} options (optional) * - expectedEventTimings {Number} Number of EVENT_TIMINGS events to wait for. * In case of filtering, we get less of such events.
*/ function waitForNetworkEvents(monitor, getRequests, options = {}) { returnnew Promise(resolve => { const panel = monitor.panelWin;
let networkEvent = 0;
let payloadReady = 0;
let eventTimings = 0;
// Use a set to monitor blocked events, because a network resource might // only receive its blockedReason in onPayloadReady.
let nonBlockedNetworkEvents = new Set();
function onNetworkEvent(resource) {
networkEvent++; if (!resource.blockedReason) {
nonBlockedNetworkEvents.add(resource.actor);
}
maybeResolve(TEST_EVENTS.NETWORK_EVENT, resource.actor);
}
function onPayloadReady(resource) {
payloadReady++; if (resource.blockedReason) {
nonBlockedNetworkEvents.delete(resource.actor);
}
maybeResolve(EVENTS.PAYLOAD_READY, resource.actor);
}
function onEventTimings(response) {
eventTimings++;
maybeResolve(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
}
function onClearNetworkResources() { // Reset all counters.
networkEvent = 0;
nonBlockedNetworkEvents = new Set();
payloadReady = 0;
eventTimings = 0;
}
function maybeResolve(event, actor) { const { document } = monitor.panelWin; // Wait until networkEvent, payloadReady and event timings finish for each request. // The UI won't fetch timings when: // * hidden in background, // * for any blocked request,
let expectedEventTimings =
document.visibilityState == "hidden" ? 0 : nonBlockedNetworkEvents.size;
let expectedPayloadReady = getRequests; // Typically ignore this option if it is undefined or null if (typeof options?.expectedEventTimings == "number") {
expectedEventTimings = options.expectedEventTimings;
} if (typeof options?.expectedPayloadReady == "number") {
expectedPayloadReady = options.expectedPayloadReady;
}
info( "> Network event progress: " + "NetworkEvent: " +
networkEvent + "/" +
getRequests + ", " + "PayloadReady: " +
payloadReady + "/" +
expectedPayloadReady + ", " + "EventTimings: " +
eventTimings + "/" +
expectedEventTimings + ", " + "got " +
event + " for " +
actor
);
if (fuzzyUrl) {
ok(
requestItem.method.startsWith(method), "The attached method is correct."
);
ok(requestItem.url.startsWith(url), "The attached url is correct.");
} else {
is(requestItem.method, method, "The attached method is correct.");
is(requestItem.url, url.split("#")[0], "The attached url is correct.");
}
is(
target.querySelector(".requests-list-method").textContent,
method, "The displayed method is correct."
);
if (fuzzyUrl) {
ok(
target
.querySelector(".requests-list-file")
.textContent.startsWith(requestedFile), "The displayed file is correct."
);
ok(
target
.querySelector(".requests-list-file")
.getAttribute("title")
.startsWith(fileToolTip), "The tooltip file is correct."
);
} else {
is(
target.querySelector(".requests-list-file").textContent,
requestedFile, "The displayed file is correct."
);
is(
target.querySelector(".requests-list-file").getAttribute("title"),
fileToolTip, "The tooltip file is correct."
);
}
is(
target.querySelector(".requests-list-protocol").textContent,
protocol, "The displayed protocol is correct."
);
is(
target.querySelector(".requests-list-protocol").getAttribute("title"),
protocol, "The tooltip protocol is correct."
);
is(
target.querySelector(".requests-list-domain").textContent,
host, "The displayed domain is correct."
);
is(
target.querySelector(".requests-list-remoteip").textContent,
remoteIP, "The displayed remote IP is correct."
);
is(
target.querySelector(".requests-list-remoteip").getAttribute("title"),
remoteIP, "The tooltip remote IP is correct."
);
is(
target.querySelector(".requests-list-scheme").textContent,
scheme, "The displayed scheme is correct."
);
is(
target.querySelector(".requests-list-scheme").getAttribute("title"),
scheme, "The tooltip scheme is correct."
);
is(
target.querySelector(".requests-list-duration-time").textContent,
duration, "The displayed duration is correct."
);
is(
target.querySelector(".requests-list-duration-time").getAttribute("title"),
duration, "The tooltip duration is correct."
);
is(
target.querySelector(".requests-list-latency-time").textContent,
latency, "The displayed latency is correct."
);
is(
target.querySelector(".requests-list-latency-time").getAttribute("title"),
latency, "The tooltip latency is correct."
);
if (status !== undefined) { const value = target
.querySelector(".requests-list-status-code")
.getAttribute("data-status-code"); const codeValue = target.querySelector( ".requests-list-status-code"
).textContent; const tooltip = target
.querySelector(".requests-list-status-code")
.getAttribute("title");
info("Displayed status: " + value);
info("Displayed code: " + codeValue);
info("Tooltip status: " + tooltip);
is(
`${value}`,
displayedStatus ? `${displayedStatus}` : `${status}`, "The displayed status is correct."
);
is(`${codeValue}`, `${status}`, "The displayed status code is correct.");
is(tooltip, status + " " + statusText, "The tooltip status is correct.");
} if (cause !== undefined) { const value = Array.from(
target.querySelector(".requests-list-initiator").childNodes
)
.filter(node => node.nodeType === Node.ELEMENT_NODE)
.map(({ textContent }) => textContent)
.join(""); const tooltip = target
.querySelector(".requests-list-initiator")
.getAttribute("title");
info("Displayed cause: " + value);
info("Tooltip cause: " + tooltip);
ok(value.includes(cause.type), "The displayed cause is correct.");
ok(tooltip.includes(cause.type), "The tooltip cause is correct.");
} if (type !== undefined) { const value = target.querySelector(".requests-list-type").textContent;
let tooltip = target
.querySelector(".requests-list-type")
.getAttribute("title");
info("Displayed type: " + value);
info("Tooltip type: " + tooltip);
is(value, type, "The displayed type is correct."); if (Object.is(tooltip, null)) {
tooltip = undefined;
}
is(tooltip, fullMimeType, "The tooltip type is correct.");
} if (transferred !== undefined) { const value = target.querySelector( ".requests-list-transferred"
).textContent; const tooltip = target
.querySelector(".requests-list-transferred")
.getAttribute("title");
info("Displayed transferred size: " + value);
info("Tooltip transferred size: " + tooltip);
is(value, transferred, "The displayed transferred size is correct.");
is(tooltip, transferred, "The tooltip transferred size is correct.");
} if (size !== undefined) { const value = target.querySelector(".requests-list-size").textContent; const tooltip = target
.querySelector(".requests-list-size")
.getAttribute("title");
info("Displayed size: " + value);
info("Tooltip size: " + tooltip);
is(value, size, "The displayed size is correct.");
is(tooltip, size, "The tooltip size is correct.");
} if (time !== undefined) { const value = target.querySelector( ".requests-list-timings-total"
).textContent; const tooltip = target
.querySelector(".requests-list-timings-total")
.getAttribute("title");
info("Displayed time: " + value);
info("Tooltip time: " + tooltip); Assert.greaterOrEqual(
~~value.match(/[0-9]+/),
0, "The displayed time is correct."
); Assert.greaterOrEqual(
~~tooltip.match(/[0-9]+/),
0, "The tooltip time is correct."
);
}
if (visibleIndex !== -1) { if (visibleIndex % 2 === 0) {
ok(target.classList.contains("even"), "Item should have 'even' class.");
ok(!target.classList.contains("odd"), "Item shouldn't have 'odd' class.");
} else {
ok(
!target.classList.contains("even"), "Item shouldn't have 'even' class."
);
ok(target.classList.contains("odd"), "Item should have 'odd' class.");
}
}
}
/** * Tests if a button for a filter of given type is the only one checked. * * @param string filterType * The type of the filter that should be the only one checked.
*/ function testFilterButtons(monitor, filterType) { const doc = monitor.panelWin.document; const target = doc.querySelector( ".requests-list-filter-" + filterType + "-button"
);
ok(target, `Filter button '${filterType}' was found`); const buttons = [
...doc.querySelectorAll(".requests-list-filter-buttons button"),
];
ok(!!buttons.length, "More than zero filter buttons were found");
// Only target should be checked. const checkStatus = buttons.map(button => (button == target ? 1 : 0));
testFilterButtonsCustom(monitor, checkStatus);
}
/** * Tests if filter buttons have 'checked' attributes set correctly. * * @param array aIsChecked * An array specifying if a button at given index should have a * 'checked' attribute. For example, if the third item of the array * evaluates to true, the third button should be checked.
*/ function testFilterButtonsCustom(monitor, isChecked) { const doc = monitor.panelWin.document; const buttons = doc.querySelectorAll(".requests-list-filter-buttons button"); for (let i = 0; i < isChecked.length; i++) { const button = buttons[i]; if (isChecked[i]) {
is(
button.getAttribute("aria-pressed"), "true", "The " + button.id + " button should set 'aria-pressed' = true."
);
} else {
is(
button.getAttribute("aria-pressed"), "false", "The " + button.id + " button should set 'aria-pressed' = false."
);
}
}
}
/** * Performs a single XMLHttpRequest and returns a promise that resolves once * the request has loaded. * * @param Object data * { method: the request method (default: "GET"), * url: the url to request (default: content.location.href), * body: the request body to send (default: ""), * nocache: append an unique token to the query string (default: true), * requestHeaders: set request headers (default: none) * } * * @return Promise A promise that's resolved with object * { status: XMLHttpRequest.status, * response: XMLHttpRequest.response } *
*/ function promiseXHR(data) { returnnew Promise(resolve => { const xhr = new content.XMLHttpRequest();
const method = data.method || "GET";
let url = data.url || content.location.href; const body = data.body || "";
if (data.nocache) {
url += "?devtools-cachebust=" + Math.random();
}
// Set request headers if (data.requestHeaders) {
data.requestHeaders.forEach(header => {
xhr.setRequestHeader(header.name, header.value);
});
}
xhr.send(body);
});
}
/** * Performs a single websocket request and returns a promise that resolves once * the request has loaded. * * @param Object data * { url: the url to request (default: content.location.href), * nocache: append an unique token to the query string (default: true), * } * * @return Promise A promise that's resolved with object * { status: websocket status(101), * response: empty string } *
*/ function promiseWS(data) { returnnew Promise(resolve => {
let url = data.url;
if (data.nocache) {
url += "?devtools-cachebust=" + Math.random();
}
/* Create websocket instance */ const socket = new content.WebSocket(url);
/* Since we only use HTTP server to mock websocket, so just ignore the error */
socket.onclose = () => {
socket.close();
resolve({
status: 101,
response: "",
});
};
/** * Perform the specified requests in the context of the page content. * * @param Array requests * An array of objects specifying the requests to perform. See * shared/test/frame-script-utils.js for more information. * * @return A promise that resolves once the requests complete.
*/
async function performRequestsInContent(requests) { if (!Array.isArray(requests)) {
requests = [requests];
}
const responses = [];
info("Performing requests in the context of the content.");
function testColumnsAlignment(headers, requestList) { const firstRequestLine = requestList.childNodes[0];
// Find number of columns const numberOfColumns = headers.childElementCount; for (let i = 0; i < numberOfColumns; i++) { const headerColumn = headers.childNodes[i]; const requestColumn = firstRequestLine.childNodes[i];
is(
headerColumn.getBoundingClientRect().left,
requestColumn.getBoundingClientRect().left, "Headers for columns number " + i + " are aligned."
);
}
}
ok(
document.querySelector(`#requests-list-${column}-button`),
`Column ${column} should be visible`
);
}
/** * Select a request and switch to its response panel. * * @param {Number} index The request index to be selected
*/
async function selectIndexAndWaitForSourceEditor(monitor, index) { const { document } = monitor.panelWin; const onResponseContent = monitor.panelWin.api.once(
TEST_EVENTS.RECEIVED_RESPONSE_CONTENT
); // Select the request first, as it may try to fetch whatever is the current request's // responseContent if we select the ResponseTab first.
EventUtils.sendMouseEvent(
{ type: "mousedown" },
document.querySelectorAll(".request-list-item")[index]
); // We may already be on the ResponseTab, so only select it if needed. const editor = document.querySelector("#response-panel .CodeMirror-code"); if (!editor) { const waitDOM = waitForDOM(document, "#response-panel .CodeMirror-code");
document.querySelector("#response-tab").click();
await waitDOM;
}
await onResponseContent;
}
/** * Helper function for executing XHRs on a test page. * * @param {Number} count Number of requests to be executed.
*/
async function performRequests(monitor, tab, count) { const wait = waitForNetworkEvents(monitor, count);
await ContentTask.spawn(tab.linkedBrowser, count, requestCount => {
content.wrappedJSObject.performRequests(requestCount);
});
await wait;
}
/** * Helper function for retrieving `.CodeMirror` content
*/ function getCodeMirrorValue(monitor) { const { document } = monitor.panelWin; return document.querySelector(".CodeMirror").CodeMirror.getValue();
}
/** * Helper function opening the options menu
*/ function openSettingsMenu(monitor) { const { document } = monitor.panelWin;
document.querySelector(".netmonitor-settings-menu-button").click();
}
function getSettingsMenuItem(monitor, itemKey) { // The settings menu is injected into the toolbox document, // so we must use the panelWin parent to query for items const { parent } = monitor.panelWin; const { document } = parent;
/** * Wait for lazy fields to be loaded in a request. * * @param Object Store redux store containing request list. * @param array fields array of strings which contain field names to be checked * on the request.
*/ function waitForRequestData(store, fields, id) { return waitUntil(() => {
let item; if (id) {
item = getRequestById(store.getState(), id);
} else {
item = getSortedRequests(store.getState())[0];
} if (!item) { returnfalse;
} for (const field of fields) { if (!item[field]) { returnfalse;
}
} returntrue;
});
}
// Telemetry
/** * Helper for verifying telemetry event. * * @param Object expectedEvent object representing expected event data. * @param Object query fields specifying category, method and object * of the target telemetry event.
*/ function checkTelemetryEvent(expectedEvent, query) { const events = queryTelemetryEvents(query);
is(events.length, 1, "There was only 1 event logged");
const [event] = events; Assert.greater(
Number(event.session_id),
0, "There is a valid session_id in the logged event"
);
const f = e => JSON.stringify(e, null, 2);
is(
f(event),
f({
...expectedEvent,
session_id: event.session_id,
}), "The event has the expected data"
);
}
// Return the `extra` field (which is event[5]e). return filtersChangedEvents.map(event => event[5]);
} /** * Check that the provided requests match the requests displayed in the netmonitor. * * @param {array} requests * The expected requests. * @param {object} monitor * The netmonitor instance. * @param {object=} options * @param {boolean} allowDifferentOrder * When set to true, requests are allowed to be in a different order in the * netmonitor than in the expected requests array. Defaults to false.
*/ function validateRequests(requests, monitor, options = {}) { const { allowDifferentOrder } = options; const { document, store, windowRequire } = monitor.panelWin;
if (stack) {
ok(stacktrace, `Request #${i} has a stacktrace`); Assert.greater(
stackLen,
0,
`Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`
);
// if "stack" is array, check the details about the top stack frames if (Array.isArray(stack)) {
stack.forEach((frame, j) => { // If the `fn` is "*", it means the request is triggered from chrome // resources, e.g. `resource:///modules/XX.jsm`, so we skip checking // the function name for now (bug 1280266). if (frame.file.startsWith("resource:///")) {
todo(false, "Requests from chrome resource should not be included");
} else {
let value = stacktrace[j].functionName; if (Object.is(value, null)) {
value = undefined;
}
is(
value,
frame.fn,
`Request #${i} has the correct function on JS stack frame #${j}`
);
is(
stacktrace[j].filename.split("/").pop(),
frame.file.split("/").pop(),
`Request #${i} has the correct file on JS stack frame #${j}`
);
is(
stacktrace[j].lineNumber,
frame.line,
`Request #${i} has the correct line number on JS stack frame #${j}`
);
value = stacktrace[j].asyncCause; if (Object.is(value, null)) {
value = undefined;
}
is(
value,
frame.asyncCause,
`Request #${i} has the correct async cause on JS stack frame #${j}`
);
}
});
}
} else {
is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
}
});
}
/** * @see getNetmonitorContextMenuItem in shared-head.js
*/ function getContextMenuItem(monitor, id) { return getNetmonitorContextMenuItem(monitor, id);
}
/** * @see selectNetmonitorContextMenuItem in shared-head.js
*/
async function selectContextMenuItem(monitor, id) { return selectNetmonitorContextMenuItem(monitor, id);
}
/** * Wait for DOM being in specific state. But, do not wait * for change if it's in the expected state already.
*/
async function waitForDOMIfNeeded(target, selector, expectedLength = 1) { returnnew Promise(resolve => { const elements = target.querySelectorAll(selector); if (elements.length == expectedLength) {
resolve(elements);
} else {
waitForDOM(target, selector, expectedLength).then(elems => {
resolve(elems);
});
}
});
}
/** * Helper for blocking or unblocking a request via the list item's context menu. * * @param {Element} element * Target request list item to be right clicked to bring up its context menu. * @param {Object} monitor * The netmonitor instance used for retrieving a context menu element. * @param {Object} store * The redux store (wait-service middleware required). * @param {String} action * The action, block or unblock, to construct a corresponding context menu id.
*/
async function toggleBlockedUrl(element, monitor, store, action = "block") {
EventUtils.sendMouseEvent({ type: "contextmenu" }, element); const contextMenuId = `request-list-context-${action}-url`; const onRequestComplete = waitForDispatch(
store, "REQUEST_BLOCKING_UPDATE_COMPLETE"
);
await selectContextMenuItem(monitor, contextMenuId);
info(`Wait for selected request to be ${action}ed`);
await onRequestComplete;
info(`Selected request is now ${action}ed`);
}
/** * Find and click an element * * @param {Element} element * Target element to be clicked * @param {Object} monitor * The netmonitor instance used for retrieving the window.
*/
function clickElement(element, monitor) {
EventUtils.synthesizeMouseAtCenter(element, {}, monitor.panelWin);
}
/** * Register a listener to be notified when a favicon finished loading and * dispatch a "devtools:test:favicon" event to the favicon's link element. * * @param {Browser} browser * Target browser to observe the favicon load.
*/ function registerFaviconNotifier(browser) { const listener = async name => { if (name == "SetIcon" || name == "SetFailedIcon") {
await SpecialPowers.spawn(browser, [], async () => {
content.document
.querySelector("link[rel='icon']")
.dispatchEvent(new content.CustomEvent("devtools:test:favicon"));
});
LinkHandlerParent.removeListenerForTests(listener);
}
};
LinkHandlerParent.addListenerForTests(listener);
}
/** * Predicates used when sorting items. * * @param object first * The first item used in the comparison. * @param object second * The second item used in the comparison. * @return number * <0 to sort first to a lower index than second * =0 to leave first and second unchanged with respect to each other * >0 to sort second to a lower index than first
*/
function compareValues(first, second) { if (first === second) { return 0;
} return first > second ? 1 : -1;
}
/** * Click on the "Response" tab to open "Response" panel in the sidebar. * @param {Document} doc * Network panel document. * @param {String} name * Network panel sidebar tab name.
*/ const clickOnSidebarTab = (doc, name) => {
AccessibilityUtils.setEnv({ // Keyboard accessibility is handled on the sidebar tabs container level // (nav). Users can use arrow keys to navigate between and select tabs.
nonNegativeTabIndexRule: false,
});
EventUtils.sendMouseEvent(
{ type: "click" },
doc.querySelector(`#${name}-tab`)
);
AccessibilityUtils.resetEnv();
};
/** * Add a new blocked request URL pattern. The request blocking sidepanel should * already be opened. * * @param {string} pattern * The URL pattern to add to block requests. * @param {Object} monitor * The netmonitor instance.
*/
async function addBlockedRequest(pattern, monitor) {
info("Add a blocked request for the URL pattern " + pattern); const doc = monitor.panelWin.document;
const addRequestForm = await waitFor(() =>
doc.querySelector( "#network-action-bar-blocked-panel .request-blocking-add-form"
)
);
ok(!!addRequestForm, "The request blocking side panel is not available");
info("Wait for the add input to get focus");
await waitFor(() =>
addRequestForm.querySelector("input.devtools-searchinput:focus")
);
/** * Check if the provided .request-list-item element corresponds to a blocked * request. * * @param {Element} * The request's DOM element. * @returns {boolean} * True if the request is displayed as blocked, false otherwise.
*/ function checkRequestListItemBlocked(item) { return item.className.includes("blocked");
}
/** * Type the provided string the netmonitor window. The correct input should be * focused prior to using this helper. * * @param {string} string * The string to type. * @param {Object} monitor * The netmonitor instance used to type the string.
*/ function typeInNetmonitor(string, monitor) { for (const ch of string) {
EventUtils.synthesizeKey(ch, {}, monitor.panelWin);
}
}
/** * Check if a valid numerical size is displayed in the request column for the * provided request. * * @param {Element} request * A request element from the netmonitor requests list. * @return {boolean} * True if the size column contains a valid size, false otherwise. *
*/ function hasValidSize(request) { const VALID_SIZE_RE = /^\d+(\.\d+)? \w+/; return VALID_SIZE_RE.test(
request.querySelector(".requests-list-size").innerText
);
}
function getThrottleProfileItem(monitor, profileId) { const toolboxDoc = monitor.toolbox.doc;
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.