diff --git a/fakemui/icons/SkipNext.tsx b/fakemui/icons/SkipNext.tsx
new file mode 100644
index 000000000..2d9650234
--- /dev/null
+++ b/fakemui/icons/SkipNext.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const SkipNext = (props: IconProps) => (
+
+
+
+
+)
diff --git a/fakemui/icons/SkipPrevious.tsx b/fakemui/icons/SkipPrevious.tsx
new file mode 100644
index 000000000..1118f9a7c
--- /dev/null
+++ b/fakemui/icons/SkipPrevious.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { Icon, IconProps } from './Icon'
+
+export const SkipPrevious = (props: IconProps) => (
+
+
+
+
+)
diff --git a/packages/form_builder/seed/metadata.json b/packages/form_builder/seed/metadata.json
index d40848457..83f5dc8c2 100644
--- a/packages/form_builder/seed/metadata.json
+++ b/packages/form_builder/seed/metadata.json
@@ -26,11 +26,13 @@
"tests": {
"scripts": [
"tests/metadata.test.lua",
- "tests/components.test.lua"
+ "tests/components.test.lua",
+ "tests/validate.test.lua"
],
"cases": [
"tests/metadata.cases.json",
- "tests/components.cases.json"
+ "tests/components.cases.json",
+ "tests/validate.cases.json"
]
},
"minLevel": 1
diff --git a/packages/form_builder/seed/scripts/tests/validate.cases.json b/packages/form_builder/seed/scripts/tests/validate.cases.json
new file mode 100644
index 000000000..053df0b5d
--- /dev/null
+++ b/packages/form_builder/seed/scripts/tests/validate.cases.json
@@ -0,0 +1,61 @@
+{
+ "required": {
+ "customMessage": [
+ { "message": "Please fill this in" },
+ { "message": "Required field" },
+ { "message": "Cannot be empty" }
+ ]
+ },
+ "email": {
+ "validPatterns": [
+ { "email": "test@example.com" },
+ { "email": "user.name@domain.org" },
+ { "email": "admin@sub.domain.co.uk" },
+ { "email": "a@b.c" }
+ ],
+ "invalidPatterns": [
+ { "email": "invalid" },
+ { "email": "@nodomain.com" },
+ { "email": "noat.com" },
+ { "email": "spaces in@email.com" }
+ ],
+ "customMessage": [
+ { "message": "Enter valid email address" },
+ { "message": "Email format invalid" }
+ ]
+ },
+ "minLength": {
+ "rules": [
+ { "length": 1 },
+ { "length": 8 },
+ { "length": 16 },
+ { "length": 100 }
+ ],
+ "customMessage": [
+ { "length": 8, "message": "Password must be 8+ characters" },
+ { "length": 3, "message": "Username too short" }
+ ]
+ },
+ "maxLength": {
+ "rules": [
+ { "length": 50 },
+ { "length": 100 },
+ { "length": 255 },
+ { "length": 1000 }
+ ]
+ },
+ "pattern": {
+ "rules": [
+ { "name": "phone", "regex": "^%d%d%d%-%d%d%d%-%d%d%d%d$", "message": "Invalid phone" },
+ { "name": "alphanumeric", "regex": "^%w+$", "message": "Letters and numbers only" },
+ { "name": "zip", "regex": "^%d%d%d%d%d$", "message": "Invalid ZIP code" }
+ ],
+ "matchTests": [
+ { "regex": "^%d+$", "value": "12345", "expected": true },
+ { "regex": "^%d+$", "value": "abc", "expected": false },
+ { "regex": "^%w+$", "value": "test123", "expected": true },
+ { "regex": "^%w+$", "value": "test 123", "expected": false },
+ { "regex": "^%a+$", "value": "onlyletters", "expected": true }
+ ]
+ }
+}
diff --git a/packages/form_builder/seed/scripts/tests/validate.test.lua b/packages/form_builder/seed/scripts/tests/validate.test.lua
new file mode 100644
index 000000000..d7418041d
--- /dev/null
+++ b/packages/form_builder/seed/scripts/tests/validate.test.lua
@@ -0,0 +1,105 @@
+-- Validation Tests
+-- Parameterized tests for form validation rules
+
+local describe = require("lua_test.describe")
+local it = require("lua_test.it")
+local it_each = require("lua_test.it_each")
+local expect = require("lua_test.expect")
+
+local cases = require("tests.validate.cases")
+local required = require("validate.required")
+local email = require("validate.email")
+local min_length = require("validate.min_length")
+local max_length = require("validate.max_length")
+local pattern = require("validate.pattern")
+
+describe("form validation", function()
+
+ describe("required", function()
+ it("should create required rule with default message", function()
+ local rule = required()
+ expect(rule.type).toBe("required")
+ expect(rule.message).toBe("This field is required")
+ end)
+
+ it_each(cases.required.customMessage, "should use custom message: '$message'", function(case)
+ local rule = required(case.message)
+ expect(rule.message).toBe(case.message)
+ end)
+ end)
+
+ describe("email", function()
+ it("should create email rule with pattern", function()
+ local rule = email()
+ expect(rule.type).toBe("email")
+ expect(rule.pattern).toBeTruthy()
+ end)
+
+ it_each(cases.email.validPatterns, "pattern should match '$email'", function(case)
+ local rule = email()
+ local matches = string.match(case.email, rule.pattern)
+ expect(matches).toBeTruthy()
+ end)
+
+ it_each(cases.email.invalidPatterns, "pattern should reject '$email'", function(case)
+ local rule = email()
+ local matches = string.match(case.email, rule.pattern)
+ expect(matches).toBe(nil)
+ end)
+
+ it_each(cases.email.customMessage, "should use custom message: '$message'", function(case)
+ local rule = email(case.message)
+ expect(rule.message).toBe(case.message)
+ end)
+ end)
+
+ describe("min_length", function()
+ it_each(cases.minLength.rules, "should create rule for min $length chars", function(case)
+ local rule = min_length(case.length)
+ expect(rule.type).toBe("minLength")
+ expect(rule.value).toBe(case.length)
+ end)
+
+ it("should generate default message with length", function()
+ local rule = min_length(8)
+ expect(rule.message).toContain("8")
+ end)
+
+ it_each(cases.minLength.customMessage, "should use custom message", function(case)
+ local rule = min_length(case.length, case.message)
+ expect(rule.message).toBe(case.message)
+ end)
+ end)
+
+ describe("max_length", function()
+ it_each(cases.maxLength.rules, "should create rule for max $length chars", function(case)
+ local rule = max_length(case.length)
+ expect(rule.type).toBe("maxLength")
+ expect(rule.value).toBe(case.length)
+ end)
+
+ it("should generate default message with length", function()
+ local rule = max_length(100)
+ expect(rule.message).toContain("100")
+ end)
+ end)
+
+ describe("pattern", function()
+ it_each(cases.pattern.rules, "should create rule with pattern: $name", function(case)
+ local rule = pattern(case.regex, case.message)
+ expect(rule.type).toBe("pattern")
+ expect(rule.pattern).toBe(case.regex)
+ end)
+
+ it_each(cases.pattern.matchTests, "pattern '$regex' should match '$value'", function(case)
+ local matches = string.match(case.value, case.regex)
+ expect(matches ~= nil).toBe(case.expected)
+ end)
+ end)
+
+end)
+
+return {
+ name = "validate.test",
+ description = "Tests for form validation rules"
+}