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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
import { useState, type ReactNode } from 'react';
import { act, render, screen, waitFor, within } 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 { SessionActor, TriggerCatalog, TriggerDefinition } from '../types';
import { AppShell, type RouteId } from './app-shell';
import { MinecraftIcon } from './minecraft-icon';

const timestamp = '2026-01-01T00:00:00Z';

function themeClient(roles: SessionActor['roles'] = ['owner'], permissions: string[] = []): ApiClient {
  const client = new ApiClient();
  vi.spyOn(client, 'session').mockResolvedValue({ authenticated: true, csrfToken: 'csrf', actor: {
    id: 'theme-observer', displayName: 'Theme account', roles, permissions, totpVerified: true,
  } });
  vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: timestamp, uptimeSeconds: 1, playersOnline: 2 });
  vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: true, moderationEnabled: false,
    claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
  vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
  return client;
}

function ShellHarness({ initialRoute, navigate, children }: {
  initialRoute: RouteId; navigate: (route: RouteId) => void; children?: ReactNode;
}) {
  const [route, setRoute] = useState(initialRoute);
  return <AppShell route={route} navigate={(next) => { navigate(next); setRoute(next); }}>
    {children ?? <h1>Workspace fixture</h1>}
  </AppShell>;
}

function renderShell({ client = themeClient(), route = 'overview', locale = 'en-US' }: {
  client?: ApiClient; route?: RouteId; locale?: 'en-US' | 'zh-CN';
} = {}) {
  localStorage.setItem('xfesm.locale', locale);
  const navigate = vi.fn();
  const view = render(<I18nProvider><ServerProvider client={client}>
    <ShellHarness initialRoute={route} navigate={navigate} />
  </ServerProvider></I18nProvider>);
  return { ...view, navigate };
}

