| // Copyright 2021 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 SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as Bindings from '../../models/bindings/bindings.js'; |
| import * as Workspace from '../../models/workspace/workspace.js'; |
| import { |
| raf, |
| renderElementIntoDOM, |
| } from '../../testing/DOMHelpers.js'; |
| import {createTarget} from '../../testing/EnvironmentHelpers.js'; |
| import { |
| describeWithMockConnection, |
| dispatchEvent, |
| } from '../../testing/MockConnection.js'; |
| import type * as ReportView from '../../ui/components/report_view/report_view.js'; |
| |
| import * as Application from './application.js'; |
| |
| const makeFrame = (target: SDK.Target.Target) => { |
| const newFrame: SDK.ResourceTreeModel.ResourceTreeFrame = { |
| url: 'https://www.example.com/path/page.html', |
| securityOrigin: 'https://www.example.com', |
| displayName: () => 'TestTitle', |
| unreachableUrl: () => '', |
| adFrameType: () => Protocol.Page.AdFrameType.None, |
| adFrameStatus: () => undefined, |
| getAdScriptAncestry: () => null, |
| resourceForURL: () => null, |
| isSecureContext: () => true, |
| isCrossOriginIsolated: () => true, |
| getCrossOriginIsolatedContextType: () => Protocol.Page.CrossOriginIsolatedContextType.NotIsolatedFeatureDisabled, |
| getSecureContextType: () => Protocol.Page.SecureContextType.SecureLocalhost, |
| getGatedAPIFeatures: () => |
| [Protocol.Page.GatedAPIFeatures.SharedArrayBuffers, |
| Protocol.Page.GatedAPIFeatures.SharedArrayBuffersTransferAllowed], |
| getOwnerDOMNodeOrDocument: () => Promise.resolve({ |
| nodeName: () => 'iframe', |
| nodeType: () => Node.ELEMENT_NODE, |
| pseudoType: () => undefined, |
| isViewTransitionPseudoNode: () => false, |
| nodeNameInCorrectCase: () => 'iframe', |
| getAttribute: () => null, |
| }), |
| resourceTreeModel: () => target.model(SDK.ResourceTreeModel.ResourceTreeModel), |
| getCreationStackTraceData: () => ({ |
| creationStackTrace: { |
| callFrames: [{ |
| functionName: 'function1', |
| url: 'http://www.example.com/script.js', |
| lineNumber: 15, |
| columnNumber: 10, |
| scriptId: 'someScriptId', |
| }], |
| }, |
| creationStackTraceTarget: target, |
| }), |
| getOriginTrials: async () => ([ |
| { |
| trialName: 'AppCache', |
| status: 'Enabled', |
| tokensWithStatus: [{ |
| status: 'Success', |
| rawTokenText: 'Text', |
| parsedToken: { |
| trialName: 'AppCache', |
| origin: 'https://foo.com', |
| expiryTime: 1000, |
| usageRestriction: 'None', |
| isThirdParty: false, |
| matchSubDomains: false, |
| }, |
| }], |
| }, |
| ]), |
| getPermissionsPolicyState: () => Promise.resolve(null), |
| parentFrame: () => null, |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame; |
| return newFrame; |
| }; |
| |
| describeWithMockConnection('FrameDetailsView', () => { |
| beforeEach(() => { |
| const workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true}); |
| const targetManager = SDK.TargetManager.TargetManager.instance(); |
| const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true}); |
| Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ |
| forceNew: true, |
| resourceMapping: new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace), |
| targetManager, |
| ignoreListManager, |
| workspace, |
| }); |
| }); |
| |
| it('renders with a title', async () => { |
| const frame = makeFrame(createTarget()); |
| const component = new Application.FrameDetailsView.FrameDetailsReportView(); |
| component.frame = frame; |
| renderElementIntoDOM(component); |
| |
| await component.updateComplete; |
| |
| const report = component.contentElement.querySelector('devtools-report') as ReportView.ReportView.Report; |
| |
| const titleElement = report.shadowRoot!.querySelector('.report-title'); |
| assert.strictEqual(titleElement?.textContent, frame.displayName()); |
| }); |
| |
| it('renders report keys and values', async () => { |
| |
| const target = createTarget(); |
| const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); |
| assert.exists(debuggerModel); |
| sinon.stub(SDK.DebuggerModel.DebuggerModel, 'modelForDebuggerId').resolves(debuggerModel); |
| |
| const scriptParsedEvent1: Protocol.Debugger.ScriptParsedEvent = { |
| scriptId: '123' as Protocol.Runtime.ScriptId, |
| url: 'https://www.google.com/ad-script1.js', |
| startLine: 0, |
| startColumn: 0, |
| endLine: 10, |
| endColumn: 10, |
| executionContextId: 1234 as Protocol.Runtime.ExecutionContextId, |
| hash: '', |
| buildId: '', |
| }; |
| dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent1); |
| |
| const scriptParsedEvent2: Protocol.Debugger.ScriptParsedEvent = { |
| scriptId: '456' as Protocol.Runtime.ScriptId, |
| url: 'https://www.google.com/ad-script2.js', |
| startLine: 0, |
| startColumn: 0, |
| endLine: 10, |
| endColumn: 10, |
| executionContextId: 1234 as Protocol.Runtime.ExecutionContextId, |
| hash: '', |
| buildId: '', |
| }; |
| dispatchEvent(target, 'Debugger.scriptParsed', scriptParsedEvent2); |
| |
| const frame = makeFrame(target); |
| frame.adFrameType = () => Protocol.Page.AdFrameType.Root; |
| frame.parentFrame = () => ({ |
| getAdScriptAncestry: () => ({ |
| ancestryChain: [ |
| { |
| scriptId: '123' as Protocol.Runtime.ScriptId, |
| debuggerId: '42' as Protocol.Runtime.UniqueDebuggerId, |
| }, |
| { |
| scriptId: '456' as Protocol.Runtime.ScriptId, |
| debuggerId: '42' as Protocol.Runtime.UniqueDebuggerId, |
| } |
| ], |
| rootScriptFilterlistRule: '/ad-script2.$script', |
| }), |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); |
| const networkManager = target.model(SDK.NetworkManager.NetworkManager); |
| assert.exists(networkManager); |
| sinon.stub(networkManager, 'getSecurityIsolationStatus').resolves({ |
| coep: { |
| value: Protocol.Network.CrossOriginEmbedderPolicyValue.None, |
| reportOnlyValue: Protocol.Network.CrossOriginEmbedderPolicyValue.None, |
| }, |
| coop: { |
| value: Protocol.Network.CrossOriginOpenerPolicyValue.SameOrigin, |
| reportOnlyValue: Protocol.Network.CrossOriginOpenerPolicyValue.SameOrigin, |
| }, |
| csp: [{ |
| source: Protocol.Network.ContentSecurityPolicySource.HTTP, |
| isEnforced: true, |
| effectiveDirectives: |
| 'base-uri \'self\'; object-src \'none\'; script-src \'strict-dynamic\' \'unsafe-inline\' https: http: \'nonce-GsVjHiIoejpPhMPOHDQZ90yc9eJn1s\' \'unsafe-eval\'; report-uri https://www.example.com/csp', |
| }], |
| }); |
| |
| const component = new Application.FrameDetailsView.FrameDetailsReportView(); |
| component.frame = frame; |
| renderElementIntoDOM(component); |
| |
| await component.updateComplete; |
| |
| await raf(); |
| |
| const keys = [...component.contentElement.querySelectorAll('devtools-report-key')].map(k => k.deepInnerText()); |
| assert.deepEqual(keys, [ |
| 'URL', |
| 'Origin', |
| 'Owner Element', |
| 'Frame Creation Stack Trace', |
| 'Ad Status', |
| 'Creator Ad Script Ancestry', |
| 'Root Script Filterlist Rule', |
| 'Secure Context', |
| 'Cross-Origin Isolated', |
| 'Cross-Origin Embedder Policy (COEP)', |
| 'Cross-Origin Opener Policy (COOP)', |
| 'Content-Security-Policy', |
| 'SharedArrayBuffers', |
| 'Measure Memory', |
| ]); |
| |
| const values = [...component.contentElement.querySelectorAll('devtools-report-value')].map(v => v.deepInnerText()); |
| assert.deepEqual(values, [ |
| 'https://www.example.com/path/page.html', |
| 'https://www.example.com', |
| 'iframe', |
| '\tfunction1\t@\twww.example.com/script.js:16', |
| 'root', |
| 'ad-script1.js:1', |
| '/ad-script2.$script', |
| 'Yes\nLocalhost is always a secure context', |
| 'Yes', |
| 'none', |
| 'same-origin', |
| `HTTP header |
| base-uri: 'self' |
| object-src: 'none' |
| script-src: 'strict-dynamic', 'unsafe-inline', https:, http:, 'nonce-GsVjHiIoejpPhMPOHDQZ90yc9eJn1s', 'unsafe-eval' |
| report-uri: https://www.example.com/csp`, |
| 'available, transferable', |
| 'available\nLearn more', |
| ]); |
| |
| const stackTrace = component.contentElement.querySelector( |
| 'devtools-report-value[jslog="Section; context: frame-creation-stack-trace"] devtools-widget')!; |
| |
| const stackTraceText = stackTrace.deepInnerText().split('\n'); |
| |
| assert.deepEqual(stackTraceText[0], '\tfunction1\t@\twww.example.com/script.js:16'); |
| |
| const adStatusList = |
| component.contentElement.querySelector('devtools-report-value.ad-status-list devtools-expandable-list'); |
| assert.exists(adStatusList); |
| const adStatusExpandableButton = adStatusList.shadowRoot!.querySelector('button'); |
| assert.notExists(adStatusExpandableButton); |
| const adStatusItem = adStatusList.shadowRoot!.querySelector('.expandable-list-items'); |
| assert.exists(adStatusItem); |
| assert.strictEqual(adStatusItem.textContent?.trim(), 'root'); |
| |
| const adScriptAncestryList = component.contentElement.querySelector( |
| 'devtools-report-value.creator-ad-script-ancestry-list devtools-expandable-list'); |
| assert.exists(adScriptAncestryList); |
| const adScriptAncestryExpandableButton = adScriptAncestryList.shadowRoot!.querySelector('button'); |
| assert.exists(adScriptAncestryExpandableButton); |
| adScriptAncestryExpandableButton!.click(); |
| await raf(); |
| |
| const adScriptAncestryItems = |
| adScriptAncestryList!.shadowRoot!.querySelectorAll('.expandable-list-items .devtools-link'); |
| const adScriptsText = Array.from(adScriptAncestryItems).map(adScript => adScript.textContent?.trim()); |
| |
| assert.deepEqual(adScriptsText, ['ad-script1.js:1', 'ad-script2.js:1']); |
| }); |
| }); |