blob: 25df7d7ffb2b4e8532d94809f8f3221f319c4c7e [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 {afterEach, beforeEach, describe, it} from 'mocha';
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 AiAssistance from '../../models/ai_assistance/ai_assistance.js';
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
import {MockCDPConnection} from '../../testing/MockCDPConnection.js';
import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js';
import {setupSettingsHooks} from '../../testing/SettingsHelpers.js';
import {TestUniverse} from '../../testing/TestUniverse.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Application from './application.js';
const {urlString} = Platform.DevToolsPath;
describe('ExpandableApplicationPanelTreeElement', () => {
setupLocaleHooks();
setupSettingsHooks();
setupRuntimeHooks();
let universe: TestUniverse;
let target: SDK.Target.Target;
let panel: sinon.SinonStubbedInstance<Application.ResourcesPanel.ResourcesPanel>;
beforeEach(() => {
universe = new TestUniverse();
sinon.stub(Common.Settings.Settings, 'instance').returns(universe.settings);
sinon.stub(SDK.TargetManager.TargetManager, 'instance').returns(universe.targetManager);
const connection = new MockCDPConnection();
target = universe.createTarget({url: urlString`https://example.com/page`, connection});
target.setInspectedURL(urlString`https://example.com/page`);
universe.targetManager.setScopeTarget(target);
panel = sinon.createStubInstance(Application.ResourcesPanel.ResourcesPanel);
UI.Context.Context.instance().setFlavor(AiAssistance.StorageItem.StorageItem, null);
});
afterEach(() => {
UI.Context.Context.instance().setFlavor(AiAssistance.StorageItem.StorageItem, null);
});
function createLocalStorageCategory(origins: string[]): {
treeOutline: UI.TreeOutline.TreeOutlineInShadow,
category: Application.ApplicationPanelTreeElement.ExpandableApplicationPanelTreeElement,
children: Application.ApplicationPanelSidebar.DOMStorageTreeElement[],
} {
const domStorageModel = target.model(SDK.DOMStorageModel.DOMStorageModel);
assert.exists(domStorageModel);
const treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
const category = new Application.ApplicationPanelTreeElement.ExpandableApplicationPanelTreeElement(
panel, 'Local storage', 'No local storage', 'Local storage description', 'local-storage');
treeOutline.appendChild(category);
const children = origins.map(origin => {
const domStorage = new SDK.DOMStorageModel.DOMStorage(domStorageModel, `${origin}/`, true);
const child = new Application.ApplicationPanelSidebar.DOMStorageTreeElement(panel, domStorage);
category.appendChild(child);
return child;
});
return {treeOutline, category, children};
}
it('expands and selects the main-origin child when user-selected from a collapsed state', () => {
const {treeOutline, category, children} =
createLocalStorageCategory(['https://ads.third-party.test', 'https://example.com', 'https://cdn.example.org']);
assert.isFalse(category.expanded);
category.select(/* omitFocus= */ false, /* selectedByUser= */ true);
assert.isTrue(category.expanded);
assert.isTrue(universe.settings.createSetting('resources-local-storage-expanded', false).get());
assert.strictEqual(treeOutline.selectedTreeElement, children[1]);
sinon.assert.notCalled(panel.showCategoryView);
sinon.assert.calledOnce(panel.showDOMStorage);
assert.deepEqual(panel.setLastSelectedItemPath.lastCall?.args[0], [
urlString`storage://https://example.com//local`,
urlString`category://Local storage`,
]);
const flavor = UI.Context.Context.instance().flavor(AiAssistance.StorageItem.StorageItem);
assert.instanceOf(flavor, AiAssistance.StorageItem.DOMStorageItem);
assert.strictEqual(flavor.origin, 'https://example.com');
assert.isFalse(flavor.isGenericContext);
});
it('selects the sole child when no child matches the primary target origin', () => {
const {treeOutline, category, children} = createLocalStorageCategory(['https://sub.third-party.test']);
category.select(false, true);
assert.isTrue(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, children[0]);
sinon.assert.notCalled(panel.showCategoryView);
sinon.assert.calledOnce(panel.showDOMStorage);
});
it('expands and shows the category view when multiple non-matching origins are present', () => {
const {treeOutline, category} =
createLocalStorageCategory(['https://ads.third-party.test', 'https://cdn.example.org']);
category.select(false, true);
assert.isTrue(category.expanded);
assert.isTrue(universe.settings.createSetting('resources-local-storage-expanded', false).get());
assert.strictEqual(treeOutline.selectedTreeElement, category);
sinon.assert.calledOnceWithExactly(panel.showCategoryView, 'Local storage', 'Local storage',
'Local storage description', null);
sinon.assert.notCalled(panel.showDOMStorage);
});
it('selects the main-origin child when clicking an already-expanded category from outside', () => {
const {treeOutline, category, children} =
createLocalStorageCategory(['https://ads.third-party.test', 'https://example.com']);
category.expand();
assert.isTrue(category.expanded);
const sibling =
new Application.ApplicationPanelTreeElement.ApplicationPanelTreeElement(panel, 'Manifest', false, 'manifest');
sibling.itemURL = urlString`manifest://example`;
treeOutline.insertChild(sibling, 0);
sibling.select(false, true);
panel.showCategoryView.resetHistory();
panel.showDOMStorage.resetHistory();
category.select(false, true);
assert.isTrue(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, children[1]);
sinon.assert.notCalled(panel.showCategoryView);
sinon.assert.calledOnce(panel.showDOMStorage);
});
it('keeps the category selected when navigating up from a selected child', () => {
const {treeOutline, category, children} = createLocalStorageCategory(['https://example.com']);
category.expand();
children[0].select(false, true);
panel.showDOMStorage.resetHistory();
category.select(false, true);
assert.isTrue(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, category);
sinon.assert.calledOnceWithExactly(panel.showCategoryView, 'Local storage', 'Local storage',
'Local storage description', null);
sinon.assert.notCalled(panel.showDOMStorage);
});
it('keeps the category selected and collapsed when collapsed while a child is selected', () => {
const {treeOutline, category, children} = createLocalStorageCategory(['https://example.com']);
category.expand();
children[0].select(false, true);
panel.showDOMStorage.resetHistory();
category.collapse();
assert.isFalse(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, category);
sinon.assert.calledOnceWithExactly(panel.showCategoryView, 'Local storage', 'Local storage',
'Local storage description', null);
sinon.assert.notCalled(panel.showDOMStorage);
});
it('re-expands and selects the child when clicking a collapsed, already-selected category', () => {
const {treeOutline, category, children} = createLocalStorageCategory(['https://example.com']);
category.expand();
children[0].select(false, true);
category.collapse();
panel.showDOMStorage.resetHistory();
category.select(false, true);
assert.isTrue(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, children[0]);
sinon.assert.calledOnce(panel.showDOMStorage);
});
it('does not auto-expand or select a child on programmatic selection', () => {
const {treeOutline, category} = createLocalStorageCategory(['https://example.com']);
category.select();
assert.isFalse(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, category);
sinon.assert.calledOnceWithExactly(panel.showCategoryView, 'Local storage', 'Local storage',
'Local storage description', null);
sinon.assert.notCalled(panel.showDOMStorage);
});
it('keeps an empty storage category selected and shows the empty headline', () => {
const {treeOutline, category} = createLocalStorageCategory([]);
category.select(false, true);
assert.isFalse(category.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, category);
sinon.assert.calledOnceWithExactly(panel.showCategoryView, 'Local storage', 'No local storage',
'Local storage description', null);
});
it('matches unreachable main-frame origin for Cookies when the main document failed to load', () => {
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
const frame = sinon.createStubInstance(SDK.ResourceTreeModel.ResourceTreeFrame);
frame.resourceTreeModel.returns(resourceTreeModel);
frame.unreachableUrl.returns(urlString`https://offline.example.test/error`);
resourceTreeModel.mainFrame = frame;
target.setInspectedURL(urlString`chrome-error://chromewebdata/`);
const treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
const cookiesCategory = new Application.ApplicationPanelTreeElement.ExpandableApplicationPanelTreeElement(
panel, 'Cookies', 'No cookies', 'Cookies description', 'cookies');
treeOutline.appendChild(cookiesCategory);
const thirdPartyCookie = new Application.ApplicationPanelSidebar.CookieTreeElement(
panel, frame, new Common.ParsedURL.ParsedURL('https://ads.third-party.test/'));
const unreachableCookie = new Application.ApplicationPanelSidebar.CookieTreeElement(
panel, frame, new Common.ParsedURL.ParsedURL('https://offline.example.test/error'));
cookiesCategory.appendChild(thirdPartyCookie);
cookiesCategory.appendChild(unreachableCookie);
cookiesCategory.select(false, true);
assert.isTrue(cookiesCategory.expanded);
assert.strictEqual(treeOutline.selectedTreeElement, unreachableCookie);
sinon.assert.calledOnceWithExactly(panel.showCookies, target, 'https://offline.example.test');
});
});