UNPKG

chrome-devtools-frontend

Version:
1,132 lines (949 loc) • 43.3 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 type * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import { dispatchClickEvent, dispatchKeyDownEvent, getCleanTextContentFromElements, raf, } from '../../testing/DOMHelpers.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import { describeWithMockConnection, } from '../../testing/MockConnection.js'; import {getCellElementFromNodeAndColumnId, selectNodeByKey} from '../../testing/StorageItemsViewHelpers.js'; import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js'; import type * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Resources from './application.js'; import type * as ApplicationComponents from './components/components.js'; import View = Resources.SharedStorageItemsView; class SharedStorageItemsListener { #dispatcher: Common.ObjectWrapper.ObjectWrapper<View.SharedStorageItemsDispatcher.EventTypes>; #cleared: boolean = false; #filteredCleared: boolean = false; #refreshed: boolean = false; #deletedKeys: Array<String> = []; #editedEvents: Array<View.SharedStorageItemsDispatcher.ItemEditedEvent> = []; constructor(dispatcher: Common.ObjectWrapper.ObjectWrapper<View.SharedStorageItemsDispatcher.EventTypes>) { this.#dispatcher = dispatcher; this.#dispatcher.addEventListener(View.SharedStorageItemsDispatcher.Events.ITEMS_CLEARED, this.#itemsCleared, this); this.#dispatcher.addEventListener( View.SharedStorageItemsDispatcher.Events.FILTERED_ITEMS_CLEARED, this.#filteredItemsCleared, this); this.#dispatcher.addEventListener( View.SharedStorageItemsDispatcher.Events.ITEMS_REFRESHED, this.#itemsRefreshed, this); this.#dispatcher.addEventListener(View.SharedStorageItemsDispatcher.Events.ITEM_DELETED, this.#itemDeleted, this); this.#dispatcher.addEventListener(View.SharedStorageItemsDispatcher.Events.ITEM_EDITED, this.#itemEdited, this); } dispose(): void { this.#dispatcher.removeEventListener( View.SharedStorageItemsDispatcher.Events.ITEMS_CLEARED, this.#itemsCleared, this); this.#dispatcher.removeEventListener( View.SharedStorageItemsDispatcher.Events.FILTERED_ITEMS_CLEARED, this.#filteredItemsCleared, this); this.#dispatcher.removeEventListener( View.SharedStorageItemsDispatcher.Events.ITEMS_REFRESHED, this.#itemsRefreshed, this); this.#dispatcher.removeEventListener( View.SharedStorageItemsDispatcher.Events.ITEM_DELETED, this.#itemDeleted, this); this.#dispatcher.removeEventListener(View.SharedStorageItemsDispatcher.Events.ITEM_EDITED, this.#itemEdited, this); } get deletedKeys(): Array<String> { return this.#deletedKeys; } get editedEvents(): Array<View.SharedStorageItemsDispatcher.ItemEditedEvent> { return this.#editedEvents; } resetRefreshed(): void { this.#refreshed = false; } #itemsCleared(): void { this.#cleared = true; } #filteredItemsCleared(): void { this.#filteredCleared = true; } #itemsRefreshed(): void { this.#refreshed = true; } #itemDeleted(event: Common.EventTarget.EventTargetEvent<View.SharedStorageItemsDispatcher.ItemDeletedEvent>): void { this.#deletedKeys.push(event.data.key); } #itemEdited(event: Common.EventTarget.EventTargetEvent<View.SharedStorageItemsDispatcher.ItemEditedEvent>): void { this.#editedEvents.push(event.data); } async waitForItemsCleared(): Promise<void> { if (!this.#cleared) { await this.#dispatcher.once(View.SharedStorageItemsDispatcher.Events.ITEMS_CLEARED); } this.#cleared = true; } async waitForFilteredItemsCleared(): Promise<void> { if (!this.#filteredCleared) { await this.#dispatcher.once(View.SharedStorageItemsDispatcher.Events.FILTERED_ITEMS_CLEARED); } this.#filteredCleared = true; } async waitForItemsRefreshed(): Promise<void> { if (!this.#refreshed) { await this.#dispatcher.once(View.SharedStorageItemsDispatcher.Events.ITEMS_REFRESHED); } this.#refreshed = true; } async waitForItemsDeletedTotal(total: number): Promise<void> { while (this.#deletedKeys.length < total) { await this.#dispatcher.once(View.SharedStorageItemsDispatcher.Events.ITEM_DELETED); } } async waitForItemsEditedTotal(total: number): Promise<void> { while (this.#editedEvents.length < total) { await this.#dispatcher.once(View.SharedStorageItemsDispatcher.Events.ITEM_EDITED); } } } describeWithMockConnection('SharedStorageItemsView', function() { let target: SDK.Target.Target; let sharedStorageModel: Resources.SharedStorageModel.SharedStorageModel|null; let sharedStorage: Resources.SharedStorageModel.SharedStorageForOrigin; const TEST_ORIGIN = 'http://a.test'; const METADATA = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 3, remainingBudget: 2.5, bytesUsed: 30, } as Protocol.Storage.SharedStorageMetadata; const METADATA_NO_ENTRIES = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 0, remainingBudget: 2.5, bytesUsed: 0, } as Protocol.Storage.SharedStorageMetadata; const METADATA_2_ENTRIES = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 2, remainingBudget: 2.5, bytesUsed: 20, } as Protocol.Storage.SharedStorageMetadata; const METADATA_4_ENTRIES = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 4, remainingBudget: 2.5, bytesUsed: 38, } as Protocol.Storage.SharedStorageMetadata; const ENTRIES = [ { key: 'key1', value: 'a', } as Protocol.Storage.SharedStorageEntry, { key: 'key2', value: 'b', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_1 = [ { key: 'key2', value: 'b', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_2 = [ { key: 'key1', value: 'a', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_KEY_EDITED_1 = [ { key: 'key1', value: 'a', } as Protocol.Storage.SharedStorageEntry, { key: 'key0', value: 'b', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_KEY_EDITED_2 = [ { key: 'key1', value: 'b', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_VALUE_EDITED = [ { key: 'key1', value: 'a', } as Protocol.Storage.SharedStorageEntry, { key: 'key2', value: 'd', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, ]; const ENTRIES_NEW_KEY = [ { key: 'key1', value: 'a', } as Protocol.Storage.SharedStorageEntry, { key: 'key2', value: 'b', } as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as Protocol.Storage.SharedStorageEntry, { key: 'key4', value: '', } as Protocol.Storage.SharedStorageEntry, ]; beforeEach(() => { target = createTarget(); sharedStorageModel = target.model(Resources.SharedStorageModel.SharedStorageModel); assert.exists(sharedStorageModel); sharedStorage = new Resources.SharedStorageModel.SharedStorageForOrigin(sharedStorageModel, TEST_ORIGIN); assert.strictEqual(sharedStorage.securityOrigin, TEST_ORIGIN); }); it('displays metadata and entries', async () => { assert.exists(sharedStorageModel); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ metadata: METADATA, getError: () => undefined, }); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ entries: ENTRIES, getError: () => undefined, }); const view = await View.SharedStorageItemsView.createView(sharedStorage); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.deepEqual(view.getEntriesForTesting(), ENTRIES); const metadataView = view.innerSplitWidget.sidebarWidget()?.contentElement.firstChild as ApplicationComponents.SharedStorageMetadataView.SharedStorageMetadataView; assert.exists(metadataView); assert.isNotNull(metadataView.shadowRoot); await RenderCoordinator.done(); const keys = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-key'); assert.deepEqual(keys, [ 'Origin', 'Creation Time', 'Number of Entries', 'Number of Bytes Used', 'Entropy Budget for Fenced Frames', ]); const values = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-value'); assert.deepEqual(values, [ TEST_ORIGIN, (new Date(100 * 1e3)).toLocaleString(), '3', '30', '2.5', ]); view.detach(); }); it('displays metadata with placeholder message if origin is not using API', async () => { assert.exists(sharedStorageModel); sinon.stub(sharedStorage, 'getMetadata').resolves(null); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ entries: [], getError: () => undefined, }); const view = await View.SharedStorageItemsView.createView(sharedStorage); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.lengthOf(view.getEntriesForTesting(), 0); const metadataView = view.innerSplitWidget.sidebarWidget()?.contentElement.firstChild as ApplicationComponents.SharedStorageMetadataView.SharedStorageMetadataView; assert.exists(metadataView); assert.isNotNull(metadataView.shadowRoot); await RenderCoordinator.done(); const keys = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-key'); assert.deepEqual(keys, [ 'Origin', 'Creation Time', 'Number of Entries', 'Number of Bytes Used', 'Entropy Budget for Fenced Frames', ]); const values = getCleanTextContentFromElements(metadataView.shadowRoot, 'devtools-report-value'); assert.deepEqual(values, [ TEST_ORIGIN, 'Not yet created', '0', '0', '0', ]); view.detach(); }); it('has placeholder sidebar when there are no entries', async () => { assert.exists(sharedStorageModel); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ metadata: METADATA_NO_ENTRIES, getError: () => undefined, }); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ entries: [], getError: () => undefined, }); const view = await View.SharedStorageItemsView.createView(sharedStorage); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.notInstanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); assert.exists(view.contentElement.querySelector('.empty-state')); view.detach(); }); it('updates sidebarWidget upon receiving SelectedNode Event', async () => { assert.exists(sharedStorageModel); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ metadata: METADATA, getError: () => undefined, }); sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries') .withArgs({ownerOrigin: TEST_ORIGIN}) .resolves({ entries: ENTRIES, getError: () => undefined, }); const view = await View.SharedStorageItemsView.createView(sharedStorage); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); view.markAsRoot(); view.show(document.body); await refreshedPromise; // Select the second row. assert.exists(selectNodeByKey(view.dataGrid, 'key2')); await raf(); assert.instanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); view.detach(); }); it('refreshes when "Refresh" is clicked', async () => { assert.exists(sharedStorageModel); 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, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise1 = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise1; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Clicking "Refresh" will cause `getMetadata()` and `getEntries()` to be called. itemsListener.resetRefreshed(); const refreshedPromise2 = itemsListener.waitForItemsRefreshed(); dispatchClickEvent(view.refreshButton.element); await raf(); await refreshedPromise2; assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); view.detach(); }); it('clears entries when "Delete All" is clicked', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA_NO_ENTRIES, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: [], getError: () => undefined, }); const clearSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_clearSharedStorageEntries').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Clicking "Delete All" will cause `clear()`, `getMetadata()`, and `getEntries()` to be called. const clearedPromise = itemsListener.waitForItemsCleared(); dispatchClickEvent(view.deleteAllButton.element); await raf(); await clearedPromise; assert.isTrue(clearSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), []); view.detach(); }); it('clears filtered entries when "Delete All" is clicked with a filter set', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(3).resolves({ metadata: METADATA_2_ENTRIES, getError: () => undefined, }); getMetadataSpy.onCall(4).resolves({ metadata: METADATA_2_ENTRIES, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(2).resolves({ entries: ENTRIES_2, getError: () => undefined, }); getEntriesSpy.onCall(3).resolves({ entries: ENTRIES_2, getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise1 = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise1; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Adding a filter to the text box will cause `getMetadata()`, and `getEntries()` to be called. itemsListener.resetRefreshed(); const refreshedPromise2 = itemsListener.waitForItemsRefreshed(); view.filterItem.dispatchEventToListeners(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, 'b'); await raf(); await refreshedPromise2; assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); // Only the filtered entries are displayed. assert.deepEqual(view.getEntriesForTesting(), ENTRIES_1); // Clicking "Delete All" will cause `deleteEntry()`, `getMetadata()`, and `getEntries()` to be called. const clearedPromise = itemsListener.waitForFilteredItemsCleared(); dispatchClickEvent(view.deleteAllButton.element); await raf(); await clearedPromise; assert.isTrue(deleteEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key2'})); assert.strictEqual(getMetadataSpy.callCount, 4); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledThrice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); // The filtered entries are cleared. assert.deepEqual(view.getEntriesForTesting(), []); // Changing the filter in the text box will cause `getMetadata()`, and `getEntries()` to be called. itemsListener.resetRefreshed(); const refreshedPromise3 = itemsListener.waitForItemsRefreshed(); view.filterItem.dispatchEventToListeners(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, ''); await raf(); await refreshedPromise3; assert.strictEqual(getMetadataSpy.callCount, 5); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.strictEqual(getEntriesSpy.callCount, 4); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES_2); view.detach(); }); it('deletes selected entry when "Delete Selected" is clicked', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA_2_ENTRIES, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: [], getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the second row. assert.exists(selectNodeByKey(view.dataGrid, 'key2')); await raf(); // Clicking "Delete Selected" will cause `deleteEntry()`, `getMetadata()`, and `getEntries()` to be called. const deletedPromise = itemsListener.waitForItemsDeletedTotal(1); dispatchClickEvent(view.deleteSelectedButton.element); await raf(); await deletedPromise; assert.isTrue(deleteEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key2'})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), []); assert.deepEqual(itemsListener.deletedKeys, ['key2']); view.detach(); }); it('edits key of selected entry to a non-preexisting key', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: ENTRIES_KEY_EDITED_1, getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the second row. const node = selectNodeByKey(view.dataGrid, 'key2'); assert.exists(node); await raf(); const selectedNode = node as DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>; view.dataGrid.startEditingNextEditableColumnOfDataGridNode(selectedNode, 'key', true); const cellElement = getCellElementFromNodeAndColumnId(view.dataGrid, selectedNode, 'key'); assert.exists(cellElement); // Editing a key will cause `deleteEntry()`, `setEntry()`, `getMetadata()`, and `getEntries()` to be called. const editedPromise = itemsListener.waitForItemsEditedTotal(1); cellElement.textContent = 'key0'; dispatchKeyDownEvent(cellElement, {key: 'Enter'}); await raf(); await editedPromise; assert.isTrue(deleteEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key2'})); assert.isTrue( setEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key0', value: 'b', ignoreIfPresent: false})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES_KEY_EDITED_1); assert.deepEqual(itemsListener.editedEvents, [ {columnIdentifier: 'key', oldText: 'key2', newText: 'key0'} as View.SharedStorageItemsDispatcher.ItemEditedEvent, ]); view.detach(); }); it('edits key of selected entry to a preexisting key', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA_2_ENTRIES, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: ENTRIES_KEY_EDITED_2, getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the second row. const node = selectNodeByKey(view.dataGrid, 'key2'); assert.exists(node); await raf(); const selectedNode = node as DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>; view.dataGrid.startEditingNextEditableColumnOfDataGridNode(selectedNode, 'key', true); const cellElement = getCellElementFromNodeAndColumnId(view.dataGrid, selectedNode, 'key'); assert.exists(cellElement); // Editing a key will cause `deleteEntry()`, `setEntry()`, `getMetadata()`, and `getEntries()` to be called. const editedPromise = itemsListener.waitForItemsEditedTotal(1); cellElement.textContent = 'key1'; dispatchKeyDownEvent(cellElement, {key: 'Enter'}); await raf(); await editedPromise; assert.isTrue(deleteEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key2'})); assert.isTrue( setEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key1', value: 'b', ignoreIfPresent: false})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES_KEY_EDITED_2); assert.deepEqual(itemsListener.editedEvents, [ {columnIdentifier: 'key', oldText: 'key2', newText: 'key1'} as View.SharedStorageItemsDispatcher.ItemEditedEvent, ]); // Verify that the preview loads. assert.instanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); view.detach(); }); it('edits value of selected entry to a new value', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: ENTRIES_VALUE_EDITED, getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the second row. const node = selectNodeByKey(view.dataGrid, 'key2'); assert.exists(node); await raf(); const selectedNode = node as DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>; view.dataGrid.startEditingNextEditableColumnOfDataGridNode(selectedNode, 'value', true); const cellElement = getCellElementFromNodeAndColumnId(view.dataGrid, selectedNode, 'value'); assert.exists(cellElement); // Editing a value will cause `setEntry()`, `getMetadata()`, and `getEntries()` to be called. const editedPromise = itemsListener.waitForItemsEditedTotal(1); cellElement.textContent = 'd'; dispatchKeyDownEvent(cellElement, {key: 'Enter'}); await raf(); await editedPromise; assert.isTrue(deleteEntrySpy.notCalled); assert.isTrue( setEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key2', value: 'd', ignoreIfPresent: false})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES_VALUE_EDITED); assert.deepEqual(itemsListener.editedEvents, [ {columnIdentifier: 'value', oldText: 'b', newText: 'd'} as View.SharedStorageItemsDispatcher.ItemEditedEvent, ]); // Verify that the preview loads. assert.instanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); view.detach(); }); it('adds an entry when the key cell of the empty data row is edited', async () => { assert.exists(sharedStorageModel); const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata'); getMetadataSpy.onCall(0).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(1).resolves({ metadata: METADATA, getError: () => undefined, }); getMetadataSpy.onCall(2).resolves({ metadata: METADATA_4_ENTRIES, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries'); getEntriesSpy.onCall(0).resolves({ entries: ENTRIES, getError: () => undefined, }); getEntriesSpy.onCall(1).resolves({ entries: ENTRIES_NEW_KEY, getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the empty (null) row. const node = selectNodeByKey(view.dataGrid, null); assert.exists(node); await raf(); const selectedNode = node as DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>; view.dataGrid.startEditingNextEditableColumnOfDataGridNode(selectedNode, 'key', true); const cellElement = getCellElementFromNodeAndColumnId(view.dataGrid, selectedNode, 'key'); assert.exists(cellElement); // Editing a key will cause `setEntry()`, `getMetadata()`, and `getEntries()` to be called. const editedPromise = itemsListener.waitForItemsEditedTotal(1); cellElement.textContent = 'key4'; dispatchKeyDownEvent(cellElement, {key: 'Enter'}); await raf(); await editedPromise; assert.isTrue(deleteEntrySpy.notCalled); assert.isTrue( setEntrySpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN, key: 'key4', value: '', ignoreIfPresent: false})); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES_NEW_KEY); assert.deepEqual(itemsListener.editedEvents, [ {columnIdentifier: 'key', oldText: null, newText: 'key4'}, ]); // Verify that the preview loads. assert.instanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); view.detach(); }); it('attempting to edit key of selected entry to an empty key cancels the edit', async () => { assert.exists(sharedStorageModel); 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 deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); // Creating will cause `getMetadata()` to be called. const view = await View.SharedStorageItemsView.createView(sharedStorage); await RenderCoordinator.done({waitForWork: true}); assert.isTrue(getMetadataSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); const itemsListener = new SharedStorageItemsListener(view.sharedStorageItemsDispatcher); const refreshedPromise1 = itemsListener.waitForItemsRefreshed(); // Showing will cause `getMetadata()` and `getEntries()` to be called. view.markAsRoot(); view.show(document.body); await refreshedPromise1; assert.isTrue(getMetadataSpy.calledTwice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledOnceWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Select the second row. const node = selectNodeByKey(view.dataGrid, 'key2'); assert.exists(node); await raf(); const selectedNode = node as DataGrid.DataGrid.DataGridNode<Protocol.Storage.SharedStorageEntry>; view.dataGrid.startEditingNextEditableColumnOfDataGridNode(selectedNode, 'key', true); const cellElement = getCellElementFromNodeAndColumnId(view.dataGrid, selectedNode, 'key'); assert.exists(cellElement); // Editing a key with the edit canceled will cause `getMetadata()` and `getEntries()` to be called. itemsListener.resetRefreshed(); const refreshedPromise2 = itemsListener.waitForItemsRefreshed(); cellElement.textContent = ''; dispatchKeyDownEvent(cellElement, {key: 'Enter'}); await raf(); await refreshedPromise2; assert.isTrue(deleteEntrySpy.notCalled); assert.isTrue(setEntrySpy.notCalled); assert.isTrue(getMetadataSpy.calledThrice); assert.isTrue(getMetadataSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.isTrue(getEntriesSpy.calledTwice); assert.isTrue(getEntriesSpy.alwaysCalledWithExactly({ownerOrigin: TEST_ORIGIN})); assert.deepEqual(view.getEntriesForTesting(), ENTRIES); // Verify that the preview loads. assert.instanceOf(view.outerSplitWidget.sidebarWidget(), UI.SearchableView.SearchableView); view.detach(); }); });