feat(web): make instance overview card-first
This commit is contained in:
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user