XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ServerProvider } from '../context/server-context';
import { ApiClient } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { MenuCatalog, MenuDefinition, TriggerCatalog } from '../types';
import { ImagePicker, MenusPage } from './menus';

const timestamp = '2026-09-04T00:00:00Z';
const menuCatalog: MenuCatalog = {
  controlTypes: ['text', 'rich_text', 'button', 'icon_button', 'image', 'panel', 'border', 'progress',
    'input', 'checkbox', 'slider', 'item_slot', 'separator', 'spacer'],
  popupPositions: ['center', 'top', 'bottom', 'left', 'right', 'top_left', 'top_right',
    'bottom_left', 'bottom_right', 'custom'],
  controlEvents: ['click', 'double_click', 'change', 'submit', 'toggle', 'hover', 'focus', 'blur'],
  triggers: [{ id: '00000000-0000-0000-0000-000000000001', name: 'Menu click', enabled: true }],
};
const triggerCatalog: TriggerCatalog = {
  defaultMessageSender: 'XFEServerManager', events: ['menu.control'], operators: ['eq'], actions: ['log'],
  actionParameters: { log: ['message', 'level'] }, variables: [{ key: 'player.name', nameZh: '玩家名称',
    nameEn: 'Player name', descriptionZh: '事件玩家', descriptionEn: 'Event player', category: 'player',
    scopes: ['menu.*'], templateAllowed: true, conditionAllowed: true }],
};

function savedMenu(controls: MenuDefinition['controls']): MenuDefinition {
  return {
    id: '00000000-0000-0000-0000-000000000100', name: '新菜单', description: '', width: 360, height: 240,
    popupPosition: 'center', offsetX: 0, offsetY: 0, pauseGame: false, backgroundColor: '#101713e8',
    controls, revision: 0, createdBy: 'owner', createdAt: timestamp, updatedAt: timestamp,
  };
}

function client(writable = true): ApiClient {
  const api = new ApiClient();
  vi.spyOn(api, 'session').mockResolvedValue({ authenticated: true, csrfToken: 'csrf', actor: {
    id: 'owner', displayName: 'Owner', roles: writable ? ['owner'] : ['observer'],
    permissions: writable ? ['menu_read', 'menu_write'] : ['menu_read'], totpVerified: true,
  } });
  vi.spyOn(api, 'status').mockResolvedValue({ sampledAt: timestamp, uptimeSeconds: 1, playersOnline: 0 });
  vi.spyOn(api, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false,
    claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false, joinExperienceEnabled: true });
  vi.spyOn(api, 'subscribe').mockReturnValue(() => undefined);
  vi.spyOn(api, 'menuWorkspace').mockResolvedValue({ menus: [], totalMenus: 0 });
  vi.spyOn(api, 'menuCatalog').mockResolvedValue(menuCatalog);
  vi.spyOn(api, 'triggerCatalog').mockResolvedValue(triggerCatalog);
  vi.spyOn(api, 'createMenu').mockImplementation(async (draft) => {
    const menu = savedMenu(draft.controls);
    return { menu, workspace: { menus: [menu], totalMenus: 1 } };
  });
  return api;
}

function renderMenus(api: ApiClient) {
  return render(<I18nProvider><ServerProvider client={api}><MenusPage /></ServerProvider></I18nProvider>);
}

describe('menu builder', () => {
  afterEach(() => vi.restoreAllMocks());

  it('creates a positioned button, binds an event and saves with Ctrl+S', async () => {
    const api = client();
    const user = userEvent.setup();
    renderMenus(api);

    expect(await screen.findByText('UI 菜单')).toBeInTheDocument();
    await user.click(await screen.findByTitle('按钮 / Button'));
    expect(screen.getAllByText('button_1').length).toBeGreaterThan(0);
    await user.click(screen.getByRole('button', { name: '添加' }));
    expect(screen.getByLabelText('事件')).toHaveValue('click');

    fireEvent.keyDown(document, { key: 's', ctrlKey: true });
    await waitFor(() => expect(api.createMenu).toHaveBeenCalledTimes(1));
    const request = vi.mocked(api.createMenu).mock.calls[0][0];
    expect(request.controls[0]).toMatchObject({ id: 'button_1', type: 'button', x: 16, y: 16 });
    expect(request.controls[0].events[0]).toMatchObject({ event: 'click', closeMenu: true });
  });

  it('keeps all mutation entry points disabled for a menu reader', async () => {
    renderMenus(client(false));
    expect(await screen.findByText('只读')).toBeInTheDocument();
    expect(await screen.findByTitle('按钮 / Button')).toBeDisabled();
    expect(screen.getByRole('button', { name: /保存 Ctrl\+S/ })).toBeDisabled();
  });

  it('does not delete the selected control when Backspace edits rich text', async () => {
    const api = client();
    const user = userEvent.setup();
    renderMenus(api);

    await screen.findByText('UI 菜单');
    await user.click(await screen.findByTitle('富文本 / Rich text'));
    const editor = screen.getByRole('textbox', { name: '富文本内容 / Rich text content' });
    editor.focus();
    fireEvent.keyDown(editor, { key: 'Backspace' });
    fireEvent.keyDown(document, { key: 's', ctrlKey: true });

    await waitFor(() => expect(api.createMenu).toHaveBeenCalledTimes(1));
    expect(vi.mocked(api.createMenu).mock.calls[0][0].controls)
      .toEqual([expect.objectContaining({ id: 'rich_text_1', type: 'rich_text' })]);
  });

  it('loads a real preview for vanilla and mod resource references', async () => {
    const api = client();
    vi.spyOn(api, 'menuImages').mockResolvedValue([{ reference: 'item:minecraft:apple',
      name: 'minecraft:apple', type: 'item', source: '原版/模组物品' }]);
    vi.spyOn(api, 'menuImagePreview').mockResolvedValue({ reference: 'item:minecraft:apple',
      mimeType: 'image/png', dataUrl: 'data:image/png;base64,cHJldmlldw==' });
    render(<ImagePicker api={api} value="" onSelect={vi.fn()} onClose={vi.fn()} />);

    expect(await screen.findByText('minecraft:apple')).toBeInTheDocument();
    await waitFor(() => expect(api.menuImagePreview).toHaveBeenCalledWith('item:minecraft:apple'));
    await waitFor(() => expect(document.querySelector('.menu-image-picker__preview img'))
      .toHaveAttribute('src', 'data:image/png;base64,cHJldmlldw=='));
  });
});