feat(lua_test): add unit testing framework with BDD-style organization

- Implemented core testing functionalities including describe/it blocks, before/after hooks, and assertion methods.
- Added support for mocks and spies to facilitate testing of functions and methods.
- Introduced helper utilities for generating test data, parameterized tests, and snapshot testing.
- Developed a test runner that executes suites and generates detailed reports in both text and JSON formats.
- Created a manifest for the lua_test package to define scripts and their purposes.

feat(screenshot_analyzer): introduce screenshot analysis package

- Added metadata for the screenshot_analyzer package, detailing its components and scripts.
- Defined dependencies and bindings for browser interactions.
This commit is contained in:
2025-12-30 01:15:59 +00:00
parent 0690ab79c3
commit aa01e42ae8
24 changed files with 2705 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
/**
* Bindings Context
*
* Combines DBAL and browser bindings into a unified context
* that can be injected into Lua script execution.
*/
import type { JsonValue } from '@/types/utility-types'
import { createBrowserBindings, BROWSER_LUA_BINDINGS, type BrowserBindings } from './browser-bindings'
import { createDBALBindings, DBAL_LUA_BINDINGS, type DBALBindings } from './dbal-bindings'
export interface BindingsConfig {
enableDBAL?: boolean
enableBrowser?: boolean
dbalAdapter?: {
kvGet?: (key: string) => Promise<JsonValue | null>
kvSet?: (key: string, value: JsonValue, ttl?: number) => Promise<void>
kvDelete?: (key: string) => Promise<boolean>
kvListAdd?: (key: string, items: string[]) => Promise<void>
kvListGet?: (key: string) => Promise<string[]>
blobUpload?: (name: string, data: Uint8Array, metadata?: Record<string, string>) => Promise<void>
blobDownload?: (name: string) => Promise<Uint8Array>
blobDelete?: (name: string) => Promise<void>
blobList?: () => Promise<string[]>
}
}
export interface BindingsContext {
dbal?: DBALBindings
browser?: BrowserBindings
luaPrelude: string
contextFunctions: Record<string, (...args: unknown[]) => unknown>
}
/**
* Creates a combined bindings context for Lua execution
*/
export function createBindingsContext(config: BindingsConfig = {}): BindingsContext {
const context: BindingsContext = {
luaPrelude: '',
contextFunctions: {},
}
// Add DBAL bindings if enabled
if (config.enableDBAL && config.dbalAdapter) {
context.dbal = createDBALBindings(config.dbalAdapter)
context.luaPrelude += DBAL_LUA_BINDINGS + '\n'
// Register DBAL context functions
context.contextFunctions['__dbal_kv_get'] = context.dbal.kv.get
context.contextFunctions['__dbal_kv_set'] = context.dbal.kv.set
context.contextFunctions['__dbal_kv_delete'] = context.dbal.kv.delete
context.contextFunctions['__dbal_kv_list_add'] = context.dbal.kv.listAdd
context.contextFunctions['__dbal_kv_list_get'] = context.dbal.kv.listGet
context.contextFunctions['__dbal_blob_upload'] = context.dbal.blob.upload
context.contextFunctions['__dbal_blob_download'] = context.dbal.blob.download
context.contextFunctions['__dbal_blob_delete'] = context.dbal.blob.delete
context.contextFunctions['__dbal_blob_list'] = context.dbal.blob.list
context.contextFunctions['__dbal_cache_get'] = context.dbal.cache.get
context.contextFunctions['__dbal_cache_set'] = context.dbal.cache.set
context.contextFunctions['__dbal_cache_clear'] = context.dbal.cache.clear
}
// Add browser bindings if enabled
if (config.enableBrowser) {
context.browser = createBrowserBindings()
context.luaPrelude += BROWSER_LUA_BINDINGS + '\n'
// Register browser context functions
context.contextFunctions['__browser_page_get_title'] = context.browser.page.getTitle
context.contextFunctions['__browser_page_get_url'] = context.browser.page.getUrl
context.contextFunctions['__browser_page_get_viewport'] = context.browser.page.getViewport
context.contextFunctions['__browser_page_get_user_agent'] = context.browser.page.getUserAgent
context.contextFunctions['__browser_page_get_text_content'] = context.browser.page.getTextContent
context.contextFunctions['__browser_page_get_html_sample'] = context.browser.page.getHtmlSample
context.contextFunctions['__browser_dom_query_selector'] = context.browser.dom.querySelector
context.contextFunctions['__browser_dom_query_selector_all'] = context.browser.dom.querySelectorAll
context.contextFunctions['__browser_dom_get_element_count'] = context.browser.dom.getElementCount
context.contextFunctions['__browser_storage_get_local'] = context.browser.storage.getLocal
context.contextFunctions['__browser_storage_set_local'] = context.browser.storage.setLocal
context.contextFunctions['__browser_storage_get_session'] = context.browser.storage.getSession
context.contextFunctions['__browser_storage_set_session'] = context.browser.storage.setSession
context.contextFunctions['__browser_clipboard_write_text'] = context.browser.clipboard.writeText
context.contextFunctions['__browser_clipboard_read_text'] = context.browser.clipboard.readText
}
return context
}
/**
* Combined Lua bindings prelude for scripts that need both DBAL and browser access
*/
export const COMBINED_LUA_BINDINGS = DBAL_LUA_BINDINGS + '\n' + BROWSER_LUA_BINDINGS

View File

@@ -0,0 +1,185 @@
/**
* Browser API Bindings for Lua Scripts
*
* Provides Lua scripts access to browser APIs like DOM info, page metadata,
* and screenshot/capture functionality through a safe bridge.
*/
export interface BrowserBindings {
page: {
getTitle: () => string
getUrl: () => string
getViewport: () => { width: number; height: number }
getUserAgent: () => string
getTextContent: (maxLength?: number) => string
getHtmlSample: (maxLength?: number) => string
}
dom: {
querySelector: (selector: string) => { exists: boolean; text?: string; tagName?: string }
querySelectorAll: (selector: string) => Array<{ text: string; tagName: string }>
getElementCount: (selector: string) => number
}
storage: {
getLocal: (key: string) => string | null
setLocal: (key: string, value: string) => void
getSession: (key: string) => string | null
setSession: (key: string, value: string) => void
}
clipboard: {
writeText: (text: string) => Promise<void>
readText: () => Promise<string>
}
}
/**
* Creates browser bindings that can be injected into Lua execution context
* Only available in browser environment
*/
export function createBrowserBindings(): BrowserBindings {
const isBrowser = typeof window !== 'undefined'
return {
page: {
getTitle: () => (isBrowser ? document.title : ''),
getUrl: () => (isBrowser ? window.location.href : ''),
getViewport: () =>
isBrowser ? { width: window.innerWidth, height: window.innerHeight } : { width: 0, height: 0 },
getUserAgent: () => (isBrowser ? navigator.userAgent : ''),
getTextContent: (maxLength = 5000) =>
isBrowser ? document.body.innerText.substring(0, maxLength) : '',
getHtmlSample: (maxLength = 3000) =>
isBrowser ? document.body.innerHTML.substring(0, maxLength) : '',
},
dom: {
querySelector: (selector: string) => {
if (!isBrowser) return { exists: false }
try {
const el = document.querySelector(selector)
if (!el) return { exists: false }
return {
exists: true,
text: el.textContent?.substring(0, 500) ?? '',
tagName: el.tagName.toLowerCase(),
}
} catch {
return { exists: false }
}
},
querySelectorAll: (selector: string) => {
if (!isBrowser) return []
try {
const elements = document.querySelectorAll(selector)
return Array.from(elements)
.slice(0, 100)
.map(el => ({
text: el.textContent?.substring(0, 200) ?? '',
tagName: el.tagName.toLowerCase(),
}))
} catch {
return []
}
},
getElementCount: (selector: string) => {
if (!isBrowser) return 0
try {
return document.querySelectorAll(selector).length
} catch {
return 0
}
},
},
storage: {
getLocal: (key: string) => (isBrowser ? localStorage.getItem(key) : null),
setLocal: (key: string, value: string) => {
if (isBrowser) localStorage.setItem(key, value)
},
getSession: (key: string) => (isBrowser ? sessionStorage.getItem(key) : null),
setSession: (key: string, value: string) => {
if (isBrowser) sessionStorage.setItem(key, value)
},
},
clipboard: {
writeText: async (text: string) => {
if (isBrowser) await navigator.clipboard.writeText(text)
},
readText: async () => {
if (!isBrowser) return ''
try {
return await navigator.clipboard.readText()
} catch {
return ''
}
},
},
}
}
/**
* Lua code template for browser operations
* These functions are available in the Lua context when browser bindings are enabled
*/
export const BROWSER_LUA_BINDINGS = `
-- Page information bindings
function page_get_title()
return __browser_page_get_title()
end
function page_get_url()
return __browser_page_get_url()
end
function page_get_viewport()
return __browser_page_get_viewport()
end
function page_get_user_agent()
return __browser_page_get_user_agent()
end
function page_get_text_content(max_length)
return __browser_page_get_text_content(max_length or 5000)
end
function page_get_html_sample(max_length)
return __browser_page_get_html_sample(max_length or 3000)
end
-- DOM bindings
function dom_query_selector(selector)
return __browser_dom_query_selector(selector)
end
function dom_query_selector_all(selector)
return __browser_dom_query_selector_all(selector)
end
function dom_get_element_count(selector)
return __browser_dom_get_element_count(selector)
end
-- Storage bindings
function storage_get_local(key)
return __browser_storage_get_local(key)
end
function storage_set_local(key, value)
return __browser_storage_set_local(key, value)
end
function storage_get_session(key)
return __browser_storage_get_session(key)
end
function storage_set_session(key, value)
return __browser_storage_set_session(key, value)
end
-- Clipboard bindings
function clipboard_write_text(text)
return __browser_clipboard_write_text(text)
end
function clipboard_read_text()
return __browser_clipboard_read_text()
end
`

