From e26daf4dd53ff8d1c229771a8e93e1407e4e3618 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 11 Mar 2026 00:49:50 -0400 Subject: [PATCH] Add unit tests for FormPickValue component 7 tests covering mode defaults, mode changes, grow-on-connect behavior, and JSON-encoded tool_state from the API. Co-Authored-By: Claude Opus 4.6 --- .../Editor/Forms/FormPickValue.test.ts | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 client/src/components/Workflow/Editor/Forms/FormPickValue.test.ts diff --git a/client/src/components/Workflow/Editor/Forms/FormPickValue.test.ts b/client/src/components/Workflow/Editor/Forms/FormPickValue.test.ts new file mode 100644 index 00000000000..e1a6ae747b4 --- /dev/null +++ b/client/src/components/Workflow/Editor/Forms/FormPickValue.test.ts @@ -0,0 +1,157 @@ +import { createTestingPinia } from "@pinia/testing"; +import { getLocalVue } from "@tests/vitest/helpers"; +import { shallowMount, type Wrapper } from "@vue/test-utils"; +import { PiniaVuePlugin } from "pinia"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import Vue from "vue"; + +import type { Step } from "@/stores/workflowStepStore"; + +import FormElement from "@/components/Form/FormElement.vue"; + +import FormPickValue from "./FormPickValue.vue"; + +const localVue = getLocalVue(); +localVue.use(PiniaVuePlugin); + +interface EmittedState { + mode: string; + num_inputs: number; +} + +function makeStep(overrides: Partial = {}): Step { + return { + id: 0, + name: "Pick Value", + type: "pick_value", + inputs: [], + outputs: [{ name: "output", extensions: ["input"], optional: false }], + input_connections: {}, + position: { left: 0, top: 0 }, + tool_state: { mode: "first_non_null", num_inputs: 2 }, + workflow_outputs: [], + ...overrides, + } as Step; +} + +function mountPickValue(step?: Step): Wrapper { + return shallowMount(FormPickValue, { + propsData: { + step: step ?? makeStep(), + }, + localVue, + pinia: createTestingPinia({ createSpy: vi.fn }), + provide: { + workflowId: "mock-workflow", + }, + }); +} + +function getLastEmittedState(wrapper: Wrapper): EmittedState { + const events = wrapper.emitted().onChange!; + return events[events.length - 1]![0] as EmittedState; +} + +function getEmittedCount(wrapper: Wrapper): number { + return wrapper.emitted().onChange!.length; +} + +describe("FormPickValue", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("mode defaults", () => { + it("defaults to first_non_null when tool_state is empty", () => { + const wrapper = mountPickValue(makeStep({ tool_state: {} })); + const state = getLastEmittedState(wrapper); + expect(state.mode).toBe("first_non_null"); + expect(state.num_inputs).toBe(2); + }); + + it("preserves mode from tool_state", () => { + const wrapper = mountPickValue(makeStep({ tool_state: { mode: "all_non_null", num_inputs: 3 } })); + const state = getLastEmittedState(wrapper); + expect(state.mode).toBe("all_non_null"); + expect(state.num_inputs).toBe(3); + }); + }); + + describe("mode changes", () => { + it("emits onChange with updated mode", () => { + const wrapper = mountPickValue(); + const formElement = wrapper.findComponent(FormElement); + formElement.vm.$emit("input", "the_only_non_null"); + + const state = getLastEmittedState(wrapper); + expect(state.mode).toBe("the_only_non_null"); + expect(state.num_inputs).toBe(2); + }); + + it("preserves num_inputs when changing mode", () => { + const wrapper = mountPickValue(makeStep({ tool_state: { mode: "first_non_null", num_inputs: 5 } })); + const formElement = wrapper.findComponent(FormElement); + formElement.vm.$emit("input", "all_non_null"); + + const state = getLastEmittedState(wrapper); + expect(state.mode).toBe("all_non_null"); + expect(state.num_inputs).toBe(5); + }); + }); + + describe("grow-on-connect", () => { + it("increments num_inputs when last terminal gets connected", async () => { + const step = makeStep({ tool_state: { mode: "first_non_null", num_inputs: 2 } }); + const wrapper = mountPickValue(step); + + // Simulate connecting to the last empty terminal (input_2, since num_inputs=2) + await wrapper.setProps({ + step: { + ...step, + input_connections: { + input_0: [{ id: 1, output_name: "output" }], + input_1: [{ id: 2, output_name: "output" }], + input_2: [{ id: 3, output_name: "output" }], + }, + }, + }); + + expect(getLastEmittedState(wrapper).num_inputs).toBe(3); + }); + + it("does not increment when a non-last terminal gets connected", async () => { + const step = makeStep({ tool_state: { mode: "first_non_null", num_inputs: 3 } }); + const wrapper = mountPickValue(step); + const countBefore = getEmittedCount(wrapper); + + // Connect to input_1 (not the last terminal input_3) + await wrapper.setProps({ + step: { + ...step, + input_connections: { + input_1: [{ id: 2, output_name: "output" }], + }, + }, + }); + + expect(getEmittedCount(wrapper)).toBe(countBefore); + }); + }); + + describe("JSON-encoded tool_state", () => { + it("handles JSON-string-encoded values from API", () => { + // The build_module API may return tool_state values as JSON strings + const wrapper = mountPickValue( + makeStep({ + tool_state: { + mode: JSON.stringify("the_only_non_null"), + num_inputs: JSON.stringify(4), + }, + }) + ); + const state = getLastEmittedState(wrapper); + expect(state.mode).toBe("the_only_non_null"); + expect(state.num_inputs).toBe(4); + }); + }); +});