diff --git a/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap b/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap new file mode 100644 index 0000000000..2d5653372c --- /dev/null +++ b/packages/core/src/features/catalog/__snapshots__/entity-running.test.tsx.snap @@ -0,0 +1,1608 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`entity running technical tests when navigated to catalog renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ca +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 1 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`entity running technical tests when navigated to catalog when details panel is opened renders 1`] = ` + +
+
+
+
+
+ + + home + + +
+
+
+ + + arrow_back + + +
+
+
+ + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ca +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 1 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ Mock: a catalog entity + + + content_copy + + +
+ Copy +
+
+ + + close + + +
+ Close +
+
+
+
+
+
+ ace +
+
+ Click to open +
+
+ +
+
+
+
+
+
+ +`; diff --git a/packages/core/src/features/catalog/entity-running.test.tsx b/packages/core/src/features/catalog/entity-running.test.tsx new file mode 100644 index 0000000000..36947a7d60 --- /dev/null +++ b/packages/core/src/features/catalog/entity-running.test.tsx @@ -0,0 +1,232 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import asyncFn, { type AsyncFnMock } from "@async-fn/jest"; +import type { DiContainer } from "@ogre-tools/injectable"; +import type { RenderResult } from "@testing-library/react"; +import appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable"; +import type { AppEvent } from "../../common/app-event-bus/event-bus"; +import type { CatalogEntityActionContext } from "../../common/catalog"; +import { CatalogCategory, categoryVersion, CatalogEntity } from "../../common/catalog"; +import catalogCategoryRegistryInjectable from "../../common/catalog/category-registry.injectable"; +import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import { flushPromises } from "../../common/test-utils/flush-promises"; +import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time"; +import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../renderer/api/catalog/entity/registry"; +import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; + +class MockCatalogCategory extends CatalogCategory { + apiVersion = "catalog.k8slens.dev/v1alpha1"; + kind = "CatalogCategory"; + metadata = { + name: "mock", + icon: "gear", + }; + spec = { + group: "entity.k8slens.dev", + versions: [ + categoryVersion("v1alpha1", (() => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + return function (data: any) { + const entity = new MockCatalogEntity(data); + + entity.onRun = self.onRun; + + return entity; + } as any; + })()), + ], + names: { + kind: "Mock", + }, + }; + + constructor(private onRun: (context: CatalogEntityActionContext) => void | Promise) { + super(); + } +} + +class MockCatalogEntity extends CatalogEntity { + public apiVersion = "entity.k8slens.dev/v1alpha1"; + public kind = "Mock"; +} + +function createMockCatalogEntity() { + return new MockCatalogEntity({ + metadata: { + uid: "a_catalogEntity_uid", + name: "a catalog entity", + labels: { + test: "label", + }, + }, + status: { + phase: "", + }, + spec: {}, + }); +} + +describe("entity running technical tests", () => { + let builder: ApplicationBuilder; + let windowDi: DiContainer; + let rendered: RenderResult; + let appEventListener: jest.MockedFunction<(event: AppEvent) => void>; + let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise>; + let catalogEntityRegistry: CatalogEntityRegistry; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + builder.afterWindowStart((windowDi) => { + onRun = jest.fn(); + + const catalogCategoryRegistery = windowDi.inject(catalogCategoryRegistryInjectable); + + catalogCategoryRegistery.add(new MockCatalogCategory(onRun)); + + catalogEntityRegistry = windowDi.inject(catalogEntityRegistryInjectable); + + const catalogEntityItem = createMockCatalogEntity(); + + catalogEntityRegistry.updateItems([catalogEntityItem]); + + appEventListener = jest.fn(); + windowDi.inject(appEventBusInjectable).addListener(appEventListener); + }); + + testUsingFakeTime(); + rendered = await builder.render(); + windowDi = builder.applicationWindow.only.di; + }); + + describe("when navigated to catalog", () => { + beforeEach(() => { + const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable); + + navigateToCatalog(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("when details panel is opened", () => { + beforeEach(() => { + rendered.getByTestId("icon-for-menu-actions-for-catalog-for-a_catalogEntity_uid").click(); + advanceFakeTime(500); + rendered.getByTestId("open-details-menu-item-for-a_catalogEntity_uid").click(); + advanceFakeTime(500); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => { + let onBeforeRunMock: AsyncFnMock; + + beforeEach(() => { + onBeforeRunMock = asyncFn(); + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + }); + + it("calls on before run event", () => { + const target = onBeforeRunMock.mock.calls[0][0].target; + const actual = { id: target.getId(), name: target.getName() }; + + expect(actual).toEqual({ + id: "a_catalogEntity_uid", + name: "a catalog entity", + }); + }); + + it("does not call onRun yet", () => { + expect(onRun).not.toHaveBeenCalled(); + }); + + it("when before run event resolves, calls onRun", async () => { + await onBeforeRunMock.resolve(); + + expect(onRun).toHaveBeenCalled(); + }); + }); + + it("onBeforeRun prevents event => onRun wont be triggered", async () => { + const onBeforeRunMock = jest.fn((event) => event.preventDefault()); + + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); + + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + + expect(onRun).not.toHaveBeenCalled(); + }); + + it("addOnBeforeRun throw an exception => onRun will be triggered", async () => { + catalogEntityRegistry.addOnBeforeRun(() => { + throw new Error("some error"); + }); + + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + + await flushPromises(); + + expect(onRun).toHaveBeenCalled(); + }); + + it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => { + onRun.mockImplementation(() => done()); + + catalogEntityRegistry.addOnBeforeRun(async () => {}); + + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + }); + + it("addOnRunHook return a promise and prevents event wont be triggered", async () => { + catalogEntityRegistry.addOnBeforeRun(async (event) => event.preventDefault()); + + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + + expect(onRun).not.toHaveBeenCalled(); + }); + + it("addOnRunHook return a promise and reject => onRun will be triggered", async () => { + const onBeforeRunMock = asyncFn(); + + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); + + rendered.getByTestId("detail-panel-hot-bar-icon").click(); + + await onBeforeRunMock.reject(); + + expect(onRun).toHaveBeenCalled(); + }); + + it("emits catalog open AppEvent", () => { + expect(appEventListener).toHaveBeenCalledWith( { + action: "open", + name: "catalog", + }); + }); + + it("emits catalog change AppEvent when changing the category", () => { + rendered.getByText("Web Links").click(); + + expect(appEventListener).toHaveBeenCalledWith({ + action: "change-category", + name: "catalog", + params: { + category: "Web Links", + }, + }); + }); + }); + }); +}); diff --git a/packages/core/src/renderer/api/catalog/entity/registry.ts b/packages/core/src/renderer/api/catalog/entity/registry.ts index 9971be8462..5a04359e28 100644 --- a/packages/core/src/renderer/api/catalog/entity/registry.ts +++ b/packages/core/src/renderer/api/catalog/entity/registry.ts @@ -23,7 +23,7 @@ export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promis interface Dependencies { navigate: Navigate; readonly categoryRegistry: CatalogCategoryRegistry; - logger: Logger; + readonly logger: Logger; } export class CatalogEntityRegistry { @@ -243,20 +243,22 @@ export class CatalogEntityRegistry { * Perform the onBeforeRun check and, if successful, then proceed to call `entity`'s onRun method * @param entity The instance to invoke the hooks and then execute the onRun */ - onRun(entity: CatalogEntity): void { - this.onBeforeRun(entity) - .then(doOnRun => { - if (doOnRun) { - return entity.onRun?.({ - navigate: this.dependencies.navigate, - setCommandPaletteContext: (entity) => { - this.activeEntity = entity; - }, - }); - } else { - this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`); - } - }) - .catch(error => this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error)); + async onRun(entity: CatalogEntity) { + try { + const doOnRun = await this.onBeforeRun(entity); + + if (!doOnRun) { + this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`); + } + + await entity.onRun?.({ + navigate: this.dependencies.navigate, + setCommandPaletteContext: (entity) => { + this.activeEntity = entity; + }, + }); + } catch (error) { + this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error); + } } } diff --git a/packages/core/src/renderer/components/+catalog/catalog.test.tsx b/packages/core/src/renderer/components/+catalog/catalog.test.tsx deleted file mode 100644 index ec35fb1d4a..0000000000 --- a/packages/core/src/renderer/components/+catalog/catalog.test.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import { screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Catalog } from "./catalog"; -import type { CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; -import { CatalogEntity } from "../../../common/catalog"; -import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../api/catalog/entity/registry"; -import type { CatalogEntityStore } from "./catalog-entity-store.injectable"; -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import type { DiContainer } from "@ogre-tools/injectable"; -import catalogEntityStoreInjectable from "./catalog-entity-store.injectable"; -import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; -import type { DiRender } from "../test-utils/renderFor"; -import { renderFor } from "../test-utils/renderFor"; -import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; -import type { AppEvent } from "../../../common/app-event-bus/event-bus"; -import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; -import { computed } from "mobx"; -import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; -import type { AsyncFnMock } from "@async-fn/jest"; -import asyncFn from "@async-fn/jest"; -import { flushPromises } from "../../../common/test-utils/flush-promises"; -import userStoreInjectable from "../../../common/user-store/user-store.injectable"; -import releaseChannelInjectable from "../../../common/vars/release-channel.injectable"; -import defaultUpdateChannelInjectable from "../../../features/application-update/common/selected-update-channel/default-update-channel.injectable"; -import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable"; - -class MockCatalogEntity extends CatalogEntity { - public apiVersion = "api"; - public kind = "kind"; - - constructor(data: CatalogEntityData, public onRun: (context: CatalogEntityActionContext) => void | Promise) { - super(data); - } -} - -function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise) { - return new MockCatalogEntity({ - metadata: { - uid: "a_catalogEntity_uid", - name: "a catalog entity", - labels: { - test: "label", - }, - }, - status: { - phase: "", - }, - spec: {}, - }, onRun); -} - -describe("", () => { - let di: DiContainer; - let catalogEntityStore: CatalogEntityStore; - let catalogEntityRegistry: CatalogEntityRegistry; - let appEventListener: jest.MockedFunction<(event: AppEvent) => void>; - let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise>; - let catalogEntityItem: MockCatalogEntity; - let render: DiRender; - - beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); - - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - di.override(currentlyInClusterFrameInjectable, () => false); - di.override(broadcastMessageInjectable, () => async () => {}); - - di.permitSideEffects(getConfigurationFileModelInjectable); - - render = renderFor(di); - onRun = jest.fn(); - catalogEntityItem = createMockCatalogEntity(onRun); - catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); - - appEventListener = jest.fn(); - di.inject(appEventBusInjectable).addListener(appEventListener); - - catalogEntityStore = di.inject(catalogEntityStoreInjectable); - Object.assign(catalogEntityStore, { - selectedItem: computed(() => catalogEntityItem), - }); - - di.override(releaseChannelInjectable, () => ({ - get: () => "latest" as const, - init: async () => {}, - })); - await di.inject(defaultUpdateChannelInjectable).init(); - di.inject(userStoreInjectable).load(); - }); - - describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => { - let onBeforeRunMock: AsyncFnMock; - - beforeEach(() => { - onBeforeRunMock = asyncFn(); - - catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - }); - - it("calls on before run event", () => { - const target = onBeforeRunMock.mock.calls[0][0].target; - - const actual = { id: target.getId(), name: target.getName() }; - - expect(actual).toEqual({ - id: "a_catalogEntity_uid", - name: "a catalog entity", - }); - }); - - it("does not call onRun yet", () => { - expect(onRun).not.toHaveBeenCalled(); - }); - - it("when before run event resolves, calls onRun", async () => { - await onBeforeRunMock.resolve(); - - expect(onRun).toHaveBeenCalled(); - }); - }); - - it("onBeforeRun prevents event => onRun wont be triggered", async () => { - const onBeforeRunMock = jest.fn((event) => event.preventDefault()); - - catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - - await flushPromises(); - - expect(onRun).not.toHaveBeenCalled(); - }); - - it("addOnBeforeRun throw an exception => onRun will be triggered", async () => { - const onBeforeRunMock = jest.fn(() => { - throw new Error("some error"); - }); - - catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - - await flushPromises(); - - expect(onRun).toHaveBeenCalled(); - }); - - it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => { - onRun.mockImplementation(() => done()); - - catalogEntityRegistry.addOnBeforeRun( - async () => { - // no op - }, - ); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - }); - - it("addOnRunHook return a promise and prevents event wont be triggered", async () => { - const onBeforeRunMock = asyncFn(); - - catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - - onBeforeRunMock.mock.calls[0][0].preventDefault(); - - await onBeforeRunMock.resolve(); - - expect(onRun).not.toHaveBeenCalled(); - }); - - it("addOnRunHook return a promise and reject => onRun will be triggered", async () => { - const onBeforeRunMock = asyncFn(); - - catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - - await onBeforeRunMock.reject(); - - expect(onRun).toHaveBeenCalled(); - }); - - it("emits catalog open AppEvent", () => { - render(); - - expect(appEventListener).toHaveBeenCalledWith( { - action: "open", - name: "catalog", - }); - }); - - it("emits catalog change AppEvent when changing the category", () => { - render(); - - userEvent.click(screen.getByText("Web Links")); - - expect(appEventListener).toHaveBeenCalledWith({ - action: "change-category", - name: "catalog", - params: { - category: "Web Links", - }, - }); - }); -});