Files
Genarrative/src/components/match3d-runtime/Match3DRuntimeShell.test.tsx
T
2026-05-07 23:30:54 +08:00

702 lines
22 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useEffect } from 'react';
import { afterEach, expect, test, vi } from 'vitest';
import type {
Match3DClickItemRequest,
Match3DRunSnapshot,
} from '../../../packages/shared/src/contracts/match3dRuntime';
import {
confirmLocalMatch3DClick,
startLocalMatch3DRun,
} from '../../services/match3d-runtime';
import {
MATCH3D_RENDER_ITEM_SCALE,
resolveRenderableItemFrame,
} from './match3dRuntimePresentation';
import {
MATCH3D_EXTRUDED_READABLE_SHAPES,
MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE,
MATCH3D_TRAY_MODEL_TARGET_SIZE,
buildMatch3DPhysicsEntrySignature,
createMatch3DCannonShape,
createMatch3DThreeGeometry,
measureMatch3DItemPreviewDimension,
resolveMatch3DColliderBounds,
resolveMatch3DBoardDepthPlan,
resolveMatch3DBoundaryRadius,
resolveMatch3DPhysicsStabilityPlan,
resolveMatch3DSpawnTimingPlan,
resolveMatch3DStackTargetY,
resolveMatch3DSpawnDelay,
resolveMatch3DSpawnY,
resolveMatch3DTrayPreviewRotation,
resolveMatch3DTrayPreviewReferenceDimension,
resolveMatch3DTrayPreviewScale,
} from './Match3DPhysicsBoard';
import { resolveGeometryAsset } from './match3dVisualAssets';
import { Match3DRuntimeShell } from './Match3DRuntimeShell';
vi.mock('./Match3DPhysicsBoard', async (importOriginal) => {
const actual =
await importOriginal<typeof import('./Match3DPhysicsBoard')>();
return {
...actual,
Match3DPhysicsBoard: ({ onFallback }: { onFallback: () => void }) => {
useEffect(() => {
const shouldKeep3D =
(
globalThis as typeof globalThis & {
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
}
).__MATCH3D_KEEP_3D_TEST_RENDER__ === true;
if (!shouldKeep3D) {
onFallback();
}
}, [onFallback]);
return <div data-testid="match3d-physics-board-fallback" />;
},
Match3DTrayPreviewBoard: () => (
<div data-testid="match3d-tray-model-board" />
),
};
});
afterEach(() => {
delete (
globalThis as typeof globalThis & {
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
}
).__MATCH3D_KEEP_3D_TEST_RENDER__;
});
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('显示层把可消除物整体半径放大 2 倍且保留相对比例', () => {
const run = startLocalMatch3DRun(25);
const firstItemByType = [...new Map(
run.items.map((item) => [item.itemTypeId, item]),
).values()];
const smallItem = firstItemByType.reduce((smallest, item) =>
item.radius < smallest.radius ? item : smallest,
);
const largeItem = firstItemByType.reduce((largest, item) =>
item.radius > largest.radius ? item : largest,
);
const smallFrame = resolveRenderableItemFrame(smallItem);
const largeFrame = resolveRenderableItemFrame(largeItem);
expect(smallFrame.radius).toBeCloseTo(
smallItem.radius * MATCH3D_RENDER_ITEM_SCALE,
);
expect(largeFrame.radius / smallFrame.radius).toBeCloseTo(
largeItem.radius / smallItem.radius,
);
});
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));
await waitFor(() => expect(onOptimisticRunChange).toHaveBeenCalledTimes(2));
});
test('3D 模式下备选栏使用共享模型预览层,避免挤占中心棋盘上下文', () => {
(
globalThis as typeof globalThis & {
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
}
).__MATCH3D_KEEP_3D_TEST_RENDER__ = true;
const run = startLocalMatch3DRun(1);
const selectedItem = run.items[0]!;
const nextRun: Match3DRunSnapshot = {
...run,
items: run.items.map((item, index) =>
index === 0
? {
...item,
state: 'InTray' as const,
clickable: false,
traySlotIndex: 0,
}
: item,
),
traySlots: run.traySlots.map((slot) =>
slot.slotIndex === 0
? {
slotIndex: 0,
itemInstanceId: selectedItem.itemInstanceId,
itemTypeId: selectedItem.itemTypeId,
visualKey: selectedItem.visualKey,
}
: slot,
),
};
renderRuntime(nextRun);
expect(screen.getByTestId('match3d-tray-model-board')).toBeTruthy();
});
test('3D 物理条目签名随 run 和视觉资源变化,避免旧模型复用到新局', () => {
const run = startLocalMatch3DRun(10);
const item = run.items[0]!;
const sameIdDifferentVisual = {
...item,
visualKey:
item.visualKey === 'block-red-2x4'
? 'block-blue-1x2'
: 'block-red-2x4',
};
expect(buildMatch3DPhysicsEntrySignature(run.runId, item)).not.toBe(
buildMatch3DPhysicsEntrySignature(`${run.runId}-next`, item),
);
expect(buildMatch3DPhysicsEntrySignature(run.runId, item)).not.toBe(
buildMatch3DPhysicsEntrySignature(run.runId, sameIdDifferentVisual),
);
});
test('本地试玩按消除次数生成类型并在 25 类封顶', () => {
const smallRun = startLocalMatch3DRun(12);
const largeRun = startLocalMatch3DRun(100);
const countTypes = (run: Match3DRunSnapshot) =>
new Set(run.items.map((item) => item.itemTypeId)).size;
expect(countTypes(smallRun)).toBe(12);
expect(countTypes(largeRun)).toBe(25);
expect(largeRun.items).toHaveLength(300);
});
test('25 次以内生成不重复积木视觉签名', () => {
const run = startLocalMatch3DRun(25);
const firstItemByType = new Map(
run.items.map((item) => [item.itemTypeId, item]),
);
const visualKeys = new Set(
[...firstItemByType.values()].map((item) => item.visualKey),
);
const signatures = new Set(
[...firstItemByType.values()].map(
(item) => {
const asset = resolveGeometryAsset(item.visualKey);
return `${asset.shape}-${asset.fill}-${asset.studsX}x${asset.studsY}-${asset.heightScale}`;
},
),
);
expect(firstItemByType.size).toBe(25);
expect(visualKeys.size).toBe(25);
expect(signatures.size).toBe(25);
});
test('积木池覆盖参考图里的特殊件', () => {
const shapes = new Set(
startLocalMatch3DRun(25).items.map((item) =>
resolveGeometryAsset(item.visualKey).shape,
),
);
expect(shapes).toContain('brick');
expect(shapes).toContain('tile');
expect(shapes).toContain('slope');
expect(shapes).toContain('cylinder');
expect(shapes).toContain('ring');
expect(shapes).toContain('arch');
expect(shapes).toContain('cone');
});
test('3D 特殊积木件使用可辨认挤出轮廓而不是基础代理体', async () => {
const three = await import('three');
for (const shape of MATCH3D_EXTRUDED_READABLE_SHAPES) {
const geometry = createMatch3DThreeGeometry(three, shape, 1);
expect(geometry.type).toBe('ExtrudeGeometry');
}
});
test('15 次消除时每种视觉模型只对应一次消除目标', () => {
const run = startLocalMatch3DRun(15);
const countByVisualKey = new Map<string, number>();
const typeByVisualKey = new Map<string, Set<string>>();
for (const item of run.items) {
countByVisualKey.set(
item.visualKey,
(countByVisualKey.get(item.visualKey) ?? 0) + 1,
);
typeByVisualKey.set(item.visualKey, typeByVisualKey.get(item.visualKey) ?? new Set());
typeByVisualKey.get(item.visualKey)!.add(item.itemTypeId);
}
expect(countByVisualKey.size).toBe(15);
expect([...countByVisualKey.values()]).toEqual(Array(15).fill(3));
expect(
[...typeByVisualKey.values()].every((itemTypeIds) => itemTypeIds.size === 1),
).toBe(true);
});
test('25 次以内的随机抽取不会刷新重复物品', () => {
for (const clearCount of [1, 12, 15, 24, 25]) {
const run = startLocalMatch3DRun(clearCount);
const visualKeys = new Set(run.items.map((item) => item.visualKey));
expect(visualKeys.size).toBe(clearCount);
}
});
test('25 类型局面按五档体积比例生成尺寸', () => {
const run = startLocalMatch3DRun(25);
const radiusByVisualKey = new Map<string, number>();
for (const item of run.items) {
radiusByVisualKey.set(item.visualKey, item.radius);
}
const baseRadius = [...radiusByVisualKey.values()].find(
(radius) => Math.abs(radius / 0.072 - 1) < 0.01,
);
expect(baseRadius).toBeTruthy();
const tierCounts = new Map<string, number>();
for (const radius of radiusByVisualKey.values()) {
const relativeVolume = Math.pow(radius / baseRadius!, 3);
const tier =
relativeVolume >= 1.6
? 'XL'
: relativeVolume >= 1.25
? 'L'
: relativeVolume >= 0.65 && relativeVolume <= 0.85
? 'XS'
: relativeVolume <= 0.5
? 'S'
: 'M';
tierCounts.set(tier, (tierCounts.get(tier) ?? 0) + 1);
}
expect(tierCounts.get('XL')).toBe(5);
expect(tierCounts.get('L')).toBe(8);
expect(tierCounts.get('M')).toBe(7);
expect(tierCounts.get('XS')).toBe(4);
expect(tierCounts.get('S')).toBe(1);
});
test('同一视觉模型在复用时保持唯一尺寸', () => {
const run = startLocalMatch3DRun(30);
const radiiByVisualKey = new Map<string, Set<number>>();
for (const item of run.items) {
const radii = radiiByVisualKey.get(item.visualKey) ?? new Set<number>();
radii.add(Math.round(item.radius * 10_000));
radiiByVisualKey.set(item.visualKey, radii);
}
expect(radiiByVisualKey.size).toBe(25);
expect([...radiiByVisualKey.values()].every((radii) => radii.size === 1)).toBe(true);
});
test('托盘 3D 预览保留场内模型的相对尺寸比例', async () => {
const three = await import('three');
const run = startLocalMatch3DRun(25);
const firstItemByType = [...new Map(
run.items.map((item) => [item.itemTypeId, item]),
).values()];
const referenceDimension = resolveMatch3DTrayPreviewReferenceDimension(
three,
firstItemByType,
);
const previewRatios = new Set(
firstItemByType.map((item) =>
Math.round(
(measureMatch3DItemPreviewDimension(three, item) /
referenceDimension) *
1_000,
),
),
);
expect(previewRatios.size).toBeGreaterThan(1);
});
test('托盘 3D 预览放大模型并展示俯视 3/4 体积感', () => {
expect(MATCH3D_TRAY_MODEL_TARGET_SIZE).toBeGreaterThanOrEqual(0.85);
expect(MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE).toBeGreaterThanOrEqual(0.9);
const brickRotation = resolveMatch3DTrayPreviewRotation('block-red-2x4');
const tileRotation = resolveMatch3DTrayPreviewRotation(
'block-lavender-tile-2x2',
);
const slopeRotation = resolveMatch3DTrayPreviewRotation(
'block-purple-slope-1x2',
);
expect(brickRotation.x).toBeLessThan(-0.28);
expect(brickRotation.z).toBeGreaterThan(0.2);
expect(brickRotation.y).toBeGreaterThan(0.6);
expect(tileRotation.x).toBeLessThan(-0.25);
expect(tileRotation.z).toBeGreaterThan(0.2);
expect(slopeRotation.x).toBeLessThan(-0.3);
expect(slopeRotation.z).toBeGreaterThan(0.22);
});
test('托盘 3D 预览为小模型保留最低可读显示尺寸', () => {
const smallDimension = 0.4;
const referenceDimension = 1;
const scale = resolveMatch3DTrayPreviewScale(
smallDimension,
referenceDimension,
);
expect(scale * smallDimension).toBeGreaterThanOrEqual(
MATCH3D_TRAY_MODEL_TARGET_SIZE *
MATCH3D_TRAY_MODEL_MIN_RELATIVE_SIZE,
);
expect(scale).toBeGreaterThan(
MATCH3D_TRAY_MODEL_TARGET_SIZE / referenceDimension,
);
});
test('积木 3D 资源可以为本局类型创建几何体', async () => {
const three = await import('three');
const run = startLocalMatch3DRun(15);
const firstItemByType = new Map(
run.items.map((item) => [item.itemTypeId, item]),
);
expect(firstItemByType.size).toBe(15);
for (const item of firstItemByType.values()) {
const shape = resolveGeometryAsset(item.visualKey).shape;
const geometry = createMatch3DThreeGeometry(three, shape, 1);
expect(geometry).toBeTruthy();
}
});
test('3D 物体碰撞体按同款视觉尺寸生成', async () => {
const cannon = await import('cannon-es');
const longBrick = resolveGeometryAsset('block-black-1x8');
const tile = resolveGeometryAsset('block-lavender-tile-2x2');
const cylinder = resolveGeometryAsset('block-green-cylinder');
const radius = 1;
const longBrickBounds = resolveMatch3DColliderBounds(longBrick, radius);
const longBrickShape = createMatch3DCannonShape(cannon, longBrick, radius);
expect(longBrickShape.type).toBe(cannon.Shape.types.BOX);
expect((longBrickShape as import('cannon-es').Box).halfExtents.x * 2).toBeCloseTo(
longBrickBounds.width,
);
expect((longBrickShape as import('cannon-es').Box).halfExtents.z * 2).toBeCloseTo(
longBrickBounds.depth,
);
const tileBounds = resolveMatch3DColliderBounds(tile, radius);
const tileShape = createMatch3DCannonShape(cannon, tile, radius);
expect((tileShape as import('cannon-es').Box).halfExtents.y * 2).toBeCloseTo(
tileBounds.height,
);
const cylinderBounds = resolveMatch3DColliderBounds(cylinder, radius);
const cylinderShape = createMatch3DCannonShape(cannon, cylinder, radius);
expect(cylinderShape.type).toBe(cannon.Shape.types.CYLINDER);
expect(
(cylinderShape as import('cannon-es').Cylinder).height,
).toBeCloseTo(cylinderBounds.height);
});
test('中心场地 3D 纵深随物体总量增加并随消除进度回补', () => {
const smallDepthPlan = resolveMatch3DBoardDepthPlan(30, 30);
const largeDepthPlan = resolveMatch3DBoardDepthPlan(300, 300);
const earlyBottomY = resolveMatch3DStackTargetY(300, 300, 0);
const lateBottomY = resolveMatch3DStackTargetY(300, 60, 0);
expect(largeDepthPlan.initialDepth).toBeGreaterThan(
smallDepthPlan.initialDepth,
);
expect(largeDepthPlan.layerCapacity).toBeLessThan(
smallDepthPlan.layerCapacity,
);
expect(largeDepthPlan.layerCount).toBeGreaterThan(
smallDepthPlan.layerCount,
);
expect(largeDepthPlan.surfaceY).toBeGreaterThan(largeDepthPlan.baseY);
expect(lateBottomY).toBeGreaterThan(earlyBottomY);
expect(lateBottomY).toBeLessThanOrEqual(largeDepthPlan.surfaceY);
});
test('高数量 3D 局面使用更稳定的物理参数', () => {
const smallPlan = resolveMatch3DPhysicsStabilityPlan(30);
const largePlan = resolveMatch3DPhysicsStabilityPlan(300);
expect(largePlan.contactFriction).toBeGreaterThan(
smallPlan.contactFriction,
);
expect(largePlan.contactRestitution).toBeLessThan(
smallPlan.contactRestitution,
);
expect(largePlan.linearDamping).toBeGreaterThan(smallPlan.linearDamping);
expect(largePlan.angularDamping).toBeGreaterThan(smallPlan.angularDamping);
expect(largePlan.solverIterations).toBeGreaterThan(
smallPlan.solverIterations,
);
expect(largePlan.maxHorizontalSpeed).toBeLessThan(
smallPlan.maxHorizontalSpeed,
);
});
test('3D 真实边界半径比视觉半径更保守,避免长条贴边穿出锅壁', () => {
const longBrick = resolveGeometryAsset('block-black-1x8');
const radius = 1;
const boundaryRadius = resolveMatch3DBoundaryRadius(longBrick, radius);
const visualRadius = Math.hypot(
resolveMatch3DColliderBounds(longBrick, radius).width / 2,
resolveMatch3DColliderBounds(longBrick, radius).depth / 2,
);
expect(boundaryRadius).toBeCloseTo(visualRadius);
expect(boundaryRadius).toBeGreaterThan(2.4);
});
test('100 次局面的新物体会按层级延迟生成并逐层回落', () => {
const fastTimingPlan = resolveMatch3DSpawnTimingPlan(29);
const smallDepthPlan = resolveMatch3DBoardDepthPlan(30, 30);
const largeDepthPlan = resolveMatch3DBoardDepthPlan(300, 300);
const smallTimingPlan = resolveMatch3DSpawnTimingPlan(30);
const largeTimingPlan = resolveMatch3DSpawnTimingPlan(300);
const bottomDelay = resolveMatch3DSpawnDelay(0, largeDepthPlan.layerCapacity);
const middleDelay = resolveMatch3DSpawnDelay(30, largeDepthPlan.layerCapacity);
const topDelay = resolveMatch3DSpawnDelay(120, largeDepthPlan.layerCapacity);
const dynamicCapacityDelay = resolveMatch3DSpawnDelay(
120,
largeDepthPlan.layerCapacity,
);
const defaultCapacityDelay = resolveMatch3DSpawnDelay(
120,
smallDepthPlan.layerCapacity,
);
expect(bottomDelay).toBe(0);
expect(middleDelay).toBeGreaterThan(bottomDelay);
expect(topDelay).toBeGreaterThan(middleDelay);
expect(dynamicCapacityDelay).toBeGreaterThan(defaultCapacityDelay);
expect(smallTimingPlan.frameSpawnLimit).toBeLessThan(
fastTimingPlan.frameSpawnLimit,
);
expect(smallTimingPlan.burstSize).toBeLessThan(fastTimingPlan.burstSize);
expect(smallTimingPlan.layerDelayMs).toBeGreaterThan(
fastTimingPlan.layerDelayMs,
);
expect(
resolveMatch3DSpawnDelay(29, smallDepthPlan.layerCapacity, smallTimingPlan),
).toBeGreaterThan(450);
expect(largeTimingPlan.initialDelayMs).toBeGreaterThan(
smallTimingPlan.initialDelayMs,
);
expect(largeTimingPlan.frameSpawnLimit).toBeLessThan(
smallTimingPlan.frameSpawnLimit,
);
expect(largeTimingPlan.burstSize).toBeLessThanOrEqual(6);
expect(largeTimingPlan.layerDelayMs).toBeGreaterThanOrEqual(
smallTimingPlan.layerDelayMs,
);
expect(
resolveMatch3DSpawnDelay(299, largeDepthPlan.layerCapacity, largeTimingPlan),
).toBeGreaterThan(5000);
});
test('3D 新物体生成高度会避让同位置已有堆叠', () => {
const plannedSpawnY = 2;
const raisedSpawnY = resolveMatch3DSpawnY(
plannedSpawnY,
0.8,
0.7,
{ x: 0.1, z: 0.1 },
[
{
boundaryRadius: 0.7,
colliderHeight: 0.9,
x: 0.18,
y: 2.4,
z: 0.15,
},
],
);
const unchangedSpawnY = resolveMatch3DSpawnY(
plannedSpawnY,
0.8,
0.7,
{ x: 0.1, z: 0.1 },
[
{
boundaryRadius: 0.7,
colliderHeight: 0.9,
x: 3,
y: 4,
z: 3,
},
],
);
expect(raisedSpawnY).toBeGreaterThan(plannedSpawnY);
expect(unchangedSpawnY).toBe(plannedSpawnY);
});
test('积木视觉键不会被统一兜底成红色苹字', () => {
const run = startLocalMatch3DRun(2);
run.items = run.items.slice(0, 2).map((item, index) => ({
...item,
itemInstanceId: `block-${index}`,
itemTypeId: `block-type-${index}`,
visualKey: index === 0 ? 'block-red-2x4' : 'block-blue-1x2',
x: 0.42 + index * 0.16,
y: 0.5,
layer: index,
clickable: true,
}));
renderRuntime(run);
expect(screen.getByTestId('match3d-visual-block-red-2x4')).toBeTruthy();
expect(screen.getByTestId('match3d-visual-block-blue-1x2')).toBeTruthy();
expect(screen.queryAllByText('苹')).toHaveLength(0);
});
test('积木视觉键渲染为无文字纯色图标', () => {
const run = startLocalMatch3DRun(3);
run.items = run.items.slice(0, 3).map((item, index) => ({
...item,
itemInstanceId: `block-icon-${index}`,
itemTypeId: `block-icon-type-${index}`,
visualKey:
index === 0
? 'block-red-2x4'
: index === 1
? 'block-clear-ring'
: 'block-mint-arch',
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-block-red-2x4')).toBeTruthy();
expect(
screen.getByTestId('match3d-visual-block-clear-ring').getAttribute('data-shape'),
).toBe('ring');
expect(
screen
.getByTestId('match3d-visual-block-mint-arch')
.getAttribute('data-shape'),
).toBe('arch');
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: `block-geometry-${index}`,
itemTypeId: `block-geometry-type-${index}`,
visualKey:
index === 0
? 'block-black-1x8'
: index === 1
? 'block-purple-slope-1x2'
: 'block-green-cylinder',
x: 0.35 + index * 0.15,
y: 0.5,
layer: index,
clickable: true,
}));
renderRuntime(run);
expect(
screen.getByTestId('match3d-visual-block-black-1x8').getAttribute('data-shape'),
).toBe('brick');
expect(
screen
.getByTestId('match3d-visual-block-purple-slope-1x2')
.getAttribute('data-shape'),
).toBe('slope');
expect(
screen
.getByTestId('match3d-visual-block-green-cylinder')
.getAttribute('data-shape'),
).toBe('cylinder');
});
test('异常旧坐标只做显示层收束,不让物品贴出圆形空间', () => {
const run = startLocalMatch3DRun(1);
const item = run.items[0]!;
run.items = [
{
...item,
itemInstanceId: 'legacy-outside',
visualKey: 'block-red-2x4',
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);
});