mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
feat(cloudflare): add domain move and batch operations in group detail page
This commit is contained in:
@@ -39,17 +39,74 @@ vi.mock('@/lib/services/openflare', async (importOriginal) => {
|
||||
CloudflareService: {
|
||||
...actual.CloudflareService,
|
||||
getGroup: vi.fn(),
|
||||
listGroups: vi.fn(),
|
||||
listAvailableDomains: vi.fn(),
|
||||
updateGroup: vi.fn(),
|
||||
createMember: vi.fn(),
|
||||
updateMember: vi.fn(),
|
||||
syncMember: vi.fn(),
|
||||
removeMember: vi.fn(),
|
||||
moveMember: vi.fn(),
|
||||
batchMoveMembers: vi.fn(),
|
||||
batchRemoveMembers: vi.fn(),
|
||||
},
|
||||
NodeService: { ...actual.NodeService, listNodes: vi.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
const mockGroup = {
|
||||
id: 7,
|
||||
name: '生产节点',
|
||||
primary_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
backup_node: null,
|
||||
active_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
default_proxied: true,
|
||||
enabled: true,
|
||||
member_count: 2,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
|
||||
const mockTargetGroup = {
|
||||
id: 8,
|
||||
name: '备用分组',
|
||||
primary_node: { id: 2, name: '备用节点', ip: '192.0.2.2' },
|
||||
backup_node: null,
|
||||
active_node: { id: 2, name: '备用节点', ip: '192.0.2.2' },
|
||||
default_proxied: false,
|
||||
enabled: true,
|
||||
member_count: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
|
||||
const mockMembers = [
|
||||
{
|
||||
id: 101,
|
||||
group_id: 7,
|
||||
zone_domain_id: 1,
|
||||
domain: 'a.example.com',
|
||||
zone_id: 10,
|
||||
proxied: true,
|
||||
desired_ip: '192.0.2.1',
|
||||
sync_status: 'ok' as const,
|
||||
last_error: '',
|
||||
synced_at: null,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
group_id: 7,
|
||||
zone_domain_id: 2,
|
||||
domain: 'b.example.com',
|
||||
zone_id: 10,
|
||||
proxied: false,
|
||||
desired_ip: '192.0.2.1',
|
||||
sync_status: 'ok' as const,
|
||||
last_error: '',
|
||||
synced_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
function renderPage() {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
@@ -72,25 +129,36 @@ describe('Cloudflare group detail refresh', () => {
|
||||
mockGroupId = '7';
|
||||
mockParamId = '7';
|
||||
vi.mocked(CloudflareService.getGroup).mockReset();
|
||||
vi.mocked(CloudflareService.listGroups).mockReset();
|
||||
vi.mocked(CloudflareService.listAvailableDomains).mockReset();
|
||||
vi.mocked(CloudflareService.moveMember).mockReset();
|
||||
vi.mocked(CloudflareService.batchMoveMembers).mockReset();
|
||||
vi.mocked(CloudflareService.batchRemoveMembers).mockReset();
|
||||
vi.mocked(NodeService.listNodes).mockReset();
|
||||
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
|
||||
window.HTMLElement.prototype.setPointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
vi.mocked(CloudflareService.getGroup).mockResolvedValue({
|
||||
group: {
|
||||
id: 7,
|
||||
name: '生产节点',
|
||||
primary_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
backup_node: null,
|
||||
active_node: { id: 1, name: '主节点', ip: '192.0.2.1' },
|
||||
default_proxied: true,
|
||||
enabled: true,
|
||||
member_count: 0,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
group: mockGroup,
|
||||
members: [],
|
||||
});
|
||||
vi.mocked(CloudflareService.listGroups).mockResolvedValue([
|
||||
mockGroup,
|
||||
mockTargetGroup,
|
||||
]);
|
||||
vi.mocked(CloudflareService.listAvailableDomains).mockResolvedValue([]);
|
||||
vi.mocked(NodeService.listNodes).mockResolvedValue([]);
|
||||
vi.mocked(CloudflareService.moveMember).mockResolvedValue({
|
||||
...mockMembers[0],
|
||||
group_id: 8,
|
||||
});
|
||||
vi.mocked(CloudflareService.batchMoveMembers).mockResolvedValue(undefined);
|
||||
vi.mocked(CloudflareService.batchRemoveMembers).mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -141,3 +209,174 @@ describe('Cloudflare group detail refresh', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cloudflare group detail move and batch operations', () => {
|
||||
beforeEach(() => {
|
||||
mockGroupId = '7';
|
||||
mockParamId = '7';
|
||||
vi.mocked(CloudflareService.getGroup).mockReset();
|
||||
vi.mocked(CloudflareService.listGroups).mockReset();
|
||||
vi.mocked(CloudflareService.listAvailableDomains).mockReset();
|
||||
vi.mocked(CloudflareService.moveMember).mockReset();
|
||||
vi.mocked(CloudflareService.batchMoveMembers).mockReset();
|
||||
vi.mocked(CloudflareService.batchRemoveMembers).mockReset();
|
||||
vi.mocked(NodeService.listNodes).mockReset();
|
||||
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
|
||||
window.HTMLElement.prototype.setPointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
|
||||
window.HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
vi.mocked(CloudflareService.getGroup).mockResolvedValue({
|
||||
group: mockGroup,
|
||||
members: mockMembers,
|
||||
});
|
||||
vi.mocked(CloudflareService.listGroups).mockResolvedValue([
|
||||
mockGroup,
|
||||
mockTargetGroup,
|
||||
]);
|
||||
vi.mocked(CloudflareService.listAvailableDomains).mockResolvedValue([]);
|
||||
vi.mocked(NodeService.listNodes).mockResolvedValue([]);
|
||||
vi.mocked(CloudflareService.moveMember).mockResolvedValue({
|
||||
...mockMembers[0],
|
||||
group_id: 8,
|
||||
});
|
||||
vi.mocked(CloudflareService.batchMoveMembers).mockResolvedValue(undefined);
|
||||
vi.mocked(CloudflareService.batchRemoveMembers).mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports selecting all members and toggling selection', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('a.example.com')).toBeVisible();
|
||||
expect(screen.getByText('b.example.com')).toBeVisible();
|
||||
|
||||
// Initially batch toolbar is not visible
|
||||
expect(screen.queryByText(/已选择/)).not.toBeInTheDocument();
|
||||
|
||||
// Select all via header checkbox
|
||||
const selectAllCheckbox = screen.getByRole('checkbox', {
|
||||
name: 'Select all',
|
||||
});
|
||||
fireEvent.click(selectAllCheckbox);
|
||||
|
||||
// Batch toolbar should appear with count 2
|
||||
expect(await screen.findByText('已选择 2 项')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '批量移动' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '批量删除' })).toBeVisible();
|
||||
|
||||
// Clear selection
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消选择' }));
|
||||
expect(screen.queryByText(/已选择/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens single move dialog and triggers moveMember', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('a.example.com')).toBeVisible();
|
||||
|
||||
// Click single move button on first row
|
||||
const moveButtons = screen.getAllByRole('button', { name: '移动' });
|
||||
fireEvent.click(moveButtons[0]);
|
||||
|
||||
// Move dialog should show single member title
|
||||
expect(
|
||||
await screen.findByRole('heading', {
|
||||
name: '移动域名「a.example.com」',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// Open target group select
|
||||
const selectTrigger = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(selectTrigger, { key: 'ArrowDown' });
|
||||
|
||||
// Select target group
|
||||
const targetOption = await screen.findByRole('option', {
|
||||
name: /备用分组/,
|
||||
});
|
||||
fireEvent.click(targetOption);
|
||||
|
||||
// Submit move
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认移动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CloudflareService.moveMember).toHaveBeenCalledWith(7, 101, 8);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens batch move dialog and triggers batchMoveMembers', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('a.example.com')).toBeVisible();
|
||||
|
||||
// Select all members
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'Select all' }));
|
||||
expect(await screen.findByText('已选择 2 项')).toBeVisible();
|
||||
|
||||
// Click batch move button
|
||||
fireEvent.click(screen.getByRole('button', { name: '批量移动' }));
|
||||
|
||||
// Dialog title should show batch title
|
||||
expect(
|
||||
await screen.findByRole('heading', {
|
||||
name: '批量移动域名(共 2 项)',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// Open target group select
|
||||
const selectTrigger = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(selectTrigger, { key: 'ArrowDown' });
|
||||
|
||||
// Select target group
|
||||
const targetOption = await screen.findByRole('option', {
|
||||
name: /备用分组/,
|
||||
});
|
||||
fireEvent.click(targetOption);
|
||||
|
||||
// Submit move
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认移动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CloudflareService.batchMoveMembers).toHaveBeenCalledWith(
|
||||
7,
|
||||
[101, 102],
|
||||
8,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens batch remove dialog and triggers batchRemoveMembers', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('a.example.com')).toBeVisible();
|
||||
|
||||
// Select first member only
|
||||
fireEvent.click(
|
||||
screen.getByRole('checkbox', { name: 'Select a.example.com' }),
|
||||
);
|
||||
expect(await screen.findByText('已选择 1 项')).toBeVisible();
|
||||
|
||||
// Click batch remove
|
||||
fireEvent.click(screen.getByRole('button', { name: '批量删除' }));
|
||||
|
||||
// Confirmation dialog appears
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '确认批量删除域名' }),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
screen.getByText(/确认从当前分组移出已选中的 1 个域名吗?/),
|
||||
).toBeVisible();
|
||||
|
||||
// Confirm deletion
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认删除' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(CloudflareService.batchRemoveMembers).toHaveBeenCalledWith(
|
||||
7,
|
||||
[101],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,4 +55,29 @@ describe('CloudflareService', () => {
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls move and batch member endpoints with correct paths and payloads', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue(response({}));
|
||||
|
||||
await CloudflareService.moveMember(7, 9, 8);
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/d/cloudflare/groups/7/members/9/move',
|
||||
{ target_group_id: 8 },
|
||||
undefined,
|
||||
);
|
||||
|
||||
await CloudflareService.batchMoveMembers(7, [9, 10], 8);
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/d/cloudflare/groups/7/members/batch-move',
|
||||
{ member_ids: [9, 10], target_group_id: 8 },
|
||||
undefined,
|
||||
);
|
||||
|
||||
await CloudflareService.batchRemoveMembers(7, [9, 10]);
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/d/cloudflare/groups/7/members/batch-remove',
|
||||
{ member_ids: [9, 10] },
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user