Split RequestHeadersView into multiple components. [2/3]

This stack of CLs extracts multiple separate components from the
`RequestHeadersView`. The existing logic and UI stay almost unchanged.

Part 2/3:
RequestHeaderSection: contains the logic for request headers, renders
multiple HeaderSectionRows

Bug: 1297533
Change-Id: If28c6c309220cc9513e5e7e040744b36bf899e41
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3870838
Commit-Queue: Wolfgang Beyer <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni
index 91de418..98bf8ac 100644
--- a/config/gni/devtools_grd_files.gni
+++ b/config/gni/devtools_grd_files.gni
@@ -1140,6 +1140,8 @@
   "front_end/panels/network/blockedURLsPane.css.js",
   "front_end/panels/network/components/HeaderSectionRow.css.js",
   "front_end/panels/network/components/HeaderSectionRow.js",
+  "front_end/panels/network/components/RequestHeaderSection.css.js",
+  "front_end/panels/network/components/RequestHeaderSection.js",
   "front_end/panels/network/components/RequestHeadersView.css.js",
   "front_end/panels/network/components/RequestHeadersView.js",
   "front_end/panels/network/components/RequestTrustTokensView.css.js",
diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json
index 1023a50..7e2699e 100644
--- a/front_end/core/i18n/locales/en-US.json
+++ b/front_end/core/i18n/locales/en-US.json
@@ -6869,6 +6869,18 @@
   "panels/network/components/HeaderSectionRow.ts | learnMoreInTheIssuesTab": {
     "message": "Learn more in the issues tab"
   },
+  "panels/network/components/RequestHeaderSection.ts | learnMore": {
+    "message": "Learn more"
+  },
+  "panels/network/components/RequestHeaderSection.ts | onlyProvisionalHeadersAre": {
+    "message": "Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers."
+  },
+  "panels/network/components/RequestHeaderSection.ts | provisionalHeadersAreShown": {
+    "message": "Provisional headers are shown."
+  },
+  "panels/network/components/RequestHeaderSection.ts | provisionalHeadersAreShownDisableCache": {
+    "message": "Provisional headers are shown. Disable cache to see full headers."
+  },
   "panels/network/components/RequestHeadersView.ts | chooseThisOptionIfTheResourceAnd": {
     "message": "Choose this option if the resource and the document are served from the same site."
   },
@@ -6896,21 +6908,9 @@
   "panels/network/components/RequestHeadersView.ts | headerOverrides": {
     "message": "Header overrides"
   },
-  "panels/network/components/RequestHeadersView.ts | learnMore": {
-    "message": "Learn more"
-  },
   "panels/network/components/RequestHeadersView.ts | onlyChooseThisOptionIfAn": {
     "message": "Only choose this option if an arbitrary website including this resource does not impose a security risk."
   },
-  "panels/network/components/RequestHeadersView.ts | onlyProvisionalHeadersAre": {
-    "message": "Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers."
-  },
-  "panels/network/components/RequestHeadersView.ts | provisionalHeadersAreShown": {
-    "message": "Provisional headers are shown."
-  },
-  "panels/network/components/RequestHeadersView.ts | provisionalHeadersAreShownDisableCache": {
-    "message": "Provisional headers are shown. Disable cache to see full headers."
-  },
   "panels/network/components/RequestHeadersView.ts | raw": {
     "message": "Raw"
   },
diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json
index 7044e25..1e74947 100644
--- a/front_end/core/i18n/locales/en-XL.json
+++ b/front_end/core/i18n/locales/en-XL.json
@@ -6869,6 +6869,18 @@
   "panels/network/components/HeaderSectionRow.ts | learnMoreInTheIssuesTab": {
     "message": "L̂éâŕn̂ ḿôŕê ín̂ t́ĥé îśŝúêś t̂áb̂"
   },