View File

@@ -0,0 +1,140 @@
/**
* DBAL Bindings for Lua Scripts
*
* Provides Lua scripts access to DBAL operations (KV store, blob storage, cached data)
* through a bridge that connects to the TypeScript DBAL client.
*/
import type { JsonValue } from '@/types/utility-types'
export interface DBALBindings {
kv: {
get: (key: string) => Promise<JsonValue | null>
set: (key: string, value: JsonValue, ttl?: number) => Promise<void>
delete: (key: string) => Promise<boolean>
listAdd: (key: string, items: string[]) => Promise<void>
listGet: (key: string) => Promise<string[]>
}
blob: {
upload: (name: string, data: string, metadata?: Record<string, string>) => Promise<void>
download: (name: string) => Promise<string>
delete: (name: string) => Promise<void>
list: () => Promise<string[]>
}
cache: {
get: (key: string) => Promise<JsonValue | null>
set: (key: string, value: JsonValue, ttl?: number) => Promise<void>
clear: (key: string) => Promise<void>
}
}
/**
* Creates DBAL bindings that can be injected into Lua execution context
*/
export function createDBALBindings(adapter: {
kvGet?: (key: string) => Promise<JsonValue | null>
kvSet?: (key: string, value: JsonValue, ttl?: number) => Promise<void>
kvDelete?: (key: string) => Promise<boolean>
kvListAdd?: (key: string, items: string[]) => Promise<void>
kvListGet?: (key: string) => Promise<string[]>
blobUpload?: (name: string, data: Uint8Array, metadata?: Record<string, string>) => Promise<void>
blobDownload?: (name: string) => Promise<Uint8Array>
blobDelete?: (name: string) => Promise<void>
blobList?: () => Promise<string[]>
}): DBALBindings {
return {
kv: {
get: async (key: string) => adapter.kvGet?.(key) ?? null,
set: async (key: string, value: JsonValue, ttl?: number) => {
await adapter.kvSet?.(key, value, ttl)
},
delete: async (key: string) => (await adapter.kvDelete?.(key)) ?? false,
listAdd: async (key: string, items: string[]) => {
await adapter.kvListAdd?.(key, items)
},
listGet: async (key: string) => (await adapter.kvListGet?.(key)) ?? [],
},
blob: {
upload: async (name: string, data: string, metadata?: Record<string, string>) => {
const encoder = new TextEncoder()
await adapter.blobUpload?.(name, encoder.encode(data), metadata)
},
download: async (name: string) => {
const data = await adapter.blobDownload?.(name)
if (!data) return ''
const decoder = new TextDecoder()
return decoder.decode(data)
},
delete: async (name: string) => {
await adapter.blobDelete?.(name)
},
list: async () => (await adapter.blobList?.()) ?? [],
},
cache: {
get: async (key: string) => adapter.kvGet?.(key) ?? null,
set: async (key: string, value: JsonValue, ttl?: number) => {
await adapter.kvSet?.(key, value, ttl)
},
clear: async (key: string) => {
await adapter.kvDelete?.(key)
},
},
}
}
/**
* Lua code template for DBAL operations
* These functions are available in the Lua context when DBAL bindings are enabled
*/
export const DBAL_LUA_BINDINGS = `
-- DBAL KV Store bindings
function kv_get(key)
return __dbal_kv_get(key)
end
function kv_set(key, value, ttl)
return __dbal_kv_set(key, value, ttl)
end
function kv_delete(key)
return __dbal_kv_delete(key)
end
function kv_list_add(key, items)
return __dbal_kv_list_add(key, items)
end
function kv_list_get(key)
return __dbal_kv_list_get(key)
end
-- DBAL Blob Storage bindings
function blob_upload(name, data, metadata)
return __dbal_blob_upload(name, data, metadata)
end
function blob_download(name)
return __dbal_blob_download(name)
end
function blob_delete(name)
return __dbal_blob_delete(name)
end
function blob_list()
return __dbal_blob_list()
end
-- DBAL Cache bindings
function cache_get(key)
return __dbal_cache_get(key)
end
function cache_set(key, value, ttl)
return __dbal_cache_set(key, value, ttl)
end
function cache_clear(key)
return __dbal_cache_clear(key)
end
`

View File

@@ -0,0 +1,14 @@
/**
* Lua Bindings Index
*
* Exports all Lua binding modules for easy import
*/
export { createBrowserBindings, BROWSER_LUA_BINDINGS } from './browser-bindings'
export type { BrowserBindings } from './browser-bindings'
export { createDBALBindings, DBAL_LUA_BINDINGS } from './dbal-bindings'
export type { DBALBindings } from './dbal-bindings'
export { createBindingsContext, COMBINED_LUA_BINDINGS } from './bindings-context'
export type { BindingsContext, BindingsConfig } from './bindings-context'

View File

@@ -0,0 +1,16 @@
/**
* Bridge Index
*
* Exports package bridge utilities for connecting Lua packages to React hooks
*/
export {
registerHookBridge,
getHookBridge,
hasHookBridge,
getRegisteredHooks,
createHookContextFunctions,
generateHookLuaBindings,
} from './package-bridge'
export type { HookBridge, HookInstance, PackageBridgeConfig } from './package-bridge'

View File

