diff --git a/src/renderer/components/dock/__test__/dock-tab.store.test.ts b/src/renderer/components/dock/__test__/dock-tab.store.test.ts new file mode 100644 index 0000000000..f78160518c --- /dev/null +++ b/src/renderer/components/dock/__test__/dock-tab.store.test.ts @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { jest } from "@jest/globals"; +import { observable, reaction, toJS } from "mobx"; +import { StorageHelper } from "../../../utils"; +import { DockTabStore } from "../dock-tab.store"; +import type { TabId } from "../dock.store"; + +type UtilsModule = typeof import("../../../utils"); + +jest.mock("../../../utils", () => { + const localStorageMock = observable.map({}); + const Utils = jest.requireActual("../../../utils") as UtilsModule; + + // Partial module mocking + // https://jestjs.io/docs/mock-functions#mocking-partials + return { + ...Utils, + createStorage: jest.fn((key: string, defaultValue: unknown) => { + return new StorageHelper(key, { + defaultValue, + storage: { + getItem(key: TabId) { + localStorageMock.get(key); + }, + setItem(key: TabId, value: unknown) { + localStorageMock.set(key, value); + }, + removeItem(key: TabId) { + localStorageMock.delete(key); + }, + }, + }); + }), + }; +}); + +// TODO: add more tests +describe(`[DockTabStore]: dock tabs data class-helper`, () => { + test("options.autoInit = true (default)", async () => { + const tabStorage = new DockTabStore({ autoInit: true }); + + expect(tabStorage.initialized).toBeFalsy(); + expect(tabStorage.dataReady).toBeFalsy(); + await tabStorage.whenReady; + + expect(tabStorage.dataReady).toBeTruthy(); + expect(tabStorage.initialized).toBeTruthy(); + }); + + test("options.autoInit = false", async () => { + const tabStorage = new DockTabStore({ autoInit: false }); + + await tabStorage.whenReady; + expect(tabStorage.dataReady).toBeTruthy(); + expect(tabStorage.initialized).toBeFalsy(); + + await tabStorage.init(); + expect(tabStorage.initialized).toBeTruthy(); + }); + + test(`dockTabStore.getData(tabId) is deeply observable`, async () => { + interface TabDataStorageShape { + [key: string]: any; + account: { + name: string; + description?: string; + } + } + + const tabDataHistory: TabDataStorageShape[] = []; + const randomTabId = getRandomTabId(); + const tabStorage = new DockTabStore(); + + await tabStorage.whenReady; + + reaction(() => toJS(tabStorage.getData(randomTabId)), + data => tabDataHistory.push(data), + ); + + tabStorage.setData(randomTabId, { account: { name: "test" } }); + + expect(tabStorage.getData(randomTabId)).toBe(tabStorage.data[randomTabId]); + expect(tabStorage.getData(randomTabId).account.name).toBe("test"); + tabStorage.data[randomTabId].account.name = "updated"; // update deep in the tree + + expect(tabDataHistory.length).toBe(2); + expect(tabDataHistory[1].account.name).toBe("updated"); + }); + +}); + +export function getRandomTabId(): TabId { + return `tab-id-${(Math.random() * Date.now()).toString(16)}`; +} + diff --git a/src/renderer/components/dock/create-resource.store.ts b/src/renderer/components/dock/create-resource.store.ts index 3196475be7..e0d1c53758 100644 --- a/src/renderer/components/dock/create-resource.store.ts +++ b/src/renderer/components/dock/create-resource.store.ts @@ -57,7 +57,7 @@ export class CreateResourceStore extends DockTabStore { return path.resolve(os.homedir(), "~/.k8slens/templates"); } - protected async init() { + async init() { super.init(); // scan all available templates immediately diff --git a/src/renderer/components/dock/dock-tab.store.ts b/src/renderer/components/dock/dock-tab.store.ts index e19db2adc0..d56e571b60 100644 --- a/src/renderer/components/dock/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab.store.ts @@ -24,18 +24,21 @@ import { autoBind, createStorage, disposer, StorageHelper } from "../../utils"; import { dockStore, TabId } from "./dock.store"; export interface DockTabStoreOptions { - autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true + autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events (default: true) storageKey?: string; // save data to persistent storage under the key } -export class DockTabStore { - @observable.ref private storage?: StorageHelper>; // available only with `options.storageKey` - @observable private _data: Record = {}; +export type DockTabStoreShape = Record; + +export class DockTabStore { + @observable.ref private storage?: StorageHelper>; // available only with `options.storageKey` + @observable private _data: DockTabStoreShape = {}; @observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource + @observable initialized = false; protected dispose = disposer(); - get data() { + get data(): DockTabStoreShape { if (this.options.storageKey) { return this.storage.get(); } @@ -43,7 +46,7 @@ export class DockTabStore { return this._data; } - set data(value: Record) { + set data(value: DockTabStoreShape) { if (this.options.storageKey) { this.storage.set(value); } else { @@ -51,7 +54,7 @@ export class DockTabStore { } } - protected constructor(protected options: DockTabStoreOptions = {}) { + constructor(protected options: DockTabStoreOptions = {}) { makeObservable(this); // must be called *before* autoBind() when used with mobx's method decorators autoBind(this); @@ -74,12 +77,13 @@ export class DockTabStore { return Promise.all([ dockStore.whenReady, this.storage?.whenReady, - ]); + ]).then(() => this.dataReady = true); } @action - protected init() { - this.dataReady = true; + async init(): Promise { + if (this.initialized) return; + this.initialized = true; this.dispose.push( dockStore.onTabClose(({ tabId }) => this.clearData(tabId)), @@ -102,8 +106,9 @@ export class DockTabStore { @action reset() { - this.data = {}; + this.data = {}; // clears json-file storage when initialized with `opts.storageKey` this.dataReady = false; + this.initialized = false; } @action diff --git a/src/renderer/components/dock/edit-resource.store.ts b/src/renderer/components/dock/edit-resource.store.ts index 4673221221..78fc221a99 100644 --- a/src/renderer/components/dock/edit-resource.store.ts +++ b/src/renderer/components/dock/edit-resource.store.ts @@ -40,7 +40,7 @@ export class EditResourceStore extends DockTabStore { autoBind(this); } - protected init() { + async init() { super.init(); this.dispose.push( diff --git a/src/renderer/components/dock/install-chart.store.ts b/src/renderer/components/dock/install-chart.store.ts index 0392a93b97..36bebd6b3b 100644 --- a/src/renderer/components/dock/install-chart.store.ts +++ b/src/renderer/components/dock/install-chart.store.ts @@ -45,7 +45,7 @@ export class InstallChartStore extends DockTabStore { makeObservable(this); } - protected init() { + async init() { super.init(); this.dispose.push( diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts index 7872d95a6d..f39165a278 100644 --- a/src/renderer/components/dock/log-tab.store.ts +++ b/src/renderer/components/dock/log-tab.store.ts @@ -59,7 +59,7 @@ export class LogTabStore extends DockTabStore { ]); } - protected init() { + async init() { super.init(); this.dispose.push( diff --git a/src/renderer/components/dock/terminal-window.scss b/src/renderer/components/dock/terminal-window.scss index e28eea9dfc..7b56b3baa3 100644 --- a/src/renderer/components/dock/terminal-window.scss +++ b/src/renderer/components/dock/terminal-window.scss @@ -25,5 +25,5 @@ flex: 1; height: 100%; overflow: hidden; - margin: 10px; + margin: $padding*2 $padding $padding $padding*2; } \ No newline at end of file diff --git a/src/renderer/components/dock/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart.store.ts index f22453e5d3..dfc785f8b0 100644 --- a/src/renderer/components/dock/upgrade-chart.store.ts +++ b/src/renderer/components/dock/upgrade-chart.store.ts @@ -41,7 +41,7 @@ export class UpgradeChartStore extends DockTabStore { makeObservable(this); } - protected async init() { + async init() { super.init(); await releaseStore.loadFromContextNamespaces();