UNPKG

chrome-devtools-frontend

Version:
660 lines (552 loc) 29.4 kB
// Copyright 2022 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 * as Host from '../../../core/host/host.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Protocol from '../../../generated/protocol.js'; import { dispatchCopyEvent, dispatchInputEvent, dispatchKeyDownEvent, dispatchPasteEvent, getCleanTextContentFromElements, renderElementIntoDOM, } from '../../../testing/DOMHelpers.js'; import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js'; import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js'; import * as NetworkComponents from './components.js'; import type {EditableSpan} from './EditableSpan.js'; async function renderHeaderSectionRow(header: NetworkComponents.HeaderSectionRow.HeaderDescriptor): Promise<{ component: NetworkComponents.HeaderSectionRow.HeaderSectionRow, nameEditable: HTMLSpanElement | null, valueEditable: HTMLSpanElement | null, scrollIntoViewSpy: sinon.SinonSpy, }> { const component = new NetworkComponents.HeaderSectionRow.HeaderSectionRow(); const scrollIntoViewSpy = sinon.spy(component, 'scrollIntoView'); renderElementIntoDOM(component); sinon.assert.notCalled(scrollIntoViewSpy); component.data = {header}; await RenderCoordinator.done(); assert.isNotNull(component.shadowRoot); let nameEditable: HTMLSpanElement|null = null; const nameEditableComponent = component.shadowRoot.querySelector<NetworkComponents.EditableSpan.EditableSpan>( '.header-name devtools-editable-span'); if (nameEditableComponent) { assert.instanceOf(nameEditableComponent, HTMLElement); assert.isNotNull(nameEditableComponent.shadowRoot); nameEditable = nameEditableComponent.shadowRoot.querySelector('.editable'); assert.instanceOf(nameEditable, HTMLSpanElement); } let valueEditable: HTMLSpanElement|null = null; const valueEditableComponent = component.shadowRoot.querySelector<NetworkComponents.EditableSpan.EditableSpan>( '.header-value devtools-editable-span'); if (valueEditableComponent) { assert.instanceOf(valueEditableComponent, HTMLElement); assert.isNotNull(valueEditableComponent.shadowRoot); valueEditable = valueEditableComponent.shadowRoot.querySelector('.editable'); assert.instanceOf(valueEditable, HTMLSpanElement); } return {component, nameEditable, valueEditable, scrollIntoViewSpy}; } const hasReloadPrompt = (shadowRoot: ShadowRoot) => { return Boolean( shadowRoot.querySelector('devtools-icon[title="Refresh the page/request for these changes to take effect"]')); }; describeWithEnvironment('HeaderSectionRow', () => { it('emits UMA event when a header value is being copied', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: 'someHeaderValue', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.DISABLED, }; const {component, scrollIntoViewSpy} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); sinon.assert.notCalled(scrollIntoViewSpy); const spy = sinon.spy(Host.userMetrics, 'actionTaken'); const headerValue = component.shadowRoot.querySelector('.header-value'); assert.instanceOf(headerValue, HTMLElement); sinon.assert.notCalled(spy); dispatchCopyEvent(headerValue); sinon.assert.calledWith(spy, Host.UserMetrics.Action.NetworkPanelCopyValue); }); it('renders detailed reason for blocked requests', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('cross-origin-resource-policy'), value: null, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.DISABLED, headerNotSet: true, blockedDetails: { explanation: () => 'To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers:', examples: [ { codeSnippet: 'Cross-Origin-Resource-Policy: same-site', comment: () => 'Choose this option if the resource and the document are served from the same site.', }, { codeSnippet: 'Cross-Origin-Resource-Policy: cross-origin', comment: () => 'Only choose this option if an arbitrary website including this resource does not impose a security risk.', }, ], link: {url: 'https://web.dev/coop-coep/'}, }, }; const {component} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); const headerName = component.shadowRoot.querySelector('.header-name'); assert.instanceOf(headerName, HTMLDivElement); const regex = /^\s*not-set\s*cross-origin-resource-policy\s*$/; assert.isTrue(regex.test(headerName.textContent || '')); const headerValue = component.shadowRoot.querySelector('.header-value'); assert.instanceOf(headerValue, HTMLDivElement); assert.strictEqual(headerValue.textContent?.trim(), ''); assert.strictEqual( getCleanTextContentFromElements(component.shadowRoot, '.call-to-action')[0], `To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers: Cross-Origin-Resource-Policy: same-site Choose this option if the resource and the document are served from the same site. Cross-Origin-Resource-Policy: cross-origin Only choose this option if an arbitrary website including this resource does not impose a security risk. Learn more`, ); }); it('displays decoded "x-client-data"-header', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('x-client-data'), value: 'CJa2yQEIpLbJAQiTocsB', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.DISABLED, }; const {component} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); const headerName = component.shadowRoot.querySelector('.header-name'); assert.instanceOf(headerName, HTMLDivElement); assert.strictEqual(headerName.textContent?.trim(), 'x-client-data'); const headerValue = component.shadowRoot.querySelector('.header-value'); assert.instanceOf(headerValue, HTMLDivElement); assert.isTrue(headerValue.classList.contains('flex-columns')); assert.isTrue( (getCleanTextContentFromElements(component.shadowRoot, '.header-value')[0]).startsWith('CJa2yQEIpLbJAQiTocsB')); assert.strictEqual( getCleanTextContentFromElements(component.shadowRoot, '.header-value code')[0], 'message ClientVariations { // Active Google-visible variation IDs on this client. These are reported for analysis, but do not directly affect any server-side behavior. repeated int32 variation_id = [3300118, 3300132, 3330195];\n}', ); }); it('displays info about blocked "Set-Cookie"-headers', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('set-cookie'), value: 'secure=only; Secure', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.DISABLED, setCookieBlockedReasons: [Protocol.Network.SetCookieBlockedReason.SecureOnly, Protocol.Network.SetCookieBlockedReason.OverwriteSecure], }; const {component} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); const headerName = component.shadowRoot.querySelector('.header-name'); assert.instanceOf(headerName, HTMLDivElement); assert.strictEqual(headerName.textContent?.trim(), 'set-cookie'); const headerValue = component.shadowRoot.querySelector('.header-value'); assert.instanceOf(headerValue, HTMLDivElement); assert.strictEqual(headerValue.textContent?.trim(), 'secure=only; Secure'); const icon = component.shadowRoot.querySelector('devtools-icon'); assert.instanceOf(icon, HTMLElement); assert.strictEqual( icon.title, 'This attempt to set a cookie via a Set-Cookie header was blocked because it had the ' + '"Secure" attribute but was not received over a secure connection.\nThis attempt to ' + 'set a cookie via a Set-Cookie header was blocked because it was not sent over a ' + 'secure connection and would have overwritten a cookie with the Secure attribute.'); }); it('can be highlighted', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: 'someHeaderValue', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.DISABLED, highlight: true, }; const {component, scrollIntoViewSpy} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); const headerRowElement = component.shadowRoot.querySelector('.row.header-highlight'); assert.instanceOf(headerRowElement, HTMLDivElement); sinon.assert.calledOnce(scrollIntoViewSpy); }); it('allows editing header name and header value', async () => { const originalHeaderName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const originalHeaderValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: originalHeaderName, value: originalHeaderValue, nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderName = 'new-header-name'; const editedHeaderValue = 'new value for header'; const {component, nameEditable, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let headerValueFromEvent = ''; let headerNameFromEvent = ''; let headerEditedEventCount = 0; component.addEventListener('headeredited', event => { headerEditedEventCount++; headerValueFromEvent = event.headerValue; headerNameFromEvent = event.headerName; }); assert.instanceOf(nameEditable, HTMLSpanElement); assert.isTrue(hasReloadPrompt(component.shadowRoot)); nameEditable.focus(); nameEditable.innerText = editedHeaderName; dispatchInputEvent(nameEditable, {inputType: 'insertText', data: editedHeaderName, bubbles: true, composed: true}); nameEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 1); assert.strictEqual(headerNameFromEvent, editedHeaderName); assert.strictEqual(headerValueFromEvent, originalHeaderValue); assert.isTrue(hasReloadPrompt(component.shadowRoot)); assert.instanceOf(valueEditable, HTMLSpanElement); valueEditable.focus(); valueEditable.innerText = editedHeaderValue; dispatchInputEvent( valueEditable, {inputType: 'insertText', data: editedHeaderValue, bubbles: true, composed: true}); valueEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 2); assert.strictEqual(headerNameFromEvent, editedHeaderName); assert.strictEqual(headerValueFromEvent, editedHeaderValue); assert.isTrue(hasReloadPrompt(component.shadowRoot)); nameEditable.focus(); nameEditable.innerText = originalHeaderName; dispatchInputEvent( nameEditable, {inputType: 'insertText', data: originalHeaderName, bubbles: true, composed: true}); nameEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 3); assert.strictEqual(headerNameFromEvent, originalHeaderName); assert.strictEqual(headerValueFromEvent, editedHeaderValue); assert.isTrue(hasReloadPrompt(component.shadowRoot)); valueEditable.focus(); valueEditable.innerText = originalHeaderValue; dispatchInputEvent( valueEditable, {inputType: 'insertText', data: originalHeaderValue, bubbles: true, composed: true}); valueEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 4); assert.strictEqual(headerNameFromEvent, originalHeaderName); assert.strictEqual(headerValueFromEvent, originalHeaderValue); assert.isTrue(hasReloadPrompt(component.shadowRoot)); }); it('does not allow setting an emtpy header name', async () => { const headerName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: headerName, value: 'someHeaderValue', nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const {component, nameEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let headerEditedEventCount = 0; component.addEventListener('headeredited', () => { headerEditedEventCount++; }); assert.instanceOf(nameEditable, HTMLElement); nameEditable.focus(); nameEditable.innerText = ''; nameEditable.blur(); assert.strictEqual(headerEditedEventCount, 0); assert.strictEqual(nameEditable.innerText, 'Some-Header-Name'); }); it('resets edited value on escape key', async () => { const originalHeaderValue = 'special chars: \'\"\\.,;!?@_-+/=<>()[]{}|*&^%$#§±`~'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: originalHeaderValue, originalValue: originalHeaderValue, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const {component, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let eventCount = 0; component.addEventListener('headeredited', () => { eventCount++; }); assert.instanceOf(valueEditable, HTMLElement); assert.strictEqual(valueEditable.innerText, originalHeaderValue); valueEditable.focus(); valueEditable.innerText = 'new value for header'; dispatchKeyDownEvent(valueEditable, {key: 'Escape', bubbles: true, composed: true}); assert.strictEqual(eventCount, 0); assert.strictEqual(valueEditable.innerText, originalHeaderValue); const row = component.shadowRoot.querySelector('.row'); assert.isFalse(row?.classList.contains('header-overridden')); }); it('confirms edited value and exits editing mode on "Enter"-key', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: 'someHeaderValue', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderValue = 'new value for header'; const {component, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let headerValueFromEvent = ''; let eventCount = 0; component.addEventListener('headeredited', event => { headerValueFromEvent = event.headerValue; eventCount++; }); assert.instanceOf(valueEditable, HTMLElement); valueEditable.focus(); valueEditable.innerText = editedHeaderValue; dispatchKeyDownEvent(valueEditable, {key: 'Enter', bubbles: true}); assert.strictEqual(headerValueFromEvent, editedHeaderValue); assert.strictEqual(eventCount, 1); }); it('adds and removes `header-overridden` class correctly', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: 'someHeaderValue', originalValue: 'someHeaderValue', valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, highlight: true, }; const {component, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(valueEditable, HTMLElement); const row = component.shadowRoot.querySelector('.row'); assert.isFalse(row?.classList.contains('header-overridden')); assert.isTrue(row?.classList.contains('header-highlight')); assert.isFalse(hasReloadPrompt(component.shadowRoot)); valueEditable.focus(); valueEditable.innerText = 'a'; dispatchInputEvent(valueEditable, {inputType: 'insertText', data: 'a', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.isTrue(row?.classList.contains('header-overridden')); assert.isFalse(row?.classList.contains('header-highlight')); assert.isTrue(hasReloadPrompt(component.shadowRoot)); dispatchKeyDownEvent(valueEditable, {key: 'Escape', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.isFalse(component.shadowRoot.querySelector('.row')?.classList.contains('header-overridden')); }); it('adds and removes `header-overridden` class correctly when editing unset headers', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: null, originalValue: null, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const {component, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(valueEditable, HTMLElement); const row = component.shadowRoot.querySelector('.row'); assert.isFalse(row?.classList.contains('header-overridden')); valueEditable.focus(); valueEditable.innerText = 'a'; dispatchInputEvent(valueEditable, {inputType: 'insertText', data: 'a', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.isTrue(row?.classList.contains('header-overridden')); dispatchKeyDownEvent(valueEditable, {key: 'Escape', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.isFalse(component.shadowRoot.querySelector('.row')?.classList.contains('header-overridden')); }); it('shows error-icon when header name contains disallowed characters', async () => { const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: Platform.StringUtilities.toLowerCaseString('some-header-name'), value: 'someHeaderValue', originalValue: 'someHeaderValue', nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const {component, nameEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(nameEditable, HTMLElement); const row = component.shadowRoot.querySelector('.row'); assert.instanceOf(row, HTMLDivElement); assert.isNull(row.querySelector('devtools-icon.disallowed-characters')); assert.isTrue(hasReloadPrompt(component.shadowRoot)); nameEditable.focus(); nameEditable.innerText = '*'; dispatchInputEvent(nameEditable, {inputType: 'insertText', data: '*', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.instanceOf(row.querySelector('devtools-icon.disallowed-characters'), HTMLElement); assert.isTrue(hasReloadPrompt(component.shadowRoot)); dispatchKeyDownEvent(nameEditable, {key: 'Escape', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.isNull(row.querySelector('devtools-icon.disallowed-characters')); assert.isTrue(hasReloadPrompt(component.shadowRoot)); }); it('split header name and value on pasted content', async () => { const originalHeaderName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const originalHeaderValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: originalHeaderName, value: originalHeaderValue, nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderName = 'permissions-Policy: unload=(https://xyz.com)'; const {component, nameEditable, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(nameEditable, HTMLElement); assert.instanceOf(valueEditable, HTMLElement); let headerValueFromEvent = ''; let headerNameFromEvent = ''; let headerEditedEventCount = 0; component.addEventListener('headeredited', event => { headerValueFromEvent = event.headerValue; headerNameFromEvent = event.headerName; headerEditedEventCount++; }); const dt = new DataTransfer(); dt.setData('text/plain', editedHeaderName); // update name on blur nameEditable.focus(); dispatchPasteEvent(nameEditable, {clipboardData: dt, bubbles: true, composed: true}); nameEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 1); assert.strictEqual(headerNameFromEvent, 'permissions-policy'); assert.strictEqual(headerValueFromEvent, 'someHeaderValue'); // update value on blur valueEditable.blur(); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 2); assert.strictEqual(headerNameFromEvent, 'permissions-policy'); assert.strictEqual(headerValueFromEvent, 'unload=(https://xyz.com)'); // final value on UI const nameEl = component.shadowRoot.querySelector('.header-name devtools-editable-span') as EditableSpan; const valueEl = component.shadowRoot.querySelector('.header-value devtools-editable-span') as EditableSpan; assert.strictEqual(nameEl.value, 'Permissions-Policy'); assert.strictEqual(valueEl.value, 'unload=(https://xyz.com)'); }); it('set and revert pasted header name on escape', async () => { const originalHeaderName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const originalHeaderValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: originalHeaderName, value: originalHeaderValue, nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderName = ':abc'; const {component, nameEditable, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(nameEditable, HTMLElement); assert.instanceOf(valueEditable, HTMLElement); let headerEditedEventCount = 0; component.addEventListener('headeredited', () => { headerEditedEventCount++; }); const dt = new DataTransfer(); dt.setData('text/plain', editedHeaderName); nameEditable.focus(); dispatchPasteEvent(nameEditable, {clipboardData: dt, bubbles: true, composed: true}); const nameEl = component.shadowRoot.querySelector('.header-name devtools-editable-span') as EditableSpan; const valueEl = component.shadowRoot.querySelector('.header-value devtools-editable-span') as EditableSpan; await RenderCoordinator.done(); assert.strictEqual(nameEl.value, ':Abc'); assert.strictEqual(valueEl.value, originalHeaderValue); dispatchKeyDownEvent(nameEditable, {key: 'Escape', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 0); assert.strictEqual(nameEl.value, 'Some-Header-Name'); }); it('revert pasted header name and value on escape', async () => { const originalHeaderName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const originalHeaderValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: originalHeaderName, value: originalHeaderValue, nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderName = 'permissions-Policy: unload=(https://xyz.com)'; const {component, nameEditable, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); assert.instanceOf(nameEditable, HTMLElement); assert.instanceOf(valueEditable, HTMLElement); let headerEditedEventCount = 0; component.addEventListener('headeredited', () => { headerEditedEventCount++; }); const dt = new DataTransfer(); dt.setData('text/plain', editedHeaderName); nameEditable.focus(); dispatchPasteEvent(nameEditable, {clipboardData: dt, bubbles: true, composed: true}); const nameEl = component.shadowRoot.querySelector('.header-name devtools-editable-span') as EditableSpan; const valueEl = component.shadowRoot.querySelector('.header-value devtools-editable-span') as EditableSpan; await RenderCoordinator.done(); assert.strictEqual(nameEl.value, 'Permissions-Policy'); assert.strictEqual(valueEl.value, 'unload=(https://xyz.com)'); dispatchKeyDownEvent(valueEditable, {key: 'Escape', bubbles: true, composed: true}); await RenderCoordinator.done(); assert.strictEqual(headerEditedEventCount, 0); assert.strictEqual(nameEl.value, 'Some-Header-Name'); assert.strictEqual(valueEl.value, 'someHeaderValue'); }); it('recoginzes only alphanumeric characters, dashes, and underscores as valid in header names', () => { assert.isTrue(NetworkComponents.HeaderSectionRow.isValidHeaderName('AlphaNumeric123')); assert.isFalse(NetworkComponents.HeaderSectionRow.isValidHeaderName('Alpha Numeric')); assert.isFalse(NetworkComponents.HeaderSectionRow.isValidHeaderName('AlphaNumeric123!')); assert.isTrue(NetworkComponents.HeaderSectionRow.isValidHeaderName('With-dashes_and_underscores')); assert.isFalse(NetworkComponents.HeaderSectionRow.isValidHeaderName('no*')); }); it('allows removing a header override', async () => { const headerName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const headerValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: headerName, value: headerValue, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const {component} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let headerValueFromEvent = ''; let headerNameFromEvent = ''; let headerRemovedEventCount = 0; component.addEventListener('headerremoved', event => { headerRemovedEventCount++; headerValueFromEvent = (event as NetworkComponents.HeaderSectionRow.HeaderRemovedEvent).headerValue; headerNameFromEvent = (event as NetworkComponents.HeaderSectionRow.HeaderRemovedEvent).headerName; }); const removeHeaderButton = component.shadowRoot.querySelector('.remove-header') as HTMLElement; removeHeaderButton.click(); assert.strictEqual(headerRemovedEventCount, 1); assert.strictEqual(headerNameFromEvent, headerName); assert.strictEqual(headerValueFromEvent, headerValue); }); it('removes leading/trailing whitespace when editing header names/values', async () => { const originalHeaderName = Platform.StringUtilities.toLowerCaseString('some-header-name'); const originalHeaderValue = 'someHeaderValue'; const headerData: NetworkComponents.HeaderSectionRow.HeaderDescriptor = { name: originalHeaderName, value: originalHeaderValue, nameEditable: true, valueEditable: NetworkComponents.HeaderSectionRow.EditingAllowedStatus.ENABLED, }; const editedHeaderName = ' new-header-name '; const editedHeaderValue = ' new value for header '; const {component, nameEditable, valueEditable} = await renderHeaderSectionRow(headerData); assert.isNotNull(component.shadowRoot); let headerValueFromEvent = ''; let headerNameFromEvent = ''; let headerEditedEventCount = 0; component.addEventListener('headeredited', event => { headerEditedEventCount++; headerValueFromEvent = event.headerValue; headerNameFromEvent = event.headerName; }); assert.instanceOf(nameEditable, HTMLElement); nameEditable.focus(); nameEditable.innerText = editedHeaderName; nameEditable.blur(); assert.strictEqual(headerEditedEventCount, 1); assert.strictEqual(headerNameFromEvent, editedHeaderName.trim()); assert.strictEqual(headerValueFromEvent, originalHeaderValue); assert.instanceOf(valueEditable, HTMLElement); valueEditable.focus(); valueEditable.innerText = editedHeaderValue; valueEditable.blur(); assert.strictEqual(headerEditedEventCount, 2); assert.strictEqual(headerNameFromEvent, editedHeaderName.trim()); assert.strictEqual(headerValueFromEvent, editedHeaderValue.trim()); }); });