mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
fix(client): work through the findings from the coverage pass
214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out.
This commit is contained in:
@@ -560,7 +560,7 @@ describe('AddonManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-ADMIN-ADDON-027: an error frame in the pull stream is tolerated and the row returns to idle', async () => {
|
||||
it('FE-ADMIN-ADDON-027: an error frame in the pull stream aborts the pull and is reported', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(
|
||||
addonsRoute([llmAddon({ provider: 'local' })]),
|
||||
@@ -575,6 +575,8 @@ describe('AddonManager', () => {
|
||||
await screen.findByText('No models installed yet — pull one below.');
|
||||
await user.click(screen.getByRole('button', { name: 'Pull' }));
|
||||
|
||||
await screen.findByText('manifest not found');
|
||||
expect(screen.queryByText('Model pulled')).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Pull' })).toBeEnabled());
|
||||
expect(screen.queryByText('Pulling…')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1242,6 +1242,21 @@ describe('AdminPluginsPanel — Discover cards and the detail modal', () => {
|
||||
expect(screen.queryByText('Setup')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PLUGINS-PANEL-043b: a manifest that omits the lists still renders the modal', async () => {
|
||||
const detail = manifestDetail({ size: 0 })
|
||||
delete (detail.manifest as Record<string, unknown>).egress
|
||||
delete (detail.manifest as Record<string, unknown>).settings
|
||||
delete (detail.manifest as Record<string, unknown>).permissions
|
||||
discoverWith({}, detail)
|
||||
render(<AdminPluginsPanel />)
|
||||
await clickDiscover()
|
||||
fireEvent.click(await screen.findByText('Gotify'))
|
||||
|
||||
expect(await screen.findByText('Needs no special access.')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Connects to')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Setup')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('FE-COMP-PLUGINS-PANEL-044: an incompatible entry explains the blocked button in the modal body', async () => {
|
||||
discoverWith({ compatible: false, latestCompatible: null, trek: '>=4.0.0', hostVersion: '3.3.0' })
|
||||
render(<AdminPluginsPanel />)
|
||||
|
||||
@@ -96,15 +96,19 @@ interface RegistryDetail extends RegistryItem {
|
||||
size: number | null
|
||||
publishedAt: string | null
|
||||
manifest: {
|
||||
permissions: string[]
|
||||
egress: string[]
|
||||
// Optional because a registry may omit an empty list — the detail view has to
|
||||
// degrade rather than throw halfway through its render.
|
||||
permissions?: string[]
|
||||
egress?: string[]
|
||||
/** The plugin needs OPERATOR-supplied hosts — its egress list is not the whole story. */
|
||||
operatorEgress?: boolean
|
||||
settings: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }>
|
||||
settings?: Array<{ key: string; label: string; inputType: string; scope: string; required: boolean }>
|
||||
license: string | null
|
||||
icon: string | null
|
||||
requiredAddons?: string[]
|
||||
pluginDependencies?: PluginDep[]
|
||||
/** Display slice of the manifest's capabilities — drives the same chips as an installed row. */
|
||||
capabilities?: { widget?: { slot?: string }; tripPage?: { replaces?: string[] } }
|
||||
} | null
|
||||
}
|
||||
|
||||
@@ -300,13 +304,8 @@ function installOffer(item: RegistryItem, t: T): { blocked: boolean; version?: s
|
||||
return { blocked: true, label: t('admin.plugins.incompatible'), title }
|
||||
}
|
||||
|
||||
function ReviewedBadge({ t, compact }: { t: T; compact?: boolean }) {
|
||||
if (compact) return <ShieldCheck size={13} className="text-success shrink-0" aria-label={t('admin.plugins.reviewed')} />
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold bg-success-soft text-success">
|
||||
<ShieldCheck size={11} /> {t('admin.plugins.reviewed')}
|
||||
</span>
|
||||
)
|
||||
function ReviewedBadge({ t }: { t: T }) {
|
||||
return <ShieldCheck size={13} className="text-success shrink-0" aria-label={t('admin.plugins.reviewed')} />
|
||||
}
|
||||
|
||||
/** Marks a manually-uploaded (sideloaded) plugin: no registry, unsigned, not reviewed. */
|
||||
@@ -1149,7 +1148,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-[14.5px] font-semibold tracking-[-.006em] text-content">{p.name}</span>
|
||||
{p.version && <span className="text-[11.5px] text-content-faint font-medium tabular-nums">v{p.version}</span>}
|
||||
{p.reviewed_at && <ReviewedBadge t={t} compact />}
|
||||
{p.reviewed_at && <ReviewedBadge t={t} />}
|
||||
{p.source_repo === 'local:upload' && <SideloadedBadge t={t} />}
|
||||
{p.source_repo === 'local:link' && <DevLinkBadge t={t} />}
|
||||
{/* Registry plugins only — a sideloaded/dev-linked plugin already says something
|
||||
@@ -1401,7 +1400,10 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
}, [item.id])
|
||||
|
||||
const manifest = detail?.manifest ?? null
|
||||
const caps = manifest ? deriveCaps(manifest.permissions, {}, t) : []
|
||||
const permissions = manifest?.permissions ?? []
|
||||
const egress = manifest?.egress ?? []
|
||||
const settings = manifest?.settings ?? []
|
||||
const caps = manifest ? deriveCaps(permissions, manifest.capabilities ?? {}, t) : []
|
||||
const repoUrl = `https://github.com/${item.repo}`
|
||||
const homepage = item.homepage && /^https?:\/\//i.test(item.homepage) && item.homepage !== repoUrl ? item.homepage : null
|
||||
const sizeKb = detail?.size ? Math.max(1, Math.round(detail.size / 1024)) : null
|
||||
@@ -1425,7 +1427,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
<div className="flex-1 min-w-0 pt-8">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-lg font-semibold tracking-tight text-content">{item.name}</h3>
|
||||
{item.reviewedAt && <ReviewedBadge t={t} compact />}
|
||||
{item.reviewedAt && <ReviewedBadge t={t} />}
|
||||
</div>
|
||||
<p className="text-[12.5px] text-content-faint mt-0.5">{item.author}{item.latest ? ` · v${item.latest}` : ''}</p>
|
||||
</div>
|
||||
@@ -1451,7 +1453,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
{manifest && (
|
||||
<div className="mt-5">
|
||||
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-content-muted">{t('admin.plugins.accessTitle')}</h4>
|
||||
{caps.filter(c => !c.net).length === 0 && !manifest.permissions.includes('db:own') ? (
|
||||
{caps.filter(c => !c.net).length === 0 && !permissions.includes('db:own') ? (
|
||||
<p className="text-xs text-content-faint mt-2">{t('admin.plugins.noAccess')}</p>
|
||||
) : (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
@@ -1460,7 +1462,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
<c.icon size={15} className="text-accent mt-0.5 shrink-0" /><span>{c.label}</span>
|
||||
</div>
|
||||
))}
|
||||
{manifest.permissions.includes('db:own') && (
|
||||
{permissions.includes('db:own') && (
|
||||
<div className="flex items-start gap-2.5 text-[13px] text-content-secondary py-0.5">
|
||||
<Database size={15} className="text-accent mt-0.5 shrink-0" /><span>{t('admin.plugins.perm.db:own')}</span>
|
||||
</div>
|
||||
@@ -1470,11 +1472,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
</div>
|
||||
)}
|
||||
|
||||
{manifest && (manifest.egress.length > 0 || manifest.operatorEgress) && (
|
||||
{manifest && (egress.length > 0 || manifest.operatorEgress) && (
|
||||
<div className="mt-5">
|
||||
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-content-muted">{t('admin.plugins.connectsTitle')}</h4>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
||||
{manifest.egress.map(h => (
|
||||
{egress.map(h => (
|
||||
<code key={h} className="text-[12px] font-mono text-info bg-info-soft rounded-md px-2 py-1">{h}</code>
|
||||
))}
|
||||
{/* The hosts above are NOT the whole story for this plugin: it talks to a
|
||||
@@ -1493,11 +1495,11 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local
|
||||
</div>
|
||||
)}
|
||||
|
||||
{manifest && manifest.settings.length > 0 && (
|
||||
{manifest && settings.length > 0 && (
|
||||
<div className="mt-5">
|
||||
<h4 className="text-[11px] font-semibold uppercase tracking-wider text-content-muted">{t('admin.plugins.setupTitle')}</h4>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{manifest.settings.map(s => (
|
||||
{settings.map(s => (
|
||||
<li key={s.key} className="flex items-center gap-2 text-xs text-content-muted flex-wrap">
|
||||
<span className="font-medium">{s.label}</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-surface-tertiary text-content-faint">{t(`admin.plugins.scope.${s.scope}` as never)}</span>
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function CategoryManager() {
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
// The Save button carries disabled={… || !form.name.trim()}, so the name is set here.
|
||||
const handleSave = async () => {
|
||||
if (!form.name.trim()) { toast.error(t('categories.toast.nameRequired')); return }
|
||||
setIsSaving(true)
|
||||
try {
|
||||
if (editingId) {
|
||||
|
||||
@@ -48,8 +48,8 @@ function normalizeProvider(value: unknown): MapProvider {
|
||||
return value === 'mapbox-gl' || value === 'maplibre-gl' ? value : 'leaflet'
|
||||
}
|
||||
|
||||
function styleForProvider(provider: MapProvider, style?: string | null): string {
|
||||
if (provider === 'leaflet') return style || MAPBOX_DEFAULT_STYLE
|
||||
/** Only the GL providers keep a style — Leaflet is handled by its callers. */
|
||||
function styleForProvider(provider: GlMapProvider, style?: string | null): string {
|
||||
if (provider === 'mapbox-gl' && isOpenFreeMapStyle(style)) return MAPBOX_DEFAULT_STYLE
|
||||
return normalizeStyleForProvider(provider, style)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,20 @@ describe('DevNotificationsPanel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-008: error toast shown on API failure', async () => {
|
||||
it('FE-ADMIN-DEVNOTIF-008: the server error field is what the toast shows', async () => {
|
||||
server.use(
|
||||
http.post('/api/admin/dev/test-notification', () =>
|
||||
HttpResponse.json({ error: 'No channel configured' }, { status: 500 }),
|
||||
),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
render(<><ToastContainer /><DevNotificationsPanel /></>);
|
||||
await screen.findByText('Type Testing');
|
||||
await user.click(screen.getByText('Simple → Me').closest('button')!);
|
||||
await screen.findByText('No channel configured');
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-008b: a failure without an error field falls back to the generic text', async () => {
|
||||
server.use(
|
||||
http.post('/api/admin/dev/test-notification', () =>
|
||||
HttpResponse.json({ message: 'Server error' }, { status: 500 }),
|
||||
@@ -120,7 +133,7 @@ describe('DevNotificationsPanel', () => {
|
||||
render(<><ToastContainer /><DevNotificationsPanel /></>);
|
||||
await screen.findByText('Type Testing');
|
||||
await user.click(screen.getByText('Simple → Me').closest('button')!);
|
||||
await screen.findByText(/failed|error/i);
|
||||
await screen.findByText('Failed');
|
||||
});
|
||||
|
||||
it('FE-ADMIN-DEVNOTIF-009: changing trip selector updates payload targetId', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { adminApi, tripsApi } from '../../api/client'
|
||||
import { getApiErrorMessage } from '../../utils/apiError'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import {
|
||||
@@ -46,8 +47,8 @@ export default function DevNotificationsPanel(): React.ReactElement {
|
||||
try {
|
||||
await adminApi.sendTestNotification(payload)
|
||||
toast.success(`Sent: ${label}`)
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed')
|
||||
} catch (err: unknown) {
|
||||
toast.error(getApiErrorMessage(err, 'Failed'))
|
||||
} finally {
|
||||
setSending(null)
|
||||
}
|
||||
|
||||
@@ -697,7 +697,7 @@ describe('PackingTemplateManager', () => {
|
||||
|
||||
await user.click(categoryButtons('Clothing')[2]);
|
||||
|
||||
expect(await screen.findByText('Failed to delete template')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Failed to delete category')).toBeInTheDocument();
|
||||
expect(screen.getByText('Clothing')).toBeInTheDocument();
|
||||
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
||||
});
|
||||
@@ -798,7 +798,7 @@ describe('PackingTemplateManager', () => {
|
||||
|
||||
await user.click(itemButtons('T-shirt')[1]);
|
||||
|
||||
expect(await screen.findByText('Failed to delete template')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Failed to delete item')).toBeInTheDocument();
|
||||
expect(screen.getByText('T-shirt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -115,12 +115,13 @@ export default function PackingTemplateManager() {
|
||||
await adminApi.deleteTemplateCategory(expandedId, catId)
|
||||
setCategories(prev => prev.filter(c => c.id !== catId))
|
||||
setItems(prev => prev.filter(i => i.category_id !== catId))
|
||||
} catch { toast.error(t('admin.packingTemplates.deleteError')) }
|
||||
} catch { toast.error(t('admin.packingTemplates.deleteCategoryError')) }
|
||||
}
|
||||
|
||||
// Item CRUD
|
||||
const handleAddItem = async (catId: number) => {
|
||||
if (!newItemName.trim() || !expandedId) return
|
||||
// The name is already guaranteed non-empty by the button and the Enter handler.
|
||||
if (!expandedId) return
|
||||
try {
|
||||
const data = await adminApi.addTemplateItem(expandedId, catId, { name: newItemName.trim() })
|
||||
setItems(prev => [...prev, data.item])
|
||||
@@ -143,7 +144,7 @@ export default function PackingTemplateManager() {
|
||||
try {
|
||||
await adminApi.deleteTemplateItem(expandedId, itemId)
|
||||
setItems(prev => prev.filter(i => i.id !== itemId))
|
||||
} catch { toast.error(t('admin.packingTemplates.deleteError')) }
|
||||
} catch { toast.error(t('admin.packingTemplates.deleteItemError')) }
|
||||
}
|
||||
|
||||
const inputStyle = 'w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent outline-none'
|
||||
|
||||
Reference in New Issue
Block a user