mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-27 23:34:56 +00:00
feat(lua_test): add README, components, index, example tests, and icon for Lua testing framework
This commit is contained in:
309
packages/lua_test/README.md
Normal file
309
packages/lua_test/README.md
Normal 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!
|
||||
═══════════════════════════════════════
|
||||
```
|
||||
23
packages/lua_test/seed/components.json
Normal file
23
packages/lua_test/seed/components.json
Normal 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 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
23
packages/lua_test/seed/index.ts
Normal file
23
packages/lua_test/seed/index.ts
Normal 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
|
||||
@@ -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
|
||||
12
packages/lua_test/static_content/icon.svg
Normal file
12
packages/lua_test/static_content/icon.svg
Normal 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 |
Reference in New Issue
Block a user