chrome-devtools-frontend
Version:
Chrome DevTools UI
250 lines (194 loc) • 9.74 kB
text/typescript
// Copyright 2024 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} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import * as Resources from './application.js';
const {urlString} = Platform.DevToolsPath;
class ExtensionStorageListener {
#model: Resources.ExtensionStorageModel.ExtensionStorageModel;
#storagesWatched: Resources.ExtensionStorageModel.ExtensionStorage[];
constructor(model: Resources.ExtensionStorageModel.ExtensionStorageModel) {
this.#model = model;
this.#storagesWatched = [];
this.#model.addEventListener(
Resources.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED, this.#extensionStorageAdded, this);
this.#model.addEventListener(
Resources.ExtensionStorageModel.Events.EXTENSION_STORAGE_REMOVED, this.#extensionStorageRemoved, this);
}
dispose(): void {
this.#model.removeEventListener(
Resources.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED, this.#extensionStorageAdded, this);
this.#model.removeEventListener(
Resources.ExtensionStorageModel.Events.EXTENSION_STORAGE_REMOVED, this.#extensionStorageRemoved, this);
}
#extensionStorageAdded(event: Common.EventTarget.EventTargetEvent<Resources.ExtensionStorageModel.ExtensionStorage>):
void {
const storage = event.data;
this.#storagesWatched.push(storage);
}
#extensionStorageRemoved(
event: Common.EventTarget.EventTargetEvent<Resources.ExtensionStorageModel.ExtensionStorage>): void {
const storage = event.data;
const index = this.#storagesWatched.indexOf(storage);
if (index === -1) {
return;
}
this.#storagesWatched = this.#storagesWatched.splice(index, 1);
}
async waitForStoragesAdded(expectedCount: number): Promise<void> {
while (this.#storagesWatched.length < expectedCount) {
await this.#model.once(Resources.ExtensionStorageModel.Events.EXTENSION_STORAGE_ADDED);
}
}
}
describeWithMockConnection('ExtensionStorageModel', () => {
let extensionStorageModel: Resources.ExtensionStorageModel.ExtensionStorageModel;
let extensionStorage: Resources.ExtensionStorageModel.ExtensionStorage;
let target: SDK.Target.Target;
let listener: ExtensionStorageListener;
const initId = 'extensionid';
const initName = 'Test Extension';
const initStorageArea = Protocol.Extensions.StorageArea.Local;
beforeEach(() => {
target = createTarget();
extensionStorageModel = new Resources.ExtensionStorageModel.ExtensionStorageModel(target);
extensionStorage =
new Resources.ExtensionStorageModel.ExtensionStorage(extensionStorageModel, initId, initName, initStorageArea);
listener = new ExtensionStorageListener(extensionStorageModel);
});
const createMockExecutionContext = (id: number, origin: string): Protocol.Runtime.ExecutionContextDescription => {
return {
id: id as Protocol.Runtime.ExecutionContextId,
uniqueId: '',
origin: urlString`${origin}`,
name: 'Test Extension',
};
};
it('ExtensionStorage is instantiated correctly', () => {
assert.strictEqual(extensionStorage.extensionId, initId);
assert.strictEqual(extensionStorage.name, initName);
assert.strictEqual(extensionStorage.storageArea, initStorageArea);
});
const STORAGE_AREAS = [
Protocol.Extensions.StorageArea.Session,
Protocol.Extensions.StorageArea.Local,
Protocol.Extensions.StorageArea.Sync,
Protocol.Extensions.StorageArea.Managed,
];
const ENTRIES = {
foo: 'bar',
};
it('invokes storageAgent', async () => {
const getSpy = sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems').resolves({
data: ENTRIES,
getError: () => undefined,
});
const setSpy = sinon.stub(extensionStorageModel.agent, 'invoke_setStorageItems').resolves({
getError: () => undefined,
});
const removeSpy = sinon.stub(extensionStorageModel.agent, 'invoke_removeStorageItems').resolves({
getError: () => undefined,
});
const clearSpy = sinon.stub(extensionStorageModel.agent, 'invoke_clearStorageItems').resolves({
getError: () => undefined,
});
const data = await extensionStorage.getItems();
sinon.assert.calledOnceWithExactly(getSpy, {id: initId, storageArea: initStorageArea});
assert.deepEqual(data, ENTRIES);
await extensionStorage.setItem('foo', 'baz');
sinon.assert.calledOnceWithExactly(setSpy, {id: initId, storageArea: initStorageArea, values: {foo: 'baz'}});
await extensionStorage.removeItem('foo');
sinon.assert.calledOnceWithExactly(removeSpy, {id: initId, storageArea: initStorageArea, keys: ['foo']});
await extensionStorage.clear();
sinon.assert.calledOnceWithExactly(clearSpy, {id: initId, storageArea: initStorageArea});
});
it('adds/removes ExtensionStorage on Runtime events', async () => {
sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems').resolves({
data: {},
getError: () => undefined,
});
extensionStorageModel.enable();
assert.isEmpty(extensionStorageModel.storages());
const runtime = target.model(SDK.RuntimeModel.RuntimeModel);
assert.exists(runtime);
// Each extension adds four associated storage areas.
const addedPromise = listener.waitForStoragesAdded(4);
const mockExecutionContext = createMockExecutionContext(1, `chrome-extension://${initId}/sw.js`);
runtime.executionContextCreated(mockExecutionContext);
await addedPromise;
STORAGE_AREAS.forEach(area => assert.exists(extensionStorageModel.storageForIdAndArea(initId, area)));
runtime.executionContextDestroyed(mockExecutionContext.id);
assert.isEmpty(extensionStorageModel.storages());
});
it('does not add ExtensionStorage if origin invalid', async () => {
extensionStorageModel.enable();
assert.isEmpty(extensionStorageModel.storages());
const runtime = target.model(SDK.RuntimeModel.RuntimeModel);
assert.exists(runtime);
// The scheme is not valid (not chrome-extension://) so no storage should be added.
const mockExecutionContext = createMockExecutionContext(1, 'https://example.com');
runtime.executionContextCreated(mockExecutionContext);
assert.isEmpty(extensionStorageModel.storages());
});
it('does not add ExtensionStorage if origin already added', async () => {
sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems').resolves({
data: {},
getError: () => undefined,
});
extensionStorageModel.enable();
assert.isEmpty(extensionStorageModel.storages());
// Each extension adds four associated storage areas.
const addedPromise = listener.waitForStoragesAdded(4);
const runtime = target.model(SDK.RuntimeModel.RuntimeModel);
assert.exists(runtime);
const mockExecutionContext = createMockExecutionContext(1, `chrome-extension://${initId}/sw.js`);
runtime.executionContextCreated(mockExecutionContext);
await addedPromise;
STORAGE_AREAS.forEach(area => assert.exists(extensionStorageModel.storageForIdAndArea(initId, area)));
assert.lengthOf(extensionStorageModel.storages(), 4);
runtime.executionContextCreated(mockExecutionContext);
assert.lengthOf(extensionStorageModel.storages(), 4);
});
it('removes ExtensionStorage when last ExecutionContext is removed', async () => {
sinon.stub(extensionStorageModel.agent, 'invoke_getStorageItems').resolves({
data: {},
getError: () => undefined,
});
extensionStorageModel.enable();
assert.isEmpty(extensionStorageModel.storages());
// Each extension adds four associated storage areas.
const addedPromise = listener.waitForStoragesAdded(4);
const runtime = target.model(SDK.RuntimeModel.RuntimeModel);
assert.exists(runtime);
const mockExecutionContext1 = createMockExecutionContext(1, `chrome-extension://${initId}/sw.js`);
const mockExecutionContext2 = createMockExecutionContext(2, `chrome-extension://${initId}/another.js`);
runtime.executionContextCreated(mockExecutionContext1);
runtime.executionContextCreated(mockExecutionContext2);
await addedPromise;
STORAGE_AREAS.forEach(area => assert.exists(extensionStorageModel.storageForIdAndArea(initId, area)));
assert.lengthOf(extensionStorageModel.storages(), 4);
// If a single execution context is destroyed but another remains,
// ExtensionStorage should not be removed.
runtime.executionContextDestroyed(mockExecutionContext1.id);
assert.lengthOf(extensionStorageModel.storages(), 4);
runtime.executionContextDestroyed(mockExecutionContext2.id);
assert.lengthOf(extensionStorageModel.storages(), 0);
});
it('matches service worker target on same origin', () => {
assert.isTrue(extensionStorage.matchesTarget(
createTarget({type: SDK.Target.Type.ServiceWorker, url: `chrome-extension://${initId}/sw.js`})));
});
it('matches tab target on same origin', () => {
assert.isTrue(extensionStorage.matchesTarget(
createTarget({type: SDK.Target.Type.TAB, url: `chrome-extension://${initId}/sw.js`})));
});
it('does not match service worker target on different origin', () => {
assert.isFalse(extensionStorage.matchesTarget(
createTarget({type: SDK.Target.Type.ServiceWorker, url: 'chrome-extension://other-id/sw.js'})));
});
});