기여 목록 / 8451582
[security] Migrate SecurityOriginView note section to Lit
DevToolsrefactordevtools/devtools-frontendMerged
SecurityOriginView의 note section을 Lit으로 전환하고, 표시 조건과 캐시 안내
테스트를 추가했습니다. Certificate Transparency section(CT section)이 표시되지
않을 때 일반 안내 문구도 누락되던 문제를 함께 수정했습니다.
문제 설명
기존 코드는 SCT가 없고 CT compliance가 Unknown이면 생성자에서 바로 반환해,
뒤에 있는 note section도 생성하지 않았습니다. security details가 있어도
첫 번째로 검사한 응답의 정보라는 안내와 캐시 안내가 누락될 수 있었습니다.
해결 내용
renderNoteSection()에서 캐시 안내와 첫 번째로 검사한 응답에 대한 안내를 Lit 템플릿으로 렌더링하도록 했습니다.CT section을 생략할 때 생성자에서 반환하던 코드를 바꿔, 해당 영역만 조건부로 생성하도록 했습니다.security details가 있으면CT section표시 여부와 관계없이 일반 안내 문구를 표시합니다.loadedFromCache가 참이면 캐시 안내를 먼저 표시하고,security details가 없으면note section을 생성하지 않는 조건은 유지했습니다.- 기존 CT 테스트에서
note section이 없다는 검증을 제거하고,note section의 단위 테스트 7개를 추가했습니다.
전체 코드 diff는 다음과 같습니다.
diff --git a/front_end/panels/security/SecurityPanel.test.ts b/front_end/panels/security/SecurityPanel.test.ts
index cbdfe4c..a29ad99 100644
--- a/front_end/panels/security/SecurityPanel.test.ts
+++ b/front_end/panels/security/SecurityPanel.test.ts
@@ -388,7 +388,6 @@
}));
assert.notExists(view.element.querySelector('.certificate-transparency-section'));
- assert.notExists(view.element.querySelector('.origin-view-notes'));
});
it('renders without a note when the SCT list is not empty and compliance is unknown', () => {
@@ -473,6 +472,85 @@
});
});
+ describe('note section', () => {
+ function getNoteTexts(view: Security.SecurityPanel.SecurityOriginView): string[] {
+ const notes = querySelectorErrorOnMissing(view.element, '.origin-view-notes');
+ return Array.from(notes.children, note => note.textContent ?? '');
+ }
+
+ it('renders when security details are available', () => {
+ const originState = createOriginState();
+ originState.securityDetails = {
+ protocol: 'TLS 1.3',
+ keyExchange: '',
+ cipher: 'AES_128_GCM',
+ certificateId: 0 as Protocol.Security.CertificateId,
+ subjectName: 'example.com',
+ sanList: [],
+ issuer: 'Test CA',
+ validFrom: 0,
+ validTo: 1,
+ signedCertificateTimestampList: [],
+ certificateTransparencyCompliance: Protocol.Network.CertificateTransparencyCompliance.Unknown,
+ encryptedClientHello: false,
+ };
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+ assert.deepEqual(getNoteTexts(view), ['The security details above are from the first inspected response.']);
+ });
+
+ it('does not render when security details are null', () => {
+ const originState = createOriginState();
+ originState.securityDetails = null;
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+ assert.notExists(view.element.querySelector('.origin-view-notes'));
+ });
+
+ it('renders with the cache note when loadedFromCache is true', () => {
+ const originState = createOriginState();
+ originState.loadedFromCache = true;
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+ assert.deepEqual(getNoteTexts(view), [
+ 'This response was loaded from cache. Some security details might be missing.',
+ 'The security details above are from the first inspected response.',
+ ]);
+ });
+
+ it('renders when the SCT list is not empty', () => {
+ const originState = createOriginState({
+ signedCertificateTimestampList: [{
+ logDescription: 'Test log',
+ logId: '00',
+ status: 'Verified',
+ origin: 'Embedded in certificate',
+ timestamp: 0,
+ hashAlgorithm: 'SHA-256',
+ signatureAlgorithm: 'ECDSA',
+ signatureData: '00',
+ }],
+ });
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+ assert.exists(view.element.querySelector('.origin-view-notes'));
+ });
+
+ const compliances = [
+ Protocol.Network.CertificateTransparencyCompliance.Unknown,
+ Protocol.Network.CertificateTransparencyCompliance.Compliant,
+ Protocol.Network.CertificateTransparencyCompliance.NotCompliant,
+ ];
+ for (const compliance of compliances) {
+ it(`renders when certificate transparency compliance is ${compliance}`, () => {
+ const originState = createOriginState({certificateTransparencyCompliance: compliance});
+ const view = new Security.SecurityPanel.SecurityOriginView(urlString`https://foo.bar`, originState);
+
+ assert.exists(view.element.querySelector('.origin-view-notes'));
+ });
+ }
+ });
+
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 420c4ef..8f2833b 100644
--- a/front_end/panels/security/SecurityPanel.ts
+++ b/front_end/panels/security/SecurityPanel.ts
@@ -1562,6 +1562,14 @@
// clang-format on
}
+function renderNoteSection(loadedFromCache: boolean): TemplateResult {
+ // clang-format off
+ return html`
+ ${loadedFromCache ? html`<div>${i18nString(UIStrings.thisResponseWasLoadedFromCache)}</div>` : nothing}
+ <div>${i18nString(UIStrings.theSecurityDetailsAboveAreFrom)}</div>`;
+ // clang-format on
+}
+
export class SecurityOriginView extends UI.Widget.VBox {
readonly #origin: Platform.DevToolsPath.UrlString;
readonly #titleSection: HTMLElement;
@@ -1586,16 +1594,13 @@
const sctListLength = originState.securityDetails.signedCertificateTimestampList.length;
const ctCompliance = originState.securityDetails.certificateTransparencyCompliance;
- if (!sctListLength && ctCompliance === Protocol.Network.CertificateTransparencyCompliance.Unknown) {
- return;
+ if (sctListLength || ctCompliance !== Protocol.Network.CertificateTransparencyCompliance.Unknown) {
+ this.#createCertificateTransparencySection(originState.securityDetails);
}
- this.#createCertificateTransparencySection(originState.securityDetails);
const noteSection = this.element.createChild('div', 'origin-view-section origin-view-notes');
- if (originState.loadedFromCache) {
- noteSection.createChild('div').textContent = i18nString(UIStrings.thisResponseWasLoadedFromCache);
- }
- noteSection.createChild('div').textContent = i18nString(UIStrings.theSecurityDetailsAboveAreFrom);
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
+ render(renderNoteSection(originState.loadedFromCache), noteSection);
} else if (originState.securityState === Protocol.Security.SecurityState.Secure) {
// If the security state is secure but there are no security details,
// this means that the origin is a non-cryptographic secure origin, e.g.테스트 방법
SecurityPanel.test.ts에 다음 내용을 검증하는 단위 테스트 7개를 추가했습니다.
security details가 있으면 일반 안내 문구를 표시하고,null이면 영역을 생략하는지 확인- 캐시 응답이면 캐시 안내와 일반 안내를 순서대로 표시하는지 확인
- SCT 목록에 항목이 있으면
note section을 표시하는지 확인 - CT compliance가
Unknown,Compliant,NotCompliant인 세 경우 모두note section을 표시하는지 확인
Chromium 웹 테스트 security-details-updated-with-security-state.js의 기대값은
CL 8458524에서 임시 조정했습니다. 최종 patchset의 CQ 실행이 통과했으며,
2026년 9월 28일(한국 시간) DevTools main에 병합되었습니다.
배운 점
- 한 영역을 생략하기 위한 조기 반환이 뒤에 있는 다른 영역까지 생략하는지 확인해야 합니다.
- 독립적으로 표시해야 하는 영역은 각각의 조건으로 렌더링하고, 표시 조건의 조합을 테스트로 검증해야 합니다.