UNPKG

chrome-devtools-frontend

Version:
416 lines (344 loc) • 16.6 kB
// 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 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 { getInitializedResourceTreeModel, getMainFrame, MAIN_FRAME_ID, navigate, } from '../../testing/ResourceTreeHelpers.js'; import * as Resources from './application.js'; class SharedStorageListener { #model: Resources.SharedStorageModel.SharedStorageModel; #storagesWatched: Array<Resources.SharedStorageModel.SharedStorageForOrigin>; #accessEvents: Array<Protocol.Storage.SharedStorageAccessedEvent>; #changeEvents: Map<Resources.SharedStorageModel.SharedStorageForOrigin, Array<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>>; constructor(model: Resources.SharedStorageModel.SharedStorageModel) { this.#model = model; this.#storagesWatched = new Array<Resources.SharedStorageModel.SharedStorageForOrigin>(); this.#accessEvents = new Array<Protocol.Storage.SharedStorageAccessedEvent>(); this.#changeEvents = new Map< Resources.SharedStorageModel.SharedStorageForOrigin, Array<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>>(); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED, this.#sharedStorageAdded, this); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_REMOVED, this.#sharedStorageRemoved, this); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ACCESS, this.#sharedStorageAccess, this); } dispose(): void { this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED, this.#sharedStorageAdded, this); this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_REMOVED, this.#sharedStorageRemoved, this); this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ACCESS, this.#sharedStorageAccess, this); for (const storage of this.#storagesWatched) { storage.removeEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); } } get accessEvents(): Array<Protocol.Storage.SharedStorageAccessedEvent> { return this.#accessEvents; } changeEventsForStorage(storage: Resources.SharedStorageModel.SharedStorageForOrigin): Array<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>|null { return this.#changeEvents.get(storage) || null; } changeEventsEmpty(): boolean { return this.#changeEvents.size === 0; } #sharedStorageAdded(event: Common.EventTarget.EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin>): void { const storage = (event.data as Resources.SharedStorageModel.SharedStorageForOrigin); this.#storagesWatched.push(storage); storage.addEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); } #sharedStorageRemoved( event: Common.EventTarget.EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin>): void { const storage = (event.data as Resources.SharedStorageModel.SharedStorageForOrigin); storage.removeEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); const index = this.#storagesWatched.indexOf(storage); if (index === -1) { return; } this.#storagesWatched = this.#storagesWatched.splice(index, 1); } #sharedStorageAccess(event: Common.EventTarget.EventTargetEvent<Protocol.Storage.SharedStorageAccessedEvent>): void { this.#accessEvents.push(event.data as Protocol.Storage.SharedStorageAccessedEvent); } #sharedStorageChanged( storage: Resources.SharedStorageModel.SharedStorageForOrigin, event: Common.EventTarget .EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>): void { if (!this.#changeEvents.has(storage)) { this.#changeEvents.set( storage, new Array<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>()); } this.#changeEvents.get(storage)?.push( event.data as Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent); } async waitForStoragesAdded(expectedCount: number): Promise<void> { while (this.#storagesWatched.length < expectedCount) { await this.#model.once(Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED); } } } describeWithMockConnection('SharedStorageModel', () => { let sharedStorageModel: Resources.SharedStorageModel.SharedStorageModel; let target: SDK.Target.Target; let listener: SharedStorageListener; const TEST_ORIGIN_A = 'http://a.test'; const TEST_ORIGIN_B = 'http://b.test'; const TEST_ORIGIN_C = 'http://c.test'; const METADATA = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 3, remainingBudget: 2.5, bytesUsed: 30, } as unknown as Protocol.Storage.SharedStorageMetadata; const ENTRIES = [ { key: 'key1', value: 'a', } as unknown as Protocol.Storage.SharedStorageEntry, { key: 'key2', value: 'b', } as unknown as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as unknown as Protocol.Storage.SharedStorageEntry, ]; const EVENTS = [ { accessTime: 0, type: Protocol.Storage.SharedStorageAccessType.DocumentAppend, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_A, params: {key: 'key0', value: 'value0'} as Protocol.Storage.SharedStorageAccessParams, }, { accessTime: 10, type: Protocol.Storage.SharedStorageAccessType.WorkletGet, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_A, params: {key: 'key0'} as Protocol.Storage.SharedStorageAccessParams, }, { accessTime: 15, type: Protocol.Storage.SharedStorageAccessType.WorkletLength, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_B, params: {} as Protocol.Storage.SharedStorageAccessParams, }, { accessTime: 20, type: Protocol.Storage.SharedStorageAccessType.DocumentClear, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_B, params: {} as Protocol.Storage.SharedStorageAccessParams, }, { accessTime: 100, type: Protocol.Storage.SharedStorageAccessType.WorkletSet, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_C, params: {key: 'key0', value: 'value1', ignoreIfPresent: true} as Protocol.Storage.SharedStorageAccessParams, }, { accessTime: 150, type: Protocol.Storage.SharedStorageAccessType.WorkletRemainingBudget, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_C, params: {} as Protocol.Storage.SharedStorageAccessParams, }, ]; beforeEach(async () => { target = createTarget(); await getInitializedResourceTreeModel(target); sharedStorageModel = target.model(Resources.SharedStorageModel.SharedStorageModel) as Resources.SharedStorageModel.SharedStorageModel; listener = new SharedStorageListener(sharedStorageModel); }); it('invokes storageAgent via SharedStorageForOrigin', async () => { const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata').resolves({ metadata: METADATA, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries').resolves({ entries: ENTRIES, getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const clearSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_clearSharedStorageEntries').resolves({ getError: () => undefined, }); const sharedStorage = new Resources.SharedStorageModel.SharedStorageForOrigin(sharedStorageModel, TEST_ORIGIN_A); assert.strictEqual(sharedStorage.securityOrigin, TEST_ORIGIN_A); const metadata = await sharedStorage.getMetadata(); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN_A})); assert.deepEqual(METADATA, metadata); const entries = await sharedStorage.getEntries(); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN_A})); assert.deepEqual(ENTRIES, entries); await sharedStorage.setEntry('new-key1', 'new-value1', true); assert.isTrue(setEntrySpy.calledOnceWithExactly( {ownerOrigin: TEST_ORIGIN_A, key: 'new-key1', value: 'new-value1', ignoreIfPresent: true})); await sharedStorage.deleteEntry('new-key1'); assert.isTrue(deleteEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN_A, key: 'new-key1'})); await sharedStorage.clear(); assert.isTrue(clearSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN_A})); }); it('adds/removes SharedStorageForOrigin on SecurityOrigin events', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); assert.isEmpty(sharedStorageModel.storages()); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); const addedPromise = listener.waitForStoragesAdded(1); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginAdded, TEST_ORIGIN_A); await addedPromise; assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginRemoved, TEST_ORIGIN_A); assert.isEmpty(sharedStorageModel.storages()); }); it('does not add SharedStorageForOrigin if origin invalid', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); assert.isEmpty(sharedStorageModel.storages()); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginAdded, 'invalid'); assert.isEmpty(sharedStorageModel.storages()); }); it('does not add SharedStorageForOrigin if origin already added', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); assert.isEmpty(sharedStorageModel.storages()); const addedPromise = listener.waitForStoragesAdded(1); navigate(getMainFrame(target), {url: TEST_ORIGIN_A}); await addedPromise; assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); assert.strictEqual(1, sharedStorageModel.numStoragesForTesting()); navigate(getMainFrame(target), {url: TEST_ORIGIN_A}); assert.strictEqual(1, sharedStorageModel.numStoragesForTesting()); }); it('adds/removes SecurityOrigins when model is enabled/disabled', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); const originSet = new Set([TEST_ORIGIN_A, TEST_ORIGIN_B, TEST_ORIGIN_C]); manager.updateSecurityOrigins(originSet); assert.lengthOf(manager.securityOrigins(), 3); const addedPromise = listener.waitForStoragesAdded(3); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); await addedPromise; assert.strictEqual(3, sharedStorageModel.numStoragesForTesting()); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_B)); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_C)); sharedStorageModel.disable(); assert.isEmpty(sharedStorageModel.storages()); }); it('dispatches SharedStorageAccess events to listeners', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } assert.deepEqual(EVENTS, listener.accessEvents); }); it('dispatches SharedStorageChanged events to listeners', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); await sharedStorageModel.enable(); assert.isTrue(setTrackingSpy.calledOnceWithExactly({enable: true})); // For change events whose origins aren't yet in the model, the origin is added // to the model, with the `SharedStorageAdded` event being subsequently dispatched // instead of the `SharedStorageChanged` event. const addedPromise = listener.waitForStoragesAdded(3); for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } await addedPromise; assert.strictEqual(4, sharedStorageModel.numStoragesForTesting()); assert.deepEqual(EVENTS, listener.accessEvents); assert.isTrue(listener.changeEventsEmpty()); // All events will be dispatched as `SharedStorageAccess` events, but only change // events for existing origins will be forwarded as `SharedStorageChanged` events. for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } assert.deepEqual(EVENTS.concat(EVENTS), listener.accessEvents); const storageA = sharedStorageModel.storageForOrigin(TEST_ORIGIN_A); assert.exists(storageA); assert.deepEqual(listener.changeEventsForStorage(storageA), [ { accessTime: 0, type: Protocol.Storage.SharedStorageAccessType.DocumentAppend, mainFrameId: MAIN_FRAME_ID, params: {key: 'key0', value: 'value0'} as Protocol.Storage.SharedStorageAccessParams, }, ]); const storageB = sharedStorageModel.storageForOrigin(TEST_ORIGIN_B); assert.exists(storageB); assert.deepEqual(listener.changeEventsForStorage(storageB), [ { accessTime: 20, type: Protocol.Storage.SharedStorageAccessType.DocumentClear, mainFrameId: MAIN_FRAME_ID, params: {} as Protocol.Storage.SharedStorageAccessParams, }, ]); const storageC = sharedStorageModel.storageForOrigin(TEST_ORIGIN_C); assert.exists(storageC); assert.deepEqual(listener.changeEventsForStorage(storageC), [ { accessTime: 100, type: Protocol.Storage.SharedStorageAccessType.WorkletSet, mainFrameId: MAIN_FRAME_ID, params: {key: 'key0', value: 'value1', ignoreIfPresent: true} as Protocol.Storage.SharedStorageAccessParams, }, ]); }); });