Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 104 additions & 5 deletions src/features/admin/hosts/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
import { apiClient } from '@/config/apiClient';
import {
describeUnresolvedOrganization,
formatCpu,
formatUsage,
hostMatchesSearch,
HostsIndex,
resolveOrganization,
} from '@/features/admin/hosts/index';
import type { AdminHost } from '@/integrations/api/api.patch';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@/config/apiClient', () => ({ apiClient: { get: vi.fn() } }));
const mockedGet = vi.mocked(apiClient.get);
Expand Down Expand Up @@ -99,6 +101,14 @@ beforeEach(() => {
});
afterEach(cleanup);

beforeAll(() => {
// Radix Dialog relies on DOM APIs jsdom doesn't implement.
Element.prototype.hasPointerCapture ??= () => false;
Element.prototype.setPointerCapture ??= () => undefined;
Element.prototype.releasePointerCapture ??= () => undefined;
Element.prototype.scrollIntoView ??= () => undefined;
});

function renderPage() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
Expand All @@ -115,13 +125,84 @@ describe('HostsIndex', () => {
expect(hostRequests()).toEqual(['/Admin/Host/?status=RUNNING']);
});

it('labels tenant and dedicated organizations by name', async () => {
it('lists only the organizations a host is dedicated to, by name', async () => {
renderPage();
await waitFor(() => expect(screen.getByText('org-1 (Acme)')).toBeTruthy());
expect(screen.getByText('Dedicated to org-2 (Globex)')).toBeTruthy();
await waitFor(() => expect(screen.getByText('org-2 (Globex)')).toBeTruthy());
expect(screen.getByText('Shared')).toBeTruthy();
expect(screen.queryByText('org-1 (Acme)')).toBeNull();
expect(screen.getByText('GPU 1× nvidia-l4')).toBeTruthy();
});

it("opens a host's details from its row, with the organizations that have instances on it", async () => {
renderPage();
await screen.findByText('shared-1');
expect(screen.queryByRole('dialog')).toBeNull();
await act(async () => {
fireEvent.click(screen.getByText('h-shared'));
});
const panel = screen.getByRole('dialog');
expect(await within(panel).findByText('org-1 (Acme)')).toBeTruthy();
expect(within(panel).getByText('3 instances')).toBeTruthy();
expect(within(panel).getByText('10.0.0.7')).toBeTruthy();
});

it('opens the details from the host name button', async () => {
renderPage();
await act(async () => {
fireEvent.click(await screen.findByRole('button', { name: 'dedicated-1' }));
});
expect(within(screen.getByRole('dialog')).getByText('None.')).toBeTruthy();
});

it('keeps the panel on the clicked host while a filter change refetches without it', async () => {
renderPage();
await act(async () => {
fireEvent.click(await screen.findByText('h-shared'));
});
mockedGet.mockImplementation(async (url) => {
const path = url as string;
if (path.startsWith('/Admin/Organization/')) { return { data: ORGS } as never; }
if (path.startsWith('/Location/')) { return { data: [] } as never; }
return { data: HOSTS.filter((candidate) => candidate.id !== 'h-shared') } as never;
});
await act(async () => {
fireEvent.change(screen.getByLabelText('Organization'), { target: { value: 'org-2' } });
});
await waitFor(() => expect(hostRequests()).toContain('/Admin/Host/?status=RUNNING&organizationId=org-2'));
await waitFor(() => expect(screen.queryByText('dedicated-1')).toBeTruthy());
expect(within(screen.getByRole('dialog')).getByText('shared-1')).toBeTruthy();
});

it('returns focus to the host name when the details close', async () => {
renderPage();
const button = await screen.findByRole('button', { name: 'shared-1' });
await act(async () => {
fireEvent.click(button);
});
await act(async () => {
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
});
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(document.activeElement).toBe(button);
});

it('shows the location code and provider, and a host id only when it differs from the name', async () => {
mockedGet.mockImplementation(async (url) => {
const path = url as string;
if (path.startsWith('/Admin/Organization/')) { return { data: ORGS } as never; }
if (path.startsWith('/Location/')) { return { data: [] } as never; }
return { data: [...HOSTS, host({ id: 'hos-same', name: 'hos-same', fqdn: 'hos-same.example.com' })] } as never;
});
renderPage();
await screen.findByText('shared-1');
expect(screen.getAllByText('us-ord · linode')).toHaveLength(3);
expect(screen.queryByText('Chicago, Illinois, USA')).toBeNull();
expect(screen.getAllByText('25% of 8 cores')).toHaveLength(3);
expect(screen.getByText('h-shared')).toBeTruthy();
expect(screen.getAllByText('hos-same')).toHaveLength(1);
expect(screen.getByText('hos-same.example.com')).toBeTruthy();
});

it('narrows the rows client-side without another request', async () => {
renderPage();
await screen.findByText('shared-1');
Expand Down Expand Up @@ -208,6 +289,24 @@ describe('HostsIndex', () => {
});
});

describe('formatCpu / formatUsage', () => {
it('shows sampled CPU as a share of the host cores', () => {
expect(formatCpu(2, 8)).toBe('25% of 8 cores');
expect(formatCpu(0.1, 4)).toBe('3% of 4 cores');
expect(formatCpu(null, 4)).toBe('? of 4 cores');
expect(formatCpu(1, null)).toBe('—');
});

it('treats the -1 a host leaves for an unreported value as unknown', () => {
expect(formatCpu(-1, 4)).toBe('? of 4 cores');
expect(formatUsage(-1, 16384, 1024)).toBe('? / 16');
expect(formatUsage(8499.2, 16384, 1024)).toBe('8.3 / 16');
expect(formatUsage(5, -1)).toBe('—');
expect(formatUsage(5, 0)).toBe('—');
expect(formatCpu(1, -1)).toBe('—');
});
});

describe('describeUnresolvedOrganization', () => {
it('claims no match only for a fully loaded directory', () => {
expect(describeUnresolvedOrganization('Acme', 'complete')).toBe('No organization matches “Acme”.');
Expand Down
Loading
Loading