feat(web): add runnable fleet console foundation
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildFleetTableViewModel,
|
||||
fleetStatusKind,
|
||||
type FleetInstance,
|
||||
type FleetStatus,
|
||||
} from './fleet-table-view-model.js';
|
||||
|
||||
const instances: readonly FleetInstance[] = [
|
||||
{
|
||||
id: 'charlie',
|
||||
name: 'Charlie',
|
||||
url: 'https://charlie.example',
|
||||
description: 'Warehouse router',
|
||||
tags: ['west'],
|
||||
},
|
||||
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example', tags: ['east'] },
|
||||
{ id: 'bravo', name: 'Bravo', url: 'https://bravo.example' },
|
||||
{ id: 'delta', name: '', url: 'https://delta.example' },
|
||||
];
|
||||
|
||||
const statuses: ReadonlyMap<string, FleetStatus> = new Map([
|
||||
[
|
||||
'charlie',
|
||||
{ reachable: true, authenticated: true, latencyMs: 55, summary: { carrier: 'Acme' } },
|
||||
],
|
||||
['alpha', { reachable: false, latencyMs: 8 }],
|
||||
['bravo', { reachable: true, authenticated: false, latencyMs: 20 }],
|
||||
]);
|
||||
|
||||
describe('fleet table view model', () => {
|
||||
it('classifies missing, unreachable, authentication-required, and online status', () => {
|
||||
expect(fleetStatusKind(undefined)).toBe('unknown');
|
||||
expect(fleetStatusKind({ reachable: false })).toBe('offline');
|
||||
expect(fleetStatusKind({ reachable: true, authenticated: false })).toBe('auth');
|
||||
expect(fleetStatusKind({ reachable: true, authenticated: true })).toBe('online');
|
||||
});
|
||||
|
||||
it('builds stable rows and defaults to ascending display-name order', () => {
|
||||
const model = buildFleetTableViewModel(instances, statuses);
|
||||
|
||||
expect(model.rows.map((row) => [row.id, row.displayName, row.statusKind])).toEqual([
|
||||
['alpha', 'Alpha', 'offline'],
|
||||
['bravo', 'Bravo', 'auth'],
|
||||
['charlie', 'Charlie', 'online'],
|
||||
['delta', 'delta', 'unknown'],
|
||||
]);
|
||||
expect(model.totalCount).toBe(4);
|
||||
expect(model.visibleCount).toBe(4);
|
||||
expect(model.emptyReason).toBeNull();
|
||||
});
|
||||
|
||||
it('searches user-visible instance and status fields case-insensitively', () => {
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { query: ' ACME ' }).rows.map((row) => row.id),
|
||||
).toEqual(['charlie']);
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { query: 'WEST' }).rows.map((row) => row.id),
|
||||
).toEqual(['charlie']);
|
||||
});
|
||||
|
||||
it('filters by status and reports a useful empty reason', () => {
|
||||
const filtered = buildFleetTableViewModel(instances, statuses, { filter: 'online' });
|
||||
expect(filtered.rows.map((row) => row.id)).toEqual(['charlie']);
|
||||
|
||||
expect(buildFleetTableViewModel([], new Map()).emptyReason).toBe('config');
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { query: 'not present' }).emptyReason,
|
||||
).toBe('search');
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { filter: 'online', query: 'Alpha' })
|
||||
.emptyReason,
|
||||
).toBe('search');
|
||||
expect(
|
||||
buildFleetTableViewModel([instances[1]!], statuses, { filter: 'online' }).emptyReason,
|
||||
).toBe('filter');
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { filter: 'online', query: ' ' }).emptyReason,
|
||||
).toBeNull();
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, { filter: 'offline', query: 'Bravo' })
|
||||
.emptyReason,
|
||||
).toBe('search');
|
||||
});
|
||||
|
||||
it('sorts latency with missing latency last and status by operational priority', () => {
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, {
|
||||
sort: { column: 'latency', direction: 'asc' },
|
||||
}).rows.map((row) => row.id),
|
||||
).toEqual(['alpha', 'bravo', 'charlie', 'delta']);
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, {
|
||||
sort: { column: 'latency', direction: 'desc' },
|
||||
}).rows.map((row) => row.id),
|
||||
).toEqual(['charlie', 'bravo', 'alpha', 'delta']);
|
||||
expect(
|
||||
buildFleetTableViewModel(instances, statuses, {
|
||||
sort: { column: 'status', direction: 'asc' },
|
||||
}).rows.map((row) => row.id),
|
||||
).toEqual(['charlie', 'bravo', 'alpha', 'delta']);
|
||||
});
|
||||
|
||||
it('normalizes selection to known instances and exposes visible selection state', () => {
|
||||
const model = buildFleetTableViewModel(instances, statuses, {
|
||||
filter: 'online',
|
||||
selectedIds: new Set(['charlie', 'alpha', 'missing']),
|
||||
});
|
||||
|
||||
expect(model.rows[0]?.selected).toBe(true);
|
||||
expect(model.selectedIds).toEqual(['alpha', 'charlie']);
|
||||
expect(model.visibleSelection).toEqual({
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
selectedCount: 1,
|
||||
rowCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mutate input arrays while sorting', () => {
|
||||
const originalOrder = instances.map((instance) => instance.id);
|
||||
buildFleetTableViewModel(instances, statuses, { sort: { column: 'name', direction: 'desc' } });
|
||||
expect(instances.map((instance) => instance.id)).toEqual(originalOrder);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user