import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { App } from '../app';
import { ServerProvider } from '../context/server-context';
import { ApiClient } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { StreamEvent } from '../types';
describe('policy draft editor', () => {
afterEach(() => {
location.hash = '';
});
it('builds multiple visual groups and atomically applies the validated draft', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/policies';
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner', displayName: 'owner', roles: ['owner'], permissions: ['policy_publish'], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
vi.spyOn(client, 'policies').mockResolvedValue({
items: [{ id: 'policy-3', version: 3, name: 'active', state: 'ACTIVE', ruleCount: 1, updatedAt: '2026-01-01T00:00:00Z' }],
});
vi.spyOn(client, 'policyCommands').mockResolvedValue({ commands: [] });
vi.spyOn(client, 'policy').mockResolvedValue({
id: 'policy-3',
version: 3,
name: 'active',
state: 'ACTIVE',
ruleCount: 1,
updatedAt: '2026-01-01T00:00:00Z',
checksum: 'sha256',
rules: [{ id: 'deny-kill', enabled: true } as never],
validation: { valid: true, errors: [], warnings: [] },
});
const stage = vi.spyOn(client, 'stagePolicy').mockResolvedValue({
id: 'operation-1', kind: 'policy.draft.stage', state: 'SUCCEEDED',
policy: { id: 'policy-4', version: 4, name: 'draft', state: 'DRAFT', groupCount: 2, ruleCount: 2, updatedAt: '2026-01-01T00:01:00Z' },
});
const publish = vi.spyOn(client, 'publishPolicy').mockResolvedValue({
id: 'operation-2', kind: 'policy.publish', state: 'SUCCEEDED',
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Edit' }));
const ruleId = await screen.findByLabelText('规则 ID');
expect(ruleId).toHaveValue('deny-kill');
fireEvent.change(ruleId, { target: { value: 'deny-give' } });
await user.click(screen.getByRole('button', { name: '新增策略组 / Add group' }));
expect(screen.getAllByLabelText('策略组 ID')).toHaveLength(2);
fireEvent.change(screen.getByLabelText('Draft description and audit reason'), { target: { value: 'Restrict give' } });
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
await waitFor(() => expect(stage).toHaveBeenCalled());
expect(stage.mock.calls[0][0]).toBe(3);
expect(stage.mock.calls[0][1]).toBe('Restrict give');
expect(stage.mock.calls[0][2][0]).toMatchObject({
id: 'deny-give', enabled: true, tier: 'DEFAULT', effect: 'DENY',
groupId: 'default', groupEnabled: true,
subject: { actorIds: [], roles: [], minimumOpLevel: null, maximumOpLevel: null },
command: { commandIds: [], pathPrefix: [] },
});
expect(new Set(stage.mock.calls[0][2].map((rule) => rule.groupId)).size).toBe(2);
await waitFor(() => expect(publish).toHaveBeenCalledWith('policy-4', 4, 'Restrict give'));
expect(await screen.findByText(/Policy published atomically and applied immediately/)).toBeInTheDocument();
});
it('searches and adds live trigger commands without duplicating equivalent manual command ids', async () => {
localStorage.setItem('xfesm.locale', 'zh-CN');
location.hash = '/policies';
const client = new ApiClient();
let emit: ((event: StreamEvent) => void) | undefined;
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner', displayName: 'owner', roles: ['owner'], permissions: ['policy_publish'], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockImplementation((listener) => {
emit = listener;
return () => undefined;
});
vi.spyOn(client, 'policies').mockResolvedValue({
items: [{ id: 'policy-7', version: 7, name: 'active', state: 'ACTIVE', ruleCount: 1, updatedAt: '2026-01-01T00:00:00Z' }],
});
vi.spyOn(client, 'policy').mockResolvedValue({
id: 'policy-7', version: 7, name: 'active', state: 'ACTIVE', ruleCount: 1,
updatedAt: '2026-01-01T00:00:00Z', checksum: 'sha256',
rules: [{ id: 'command-rule', enabled: true, command: { commandIds: ['/notice'], pathPrefix: [] } } as never],
validation: { valid: true, errors: [], warnings: [] },
});
const catalog = vi.spyOn(client, 'policyCommands').mockResolvedValue({ commands: [
{
id: 'minecraft:notice', root: 'notice', aliases: ['announce'], source: 'server',
usages: ['/notice <message>'],
},
{
id: 'minecraft:welcome', root: 'welcome', source: 'trigger', triggerId: 'trigger-welcome',
triggerName: '欢迎指令', arguments: ['target', 'message'],
usages: ['/welcome <target> <message>'],
},
] });
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: '编辑' }));
await waitFor(() => expect(catalog).toHaveBeenCalledOnce());
await user.click(screen.getByRole('button', { name: '添加命令 / Add command' }));
const dialog = await screen.findByRole('dialog', { name: '命令目录 / Command catalog' });
expect(dialog.closest('.page')).toBeNull();
await user.type(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }), 'target');
expect(screen.getByText('minecraft:welcome')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '添加命令 minecraft:welcome' }));
expect(screen.getByLabelText('命令 ID(如 minecraft:kill)')).toHaveValue('/notice\nminecraft:welcome');
expect(screen.getByRole('button', { name: '命令 minecraft:welcome 已添加' })).toBeDisabled();
await user.clear(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }));
await user.selectOptions(screen.getByLabelText('命令来源 / Command source'), 'server');
await user.type(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }), 'minecraft:notice');
expect(screen.getByRole('button', { name: '命令 minecraft:notice 已添加' })).toBeDisabled();
expect(screen.getByLabelText('命令 ID(如 minecraft:kill)')).toHaveValue('/notice\nminecraft:welcome');
await waitFor(() => expect(emit).toBeDefined());
act(() => emit?.({ type: 'triggers-changed', data: {} }));
await waitFor(() => expect(catalog).toHaveBeenCalledTimes(2));
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog', { name: '命令目录 / Command catalog' })).not.toBeInTheDocument();
});
});
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { App } from '../app';
import { ServerProvider } from '../context/server-context';
import { ApiClient } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { StreamEvent } from '../types';
describe('policy draft editor', () => {
afterEach(() => {
location.hash = '';
});
it('builds multiple visual groups and atomically applies the validated draft', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/policies';
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner', displayName: 'owner', roles: ['owner'], permissions: ['policy_publish'], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
vi.spyOn(client, 'policies').mockResolvedValue({
items: [{ id: 'policy-3', version: 3, name: 'active', state: 'ACTIVE', ruleCount: 1, updatedAt: '2026-01-01T00:00:00Z' }],
});
vi.spyOn(client, 'policyCommands').mockResolvedValue({ commands: [] });
vi.spyOn(client, 'policy').mockResolvedValue({
id: 'policy-3',
version: 3,
name: 'active',
state: 'ACTIVE',
ruleCount: 1,
updatedAt: '2026-01-01T00:00:00Z',
checksum: 'sha256',
rules: [{ id: 'deny-kill', enabled: true } as never],
validation: { valid: true, errors: [], warnings: [] },
});
const stage = vi.spyOn(client, 'stagePolicy').mockResolvedValue({
id: 'operation-1', kind: 'policy.draft.stage', state: 'SUCCEEDED',
policy: { id: 'policy-4', version: 4, name: 'draft', state: 'DRAFT', groupCount: 2, ruleCount: 2, updatedAt: '2026-01-01T00:01:00Z' },
});
const publish = vi.spyOn(client, 'publishPolicy').mockResolvedValue({
id: 'operation-2', kind: 'policy.publish', state: 'SUCCEEDED',
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Edit' }));
const ruleId = await screen.findByLabelText('规则 ID');
expect(ruleId).toHaveValue('deny-kill');
fireEvent.change(ruleId, { target: { value: 'deny-give' } });
await user.click(screen.getByRole('button', { name: '新增策略组 / Add group' }));
expect(screen.getAllByLabelText('策略组 ID')).toHaveLength(2);
fireEvent.change(screen.getByLabelText('Draft description and audit reason'), { target: { value: 'Restrict give' } });
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
await waitFor(() => expect(stage).toHaveBeenCalled());
expect(stage.mock.calls[0][0]).toBe(3);
expect(stage.mock.calls[0][1]).toBe('Restrict give');
expect(stage.mock.calls[0][2][0]).toMatchObject({
id: 'deny-give', enabled: true, tier: 'DEFAULT', effect: 'DENY',
groupId: 'default', groupEnabled: true,
subject: { actorIds: [], roles: [], minimumOpLevel: null, maximumOpLevel: null },
command: { commandIds: [], pathPrefix: [] },
});
expect(new Set(stage.mock.calls[0][2].map((rule) => rule.groupId)).size).toBe(2);
await waitFor(() => expect(publish).toHaveBeenCalledWith('policy-4', 4, 'Restrict give'));
expect(await screen.findByText(/Policy published atomically and applied immediately/)).toBeInTheDocument();
});
it('searches and adds live trigger commands without duplicating equivalent manual command ids', async () => {
localStorage.setItem('xfesm.locale', 'zh-CN');
location.hash = '/policies';
const client = new ApiClient();
let emit: ((event: StreamEvent) => void) | undefined;
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner', displayName: 'owner', roles: ['owner'], permissions: ['policy_publish'], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockImplementation((listener) => {
emit = listener;
return () => undefined;
});
vi.spyOn(client, 'policies').mockResolvedValue({
items: [{ id: 'policy-7', version: 7, name: 'active', state: 'ACTIVE', ruleCount: 1, updatedAt: '2026-01-01T00:00:00Z' }],
});
vi.spyOn(client, 'policy').mockResolvedValue({
id: 'policy-7', version: 7, name: 'active', state: 'ACTIVE', ruleCount: 1,
updatedAt: '2026-01-01T00:00:00Z', checksum: 'sha256',
rules: [{ id: 'command-rule', enabled: true, command: { commandIds: ['/notice'], pathPrefix: [] } } as never],
validation: { valid: true, errors: [], warnings: [] },
});
const catalog = vi.spyOn(client, 'policyCommands').mockResolvedValue({ commands: [
{
id: 'minecraft:notice', root: 'notice', aliases: ['announce'], source: 'server',
usages: ['/notice <message>'],
},
{
id: 'minecraft:welcome', root: 'welcome', source: 'trigger', triggerId: 'trigger-welcome',
triggerName: '欢迎指令', arguments: ['target', 'message'],
usages: ['/welcome <target> <message>'],
},
] });
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: '编辑' }));
await waitFor(() => expect(catalog).toHaveBeenCalledOnce());
await user.click(screen.getByRole('button', { name: '添加命令 / Add command' }));
const dialog = await screen.findByRole('dialog', { name: '命令目录 / Command catalog' });
expect(dialog.closest('.page')).toBeNull();
await user.type(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }), 'target');
expect(screen.getByText('minecraft:welcome')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '添加命令 minecraft:welcome' }));
expect(screen.getByLabelText('命令 ID(如 minecraft:kill)')).toHaveValue('/notice\nminecraft:welcome');
expect(screen.getByRole('button', { name: '命令 minecraft:welcome 已添加' })).toBeDisabled();
await user.clear(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }));
await user.selectOptions(screen.getByLabelText('命令来源 / Command source'), 'server');
await user.type(screen.getByRole('searchbox', { name: '搜索命令 / Search commands' }), 'minecraft:notice');
expect(screen.getByRole('button', { name: '命令 minecraft:notice 已添加' })).toBeDisabled();
expect(screen.getByLabelText('命令 ID(如 minecraft:kill)')).toHaveValue('/notice\nminecraft:welcome');
await waitFor(() => expect(emit).toBeDefined());
act(() => emit?.({ type: 'triggers-changed', data: {} }));
await waitFor(() => expect(catalog).toHaveBeenCalledTimes(2));
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog', { name: '命令目录 / Command catalog' })).not.toBeInTheDocument();
});
});