收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -20,22 +20,24 @@ vi.mock('../ResolvedAssetImage', () => ({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
refreshKey,
|
||||
'data-testid': dataTestId,
|
||||
}: {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
refreshKey?: string | number | null;
|
||||
'data-testid'?: string;
|
||||
}) => (
|
||||
}) =>
|
||||
src ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={className}
|
||||
data-refresh-key={refreshKey ?? undefined}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
) : null
|
||||
),
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-works/puzzleAssetClient', () => ({
|
||||
@@ -79,6 +81,24 @@ function stubReferenceImageUpload(dataUrl: string) {
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||||
}
|
||||
|
||||
test('renders missing draft notice with shared PlatformSubpanel chrome', () => {
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={{ ...createSession(), draft: null }}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const noticePanel = screen
|
||||
.getByText('还没有可编辑的拼图草稿')
|
||||
.closest('.platform-subpanel');
|
||||
|
||||
expect(noticePanel?.className).toContain('rounded-[1rem]');
|
||||
expect(noticePanel?.className).toContain('sm:p-5');
|
||||
expect(noticePanel?.className).toContain('text-[var(--platform-text-base)]');
|
||||
});
|
||||
|
||||
function createSession(
|
||||
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
|
||||
): PuzzleAgentSessionSnapshot {
|
||||
@@ -227,8 +247,27 @@ describe('PuzzleResultView', () => {
|
||||
);
|
||||
|
||||
openPuzzleLevelsTab();
|
||||
const levelImage = screen.getByRole('img', { name: '雨夜猫街' });
|
||||
const mediaFrame = levelImage.closest('div.relative');
|
||||
expect(mediaFrame?.className).toContain('aspect-[4/3]');
|
||||
expect(mediaFrame?.className).not.toContain(
|
||||
'bg-[var(--platform-subpanel-fill)]',
|
||||
);
|
||||
expect(mediaFrame?.className).toContain('rounded-none');
|
||||
expect(screen.getByText('雨夜猫街')).toBeTruthy();
|
||||
const levelTitleButton = within(
|
||||
screen.getByLabelText('拼图关卡列表'),
|
||||
).getByRole('button', { name: '雨夜猫街' });
|
||||
const levelCard = levelTitleButton.closest('.platform-subpanel');
|
||||
expect(levelCard?.className).toContain('rounded-[1.35rem]');
|
||||
expect(levelCard?.className).toContain('p-0');
|
||||
expect(levelCard?.className).toContain('overflow-hidden');
|
||||
expect(screen.getByText('获得更多积分激励')).toBeTruthy();
|
||||
fireEvent.click(screen.getByText('雨夜猫街'));
|
||||
const levelDialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
expect(within(levelDialog).getByText('关卡名称').className).toContain(
|
||||
'tracking-[0.18em]',
|
||||
);
|
||||
});
|
||||
|
||||
test('result action bar restores draft trial entry', () => {
|
||||
@@ -334,10 +373,17 @@ describe('PuzzleResultView', () => {
|
||||
target: { value: '暖灯猫街合集' },
|
||||
});
|
||||
|
||||
expect(screen.getByText('保存中').className).toContain(
|
||||
'border-[var(--platform-warm-border)]',
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(screen.getByText('已自动保存').className).toContain(
|
||||
'border-emerald-200',
|
||||
);
|
||||
expect(puzzleWorksService.updatePuzzleWork).toHaveBeenCalledWith(
|
||||
'puzzle-profile-session-1',
|
||||
expect.objectContaining({
|
||||
@@ -356,6 +402,38 @@ describe('PuzzleResultView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('auto save failure badge uses PlatformPillBadge danger chrome', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(puzzleWorksService.updatePuzzleWork).mockRejectedValue(
|
||||
new Error('保存失败'),
|
||||
);
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
profileId="puzzle-profile-session-1"
|
||||
onBack={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '作品信息' }));
|
||||
fireEvent.change(screen.getByLabelText('作品名称'), {
|
||||
target: { value: '暖灯猫街异常版' },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
const failureBadge = screen
|
||||
.getAllByText('保存失败')
|
||||
.find((element) => element.tagName.toLowerCase() === 'span');
|
||||
expect(failureBadge?.className).toContain(
|
||||
'border-[var(--platform-button-danger-border)]',
|
||||
);
|
||||
});
|
||||
|
||||
test('opens an independent level detail dialog for generation and test play', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
const onStartTestRun = vi.fn();
|
||||
@@ -387,7 +465,9 @@ describe('PuzzleResultView', () => {
|
||||
name: '确认消耗泥点',
|
||||
});
|
||||
expect(within(confirmDialog).getByText('消耗 2 泥点')).toBeTruthy();
|
||||
fireEvent.click(within(confirmDialog).getByRole('button', { name: '确定' }));
|
||||
fireEvent.click(
|
||||
within(confirmDialog).getByRole('button', { name: '确定' }),
|
||||
);
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'generate_puzzle_images',
|
||||
@@ -405,7 +485,9 @@ describe('PuzzleResultView', () => {
|
||||
themeTags: ['猫咪', '雨夜', '暖灯'],
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
expect(screen.getByRole('progressbar', { name: '画面生成进度' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('progressbar', { name: '画面生成进度' }),
|
||||
).toBeTruthy();
|
||||
const generatePayload = onExecuteAction.mock.calls[0]![0];
|
||||
expect(JSON.parse(generatePayload.levelsJson ?? '[]')).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -421,6 +503,19 @@ describe('PuzzleResultView', () => {
|
||||
).toBeNull();
|
||||
const levelList = screen.getByLabelText('拼图关卡列表');
|
||||
expect(within(levelList).getAllByText('生成中').length).toBeGreaterThan(0);
|
||||
const generationBadges = within(levelList)
|
||||
.getAllByText('生成中')
|
||||
.map((element) => element.closest('span'));
|
||||
expect(
|
||||
generationBadges.some((badge) =>
|
||||
badge?.className.includes('bg-white/94'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
generationBadges.some((badge) =>
|
||||
badge?.className.includes('bg-amber-100'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const levelNameInput = within(dialog).getByLabelText('关卡名称');
|
||||
const formalImageTitle = within(dialog).getByText('画面图');
|
||||
@@ -439,7 +534,9 @@ describe('PuzzleResultView', () => {
|
||||
name: '关闭关卡图片预览',
|
||||
}),
|
||||
);
|
||||
expect(within(dialog).getByRole('button', { name: '更换参考图' })).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '更换参考图' }),
|
||||
).toBeTruthy();
|
||||
const pictureDescriptionInput = within(dialog).getByLabelText('画面描述');
|
||||
expect(levelNameInput.closest('.platform-subpanel')).toBeNull();
|
||||
expect(formalImageTitle.closest('.platform-subpanel')).toBeNull();
|
||||
@@ -701,7 +798,9 @@ describe('PuzzleResultView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /发布/u }));
|
||||
const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
expect(within(publishDialog).getByText('还有关卡画面正在生成。')).toBeTruthy();
|
||||
expect(
|
||||
within(publishDialog).getByText('还有关卡画面正在生成。'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(publishDialog).getByRole('button', { name: /发布到广场/u }),
|
||||
).toHaveProperty('disabled', true);
|
||||
@@ -837,11 +936,30 @@ describe('PuzzleResultView', () => {
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /发布/u }));
|
||||
const publishDialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
expect(within(publishDialog).getByText('发布检查').className).toContain(
|
||||
'tracking-[0.18em]',
|
||||
);
|
||||
expect(within(publishDialog).getByText('封面关卡').className).toContain(
|
||||
'tracking-[0.18em]',
|
||||
);
|
||||
const coverImage = within(publishDialog).getByRole('img', {
|
||||
name: '雨夜猫街',
|
||||
});
|
||||
expect(coverImage.closest('div.relative')?.className).toContain(
|
||||
'aspect-square',
|
||||
);
|
||||
expect(coverImage.closest('div.relative')?.className).toContain(
|
||||
'border-[var(--platform-subpanel-border)]',
|
||||
);
|
||||
expect(coverImage.closest('div.relative')?.className).toContain(
|
||||
'bg-white/68',
|
||||
);
|
||||
expect(coverImage.getAttribute('data-refresh-key')).toBe(
|
||||
'2026-04-26T10:00:00.000Z:/puzzle/candidate-1.png:1',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '发布拼图作品' })).getByRole(
|
||||
'button',
|
||||
{ name: /发布到广场/u },
|
||||
),
|
||||
within(publishDialog).getByRole('button', { name: /发布到广场/u }),
|
||||
);
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith(
|
||||
@@ -1286,7 +1404,8 @@ describe('PuzzleResultView', () => {
|
||||
levels: [
|
||||
{
|
||||
...createSession().draft!.levels![0]!,
|
||||
pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
|
||||
pictureReference:
|
||||
'/generated-puzzle-assets/history/saved-reference.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1302,6 +1421,14 @@ describe('PuzzleResultView', () => {
|
||||
|
||||
openPuzzleLevelsTab();
|
||||
fireEvent.click(screen.getByText('雨夜猫街'));
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
const referenceImage = within(dialog).getByAltText('拼图参考图');
|
||||
const referenceRow = referenceImage.closest('div')?.parentElement;
|
||||
expect(referenceRow?.className).toContain('flex items-center gap-3');
|
||||
expect(referenceRow?.className).toContain('bg-white/72');
|
||||
expect(within(dialog).getByText('已选择参考图').className).toContain(
|
||||
'truncate',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
|
||||
@@ -1313,7 +1440,8 @@ describe('PuzzleResultView', () => {
|
||||
expect(onExecuteAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'generate_puzzle_images',
|
||||
referenceImageSrc: '/generated-puzzle-assets/history/saved-reference.png',
|
||||
referenceImageSrc:
|
||||
'/generated-puzzle-assets/history/saved-reference.png',
|
||||
aiRedraw: true,
|
||||
}),
|
||||
);
|
||||
@@ -1367,7 +1495,8 @@ describe('PuzzleResultView', () => {
|
||||
levels: [
|
||||
{
|
||||
...createSession().draft!.levels![0]!,
|
||||
pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
|
||||
pictureReference:
|
||||
'/generated-puzzle-assets/history/saved-reference.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1387,18 +1516,22 @@ describe('PuzzleResultView', () => {
|
||||
|
||||
expect(within(dialog).getByText('画面图')).toBeTruthy();
|
||||
expect(within(dialog).getByLabelText('上传参考图')).toBeTruthy();
|
||||
expect(within(dialog).getByRole('switch', { name: 'AI重绘' })).toHaveProperty(
|
||||
'checked',
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
within(dialog).getByRole('switch', { name: 'AI重绘' }),
|
||||
).toHaveProperty('checked', true);
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '选择历史图片' }),
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.change(within(dialog).getByLabelText('画面AI重绘要求(提示词)'), {
|
||||
target: { value: '只重绘第一关猫街画面' },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.change(
|
||||
within(dialog).getByLabelText('画面AI重绘要求(提示词)'),
|
||||
{
|
||||
target: { value: '只重绘第一关猫街画面' },
|
||||
},
|
||||
);
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
|
||||
'button',
|
||||
@@ -1429,7 +1562,8 @@ describe('PuzzleResultView', () => {
|
||||
levels: [
|
||||
{
|
||||
...createSession().draft!.levels![0]!,
|
||||
pictureReference: '/generated-puzzle-assets/history/saved-reference.png',
|
||||
pictureReference:
|
||||
'/generated-puzzle-assets/history/saved-reference.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1448,7 +1582,9 @@ describe('PuzzleResultView', () => {
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
|
||||
fireEvent.click(within(dialog).getByRole('switch', { name: 'AI重绘' }));
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
|
||||
'button',
|
||||
@@ -1497,14 +1633,18 @@ describe('PuzzleResultView', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(dialog).getByRole('switch', { name: 'AI重绘' })).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('switch', { name: 'AI重绘' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
expect(within(dialog).getByAltText('拼图参考图')).toHaveProperty(
|
||||
'src',
|
||||
uploadedDataUrl,
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole('switch', { name: 'AI重绘' }));
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
|
||||
'button',
|
||||
@@ -1539,11 +1679,10 @@ describe('PuzzleResultView', () => {
|
||||
openPuzzleLevelsTab();
|
||||
fireEvent.click(screen.getByText('雨夜猫街'));
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
const referenceInputs = within(dialog).getAllByLabelText('上传参考图', {
|
||||
const referenceInput = within(dialog).getByLabelText('上传描述参考图', {
|
||||
selector: 'input',
|
||||
});
|
||||
expect(referenceInputs.length).toBeGreaterThanOrEqual(2);
|
||||
fireEvent.change(referenceInputs[referenceInputs.length - 1]!, {
|
||||
fireEvent.change(referenceInput, {
|
||||
target: {
|
||||
files: [new File(['x'], 'prompt-reference.png', { type: 'image/png' })],
|
||||
},
|
||||
@@ -1556,7 +1695,9 @@ describe('PuzzleResultView', () => {
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '确认消耗泥点' })).getByRole(
|
||||
'button',
|
||||
@@ -1628,7 +1769,19 @@ describe('PuzzleResultView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('智能修订拼图草稿'), {
|
||||
const creativeDraftInput = screen.getByLabelText('智能修订拼图草稿');
|
||||
const creativeDraftPanel = creativeDraftInput.closest('.platform-subpanel');
|
||||
const creativeDraftIconBadge = creativeDraftPanel?.querySelector(
|
||||
'span[aria-hidden="true"]',
|
||||
);
|
||||
|
||||
expect(creativeDraftPanel?.className).toContain('rounded-[1.35rem]');
|
||||
expect(creativeDraftPanel?.className).toContain('sm:p-4');
|
||||
expect(creativeDraftIconBadge?.className).toContain('h-9');
|
||||
expect(creativeDraftIconBadge?.className).toContain('rounded-full');
|
||||
expect(creativeDraftIconBadge?.className).toContain('bg-white/72');
|
||||
|
||||
fireEvent.change(creativeDraftInput, {
|
||||
target: { value: '把标题改得轻松一点' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改' }));
|
||||
|
||||
Reference in New Issue
Block a user