feat(lua_test): add README, components, index, example tests, and icon for Lua testing framework

This commit is contained in:
2025-12-30 01:16:07 +00:00
parent aa01e42ae8
commit a7be452ddb
5 changed files with 575 additions and 0 deletions

309
packages/lua_test/README.md Normal file
View File

@@ -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!
═══════════════════════════════════════
```

View File

@@ -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 }
}
}
]
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="8" fill="#1976d2"/>
<g fill="white">
<!-- Test tube / beaker icon -->
<path d="M22 12h20v4H22z"/>
<path d="M24 16h16v24l8 16H16l8-16z" fill="none" stroke="white" stroke-width="2"/>
<!-- Checkmark -->
<path d="M26 38l6 6 12-12" fill="none" stroke="#4caf50" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Lua moon reference -->
<circle cx="48" cy="16" r="6" fill="#ffc107"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B