/* @vitest-environment jsdom */ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { expect, test, vi } from 'vitest'; import type { Match3DClickItemRequest, Match3DRunSnapshot, } from '../../../packages/shared/src/contracts/match3dRuntime'; import { confirmLocalMatch3DClick, startLocalMatch3DRun, } from '../../services/match3d-runtime'; import { Match3DRuntimeShell } from './Match3DRuntimeShell'; function renderRuntime(run: Match3DRunSnapshot) { let currentRun = run; let authorityRun = run; const onClickItem = vi.fn(async (payload: Match3DClickItemRequest) => { const result = await confirmLocalMatch3DClick(authorityRun, payload); authorityRun = result.run; return result; }); const onOptimisticRunChange = vi.fn((nextRun: Match3DRunSnapshot) => { currentRun = nextRun; rerender( , ); }); const { rerender } = render( , ); return { onClickItem, onOptimisticRunChange, }; } test('展示圆形空间和 7 格备选栏', () => { renderRuntime(startLocalMatch3DRun(4)); expect(screen.getByTestId('match3d-board')).toBeTruthy(); expect(screen.getAllByTestId('match3d-tray-slot')).toHaveLength(7); }); test('点击可见物品后先乐观入槽再等待确认', async () => { const run = startLocalMatch3DRun(4); const clickableItem = run.items.find((item) => item.clickable); expect(clickableItem).toBeTruthy(); const { onClickItem, onOptimisticRunChange } = renderRuntime(run); fireEvent.click( screen.getByTestId(`match3d-item-${clickableItem!.itemInstanceId}`), ); expect(onOptimisticRunChange).toHaveBeenCalled(); await waitFor(() => expect(onClickItem).toHaveBeenCalledTimes(1)); }); test('后端形状视觉键不会被统一兜底成红色苹字', () => { const run = startLocalMatch3DRun(2); run.items = run.items.slice(0, 2).map((item, index) => ({ ...item, itemInstanceId: `shape-${index}`, itemTypeId: `shape-type-${index}`, visualKey: index === 0 ? 'red_circle' : 'yellow_triangle', x: 0.42 + index * 0.16, y: 0.5, layer: index, clickable: true, })); renderRuntime(run); expect(screen.getByTestId('match3d-visual-red_circle')).toBeTruthy(); expect(screen.getByTestId('match3d-visual-yellow_triangle')).toBeTruthy(); expect(screen.queryAllByText('苹')).toHaveLength(0); }); test('水果题材视觉键也渲染为无文字纯色几何体', () => { const run = startLocalMatch3DRun(3); run.items = run.items.slice(0, 3).map((item, index) => ({ ...item, itemInstanceId: `fruit-${index}`, itemTypeId: `fruit-type-${index}`, visualKey: index === 0 ? 'watermelon-green' : index === 1 ? 'apple-red' : 'grape-purple', x: 0.35 + index * 0.15, y: 0.5, radius: index === 0 ? 0.12 : index === 1 ? 0.09 : 0.07, layer: index, clickable: true, })); renderRuntime(run); expect(screen.getByTestId('match3d-visual-watermelon-green')).toBeTruthy(); expect( screen.getByTestId('match3d-visual-apple-red').getAttribute('data-shape'), ).toBe('heart'); expect( screen .getByTestId('match3d-visual-grape-purple') .getAttribute('data-shape'), ).toBe('star'); expect(screen.queryByText('苹果')).toBeNull(); expect(screen.queryByText('苹')).toBeNull(); }); test('运行态支持梯形和平行四边形等差异化几何造型', () => { const run = startLocalMatch3DRun(3); run.items = run.items.slice(0, 3).map((item, index) => ({ ...item, itemInstanceId: `geometry-${index}`, itemTypeId: `geometry-type-${index}`, visualKey: index === 0 ? 'peach-pink' : index === 1 ? 'banana-yellow' : 'orange_hexagon', x: 0.35 + index * 0.15, y: 0.5, layer: index, clickable: true, })); renderRuntime(run); expect( screen.getByTestId('match3d-visual-peach-pink').getAttribute('data-shape'), ).toBe('trapezoid'); expect( screen .getByTestId('match3d-visual-banana-yellow') .getAttribute('data-shape'), ).toBe('parallelogram'); expect( screen .getByTestId('match3d-visual-orange_hexagon') .getAttribute('data-shape'), ).toBe('hexagon'); }); test('异常旧坐标只做显示层收束,不让物品贴出圆形空间', () => { const run = startLocalMatch3DRun(1); const item = run.items[0]!; run.items = [ { ...item, itemInstanceId: 'legacy-outside', visualKey: 'apple-red', x: -0.4, y: 0.5, radius: 0.1, clickable: true, }, ]; renderRuntime(run); const token = screen.getByTestId( 'match3d-item-legacy-outside', ) as HTMLElement; expect(parseFloat(token.style.left)).toBeGreaterThanOrEqual(0); expect(parseFloat(token.style.left)).toBeLessThanOrEqual(100); });