chrome-devtools-frontend
Version:
Chrome DevTools UI
1,064 lines (905 loc) • 46.2 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as HAR from '../../models/har/har.js';
import * as Logs from '../../models/logs/logs.js';
import {
findMenuItemWithLabel,
getContextMenuForElement,
getMenu,
getMenuItemLabels,
} from '../../testing/ContextMenuHelpers.js';
import {dispatchClickEvent, raf, renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {
createTarget,
describeWithEnvironment,
registerNoopActions,
stubNoopSettings
} from '../../testing/EnvironmentHelpers.js';
import {expectCalled} from '../../testing/ExpectStubCall.js';
import {stubFileManager} from '../../testing/FileManagerHelpers.js';
import {describeWithMockConnection, dispatchEvent} from '../../testing/MockConnection.js';
import {activate} from '../../testing/ResourceTreeHelpers.js';
import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Network from './network.js';
const {urlString} = Platform.DevToolsPath;
describeWithMockConnection('NetworkLogView', () => {
let target: SDK.Target.Target;
let networkLogView: Network.NetworkLogView.NetworkLogView;
let networkLog: Logs.NetworkLog.NetworkLog;
beforeEach(() => {
const dummyStorage = new Common.Settings.SettingsStorage({});
for (const settingName of ['network-color-code-resource-types', 'network.group-by-frame']) {
Common.Settings.registerSettingExtension({
settingName,
settingType: Common.Settings.SettingType.BOOLEAN,
defaultValue: false,
});
}
Common.Settings.Settings.instance({
forceNew: true,
syncedStorage: dummyStorage,
globalStorage: dummyStorage,
localStorage: dummyStorage,
});
registerNoopActions(['network.toggle-recording', 'inspector-main.reload']);
sinon.stub(UI.ShortcutRegistry.ShortcutRegistry, 'instance').returns({
shortcutTitleForAction: () => {},
shortcutsForAction: () => [],
} as unknown as UI.ShortcutRegistry.ShortcutRegistry);
networkLog = Logs.NetworkLog.NetworkLog.instance();
const tabTarget = createTarget({type: SDK.Target.Type.TAB});
createTarget({parentTarget: tabTarget, subtype: 'prerender'});
target = createTarget({parentTarget: tabTarget});
});
afterEach(() => {
if (networkLogView) {
networkLogView.detach();
}
});
let nextId = 0;
function createNetworkRequest(
url: string,
options: {requestHeaders?: SDK.NetworkRequest.NameValue[], finished?: boolean, target?: SDK.Target.Target}):
SDK.NetworkRequest.NetworkRequest {
const effectiveTarget = options.target || target;
const networkManager = effectiveTarget.model(SDK.NetworkManager.NetworkManager);
assert.exists(networkManager);
let request: SDK.NetworkRequest.NetworkRequest|undefined;
const onRequestStarted = (event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.RequestStartedEvent>) => {
request = event.data.request;
};
networkManager.addEventListener(SDK.NetworkManager.Events.RequestStarted, onRequestStarted);
dispatchEvent(
effectiveTarget, 'Network.requestWillBeSent',
{requestId: `request${++nextId}`, loaderId: 'loaderId', request: {url}} as unknown as
Protocol.Network.RequestWillBeSentEvent);
networkManager.removeEventListener(SDK.NetworkManager.Events.RequestStarted, onRequestStarted);
assert.exists(request);
request.requestMethod = 'GET';
if (options.requestHeaders) {
request.setRequestHeaders(options.requestHeaders);
}
if (options.finished) {
request.finished = true;
}
return request;
}
function createEnvironment() {
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
return {rootNode, filterBar, networkLogView};
}
it('generates a valid curl command when some headers don\'t have values', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [
{name: 'header-with-value', value: 'some value'},
{name: 'no-value-header', value: ''},
],
});
const actual = await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix');
const expected =
'curl \'http://localhost\' \\\n -H \'header-with-value: some value\' \\\n -H \'no-value-header;\'';
assert.strictEqual(actual, expected);
});
// Note this isn't an ideal test as the internal headers are generated rather than explicitly added,
// are only added on HTTP/2 and HTTP/3, have a preceeding colon like `:authority` but it still tests
// the stripping function.
it('generates a valid curl command while stripping internal headers', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [
{name: 'authority', value: 'www.example.com'},
],
});
const actual = await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix');
const expected = 'curl \'http://localhost\'';
assert.strictEqual(actual, expected);
});
it('generates a valid curl command when header values contain double quotes', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [{name: 'cookie', value: 'eva="Sg4="'}],
});
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix'),
'curl \'http://localhost\' -b \'eva=\"Sg4=\"\'',
);
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'win'),
'curl ^"http://localhost^" -b ^"eva=^\\^"Sg4=^\\^"^"',
);
});
it('generates a valid curl command when header values contain percentages', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [{name: 'cookie', value: 'eva=%22Sg4%3D%22'}],
});
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix'),
'curl \'http://localhost\' -b \'eva=%22Sg4%3D%22\'',
);
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'win'),
'curl ^"http://localhost^" -b ^"eva=^%^22Sg4^%^3D^%^22^"',
);
});
it('generates a valid curl command when header values contain newline and ampersand', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [{name: 'cookie', value: 'query=evil\n\n & cmd /c calc.exe \n\n'}],
});
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix'),
'curl \'http://localhost\' -b $\'query=evil\\n\\n & cmd /c calc.exe \\n\\n\'',
);
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'win'),
'curl ^\"http://localhost^\" -b ^\"query=evil^\n\n^\n\n ^& cmd /c calc.exe ^\n\n^\n\n^\"',
);
});
it('generates a valid curl command when header values contain CRLF', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [{name: 'cookie', value: 'query=evil\r\n & cmd /c calc.exe \n\n'}],
});
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix'),
'curl \'http://localhost\' -b $\'query=evil\\r\\n & cmd /c calc.exe \\n\\n\'',
);
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'win'),
'curl ^\"http://localhost^\" -b ^\"query=evil^\n\n ^& cmd /c calc.exe ^\n\n^\n\n^\"',
);
});
it('generates a valid curl command when header values contain CR only', async () => {
const request = createNetworkRequest(urlString`http://localhost`, {
requestHeaders: [{name: 'cookie', value: 'query=evil\r & cmd /c calc.exe'}],
});
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'unix'),
'curl \'http://localhost\' -b $\'query=evil\\r & cmd /c calc.exe\'',
);
assert.strictEqual(
await Network.NetworkLogView.NetworkLogView.generateCurlCommand(request, 'win'),
'curl ^\"http://localhost^\" -b ^\"query=evil^\n\n ^& cmd /c calc.exe^\"',
);
});
const tests = (inScope: boolean) => () => {
beforeEach(() => {
networkLogView = createNetworkLogView();
SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
});
it('adds dividers on main frame load events', async () => {
const addEventDividers = sinon.spy(networkLogView.columns(), 'addEventDividers');
networkLogView.setRecording(true);
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.Load, {resourceTreeModel, loadTime: 5});
resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.DOMContentLoaded, 6);
if (inScope) {
sinon.assert.calledTwice(addEventDividers);
sinon.assert.calledWith(addEventDividers.getCall(0), [5], 'network-load-divider');
sinon.assert.calledWith(addEventDividers.getCall(1), [6], 'network-dcl-divider');
} else {
sinon.assert.notCalled(addEventDividers);
}
});
it('can export all as HAR', async () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
const harWriterWrite = sinon.stub(HAR.Writer.Writer, 'write').resolves();
const URL_HOST = 'example.com';
target.setInspectedURL(urlString`${`http://${URL_HOST}/foo`}`);
const fileManager = stubFileManager();
const FINISHED_REQUEST_1 = createNetworkRequest('http://example.com/', {finished: true});
const FINISHED_REQUEST_2 = createNetworkRequest('http://example.com/favicon.ico', {finished: true});
const UNFINISHED_REQUEST = createNetworkRequest('http://example.com/background.bmp', {finished: false});
sinon.stub(Logs.NetworkLog.NetworkLog.instance(), 'requests').returns([
FINISHED_REQUEST_1,
FINISHED_REQUEST_2,
UNFINISHED_REQUEST,
]);
await networkLogView.exportAll({sanitize: false});
if (inScope) {
assert.isTrue(
harWriterWrite.calledOnceWith(sinon.match.any, [FINISHED_REQUEST_1, FINISHED_REQUEST_2], sinon.match.any));
sinon.assert.calledOnce(fileManager.save);
sinon.assert.calledOnce(fileManager.close);
} else {
sinon.assert.notCalled(harWriterWrite);
sinon.assert.notCalled(fileManager.save);
sinon.assert.notCalled(fileManager.close);
}
});
it('can import and filter from HAR', async () => {
const URL_1 = urlString`http://example.com/`;
const URL_2 = urlString`http://example.com/favicon.ico`;
function makeHarEntry(url: Platform.DevToolsPath.UrlString) {
return {
request: {method: 'GET', url, headersSize: -1, bodySize: 0},
response: {status: 0, content: {size: 0, mimeType: 'x-unknown'}, headersSize: -1, bodySize: -1},
startedDateTime: null,
time: null,
timings: {blocked: null, dns: -1, ssl: -1, connect: -1, send: 0, wait: 0, receive: 0},
};
}
const har = {
log: {
version: '1.2',
creator: {name: 'WebInspector', version: '537.36'},
entries: [makeHarEntry(URL_1), makeHarEntry(URL_2)],
},
};
renderElementIntoDOM(networkLogView);
const blob = new Blob([JSON.stringify(har)], {type: 'text/plain'});
const file = new File([blob], 'log.har');
await networkLogView.onLoadFromFile(file);
await RenderCoordinator.done({waitForWork: true});
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [URL_1, URL_2]);
networkLogView.setTextFilterValue('favicon');
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [URL_2]);
});
it('shows summary toolbar with content', () => {
target.setInspectedURL(urlString`http://example.com/`);
const request = createNetworkRequest('http://example.com/', {finished: true});
request.endTime = 0.669414;
request.setIssueTime(0.435136, 0.435136);
request.setResourceType(Common.ResourceType.resourceTypes.Document);
networkLogView.setRecording(true);
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
resourceTreeModel.dispatchEventToListeners(
SDK.ResourceTreeModel.Events.Load, {resourceTreeModel, loadTime: 0.686191});
resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.DOMContentLoaded, 0.683709);
renderElementIntoDOM(networkLogView);
const toolbar = networkLogView.summaryToolbar();
const textElements = toolbar.querySelectorAll('.toolbar-text');
assert.exists(textElements);
const textContents = [...textElements].map(item => item.textContent);
if (inScope) {
assert.deepEqual(textContents, [
'1 requests',
'0\u00a0B transferred',
'0\u00a0B resources',
'Finish: 234\u00a0ms',
'DOMContentLoaded: 249\u00a0ms',
'Load: 251\u00a0ms',
]);
} else {
assert.lengthOf(textElements, 0);
}
});
};
describe('in scope', tests(true));
describe('out of scope', tests(false));
const handlesSwitchingScope = (preserveLog: boolean) => async () => {
Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log').set(preserveLog);
SDK.TargetManager.TargetManager.instance().setScopeTarget(target);
const anotherTarget = createTarget();
const networkManager = target.model(SDK.NetworkManager.NetworkManager);
assert.exists(networkManager);
const request1 = createNetworkRequest('url1', {target});
const request2 = createNetworkRequest('url2', {target});
const request3 = createNetworkRequest('url3', {target: anotherTarget});
networkLogView = createNetworkLogView();
renderElementIntoDOM(networkLogView);
await RenderCoordinator.done();
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()), [request1, request2]);
SDK.TargetManager.TargetManager.instance().setScopeTarget(anotherTarget);
await RenderCoordinator.done();
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()),
preserveLog ? [request1, request2, request3] : [request3]);
};
it('replaces requests when switching scope with preserve log off', handlesSwitchingScope(false));
it('appends requests when switching scope with preserve log on', handlesSwitchingScope(true));
it('appends requests on prerender activation with preserve log on', async () => {
Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log').set(true);
SDK.TargetManager.TargetManager.instance().setScopeTarget(target);
const anotherTarget = createTarget();
const networkManager = target.model(SDK.NetworkManager.NetworkManager);
assert.exists(networkManager);
const request1 = createNetworkRequest('url1', {target});
const request2 = createNetworkRequest('url2', {target});
const request3 = createNetworkRequest('url3', {target: anotherTarget});
networkLogView = createNetworkLogView();
renderElementIntoDOM(networkLogView);
await RenderCoordinator.done();
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()), [request1, request2]);
activate(target);
await RenderCoordinator.done();
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()),
[request1, request2, request3]);
});
it('can hide Chrome extension requests', async () => {
createNetworkRequest('chrome-extension://url1', {target});
createNetworkRequest('url2', {target});
let rootNode;
let filterBar;
({rootNode, filterBar, networkLogView} = createEnvironment());
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()),
[urlString`chrome-extension://url1`, urlString`url2`]);
const dropdown = await getMoreTypesDropdown(filterBar);
if (!dropdown) {
return;
}
let softMenu = getMenu(() => dropdown.click());
let hideExtensionURL = getDropdownItem(softMenu, 'Hide extension URLs');
assert.isFalse(hideExtensionURL.buildDescriptor().checked);
softMenu.invokeHandler(hideExtensionURL.id());
softMenu.discard();
softMenu = getMenu(() => dropdown.click());
hideExtensionURL = getDropdownItem(softMenu, 'Hide extension URLs');
assert.isTrue(hideExtensionURL.buildDescriptor().checked);
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [urlString`url2`]);
softMenu.discard();
});
it('displays correct count for more filters', async () => {
let filterBar;
({filterBar, networkLogView} = createEnvironment());
const dropdown = await getMoreTypesDropdown(filterBar);
if (!dropdown) {
return;
}
assert.strictEqual(getMoreFiltersActiveCount(filterBar), '0');
assert.isTrue(getCountAdorner(filterBar)?.classList.contains('hidden'));
const softMenu = getMenu(() => dropdown.click());
await selectMoreFiltersOption(softMenu, 'Hide extension URLs');
assert.strictEqual(getMoreFiltersActiveCount(filterBar), '1');
assert.isFalse(getCountAdorner(filterBar)?.classList.contains('hidden'));
softMenu.discard();
});
it('can filter requests with blocked response cookies', async () => {
const request1 = createNetworkRequest('url1', {target});
request1.blockedResponseCookies = () => [{
blockedReasons: [Protocol.Network.SetCookieBlockedReason.SameSiteNoneInsecure],
cookie: null,
cookieLine: 'foo=bar; SameSite=None',
}];
createNetworkRequest('url2', {target});
let rootNode;
let filterBar;
({rootNode, filterBar, networkLogView} = createEnvironment());
assert.deepEqual(
rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()),
[urlString`url1`, urlString`url2`]);
const dropdown = await getMoreTypesDropdown(filterBar);
if (!dropdown) {
return;
}
let softMenu = getMenu(() => dropdown.click());
let blockedResponseCookies = getDropdownItem(softMenu, 'Blocked response cookies');
assert.isFalse(blockedResponseCookies.buildDescriptor().checked);
softMenu.invokeHandler(blockedResponseCookies.id());
softMenu.discard();
softMenu = getMenu(() => dropdown.click());
blockedResponseCookies = getDropdownItem(softMenu, 'Blocked response cookies');
assert.isTrue(blockedResponseCookies.buildDescriptor().checked);
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlString`url1`,
]);
softMenu.discard();
});
it('lists selected options in more filters tooltip', async () => {
let filterBar;
({filterBar, networkLogView} = createEnvironment());
const dropdown = await getMoreTypesDropdown(filterBar);
assert.exists(dropdown);
assert.strictEqual(dropdown.title, 'Show only/hide requests');
const softMenu = getMenu(() => dropdown.click());
await selectMoreFiltersOption(softMenu, 'Blocked response cookies');
await selectMoreFiltersOption(softMenu, 'Hide extension URLs');
assert.strictEqual(dropdown.title, 'Hide extension URLs, Blocked response cookies');
softMenu.discard();
});
it('updates tooltip to default when more filters option deselected', async () => {
let filterBar;
({filterBar, networkLogView} = createEnvironment());
const dropdown = await getMoreTypesDropdown(filterBar);
assert.exists(dropdown);
assert.strictEqual(dropdown.title, 'Show only/hide requests');
const softMenu = getMenu(() => dropdown.click());
await selectMoreFiltersOption(softMenu, 'Blocked response cookies');
assert.strictEqual(dropdown.title, 'Blocked response cookies');
await selectMoreFiltersOption(softMenu, 'Blocked response cookies');
assert.strictEqual(dropdown.title, 'Show only/hide requests');
softMenu.discard();
});
it('can remove requests', async () => {
networkLogView = createNetworkLogView();
const request = createNetworkRequest('url1', {target});
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.lengthOf(rootNode.children, 1);
networkLog.dispatchEventToListeners(Logs.NetworkLog.Events.RequestRemoved, {request});
assert.lengthOf(rootNode.children, 0);
});
it('correctly shows/hides "Copy all as HAR (with sensitive data)" menu item', async () => {
const networkShowOptionsToGenerateHarWithSensitiveDataSetting = Common.Settings.Settings.instance().createSetting(
'network.show-options-to-generate-har-with-sensitive-data', false);
createNetworkRequest('url1', {target});
networkLogView = createNetworkLogView(new UI.FilterBar.FilterBar('network-panel', true));
renderElementIntoDOM(networkLogView);
networkLogView.columns().dataGrid().rootNode().children[0].select();
const {element} = networkLogView.columns().dataGrid();
{
// Setting is disabled (default), menu item must be hidden.
networkShowOptionsToGenerateHarWithSensitiveDataSetting.set(false);
const contextMenu = getContextMenuForElement(element);
const clipboardSection = contextMenu.clipboardSection();
const copyMenu = findMenuItemWithLabel(clipboardSection, 'Copy') as UI.ContextMenu.SubMenu;
assert.isUndefined(findMenuItemWithLabel(copyMenu.footerSection(), 'Copy all as HAR (with sensitive data)'));
}
{
// Setting is enabled, menu item must be shown.
networkShowOptionsToGenerateHarWithSensitiveDataSetting.set(true);
const contextMenu = getContextMenuForElement(element);
const clipboardSection = contextMenu.clipboardSection();
const copyMenu = findMenuItemWithLabel(clipboardSection, 'Copy') as UI.ContextMenu.SubMenu;
assert.isDefined(findMenuItemWithLabel(copyMenu.footerSection(), 'Copy all as HAR (with sensitive data)'));
}
});
it('correctly shows and hides waterfall column', async () => {
const columnSettings = Common.Settings.Settings.instance().createSetting('network-log-columns', {});
columnSettings.set({
waterfall: {visible: false, title: 'waterfall'},
});
networkLogView = createNetworkLogView();
let columns = networkLogView.columns();
let networkColumnWidget = columns.dataGrid().asWidget().parentWidget();
assert.instanceOf(networkColumnWidget, UI.SplitWidget.SplitWidget);
assert.strictEqual((networkColumnWidget).showMode(), UI.SplitWidget.ShowMode.ONLY_MAIN);
columnSettings.set({
waterfall: {visible: true, title: 'waterfall'},
});
networkLogView = createNetworkLogView();
columns = networkLogView.columns();
columns.switchViewMode(true);
networkColumnWidget = columns.dataGrid().asWidget().parentWidget();
assert.instanceOf(networkColumnWidget, UI.SplitWidget.SplitWidget);
assert.strictEqual((networkColumnWidget).showMode(), UI.SplitWidget.ShowMode.BOTH);
});
function createOverrideRequests() {
const urlNotOverridden = urlString`url-not-overridden`;
const urlHeaderOverridden = urlString`url-header-overridden`;
const urlContentOverridden = urlString`url-content-overridden`;
const urlHeaderAndContentOverridden = urlString`url-header-und-content-overridden`;
createNetworkRequest(urlNotOverridden, {target});
const r2 = createNetworkRequest(urlHeaderOverridden, {target});
const r3 = createNetworkRequest(urlContentOverridden, {target});
const r4 = createNetworkRequest(urlHeaderAndContentOverridden, {target});
// set up overrides
r2.originalResponseHeaders = [{name: 'content-type', value: 'x'}];
r2.responseHeaders = [{name: 'content-type', value: 'overriden'}];
r3.hasOverriddenContent = true;
r4.originalResponseHeaders = [{name: 'age', value: 'x'}];
r4.responseHeaders = [{name: 'age', value: 'overriden'}];
r4.hasOverriddenContent = true;
return {urlNotOverridden, urlHeaderOverridden, urlContentOverridden, urlHeaderAndContentOverridden};
}
it('can apply filter - has-overrides:yes', async () => {
const {urlHeaderOverridden, urlContentOverridden, urlHeaderAndContentOverridden} = createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-overrides:yes');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlHeaderOverridden,
urlContentOverridden,
urlHeaderAndContentOverridden,
]);
});
it('can apply filter - has-overrides:no', async () => {
const {urlNotOverridden} = createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-overrides:no');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlNotOverridden,
]);
});
it('can apply filter - has-overrides:headers', async () => {
const {urlHeaderOverridden, urlHeaderAndContentOverridden} = createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-overrides:headers');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlHeaderOverridden,
urlHeaderAndContentOverridden,
]);
});
it('can apply filter - has-overrides:content', async () => {
const {urlContentOverridden, urlHeaderAndContentOverridden} = createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-overrides:content');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlContentOverridden,
urlHeaderAndContentOverridden,
]);
});
it('can apply filter - has-overrides:tent', async () => {
const {urlHeaderAndContentOverridden, urlContentOverridden} = createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-overrides:tent'); // partial text
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
assert.deepEqual(rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url()), [
urlContentOverridden,
urlHeaderAndContentOverridden,
]);
});
function createRequestsWithAndWithoutTestHeader() {
const urlWithTestHeader = urlString`https://example.com/request-with-test-header`;
const urlWithoutTestHeader = urlString`https://example.com/request-without-test-header`;
const requestWithHeader = createNetworkRequest(urlWithTestHeader, {target});
const requestWithoutHeader = createNetworkRequest(urlWithoutTestHeader, {target});
requestWithHeader.requestHeaders = () => [{name: 'Accept-Language', value: 'US'}];
requestWithoutHeader.requestHeaders = () => [{name: 'Cache-Control', value: 'public'}];
return {
urlWithTestHeader,
urlWithoutTestHeader,
};
}
it('filters requests with has-request-header', async () => {
const {urlWithTestHeader} = createRequestsWithAndWithoutTestHeader();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-request-header:Accept-Language');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
const visibleUrls = rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url());
assert.deepEqual(visibleUrls, [urlWithTestHeader]);
});
it('does not match any request if header name is not present', async () => {
createRequestsWithAndWithoutTestHeader();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
networkLogView.setTextFilterValue('has-request-header:Nonexistent-Header');
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
const visibleUrls = rootNode.children.map(n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url());
assert.deepEqual(visibleUrls, []);
});
it('filters localized resource categories', async () => {
// "simulate" other locale by stubbing out resource categories with a different text
sinon.stub(Common.ResourceType.resourceCategories.Document, 'title')
.returns(i18n.i18n.lockedString('<localized document>'));
sinon.stub(Common.ResourceType.resourceCategories.XHR, 'title').returns(i18n.i18n.lockedString('<localized xhr>'));
const documentRequest = createNetworkRequest('urlDocument', {finished: true});
documentRequest.setResourceType(Common.ResourceType.resourceTypes.Document);
const fetchRequest = createNetworkRequest('urlFetch', {finished: true});
fetchRequest.setResourceType(Common.ResourceType.resourceTypes.Fetch);
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
renderElementIntoDOM(networkLogView);
const rootNode = networkLogView.columns().dataGrid().rootNode();
const shownRequestUrls = () => rootNode.children.map(
n => (n as Network.NetworkDataGridNode.NetworkNode).request()?.url() as string | undefined);
const setting = Common.Settings.Settings.instance().createSetting('network-resource-type-filters', {});
setting.set({all: true});
assert.deepEqual(shownRequestUrls(), ['urlDocument', 'urlFetch']);
setting.set({[Common.ResourceType.resourceCategories.Document.name]: true});
assert.deepEqual(shownRequestUrls(), ['urlDocument']);
setting.set({[Common.ResourceType.resourceCategories.XHR.name]: true});
assert.deepEqual(shownRequestUrls(), ['urlFetch']);
});
it('"Copy all" commands respects filters', async () => {
createOverrideRequests();
const filterBar = new UI.FilterBar.FilterBar('network-panel', true);
networkLogView = createNetworkLogView(filterBar);
renderElementIntoDOM(networkLogView);
const copyText = sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'copyText').resolves();
// Set network filter
networkLogView.setTextFilterValue('has-overrides:headers');
// Get DataGrid
const dataGrid = networkLogView.columns().dataGrid().element;
assert.isDefined(dataGrid);
// Select first element
networkLogView.columns().dataGrid().rootNode().children[0].select();
// Get context menu, clipboard section
const contextMenu = getContextMenuForElement(dataGrid);
const clipboardSection = contextMenu.clipboardSection();
// Assert that there is only one entry (for 'Copy') in the clipboard section
assert.deepEqual(['Copy'], getMenuItemLabels(clipboardSection));
const copyItem = clipboardSection.items[0];
// Use the 'Copy' sub-menu, get menu items from the footer section
const footerSection = (copyItem as UI.ContextMenu.SubMenu).footerSection();
const copyAllURLs = findMenuItemWithLabel(footerSection, 'Copy all listed URLs');
assert.isDefined(copyAllURLs);
contextMenu.invokeHandler(copyAllURLs.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`url-header-overridden
url-header-und-content-overridden`]);
copyText.resetHistory();
const copyAllCurlComnmands = findMenuItemWithLabel(
footerSection, Host.Platform.isWin() ? 'Copy all listed as cURL (bash)' : 'Copy all listed as cURL');
assert.isDefined(copyAllCurlComnmands);
contextMenu.invokeHandler(copyAllCurlComnmands.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`curl 'url-header-overridden' ;
curl 'url-header-und-content-overridden'`]);
copyText.resetHistory();
const copyAllFetchCall = findMenuItemWithLabel(footerSection, 'Copy all listed as fetch');
assert.isDefined(copyAllFetchCall);
contextMenu.invokeHandler(copyAllFetchCall.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`fetch("url-header-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
}); ;
fetch("url-header-und-content-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});`]);
copyText.resetHistory();
const copyAllPowerShell = findMenuItemWithLabel(footerSection, 'Copy all listed as PowerShell');
assert.isDefined(copyAllPowerShell);
contextMenu.invokeHandler(copyAllPowerShell.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`Invoke-WebRequest -UseBasicParsing -Uri "url-header-overridden";\r
Invoke-WebRequest -UseBasicParsing -Uri "url-header-und-content-overridden"`]);
// Clear network filter
networkLogView.setTextFilterValue('');
copyText.resetHistory();
contextMenu.invokeHandler(copyAllURLs.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`url-not-overridden
url-header-overridden
url-content-overridden
url-header-und-content-overridden`]);
copyText.resetHistory();
contextMenu.invokeHandler(copyAllCurlComnmands.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`curl 'url-not-overridden' ;
curl 'url-header-overridden' ;
curl 'url-content-overridden' ;
curl 'url-header-und-content-overridden'`]);
copyText.resetHistory();
contextMenu.invokeHandler(copyAllFetchCall.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`fetch("url-not-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
}); ;
fetch("url-header-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
}); ;
fetch("url-content-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
}); ;
fetch("url-header-und-content-overridden", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});`]);
copyText.resetHistory();
contextMenu.invokeHandler(copyAllPowerShell.id());
await expectCalled(copyText);
sinon.assert.callCount(copyText, 1);
assert.deepEqual(copyText.lastCall.args, [`Invoke-WebRequest -UseBasicParsing -Uri "url-not-overridden";\r
Invoke-WebRequest -UseBasicParsing -Uri "url-header-overridden";\r
Invoke-WebRequest -UseBasicParsing -Uri "url-content-overridden";\r
Invoke-WebRequest -UseBasicParsing -Uri "url-header-und-content-overridden"`]);
copyText.resetHistory();
});
it('skips unknown columns without title in persistence setting', async () => {
const columnSettings = Common.Settings.Settings.instance().createSetting('network-log-columns', {});
columnSettings.set({
'--this-does-not-exist-for-sure': {visible: false},
});
networkLogView = createNetworkLogView();
const columns = networkLogView.columns().dataGrid().columns;
assert.notExists(columns['--this-does-not-exist-for-sure']);
});
it('treats unknown columns with title and prefix in persistence setting as custom header', async () => {
const columnSettings = Common.Settings.Settings.instance().createSetting('network-log-columns', {});
// Custom request and response headers are prefixed with 'request-header-' and 'response-header-'
// respectively, so this column should be treated as a custom header.
const requestHeaderId = 'request-header-custom-request-header';
const responseHeaderId = 'response-header-custom-response-header';
const customRequestTitle = 'Custom-Request-Header';
const customResponseTitle = 'Custom-Response-Header';
columnSettings.set({
[requestHeaderId]: {visible: false, title: customRequestTitle},
[responseHeaderId]: {visible: false, title: customResponseTitle},
});
networkLogView = createNetworkLogView();
const dataGrid = networkLogView.columns().dataGrid();
const columns = dataGrid.columns;
assert.exists(columns[requestHeaderId], 'Custom request header column should exist');
assert.exists(columns[responseHeaderId], 'Custom response header column should exist');
const contextMenuShow = sinon.stub(UI.ContextMenu.ContextMenu.prototype, 'show').resolves();
const header = dataGrid.element.querySelector('thead');
assert.exists(header);
const event = new MouseEvent('contextmenu');
sinon.stub(event, 'target').value(header);
dataGrid.element.dispatchEvent(event);
sinon.assert.calledOnce(contextMenuShow);
const contextMenu = contextMenuShow.thisValues[0];
const requestHeadersSubMenu = contextMenu.footerSection().items.find(
(item: UI.ContextMenu.Item) => item.buildDescriptor().label === 'Request Headers');
assert.exists(requestHeadersSubMenu, '"Request Headers" submenu should exist');
assert.instanceOf(requestHeadersSubMenu, UI.ContextMenu.SubMenu);
const customRequestHeaderItem = requestHeadersSubMenu.defaultSection().items.find(
(item: UI.ContextMenu.Item) => item.buildDescriptor().label === customRequestTitle);
assert.exists(customRequestHeaderItem, 'Custom request header item should be in the "Request Headers" submenu');
const responseHeadersSubMenu = contextMenu.footerSection().items.find(
(item: UI.ContextMenu.Item) => item.buildDescriptor().label === 'Response Headers');
assert.exists(responseHeadersSubMenu, '"Response Headers" submenu should exist');
assert.instanceOf(responseHeadersSubMenu, UI.ContextMenu.SubMenu);
const customResponseHeaderItem = responseHeadersSubMenu.defaultSection().items.find(
(item: UI.ContextMenu.Item) => item.buildDescriptor().label === customResponseTitle);
assert.exists(customResponseHeaderItem, 'Custom response header item should be in the "Response Headers" submenu');
});
});
describeWithMockConnection('NetworkLogView placeholder', () => {
const START_RECORDING_ID = 'network.toggle-recording';
const RELOAD_ID = 'inspector-main.reload';
beforeEach(() => {
stubNoopSettings();
UI.ActionRegistration.registerActionExtension({
actionId: START_RECORDING_ID,
category: UI.ActionRegistration.ActionCategory.NETWORK,
title: () => 'mock' as Platform.UIString.LocalizedString,
toggleable: true,
});
UI.ActionRegistration.registerActionExtension({
actionId: RELOAD_ID,
category: UI.ActionRegistration.ActionCategory.NETWORK,
title: () => 'mock' as Platform.UIString.LocalizedString,
toggleable: true,
});
sinon.stub(UI.ShortcutRegistry.ShortcutRegistry, 'instance').returns({
shortcutTitleForAction: () => 'Ctrl',
shortcutsForAction: () => [new UI.KeyboardShortcut.KeyboardShortcut(
[{key: UI.KeyboardShortcut.Keys.Ctrl.code, name: 'Ctrl'}], '', UI.KeyboardShortcut.Type.DEFAULT_SHORTCUT)],
} as unknown as UI.ShortcutRegistry.ShortcutRegistry);
});
it('shows instruction to start recording', async () => {
const networkLogView = createNetworkLogView();
testPlaceholderText(
networkLogView, 'No network activity recorded',
'Record network log to display network activity by using the \"Start recording\" button or by pressing Ctrl.');
testPlaceholderButton(networkLogView, 'Start recording', START_RECORDING_ID);
});
it('shows placeholder with instruction to reload page if already recording', async () => {
const networkLogView = createNetworkLogView();
networkLogView.setRecording(true);
testPlaceholderText(
networkLogView, 'Currently recording network activity',
'Perform a request or reload the page by using the \"Reload page\" button or by pressing Ctrl.');
testPlaceholderButton(networkLogView, 'Reload page', RELOAD_ID);
});
});
describeWithEnvironment('NetworkLogView', () => {
it('renders when actions aren\'t registered', async () => {
stubNoopSettings();
sinon.stub(UI.ShortcutRegistry.ShortcutRegistry, 'instance').returns({
shortcutTitleForAction: () => 'Ctrl',
shortcutsForAction: () => [new UI.KeyboardShortcut.KeyboardShortcut(
[{key: UI.KeyboardShortcut.Keys.Ctrl.code, name: 'Ctrl'}], '', UI.KeyboardShortcut.Type.DEFAULT_SHORTCUT)],
} as unknown as UI.ShortcutRegistry.ShortcutRegistry);
try {
createNetworkLogView();
} catch {
assert.fail('Creating the network view without registring the actions shouldn\'t fail.');
}
});
});
function testPlaceholderText(
networkLogView: Network.NetworkLogView.NetworkLogView, expectedHeaderText: string,
expectedDescriptionText: string) {
const emptyWidget = networkLogView.element.querySelector('.empty-state');
const header = emptyWidget?.querySelector('.empty-state-header')?.textContent;
const description = emptyWidget?.querySelector('.empty-state-description > span')?.textContent;
assert.deepEqual(header, expectedHeaderText);
assert.deepEqual(description, expectedDescriptionText);
}
function testPlaceholderButton(
networkLogView: Network.NetworkLogView.NetworkLogView, expectedButtonText: string, actionId: string) {
const button = networkLogView.element.querySelector('.empty-state devtools-button');
assert.exists(button);
assert.deepEqual(button.textContent, expectedButtonText);
const action = UI.ActionRegistry.ActionRegistry.instance().getAction(actionId);
const spy = sinon.spy(action, 'execute');
sinon.assert.notCalled(spy);
dispatchClickEvent(button);
sinon.assert.calledOnce(spy);
}
async function getMoreTypesDropdown(filterBar: UI.FilterBar.FilterBar): Promise<HTMLElement> {
return filterBar.element.querySelector('[aria-label="Show only/hide requests dropdown"]')
?.querySelector('.toolbar-button') as HTMLElement;
}
function getCountAdorner(filterBar: UI.FilterBar.FilterBar): HTMLElement|null {
const button = filterBar.element.querySelector('[aria-label="Show only/hide requests dropdown"]')
?.querySelector('.toolbar-button');
return button?.querySelector('.active-filters-count') ?? null;
}
function getMoreFiltersActiveCount(filterBar: UI.FilterBar.FilterBar): string {
const countAdorner = getCountAdorner(filterBar);
const count = countAdorner?.firstChild?.textContent ?? '';
return count;
}
function getDropdownItem(softMenu: UI.ContextMenu.ContextMenu, label: string) {
const item = findMenuItemWithLabel(softMenu.defaultSection(), label);
assertNotNullOrUndefined(item);
return item;
}
async function selectMoreFiltersOption(softMenu: UI.ContextMenu.ContextMenu, option: string) {
const item = getDropdownItem(softMenu, option);
softMenu.invokeHandler(item.id());
await raf();
}
function createNetworkLogView(filterBar?: UI.FilterBar.FilterBar): Network.NetworkLogView.NetworkLogView {
if (!filterBar) {
filterBar = {addFilter: () => {}, filterButton: () => ({addEventListener: () => {}}), addDivider: () => {}} as
unknown as UI.FilterBar.FilterBar;
}
return new Network.NetworkLogView.NetworkLogView(
filterBar, document.createElement('div'),
Common.Settings.Settings.instance().createSetting('network-log-large-rows', false));
}