+  "panels/network/components/RequestHeaderSection.ts | learnMore": {
+    "message": "L̂éâŕn̂ ḿôŕê"
+  },
+  "panels/network/components/RequestHeaderSection.ts | onlyProvisionalHeadersAre": {
+    "message": "Ôńl̂ý p̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê áv̂áîĺâb́l̂é b̂éĉáûśê t́ĥíŝ ŕêq́ûéŝt́ ŵáŝ ńôt́ ŝén̂t́ ôv́êŕ t̂h́ê ńêt́ŵór̂ḱ âńd̂ ín̂śt̂éâd́ ŵáŝ śêŕv̂éd̂ f́r̂óm̂ á l̂óĉál̂ ćâćĥé, ŵh́îćĥ d́ôéŝń’t̂ śt̂ór̂é t̂h́ê ór̂íĝín̂ál̂ ŕêq́ûéŝt́ ĥéâd́êŕŝ. D́îśâb́l̂é ĉáĉh́ê t́ô śêé f̂úl̂ĺ r̂éq̂úêśt̂ h́êád̂ér̂ś."
+  },
+  "panels/network/components/RequestHeaderSection.ts | provisionalHeadersAreShown": {
+    "message": "P̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê śĥóŵń."
+  },
+  "panels/network/components/RequestHeaderSection.ts | provisionalHeadersAreShownDisableCache": {
+    "message": "P̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê śĥóŵń. D̂íŝáb̂ĺê ćâćĥé t̂ó ŝéê f́ûĺl̂ h́êád̂ér̂ś."
+  },
   "panels/network/components/RequestHeadersView.ts | chooseThisOptionIfTheResourceAnd": {
     "message": "Ĉh́ôóŝé t̂h́îś ôṕt̂íôń îf́ t̂h́ê ŕêśôúr̂ćê án̂d́ t̂h́ê d́ôćûḿêńt̂ ár̂é ŝér̂v́êd́ f̂ŕôḿ t̂h́ê śâḿê śît́ê."
   },
@@ -6896,21 +6908,9 @@
   "panels/network/components/RequestHeadersView.ts | headerOverrides": {
     "message": "Ĥéâd́êŕ ôv́êŕr̂íd̂éŝ"
   },
-  "panels/network/components/RequestHeadersView.ts | learnMore": {
-    "message": "L̂éâŕn̂ ḿôŕê"
-  },
   "panels/network/components/RequestHeadersView.ts | onlyChooseThisOptionIfAn": {
     "message": "Ôńl̂ý ĉh́ôóŝé t̂h́îś ôṕt̂íôń îf́ âń âŕb̂ít̂ŕâŕŷ ẃêb́ŝít̂é îńĉĺûd́îńĝ t́ĥíŝ ŕêśôúr̂ćê d́ôéŝ ńôt́ îḿp̂óŝé â śêćûŕît́ŷ ŕîśk̂."
   },
-  "panels/network/components/RequestHeadersView.ts | onlyProvisionalHeadersAre": {
-    "message": "Ôńl̂ý p̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê áv̂áîĺâb́l̂é b̂éĉáûśê t́ĥíŝ ŕêq́ûéŝt́ ŵáŝ ńôt́ ŝén̂t́ ôv́êŕ t̂h́ê ńêt́ŵór̂ḱ âńd̂ ín̂śt̂éâd́ ŵáŝ śêŕv̂éd̂ f́r̂óm̂ á l̂óĉál̂ ćâćĥé, ŵh́îćĥ d́ôéŝń’t̂ śt̂ór̂é t̂h́ê ór̂íĝín̂ál̂ ŕêq́ûéŝt́ ĥéâd́êŕŝ. D́îśâb́l̂é ĉáĉh́ê t́ô śêé f̂úl̂ĺ r̂éq̂úêśt̂ h́êád̂ér̂ś."
-  },
-  "panels/network/components/RequestHeadersView.ts | provisionalHeadersAreShown": {
-    "message": "P̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê śĥóŵń."
-  },
-  "panels/network/components/RequestHeadersView.ts | provisionalHeadersAreShownDisableCache": {
-    "message": "P̂ŕôv́îśîón̂ál̂ h́êád̂ér̂ś âŕê śĥóŵń. D̂íŝáb̂ĺê ćâćĥé t̂ó ŝéê f́ûĺl̂ h́êád̂ér̂ś."
-  },
   "panels/network/components/RequestHeadersView.ts | raw": {
     "message": "R̂áŵ"
   },
