chrome-devtools-frontend
Version:
Chrome DevTools UI
480 lines (413 loc) • 22 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * 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';
import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
import {
describeWithMockConnection,
setMockConnectionResponseHandler,
} from '../../testing/MockConnection.js';
import {createResource, getMainFrame} from '../../testing/ResourceTreeHelpers.js';
import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Application from './application.js';
const {urlString} = Platform.DevToolsPath;
class SharedStorageTreeElementListener {
#sidebar: Application.ApplicationPanelSidebar.ApplicationPanelSidebar;
#originsAdded: string[] = [];
constructor(sidebar: Application.ApplicationPanelSidebar.ApplicationPanelSidebar) {
this.#sidebar = sidebar;
this.#sidebar.sharedStorageTreeElementDispatcher.addEventListener(
Application.ApplicationPanelSidebar.SharedStorageTreeElementDispatcher.Events.SHARED_STORAGE_TREE_ELEMENT_ADDED,
this.#treeElementAdded, this);
}
dispose(): void {
this.#sidebar.sharedStorageTreeElementDispatcher.removeEventListener(
Application.ApplicationPanelSidebar.SharedStorageTreeElementDispatcher.Events.SHARED_STORAGE_TREE_ELEMENT_ADDED,
this.#treeElementAdded, this);
}
#treeElementAdded(
event: Common.EventTarget.EventTargetEvent<Application.ApplicationPanelSidebar.SharedStorageTreeElementDispatcher
.SharedStorageTreeElementAddedEvent>): void {
this.#originsAdded.push(event.data.origin);
}
async waitForElementsAdded(expectedCount: number): Promise<void> {
while (this.#originsAdded.length < expectedCount) {
await this.#sidebar.sharedStorageTreeElementDispatcher.once(
Application.ApplicationPanelSidebar.SharedStorageTreeElementDispatcher.Events
.SHARED_STORAGE_TREE_ELEMENT_ADDED);
}
}
}
describeWithMockConnection('ApplicationPanelSidebar', () => {
let target: SDK.Target.Target;
const TEST_ORIGIN_A = 'http://www.example.com/';
const TEST_SITE_A = 'http://example.com';
const TEST_ORIGIN_B = 'http://www.example.org/';
const TEST_ORIGIN_C = 'http://www.example.net/';
const TEST_SITE_C = 'http://example.net';
const TEST_EXTENSION_NAME = 'Test Extension';
const ID = 'main' as Protocol.Page.FrameId;
const EVENTS = [
{
accessTime: 0,
method: Protocol.Storage.SharedStorageAccessMethod.Append,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_A,
ownerSite: TEST_SITE_A,
params: {key: 'key0', value: 'value0'} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.Window,
},
{
accessTime: 10,
method: Protocol.Storage.SharedStorageAccessMethod.Get,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_A,
ownerSite: TEST_SITE_A,
params: {key: 'key0'} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet,
},
{
accessTime: 15,
method: Protocol.Storage.SharedStorageAccessMethod.Length,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_A,
ownerSite: TEST_SITE_A,
params: {} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet,
},
{
accessTime: 20,
method: Protocol.Storage.SharedStorageAccessMethod.Clear,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_C,
ownerSite: TEST_SITE_C,
params: {} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.Window,
},
{
accessTime: 100,
method: Protocol.Storage.SharedStorageAccessMethod.Set,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_C,
ownerSite: TEST_SITE_C,
params: {key: 'key0', value: 'value1', ignoreIfPresent: true} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet,
},
{
accessTime: 150,
method: Protocol.Storage.SharedStorageAccessMethod.RemainingBudget,
mainFrameId: ID,
ownerOrigin: TEST_ORIGIN_C,
ownerSite: TEST_SITE_C,
params: {} as Protocol.Storage.SharedStorageAccessParams,
scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet,
},
];
beforeEach(() => {
stubNoopSettings();
SDK.ChildTargetManager.ChildTargetManager.install();
const tabTarget = createTarget({type: SDK.Target.Type.TAB});
createTarget({parentTarget: tabTarget, subtype: 'prerender'});
target = createTarget({parentTarget: tabTarget});
sinon.stub(UI.ViewManager.ViewManager.instance(), 'showView').resolves(); // Silence console error
setMockConnectionResponseHandler('Storage.getSharedStorageEntries', () => ({}));
setMockConnectionResponseHandler('Storage.setSharedStorageTracking', () => ({}));
});
// Flaking on multiple bots on CQ.
it.skip('[crbug.com/40278557] shows cookies for all frames', async () => {
Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const sidebar = await Application.ResourcesPanel.ResourcesPanel.showAndGetSidebar();
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
sinon.stub(resourceTreeModel, 'frames').returns([
{
url: 'http://www.example.com/',
unreachableUrl: () => null,
resourceTreeModel: () => resourceTreeModel,
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
{
url: 'http://www.example.com/admin/',
unreachableUrl: () => null,
resourceTreeModel: () => resourceTreeModel,
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
{
url: 'http://www.example.org/',
unreachableUrl: () => null,
resourceTreeModel: () => resourceTreeModel,
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
]);
resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.CachedResourcesLoaded, resourceTreeModel);
assert.strictEqual(sidebar.cookieListTreeElement.childCount(), 2);
assert.deepEqual(
sidebar.cookieListTreeElement.children().map(e => e.title),
['http://www.example.com', 'http://www.example.org']);
});
it('shows shared storages and events for origins using shared storage', async () => {
const securityOriginManager = target.model(SDK.SecurityOriginManager.SecurityOriginManager);
assert.exists(securityOriginManager);
sinon.stub(securityOriginManager, 'securityOrigins').returns([
TEST_ORIGIN_A,
TEST_ORIGIN_B,
TEST_ORIGIN_C,
]);
const sharedStorageModel = target.model(Application.SharedStorageModel.SharedStorageModel);
assert.exists(sharedStorageModel);
const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({
getError: () => undefined,
});
Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const sidebar = await Application.ResourcesPanel.ResourcesPanel.showAndGetSidebar();
const listener = new SharedStorageTreeElementListener(sidebar);
const addedPromise = listener.waitForElementsAdded(4);
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(resourceTreeModel);
resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.CachedResourcesLoaded, resourceTreeModel);
await addedPromise;
sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true});
assert.strictEqual(sidebar.sharedStorageListTreeElement.childCount(), 4);
assert.deepEqual(sidebar.sharedStorageListTreeElement.children().map(e => e.title), [
'https://example.com', // frame origin
TEST_ORIGIN_A,
TEST_ORIGIN_B,
TEST_ORIGIN_C,
]);
sidebar.sharedStorageListTreeElement.view.setDefaultIdForTesting(ID);
for (const event of EVENTS) {
sharedStorageModel.dispatchEventToListeners(Application.SharedStorageModel.Events.SHARED_STORAGE_ACCESS, event);
}
assert.deepEqual(sidebar.sharedStorageListTreeElement.view.getEventsForTesting(), EVENTS);
});
it('shows extension storage based on added models', async () => {
for (const useTreeView of [false, true]) {
Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const sidebar = await Application.ResourcesPanel.ResourcesPanel.showAndGetSidebar();
// Cast to any allows overriding private method.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sinon.stub(sidebar, 'useTreeViewForExtensionStorage' as any).returns(useTreeView);
const extensionStorageModel = target.model(Application.ExtensionStorageModel.ExtensionStorageModel);
assert.exists(extensionStorageModel);
const makeFakeExtensionStorage = (storageArea: Protocol.Extensions.StorageArea) =>
new Application.ExtensionStorageModel.ExtensionStorage(
extensionStorageModel, '', TEST_EXTENSION_NAME, storageArea);
const fakeModelLocal = makeFakeExtensionStorage(Protocol.Extensions.StorageArea.Local);
const fakeModelSession = makeFakeExtensionStorage(Protocol.Extensions.StorageArea.Session);
extensionStorageModel.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED, fakeModelLocal);
extensionStorageModel.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED, fakeModelSession);
if (useTreeView) {
assert.strictEqual(sidebar.extensionStorageListTreeElement.childCount(), 1);
assert.strictEqual(sidebar.extensionStorageListTreeElement.children()[0].title, TEST_EXTENSION_NAME);
assert.deepEqual(
sidebar.extensionStorageListTreeElement.children()[0].children().map(e => e.title), ['Session', 'Local']);
} else {
assert.strictEqual(sidebar.extensionStorageListTreeElement.childCount(), 2);
assert.deepEqual(sidebar.extensionStorageListTreeElement.children().map(e => e.title), ['Session', 'Local']);
}
extensionStorageModel.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_REMOVED, fakeModelLocal);
extensionStorageModel.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_REMOVED, fakeModelSession);
assert.strictEqual(sidebar.extensionStorageListTreeElement.childCount(), 0);
}
});
it('does not add extension storage if already added by another model', async () => {
Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const sidebar = await Application.ResourcesPanel.ResourcesPanel.showAndGetSidebar();
// Fakes adding an ExtensionStorage to the ExtensionStorageModel for
// `target`. Returns a function that can be used to trigger a removal.
const addFakeExtensionStorage = (target: SDK.Target.Target): () => void => {
const model = target.model(Application.ExtensionStorageModel.ExtensionStorageModel);
assert.exists(model);
const extensionStorage = new Application.ExtensionStorageModel.ExtensionStorage(
model, '', TEST_EXTENSION_NAME, Protocol.Extensions.StorageArea.Local);
const stub = sinon.stub(model, 'storageForIdAndArea').returns(extensionStorage);
model.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED, extensionStorage);
return () => {
stub.restore();
model.dispatchEventToListeners(
Application.ExtensionStorageModel.Events.EXTENSION_STORAGE_REMOVED, extensionStorage);
};
};
// Add a fake extension storage to the main target. The UI should be updated.
addFakeExtensionStorage(target);
assert.strictEqual(sidebar.extensionStorageListTreeElement.children()[0].childCount(), 1);
// Add a fake extension storage using a non-main target (e.g, an iframe).
// Make sure we don't add a second entry to the UI.
const removeFrameStorage =
addFakeExtensionStorage(createTarget({type: SDK.Target.Type.FRAME, parentTarget: target}));
assert.strictEqual(sidebar.extensionStorageListTreeElement.children()[0].childCount(), 1);
// Removing the frame also shouldn't do anything, since the main frame
// still exists.
removeFrameStorage();
assert.strictEqual(sidebar.extensionStorageListTreeElement.children()[0].childCount(), 1);
});
async function getExpectedCall(expectedCall: string): Promise<sinon.SinonSpy> {
Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const sidebar = await Application.ResourcesPanel.ResourcesPanel.showAndGetSidebar();
const components = expectedCall.split('.');
assert.lengthOf(components, 2);
// @ts-expect-error
const object = sidebar[components[0]];
assert.exists(object);
return sinon.spy(object, components[1]);
}
const MOCK_EVENT_ITEM = {
addEventListener: () => {},
securityOrigin: 'https://example.com',
databaseId: new Application.IndexedDBModel.DatabaseId({storageKey: ''}, ''),
getEntries: () => Promise.resolve([]),
};
const testUiUpdate = <Events, T extends keyof Events>(
event: T, modelClass: new (arg1: SDK.Target.Target) => SDK.SDKModel.SDKModel<Events>, expectedCallString: string,
inScope: boolean) => async () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
const expectedCall = await getExpectedCall(expectedCallString);
const model = target.model(modelClass);
await RenderCoordinator.done({waitForWork: true});
assert.exists(model);
const data = [{...MOCK_EVENT_ITEM, model}] as Common.EventTarget.EventPayloadToRestParameters<Events, T>;
model.dispatchEventToListeners(event as Platform.TypeScriptUtilities.NoUnion<T>, ...data);
await new Promise(resolve => setTimeout(resolve, 0));
assert.strictEqual(expectedCall.called, inScope);
};
it('adds interest group event on in scope event',
testUiUpdate(
Application.InterestGroupStorageModel.Events.INTEREST_GROUP_ACCESS,
Application.InterestGroupStorageModel.InterestGroupStorageModel, 'interestGroupTreeElement.addEvent', true));
// Failing on the toolbar button CL together with some AnimationTimeline tests
it.skip(
'[crbug.com/354673294] does not add interest group event on out of scope event',
testUiUpdate(
Application.InterestGroupStorageModel.Events.INTEREST_GROUP_ACCESS,
Application.InterestGroupStorageModel.InterestGroupStorageModel, 'interestGroupTreeElement.addEvent', false));
it('adds DOM storage on in scope event',
testUiUpdate(
Application.DOMStorageModel.Events.DOM_STORAGE_ADDED, Application.DOMStorageModel.DOMStorageModel,
'sessionStorageListTreeElement.appendChild', true));
// Failing on the toolbar button CL together with some AnimationTimeline tests
it.skip(
'[crbug.com/354673294] does not add DOM storage on out of scope event',
testUiUpdate(
Application.DOMStorageModel.Events.DOM_STORAGE_ADDED, Application.DOMStorageModel.DOMStorageModel,
'sessionStorageListTreeElement.appendChild', false));
it('adds indexed DB on in scope event',
testUiUpdate(
Application.IndexedDBModel.Events.DatabaseAdded, Application.IndexedDBModel.IndexedDBModel,
'indexedDBListTreeElement.appendChild', true));
// Failing on the toolbar button CL together with some AnimationTimeline tests
it.skip(
'[crbug.com/354673294] does not add indexed DB on out of scope event',
testUiUpdate(
Application.IndexedDBModel.Events.DatabaseAdded, Application.IndexedDBModel.IndexedDBModel,
'indexedDBListTreeElement.appendChild', false));
it('adds shared storage on in scope event',
testUiUpdate(
Application.SharedStorageModel.Events.SHARED_STORAGE_ADDED, Application.SharedStorageModel.SharedStorageModel,
'sharedStorageListTreeElement.appendChild', true));
// Failing on the toolbar button CL together with some AnimationTimeline tests
it.skip(
'[crbug.com/354673294] does not add shared storage on out of scope event',
testUiUpdate(
Application.SharedStorageModel.Events.SHARED_STORAGE_ADDED, Application.SharedStorageModel.SharedStorageModel,
'sharedStorageListTreeElement.appendChild', false));
const MOCK_GETTER_ITEM = {
...MOCK_EVENT_ITEM,
...MOCK_EVENT_ITEM.databaseId,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testUiUpdateOnScopeChange = <T extends SDK.SDKModel.SDKModel<any>>(
modelClass: new (arg1: SDK.Target.Target) => T, getter: keyof T, expectedCallString: string) => async () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(null);
const expectedCall = await getExpectedCall(expectedCallString);
const model = target.model(modelClass);
assert.exists(model);
sinon.stub(model, getter).returns([MOCK_GETTER_ITEM]);
SDK.TargetManager.TargetManager.instance().setScopeTarget(target);
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.called(expectedCall);
};
it('adds DOM storage element after scope change',
testUiUpdateOnScopeChange(
Application.DOMStorageModel.DOMStorageModel, 'storages', 'sessionStorageListTreeElement.appendChild'));
it('adds shared storage after scope change',
testUiUpdateOnScopeChange(
Application.SharedStorageModel.SharedStorageModel, 'storages', 'sharedStorageListTreeElement.appendChild'));
it('adds indexed db after scope change',
testUiUpdateOnScopeChange(
Application.IndexedDBModel.IndexedDBModel, 'databases', 'indexedDBListTreeElement.appendChild'));
it('uses extension name when available for tree element title', () => {
const panel = Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const extensionName = 'Test Extension';
assert.strictEqual(
new Application.ApplicationPanelSidebar.ExtensionStorageTreeParentElement(panel, 'id', extensionName).title,
extensionName);
});
it('uses extension id as fallback for tree element title', () => {
const panel = Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const extensionId = 'id';
assert.strictEqual(
new Application.ApplicationPanelSidebar.ExtensionStorageTreeParentElement(panel, extensionId, '').title,
extensionId);
});
});
describeWithMockConnection('IDBDatabaseTreeElement', () => {
beforeEach(() => {
stubNoopSettings();
});
it('only becomes selectable after database is updated', () => {
const target = createTarget();
const model = target.model(Application.IndexedDBModel.IndexedDBModel);
assert.exists(model);
const panel = Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const databaseId = new Application.IndexedDBModel.DatabaseId({storageKey: ''}, '');
const treeElement = new Application.ApplicationPanelSidebar.IDBDatabaseTreeElement(panel, model, databaseId);
assert.isFalse(treeElement.selectable);
treeElement.update(new Application.IndexedDBModel.Database(databaseId, 1), false);
assert.isTrue(treeElement.selectable);
});
});
describeWithMockConnection('ResourcesSection', () => {
const tests = (inScope: boolean) => () => {
let target: SDK.Target.Target;
beforeEach(() => {
stubNoopSettings();
SDK.FrameManager.FrameManager.instance({forceNew: true});
target = createTarget();
});
it('adds tree elements for a frame and resource', () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
const panel = Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const treeElement = new UI.TreeOutline.TreeElement();
new Application.ApplicationPanelSidebar.ResourcesSection(panel, treeElement);
const model = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
assert.exists(model);
assert.strictEqual(treeElement.childCount(), 0);
const frame = getMainFrame(target);
const url = urlString`http://example.com`;
assert.strictEqual(treeElement.firstChild()?.childCount() ?? 0, 0);
createResource(frame, url, 'text/html', '');
assert.strictEqual(treeElement.firstChild()?.childCount() ?? 0, inScope ? 1 : 0);
});
it('picks up existing frames and resource', () => {
SDK.TargetManager.TargetManager.instance().setScopeTarget(null);
const panel = Application.ResourcesPanel.ResourcesPanel.instance({forceNew: true});
const treeElement = new UI.TreeOutline.TreeElement();
new Application.ApplicationPanelSidebar.ResourcesSection(panel, treeElement);
const url = urlString`http://example.com`;
createResource(getMainFrame(target), url, 'text/html', '');
assert.strictEqual(treeElement.firstChild()?.childCount() ?? 0, 0);
assert.strictEqual(treeElement.childCount(), 0);
SDK.TargetManager.TargetManager.instance().setScopeTarget(inScope ? target : null);
assert.strictEqual(treeElement.childCount(), inScope ? 1 : 0);
assert.strictEqual(treeElement.firstChild()?.childCount() ?? 0, inScope ? 1 : 0);
});
};
describe('in scope', tests(true));
describe('out of scope', tests(false));
});