diff --git a/fakemui/icons/CenterFocusStrong.tsx b/fakemui/icons/CenterFocusStrong.tsx
new file mode 100644
index 000000000..bad3738ec
--- /dev/null
+++ b/fakemui/icons/CenterFocusStrong.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const CenterFocusStrong = (props: IconProps) => (
+
+
+
+
+
+
+
+)
diff --git a/fakemui/icons/CompareArrows.tsx b/fakemui/icons/CompareArrows.tsx
new file mode 100644
index 000000000..4c8401c1a
--- /dev/null
+++ b/fakemui/icons/CompareArrows.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const CompareArrows = (props: IconProps) => (
+
+
+
+
+
+
+)
diff --git a/fakemui/icons/Launch.tsx b/fakemui/icons/Launch.tsx
new file mode 100644
index 000000000..17620d34a
--- /dev/null
+++ b/fakemui/icons/Launch.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const Launch = (props: IconProps) => (
+
+
+
+
+
+
+)
diff --git a/fakemui/icons/NavigateBefore.tsx b/fakemui/icons/NavigateBefore.tsx
new file mode 100644
index 000000000..e2fa24a9f
--- /dev/null
+++ b/fakemui/icons/NavigateBefore.tsx
@@ -0,0 +1,8 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const NavigateBefore = (props: IconProps) => (
+
+
+
+)
diff --git a/fakemui/icons/SwapHoriz.tsx b/fakemui/icons/SwapHoriz.tsx
new file mode 100644
index 000000000..b8fbf3ab0
--- /dev/null
+++ b/fakemui/icons/SwapHoriz.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const SwapHoriz = (props: IconProps) => (
+
+
+
+
+)
diff --git a/fakemui/icons/SwapVert.tsx b/fakemui/icons/SwapVert.tsx
new file mode 100644
index 000000000..54fb8c761
--- /dev/null
+++ b/fakemui/icons/SwapVert.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const SwapVert = (props: IconProps) => (
+
+
+
+
+)
diff --git a/fakemui/icons/UnfoldLess.tsx b/fakemui/icons/UnfoldLess.tsx
new file mode 100644
index 000000000..8bc68b2f9
--- /dev/null
+++ b/fakemui/icons/UnfoldLess.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const UnfoldLess = (props: IconProps) => (
+
+
+
+
+)
diff --git a/fakemui/icons/UnfoldMore.tsx b/fakemui/icons/UnfoldMore.tsx
new file mode 100644
index 000000000..f92c3fd95
--- /dev/null
+++ b/fakemui/icons/UnfoldMore.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const UnfoldMore = (props: IconProps) => (
+
+
+
+
+)
diff --git a/fakemui/icons/ZoomInMap.tsx b/fakemui/icons/ZoomInMap.tsx
new file mode 100644
index 000000000..d49e4fcb3
--- /dev/null
+++ b/fakemui/icons/ZoomInMap.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const ZoomInMap = (props: IconProps) => (
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/fakemui/icons/ZoomOutMap.tsx b/fakemui/icons/ZoomOutMap.tsx
new file mode 100644
index 000000000..316b600ba
--- /dev/null
+++ b/fakemui/icons/ZoomOutMap.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const ZoomOutMap = (props: IconProps) => (
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/packages/workflow_editor/seed/metadata.json b/packages/workflow_editor/seed/metadata.json
index bbaf5f207..10063697b 100644
--- a/packages/workflow_editor/seed/metadata.json
+++ b/packages/workflow_editor/seed/metadata.json
@@ -22,10 +22,14 @@
},
"tests": {
"scripts": [
- "tests/status.test.lua"
+ "tests/status.test.lua",
+ "tests/editor.test.lua",
+ "tests/run.test.lua"
],
"cases": [
- "tests/status.cases.json"
+ "tests/status.cases.json",
+ "tests/editor.cases.json",
+ "tests/run.cases.json"
]
},
"minLevel": 5
diff --git a/packages/workflow_editor/seed/scripts/tests/editor.cases.json b/packages/workflow_editor/seed/scripts/tests/editor.cases.json
new file mode 100644
index 000000000..567c91267
--- /dev/null
+++ b/packages/workflow_editor/seed/scripts/tests/editor.cases.json
@@ -0,0 +1,121 @@
+{
+ "render": [
+ {
+ "desc": "Renders new workflow with default name",
+ "input": null,
+ "expected_name": "New Workflow",
+ "expected_steps_count": 0
+ },
+ {
+ "desc": "Renders existing workflow with name",
+ "input": {
+ "id": "wf-123",
+ "name": "Deploy Pipeline",
+ "steps": []
+ },
+ "expected_id": "wf-123",
+ "expected_name": "Deploy Pipeline",
+ "expected_steps_count": 0
+ },
+ {
+ "desc": "Renders workflow with steps",
+ "input": {
+ "id": "wf-456",
+ "name": "Build & Test",
+ "steps": [
+ { "type": "build" },
+ { "type": "test" }
+ ]
+ },
+ "expected_id": "wf-456",
+ "expected_name": "Build & Test",
+ "expected_steps_count": 2
+ },
+ {
+ "desc": "Renders workflow without ID",
+ "input": {
+ "name": "CI Pipeline"
+ },
+ "expected_name": "CI Pipeline",
+ "expected_steps_count": 0
+ }
+ ],
+ "add_step": [
+ {
+ "desc": "Adds build step without config",
+ "step_type": "build",
+ "config": null
+ },
+ {
+ "desc": "Adds test step with config",
+ "step_type": "test",
+ "config": {
+ "framework": "jest",
+ "coverage": true
+ }
+ },
+ {
+ "desc": "Adds deploy step with target",
+ "step_type": "deploy",
+ "config": {
+ "target": "production",
+ "region": "us-east-1"
+ }
+ },
+ {
+ "desc": "Adds notification step",
+ "step_type": "notify",
+ "config": {
+ "channel": "slack",
+ "webhook": "https://hooks.slack.com/..."
+ }
+ },
+ {
+ "desc": "Adds approval step",
+ "step_type": "approval",
+ "config": {
+ "approvers": ["admin", "devops"]
+ }
+ },
+ {
+ "desc": "Adds script step with inline code",
+ "step_type": "script",
+ "config": {
+ "language": "bash",
+ "code": "echo 'Hello World'"
+ }
+ }
+ ],
+ "connect_steps": [
+ {
+ "desc": "Connects two steps without condition",
+ "from": "step-1",
+ "to": "step-2",
+ "condition": null
+ },
+ {
+ "desc": "Connects steps with success condition",
+ "from": "build",
+ "to": "test",
+ "condition": "on_success"
+ },
+ {
+ "desc": "Connects steps with failure condition",
+ "from": "test",
+ "to": "notify-failure",
+ "condition": "on_failure"
+ },
+ {
+ "desc": "Connects steps with always condition",
+ "from": "deploy",
+ "to": "cleanup",
+ "condition": "always"
+ },
+ {
+ "desc": "Connects approval to deploy",
+ "from": "approval-step",
+ "to": "deploy-prod",
+ "condition": "approved"
+ }
+ ]
+}
diff --git a/packages/workflow_editor/seed/scripts/tests/editor.test.lua b/packages/workflow_editor/seed/scripts/tests/editor.test.lua
new file mode 100644
index 000000000..08cbf4f08
--- /dev/null
+++ b/packages/workflow_editor/seed/scripts/tests/editor.test.lua
@@ -0,0 +1,48 @@
+-- Workflow editor tests
+-- Uses lua_test framework with parameterized test cases
+
+describe("Workflow Editor", function()
+ local cases = load_cases("editor.cases.json")
+ local editor = require("editor")
+
+ describe("render", function()
+ it_each(cases.render, "$desc", function(tc)
+ local result = editor.render(tc.input)
+ expect(result.type).toBe("workflow_editor")
+ expect(result.props.name).toBe(tc.expected_name)
+ if tc.expected_id then
+ expect(result.props.id).toBe(tc.expected_id)
+ end
+ if tc.expected_steps_count ~= nil then
+ expect(#result.props.steps).toBe(tc.expected_steps_count)
+ end
+ end)
+ end)
+
+ describe("add_step", function()
+ it_each(cases.add_step, "$desc", function(tc)
+ local result = editor.add_step(tc.step_type, tc.config)
+ expect(result.type).toBe("workflow_step")
+ expect(result.step_type).toBe(tc.step_type)
+ expect(result.position.x).toBe(0)
+ expect(result.position.y).toBe(0)
+ if tc.config then
+ expect(result.config).toBeDefined()
+ end
+ end)
+ end)
+
+ describe("connect_steps", function()
+ it_each(cases.connect_steps, "$desc", function(tc)
+ local result = editor.connect_steps(tc.from, tc.to, tc.condition)
+ expect(result.type).toBe("connection")
+ expect(result.from).toBe(tc.from)
+ expect(result.to).toBe(tc.to)
+ if tc.condition then
+ expect(result.condition).toBe(tc.condition)
+ else
+ expect(result.condition).toBeNil()
+ end
+ end)
+ end)
+end)
diff --git a/packages/workflow_editor/seed/scripts/tests/run.cases.json b/packages/workflow_editor/seed/scripts/tests/run.cases.json
new file mode 100644
index 000000000..b8c79bae6
--- /dev/null
+++ b/packages/workflow_editor/seed/scripts/tests/run.cases.json
@@ -0,0 +1,167 @@
+{
+ "render": [
+ {
+ "desc": "Renders completed run",
+ "input": {
+ "workflow_name": "Deploy Production",
+ "run_number": 42,
+ "started_at": "2025-01-15 14:30:00",
+ "duration": "5m 32s",
+ "status": "success"
+ },
+ "expected_title": "Deploy Production",
+ "expected_subtitle": "Run #42",
+ "expected_duration": "5m 32s",
+ "expected_status": "success"
+ },
+ {
+ "desc": "Renders failed run",
+ "input": {
+ "workflow_name": "CI Pipeline",
+ "run_number": 128,
+ "started_at": "2025-01-15 15:45:00",
+ "duration": "2m 15s",
+ "status": "failure"
+ },
+ "expected_title": "CI Pipeline",
+ "expected_subtitle": "Run #128",
+ "expected_duration": "2m 15s",
+ "expected_status": "failure"
+ },
+ {
+ "desc": "Renders running workflow",
+ "input": {
+ "workflow_name": "Build & Test",
+ "run_number": 99,
+ "started_at": "2025-01-15 16:00:00",
+ "status": "running"
+ },
+ "expected_title": "Build & Test",
+ "expected_subtitle": "Run #99",
+ "expected_duration": "running",
+ "expected_status": "running"
+ },
+ {
+ "desc": "Renders cancelled run",
+ "input": {
+ "workflow_name": "Nightly Build",
+ "run_number": 7,
+ "started_at": "2025-01-15 02:00:00",
+ "duration": "1m 05s",
+ "status": "cancelled"
+ },
+ "expected_title": "Nightly Build",
+ "expected_subtitle": "Run #7",
+ "expected_duration": "1m 05s",
+ "expected_status": "cancelled"
+ },
+ {
+ "desc": "Renders queued run",
+ "input": {
+ "workflow_name": "Integration Tests",
+ "run_number": 200,
+ "started_at": "2025-01-15 17:00:00",
+ "status": "queued"
+ },
+ "expected_title": "Integration Tests",
+ "expected_subtitle": "Run #200",
+ "expected_duration": "running",
+ "expected_status": "queued"
+ },
+ {
+ "desc": "Renders run with very long duration",
+ "input": {
+ "workflow_name": "Performance Tests",
+ "run_number": 1,
+ "started_at": "2025-01-15 10:00:00",
+ "duration": "1h 45m 30s",
+ "status": "success"
+ },
+ "expected_title": "Performance Tests",
+ "expected_subtitle": "Run #1",
+ "expected_duration": "1h 45m 30s",
+ "expected_status": "success"
+ }
+ ],
+ "render_list": [
+ {
+ "desc": "Renders single run",
+ "input": [
+ {
+ "workflow_name": "Deploy",
+ "run_number": 1,
+ "started_at": "2025-01-15 14:00:00",
+ "duration": "3m",
+ "status": "success"
+ }
+ ],
+ "expected_count": 1
+ },
+ {
+ "desc": "Renders multiple runs",
+ "input": [
+ {
+ "workflow_name": "Build",
+ "run_number": 1,
+ "started_at": "2025-01-15 10:00:00",
+ "duration": "2m",
+ "status": "success"
+ },
+ {
+ "workflow_name": "Test",
+ "run_number": 2,
+ "started_at": "2025-01-15 11:00:00",
+ "duration": "5m",
+ "status": "success"
+ },
+ {
+ "workflow_name": "Deploy",
+ "run_number": 3,
+ "started_at": "2025-01-15 12:00:00",
+ "status": "running"
+ }
+ ],
+ "expected_count": 3
+ },
+ {
+ "desc": "Renders many runs in grid",
+ "input": [
+ {
+ "workflow_name": "Run 1",
+ "run_number": 1,
+ "started_at": "2025-01-15 08:00:00",
+ "duration": "1m",
+ "status": "success"
+ },
+ {
+ "workflow_name": "Run 2",
+ "run_number": 2,
+ "started_at": "2025-01-15 09:00:00",
+ "duration": "2m",
+ "status": "success"
+ },
+ {
+ "workflow_name": "Run 3",
+ "run_number": 3,
+ "started_at": "2025-01-15 10:00:00",
+ "duration": "1m",
+ "status": "failure"
+ },
+ {
+ "workflow_name": "Run 4",
+ "run_number": 4,
+ "started_at": "2025-01-15 11:00:00",
+ "status": "running"
+ },
+ {
+ "workflow_name": "Run 5",
+ "run_number": 5,
+ "started_at": "2025-01-15 12:00:00",
+ "duration": "3m",
+ "status": "success"
+ }
+ ],
+ "expected_count": 5
+ }
+ ]
+}
diff --git a/packages/workflow_editor/seed/scripts/tests/run.test.lua b/packages/workflow_editor/seed/scripts/tests/run.test.lua
new file mode 100644
index 000000000..3b647c21c
--- /dev/null
+++ b/packages/workflow_editor/seed/scripts/tests/run.test.lua
@@ -0,0 +1,53 @@
+-- Workflow run card tests
+-- Uses lua_test framework with parameterized test cases
+
+describe("Workflow Run Cards", function()
+ local cases = load_cases("run.cases.json")
+ local run = require("run")
+
+ describe("render", function()
+ it_each(cases.render, "$desc", function(tc)
+ local result = run.render(tc.input)
+ expect(result.type).toBe("card")
+ expect(result.props.title).toBe(tc.expected_title)
+ expect(result.props.subtitle).toBe(tc.expected_subtitle)
+ expect(#result.children).toBe(3)
+
+ -- Check started timestamp
+ expect(result.children[1].type).toBe("text")
+ expect(result.children[1].content).toContain("Started:")
+
+ -- Check duration
+ expect(result.children[2].type).toBe("text")
+ expect(result.children[2].content).toContain("Duration:")
+ if tc.expected_duration then
+ expect(result.children[2].content).toContain(tc.expected_duration)
+ end
+
+ -- Check status badge
+ expect(result.children[3].type).toBe("status_badge")
+ expect(result.children[3].status).toBe(tc.expected_status)
+ end)
+ end)
+
+ describe("render_list", function()
+ it_each(cases.render_list, "$desc", function(tc)
+ local result = run.render_list(tc.input)
+ expect(result.type).toBe("grid")
+ expect(result.columns).toBe(2)
+ expect(#result.children).toBe(tc.expected_count)
+
+ -- Verify all children are cards
+ for i, child in ipairs(result.children) do
+ expect(child.type).toBe("card")
+ end
+ end)
+
+ it("renders empty list", function()
+ local result = run.render_list({})
+ expect(result.type).toBe("grid")
+ expect(result.columns).toBe(2)
+ expect(#result.children).toBe(0)
+ end)
+ end)
+end)