diff --git a/front_end/panels/network/components/BUILD.gn b/front_end/panels/network/components/BUILD.gn
index 93ad7be..92298a0 100644
--- a/front_end/panels/network/components/BUILD.gn
+++ b/front_end/panels/network/components/BUILD.gn
@@ -10,6 +10,7 @@
 generate_css("css_files") {
   sources = [
     "HeaderSectionRow.css",
+    "RequestHeaderSection.css",
     "RequestHeadersView.css",
     "RequestTrustTokensView.css",
     "WebBundleInfoView.css",
@@ -19,6 +20,7 @@
 devtools_module("components") {
   sources = [
     "HeaderSectionRow.ts",
+    "RequestHeaderSection.ts",
     "RequestHeadersView.ts",
     "RequestTrustTokensView.ts",
     "WebBundleInfoView.ts",
diff --git a/front_end/panels/network/components/RequestHeaderSection.css b/front_end/panels/network/components/RequestHeaderSection.css
new file mode 100644
index 0000000..6d4e98e
--- /dev/null
+++ b/front_end/panels/network/components/RequestHeaderSection.css
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+:host {
+  display: block;
+}
+
+devtools-header-section-row:last-of-type {
+  margin-bottom: 10px;
+}
+
+devtools-header-section-row:first-of-type {
+  margin-top: 2px;
+}
+
+.call-to-action {
+  background-color: var(--color-background-elevation-1);
+  padding: 8px;
+  border-radius: 2px;
+}
+
+.call-to-action-body {
+  padding: 6px 0;
+  margin-left: 9.5px;
+  border-left: 2px solid var(--issue-color-yellow);
+  padding-left: 18px;
+  line-height: 20px;
+}
+
+.call-to-action .explanation {
+  font-weight: bold;
+}
+
+.call-to-action code {
+  font-size: 90%;
+}
+
+.call-to-action .example .comment::before {
+  content: " — ";
+}
+
+.link,
+.devtools-link {
+  color: var(--color-link);
+  text-decoration: underline;
+  cursor: pointer;
+  padding: 2px 0; /* adjust focus ring size */
+}
+
+.explanation .link {
+  font-weight: normal;
+}
+
+.inline-icon {
+  vertical-align: middle;
+}
diff --git a/front_end/panels/network/components/RequestHeaderSection.ts b/front_end/panels/network/components/RequestHeaderSection.ts
new file mode 100644
index 0000000..058939f
--- /dev/null
+++ b/front_end/panels/network/components/RequestHeaderSection.ts
@@ -0,0 +1,133 @@
+// 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 ComponentHelpers from '../../../ui/components/helpers/helpers.js';
+import * as LitHtml from '../../../ui/lit-html/lit-html.js';
+
+import type * as SDK from '../../../core/sdk/sdk.js';
+import * as i18n from '../../../core/i18n/i18n.js';
+import * as NetworkForward from '../forward/forward.js';
+import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
+import * as Platform from '../../../core/platform/platform.js';
+import {type HeaderDescriptor, HeaderSectionRow, type HeaderSectionRowData} from './HeaderSectionRow.js';
+
+import requestHeaderSectionStyles from './RequestHeaderSection.css.js';
+
+const {render, html} = LitHtml;
+
+const UIStrings = {
+  /**
+  *@description Text that is usually a hyperlink to more documentation
+  */
+  learnMore: 'Learn more',
+  /**
+  *@description Message to explain lack of raw headers for a particular network request
+  */
+  provisionalHeadersAreShownDisableCache: 'Provisional headers are shown. Disable cache to see full headers.',
+  /**
+  *@description Tooltip to explain lack of raw headers for a particular network request
+  */
+  onlyProvisionalHeadersAre:
+      'Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers.',
+  /**
+  *@description Message to explain lack of raw headers for a particular network request
+  */
+  provisionalHeadersAreShown: 'Provisional headers are shown.',
+};
+
+const str_ = i18n.i18n.registerUIStrings('panels/network/components/RequestHeaderSection.ts', UIStrings);
+const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
+
+export interface RequestHeaderSectionData {
+  request: SDK.NetworkRequest.NetworkRequest;
+  toReveal?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, header?: string};
+}
+
+export class RequestHeaderSection extends HTMLElement {
+  static readonly litTagName = LitHtml.literal`devtools-request-header-section`;
+  readonly #shadow = this.attachShadow({mode: 'open'});
+  #request?: Readonly<SDK.NetworkRequest.NetworkRequest>;
+  #headers: HeaderDescriptor[] = [];
+
+  connectedCallback(): void {
+    this.#shadow.adoptedStyleSheets = [requestHeaderSectionStyles];
+  }
+
+  set data(data: RequestHeaderSectionData) {
+    this.#request = data.request;
+
+    this.#headers = this.#request.requestHeaders().map(header => ({...header, headerNotSet: false}));
+    this.#headers.sort(function(a, b) {
+      return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase());
+    });
+
+    if (data.toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.Request) {
+      this.#headers.filter(header => header.name.toUpperCase() === data.toReveal?.header?.toUpperCase())
+          .forEach(header => {
+            header.highlight = true;
+          });
+    }
+
+    this.#render();
+  }
+
+  #render(): void {
+    if (!this.#request) {
+      return;
+    }
+
+    // Disabled until https://crbug.com/1079231 is fixed.
+    // clang-format off
+    render(html`
+      ${this.#maybeRenderProvisionalHeadersWarning()}
+      ${this.#headers.map(header => html`
+        <${HeaderSectionRow.litTagName} .data=${{
+          header: header,
+        } as HeaderSectionRowData}></${HeaderSectionRow.litTagName}>
+      `)}
+    `, this.#shadow, {host: this});
+    // clang-format on
+  }
+
+  #maybeRenderProvisionalHeadersWarning(): LitHtml.LitTemplate {
+    if (!this.#request || this.#request.requestHeadersText() !== undefined) {
+      return LitHtml.nothing;
+    }
+
+    let cautionText;
+    let cautionTitle = '';
+    if (this.#request.cachedInMemory() || this.#request.cached()) {
+      cautionText = i18nString(UIStrings.provisionalHeadersAreShownDisableCache);
+      cautionTitle = i18nString(UIStrings.onlyProvisionalHeadersAre);
+    } else {
+      cautionText = i18nString(UIStrings.provisionalHeadersAreShown);
+    }
+    // Disabled until https://crbug.com/1079231 is fixed.
+    // clang-format off
+    return html`
+      <div class="call-to-action">
+        <div class="call-to-action-body">
+          <div class="explanation" title=${cautionTitle}>
+            <${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
+                iconName: 'clear-warning_icon',
+                width: '12px',
+                height: '12px',
+              } as IconButton.Icon.IconData}>
+            </${IconButton.Icon.Icon.litTagName}>
+            ${cautionText} <x-link href="https://developer.chrome.com/docs/devtools/network/reference/#provisional-headers" class="link">${i18nString(UIStrings.learnMore)}</x-link>
+          </div>
+        </div>
+      </div>
+    `;
+    // clang-format on
+  }
+}
+
+ComponentHelpers.CustomElements.defineComponent('devtools-request-header-section', RequestHeaderSection);
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'devtools-request-header-section': RequestHeaderSection;
+  }
+}
diff --git a/front_end/panels/network/components/RequestHeadersView.ts b/front_end/panels/network/components/RequestHeadersView.ts
index ba9dab4..ee24da8 100644
--- a/front_end/panels/network/components/RequestHeadersView.ts
+++ b/front_end/panels/network/components/RequestHeadersView.ts
@@ -21,6 +21,7 @@
 import * as LitHtml from '../../../ui/lit-html/lit-html.js';
 import * as Sources from '../../sources/sources.js';
 import {type HeaderSectionRowData, HeaderSectionRow, type HeaderDescriptor} from './HeaderSectionRow.js';
