- 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>
90 lines
3.5 KiB
TypeScript
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())
|
|
})
|
|
})
|