收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -51,7 +51,9 @@ vi.mock('../../services/match3d-works', () => ({
vi.mock('../../services/match3dSpritesheetParser', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../services/match3dSpritesheetParser')>();
await importOriginal<
typeof import('../../services/match3dSpritesheetParser')
>();
return {
...actual,
loadMatch3DSpritesheetAssetRegions:
@@ -146,6 +148,96 @@ function createReadyGeneratedItemAsset(index: number) {
}
describe('Match3DResultView', () => {
test('标准白底面板使用 PlatformSubpanel lg 外壳', async () => {
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions.mockResolvedValue(
Array.from({ length: 10 }, (_, index) => ({
label: `${index + 1}`,
x: index * 2,
y: 0,
width: 2,
height: 2,
sheetWidth: 20,
sheetHeight: 2,
imageSrc: `data:image/png;base64,item-${index + 1}`,
})),
);
const { container } = render(
<Match3DResultView
profile={createProfile({
generatedItemAssets: [
createReadyGeneratedItemAsset(1),
createReadyGeneratedItemAsset(2),
],
generatedBackgroundAsset: {
prompt: '果园主题抓大鹅竖屏背景',
imageSrc:
'/generated-match3d-assets/session/profile/background/background.png',
imageObjectKey: null,
uiSpritesheetPrompt: 'UI spritesheet',
uiSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
uiSpritesheetImageObjectKey: null,
itemSpritesheetPrompt: '物品 spritesheet',
itemSpritesheetImageSrc:
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
itemSpritesheetImageObjectKey: null,
status: 'image_ready',
error: null,
},
})}
onBack={() => {}}
onStartTestRun={() => {}}
/>,
);
const getStandardPanels = () =>
Array.from(
container.querySelectorAll('section.platform-subpanel'),
).filter(
(panel) =>
panel.className.includes('rounded-[1.35rem]') &&
panel.className.includes('p-4') &&
panel.className.includes('sm:p-5'),
);
expect(getStandardPanels()).toHaveLength(1);
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
expect(getStandardPanels()).toHaveLength(2);
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
fireEvent.click(screen.getByRole('button', { name: 'UI素材' }));
await waitFor(() => {
expect(screen.getByAltText('物品素材图')).toBeTruthy();
});
expect(getStandardPanels()).toHaveLength(2);
});
test('标准字段标题使用 PlatformFieldLabel section 外观', () => {
render(
<Match3DResultView
profile={createProfile()}
onBack={() => {}}
onStartTestRun={() => {}}
/>,
);
const workNameLabel = screen.getByText('作品名称');
expect(workNameLabel.className).toContain('tracking-[0.18em]');
fireEvent.click(screen.getByRole('button', { name: '发布' }));
expect(screen.getByText('发布检查').className).toContain(
'tracking-[0.18em]',
);
expect(screen.getByText('封面图').className).toContain('tracking-[0.18em]');
expect(screen.getByText('封面图').className).toContain('block');
});
test('作品信息 Tab 字段命名对齐拼图草稿且描述可为空', async () => {
const profile = createProfile();
vi.mocked(match3dWorksService.updateMatch3DWork).mockResolvedValue({
@@ -583,7 +675,9 @@ describe('Match3DResultView', () => {
within(publishDialog).getByRole('button', { name: '发布到广场' }),
).toHaveProperty('disabled', true);
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
fireEvent.click(within(publishDialog).getByRole('button', { name: '取消' }));
fireEvent.click(
within(publishDialog).getByRole('button', { name: '取消' }),
);
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
expect(screen.getByText('已生成物品种类')).toBeTruthy();
expect(screen.getAllByText('2 种').length).toBeGreaterThan(0);
@@ -638,9 +732,10 @@ describe('Match3DResultView', () => {
fireEvent.click(screen.getByRole('button', { name: '发布' }));
fireEvent.click(
within(
screen.getByRole('dialog', { name: '发布抓大鹅作品' }),
).getByRole('button', { name: '发布到广场' }),
within(screen.getByRole('dialog', { name: '发布抓大鹅作品' })).getByRole(
'button',
{ name: '发布到广场' },
),
);
await waitFor(() => {
@@ -704,8 +799,14 @@ describe('Match3DResultView', () => {
expect(screen.getByRole('button', { name: '物品' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'UI素材' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '背景音乐' })).toBeNull();
expect(screen.getAllByRole('button', { name: /打开.+物品素材/u }))
.toHaveLength(20);
expect(
screen.getAllByRole('button', { name: /打开.+物品素材/u }),
).toHaveLength(20);
const assetPreviewFrame = screen
.getByRole('button', { name: '打开水果核心物件物品素材' })
.querySelector('div.relative');
expect(assetPreviewFrame?.className).toContain('aspect-square');
expect(assetPreviewFrame?.className).toContain('bg-white/82');
fireEvent.click(
screen.getByRole('button', { name: '打开水果核心物件物品素材' }),
@@ -851,6 +952,11 @@ describe('Match3DResultView', () => {
fireEvent.change(screen.getByLabelText('物品名称 4'), {
target: { value: '苹果' },
});
const batchDialog = screen.getByRole('dialog', { name: '批量新增物品' });
const parsedNameBadge = within(batchDialog).getByText('蓝莓');
expect(parsedNameBadge.className).toContain('rounded-full');
expect(parsedNameBadge.className).toContain('bg-white/72');
expect(parsedNameBadge.className).not.toContain('platform-pill');
expect(
screen.getByRole('button', { name: /生成物品素材 · 2泥点/u }),
).toBeTruthy();
@@ -908,7 +1014,16 @@ describe('Match3DResultView', () => {
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
expect(screen.queryByRole('dialog', { name: '批量新增物品' })).toBeNull();
expect(screen.getByLabelText('物品素材生成进度')).toBeTruthy();
const generationProgress = screen.getByLabelText('物品素材生成进度');
expect(generationProgress.className).toContain(
'border-[var(--platform-subpanel-border)]',
);
expect(generationProgress.className).toContain('rounded-[1rem]');
expect(generationProgress.className).toContain('bg-white/62');
expect(
screen.getByRole('progressbar', { name: '物品素材生成进度百分比' })
.className,
).toContain('platform-progress-track');
deferred.resolve({
item: createProfile({
@@ -967,6 +1082,12 @@ describe('Match3DResultView', () => {
'value',
'苹果',
);
const targetItemBadge = within(
screen.getByRole('dialog', { name: '批量重新生成物品' }),
).getByText('苹果');
expect(targetItemBadge.className).toContain('rounded-full');
expect(targetItemBadge.className).toContain('bg-white/72');
expect(targetItemBadge.className).not.toContain('platform-pill');
fireEvent.change(screen.getByLabelText('重新生成物品名称 2'), {
target: { value: '' },
});
@@ -1079,6 +1200,19 @@ describe('Match3DResultView', () => {
).toBe('true');
expect(screen.getByText('36 件')).toBeTruthy();
expect(screen.getAllByText('9 种').length).toBeGreaterThan(0);
const difficultySummaryPanel = screen.getByTestId(
'match3d-difficulty-summary-panel',
);
expect(difficultySummaryPanel.className.split(/\s+/u)).not.toContain(
'platform-subpanel',
);
expect(difficultySummaryPanel.className).toContain('bg-white/72');
expect(difficultySummaryPanel.className).toContain('rounded-[1rem]');
expect(difficultySummaryPanel.className).toContain('p-3');
const difficultyBadge = screen.getByText('难度 4');
expect(difficultyBadge.className).toContain('rounded-full');
expect(difficultyBadge.className).toContain('bg-[var(--platform-accent)]');
expect(difficultyBadge.className).toContain('text-white');
fireEvent.change(difficultySlider, { target: { value: '3' } });
@@ -1093,6 +1227,7 @@ describe('Match3DResultView', () => {
});
expect(screen.getByText('63 件')).toBeTruthy();
expect(screen.getAllByText('20 种').length).toBeGreaterThan(0);
expect(screen.getByText('难度 8').className).toContain('rounded-full');
expect(onSaved).toHaveBeenCalledWith(
expect.objectContaining({
clearCount: 21,
@@ -1154,6 +1289,13 @@ describe('Match3DResultView', () => {
fireEvent.click(screen.getByRole('button', { name: '打开物品1物品素材' }));
expect(screen.getByDisplayValue('物品1')).toBeTruthy();
const itemDetailPanel = screen
.getByDisplayValue('物品1')
.closest('section');
expect(itemDetailPanel?.className).toContain('platform-subpanel');
expect(itemDetailPanel?.className).toContain('rounded-[1.5rem]');
expect(itemDetailPanel?.className).toContain('p-3');
expect(itemDetailPanel?.className).toContain('sm:p-5');
expect(
[...document.querySelectorAll('img')].some((image) =>
image
@@ -1218,28 +1360,32 @@ describe('Match3DResultView', () => {
const preview = screen.getByLabelText('物品1五视角预览');
const stage = screen.getByTestId('match3d-item-preview-stage');
const focusImage = screen.getByTestId('match3d-item-preview-focus-image');
const stageImage = stage.querySelector('img');
const thumbnails = screen.getByTestId('match3d-item-preview-thumbnails');
expect(stage.className).toContain('aspect-square');
expect(stage.className).toContain('max-w-[22rem]');
expect(focusImage.className).toContain('place-items-center');
expect(focusImage.querySelector('img')?.className).toContain('p-3');
expect(stage.className).toContain('bg-white/82');
expect(stageImage?.className).toContain('p-3');
expect(thumbnails.style.gridAutoColumns).toBe('calc((100% - 1.5rem) / 4)');
const activeThumbnailFrame = screen
.getByRole('button', { name: '切换物品1视角3' })
.querySelector('div.relative');
expect(activeThumbnailFrame?.className).toContain('aspect-square');
expect(activeThumbnailFrame?.className).toContain('rounded-[0.65rem]');
expect(preview.querySelectorAll('img')).toHaveLength(6);
expect(
screen
.getByRole('button', { name: '切换物品1视角3' })
.getAttribute('aria-pressed'),
).toBe('true');
expect(
screen.queryByTestId('match3d-item-preview-focus-frame'),
).toBeNull();
expect(screen.queryByTestId('match3d-item-preview-focus-frame')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '切换物品1视角5' }));
expect(
screen
.getByTestId('match3d-item-preview-focus-image')
.getAttribute('data-preview-src'),
.getByTestId('match3d-item-preview-stage')
.querySelector('img')
?.getAttribute('src'),
).toContain('views/view-05.png');
});
@@ -1382,8 +1528,12 @@ describe('Match3DResultView', () => {
'/generated-match3d-assets/session/profile/ui-spritesheet/ui.png',
);
expect(
screen.queryByLabelText('UI背景图画面描述提示词'),
).toBeNull();
screen.getByAltText('游戏背景图').closest('div.relative')?.className,
).toContain('aspect-[9/16]');
expect(
screen.getByAltText('UI素材图').closest('div.relative')?.className,
).toContain('aspect-square');
expect(screen.queryByLabelText('UI背景图画面描述提示词')).toBeNull();
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '预览UI页面' }));
@@ -1558,20 +1708,35 @@ describe('Match3DResultView', () => {
expect(screen.getByAltText('物品素材图').getAttribute('src')).toBe(
'/generated-match3d-assets/session/profile/item-spritesheet/items.png',
);
expect(
screen.getByAltText('物品素材图').closest('div.relative')?.className,
).toContain('aspect-square');
await waitFor(() => {
expect(
document.querySelector('.platform-media-tile-grid')?.className,
).toContain('grid-cols-5');
expect(
screen
.getByTestId('match3d-item-spritesheet-preview-0-0')
.getAttribute('src'),
.querySelector('img')
?.getAttribute('src'),
).toBe('data:image/png;base64,item-1');
expect(
screen
.getByTestId('match3d-item-spritesheet-preview-1-4')
.getAttribute('src'),
.querySelector('img')
?.getAttribute('src'),
).toBe('data:image/png;base64,item-10');
});
expect(screen.getByText('草莓')).toBeTruthy();
expect(screen.getByText('苹果')).toBeTruthy();
const strawberryGroupCard = screen.getByText('草莓').parentElement;
expect(strawberryGroupCard?.className).toContain(
'border-[var(--platform-subpanel-border)]',
);
expect(strawberryGroupCard?.className).toContain('rounded-[1rem]');
expect(strawberryGroupCard?.className).toContain('bg-white/58');
expect(strawberryGroupCard?.className).toContain('p-3');
expect(
match3dSpritesheetParser.loadMatch3DSpritesheetAssetRegions,
).toHaveBeenCalledWith(
@@ -1633,8 +1798,12 @@ describe('Match3DResultView', () => {
expect(screen.queryByLabelText('容器形象画面描述提示词')).toBeNull();
expect(screen.queryByRole('button', { name: '容器形象' })).toBeNull();
expect(screen.queryByRole('button', { name: /重新生成/u })).toBeNull();
expect(match3dWorksService.generateMatch3DBackgroundImage).not.toHaveBeenCalled();
expect(match3dWorksService.generateMatch3DContainerImage).not.toHaveBeenCalled();
expect(
match3dWorksService.generateMatch3DBackgroundImage,
).not.toHaveBeenCalled();
expect(
match3dWorksService.generateMatch3DContainerImage,
).not.toHaveBeenCalled();
});
test('历史草稿同时带旧 draft 和 profile 素材时以 profile 多视角素材补齐试玩资产', async () => {

File diff suppressed because it is too large Load Diff