import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { 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';
function preparedClient(): ApiClient {
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner-id', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 1 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
vi.spyOn(client, 'players').mockResolvedValue({ items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }] });
return client;
}
describe('player inventory management', () => {
it('previews and submits one slot with the snapshot revision', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = preparedClient();
vi.spyOn(client, 'playerInventory').mockResolvedValue({
playerUuid: '11111111-1111-1111-1111-111111111111',
container: 'inventory',
revision: 12,
online: true,
slots: [{ slot: 0, itemId: 'minecraft:stone', count: 2, structuredData: {}, opaqueDataPresent: false, opaqueDataFingerprint: '' }],
});
const update = vi.spyOn(client, 'updatePlayerInventory').mockResolvedValue({
id: 'inventory-op', kind: 'inventory.update', state: 'SUCCEEDED', revision: 13, diffs: [],
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
await user.click(screen.getByRole('tab', { name: 'Inventory' }));
await user.click(await screen.findByRole('button', { name: 'Slot 0: minecraft:stone × 2' }));
const item = screen.getByLabelText('Item registry ID');
await user.clear(item);
await user.type(item, 'minecraft:diamond');
const count = screen.getByLabelText('Count (0–127)');
await user.clear(count);
await user.type(count, '4');
await user.type(screen.getByLabelText(/Audit reason/), 'Replace reward item');
await user.click(screen.getByRole('button', { name: 'Preview slot change' }));
expect(update).not.toHaveBeenCalled();
expect(screen.getByText(/minecraft:stone × 2/)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Confirm CAS update' }));
await waitFor(() => expect(update).toHaveBeenCalledOnce());
expect(update).toHaveBeenCalledWith('11111111-1111-1111-1111-111111111111', 'inventory', 12, {
slot: 0,
itemId: 'minecraft:diamond',
count: 4,
structuredData: {},
opaqueDataPresent: false,
opaqueDataFingerprint: '',
}, 'Replace reward item');
});
it('blocks edits to slots whose opaque data cannot be preserved', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = preparedClient();
vi.spyOn(client, 'playerInventory').mockResolvedValue({
playerUuid: '11111111-1111-1111-1111-111111111111', container: 'inventory', revision: 2, online: true,
slots: [{ slot: 5, itemId: 'minecraft:book', count: 1, structuredData: {}, opaqueDataPresent: true, opaqueDataFingerprint: 'abc' }],
});
const update = vi.spyOn(client, 'updatePlayerInventory');
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
await user.click(screen.getByRole('tab', { name: 'Inventory' }));
await user.click(await screen.findByRole('button', { name: 'Slot 5: minecraft:book × 1' }));
expect(screen.getByText(/editing is blocked to prevent data loss/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Preview slot change' })).toBeDisabled();
expect(update).not.toHaveBeenCalled();
});
});
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { 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';
function preparedClient(): ApiClient {
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'owner-id', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 1 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
vi.spyOn(client, 'players').mockResolvedValue({ items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }] });
return client;
}
describe('player inventory management', () => {
it('previews and submits one slot with the snapshot revision', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = preparedClient();
vi.spyOn(client, 'playerInventory').mockResolvedValue({
playerUuid: '11111111-1111-1111-1111-111111111111',
container: 'inventory',
revision: 12,
online: true,
slots: [{ slot: 0, itemId: 'minecraft:stone', count: 2, structuredData: {}, opaqueDataPresent: false, opaqueDataFingerprint: '' }],
});
const update = vi.spyOn(client, 'updatePlayerInventory').mockResolvedValue({
id: 'inventory-op', kind: 'inventory.update', state: 'SUCCEEDED', revision: 13, diffs: [],
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
await user.click(screen.getByRole('tab', { name: 'Inventory' }));
await user.click(await screen.findByRole('button', { name: 'Slot 0: minecraft:stone × 2' }));
const item = screen.getByLabelText('Item registry ID');
await user.clear(item);
await user.type(item, 'minecraft:diamond');
const count = screen.getByLabelText('Count (0–127)');
await user.clear(count);
await user.type(count, '4');
await user.type(screen.getByLabelText(/Audit reason/), 'Replace reward item');
await user.click(screen.getByRole('button', { name: 'Preview slot change' }));
expect(update).not.toHaveBeenCalled();
expect(screen.getByText(/minecraft:stone × 2/)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Confirm CAS update' }));
await waitFor(() => expect(update).toHaveBeenCalledOnce());
expect(update).toHaveBeenCalledWith('11111111-1111-1111-1111-111111111111', 'inventory', 12, {
slot: 0,
itemId: 'minecraft:diamond',
count: 4,
structuredData: {},
opaqueDataPresent: false,
opaqueDataFingerprint: '',
}, 'Replace reward item');
});
it('blocks edits to slots whose opaque data cannot be preserved', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = preparedClient();
vi.spyOn(client, 'playerInventory').mockResolvedValue({
playerUuid: '11111111-1111-1111-1111-111111111111', container: 'inventory', revision: 2, online: true,
slots: [{ slot: 5, itemId: 'minecraft:book', count: 1, structuredData: {}, opaqueDataPresent: true, opaqueDataFingerprint: 'abc' }],
});
const update = vi.spyOn(client, 'updatePlayerInventory');
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
await user.click(screen.getByRole('tab', { name: 'Inventory' }));
await user.click(await screen.findByRole('button', { name: 'Slot 5: minecraft:book × 1' }));
expect(screen.getByText(/editing is blocked to prevent data loss/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Preview slot change' })).toBeDisabled();
expect(update).not.toHaveBeenCalled();
});
});