Merge pull request #512 from Vaso73/i18n/slovak-optimization-source

i18n: localize Slovak optimization source viewer and handle stale responses
This commit is contained in:
MacRimi
2026-10-21 22:36:28 +01:00
committed by GitHub
6 changed files with 265 additions and 17 deletions
+1
View File
@@ -31,6 +31,7 @@ PYTHON_FILES = (
"AppImage/scripts/tests/test_health_persistence_recurrence.py",
)
NODE_FILES = (
"tests/test_optimization_source.cjs",
"tests/lxc_updates/test_app_editor_errors.cjs",
"tests/test_oci_update_cancellation.cjs",
"tests/test_host_backup_messages.cjs",
+46 -17
View File
@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, useRef } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff, Code, X, Copy, Sparkles, ArrowUpCircle, BellOff, Globe2, Github } from "lucide-react"
import { Badge } from "./ui/badge"
@@ -19,6 +19,7 @@ import { SUPPORTED_LANGUAGES, useI18n } from "../lib/i18n/provider"
import { useIsPbs } from "../lib/use-platform"
import type { LanguageCode } from "../lib/i18n/languages"
import { NavTabOrderCard } from "./nav-tab-order-card"
import { optimizationSourceError, optimizationSourceName, type OptimizationSourceError, type OptimizationSourceResponse } from "../lib/optimization-source-errors"
// GitHub Dark color palette for bash syntax highlighting
const BASH_KEYWORDS = new Set([
@@ -354,15 +355,31 @@ export function Settings() {
open: boolean
loading: boolean
toolName: string
toolKey: string
version: string
availableVersion: string
functionName: string
source: string
script: string
error: string
error: OptimizationSourceError | null
deprecated: boolean
}>({ open: false, loading: false, toolName: '', version: '', availableVersion: '', functionName: '', source: '', script: '', error: '', deprecated: false })
}>({ open: false, loading: false, toolName: '', toolKey: '', version: '', availableVersion: '', functionName: '', source: '', script: '', error: null, deprecated: false })
const [codeCopied, setCodeCopied] = useState(false)
const codeRequest = useRef(0)
const closeCodeModal = () => {
codeRequest.current += 1
setCodeModal(prev => ({ ...prev, open: false }))
setCodeCopied(false)
}
useEffect(() => () => { codeRequest.current += 1 }, [])
useEffect(() => {
if (!codeModal.open) return
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") closeCodeModal()
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [codeModal.open])
// Health Monitor suppression settings
const [suppressionCategories, setSuppressionCategories] = useState<SuppressionCategory[]>([])
@@ -663,31 +680,39 @@ export function Settings() {
}
const viewToolSource = async (tool: ProxMenuxTool) => {
const request = ++codeRequest.current
setCodeCopied(false)
setCodeModal({
open: true,
loading: true,
toolName: tool.name,
toolKey: tool.key,
version: tool.version || '1.0',
availableVersion: tool.available_version || tool.version || '1.0',
functionName: '',
source: '',
script: '',
error: '',
error: null,
deprecated: !!tool.deprecated,
})
try {
const data = await fetchApi(`/api/proxmenux/tool-source/${tool.key}`)
if (data.success) {
setCodeModal(prev => ({ ...prev, loading: false, functionName: data.function, source: data.source, script: data.script, deprecated: !!data.deprecated }))
const data = await fetchApi<OptimizationSourceResponse | null>(`/api/proxmenux/tool-source/${tool.key}`)
if (request !== codeRequest.current) return
if (data?.success === true && typeof data.source === "string" && data.source.trim()) {
const source = data.source
setCodeModal(prev => ({ ...prev, loading: false, functionName: typeof data.function === "string" ? data.function : '', source, script: typeof data.script === "string" ? data.script : '', deprecated: !!data.deprecated }))
} else {
setCodeModal(prev => ({ ...prev, loading: false, error: data.error || 'Source code not available' }))
setCodeModal(prev => ({ ...prev, loading: false, error: { kind: "unavailable", detail: typeof data?.error === "string" ? data.error : undefined } }))
}
} catch {
setCodeModal(prev => ({ ...prev, loading: false, error: 'Failed to load source code' }))
} catch (error: any) {
if (request !== codeRequest.current) return
const detail = typeof error?.body?.error === "string" ? error.body.error : undefined
setCodeModal(prev => ({ ...prev, loading: false, error: { kind: "loadFailed", detail } }))
}
}
const copySourceCode = async () => {
const request = codeRequest.current
const text = codeModal.source
let ok = false
@@ -721,9 +746,9 @@ export function Settings() {
}
}
if (ok) {
if (ok && request === codeRequest.current) {
setCodeCopied(true)
setTimeout(() => setCodeCopied(false), 2000)
setTimeout(() => { if (request === codeRequest.current) setCodeCopied(false) }, 2000)
}
}
@@ -2153,10 +2178,13 @@ export function Settings() {
{/* Code Viewer Modal */}
{codeModal.open && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ margin: 0 }} onClick={closeCodeModal}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div
className="relative bg-card border border-border rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col"
role="dialog"
aria-modal="true"
aria-label={t("settings.optimizations.sourceDialogTitle", { name: optimizationSourceName(codeModal.toolKey, codeModal.toolName, t) })}
onClick={e => e.stopPropagation()}
>
{/* Header */}
@@ -2165,7 +2193,7 @@ export function Settings() {
<Code className={`h-5 w-5 flex-shrink-0 ${codeModal.deprecated ? 'text-amber-500' : 'text-orange-500'}`} />
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-semibold truncate">{codeModal.toolName}</h3>
<h3 className="text-sm font-semibold truncate">{optimizationSourceName(codeModal.toolKey, codeModal.toolName, t)}</h3>
{codeModal.deprecated && (
<span className="text-[9px] uppercase tracking-wider text-amber-500 bg-amber-500/10 border border-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">
{t("settings.optimizations.legacy")}
@@ -2201,7 +2229,8 @@ export function Settings() {
</button>
)}
<button
onClick={() => setCodeModal(prev => ({ ...prev, open: false }))}
onClick={closeCodeModal}
aria-label={t("actions.close")}
className="p-1.5 rounded-md hover:bg-muted transition-colors"
>
<X className="h-4 w-4" />
@@ -2211,13 +2240,13 @@ export function Settings() {
{/* Body */}
<div className="flex-1 overflow-auto p-0">
{codeModal.loading ? (
<div className="flex items-center justify-center py-16">
<div className="flex items-center justify-center py-16" role="status" aria-label={t("settings.optimizations.sourceLoading")}>
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
</div>
) : codeModal.error ? (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<Code className="h-10 w-10 mb-3 opacity-40" />
<p className="text-sm">{codeModal.error}</p>
<p className="text-sm" role="alert">{optimizationSourceError(codeModal.error, t)}</p>
</div>
) : (
<pre
@@ -0,0 +1,39 @@
type Translate = (key: string, params?: Record<string, string | number>) => string
const SOURCE_TOOLS = new Set([
"subscription_banner", "time_sync", "apt_languages", "journald", "logrotate",
"system_limits", "memory_settings", "zram_swap", "kernel_panic", "apt_ipv4",
"kexec", "rpc", "motd", "system_utils", "network_optimization", "bashrc_custom",
"figurine", "fastfetch", "log2ram", "zfs_autotrim", "amd_fixes", "persistent_network",
"vfio_iommu", "lvm_repair", "repo_cleanup", "proxmox_repos", "entropy",
])
export function optimizationSourceName(tool: string, fallback: string, t: Translate): string {
return SOURCE_TOOLS.has(tool) ? t(`settings.optimizations.sourceToolNames.${tool}`) : fallback
}
export interface OptimizationSourceError {
kind: "unavailable" | "loadFailed"
detail?: string
}
export interface OptimizationSourceResponse {
success?: unknown
source?: unknown
function?: unknown
script?: unknown
deprecated?: unknown
error?: unknown
}
// Keep the blueprint's English API contract intact; translate only its display.
export function optimizationSourceError(error: OptimizationSourceError, t: Translate): string {
const detail = error.detail || ""
const key = "settings.optimizations."
if (detail === "Function not found in available scripts") return t(key + "sourceFunctionMissing")
if (detail.startsWith("Unknown tool: ")) return t(key + "sourceUnknownTool", { tool: detail.slice(14) })
if (detail.startsWith("No function mapping for ")) return t(key + "sourceNoMapping", { tool: detail.slice(24) })
if (detail === "Source code not available") return t(key + "sourceUnavailable")
const message = t(key + (error.kind === "unavailable" ? "sourceUnavailable" : "sourceLoadFailed"))
return detail ? `${message} ${detail}` : message
}
+36
View File
@@ -2544,6 +2544,42 @@
"updateManyDescription": "Re-running {count} post-install functions in sequence.",
"legacySourceTitle": "Legacy optimization - click to view source",
"sourceTitle": "Click to view source code",
"sourceDialogTitle": "Source code: {name}",
"sourceLoading": "Loading source code…",
"sourceUnavailable": "Source code is not available.",
"sourceLoadFailed": "Could not load the source code. Try again.",
"sourceFunctionMissing": "The function was not found in the available scripts.",
"sourceUnknownTool": "Unknown tool: {tool}",
"sourceNoMapping": "No function is assigned to the tool {tool}.",
"sourceToolNames": {
"subscription_banner": "Subscription Banner Removal",
"time_sync": "Time Synchronization",
"apt_languages": "APT Language Skip",
"journald": "Journald Optimization",
"logrotate": "Logrotate Optimization",
"system_limits": "System Limits Increase",
"memory_settings": "Memory Settings Optimization",
"zram_swap": "ZRAM Swap Profile",
"kernel_panic": "Kernel Panic Configuration",
"apt_ipv4": "APT IPv4 Force",
"kexec": "kexec for quick reboots",
"rpc": "RPC / rpcbind Disable",
"motd": "Custom MOTD Banner",
"system_utils": "System Utilities",
"network_optimization": "Network Optimizations",
"bashrc_custom": "Bashrc Customization",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (SSD Protection)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "AMD CPU (Ryzen/EPYC) fixes",
"persistent_network": "Setting persistent network interfaces",
"vfio_iommu": "VFIO/IOMMU Passthrough",
"lvm_repair": "LVM PV Headers Repair",
"repo_cleanup": "Repository Cleanup",
"proxmox_repos": "Proxmox APT Repositories",
"entropy": "Entropy Generation (haveged)"
},
"legacy": "legacy",
"updateToolTitle": "Update {name} to v{version}",
"availableUpdates": "Available updates",
+36
View File
@@ -2416,6 +2416,42 @@
"updateManyDescription": "Postupne sa spustí {count} post-install funkcií.",
"legacySourceTitle": "Staršia optimalizácia - kliknutím zobrazíte zdroj",
"sourceTitle": "Kliknutím zobrazíte zdrojový kód",
"sourceDialogTitle": "Zdrojový kód: {name}",
"sourceLoading": "Načítava sa zdrojový kód…",
"sourceUnavailable": "Zdrojový kód nie je dostupný.",
"sourceLoadFailed": "Zdrojový kód sa nepodarilo načítať. Skús to znova.",
"sourceFunctionMissing": "Funkcia sa nenašla v dostupných skriptoch.",
"sourceUnknownTool": "Neznámy nástroj: {tool}",
"sourceNoMapping": "K nástroju {tool} nie je priradená funkcia.",
"sourceToolNames": {
"subscription_banner": "Odstránenie upozornenia na predplatné",
"time_sync": "Synchronizácia času",
"apt_languages": "Vynechanie jazykových balíkov APT",
"journald": "Optimalizácia journald",
"logrotate": "Optimalizácia logrotate",
"system_limits": "Zvýšenie systémových limitov",
"memory_settings": "Optimalizácia nastavení pamäte",
"zram_swap": "Profil swapu ZRAM",
"kernel_panic": "Nastavenie správania pri páde jadra",
"apt_ipv4": "Vynútenie IPv4 pre APT",
"kexec": "Rýchly reštart cez kexec",
"rpc": "Vypnutie RPC / rpcbind",
"motd": "Vlastná uvítacia správa MOTD",
"system_utils": "Systémové nástroje",
"network_optimization": "Optimalizácia siete",
"bashrc_custom": "Prispôsobenie bashrc",
"figurine": "Figurine",
"fastfetch": "Fastfetch",
"log2ram": "Log2ram (ochrana SSD)",
"zfs_autotrim": "ZFS Autotrim",
"amd_fixes": "Opravy pre procesory AMD (Ryzen/EPYC)",
"persistent_network": "Trvalé názvy sieťových rozhraní",
"vfio_iommu": "Priame priradenie zariadení (VFIO/IOMMU)",
"lvm_repair": "Oprava hlavičiek fyzických zväzkov LVM",
"repo_cleanup": "Vyčistenie repozitárov",
"proxmox_repos": "Repozitáre APT pre Proxmox",
"entropy": "Generovanie entropie (haveged)"
},
"legacy": "staršie",
"updateToolTitle": "Aktualizovať {name} na v{version}",
"availableUpdates": "Dostupné aktualizácie",
+107
View File
@@ -0,0 +1,107 @@
// Actual Settings handlers and real fetchApi; all responses are inert fixtures.
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm')
const {execFileSync}=require('node:child_process')
const root=path.resolve(__dirname,'..'),app=path.join(root,'AppImage')
const ts=require(path.join(app,'node_modules/typescript'))
const catalogs=Object.fromEntries(['en','sk','de'].map(locale=>[locale,JSON.parse(fs.readFileSync(path.join(app,`messages/${locale}/common.json`)))]))
const translate=locale=>(key,params={})=>{
const find=data=>key.split('.').reduce((item,name)=>item?.[name],data)
return (find(catalogs[locale])??find(catalogs.en)??key).replace(/\{(\w+)\}/g,(all,key)=>params[key]??all)
}
const flatten=node=>!node||typeof node!=='object'?[]:[node,...[node.props?.children].flat(Infinity).flatMap(flatten)]
const text=node=>node==null||node===false?'':typeof node!=='object'?String(node):[node.props?.children].flat(Infinity).map(text).join(' ')
const compile=source=>ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,jsx:ts.JsxEmit.ReactJSX}}).outputText
const source=process.argv.includes('--baseline')?execFileSync('git',['show','e826680c:AppImage/components/settings.tsx'],{cwd:root,encoding:'utf8'}):fs.readFileSync(path.join(app,'components/settings.tsx'),'utf8')
const formatter={exports:{}};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/optimization-source-errors.ts'),'utf8')),formatter)
const tools=['one','two'].map(key=>({key,name:'Demo '+key,enabled:true,has_source:true,version:'1.0',available_version:'1.1'}))
const sourceBody={success:true,function:'demo',script:'demo.sh',source:'demo() {\n echo "<tag>& $HOME"\n}\n',deprecated:false}
function fixture(locale='sk',catalog=tools){
let cursor=0,states=[],refs=[],effects=[],memo=[],tree,language=locale,reply=()=>sourceBody
const calls=[],events={},timers=[],copies=[];let fallbackCopies=0
const t=(key,params)=>translate(language)(key,params)
const react={
useState(value){const i=cursor++;if(!(i in states))states[i]=typeof value==='function'?value():value;return[states[i],v=>{states[i]=typeof v==='function'?v(states[i]):v}]},
useRef(value){const i=cursor++;return refs[i]??=( {current:value})},
useEffect(fn,deps){const i=cursor++,old=memo[i];if(!old||deps?.some((v,n)=>v!==old.deps?.[n]))effects.push(()=>{old?.cleanup?.();memo[i]={deps,cleanup:fn()}})},
}
const response=(body,status=200)=>({ok:status<400,status,statusText:'fixture',headers:{get:()=> 'application/json'},json:async()=>body})
const fetch=async(url,options={})=>{
const endpoint=String(url).replace(/^https?:\/\/[^/]+/,'');calls.push({endpoint,method:options.method||'GET'})
assert.equal(options.method||'GET','GET','Source viewer may never mutate')
if(endpoint.includes('/tool-source/')){const result=await reply(endpoint);if(result instanceof Error)throw result;return result?.http?response(result.body,result.http):response(result)}
if(endpoint.endsWith('/installed-tools'))return response({success:true,installed_tools:catalog,updates_available_count:0})
return response({categories:[],storages:[],interfaces:[],dismissed:[],success:true,candidates:[],configured:false})
}
const api={exports:{},fetch,process:{env:{}},console:{error(){}}};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/api-config.ts'),'utf8')),api)
const add=(name,handler)=>{(events[name]??=new Set()).add(handler)},remove=(name,handler)=>events[name]?.delete(handler)
const win={addEventListener:add,removeEventListener:remove,setTimeout:fn=>{timers.push(fn);return timers.length},isSecureContext:true}
const doc={addEventListener:add,removeEventListener:remove,body:{appendChild(){},removeChild(){}},createElement:()=>({style:{},focus(){},select(){}}),execCommand:()=>{fallbackCopies++;return true}}
const clipboard={writeText:async value=>copies.push(value)}
const context={exports:{},window:win,document:doc,navigator:{clipboard},console:{error(){}},setTimeout:win.setTimeout,
require:name=>{
if(name==='react')return react
if(name==='react/jsx-runtime')return{jsx:(type,props)=>({type,props}),jsxs:(type,props)=>({type,props})}
if(name.includes('optimization-source-errors'))return formatter.exports
if(name.includes('api-config'))return api.exports
if(name.includes('i18n/provider'))return{SUPPORTED_LANGUAGES:[],useI18n:()=>({t,language,setLanguage:value=>language=value})}
if(name.includes('use-platform'))return{useIsPbs:()=>false}
if(name.includes('format-network'))return{getNetworkUnit:()=> 'Bytes'}
return new Proxy({},{get:(_,key)=>key})
}}
vm.runInNewContext(compile(source),context)
const render=()=>{cursor=0;effects=[];tree=context.exports.Settings();for(const effect of effects)effect();return tree}
const settle=async()=>{for(let i=0;i<24;i++)await Promise.resolve();render()}
const tool=name=>flatten(tree).find(node=>node.props?.title===t('settings.optimizations.sourceTitle')&&text(node).includes(catalog.find(tool=>tool.key===name)?.name||'Demo '+name))
const dialog=()=>flatten(tree).find(node=>node.props?.role==='dialog')
const close=()=>flatten(tree).find(node=>node.props?.['aria-label']===t('actions.close')).props.onClick()
return{render,settle,tool,dialog,close,calls,copies,timers,tree:()=>tree,setReply:value=>reply=value,t,
language:value=>{language=value;render()},escape:()=>events.keydown?.forEach(fn=>fn({key:'Escape'})),
unmount:()=>memo.forEach(effect=>effect?.cleanup?.()),clipboard,win,fallbackCopies:()=>fallbackCopies}
}
async function main(){
if(process.argv.includes('--baseline')){
const f=fixture();f.render();await f.settle();f.setReply(()=>({http:404,body:{success:false,error:'Function not found in available scripts'}}));await f.tool('one').props.onClick();f.render();
assert.ok(text(f.tree()).includes('Failed to load source code'));assert.ok(!text(f.tree()).includes(f.t('settings.optimizations.sourceFunctionMissing')))
console.log('REPRODUCED baseline: real Settings + fetchApi 404 displays English Failed to load source code');return
}
for(const locale of ['en','sk','de']){
const f=fixture(locale);f.render();await f.settle()
for(const [reply,key,param]of [
[{success:false},'sourceUnavailable'],[null,'sourceUnavailable'],[{success:'yes',source:'invalid'},'sourceUnavailable'],[{success:true,source:''},'sourceUnavailable'],[{success:true,source:' '},'sourceUnavailable'],[{success:true,source:17},'sourceUnavailable'],
[{http:404,body:{success:false,error:'Function not found in available scripts'}},'sourceFunctionMissing'],
[{http:404,body:{success:false,error:'Unknown tool: demo'}},'sourceUnknownTool',{tool:'demo'}],
[{http:404,body:{success:false,error:'No function mapping for demo'}},'sourceNoMapping',{tool:'demo'}],
[{success:false,error:'Source code not available'},'sourceUnavailable'],[new Error('network unreachable'),'sourceLoadFailed']]){
f.setReply(()=>reply);await f.tool('one').props.onClick();f.render();assert.ok(text(f.dialog()).includes(f.t('settings.optimizations.'+key,param)),key)
}
f.setReply(()=>({http:500,body:{success:false,error:'PermissionError /opt/demo <unsafe>&'}}));await f.tool('one').props.onClick();f.render();assert.ok(text(f.dialog()).includes('PermissionError /opt/demo <unsafe>&'))
f.language('sk');assert.ok(text(f.dialog()).includes(translate('sk')('settings.optimizations.sourceLoadFailed')))
f.setReply(()=>sourceBody);await f.tool('one').props.onClick();f.render();
const backdrop=flatten(f.tree()).find(node=>node.props?.className?.startsWith('fixed inset-0')&&flatten(node).some(child=>child.props?.role==='dialog'))
assert.equal(backdrop.props.style.margin,0,'parent space-y must not offset the fixed backdrop')
const pre=flatten(f.dialog()).find(node=>node.type==='pre');assert.ok(pre.props.dangerouslySetInnerHTML.__html.includes('&lt;'));assert.ok(!pre.props.dangerouslySetInnerHTML.__html.includes('<tag>'))
const copy=flatten(f.dialog()).find(node=>node.props?.title===f.t('actions.copyToClipboard'));await copy.props.onClick();f.render();assert.equal(f.copies.at(-1),sourceBody.source)
f.win.isSecureContext=false;await copy.props.onClick();assert.equal(f.fallbackCopies(),1)
const before=f.calls.length;f.close();f.render();assert.equal(f.dialog(),undefined);assert.equal(f.calls.length,before)
}
const names=catalogs.en.settings.optimizations.sourceToolNames
const metadata=JSON.parse(execFileSync('python3',['-c',"import ast,json,sys;t=ast.parse(open(sys.argv[1]).read());n=next(n for n in t.body if isinstance(n,ast.Assign) and isinstance(n.targets[0],ast.Name) and n.targets[0].id=='TOOL_METADATA');print(json.dumps({k:v['name'] for k,v in ast.literal_eval(n.value).items()}))",path.join(app,'scripts/flask_proxmenux_routes.py')],{encoding:'utf8'}))
assert.deepEqual(names,metadata,'source title names must match real backend metadata')
for(const [key,name]of Object.entries(names)){
assert.equal(formatter.exports.optimizationSourceName(key,name,translate('en')),name)
assert.equal(formatter.exports.optimizationSourceName(key,name,translate('sk')),catalogs.sk.settings.optimizations.sourceToolNames[key])
assert.equal(formatter.exports.optimizationSourceName(key,name,translate('de')),name)
}
assert.equal(Object.keys(names).length,27)
assert.equal(formatter.exports.optimizationSourceName('future','Future tool',translate('sk')),'Future tool')
const known=fixture('sk',[{...tools[0],key:'time_sync',name:'Time Synchronization'}]);known.render();await known.settle();await known.tool('time_sync').props.onClick();known.render();assert.ok(text(known.dialog()).includes('Synchronizácia času'));known.language('en');assert.ok(text(known.dialog()).includes('Time Synchronization'))
const f=fixture();f.render();await f.settle();let resolveOld;f.setReply(()=>new Promise(resolve=>resolveOld=resolve));const old=f.tool('one').props.onClick();f.render();assert.ok(flatten(f.dialog()).some(node=>node.props?.role==='status'))
f.close();f.render();resolveOld(sourceBody);await old;f.render();assert.equal(f.dialog(),undefined,'late success never reopens closed viewer')
let resolveStale;f.setReply(()=>new Promise(resolve=>resolveStale=resolve));const stale=f.tool('one').props.onClick();f.setReply(()=>({...sourceBody,source:'two() { :; }'}));await f.tool('two').props.onClick();f.render();resolveStale({http:404,body:{error:'Function not found in available scripts'}});await stale;f.render();assert.ok(text(f.dialog()).includes('Demo two'));assert.equal(flatten(f.dialog()).some(node=>node.props?.role==='alert'),false)
const before=f.calls.length;f.escape();f.render();assert.equal(f.dialog(),undefined);assert.equal(f.calls.length,before)
f.setReply(()=>sourceBody);await f.tool('one').props.onClick();f.render();let copyResolve;f.clipboard.writeText=()=>new Promise(resolve=>copyResolve=resolve);const copying=flatten(f.dialog()).find(node=>node.props?.title===f.t('actions.copyToClipboard')).props.onClick();f.close();f.render();copyResolve();await copying;f.render();assert.equal(f.dialog(),undefined)
let unmountedResolve;f.setReply(()=>new Promise(resolve=>unmountedResolve=resolve));const unmounted=f.tool('one').props.onClick();f.render();const snapshot=text(f.tree());f.unmount();unmountedResolve(sourceBody);await unmounted;f.render();assert.equal(text(f.tree()),snapshot,'response after unmount cannot update modal state')
assert.equal(f.calls.filter(call=>call.method!=='GET').length,0);assert.equal(f.calls.filter(call=>/scan|update|script-runner/.test(call.endpoint)).length,0)
console.log('PASS source viewer: actual Settings/fetchApi, EN/SK/fallback, 404/500/network/malformed/empty, exact copy, late requests and close/Esc; mutations/scan/update 0')
}
main().catch(error=>{console.error(error);process.exitCode=1})