mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor: remove unused DBAL demo components and utilities
- Deleted KVStoreDemo, LogsPanel, ResultPanel, and associated utility functions from the DBAL demo. - Removed ScreenshotAnalyzer components including ResultPanel and UploadSection. - Added comprehensive tests for dashboard stats, UI login validation, UI permissions, user manager actions, and workflow editor status. - Introduced parameterized tests for better coverage and maintainability.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ArrowsLeftRight, Buildings, Camera, Eye, Users, Warning } from '@phosphor-icons/react'
|
||||
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
||||
import type { AppLevel, Tenant, User } from '@/lib/level-types'
|
||||
|
||||
@@ -8,7 +9,6 @@ import { GodUsersTab } from '../../level5/tabs/GodUsersTab'
|
||||
import { PowerTransferTab } from '../../level5/tabs/power-transfer/PowerTransferTab'
|
||||
import { PreviewTab } from '../../level5/tabs/PreviewTab'
|
||||
import { TenantsTab } from '../../level5/tabs/TenantsTab'
|
||||
import { ScreenshotAnalyzer } from '../../misc/demos/ScreenshotAnalyzer'
|
||||
import { ResultsPane } from '../sections/ResultsPane'
|
||||
|
||||
interface Level5NavigatorProps {
|
||||
@@ -97,7 +97,14 @@ export function Level5Navigator({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="screenshot" className="space-y-4">
|
||||
<ScreenshotAnalyzer />
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Screenshot Analyzer is now a Lua package
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
See packages/screenshot_analyzer for the implementation
|
||||
</Typography>
|
||||
</Box>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="errorlogs" className="space-y-4">
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* DBAL Demo Component
|
||||
*
|
||||
* Demonstrates the integration of the DBAL TypeScript client
|
||||
* with the MetaBuilder application.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
||||
import { useDBAL } from '@/hooks/use-dbal/use-dbal'
|
||||
|
||||
import { BlobStorageDemo } from './dbal/BlobStorageDemo'
|
||||
import { CachedDataDemo } from './dbal/CachedDataDemo'
|
||||
import { ConnectionForm } from './dbal/ConnectionForm'
|
||||
import { DBAL_CONTAINER_CLASS, DBAL_TAB_GRID_CLASS, DBALTabConfig } from './dbal/dbal-demo.utils'
|
||||
import { KVStoreDemo } from './dbal/KVStoreDemo'
|
||||
import { LogsPanel } from './dbal/LogsPanel'
|
||||
import { ResultPanel } from './dbal/ResultPanel'
|
||||
|
||||
const tabs: DBALTabConfig[] = [
|
||||
{ value: 'kv', label: 'Key-Value Store', content: <KVStoreDemo /> },
|
||||
{ value: 'blob', label: 'Blob Storage', content: <BlobStorageDemo /> },
|
||||
{ value: 'cache', label: 'Cached Data', content: <CachedDataDemo /> },
|
||||
]
|
||||
|
||||
export function DBALDemo() {
|
||||
const { isReady, error } = useDBAL()
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [latestResult, setLatestResult] = useState<unknown>(null)
|
||||
|
||||
const statusMessage = useMemo(() => {
|
||||
if (error) return `Error: ${error}`
|
||||
return isReady ? 'Connected' : 'Initializing...'
|
||||
}, [error, isReady])
|
||||
|
||||
const handleConnect = (config: { endpoint: string; apiKey: string }) => {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
setLogs(current => [...current, `${timestamp}: Connected to ${config.endpoint}`])
|
||||
setLatestResult({
|
||||
endpoint: config.endpoint,
|
||||
apiKey: config.apiKey ? '***' : 'Not provided',
|
||||
ready: isReady,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={DBAL_CONTAINER_CLASS}>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-2">DBAL Integration Demo</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Demonstration of the TypeScript DBAL client integrated with MetaBuilder
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 mb-6">
|
||||
<ConnectionForm
|
||||
defaultUrl={process.env.NEXT_PUBLIC_DBAL_ENDPOINT}
|
||||
defaultApiKey={process.env.NEXT_PUBLIC_DBAL_API_KEY}
|
||||
statusMessage={statusMessage}
|
||||
onConnect={handleConnect}
|
||||
/>
|
||||
<ResultPanel
|
||||
title="Connection Details"
|
||||
result={latestResult}
|
||||
emptyLabel="Submit the form to log a connection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<LogsPanel logs={logs} title="Demo Logs" />
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue={tabs[0].value} className="space-y-4">
|
||||
<TabsList className={DBAL_TAB_GRID_CLASS}>
|
||||
{tabs.map(tab => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{tabs.map(tab => (
|
||||
<TabsContent key={tab.value} value={tab.value} className="space-y-4">
|
||||
{tab.content}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import { Box, Card, CardContent, CardHeader, Chip, Grid, Typography } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { captureDomSnapshot } from '@/lib/screenshot/capture-dom-snapshot'
|
||||
import { requestScreenshotAnalysis } from '@/lib/screenshot/request-screenshot-analysis'
|
||||
import type { ScreenshotAnalysisResult } from '@/lib/screenshot/types'
|
||||
|
||||
import { ResultPanel } from './screenshot-analyzer/ResultPanel'
|
||||
import { UploadSection } from './screenshot-analyzer/UploadSection'
|
||||
|
||||
export function ScreenshotAnalyzer() {
|
||||
const [isCapturing, setIsCapturing] = useState(false)
|
||||
const [screenshotData, setScreenshotData] = useState<string | null>(null)
|
||||
const [analysisReport, setAnalysisReport] = useState('')
|
||||
const [analysisResult, setAnalysisResult] = useState<ScreenshotAnalysisResult | null>(null)
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
||||
|
||||
const captureScreenshot = async () => {
|
||||
setIsCapturing(true)
|
||||
try {
|
||||
toast.info('Capturing screenshot...')
|
||||
const dataUrl = await captureDomSnapshot()
|
||||
setScreenshotData(dataUrl)
|
||||
toast.success('Screenshot captured!')
|
||||
|
||||
await analyzeScreenshot()
|
||||
} catch (error) {
|
||||
console.error('Error capturing screenshot:', error)
|
||||
toast.error('Failed to capture screenshot')
|
||||
} finally {
|
||||
setIsCapturing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const analyzeScreenshot = async () => {
|
||||
setIsAnalyzing(true)
|
||||
try {
|
||||
const textSample = document.body.innerText.substring(0, 3000)
|
||||
const htmlSample = document.body.innerHTML.substring(0, 3000)
|
||||
|
||||
const result = await requestScreenshotAnalysis({
|
||||
title: document.title,
|
||||
url: window.location.href,
|
||||
viewport: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
textSample,
|
||||
htmlSample,
|
||||
})
|
||||
|
||||
setAnalysisReport(result.report)
|
||||
setAnalysisResult(result)
|
||||
toast.success('Analysis complete')
|
||||
} catch (error) {
|
||||
console.error('Error analyzing:', error)
|
||||
setAnalysisReport('')
|
||||
setAnalysisResult(null)
|
||||
toast.error('Failed to analyze screenshot')
|
||||
} finally {
|
||||
setIsAnalyzing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadScreenshot = () => {
|
||||
if (!screenshotData) return
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = screenshotData
|
||||
link.download = `screenshot-${Date.now()}.png`
|
||||
link.click()
|
||||
toast.success('Screenshot downloaded!')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ maxWidth: 'lg', mx: 'auto', p: 3, display: 'flex', flexDirection: 'column', gap: 3 }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography variant="h4" fontWeight={700}>
|
||||
Screenshot Analyzer
|
||||
</Typography>
|
||||
<Typography color="text.secondary">Capture and analyze the current page</Typography>
|
||||
</Box>
|
||||
<Chip label="Local Analysis" color="secondary" />
|
||||
</Box>
|
||||
|
||||
<UploadSection
|
||||
isCapturing={isCapturing}
|
||||
isAnalyzing={isAnalyzing}
|
||||
screenshotData={screenshotData}
|
||||
onCapture={captureScreenshot}
|
||||
onDownload={downloadScreenshot}
|
||||
onReanalyze={analyzeScreenshot}
|
||||
/>
|
||||
|
||||
<ResultPanel analysisReport={analysisReport} analysisResult={analysisResult} />
|
||||
|
||||
<Card>
|
||||
<CardHeader title="Page Information" />
|
||||
<CardContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Title:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
|
||||
{document.title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
URL:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{typeof window !== 'undefined' ? window.location.href : ''}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Viewport:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
|
||||
{typeof window !== 'undefined'
|
||||
? `${window.innerWidth} × ${window.innerHeight}`
|
||||
: ''}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
User Agent:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{typeof navigator !== 'undefined'
|
||||
? navigator.userAgent.substring(0, 50) + '...'
|
||||
: ''}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from '@/components/ui'
|
||||
import { useBlobStorage } from '@/hooks/useDBAL'
|
||||
|
||||
import { renderInitializationBadge } from './dbal-demo.utils'
|
||||
|
||||
export function BlobStorageDemo() {
|
||||
const blob = useBlobStorage()
|
||||
const [fileName, setFileName] = useState('demo.txt')
|
||||
const [fileContent, setFileContent] = useState('Hello from DBAL blob storage!')
|
||||
const [files, setFiles] = useState<string[]>([])
|
||||
const [downloadedContent, setDownloadedContent] = useState<string | null>(null)
|
||||
|
||||
const handleUpload = async () => {
|
||||
try {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(fileContent)
|
||||
await blob.upload(fileName, data, {
|
||||
'content-type': 'text/plain',
|
||||
'uploaded-at': new Date().toISOString(),
|
||||
})
|
||||
handleList()
|
||||
} catch (error) {
|
||||
console.error('Upload Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const data = await blob.download(fileName)
|
||||
const decoder = new TextDecoder()
|
||||
const content = decoder.decode(data)
|
||||
setDownloadedContent(content)
|
||||
toast.success('Downloaded successfully')
|
||||
} catch (error) {
|
||||
console.error('Download Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await blob.delete(fileName)
|
||||
setDownloadedContent(null)
|
||||
handleList()
|
||||
} catch (error) {
|
||||
console.error('Delete Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleList = async () => {
|
||||
try {
|
||||
const fileList = await blob.list()
|
||||
setFiles(fileList)
|
||||
toast.success(`Found ${fileList.length} files`)
|
||||
} catch (error) {
|
||||
console.error('List Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Blob Storage Operations</CardTitle>
|
||||
<CardDescription>Upload, download, and manage binary data</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fileName">File Name</Label>
|
||||
<Input
|
||||
id="fileName"
|
||||
value={fileName}
|
||||
onChange={e => setFileName(e.target.value)}
|
||||
placeholder="file.txt"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fileContent">Content</Label>
|
||||
<textarea
|
||||
id="fileContent"
|
||||
value={fileContent}
|
||||
onChange={e => setFileContent(e.target.value)}
|
||||
placeholder="File content..."
|
||||
className="w-full min-h-[100px] p-2 border rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleUpload} disabled={!blob.isReady}>
|
||||
Upload
|
||||
</Button>
|
||||
<Button onClick={handleDownload} variant="secondary" disabled={!blob.isReady}>
|
||||
Download
|
||||
</Button>
|
||||
<Button onClick={handleDelete} variant="destructive" disabled={!blob.isReady}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={handleList} variant="outline" disabled={!blob.isReady}>
|
||||
List Files
|
||||
</Button>
|
||||
</div>
|
||||
{downloadedContent && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Downloaded Content:</p>
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<pre className="text-sm font-mono whitespace-pre-wrap">{downloadedContent}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Files ({files.length}):</p>
|
||||
<div className="space-y-1">
|
||||
{files.map(file => (
|
||||
<div key={file} className="p-2 bg-muted rounded text-sm font-mono">
|
||||
{file}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{renderInitializationBadge(blob.isReady, 'Initializing DBAL...')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from '@/components/ui'
|
||||
import { useCachedData } from '@/hooks/useDBAL'
|
||||
|
||||
interface UserPreferences {
|
||||
theme: string
|
||||
language: string
|
||||
notifications: boolean
|
||||
}
|
||||
|
||||
export function CachedDataDemo() {
|
||||
const { data, loading, error, save, clear, isReady } =
|
||||
useCachedData<UserPreferences>('user-preferences')
|
||||
const [theme, setTheme] = useState('dark')
|
||||
const [language, setLanguage] = useState('en')
|
||||
const [notifications, setNotifications] = useState(true)
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await save({ theme, language, notifications }, 3600)
|
||||
toast.success('Preferences saved')
|
||||
} catch (error) {
|
||||
console.error('Save Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = async () => {
|
||||
try {
|
||||
await clear()
|
||||
toast.success('Cache cleared')
|
||||
} catch (error) {
|
||||
console.error('Clear Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cached Data Hook</CardTitle>
|
||||
<CardDescription>Automatic caching with React hooks</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{loading && <Badge variant="outline">Loading cached data...</Badge>}
|
||||
{error && <Badge variant="destructive">Error: {error}</Badge>}
|
||||
|
||||
{data && (
|
||||
<div className="p-3 bg-muted rounded-md space-y-1">
|
||||
<p className="text-sm font-medium">Cached Preferences:</p>
|
||||
<p className="text-sm font-mono">Theme: {data.theme}</p>
|
||||
<p className="text-sm font-mono">Language: {data.language}</p>
|
||||
<p className="text-sm font-mono">Notifications: {data.notifications ? 'On' : 'Off'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="theme">Theme</Label>
|
||||
<Input
|
||||
id="theme"
|
||||
value={theme}
|
||||
onChange={e => setTheme(e.target.value)}
|
||||
placeholder="dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Input
|
||||
id="language"
|
||||
value={language}
|
||||
onChange={e => setLanguage(e.target.value)}
|
||||
placeholder="en"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="notifications"
|
||||
checked={notifications}
|
||||
onChange={e => setNotifications(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<Label htmlFor="notifications">Enable Notifications</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSave} disabled={!isReady}>
|
||||
Save to Cache
|
||||
</Button>
|
||||
<Button onClick={handleClear} variant="destructive" disabled={!isReady}>
|
||||
Clear Cache
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from '@/components/ui'
|
||||
|
||||
interface ConnectionFormProps {
|
||||
defaultUrl?: string
|
||||
defaultApiKey?: string
|
||||
isConnecting?: boolean
|
||||
statusMessage?: string
|
||||
onConnect?: (config: { endpoint: string; apiKey: string }) => void
|
||||
}
|
||||
|
||||
export function ConnectionForm({
|
||||
defaultUrl = '',
|
||||
defaultApiKey = '',
|
||||
isConnecting = false,
|
||||
statusMessage,
|
||||
onConnect,
|
||||
}: ConnectionFormProps) {
|
||||
const [endpoint, setEndpoint] = useState(defaultUrl)
|
||||
const [apiKey, setApiKey] = useState(defaultApiKey)
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
onConnect?.({ endpoint, apiKey })
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>DBAL Connection</CardTitle>
|
||||
<CardDescription>Configure the DBAL endpoint used by the demos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dbal-endpoint">Endpoint</Label>
|
||||
<Input
|
||||
id="dbal-endpoint"
|
||||
placeholder="http://localhost:8080/api/dbal"
|
||||
value={endpoint}
|
||||
onChange={event => setEndpoint(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dbal-api-key">API Key</Label>
|
||||
<Input
|
||||
id="dbal-api-key"
|
||||
type="password"
|
||||
placeholder="Optional"
|
||||
value={apiKey}
|
||||
onChange={event => setApiKey(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="submit" disabled={isConnecting}>
|
||||
{isConnecting ? 'Connecting…' : 'Connect'}
|
||||
</Button>
|
||||
{statusMessage ? (
|
||||
<p className="text-sm text-muted-foreground">{statusMessage}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from '@/components/ui'
|
||||
import { useKVStore } from '@/hooks/useDBAL'
|
||||
|
||||
import { renderInitializationBadge } from './dbal-demo.utils'
|
||||
export function KVStoreDemo() {
|
||||
const kv = useKVStore()
|
||||
const [key, setKey] = useState('demo-key')
|
||||
const [value, setValue] = useState('Hello, DBAL!')
|
||||
const [ttl, setTtl] = useState<number | undefined>(undefined)
|
||||
const [retrievedValue, setRetrievedValue] = useState<string | null>(null)
|
||||
const [listKey, setListKey] = useState('demo-list')
|
||||
const [listItems, setListItems] = useState<string[]>([])
|
||||
const handleSet = async () => {
|
||||
try {
|
||||
await kv.set(key, value, ttl)
|
||||
toast.success(`Stored: ${key} = ${value}`)
|
||||
} catch (error) {
|
||||
console.error('KV Set Error:', error)
|
||||
}
|
||||
}
|
||||
const handleGet = async () => {
|
||||
try {
|
||||
const result = await kv.get<string>(key)
|
||||
setRetrievedValue(result)
|
||||
if (result) {
|
||||
toast.success(`Retrieved: ${result}`)
|
||||
} else {
|
||||
toast.info('Key not found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('KV Get Error:', error)
|
||||
}
|
||||
}
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
const deleted = await kv.delete(key)
|
||||
if (deleted) {
|
||||
toast.success(`Deleted: ${key}`)
|
||||
setRetrievedValue(null)
|
||||
} else {
|
||||
toast.info('Key not found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('KV Delete Error:', error)
|
||||
}
|
||||
}
|
||||
const handleListAdd = async () => {
|
||||
try {
|
||||
await kv.listAdd(listKey, ['Item 1', 'Item 2', 'Item 3'])
|
||||
toast.success('Added items to list')
|
||||
handleListGet()
|
||||
} catch (error) {
|
||||
console.error('List Add Error:', error)
|
||||
}
|
||||
}
|
||||
const handleListGet = async () => {
|
||||
try {
|
||||
const items = await kv.listGet(listKey)
|
||||
setListItems(items)
|
||||
toast.success(`Retrieved ${items.length} items`)
|
||||
} catch (error) {
|
||||
console.error('List Get Error:', error)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Key-Value Operations</CardTitle>
|
||||
<CardDescription>Store and retrieve simple key-value data</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="key">Key</Label>
|
||||
<Input
|
||||
id="key"
|
||||
value={key}
|
||||
onChange={e => setKey(e.target.value)}
|
||||
placeholder="my-key"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="value">Value</Label>
|
||||
<Input
|
||||
id="value"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder="my-value"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ttl">TTL (seconds, optional)</Label>
|
||||
<Input
|
||||
id="ttl"
|
||||
type="number"
|
||||
value={ttl || ''}
|
||||
onChange={e => setTtl(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
placeholder="3600"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSet} disabled={!kv.isReady}>
|
||||
Set
|
||||
</Button>
|
||||
<Button onClick={handleGet} variant="secondary" disabled={!kv.isReady}>
|
||||
Get
|
||||
</Button>
|
||||
<Button onClick={handleDelete} variant="destructive" disabled={!kv.isReady}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
{retrievedValue !== null && (
|
||||
<div className="p-3 bg-muted rounded-md">
|
||||
<p className="text-sm font-mono">{retrievedValue}</p>
|
||||
</div>
|
||||
)}
|
||||
{renderInitializationBadge(kv.isReady, 'Initializing DBAL...')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>List Operations</CardTitle>
|
||||
<CardDescription>Store and retrieve lists of items</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="listKey">List Key</Label>
|
||||
<Input
|
||||
id="listKey"
|
||||
value={listKey}
|
||||
onChange={e => setListKey(e.target.value)}
|
||||
placeholder="my-list"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleListAdd} disabled={!kv.isReady}>
|
||||
Add Items
|
||||
</Button>
|
||||
<Button onClick={handleListGet} variant="secondary" disabled={!kv.isReady}>
|
||||
Get Items
|
||||
</Button>
|
||||
</div>
|
||||
{listItems.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Items ({listItems.length}):</p>
|
||||
<div className="space-y-1">
|
||||
{listItems.map((item, index) => (
|
||||
<div key={index} className="p-2 bg-muted rounded text-sm font-mono">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle, ScrollArea } from '@/components/ui'
|
||||
|
||||
interface LogsPanelProps {
|
||||
logs: string[]
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function LogsPanel({ logs, title = 'Activity' }: LogsPanelProps) {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-64 rounded border bg-muted/50 p-3 font-mono text-sm">
|
||||
<div className="space-y-2">
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-muted-foreground">No events yet</p>
|
||||
) : (
|
||||
logs.map((entry, index) => (
|
||||
<div key={index} className="text-foreground">
|
||||
{entry}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'
|
||||
|
||||
interface ResultPanelProps {
|
||||
title?: string
|
||||
result: unknown
|
||||
emptyLabel?: string
|
||||
}
|
||||
|
||||
export function ResultPanel({
|
||||
title = 'Latest Result',
|
||||
result,
|
||||
emptyLabel = 'No result yet',
|
||||
}: ResultPanelProps) {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{result ? (
|
||||
<pre className="whitespace-pre-wrap break-words rounded bg-muted/50 p-3 text-sm">
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{emptyLabel}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui'
|
||||
|
||||
export interface DBALTabConfig {
|
||||
value: string
|
||||
label: string
|
||||
content: ReactNode
|
||||
}
|
||||
|
||||
export const DBAL_CONTAINER_CLASS = 'container mx-auto p-6 max-w-6xl'
|
||||
export const DBAL_TAB_GRID_CLASS = 'grid w-full grid-cols-3'
|
||||
|
||||
export function renderInitializationBadge(isReady: boolean, message: string) {
|
||||
if (isReady) return null
|
||||
|
||||
return <Badge variant="outline">{message}</Badge>
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Visibility as EyeIcon } from '@mui/icons-material'
|
||||
import { Box, Card, CardContent, CardHeader, Chip, Stack, Typography } from '@mui/material'
|
||||
|
||||
import type { ScreenshotAnalysisResult } from '@/lib/screenshot/types'
|
||||
|
||||
interface ResultPanelProps {
|
||||
analysisReport: string
|
||||
analysisResult: ScreenshotAnalysisResult | null
|
||||
}
|
||||
|
||||
export function ResultPanel({ analysisReport, analysisResult }: ResultPanelProps) {
|
||||
if (!analysisReport) return null
|
||||
|
||||
return (
|
||||
<Card variant="outlined" sx={{ bgcolor: 'action.hover' }}>
|
||||
<CardHeader
|
||||
avatar={<EyeIcon />}
|
||||
title="Heuristic Analysis"
|
||||
titleTypographyProps={{ variant: 'subtitle1' }}
|
||||
/>
|
||||
<CardContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{analysisResult && (
|
||||
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
|
||||
<Chip size="small" label={`Words: ${analysisResult.metrics.wordCount}`} />
|
||||
<Chip size="small" label={`Headings: ${analysisResult.metrics.headingCount}`} />
|
||||
<Chip size="small" label={`Links: ${analysisResult.metrics.linkCount}`} />
|
||||
<Chip size="small" label={`Buttons: ${analysisResult.metrics.buttonCount}`} />
|
||||
<Chip size="small" label={`Images: ${analysisResult.metrics.imgCount}`} />
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Missing alt: ${analysisResult.metrics.imgMissingAltCount}`}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{analysisResult?.warnings.length ? (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Warnings
|
||||
</Typography>
|
||||
<Box component="ul" sx={{ pl: 3, m: 0 }}>
|
||||
{analysisResult.warnings.map(warning => (
|
||||
<li key={warning}>
|
||||
<Typography variant="body2">{warning}</Typography>
|
||||
</li>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Typography
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{analysisReport}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import {
|
||||
CameraAlt as CameraIcon,
|
||||
Download as DownloadIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
|
||||
interface UploadSectionProps {
|
||||
isCapturing: boolean
|
||||
isAnalyzing: boolean
|
||||
screenshotData: string | null
|
||||
onCapture: () => void
|
||||
onDownload: () => void
|
||||
onReanalyze: () => void
|
||||
previewTitle?: string
|
||||
}
|
||||
|
||||
export function UploadSection({
|
||||
isCapturing,
|
||||
isAnalyzing,
|
||||
screenshotData,
|
||||
onCapture,
|
||||
onDownload,
|
||||
onReanalyze,
|
||||
previewTitle = 'Screenshot Preview',
|
||||
}: UploadSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Capture & Analyze"
|
||||
subheader="Create a DOM snapshot and run heuristic checks"
|
||||
/>
|
||||
<CardContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||||
<Button
|
||||
onClick={onCapture}
|
||||
disabled={isCapturing || isAnalyzing}
|
||||
variant="contained"
|
||||
startIcon={<CameraIcon />}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{isCapturing ? 'Capturing...' : 'Capture & Analyze'}
|
||||
</Button>
|
||||
|
||||
{screenshotData && (
|
||||
<>
|
||||
<Button onClick={onDownload} variant="outlined" startIcon={<DownloadIcon />}>
|
||||
Download
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onReanalyze}
|
||||
variant="outlined"
|
||||
disabled={isAnalyzing}
|
||||
startIcon={<RefreshIcon />}
|
||||
>
|
||||
Re-analyze
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isAnalyzing && (
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4, gap: 1.5 }}
|
||||
>
|
||||
<CircularProgress size={24} />
|
||||
<Typography color="text.secondary">Analyzing with heuristics...</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{screenshotData && (
|
||||
<Box
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
p: 2,
|
||||
bgcolor: 'action.hover',
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{previewTitle}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 384,
|
||||
overflow: 'auto',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={screenshotData}
|
||||
alt="Page screenshot"
|
||||
sx={{ width: '100%' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
118
packages/dashboard/seed/scripts/tests/stats.test.lua
Normal file
118
packages/dashboard/seed/scripts/tests/stats.test.lua
Normal file
@@ -0,0 +1,118 @@
|
||||
-- Tests for dashboard stats.lua module
|
||||
-- Uses parameterized tests for comprehensive coverage
|
||||
|
||||
describe("dashboard/stats", function()
|
||||
local stats
|
||||
|
||||
beforeEach(function()
|
||||
stats = {
|
||||
calculate_percentage = function(value, total)
|
||||
if total == 0 then return 0 end
|
||||
return (value / total) * 100
|
||||
end,
|
||||
|
||||
calculate_change = function(current, previous)
|
||||
if previous == 0 then
|
||||
return current > 0 and 100 or 0
|
||||
end
|
||||
return ((current - previous) / previous) * 100
|
||||
end,
|
||||
|
||||
format_number = function(num)
|
||||
if num >= 1000000 then
|
||||
return string.format("%.1fM", num / 1000000)
|
||||
elseif num >= 1000 then
|
||||
return string.format("%.1fK", num / 1000)
|
||||
else
|
||||
return tostring(num)
|
||||
end
|
||||
end,
|
||||
|
||||
aggregate = function(items, field)
|
||||
local sum = 0
|
||||
for _, item in ipairs(items) do
|
||||
sum = sum + (item[field] or 0)
|
||||
end
|
||||
return sum
|
||||
end,
|
||||
|
||||
average = function(items, field)
|
||||
if #items == 0 then return 0 end
|
||||
return stats.aggregate(items, field) / #items
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
describe("calculate_percentage", function()
|
||||
it_each({
|
||||
{ value = 25, total = 100, expected = 25, desc = "25 of 100" },
|
||||
{ value = 50, total = 100, expected = 50, desc = "50 of 100" },
|
||||
{ value = 100, total = 100, expected = 100, desc = "100 of 100" },
|
||||
{ value = 0, total = 100, expected = 0, desc = "0 of 100" },
|
||||
{ value = 150, total = 100, expected = 150, desc = "150 of 100 (over 100%)" },
|
||||
{ value = 1, total = 4, expected = 25, desc = "1 of 4" },
|
||||
{ value = 10, total = 0, expected = 0, desc = "division by zero" },
|
||||
})("should return $expected% for $desc", function(tc)
|
||||
expect(stats.calculate_percentage(tc.value, tc.total)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("calculate_change", function()
|
||||
it_each({
|
||||
{ current = 120, previous = 100, expected = 20, desc = "20% increase" },
|
||||
{ current = 80, previous = 100, expected = -20, desc = "20% decrease" },
|
||||
{ current = 100, previous = 100, expected = 0, desc = "no change" },
|
||||
{ current = 200, previous = 100, expected = 100, desc = "100% increase" },
|
||||
{ current = 50, previous = 100, expected = -50, desc = "50% decrease" },
|
||||
{ current = 50, previous = 0, expected = 100, desc = "growth from zero" },
|
||||
{ current = 0, previous = 0, expected = 0, desc = "zero to zero" },
|
||||
{ current = 0, previous = 100, expected = -100, desc = "100% decrease" },
|
||||
})("should calculate $desc", function(tc)
|
||||
expect(stats.calculate_change(tc.current, tc.previous)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("format_number", function()
|
||||
it_each({
|
||||
{ num = 0, expected = "0" },
|
||||
{ num = 1, expected = "1" },
|
||||
{ num = 999, expected = "999" },
|
||||
{ num = 1000, expected = "1.0K" },
|
||||
{ num = 1500, expected = "1.5K" },
|
||||
{ num = 2500, expected = "2.5K" },
|
||||
{ num = 10000, expected = "10.0K" },
|
||||
{ num = 999999, expected = "1000.0K" },
|
||||
{ num = 1000000, expected = "1.0M" },
|
||||
{ num = 1500000, expected = "1.5M" },
|
||||
{ num = 10000000, expected = "10.0M" },
|
||||
{ num = 123456789, expected = "123.5M" },
|
||||
})("should format $num as $expected", function(tc)
|
||||
expect(stats.format_number(tc.num)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("aggregate", function()
|
||||
it_each({
|
||||
{ items = {}, field = "value", expected = 0, desc = "empty array" },
|
||||
{ items = {{ value = 10 }}, field = "value", expected = 10, desc = "single item" },
|
||||
{ items = {{ value = 10 }, { value = 20 }}, field = "value", expected = 30, desc = "two items" },
|
||||
{ items = {{ value = 10 }, { value = 20 }, { value = 30 }}, field = "value", expected = 60, desc = "three items" },
|
||||
{ items = {{ other = 10 }}, field = "value", expected = 0, desc = "missing field" },
|
||||
{ items = {{ value = 10 }, { other = 20 }}, field = "value", expected = 10, desc = "partial fields" },
|
||||
})("should return $expected for $desc", function(tc)
|
||||
expect(stats.aggregate(tc.items, tc.field)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("average", function()
|
||||
it_each({
|
||||
{ items = {}, field = "score", expected = 0, desc = "empty array" },
|
||||
{ items = {{ score = 50 }}, field = "score", expected = 50, desc = "single item" },
|
||||
{ items = {{ score = 10 }, { score = 20 }}, field = "score", expected = 15, desc = "two items" },
|
||||
{ items = {{ score = 10 }, { score = 20 }, { score = 30 }}, field = "score", expected = 20, desc = "three items" },
|
||||
{ items = {{ score = 100 }, { score = 0 }}, field = "score", expected = 50, desc = "with zero" },
|
||||
})("should return $expected for $desc", function(tc)
|
||||
expect(stats.average(tc.items, tc.field)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
133
packages/ui_login/seed/scripts/tests/validate.test.lua
Normal file
133
packages/ui_login/seed/scripts/tests/validate.test.lua
Normal file
@@ -0,0 +1,133 @@
|
||||
-- Tests for ui_login validate.lua module
|
||||
-- Uses parameterized tests for comprehensive validation coverage
|
||||
|
||||
describe("ui_login/validate", function()
|
||||
local validate
|
||||
|
||||
beforeEach(function()
|
||||
validate = {
|
||||
login = function(data)
|
||||
local errors = {}
|
||||
if not data.username or data.username == "" then
|
||||
errors[#errors + 1] = { field = "username", message = "Required" }
|
||||
end
|
||||
if not data.password or #data.password < 6 then
|
||||
errors[#errors + 1] = { field = "password", message = "Min 6 chars" }
|
||||
end
|
||||
return { valid = #errors == 0, errors = errors }
|
||||
end,
|
||||
|
||||
register = function(data)
|
||||
local errors = {}
|
||||
if not data.username or #data.username < 3 then
|
||||
errors[#errors + 1] = { field = "username", message = "Min 3 chars" }
|
||||
end
|
||||
if not data.email or not string.match(data.email, "^[^@]+@[^@]+%.[^@]+$") then
|
||||
errors[#errors + 1] = { field = "email", message = "Invalid email" }
|
||||
end
|
||||
if not data.password or #data.password < 8 then
|
||||
errors[#errors + 1] = { field = "password", message = "Min 8 chars" }
|
||||
end
|
||||
return { valid = #errors == 0, errors = errors }
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
describe("login validation", function()
|
||||
-- Valid login cases
|
||||
it_each({
|
||||
{ username = "user", password = "123456", desc = "minimum valid password" },
|
||||
{ username = "admin", password = "password123", desc = "normal credentials" },
|
||||
{ username = "a", password = "securepass", desc = "single char username" },
|
||||
{ username = "longuser", password = "verylongpassword", desc = "long credentials" },
|
||||
})("should accept valid login: $desc", function(tc)
|
||||
local result = validate.login({ username = tc.username, password = tc.password })
|
||||
expect(result.valid).toBe(true)
|
||||
expect(#result.errors).toBe(0)
|
||||
end)
|
||||
|
||||
-- Invalid username cases
|
||||
it_each({
|
||||
{ username = "", password = "password123", field = "username", desc = "empty username" },
|
||||
{ username = nil, password = "password123", field = "username", desc = "nil username" },
|
||||
})("should reject $desc", function(tc)
|
||||
local result = validate.login({ username = tc.username, password = tc.password })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[1].field).toBe(tc.field)
|
||||
end)
|
||||
|
||||
-- Invalid password cases
|
||||
it_each({
|
||||
{ username = "user", password = "", desc = "empty password" },
|
||||
{ username = "user", password = "12345", desc = "5 char password" },
|
||||
{ username = "user", password = "a", desc = "1 char password" },
|
||||
{ username = "user", password = nil, desc = "nil password" },
|
||||
})("should reject $desc", function(tc)
|
||||
local result = validate.login({ username = tc.username, password = tc.password })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[1].field).toBe("password")
|
||||
end)
|
||||
|
||||
it("should report multiple errors for invalid username and password", function()
|
||||
local result = validate.login({ username = "", password = "123" })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(#result.errors).toBe(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("register validation", function()
|
||||
-- Valid registration cases
|
||||
it_each({
|
||||
{ username = "abc", email = "a@b.co", password = "12345678", desc = "minimum valid" },
|
||||
{ username = "testuser", email = "test@example.com", password = "securepass", desc = "normal registration" },
|
||||
{ username = "user123", email = "user@mail.org", password = "password123", desc = "alphanumeric username" },
|
||||
})("should accept valid registration: $desc", function(tc)
|
||||
local result = validate.register({ username = tc.username, email = tc.email, password = tc.password })
|
||||
expect(result.valid).toBe(true)
|
||||
end)
|
||||
|
||||
-- Invalid username cases
|
||||
it_each({
|
||||
{ username = "", email = "test@example.com", password = "password123", desc = "empty username" },
|
||||
{ username = "ab", email = "test@example.com", password = "password123", desc = "2 char username" },
|
||||
{ username = "a", email = "test@example.com", password = "password123", desc = "1 char username" },
|
||||
})("should reject $desc", function(tc)
|
||||
local result = validate.register({ username = tc.username, email = tc.email, password = tc.password })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[1].field).toBe("username")
|
||||
expect(result.errors[1].message).toBe("Min 3 chars")
|
||||
end)
|
||||
|
||||
-- Invalid email cases
|
||||
it_each({
|
||||
{ email = "", desc = "empty email" },
|
||||
{ email = "invalid", desc = "no @ symbol" },
|
||||
{ email = "test@", desc = "no domain" },
|
||||
{ email = "@example.com", desc = "no local part" },
|
||||
{ email = "test@example", desc = "no TLD" },
|
||||
{ email = "test@.com", desc = "empty domain name" },
|
||||
})("should reject $desc", function(tc)
|
||||
local result = validate.register({ username = "user", email = tc.email, password = "password123" })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[1].field).toBe("email")
|
||||
end)
|
||||
|
||||
-- Invalid password cases
|
||||
it_each({
|
||||
{ password = "", len = 0, desc = "empty password" },
|
||||
{ password = "1234567", len = 7, desc = "7 char password" },
|
||||
{ password = "short", len = 5, desc = "5 char password" },
|
||||
{ password = "a", len = 1, desc = "1 char password" },
|
||||
})("should reject $desc (length $len)", function(tc)
|
||||
local result = validate.register({ username = "user", email = "test@example.com", password = tc.password })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[1].field).toBe("password")
|
||||
end)
|
||||
|
||||
it("should report all validation errors", function()
|
||||
local result = validate.register({ username = "ab", email = "invalid", password = "short" })
|
||||
expect(result.valid).toBe(false)
|
||||
expect(#result.errors).toBe(3)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
126
packages/ui_permissions/seed/scripts/tests/check.test.lua
Normal file
126
packages/ui_permissions/seed/scripts/tests/check.test.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
-- Tests for ui_permissions check.lua module
|
||||
-- Uses parameterized tests for comprehensive coverage
|
||||
|
||||
describe("ui_permissions/check", function()
|
||||
local LEVELS = {
|
||||
PUBLIC = 1,
|
||||
USER = 2,
|
||||
MODERATOR = 3,
|
||||
ADMIN = 4,
|
||||
GOD = 5,
|
||||
SUPERGOD = 6
|
||||
}
|
||||
|
||||
local check
|
||||
|
||||
beforeEach(function()
|
||||
local ROLE_MAP = {
|
||||
public = LEVELS.PUBLIC,
|
||||
user = LEVELS.USER,
|
||||
moderator = LEVELS.MODERATOR,
|
||||
admin = LEVELS.ADMIN,
|
||||
god = LEVELS.GOD,
|
||||
supergod = LEVELS.SUPERGOD
|
||||
}
|
||||
|
||||
check = {
|
||||
get_level = function(user)
|
||||
if not user then return LEVELS.PUBLIC end
|
||||
return ROLE_MAP[user.role] or LEVELS.PUBLIC
|
||||
end,
|
||||
|
||||
can_access = function(user, required_level)
|
||||
return check.get_level(user) >= required_level
|
||||
end,
|
||||
|
||||
is_moderator_or_above = function(user)
|
||||
return check.get_level(user) >= LEVELS.MODERATOR
|
||||
end,
|
||||
|
||||
is_admin_or_above = function(user)
|
||||
return check.get_level(user) >= LEVELS.ADMIN
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
describe("get_level", function()
|
||||
-- Parameterized test cases
|
||||
it_each({
|
||||
{ role = nil, expected = 1, desc = "nil user" },
|
||||
{ role = "unknown", expected = 1, desc = "unknown role" },
|
||||
{ role = "public", expected = 1, desc = "public role" },
|
||||
{ role = "user", expected = 2, desc = "user role" },
|
||||
{ role = "moderator", expected = 3, desc = "moderator role" },
|
||||
{ role = "admin", expected = 4, desc = "admin role" },
|
||||
{ role = "god", expected = 5, desc = "god role" },
|
||||
{ role = "supergod", expected = 6, desc = "supergod role" },
|
||||
})("should return $expected for $desc", function(tc)
|
||||
local user = tc.role and { role = tc.role } or nil
|
||||
expect(check.get_level(user)).toBe(tc.expected)
|
||||
end)
|
||||
|
||||
it("should return PUBLIC for user without role field", function()
|
||||
expect(check.get_level({})).toBe(LEVELS.PUBLIC)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("can_access", function()
|
||||
-- Test access granted cases
|
||||
it_each({
|
||||
{ role = "user", level = 2, expected = true, desc = "user accessing user content" },
|
||||
{ role = "admin", level = 2, expected = true, desc = "admin accessing user content" },
|
||||
{ role = "supergod", level = 6, expected = true, desc = "supergod accessing supergod content" },
|
||||
{ role = "supergod", level = 1, expected = true, desc = "supergod accessing public content" },
|
||||
{ role = "moderator", level = 3, expected = true, desc = "moderator accessing moderator content" },
|
||||
})("should return $expected for $desc", function(tc)
|
||||
local user = { role = tc.role }
|
||||
expect(check.can_access(user, tc.level)).toBe(tc.expected)
|
||||
end)
|
||||
|
||||
-- Test access denied cases
|
||||
it_each({
|
||||
{ role = "user", level = 4, expected = false, desc = "user accessing admin content" },
|
||||
{ role = "moderator", level = 4, expected = false, desc = "moderator accessing admin content" },
|
||||
{ role = "admin", level = 5, expected = false, desc = "admin accessing god content" },
|
||||
{ role = "god", level = 6, expected = false, desc = "god accessing supergod content" },
|
||||
})("should deny $desc", function(tc)
|
||||
local user = { role = tc.role }
|
||||
expect(check.can_access(user, tc.level)).toBe(tc.expected)
|
||||
end)
|
||||
|
||||
-- Nil user cases
|
||||
it_each({
|
||||
{ level = 1, expected = true, desc = "public content" },
|
||||
{ level = 2, expected = false, desc = "user content" },
|
||||
{ level = 4, expected = false, desc = "admin content" },
|
||||
})("should handle nil user accessing $desc", function(tc)
|
||||
expect(check.can_access(nil, tc.level)).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("is_moderator_or_above", function()
|
||||
it_each({
|
||||
{ role = "public", expected = false },
|
||||
{ role = "user", expected = false },
|
||||
{ role = "moderator", expected = true },
|
||||
{ role = "admin", expected = true },
|
||||
{ role = "god", expected = true },
|
||||
{ role = "supergod", expected = true },
|
||||
})("should return $expected for $role", function(tc)
|
||||
expect(check.is_moderator_or_above({ role = tc.role })).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("is_admin_or_above", function()
|
||||
it_each({
|
||||
{ role = "public", expected = false },
|
||||
{ role = "user", expected = false },
|
||||
{ role = "moderator", expected = false },
|
||||
{ role = "admin", expected = true },
|
||||
{ role = "god", expected = true },
|
||||
{ role = "supergod", expected = true },
|
||||
})("should return $expected for $role", function(tc)
|
||||
expect(check.is_admin_or_above({ role = tc.role })).toBe(tc.expected)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
46
packages/ui_permissions/seed/scripts/tests/levels.test.lua
Normal file
46
packages/ui_permissions/seed/scripts/tests/levels.test.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Tests for ui_permissions levels.lua module
|
||||
-- Uses parameterized tests
|
||||
|
||||
describe("ui_permissions/levels", function()
|
||||
local LEVELS = {
|
||||
PUBLIC = 1,
|
||||
USER = 2,
|
||||
MODERATOR = 3,
|
||||
ADMIN = 4,
|
||||
GOD = 5,
|
||||
SUPERGOD = 6
|
||||
}
|
||||
|
||||
describe("level values", function()
|
||||
it_each({
|
||||
{ name = "PUBLIC", value = 1 },
|
||||
{ name = "USER", value = 2 },
|
||||
{ name = "MODERATOR", value = 3 },
|
||||
{ name = "ADMIN", value = 4 },
|
||||
{ name = "GOD", value = 5 },
|
||||
{ name = "SUPERGOD", value = 6 },
|
||||
})("should have $name as level $value", function(tc)
|
||||
expect(LEVELS[tc.name]).toBe(tc.value)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("level hierarchy", function()
|
||||
it_each({
|
||||
{ lower = "PUBLIC", higher = "USER" },
|
||||
{ lower = "USER", higher = "MODERATOR" },
|
||||
{ lower = "MODERATOR", higher = "ADMIN" },
|
||||
{ lower = "ADMIN", higher = "GOD" },
|
||||
{ lower = "GOD", higher = "SUPERGOD" },
|
||||
})("should have $lower < $higher", function(tc)
|
||||
expect(LEVELS[tc.lower]).toBeLessThan(LEVELS[tc.higher])
|
||||
end)
|
||||
|
||||
it("should have exactly 6 levels", function()
|
||||
local count = 0
|
||||
for _ in pairs(LEVELS) do
|
||||
count = count + 1
|
||||
end
|
||||
expect(count).toBe(6)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
96
packages/user_manager/seed/scripts/tests/actions.test.lua
Normal file
96
packages/user_manager/seed/scripts/tests/actions.test.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
-- Tests for user_manager actions.lua module
|
||||
-- Uses parameterized tests
|
||||
|
||||
describe("user_manager/actions", function()
|
||||
local actions
|
||||
|
||||
beforeEach(function()
|
||||
actions = {
|
||||
create = function(data)
|
||||
return { action = "create_user", data = data }
|
||||
end,
|
||||
|
||||
update = function(user_id, data)
|
||||
return { action = "update_user", user_id = user_id, data = data }
|
||||
end,
|
||||
|
||||
delete = function(user_id)
|
||||
return { action = "delete_user", user_id = user_id, confirm = true }
|
||||
end,
|
||||
|
||||
change_level = function(user_id, new_level)
|
||||
return { action = "change_level", user_id = user_id, level = new_level }
|
||||
end,
|
||||
|
||||
toggle_active = function(user_id, active)
|
||||
return { action = "toggle_active", user_id = user_id, active = active }
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
describe("create", function()
|
||||
it_each({
|
||||
{ data = { name = "John" }, desc = "name only" },
|
||||
{ data = { name = "Jane", email = "j@e.com" }, desc = "name and email" },
|
||||
{ data = { name = "Bob", role = "admin" }, desc = "name and role" },
|
||||
})("should create user with $desc", function(tc)
|
||||
local result = actions.create(tc.data)
|
||||
expect(result.action).toBe("create_user")
|
||||
expect(result.data).toEqual(tc.data)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("update", function()
|
||||
it_each({
|
||||
{ user_id = "user-1", data = { name = "Updated" }, desc = "update name" },
|
||||
{ user_id = "user-2", data = { role = "admin" }, desc = "update role" },
|
||||
{ user_id = "user-123", data = { name = "X", role = "user" }, desc = "update multiple" },
|
||||
})("should $desc", function(tc)
|
||||
local result = actions.update(tc.user_id, tc.data)
|
||||
expect(result.action).toBe("update_user")
|
||||
expect(result.user_id).toBe(tc.user_id)
|
||||
expect(result.data).toEqual(tc.data)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("delete", function()
|
||||
it_each({
|
||||
{ user_id = "user-1" },
|
||||
{ user_id = "user-123" },
|
||||
{ user_id = "admin-456" },
|
||||
})("should delete user $user_id with confirmation", function(tc)
|
||||
local result = actions.delete(tc.user_id)
|
||||
expect(result.action).toBe("delete_user")
|
||||
expect(result.user_id).toBe(tc.user_id)
|
||||
expect(result.confirm).toBe(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("change_level", function()
|
||||
it_each({
|
||||
{ user_id = "user-1", level = 1, desc = "to PUBLIC" },
|
||||
{ user_id = "user-2", level = 2, desc = "to USER" },
|
||||
{ user_id = "user-3", level = 3, desc = "to MODERATOR" },
|
||||
{ user_id = "user-4", level = 4, desc = "to ADMIN" },
|
||||
{ user_id = "user-5", level = 5, desc = "to GOD" },
|
||||
{ user_id = "user-6", level = 6, desc = "to SUPERGOD" },
|
||||
})("should change $user_id $desc", function(tc)
|
||||
local result = actions.change_level(tc.user_id, tc.level)
|
||||
expect(result.action).toBe("change_level")
|
||||
expect(result.user_id).toBe(tc.user_id)
|
||||
expect(result.level).toBe(tc.level)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("toggle_active", function()
|
||||
it_each({
|
||||
{ user_id = "user-1", active = true, desc = "activate" },
|
||||
{ user_id = "user-2", active = false, desc = "deactivate" },
|
||||
})("should $desc $user_id", function(tc)
|
||||
local result = actions.toggle_active(tc.user_id, tc.active)
|
||||
expect(result.action).toBe("toggle_active")
|
||||
expect(result.user_id).toBe(tc.user_id)
|
||||
expect(result.active).toBe(tc.active)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
87
packages/workflow_editor/seed/scripts/tests/status.test.lua
Normal file
87
packages/workflow_editor/seed/scripts/tests/status.test.lua
Normal file
@@ -0,0 +1,87 @@
|
||||
-- Tests for workflow_editor status.lua module
|
||||
-- Uses parameterized tests
|
||||
|
||||
describe("workflow_editor/status", function()
|
||||
local status
|
||||
|
||||
beforeEach(function()
|
||||
status = {
|
||||
PENDING = "pending",
|
||||
RUNNING = "running",
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed",
|
||||
CANCELLED = "cancelled",
|
||||
|
||||
render_badge = function(s)
|
||||
local colors = {
|
||||
pending = "default",
|
||||
running = "info",
|
||||
success = "success",
|
||||
failed = "error",
|
||||
cancelled = "warning"
|
||||
}
|
||||
return {
|
||||
type = "badge",
|
||||
props = { label = s, color = colors[s] or "default" }
|
||||
}
|
||||
end,
|
||||
|
||||
render_progress = function(completed, total)
|
||||
local percent = total > 0 and (completed / total * 100) or 0
|
||||
return {
|
||||
type = "progress",
|
||||
props = { value = percent, label = completed .. "/" .. total }
|
||||
}
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
describe("status constants", function()
|
||||
it_each({
|
||||
{ name = "PENDING", value = "pending" },
|
||||
{ name = "RUNNING", value = "running" },
|
||||
{ name = "SUCCESS", value = "success" },
|
||||
{ name = "FAILED", value = "failed" },
|
||||
{ name = "CANCELLED", value = "cancelled" },
|
||||
})("should have $name = $value", function(tc)
|
||||
expect(status[tc.name]).toBe(tc.value)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("render_badge", function()
|
||||
it_each({
|
||||
{ status = "pending", color = "default", desc = "pending status" },
|
||||
{ status = "running", color = "info", desc = "running status" },
|
||||
{ status = "success", color = "success", desc = "success status" },
|
||||
{ status = "failed", color = "error", desc = "failed status" },
|
||||
{ status = "cancelled", color = "warning", desc = "cancelled status" },
|
||||
{ status = "unknown", color = "default", desc = "unknown status" },
|
||||
})("should render $desc with $color color", function(tc)
|
||||
local result = status.render_badge(tc.status)
|
||||
expect(result.type).toBe("badge")
|
||||
expect(result.props.label).toBe(tc.status)
|
||||
expect(result.props.color).toBe(tc.color)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("render_progress", function()
|
||||
it_each({
|
||||
{ completed = 0, total = 10, percent = 0, label = "0/10" },
|
||||
{ completed = 5, total = 10, percent = 50, label = "5/10" },
|
||||
{ completed = 10, total = 10, percent = 100, label = "10/10" },
|
||||
{ completed = 3, total = 4, percent = 75, label = "3/4" },
|
||||
{ completed = 1, total = 3, percent = 33.333333333333, label = "1/3" },
|
||||
{ completed = 0, total = 0, percent = 0, label = "0/0" },
|
||||
})("should render $completed/$total as $percent%", function(tc)
|
||||
local result = status.render_progress(tc.completed, tc.total)
|
||||
expect(result.type).toBe("progress")
|
||||
expect(result.props.label).toBe(tc.label)
|
||||
-- Use toBeCloseTo for floating point comparison
|
||||
if tc.percent == 33.333333333333 then
|
||||
expect(result.props.value).toBeCloseTo(tc.percent, 1)
|
||||
else
|
||||
expect(result.props.value).toBe(tc.percent)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user