收口前端平台组件库能力

新增 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

@@ -19,6 +19,7 @@ import type {
import type {
ConfirmWechatProfileRechargeOrderResponse,
CreateProfileRechargeOrderResponse,
ProfilePlayStatsResponse,
ProfileReferralInviteCenterResponse,
ProfileTaskCenterResponse,
} from '../../../packages/shared/src/contracts/runtime';
@@ -777,6 +778,8 @@ function ProfileHomeViewHarness({
userOverrides = {},
activeTab = 'profile',
profileTaskRefreshKey = 0,
profilePlayStats = null,
isProfilePlayStatsOpen = false,
}: {
onRechargeSuccess?: () => void | Promise<void>;
profileDashboardOverrides?: Partial<
@@ -785,6 +788,8 @@ function ProfileHomeViewHarness({
userOverrides?: Partial<AuthUser>;
activeTab?: RpgEntryHomeViewProps['activeTab'];
profileTaskRefreshKey?: number;
profilePlayStats?: ProfilePlayStatsResponse | null;
isProfilePlayStatsOpen?: boolean;
}) {
return (
<AuthUiContext.Provider
@@ -837,6 +842,8 @@ function ProfileHomeViewHarness({
isLoadingPlatform={false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
profilePlayStats={profilePlayStats}
isProfilePlayStatsOpen={isProfilePlayStatsOpen}
platformError={null}
dashboardError={null}
onContinueGame={vi.fn()}
@@ -860,6 +867,10 @@ function renderProfileView(
> = {},
userOverrides: Partial<AuthUser> = {},
profileTaskRefreshKey = 0,
profileStatsOptions: {
profilePlayStats?: ProfilePlayStatsResponse | null;
isProfilePlayStatsOpen?: boolean;
} = {},
) {
return render(
<ProfileHomeViewHarness
@@ -867,6 +878,8 @@ function renderProfileView(
profileDashboardOverrides={profileDashboardOverrides}
userOverrides={userOverrides}
profileTaskRefreshKey={profileTaskRefreshKey}
profilePlayStats={profileStatsOptions.profilePlayStats}
isProfilePlayStatsOpen={profileStatsOptions.isProfilePlayStatsOpen}
/>,
);
}
@@ -1141,7 +1154,10 @@ afterEach(() => {
configurable: true,
value: undefined,
});
Reflect.deleteProperty(globalThis as Record<string, unknown>, 'BarcodeDetector');
Reflect.deleteProperty(
globalThis as Record<string, unknown>,
'BarcodeDetector',
);
window.wx = undefined;
document
.querySelectorAll(
@@ -1238,11 +1254,16 @@ test('opens wallet ledger modal from narrative coin card', async () => {
const user = userEvent.setup();
renderProfileView();
await user.click(
screen.getByRole('button', { name: /\s*0/u }),
);
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('泥点账单')).toBeTruthy();
const ledgerModal = screen
.getByText('泥点账单')
.closest('.fixed') as HTMLElement;
const balanceBadge = within(ledgerModal).getByText('29泥点');
expect(balanceBadge.className).toContain('rounded-full');
expect(balanceBadge.className).toContain('border-rose-100');
expect(balanceBadge.className).toContain('bg-white/70');
expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
expect(screen.getByText('资产操作消耗')).toBeTruthy();
expect(screen.getByText('-1')).toBeTruthy();
@@ -1577,7 +1598,8 @@ test('profile recharge modal posts membership goods virtual payment params in mi
);
expect(requestId).toBeTruthy();
const payParams = JSON.parse(
new URL(`https://mini.test${navigateUrl}`).searchParams.get('payParams') ?? '{}',
new URL(`https://mini.test${navigateUrl}`).searchParams.get('payParams') ??
'{}',
);
const signData = JSON.parse(payParams.signData);
expect(payParams.mode).toBe('short_series_goods');
@@ -1654,9 +1676,7 @@ test('profile recharge modal releases submitting state and shows virtual payment
expect(
await screen.findByRole('dialog', { name: '支付未完成' }),
).toBeTruthy();
expect(
screen.getByText(/requestVirtualPayment:fail sandbox/u),
).toBeTruthy();
expect(screen.getByText(/requestVirtualPayment:fail sandbox/u)).toBeTruthy();
await waitFor(() => {
expect(
within(screen.getByRole('button', { name: /60/u })).getByText(
@@ -2222,7 +2242,9 @@ test('profile recharge modal blocks tab navigation while virtual payment confirm
hasPointsRecharged: false,
},
});
mockWatchWechatRpgProfileRechargeOrder.mockReturnValueOnce(new Promise(() => undefined));
mockWatchWechatRpgProfileRechargeOrder.mockReturnValueOnce(
new Promise(() => undefined),
);
renderProfileView();
await openRechargeModal(user);
@@ -2527,7 +2549,9 @@ test('profile daily task shortcut reflects task progress and claim updates', asy
await waitFor(() => {
expect(within(dailyTask).getByText('1 / 1')).toBeTruthy();
});
expect(dailyTask.querySelector('.platform-profile-daily-task-card__action')).toBeNull();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__action'),
).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
@@ -2695,6 +2719,55 @@ test('profile total play time card always uses hours', async () => {
await screen.findByText('1 / 1');
});
test('profile played modal summary and work type use platform pill badges', async () => {
const user = userEvent.setup();
renderProfileView(
vi.fn(),
{
totalPlayTimeMs: 90 * 60 * 1000,
playedWorldCount: 1,
},
{},
0,
{
isProfilePlayStatsOpen: true,
profilePlayStats: {
totalPlayTimeMs: 90 * 60 * 1000,
playedWorks: [
{
worldKey: 'custom:world-1',
ownerUserId: 'user-1',
profileId: 'world-1',
worldType: 'CUSTOM',
worldTitle: '潮雾列岛',
worldSubtitle: '旧灯塔与失控航路',
firstPlayedAt: '2026-04-18T12:00:00.000Z',
lastPlayedAt: '2026-04-19T12:00:00.000Z',
lastObservedPlayTimeMs: 30 * 60 * 1000,
},
],
updatedAt: '2026-04-19T12:00:00.000Z',
},
},
);
await user.click(screen.getByRole('button', { name: //u }));
const playedModal = screen
.getByText('潮雾列岛')
.closest('.fixed') as HTMLElement;
const totalPlayTimeBadge = within(playedModal).getByText('1.5小时');
expect(totalPlayTimeBadge.className).toContain('rounded-full');
expect(totalPlayTimeBadge.className).toContain('border-rose-100');
expect(totalPlayTimeBadge.className).toContain('bg-rose-50');
const workTypeBadge = within(playedModal).getByText('RPG');
expect(workTypeBadge.className).toContain('rounded-full');
expect(workTypeBadge.className).toContain('bg-rose-50');
expect(workTypeBadge.className).toContain('text-[#ff4056]');
});
test('profile played works card shows count unit', async () => {
renderProfileView(vi.fn(), {
playedWorldCount: 1,
@@ -2744,9 +2817,13 @@ test('mobile profile page matches the reference layout sections', async () => {
const profilePage = container.querySelector('.platform-profile-page');
expect(profilePage).toBeTruthy();
expect(profilePage?.classList.contains('platform-page-stage')).toBe(false);
expect(profilePage?.querySelector('.platform-profile-scene-decor')).toBeTruthy();
expect(
profilePage?.querySelector('.platform-profile-scene-decor'),
).toBeTruthy();
expect(profilePage?.classList.contains('platform-profile-page')).toBe(true);
expect(profilePage?.getAttribute('style') ?? '').not.toContain('overflow: hidden');
expect(profilePage?.getAttribute('style') ?? '').not.toContain(
'overflow: hidden',
);
const topbar = container.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
@@ -2763,30 +2840,50 @@ test('mobile profile page matches the reference layout sections', async () => {
).toBeNull();
const membershipCard = screen.getByRole('button', { name: '查看权益' });
expect(membershipCard.className).toContain('platform-profile-membership-card');
expect(
within(membershipCard).getByText('普通用户').className,
).toContain('platform-profile-membership-card__title');
expect(membershipCard.className).toContain(
'platform-profile-membership-card',
);
expect(within(membershipCard).getByText('普通用户').className).toContain(
'platform-profile-membership-card__title',
);
expect(within(membershipCard).getByText('普通用户')).toBeTruthy();
expect(within(membershipCard).getByText('升级会员,享专属特权与福利')).toBeTruthy();
expect(
within(membershipCard).getByText('升级会员,享专属特权与福利'),
).toBeTruthy();
const statPanel = screen.getByRole('region', { name: '我的数据' });
expect(statPanel.className).toContain('platform-profile-stats-panel');
expect(statPanel.querySelector('.platform-profile-stats-grid')).toBeTruthy();
expect(within(statPanel).getByRole('button', { name: /\s*70/u })).toBeTruthy();
expect(within(statPanel).getByRole('button', { name: /\s*0/u })).toBeTruthy();
expect(within(statPanel).getByRole('button', { name: /\s*0/u })).toBeTruthy();
expect(
within(statPanel).getByRole('button', { name: /\s*70/u }),
).toBeTruthy();
expect(
within(statPanel).getByRole('button', { name: /\s*0/u }),
).toBeTruthy();
expect(
within(statPanel).getByRole('button', { name: /\s*0/u }),
).toBeTruthy();
expect(
within(statPanel).getByRole('button', { name: /\s*70/u }).className,
).toContain('platform-profile-stat-card');
expect(statPanel.querySelectorAll('.platform-profile-stat-card__icon')).toHaveLength(3);
expect(
statPanel.querySelectorAll('.platform-profile-stat-card__icon'),
).toHaveLength(3);
const dailyTask = screen.getByRole('button', { name: //u });
expect(dailyTask.className).toContain('platform-profile-daily-task-card');
expect(dailyTask.querySelector('.platform-profile-daily-task-card__title')).toBeTruthy();
expect(dailyTask.querySelector('.platform-profile-daily-task-card__desc')).toBeTruthy();
expect(dailyTask.querySelector('.platform-profile-daily-task-card__progress')).toBeTruthy();
expect(dailyTask.querySelector('.platform-profile-daily-task-card__action')).toBeNull();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__title'),
).toBeTruthy();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__desc'),
).toBeTruthy();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__progress'),
).toBeTruthy();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__action'),
).toBeNull();
expect(dailyTask.textContent).not.toContain('去完成');
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
expect(await within(dailyTask).findByText('1 / 1')).toBeTruthy();
@@ -2804,28 +2901,21 @@ test('mobile profile page matches the reference layout sections', async () => {
?.classList.contains('platform-profile-shortcut-grid'),
).toBe(true);
expect(
shortcutRegion
.querySelector('.platform-profile-shortcut-grid')
?.className,
shortcutRegion.querySelector('.platform-profile-shortcut-grid')?.className,
).toContain('!grid-cols-4');
expect(
shortcutRegion
.querySelector('.platform-profile-shortcut-grid')
?.className,
shortcutRegion.querySelector('.platform-profile-shortcut-grid')?.className,
).toContain('w-full');
for (const shortcutButton of shortcutRegion.querySelectorAll(
'.platform-profile-shortcut-button',
)) {
expect(shortcutButton.className).toContain('w-full');
}
for (const label of [
'泥点充值',
'兑换码',
'玩家社区',
'反馈与建议',
]) {
for (const label of ['泥点充值', '兑换码', '玩家社区', '反馈与建议']) {
expect(
within(shortcutRegion).getByRole('button', { name: new RegExp(label, 'u') }),
within(shortcutRegion).getByRole('button', {
name: new RegExp(label, 'u'),
}),
).toBeTruthy();
}
expect(
@@ -2840,10 +2930,18 @@ test('mobile profile page matches the reference layout sections', async () => {
).toBeNull();
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
expect(within(settingsRegion).getByRole('button', { name: //u })).toBeTruthy();
expect(within(settingsRegion).queryByRole('button', { name: //u })).toBeNull();
expect(within(settingsRegion).queryByRole('button', { name: //u })).toBeNull();
expect(settingsRegion.querySelectorAll('.platform-profile-settings-row')).toHaveLength(1);
expect(
within(settingsRegion).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(
settingsRegion.querySelectorAll('.platform-profile-settings-row'),
).toHaveLength(1);
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
@@ -2852,9 +2950,15 @@ test('mobile profile page matches the reference layout sections', async () => {
const profileHeader = profilePage?.querySelector('.platform-profile-header');
expect(profileHeader).toBeTruthy();
expect(profileHeader?.querySelector('.platform-profile-header__identity-row')).toBeTruthy();
expect(profileHeader?.querySelector('.platform-profile-header__name')).toBeTruthy();
expect(profileHeader?.querySelector('.platform-profile-header__code')).toBeTruthy();
expect(
profileHeader?.querySelector('.platform-profile-header__identity-row'),
).toBeTruthy();
expect(
profileHeader?.querySelector('.platform-profile-header__name'),
).toBeTruthy();
expect(
profileHeader?.querySelector('.platform-profile-header__code'),
).toBeTruthy();
const legalRegion = screen.getByRole('region', { name: '法律信息' });
expect(legalRegion.className).toContain('platform-profile-legal-strip');
@@ -2863,7 +2967,9 @@ test('mobile profile page matches the reference layout sections', async () => {
expect(legalRegion.textContent).toContain('免责声明');
expect(legalRegion.textContent).toContain(ICP_RECORD_NUMBER);
expect(legalRegion.textContent).toContain('2026025677');
expect(legalRegion.querySelector('.platform-profile-legal-strip__link')).toBeTruthy();
expect(
legalRegion.querySelector('.platform-profile-legal-strip__link'),
).toBeTruthy();
});
test('profile scan action opens camera scanner instead of recharge panel', async () => {
@@ -2989,10 +3095,7 @@ test('profile community shortcut shows reward subtitle and invited users', async
});
test('profile page hides legacy redeem invite secondary shortcut for fresh accounts', async () => {
renderProfileView(
vi.fn(),
{},
);
renderProfileView(vi.fn(), {});
const communityButton = screen.getByRole('button', { name: //u });
@@ -3080,10 +3183,7 @@ test('profile redeem invite query modal submits code after login', async () => {
const onRechargeSuccess = vi.fn();
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
renderProfileView(
onRechargeSuccess,
{},
);
renderProfileView(onRechargeSuccess, {});
expect(await screen.findByLabelText('邀请码')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '提交' }));
@@ -3105,9 +3205,7 @@ test('profile task center reloads when refresh key changes', async () => {
expect(mockGetRpgProfileTasks).toHaveBeenCalledTimes(1);
});
rerender(
<ProfileHomeViewHarness profileTaskRefreshKey={1} />,
);
rerender(<ProfileHomeViewHarness profileTaskRefreshKey={1} />);
await waitFor(() => {
expect(mockGetRpgProfileTasks).toHaveBeenCalledTimes(2);
@@ -3152,12 +3250,20 @@ test('profile page shows legal entries and hides archive shortcuts', async () =>
const dailyTask = screen.getByRole('button', { name: //u });
expect(dailyTask).toBeTruthy();
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
expect(dailyTask.querySelector('.platform-profile-daily-task-card__action')).toBeNull();
expect(
dailyTask.querySelector('.platform-profile-daily-task-card__action'),
).toBeNull();
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
expect(within(settingsRegion).getByRole('button', { name: //u })).toBeTruthy();
expect(within(settingsRegion).queryByRole('button', { name: //u })).toBeNull();
expect(within(settingsRegion).queryByRole('button', { name: //u })).toBeNull();
expect(
within(settingsRegion).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
@@ -3737,10 +3843,15 @@ test('logged out recommend runtime keeps detail callback idle', async () => {
test('logged out desktop recommend page renders runtime directly', () => {
mockDesktopLayout();
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
}, 'home', true);
renderLoggedOutHomeView(
vi.fn(),
{
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
},
'home',
true,
);
expect(document.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
@@ -3755,10 +3866,14 @@ test('logged out recommend page can enter runtime without login gate', () => {
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
onOpenGalleryDetail,
recommendRuntimeContent: <div data-testid="recommend-runtime"></div>,
recommendRuntimeContent: (
<div data-testid="recommend-runtime"></div>
),
});
expect(screen.queryByRole('button', { name: / /u })).toBeNull();
expect(
screen.queryByRole('button', { name: / /u }),
).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(openLoginModal).not.toHaveBeenCalled();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
@@ -3767,7 +3882,8 @@ test('logged out recommend page can enter runtime without login gate', () => {
test('mobile recommend meta matches active jump hop runtime entry', () => {
renderLoggedOutHomeView(vi.fn(), {
latestEntries: [puzzlePublicEntry, jumpHopPublicEntry],
activeRecommendEntryKey: 'jump-hop:jump-hop-user-1:jump-hop-profile-public-1',
activeRecommendEntryKey:
'jump-hop:jump-hop-user-1:jump-hop-profile-public-1',
recommendRuntimeContent: (
<div data-testid="recommend-runtime"></div>
),
@@ -3932,7 +4048,9 @@ test('logged out mobile recommend page renders runtime instead of cover', () =>
document.querySelector('.platform-public-work-card__cover'),
).toBeNull();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
expect(screen.queryByRole('button', { name: / /u })).toBeNull();
expect(
screen.queryByRole('button', { name: / /u }),
).toBeNull();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -4013,7 +4131,9 @@ test('mobile recommend preloads every fetched work cover', () => {
);
expect(preloadedSrcs).not.toContain('/generated-recommend/current.png');
return waitFor(() => {
expect(preloadedSrcs).toContain('https://signed.example.com/current-cover.png');
expect(preloadedSrcs).toContain(
'https://signed.example.com/current-cover.png',
);
});
});
@@ -4037,7 +4157,9 @@ test('mobile recommend runtime cover keeps the labeled work card visual', () =>
expect(coverImage?.getAttribute('src')).toBe('card-cover-1.png');
expect(coverImage?.getAttribute('src')).not.toBe('card-cover-fallback.png');
expect(cover?.querySelector('.platform-recommend-runtime-preview')).toBeTruthy();
expect(
cover?.querySelector('.platform-recommend-runtime-preview'),
).toBeTruthy();
expect(cover?.textContent).toContain('拼图');
expect(cover?.textContent).toContain('轮播拼图');
});
@@ -4539,7 +4661,9 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
expect(onLikeRecommendEntry).toHaveBeenCalledWith(firstEntry);
expect(onShareRecommendEntry).toHaveBeenCalledWith(firstEntry);
expect(onRemixRecommendEntry).toHaveBeenCalledWith(firstEntry);
expect(activeRecommendCard.getByRole('button', { name: '分享' })).toBeTruthy();
expect(
activeRecommendCard.getByRole('button', { name: '分享' }),
).toBeTruthy();
act(() => {
dispatchPointerEvent(meta, 'pointerdown', { pointerId: 1, clientY: 300 });