feat(web): make instance overview card-first

This commit is contained in:
chick
2026-07-18 23:23:41 +08:00
parent abfd07f8a3
commit 2411801240
5 changed files with 719 additions and 223 deletions
+81 -5
View File
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { cleanup, render, screen, within } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -7,6 +7,7 @@ import { AppShell, type InstanceContext } from './app-shell.js';
import type { AuditDataSource } from './audit/audit-page.js';
import type { EventStreamClient } from './events/event-stream-client.js';
import { type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
import type { InstanceDataSource } from './instances/instance-api-data-source.js';
import type { JobsDataSource } from './jobs/jobs-page.js';
afterEach(() => {
@@ -91,6 +92,82 @@ describe('React AppShell and Fleet vertical slice', () => {
).toBe(false);
});
it('makes cards, add, detail, edit, and safe deletion available from the overview', async () => {
const user = userEvent.setup();
const remove = vi.fn<InstanceDataSource['delete']>().mockResolvedValue(undefined);
const instanceDataSource: InstanceDataSource = {
get: vi.fn().mockResolvedValue({
id: 'bravo',
name: 'Bravo',
origin: 'http://bravo.example:8080',
tags: ['west'],
revision: 7,
credentialConfigured: true,
}),
create: vi.fn(),
update: vi.fn(),
testConnection: vi.fn(),
delete: remove,
};
render(
<AppShell
pathname="/fleet"
fleetDataSource={source(async () => snapshot)}
instanceDataSource={instanceDataSource}
/>,
);
expect(await screen.findByRole('region', { name: '实例卡片' })).toBeTruthy();
expect(screen.getByRole('link', { name: '添加实例' }).getAttribute('href')).toBe(
'/instances/new',
);
const card = screen.getByRole('article', { name: 'Bravo 实例概览' });
expect(within(card).getByText('40 ms')).toBeTruthy();
expect(within(card).getByText('2.0')).toBeTruthy();
expect(within(card).getByRole('link', { name: '管理 Bravo' }).getAttribute('href')).toBe(
'/instances/bravo/overview',
);
expect(within(card).getByRole('link', { name: '编辑 Bravo' }).getAttribute('href')).toBe(
'/settings/instances/bravo',
);
await user.click(within(card).getByRole('button', { name: '删除 Bravo' }));
expect(within(card).getByText('删除后无法撤销。')).toBeTruthy();
await user.type(within(card).getByRole('textbox', { name: '输入 bravo 以确认删除' }), 'bravo');
await user.click(within(card).getByRole('button', { name: '确认删除 Bravo' }));
expect(remove).toHaveBeenCalledWith('bravo', 7);
expect(within(card).getByRole('status').textContent).toContain('删除请求已提交');
});
it('loads route-owned instance context so overview-card navigation opens detail management', async () => {
const instanceDataSource: InstanceDataSource = {
get: vi.fn().mockResolvedValue({
id: 'bravo',
name: 'Bravo',
origin: 'http://bravo.example:8080',
tags: ['west'],
revision: 7,
credentialConfigured: true,
}),
create: vi.fn(),
update: vi.fn(),
testConnection: vi.fn(),
delete: vi.fn(),
};
render(
<AppShell pathname="/instances/bravo/overview" instanceDataSource={instanceDataSource} />,
);
expect(screen.getByRole('status').textContent).toContain('正在加载实例');
expect(await screen.findByText('Bravo')).toBeTruthy();
expect(screen.getByRole('heading', { name: '概览' })).toBeTruthy();
expect(screen.getByRole('link', { name: '编辑实例' }).getAttribute('href')).toBe(
'/settings/instances/bravo',
);
});
it('supports accessible search, status filtering, sorting, and visible selection', async () => {
const user = userEvent.setup();
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
@@ -164,7 +241,6 @@ describe('React AppShell and Fleet vertical slice', () => {
});
it('paginates fleet rows and selects only the current page', async () => {
const user = userEvent.setup();
const many: FleetSnapshot = {
instances: Array.from({ length: 11 }, (_, index) => ({
id: `instance-${String(index + 1).padStart(2, '0')}`,
@@ -178,9 +254,9 @@ describe('React AppShell and Fleet vertical slice', () => {
expect(screen.queryByRole('row', { name: /Instance 11/ })).toBeNull();
expect(screen.getByText('第 1 页,共 2 页')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
fireEvent.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
expect(screen.getByText('已选择 10 项')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '下一页' }));
fireEvent.click(screen.getByRole('button', { name: '下一页' }));
expect(await screen.findByRole('row', { name: /Instance 11/ })).toBeTruthy();
expect(screen.getByText('第 2 页,共 2 页')).toBeTruthy();
});
@@ -205,7 +281,7 @@ describe('React AppShell and Fleet vertical slice', () => {
rerender(<AppShell pathname="/instances/someone-else/messages" instance={instance} />);
expect(screen.queryByText('Owner modem')).toBeNull();
expect(screen.getByText(/此路由缺少实例上下文/i)).toBeTruthy();
expect(screen.getByRole('status').textContent).toContain('正在加载实例');
});
it.each([