diff --git a/front_end/panels/application/preloading/PreloadingView.ts b/front_end/panels/application/preloading/PreloadingView.ts index e56142c72a5..feeab636150 100644 --- a/front_end/panels/application/preloading/PreloadingView.ts +++ b/front_end/panels/application/preloading/PreloadingView.ts @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import type * as Common from '../../../core/common/common.js'; +import * as Common from '../../../core/common/common.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import {assertNotNullOrUndefined} from '../../../core/platform/platform.js'; 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 Buttons from '../../../ui/components/buttons/buttons.js'; import type * as DataGrid from '../../../ui/components/data_grid/data_grid.js'; import * as SplitView from '../../../ui/components/split_view/split_view.js'; // eslint-disable-next-line rulesdir/es_modules_import @@ -72,6 +73,10 @@ const UIStrings = { *@description Text in grid and details: Preloading failed. */ statusFailure: 'Failure', + /** + *@description Text to pretty print a file + */ + prettyPrint: 'Pretty print', }; const str_ = i18n.i18n.registerUIStrings('panels/application/preloading/PreloadingView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); @@ -179,6 +184,8 @@ export class PreloadingRuleSetView extends UI.Widget.VBox { private readonly ruleSetGrid = new PreloadingComponents.RuleSetGrid.RuleSetGrid(); private readonly ruleSetDetails = new PreloadingComponents.RuleSetDetailsView.RuleSetDetailsView(); + private shouldPrettyPrint = Common.Settings.Settings.instance().moduleSetting('auto-pretty-print-minified').get(); + constructor(model: SDK.PreloadingModel.PreloadingModel) { super(/* isWebComponent */ true, /* delegatesFocus */ false); @@ -209,9 +216,16 @@ export class PreloadingRuleSetView extends UI.Widget.VBox { this.warningsView.show(this.warningsContainer); this.ruleSetGrid.addEventListener('cellfocused', this.onRuleSetsGridCellFocused.bind(this)); + + const onPrettyPrintToggle = (): void => { + this.shouldPrettyPrint = !this.shouldPrettyPrint; + this.updateRuleSetDetails(); + }; + LitHtml.render( LitHtml.html` - <${SplitView.SplitView.SplitView.litTagName} .horizontal=${true} style="--min-sidebar-size: 0px"> + <${SplitView.SplitView.SplitView.litTagName} .horizontal=${ + true} style="--min-sidebar-size: max(100vh-200px, 0px)">
${this.ruleSetGrid}
@@ -219,7 +233,21 @@ export class PreloadingRuleSetView extends UI.Widget.VBox { jslog=${VisualLogging.section('rule-set-details')}> ${this.ruleSetDetails} - `, + +
+ <${Buttons.Button.Button.litTagName} + .iconName=${'brackets'} + .toggledIconName=${'brackets'} + .toggled=${this.shouldPrettyPrint} + .toggleType=${Buttons.Button.ToggleType.PRIMARY} + .title=${i18nString(UIStrings.prettyPrint)} + .variant=${Buttons.Button.Variant.ICON_TOGGLE} + .size=${Buttons.Button.Size.SMALL} + @click=${onPrettyPrintToggle} + jslog=${VisualLogging.action().track({click: true}).context('preloading-status-panel-pretty-print')}> + +
+ `, this.contentElement, {host: this}); this.hsplit = this.contentElement.querySelector('devtools-split-view') as SplitView.SplitView.SplitView; } @@ -249,6 +277,7 @@ export class PreloadingRuleSetView extends UI.Widget.VBox { private updateRuleSetDetails(): void { const id = this.focusedRuleSetId; const ruleSet = id === null ? null : this.model.getRuleSetById(id); + this.ruleSetDetails.shouldPrettyPrint = this.shouldPrettyPrint; this.ruleSetDetails.data = ruleSet; if (ruleSet === null) { diff --git a/front_end/panels/application/preloading/components/RuleSetDetailsView.test.ts b/front_end/panels/application/preloading/components/RuleSetDetailsView.test.ts index e000e3b1826..85ff904906d 100644 --- a/front_end/panels/application/preloading/components/RuleSetDetailsView.test.ts +++ b/front_end/panels/application/preloading/components/RuleSetDetailsView.test.ts @@ -14,9 +14,11 @@ import * as PreloadingComponents from './components.js'; const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance(); -async function renderRuleSetDetailsView(data: PreloadingComponents.RuleSetDetailsView.RuleSetDetailsViewData): - Promise { +async function renderRuleSetDetailsView( + data: PreloadingComponents.RuleSetDetailsView.RuleSetDetailsViewData, + shouldPrettyPrint: boolean): Promise { const component = new PreloadingComponents.RuleSetDetailsView.RuleSetDetailsView(); + component.shouldPrettyPrint = shouldPrettyPrint; component.data = data; renderElementIntoDOM(component); assert.isNotNull(component.shadowRoot); @@ -29,7 +31,8 @@ describeWithEnvironment('RuleSetDetailsView', () => { it('renders nothing if not selected', async () => { const data = null; - const component = await renderRuleSetDetailsView(data); + const component = await renderRuleSetDetailsView(data, false); + await coordinator.done({waitForWork: true}); assert.isNotNull(component.shadowRoot); assert.strictEqual(component.shadowRoot.textContent, ''); }); @@ -50,7 +53,8 @@ describeWithEnvironment('RuleSetDetailsView', () => { `, backendNodeId: 1 as Protocol.DOM.BackendNodeId, }; - const component = await renderRuleSetDetailsView(data); + const component = await renderRuleSetDetailsView(data, false); + await coordinator.done({waitForWork: true}); assert.deepEqual(component.shadowRoot?.getElementById('error-message-text')?.textContent, undefined); const textEditor = component.shadowRoot?.querySelector('devtools-text-editor') as TextEditor.TextEditor.TextEditor; @@ -74,7 +78,8 @@ describeWithEnvironment('RuleSetDetailsView', () => { url: 'https://example.com/speculationrules.json', requestId: 'reqeustId' as Protocol.Network.RequestId, }; - const component = await renderRuleSetDetailsView(data); + const component = await renderRuleSetDetailsView(data, false); + await coordinator.done({waitForWork: true}); assert.deepEqual(component.shadowRoot?.getElementById('error-message-text')?.textContent, undefined); const textEditor = component.shadowRoot?.querySelector('devtools-text-editor') as TextEditor.TextEditor.TextEditor; assert.strictEqual(textEditor.state.doc.toString(), data.sourceText); @@ -94,7 +99,8 @@ describeWithEnvironment('RuleSetDetailsView', () => { errorType: Protocol.Preload.RuleSetErrorType.SourceIsNotJsonObject, errorMessage: 'Line: 6, column: 1, Syntax error.', }; - const component = await renderRuleSetDetailsView(data); + const component = await renderRuleSetDetailsView(data, false); + await coordinator.done({waitForWork: true}); assert.deepEqual( component.shadowRoot?.getElementById('error-message-text')?.textContent, 'Line: 6, column: 1, Syntax error.'); const textEditor = component.shadowRoot?.querySelector('devtools-text-editor') as TextEditor.TextEditor.TextEditor; @@ -118,11 +124,38 @@ describeWithEnvironment('RuleSetDetailsView', () => { errorType: Protocol.Preload.RuleSetErrorType.InvalidRulesSkipped, errorMessage: 'A list rule must have a "urls" array.', }; - const component = await renderRuleSetDetailsView(data); + const component = await renderRuleSetDetailsView(data, false); + await coordinator.done({waitForWork: true}); assert.deepEqual( component.shadowRoot?.getElementById('error-message-text')?.textContent, 'A list rule must have a "urls" array.'); const textEditor = component.shadowRoot?.querySelector('devtools-text-editor') as TextEditor.TextEditor.TextEditor; assert.strictEqual(textEditor.state.doc.toString(), data.sourceText); }); + + it('renders formatted rule set', async () => { + const data: Protocol.Preload.RuleSet = { + id: 'ruleSetId:1' as Protocol.Preload.RuleSetId, + loaderId: 'loaderId:1' as Protocol.Network.LoaderId, + sourceText: '{"prefetch":[{"source": "list","urls": ["/subresource.js"]}]}', + backendNodeId: 1 as Protocol.DOM.BackendNodeId, + }; + const component = await renderRuleSetDetailsView(data, true); + await coordinator.done({waitForWork: true}); + assert.deepEqual(component.shadowRoot?.getElementById('error-message-text')?.textContent, undefined); + + const textEditor = component.shadowRoot?.querySelector('devtools-text-editor') as TextEditor.TextEditor.TextEditor; + // Formatted sourceText should be different from the data.sourceText in this case. + assert.notEqual(textEditor.state.doc.toString(), data.sourceText); + assert.strictEqual(textEditor.state.doc.toString(), `{ + "prefetch": [ + { + "source": "list", + "urls": [ + "/subresource.js" + ] + } + ] +}`); + }); }); diff --git a/front_end/panels/application/preloading/components/RuleSetDetailsView.ts b/front_end/panels/application/preloading/components/RuleSetDetailsView.ts index 087be71f72f..45ace2acac0 100644 --- a/front_end/panels/application/preloading/components/RuleSetDetailsView.ts +++ b/front_end/panels/application/preloading/components/RuleSetDetailsView.ts @@ -5,6 +5,7 @@ import {assertNotNullOrUndefined} from '../../../../core/platform/platform.js'; import * as SDK from '../../../../core/sdk/sdk.js'; import type * as Protocol from '../../../../generated/protocol.js'; +import * as Formatter from '../../../../models/formatter/formatter.js'; import * as CodeMirror from '../../../../third_party/codemirror.next/codemirror.next.js'; import * as CodeHighlighter from '../../../../ui/components/code_highlighter/code_highlighter.js'; import * as IconButton from '../../../../ui/components/icon_button/icon_button.js'; @@ -26,6 +27,7 @@ export type RuleSetDetailsViewData = RuleSet|null; export class RuleSetDetailsView extends LegacyWrapper.LegacyWrapper.WrappableComponent { readonly #shadow = this.attachShadow({mode: 'open'}); #data: RuleSetDetailsViewData = null; + #shouldPrettyPrint: boolean = true; #editorState?: CodeMirror.EditorState; connectedCallback(): void { @@ -37,7 +39,13 @@ export class RuleSetDetailsView extends LegacyWrapper.LegacyWrapper.WrappableCom void this.#render(); } + set shouldPrettyPrint(shouldPrettyPrint: boolean) { + this.#shouldPrettyPrint = shouldPrettyPrint; + } + async #render(): Promise { + const sourceText = await this.#getSourceText(); + await coordinator.write('RuleSetDetailsView render', () => { if (this.#data === null) { LitHtml.render(LitHtml.nothing, this.#shadow, {host: this}); @@ -52,7 +60,7 @@ export class RuleSetDetailsView extends LegacyWrapper.LegacyWrapper.WrappableCom ${this.#maybeError()}
- ${this.#renderSource()} + ${this.#renderSource(sourceText)}
`, this.#shadow, {host: this}); // clang-format on @@ -85,28 +93,37 @@ export class RuleSetDetailsView extends LegacyWrapper.LegacyWrapper.WrappableCom // clang-format on } - #renderSource(): LitHtml.LitTemplate { + #renderSource(sourceText: string): LitHtml.LitTemplate { this.#editorState = CodeMirror.EditorState.create({ - doc: this.#data?.sourceText, + doc: sourceText, extensions: [ - TextEditor.Config.baseConfiguration(this.#data?.sourceText || ''), + TextEditor.Config.baseConfiguration(sourceText || ''), CodeMirror.lineNumbers(), CodeMirror.EditorState.readOnly.of(true), codeMirrorJsonType as CodeMirror.Extension, CodeMirror.syntaxHighlighting(CodeHighlighter.CodeHighlighter.highlightStyle), ], }); - // Disabled until https://crbug.com/1079231 is fixed. // clang-format off // TODO(https://crbug.com/1425354): Add Raw button. - return LitHtml.html` - <${TextEditor.TextEditor.TextEditor.litTagName} .style.flexGrow = '1' .state=${ - this.#editorState - }> - `; + return LitHtml.html` + <${TextEditor.TextEditor.TextEditor.litTagName} .style.flexGrow = '1' .state=${ + this.#editorState + }> + `; // clang-format on } + + async #getSourceText(): Promise { + if (this.#shouldPrettyPrint && this.#data?.sourceText !== undefined) { + const formattedResult = + await Formatter.ScriptFormatter.formatScriptContent('application/json', this.#data.sourceText); + return formattedResult.formattedContent; + } + + return this.#data?.sourceText || ''; + } } customElements.define('devtools-resources-rulesets-details-view', RuleSetDetailsView);