Skip to main content
Glama
component_actions.test.ts22 kB
import { beforeEach, expect, test, vi } from "vitest"; import { computed, ref } from "vue"; import { ActionKind, ActionState } from "@/api/sdf/dal/action"; import { ActionPrototypeView, BifrostComponent, ActionPrototypeViewList, BifrostActionViewList, } from "@/workers/types/entity_kind_types"; // REQUIRED for all testing import { CONTEXT } from "@/newhotness/testing/context1"; /** * Tests for useComponentActions composable - refreshEnabled logic * * These tests validate the refreshEnabled computed property behavior. * The refreshEnabled property controls visibility of the refresh button on the ComponentDetails * resource panel. */ // Track query responses for each test let mockQueryResponses: Map<string, unknown>; // Mock heimdall using the inner pattern like other tests type HeimdallInner = typeof import("@/store/realtime/heimdall_inner"); vi.mock("@/store/realtime/heimdall", async () => { const inner = await vi.importActual<HeimdallInner>( "@/store/realtime/heimdall_inner", ); return { useMakeKey: () => inner.innerUseMakeKey(CONTEXT.value), useMakeKeyForHead: () => (kind: string, id?: string) => { const ctx = CONTEXT.value; return computed(() => [ ctx.workspacePk.value, ctx.headChangeSetId.value, kind, id ?? ctx.workspacePk.value, ]); }, useMakeArgs: () => inner.innerUseMakeArgs(CONTEXT.value), useMakeArgsForHead: () => (kind: string, id?: string) => { const ctx = CONTEXT.value; return { workspaceId: ctx.workspacePk.value, changeSetId: ctx.headChangeSetId.value, kind, id: id ?? ctx.workspacePk.value, }; }, bifrost: vi.fn(), bifrostExists: vi.fn(), }; }); // Mock vue-router vi.mock("vue-router", () => ({ useRoute: () => ({ params: { workspacePk: "test-workspace", changeSetId: "test-changeset" }, }), })); // Mock the api composables vi.mock("../api_composables", () => ({ routes: { RefreshAction: "refresh-action", ActionCancel: "action-cancel", ActionAdd: "action-add", }, useApi: () => ({ endpoint: vi.fn(() => ({ put: vi.fn(), post: vi.fn(), })), setWatchFn: vi.fn(), bifrosting: ref(false), ok: vi.fn(() => true), navigateToNewChangeSet: vi.fn(), }), })); // Mock @tanstack/vue-query vi.mock("@tanstack/vue-query", () => ({ useQuery: (options: { queryKey: { value: unknown[] }; queryFn: () => Promise<unknown>; enabled?: { value: boolean }; }) => { // Create a simple string key from the query key array const keyArray = options.queryKey.value; const simpleKey = keyArray .map((item) => { // Unwrap refs/computed const unwrapped = item !== null && typeof item === "object" && "value" in item && "effect" in item ? (item as { value: unknown }).value : item; if (typeof unwrapped === "string") return unwrapped; if (typeof unwrapped === "object" && unwrapped !== null) { // Check if it's a WeakReference with an id property if ("id" in unwrapped && typeof unwrapped.id === "string") { return unwrapped.id; } return JSON.stringify(unwrapped); } return String(unwrapped); }) .join("|"); const data = ref(mockQueryResponses.get(simpleKey) ?? null); return { data, isLoading: ref(false), isFetched: ref(true), }; }, })); // Mock context to allow overriding onHead per test let mockOnHead = false; vi.mock("./context", () => ({ useContext: () => { const baseContext = CONTEXT.value; return { ...baseContext, onHead: computed(() => mockOnHead), }; }, })); beforeEach(() => { vi.clearAllMocks(); mockQueryResponses = new Map(); mockOnHead = false; }); /** * The refreshEnabled computed property should return true when ALL of the following are true: * 1. refreshActionPrototype.value exists (the schema variant has a refresh action defined) * 2. component.value?.hasResource is true (the component has a resource) * 3. EITHER: * - ctx.onHead.value is true (viewing component on HEAD), OR * - componentExistsOnHead.value is true (component was applied to HEAD, viewing from change set) */ test("refreshEnabled returns true when on HEAD with resource and refresh action", async () => { // Given: User is on HEAD with a component that has a resource and refresh action mockOnHead = true; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "refresh-prototype", kind: ActionKind.Refresh, name: "Refresh", } as ActionPrototypeView, ], }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [], }; // Set up query responses with full keys including workspace and changeset IDs from CONTEXT const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.headChangeSetId.value}|ComponentInList|test-component`, true, ); const { useComponentActions } = await import("./component_actions"); const { refreshEnabled } = useComponentActions(mockComponent); // Then: refreshEnabled should be true expect(refreshEnabled.value).toBe(true); }); test("refreshEnabled returns true when in change set with component that exists on HEAD", async () => { // Given: User is in a change set, component exists on HEAD with resource and refresh action mockOnHead = false; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "refresh-prototype", kind: ActionKind.Refresh, name: "Refresh", } as ActionPrototypeView, ], }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [], }; // Set up query responses with full keys including workspace and changeset IDs from CONTEXT const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.headChangeSetId.value}|ComponentInList|test-component`, true, // componentExistsOnHead = true ); const { useComponentActions } = await import("./component_actions"); const { refreshEnabled } = useComponentActions(mockComponent); // Then: refreshEnabled should be true expect(refreshEnabled.value).toBe(true); }); test("refreshEnabled returns false when not on HEAD and component doesn't exist on HEAD", async () => { // Given: User is in a change set, component doesn't exist on HEAD yet mockOnHead = false; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "refresh-prototype", kind: ActionKind.Refresh, name: "Refresh", } as ActionPrototypeView, ], }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [], }; // Set up query responses with full keys including workspace and changeset IDs from CONTEXT const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.headChangeSetId.value}|ComponentInList|test-component`, false, // componentExistsOnHead = false ); const { useComponentActions } = await import("./component_actions"); const { refreshEnabled } = useComponentActions(mockComponent); // Then: refreshEnabled should be false (can't refresh a resource that hasn't been applied to HEAD yet) expect(refreshEnabled.value).toBe(false); }); test("refreshEnabled returns false when component has no resource", async () => { // Given: User is on HEAD but component has no resource mockOnHead = true; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: false, // No resource! schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "refresh-prototype", kind: ActionKind.Refresh, name: "Refresh", } as ActionPrototypeView, ], }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [], }; // Set up query responses with full keys including workspace and changeset IDs from CONTEXT const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { refreshEnabled } = useComponentActions(mockComponent); // Then: refreshEnabled should be false (can't refresh a non-existent resource) expect(refreshEnabled.value).toBe(false); }); test("refreshEnabled returns false when no refresh action prototype exists", async () => { // Given: User is on HEAD but schema variant has no refresh action mockOnHead = true; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [], // No refresh action! }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [], }; // Set up query responses with full keys including workspace and changeset IDs from CONTEXT const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { refreshEnabled } = useComponentActions(mockComponent); // Then: refreshEnabled should be false (schema doesn't support refresh) expect(refreshEnabled.value).toBe(false); }); /** * Tests for actionByPrototype change set filtering * * These tests validate that actionByPrototype correctly filters actions based on * their originatingChangeSetId when viewing a change set (not HEAD). */ test("actionByPrototype shows all actions when on HEAD regardless of originatingChangeSetId", async () => { // Given: User is on HEAD with actions from multiple change sets mockOnHead = true; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "create-prototype", kind: ActionKind.Create, name: "Create", } as ActionPrototypeView, { id: "update-prototype", kind: ActionKind.Update, name: "Update", } as ActionPrototypeView, ], }; const mockActionViewList: BifrostActionViewList = { id: CONTEXT.value.changeSetId.value, actions: [ { id: "action-1", prototypeId: "create-prototype", componentId: "test-component", name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: "changeset-1", myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, { id: "action-2", prototypeId: "update-prototype", componentId: "test-component", name: "Update", kind: ActionKind.Update, state: ActionState.Queued, originatingChangeSetId: "changeset-2", myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, ], }; // Set up query responses const ctx = CONTEXT.value; mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { actionByPrototype } = useComponentActions(mockComponent); // Then: Both actions should be visible on HEAD expect(Object.keys(actionByPrototype.value)).toHaveLength(2); expect(actionByPrototype.value["create-prototype"]).toBeDefined(); expect(actionByPrototype.value["update-prototype"]).toBeDefined(); }); test("actionByPrototype filters actions to only show those from current change set", async () => { // Given: User is in a change set with actions from multiple change sets mockOnHead = false; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "create-prototype", kind: ActionKind.Create, name: "Create", } as ActionPrototypeView, { id: "update-prototype", kind: ActionKind.Update, name: "Update", } as ActionPrototypeView, ], }; const ctx = CONTEXT.value; const currentChangeSetId = ctx.changeSetId.value; const mockActionViewList: BifrostActionViewList = { id: currentChangeSetId, actions: [ { id: "action-1", prototypeId: "create-prototype", componentId: "test-component", name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: currentChangeSetId, // From current change set myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, { id: "action-2", prototypeId: "update-prototype", componentId: "test-component", name: "Update", kind: ActionKind.Update, state: ActionState.Queued, originatingChangeSetId: "different-changeset", // From different change set myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, ], }; // Set up query responses mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { actionByPrototype } = useComponentActions(mockComponent); // Then: Only action from current change set should be visible expect(Object.keys(actionByPrototype.value)).toHaveLength(1); const createAction = actionByPrototype.value["create-prototype"]; expect(createAction).toBeDefined(); expect(createAction?.originatingChangeSetId).toBe(currentChangeSetId); expect(actionByPrototype.value["update-prototype"]).toBeUndefined(); }); test("actionByPrototype shows no actions when all actions are from different change sets", async () => { // Given: User is in a change set but all actions are from other change sets mockOnHead = false; const mockComponent = ref<BifrostComponent>({ id: "test-component", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "create-prototype", kind: ActionKind.Create, name: "Create", } as ActionPrototypeView, ], }; const ctx = CONTEXT.value; const mockActionViewList: BifrostActionViewList = { id: ctx.changeSetId.value, actions: [ { id: "action-1", prototypeId: "create-prototype", componentId: "test-component", name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: "different-changeset", // From different change set myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, { id: "action-2", prototypeId: "create-prototype", componentId: "test-component", name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: "another-changeset", // From another different change set myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, ], }; // Set up query responses mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { actionByPrototype } = useComponentActions(mockComponent); // Then: No actions should be visible expect(Object.keys(actionByPrototype.value)).toHaveLength(0); }); test("actionByPrototype correctly filters multiple actions for different components", async () => { // Given: User is in a change set with actions for multiple components mockOnHead = false; const mockComponent = ref<BifrostComponent>({ id: "test-component-1", hasResource: true, schemaVariantId: { id: "test-variant" }, } as BifrostComponent); const mockActionPrototypeViewList: ActionPrototypeViewList = { id: "test-variant", actionPrototypes: [ { id: "create-prototype", kind: ActionKind.Create, name: "Create", } as ActionPrototypeView, ], }; const ctx = CONTEXT.value; const currentChangeSetId = ctx.changeSetId.value; const mockActionViewList: BifrostActionViewList = { id: currentChangeSetId, actions: [ { id: "action-1", prototypeId: "create-prototype", componentId: "test-component-1", // Current component, current change set name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: currentChangeSetId, myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, { id: "action-2", prototypeId: "create-prototype", componentId: "test-component-2", // Different component, current change set name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: currentChangeSetId, myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, { id: "action-3", prototypeId: "create-prototype", componentId: "test-component-1", // Current component, different change set name: "Create", kind: ActionKind.Create, state: ActionState.Queued, originatingChangeSetId: "different-changeset", myDependencies: [], dependentOn: [], holdStatusInfluencedBy: [], }, ], }; // Set up query responses mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionPrototypeViewList|test-variant`, mockActionPrototypeViewList, ); mockQueryResponses.set( `${ctx.workspacePk.value}|${ctx.changeSetId.value}|ActionViewList|${ctx.workspacePk.value}`, mockActionViewList, ); const { useComponentActions } = await import("./component_actions"); const { actionByPrototype } = useComponentActions(mockComponent); // Then: Only action for current component AND current change set should be visible expect(Object.keys(actionByPrototype.value)).toHaveLength(1); const createAction = actionByPrototype.value["create-prototype"]; expect(createAction).toBeDefined(); expect(createAction?.id).toBe("action-1"); expect(createAction?.componentId).toBe("test-component-1"); expect(createAction?.originatingChangeSetId).toBe(currentChangeSetId); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server