From a7be452ddb5d98d7e07ac0c492cc611d421bc231 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Tue, 30 Dec 2025 01:16:07 +0000 Subject: [PATCH] feat(lua_test): add README, components, index, example tests, and icon for Lua testing framework --- packages/lua_test/README.md | 309 ++++++++++++++++++ packages/lua_test/seed/components.json | 23 ++ packages/lua_test/seed/index.ts | 23 ++ .../examples/login-validation.test.lua | 208 ++++++++++++ packages/lua_test/static_content/icon.svg | 12 + 5 files changed, 575 insertions(+) create mode 100644 packages/lua_test/README.md create mode 100644 packages/lua_test/seed/components.json create mode 100644 packages/lua_test/seed/index.ts create mode 100644 packages/lua_test/seed/scripts/examples/login-validation.test.lua create mode 100644 packages/lua_test/static_content/icon.svg diff --git a/packages/lua_test/README.md b/packages/lua_test/README.md new file mode 100644 index 000000000..58ed7e3ee --- /dev/null +++ b/packages/lua_test/README.md @@ -0,0 +1,309 @@ +# Lua Test Package + +A unit testing framework for Lua scripts in MetaBuilder packages. + +## Features + +- **BDD-style syntax**: `describe`/`it` blocks for organizing tests +- **Rich assertions**: `expect()` with chainable matchers +- **Mocks and spies**: Track function calls and replace implementations +- **Lifecycle hooks**: `beforeAll`, `afterAll`, `beforeEach`, `afterEach` +- **Test filtering**: Run specific tests with `.only` or name filters +- **Detailed reporting**: Text and JSON output formats + +## Quick Start + +```lua +local framework = require("lua_test/framework") +local assertions = require("lua_test/assertions") + +local describe = framework.describe +local it = framework.it +local expect = assertions.expect + +describe("My Module", function() + + it("should do something", function() + expect(1 + 1).toBe(2) + end) + + it("should handle strings", function() + expect("hello").toContain("ell") + expect("hello").toHaveLength(5) + end) + +end) +``` + +## Assertions + +### Basic Matchers + +```lua +expect(value).toBe(expected) -- Strict equality (==) +expect(value).toEqual(expected) -- Deep equality for tables +expect(value).toBeNil() -- Check for nil +expect(value).toBeTruthy() -- Truthy check +expect(value).toBeFalsy() -- Falsy check +expect(value).toBeType("string") -- Type check +``` + +### Comparison Matchers + +```lua +expect(num).toBeGreaterThan(5) +expect(num).toBeLessThan(10) +expect(3.14159).toBeCloseTo(3.14, 2) -- Floating point comparison +``` + +### String Matchers + +```lua +expect(str).toContain("substring") +expect(str).toMatch("^pattern") -- Lua pattern matching +expect(str).toHaveLength(5) +``` + +### Collection Matchers + +```lua +expect(table).toContain(element) +expect(table).toHaveLength(3) +expect(table).toHaveProperty("key") +expect(table).toHaveProperty("key", value) +``` + +### Error Matchers + +```lua +expect(function() error("boom") end).toThrow() +expect(function() error("boom") end).toThrow("boom") +``` + +### Negation + +Use `.never` to negate any matcher: + +```lua +expect(5).never.toBe(10) +expect("hello").never.toContain("world") +``` + +## Mocks + +### Mock Functions + +```lua +local mocks = require("lua_test/mocks") + +local mockFn = mocks.fn(function(x) return x * 2 end) + +mockFn(5) +mockFn(10) + +mockFn.getCallCount() -- 2 +mockFn.wasCalled() -- true +mockFn.wasCalledWith(5) -- true +mockFn.getLastCall() -- {10} +mockFn.getCalls() -- {{5}, {10}} +``` + +### Mock Return Values + +```lua +local mockFn = mocks.fn() +mockFn.mockReturnValue(42) + +mockFn() -- 42 +mockFn() -- 42 + +-- Or return different values +mockFn.mockReturnValueOnce(1) +mockFn.mockReturnValueOnce(2) +mockFn() -- 1 +mockFn() -- 2 +mockFn() -- nil (or previous mockReturnValue) +``` + +### Spies + +```lua +local myModule = { calculate = function(x) return x * 2 end } + +local spy = mocks.spyOn(myModule, "calculate") + +myModule.calculate(5) + +spy.getCallCount() -- 1 +spy.wasCalledWith(5) -- true + +spy.mockRestore() -- Restore original function +``` + +## Lifecycle Hooks + +```lua +describe("My Suite", function() + + beforeAll(function() + -- Run once before all tests in this suite + end) + + afterAll(function() + -- Run once after all tests in this suite + end) + + beforeEach(function() + -- Run before each test + end) + + afterEach(function() + -- Run after each test + end) + + it("test 1", function() end) + it("test 2", function() end) + +end) +``` + +## Test Filtering + +### Skip Tests + +```lua +xit("this test is skipped", function() + -- Won't run +end) +``` + +### Focus Tests + +```lua +fit("only this test runs", function() + -- Other tests in the suite are skipped +end) +``` + +### Filter by Name + +```lua +framework.configure({ filter = "validation" }) +-- Only tests with "validation" in their name will run +``` + +## Running Tests + +```lua +local runner = require("lua_test/runner") +local framework = require("lua_test/framework") + +-- Define your tests with describe/it +-- ... + +-- Run all suites +local results = runner.runAll(framework.getSuites()) + +-- Print report +print(runner.formatReport(results)) + +-- Or get JSON for programmatic use +local json = runner.formatJSON(results) +``` + +## Helpers + +```lua +local helpers = require("lua_test/helpers") + +-- Generate test data +local users = helpers.generateTestData({ + id = "$index", + name = function(i) return "User " .. i end, + score = "$random" +}, 10) + +-- Table utilities +helpers.table.clone(t) +helpers.table.merge(t1, t2) +helpers.table.keys(t) +helpers.table.values(t) +helpers.table.size(t) + +-- String utilities +helpers.string.trim(s) +helpers.string.split(s, delimiter) +helpers.string.startsWith(s, prefix) +helpers.string.endsWith(s, suffix) + +-- Create test context +local ctx = helpers.createContext({ user = nil }) +ctx.set("user", { name = "Alice" }) +ctx.get("user") +ctx.reset() +``` + +## Example: Testing a Package + +```lua +-- tests/validate.test.lua +local framework = require("lua_test/framework") +local assertions = require("lua_test/assertions") +local runner = require("lua_test/runner") + +local describe = framework.describe +local it = framework.it +local beforeEach = framework.beforeEach +local expect = assertions.expect + +-- Import the module to test +local validate = require("ui_login/validate") + +describe("ui_login validation", function() + + describe("login", function() + it("should reject empty username", function() + local result = validate.login({ username = "", password = "password123" }) + expect(result.valid).toBe(false) + expect(result.errors[1].field).toBe("username") + end) + + it("should accept valid credentials", function() + local result = validate.login({ username = "user", password = "password123" }) + expect(result.valid).toBe(true) + end) + end) + +end) + +-- Run and report +local results = runner.runAll(framework.getSuites()) +print(runner.formatReport(results)) +``` + +## Output Example + +``` +═══════════════════════════════════════ + TEST RESULTS REPORT +═══════════════════════════════════════ + +📦 ui_login validation + 📦 login + ✅ should reject empty username (0.12ms) + ✅ should accept valid credentials (0.08ms) + 📦 register + ✅ should reject invalid email (0.15ms) + ✅ should accept valid registration (0.10ms) + +─────────────────────────────────────── +Summary: + Total: 4 tests + Passed: 4 ✅ + Failed: 0 ❌ + Skipped: 0 ⏭️ + Duration: 0.45ms + +🎉 All tests passed! +═══════════════════════════════════════ +``` diff --git a/packages/lua_test/seed/components.json b/packages/lua_test/seed/components.json new file mode 100644 index 000000000..c44853e75 --- /dev/null +++ b/packages/lua_test/seed/components.json @@ -0,0 +1,23 @@ +{ + "components": [ + { + "name": "TestRunner", + "description": "Interactive test runner component for executing Lua tests", + "props": { + "packageId": { "type": "string", "description": "Package ID to test" }, + "autoRun": { "type": "boolean", "default": false }, + "filter": { "type": "string", "description": "Filter tests by name" }, + "verbose": { "type": "boolean", "default": true } + } + }, + { + "name": "TestResults", + "description": "Displays test results in a formatted view", + "props": { + "results": { "type": "object", "required": true }, + "showDuration": { "type": "boolean", "default": true }, + "expandErrors": { "type": "boolean", "default": true } + } + } + ] +} diff --git a/packages/lua_test/seed/index.ts b/packages/lua_test/seed/index.ts new file mode 100644 index 000000000..69cf2d587 --- /dev/null +++ b/packages/lua_test/seed/index.ts @@ -0,0 +1,23 @@ +/** + * lua_test package exports + * Unit testing framework for Lua scripts in MetaBuilder packages + */ + +import componentsJson from './components.json' +import metadataJson from './metadata.json' + +// Export seed data for package loader +export const packageSeed = { + metadata: metadataJson, + components: componentsJson.components, + scripts: [ + { name: 'init', file: 'init.lua', category: 'lifecycle' }, + { name: 'framework', file: 'framework.lua', category: 'core' }, + { name: 'assertions', file: 'assertions.lua', category: 'core' }, + { name: 'mocks', file: 'mocks.lua', category: 'utilities' }, + { name: 'runner', file: 'runner.lua', category: 'core' }, + { name: 'helpers', file: 'helpers.lua', category: 'utilities' }, + ], +} + +export default packageSeed diff --git a/packages/lua_test/seed/scripts/examples/login-validation.test.lua b/packages/lua_test/seed/scripts/examples/login-validation.test.lua new file mode 100644 index 000000000..b5926b8e1 --- /dev/null +++ b/packages/lua_test/seed/scripts/examples/login-validation.test.lua @@ -0,0 +1,208 @@ +-- Example test file demonstrating the lua_test framework +-- This tests the ui_login validation module + +-- Import test framework modules (these would be loaded by the test runner) +-- local framework = require("framework") +-- local assertions = require("assertions") +-- local mocks = require("mocks") + +-- For demonstration, we inline the example + +local M = {} + +-- Example: Testing the ui_login validate.lua module +function M.defineTests(framework, assertions, mocks) + local describe = framework.describe + local it = framework.it + local beforeEach = framework.beforeEach + local expect = assertions.expect + + describe("ui_login validation", function() + + describe("login validation", function() + it("should reject empty username", function() + local validate = { + login = function(data) + local errors = {} + if not data.username or data.username == "" then + errors[#errors + 1] = { field = "username", message = "Required" } + end + if not data.password or #data.password < 6 then + errors[#errors + 1] = { field = "password", message = "Min 6 chars" } + end + return { valid = #errors == 0, errors = errors } + end + } + + local result = validate.login({ username = "", password = "password123" }) + expect(result.valid).toBe(false) + expect(#result.errors).toBe(1) + expect(result.errors[1].field).toBe("username") + end) + + it("should reject short password", function() + local validate = { + login = function(data) + local errors = {} + if not data.username or data.username == "" then + errors[#errors + 1] = { field = "username", message = "Required" } + end + if not data.password or #data.password < 6 then + errors[#errors + 1] = { field = "password", message = "Min 6 chars" } + end + return { valid = #errors == 0, errors = errors } + end + } + + local result = validate.login({ username = "user", password = "12345" }) + expect(result.valid).toBe(false) + expect(result.errors[1].field).toBe("password") + end) + + it("should accept valid credentials", function() + local validate = { + login = function(data) + local errors = {} + if not data.username or data.username == "" then + errors[#errors + 1] = { field = "username", message = "Required" } + end + if not data.password or #data.password < 6 then + errors[#errors + 1] = { field = "password", message = "Min 6 chars" } + end + return { valid = #errors == 0, errors = errors } + end + } + + local result = validate.login({ username = "user", password = "password123" }) + expect(result.valid).toBe(true) + expect(#result.errors).toBe(0) + end) + end) + + describe("register validation", function() + local validate + + beforeEach(function() + validate = { + register = function(data) + local errors = {} + if not data.username or #data.username < 3 then + errors[#errors + 1] = { field = "username", message = "Min 3 chars" } + end + if not data.email or not string.match(data.email, "^[^@]+@[^@]+%.[^@]+$") then + errors[#errors + 1] = { field = "email", message = "Invalid email" } + end + if not data.password or #data.password < 8 then + errors[#errors + 1] = { field = "password", message = "Min 8 chars" } + end + return { valid = #errors == 0, errors = errors } + end + } + end) + + it("should reject invalid email", function() + local result = validate.register({ + username = "user", + email = "invalid-email", + password = "password123" + }) + expect(result.valid).toBe(false) + expect(result.errors[1].field).toBe("email") + end) + + it("should accept valid registration", function() + local result = validate.register({ + username = "user", + email = "user@example.com", + password = "password123" + }) + expect(result.valid).toBe(true) + end) + end) + + end) + + -- Example with mocks + describe("mock examples", function() + it("should track function calls", function() + local mockFn = mocks.fn(function(x) return x * 2 end) + + mockFn(5) + mockFn(10) + + expect(mockFn.getCallCount()).toBe(2) + expect(mockFn.wasCalledWith(5)).toBe(true) + expect(mockFn.getLastCall()[1]).toBe(10) + end) + + it("should support mock return values", function() + local mockFn = mocks.fn() + mockFn.mockReturnValue(42) + + expect(mockFn()).toBe(42) + expect(mockFn()).toBe(42) + end) + end) + + -- Example with assertions + describe("assertion examples", function() + it("should support various matchers", function() + -- Type checks + expect("hello").toBeType("string") + expect(123).toBeType("number") + expect({}).toBeType("table") + + -- Truthiness + expect(true).toBeTruthy() + expect(false).toBeFalsy() + expect(nil).toBeFalsy() + + -- Comparisons + expect(10).toBeGreaterThan(5) + expect(5).toBeLessThan(10) + expect(3.14159).toBeCloseTo(3.14, 2) + + -- String matching + expect("hello world").toContain("world") + expect("test@example.com").toMatch("^[^@]+@[^@]+$") + + -- Collections + expect({1, 2, 3}).toHaveLength(3) + expect({1, 2, 3}).toContain(2) + + -- Negation + expect(5).never.toBe(10) + expect("hello").never.toContain("goodbye") + end) + + it("should support deep equality", function() + local obj1 = { name = "test", nested = { value = 42 } } + local obj2 = { name = "test", nested = { value = 42 } } + local obj3 = { name = "test", nested = { value = 99 } } + + expect(obj1).toEqual(obj2) + expect(obj1).never.toEqual(obj3) + end) + + it("should support property checks", function() + local user = { name = "Alice", age = 30, role = "admin" } + + expect(user).toHaveProperty("name") + expect(user).toHaveProperty("age", 30) + expect(user).never.toHaveProperty("email") + end) + + it("should support error throwing", function() + expect(function() + error("Something went wrong") + end).toThrow("Something went wrong") + + expect(function() + return 42 + end).never.toThrow() + end) + end) + +end + +return M diff --git a/packages/lua_test/static_content/icon.svg b/packages/lua_test/static_content/icon.svg new file mode 100644 index 000000000..4d372dbd4 --- /dev/null +++ b/packages/lua_test/static_content/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + +