180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
/* @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(
|
|
<Match3DRuntimeShell
|
|
run={currentRun}
|
|
onBack={vi.fn()}
|
|
onRestart={vi.fn()}
|
|
onOptimisticRunChange={onOptimisticRunChange}
|
|
onClickItem={onClickItem}
|
|
/>,
|
|
);
|
|
});
|
|
const { rerender } = render(
|
|
<Match3DRuntimeShell
|
|
run={currentRun}
|
|
onBack={vi.fn()}
|
|
onRestart={vi.fn()}
|
|
onOptimisticRunChange={onOptimisticRunChange}
|
|
onClickItem={onClickItem}
|
|
/>,
|
|
);
|
|
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);
|
|
});
|