Files
technitium-console/src/screens/lists/Lists.stale.test.tsx
T
DenozordecandClaude Opus 5.5 ba3cdd08d2
CI / check (push) Successful in 2m39s
Hub / check (push) Successful in 50s
CI / image (push) Successful in 1m30s
Hub / image (push) Successful in 1m37s
UI polish across sections: one server selector, skeletons, inline errors
- Every section puts the hub's server selector in the chrome slot, the
  same place and gap as the Dashboard.
- Data regions load with a Skeleton and fail inline with Retry; KPIs show
  the no-data dash instead of 0 when a server did not answer. Settings
  keeps its header and selector while loading or failing.
- Cache, Apps and Logs headers group secondary or hub-refused actions in
  menus; app descriptions clamp to two lines; Unknown cells say why.
- DNS Client: Protocol label, Resolve and Import together. DHCP and Query
  Logs empty states with actions. View Logs list capped with scroll.
- Blocking: Lists KPI tiles like the Overview, app list table fits its
  panel with one-line URLs. Status re-reads on tab return at most once
  a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:42:28 +07:00

90 lines
3.5 KiB
TypeScript

/*
Stale data in Cache, Allowed and Blocked.
The three are **the same component** with three wrappers, so they inherit the
collection pattern Zones settled, and they had the same gap Zones did: when a
refresh failed the previous tree stayed, which is right, but nothing said it was
no longer current.
It is tested on `Cache` because it is one of the three and the component is
shared; what tells the other two apart is which endpoint they call, not how they
treat a failure.
The node is found by its role: Cache draws a child as a chip whose name is split
at its dots (`ui/DomainName`), so its text is no longer one element.
*/
import { render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as client from '../../api/client'
import { Cache } from './Lists'
afterEach(() => vi.restoreAllMocks())
const NODE = { domain: '', zones: ['home.test'], records: [] }
/** Answers well the first time and badly from the second on. */
function goodThenBroken() {
let n = 0
return vi.spyOn(client, 'apiRequest').mockImplementation(async () => {
n += 1
return n === 1
? ({ kind: 'ok', data: { status: 'ok', response: NODE } } as never)
: ({ kind: 'error', message: 'boom' } as never)
})
}
describe('stale data in the lists', () => {
it('while it goes well, there is no mark', async () => {
vi.spyOn(client, 'apiRequest').mockResolvedValue({
kind: 'ok', data: { status: 'ok', response: NODE },
} as never)
render(<Cache token="t" />)
expect(await screen.findByRole('button', { name: 'home.test' })).toBeInTheDocument()
expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument()
})
it('when a refresh fails, the tree stays and it is reported once', async () => {
goodThenBroken()
const { rerender } = render(<Cache token="t" />)
await screen.findByRole('button', { name: 'home.test' })
/* Changing node is what fires the second `load`. */
rerender(<Cache token="t2" />)
await screen.findByText(/Could not refresh/)
expect(screen.getByRole('button', { name: 'home.test' })).toBeInTheDocument()
expect(screen.getAllByRole('alert')).toHaveLength(1)
expect(screen.queryByText(/boom/)).not.toBeInTheDocument()
})
/* With no previous data there is nothing to go stale: the server notice speaks. */
it('when the first load fails, it is an ordinary error', async () => {
vi.spyOn(client, 'apiRequest').mockResolvedValue({ kind: 'error', message: 'boom' } as never)
render(<Cache token="t" />)
/* The notice carries the server's message and the region repeats it beside its Retry. */
expect((await screen.findAllByText(/boom/)).length).toBeGreaterThan(0)
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument()
expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument()
})
it('and when it goes well again, the mark goes away', async () => {
let n = 0
vi.spyOn(client, 'apiRequest').mockImplementation(async () => {
n += 1
return n === 2
? ({ kind: 'error', message: 'boom' } as never)
: ({ kind: 'ok', data: { status: 'ok', response: NODE } } as never)
})
const { rerender } = render(<Cache token="t" />)
await screen.findByRole('button', { name: 'home.test' })
rerender(<Cache token="t2" />)
await screen.findByText(/Could not refresh/)
rerender(<Cache token="t3" />)
await waitFor(() => expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument())
})
})