| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import sinon from 'sinon'; | |
| import type * as Common from '../core/common/common.js'; | |
| import * as Platform from '../core/platform/platform.js'; | |
| import * as SDK from '../core/sdk/sdk.js'; | |
| import type * as TextUtils from '../core/text_utils/text_utils.js'; | |
| import type * as Protocol from '../generated/protocol.js'; | |
| import * as Logs from '../models/logs/logs.js'; | |
| const {urlString} = Platform.DevToolsPath; | |
| /** | |
| * Configuration options for creating a mock {@link SDK.NetworkRequest.NetworkRequest}. | |
| */ | |
| export interface CreateNetworkRequestOptions { | |
| /** The URL for the request. Defaults to `'https://example.com'`. */ | |
| url?: string|Platform.DevToolsPath.UrlString; | |
| /** | |
| * The document/initiator page URL. Defaults to the request's `url`, making the | |
| * request same-origin by default. Specify a different URL or empty string to test | |
| * cross-origin behavior. | |
| */ | |
| documentURL?: string|Platform.DevToolsPath.UrlString; | |
| /** The request identifier. Defaults to `'requestId'`. */ | |
| requestId?: string|Protocol.Network.RequestId; | |
| /** | |
| * If true, creates a request via `createWithoutBackendRequest`, which leaves | |
| * `backendRequestId()` undefined and omits frame/loader associations. | |
| */ | |
| withoutBackend?: boolean; | |
| statusCode?: number; | |
| statusText?: string; | |
| requestMethod?: string; | |
| requestHeaders?: SDK.NetworkRequest.NameValue[]; | |
| responseHeaders?: SDK.NetworkRequest.NameValue[]; | |
| originalResponseHeaders?: SDK.NetworkRequest.NameValue[]; | |
| fromMemoryCache?: boolean; | |
| /** | |
| * Content data or a content data provider function. When provided, configures | |
| * `request.setContentDataProvider()`. Pass a function returning `{error: string}` | |
| * to simulate content retrieval failures. | |
| */ | |
| contentData?: TextUtils.ContentData.ContentData|(() => Promise<TextUtils.ContentData.ContentDataOrError>); | |
| /** | |
| * If true, creates a request via `createForImportedHar`, marking it as imported | |
| * from a HAR archive and leaving `backendRequestId()` undefined. | |
| */ | |
| isImportedHar?: boolean; | |
| mimeType?: string; | |
| resourceType?: Common.ResourceType.ResourceType; | |
| finished?: boolean; | |
| failed?: boolean; | |
| charset?: string; | |
| timing?: Protocol.Network.ResourceTiming; | |
| serviceWorkerRouterInfo?: Protocol.Network.ServiceWorkerRouterInfo; | |
| fetchedViaServiceWorker?: boolean; | |
| initiator?: Protocol.Network.Initiator|null; | |
| frameId?: string|Protocol.Page.FrameId|null; | |
| loaderId?: string|Protocol.Network.LoaderId|null; | |
| } | |
| /** | |
| * Creates and configures a synthetic {@link SDK.NetworkRequest.NetworkRequest} for unit testing. | |
| * Automatically handles string-to-UrlString conversion and provides default values | |
| * so callers only need to specify properties relevant to their test case. | |
| */ | |
| export function createNetworkRequest(options: CreateNetworkRequestOptions = {}): SDK.NetworkRequest.NetworkRequest { | |
| const reqId = (options.requestId ?? 'requestId') as Protocol.Network.RequestId; | |
| const rawUrl = options.url ?? 'https://example.com'; | |
| const reqUrl = typeof rawUrl === 'string' ? urlString`${rawUrl}` : rawUrl; | |
| // Default documentURL to reqUrl so that requests are same-origin by default, | |
| // avoiding unintended cross-origin redactions or security checks in tests. | |
| const rawDocUrl = options.documentURL ?? reqUrl; | |
| const docUrl = typeof rawDocUrl === 'string' ? urlString`${rawDocUrl}` : rawDocUrl; | |
| const frameId = | |
| (options.frameId !== undefined && options.frameId !== null) ? options.frameId as Protocol.Page.FrameId : null; | |
| const loaderId = (options.loaderId !== undefined && options.loaderId !== null) ? | |
| options.loaderId as Protocol.Network.LoaderId : | |
| null; | |
| const initiator = options.initiator ?? null; | |
| const request = options.isImportedHar ? | |
| SDK.NetworkRequest.NetworkRequest.createForImportedHar(reqId, reqUrl, docUrl, initiator) : | |
| options.withoutBackend ? | |
| SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest(reqId, reqUrl, docUrl, initiator) : | |
| SDK.NetworkRequest.NetworkRequest.create(reqId, reqUrl, docUrl, frameId, loaderId, initiator); | |
| if (options.statusCode !== undefined) { | |
| request.statusCode = options.statusCode; | |
| } | |
| if (options.statusText !== undefined) { | |
| request.statusText = options.statusText; | |
| } | |
| if (options.requestMethod !== undefined) { | |
| request.requestMethod = options.requestMethod; | |
| } | |
| if (options.requestHeaders) { | |
| request.setRequestHeaders(options.requestHeaders); | |
| } | |
| if (options.responseHeaders) { | |
| request.responseHeaders = options.responseHeaders; | |
| } | |
| if (options.originalResponseHeaders) { | |
| request.originalResponseHeaders = options.originalResponseHeaders; | |
| } | |
| if (options.fromMemoryCache) { | |
| request.setFromMemoryCache(); | |
| } | |
| if (options.mimeType !== undefined) { | |
| request.mimeType = options.mimeType; | |
| } | |
| if (options.resourceType !== undefined) { | |
| request.setResourceType(options.resourceType); | |
| } | |
| if (options.finished !== undefined) { | |
| request.finished = options.finished; | |
| } | |
| if (options.failed !== undefined) { | |
| request.failed = options.failed; | |
| } | |
| if (options.charset !== undefined) { | |
| request.setCharset(options.charset); | |
| } | |
| if (options.timing !== undefined) { | |
| request.timing = options.timing; | |
| } | |
| if (options.serviceWorkerRouterInfo !== undefined) { | |
| request.serviceWorkerRouterInfo = options.serviceWorkerRouterInfo; | |
| } | |
| if (options.fetchedViaServiceWorker !== undefined) { | |
| request.fetchedViaServiceWorker = options.fetchedViaServiceWorker; | |
| } | |
| if (options.contentData) { | |
| const dataOrFn = options.contentData; | |
| request.setContentDataProvider(typeof dataOrFn === 'function' ? dataOrFn : () => Promise.resolve(dataOrFn)); | |
| } | |
| return request; | |
| } | |
| /** | |
| * Configuration options for {@link stubInitiatorGraph}. | |
| */ | |
| export interface StubInitiatorGraphOptions { | |
| /** | |
| * Upstream requests that triggered the target request. Do not include the target | |
| * request itself; it is automatically added as the primary initiator. | |
| * Defaults to a single synthetic cross-origin initiator. | |
| */ | |
| initiators?: SDK.NetworkRequest.NetworkRequest[]; | |
| /** | |
| * Downstream requests triggered by the target request. | |
| * Defaults to two synthetic cross-origin child requests. | |
| */ | |
| initiated?: SDK.NetworkRequest.NetworkRequest[]; | |
| /** | |
| * The NetworkLog instance on which to install the Sinon stub. | |
| * Defaults to `Logs.NetworkLog.NetworkLog.instance()`. | |
| */ | |
| networkLog?: Logs.NetworkLog.NetworkLog; | |
| } | |
| /** | |
| * Stubs {@link Logs.NetworkLog.NetworkLog.initiatorGraphForRequest} for the given request | |
| * using Sinon. | |
| * | |
| * If `initiators` or `initiated` lists are omitted, generates default synthetic requests | |
| * simulating a typical multi-level initiator chain with cross-origin boundaries. | |
| * | |
| * @param request The network request whose initiator graph is being queried. | |
| * @param options Custom requests or NetworkLog instance to configure the stub. | |
| * @returns The lists of ancestor (`initiators`) and descendant (`initiated`) requests used, | |
| * along with the Sinon stub. | |
| */ | |
| export function stubInitiatorGraph( | |
| request: SDK.NetworkRequest.NetworkRequest, | |
| options: StubInitiatorGraphOptions = {}, | |
| ): { | |
| initiators: SDK.NetworkRequest.NetworkRequest[], | |
| initiated: SDK.NetworkRequest.NetworkRequest[], | |
| stub: sinon.SinonStub<[SDK.NetworkRequest.NetworkRequest], Logs.NetworkLog.InitiatorGraph>, | |
| } { | |
| // Explicitly use EmptyUrlString so that default synthetic requests are treated as | |
| // cross-origin, exercising URL redaction and security checks in downstream formatters. | |
| const initiators = options.initiators ?? [ | |
| createNetworkRequest({ | |
| requestId: 'requestId-initiator', | |
| url: urlString`https://www.initiator.com`, | |
| documentURL: Platform.DevToolsPath.EmptyUrlString, | |
| }), | |
| ]; | |
| const initiated = options.initiated ?? [ | |
| createNetworkRequest({ | |
| requestId: 'requestId-initiated-1', | |
| url: urlString`https://www.example.com/1`, | |
| documentURL: Platform.DevToolsPath.EmptyUrlString, | |
| }), | |
| createNetworkRequest({ | |
| requestId: 'requestId-initiated-2', | |
| url: urlString`https://www.example.com/2`, | |
| documentURL: Platform.DevToolsPath.EmptyUrlString, | |
| }), | |
| ]; | |
| const networkLog = options.networkLog ?? Logs.NetworkLog.NetworkLog.instance(); | |
| const stub = sinon.stub(networkLog, 'initiatorGraphForRequest'); | |
| // Default fallback for any request not explicitly handled: | |
| stub.returns({ | |
| initiators: new Set<SDK.NetworkRequest.NetworkRequest>(), | |
| initiated: new Map<SDK.NetworkRequest.NetworkRequest, SDK.NetworkRequest.NetworkRequest>(), | |
| }); | |
| // InitiatorGraph.initiated is a Map<ChildRequest, ParentInitiator>: | |
| // - [request, init]: target request was initiated by upstream ancestor `init`. | |
| // - [init, request]: downstream child `init` was initiated by target `request`. | |
| stub.withArgs(sinon.match.same(request)).returns({ | |
| initiators: new Set([request, ...initiators]), | |
| initiated: new Map([ | |
| ...initiators.map(init => [request, init] as const), | |
| ...initiated.map(init => [init, request] as const), | |
| ]), | |
| }); | |
| // Downstream consumers may recursively query initiatorGraphForRequest on child requests: | |
| for (const init of initiated) { | |
| stub.withArgs(sinon.match.same(init)).returns({ | |
| initiators: new Set([]), | |
| initiated: new Map([[init, request]]), | |
| }); | |
| } | |
| return {initiators, initiated, stub}; | |
| } |