| // Copyright 2025 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 {assertScreenshot, renderElementIntoDOM} from '../../../testing/DOMHelpers.js'; |
| import { |
| describeWithEnvironment, |
| } from '../../../testing/EnvironmentHelpers.js'; |
| import { |
| setUpEnvironment, |
| } from '../../../testing/OverridesHelpers.js'; |
| import {createViewFunctionStub, type ViewFunctionStub} from '../../../testing/ViewFunctionHelpers.js'; |
| import * as UI from '../../../ui/legacy/legacy.js'; |
| |
| import * as NetworkComponents from './components.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| |
| function createNetworkRequest() { |
| const networkRequest = SDK.NetworkRequest.NetworkRequest.createForSocket( |
| 'requestId' as Protocol.Network.RequestId, urlString`www.example.com/some/path:3000`); |
| networkRequest.hasNetworkData = true; |
| networkRequest.setRemoteAddress('www.example.com', 3000); |
| networkRequest.protocol = 'tcp'; |
| |
| networkRequest.statusText = 'Opening'; |
| networkRequest.directSocketInfo = { |
| type: SDK.NetworkRequest.DirectSocketType.TCP, |
| status: SDK.NetworkRequest.DirectSocketStatus.OPENING, |
| createOptions: { |
| remoteAddr: 'www.example.com/some/path', |
| remotePort: 3000, |
| noDelay: false, |
| keepAliveDelay: 1001, |
| sendBufferSize: 1002, |
| receiveBufferSize: 1003, |
| dnsQueryType: Protocol.Network.DirectSocketDnsQueryType.Ipv4, |
| }, |
| openInfo: {remoteAddr: 'www.sample.com', remotePort: 3005, localAddr: '127.0.0.1', localPort: 9472} |
| }; |
| networkRequest.setResourceType(Common.ResourceType.resourceTypes.DirectSocket); |
| networkRequest.setIssueTime(Date.now(), Date.now()); |
| return networkRequest; |
| } |
| let view: ViewFunctionStub<typeof NetworkComponents.DirectSocketConnectionView.DirectSocketConnectionView>; |
| let socketConnectionView: NetworkComponents.DirectSocketConnectionView.DirectSocketConnectionView; |
| |
| describeWithEnvironment('DirectSocketConnectionView', () => { |
| beforeEach(() => { |
| setUpEnvironment(); |
| |
| view = createViewFunctionStub(NetworkComponents.DirectSocketConnectionView.DirectSocketConnectionView); |
| socketConnectionView = |
| new NetworkComponents.DirectSocketConnectionView.DirectSocketConnectionView(createNetworkRequest(), view); |
| socketConnectionView.wasShown(); |
| }); |
| |
| const categoryName = NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL; |
| |
| describe('Category toggle', () => { |
| it('opens', async () => { |
| view.input.onToggleCategory({target: {open: true} as HTMLDetailsElement} as unknown as Event, categoryName); |
| |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| |
| it('closes', async () => { |
| view.input.onToggleCategory({target: {open: false} as HTMLDetailsElement} as unknown as Event, categoryName); |
| |
| assert.isFalse((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| it('opens after close', async () => { |
| view.input.onToggleCategory({target: {open: true} as HTMLDetailsElement} as unknown as Event, categoryName); |
| |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| }); |
| |
| describe('Handles arrow keys on category', () => { |
| it('opens', async () => { |
| // set initial state as closed |
| view.input.onToggleCategory({target: {open: false} as HTMLDetailsElement} as unknown as Event, categoryName); |
| assert.isFalse((await view.nextInput).openCategories.includes(categoryName)); |
| |
| // make keyboard event to open |
| view.input.onSummaryKeyDown( |
| { |
| target: { |
| parentElement: { |
| // current state |
| open: false |
| } |
| }, |
| key: 'ArrowRight' |
| } as unknown as KeyboardEvent, |
| categoryName); |
| |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| |
| it('closes', async () => { |
| // set initial state as opened |
| view.input.onToggleCategory({target: {open: true} as HTMLDetailsElement} as unknown as Event, categoryName); |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| |
| // make keyboard event to close |
| view.input.onSummaryKeyDown( |
| { |
| target: { |
| parentElement: { |
| // current state |
| open: true |
| } |
| }, |
| key: 'ArrowLeft' |
| } as unknown as KeyboardEvent, |
| categoryName); |
| |
| assert.isFalse((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| |
| it('does nothing if target is absent', async () => { |
| // set initial state as opened |
| view.input.onToggleCategory({target: {open: true} as HTMLDetailsElement} as unknown as Event, categoryName); |
| // make keyboard event without target |
| view.input.onSummaryKeyDown({key: 'ArrowLeft'} as unknown as KeyboardEvent, categoryName); |
| |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| |
| it('ignores unknown keys', async () => { |
| // set initial state as opened |
| view.input.onToggleCategory({target: {open: true} as HTMLDetailsElement} as unknown as Event, categoryName); |
| view.input.onSummaryKeyDown( |
| { |
| target: { |
| parentElement: { |
| // current state |
| open: true |
| } |
| }, |
| // unknown key |
| key: 'ArrowDown' |
| } as unknown as KeyboardEvent, |
| categoryName); |
| |
| assert.isTrue((await view.nextInput).openCategories.includes(categoryName)); |
| }); |
| }); |
| }); |
| |
| describeWithEnvironment('view', () => { |
| let target!: HTMLElement; |
| const view = NetworkComponents.DirectSocketConnectionView.DEFAULT_VIEW; |
| |
| beforeEach(async () => { |
| const container = document.createElement('div'); |
| renderElementIntoDOM(container); |
| const widget = new UI.Widget.Widget(); |
| widget.markAsRoot(); |
| widget.show(container); |
| target = widget.element; |
| target.style.display = 'flex'; |
| target.style.flexDirection = 'column'; |
| target.style.width = '500px'; |
| target.style.height = '400px'; |
| }); |
| |
| it('all categories are opened', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.TCP, |
| status: SDK.NetworkRequest.DirectSocketStatus.OPENING, |
| createOptions: { |
| remoteAddr: 'www.example.com/some/path', |
| remotePort: 3000, |
| noDelay: false, |
| keepAliveDelay: 1001, |
| sendBufferSize: 1002, |
| receiveBufferSize: 1003, |
| dnsQueryType: Protocol.Network.DirectSocketDnsQueryType.Ipv4, |
| }, |
| openInfo: {remoteAddr: 'www.sample.com', remotePort: 3005, localAddr: '127.0.0.1', localPort: 9472} |
| }, |
| openCategories: [ |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPEN_INFO, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPTIONS |
| ], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {} |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/all_categories_open.png'); |
| }); |
| |
| it('all categories are opened with some values absent', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.TCP, |
| status: SDK.NetworkRequest.DirectSocketStatus.ABORTED, |
| errorMessage: 'Cannot resolve hostname. And long error message goes next next next next', |
| createOptions: { |
| remoteAddr: 'www.example.com/some/path', |
| remotePort: 3000, |
| }, |
| }, |
| openCategories: [ |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPEN_INFO, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPTIONS |
| ], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {} |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/all_categories_open_values_absent.png'); |
| }); |
| |
| it('all categories are closed', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.UDP_BOUND, |
| status: SDK.NetworkRequest.DirectSocketStatus.CLOSED, |
| createOptions: { |
| remoteAddr: 'www.example.com/some/path', |
| remotePort: 3000, |
| noDelay: false, |
| keepAliveDelay: 2001, |
| sendBufferSize: 2002, |
| receiveBufferSize: 2003, |
| dnsQueryType: Protocol.Network.DirectSocketDnsQueryType.Ipv4, |
| }, |
| openInfo: {remoteAddr: 'www.sample.com', remotePort: 3005, localAddr: '127.0.0.1', localPort: 9472} |
| }, |
| openCategories: [], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {} |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/all_categories_closed.png'); |
| }); |
| |
| it('renders UDP Bound options with multicast groups and address sharing', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.UDP_BOUND, |
| status: SDK.NetworkRequest.DirectSocketStatus.OPEN, |
| createOptions: { |
| localAddr: '0.0.0.0', |
| localPort: 1234, |
| multicastAllowAddressSharing: true, |
| }, |
| joinedMulticastGroups: new Set(['224.0.0.1', '224.0.0.2']), |
| }, |
| openCategories: [ |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPTIONS, |
| ], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {}, |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/udp_bound_multicast.png'); |
| }); |
| |
| it('renders UDP Connected options with multicast TTL and loopback', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.UDP_CONNECTED, |
| status: SDK.NetworkRequest.DirectSocketStatus.OPEN, |
| createOptions: { |
| remoteAddr: 'www.example.com', |
| remotePort: 5678, |
| multicastTimeToLive: 64, |
| multicastLoopback: false, |
| }, |
| }, |
| openCategories: [ |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPTIONS, |
| ], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {}, |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/udp_connected_multicast.png'); |
| }); |
| |
| it('hides UDP multicast options when not present', async () => { |
| const viewInput: NetworkComponents.DirectSocketConnectionView.ViewInput = { |
| socketInfo: { |
| type: SDK.NetworkRequest.DirectSocketType.UDP_BOUND, |
| status: SDK.NetworkRequest.DirectSocketStatus.OPEN, |
| createOptions: { |
| localAddr: '0.0.0.0', |
| localPort: 1234, |
| }, |
| }, |
| openCategories: [ |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_GENERAL, |
| NetworkComponents.DirectSocketConnectionView.CATEGORY_NAME_OPTIONS, |
| ], |
| onSummaryKeyDown: () => {}, |
| onToggleCategory: () => {}, |
| onCopyRow: () => {}, |
| }; |
| |
| view(viewInput, undefined, target); |
| await assertScreenshot('direct_socket_connection_view/udp_no_multicast.png'); |
| }); |
| }); |