feat(jump-hop): optimize generated assets and runtime background

This commit is contained in:
2026-06-04 22:34:19 +08:00
parent c442c3c3f0
commit 0041b95f72
17 changed files with 1160 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { act, fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import type {
@@ -229,7 +229,31 @@ test('跳一跳蓄力时角色沿拖拽方向拉伸', async () => {
);
});
test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
test('跳一跳运行态游玩中只保留得分并隐藏常驻排行榜', () => {
const runtimeRequestOptions = {
runtimeGuestToken: 'runtime-guest-token',
};
render(
<JumpHopRuntimeShell
profile={buildProfile()}
run={buildRun()}
runtimeRequestOptions={runtimeRequestOptions}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(useJumpHopLeaderboard).not.toHaveBeenCalled();
expect(screen.getByTestId('jump-hop-three-scene')).toBeTruthy();
expect(screen.queryByTestId('jump-hop-runtime-leaderboard')).toBeNull();
expect(screen.queryByRole('button', { name: /重开/ })).toBeNull();
expect(screen.queryByText('进行中')).toBeNull();
expect(screen.queryByText('00:00')).toBeNull();
expect(screen.queryByRole('button', { name: /^起跳$/ })).toBeNull();
});
test('跳一跳运行态失败后在弹窗中展示排行榜', () => {
const runtimeRequestOptions = {
runtimeGuestToken: 'runtime-guest-token',
};
@@ -255,7 +279,7 @@ test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile()}
run={buildRun()}
run={buildFailedRun()}
runtimeRequestOptions={runtimeRequestOptions}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
@@ -266,12 +290,12 @@ test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
'jump-hop-profile-test',
runtimeRequestOptions,
);
expect(screen.getByTestId('jump-hop-three-scene')).toBeTruthy();
expect(screen.getByTestId('jump-hop-runtime-leaderboard')).toBeTruthy();
expect(screen.getByText('player-1')).toBeTruthy();
expect(screen.getByText('8 跳')).toBeTruthy();
expect(screen.getByText('00:08')).toBeTruthy();
expect(screen.queryByRole('button', { name: /^起跳$/ })).toBeNull();
expect(screen.getByRole('dialog', { name: '失败' })).toBeTruthy();
const leaderboard = screen.getByTestId('jump-hop-runtime-leaderboard');
expect(leaderboard).toBeTruthy();
expect(within(leaderboard).getByText('player-1')).toBeTruthy();
expect(within(leaderboard).getByText('8 跳')).toBeTruthy();
expect(within(leaderboard).getByText('00:08')).toBeTruthy();
});
test('跳一跳角色层永远压在地块层之上', () => {
@@ -356,6 +380,7 @@ test('跳一跳运行态直接渲染生成的地块切片图片', () => {
const tileImages = screen.getAllByTestId('jump-hop-tile-image');
expect(tileImages).toHaveLength(3);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const generatedReadUrlCalls = vi
.mocked(useResolvedAssetReadUrl)
.mock.calls.filter(([source]) =>
@@ -379,6 +404,25 @@ test('跳一跳运行态直接渲染生成的地块切片图片', () => {
}
});
test('跳一跳运行态提前预加载下一屏地块且不在真实图片加载前露出原型方块', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile({ tileAssets: buildTileAssets() })}
run={buildRunWithExtraPreviewPlatform()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(screen.getAllByTestId('jump-hop-tile-image')).toHaveLength(3);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const preloadImages = screen.getAllByTestId('jump-hop-tile-preload-image');
expect(preloadImages.length).toBeGreaterThan(0);
expect(preloadImages[0]?.getAttribute('src')).toContain(
'/generated-jump-hop-assets/jump-hop-profile-test/tile-',
);
});
test('跳一跳运行态首块地块落在中下方并且后续两块向中央和上方展开', () => {
render(
<JumpHopRuntimeShell
@@ -679,6 +723,14 @@ test('跳一跳松手后先播放飞行动画再切换到下一块地块', async
expect(styleText).toMatch(
/data-platform-advancing='true'\]\s+\.jump-hop-runtime__platform[\s\S]*transform 1440ms cubic-bezier/,
);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const advancingCharacterRule = styleText.match(
/\.jump-hop-runtime__stage\[data-platform-advancing='true'\]\s+\.jump-hop-runtime__character\s*\{(?<body>[\s\S]*?)\}/,
)?.groups?.body;
expect(advancingCharacterRule).toContain('transform 120ms ease');
expect(advancingCharacterRule).toContain('opacity 160ms ease');
expect(advancingCharacterRule).not.toContain('left');
expect(advancingCharacterRule).not.toContain('top');
expect(screen.getByTestId('jump-hop-three-scene').parentElement).toBe(
cameraLayer,
);
@@ -823,6 +875,50 @@ function buildRun(): JumpHopRuntimeRunSnapshotResponse {
};
}
function buildFailedRun(): JumpHopRuntimeRunSnapshotResponse {
return {
...buildRun(),
status: 'failed',
successfulJumpCount: 8,
durationMs: 8123,
score: 8,
combo: 0,
lastJump: {
chargeMs: 420,
jumpDistance: 1.62,
targetPlatformIndex: 1,
landedX: 0,
landedY: 0,
result: 'miss',
},
finishedAtMs: 9123,
};
}
function buildRunWithExtraPreviewPlatform(): JumpHopRuntimeRunSnapshotResponse {
const run = buildRun();
return {
...run,
path: {
...run.path,
platforms: [
...run.path.platforms,
{
platformId: 'p3',
tileType: 'normal',
x: 0.5,
y: 3.6,
width: 1,
height: 1,
landingRadius: 0.5,
perfectRadius: 0.2,
scoreValue: 1,
},
],
},
};
}
function buildTileAssets() {
return Array.from({ length: 25 }, (_, index) => {
const tileNumber = String(index + 1).padStart(2, '0');
@@ -845,6 +941,8 @@ function buildTileAssets() {
function buildProfile(options: {
tileAssets?: JumpHopWorkProfileResponse['tileAssets'];
coverComposite?: string | null;
coverImageSrc?: string | null;
} = {}): JumpHopWorkProfileResponse {
const characterAsset = {
assetId: 'builtin',
@@ -869,7 +967,7 @@ function buildProfile(options: {
themeTags: ['测试'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
coverImageSrc: options.coverImageSrc ?? null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-27T00:00:00Z',
@@ -901,7 +999,7 @@ function buildProfile(options: {
tileAtlasAsset: characterAsset,
tileAssets: options.tileAssets ?? [],
path: buildRun().path,
coverComposite: null,
coverComposite: options.coverComposite ?? null,
generationStatus: 'ready',
},
path: buildRun().path,
@@ -917,3 +1015,46 @@ function buildProfile(options: {
tileAssets: options.tileAssets ?? [],
};
}
test('跳一跳运行态使用 image2 背景底图铺满舞台底层', () => {
const backgroundSource =
'/generated-jump-hop-assets/jump-hop-profile-test/background/image.png';
render(
<JumpHopRuntimeShell
profile={buildProfile({ coverComposite: backgroundSource })}
run={buildRun()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
const backgroundImage = screen.getByTestId('jump-hop-stage-background-image');
expect(backgroundImage.getAttribute('src')).toBe(backgroundSource);
const backdrop = document.querySelector('.jump-hop-runtime__scene-backdrop');
expect(backdrop?.getAttribute('data-has-background')).toBe('true');
expect(useResolvedAssetReadUrl).toHaveBeenCalledWith(
backgroundSource,
expect.objectContaining({
refreshKey: backgroundSource,
}),
);
});
test('跳一跳运行态忽略旧 cover composite 占位背景', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile({
coverComposite:
'/generated-jump-hop-assets/jump-hop-profile-test/cover-composite.png',
})}
run={buildRun()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(screen.queryByTestId('jump-hop-stage-background-image')).toBeNull();
const backdrop = document.querySelector('.jump-hop-runtime__scene-backdrop');
expect(backdrop?.getAttribute('data-has-background')).toBe('false');
});