From ba1d4b24e3b97eb50b977dc212adbb2aa439b705 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:38:23 -0700 Subject: [PATCH 1/4] Draft scenario generation of test cases. --- app/components/InputStyler/InputStyler.tsx | 1 - .../subcomponents/InputComponents.tsx | 8 +- .../RuleInputOutputFieldsComponent.tsx | 4 +- .../IsolationTester.module.css | 74 ++++++++++ .../IsolationTester/IsolationTester.tsx | 136 ++++++++++++++++++ .../ScenariosManager/IsolationTester/index.ts | 1 + .../ScenariosManager/ScenariosManager.tsx | 25 ++++ app/utils/api.ts | 40 +++++- app/utils/utils.ts | 13 ++ 9 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 app/components/ScenariosManager/IsolationTester/IsolationTester.module.css create mode 100644 app/components/ScenariosManager/IsolationTester/IsolationTester.tsx create mode 100644 app/components/ScenariosManager/IsolationTester/index.ts diff --git a/app/components/InputStyler/InputStyler.tsx b/app/components/InputStyler/InputStyler.tsx index bc4d626..3541fe9 100644 --- a/app/components/InputStyler/InputStyler.tsx +++ b/app/components/InputStyler/InputStyler.tsx @@ -80,7 +80,6 @@ export default function InputStyler( ruleProperties: any ) { const updateFieldValue = (field: string, value: any) => { - console.log(field, value, "this is input change"); const updatedData = { ...rawData, [field]: value }; if (typeof setRawData === "function") { setRawData(updatedData); diff --git a/app/components/InputStyler/subcomponents/InputComponents.tsx b/app/components/InputStyler/subcomponents/InputComponents.tsx index f8e676b..3331a10 100644 --- a/app/components/InputStyler/subcomponents/InputComponents.tsx +++ b/app/components/InputStyler/subcomponents/InputComponents.tsx @@ -39,11 +39,11 @@ export const ChildFieldInput = ({ rawData, value, }: ChildFieldInputProps) => ( -
+
{each.label} {InputStyler( - item[each.name], - `${field}[${index}].${each.name}`, + item[each.field], + `${field}[${index}].${each.field}`, true, scenarios, rawData, @@ -51,7 +51,7 @@ export const ChildFieldInput = ({ const updatedArray = [...value]; updatedArray[index] = { ...updatedArray[index], - [each.name]: newData[`${field}[${index}].${each.name}`], + [each.field]: newData[`${field}[${index}].${each.field}`], }; handleInputChange?.(updatedArray, field); }, diff --git a/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx b/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx index 5cdb921..53bd3dc 100644 --- a/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx +++ b/app/components/RuleViewerEditor/subcomponents/RuleInputOutputFieldsComponent.tsx @@ -136,8 +136,8 @@ export default function RuleInputOutputFieldsComponent({ klammData?.child_fields && klammData?.child_fields.map((child) => ({ id: child.id, - name: child.name, - field: child.label, + name: child.label, + field: child.name, description: child.description, dataType: child?.bre_data_type?.name, validationCriteria: child?.bre_data_validation?.validation_criteria, diff --git a/app/components/ScenariosManager/IsolationTester/IsolationTester.module.css b/app/components/ScenariosManager/IsolationTester/IsolationTester.module.css new file mode 100644 index 0000000..9d8e3c7 --- /dev/null +++ b/app/components/ScenariosManager/IsolationTester/IsolationTester.module.css @@ -0,0 +1,74 @@ +.instructionsList { + list-style: none; + padding: 20px; + margin: 20px 0; + background-color: #f4f4f9; + border-radius: 8px; + border: 1px solid #eaeaea; +} + +.instructionsList li { + margin-bottom: 15px; + padding: 10px; + background: #fff; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-size: 16px; + line-height: 1.5; + position: relative; +} + +.instructionsList li::before { + content: counter(li); + counter-increment: li; + position: absolute; + left: -30px; + top: 50%; + transform: translateY(-50%); + background: #007bff; + color: white; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.instructionsList a { + color: #007bff; + text-decoration: none; +} + +.instructionsList a:hover { + text-decoration: underline; +} + +.instructionsList { + counter-reset: li; +} + +.upload { + margin-right: 10px; +} + +.runButton { + margin-left: 10px; +} + +.scenarioGenerator { + width: 100%; +} + +@media (max-width: 1100px) { + .scenarioGenerator { + flex-wrap: wrap; + } +} + +@media (max-width: 768px) { + .scenarioGenerator { + flex-direction: column; + width: 100%; + } +} \ No newline at end of file diff --git a/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx new file mode 100644 index 0000000..5b5acf7 --- /dev/null +++ b/app/components/ScenariosManager/IsolationTester/IsolationTester.tsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from "react"; +import { Flex, Button, message, InputNumber, Collapse } from "antd"; +import { Scenario } from "@/app/types/scenario"; +import { getCSVTests } from "@/app/utils/api"; +import { RuleMap } from "@/app/types/rulemap"; +import ScenarioFormatter from "../ScenarioFormatter"; +import styles from "./IsolationTester.module.css"; +import { DecisionGraphType } from "@gorules/jdm-editor"; +import { valueType } from "antd/es/statistic/utils"; + +interface IsolationTesterProps { + scenarios: Scenario[]; + resultsOfSimulation: Record | null | undefined; + simulationContext?: Record; + setSimulationContext: (data: any) => void; + resetTrigger: boolean; + jsonFile: string; + rulemap: RuleMap; + scenarioName?: string; + ruleContent?: DecisionGraphType; + ruleVersion?: string | boolean; +} + +export default function IsolationTester({ + scenarios, + resultsOfSimulation, + simulationContext, + setSimulationContext, + resetTrigger, + jsonFile, + rulemap, + scenarioName, + ruleContent, + ruleVersion, +}: IsolationTesterProps) { + const [simulationRun, setSimulationRun] = useState(false); + const [scenarioExpectedOutput, setScenarioExpectedOutput] = useState({}); + const [editingScenario, setEditingScenario] = useState(scenarioName && scenarioName.length > 0 ? true : false); + const [testScenarioCount, setTestScenarioCount] = useState(10); + + const handleCSVTests = async () => { + const ruleName = ruleVersion === "draft" ? "Draft" : ruleVersion === "inreview" ? "In Review" : "Published"; + try { + const csvContent = await getCSVTests(jsonFile, ruleName, ruleContent, simulationContext, testScenarioCount); + message.success(`Scenario Tests: ${csvContent}`); + } catch (error) { + message.error("Error downloading scenarios."); + console.error("Error:", error); + } + }; + + useEffect(() => { + setSimulationRun(false); + setScenarioExpectedOutput(resultsOfSimulation ?? {}); + const editScenario = { ...simulationContext, rulemap: true }; + setSimulationContext(editScenario); + setEditingScenario(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resetTrigger]); + + useEffect(() => { + const expectedOutputsMap = rulemap.resultOutputs.reduce>((acc, obj: { field?: string }) => { + if (obj?.field) { + acc[obj.field] = null; + } + return acc; + }, {}); + setScenarioExpectedOutput(expectedOutputsMap); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + +
+ +
    +
  1. + This tab allows you to test your rule by defining specific variables you would like to remain unchanged + while generating possible scenarios that combine the possibilities of the other variables you leave blank. +
  2. +
  3. + Define any variables you would like to remain unchanged. The more that you define, the more specific the + tests will be. + + {simulationContext && ( + + setSimulationContext(data)} + scenarios={scenarios} + rulemap={rulemap} + /> + + )} + + ), + }, + ]} + /> +
  4. +
  5. + {" "} + Any undefined variables will be randomly generated based on the validation values defined in klamm for + these inputs. +
  6. +
  7. + Enter the number of scenarios you would like to generate here (there is a maximum of 1000):{" "} + setTestScenarioCount(value)} + changeOnBlur + defaultValue={10} + min={1} + max={1000} + /> +
  8. +
  9. + Generate a CSV file with your created tests:{" "} + +
  10. +
+
+
+
+ ); +} diff --git a/app/components/ScenariosManager/IsolationTester/index.ts b/app/components/ScenariosManager/IsolationTester/index.ts new file mode 100644 index 0000000..409c31c --- /dev/null +++ b/app/components/ScenariosManager/IsolationTester/index.ts @@ -0,0 +1 @@ +export { default } from "./IsolationTester"; diff --git a/app/components/ScenariosManager/ScenariosManager.tsx b/app/components/ScenariosManager/ScenariosManager.tsx index 658e47a..c4e415b 100644 --- a/app/components/ScenariosManager/ScenariosManager.tsx +++ b/app/components/ScenariosManager/ScenariosManager.tsx @@ -9,6 +9,7 @@ import ScenarioGenerator from "./ScenarioGenerator"; import ScenarioResults from "./ScenarioResults"; import ScenarioCSV from "./ScenarioCSV"; import styles from "./ScenariosManager.module.css"; +import IsolationTester from "./IsolationTester"; interface ScenariosManagerProps { ruleId: string; @@ -44,6 +45,7 @@ export default function ScenariosManager({ InputsTab = "2", ResultsTab = "3", CSVTab = "4", + IsolationTesterTab = "5", } const [resetTrigger, setResetTrigger] = useState(false); @@ -123,6 +125,23 @@ export default function ScenariosManager({ ); + const isolationTestTab = ( + + + + ); + const items: TabsProps["items"] = [ { key: ScenariosManagerTabs.ScenariosTab, @@ -148,6 +167,12 @@ export default function ScenariosManager({ children: csvTab, disabled: !showAllScenarioTabs, }, + { + key: ScenariosManagerTabs.IsolationTesterTab, + label: "Isolation Tester", + children: isolationTestTab, + disabled: !showAllScenarioTabs, + }, ]; const filteredItems = showAllScenarioTabs ? items : items?.filter((item) => item.disabled !== true) || []; diff --git a/app/utils/api.ts b/app/utils/api.ts index e879a74..67cc739 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -3,7 +3,8 @@ import axios from "axios"; import { RuleDraft, RuleInfo } from "../types/ruleInfo"; import { RuleMap } from "../types/rulemap"; import { KlammBREField } from "../types/klamm"; -import { downloadFileBlob } from "./utils"; +import { downloadFileBlob, generateDescriptiveName } from "./utils"; +import { valueType } from "antd/es/statistic/utils"; const axiosAPIInstance = axios.create({ // For server side calls, need full URL, otherwise can just use /api @@ -404,3 +405,40 @@ export const getBREFieldFromName = async (fieldName: string): Promise, + testScenarioCount?: valueType | number | null +): Promise => { + try { + const response = await axiosAPIInstance.post( + "/scenario/test", + { goRulesJSONFilename, ruleContent, simulationContext, testScenarioCount }, + { + responseType: "blob", + headers: { "Content-Type": "application/json" }, + } + ); + + const scenarioName = simulationContext ? `${generateDescriptiveName(simulationContext)}` : ""; + + const filename = `${(scenarioName + goRulesJSONFilename).replace(/\.json$/, ".csv")}`; + downloadFileBlob(response.data, "text/csv", filename); + + return "CSV downloaded successfully"; + } catch (error) { + console.error(`Error getting CSV for rule run: ${error}`); + throw new Error("Error getting CSV for rule run"); + } +}; diff --git a/app/utils/utils.ts b/app/utils/utils.ts index 589dd09..c6308a8 100644 --- a/app/utils/utils.ts +++ b/app/utils/utils.ts @@ -136,3 +136,16 @@ export const getFieldValidation = (validationCriteria: string, validationType: s return validationRules; }; + +/** + * Generates a descriptive name for a scenario based on its properties + * @param obj The object representing the scenario + * @returns A string representing the descriptive name + */ + +export const generateDescriptiveName = (obj: Record): string => { + return Object.entries(obj) + .filter(([key, value]) => value !== null && key !== "rulemap") + .map(([key, value]) => `${key}_${value}`) + .join("_"); +}; From 4087a9ffa6ee0a0982526f6640b776e8c6766955 Mon Sep 17 00:00:00 2001 From: brysonjbest <103070659+brysonjbest@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:44:49 -0700 Subject: [PATCH 2/4] Update for nested scenarios test generation. --- app/components/InputStyler/InputStyler.tsx | 17 +++++++++++++ .../subcomponents/InputComponents.tsx | 13 +++++----- .../IsolationTester/IsolationTester.tsx | 2 +- .../ScenarioResults/ScenarioResults.tsx | 11 +++++++- .../ScenariosManager/ScenariosManager.tsx | 3 +++ app/types/inputs.d.ts | 1 + app/utils/api.ts | 10 +++++++- app/utils/utils.ts | 25 +++++++++++-------- 8 files changed, 63 insertions(+), 19 deletions(-) diff --git a/app/components/InputStyler/InputStyler.tsx b/app/components/InputStyler/InputStyler.tsx index 3541fe9..9130b61 100644 --- a/app/components/InputStyler/InputStyler.tsx +++ b/app/components/InputStyler/InputStyler.tsx @@ -160,6 +160,17 @@ export default function InputStyler( handleInputChange={handleInputChange} /> ); + case "multiselect": + return ( + + ); case "text": return ( typeof item === "object" && item !== null); + if (Array.isArray(value) && !allObjects) { + const stringValue = value.filter((item) => typeof item !== "object" || item === null).join(", "); + return ; + } + return ( <> { - acc[field.name] = null; + const childFieldMap = childFields.reduce((acc: { [x: string]: null }, field: { field: string | number }) => { + acc[field.field] = null; return acc; }, {}); @@ -95,9 +95,9 @@ export const ObjectArrayInput = ({ {customName} {index + 1}