UNPKG

chrome-devtools-frontend

Version:
250 lines (194 loc) • 9.74 kB
// 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'}))); }); });