Files
technitium-console/src/app/Shell.handoff.test.tsx
T
DenozordecandClaude Sonnet 5.5 f3531c64f9 Move the console onto ReUI PRO
Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 00:06:06 +07:00

195 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { StrictMode } from 'react'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Shell, type ShellSession } from './Shell'
import { ThemeProvider } from '../theme/ThemeProvider'
import * as client from '../api/client'
import * as dashboard from '../api/dashboard'
import * as dnsclient from '../api/dnsclient'
import * as apps from '../api/apps'
import * as logs from '../api/logs'
import type { InstalledApp } from '../api/apps'
import { forgetRoot } from './route'
/*
The jumps between screens, end to end through the Shell (`app/handoff.ts`):
the row menu asks, the Shell switches section, and the target screen arrives
filled in and runs its query, once.
*/
const chart = { labels: ['a', 'b'], datasets: [{ label: 'Total', data: [1, 2] }] }
const STATS = {
stats: {
totalQueries: 10, totalNoError: 10, totalServerFailure: 0, totalNxDomain: 0,
totalRefused: 0, totalAuthoritative: 0, totalRecursive: 10, totalCached: 0,
totalBlocked: 0, totalDropped: 0, totalClients: 2,
zones: 1, cachedEntries: 0, allowedZones: 0, blockedZones: 0,
allowListZones: 0, blockListZones: 0,
},
mainChartData: chart,
queryResponseChartData: chart,
queryTypeChartData: chart,
protocolTypeChartData: chart,
topClients: [{ name: '10.0.0.1', domain: 'pc.home.test', hits: 12, rateLimited: false }],
topDomains: [{ name: 'github.com', hits: 7 }],
topBlockedDomains: [],
}
const APP: InstalledApp = {
name: 'Query Logs (Sqlite)',
description: 'Logs all incoming DNS requests.',
version: '8.0',
dnsApps: [
{
classPath: 'QueryLogsSqlite.App',
description: 'Logs queries.',
recordDataTemplate: null,
isAppRecordRequestHandler: false,
isRequestController: false,
isAuthoritativeRequestHandler: false,
isRequestBlockingHandler: false,
isQueryLogger: true,
isQueryLogs: true,
isPostProcessor: false,
},
],
}
const ENTRY = {
rowNumber: 1,
timestamp: '2026-08-26T05:32:14Z',
clientIpAddress: '10.0.0.1',
protocol: 'Udp',
responseType: 'Recursive',
rcode: 'NoError',
qname: 'api.github.com',
qtype: 'HTTPS',
qclass: 'IN',
answer: null,
}
function session(canViewDnsClient = true): ShellSession {
const sections = ['Dashboard', 'Zones', 'Cache', 'Allowed', 'Blocked', 'Apps', 'DnsClient', 'Settings', 'DhcpServer', 'Administration', 'Logs']
return {
token: 'tok',
displayName: 'Administrator',
username: 'admin',
type: 'Local',
info: {
version: '15.6',
uptimestamp: '2026-08-25T13:07:31Z',
dnsServerDomain: 'dns.example.net',
permissions: Object.fromEntries(
sections.map((s) => [
s,
{ canView: s === 'DnsClient' ? canViewDnsClient : true, canModify: true, canDelete: true },
]),
),
},
}
}
function mount(s: ShellSession = session()) {
return render(
<StrictMode>
<ThemeProvider>
<Shell session={s} onLogout={() => {}} />
</ThemeProvider>
</StrictMode>,
)
}
let resolve: ReturnType<typeof vi.spyOn>
let query: ReturnType<typeof vi.spyOn>
beforeEach(() => {
localStorage.clear()
forgetRoot()
window.history.replaceState(null, '', '/')
vi.spyOn(client, 'apiRequest').mockResolvedValue({ kind: 'ok', data: { status: 'ok', response: {} } } as never)
vi.spyOn(dashboard, 'getDashboardStats').mockResolvedValue({ kind: 'ok', data: STATS } as never)
resolve = vi.spyOn(dnsclient, 'resolve').mockResolvedValue({
kind: 'ok',
data: { status: 'ok', response: { result: { answer: 'ok' } } },
} as never)
vi.spyOn(apps, 'listApps').mockResolvedValue({
kind: 'ok',
data: { status: 'ok', response: { apps: [APP] } },
} as never)
query = vi.spyOn(logs, 'queryLogs').mockResolvedValue({
kind: 'ok',
data: { response: { pageNumber: 1, totalPages: 1, totalEntries: 1, entries: [ENTRY] } },
} as never)
})
afterEach(() => vi.restoreAllMocks())
async function pick(row: string, item: string) {
await userEvent.click(await screen.findByRole('button', { name: `Actions for ${row}` }))
await userEvent.click(screen.getByRole('menuitem', { name: item }))
}
const sidebar = () => screen.getByRole('navigation', { name: 'Sections' })
describe('Shell: the jumps of the row menus', () => {
it('Query DNS Server opens DNS Client filled in and runs the query once', async () => {
mount()
await pick('github.com', 'Query DNS Server')
expect(await screen.findByRole('heading', { name: 'DNS Client' })).toBeInTheDocument()
await waitFor(() => expect(resolve).toHaveBeenCalledTimes(1))
expect(resolve.mock.calls[0][1]).toMatchObject({
server: 'this-server',
domain: 'github.com',
type: 'A',
protocol: 'UDP',
dnssec: false,
eDnsClientSubnet: '',
node: '',
})
expect(window.location.pathname).toBe('/dnsclient/')
})
it('coming back to DNS Client later finds a clean form and runs nothing', async () => {
mount()
await pick('github.com', 'Query DNS Server')
await waitFor(() => expect(resolve).toHaveBeenCalledTimes(1))
await userEvent.click(within(sidebar()).getByRole('link', { name: 'Dashboard' }))
await userEvent.click(within(sidebar()).getByRole('link', { name: 'DNS Client' }))
expect(await screen.findByLabelText('Domain')).toHaveValue('')
expect(resolve).toHaveBeenCalledTimes(1)
})
it('Show Query Logs opens Logs › Query Logs filtered by the client and queries', async () => {
mount()
await pick('10.0.0.1', 'Show Query Logs')
expect(await screen.findByText('api.github.com')).toBeInTheDocument()
expect(query).toHaveBeenCalledTimes(1)
expect(query.mock.calls[0][1]).toMatchObject({ qname: '', clientIpAddress: '10.0.0.1', node: '' })
expect(screen.getByLabelText('Client IP Address')).toHaveValue('10.0.0.1')
expect(window.location.pathname).toBe('/logs/query-logs/')
})
it('from a Query Logs row, Query DNS Server goes on to DNS Client with the type of the row', async () => {
mount()
await pick('github.com', 'Show Query Logs')
expect(await screen.findByText('api.github.com')).toBeInTheDocument()
expect(query.mock.calls[0][1]).toMatchObject({ qname: 'github.com', clientIpAddress: '' })
await pick('row 1', 'Query DNS Server')
expect(await screen.findByRole('heading', { name: 'DNS Client' })).toBeInTheDocument()
await waitFor(() => expect(resolve).toHaveBeenCalledTimes(1))
expect(resolve.mock.calls[0][1]).toMatchObject({ domain: 'api.github.com', type: 'HTTPS' })
})
it('a section the user cannot see is not opened', async () => {
mount(session(false))
await pick('github.com', 'Query DNS Server')
expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeInTheDocument()
expect(resolve).not.toHaveBeenCalled()
})
})