mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 14:54:55 +00:00
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:
94
frontends/nextjs/src/lib/lua/bindings/bindings-context.ts
Normal file
94
frontends/nextjs/src/lib/lua/bindings/bindings-context.ts
Normal 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
|
||||
185
frontends/nextjs/src/lib/lua/bindings/browser-bindings.ts
Normal file
185
frontends/nextjs/src/lib/lua/bindings/browser-bindings.ts
Normal 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
|
||||
`
|
||||
140
frontends/nextjs/src/lib/lua/bindings/dbal-bindings.ts
Normal file
140
frontends/nextjs/src/lib/lua/bindings/dbal-bindings.ts
Normal 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
|
||||
`
|
||||
14
frontends/nextjs/src/lib/lua/bindings/index.ts
Normal file
14
frontends/nextjs/src/lib/lua/bindings/index.ts
Normal 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'
|
||||
@@ -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'
|
||||
@@ -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,
|
||||
},
|
||||
}))
|
||||
@@ -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 {
|
||||
|
||||
190
packages/dbal_demo/seed/components.json
Normal file
190
packages/dbal_demo/seed/components.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
packages/dbal_demo/seed/metadata.json
Normal file
21
packages/dbal_demo/seed/metadata.json
Normal 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"
|
||||
}
|
||||
104
packages/dbal_demo/seed/scripts/blob_operations.lua
Normal file
104
packages/dbal_demo/seed/scripts/blob_operations.lua
Normal 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
|
||||
92
packages/dbal_demo/seed/scripts/cache_operations.lua
Normal file
92
packages/dbal_demo/seed/scripts/cache_operations.lua
Normal 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
|
||||
75
packages/dbal_demo/seed/scripts/connection.lua
Normal file
75
packages/dbal_demo/seed/scripts/connection.lua
Normal 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
|
||||
26
packages/dbal_demo/seed/scripts/init.lua
Normal file
26
packages/dbal_demo/seed/scripts/init.lua
Normal 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
|
||||
90
packages/dbal_demo/seed/scripts/kv_operations.lua
Normal file
90
packages/dbal_demo/seed/scripts/kv_operations.lua
Normal 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
|
||||
34
packages/dbal_demo/seed/scripts/manifest.json
Normal file
34
packages/dbal_demo/seed/scripts/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
packages/lua_test/seed/metadata.json
Normal file
15
packages/lua_test/seed/metadata.json
Normal 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
|
||||
}
|
||||
360
packages/lua_test/seed/scripts/assertions.lua
Normal file
360
packages/lua_test/seed/scripts/assertions.lua
Normal 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
|
||||
145
packages/lua_test/seed/scripts/framework.lua
Normal file
145
packages/lua_test/seed/scripts/framework.lua
Normal 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
|
||||
267
packages/lua_test/seed/scripts/helpers.lua
Normal file
267
packages/lua_test/seed/scripts/helpers.lua
Normal 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
|
||||
34
packages/lua_test/seed/scripts/init.lua
Normal file
34
packages/lua_test/seed/scripts/init.lua
Normal 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
|
||||
10
packages/lua_test/seed/scripts/manifest.json
Normal file
10
packages/lua_test/seed/scripts/manifest.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
248
packages/lua_test/seed/scripts/mocks.lua
Normal file
248
packages/lua_test/seed/scripts/mocks.lua
Normal 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
|
||||
330
packages/lua_test/seed/scripts/runner.lua
Normal file
330
packages/lua_test/seed/scripts/runner.lua
Normal 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
|
||||
21
packages/screenshot_analyzer/seed/metadata.json
Normal file
21
packages/screenshot_analyzer/seed/metadata.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user