From 63e3e94629e3812781671599acca1213fd165d00 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Wed, 11 Mar 2026 13:26:23 +0100 Subject: [PATCH 01/50] [NU-2475] provide active test case ID to redux --- .../NodeContent/TestingContentElements/TestCases.tsx | 7 +++++-- .../toolbars/test/buttons/ScenarioTestButton.tsx | 12 +++++------- .../useScenarioTestPresets.tsx | 7 ++++++- designer/client/src/reducers/graph/reducer.ts | 4 ++++ designer/client/src/reducers/graph/testing.ts | 1 + designer/client/src/reducers/selectors/testCases.ts | 6 ++++++ designer/client/src/reducers/selectors/testing.ts | 2 ++ 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx index fea47e2e645..ab17449cd59 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx @@ -2,7 +2,7 @@ import { Box, styled } from "@mui/material"; import React, { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { getTestCaseOptions } from "../../../../../../reducers/selectors/testCases"; +import { getActiveTestCaseOption, getTestCaseOptions } from "../../../../../../reducers/selectors/testCases"; import { useAppSelector } from "../../../../../../store/storeHelpers"; import { useWindows } from "../../../../../../windowManager/useWindows"; import { WindowKind } from "../../../../../../windowManager/WindowKind"; @@ -17,7 +17,10 @@ const StyledTestCasesSelect = styled(TypeSelect)(() => ({ export const TestCases = () => { const { t } = useTranslation(); + const testCaseOptions = useAppSelector(getTestCaseOptions); + const activeTestCaseOption = useAppSelector(getActiveTestCaseOption); + const { open } = useWindows(); const onDisplayEnterpriseInfo = useCallback(() => { open({ kind: WindowKind.enterpriseFeatureInfo, layoutData: { width: 500 } }); @@ -25,7 +28,7 @@ export const TestCases = () => { return ( - "noop"} value={testCaseOptions[0]} /> + "noop"} value={activeTestCaseOption} /> {/**/} {/* */} {/* */} diff --git a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx index a3abe5bcd75..e0a7e9bb6fd 100644 --- a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx +++ b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx @@ -37,12 +37,10 @@ function ScenarioTestButton(props: PropsOfButton const { variant } = useContext(ToolbarButtonsContext); const side = useContext(ToolbarSideContext); - const { presets, runAllPreset, testCasePresets } = useScenarioTestPresets(); + const { presets, activeTestCasePreset } = useScenarioTestPresets(); const { hasResult, assertionsIsSuccess } = useAssertionResultsSummary(); const tooltip = useScenarioTestTooltip({ disabled, title, titleOverride }); - const [selectedPreset, setSelectedPreset] = useState(testCasePresets[0] || runAllPreset); - const [showMockFieldOnEnrichers] = useUserSettings("node.showMockFieldOnEnrichers"); const dispatch = useAppDispatch(); const handleRunTest = useCallback( @@ -51,7 +49,7 @@ function ScenarioTestButton(props: PropsOfButton ); const icon = - selectedPreset.value === RUN_ALL ? ( + activeTestCasePreset.value === RUN_ALL ? ( ) : ( @@ -60,7 +58,7 @@ function ScenarioTestButton(props: PropsOfButton return ( handleRunTest(testCase)} - name={selectedPreset.label} + name={activeTestCasePreset.label} title={tooltip || t("panels.actions.scenarioTest.button.title", "run test")} icon={icon} side={side} @@ -69,9 +67,9 @@ function ScenarioTestButton(props: PropsOfButton disabled={!testingScenarioEnabled || isLoading} type={type} presets={presets} - selected={selectedPreset} + selected={activeTestCasePreset} onPresetChange={(preset) => { - setSelectedPreset(preset); + // setSelectedPreset(preset); if (preset.value === RUN_ALL) { //TODO: Implement me when backend ready return; diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index d1c8538a519..25e7214fb9c 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { getTestCase } from "../../../../../reducers/selectors/testCases"; +import { getActiveTestCaseId } from "../../../../../reducers/selectors/testing"; import { useAppSelector } from "../../../../../store/storeHelpers"; import type { OptionHeader } from "../../../../graph/node-modal/fragment-input-definition/TypeSelect"; import { AssertionStatusIcon } from "../../../testCases/assertionResultsForNode/assertionResult/AssertionStatusIcon"; @@ -34,6 +35,10 @@ export const useScenarioTestPresets = () => { ]; }, [testCase, hasResult, assertionsIsSuccess]); + const activeTestCaseId = useAppSelector(getActiveTestCaseId); + + const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) ?? null; + const runAllPreset: Preset = useMemo( () => ({ icon: , @@ -50,5 +55,5 @@ export const useScenarioTestPresets = () => { [runAllPreset, testCasePresets, testCasesHeader], ); - return { presets, testCasePresets, runAllPreset }; + return { presets, testCasePresets, runAllPreset, activeTestCasePreset }; }; diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index 4617fdd31a6..97e9d9315ed 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -177,6 +177,10 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra scenario: adjustedScenario, scenarioLoading: false, layout: fromMeta(adjustedScenario.scenarioGraph), + testing: { + ...state.testing, + activeTestCaseId: adjustedScenario.scenarioGraph.testCases.value.id, + }, }; } case "CORRECT_INVALID_SCENARIO": { diff --git a/designer/client/src/reducers/graph/testing.ts b/designer/client/src/reducers/graph/testing.ts index ca6fb5755ce..a2249223694 100644 --- a/designer/client/src/reducers/graph/testing.ts +++ b/designer/client/src/reducers/graph/testing.ts @@ -11,6 +11,7 @@ export type TestingState = { assertionsResults: TestAssertionResults; testResultsLoading?: boolean; testData?: TestData; + activeTestCaseId?: string; }; export const initialTestingState: GraphState["testing"] = { diff --git a/designer/client/src/reducers/selectors/testCases.ts b/designer/client/src/reducers/selectors/testCases.ts index f6b2e80fd68..a7e6f71d65e 100644 --- a/designer/client/src/reducers/selectors/testCases.ts +++ b/designer/client/src/reducers/selectors/testCases.ts @@ -6,11 +6,17 @@ import type { TestingDataRecords } from "../../components/modals/TestingDataReco import { safeParseExpression } from "../../components/modals/TestingDataRecords/utils"; import type { TestCase } from "../graph/testCase"; import { getScenarioGraph } from "./graph"; +import { getActiveTestCaseId } from "./testing"; const getNodeId = (_: unknown, nodeId: string) => nodeId; export const getTestCase = createSelector(getScenarioGraph, ({ testCases }) => testCases?.value ?? ({} as TestCase)); export const getTestCaseOptions = createSelector(getTestCase, ({ name, id }) => [{ label: name, value: id }]); +export const getActiveTestCaseOption = createSelector( + getTestCaseOptions, + getActiveTestCaseId, + (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, +); export const getTestCaseAssertions = createSelector(getTestCase, ({ assertions }) => assertions); export const getTestCaseAssertionsForNode = createSelector( getTestCaseAssertions, diff --git a/designer/client/src/reducers/selectors/testing.ts b/designer/client/src/reducers/selectors/testing.ts index a6fd5283494..374b0d69e77 100644 --- a/designer/client/src/reducers/selectors/testing.ts +++ b/designer/client/src/reducers/selectors/testing.ts @@ -22,3 +22,5 @@ export const getIsTestingMode = createSelector( getProcessCounts, (results, counts) => !isEmpty(results) || !isEmpty(counts), ); + +export const getActiveTestCaseId = createSelector(getTesting, (g) => g.activeTestCaseId); From dbd9cb3c4a97ad0172e6eef71e4f8fcff9e61dcf Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 12 Mar 2026 10:00:00 +0100 Subject: [PATCH 02/50] [NU-2472] fix test --- designer/client/test/reducer-test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/designer/client/test/reducer-test.ts b/designer/client/test/reducer-test.ts index e9cf5584c8e..2e42c48ed24 100644 --- a/designer/client/test/reducer-test.ts +++ b/designer/client/test/reducer-test.ts @@ -90,6 +90,23 @@ const baseProcessState: Scenario = { ] as Edge[], stickyNotes: [], properties: null, + testCases: { + value: { + id: "testCase1", + name: "Test case 1", + inputs: "{}", + mocks: {}, + assertions: { + "Enricher ID": [ + { + expected: { expression: "10", language: "spel" }, + operator: "equals", + actual: { expression: "#input.value", language: "spel" }, + }, + ], + }, + }, + }, }, history: [], labels: [], From 0b89481f3532b1b68aad1d0937feafb13ec8dea9 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 12 Mar 2026 11:02:56 +0100 Subject: [PATCH 03/50] fix errors --- .../scenarioTestButtonContent/useScenarioTestPresets.tsx | 2 +- designer/client/src/reducers/graph/reducer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index 25e7214fb9c..749251f6a9d 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -37,7 +37,7 @@ export const useScenarioTestPresets = () => { const activeTestCaseId = useAppSelector(getActiveTestCaseId); - const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) ?? null; + const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) ?? testCasePresets[0]; const runAllPreset: Preset = useMemo( () => ({ diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index 97e9d9315ed..928b4f82c7e 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -179,7 +179,7 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra layout: fromMeta(adjustedScenario.scenarioGraph), testing: { ...state.testing, - activeTestCaseId: adjustedScenario.scenarioGraph.testCases.value.id, + activeTestCaseId: adjustedScenario?.scenarioGraph?.testCases?.value?.id ?? null, }, }; } From a48a3b477f2b7dfc8bdcc4ea1a1c9478b426b00a Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 12 Mar 2026 11:27:36 +0100 Subject: [PATCH 04/50] [NU-2475] get active test case from state --- .../scenarioTestButtonContent/useScenarioTestPresets.tsx | 2 +- designer/client/src/reducers/graph/reducer.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index 749251f6a9d..f1f863fa064 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -37,7 +37,7 @@ export const useScenarioTestPresets = () => { const activeTestCaseId = useAppSelector(getActiveTestCaseId); - const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) ?? testCasePresets[0]; + const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId); const runAllPreset: Preset = useMemo( () => ({ diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index 928b4f82c7e..d9dc3c77408 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -26,7 +26,7 @@ import { appendHistorySquashLogic } from "./historySquash"; import type { NestedKeyOf } from "./lodashWrappers"; import { omit, pick } from "./lodashWrappers"; import { selectionState } from "./selectionState"; -import { testCaseReducer } from "./testCase"; +import { initialTestCasesState, testCaseReducer } from "./testCase"; import { initialTestingState, testingReducer } from "./testing"; import type { GraphState } from "./types"; import { VisibleDataType } from "./types"; @@ -49,7 +49,7 @@ const emptyGraphState: GraphState = { edges: [], properties: null, stickyNotes: [], - testCases: null, + testCases: initialTestCasesState, }, } as Scenario, layout: [], @@ -179,7 +179,8 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra layout: fromMeta(adjustedScenario.scenarioGraph), testing: { ...state.testing, - activeTestCaseId: adjustedScenario?.scenarioGraph?.testCases?.value?.id ?? null, + activeTestCaseId: + adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id, }, }; } From cb1b3494e329b22cdf9348036db0a4b5efc8c2fb Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Fri, 13 Mar 2026 11:10:14 +0100 Subject: [PATCH 05/50] [NU-2472] provide default test cases to imported scenario --- designer/client/src/reducers/graph/reducer.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index d9dc3c77408..76194fff8ea 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -128,6 +128,7 @@ const adjustScenarioData = flow( addStickyNotesToNodes, appendScenarioNameToProperties, produce((draft: WritableDraft) => { + draft.scenarioGraph.testCases = draft.scenarioGraph.testCases || initialTestCasesState; draft.scenarioGraph.nodes?.forEach((nodeDraft) => { appendUuidToParameters(nodeDraft); fixEmptyValues(nodeDraft); @@ -152,6 +153,9 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra const newNodeids = sortBy(adjustedScenario.scenarioGraph.nodes.map((n) => n.id)); const newLayout = isEqual(oldNodeIds, newNodeids) ? state.layout : null; + const activeTestCaseId = + adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id; + return { ...state, scenarioLoading: false, @@ -160,6 +164,10 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra ...state.scenario, ...adjustedScenario, }, + testing: { + ...state.testing, + activeTestCaseId, + }, }; } case "UPDATE_TEST_CAPABILITIES": { @@ -172,6 +180,9 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra } case "DISPLAY_PROCESS": { const adjustedScenario = adjustScenarioData(action.scenario); + const activeTestCaseId = + adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id; + return { ...state, scenario: adjustedScenario, @@ -179,8 +190,7 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra layout: fromMeta(adjustedScenario.scenarioGraph), testing: { ...state.testing, - activeTestCaseId: - adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id, + activeTestCaseId, }, }; } From cc73af5daa4f1fd9c9407232181acd38904dac27 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Mon, 16 Mar 2026 09:51:05 +0100 Subject: [PATCH 06/50] [NU-2475] handle multiple test cases --- .../client/src/actions/nk/testCasesActions.ts | 33 ++++--- .../graph/node-modal/node/NodeDetails.tsx | 4 +- .../useNodeTypeDetailsContentLogic.tsx | 4 +- .../test/buttons/ScenarioTestButton.tsx | 4 +- .../useScenarioTestPresets.tsx | 8 +- .../components/toolbars/testCases/results.tsx | 4 +- .../toolbars/testCases/testCaseHeader.tsx | 4 +- designer/client/src/reducers/graph/reducer.ts | 10 ++- .../client/src/reducers/graph/testCase.ts | 87 +++++++++---------- .../src/reducers/selectors/testCases.ts | 23 +++-- designer/client/src/types/scenarioGraph.ts | 2 +- 11 files changed, 98 insertions(+), 85 deletions(-) diff --git a/designer/client/src/actions/nk/testCasesActions.ts b/designer/client/src/actions/nk/testCasesActions.ts index 3c2cec16daf..bca0d1519bb 100644 --- a/designer/client/src/actions/nk/testCasesActions.ts +++ b/designer/client/src/actions/nk/testCasesActions.ts @@ -1,12 +1,14 @@ import type { WithUuid } from "../../components/graph/node-modal/appendUuid"; import type { ExpressionObj } from "../../components/graph/node-modal/editors/expression/types"; import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; +import type { TestCase } from "../../reducers/graph/testCase"; import { getTestCaseAssertions, getTestCaseAssertionsForNode, getInputDataRecords, getTestCaseMocks, } from "../../reducers/selectors/testCases"; +import { getActiveTestCaseId } from "../../reducers/selectors/testing"; import type { ThunkAction } from "../reduxTypes"; export type Assertion = { description?: string; expected: ExpressionObj; operator: "equals" | "notEquals"; actual: ExpressionObj }; @@ -15,10 +17,7 @@ export type Assertions = Record; export type Mock = { expression: ExpressionObj }; export type Mocks = Record; -export type TestCasesActions = - | { type: "SET_TEST_CASE_ASSERTIONS"; assertions: Assertions } - | { type: "SET_TEST_CASE_INPUTS"; inputs: string } - | { type: "SET_TEST_CASE_MOCKS"; mocks: Mocks }; +export type TestCasesActions = { type: "UPDATE_TEST_CASE"; testCaseId: string; updates: Partial }; export function setTestCaseAssertions(nodeId: string, updater: (prev: WithUuid[]) => WithUuid[]): ThunkAction { return (dispatch, getState) => { @@ -27,9 +26,14 @@ export function setTestCaseAssertions(nodeId: string, updater: (prev: WithUuid Testi const state = getState(); const prev = getInputDataRecords(state); const next = updater(prev); + const activeTestCaseId = getActiveTestCaseId(state); dispatch({ - type: "SET_TEST_CASE_INPUTS", - inputs: JSON.stringify(next), + type: "UPDATE_TEST_CASE", + testCaseId: activeTestCaseId, + updates: { + inputs: JSON.stringify(next), + }, }); }; } @@ -52,9 +60,14 @@ export function setTestCaseMock(nodeId: string, expression: ExpressionObj): Thun const state = getState(); const mocks = getTestCaseMocks(state); + const activeTestCaseId = getActiveTestCaseId(state); + dispatch({ - type: "SET_TEST_CASE_MOCKS", - mocks: { ...mocks, [nodeId]: { expression } }, + type: "UPDATE_TEST_CASE", + testCaseId: activeTestCaseId, + updates: { + mocks: { ...mocks, [nodeId]: { expression } }, + }, }); }; } diff --git a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx index c26e8636703..2e5c9a93ddc 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx @@ -12,7 +12,7 @@ import { visualizationUrl } from "../../../../common/VisualizationUrl"; import { BASE_PATH } from "../../../../config"; import type { RootState } from "../../../../reducers"; import { getCreatorType } from "../../../../reducers/selectors/getCreator"; -import { getTestCase } from "../../../../reducers/selectors/testCases"; +import { getActiveTestCase } from "../../../../reducers/selectors/testCases"; import { useAppSelector } from "../../../../store/storeHelpers"; import type { Edge } from "../../../../types/edge"; import type { NodeType } from "../../../../types/node"; @@ -92,7 +92,7 @@ function NodeDetails(props: NodeDetailsProps): React.JSX.Element { const { node, editedNode, onChange, outputEdges, performNodeEdit, editState } = useNodeState(data.meta); const [generalErrors] = useGetNodeErrors(node); - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); const hasNodeTestCasesErrors = useAppSelector((state) => hasValidationTestCasesErrors(state, { nodeId: node.id, testCaseId: testCase.name }), ); diff --git a/designer/client/src/components/graph/node-modal/useNodeTypeDetailsContentLogic.tsx b/designer/client/src/components/graph/node-modal/useNodeTypeDetailsContentLogic.tsx index aa0689ad2e2..2c06b9f9742 100644 --- a/designer/client/src/components/graph/node-modal/useNodeTypeDetailsContentLogic.tsx +++ b/designer/client/src/components/graph/node-modal/useNodeTypeDetailsContentLogic.tsx @@ -7,7 +7,7 @@ import { validateNodeData, } from "../../../actions/nk/nodeDetails"; import type { RootState } from "../../../reducers"; -import { getTestCase, getTestCaseNodeValidationData } from "../../../reducers/selectors/testCases"; +import { getActiveTestCase, getTestCaseNodeValidationData } from "../../../reducers/selectors/testCases"; import { useAppDispatch, useAppSelector } from "../../../store/storeHelpers"; import type { Edge } from "../../../types/edge"; import type { NodeType, Parameter } from "../../../types/node"; @@ -168,7 +168,7 @@ export function useGetNodeTestCasesErrors(node: NodeType): { enricherMockErrors: NodeValidationError[]; assertionsErrors: Record; } { - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); const currentTestCasesErrors = useAppSelector((state: RootState) => getValidationTestCasesErrors(state, { nodeId: node.id, testCaseId: testCase.name }), ); diff --git a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx index e0a7e9bb6fd..70ada0013f5 100644 --- a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx +++ b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx @@ -6,7 +6,7 @@ import { testScenarioWithTestCase } from "../../../../actions/nk/testingActions" import TestingIcon from "../../../../assets/img/toolbarButtons/test.svg"; import { useUserSettings } from "../../../../common/useUserSettings"; import type { TestCase } from "../../../../reducers/graph/testCase"; -import { getTestCase } from "../../../../reducers/selectors/testCases"; +import { getActiveTestCase } from "../../../../reducers/selectors/testCases"; import { getTestResultsLoading } from "../../../../reducers/selectors/testing"; import { ToolbarsSide } from "../../../../reducers/toolbars"; import { useAppDispatch, useAppSelector } from "../../../../store/storeHelpers"; @@ -30,7 +30,7 @@ export type ScenarioTestButtonProps = { function ScenarioTestButton(props: PropsOfButton) { const { disabled, title, titleOverride, type } = props; const { t } = useTranslation(); - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); const isLoading = useAppSelector(getTestResultsLoading); const testingScenarioEnabled = useTestingScenarioEnabled({ disabled }); diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index f1f863fa064..30fc94ad470 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { getTestCase } from "../../../../../reducers/selectors/testCases"; +import { getActiveTestCase } from "../../../../../reducers/selectors/testCases"; import { getActiveTestCaseId } from "../../../../../reducers/selectors/testing"; import { useAppSelector } from "../../../../../store/storeHelpers"; import type { OptionHeader } from "../../../../graph/node-modal/fragment-input-definition/TypeSelect"; @@ -21,7 +21,7 @@ export type Preset = { export const useScenarioTestPresets = () => { const { t } = useTranslation(); - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); const { hasResult, assertionsIsSuccess } = useAssertionResultsSummary(); const testCasePresets: Preset[] = useMemo(() => { @@ -35,9 +35,7 @@ export const useScenarioTestPresets = () => { ]; }, [testCase, hasResult, assertionsIsSuccess]); - const activeTestCaseId = useAppSelector(getActiveTestCaseId); - - const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId); + const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === testCase.id) || null; const runAllPreset: Preset = useMemo( () => ({ diff --git a/designer/client/src/components/toolbars/testCases/results.tsx b/designer/client/src/components/toolbars/testCases/results.tsx index e26207351ae..76ec78ef638 100644 --- a/designer/client/src/components/toolbars/testCases/results.tsx +++ b/designer/client/src/components/toolbars/testCases/results.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { testScenarioWithTestCase } from "../../../actions/nk/testingActions"; import { useUserSettings } from "../../../common/useUserSettings"; import type { TestAssertionResults } from "../../../http/resultsWithCountsDto"; -import { getTestCase } from "../../../reducers/selectors/testCases"; +import { getActiveTestCase } from "../../../reducers/selectors/testCases"; import { getTestResultsLoading } from "../../../reducers/selectors/testing"; import { useAppDispatch, useAppSelector } from "../../../store/storeHelpers"; import type { NodeType } from "../../../types/node"; @@ -21,7 +21,7 @@ interface Props { } export const Results = ({ testAssertionResults }: Props) => { - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); const dispatch = useAppDispatch(); const [showMockFieldOnEnrichers] = useUserSettings("node.showMockFieldOnEnrichers"); const isLoading = useAppSelector(getTestResultsLoading); diff --git a/designer/client/src/components/toolbars/testCases/testCaseHeader.tsx b/designer/client/src/components/toolbars/testCases/testCaseHeader.tsx index 6db5dbf0900..b6bf7654764 100644 --- a/designer/client/src/components/toolbars/testCases/testCaseHeader.tsx +++ b/designer/client/src/components/toolbars/testCases/testCaseHeader.tsx @@ -1,11 +1,11 @@ import { Box, Typography } from "@mui/material"; import React from "react"; -import { getTestCase } from "../../../reducers/selectors/testCases"; +import { getActiveTestCase } from "../../../reducers/selectors/testCases"; import { useAppSelector } from "../../../store/storeHelpers"; export const TestCaseHeader = () => { - const testCase = useAppSelector(getTestCase); + const testCase = useAppSelector(getActiveTestCase); return ( diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index 76194fff8ea..ad5cd65dd97 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -136,6 +136,10 @@ const adjustScenarioData = flow( }), ); +const getDefaultActiveTestCaseId = (actionGraph: ScenarioGraph, stateGraph: ScenarioGraph): string | null => { + return actionGraph?.testCases?.list[0]?.id || stateGraph?.testCases?.list[0]?.id; +}; + const graphReducer: Reducer = (state = emptyGraphState, action): GraphState => { const currentNodes = state.scenario.scenarioGraph.nodes; const currentEdges = state.scenario.scenarioGraph.edges; @@ -153,8 +157,7 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra const newNodeids = sortBy(adjustedScenario.scenarioGraph.nodes.map((n) => n.id)); const newLayout = isEqual(oldNodeIds, newNodeids) ? state.layout : null; - const activeTestCaseId = - adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id; + const activeTestCaseId = getDefaultActiveTestCaseId(adjustedScenario.scenarioGraph, state.scenario.scenarioGraph); return { ...state, @@ -180,8 +183,7 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra } case "DISPLAY_PROCESS": { const adjustedScenario = adjustScenarioData(action.scenario); - const activeTestCaseId = - adjustedScenario?.scenarioGraph?.testCases?.value?.id || state.scenario?.scenarioGraph?.testCases?.value?.id; + const activeTestCaseId = getDefaultActiveTestCaseId(adjustedScenario.scenarioGraph, state.scenario.scenarioGraph); return { ...state, diff --git a/designer/client/src/reducers/graph/testCase.ts b/designer/client/src/reducers/graph/testCase.ts index 965e6e3cbf0..7ed1268e44b 100644 --- a/designer/client/src/reducers/graph/testCase.ts +++ b/designer/client/src/reducers/graph/testCase.ts @@ -1,9 +1,8 @@ +import { produce } from "immer"; + import type { Assertions, Mocks } from "../../actions/nk/testCasesActions"; import type { Reducer } from "../../actions/reduxTypes"; -import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; -import { safeParseExpression } from "../../components/modals/TestingDataRecords/utils"; import type { ScenarioGraph } from "../../types/scenarioGraph"; -import { omit } from "./lodashWrappers"; export interface TestCase { id: string; @@ -14,63 +13,57 @@ export interface TestCase { } export const initialTestCasesState: ScenarioGraph["testCases"] = { - value: { - id: "f8757b06-6610-4900-90cc-fd3963356e8e", - name: "Test case 1", - inputs: "[]", - mocks: {}, - assertions: {}, - }, + list: [ + { + id: "f8757b06-6610-4900-90cc-fd3963356e8e", + name: "Test case 1", + inputs: "[]", + mocks: {}, + assertions: {}, + }, + { + id: "f8757b06-6610-4900-90cc-fd3963356e11", + name: "Test case 2", + inputs: "[]", + mocks: {}, + assertions: {}, + }, + ], }; -export const testCaseReducer: Reducer = (state = initialTestCasesState, action) => { +export const testCaseReducer: Reducer = produce((draft, action) => { switch (action.type) { - case "SET_TEST_CASE_ASSERTIONS": - return { - ...state, - value: { - ...state.value, - assertions: action.assertions, - }, - }; - case "SET_TEST_CASE_INPUTS": - return { - ...state, - value: { - ...state.value, - inputs: action.inputs, - }, - }; - case "SET_TEST_CASE_MOCKS": - return { - ...state, - value: { - ...state.value, - mocks: action.mocks, - }, - }; + case "UPDATE_TEST_CASE": { + const testCase = draft.list.find((tc) => tc.id === action.testCaseId); + if (!testCase) return; + + Object.assign(testCase, action.updates); + + break; + } + case "DELETE_NODES": return { - ...state, - value: cleanTestCaseState(state, action.ids), + ...draft, + value: cleanTestCaseState(draft, action.ids), }; case "ADD_NODE_REPLACE": return { - ...state, - value: cleanTestCaseState(state, [action.old.id]), + ...draft, + value: cleanTestCaseState(draft, [action.old.id]), }; default: - return state; + return draft; } -}; +}); const cleanTestCaseState = (state: ScenarioGraph["testCases"], ids: string[]) => { return { - ...state.value, - assertions: omit(state.value.assertions, ids), - mocks: omit(state.value.mocks, ids), - inputs: JSON.stringify( - safeParseExpression(state.value.inputs)?.filter((input) => !ids.includes(input.sourceId)), - ), + ...state.list, + // assertions: omit(state.value.assertions, ids), + // mocks: omit(state.value.mocks, ids), + // inputs: JSON.stringify( + // safeParseExpression(state.value.inputs)?.filter((input) => !ids.includes(input.sourceId)), + // ), }; }; diff --git a/designer/client/src/reducers/selectors/testCases.ts b/designer/client/src/reducers/selectors/testCases.ts index a7e6f71d65e..7f1e0cb29fb 100644 --- a/designer/client/src/reducers/selectors/testCases.ts +++ b/designer/client/src/reducers/selectors/testCases.ts @@ -10,27 +10,36 @@ import { getActiveTestCaseId } from "./testing"; const getNodeId = (_: unknown, nodeId: string) => nodeId; -export const getTestCase = createSelector(getScenarioGraph, ({ testCases }) => testCases?.value ?? ({} as TestCase)); -export const getTestCaseOptions = createSelector(getTestCase, ({ name, id }) => [{ label: name, value: id }]); +export const getTestCases = createSelector(getScenarioGraph, ({ testCases }) => testCases?.list ?? []); +export const getActiveTestCase = createSelector(getTestCases, getActiveTestCaseId, (testCases, activeTestCaseId) => + testCases.find((testCase) => testCase.id === activeTestCaseId), +); + +export const getTestCaseOptions = createSelector(getTestCases, (testCases) => + testCases.map(({ id, name }) => ({ label: name, value: id })), +); export const getActiveTestCaseOption = createSelector( getTestCaseOptions, getActiveTestCaseId, (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, ); -export const getTestCaseAssertions = createSelector(getTestCase, ({ assertions }) => assertions); +export const getTestCaseAssertions = createSelector(getActiveTestCase, ({ assertions }) => assertions); export const getTestCaseAssertionsForNode = createSelector( getTestCaseAssertions, getNodeId, (assertions, nodeId) => assertions[nodeId]?.map(withUuid) || [], ); -export const getTestCaseMocks = createSelector(getTestCase, ({ mocks }) => mocks); +export const getTestCaseMocks = createSelector(getActiveTestCase, ({ mocks }) => mocks); export const getTestCaseMockForNode = createSelector( getTestCaseMocks, getNodeId, (mocks, nodeId) => mocks[nodeId] || { expression: MockExpressionParameter.defaultValue }, ); -export const getInputDataRecords = createSelector(getTestCase, ({ inputs }) => safeParseExpression(inputs) || []); +export const getInputDataRecords = createSelector( + getActiveTestCase, + ({ inputs }) => safeParseExpression(inputs) || [], +); const getSourceId = (_: unknown, sourceId: string) => sourceId; export const getInputDataRecordsForSingleSource = createSelector([getInputDataRecords, getSourceId], (testCaseInputs, sourceId: string) => @@ -38,9 +47,7 @@ export const getInputDataRecordsForSingleSource = createSelector([getInputDataRe ); export const hasInputDataRecordsDefined = createSelector(getInputDataRecords, (inputDataRecords) => inputDataRecords.length > 0); -export const getTestCaseNodeValidationData = createSelector(getScenarioGraph, getNodeId, ({ testCases }, nodeId) => { - const testCase = testCases?.value ?? ({} as TestCase); - +export const getTestCaseNodeValidationData = createSelector(getActiveTestCase, getNodeId, (testCase, nodeId) => { return { [testCase.name]: { ...testCase, diff --git a/designer/client/src/types/scenarioGraph.ts b/designer/client/src/types/scenarioGraph.ts index 42637f45537..578434ee451 100644 --- a/designer/client/src/types/scenarioGraph.ts +++ b/designer/client/src/types/scenarioGraph.ts @@ -10,7 +10,7 @@ export type ScenarioGraph = { edges: Edge[]; properties: PropertiesType; stickyNotes: NodeType[]; - testCases: { value: TestCase }; + testCases: { list: TestCase[] }; }; export type Category = string; From 9258dbf7d9f50e7c5d180b9c83b0af5692a34855 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Mon, 16 Mar 2026 12:58:21 +0100 Subject: [PATCH 07/50] [NU-2475] display all test cases in presets --- .../useScenarioTestPresets.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index 30fc94ad470..bbd90dd614d 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { getActiveTestCase } from "../../../../../reducers/selectors/testCases"; +import { getTestCases } from "../../../../../reducers/selectors/testCases"; import { getActiveTestCaseId } from "../../../../../reducers/selectors/testing"; import { useAppSelector } from "../../../../../store/storeHelpers"; import type { OptionHeader } from "../../../../graph/node-modal/fragment-input-definition/TypeSelect"; @@ -21,21 +21,20 @@ export type Preset = { export const useScenarioTestPresets = () => { const { t } = useTranslation(); - const testCase = useAppSelector(getActiveTestCase); + const testCases = useAppSelector(getTestCases); + const activeTestCaseId = useAppSelector(getActiveTestCaseId); const { hasResult, assertionsIsSuccess } = useAssertionResultsSummary(); const testCasePresets: Preset[] = useMemo(() => { - if (Object.keys(testCase).length === 0) return []; - return [ - { - icon: hasResult ? : null, - label: testCase.name, - value: testCase.id, - }, - ]; - }, [testCase, hasResult, assertionsIsSuccess]); - - const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === testCase.id) || null; + if (testCases.length === 0) return []; + return testCases.map((testCase) => ({ + icon: hasResult ? : null, + label: testCase.name, + value: testCase.id, + })); + }, [testCases, hasResult, assertionsIsSuccess]); + + const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) || null; const runAllPreset: Preset = useMemo( () => ({ From 3ecfea71bcee9a070f23a2fc23acf776d3d33384 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Mon, 16 Mar 2026 13:15:54 +0100 Subject: [PATCH 08/50] [NU-2475] change active test case --- designer/client/src/actions/nk/testingActions.ts | 11 +++++++++-- .../TestingContentElements/TestCases.tsx | 14 ++++++++++++-- .../toolbars/test/buttons/ScenarioTestButton.tsx | 14 ++++++++++---- designer/client/src/reducers/graph/testing.ts | 6 ++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/designer/client/src/actions/nk/testingActions.ts b/designer/client/src/actions/nk/testingActions.ts index f44e18bfe0f..d1af860d255 100644 --- a/designer/client/src/actions/nk/testingActions.ts +++ b/designer/client/src/actions/nk/testingActions.ts @@ -1,7 +1,6 @@ import type { ProcessName } from "src/components/Process/types"; import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; -import { mapInputDataRecordsToRunTestsFormat } from "../../components/modals/TestingDataRecords/utils"; import HttpService from "../../http/HttpService/instance"; import type { SourceWithParametersTest } from "../../http/HttpService/types"; import type { ResultsWithCountsDto, TestAssertionResults, TestResultsDto } from "../../http/resultsWithCountsDto"; @@ -71,7 +70,8 @@ export type TestsActions = } | { type: "CLEAR_TEST_ASSERTIONS_RESULTS"; - }; + } + | { type: "CHANGE_ACTIVE_TEST_CASE"; testCaseId: string }; function wrapWithTestAction( fn: ( @@ -116,6 +116,13 @@ export function displayTestAssertionsResults(assertionsResults: TestAssertionRes }; } +export function changeActiveTestCase(testCaseId: string): Action { + return { + type: "CHANGE_ACTIVE_TEST_CASE", + testCaseId, + }; +} + function testingActions({ counts, results, assertionsResults }: ResultsWithCountsDto, testData?: SourceWithParametersTest): ThunkAction { return (dispatch) => { dispatch(displayProcessCounts(counts)); diff --git a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx index ab17449cd59..da7089ff84f 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx @@ -2,8 +2,9 @@ import { Box, styled } from "@mui/material"; import React, { useCallback } from "react"; import { useTranslation } from "react-i18next"; +import { changeActiveTestCase } from "../../../../../../actions/nk/testingActions"; import { getActiveTestCaseOption, getTestCaseOptions } from "../../../../../../reducers/selectors/testCases"; -import { useAppSelector } from "../../../../../../store/storeHelpers"; +import { useAppDispatch, useAppSelector } from "../../../../../../store/storeHelpers"; import { useWindows } from "../../../../../../windowManager/useWindows"; import { WindowKind } from "../../../../../../windowManager/WindowKind"; import { StyledButton } from "../../../../styledButton"; @@ -21,14 +22,23 @@ export const TestCases = () => { const testCaseOptions = useAppSelector(getTestCaseOptions); const activeTestCaseOption = useAppSelector(getActiveTestCaseOption); + const dispatch = useAppDispatch(); + const { open } = useWindows(); const onDisplayEnterpriseInfo = useCallback(() => { open({ kind: WindowKind.enterpriseFeatureInfo, layoutData: { width: 500 } }); }, [open]); + const changeActiveTestCaseOption = useCallback( + (testCaseId: string) => { + dispatch(changeActiveTestCase(testCaseId)); + }, + [dispatch], + ); + return ( - "noop"} value={activeTestCaseOption} /> + {/**/} {/* */} {/* */} diff --git a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx index 70ada0013f5..fe5927a1282 100644 --- a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx +++ b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx @@ -1,8 +1,8 @@ import { alpha, styled } from "@mui/material"; -import React, { useCallback, useContext, useState } from "react"; +import React, { useCallback, useContext } from "react"; import { useTranslation } from "react-i18next"; -import { testScenarioWithTestCase } from "../../../../actions/nk/testingActions"; +import { changeActiveTestCase, testScenarioWithTestCase } from "../../../../actions/nk/testingActions"; import TestingIcon from "../../../../assets/img/toolbarButtons/test.svg"; import { useUserSettings } from "../../../../common/useUserSettings"; import type { TestCase } from "../../../../reducers/graph/testCase"; @@ -55,6 +55,13 @@ function ScenarioTestButton(props: PropsOfButton ); + const changeActiveTestCaseOption = useCallback( + (testCaseId: string) => { + dispatch(changeActiveTestCase(testCaseId)); + }, + [dispatch], + ); + return ( handleRunTest(testCase)} @@ -69,13 +76,12 @@ function ScenarioTestButton(props: PropsOfButton presets={presets} selected={activeTestCasePreset} onPresetChange={(preset) => { - // setSelectedPreset(preset); if (preset.value === RUN_ALL) { //TODO: Implement me when backend ready return; } //TODO: Handle multiple test selection when backend ready - handleRunTest(testCase); + changeActiveTestCaseOption(preset.value); }} /> ); diff --git a/designer/client/src/reducers/graph/testing.ts b/designer/client/src/reducers/graph/testing.ts index a2249223694..77376283662 100644 --- a/designer/client/src/reducers/graph/testing.ts +++ b/designer/client/src/reducers/graph/testing.ts @@ -69,6 +69,12 @@ export const testingReducer: Reducer = (state = initialTe testResults: action.results?.results || null, }; } + case "CHANGE_ACTIVE_TEST_CASE": { + return { + ...state, + activeTestCaseId: action.testCaseId, + }; + } default: return state; } From 9067a8ac5776ca59b7ace8f1b071948a8ec97841 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Tue, 17 Mar 2026 10:48:51 +0100 Subject: [PATCH 09/50] [NU-2475] clear test case state on change --- designer/client/src/actions/nk/liveData.ts | 3 +- .../client/src/actions/nk/testingActions.ts | 18 ++++-- .../test/buttons/ScenarioTestButton.tsx | 55 ++++++++++++------- .../useScenarioTestPresets.tsx | 7 ++- designer/client/src/reducers/graph/reducer.ts | 17 ++++-- designer/client/src/reducers/graph/testing.ts | 1 + .../src/reducers/selectors/testCases.ts | 9 +-- 7 files changed, 74 insertions(+), 36 deletions(-) diff --git a/designer/client/src/actions/nk/liveData.ts b/designer/client/src/actions/nk/liveData.ts index 5c66d11a43d..fafbb0206ac 100644 --- a/designer/client/src/actions/nk/liveData.ts +++ b/designer/client/src/actions/nk/liveData.ts @@ -8,6 +8,7 @@ import { getScenario } from "../../reducers/selectors/graph"; import type { ThunkAction } from "../reduxTypes"; import { AbortControllersStack } from "./abortControllersStack"; import { hideTestRunDetails } from "./process"; +import { clearTestAssertionsResults } from "./testingActions"; export enum Initiator { init = "initial", @@ -84,7 +85,7 @@ export function startLiveData(initiator: Initiator | null = null, showErrors = f if (!getHasPauseReasons(getState())) { if (!getIsLiveDataWorking(getState())) { dispatch(hideTestRunDetails()); - dispatch({ type: "CLEAR_TEST_ASSERTIONS_RESULTS" }); + dispatch(clearTestAssertionsResults()); } dispatch(fetchAndDisplayLiveData(showErrors)); } diff --git a/designer/client/src/actions/nk/testingActions.ts b/designer/client/src/actions/nk/testingActions.ts index d1af860d255..97e504f7572 100644 --- a/designer/client/src/actions/nk/testingActions.ts +++ b/designer/client/src/actions/nk/testingActions.ts @@ -9,7 +9,7 @@ import { getProcessName, getScenarioGraph } from "../../reducers/selectors/graph import type { ScenarioGraph } from "../../types/scenarioGraph"; import type { Action, ThunkAction } from "../reduxTypes"; import { checkPendingChanges } from "./checkPendingChanges"; -import { displayProcessCounts } from "./displayProcessCounts"; +import { clearProcessCounts, displayProcessCounts } from "./displayProcessCounts"; export function testProcessFromFile(testDataFile: File): ThunkAction { return wrapWithTestAction((processName, scenarioGraph) => @@ -116,10 +116,14 @@ export function displayTestAssertionsResults(assertionsResults: TestAssertionRes }; } -export function changeActiveTestCase(testCaseId: string): Action { - return { - type: "CHANGE_ACTIVE_TEST_CASE", - testCaseId, +export function changeActiveTestCase(testCaseId: string): ThunkAction { + return async (dispatch) => { + dispatch(clearTestAssertionsResults()); + dispatch(clearProcessCounts()); + dispatch({ + type: "CHANGE_ACTIVE_TEST_CASE", + testCaseId, + }); }; } @@ -130,3 +134,7 @@ function testingActions({ counts, results, assertionsResults }: ResultsWithCount dispatch(displayTestAssertionsResults(assertionsResults)); }; } + +export function clearTestAssertionsResults(): Action { + return { type: "CLEAR_TEST_ASSERTIONS_RESULTS" }; +} diff --git a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx index fe5927a1282..6e90dfd2375 100644 --- a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx +++ b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx @@ -1,12 +1,11 @@ import { alpha, styled } from "@mui/material"; -import React, { useCallback, useContext } from "react"; +import React, { useCallback, useContext, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { changeActiveTestCase, testScenarioWithTestCase } from "../../../../actions/nk/testingActions"; import TestingIcon from "../../../../assets/img/toolbarButtons/test.svg"; import { useUserSettings } from "../../../../common/useUserSettings"; -import type { TestCase } from "../../../../reducers/graph/testCase"; -import { getActiveTestCase } from "../../../../reducers/selectors/testCases"; +import { getActiveTestCase, getTestCases } from "../../../../reducers/selectors/testCases"; import { getTestResultsLoading } from "../../../../reducers/selectors/testing"; import { ToolbarsSide } from "../../../../reducers/toolbars"; import { useAppDispatch, useAppSelector } from "../../../../store/storeHelpers"; @@ -18,6 +17,7 @@ import type { CustomButtonTypes } from "../../../toolbarSettings/buttons/buttons import type { PropsOfButton } from "../../../toolbarSettings/buttons/types"; import { TestingIconWithAssertionStatus } from "./scenarioTestButtonContent/TestingIconWithAssertionStatus"; import { useAssertionResultsSummary } from "./scenarioTestButtonContent/useAssertionResultsSummary"; +import type { Preset} from "./scenarioTestButtonContent/useScenarioTestPresets"; import { RUN_ALL, useScenarioTestPresets } from "./scenarioTestButtonContent/useScenarioTestPresets"; import { useScenarioTestTooltip } from "./scenarioTestButtonContent/useScenarioTestTooltip"; @@ -31,6 +31,7 @@ function ScenarioTestButton(props: PropsOfButton const { disabled, title, titleOverride, type } = props; const { t } = useTranslation(); const testCase = useAppSelector(getActiveTestCase); + const testCases = useAppSelector(getTestCases); const isLoading = useAppSelector(getTestResultsLoading); const testingScenarioEnabled = useTestingScenarioEnabled({ disabled }); @@ -44,16 +45,23 @@ function ScenarioTestButton(props: PropsOfButton const [showMockFieldOnEnrichers] = useUserSettings("node.showMockFieldOnEnrichers"); const dispatch = useAppDispatch(); const handleRunTest = useCallback( - (testCase: TestCase) => dispatch(testScenarioWithTestCase(testCase, showMockFieldOnEnrichers)), - [dispatch, showMockFieldOnEnrichers], + (testCaseId: string) => { + const testCaseToRun = testCases.find((tc) => tc.id === testCaseId) || testCase; + + dispatch(testScenarioWithTestCase(testCaseToRun, showMockFieldOnEnrichers)); + }, + [dispatch, showMockFieldOnEnrichers, testCase, testCases], ); - const icon = - activeTestCasePreset.value === RUN_ALL ? ( - - ) : ( - - ); + const icon = useMemo( + () => + activeTestCasePreset?.value === RUN_ALL ? ( + + ) : ( + + ), + [activeTestCasePreset?.value, assertionsIsSuccess, hasResult], + ); const changeActiveTestCaseOption = useCallback( (testCaseId: string) => { @@ -62,9 +70,23 @@ function ScenarioTestButton(props: PropsOfButton [dispatch], ); + const handleRunActiveTest = useCallback(() => handleRunTest(testCase?.id), [handleRunTest, testCase]); + + const handlePresetChange = useCallback( + (preset: Preset) => { + if (preset.value === RUN_ALL) { + //TODO: Implement me when backend ready + return; + } + changeActiveTestCaseOption(preset.value); + handleRunTest(preset.value); + }, + [changeActiveTestCaseOption, handleRunTest], + ); + return ( handleRunTest(testCase)} + onClick={handleRunActiveTest} name={activeTestCasePreset.label} title={tooltip || t("panels.actions.scenarioTest.button.title", "run test")} icon={icon} @@ -75,14 +97,7 @@ function ScenarioTestButton(props: PropsOfButton type={type} presets={presets} selected={activeTestCasePreset} - onPresetChange={(preset) => { - if (preset.value === RUN_ALL) { - //TODO: Implement me when backend ready - return; - } - //TODO: Handle multiple test selection when backend ready - changeActiveTestCaseOption(preset.value); - }} + onPresetChange={handlePresetChange} /> ); } diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index bbd90dd614d..ba272ce82f8 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -28,11 +28,14 @@ export const useScenarioTestPresets = () => { const testCasePresets: Preset[] = useMemo(() => { if (testCases.length === 0) return []; return testCases.map((testCase) => ({ - icon: hasResult ? : null, + icon: + hasResult && activeTestCaseId === testCase.id ? ( + + ) : null, label: testCase.name, value: testCase.id, })); - }, [testCases, hasResult, assertionsIsSuccess]); + }, [testCases, hasResult, activeTestCaseId, assertionsIsSuccess]); const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) || null; diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index ad5cd65dd97..1add00fd29e 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -27,6 +27,7 @@ import type { NestedKeyOf } from "./lodashWrappers"; import { omit, pick } from "./lodashWrappers"; import { selectionState } from "./selectionState"; import { initialTestCasesState, testCaseReducer } from "./testCase"; +import type { TestingState } from "./testing"; import { initialTestingState, testingReducer } from "./testing"; import type { GraphState } from "./types"; import { VisibleDataType } from "./types"; @@ -136,8 +137,8 @@ const adjustScenarioData = flow( }), ); -const getDefaultActiveTestCaseId = (actionGraph: ScenarioGraph, stateGraph: ScenarioGraph): string | null => { - return actionGraph?.testCases?.list[0]?.id || stateGraph?.testCases?.list[0]?.id; +const getDefaultActiveTestCaseId = (actionGraph: ScenarioGraph, stateGraph: ScenarioGraph, testing: TestingState): string | null => { + return testing.activeTestCaseId || actionGraph?.testCases?.list[0]?.id || stateGraph.testCases.list[0]?.id; }; const graphReducer: Reducer = (state = emptyGraphState, action): GraphState => { @@ -157,7 +158,11 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra const newNodeids = sortBy(adjustedScenario.scenarioGraph.nodes.map((n) => n.id)); const newLayout = isEqual(oldNodeIds, newNodeids) ? state.layout : null; - const activeTestCaseId = getDefaultActiveTestCaseId(adjustedScenario.scenarioGraph, state.scenario.scenarioGraph); + const activeTestCaseId = getDefaultActiveTestCaseId( + adjustedScenario.scenarioGraph, + state.scenario.scenarioGraph, + state.testing, + ); return { ...state, @@ -183,7 +188,11 @@ const graphReducer: Reducer = (state = emptyGraphState, action): Gra } case "DISPLAY_PROCESS": { const adjustedScenario = adjustScenarioData(action.scenario); - const activeTestCaseId = getDefaultActiveTestCaseId(adjustedScenario.scenarioGraph, state.scenario.scenarioGraph); + const activeTestCaseId = getDefaultActiveTestCaseId( + adjustedScenario.scenarioGraph, + state.scenario.scenarioGraph, + state.testing, + ); return { ...state, diff --git a/designer/client/src/reducers/graph/testing.ts b/designer/client/src/reducers/graph/testing.ts index 77376283662..900e945d0dc 100644 --- a/designer/client/src/reducers/graph/testing.ts +++ b/designer/client/src/reducers/graph/testing.ts @@ -72,6 +72,7 @@ export const testingReducer: Reducer = (state = initialTe case "CHANGE_ACTIVE_TEST_CASE": { return { ...state, + testResults: null, activeTestCaseId: action.testCaseId, }; } diff --git a/designer/client/src/reducers/selectors/testCases.ts b/designer/client/src/reducers/selectors/testCases.ts index 7f1e0cb29fb..cad641aa96f 100644 --- a/designer/client/src/reducers/selectors/testCases.ts +++ b/designer/client/src/reducers/selectors/testCases.ts @@ -4,7 +4,6 @@ import { withUuid } from "../../components/graph/node-modal/appendUuid"; import { MockExpressionParameter } from "../../components/graph/node-modal/editors/expression/MockExpressionField"; import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; import { safeParseExpression } from "../../components/modals/TestingDataRecords/utils"; -import type { TestCase } from "../graph/testCase"; import { getScenarioGraph } from "./graph"; import { getActiveTestCaseId } from "./testing"; @@ -23,14 +22,14 @@ export const getActiveTestCaseOption = createSelector( getActiveTestCaseId, (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, ); -export const getTestCaseAssertions = createSelector(getActiveTestCase, ({ assertions }) => assertions); +export const getTestCaseAssertions = createSelector(getActiveTestCase, (testCase) => testCase?.assertions); export const getTestCaseAssertionsForNode = createSelector( getTestCaseAssertions, getNodeId, (assertions, nodeId) => assertions[nodeId]?.map(withUuid) || [], ); -export const getTestCaseMocks = createSelector(getActiveTestCase, ({ mocks }) => mocks); +export const getTestCaseMocks = createSelector(getActiveTestCase, (testCase) => testCase?.mocks); export const getTestCaseMockForNode = createSelector( getTestCaseMocks, getNodeId, @@ -38,7 +37,7 @@ export const getTestCaseMockForNode = createSelector( ); export const getInputDataRecords = createSelector( getActiveTestCase, - ({ inputs }) => safeParseExpression(inputs) || [], + (testCase) => safeParseExpression(testCase.inputs) || [], ); const getSourceId = (_: unknown, sourceId: string) => sourceId; @@ -48,6 +47,8 @@ export const getInputDataRecordsForSingleSource = createSelector([getInputDataRe export const hasInputDataRecordsDefined = createSelector(getInputDataRecords, (inputDataRecords) => inputDataRecords.length > 0); export const getTestCaseNodeValidationData = createSelector(getActiveTestCase, getNodeId, (testCase, nodeId) => { + if (!testCase) return {}; + return { [testCase.name]: { ...testCase, From 1b2d6f9a399d20366410772ab96873fba4a7d81f Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Tue, 17 Mar 2026 11:59:36 +0100 Subject: [PATCH 10/50] [NU-2475] add save as test case dialog --- .../{TestCases.tsx => TestCasesSelector.tsx} | 13 ++++++- .../graph/node-modal/node/NodeDetails.tsx | 7 ++-- .../modals/saveAsTestCaseDialog.tsx | 38 +++++++++++++++++++ .../src/windowManager/ContentGetter.tsx | 6 +++ .../client/src/windowManager/WindowKind.tsx | 2 +- 5 files changed, 59 insertions(+), 7 deletions(-) rename designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/{TestCases.tsx => TestCasesSelector.tsx} (84%) create mode 100644 designer/client/src/components/modals/saveAsTestCaseDialog.tsx diff --git a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCasesSelector.tsx similarity index 84% rename from designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx rename to designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCasesSelector.tsx index da7089ff84f..4e4288985b3 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCases.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeContent/TestingContentElements/TestCasesSelector.tsx @@ -16,7 +16,7 @@ const StyledTestCasesSelect = styled(TypeSelect)(() => ({ maxWidth: "400px", })); -export const TestCases = () => { +export const TestCasesSelector = () => { const { t } = useTranslation(); const testCaseOptions = useAppSelector(getTestCaseOptions); @@ -29,6 +29,10 @@ export const TestCases = () => { open({ kind: WindowKind.enterpriseFeatureInfo, layoutData: { width: 500 } }); }, [open]); + const openSaveAsDialog = useCallback(() => { + open({ kind: WindowKind.saveAsTestCase, title: "Save as", layoutData: { width: 500 } }); + }, [open]); + const changeActiveTestCaseOption = useCallback( (testCaseId: string) => { dispatch(changeActiveTestCase(testCaseId)); @@ -36,6 +40,11 @@ export const TestCases = () => { [dispatch], ); + const handleSaveAsClick = useCallback(() => { + // onDisplayEnterpriseInfo(); + openSaveAsDialog(); + }, [openSaveAsDialog]); + return ( @@ -45,7 +54,7 @@ export const TestCases = () => { {/* */} {/**/} - + {t("node.row.add.text", "+")} diff --git a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx index 2e5c9a93ddc..d3d7dadf922 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx @@ -34,10 +34,9 @@ import { useGetNodeErrors } from "../useNodeTypeDetailsContentLogic"; import { EditStateFeedback } from "./EditStateFeedback"; import { GeneralContent } from "./NodeContent/GeneralContent"; import type { TabDef } from "./NodeContent/TabsWrapper"; -import { NodeDetailsTab } from "./NodeContent/TabsWrapper"; -import { TabsWrapper } from "./NodeContent/TabsWrapper"; +import { NodeDetailsTab, TabsWrapper } from "./NodeContent/TabsWrapper"; import { TestingContent } from "./NodeContent/TestingContent"; -import { TestCases } from "./NodeContent/TestingContentElements/TestCases"; +import { TestCasesSelector } from "./NodeContent/TestingContentElements/TestCasesSelector"; import { getReadOnly } from "./selectors"; import { useDialogActions } from "./useDialogActions"; import { useNodeState } from "./useNodeState"; @@ -176,7 +175,7 @@ function NodeDetails(props: NodeDetailsProps): React.JSX.Element { label: t("nodeDetails.tabs.testing.name", "Testing"), content: testingContent, disabled: !testingTabVisible, - additionalTabContent: , + additionalTabContent: , showErrorIndicator: hasNodeTestCasesErrors, }, ], diff --git a/designer/client/src/components/modals/saveAsTestCaseDialog.tsx b/designer/client/src/components/modals/saveAsTestCaseDialog.tsx new file mode 100644 index 00000000000..615aa423168 --- /dev/null +++ b/designer/client/src/components/modals/saveAsTestCaseDialog.tsx @@ -0,0 +1,38 @@ +import type { WindowContentProps } from "@touk/window-manager"; +import React, { useCallback, useState } from "react"; + +import { WindowContent } from "../../windowManager/WindowContent"; +import Input from "../graph/node-modal/editors/field/Input"; +import { NodeRow } from "../graph/node-modal/node/NodeRow"; +import { NodeValue } from "../graph/node-modal/node/NodeValue"; +import { useDialogActions } from "../graph/node-modal/node/useDialogActions"; +import { NodeTable } from "../graph/node-modal/NodeDetailsContent/NodeTable"; + +const SaveAsTestCaseDialog = (props: WindowContentProps) => { + const [testCaseName, setTestCaseName] = useState(""); + + const { cancel, apply } = useDialogActions({ + onClose: props.close, + onApply: () => { + return Promise.resolve(); + }, + }); + + const handleNameChange = useCallback((e: React.ChangeEvent) => { + setTestCaseName(e.target.value); + }, []); + + return ( + + + + + + + + + + ); +}; + +export default SaveAsTestCaseDialog; diff --git a/designer/client/src/windowManager/ContentGetter.tsx b/designer/client/src/windowManager/ContentGetter.tsx index bd8c45a0d11..8b12878c98d 100644 --- a/designer/client/src/windowManager/ContentGetter.tsx +++ b/designer/client/src/windowManager/ContentGetter.tsx @@ -79,6 +79,10 @@ const EnterpriseFeatureInfo = loadable( }, ); +const SaveAsTestCaseDialog = loadable(() => import("../components/modals/saveAsTestCaseDialog"), { + fallback: , +}); + const contentGetter = (props: WindowContentProps) => { switch (props.data.kind) { case WindowKind.addFragment: @@ -133,6 +137,8 @@ const contentGetter = (props: WindowContentProps) => { return ; case WindowKind.enterpriseFeatureInfo: return ; + case WindowKind.saveAsTestCase: + return ; default: return ( diff --git a/designer/client/src/windowManager/WindowKind.tsx b/designer/client/src/windowManager/WindowKind.tsx index 281a1977954..0ecffda7e60 100644 --- a/designer/client/src/windowManager/WindowKind.tsx +++ b/designer/client/src/windowManager/WindowKind.tsx @@ -24,7 +24,7 @@ export enum WindowKind { editProperties, remote, withChildren, - scenarioTest, aiAssistant, enterpriseFeatureInfo, + saveAsTestCase, } From 40b2c5a395749a25e8034830a5c88da5d5a044c4 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Tue, 17 Mar 2026 13:07:47 +0100 Subject: [PATCH 11/50] [NU-2475] improvements --- .../graph/node-modal/node/NodeDetails.tsx | 5 ++-- .../test/buttons/ScenarioTestButton.tsx | 15 ++++-------- designer/client/src/reducers/graph/reducer.ts | 2 +- .../client/src/reducers/graph/testCase.ts | 23 +++++++++++-------- .../src/reducers/selectors/testCases.ts | 7 +++--- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx index 2e5c9a93ddc..a07dd264fef 100644 --- a/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx +++ b/designer/client/src/components/graph/node-modal/node/NodeDetails.tsx @@ -34,8 +34,7 @@ import { useGetNodeErrors } from "../useNodeTypeDetailsContentLogic"; import { EditStateFeedback } from "./EditStateFeedback"; import { GeneralContent } from "./NodeContent/GeneralContent"; import type { TabDef } from "./NodeContent/TabsWrapper"; -import { NodeDetailsTab } from "./NodeContent/TabsWrapper"; -import { TabsWrapper } from "./NodeContent/TabsWrapper"; +import { NodeDetailsTab, TabsWrapper } from "./NodeContent/TabsWrapper"; import { TestingContent } from "./NodeContent/TestingContent"; import { TestCases } from "./NodeContent/TestingContentElements/TestCases"; import { getReadOnly } from "./selectors"; @@ -94,7 +93,7 @@ function NodeDetails(props: NodeDetailsProps): React.JSX.Element { const [generalErrors] = useGetNodeErrors(node); const testCase = useAppSelector(getActiveTestCase); const hasNodeTestCasesErrors = useAppSelector((state) => - hasValidationTestCasesErrors(state, { nodeId: node.id, testCaseId: testCase.name }), + hasValidationTestCasesErrors(state, { nodeId: node.id, testCaseId: testCase?.name }), ); const { cancel, apply } = useNodeDetailsButtons({ editedNode, outputEdges, performNodeEdit, close, readOnly }); diff --git a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx index 6e90dfd2375..cc2748dae78 100644 --- a/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx +++ b/designer/client/src/components/toolbars/test/buttons/ScenarioTestButton.tsx @@ -17,7 +17,7 @@ import type { CustomButtonTypes } from "../../../toolbarSettings/buttons/buttons import type { PropsOfButton } from "../../../toolbarSettings/buttons/types"; import { TestingIconWithAssertionStatus } from "./scenarioTestButtonContent/TestingIconWithAssertionStatus"; import { useAssertionResultsSummary } from "./scenarioTestButtonContent/useAssertionResultsSummary"; -import type { Preset} from "./scenarioTestButtonContent/useScenarioTestPresets"; +import type { Preset } from "./scenarioTestButtonContent/useScenarioTestPresets"; import { RUN_ALL, useScenarioTestPresets } from "./scenarioTestButtonContent/useScenarioTestPresets"; import { useScenarioTestTooltip } from "./scenarioTestButtonContent/useScenarioTestTooltip"; @@ -63,13 +63,6 @@ function ScenarioTestButton(props: PropsOfButton [activeTestCasePreset?.value, assertionsIsSuccess, hasResult], ); - const changeActiveTestCaseOption = useCallback( - (testCaseId: string) => { - dispatch(changeActiveTestCase(testCaseId)); - }, - [dispatch], - ); - const handleRunActiveTest = useCallback(() => handleRunTest(testCase?.id), [handleRunTest, testCase]); const handlePresetChange = useCallback( @@ -78,16 +71,16 @@ function ScenarioTestButton(props: PropsOfButton //TODO: Implement me when backend ready return; } - changeActiveTestCaseOption(preset.value); + dispatch(changeActiveTestCase(preset.value)); handleRunTest(preset.value); }, - [changeActiveTestCaseOption, handleRunTest], + [dispatch, handleRunTest], ); return ( { +const getDefaultActiveTestCaseId = (actionGraph: ScenarioGraph, stateGraph: ScenarioGraph, testing: TestingState): string | undefined => { return testing.activeTestCaseId || actionGraph?.testCases?.list[0]?.id || stateGraph.testCases.list[0]?.id; }; diff --git a/designer/client/src/reducers/graph/testCase.ts b/designer/client/src/reducers/graph/testCase.ts index 7ed1268e44b..78e6b550975 100644 --- a/designer/client/src/reducers/graph/testCase.ts +++ b/designer/client/src/reducers/graph/testCase.ts @@ -2,7 +2,10 @@ import { produce } from "immer"; import type { Assertions, Mocks } from "../../actions/nk/testCasesActions"; import type { Reducer } from "../../actions/reduxTypes"; +import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; +import { safeParseExpression } from "../../components/modals/TestingDataRecords/utils"; import type { ScenarioGraph } from "../../types/scenarioGraph"; +import { omit } from "./lodashWrappers"; export interface TestCase { id: string; @@ -45,12 +48,12 @@ export const testCaseReducer: Reducer = produce((dra case "DELETE_NODES": return { ...draft, - value: cleanTestCaseState(draft, action.ids), + list: cleanTestCaseState(draft, action.ids), }; case "ADD_NODE_REPLACE": return { ...draft, - value: cleanTestCaseState(draft, [action.old.id]), + list: cleanTestCaseState(draft, [action.old.id]), }; default: return draft; @@ -58,12 +61,12 @@ export const testCaseReducer: Reducer = produce((dra }); const cleanTestCaseState = (state: ScenarioGraph["testCases"], ids: string[]) => { - return { - ...state.list, - // assertions: omit(state.value.assertions, ids), - // mocks: omit(state.value.mocks, ids), - // inputs: JSON.stringify( - // safeParseExpression(state.value.inputs)?.filter((input) => !ids.includes(input.sourceId)), - // ), - }; + return state.list.map((testCase) => ({ + ...testCase, + assertions: omit(testCase.assertions, ids), + mocks: omit(testCase.mocks, ids), + inputs: JSON.stringify( + safeParseExpression(testCase.inputs)?.filter((input) => !ids.includes(input.sourceId)), + ), + })); }; diff --git a/designer/client/src/reducers/selectors/testCases.ts b/designer/client/src/reducers/selectors/testCases.ts index cad641aa96f..780dc6a13b4 100644 --- a/designer/client/src/reducers/selectors/testCases.ts +++ b/designer/client/src/reducers/selectors/testCases.ts @@ -1,5 +1,6 @@ import { createSelector } from "reselect"; +import type { Assertions, Mocks } from "../../actions/nk/testCasesActions"; import { withUuid } from "../../components/graph/node-modal/appendUuid"; import { MockExpressionParameter } from "../../components/graph/node-modal/editors/expression/MockExpressionField"; import type { TestingDataRecords } from "../../components/modals/TestingDataRecords/Table"; @@ -22,14 +23,14 @@ export const getActiveTestCaseOption = createSelector( getActiveTestCaseId, (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, ); -export const getTestCaseAssertions = createSelector(getActiveTestCase, (testCase) => testCase?.assertions); +export const getTestCaseAssertions = createSelector(getActiveTestCase, (testCase) => testCase?.assertions ?? ({} as Assertions)); export const getTestCaseAssertionsForNode = createSelector( getTestCaseAssertions, getNodeId, (assertions, nodeId) => assertions[nodeId]?.map(withUuid) || [], ); -export const getTestCaseMocks = createSelector(getActiveTestCase, (testCase) => testCase?.mocks); +export const getTestCaseMocks = createSelector(getActiveTestCase, (testCase) => testCase?.mocks ?? ({} as Mocks)); export const getTestCaseMockForNode = createSelector( getTestCaseMocks, getNodeId, @@ -37,7 +38,7 @@ export const getTestCaseMockForNode = createSelector( ); export const getInputDataRecords = createSelector( getActiveTestCase, - (testCase) => safeParseExpression(testCase.inputs) || [], + (testCase) => safeParseExpression(testCase?.inputs) || [], ); const getSourceId = (_: unknown, sourceId: string) => sourceId; From 2a69366e8a5a804b6ff0a8935513291093b233e7 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Wed, 18 Mar 2026 11:35:20 +0100 Subject: [PATCH 12/50] [NU-2475] fix build --- .../scenarioTestButtonContent/useScenarioTestPresets.tsx | 4 ---- designer/client/src/reducers/selectors/testCases.ts | 5 ----- 2 files changed, 9 deletions(-) diff --git a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx index 7812fe55909..ba272ce82f8 100644 --- a/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx +++ b/designer/client/src/components/toolbars/test/buttons/scenarioTestButtonContent/useScenarioTestPresets.tsx @@ -39,10 +39,6 @@ export const useScenarioTestPresets = () => { const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId) || null; - const activeTestCaseId = useAppSelector(getActiveTestCaseId); - - const activeTestCasePreset = testCasePresets.find((testCasePreset) => testCasePreset.value === activeTestCaseId); - const runAllPreset: Preset = useMemo( () => ({ icon: , diff --git a/designer/client/src/reducers/selectors/testCases.ts b/designer/client/src/reducers/selectors/testCases.ts index a40f36484cd..ba12111b71d 100644 --- a/designer/client/src/reducers/selectors/testCases.ts +++ b/designer/client/src/reducers/selectors/testCases.ts @@ -23,11 +23,6 @@ export const getActiveTestCaseOption = createSelector( getActiveTestCaseId, (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, ); -export const getActiveTestCaseOption = createSelector( - getTestCaseOptions, - getActiveTestCaseId, - (testCaseOptions, activeTestCaseId) => testCaseOptions.find((option) => option.value === activeTestCaseId) || null, -); export const getTestCaseAssertions = createSelector(getActiveTestCase, (testCase) => testCase?.assertions ?? ({} as Assertions)); export const getTestCaseAssertionsForNode = createSelector( getTestCaseAssertions, From 28200b43d7f095b3a192455d23d9b0217c792ccf Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Wed, 18 Mar 2026 12:55:24 +0100 Subject: [PATCH 13/50] [NU-2475] fix action on test case header --- .../src/components/toolbars/testCases/testCase.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/designer/client/src/components/toolbars/testCases/testCase.tsx b/designer/client/src/components/toolbars/testCases/testCase.tsx index ffd25c15e84..064e1f351ef 100644 --- a/designer/client/src/components/toolbars/testCases/testCase.tsx +++ b/designer/client/src/components/toolbars/testCases/testCase.tsx @@ -3,14 +3,13 @@ import SvgIcon from "@mui/material/SvgIcon/SvgIcon"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; -import { testScenarioWithTestCase } from "../../../actions/nk/testingActions"; import TestingIcon from "../../../assets/img/toolbarButtons/test.svg"; -import { useUserSettings } from "../../../common/useUserSettings"; import type { TestCase } from "../../../reducers/graph/testCase"; import { getTestAssertionResults, getTestResultsLoading } from "../../../reducers/selectors/testing"; -import { useAppDispatch, useAppSelector } from "../../../store/storeHelpers"; +import { useAppSelector } from "../../../store/storeHelpers"; import { Expandable } from "../../common/Expandable"; import { InfoTooltip } from "../../graph/node-modal/editors/InfoTooltip/InfoTooltip"; +import { useRunTestScenario } from "../test/useRunTestScenario"; import { AssertionResultsBadge } from "./assertionResultsForNode/AssertionResultsBadge"; import { Definitions } from "./definitions"; import { Footer } from "./footer"; @@ -54,15 +53,15 @@ const TestCaseTitle = ({ testCase }: TestCaseTitleProps) => { const testAssertionResults = useAppSelector(getTestAssertionResults); const allResults = Object.values(testAssertionResults).flat(); const isLoading = useAppSelector(getTestResultsLoading); - const [showMockFieldOnEnrichers] = useUserSettings("node.showMockFieldOnEnrichers"); - const dispatch = useAppDispatch(); + + const { runTest } = useRunTestScenario(); const handleRun = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - dispatch(testScenarioWithTestCase(testCase, showMockFieldOnEnrichers)); + runTest(testCase); }, - [dispatch, testCase, showMockFieldOnEnrichers], + [runTest, testCase], ); return ( From e2845b7244d43445990437d7225c6fb1920786e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20J=C4=99drzejewski?= Date: Wed, 18 Mar 2026 10:31:17 +0100 Subject: [PATCH 14/50] Do not encode single test case --- .../ui/api/ProcessesResourcesSpec.scala | 5 ---- .../ui/api/ValidationResourcesSpec.scala | 3 +-- .../engine/test/testcase/TestCase.scala | 23 +++++++------------ 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala index 3b46a2b6177..2e17cbae003 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala @@ -1531,11 +1531,6 @@ class ProcessesResourcesSpec getProcess(processName) ~> check { val scenarioDetails = responseAs[ScenarioWithDetails] scenarioDetails.scenarioGraph.flatMap(_.testCases) shouldBe Some(TestCases(NonEmptyList.one(testCase))) - - val json = responseAs[Json] - val testCasesCursor = json.hcursor.downField("scenarioGraph").downField("testCases") - testCasesCursor.downField("value").as[TestCase].value shouldBe testCase - testCasesCursor.downField("list").as[List[TestCase]].value shouldBe List(testCase) } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ValidationResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ValidationResourcesSpec.scala index 4adbfb881cd..7307aca49b5 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ValidationResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ValidationResourcesSpec.scala @@ -468,8 +468,7 @@ class ValidationResourcesSpec } } - // TODO NU-2470: unignore when FE is updated to use "list" field - ignore should "find error for duplicate test case names" in { + it should "find error for duplicate test case names" in { val scenarioGraph = ScenarioBuilder .streaming("testCaseValidationScenario") .additionalFields(properties = Map("requiredStringProperty" -> "some value")) diff --git a/scenario-api/src/main/scala/pl/touk/nussknacker/engine/test/testcase/TestCase.scala b/scenario-api/src/main/scala/pl/touk/nussknacker/engine/test/testcase/TestCase.scala index bae3e8eb3f1..1b0dc67e2bd 100644 --- a/scenario-api/src/main/scala/pl/touk/nussknacker/engine/test/testcase/TestCase.scala +++ b/scenario-api/src/main/scala/pl/touk/nussknacker/engine/test/testcase/TestCase.scala @@ -3,8 +3,9 @@ package pl.touk.nussknacker.engine.test.testcase import cats.data.NonEmptyList import enumeratum.{CirceEnum, Enum, EnumEntry} import enumeratum.EnumEntry.LowerCamelcase -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, Encoder} import io.circe.generic.JsonCodec +import io.circe.generic.semiauto.deriveEncoder import io.circe.syntax._ import pl.touk.nussknacker.engine.api.NodeId import pl.touk.nussknacker.engine.graph.expression.Expression @@ -13,23 +14,15 @@ final case class TestCases(list: NonEmptyList[TestCase]) object TestCases { - // TODO: remove encoder and switch decoder to use "list" and fallback to "value" when FE is updated to use "list" field - - implicit val encoder: Encoder[TestCases] = Encoder.instance { testCases => - Json.obj( - "list" -> testCases.list.asJson, - // Return "value" for backward compatibility - "value" -> testCases.list.head.asJson - ) - } + implicit val encoder: Encoder[TestCases] = deriveEncoder implicit val decoder: Decoder[TestCases] = Decoder.instance { cursor => - val valueCursor = cursor.downField("value") - // For backward compatibility, if "value" field is present, try to decode a single test case from "value" field - if (valueCursor.succeeded) { - valueCursor.as[TestCase].map(testCase => TestCases(NonEmptyList.one(testCase))) + val listCursor = cursor.downField("list") + if (listCursor.succeeded) { + listCursor.as[NonEmptyList[TestCase]].map(TestCases(_)) } else { - cursor.downField("list").as[NonEmptyList[TestCase]].map(TestCases(_)) + // For backward compatibility, if "list" field is not present, try to decode a single test case from "value" field + cursor.downField("value").as[TestCase].map(testCase => TestCases(NonEmptyList.one(testCase))) } } From f98de321e87dc48f8cbbff2e267a9ac020c29420 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Wed, 18 Mar 2026 13:17:18 +0100 Subject: [PATCH 15/50] [NU-2475] fix checking test data per test case --- .../client/src/actions/nk/testCasesActions.ts | 17 ++++++++++++----- .../toolbars/test/useRunTestScenario.ts | 5 +++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/designer/client/src/actions/nk/testCasesActions.ts b/designer/client/src/actions/nk/testCasesActions.ts index 635b05047c1..c2c1babe330 100644 --- a/designer/client/src/actions/nk/testCasesActions.ts +++ b/designer/client/src/actions/nk/testCasesActions.ts @@ -21,9 +21,12 @@ export function setTestCaseAssertions(nodeId: string, updater: (prev: WithUuid { const sourceNodeAvailable = sourceNodes?.[0]; + const testDataDefined = (safeParseExpression(testCase?.inputs) || []).length > 0; if (!testDataDefined && sourceNodeAvailable) { openAddTestDataNode(sourceNodeAvailable); From e53c355ac30d5211d3f842e14d715007060ad4d9 Mon Sep 17 00:00:00 2001 From: Dawid Poliszak Date: Thu, 19 Mar 2026 09:17:52 +0100 Subject: [PATCH 16/50] [NU-2475] pass test case to specific item --- .../client/src/components/toolbars/testCases/footer.tsx | 4 ++-- .../client/src/components/toolbars/testCases/results.tsx | 6 +++--- .../client/src/components/toolbars/testCases/testCase.tsx | 2 +- .../src/components/toolbars/testCases/testCasesPanel.tsx | 8 +++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/designer/client/src/components/toolbars/testCases/footer.tsx b/designer/client/src/components/toolbars/testCases/footer.tsx index 5d08cbf707f..0f52872a7a1 100644 --- a/designer/client/src/components/toolbars/testCases/footer.tsx +++ b/designer/client/src/components/toolbars/testCases/footer.tsx @@ -1,6 +1,6 @@ -import { Box, styled, Typography } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import moment from "moment"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { usePreviousDifferent } from "rooks"; diff --git a/designer/client/src/components/toolbars/testCases/results.tsx b/designer/client/src/components/toolbars/testCases/results.tsx index a4e2b554ee7..866b665b68f 100644 --- a/designer/client/src/components/toolbars/testCases/results.tsx +++ b/designer/client/src/components/toolbars/testCases/results.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { TestAssertionResults } from "../../../http/resultsWithCountsDto"; -import { getActiveTestCase } from "../../../reducers/selectors/testCases"; +import type { TestCase } from "../../../reducers/graph/testCase"; import { getTestResultsLoading } from "../../../reducers/selectors/testing"; import { useAppSelector } from "../../../store/storeHelpers"; import type { NodeType } from "../../../types/node"; @@ -17,11 +17,11 @@ import { AssertionResultsForNodeTitle } from "./assertionResultsForNode/assertio import { useScenarioNodeOrder } from "./useScenarioNodeOrder"; interface Props { + testCase: TestCase; testAssertionResults: TestAssertionResults; } -export const Results = ({ testAssertionResults }: Props) => { - const testCase = useAppSelector(getActiveTestCase); +export const Results = ({ testAssertionResults, testCase }: Props) => { const isLoading = useAppSelector(getTestResultsLoading); const testingScenarioEnabled = useTestingScenarioEnabled({ disabled: false }); diff --git a/designer/client/src/components/toolbars/testCases/testCase.tsx b/designer/client/src/components/toolbars/testCases/testCase.tsx index 064e1f351ef..08da7a46cfe 100644 --- a/designer/client/src/components/toolbars/testCases/testCase.tsx +++ b/designer/client/src/components/toolbars/testCases/testCase.tsx @@ -36,7 +36,7 @@ export const TestCaseExpandable = ({ testCase }: TestCaseExpandableProps) => { detailsSx={{ p: 0 }} > - {mode === "results" && } + {mode === "results" && } {mode === "definitions" && }