| // 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); | |
| }); | |
| }); | |
| }); |