UNPKG

chrome-devtools-frontend

Version:
574 lines (497 loc) • 20.1 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 Workspace from '../../../models/workspace/workspace.js'; import { getContextMenuForElement, } from '../../../testing/ContextMenuHelpers.js'; import { dispatchFocusEvent, dispatchFocusOutEvent, dispatchInputEvent, dispatchKeyDownEvent, dispatchPasteEvent, renderElementIntoDOM, } from '../../../testing/DOMHelpers.js'; import { deinitializeGlobalVars, initializeGlobalVars, } from '../../../testing/EnvironmentHelpers.js'; import {createFileSystemUISourceCode} from '../../../testing/UISourceCodeHelpers.js'; import { recordedMetricsContain, resetRecordedMetrics, } from '../../../testing/UserMetricsHelpers.js'; import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js'; import * as SourcesComponents from './components.js'; const {urlString} = Platform.DevToolsPath; describe('HeadersView', () => { const commitWorkingCopySpy = sinon.spy(); before(async () => { await initializeGlobalVars(); }); after(async () => { await deinitializeGlobalVars(); }); beforeEach(() => { commitWorkingCopySpy.resetHistory(); resetRecordedMetrics(); }); async function renderEditor(): Promise<SourcesComponents.HeadersView.HeadersViewComponent> { const editor = new SourcesComponents.HeadersView.HeadersViewComponent(); editor.data = { headerOverrides: [ { applyTo: '*', headers: [ { name: 'server', value: 'DevTools Unit Test Server', }, { name: 'access-control-allow-origin', value: '*', }, ], }, { applyTo: '*.jpg', headers: [ { name: 'jpg-header', value: 'only for jpg files', }, ], }, ], parsingError: false, uiSourceCode: { name: () => '.headers', setWorkingCopy: () => {}, commitWorkingCopy: commitWorkingCopySpy, } as unknown as Workspace.UISourceCode.UISourceCode, }; renderElementIntoDOM(editor); assert.isNotNull(editor.shadowRoot); await RenderCoordinator.done(); return editor; } async function renderEditorWithinWrapper(): Promise<SourcesComponents.HeadersView.HeadersViewComponent> { const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const headers = `[ { "applyTo": "*", "headers": [ { "name": "server", "value": "DevTools Unit Test Server" }, { "name": "access-control-allow-origin", "value": "*" } ] }, { "applyTo": "*.jpg", "headers": [{ "name": "jpg-header", "value": "only for jpg files" }] } ]`; const {uiSourceCode, project} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/example.html`, mimeType: 'text/html', content: headers, }); uiSourceCode.commitWorkingCopy = commitWorkingCopySpy; project.canSetFileContent = () => true; const editorWrapper = new SourcesComponents.HeadersView.HeadersView(uiSourceCode); await uiSourceCode.requestContentData(); await RenderCoordinator.done(); const editor = editorWrapper.getComponent(); renderElementIntoDOM(editor); assert.isNotNull(editor.shadowRoot); await RenderCoordinator.done(); workspace.removeProject(project); return editor; } async function changeEditable(editable: HTMLElement, value: string): Promise<void> { dispatchFocusEvent(editable, {bubbles: true}); editable.innerText = value; dispatchInputEvent(editable, {inputType: 'insertText', data: value, bubbles: true, composed: true}); dispatchFocusOutEvent(editable, {bubbles: true}); await RenderCoordinator.done(); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); } async function pressButton(shadowRoot: ShadowRoot, rowIndex: number, selector: string): Promise<void> { const rowElements = shadowRoot.querySelectorAll('.row'); const button = rowElements[rowIndex].querySelector(selector); assert.instanceOf(button, HTMLElement); button.click(); await RenderCoordinator.done(); } function getRowContent(shadowRoot: ShadowRoot): string[] { const rows = Array.from(shadowRoot.querySelectorAll('.row')); return rows.map(row => { return Array.from(row.querySelectorAll('div, .editable')) .map(element => (element as HTMLElement).innerText) .join(''); }); } function getSingleRowContent(shadowRoot: ShadowRoot, rowIndex: number): string { const rows = Array.from(shadowRoot.querySelectorAll('.row')); assert.isTrue(rows.length > rowIndex); return Array.from(rows[rowIndex].querySelectorAll('div, .editable')) .map(element => (element as HTMLElement).innerText) .join(''); } function isWholeElementContentSelected(element: HTMLElement): boolean { const textContent = element.textContent; if (!textContent || textContent.length < 1 || !element.hasSelection()) { return false; } const selection = element.getComponentSelection(); if (!selection || selection.rangeCount < 1) { return false; } if (selection.anchorNode !== selection.focusNode) { return false; } const range = selection.getRangeAt(0); return (range.endOffset - range.startOffset === textContent.length); } it('shows an error message when parsingError is true', async () => { const editor = new SourcesComponents.HeadersView.HeadersViewComponent(); editor.data = { headerOverrides: [], parsingError: true, uiSourceCode: { name: () => '.headers', } as Workspace.UISourceCode.UISourceCode, }; renderElementIntoDOM(editor); assert.isNotNull(editor.shadowRoot); await RenderCoordinator.done(); const errorHeader = editor.shadowRoot.querySelector('.error-header'); assert.strictEqual(errorHeader?.textContent, 'Error when parsing \'.headers\'.'); }); it('displays data and allows editing', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); const addRuleButton = editor.shadowRoot.querySelector('.add-block'); assert.instanceOf(addRuleButton, HTMLElement); assert.strictEqual(addRuleButton.textContent?.trim(), 'Add override rule'); const learnMoreLink = editor.shadowRoot.querySelector('.learn-more-row x-link'); assert.instanceOf(learnMoreLink, HTMLElement); assert.strictEqual(learnMoreLink.title, 'https://goo.gle/devtools-override'); const editables = editor.shadowRoot.querySelectorAll('.editable'); await changeEditable(editables[0] as HTMLElement, 'index.html'); await changeEditable(editables[1] as HTMLElement, 'content-type'); await changeEditable(editables[4] as HTMLElement, 'example.com'); await changeEditable(editables[7] as HTMLElement, 'is image'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:index.html', 'content-type:DevTools Unit Test Server', 'access-control-allow-origin:example.com', 'Apply to:*.jpg', 'jpg-header:is image', ]); sinon.assert.callCount(commitWorkingCopySpy, 4); }); it('resets edited value to previous state on Escape key', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); assert.deepEqual(getSingleRowContent(editor.shadowRoot, 1), 'server:DevTools Unit Test Server'); const editables = editor.shadowRoot.querySelectorAll('.editable'); assert.lengthOf(editables, 8); const headerValue = editables[2] as HTMLElement; headerValue.focus(); headerValue.innerText = 'discard_me'; assert.deepEqual(getSingleRowContent(editor.shadowRoot, 1), 'server:discard_me'); dispatchKeyDownEvent(headerValue, { key: 'Escape', bubbles: true, }); await RenderCoordinator.done(); assert.deepEqual(getSingleRowContent(editor.shadowRoot, 1), 'server:DevTools Unit Test Server'); const headerName = editables[1] as HTMLElement; headerName.focus(); headerName.innerText = 'discard_me_2'; assert.deepEqual(getSingleRowContent(editor.shadowRoot, 1), 'discard_me_2:DevTools Unit Test Server'); dispatchKeyDownEvent(headerName, { key: 'Escape', bubbles: true, }); await RenderCoordinator.done(); assert.deepEqual(getSingleRowContent(editor.shadowRoot, 1), 'server:DevTools Unit Test Server'); }); it('selects the whole content when clicking on an editable field', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const editables = editor.shadowRoot.querySelectorAll('.editable'); let element = editables[0] as HTMLElement; element.focus(); assert.isTrue(isWholeElementContentSelected(element)); element = editables[1] as HTMLElement; element.focus(); assert.isTrue(isWholeElementContentSelected(element)); element = editables[2] as HTMLElement; element.focus(); assert.isTrue(isWholeElementContentSelected(element)); }); it('un-selects the content when an editable field loses focus', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const editables = editor.shadowRoot.querySelectorAll('.editable'); const element = editables[0] as HTMLElement; element.focus(); assert.isTrue(isWholeElementContentSelected(element)); element.blur(); assert.isFalse(element.hasSelection()); }); it('handles pressing \'Enter\' key by removing focus and moving it to the next field if possible', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const editables = editor.shadowRoot.querySelectorAll('.editable'); assert.lengthOf(editables, 8); const lastHeaderName = editables[6] as HTMLSpanElement; const lastHeaderValue = editables[7] as HTMLSpanElement; assert.isFalse(lastHeaderName.hasSelection()); assert.isFalse(lastHeaderValue.hasSelection()); lastHeaderName.focus(); assert.isTrue(isWholeElementContentSelected(lastHeaderName)); assert.isFalse(lastHeaderValue.hasSelection()); dispatchKeyDownEvent(lastHeaderName, {key: 'Enter', bubbles: true}); assert.isFalse(lastHeaderName.hasSelection()); assert.isTrue(isWholeElementContentSelected(lastHeaderValue)); dispatchKeyDownEvent(lastHeaderValue, {key: 'Enter', bubbles: true}); for (const editable of editables) { assert.isFalse(editable.hasSelection()); } }); it('sets empty \'ApplyTo\' to \'*\'', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const editables = editor.shadowRoot.querySelectorAll('.editable'); assert.lengthOf(editables, 8); const applyTo = editables[5] as HTMLSpanElement; assert.strictEqual(applyTo.innerHTML, '*.jpg'); applyTo.innerText = ''; dispatchInputEvent(applyTo, {inputType: 'deleteContentBackward', data: null, bubbles: true}); assert.strictEqual(applyTo.innerHTML, ''); dispatchFocusOutEvent(applyTo, {bubbles: true}); assert.strictEqual(applyTo.innerHTML, '*'); sinon.assert.callCount(commitWorkingCopySpy, 1); }); it('removes the entire header when the header name is deleted', async () => { const editor = await renderEditorWithinWrapper(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); const editables = editor.shadowRoot.querySelectorAll('.editable'); assert.lengthOf(editables, 8); const headerName = editables[1] as HTMLSpanElement; assert.strictEqual(headerName.innerHTML, 'server'); headerName.innerText = ''; dispatchInputEvent(headerName, {inputType: 'deleteContentBackward', data: null, bubbles: true}); assert.strictEqual(headerName.innerHTML, ''); dispatchFocusOutEvent(headerName, {bubbles: true}); await RenderCoordinator.done(); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); sinon.assert.callCount(commitWorkingCopySpy, 1); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); }); it('allows adding headers', async () => { const editor = await renderEditorWithinWrapper(); await RenderCoordinator.done(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); await pressButton(editor.shadowRoot, 1, '.add-header'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'header-name-1:header value', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); const editables = editor.shadowRoot.querySelectorAll('.editable'); await changeEditable(editables[3] as HTMLElement, 'cache-control'); await changeEditable(editables[4] as HTMLElement, 'max-age=1000'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'cache-control:max-age=1000', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); }); it('allows adding "ApplyTo"-blocks', async () => { const editor = await renderEditorWithinWrapper(); await RenderCoordinator.done(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); const button = editor.shadowRoot.querySelector('.add-block'); assert.instanceOf(button, HTMLElement); button.click(); await RenderCoordinator.done(); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', 'Apply to:*', 'header-name-1:header value', ]); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); const editables = editor.shadowRoot.querySelectorAll('.editable'); await changeEditable(editables[8] as HTMLElement, 'articles/*'); await changeEditable(editables[9] as HTMLElement, 'cache-control'); await changeEditable(editables[10] as HTMLElement, 'max-age=1000'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', 'Apply to:articles/*', 'cache-control:max-age=1000', ]); }); it('allows removing headers', async () => { const editor = await renderEditorWithinWrapper(); await RenderCoordinator.done(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); await pressButton(editor.shadowRoot, 1, '.remove-header'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); let hiddenDeleteElements = await editor.shadowRoot.querySelectorAll('.row.padded > .remove-header[hidden]'); assert.lengthOf(hiddenDeleteElements, 0, 'remove-header button is visible'); await pressButton(editor.shadowRoot, 1, '.remove-header'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'header-name-1:header value', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); hiddenDeleteElements = await editor.shadowRoot.querySelectorAll('.row.padded > .remove-header[hidden]'); assert.lengthOf(hiddenDeleteElements, 1, 'remove-header button is hidden'); }); it('allows removing "ApplyTo"-blocks', async () => { const editor = await renderEditorWithinWrapper(); await RenderCoordinator.done(); assert.isNotNull(editor.shadowRoot); let rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*', 'server:DevTools Unit Test Server', 'access-control-allow-origin:*', 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); await pressButton(editor.shadowRoot, 0, '.remove-block'); rows = getRowContent(editor.shadowRoot); assert.deepEqual(rows, [ 'Apply to:*.jpg', 'jpg-header:only for jpg files', ]); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); }); it('removes formatting for pasted content', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const editables = editor.shadowRoot.querySelectorAll('.editable'); assert.lengthOf(editables, 8); assert.deepEqual(getSingleRowContent(editor.shadowRoot, 2), 'access-control-allow-origin:*'); const headerValue = editables[4] as HTMLSpanElement; headerValue.focus(); const dt = new DataTransfer(); dt.setData('text/plain', 'foo\nbar'); dt.setData('text/html', 'This is <b>bold</b>'); dispatchPasteEvent(headerValue, {clipboardData: dt, bubbles: true}); await RenderCoordinator.done(); assert.deepEqual(getSingleRowContent(editor.shadowRoot, 2), 'access-control-allow-origin:foo bar'); assert.isTrue(recordedMetricsContain( Host.InspectorFrontendHostAPI.EnumeratedHistogram.ActionTaken, Host.UserMetrics.Action.HeaderOverrideHeadersFileEdited)); }); it('shows context menu', async () => { const editor = await renderEditor(); assert.isNotNull(editor.shadowRoot); const contextMenu = getContextMenuForElement(editor); assert.exists(contextMenu); }); });