OSSCA Chromium

기여 목록 / 7969697

[security] Render the origin display with Lit

DevToolsrefactordevtools/devtools-frontendMerged

SecurityOriginView의 security state 아이콘과 Origin URL 표시를 Lit 템플릿으로 전환했습니다.

문제 설명

SecurityOriginView는 security state 아이콘과 URL 표시를 DOM 생성·수정 코드로 구성하고 있었습니다. 이 부분을 Lit 템플릿으로 옮겨 생성 시점과 security state 변경 시점에 같은 렌더링 함수를 사용하도록 했습니다.

해결 내용

  • URL을 Lit 템플릿으로 만드는 renderHighlightedUrl()을 추가했습니다. ://가 없는 URL은 일반 텍스트로, 있는 URL은 security state에 따라 scheme을 강조하도록 했습니다.
  • #renderOriginDisplay()에서 아이콘과 URL을 함께 렌더링하고, 생성자와 setSecurityState()에서 호출하도록 했습니다.
  • SecurityPanelSidebar는 Element를 필요로 하므로, 기존 createHighlightedUrl()은 Lit 결과를 렌더링한 요소를 반환하는 함수로 유지했습니다.
  • URL 표시를 확인하는 단위 테스트 2개와 AUTHORS 항목을 추가했습니다.

전체 코드 diff는 다음과 같습니다.

--- a/front_end/panels/security/SecurityPanel.test.ts
+++ b/front_end/panels/security/SecurityPanel.test.ts
@@ -14,6 +14,32 @@
 import * as Security from './security.js';

 const {urlString} = Platform.DevToolsPath;
