blob: 5764f8f32b9ea78ff40a28d8aef180ed6c2ad424 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import {describe, it} from 'mocha';
import sinon from 'sinon';
import * as Platform from '../../core/platform/platform.js';
import * as Protocol from '../../generated/protocol.js';
import {assertScreenshot, doubleRaf, renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import * as Security from './security.js';
const {urlString} = Platform.DevToolsPath;
describeWithEnvironment('SecurityPanelSidebar', () => {
it('initializes with default sections, overview element, and origin groups', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
await doubleRaf();
const overview = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelector(
'.security-main-view-sidebar-tree-item');
assert.exists(overview);
assert.isTrue(overview.textContent?.includes('Overview'));
const originGroups = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-origins');
assert.lengthOf(originGroups, 4);
const groupTitles =
Array.from(originGroups).map(g => g.querySelector('.security-sidebar-origins-title')?.textContent?.trim());
assert.include(groupTitles, 'Main origin');
assert.include(groupTitles, 'Non-secure origins');
assert.include(groupTitles, 'Secure origins');
assert.include(groupTitles, 'Unknown / canceled');
});
it('adds and updates origins across category groups', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
const secureOrigin = urlString`https://secure.example.com`;
const insecureOrigin = urlString`http://insecure.example.com`;
const unknownOrigin = urlString`https://unknown.example.com`;
sidebar.origins = new Map([
[secureOrigin, Protocol.Security.SecurityState.Secure],
[insecureOrigin, Protocol.Security.SecurityState.Insecure],
[unknownOrigin, Protocol.Security.SecurityState.Unknown],
]);
await doubleRaf();
const originItems = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item');
assert.lengthOf(originItems, 3);
// Update insecure origin to secure
sidebar.origins = new Map([
[secureOrigin, Protocol.Security.SecurityState.Secure],
[insecureOrigin, Protocol.Security.SecurityState.Secure],
[unknownOrigin, Protocol.Security.SecurityState.Unknown],
]);
await doubleRaf();
assert.lengthOf(sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item'),
3);
});
it('updates highlighted URL DOM elements in place when security state changes within a group', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
const origin = urlString`https://example.com`;
sidebar.mainOrigin = origin;
sidebar.origins = new Map([[origin, Protocol.Security.SecurityState.Secure]]);
await sidebar.updateComplete;
const template = sidebar.contentElement.querySelector('devtools-tree')?.querySelector('template');
assert.exists(template);
const highlightedUrl = template.content.querySelector('.highlighted-url');
assert.exists(highlightedUrl);
assert.exists(highlightedUrl.querySelector('.url-scheme-secure'));
sidebar.origins = new Map([[origin, Protocol.Security.SecurityState.Insecure]]);
await sidebar.updateComplete;
const newHighlightedUrl = template.content.querySelector('.highlighted-url');
assert.strictEqual(newHighlightedUrl, highlightedUrl);
assert.exists(highlightedUrl.querySelector('.url-scheme-insecure'));
});
it('handles main origin assignment and updates', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
const mainOrigin = urlString`https://main.example.com`;
sidebar.mainOrigin = mainOrigin;
assert.strictEqual(sidebar.mainOrigin, mainOrigin);
sidebar.origins = new Map([[mainOrigin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
const items = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item');
const originElement = Array.from(items).find(item => item.textContent?.includes(mainOrigin));
assert.exists(originElement);
});
it('updates main origin and styling on cross-origin navigation', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
// Initial navigation to example.com
const initialOrigin = urlString`https://example.com`;
sidebar.mainOrigin = initialOrigin;
sidebar.origins = new Map([[initialOrigin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
const shadow = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!;
let items = shadow.querySelectorAll('.security-sidebar-tree-item');
assert.lengthOf(items, 1);
assert.isTrue(items[0].textContent?.includes(initialOrigin));
// Verify padlock icon and https scheme elements exist
const initialScheme = items[0].querySelector('.url-scheme-secure');
assert.exists(initialScheme);
assert.strictEqual(initialScheme?.textContent, 'https');
const initialLockIcon = items[0].querySelector('.security-property-secure');
assert.exists(initialLockIcon);
// Navigate to example.net (cross-origin navigation clears origins and sets new main origin)
sidebar.origins = new Map();
const newOrigin = urlString`https://example.net`;
sidebar.mainOrigin = newOrigin;
sidebar.origins = new Map([[newOrigin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
items = shadow.querySelectorAll('.security-sidebar-tree-item');
assert.lengthOf(items, 1);
assert.isTrue(items[0].textContent?.includes(newOrigin));
assert.isFalse(items[0].textContent?.includes(initialOrigin));
const newScheme = items[0].querySelector('.url-scheme-secure');
assert.exists(newScheme);
assert.strictEqual(newScheme?.textContent, 'https');
const newLockIcon = items[0].querySelector('.security-property-secure');
assert.exists(newLockIcon);
});
it('clears origins correctly and restores reload message', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
const origin = urlString`https://secure.example.com`;
sidebar.origins = new Map([[origin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
assert.lengthOf(sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item'),
1);
sidebar.origins = new Map();
await doubleRaf();
assert.lengthOf(sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item'),
0);
const reloadMsg = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelector(
'.security-main-view-reload-message');
assert.exists(reloadMsg);
});
it('toggles visibility of origin groups', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
const origin = urlString`https://secure.example.com`;
sidebar.origins = new Map([[origin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
sidebar.originsHidden = true;
sidebar.showOriginsUnconditionally = false;
await doubleRaf();
let groups = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-origins');
for (const group of groups) {
assert.isTrue(group.classList.contains('hidden'));
}
sidebar.originsHidden = false;
sidebar.showOriginsUnconditionally = true;
await doubleRaf();
groups = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-origins');
for (const group of groups) {
assert.isFalse(group.classList.contains('hidden'));
}
});
it('triggers onShowOrigin callback when an origin element is clicked', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar);
await doubleRaf();
const origin = urlString`https://secure.example.com`;
sidebar.origins = new Map([[origin, Protocol.Security.SecurityState.Secure]]);
await doubleRaf();
const callback = sinon.stub();
sidebar.onShowOrigin = callback;
const items = sidebar.contentElement.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll(
'.security-sidebar-tree-item');
const originItem = Array.from(items).find(item => item.textContent?.includes(origin)) as HTMLElement;
assert.exists(originItem);
originItem.click();
sinon.assert.calledOnce(callback);
assert.strictEqual(callback.firstCall.args[0], origin);
});
describe('visual screenshots', () => {
it('renders initial sidebar state', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar, {includeCommonStyles: true});
await doubleRaf();
await assertScreenshot('security/security_sidebar_initial.png');
});
it('renders populated sidebar with categorized origins', async () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
renderElementIntoDOM(sidebar, {includeCommonStyles: true});
const mainOrigin = urlString`https://devtools.test`;
const secureOrigin = urlString`https://secure.example.com`;
const insecureOrigin = urlString`http://insecure.example.com`;
const unknownOrigin = urlString`https://unknown.example.com`;
sidebar.mainOrigin = mainOrigin;
sidebar.origins = new Map([
[mainOrigin, Protocol.Security.SecurityState.Secure],
[secureOrigin, Protocol.Security.SecurityState.Secure],
[insecureOrigin, Protocol.Security.SecurityState.Insecure],
[unknownOrigin, Protocol.Security.SecurityState.Unknown],
]);
await doubleRaf();
await assertScreenshot('security/security_sidebar_populated.png');
});
});
describe('declarative setters', () => {
it('updates mainOrigin and triggers requestUpdate only when value changes', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
sidebar.mainOrigin = 'https://example.com';
assert.strictEqual(sidebar.mainOrigin, 'https://example.com');
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.mainOrigin = 'https://example.com';
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.mainOrigin = 'https://other.com';
assert.strictEqual(sidebar.mainOrigin, 'https://other.com');
sinon.assert.callCount(requestUpdateSpy, 2);
});
it('updates origins and triggers requestUpdate only when reference changes', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
const originsMap = new Map([[urlString`https://example.com`, Protocol.Security.SecurityState.Secure]]);
sidebar.origins = originsMap;
assert.strictEqual(sidebar.origins, originsMap);
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.origins = originsMap;
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.origins = new Map();
sinon.assert.callCount(requestUpdateSpy, 2);
});
it('updates originsHidden and triggers requestUpdate only when value changes', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
assert.isFalse(sidebar.originsHidden);
sidebar.originsHidden = false;
sinon.assert.notCalled(requestUpdateSpy);
sidebar.originsHidden = true;
assert.isTrue(sidebar.originsHidden);
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.originsHidden = true;
sinon.assert.calledOnce(requestUpdateSpy);
});
it('updates showOriginsUnconditionally and triggers requestUpdate only when value changes', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
assert.isFalse(sidebar.showOriginsUnconditionally);
sidebar.showOriginsUnconditionally = false;
sinon.assert.notCalled(requestUpdateSpy);
sidebar.showOriginsUnconditionally = true;
assert.isTrue(sidebar.showOriginsUnconditionally);
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.showOriginsUnconditionally = true;
sinon.assert.calledOnce(requestUpdateSpy);
});
it('updates overviewSecurityState and triggers requestUpdate only when value changes', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
assert.strictEqual(sidebar.overviewSecurityState, Protocol.Security.SecurityState.Unknown);
sidebar.overviewSecurityState = Protocol.Security.SecurityState.Unknown;
sinon.assert.notCalled(requestUpdateSpy);
sidebar.overviewSecurityState = Protocol.Security.SecurityState.Secure;
assert.strictEqual(sidebar.overviewSecurityState, Protocol.Security.SecurityState.Secure);
sinon.assert.calledOnce(requestUpdateSpy);
sidebar.overviewSecurityState = Protocol.Security.SecurityState.Secure;
sinon.assert.calledOnce(requestUpdateSpy);
});
it('updates activeOrigin and selection without invoking onShowOrigin', () => {
const sidebar = new Security.SecurityPanelSidebar.SecurityPanelSidebar();
const onShowOriginSpy = sinon.spy();
sidebar.onShowOrigin = onShowOriginSpy;
const requestUpdateSpy = sinon.spy(sidebar, 'requestUpdate');
const origin = urlString`https://example.com`;
sidebar.activeOrigin = origin;
assert.strictEqual(sidebar.activeOrigin, origin);
sinon.assert.calledOnce(requestUpdateSpy);
sinon.assert.notCalled(onShowOriginSpy);
sidebar.activeOrigin = origin;
sinon.assert.calledOnce(requestUpdateSpy);
sinon.assert.notCalled(onShowOriginSpy);
sidebar.activeOrigin = null;
assert.isNull(sidebar.activeOrigin);
sinon.assert.callCount(requestUpdateSpy, 2);
sinon.assert.notCalled(onShowOriginSpy);
});
});
describe('DEFAULT_VIEW', () => {
function renderView(input: Partial<Security.SecurityPanelSidebar.ViewInput> = {}):
{container: HTMLElement, input: Security.SecurityPanelSidebar.ViewInput} {
const container = document.createElement('div');
renderElementIntoDOM(container);
const fullInput: Security.SecurityPanelSidebar.ViewInput = {
mainOrigin: null,
origins: new Map(),
originsHidden: false,
showOriginsUnconditionally: false,
overviewSecurityState: Protocol.Security.SecurityState.Unknown,
selectedElementId: 'overview',
onElementSelected: sinon.stub(),
onShowOrigin: sinon.stub(),
...input,
};
Security.SecurityPanelSidebar.DEFAULT_VIEW(fullInput, undefined, container);
return {container, input: fullInput};
}
it('renders overview element and origin groups with defaults', async () => {
const {container} = renderView();
await doubleRaf();
const tree = container.querySelector('devtools-tree');
assert.exists(tree);
const shadow = tree.shadowRoot;
assert.exists(shadow);
const overview = shadow.querySelector('.security-main-view-sidebar-tree-item');
assert.exists(overview);
assert.isTrue(overview.textContent?.includes('Overview'));
const groups = shadow.querySelectorAll('.security-sidebar-origins');
assert.lengthOf(groups, 4);
const groupTitles =
Array.from(groups).map(g => g.querySelector('.security-sidebar-origins-title')?.textContent?.trim());
assert.include(groupTitles, 'Main origin');
assert.include(groupTitles, 'Non-secure origins');
assert.include(groupTitles, 'Secure origins');
assert.include(groupTitles, 'Unknown / canceled');
});
it('renders reload message when main origin group is empty', async () => {
const {container} = renderView({origins: new Map()});
await doubleRaf();
const reloadMsg =
container.querySelector('devtools-tree')!.shadowRoot!.querySelector('.security-main-view-reload-message');
assert.exists(reloadMsg);
assert.strictEqual(reloadMsg.textContent?.trim(), 'Reload to view details');
});
it('does not render reload message when origins are present', async () => {
const mainOrigin = urlString`https://main.example.com`;
const origins = new Map([[mainOrigin, Protocol.Security.SecurityState.Secure]]);
const {container} = renderView({mainOrigin, origins});
await doubleRaf();
const reloadMsg =
container.querySelector('devtools-tree')!.shadowRoot!.querySelector('.security-main-view-reload-message');
assert.notExists(reloadMsg);
});
it('renders categorized origins with correct security icons and highlighted URLs', async () => {
const mainOrigin = urlString`https://main.example.com`;
const secureOrigin = urlString`https://secure.example.com`;
const insecureOrigin = urlString`http://insecure.example.com`;
const unknownOrigin = urlString`https://unknown.example.com`;
const origins = new Map([
[mainOrigin, Protocol.Security.SecurityState.Secure],
[secureOrigin, Protocol.Security.SecurityState.Secure],
[insecureOrigin, Protocol.Security.SecurityState.Insecure],
[unknownOrigin, Protocol.Security.SecurityState.Unknown],
]);
const {container} = renderView({mainOrigin, origins});
await doubleRaf();
const shadow = container.querySelector('devtools-tree')!.shadowRoot!;
const originItems = shadow.querySelectorAll('.security-sidebar-tree-item');
assert.lengthOf(originItems, 4);
const secureIcon = shadow.querySelector('.security-property-secure');
assert.exists(secureIcon);
const insecureIcon = shadow.querySelector('.security-property-insecure');
assert.exists(insecureIcon);
const unknownIcon = shadow.querySelector('.security-property-unknown');
assert.exists(unknownIcon);
const highlightedUrls = shadow.querySelectorAll('.highlighted-url');
assert.lengthOf(highlightedUrls, 4);
});
it('hides origin groups when originsHidden is true', async () => {
const {container} = renderView({originsHidden: true});
await doubleRaf();
const groups =
container.querySelector('devtools-tree')!.shadowRoot!.querySelectorAll('.security-sidebar-origins');
for (const group of groups) {
assert.isTrue(group.classList.contains('hidden'));
}
});
it('invokes callbacks when overview and origins are clicked', async () => {
const mainOrigin = urlString`https://main.example.com`;
const origins = new Map([[mainOrigin, Protocol.Security.SecurityState.Secure]]);
const onElementSelected = sinon.stub();
const onShowOrigin = sinon.stub();
const {container} = renderView({mainOrigin, origins, onElementSelected, onShowOrigin});
await doubleRaf();
const shadow = container.querySelector('devtools-tree')!.shadowRoot!;
// Click overview
const overview = shadow.querySelector('.security-main-view-sidebar-tree-item') as HTMLElement;
assert.exists(overview);
overview.click();
sinon.assert.calledWith(onElementSelected, 'overview');
sinon.assert.calledWith(onShowOrigin, null);
// Click origin item
const originItem = shadow.querySelector('.security-sidebar-tree-item') as HTMLElement;
assert.exists(originItem);
originItem.click();
sinon.assert.calledWith(onElementSelected, mainOrigin);
sinon.assert.calledWith(onShowOrigin, mainOrigin);
});
});
});