/* @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);
});