describe('Minecraft-themed application chrome', () => {
  afterEach(() => {
    location.hash = '';
    localStorage.removeItem('xfesm.locale');
    sessionStorage.removeItem('xfesm.csrf');
    vi.restoreAllMocks();
  });

  it('keeps permission filtering intact instead of exposing every themed navigation slot', async () => {
    renderShell({ client: themeClient(['observer'], ['status_read', 'player_read', 'trigger_read']) });
    const navigation = within(await screen.findByRole('navigation', { name: 'Core management' }));

    for (const label of ['Overview', 'Online players', 'Triggers', 'Content studio', 'Settings']) {
      expect(navigation.getByRole('button', { name: label })).toBeInTheDocument();
    }
    for (const label of ['Economy', 'UI menus', 'Command policies', 'Audit log', 'Secure console']) {
      expect(navigation.queryByRole('button', { name: label })).not.toBeInTheDocument();
    }
    expect(screen.queryByRole('navigation', { name: 'Governance & recovery' })).not.toBeInTheDocument();
  });

  it('preserves a single active route and its accessible name when navigating themed buttons', async () => {
    const user = userEvent.setup();
    const { navigate } = renderShell({ route: 'triggers' });
    const navigation = within(await screen.findByRole('navigation', { name: 'Core management' }));
    const triggers = navigation.getByRole('button', { name: 'Triggers' });
    expect(triggers).toHaveAttribute('aria-current', 'page');
    expect(triggers).toHaveClass('nav-link--active');

    await user.click(navigation.getByRole('button', { name: 'UI menus' }));

    expect(navigate).toHaveBeenLastCalledWith('menus');
    expect(navigation.getByRole('button', { name: 'UI menus' })).toHaveAttribute('aria-current', 'page');
    expect(triggers).not.toHaveAttribute('aria-current');
    expect(screen.getAllByRole('button', { current: 'page' })).toHaveLength(1);
  });

  it('keeps pixel navigation and brand icons decorative without duplicating accessible button names', async () => {
    const { container } = renderShell({ route: 'triggers' });
    const navigation = within(await screen.findByRole('navigation', { name: 'Core management' }));
    const triggers = navigation.getByRole('button', { name: 'Triggers' });
    const icon = triggers.querySelector('[data-minecraft-icon="redstone"]');
    expect(icon).toBeInTheDocument();
    expect(icon).toHaveAttribute('aria-hidden', 'true');
    expect(triggers).toHaveAccessibleName('Triggers');
    const pixels = container.querySelectorAll('.minecraft-icon');
    expect(pixels.length).toBeGreaterThan(10);
    for (const pixel of pixels) {
      expect(pixel).toHaveAttribute('aria-hidden', 'true');
      expect(pixel).toHaveAttribute('focusable', 'false');
      expect(pixel).toHaveAttribute('viewBox', '0 0 16 16');
    }
    expect(screen.queryByRole('img')).not.toBeInTheDocument();
  });

  it('provides alternative text only when an individual pixel icon is intentionally labelled', () => {
    const { container } = render(<><MinecraftIcon name="grass" /><MinecraftIcon name="emerald" label="Emerald status" /></>);
    expect(container.querySelector('[data-minecraft-icon="grass"]')).toHaveAttribute('aria-hidden', 'true');
    const labelled = screen.getByRole('img', { name: 'Emerald status' });
    expect(labelled).not.toHaveAttribute('aria-hidden', 'true');
    expect(labelled).toHaveAttribute('focusable', 'false');
    expect(screen.getAllByRole('img')).toHaveLength(1);
  });

  it('still opens and closes the mobile sidebar and closes it after navigation', async () => {
    const user = userEvent.setup();
    const { container, navigate } = renderShell();
    const navigation = within(await screen.findByRole('navigation', { name: 'Core management' }));
    const sidebar = screen.getByRole('navigation', { name: 'Core management' }).closest('aside')!;
    expect(sidebar).not.toHaveClass('sidebar--open');

    await user.click(screen.getByRole('button', { name: 'Menu' }));
    expect(sidebar).toHaveClass('sidebar--open');
    expect(container.querySelector('.sidebar-backdrop')).toBeInTheDocument();
    await user.click(navigation.getByRole('button', { name: 'Online players' }));
    expect(navigate).toHaveBeenLastCalledWith('players');
    expect(sidebar).not.toHaveClass('sidebar--open');
    expect(container.querySelector('.sidebar-backdrop')).not.toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: 'Menu' }));
    await user.click(within(sidebar).getByRole('button', { name: 'Close' }));
    expect(sidebar).not.toHaveClass('sidebar--open');
    await user.click(screen.getByRole('button', { name: 'Menu' }));
    await user.click(container.querySelector<HTMLButtonElement>('.sidebar-backdrop')!);
    expect(sidebar).not.toHaveClass('sidebar--open');
  });

  it('keeps both language controls operable without changing the active route', async () => {
    const user = userEvent.setup();
    renderShell({ route: 'triggers', locale: 'zh-CN' });
    await screen.findByRole('navigation', { name: '核心管理' });
    await user.click(screen.getByRole('button', { name: 'English' }));
    expect(within(screen.getByRole('navigation', { name: 'Core management' }))
      .getByRole('button', { name: 'Triggers' })).toHaveAttribute('aria-current', 'page');
    expect(localStorage.getItem('xfesm.locale')).toBe('en-US');

    await user.click(screen.getByRole('button', { name: '中文' }));
    expect(within(screen.getByRole('navigation', { name: '核心管理' }))
      .getByRole('button', { name: '触发器' })).toHaveAttribute('aria-current', 'page');
    expect(localStorage.getItem('xfesm.locale')).toBe('zh-CN');
  });

  it('ties the themed connection indicators to real connection, reconnect, and SSE states', async () => {
    const client = themeClient();
    vi.mocked(client.session).mockRejectedValueOnce(new Error('Connection refused'));
    let emitState: ((state: 'open' | 'error') => void) | undefined;
    vi.mocked(client.subscribe).mockImplementation((_onEvent, onState) => { emitState = onState; return () => undefined; });
    const user = userEvent.setup();
    const { container } = renderShell({ client });
    const hud = container.querySelector('.topbar__server')!;
    const meter = container.querySelector('.experience-meter')!;
    expect(hud).toHaveAttribute('data-connection', 'connecting');
    expect(meter).toHaveAttribute('aria-hidden', 'true');
    expect(meter).toHaveAttribute('data-connected', 'false');

    await waitFor(() => expect(hud).toHaveAttribute('data-connection', 'offline'));
    expect(hud).toHaveTextContent('Management API offline');
    expect(meter).toHaveAttribute('data-connected', 'false');
    expect(screen.getByRole('alert')).toHaveTextContent('Connection refused');
    await user.click(screen.getByRole('button', { name: 'Reconnect' }));
    await waitFor(() => expect(hud).toHaveAttribute('data-connection', 'online'));
    expect(hud).toHaveTextContent('Live connection');
    expect(meter).toHaveAttribute('data-connected', 'true');
    await waitFor(() => expect(emitState).toBeDefined());

    act(() => emitState?.('error'));
    expect(hud).toHaveAttribute('data-connection', 'degraded');
    expect(hud).toHaveTextContent('Connection degraded');
    expect(meter).toHaveAttribute('data-connected', 'false');
    act(() => emitState?.('open'));
    expect(hud).toHaveAttribute('data-connection', 'online');
    expect(meter).toHaveAttribute('data-connected', 'true');
  });

  it.each([
    ['triggers', 'main-content--triggers'],
    ['content', 'main-content--content'],
  ] as const)('retains the full-width workspace hook for the %s editor', async (route, className) => {
    renderShell({ route });
    await screen.findByRole('navigation', { name: 'Core management' });
    const main = screen.getByRole('main');
    expect(main).toHaveClass('main-content', className);
    expect(main.parentElement).toHaveClass('workspace');
    expect(main.parentElement?.parentElement).toHaveClass('app-shell');
  });

  it('keeps trigger focus mode inside the full-width shell contract and restores the normal layout on exit', async () => {
    localStorage.setItem('xfesm.locale', 'en-US');
    location.hash = '/triggers';
    const client = themeClient();
    const trigger: TriggerDefinition = {
      id: 'theme-trigger', groupId: 'theme-group', name: 'Theme trigger', description: '', enabled: true,
      mode: 'visual', schemaVersion: 2, event: { type: 'player.join', configuration: {} },
      events: [{ nodeId: 'theme-event', type: 'player.join', configuration: {} }], declarations: [], functions: [], statements: [],
      conditionMode: 'all', conditions: [], actions: [], script: '', revision: 0, createdBy: 'owner',
      createdAt: timestamp, updatedAt: timestamp, migrated: false,
    };
    const catalog: TriggerCatalog = { defaultMessageSender: 'Server', events: ['player.join'], operators: [], actions: [], actionParameters: {}, variables: [],
      schemaVersion: 2, descriptors: [] };
    vi.spyOn(client, 'triggerWorkspace').mockResolvedValue({ totalTriggers: 1, groups: [{
      id: 'theme-group', name: 'Theme group', description: '', enabled: true, revision: 0, createdBy: 'owner',
      createdAt: timestamp, updatedAt: timestamp, migrated: false, triggers: [trigger],
    }] });
    vi.spyOn(client, 'triggerCatalog').mockResolvedValue(catalog);
    vi.spyOn(client, 'menuWorkspace').mockResolvedValue({ menus: [], totalMenus: 0 });
    vi.spyOn(client, 'economy').mockResolvedValue({ currencies: [], overview: { currencies: [], observedAt: timestamp } });
    vi.spyOn(client, 'triggerExecutions').mockResolvedValue({ items: [] });
    vi.spyOn(client, 'triggerEventCaptures').mockResolvedValue({ items: [] });
    vi.spyOn(client, 'triggerLibraries').mockResolvedValue({ items: [] });
    const user = userEvent.setup();
    render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
    await user.click(await screen.findByRole('button', { name: /Theme trigger/ }));
    const tree = screen.getByRole('region', { name: 'Program tree' });
    const page = tree.closest('.trigger-page')!;
    const main = screen.getByRole('main');
    const shell = main.closest('.app-shell')!;

    await user.click(screen.getByRole('button', { name: 'Focus editor' }));

    expect(page).toHaveClass('trigger-page--focused');
    expect(main).toHaveClass('main-content--triggers');
    expect(tree.parentElement).toHaveClass('galaxy-workbench');
    // These direct-child relationships drive the full-width focus-mode CSS selectors.
    expect(shell.querySelector(':scope > .sidebar')).toBeInTheDocument();
    expect(shell.querySelector(':scope > .workspace > .topbar')).toBeInTheDocument();
    expect(shell.querySelector(':scope > .workspace > .main-content')).toBe(main);
    expect(screen.getByRole('button', { name: 'Exit focus' })).toHaveAttribute('aria-pressed', 'true');
    await user.click(screen.getByRole('button', { name: 'Exit focus' }));
    await waitFor(() => expect(page).not.toHaveClass('trigger-page--focused'));
    expect(tree).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Focus editor' })).toHaveAttribute('aria-pressed', 'false');
  });
});