+import {type RequestHeaderSectionData, RequestHeaderSection} from './RequestHeaderSection.js';
 
 import requestHeadersViewStyles from './RequestHeadersView.css.js';
 
@@ -67,10 +68,6 @@
   */
   headerOverrides: 'Header overrides',
   /**
-  *@description Text that is usually a hyperlink to more documentation
-  */
-  learnMore: 'Learn more',
-  /**
   *@description Label for a checkbox to switch between raw and parsed headers
   */
   raw: 'Raw',
@@ -80,19 +77,6 @@
   onlyChooseThisOptionIfAn:
       'Only choose this option if an arbitrary website including this resource does not impose a security risk.',
   /**
-  *@description Message to explain lack of raw headers for a particular network request
-  */
-  provisionalHeadersAreShownDisableCache: 'Provisional headers are shown. Disable cache to see full headers.',
-  /**
-  *@description Tooltip to explain lack of raw headers for a particular network request
-  */
-  onlyProvisionalHeadersAre:
-      'Only provisional headers are available because this request was not sent over the network and instead was served from a local cache, which doesn’t store the original request headers. Disable cache to see full request headers.',
-  /**
-  *@description Message to explain lack of raw headers for a particular network request
-  */
-  provisionalHeadersAreShown: 'Provisional headers are shown.',
-  /**
   *@description Text in Request Headers View of the Network panel
   */
   referrerPolicy: 'Referrer Policy',
