기여 목록 / 8289248
[security] Migrate SecurityOriginView title section to Lit
DevToolsrefactordevtools/devtools-frontendMerged
SecurityOriginView의 title section을 Lit으로 전환했습니다. 렌더링, security state 갱신,
Network panel에서 해당 Origin의 요청을 표시하는 동작의 테스트를 추가했습니다.
문제 설명
SecurityOriginView의 title section에서는 Origin 아이콘과 URL만 Lit으로
렌더링하고, 제목과 Network panel 버튼은 DOM 생성·수정 코드로 구성하고
있었습니다. 이 영역 전체를 Lit 템플릿으로 전환하는 작업입니다.
해결 내용
renderTitleSection()에서 제목,security state아이콘, URL, Network panel 버튼을 함께 렌더링하도록 했습니다. 생성자와setSecurityState()에서#renderTitleSection()을 호출합니다.- 아이콘 이름을 반환하는 함수를 분리해 Lit 템플릿에서 사용하고, 기존 아이콘 요소 생성 함수도 이 함수를 사용하도록 했습니다.
- Network panel 버튼을 Lit 템플릿에 선언하고, 클릭 처리를
#revealInNetwork로 옮겼습니다. Origin의 host와 scheme을 요청 필터로 전달하는 동작은 유지했습니다. - 제목의
role="heading"과aria-level="1"을 유지했습니다. 리뷰 의견에 따라 버튼의role="link"지정과 이를 확인하던 테스트 assertion은 제거했습니다. title section의 단위 테스트 3개와 테스트에 필요한 빌드 의존성을 추가했습니다.
전체 코드 diff는 다음과 같습니다.
diff --git a/front_end/panels/security/BUILD.gn b/front_end/panels/security/BUILD.gn
index 76d480e..a5e7ef9 100644
--- a/front_end/panels/security/BUILD.gn
+++ b/front_end/panels/security/BUILD.gn
@@ -78,9 +78,11 @@
ts_deps = [
":bundle",
+ "../../core/common:bundle",
"../../core/platform:bundle",
"../../core/sdk:bundle",
"../../generated",
+ "../../panels/network/forward:bundle",
"../../testing",
]
}
diff --git a/front_end/panels/security/SecurityPanel.test.ts b/front_end/panels/security/SecurityPanel.test.ts
index cb5690d..e654871 100644
--- a/front_end/panels/security/SecurityPanel.test.ts
+++ b/front_end/panels/security/SecurityPanel.test.ts
@@ -5,6 +5,7 @@
import {assert} from 'chai';
import sinon from 'sinon';
+import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
@@ -12,6 +13,7 @@
import {createTarget, describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {createNetworkRequest} from '../../testing/NetworkRequestHelpers.js';
import {getMainFrame, navigate} from '../../testing/ResourceTreeHelpers.js';
+import * as NetworkForward from '../network/forward/forward.js';
import * as Security from './security.js';
@@ -191,6 +193,77 @@
});
});
+ describe('title section', () => {
+ it('renders the title, origin, and Network panel button', () => {
+ const origin = urlString`https://foo.bar`;
+ const view = new Security.SecurityPanel.SecurityOriginView(origin, createOriginState());
+
+ assert.isTrue(view.element.classList.contains('security-origin-view'));
+
+ const titleSection = view.element.querySelector('.title-section');
+ assert.instanceOf(titleSection, HTMLElement);
+
+ const title = titleSection.querySelector('.title-section-header');
+ assert.instanceOf(title, HTMLElement);
+ assert.strictEqual(title.textContent, 'Origin');
+ assert.strictEqual(title.getAttribute('role'), 'heading');
+ assert.strictEqual(title.getAttribute('aria-level'), '1');
+
+ const originDisplay = titleSection.querySelector('.origin-display');
+ assert.instanceOf(originDisplay, HTMLElement);
+ assert.strictEqual(originDisplay.textContent, origin);
+
+ const networkButton = titleSection.querySelector('.view-network-button devtools-button');
+ assert.instanceOf(networkButton, HTMLElement);
+ assert.strictEqual(networkButton.textContent, 'View requests in Network panel');
+ });
+
+ it('updates the origin display when the security state changes', () => {
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, createOriginState());
+
+ const initialOriginDisplay = view.element.querySelector('.origin-display');
+ assert.instanceOf(initialOriginDisplay, HTMLElement);
+
+ const initialIcon = initialOriginDisplay.querySelector('devtools-icon');
+ assert.instanceOf(initialIcon, HTMLElement);
+ assert.strictEqual(initialIcon.getAttribute('name'), 'lock');
+ assert.isTrue(initialIcon.classList.contains('security-property-secure'));
+
+ assert.exists(initialOriginDisplay.querySelector('.url-scheme-secure'));
+
+ view.setSecurityState(Protocol.Security.SecurityState.Insecure);
+
+ const updatedOriginDisplay = view.element.querySelector('.origin-display');
+ assert.instanceOf(updatedOriginDisplay, HTMLElement);
+
+ const updatedIcon = updatedOriginDisplay.querySelector('devtools-icon');
+ assert.instanceOf(updatedIcon, HTMLElement);
+ assert.strictEqual(updatedIcon.getAttribute('name'), 'warning');
+ assert.isTrue(updatedIcon.classList.contains('security-property-insecure'));
+ assert.isFalse(updatedIcon.classList.contains('security-property-secure'));
+
+ assert.exists(updatedOriginDisplay.querySelector('.url-scheme-insecure'));
+ assert.notExists(updatedOriginDisplay.querySelector('.url-scheme-secure'));
+ });
+
+ it('reveals requests in the Network panel', () => {
+ const revealStub = sinon.stub(Common.Revealer.RevealerRegistry.instance(), 'reveal').resolves();
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, createOriginState());
+
+ const networkButton = view.element.querySelector('.view-network-button devtools-button');
+ assert.instanceOf(networkButton, HTMLElement);
+ networkButton.click();
+
+ sinon.assert.calledOnce(revealStub);
+ const [requestFilter] = revealStub.firstCall.args;
+ assert.instanceOf(requestFilter, NetworkForward.UIFilter.UIRequestFilter);
+ assert.deepEqual(requestFilter.filters, [
+ {filterType: NetworkForward.UIFilter.FilterType.Domain, filterValue: 'foo.bar'},
+ {filterType: NetworkForward.UIFilter.FilterType.Scheme, filterValue: 'https'},
+ ]);
+ });
+ });
+
it('renders an empty SAN', () => {
const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, createOriginState());
diff --git a/front_end/panels/security/SecurityPanel.ts b/front_end/panels/security/SecurityPanel.ts
index b6292dd..0ed93e9 100644
--- a/front_end/panels/security/SecurityPanel.ts
+++ b/front_end/panels/security/SecurityPanel.ts
@@ -459,8 +459,7 @@
const WARNING_ICON_NAME = 'warning';
const UNKNOWN_ICON_NAME = 'indeterminate-question-box';
-export function getSecurityStateIconForDetailedView(
- securityState: Protocol.Security.SecurityState, className: string): Icon {
+function getSecurityStateIconNameForDetailedView(securityState: Protocol.Security.SecurityState): string {
let iconName: string;
switch (securityState) {
@@ -478,7 +477,12 @@
break;
}
- return createIcon(iconName, className);
+ return iconName;
+}
+
+export function getSecurityStateIconForDetailedView(securityState: Protocol.Security.SecurityState,
+ className: string): Icon {
+ return createIcon(getSecurityStateIconNameForDetailedView(securityState), className);
}
export function getSecurityStateIconForOverview(
@@ -1426,9 +1430,30 @@
// clang-format on
}
+function renderTitleSection(origin: Platform.DevToolsPath.UrlString, securityState: Protocol.Security.SecurityState,
+ onRevealInNetwork: (event: Event) => void): TemplateResult {
+ // clang-format off
+ return html`
+ <div class="title-section-header" role="heading" aria-level="1">${i18nString(UIStrings.origin)}</div>
+ <div class="origin-display">
+ <devtools-icon
+ name=${getSecurityStateIconNameForDetailedView(securityState)}
+ class=${`security-property security-property-${securityState}`}>
+ </devtools-icon>
+ ${renderHighlightedUrl(origin, securityState)}
+ </div>
+ <div class="view-network-button">
+ <devtools-button
+ .variant=${Buttons.Button.Variant.OUTLINED}
+ .jslogContext=${'reveal-in-network'}
+ @click=${onRevealInNetwork}>${i18nString(UIStrings.viewRequestsInNetworkPanel)}</devtools-button>
+ </div>`;
+ // clang-format on
+}
+
export class SecurityOriginView extends UI.Widget.VBox {
readonly #origin: Platform.DevToolsPath.UrlString;
- readonly #originDisplay: HTMLElement;
+ readonly #titleSection: HTMLElement;
constructor(origin: Platform.DevToolsPath.UrlString, originState: OriginState) {
super({jslog: `${VisualLogging.pane('security.origin-view')}`});
@@ -1438,25 +1463,8 @@
this.element.classList.add('security-origin-view');
- const titleSection = this.element.createChild('div', 'title-section');
- const titleDiv = titleSection.createChild('div', 'title-section-header');
- titleDiv.textContent = i18nString(UIStrings.origin);
- UI.ARIAUtils.markAsHeading(titleDiv, 1);
-
- 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 => {
- event.consume();
- const parsedURL = new Common.ParsedURL.ParsedURL(origin);
- void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
- {filterType: NetworkForward.UIFilter.FilterType.Domain, filterValue: parsedURL.host},
- {filterType: NetworkForward.UIFilter.FilterType.Scheme, filterValue: parsedURL.scheme},
- ]));
- }, {jslogContext: 'reveal-in-network'});
- originNetworkDiv.appendChild(originNetworkButton);
- UI.ARIAUtils.markAsLink(originNetworkButton);
+ this.#titleSection = this.element.createChild('div', 'title-section');
+ this.#renderTitleSection(originState.securityState);
if (originState.securityDetails) {
const connectionSection = this.element.createChild('div', 'origin-view-section connection-section');
@@ -1612,21 +1620,22 @@
}
setSecurityState(newSecurityState: Protocol.Security.SecurityState): void {
- this.#renderOriginDisplay(newSecurityState);
+ this.#renderTitleSection(newSecurityState);
}
- #renderOriginDisplay(securityState: Protocol.Security.SecurityState): void {
- const icon =
- getSecurityStateIconForDetailedView(securityState, `security-property security-property-${securityState}`);
-
- // clang-format off
+ #renderTitleSection(securityState: Protocol.Security.SecurityState): void {
// eslint-disable-next-line @devtools/no-lit-render-outside-of-view
- render(html`
- ${icon}
- ${renderHighlightedUrl(this.#origin, securityState)}
- `, this.#originDisplay);
- // clang-format on
+ render(renderTitleSection(this.#origin, securityState, this.#revealInNetwork), this.#titleSection);
}
+
+ #revealInNetwork = (event: Event): void => {
+ event.consume();
+ const parsedURL = new Common.ParsedURL.ParsedURL(this.#origin);
+ void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
+ {filterType: NetworkForward.UIFilter.FilterType.Domain, filterValue: parsedURL.host},
+ {filterType: NetworkForward.UIFilter.FilterType.Scheme, filterValue: parsedURL.scheme},
+ ]));
+ };
}
export class SecurityDetailsTable {테스트 방법
SecurityPanel.test.ts에 다음 세 단위 테스트를 추가했습니다.
- 제목과 제목의 접근성 속성, Origin URL, Network panel 버튼 문구 확인
security state를Secure에서Insecure로 바꾸면 아이콘과 URL scheme의 클래스가 갱신되는지 확인- 버튼 클릭 시 Revealer가 한 번 호출되고,
foo.bar와https가 각각 Domain과 Scheme 필터로 전달되는지 확인
리뷰 중 Chromium 보안 웹 테스트 4개의 실패가 보고되어, CL 8297118에서 기대값을 임시 조정했습니다. 최종 patchset의 CQ 실행은 통과했으며, 2026년 9월 10일(한국 시간) DevTools main에 병합되었습니다.
배운 점
- 일부만 Lit으로 전환된 영역을 확장할 때는 초기 렌더링뿐 아니라 상태 변경과 버튼 클릭 동작도 함께 검증해야 합니다.
- 커스텀 버튼의 접근성 처리는 바깥 요소와 내부의 실제 버튼을 구분해서
확인해야 합니다. 이 CL에서는 리뷰를 통해
role="link"지정을 제거했습니다.