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( {}} /> , ) } let resolve: ReturnType let query: ReturnType 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() }) })