| // Copyright 2024 The Chromium Authors |
| // 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 Platform from '../../core/platform/platform.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as NetworkTimeCalculator from '../../models/network_time_calculator/network_time_calculator.js'; |
| import {assertScreenshot, getCleanTextContentFromElements, renderElementIntoDOM} from '../../testing/DOMHelpers.js'; |
| import {stubNoopSettings} from '../../testing/EnvironmentHelpers.js'; |
| import {setupLocaleHooks} from '../../testing/LocaleHelpers.js'; |
| import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js'; |
| |
| import * as Network from './network.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| |
| function createNetworkRequest( |
| matchedSource: Protocol.Network.ServiceWorkerRouterSource, |
| actualSource: Protocol.Network.ServiceWorkerRouterSource): SDK.NetworkRequest.NetworkRequest { |
| const request = SDK.NetworkRequest.NetworkRequest.create( |
| 'requestId' as Protocol.Network.RequestId, urlString`http://devtools-frontend.test`, urlString``, null, null, |
| null); |
| |
| request.mimeType = 'application/wasm'; |
| request.finished = true; |
| const timingInfo: Protocol.Network.ResourceTiming = { |
| requestTime: 500, |
| proxyStart: 0, |
| proxyEnd: 0, |
| dnsStart: 0, |
| dnsEnd: 0, |
| connectStart: 0, |
| connectEnd: 0, |
| sslStart: 0, |
| sslEnd: 0, |
| workerReady: 400, |
| workerStart: 500, |
| workerRouterEvaluationStart: -200, |
| workerFetchStart: 600, |
| workerRespondWithSettled: 700, |
| sendStart: 800, |
| sendEnd: 900, |
| pushStart: 0, |
| pushEnd: 0, |
| receiveHeadersStart: 1000, |
| receiveHeadersEnd: 0, |
| }; |
| if (matchedSource === Protocol.Network.ServiceWorkerRouterSource.Cache) { |
| timingInfo.workerCacheLookupStart = -100; |
| } |
| |
| request.timing = timingInfo; |
| request.serviceWorkerRouterInfo = { |
| ruleIdMatched: 1, |
| matchedSourceType: matchedSource, |
| actualSourceType: actualSource, |
| }; |
| |
| return request; |
| } |
| |
| describe('ResourceTimingView', () => { |
| setupLocaleHooks(); |
| it('RequestTimeRanges has router evaluation field with SW router source as network', async () => { |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Network, Protocol.Network.ServiceWorkerRouterSource.Network); |
| const timingInfo = request.timing as Protocol.Network.ResourceTiming; |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| const routerEvaluationTime = timingInfo.workerRouterEvaluationStart as number; |
| const sendStart = timingInfo.sendStart; |
| |
| const routerEvaluation = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION); |
| assert.isTrue(Boolean(routerEvaluation), 'worker router evaluation exists'); |
| assert.strictEqual(routerEvaluation?.start, timingInfo.requestTime + routerEvaluationTime / 1000); |
| assert.strictEqual(routerEvaluation?.end, timingInfo.requestTime + sendStart / 1000); |
| |
| const cacheLookup = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP); |
| assert.isFalse(Boolean(cacheLookup), 'worker cache lookup does not exist'); |
| }); |
| |
| it('RequestTimeRanges has router evaluation field with SW router source as fetch-event', async () => { |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.FetchEvent, Protocol.Network.ServiceWorkerRouterSource.FetchEvent); |
| const timingInfo = request.timing as Protocol.Network.ResourceTiming; |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| const routerEvaluationTime = timingInfo.workerRouterEvaluationStart as number; |
| const workerStart = timingInfo.workerStart; |
| |
| const routerEvaluation = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION); |
| assert.isTrue(Boolean(routerEvaluation), 'worker router evaluation exists'); |
| assert.strictEqual(routerEvaluation?.start, timingInfo.requestTime + routerEvaluationTime / 1000); |
| assert.strictEqual(routerEvaluation?.end, timingInfo.requestTime + workerStart / 1000); |
| |
| const cacheLookup = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP); |
| assert.isFalse(Boolean(cacheLookup), 'worker cache lookup does not exist'); |
| }); |
| |
| it('RequestTimeRanges has router evaluation field with SW router source as cache hit', async () => { |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Cache, Protocol.Network.ServiceWorkerRouterSource.Cache); |
| const timingInfo = request.timing as Protocol.Network.ResourceTiming; |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| const routerEvaluationTime = timingInfo.workerRouterEvaluationStart as number; |
| const cacheLookupStart = timingInfo.workerCacheLookupStart as number; |
| |
| const routerEvaluation = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION); |
| assert.isTrue(Boolean(routerEvaluation), 'worker router evaluation exists'); |
| assert.strictEqual(routerEvaluation?.start, timingInfo.requestTime + routerEvaluationTime / 1000); |
| assert.strictEqual(routerEvaluation?.end, timingInfo.requestTime + cacheLookupStart / 1000); |
| |
| const cacheLookup = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP); |
| assert.isTrue(Boolean(cacheLookup), 'worker cache lookup does not exist'); |
| assert.strictEqual(cacheLookup?.start, timingInfo.requestTime + cacheLookupStart / 1000); |
| assert.strictEqual(cacheLookup?.end, timingInfo.requestTime + timingInfo.receiveHeadersStart / 1000); |
| }); |
| |
| it('RequestTimeRanges has router evaluation field with SW router source as cache miss', async () => { |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Cache, Protocol.Network.ServiceWorkerRouterSource.Network); |
| const timingInfo = request.timing as Protocol.Network.ResourceTiming; |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| const routerEvaluationTime = timingInfo.workerRouterEvaluationStart as number; |
| const cacheLookupStart = timingInfo.workerCacheLookupStart as number; |
| |
| const routerEvaluation = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_ROUTER_EVALUATION); |
| assert.isTrue(Boolean(routerEvaluation), 'worker router evaluation exists'); |
| assert.strictEqual(routerEvaluation?.start, timingInfo.requestTime + routerEvaluationTime / 1000); |
| assert.strictEqual(routerEvaluation?.end, timingInfo.requestTime + cacheLookupStart / 1000); |
| |
| const cacheLookup = timeRanges.find( |
| timeRange => timeRange.name === NetworkTimeCalculator.RequestTimeRangeNames.SERVICE_WORKER_CACHE_LOOKUP); |
| assert.isTrue(Boolean(cacheLookup), 'worker cache lookup does not exist'); |
| assert.strictEqual(cacheLookup?.start, timingInfo.requestTime + cacheLookupStart / 1000); |
| assert.strictEqual(cacheLookup?.end, timingInfo.requestTime + timingInfo.sendStart / 1000); |
| }); |
| |
| it('Timing table has router evaluation field with detail tabs', async () => { |
| stubNoopSettings(); |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Network, Protocol.Network.ServiceWorkerRouterSource.Network); |
| |
| const component = Network.RequestTimingView.RequestTimingView.create( |
| request, new NetworkTimeCalculator.NetworkTimeCalculator(true)); |
| const div = document.createElement('div'); |
| renderElementIntoDOM(div); |
| component.markAsRoot(); |
| component.show(div); |
| |
| await component.updateComplete; |
| |
| // Test if we correctly set details element |
| const routerEvaluationDetailsElement = document.querySelector('.router-evaluation-timing-bar-details'); |
| assert.isNotNull(routerEvaluationDetailsElement, 'router evaluation details does not exist'); |
| assert.strictEqual( |
| routerEvaluationDetailsElement.childElementCount, 1, |
| 'router evaluation details child element count does not match'); |
| assert.isNotNull(routerEvaluationDetailsElement.firstElementChild, 'router evaluation first element is non null'); |
| |
| // Test if we correctly set the tree item inside shadow root |
| const shadowElement = routerEvaluationDetailsElement.firstElementChild.shadowRoot; |
| assert.isNotNull(shadowElement, 'shadow element does not exist'); |
| const content = getCleanTextContentFromElements(shadowElement, '.network-fetch-details-treeitem'); |
| assert.lengthOf(content, 2, 'does not match the tree item'); |
| |
| // Check the content of the view. Since the value is set from matched to actual, |
| // the order should be the same. |
| const networkString = String(Protocol.Network.ServiceWorkerRouterSource.Network); |
| assert.strictEqual(content[0], `Matched source: ${networkString}`, 'matched source does not match'); |
| assert.strictEqual(content[1], `Actual source: ${networkString}`, 'actual source does not match'); |
| }); |
| |
| it('Timing table shows throttling indicator', async () => { |
| stubNoopSettings(); |
| const container = document.createElement('div'); |
| renderElementIntoDOM(container, {includeCommonStyles: true}); |
| |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Cache, Protocol.Network.ServiceWorkerRouterSource.Cache); |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| |
| const wasThrottled = new SDK.NetworkManager.AppliedNetworkConditions(SDK.NetworkManager.Slow3GConditions, ''); |
| const input: Parameters<typeof Network.RequestTimingView.DEFAULT_VIEW>[0] = { |
| requestUnfinished: false, |
| requestStartTime: 0, |
| requestIssueTime: 0, |
| totalDuration: 100, |
| startTime: 0, |
| endTime: 100, |
| timeRanges, |
| calculator: new NetworkTimeCalculator.NetworkTimeCalculator(true), |
| serverTimings: [], |
| wasThrottled |
| }; |
| |
| Network.RequestTimingView.DEFAULT_VIEW(input, {}, container); |
| await assertScreenshot('network/request-timing-view-throttling.png'); |
| |
| const icon = container.querySelector<HTMLElement>('devtools-icon[name=watch]'); |
| assert.exists(icon); |
| const revealStub = sinon.stub(Common.Revealer.RevealerRegistry.instance(), 'reveal'); |
| icon.click(); |
| sinon.assert.calledOnceWithExactly(revealStub, wasThrottled, false); |
| }); |
| |
| it('correctly passes requestUnfinished to the view', async () => { |
| stubNoopSettings(); |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Network, Protocol.Network.ServiceWorkerRouterSource.Network); |
| request.finished = false; |
| const calculator = new NetworkTimeCalculator.NetworkTimeCalculator(true); |
| |
| const viewStub = createViewFunctionStub(Network.RequestTimingView.RequestTimingView); |
| const component = new Network.RequestTimingView.RequestTimingView(undefined, viewStub); |
| renderElementIntoDOM(component); |
| |
| component.request = request; |
| component.calculator = calculator; |
| |
| const input = await viewStub.nextInput; |
| assert.isTrue(input.requestUnfinished, 'requestUnfinished should be true when request is not finished'); |
| |
| const requestFinished = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Network, Protocol.Network.ServiceWorkerRouterSource.Network); |
| requestFinished.finished = true; |
| |
| component.request = requestFinished; |
| |
| const inputFinished = await viewStub.nextInput; |
| assert.isFalse(inputFinished.requestUnfinished, 'requestUnfinished should be false when request is finished'); |
| }); |
| |
| it('shows caution message in DEFAULT_VIEW if and only if requestUnfinished is true', async () => { |
| stubNoopSettings(); |
| const container = document.createElement('div'); |
| renderElementIntoDOM(container); |
| |
| const request = createNetworkRequest( |
| Protocol.Network.ServiceWorkerRouterSource.Network, Protocol.Network.ServiceWorkerRouterSource.Network); |
| const timeRanges = NetworkTimeCalculator.calculateRequestTimeRanges(request, 100); |
| const calculator = new NetworkTimeCalculator.NetworkTimeCalculator(true); |
| |
| const baseInput: Parameters<typeof Network.RequestTimingView.DEFAULT_VIEW>[0] = { |
| requestUnfinished: false, |
| requestStartTime: 0, |
| requestIssueTime: 0, |
| totalDuration: 100, |
| startTime: 0, |
| endTime: 100, |
| timeRanges, |
| calculator, |
| serverTimings: [], |
| }; |
| |
| // Case 1: requestUnfinished = true |
| Network.RequestTimingView.DEFAULT_VIEW({...baseInput, requestUnfinished: true}, {}, container); |
| const cautionElementTrue = container.querySelector('.caution'); |
| assert.isNotNull(cautionElementTrue, 'caution element should exist when requestUnfinished is true'); |
| assert.include(cautionElementTrue?.textContent, 'CAUTION: request is not finished yet!'); |
| |
| // Case 2: requestUnfinished = false |
| Network.RequestTimingView.DEFAULT_VIEW({...baseInput, requestUnfinished: false}, {}, container); |
| const cautionElementFalse = container.querySelector('.caution'); |
| assert.isNull(cautionElementFalse, 'caution element should not exist when requestUnfinished is false'); |
| }); |
| }); |