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:
2025-12-30 01:30:52 +00:00
parent a920657d03
commit 7cd954b038
18 changed files with 615 additions and 996 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)