@@ -0,0 +1,178 @@
/**
* Package Bridge
*
* Connects Lua packages to React hooks, enabling hybrid components
* that combine declarative Lua logic with React's reactivity.
*/
import type { JsonValue } from '@/types/utility-types'
import type { ComponentBindings } from '../types'
export interface HookBridge {
/** Name of the hook */
name: string
/** Hook factory function */
factory: () => HookInstance
}
export interface HookInstance {
/** Current state */
state: Record<string, JsonValue>
/** Available actions */
actions: Record<string, (...args: unknown[]) => Promise<unknown> | unknown>
/** Cleanup function */
cleanup?: () => void
}
export interface PackageBridgeConfig {
/** Component bindings configuration */
bindings?: ComponentBindings
/** Hook factories to register */
hooks?: HookBridge[]
}
/**
* Registry of available hook bridges
*/
const hookRegistry = new Map<string, () => HookInstance>()
/**
* Register a hook bridge factory
*/
export function registerHookBridge(name: string, factory: () => HookInstance) {
hookRegistry.set(name, factory)
}
/**
* Get a hook bridge instance by name
*/
export function getHookBridge(name: string): HookInstance | null {
const factory = hookRegistry.get(name)
return factory ? factory() : null
}
/**
* Check if a hook bridge is registered
*/
export function hasHookBridge(name: string): boolean {
return hookRegistry.has(name)
}
/**
* Get all registered hook bridge names
*/
export function getRegisteredHooks(): string[] {
return Array.from(hookRegistry.keys())
}
/**
* Create Lua context functions from hook bridge
*/
export function createHookContextFunctions(
hookName: string,
instance: HookInstance
): Record<string, (...args: unknown[]) => unknown> {
const prefix = `__hook_${hookName}_`
const functions: Record<string, (...args: unknown[]) => unknown> = {}
// State getter
functions[`${prefix}get_state`] = () => instance.state
// Action wrappers
for (const [actionName, action] of Object.entries(instance.actions)) {
functions[`${prefix}${actionName}`] = action
}
return functions
}
/**
* Generate Lua binding code for a hook bridge
*/
export function generateHookLuaBindings(hookName: string, instance: HookInstance): string {
const prefix = `__hook_${hookName}_`
const lines: string[] = [
`-- Hook bindings for ${hookName}`,
`local ${hookName} = {}`,
'',
`function ${hookName}.get_state()`,
` return ${prefix}get_state()`,
`end`,
'',
]
for (const actionName of Object.keys(instance.actions)) {
lines.push(`function ${hookName}.${actionName}(...)`)
lines.push(` return ${prefix}${actionName}(...)`)
lines.push(`end`)
lines.push('')
}
return lines.join('\n')
}
// Pre-register common hook bridges
/**
* DBAL hook bridge - connects to useDBAL hook
*/
registerHookBridge('dbal', () => ({
state: {
isReady: false,
error: null,
},
actions: {
// These will be connected to actual hook in React component
connect: async (_endpoint: unknown, _apiKey: unknown) => ({ success: true }),
disconnect: async () => ({ success: true }),
},
}))
/**
* KV Store hook bridge - connects to useKVStore hook
*/
registerHookBridge('kv_store', () => ({
state: {
isInitialized: false,
},
actions: {
get: async (_key: unknown) => null,
set: async (_key: unknown, _value: unknown, _ttl?: unknown) => undefined,
delete: async (_key: unknown) => false,
list_add: async (_key: unknown, _items: unknown) => undefined,
list_get: async (_key: unknown) => [],
},
}))
/**
* Blob Storage hook bridge - connects to useBlobStorage hook
*/
registerHookBridge('blob_storage', () => ({
state: {
isInitialized: false,
},
actions: {
upload: async (_name: unknown, _data: unknown, _metadata?: unknown) => undefined,
download: async (_name: unknown) => '',
delete: async (_name: unknown) => undefined,
list: async () => [],
},
}))
/**
* Cached Data hook bridge - connects to useCachedData hook
*/
registerHookBridge('cached_data', () => ({
state: {
data: null,
loading: false,
error: null,
isReady: false,
},
actions: {
save: async (_data: unknown, _ttl?: unknown) => undefined,
clear: async () => undefined,
refresh: async () => undefined,
},
}))

View File

