diff --git a/captain/models/test_sequencer.py b/captain/models/test_sequencer.py index 0c02e7408..d0239b60a 100644 --- a/captain/models/test_sequencer.py +++ b/captain/models/test_sequencer.py @@ -20,6 +20,7 @@ class TestTypes(StrEnum): flojoy = "flojoy" matlab = "matlab" placeholder = "placeholder" + robotframework = "robotframework" class StatusTypes(StrEnum): @@ -66,6 +67,7 @@ class Test(BaseModel): max_value: Optional[float] = Field(None, alias="maxValue") measured_value: Optional[float] = Field(None, alias="measuredValue") unit: Optional[str] = Field(None, alias="unit") + args: Optional[List[str]] = Field(None, alias="args") class Role(StrEnum): diff --git a/captain/routes/test_sequence.py b/captain/routes/test_sequence.py index 51b2fa216..84971ea4b 100644 --- a/captain/routes/test_sequence.py +++ b/captain/routes/test_sequence.py @@ -3,7 +3,10 @@ import pydantic from captain.models.pytest.pytest_models import TestDiscoverContainer from captain.models.test_sequencer import TestSequenceRun -from captain.utils.pytest.discover_tests import discover_pytest_file +from captain.utils.pytest.discover_tests import ( + discover_pytest_file, + discover_robot_file, +) from captain.utils.config import ts_manager from captain.utils.test_sequencer.handle_data import handle_data from captain.utils.logger import logger @@ -34,13 +37,13 @@ async def websocket_endpoint(websocket: WebSocket, socket_id: str): logger.info(f"Client {socket_id} is disconnected") -class DiscoverPytestParams(BaseModel): +class DiscoverParams(BaseModel): path: str one_file: bool = Field(..., alias="oneFile") -@router.get("/discover-pytest/") -async def discover_pytest(params: DiscoverPytestParams = Depends()): +@router.get("/discover/pytest/") +async def discover_pytest(params: DiscoverParams = Depends()): path = params.path one_file = params.one_file return_val, missing_lib, errors = [], [], [] # For passing info between threads @@ -55,3 +58,21 @@ async def discover_pytest(params: DiscoverPytestParams = Depends()): missing_libraries=missing_lib, error=errors[0] if len(errors) > 0 else None, ) + + +@router.get("/discover/robot/") +async def discover_robot(params: DiscoverParams = Depends()): + path = params.path + one_file = params.one_file + return_val, errors = [], [] # For passing info between threads + thread = Thread( + target=discover_robot_file, + args=(path, one_file, return_val, errors), + ) + thread.start() + thread.join() + return TestDiscoverContainer( + response=return_val, + missing_libraries=[], + error=errors[0] if len(errors) > 0 else None, + ) diff --git a/captain/utils/pytest/discover_tests.py b/captain/utils/pytest/discover_tests.py index c6b183306..b84c16b60 100644 --- a/captain/utils/pytest/discover_tests.py +++ b/captain/utils/pytest/discover_tests.py @@ -1,3 +1,4 @@ +import logging import os from captain.utils.logger import logger from typing import List, Union @@ -12,6 +13,7 @@ from captain.utils.import_utils import unload_module import re import pathlib +from robot.running.builder import TestSuiteBuilder def extract_error(report: RootModel): @@ -84,3 +86,28 @@ def dfs( ) else: return_val.extend(test_list) + + +def discover_robot_file(path: str, one_file: bool, return_val: list, errors: list): + try: + builder = TestSuiteBuilder() + suite = builder.build(path) + logging.info( + f"Suite: {suite} - suites in it: {suite.suites} - tests in it: {suite.tests}" + ) + if one_file: + return_val.append( + TestDiscoveryResponse( + test_name=suite.full_name, path=pathlib.Path(path).as_posix() + ) + ) + else: + for test in suite.tests: + return_val.append( + TestDiscoveryResponse( + test_name=test.full_name, path=pathlib.Path(path).as_posix() + ) + ) + + except Exception as e: + errors.append(str(e)) diff --git a/captain/utils/test_sequencer/run_test_sequence.py b/captain/utils/test_sequencer/run_test_sequence.py index 1783f2e00..c17221c43 100644 --- a/captain/utils/test_sequencer/run_test_sequence.py +++ b/captain/utils/test_sequencer/run_test_sequence.py @@ -186,6 +186,44 @@ def _run_placeholder(node: TestNode) -> Extract: ) +@_with_stream_test_result +def _run_robotframework(node: TestNode) -> Extract: + """ + runs python file. + @params file_path: path to the file + @returns: + bool: result of the test + float: time taken to execute the test + str: error message if any + """ + start_time = time.time() + logger.info(f"[Robot Framework Runner] Running {node.path}") + if node.args is not None: + cmd = ["robot", "--test", *node.args, node.path] + else: + cmd = ["robot", node.path] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + logger.info(f"[Robot Framework Runner] Running {result}") + end_time = time.time() + if result.returncode == 0: + is_pass = True + else: + logger.info( + f"TEST {node.path} FAILED:\nSTDOUT: {result.stdout.decode()}\nSTDERR: {result.stderr.decode()}" + ) + is_pass = False + return ( + lambda _: None, + TestResult( + node, + is_pass, + end_time - start_time, + result.stderr.decode() if not is_pass else None, + utcnow_str(), + ), + ) + + def _eval_condition( result_dict: dict[str, TestResult], condition: str, identifiers: set[str] ): @@ -266,6 +304,7 @@ def get_next_children_from_context(context: Context): TestTypes.python: (None, _run_python), TestTypes.pytest: (None, _run_pytest), TestTypes.placeholder: (None, _run_placeholder), + TestTypes.robotframework: (None, _run_robotframework), }, ), "conditional": ( diff --git a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy new file mode 100644 index 000000000..4c6042803 --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy @@ -0,0 +1 @@ +{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} \ No newline at end of file diff --git a/examples/test-sequencer-robot-framework-example/TestExample.robot b/examples/test-sequencer-robot-framework-example/TestExample.robot new file mode 100644 index 000000000..935164cb1 --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/TestExample.robot @@ -0,0 +1,19 @@ +*** Settings *** +Library flojoy_cloud.test_sequencer +Library calculate.py + +*** Test Cases *** +TEST EXPORT + ${result} Calculate 3 + 1 + # Export the `result` so it's display in the sequencer + # + this value will be upload to Flojoy Cloud + Export ${result} + Should Not Be Equal 4 ${result} + +TEST ASSERT + ${result} Calculate 1 + 1 + Export ${result} + # Call the `is_in_range` from the test_sequencer + ${ok} Is In Range ${result} + Should Be True ${ok} + diff --git a/examples/test-sequencer-robot-framework-example/calculate.py b/examples/test-sequencer-robot-framework-example/calculate.py new file mode 100644 index 000000000..4df04fb4e --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/calculate.py @@ -0,0 +1,5 @@ +def calculate(term): + if term == "": + return 0 + else: + return eval(term) diff --git a/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt b/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt new file mode 100644 index 000000000..f1e0ac08a --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt @@ -0,0 +1 @@ +psutil==5.9.8 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 9c377d09a..dd3dabbc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -2533,7 +2533,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -4047,6 +4046,17 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "robotframework" +version = "7.0" +description = "Generic automation framework for acceptance testing and robotic process automation (RPA)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "robotframework-7.0-py3-none-any.whl", hash = "sha256:865f427c4e4ec8c0b71a24dedbdad6668adfecc9fce04d77d02e1b8e54b77f41"}, + {file = "robotframework-7.0.zip", hash = "sha256:04623f758346c917db182e17591ffa474090560c02ed5a64343902e72b7b4bd5"}, +] + [[package]] name = "rpds-py" version = "0.18.0" @@ -5421,4 +5431,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "c90e45542824c888f1fc21254f20d704111fe23fde01d1200109427233736f49" +content-hash = "4cbeb9ef3f59318e9d5ca01d662c776c37627aa04dd98157c36d26bf87b43356" diff --git a/pyproject.toml b/pyproject.toml index 1cb7483e4..d8646857c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ nimodinst = "^1.4.7" flojoy-cloud = "^0.2.1" bcrypt = "^4.1.2" tinymovr = "^1.6.5" +robotframework = "^7.0" [tool.poetry.group.blocks.dependencies] scikit-image = "^0.22.0" @@ -75,6 +76,7 @@ optional = true scikit-learn = "^1.3.2" [tool.poetry.group.user.dependencies] +psutil = "5.9.8" [build-system] requires = ["poetry-core"] diff --git a/src/main/ipc-main-handlers.ts b/src/main/ipc-main-handlers.ts index 59af85398..6b252b129 100644 --- a/src/main/ipc-main-handlers.ts +++ b/src/main/ipc-main-handlers.ts @@ -154,7 +154,7 @@ export const registerIpcMainHandlers = () => { ipcMain.handle( API.openTestPicker, - async (e) => await openFilePicker(e, "Test", ["json", "py"]), + async (e) => await openFilePicker(e, "Test", ["json", "py", "robot"]), ); ipcMain.handle(API.openEditorWindow, (_, filepath) => { createEditorWindow(filepath); diff --git a/src/renderer/hooks/useTestImport.ts b/src/renderer/hooks/useTestImport.ts index 8fdeddce7..deefc0a0a 100644 --- a/src/renderer/hooks/useTestImport.ts +++ b/src/renderer/hooks/useTestImport.ts @@ -1,16 +1,22 @@ -import { TestDiscoverContainer } from "@/renderer/types/test-sequencer"; +import { + TestDiscoverContainer, +} from "@/renderer/types/test-sequencer"; import { createNewTest, useDisplayedSequenceState, } from "./useTestSequencerState"; import { map } from "lodash"; -import { ImportTestSettings } from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal"; +import { + ImportTestSettings, + discoverableTestTypes as DiscoverableTestTypes, +} from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal"; import { toast } from "sonner"; import { useCallback } from "react"; -import { discoverPytest } from "@/renderer/lib/api"; +import { discoverPytest, discoverRobot } from "@/renderer/lib/api"; import { useSequencerModalStore } from "../stores/modal"; import { toastResultPromise } from "../utils/report-error"; import { Result, err, ok } from "neverthrow"; +import { match } from "ts-pattern"; function parseDiscoverContainer( data: TestDiscoverContainer, @@ -21,6 +27,10 @@ function parseDiscoverContainer( name: container.testName, path: container.path, type: settings.importType, + args: + settings.importType === "robotframework" && !settings.importAsOneRef + ? [container.testName] + : undefined, }); return new_elem; }); @@ -47,20 +57,27 @@ export const useDiscoverAndImportTests = () => { settings: ImportTestSettings, setModalOpen: (val: boolean) => void, ): Promise> { - let data: TestDiscoverContainer; - if (settings.importType == "python") { - data = { - response: [{ testName: path, path: path }], - missingLibraries: [], - error: null, - }; - } else { - const res = await discoverPytest(path, settings.importAsOneRef); - if (res.isErr()) { - return err(res.error); - } - data = res.value; + const dataResponse = await match(settings.importType) + .with("python", async () => { + return ok({ + response: [{ testName: path, path: path }], + missingLibraries: [], + error: null, + }); + }) + .with( + "pytest", + async () => await discoverPytest(path, settings.importAsOneRef), + ) + .with( + "robotframework", + async () => await discoverRobot(path, settings.importAsOneRef), + ) + .exhaustive(); + if (dataResponse.isErr()) { + return err(dataResponse.error); } + const data = dataResponse.value; if (data.error) { return err(Error(data.error)); } @@ -126,7 +143,7 @@ export const useDiscoverAndImportTests = () => { return openFilePicker; }; -export const useDiscoverPytestElements = () => { +export const useDiscoverElements = () => { const handleUserDepInstall = useCallback(async (depName: string) => { const promise = () => window.api.poetryInstallDepUserGroup(depName); toast.promise(promise, { @@ -140,7 +157,15 @@ export const useDiscoverPytestElements = () => { }, []); async function getTests(path: string) { - const res = await discoverPytest(path, false); + let res: Result; + let type: DiscoverableTestTypes; + if (path.endsWith(".robot")) { + res = await discoverRobot(path, false); + type = "robotframework"; + } else { + res = await discoverPytest(path, false); + type = "pytest"; + } if (res.isErr()) { return err(res.error); } @@ -161,7 +186,7 @@ export const useDiscoverPytestElements = () => { } const newElems = parseDiscoverContainer(data, { importAsOneRef: false, - importType: "pytest", + importType: type, }); if (newElems.length === 0) { return err(Error("No tests were found in the specified file.")); diff --git a/src/renderer/hooks/useTestSequencerState.ts b/src/renderer/hooks/useTestSequencerState.ts index cc91fc329..570d9f48f 100644 --- a/src/renderer/hooks/useTestSequencerState.ts +++ b/src/renderer/hooks/useTestSequencerState.ts @@ -109,6 +109,7 @@ export const NewTest = z.object({ minValue: z.number().optional(), maxValue: z.number().optional(), unit: z.string().optional(), + args: z.string().array().optional(), }); export type NewTest = z.infer; @@ -131,6 +132,7 @@ export function createNewTest(test: NewTest): Test { minValue: test.minValue, maxValue: test.maxValue, unit: test.unit, + args: test.args, }; return newTest; } diff --git a/src/renderer/lib/api.ts b/src/renderer/lib/api.ts index 2a60fad49..42ad21952 100644 --- a/src/renderer/lib/api.ts +++ b/src/renderer/lib/api.ts @@ -139,7 +139,16 @@ export const setLogLevel = async (level: string) => { }; export const discoverPytest = async (path: string, oneFile: boolean) => { - return get("discover-pytest", TestDiscoverContainer, { + return get("discover/pytest", TestDiscoverContainer, { + searchParams: { + path, + oneFile, + }, + }); +}; + +export const discoverRobot = async (path: string, oneFile: boolean) => { + return get("discover/robot", TestDiscoverContainer, { searchParams: { path, oneFile, diff --git a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx index 2c5dc7c74..7a560938e 100644 --- a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx @@ -2,7 +2,14 @@ import { useSequencerModalStore } from "@/renderer/stores/modal"; import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; import { Button } from "@/renderer/components/ui/button"; import { ACTIONS_HEIGHT } from "@/renderer/routes/common/Layout"; -import { FlaskConical, Import, LayoutGrid, Plus, Route } from "lucide-react"; +import { + FlaskConical, + Import, + LayoutGrid, + Plus, + Route, + TestTube, +} from "lucide-react"; import { StatusType, Test, @@ -65,11 +72,11 @@ export function DesignBar() { }, [elems, sequences, cycleRuns]); const [displayTotal, setDisplayTotal] = useState(false); + const [isGalleryOpen, setIsGalleryOpen] = useState(false); const [ isCreatePlaceholderTestModalOpen, setIsCreatePlaceholderTestModalOpen, ] = useState(false); - const [isGalleryOpen, setIsGalleryOpen] = useState(false); return (
@@ -103,7 +110,7 @@ export function DesignBar() { }} data-testid="import-test-button" > - diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx index 1785dbeeb..32a341b0c 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx @@ -447,7 +447,11 @@ export function TestTable() { const [openLinkedTestModal, setOpenLinkedTestModal] = useState(false); const testRef = useRef(-1); - const handleChangeLinkedTest = (newPath: string, testType: ImportType) => { + const handleChangeLinkedTest = ( + newPath: string, + testType: ImportType, + args: string[] | undefined, + ) => { setElems((data) => { const new_data = [...data]; const test = new_data[testRef.current] as Test; @@ -455,6 +459,7 @@ export function TestTable() { ...test, path: newPath, testType: testType, + args: args, }; return new_data; }); diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx index a39362c0b..55862aef2 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx @@ -15,7 +15,7 @@ import { SelectTrigger, SelectValue, } from "@/renderer/components/ui/select"; -import { useDiscoverPytestElements } from "@/renderer/hooks/useTestImport"; +import { useDiscoverElements } from "@/renderer/hooks/useTestImport"; import { TestSequenceElement } from "@/renderer/types/test-sequencer"; import { toast } from "sonner"; @@ -26,29 +26,33 @@ export const ChangeLinkedTestModal = ({ }: { isModalOpen: boolean; setModalOpen: (value: boolean) => void; - handleSubmit: (path: string, testType: ImportType) => void; + handleSubmit: ( + path: string, + testType: ImportType, + args: string[] | undefined, + ) => void; }) => { const [availableTests, setAvailableTests] = useState( [], ); - const [selectedPath, setSelectedPath] = useState(""); - + const [selectedTestName, setSelectedPath] = useState(""); const { setIsDepManagerModalOpen } = useAppStore( useShallow((state) => ({ setIsDepManagerModalOpen: state.setIsDepManagerModalOpen, })), ); - const discoverPytestElement = useDiscoverPytestElements(); + const discoverElement = useDiscoverElements(); - const handleDiscoverPytestElements = async (filePath: string) => { - const result = await discoverPytestElement(filePath); + const handleDiscoverElements = async (filePath: string) => { + const result = await discoverElement(filePath); if (result.isOk()) { setAvailableTests(result.value); if (result.value.length > 0) { setSelectedPath(result.value[0].path); } } else { + toast.error(`Failed to discover tests: ${result.error}`); console.error(result.error); } }; @@ -57,35 +61,46 @@ export const ChangeLinkedTestModal = ({ const res = await window.api.openTestPicker(); if (!res) return; if (res.filePath) { - await handleDiscoverPytestElements(res.filePath); + await handleDiscoverElements(res.filePath); } }; - const handleSubmitByType = (testType: ImportType) => { - if (testType === "pytest") { - if (selectedPath === "") { - toast.error("Please select a test to link to"); - } - handleSubmit(selectedPath, testType); - } else { - window.api.openTestPicker().then((result) => { - if (!result) { - return; - } - const { filePath } = result; - handleSubmit(filePath, testType); - }); + const handleSubmitIndividualTest = () => { + if (selectedTestName === "") { + toast.error("Please select a test to link to"); + } + const test = availableTests.find( + (test) => test.type === "test" && test.testName === selectedTestName, + ); + if (test?.type !== "test" || test.testType === "placeholder") { + return; } + handleSubmit(test.path, test.testType, test.args); setModalOpen(false); }; + const handleSubmitPythonScript = () => { + window.api.openTestPicker().then((result) => { + if (!result) { + return; + } + const { filePath } = result; + if (!filePath.endsWith(".py")) { + toast.error("Please select a Python file"); + return; + } + handleSubmit(filePath, "python", undefined); + setModalOpen(false); + }); + }; + return (

Select a test to link to

-

Pytest

+

Pytest & Robot Framework

-
- -
+

Python Script

-
diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx index 4df0c7234..98a2fb9bc 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx @@ -15,7 +15,8 @@ export type ImportTestSettings = { importType: ImportType; }; -export type ImportType = "pytest" | "python"; +export type discoverableTestTypes = "pytest" | "robotframework"; +export type ImportType = discoverableTestTypes | "python"; export const ImportTestModal = () => { const { isImportTestModalOpen, setIsImportTestModalOpen } = @@ -57,6 +58,13 @@ export const ImportTestModal = () => { > Pytest & Unittest + diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx index 071f387ea..86af7720f 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx @@ -38,6 +38,12 @@ export const SequencerGalleryModal = ({ "Learn how to inject the minimum and maximum expected values into a test and export the result.", dirPath: "examples/test-sequencer-expected-exported-example/", }, + { + title: "Robot Framework & Flojoy", + description: + "Learn how to inject the minimum and maximum expected values into a robot test and export the result.", + dirPath: "examples/test-sequencer-robot-framework-example/", + }, ]; return ( diff --git a/src/renderer/types/test-sequencer.ts b/src/renderer/types/test-sequencer.ts index 9d4b8a2a5..1c9fdde0d 100644 --- a/src/renderer/types/test-sequencer.ts +++ b/src/renderer/types/test-sequencer.ts @@ -7,9 +7,8 @@ export type LockedContextType = { export const TestType = z.enum([ "pytest", "python", - "flojoy", - "matlab", "placeholder", + "robotframework", ]); export type TestType = z.infer; @@ -78,6 +77,7 @@ export const Test = z.object({ maxValue: z.number().optional(), measuredValue: z.number().optional(), unit: z.string().optional(), + args: z.string().array().optional(), }); export type Test = z.infer;