+
+describe('createHighlightedUrl', () => {
+  it('renders a URL without a scheme separator as plain text', () => {
+    const highlightedUrl =
+        Security.SecurityPanel.createHighlightedUrl(urlString`foo.bar`, Protocol.Security.SecurityState.Secure);
+
+    assert.strictEqual(highlightedUrl.textContent, 'foo.bar');
+    assert.isFalse(highlightedUrl.classList.contains('highlighted-url'));
+  });
+
+  it('renders a URL with a highlighted scheme', () => {
+    const highlightedUrl =
+        Security.SecurityPanel.createHighlightedUrl(urlString`https://foo.bar`, Protocol.Security.SecurityState.Secure);
+
+    assert.strictEqual(highlightedUrl.textContent, 'https://foo.bar');
+    assert.isTrue(highlightedUrl.classList.contains('highlighted-url'));
+
+    const scheme = highlightedUrl.querySelector('.url-scheme-secure');
+    assert.isNotNull(scheme);
+    assert.strictEqual(scheme.textContent, 'https');
+
+    const schemeSeparator = highlightedUrl.querySelector('.url-scheme-separator');
+    assert.isNotNull(schemeSeparator);
+    assert.strictEqual(schemeSeparator.textContent, '://');
+  });
+});

 describeWithEnvironment('SecurityPanelSidebarTree', () => {
   describe('updateOrigin', () => {
--- a/AUTHORS
+++ b/AUTHORS
@@ -49,6 +49,7 @@
 Han Xu <suyanhanx@gmail.com>
 Helmut Januschka <helmut@januschka.com>
 Henry Lim <henry@limhenry.xyz>
+Hojeong Park <parkhj062@gmail.com>
 Jacky Hu <flameddd@gmail.com>
 Jake Mulhern <mulherje@gmail.com>
 Jan Keitel <jan.keitel@gmail.com>
--- a/front_end/panels/security/SecurityPanel.ts
+++ b/front_end/panels/security/SecurityPanel.ts
@@ -12,7 +12,7 @@
 import * as NetworkForward from '../../panels/network/forward/forward.js';
 import {createIcon, type Icon} from '../../ui/kit/kit.js';
 import * as UI from '../../ui/legacy/legacy.js';
-import {html, render} from '../../ui/lit/lit.js';
+import {html, render, type TemplateResult} from '../../ui/lit/lit.js';
 import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

 import lockIconStyles from './lockIcon.css.js';
@@ -510,26 +510,31 @@
   return createIcon(iconName, className);
 }

-export function createHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): Element {
+function renderHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): TemplateResult {
   const schemeSeparator = '://';
   const index = url.indexOf(schemeSeparator);

   // If the separator is not found, just display the text without highlighting.
   if (index === -1) {
-    const text = document.createElement('span');
-    text.textContent = url;
-    return text;
-  }
-
-  const highlightedUrl = document.createElement('span');
-  highlightedUrl.classList.add('highlighted-url');
+    return html`<span>${url}</span>`;
+  }
+
   const scheme = url.substr(0, index);
   const content = url.substr(index + schemeSeparator.length);
-  highlightedUrl.createChild('span', 'url-scheme-' + securityState).textContent = scheme;
-  highlightedUrl.createChild('span', 'url-scheme-separator').textContent = schemeSeparator;
-  highlightedUrl.createChild('span').textContent = content;
-
-  return highlightedUrl;
+
+  return html`
+    <span class="highlighted-url">
+      <span class=${`url-scheme-${securityState}`}>${scheme}</span>
+      <span class="url-scheme-separator">${schemeSeparator}</span>
+      <span>${content}</span>
+    </span>`;
+}
+
+export function createHighlightedUrl(url: Platform.DevToolsPath.UrlString, securityState: string): Element {
+  const fragment = document.createDocumentFragment();
+  // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
+  render(renderHighlightedUrl(url, securityState), fragment);
+  return fragment.firstElementChild as Element;
 }

 export interface ViewInput {
@@ -1315,11 +1320,14 @@
 }

 export class SecurityOriginView extends UI.Widget.VBox {
-  private readonly originLockIcon: HTMLElement;
+  readonly #origin: Platform.DevToolsPath.UrlString;
+  readonly #originDisplay: HTMLElement;
+
   constructor(origin: Platform.DevToolsPath.UrlString, originState: OriginState) {
     super({jslog: `${VisualLogging.pane('security.origin-view')}`});
     this.registerRequiredCSS(originViewStyles, lockIconStyles);
     this.setMinimumSize(200, 100);
+    this.#origin = origin;

     this.element.classList.add('security-origin-view');

@@ -1328,13 +1336,8 @@
     titleDiv.textContent = i18nString(UIStrings.origin);
     UI.ARIAUtils.markAsHeading(titleDiv, 1);

-    const originDisplay = titleSection.createChild('div', 'origin-display');
-    this.originLockIcon = originDisplay.createChild('span');
-    const icon = getSecurityStateIconForDetailedView(
-        originState.securityState, `security-property security-property-${originState.securityState}`);
-    this.originLockIcon.appendChild(icon);
-
-    originDisplay.appendChild(createHighlightedUrl(origin, originState.securityState));
+    this.#originDisplay = titleSection.createChild('div', 'origin-display');
+    this.#renderOriginDisplay(originState.securityState);

     const originNetworkDiv = titleSection.createChild('div', 'view-network-button');
     const originNetworkButton = UI.UIUtils.createTextButton(i18nString(UIStrings.viewRequestsInNetworkPanel), event => {
@@ -1573,10 +1576,20 @@
   }

   setSecurityState(newSecurityState: Protocol.Security.SecurityState): void {
-    this.originLockIcon.removeChildren();
-    const icon = getSecurityStateIconForDetailedView(
-        newSecurityState, `security-property security-property-${newSecurityState}`);
-    this.originLockIcon.appendChild(icon);
+    this.#renderOriginDisplay(newSecurityState);
+  }
+
+  #renderOriginDisplay(securityState: Protocol.Security.SecurityState): void {
+    const icon =
+        getSecurityStateIconForDetailedView(securityState, `security-property security-property-${securityState}`);
+
+    // clang-format off
+    // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
+    render(html`
+      ${icon}
+      ${renderHighlightedUrl(this.#origin, securityState)}
+    `, this.#originDisplay);
+    // clang-format on
   }
 }

테스트 방법

SecurityPanel.test.ts에 다음 두 단위 테스트를 추가했습니다.

  • foo.bar처럼 ://가 없는 URL은 강조 없이 표시되는지 확인
  • https://foo.bar는 URL 텍스트와 scheme의 강조 클래스, :// 표시가 올바른지 확인

리뷰 과정에서 DOM 구조를 출력하는 Chromium 웹 테스트의 실패를 확인했습니다. 리뷰어 안내에 따라 CL 8038580에서 기대값을 임시 조정하고, 변경이 Chromium에 반영된 뒤 CL 8042189에서 기대 결과를 갱신했습니다. 이 CL은 CQ를 거쳐 2026년 7월 4일(한국 시간) DevTools main에 병합되었습니다.

배운 점

  • Lit 전환 시 호출부가 템플릿과 DOM 요소 중 무엇을 필요로 하는지 확인해야 합니다. Origin 표시에는 템플릿을 직접 사용하고, Sidebar에는 요소를 반환하는 함수를 유지했습니다.
  • DOM을 출력하는 테스트는 Lit이 추가하는 주석 노드나 요소 구조 변경에도 영향을 받으므로, Chromium 쪽 기대 결과도 함께 확인해야 합니다.

참고 자료