| // Copyright 2021 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 {assert} from 'chai'; |
| |
| import {unregisterAllServiceWorkers} from '../../conductor/hooks.js'; |
| import {type BrowserAndPages} from '../../conductor/puppeteer-state.js'; |
| import { |
| click, |
| getBrowserAndPages, |
| pressKey, |
| step, |
| waitFor, |
| waitForElementWithTextContent, |
| waitForFunction, |
| } from '../../shared/helper.js'; |
| import {describe, it} from '../../shared/mocha-extensions.js'; |
| import { |
| getAllRequestNames, |
| navigateToNetworkTab, |
| selectRequestByName, |
| setCacheDisabled, |
| setPersistLog, |
| waitForSelectedRequestChange, |
| waitForSomeRequestsToAppear, |
| } from '../helpers/network-helpers.js'; |
| |
| async function getRequestRowInfo(frontend: BrowserAndPages['frontend'], name: string) { |
| const statusColumn = await frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.status-column')).map(node => node.textContent); |
| }); |
| const timeColumn = await frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.time-column')).map(node => node.textContent); |
| }); |
| const typeColumn = await frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.type-column')).map(node => node.textContent); |
| }); |
| const nameColumn = await frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.name-column')).map(node => node.textContent); |
| }); |
| const index = nameColumn.findIndex(x => x === name); |
| return {status: statusColumn[index], time: timeColumn[index], type: typeColumn[index]}; |
| } |
| |
| describe('The Network Tab', async function() { |
| if (this.timeout() !== 0.0) { |
| // These tests take some time on slow windows machines. |
| this.timeout(10000); |
| } |
| const formatByteSize = (value: number) => { |
| return `${value}\xA0B`; |
| }; |
| |
| beforeEach(async () => { |
| await navigateToNetworkTab('empty.html'); |
| await setCacheDisabled(true); |
| await setPersistLog(false); |
| }); |
| |
| afterEach(async () => { |
| await unregisterAllServiceWorkers(); |
| }); |
| |
| it('can click on checkbox label to toggle checkbox', async () => { |
| await navigateToNetworkTab('resources-from-cache.html'); |
| |
| // Click the label next to the checkbox input element |
| await click('[title^="Disable cache"] + label'); |
| |
| const checkbox = await waitFor('[title^="Disable cache"]'); |
| const checked = await checkbox.evaluate(box => (box as HTMLInputElement).checked); |
| |
| assert.strictEqual(checked, false, 'The disable cache checkbox should be unchecked'); |
| }); |
| |
| it('shows Last-Modified', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| await navigateToNetworkTab('last-modified.html'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await step('Wait for the column to show up and populate its values', async () => { |
| await waitForSomeRequestsToAppear(2); |
| }); |
| |
| await step('Open the contextmenu for all network column', async () => { |
| await click('.name-column', {clickOptions: {button: 'right'}}); |
| }); |
| |
| await step('Click "Response Headers" submenu', async () => { |
| await click('aria/Response Headers'); |
| }); |
| |
| await step('Enable the Last-Modified column in the network datagrid', async () => { |
| await click('aria/Last-Modified, unchecked'); |
| }); |
| |
| await step('Wait for the "Last-Modified" column to have the expected values', async () => { |
| const expectedValues = JSON.stringify(['Last-Modified', '', 'Sun, 26 Sep 2010 22:04:35 GMT']); |
| await waitForFunction(async () => { |
| const lastModifiedColumnValues = await frontend.$$eval( |
| 'pierce/.last-modified-column', |
| cells => cells.map(element => element.textContent), |
| ); |
| return JSON.stringify(lastModifiedColumnValues) === expectedValues; |
| }); |
| }); |
| }); |
| |
| it('shows size of chunked responses', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| await navigateToNetworkTab('chunked.txt?numChunks=5'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| await waitForSomeRequestsToAppear(1); |
| |
| // Get the size of the first two network request responses (excluding header and favicon.ico). |
| const getNetworkRequestSize = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.size-column')).slice(1, 3).map(node => node.textContent); |
| }); |
| |
| assert.deepEqual(await getNetworkRequestSize(), [ |
| `${formatByteSize(210)}${formatByteSize(25)}`, |
| ]); |
| }); |
| |
| it('shows size of chunked responses for sync XHR', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| await navigateToNetworkTab('chunked_sync.html'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| await waitForSomeRequestsToAppear(2); |
| |
| // Get the size of the first two network request responses (excluding header and favicon.ico). |
| const getNetworkRequestSize = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.size-column')).slice(1, 3).map(node => node.textContent); |
| }); |
| |
| assert.deepEqual(await getNetworkRequestSize(), [ |
| `${formatByteSize(313)}${formatByteSize(128)}`, |
| `${formatByteSize(210)}${formatByteSize(25)}`, |
| ]); |
| }); |
| |
| it('the HTML response including cyrillic characters with utf-8 encoding', async () => { |
| const {target} = getBrowserAndPages(); |
| await navigateToNetworkTab('utf-8.rawresponse'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| // Wait for the column to show up and populate its values |
| await waitForSomeRequestsToAppear(1); |
| |
| // Open the HTML file that was loaded |
| await click('td.name-column'); |
| // Open the raw response HTML |
| await click('[aria-label="Response"]'); |
| // Disable pretty printing |
| await waitFor('[aria-label="Pretty print"]'); |
| await Promise.all([ |
| click('[aria-label="Pretty print"]'), |
| waitFor('[aria-label="Pretty print"][aria-pressed="true"]'), |
| ]); |
| // Wait for the raw response editor to show up |
| const codeMirrorEditor = await waitFor('[aria-label="Code editor"]'); |
| |
| const htmlRawResponse = await codeMirrorEditor.evaluate(editor => editor.textContent); |
| |
| assert.strictEqual( |
| htmlRawResponse, |
| '<html> <body>The following word is written using cyrillic letters and should look like "SUCCESS": SU\u0421\u0421\u0415SS.</body></html>'); |
| }); |
| |
| it('the correct MIME type when resources came from HTTP cache', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('resources-from-cache.html'); |
| |
| // Reload the page without a cache, to force a fresh load of the network resources |
| await setCacheDisabled(true); |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| // Wait for the column to show up and populate its values |
| await waitForSomeRequestsToAppear(2); |
| |
| // Get the size of the first two network request responses (excluding header and favicon.ico). |
| const getNetworkRequestSize = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.size-column')).slice(1, 3).map(node => node.textContent); |
| }); |
| const getNetworkRequestMimeTypes = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.type-column')).slice(1, 3).map(node => node.textContent); |
| }); |
| |
| assert.deepEqual(await getNetworkRequestSize(), [ |
| `${formatByteSize(404)}${formatByteSize(219)}`, |
| `${formatByteSize(376)}${formatByteSize(28)}`, |
| ]); |
| assert.deepEqual(await getNetworkRequestMimeTypes(), [ |
| 'document', |
| 'script', |
| ]); |
| |
| // Allow resources from the cache again and reload the page to load from cache |
| await setCacheDisabled(false); |
| await target.reload({waitUntil: 'networkidle0'}); |
| // Wait for the column to show up and populate its values |
| await waitForSomeRequestsToAppear(2); |
| |
| assert.deepEqual(await getNetworkRequestSize(), [ |
| `${formatByteSize(404)}${formatByteSize(219)}`, |
| `(memory cache)${formatByteSize(28)}`, |
| ]); |
| |
| assert.deepEqual(await getNetworkRequestMimeTypes(), [ |
| 'document', |
| 'script', |
| ]); |
| }); |
| |
| it('shows the correct initiator address space', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('fetch.html'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(2); |
| |
| await step('Open the contextmenu for all network column', async () => { |
| await click('.name-column', {clickOptions: {button: 'right'}}); |
| }); |
| |
| await step('Enable the Initiator Address Space column in the network datagrid', async () => { |
| await click('aria/Initiator Address Space, unchecked'); |
| }); |
| |
| await step('Wait for the Initiator Address Space column to have the expected values', async () => { |
| const expectedValues = JSON.stringify(['Initiator Address Space', '', 'Local']); |
| await waitForFunction(async () => { |
| const initiatorAddressSpaceValues = await frontend.$$eval( |
| 'pierce/.initiator-address-space-column', |
| cells => cells.map(element => element.textContent), |
| ); |
| return JSON.stringify(initiatorAddressSpaceValues) === expectedValues; |
| }); |
| }); |
| }); |
| |
| it('shows the correct remote address space', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('fetch.html'); |
| |
| // Reload to populate network request table |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(2); |
| |
| await step('Open the contextmenu for all network column', async () => { |
| await click('.name-column', {clickOptions: {button: 'right'}}); |
| }); |
| |
| await step('Enable the Remote Address Space column in the network datagrid', async () => { |
| await click('aria/Remote Address Space, unchecked'); |
| }); |
| |
| await step('Wait for the Remote Address Space column to have the expected values', async () => { |
| const expectedValues = JSON.stringify(['Remote Address Space', 'Local', 'Local']); |
| await waitForFunction(async () => { |
| const remoteAddressSpaceValues = await frontend.$$eval( |
| 'pierce/.remoteaddress-space-column', |
| cells => cells.map(element => element.textContent), |
| ); |
| return JSON.stringify(remoteAddressSpaceValues) === expectedValues; |
| }); |
| }); |
| }); |
| |
| it('indicates resources from the web bundle in the size column', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('resources-from-webbundle.html'); |
| |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(3); |
| await waitForElementWithTextContent(`(Web Bundle)${formatByteSize(27)}`); |
| |
| const getNetworkRequestSize = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.size-column')).slice(2, 4).map(node => node.textContent); |
| }); |
| |
| assert.sameMembers(await getNetworkRequestSize(), [ |
| `${formatByteSize(653)}${formatByteSize(0)}`, |
| `(Web Bundle)${formatByteSize(27)}`, |
| ]); |
| }); |
| |
| it('shows web bundle metadata error in the status column', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('resources-from-webbundle-with-bad-metadata.html'); |
| |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(3); |
| await waitForElementWithTextContent('Web Bundle error'); |
| await waitForFunction(async () => { |
| const [nameColumn, statusColumn] = await frontend.evaluate(() => { |
| return [ |
| Array.from(document.querySelectorAll('.name-column')).map(node => node.textContent), |
| Array.from(document.querySelectorAll('.status-column')).map(node => node.textContent), |
| ]; |
| }); |
| const webBundleStatus = statusColumn[nameColumn.indexOf('webbundle_bad_metadata.wbn/test/e2e/resources/network')]; |
| const webBundleInnerRequestStatus = |
| statusColumn[nameColumn.indexOf('uuid-in-package:020111b3-437a-4c5c-ae07-adb6bbffb720')]; |
| return webBundleStatus === 'Web Bundle error' && |
| (webBundleInnerRequestStatus === '(failed)net::ERR_INVALID_WEB_BUNDLE' || |
| // There's a race in the renderer where the subresource request will |
| // be canceled if it hasn't finished before parsing the metadata failed. |
| webBundleInnerRequestStatus === '(canceled)'); |
| }); |
| }); |
| |
| it('shows web bundle inner request error in the status column', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('resources-from-webbundle-with-bad-inner-request.html'); |
| |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(3); |
| await waitForElementWithTextContent('Web Bundle error'); |
| |
| const getNetworkRequestSize = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.status-column')).slice(2, 4).map(node => node.textContent); |
| }); |
| |
| assert.sameMembers(await getNetworkRequestSize(), ['200OK', 'Web Bundle error']); |
| }); |
| |
| it('shows web bundle icons', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('resources-from-webbundle.html'); |
| |
| await setCacheDisabled(true); |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await waitForSomeRequestsToAppear(3); |
| await waitFor('.name-column > [role="link"] > .icon'); |
| |
| const getNetworkRequestIcons = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.name-column > .icon')) |
| .slice(1, 4) |
| .map(node => (node as HTMLDivElement).title); |
| }); |
| assert.sameMembers(await getNetworkRequestIcons(), [ |
| 'Script', |
| 'WebBundle', |
| ]); |
| const getFromWebBundleIcons = () => frontend.evaluate(() => { |
| return Array.from(document.querySelectorAll('.name-column > [role="link"] > .icon')) |
| .map(node => (node as HTMLDivElement).title); |
| }); |
| assert.sameMembers(await getFromWebBundleIcons(), [ |
| 'Served from Web Bundle', |
| ]); |
| }); |
| |
| it('shows preserved pending requests as unknown', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('send_beacon_on_unload.html'); |
| |
| await setCacheDisabled(true); |
| await target.bringToFront(); |
| await target.reload({waitUntil: 'networkidle0'}); |
| |
| await frontend.bringToFront(); |
| await waitForSomeRequestsToAppear(1); |
| |
| await setPersistLog(true); |
| |
| await navigateToNetworkTab('fetch.html'); |
| await waitForSomeRequestsToAppear(1); |
| |
| // We need to wait for the network log to update. |
| await waitForFunction(async () => { |
| const {status} = await getRequestRowInfo(frontend, 'sendBeacon'); |
| // Depending on timing of the reporting, the status infomation (404) might reach DevTools in time. |
| return (status === '(unknown)' || status === '404Not Found'); |
| }); |
| }); |
| |
| it('repeats xhr request on "r" shortcut when the request is focused', async () => { |
| const {target} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('xhr.html'); |
| await target.reload({waitUntil: 'networkidle0'}); |
| await waitForSomeRequestsToAppear(2); |
| |
| await selectRequestByName('image.svg'); |
| await waitForSelectedRequestChange(null); |
| await pressKey('r'); |
| await waitForSomeRequestsToAppear(3); |
| |
| const updatedRequestNames = await getAllRequestNames(); |
| assert.deepStrictEqual(updatedRequestNames, ['xhr.html', 'image.svg', 'image.svg']); |
| }); |
| |
| it('displays focused background color when request is selected via keyboard navigation', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| |
| await navigateToNetworkTab('xhr.html'); |
| await target.reload({waitUntil: 'networkidle0'}); |
| await waitForSomeRequestsToAppear(2); |
| await selectRequestByName('xhr.html'); |
| await pressKey('ArrowDown'); |
| |
| const getSelectedRequestBgColor = () => frontend.evaluate(() => { |
| return document.querySelector('.network-log-grid tbody tr.selected')?.getAttribute('style'); |
| }); |
| |
| assert.deepStrictEqual(await getSelectedRequestBgColor(), 'background-color: var(--color-grid-focus-selected);'); |
| }); |
| |
| it('shows the request panel when clicked during a websocket message (https://crbug.com/1222382)', async () => { |
| await navigateToNetworkTab('websocket.html?infiniteMessages=true'); |
| |
| await waitForSomeRequestsToAppear(2); |
| |
| // WebSocket messages get sent every 100 milliseconds, so holding the mouse |
| // down for 300 milliseconds should suffice. |
| await selectRequestByName('localhost', {delay: 300}); |
| |
| await waitFor('.network-item-view'); |
| }); |
| |
| it('shows the main service worker request as complete', async () => { |
| const {target, frontend} = getBrowserAndPages(); |
| const promises = [ |
| waitForFunction(async () => { |
| const {status, type} = await getRequestRowInfo(frontend, 'service-worker.html/test/e2e/resources/network'); |
| return status === '200OK' && type === 'document'; |
| }), |
| waitForFunction(async () => { |
| const {status, type} = |
| await getRequestRowInfo(frontend, '⚙ service-worker.jslocalhost/test/e2e/resources/network'); |
| return status === 'Finished' && type === 'script'; |
| }), |
| ]; |
| await navigateToNetworkTab('service-worker.html'); |
| await target.waitForSelector('xpath///div[@id="content" and text()="pong"]'); |
| await Promise.all(promises); |
| }); |
| }); |