@@ -413,23 +397,7 @@
     if (!this.#request) {
       return LitHtml.nothing;
     }
-
-    const headers: HeaderDescriptor[] =
-        this.#request.requestHeaders().map(header => ({...header, headerNotSet: false}));
-    headers.sort(function(a, b) {
-      return Platform.StringUtilities.compare(a.name.toLowerCase(), b.name.toLowerCase());
-    });
-
-    if (this.#toReveal?.section === NetworkForward.UIRequestLocation.UIHeaderSection.Request) {
-      headers.filter(header => header.name.toUpperCase() === this.#toReveal?.header?.toUpperCase()).forEach(header => {
-        header.highlight = true;
-      });
-    }
-
     const requestHeadersText = this.#request.requestHeadersText();
-    if (!headers.length && requestHeadersText !== undefined) {
-      return LitHtml.nothing;
-    }
 
     const toggleShowRaw = (): void => {
       this.#showRequestHeadersText = !this.#showRequestHeadersText;
@@ -452,51 +420,16 @@
       >
         ${(this.#showRequestHeadersText && requestHeadersText) ?
             this.#renderRawHeaders(requestHeadersText, false) : html`
-          ${this.#maybeRenderProvisionalHeadersWarning()}
-          ${headers.map(header => html`
-            <${HeaderSectionRow.litTagName} .data=${{
-              header: header,
-            } as HeaderSectionRowData}></${HeaderSectionRow.litTagName}>
-          `)}
+          <${RequestHeaderSection.litTagName} .data=${{
+            request: this.#request,
+            toReveal: this.#toReveal,
+          } as RequestHeaderSectionData}></${RequestHeaderSection.litTagName}>
         `}
       </${Category.litTagName}>
     `;
     // clang-format on
   }
 
-  #maybeRenderProvisionalHeadersWarning(): LitHtml.LitTemplate {
-    if (!this.#request || this.#request.requestHeadersText() !== undefined) {
-      return LitHtml.nothing;
-    }
-
-    let cautionText;
-    let cautionTitle = '';
-    if (this.#request.cachedInMemory() || this.#request.cached()) {
-      cautionText = i18nString(UIStrings.provisionalHeadersAreShownDisableCache);
-      cautionTitle = i18nString(UIStrings.onlyProvisionalHeadersAre);
-    } else {
-      cautionText = i18nString(UIStrings.provisionalHeadersAreShown);
-    }
-    // Disabled until https://crbug.com/1079231 is fixed.
-    // clang-format off
-    return html`
-      <div class="call-to-action">
-        <div class="call-to-action-body">
-          <div class="explanation" title=${cautionTitle}>
-            <${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
-                iconName: 'clear-warning_icon',
-                width: '12px',
-                height: '12px',
-              } as IconButton.Icon.IconData}>
-            </${IconButton.Icon.Icon.litTagName}>
-            ${cautionText} <x-link href="https://developer.chrome.com/docs/devtools/network/reference/#provisional-headers" class="link">${i18nString(UIStrings.learnMore)}</x-link>
-          </div>
-        </div>
-      </div>
-    `;
-    // clang-format on
-  }
-
   #renderRawHeaders(rawHeadersText: string, forResponseHeaders: boolean): LitHtml.TemplateResult {
     const trimmed = rawHeadersText.trim();
     const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
diff --git a/front_end/panels/network/components/components.ts b/front_end/panels/network/components/components.ts
index a772dc7..aab9464 100644
--- a/front_end/panels/network/components/components.ts
+++ b/front_end/panels/network/components/components.ts
@@ -3,11 +3,13 @@
 // found in the LICENSE file.
 
 import * as HeaderSectionRow from './HeaderSectionRow.js';
+import * as RequestHeaderSection from './RequestHeaderSection.js';
 import * as RequestHeadersView from './RequestHeadersView.js';
 import * as RequestTrustTokensView from './RequestTrustTokensView.js';
 
 export {
   HeaderSectionRow,
+  RequestHeaderSection,
   RequestHeadersView,
   RequestTrustTokensView,
 };
diff --git a/test/unittests/front_end/panels/network/components/BUILD.gn b/test/unittests/front_end/panels/network/components/BUILD.gn
index 878ad8c..b062bf3 100644
--- a/test/unittests/front_end/panels/network/components/BUILD.gn
+++ b/test/unittests/front_end/panels/network/components/BUILD.gn
@@ -8,6 +8,7 @@
   testonly = true
   sources = [
     "HeaderSectionRow_test.ts",
+    "RequestHeaderSection_test.ts",
     "RequestHeadersView_test.ts",
     "RequestTrustTokensView_test.ts",
   ]
diff --git a/test/unittests/front_end/panels/network/components/RequestHeaderSection_test.ts b/test/unittests/front_end/panels/network/components/RequestHeaderSection_test.ts
new file mode 100644
index 0000000..2299f02
--- /dev/null
+++ b/test/unittests/front_end/panels/network/components/RequestHeaderSection_test.ts
@@ -0,0 +1,85 @@
+// 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 NetworkComponents from '../../../../../../front_end/panels/network/components/components.js';
+import * as Coordinator from '../../../../../../front_end/ui/components/render_coordinator/render_coordinator.js';
+import type * as SDK from '../../../../../../front_end/core/sdk/sdk.js';
+
+import {
+  assertElement,
+  assertShadowRoot,
+  getCleanTextContentFromElements,
+  renderElementIntoDOM,
+} from '../../../helpers/DOMHelpers.js';
+import {describeWithEnvironment} from '../../../helpers/EnvironmentHelpers.js';
+
+const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
+
+const {assert} = chai;
+
+async function renderRequestHeaderSection(request: SDK.NetworkRequest.NetworkRequest):
+    Promise<NetworkComponents.RequestHeaderSection.RequestHeaderSection> {
+  const component = new NetworkComponents.RequestHeaderSection.RequestHeaderSection();
+  renderElementIntoDOM(component);
+  component.data = {request};
+  await coordinator.done();
+  assertElement(component, HTMLElement);
+  assertShadowRoot(component.shadowRoot);
+  return component;
+}
+
+describeWithEnvironment('RequestHeaderSection', () => {
+  it('renders provisional headers warning', async () => {
+    const request = {
+      cachedInMemory: () => true,
+      requestHeaders: () =>
+          [{name: ':method', value: 'GET'},
+           {name: 'accept-encoding', value: 'gzip, deflate, br'},
+           {name: 'cache-control', value: 'no-cache'},
+    ],
+      requestHeadersText: () => undefined,
+    } as unknown as SDK.NetworkRequest.NetworkRequest;
+
+    const component = await renderRequestHeaderSection(request);
+    assertShadowRoot(component.shadowRoot);
+
+    assert.strictEqual(
+        getCleanTextContentFromElements(component.shadowRoot, '.call-to-action')[0],
+        'Provisional headers are shown. Disable cache to see full headers. Learn more',
+    );
+  });
+
+  it('sorts headers alphabetically', async () => {
+    const request = {
+      cachedInMemory: () => true,
+      requestHeaders: () =>
+          [{name: 'Ab', value: 'second'},
+           {name: 'test', value: 'fifth'},
+           {name: 'name', value: 'fourth'},
+           {name: 'abc', value: 'third'},
+           {name: 'aa', value: 'first'},
+    ],
+      requestHeadersText: () => 'placeholderText',
+    } as unknown as SDK.NetworkRequest.NetworkRequest;
+
+    const component = await renderRequestHeaderSection(request);
+    assertShadowRoot(component.shadowRoot);
+
+    const rows = component.shadowRoot.querySelectorAll('devtools-header-section-row');
+    const sorted = Array.from(rows).map(row => {
+      assertShadowRoot(row.shadowRoot);
+      return [
+        row.shadowRoot.querySelector('.header-name')?.textContent?.trim() || '',
+        row.shadowRoot.querySelector('.header-value')?.textContent?.trim() || '',
+      ];
+    });
+    assert.deepStrictEqual(sorted, [
+      ['aa:', 'first'],
+      ['Ab:', 'second'],
+      ['abc:', 'third'],
+      ['name:', 'fourth'],
+      ['test:', 'fifth'],
+    ]);
+  });
+});
diff --git a/test/unittests/front_end/panels/network/components/RequestHeadersView_test.ts b/test/unittests/front_end/panels/network/components/RequestHeadersView_test.ts
index 597855c..ff3fe32 100644
--- a/test/unittests/front_end/panels/network/components/RequestHeadersView_test.ts
+++ b/test/unittests/front_end/panels/network/components/RequestHeadersView_test.ts
@@ -173,22 +173,6 @@
   });
 
   // Test will be refactored in follow-up CL
