fix(nodes): per-category client email lists behind the node count chips

NodeView carried only the category counts, so the nodes page chips
could hover to nothing but the category title. GetAll/recountByGuid
now emit online/active/disabled/depleted email lists from the same
bucketing pass as the counts (statuses gain the client email;
the online list is the counted set), and the chips pop the list
over with the inbounds page's popover pattern.
This commit is contained in:
Muse
2026-10-22 15:33:46 +00:00
parent e310abcba0
commit c91fb454a8
9 changed files with 376 additions and 45 deletions
@@ -0,0 +1,101 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { fireEvent, waitFor, within } from '@testing-library/react';
import NodeList from '@/pages/nodes/NodeList';
import type { NodeRecord } from '@/schemas/node';
import { renderWithProviders } from './test-utils';
const noop = () => {};
function renderNodes(nodes: NodeRecord[]) {
return renderWithProviders(
<NodeList
nodes={nodes}
isMobile={false}
selectedIds={[]}
onSelectionChange={noop}
onAdd={noop}
onMtls={noop}
onEdit={noop}
onDelete={noop}
onProbe={noop}
onToggleEnable={noop}
onUpdateNode={noop}
onUpdateSelected={noop}
/>,
);
}
function rowFor(container: HTMLElement, name: string): HTMLElement {
const cell = within(container).getByText(name);
const row = cell.closest('tr');
if (!row) throw new Error(`row not found for ${name}`);
return row as HTMLElement;
}
// The nodes page category chips used to hover to a bare category title.
// When the backend sends the category's email list, hovering a chip must
// pop the list over (same pattern as the inbounds page); without a list
// the chip keeps the plain title tooltip.
describe('NodeList clients-cell category chips', () => {
afterEach(() => vi.restoreAllMocks());
it('pops the online and depleted email lists on hover', async () => {
const { container } = renderNodes([
{
id: 5,
name: 'agent-node',
enable: true,
status: 'online',
nodeType: 'agent',
clientCount: 9,
activeCount: 2,
activeEmails: ['active-a@x', 'active-b@x'],
depletedCount: 3,
depletedEmails: ['dep-a@x', 'dep-b@x', 'dep-c@x'],
onlineCount: 1,
onlineEmails: ['online@x'],
},
]);
const row = rowFor(container, 'agent-node');
fireEvent.mouseEnter(within(row).getByText('1'));
await waitFor(
() => {
expect(document.body.textContent).toContain('online@x');
},
{ timeout: 5000 },
);
fireEvent.mouseEnter(within(row).getByText('3'));
await waitFor(
() => {
expect(document.body.textContent).toContain('dep-b@x');
},
{ timeout: 5000 },
);
});
it('keeps the title tooltip when a category has no email list', async () => {
const { container } = renderNodes([
{
id: 6,
name: 'bare-node',
enable: true,
status: 'online',
clientCount: 4,
onlineCount: 1,
},
]);
const row = rowFor(container, 'bare-node');
fireEvent.mouseEnter(within(row).getByText('1'));
await waitFor(() => {
expect(document.body.textContent).toContain('Online');
});
expect(document.querySelector('.client-email-list')).toBeNull();
});
});