@@ -1,5 +1,17 @@
import type { JsonValue } from '@/types/utility-types'
/**
* Binding capabilities that can be enabled for a component
*/
export interface ComponentBindings {
/** Enable DBAL bindings (KV store, blob storage, cache) */
dbal?: boolean
/** Enable browser bindings (DOM, page info, storage) */
browser?: boolean
/** Custom allowed globals for Lua sandbox */
allowedGlobals?: string[]
}
export interface DeclarativeComponentConfig {
type: string
category: string
@@ -20,6 +32,10 @@ export interface DeclarativeComponentConfig {
}
children: JsonValue[]
}
/** Optional bindings to enable for this component's Lua scripts */
bindings?: ComponentBindings
/** React hooks this component requires (for hybrid components) */
requiredHooks?: string[]
}
export interface MessageFormat {

View File

@@ -0,0 +1,190 @@
{
"DBALDemo": {
"type": "DBALDemo",
"category": "demo",
"label": "DBAL Demo",
"description": "Demonstration of the TypeScript DBAL client integrated with MetaBuilder",
"icon": "database",
"props": [
{
"name": "defaultEndpoint",
"type": "string",
"label": "Default Endpoint",
"defaultValue": "",
"required": false
},
{
"name": "defaultApiKey",
"type": "string",
"label": "Default API Key",
"defaultValue": "",
"required": false
}
],
"config": {
"layout": "container",
"styling": {
"className": "dbal-demo-container"
},
"children": [
{
"type": "header",
"props": {
"title": "DBAL Integration Demo",
"description": "Demonstration of the TypeScript DBAL client integrated with MetaBuilder"
}
},
{
"type": "grid",
"props": { "columns": 2, "gap": 4 },
"children": [
{ "type": "ConnectionForm", "props": {} },
{ "type": "ResultPanel", "props": { "title": "Connection Details" } }
]
},
{ "type": "LogsPanel", "props": { "title": "Demo Logs" } },
{
"type": "tabs",
"props": {
"defaultValue": "kv",
"tabs": [
{ "value": "kv", "label": "Key-Value Store" },
{ "value": "blob", "label": "Blob Storage" },
{ "value": "cache", "label": "Cached Data" }
]
},
"children": [
{ "type": "KVStorePanel", "props": {} },
{ "type": "BlobStoragePanel", "props": {} },
{ "type": "CachedDataPanel", "props": {} }
]
}
]
},
"bindings": {
"dbal": true
},
"requiredHooks": ["dbal", "kv_store", "blob_storage", "cached_data"]
},
"KVStorePanel": {
"type": "KVStorePanel",
"category": "demo",
"label": "KV Store Panel",
"description": "Key-Value store operations panel",
"icon": "key",
"props": [],
"config": {
"layout": "card",
"styling": { "className": "kv-store-panel" },
"children": [
{
"type": "card",
"props": { "title": "Key-Value Operations", "description": "Basic get/set/delete operations" },
"children": [
{ "type": "input", "props": { "name": "key", "label": "Key", "defaultValue": "demo-key" } },
{ "type": "input", "props": { "name": "value", "label": "Value", "defaultValue": "Hello, DBAL!" } },
{ "type": "input", "props": { "name": "ttl", "label": "TTL (seconds)", "type": "number" } },
{
"type": "button-group",
"children": [
{ "type": "button", "props": { "label": "Set", "action": "kv_set" } },
{ "type": "button", "props": { "label": "Get", "action": "kv_get" } },
{ "type": "button", "props": { "label": "Delete", "action": "kv_delete", "variant": "destructive" } }
]
}
]
},
{
"type": "card",
"props": { "title": "List Operations", "description": "Add and retrieve list items" },
"children": [
{ "type": "input", "props": { "name": "listKey", "label": "List Key", "defaultValue": "demo-list" } },
{
"type": "button-group",
"children": [
{ "type": "button", "props": { "label": "Add Items", "action": "kv_list_add" } },
{ "type": "button", "props": { "label": "Get Items", "action": "kv_list_get" } }
]
},
{ "type": "list-display", "props": { "dataSource": "listItems" } }
]
}
]
},
"bindings": { "dbal": true },
"requiredHooks": ["kv_store"]
},
"BlobStoragePanel": {
"type": "BlobStoragePanel",
"category": "demo",
"label": "Blob Storage Panel",
"description": "Blob storage operations panel",
"icon": "file",
"props": [],
"config": {
"layout": "card",
"styling": { "className": "blob-storage-panel" },
"children": [
{
"type": "card",
"props": { "title": "Blob Storage Operations", "description": "Upload, download, and manage binary data" },
"children": [
{ "type": "input", "props": { "name": "fileName", "label": "File Name", "defaultValue": "demo.txt" } },
{ "type": "textarea", "props": { "name": "fileContent", "label": "Content", "defaultValue": "Hello from DBAL blob storage!" } },
{
"type": "button-group",
"children": [
{ "type": "button", "props": { "label": "Upload", "action": "blob_upload" } },
{ "type": "button", "props": { "label": "Download", "action": "blob_download" } },
{ "type": "button", "props": { "label": "Delete", "action": "blob_delete", "variant": "destructive" } },
{ "type": "button", "props": { "label": "List Files", "action": "blob_list" } }
]
},
{ "type": "code-display", "props": { "dataSource": "downloadedContent", "language": "text" } },
{ "type": "list-display", "props": { "dataSource": "files" } }
]
}
]
},
"bindings": { "dbal": true },
"requiredHooks": ["blob_storage"]
},
"CachedDataPanel": {
"type": "CachedDataPanel",
"category": "demo",
"label": "Cached Data Panel",
"description": "Cached data hook demonstration",
"icon": "clock",
"props": [],
"config": {
"layout": "card",
"styling": { "className": "cached-data-panel" },
"children": [
{
"type": "card",
"props": { "title": "Cached Data Hook", "description": "Automatic caching with React hooks" },
"children": [
{ "type": "status-badge", "props": { "condition": "loading", "text": "Loading cached data..." } },
{ "type": "status-badge", "props": { "condition": "error", "text": "Error: {{error}}", "variant": "destructive" } },
{
"type": "data-display",
"props": { "dataSource": "cachedData", "title": "Cached Preferences" }
},
{ "type": "input", "props": { "name": "theme", "label": "Theme", "defaultValue": "dark" } },
{ "type": "input", "props": { "name": "language", "label": "Language", "defaultValue": "en" } },
{ "type": "checkbox", "props": { "name": "notifications", "label": "Notifications", "defaultValue": true } },
{
"type": "button-group",
"children": [
{ "type": "button", "props": { "label": "Save Preferences", "action": "cache_save" } },
{ "type": "button", "props": { "label": "Clear Cache", "action": "cache_clear", "variant": "outline" } }
]
}
]
}
]
},
"bindings": { "dbal": true },
"requiredHooks": ["cached_data"]
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "dbal_demo",
"version": "1.0.0",
"description": "DBAL Integration Demo - Demonstrates TypeScript DBAL client with MetaBuilder",
"author": "MetaBuilder",
"license": "MIT",
"minLevel": 3,
"category": "demo",
"tags": ["dbal", "demo", "kv-store", "blob-storage", "cache"],
"dependencies": [],
"exports": {
"components": ["DBALDemo", "KVStorePanel", "BlobStoragePanel", "CachedDataPanel"],
"scripts": ["init", "kv_operations", "blob_operations", "cache_operations"]
},
"bindings": {
"dbal": true,
"browser": false
},
"requiredHooks": ["dbal", "kv_store", "blob_storage", "cached_data"],
"layout": "layout.json"
}

View File

@@ -0,0 +1,104 @@
-- Blob Storage Operations
local M = {}
-- Upload a file/blob
function M.upload(name, content, metadata)
if not name or name == "" then
return { success = false, error = "File name is required" }
end
content = content or ""
metadata = metadata or {
["content-type"] = "text/plain",
["uploaded-at"] = os.date("%Y-%m-%dT%H:%M:%SZ")
}
blob_upload(name, content, metadata)
return {
success = true,
message = "Uploaded: " .. name,
size = #content,
metadata = metadata
}
end
-- Download a file/blob
function M.download(name)
if not name or name == "" then
return { success = false, error = "File name is required" }
end
local content = blob_download(name)
if content and content ~= "" then
return {
success = true,
content = content,
size = #content,
message = "Downloaded successfully"
}
else
return {
success = false,
message = "File not found or empty"
}
end
end
-- Delete a file/blob
function M.delete(name)
if not name or name == "" then
return { success = false, error = "File name is required" }
end
blob_delete(name)
return {
success = true,
message = "Deleted: " .. name
}
end
-- List all files/blobs
function M.list()
local files = blob_list()
return {
success = true,
files = files,
count = #files,
message = "Found " .. #files .. " files"
}
end
-- Get file info (without downloading content)
function M.info(name)
if not name or name == "" then
return { success = false, error = "File name is required" }
end
local files = blob_list()
local found = false
for _, file in ipairs(files) do
if file == name then
found = true
break
end
end
if found then
return {
success = true,
name = name,
exists = true
}
else
return {
success = false,
name = name,
exists = false,
message = "File not found"
}
end
end
return M

View File

@@ -0,0 +1,92 @@
-- Cache Operations
local M = {}
-- Save data to cache
function M.save(key, data, ttl)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
end
ttl = ttl or 3600 -- Default 1 hour TTL
cache_set(key, data, ttl)
return {
success = true,
message = "Data cached with TTL: " .. ttl .. "s",
key = key,
ttl = ttl
}
end
-- Get cached data
function M.get(key)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
end
local data = cache_get(key)
if data then
return {
success = true,
data = data,
message = "Cache hit"
}
else
return {
success = false,
message = "Cache miss"
}
end
end
-- Clear cached data
function M.clear(key)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
end
cache_clear(key)
return {
success = true,
message = "Cache cleared: " .. key
}
end
-- Save user preferences (common use case)
function M.save_preferences(prefs)
prefs = prefs or {}
local data = {
theme = prefs.theme or "dark",
language = prefs.language or "en",
notifications = prefs.notifications ~= false
}
return M.save("user-preferences", data, 3600)
end
-- Get user preferences
function M.get_preferences()
local result = M.get("user-preferences")
if result.success then
return {
success = true,
preferences = result.data
}
else
-- Return defaults
return {
success = true,
preferences = {
theme = "dark",
language = "en",
notifications = true
},
is_default = true
}
end
end
return M

View File

@@ -0,0 +1,75 @@
-- Connection Management
local M = {}
M.logs = {}
M.status = "disconnected"
-- Connect to DBAL endpoint
function M.connect(endpoint, api_key)
if not endpoint or endpoint == "" then
return { success = false, error = "Endpoint is required" }
end
local timestamp = os.date("%H:%M:%S")
local log_entry = timestamp .. ": Connected to " .. endpoint
table.insert(M.logs, log_entry)
M.status = "connected"
return {
success = true,
endpoint = endpoint,
api_key = api_key and "***" or "Not provided",
timestamp = timestamp,
message = "Connected successfully"
}
end
-- Disconnect from DBAL
function M.disconnect()
local timestamp = os.date("%H:%M:%S")
table.insert(M.logs, timestamp .. ": Disconnected")
M.status = "disconnected"
return {
success = true,
message = "Disconnected"
}
end
-- Get connection status
function M.get_status()
return {
status = M.status,
is_connected = M.status == "connected"
}
end
-- Get connection logs
function M.get_logs()
return {
logs = M.logs,
count = #M.logs
}
end
-- Clear logs
function M.clear_logs()
M.logs = {}
return {
success = true,
message = "Logs cleared"
}
end
-- Add custom log entry
function M.log(message)
local timestamp = os.date("%H:%M:%S")
table.insert(M.logs, timestamp .. ": " .. message)
return {
success = true,
entry = timestamp .. ": " .. message
}
end
return M

View File

@@ -0,0 +1,26 @@
-- DBAL Demo Package Initialization
local M = {}
M.name = "dbal_demo"
M.version = "1.0.0"
function M.init()
return {
success = true,
message = "DBAL Demo package initialized"
}
end
function M.get_config()
return {
default_endpoint = "",
default_api_key = "",
tabs = {
{ value = "kv", label = "Key-Value Store" },
{ value = "blob", label = "Blob Storage" },
{ value = "cache", label = "Cached Data" }
}
}
end
return M

View File

@@ -0,0 +1,90 @@
-- KV Store Operations
local M = {}
-- Set a key-value pair
function M.set(key, value, ttl)
if not key or key == "" then
return { success = false, error = "Key is required" }
end
local result = kv_set(key, value, ttl)
return {
success = true,
message = "Stored: " .. key .. " = " .. tostring(value),
ttl = ttl
}
end
-- Get a value by key
function M.get(key)
if not key or key == "" then
return { success = false, error = "Key is required" }
end
local value = kv_get(key)
if value then
return {
success = true,
value = value,
message = "Retrieved: " .. tostring(value)
}
else
return {
success = false,
message = "Key not found"
}
end
end
-- Delete a key
function M.delete(key)
if not key or key == "" then
return { success = false, error = "Key is required" }
end
local deleted = kv_delete(key)
if deleted then
return {
success = true,
message = "Deleted: " .. key
}
else
return {
success = false,
message = "Key not found"
}
end
end
-- Add items to a list
function M.list_add(key, items)
if not key or key == "" then
return { success = false, error = "List key is required" }
end
items = items or { "Item 1", "Item 2", "Item 3" }
kv_list_add(key, items)
return {
success = true,
message = "Added " .. #items .. " items to list",
items = items
}
end
-- Get list items
function M.list_get(key)
if not key or key == "" then
return { success = false, error = "List key is required" }
end
local items = kv_list_get(key)
return {
success = true,
items = items,
count = #items,
message = "Retrieved " .. #items .. " items"
}
end
return M

View File

@@ -0,0 +1,34 @@
{
"scripts": [
{
"name": "init",
"file": "init.lua",
"category": "lifecycle",
"description": "Package initialization"
},
{
"name": "kv_operations",
"file": "kv_operations.lua",
"category": "dbal",
"description": "Key-Value store operations"
},
{
"name": "blob_operations",
"file": "blob_operations.lua",
"category": "dbal",
"description": "Blob storage operations"
},
{
"name": "cache_operations",
"file": "cache_operations.lua",
"category": "dbal",
"description": "Cache operations"
},
{
"name": "connection",
"file": "connection.lua",
"category": "dbal",
"description": "Connection management"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"packageId": "lua_test",
"name": "Lua Test",
"version": "1.0.0",
"description": "Unit testing framework for Lua scripts in MetaBuilder packages",
"icon": "static_content/icon.svg",
"author": "MetaBuilder",
"category": "tools",
"dependencies": [],
"exports": {
"components": ["TestRunner", "TestResults"],
"scripts": ["framework", "runner", "assertions", "mocks"]
},
"minLevel": 3
}

View File

@@ -0,0 +1,360 @@
-- Assertion functions for the test framework
-- Provides expect() with chainable matchers
local M = {}
-- Helper to get type with better nil handling
local function getType(value)
if value == nil then return "nil" end
return type(value)
end
-- Helper to stringify values for error messages
local function stringify(value)
local t = type(value)
if t == "string" then
return '"' .. value .. '"'
elseif t == "table" then
local parts = {}
for k, v in pairs(value) do
parts[#parts + 1] = tostring(k) .. "=" .. stringify(v)
end
return "{" .. table.concat(parts, ", ") .. "}"
elseif t == "nil" then
return "nil"
else
return tostring(value)
end
end
-- Deep equality check
local function deepEqual(a, b)
if type(a) ~= type(b) then return false end
if type(a) ~= "table" then return a == b end
-- Check all keys in a exist in b with same values
for k, v in pairs(a) do
if not deepEqual(v, b[k]) then return false end
end
-- Check all keys in b exist in a
for k, _ in pairs(b) do
if a[k] == nil then return false end
end
return true
end
-- Create assertion error
local function assertionError(message, expected, actual)
return {
type = "AssertionError",
message = message,
expected = expected,
actual = actual
}
end
-- Expect wrapper with chainable matchers
function M.expect(actual)
local expectation = {
actual = actual,
negated = false
}
-- Not modifier
expectation.never = setmetatable({}, {
__index = function(_, key)
expectation.negated = true
return expectation[key]
end
})
-- toBe - strict equality
function expectation.toBe(expected)
local pass = actual == expected
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be " .. stringify(expected)
or "Expected " .. stringify(actual) .. " to be " .. stringify(expected)
error(assertionError(msg, expected, actual))
end
return true
end
-- toEqual - deep equality
function expectation.toEqual(expected)
local pass = deepEqual(actual, expected)
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected values not to be deeply equal"
or "Expected values to be deeply equal"
error(assertionError(msg, expected, actual))
end
return true
end
-- toBeNil
function expectation.toBeNil()
local pass = actual == nil
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be nil"
or "Expected " .. stringify(actual) .. " to be nil"
error(assertionError(msg, nil, actual))
end
return true
end
-- toBeTruthy
function expectation.toBeTruthy()
local pass = actual and true or false
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be truthy"
or "Expected " .. stringify(actual) .. " to be truthy"
error(assertionError(msg, "truthy", actual))
end
return true
end
-- toBeFalsy
function expectation.toBeFalsy()
local pass = not actual
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be falsy"
or "Expected " .. stringify(actual) .. " to be falsy"
error(assertionError(msg, "falsy", actual))
end
return true
end
-- toBeType
function expectation.toBeType(expectedType)
local actualType = getType(actual)
local pass = actualType == expectedType
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected type not to be " .. expectedType .. ", got " .. actualType
or "Expected type to be " .. expectedType .. ", got " .. actualType
error(assertionError(msg, expectedType, actualType))
end
return true
end
-- toContain - for strings and tables
function expectation.toContain(expected)
local pass = false
if type(actual) == "string" and type(expected) == "string" then
pass = string.find(actual, expected, 1, true) ~= nil
elseif type(actual) == "table" then
for _, v in pairs(actual) do
if deepEqual(v, expected) then
pass = true
break
end
end
end
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to contain " .. stringify(expected)
or "Expected " .. stringify(actual) .. " to contain " .. stringify(expected)
error(assertionError(msg, expected, actual))
end
return true
end
-- toHaveLength
function expectation.toHaveLength(expectedLength)
local actualLength
if type(actual) == "string" then
actualLength = #actual
elseif type(actual) == "table" then
actualLength = #actual
else
error("toHaveLength can only be used with strings or tables")
end
local pass = actualLength == expectedLength
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected length not to be " .. expectedLength .. ", got " .. actualLength
or "Expected length to be " .. expectedLength .. ", got " .. actualLength
error(assertionError(msg, expectedLength, actualLength))
end
return true
end
-- toBeGreaterThan
function expectation.toBeGreaterThan(expected)
local pass = actual > expected
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be greater than " .. stringify(expected)
or "Expected " .. stringify(actual) .. " to be greater than " .. stringify(expected)
error(assertionError(msg, "> " .. expected, actual))
end
return true
end
-- toBeLessThan
function expectation.toBeLessThan(expected)
local pass = actual < expected
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to be less than " .. stringify(expected)
or "Expected " .. stringify(actual) .. " to be less than " .. stringify(expected)
error(assertionError(msg, "< " .. expected, actual))
end
return true
end
-- toBeCloseTo - for floating point comparison
function expectation.toBeCloseTo(expected, precision)
precision = precision or 2
local diff = math.abs(actual - expected)
local pass = diff < (10 ^ -precision) / 2
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. actual .. " not to be close to " .. expected
or "Expected " .. actual .. " to be close to " .. expected .. " (diff: " .. diff .. ")"
error(assertionError(msg, expected, actual))
end
return true
end
-- toMatch - regex pattern matching
function expectation.toMatch(pattern)
if type(actual) ~= "string" then
error("toMatch can only be used with strings")
end
local pass = string.match(actual, pattern) ~= nil
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected " .. stringify(actual) .. " not to match pattern " .. pattern
or "Expected " .. stringify(actual) .. " to match pattern " .. pattern
error(assertionError(msg, pattern, actual))
end
return true
end
-- toThrow - expects a function to throw
function expectation.toThrow(expectedMessage)
if type(actual) ~= "function" then
error("toThrow can only be used with functions")
end
local success, err = pcall(actual)
local pass = not success
if pass and expectedMessage then
local errMsg = type(err) == "table" and err.message or tostring(err)
pass = string.find(errMsg, expectedMessage, 1, true) ~= nil
end
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected function not to throw"
or "Expected function to throw" .. (expectedMessage and " with message: " .. expectedMessage or "")
error(assertionError(msg, expectedMessage or "error", success and "no error" or err))
end
return true
end
-- toHaveProperty
function expectation.toHaveProperty(key, value)
if type(actual) ~= "table" then
error("toHaveProperty can only be used with tables")
end
local pass = actual[key] ~= nil
if pass and value ~= nil then
pass = deepEqual(actual[key], value)
end
if expectation.negated then pass = not pass end
if not pass then
local msg = expectation.negated
and "Expected table not to have property " .. stringify(key)
or "Expected table to have property " .. stringify(key) .. (value and " with value " .. stringify(value) or "")
error(assertionError(msg, value, actual[key]))
end
return true
end
return expectation
end
-- Standalone assertion functions
function M.assertTrue(value, message)
if not value then
error(assertionError(message or "Expected true", true, value))
end
end
function M.assertFalse(value, message)
if value then
error(assertionError(message or "Expected false", false, value))
end
end
function M.assertEqual(actual, expected, message)
if actual ~= expected then
error(assertionError(message or "Values not equal", expected, actual))
end
end
function M.assertNotEqual(actual, expected, message)
if actual == expected then
error(assertionError(message or "Values should not be equal", "not " .. stringify(expected), actual))
end
end
function M.assertNil(value, message)
if value ~= nil then
error(assertionError(message or "Expected nil", nil, value))
end
end
function M.assertNotNil(value, message)
if value == nil then
error(assertionError(message or "Expected not nil", "not nil", nil))
end
end
function M.fail(message)
error(assertionError(message or "Test failed", nil, nil))
end
return M

View File

@@ -0,0 +1,145 @@
-- Core test framework with describe/it blocks
-- Provides BDD-style test organization
local M = {}
-- Test suite state
M._suites = {}
M._currentSuite = nil
M._config = {
timeout = 5000,
verbose = true,
stopOnFirstFailure = false,
filter = nil
}
-- Create a new test suite
function M.createSuite(name)
local suite = {
name = name,
tests = {},
beforeAll = nil,
afterAll = nil,
beforeEach = nil,
afterEach = nil,
nested = {},
parent = nil
}
return suite
end
-- Describe block - groups related tests
function M.describe(name, fn)
local parentSuite = M._currentSuite
local suite = M.createSuite(name)
suite.parent = parentSuite
if parentSuite then
parentSuite.nested[#parentSuite.nested + 1] = suite
else
M._suites[#M._suites + 1] = suite
end
M._currentSuite = suite
fn()
M._currentSuite = parentSuite
return suite
end
-- It block - defines a single test
function M.it(name, fn)
if not M._currentSuite then
error("it() must be called inside a describe() block")
end
local test = {
name = name,
fn = fn,
status = "pending",
error = nil,
duration = 0,
skipped = false
}
M._currentSuite.tests[#M._currentSuite.tests + 1] = test
return test
end
-- Skip a test
function M.xit(name, fn)
if not M._currentSuite then
error("xit() must be called inside a describe() block")
end
local test = {
name = name,
fn = fn,
status = "skipped",
error = nil,
duration = 0,
skipped = true
}
M._currentSuite.tests[#M._currentSuite.tests + 1] = test
return test
end
-- Only run this test (for debugging)
function M.fit(name, fn)
local test = M.it(name, fn)
test.only = true
return test
end
-- Setup hooks
function M.beforeAll(fn)
if M._currentSuite then
M._currentSuite.beforeAll = fn
end
end
function M.afterAll(fn)
if M._currentSuite then
M._currentSuite.afterAll = fn
end
end
function M.beforeEach(fn)
if M._currentSuite then
M._currentSuite.beforeEach = fn
end
end
function M.afterEach(fn)
if M._currentSuite then
M._currentSuite.afterEach = fn
end
end
-- Configure the test framework
function M.configure(options)
for k, v in pairs(options) do
if M._config[k] ~= nil then
M._config[k] = v
end
end
end
-- Get all registered suites
function M.getSuites()
return M._suites
end
-- Reset all suites (for fresh test runs)
function M.reset()
M._suites = {}
M._currentSuite = nil
end
-- Get current config
function M.getConfig()
return M._config
end
return M

View File

@@ -0,0 +1,267 @@
-- Test helper utilities
-- Additional utilities for writing tests
local M = {}
-- Generate test data
function M.generateTestData(template, count)
local data = {}
count = count or 10
for i = 1, count do
local item = {}
for k, v in pairs(template) do
if type(v) == "function" then
item[k] = v(i)
elseif type(v) == "string" and v:match("^%$") then
-- Template variables
local varName = v:sub(2)
if varName == "index" then
item[k] = i
elseif varName == "random" then
item[k] = math.random(1, 1000)
elseif varName == "uuid" then
item[k] = string.format("%08x-%04x-%04x-%04x-%012x",
math.random(0, 0xffffffff),
math.random(0, 0xffff),
math.random(0, 0xffff),
math.random(0, 0xffff),
math.random(0, 0xffffffffffff))
else
item[k] = v
end
else
item[k] = v
end
end
data[#data + 1] = item
end
return data
end
-- Create parameterized test cases
function M.parameterized(cases, testFn)
return function()
for _, testCase in ipairs(cases) do
testFn(testCase)
end
end
end
-- Wait for condition (for async-like testing)
function M.waitFor(condition, options)
options = options or {}
local timeout = options.timeout or 1000
local interval = options.interval or 10
local startTime = os.clock() * 1000
while (os.clock() * 1000 - startTime) < timeout do
if condition() then
return true
end
-- Note: In sandbox, we can't actually sleep, but this provides the pattern
end
if options.throwOnTimeout ~= false then
error("waitFor timed out after " .. timeout .. "ms")
end
return false
end
-- Snapshot testing helper
function M.createSnapshot(name)
local snapshots = {}
return {
-- Record a snapshot
record = function(key, value)
snapshots[key] = M.serialize(value)
end,
-- Match against recorded snapshot
match = function(key, value)
local serialized = M.serialize(value)
if snapshots[key] then
return snapshots[key] == serialized
end
-- First run - record the snapshot
snapshots[key] = serialized
return true
end,
-- Get all snapshots
getSnapshots = function()
return snapshots
end,
-- Update a snapshot
update = function(key, value)
snapshots[key] = M.serialize(value)
end
}
end
-- Serialize value for comparison
function M.serialize(value, seen)
seen = seen or {}
local t = type(value)
if t == "nil" then
return "nil"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
return tostring(value)
elseif t == "string" then
return string.format("%q", value)
elseif t == "table" then
if seen[value] then
return "<circular>"
end
seen[value] = true
local parts = {}
local keys = {}
for k in pairs(value) do
keys[#keys + 1] = k
end
table.sort(keys, function(a, b)
return tostring(a) < tostring(b)
end)
for _, k in ipairs(keys) do
parts[#parts + 1] = "[" .. M.serialize(k, seen) .. "]=" .. M.serialize(value[k], seen)
end
return "{" .. table.concat(parts, ",") .. "}"
else
return "<" .. t .. ">"
end
end
-- Table utilities for testing
M.table = {}
function M.table.clone(t)
if type(t) ~= "table" then return t end
local copy = {}
for k, v in pairs(t) do
copy[k] = M.table.clone(v)
end
return setmetatable(copy, getmetatable(t))
end
function M.table.merge(...)
local result = {}
for _, t in ipairs({...}) do
if type(t) == "table" then
for k, v in pairs(t) do
result[k] = v
end
end
end
return result
end
function M.table.keys(t)
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
return keys
end
function M.table.values(t)
local values = {}
for _, v in pairs(t) do
values[#values + 1] = v
end
return values
end
function M.table.size(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
-- String utilities for testing
M.string = {}
function M.string.trim(s)
return s:match("^%s*(.-)%s*$")
end
function M.string.split(s, delimiter)
delimiter = delimiter or "%s"
local result = {}
for match in (s .. delimiter):gmatch("(.-)" .. delimiter) do
result[#result + 1] = match
end
return result
end
function M.string.startsWith(s, prefix)
return s:sub(1, #prefix) == prefix
end
function M.string.endsWith(s, suffix)
return suffix == "" or s:sub(-#suffix) == suffix
end
-- Assertion shortcuts for common patterns
function M.assertThrows(fn, expectedMessage)
local success, err = pcall(fn)
if success then
error("Expected function to throw, but it didn't")
end
if expectedMessage then
local errStr = type(err) == "table" and err.message or tostring(err)
if not string.find(errStr, expectedMessage, 1, true) then
error("Expected error to contain: " .. expectedMessage .. ", got: " .. errStr)
end
end
return err
end
function M.assertDoesNotThrow(fn)
local success, err = pcall(fn)
if not success then
error("Expected function not to throw, but got: " .. tostring(err))
end
end
-- Test context builder
function M.createContext(initial)
local ctx = initial or {}
return {
get = function(key)
return ctx[key]
end,
set = function(key, value)
ctx[key] = value
end,
with = function(overrides)
return M.table.merge(ctx, overrides)
end,
reset = function()
ctx = initial and M.table.clone(initial) or {}
end,
getAll = function()
return M.table.clone(ctx)
end
}
end
return M

View File

@@ -0,0 +1,34 @@
-- lua_test package initialization
-- Unit testing framework for MetaBuilder Lua scripts
local M = {}
M.version = "1.0.0"
M.name = "lua_test"
function M.init()
return {
success = true,
message = "Lua Test Framework initialized",
version = M.version
}
end
function M.info()
return {
name = M.name,
version = M.version,
description = "Unit testing framework for Lua scripts",
features = {
"describe/it blocks",
"assertions (expect)",
"beforeEach/afterEach hooks",
"mocks and spies",
"async test support",
"test filtering",
"detailed reporting"
}
}
end
return M

View File

@@ -0,0 +1,10 @@
{
"scripts": [
{ "file": "init.lua", "name": "init", "category": "lifecycle", "description": "Package lifecycle initialization" },
{ "file": "framework.lua", "name": "framework", "category": "core", "description": "Core test framework with describe/it blocks" },
{ "file": "assertions.lua", "name": "assertions", "category": "core", "description": "Assertion functions (expect, assert_*)" },
{ "file": "mocks.lua", "name": "mocks", "category": "utilities", "description": "Mock and spy utilities" },
{ "file": "runner.lua", "name": "runner", "category": "core", "description": "Test runner and reporter" },
{ "file": "helpers.lua", "name": "helpers", "category": "utilities", "description": "Test helper utilities" }
]
}

View File

@@ -0,0 +1,248 @@
-- Mock and spy utilities for testing
-- Allows tracking function calls and replacing implementations
local M = {}
-- Create a mock function
function M.fn(implementation)
local mock = {
calls = {},
results = {},
implementation = implementation
}
-- The callable mock function
local callable = function(...)
local args = {...}
mock.calls[#mock.calls + 1] = args
local result
if mock.implementation then
result = {mock.implementation(...)}
else
result = {}
end
mock.results[#mock.results + 1] = result
return table.unpack(result)
end
-- Attach mock metadata to the function via a metatable
return setmetatable({}, {
__call = function(_, ...) return callable(...) end,
__index = {
-- Get call count
getCallCount = function()
return #mock.calls
end,
-- Check if called
wasCalled = function()
return #mock.calls > 0
end,
-- Check if called with specific args
wasCalledWith = function(...)
local expectedArgs = {...}
for _, callArgs in ipairs(mock.calls) do
local match = true
for i, expected in ipairs(expectedArgs) do
if callArgs[i] ~= expected then
match = false
break
end
end
if match then return true end
end
return false
end,
-- Get specific call args
getCall = function(index)
return mock.calls[index]
end,
-- Get last call args
getLastCall = function()
return mock.calls[#mock.calls]
end,
-- Get all calls
getCalls = function()
return mock.calls
end,
-- Get all results
getResults = function()
return mock.results
end,
-- Clear call history
reset = function()
mock.calls = {}
mock.results = {}
end,
-- Set return value
mockReturnValue = function(value)
mock.implementation = function() return value end
end,
-- Set return values in sequence
mockReturnValueOnce = function(value)
local originalImpl = mock.implementation
local called = false
mock.implementation = function(...)
if not called then
called = true
return value
elseif originalImpl then
return originalImpl(...)
end
end
end,
-- Set implementation
mockImplementation = function(fn)
mock.implementation = fn
end,
-- Restore original (for spies)
mockRestore = function()
mock.implementation = nil
mock.calls = {}
mock.results = {}
end
}
})
end
-- Create a spy on an existing object method
function M.spyOn(obj, methodName)
local original = obj[methodName]
if type(original) ~= "function" then
error("Cannot spy on non-function: " .. methodName)
end
local spy = M.fn(original)
-- Add restore functionality
local meta = getmetatable(spy).__index
local originalRestore = meta.mockRestore
meta.mockRestore = function()
obj[methodName] = original
originalRestore()
end
-- Replace the method
obj[methodName] = function(...)
return spy(...)
end
return spy
end
-- Create a mock object with multiple mock functions
function M.mockObject(methods)
local obj = {}
local mocks = {}
for name, impl in pairs(methods or {}) do
mocks[name] = M.fn(impl)
obj[name] = function(...) return mocks[name](...) end
end
obj._mocks = mocks
obj._resetAll = function()
for _, mock in pairs(mocks) do
mock.reset()
end
end
return obj
end
-- Timer mocks for testing time-dependent code
function M.useFakeTimers()
local timers = {
now = 0,
scheduled = {}
}
return {
-- Get current fake time
now = function()
return timers.now
end,
-- Schedule a callback (like setTimeout)
schedule = function(callback, delay)
local id = #timers.scheduled + 1
timers.scheduled[id] = {
callback = callback,
time = timers.now + delay,
id = id
}
return id
end,
-- Cancel a scheduled callback
cancel = function(id)
timers.scheduled[id] = nil
end,
-- Advance time and run scheduled callbacks
advance = function(ms)
local targetTime = timers.now + ms
-- Sort by scheduled time
local pending = {}
for _, timer in pairs(timers.scheduled) do
if timer.time <= targetTime then
pending[#pending + 1] = timer
end
end
table.sort(pending, function(a, b) return a.time < b.time end)
-- Run each callback at its scheduled time
for _, timer in ipairs(pending) do
timers.now = timer.time
timer.callback()
timers.scheduled[timer.id] = nil
end
timers.now = targetTime
end,
-- Run all pending timers
runAll = function()
while next(timers.scheduled) do
local nextTimer
local nextTime = math.huge
for id, timer in pairs(timers.scheduled) do
if timer.time < nextTime then
nextTime = timer.time
nextTimer = timer
end
end
if nextTimer then
timers.now = nextTimer.time
nextTimer.callback()
timers.scheduled[nextTimer.id] = nil
end
end
end,
-- Reset timers
reset = function()
timers.now = 0
timers.scheduled = {}
end
}
end
return M

View File

@@ -0,0 +1,330 @@
-- Test runner and reporter
-- Executes test suites and generates reports
local M = {}
-- Result types
M.STATUS = {
PASSED = "passed",
FAILED = "failed",
SKIPPED = "skipped",
PENDING = "pending"
}
-- Run a single test
function M.runTest(test, hooks)
local result = {
name = test.name,
status = M.STATUS.PENDING,
error = nil,
duration = 0
}
if test.skipped then
result.status = M.STATUS.SKIPPED
return result
end
local startTime = os.clock()
-- Run beforeEach hook
if hooks.beforeEach then
local success, err = pcall(hooks.beforeEach)
if not success then
result.status = M.STATUS.FAILED
result.error = "beforeEach failed: " .. tostring(err)
result.duration = (os.clock() - startTime) * 1000
return result
end
end
-- Run the test
local success, err = pcall(test.fn)
-- Run afterEach hook
if hooks.afterEach then
pcall(hooks.afterEach)
end
result.duration = (os.clock() - startTime) * 1000
if success then
result.status = M.STATUS.PASSED
else
result.status = M.STATUS.FAILED
if type(err) == "table" and err.type == "AssertionError" then
result.error = err.message
result.expected = err.expected
result.actual = err.actual
else
result.error = tostring(err)
end
end
return result
end
-- Run a test suite
function M.runSuite(suite, config, parentHooks)
local results = {
name = suite.name,
tests = {},
nested = {},
stats = {
total = 0,
passed = 0,
failed = 0,
skipped = 0,
duration = 0
}
}
config = config or {}
parentHooks = parentHooks or {}
local startTime = os.clock()
-- Combine hooks with parent hooks
local hooks = {
beforeEach = suite.beforeEach or parentHooks.beforeEach,
afterEach = suite.afterEach or parentHooks.afterEach
}
-- Run beforeAll hook
if suite.beforeAll then
local success, err = pcall(suite.beforeAll)
if not success then
-- Mark all tests as failed
for _, test in ipairs(suite.tests) do
results.tests[#results.tests + 1] = {
name = test.name,
status = M.STATUS.FAILED,
error = "beforeAll failed: " .. tostring(err),
duration = 0
}
results.stats.failed = results.stats.failed + 1
results.stats.total = results.stats.total + 1
end
results.stats.duration = (os.clock() - startTime) * 1000
return results
end
end
-- Check for .only tests
local hasOnly = false
for _, test in ipairs(suite.tests) do
if test.only then
hasOnly = true
break
end
end
-- Run tests
for _, test in ipairs(suite.tests) do
-- Skip if there's an "only" test and this isn't it
if hasOnly and not test.only then
local skipResult = {
name = test.name,
status = M.STATUS.SKIPPED,
duration = 0
}
results.tests[#results.tests + 1] = skipResult
results.stats.skipped = results.stats.skipped + 1
-- Apply filter if configured
elseif config.filter and not string.find(test.name, config.filter, 1, true) then
local skipResult = {
name = test.name,
status = M.STATUS.SKIPPED,
duration = 0
}
results.tests[#results.tests + 1] = skipResult
results.stats.skipped = results.stats.skipped + 1
else
local testResult = M.runTest(test, hooks)
results.tests[#results.tests + 1] = testResult
if testResult.status == M.STATUS.PASSED then
results.stats.passed = results.stats.passed + 1
elseif testResult.status == M.STATUS.FAILED then
results.stats.failed = results.stats.failed + 1
if config.stopOnFirstFailure then
break
end
elseif testResult.status == M.STATUS.SKIPPED then
results.stats.skipped = results.stats.skipped + 1
end
end
results.stats.total = results.stats.total + 1
end
-- Run nested suites
for _, nestedSuite in ipairs(suite.nested) do
local nestedResults = M.runSuite(nestedSuite, config, hooks)
results.nested[#results.nested + 1] = nestedResults
-- Aggregate stats
results.stats.total = results.stats.total + nestedResults.stats.total
results.stats.passed = results.stats.passed + nestedResults.stats.passed
results.stats.failed = results.stats.failed + nestedResults.stats.failed
results.stats.skipped = results.stats.skipped + nestedResults.stats.skipped
end
-- Run afterAll hook
if suite.afterAll then
pcall(suite.afterAll)
end
results.stats.duration = (os.clock() - startTime) * 1000
return results
end
-- Run all suites
function M.runAll(suites, config)
local allResults = {
suites = {},
stats = {
total = 0,
passed = 0,
failed = 0,
skipped = 0,
duration = 0,
suiteCount = 0
},
timestamp = os.date("%Y-%m-%dT%H:%M:%S")
}
local startTime = os.clock()
for _, suite in ipairs(suites) do
local suiteResults = M.runSuite(suite, config)
allResults.suites[#allResults.suites + 1] = suiteResults
allResults.stats.suiteCount = allResults.stats.suiteCount + 1
-- Aggregate stats
allResults.stats.total = allResults.stats.total + suiteResults.stats.total
allResults.stats.passed = allResults.stats.passed + suiteResults.stats.passed
allResults.stats.failed = allResults.stats.failed + suiteResults.stats.failed
allResults.stats.skipped = allResults.stats.skipped + suiteResults.stats.skipped
end
allResults.stats.duration = (os.clock() - startTime) * 1000
allResults.stats.success = allResults.stats.failed == 0
return allResults
end
-- Format results as text report
function M.formatReport(results, options)
options = options or {}
local indent = options.indent or ""
local verbose = options.verbose ~= false
local lines = {}
local function add(line)
lines[#lines + 1] = line
end
local function formatSuite(suite, depth)
local prefix = string.rep(" ", depth)
add(prefix .. "📦 " .. suite.name)
for _, test in ipairs(suite.tests) do
local icon = ""
if test.status == M.STATUS.PASSED then
icon = ""
elseif test.status == M.STATUS.FAILED then
icon = ""
elseif test.status == M.STATUS.SKIPPED then
icon = "⏭️"
end
local duration = string.format("(%.2fms)", test.duration)
add(prefix .. " " .. icon .. " " .. test.name .. " " .. duration)
if test.status == M.STATUS.FAILED and verbose then
add(prefix .. " Error: " .. (test.error or "Unknown error"))
if test.expected then
add(prefix .. " Expected: " .. tostring(test.expected))
end
if test.actual then
add(prefix .. " Actual: " .. tostring(test.actual))
end
end
end
for _, nested in ipairs(suite.nested) do
formatSuite(nested, depth + 1)
end
end
add("═══════════════════════════════════════")
add(" TEST RESULTS REPORT ")
add("═══════════════════════════════════════")
add("")
for _, suite in ipairs(results.suites) do
formatSuite(suite, 0)
add("")
end
add("───────────────────────────────────────")
add("Summary:")
add(string.format(" Total: %d tests", results.stats.total))
add(string.format(" Passed: %d ✅", results.stats.passed))
add(string.format(" Failed: %d ❌", results.stats.failed))
add(string.format(" Skipped: %d ⏭️", results.stats.skipped))
add(string.format(" Duration: %.2fms", results.stats.duration))
add("")
if results.stats.success then
add("🎉 All tests passed!")
else
add("💥 Some tests failed!")
end
add("═══════════════════════════════════════")
return table.concat(lines, "\n")
end
-- Format results as JSON
function M.formatJSON(results)
-- Simple JSON serialization
local function serialize(value, indent)
indent = indent or 0
local t = type(value)
if t == "nil" then
return "null"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
return tostring(value)
elseif t == "string" then
return '"' .. value:gsub('"', '\\"'):gsub("\n", "\\n") .. '"'
elseif t == "table" then
local parts = {}
local isArray = #value > 0 or next(value) == nil
if isArray then
for _, v in ipairs(value) do
parts[#parts + 1] = serialize(v, indent + 1)
end
return "[" .. table.concat(parts, ",") .. "]"
else
for k, v in pairs(value) do
parts[#parts + 1] = '"' .. tostring(k) .. '":' .. serialize(v, indent + 1)
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
return '"<' .. t .. '>"'
end
return serialize(results)
end
return M

View File

@@ -0,0 +1,21 @@
{
"name": "screenshot_analyzer",
"version": "1.0.0",
"description": "Screenshot Analyzer - Capture and analyze the current page",
"author": "MetaBuilder",
"license": "MIT",
"minLevel": 5,
"category": "demo",
"tags": ["screenshot", "analysis", "browser", "dom"],
"dependencies": [],
"exports": {
"components": ["ScreenshotAnalyzer", "UploadSection", "ResultPanel", "PageInfo"],
"scripts": ["init", "capture", "analyze", "page_info"]
},
"bindings": {
"dbal": false,
"browser": true
},
"requiredHooks": [],
"layout": "layout.json"
}