feat(edutainment): refresh baby object match flow

This commit is contained in:
2026-05-16 11:29:28 +08:00
parent 49ffa6b901
commit 45daca3647
24 changed files with 6616 additions and 659 deletions

View File

@@ -157,6 +157,7 @@ function dispatchPointerEvent(
options: {
pointerId: number;
button?: number;
buttons?: number;
clientX: number;
clientY: number;
},
@@ -164,9 +165,10 @@ function dispatchPointerEvent(
const event = new Event(type, { bubbles: true, cancelable: true });
Object.assign(event, options);
target.dispatchEvent(event);
return event;
}
function dragHand(stage: HTMLElement, button: 0 | 2) {
function setStageRect(stage: HTMLElement) {
Object.defineProperty(stage, 'getBoundingClientRect', {
configurable: true,
value: () => ({
@@ -181,34 +183,67 @@ function dragHand(stage: HTMLElement, button: 0 | 2) {
toJSON: () => ({}),
}),
});
}
function dragItemWithHand(stage: HTMLElement, button: 0 | 2, targetX: number) {
setStageRect(stage);
act(() => {
dispatchPointerEvent(stage, 'pointerdown', {
pointerId: button + 1,
button,
clientX: 20,
clientY: 140,
buttons: button === 2 ? 2 : 1,
clientX: 160,
clientY: 89,
});
});
act(() => {
dispatchPointerEvent(stage, 'pointermove', {
pointerId: button + 1,
button,
clientX: 120,
clientY: 140,
buttons: button === 2 ? 2 : 1,
clientX: targetX,
clientY: 190,
});
});
act(() => {
dispatchPointerEvent(stage, 'pointerup', {
pointerId: button + 1,
button,
clientX: 120,
clientY: 140,
buttons: 0,
clientX: targetX,
clientY: 190,
});
});
}
async function advanceRoundIntro() {
await advanceInitialTargetPreview();
await advanceGiftIntro();
}
async function advanceInitialTargetPreview() {
await act(async () => {
await vi.advanceTimersByTimeAsync(2000);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(720);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(2000);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(720);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
}
async function advanceGiftIntro() {
await act(async () => {
await vi.advanceTimersByTimeAsync(620);
});
@@ -236,6 +271,7 @@ test('shows the first gift item after gift and item animations', async () => {
);
expect(screen.getByText('将物品放入对应的篮子里')).toBeTruthy();
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');
await advanceRoundIntro();
@@ -246,6 +282,56 @@ test('shows the first gift item after gift and item animations', async () => {
vi.useRealTimers();
});
test('previews both target items before the first gift box round', async () => {
vi.useFakeTimers();
render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={createRandomSequence([0])}
/>,
);
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
expect(
within(screen.getByTestId('baby-object-intro-item')).getByAltText('苹果'),
).toBeTruthy();
expect(screen.queryByLabelText('礼物盒')).toBeNull();
await act(async () => {
await vi.advanceTimersByTimeAsync(2000);
});
expect(screen.getByTestId('baby-object-intro-item').className).toContain(
'baby-object-runtime__intro-item--flying',
);
await act(async () => {
await vi.advanceTimersByTimeAsync(720);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
expect(screen.getByTestId('baby-object-intro-item')).toBeTruthy();
expect(
within(screen.getByTestId('baby-object-intro-item')).getByAltText('香蕉'),
).toBeTruthy();
await act(async () => {
await vi.advanceTimersByTimeAsync(2000);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(720);
});
await act(async () => {
await vi.advanceTimersByTimeAsync(1000);
});
expect(screen.queryByTestId('baby-object-intro-item')).toBeNull();
expect(screen.getByLabelText('礼物盒')).toBeTruthy();
vi.useRealTimers();
});
test('applies generated visual package to stage, gift box, baskets, smoke and hud', async () => {
vi.useFakeTimers();
const { container } = render(
@@ -270,6 +356,8 @@ test('applies generated visual package to stage, gift box, baskets, smoke and hu
expect(stage.style.getPropertyValue('--baby-object-smoke-image')).toContain(
'smoke',
);
await advanceInitialTargetPreview();
expect(screen.getByAltText('礼物盒')).toBeTruthy();
expect(
container.querySelector('.baby-object-runtime__basket-shell-image'),
@@ -283,6 +371,80 @@ test('applies generated visual package to stage, gift box, baskets, smoke and hu
vi.useRealTimers();
});
test('uses default runtime hand indicators instead of per-draft generated hand assets', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const draftWithLegacyHandAssets: BabyObjectMatchDraft = {
...createVisualPackageDraft(),
visualPackage: {
...createVisualPackageDraft().visualPackage!,
assets: [
...createVisualPackageDraft().visualPackage!.assets,
{
assetId: 'legacy-left-hand',
assetKind: 'left-hand',
imageSrc: 'data:image/png;base64,legacy-left-hand',
assetObjectId: null,
generationProvider: 'vector-engine-gpt-image-2',
prompt: '旧左手',
},
{
assetId: 'legacy-right-hand',
assetKind: 'right-hand',
imageSrc: 'data:image/png;base64,legacy-right-hand',
assetObjectId: null,
generationProvider: 'vector-engine-gpt-image-2',
prompt: '旧右手',
},
],
},
};
const { container, rerender } = render(
<BabyObjectMatchRuntimeShell
draft={draftWithLegacyHandAssets}
random={random}
mocapInput={createMocapInput()}
/>,
);
await advanceRoundIntro();
rerender(
<BabyObjectMatchRuntimeShell
draft={draftWithLegacyHandAssets}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'show-default-hand',
receivedAtMs: 1,
},
})}
/>,
);
expect(screen.getByTestId('baby-object-left-hand')).toBeTruthy();
const stage = container.querySelector('.baby-object-runtime__stage');
expect(stage).toBeInstanceOf(HTMLElement);
expect(
(stage as HTMLElement).style.getPropertyValue(
'--baby-object-left-hand-image',
),
).toBe('');
expect(
(stage as HTMLElement).style.getPropertyValue(
'--baby-object-right-hand-image',
),
).toBe('');
vi.useRealTimers();
});
test('removes the gift box after smoke releases the current item', async () => {
vi.useFakeTimers();
render(
@@ -292,6 +454,8 @@ test('removes the gift box after smoke releases the current item', async () => {
/>,
);
await advanceInitialTargetPreview();
expect(screen.getByLabelText('礼物盒')).toBeTruthy();
await act(async () => {
@@ -335,10 +499,16 @@ test('keeps left and right baskets fixed while only the gift item is random', as
).toBeTruthy();
expect(screen.getByLabelText('左侧篮子 苹果')).toBeTruthy();
expect(screen.getByLabelText('右侧篮子 香蕉')).toBeTruthy();
expect(
within(screen.getByLabelText('左侧篮子 苹果')).getByText('苹果'),
).toBeTruthy();
expect(
within(screen.getByLabelText('右侧篮子 香蕉')).getByText('香蕉'),
).toBeTruthy();
vi.useRealTimers();
});
test('mocap camera-right hand movement sends the player left hand item into the left basket', async () => {
test('mocap hand must touch the current item before dropping it into a basket', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
@@ -358,13 +528,211 @@ test('mocap camera-right hand movement sends the player left hand item into the
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
hands: [{ x: 0.24, y: 0.72, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.24, y: 0.72, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
rightHand: { x: 0.24, y: 0.72, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'camera-right-horizontal-1',
text: 'drop-without-grab',
receivedAtMs: 1,
},
})}
/>,
);
expect(screen.queryByText('真棒')).toBeNull();
expect(screen.queryByText('再想一想吧')).toBeNull();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'touch-current-item',
receivedAtMs: 2,
},
})}
/>,
);
expect(screen.getByTestId('baby-object-left-hand')).toBeTruthy();
expect(screen.queryByText('真棒')).toBeNull();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.22, y: 0.78, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.22, y: 0.78, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.22, y: 0.78, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'drop-left-basket',
receivedAtMs: 3,
},
})}
/>,
);
expect(screen.getByText('真棒')).toBeTruthy();
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
vi.useRealTimers();
});
test('mocap hand uses skeleton wrist before hand landmark points in baby object runtime', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput()}
/>,
);
await advanceRoundIntro();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [
{
x: 0.5,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.62, y: 0.37 },
},
],
primaryHand: {
x: 0.5,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.62, y: 0.37 },
},
leftHand: null,
rightHand: {
x: 0.5,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.62, y: 0.37 },
},
bodyJoints: {
rightWrist: { x: 0.64, y: 0.37 },
},
},
rawPacketPreview: {
text: 'hand-points-over-item-skeleton-wrist-away',
receivedAtMs: 1,
},
})}
/>,
);
expect(screen.queryByTestId('baby-object-left-hand')).toBeTruthy();
expect(screen.queryByText('真棒')).toBeNull();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [
{
x: 0.62,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.5, y: 0.37 },
},
],
primaryHand: {
x: 0.62,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.5, y: 0.37 },
},
leftHand: null,
rightHand: {
x: 0.62,
y: 0.37,
state: 'open_palm',
side: 'right',
source: 'palm_center',
wrist: { x: 0.62, y: 0.37 },
},
bodyJoints: {
rightWrist: { x: 0.5, y: 0.37 },
},
},
rawPacketPreview: {
text: 'skeleton-wrist-over-item-hand-points-away',
receivedAtMs: 2,
},
})}
/>,
);
expect(screen.getByTestId('baby-object-left-hand').className).toContain(
'baby-object-runtime__hand--holding-left-corner',
);
vi.useRealTimers();
});
test('basket judgement accepts the enlarged basket edge while keeping center gap safe', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput()}
/>,
);
await advanceRoundIntro();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'touch-current-item-before-narrow-zone',
receivedAtMs: 1,
},
})}
@@ -378,40 +746,22 @@ test('mocap camera-right hand movement sends the player left hand item into the
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.24, y: 0.45, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
hands: [{ x: 0.37, y: 0.82, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.37, y: 0.82, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
rightHand: { x: 0.37, y: 0.82, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'camera-right-horizontal-2',
text: 'outside-enlarged-left-hitbox-center-gap',
receivedAtMs: 2,
},
})}
/>,
);
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'camera-right-horizontal-3',
receivedAtMs: 3,
},
})}
/>,
);
expect(screen.queryByText('真棒')).toBeNull();
expect(screen.queryByText('再想一想吧')).toBeNull();
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
rerender(
<BabyObjectMatchRuntimeShell
@@ -420,14 +770,14 @@ test('mocap camera-right hand movement sends the player left hand item into the
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.31, y: 0.45, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
hands: [{ x: 0.36, y: 0.62, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.36, y: 0.62, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
rightHand: { x: 0.36, y: 0.62, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'camera-right-horizontal-4',
receivedAtMs: 4,
text: 'enlarged-left-hitbox-edge',
receivedAtMs: 3,
},
})}
/>,
@@ -438,7 +788,7 @@ test('mocap camera-right hand movement sends the player left hand item into the
vi.useRealTimers();
});
test('mocap camera-left hand movement sends the player right hand item into the right basket', async () => {
test('either mocap hand can drag the current item into either basket', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
@@ -458,12 +808,12 @@ test('mocap camera-left hand movement sends the player right hand item into the
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
leftHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
rightHand: null,
},
rawPacketPreview: { text: 'camera-left-horizontal-1', receivedAtMs: 1 },
rawPacketPreview: { text: 'right-hand-touch-item', receivedAtMs: 1 },
})}
/>,
);
@@ -475,48 +825,12 @@ test('mocap camera-left hand movement sends the player right hand item into the
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.8, y: 0.45, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
leftHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
hands: [{ x: 0.78, y: 0.78, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.78, y: 0.78, state: 'open_palm', side: 'left' },
leftHand: { x: 0.78, y: 0.78, state: 'open_palm', side: 'left' },
rightHand: null,
},
rawPacketPreview: { text: 'camera-left-horizontal-2', receivedAtMs: 2 },
})}
/>,
);
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
rightHand: null,
},
rawPacketPreview: { text: 'camera-left-horizontal-3', receivedAtMs: 3 },
})}
/>,
);
expect(screen.queryByText('再想一想吧')).toBeNull();
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={random}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.73, y: 0.45, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
leftHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
rightHand: null,
},
rawPacketPreview: { text: 'camera-left-horizontal-4', receivedAtMs: 4 },
rawPacketPreview: { text: 'right-hand-drop-right', receivedAtMs: 2 },
})}
/>,
);
@@ -526,7 +840,84 @@ test('mocap camera-left hand movement sends the player right hand item into the
vi.useRealTimers();
});
test('mocap action names do not select a basket without horizontal hand movement', async () => {
test('holding hand indicator anchors to the lower item corner by hand side', async () => {
vi.useFakeTimers();
const leftHandRandom = createRandomSequence([0, 0]);
const leftHandRuntime = render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={leftHandRandom}
mocapInput={createMocapInput()}
/>,
);
await advanceRoundIntro();
leftHandRuntime.rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={leftHandRandom}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'right' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
leftHand: null,
rightHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'right' },
},
rawPacketPreview: {
text: 'left-hand-holding-corner',
receivedAtMs: 1,
},
})}
/>,
);
expect(screen.getByTestId('baby-object-left-hand').className).toContain(
'baby-object-runtime__hand--holding-left-corner',
);
leftHandRuntime.unmount();
const rightHandRandom = createRandomSequence([0, 0]);
const rightHandRuntime = render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={rightHandRandom}
mocapInput={createMocapInput()}
/>,
);
await advanceRoundIntro();
rightHandRuntime.rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={rightHandRandom}
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x: 0.5, y: 0.37, state: 'open_palm', side: 'left' }],
primaryHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
leftHand: { x: 0.5, y: 0.37, state: 'open_palm', side: 'left' },
rightHand: null,
},
rawPacketPreview: {
text: 'right-hand-holding-corner',
receivedAtMs: 2,
},
})}
/>,
);
expect(screen.getByTestId('baby-object-right-hand').className).toContain(
'baby-object-runtime__hand--holding-right-corner',
);
rightHandRuntime.unmount();
vi.useRealTimers();
});
test('mocap action names do not select a basket without touching and dragging item', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
@@ -564,7 +955,7 @@ test('mocap action names do not select a basket without horizontal hand movement
vi.useRealTimers();
});
test('mocap unknown hand horizontal movement does not select a basket', async () => {
test('mocap unknown hand movement does not grab or select a basket', async () => {
vi.useFakeTimers();
const random = createRandomSequence([0, 0]);
const { rerender } = render(
@@ -578,7 +969,8 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
await advanceRoundIntro();
for (let index = 0; index < 4; index += 1) {
const x = [0.22, 0.24, 0.22, 0.31][index] ?? 0.22;
const x = [0.5, 0.5, 0.22, 0.22][index] ?? 0.5;
const y = [0.37, 0.78, 0.78, 0.37][index] ?? 0.37;
rerender(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
@@ -586,13 +978,13 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
mocapInput={createMocapInput({
latestCommand: {
actions: [],
hands: [{ x, y: 0.45, state: 'open_palm', side: 'unknown' }],
primaryHand: { x, y: 0.45, state: 'open_palm', side: 'unknown' },
hands: [{ x, y, state: 'open_palm', side: 'unknown' }],
primaryHand: { x, y, state: 'open_palm', side: 'unknown' },
leftHand: null,
rightHand: null,
},
rawPacketPreview: {
text: `unknown-horizontal-${index + 1}`,
text: `unknown-drag-${index + 1}`,
receivedAtMs: index + 1,
},
})}
@@ -608,7 +1000,7 @@ test('mocap unknown hand horizontal movement does not select a basket', async ()
vi.useRealTimers();
});
test('left hand horizontal drag sends a correct item into the left basket', async () => {
test('left mouse hand drags a correct item into the left basket', async () => {
vi.useFakeTimers();
const { container } = render(
<BabyObjectMatchRuntimeShell
@@ -622,7 +1014,7 @@ test('left hand horizontal drag sends a correct item into the left basket', asyn
}
await advanceRoundIntro();
dragHand(stage, 0);
dragItemWithHand(stage, 0, 70);
expect(screen.getByText('真棒')).toBeTruthy();
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
@@ -657,19 +1049,55 @@ test('ignores drag input until the item animation finishes', async () => {
throw new Error('Missing baby object runtime stage');
}
dragHand(stage, 0);
dragItemWithHand(stage, 0, 70);
expect(screen.queryByText('真棒')).toBeNull();
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
await advanceRoundIntro();
dragHand(stage, 0);
dragItemWithHand(stage, 0, 70);
expect(screen.getByText('真棒')).toBeTruthy();
expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
vi.useRealTimers();
});
test('keeps the back button outside active gameplay pointer input', async () => {
vi.useFakeTimers();
const onBack = vi.fn();
render(
<BabyObjectMatchRuntimeShell
draft={createDraft()}
random={createRandomSequence([0, 0])}
onBack={onBack}
/>,
);
await advanceRoundIntro();
const backButton = screen.getByRole('button', { name: '返回' });
let pointerDownEvent!: Event;
act(() => {
pointerDownEvent = dispatchPointerEvent(backButton, 'pointerdown', {
pointerId: 9,
button: 0,
buttons: 1,
clientX: 16,
clientY: 16,
});
});
expect(pointerDownEvent.defaultPrevented).toBe(false);
expect(screen.queryByTestId('baby-object-left-hand')).toBeNull();
act(() => {
backButton.click();
});
expect(onBack).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
test('correct placement automatically shows the next gift item', async () => {
vi.useFakeTimers();
const { container } = render(
@@ -691,7 +1119,7 @@ test('correct placement automatically shows the next gift item', async () => {
within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
).toBeTruthy();
dragHand(stage, 0);
dragItemWithHand(stage, 0, 70);
expect(screen.getByText('真棒')).toBeTruthy();
@@ -722,7 +1150,7 @@ test('wrong basket keeps the item active after feedback', async () => {
}
await advanceRoundIntro();
dragHand(stage, 2);
dragItemWithHand(stage, 2, 250);
expect(screen.getByText('再想一想吧')).toBeTruthy();
expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
@@ -752,7 +1180,7 @@ test('twenty correct placements completes the level', async () => {
for (let index = 0; index < 20; index += 1) {
await advanceRoundIntro();
dragHand(stage, 0);
dragItemWithHand(stage, 0, 70);
await advanceFeedback();
}