diff --git a/src/component/index.ts b/src/component/index.ts index e781c55..71b81bf 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -1,3 +1,5 @@ +export { Operation } from "./operation.js"; +export type { OperationProps } from "./operation.js"; export { Parallel } from "./parallel.js"; export type { ParallelProps } from "./parallel.js"; export { Sequential } from "./sequential.js"; diff --git a/src/component/operation.ts b/src/component/operation.ts index 8cec2e9..8bda659 100644 --- a/src/component/operation.ts +++ b/src/component/operation.ts @@ -1 +1,32 @@ -export {}; \ No newline at end of file +import type { UComponent, UElement, UNode, UniversalProps } from "@alkdev/ujsx"; +import type { PropValue } from "@alkdev/ujsx"; + +export interface OperationProps extends UniversalProps { + name: string; + input?: PropValue; + retries?: number; + timeout?: number; +} + +export const Operation: UComponent = ( + props: OperationProps & { children?: UNode[] }, +): UElement => { + const { name, input, retries, timeout, children, ...rest } = props; + const elementProps: UniversalProps = { ...rest, name }; + if (input !== undefined) { + elementProps.input = input; + } + if (retries !== undefined) { + elementProps.retries = retries; + } + if (timeout !== undefined) { + elementProps.timeout = timeout; + } + return { + type: "operation", + props: elementProps, + children: [], + }; +}; + +Operation.displayName = "Operation"; \ No newline at end of file diff --git a/test/component/operation.test.ts b/test/component/operation.test.ts new file mode 100644 index 0000000..8c25e9f --- /dev/null +++ b/test/component/operation.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { Operation } from "../../src/component/operation.js"; +import type { OperationProps } from "../../src/component/operation.js"; +import type { UElement, UNode } from "@alkdev/ujsx"; + +describe("Operation", () => { + it("produces UElement with type operation", () => { + const el = Operation({ name: "classify" } as OperationProps & { children?: UNode[] }); + expect(el.type).toBe("operation"); + }); + + it("requires name prop", () => { + const el = Operation({ name: "architect" } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.name).toBe("architect"); + }); + + it("has no children (leaf node)", () => { + const el = Operation({ name: "classify" } as OperationProps & { children?: UNode[] }); + expect(el.children).toEqual([]); + }); + + it("ignores children even if provided (leaf node)", () => { + const el = Operation({ + name: "classify", + children: ["child1"], + } as OperationProps & { children?: UNode[] }); + expect(el.children).toEqual([]); + }); + + it("preserves optional input prop", () => { + const el = Operation({ + name: "process", + input: { key: "value" }, + } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.input).toEqual({ key: "value" }); + }); + + it("preserves optional retries prop", () => { + const el = Operation({ + name: "fetch", + retries: 3, + } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.retries).toBe(3); + }); + + it("preserves optional timeout prop", () => { + const el = Operation({ + name: "fetch", + timeout: 5000, + } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.timeout).toBe(5000); + }); + + it("omits optional props when not provided", () => { + const el = Operation({ name: "classify" } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.input).toBeUndefined(); + expect((el as UElement).props.retries).toBeUndefined(); + expect((el as UElement).props.timeout).toBeUndefined(); + }); + + it("preserves all props together", () => { + const el = Operation({ + name: "fetch", + input: "data", + retries: 2, + timeout: 10000, + } as OperationProps & { children?: UNode[] }); + expect((el as UElement).props.name).toBe("fetch"); + expect((el as UElement).props.input).toBe("data"); + expect((el as UElement).props.retries).toBe(2); + expect((el as UElement).props.timeout).toBe(10000); + }); + + it("is a valid UElement (has type, props, children)", () => { + const el = Operation({ name: "test" } as OperationProps & { children?: UNode[] }); + expect(el).toHaveProperty("type"); + expect(el).toHaveProperty("props"); + expect(el).toHaveProperty("children"); + expect(typeof el.type).toBe("string"); + expect(typeof el.props).toBe("object"); + expect(Array.isArray(el.children)).toBe(true); + }); + + it("displayName is Operation", () => { + expect(Operation.displayName).toBe("Operation"); + }); +}); \ No newline at end of file