-  it.skip('[crbug.com/1297533]: renders provisional headers warning', async () => {
-    const component = await renderHeadersComponent({
-      ...defaultRequest,
-      requestHeadersText: () => undefined,
-    } as unknown as SDK.NetworkRequest.NetworkRequest);
-    assertShadowRoot(component.shadowRoot);
-
-    const requestHeadersCategory = component.shadowRoot.querySelector('[aria-label="Request Headers"]');
-    assertElement(requestHeadersCategory, HTMLElement);
-    assert.strictEqual(
-        getCleanTextContentFromElements(requestHeadersCategory, '.call-to-action')[0],
-        'Provisional headers are shown. Disable cache to see full headers. Learn more',
-    );
-  });
-
-  // Test will be refactored in follow-up CL
   it.skip('[crbug.com/1297533]: can switch between source and parsed view', async () => {
     const component = await renderHeadersComponent(defaultRequest);
     assertShadowRoot(component.shadowRoot);
@@ -368,36 +352,6 @@
     const linkElement = responseHeadersCategory.shadowRoot.querySelector('x-link');
     assert.isNull(linkElement);
   });
-
-  it('skips rendering category if there are no headers', async () => {
-    const component = await renderHeadersComponent({
-      ...defaultRequest,
-      sortedResponseHeaders: [],
-      requestHeaders: () => [],
-    } as unknown as SDK.NetworkRequest.NetworkRequest);
-    assertShadowRoot(component.shadowRoot);
-
-    assert.isNull(component.shadowRoot.querySelector('[aria-label="Response Headers"]'));
-    assert.isNull(component.shadowRoot.querySelector('[aria-label="Request Headers"]'));
-  });
-
-  it('renders provisional headers warning for request headers even if there are no headers', async () => {
-    const component = await renderHeadersComponent({
-      ...defaultRequest,
-      sortedResponseHeaders: [],
-      requestHeaders: () => [],
-      requestHeadersText: () => undefined,
-    } as unknown as SDK.NetworkRequest.NetworkRequest);
-    assertShadowRoot(component.shadowRoot);
-
-    assert.isNull(component.shadowRoot.querySelector('[aria-label="Response Headers"]'));
-    const requestHeadersCategory = component.shadowRoot.querySelector('[aria-label="Request Headers"]');
-    assertElement(requestHeadersCategory, HTMLElement);
-    assert.strictEqual(
-        getCleanTextContentFromElements(requestHeadersCategory, '.call-to-action')[0],
-        'Provisional headers are shown. Disable cache to see full headers. Learn more',
-    );
-  });
 });
 
 describeWithEnvironment('RequestHeadersView\'s Category', () => {