合并架构调整分支

合入 codex/architecture-adjustment 的架构调整提交
保留 master 上推荐页资源等待和微信订阅时间修复

# Conflicts:
#	docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
#	src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
#	src/components/rpg-entry/RpgEntryHomeView.tsx
This commit is contained in:
2026-06-08 18:01:43 +08:00
136 changed files with 23121 additions and 7787 deletions

View File

@@ -41,7 +41,6 @@ import {
const {
mockQrCodeToDataUrl,
mockRedirectToPaymentUrl,
mockRequestJson,
mockBuildReferralCenter,
mockBuildTaskCenter,
mockClaimRpgProfileTaskReward,
@@ -129,7 +128,6 @@ const {
return {
mockQrCodeToDataUrl: qrCodeToDataUrl,
mockRedirectToPaymentUrl: redirectToPaymentUrl,
mockRequestJson: vi.fn(),
mockBuildReferralCenter: buildReferralCenter,
mockBuildTaskCenter: buildTaskCenter,
mockGetRpgProfileReferralInviteCenter: vi.fn(async () =>
@@ -309,12 +307,21 @@ const {
amountDelta: -1,
balanceAfter: 29,
sourceType: 'asset_operation_consume',
createdAt: '2026-05-03T08:00:00Z',
},
{
id: 'ledger-2',
amountDelta: 30,
balanceAfter: 30,
sourceType: 'invite_invitee_reward',
createdAt: '2026-05-03T09:00:00Z',
},
{
id: 'ledger-3',
amountDelta: 5,
balanceAfter: 35,
sourceType: 'puzzle_author_incentive_claim',
createdAt: '2026-05-03T10:00:00Z',
},
],
})),
@@ -326,6 +333,7 @@ const {
mockGetPublicAuthUserByCode,
mockGetPublicAuthUserById,
mockRefreshStoredAccessToken,
mockRequestJson,
mockUpdateAuthProfile,
} = vi.hoisted(() => ({
mockGetPublicAuthUserByCode: vi.fn(
@@ -347,15 +355,20 @@ const {
}),
),
mockRefreshStoredAccessToken: vi.fn(async () => 'jwt-refreshed-token'),
mockRequestJson: vi.fn(async () => ({
read: {
objectKey: 'generated-recommend/default.png',
signedUrl: 'https://signed.example.com/default-cover.png',
expiresAt: '2099-01-01T00:10:00Z',
},
})),
mockUpdateAuthProfile: vi.fn(),
}));
vi.mock('../../services/apiClient', () => ({
BACKGROUND_AUTH_REQUEST_OPTIONS: {
authImpact: 'background',
},
requestJson: mockRequestJson,
BACKGROUND_AUTH_REQUEST_OPTIONS: {},
refreshStoredAccessToken: mockRefreshStoredAccessToken,
requestJson: mockRequestJson,
}));
vi.mock('../../services/authService', () => ({
@@ -1045,7 +1058,7 @@ function renderStatefulLoggedOutHomeView(
function StatefulLoggedOutHomeView() {
const [activeTab, setActiveTab] =
useState<RpgEntryHomeViewProps['activeTab']>('home');
useState<RpgEntryHomeViewProps['activeTab']>('category');
return (
<AuthUiContext.Provider
@@ -1140,6 +1153,13 @@ afterEach(() => {
);
mockGetRpgProfileTasks.mockResolvedValue(mockBuildTaskCenter());
mockRefreshStoredAccessToken.mockResolvedValue('jwt-refreshed-token');
mockRequestJson.mockResolvedValue({
read: {
objectKey: 'generated-recommend/default.png',
signedUrl: 'https://signed.example.com/default-cover.png',
expiresAt: '2099-01-01T00:10:00Z',
},
});
mockClaimRpgProfileTaskReward.mockResolvedValue({
taskId: 'daily_login',
dayKey: 20260503,
@@ -1228,6 +1248,7 @@ test('opens wallet ledger modal from narrative coin card', async () => {
expect(screen.getByText('-1')).toBeTruthy();
expect(screen.getByText('填写邀请码奖励')).toBeTruthy();
expect(screen.getByText('+30')).toBeTruthy();
expect(screen.getByText('拼图作者奖励')).toBeTruthy();
});
test('profile recharge modal shows native qr code on desktop web by default', async () => {
@@ -2666,7 +2687,7 @@ test('profile total play time card always uses hours', async () => {
});
const playTimeCard = screen.getByRole('button', {
name: //u,
name: //u,
});
expect(within(playTimeCard).getByText('1.5小时')).toBeTruthy();
@@ -2680,11 +2701,11 @@ test('profile played works card shows count unit', async () => {
});
const playedCard = screen.getByRole('button', {
name: /\s*1/u,
name: /\s*1/u,
});
expect(within(playedCard).getByText('1个')).toBeTruthy();
expect(within(playedCard).queryByText('已玩游戏数量')).toBeNull();
expect(within(playedCard).getByText('已玩游戏数量')).toBeTruthy();
await screen.findByText('1 / 1');
});
@@ -2696,8 +2717,12 @@ test('profile stats cards are centered without update timestamp', async () => {
const walletCard = screen.getByRole('button', {
name: /\s*0/u,
});
const playTimeCard = screen.getByRole('button', { name: /\s*0/u });
const playedCard = screen.getByRole('button', { name: /\s*0/u });
const playTimeCard = screen.getByRole('button', {
name: /\s*0/u,
});
const playedCard = screen.getByRole('button', {
name: /\s*0/u,
});
for (const card of [walletCard, playTimeCard, playedCard]) {
expect(card.className).toContain('platform-profile-stat-card');
@@ -2749,8 +2774,8 @@ test('mobile profile page matches the reference layout sections', async () => {
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*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');
@@ -3646,19 +3671,24 @@ test('public gallery cards hide phone masked author and public user code', async
expect(within(card).queryByText('SY-00000003')).toBeNull();
});
test('logged out mobile shell defaults to recommend tab', () => {
test('logged out mobile shell defaults to discover tab', () => {
const { container } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
});
const activePanel = container.querySelector('.platform-tab-panel--active');
expect(activePanel?.id).toBe('platform-tab-panel-home');
expect(activePanel?.id).toBe('platform-tab-panel-category');
expect(
screen.getByPlaceholderText('搜索作品号、名称、作者、描述'),
).toBeTruthy();
expect(container.querySelector('.platform-mobile-topbar')).toBeTruthy();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeTruthy();
).toBeNull();
});
test('logged out recommend tab opens embedded runtime without login modal', async () => {
const user = userEvent.setup();
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
@@ -3668,6 +3698,10 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn
throw new Error('缺少底部导航');
}
await user.click(
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
expect(openLoginModal).not.toHaveBeenCalled();
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
@@ -3680,6 +3714,7 @@ test('logged out recommend tab opens embedded runtime without login modal', asyn
});
test('logged out recommend runtime keeps detail callback idle', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const { openLoginModal } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
@@ -3691,6 +3726,10 @@ test('logged out recommend runtime keeps detail callback idle', async () => {
throw new Error('缺少底部导航');
}
await user.click(
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
expect(openLoginModal).not.toHaveBeenCalled();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
@@ -3908,9 +3947,6 @@ test('mobile recommend startup keeps cover visible without loading copy', () =>
expect(
document.querySelector('.platform-recommend-runtime-cover'),
).toBeTruthy();
expect(
document.querySelector('.platform-recommend-runtime-loading'),
).toBeTruthy();
expect(screen.queryByText('加载中...')).toBeNull();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
});
@@ -4244,7 +4280,7 @@ test('mobile recommend cover waits for async runtime resources beyond the main i
).toContain('platform-recommend-runtime-cover--hidden');
});
test('mobile recommend keeps runtime visual stable when active entry changes', async () => {
test('mobile recommend next level keeps runtime visual stable when active work changes', async () => {
vi.useFakeTimers();
const animationCallbacks: FrameRequestCallback[] = [];
const flushAnimationFrames = () => {
@@ -4285,18 +4321,18 @@ test('mobile recommend keeps runtime visual stable when active entry changes', a
worldName: '当前拼图',
coverImageSrc: 'current-cover.png',
} satisfies PlatformPublicGalleryCard;
const nextEntry = {
const similarEntry = {
...puzzlePublicEntry,
workId: 'puzzle-work-next-1',
profileId: 'puzzle-profile-next-1',
workId: 'puzzle-work-similar-1',
profileId: 'puzzle-profile-similar-1',
ownerUserId: 'user-feed-2',
publicWorkCode: 'PZ-NEXT1',
worldName: '下一张拼图',
coverImageSrc: 'next-cover.png',
publicWorkCode: 'PZ-SIMILAR1',
worldName: '相似拼图',
coverImageSrc: 'similar-cover.png',
} satisfies PlatformPublicGalleryCard;
const { rerender } = renderLoggedOutHomeView(vi.fn(), {
latestEntries: [firstEntry, nextEntry],
latestEntries: [firstEntry, similarEntry],
activeRecommendEntryKey: 'puzzle:user-feed-1:puzzle-profile-feed-1',
isRecommendRuntimeReady: true,
});
@@ -4335,7 +4371,7 @@ test('mobile recommend keeps runtime visual stable when active entry changes', a
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[firstEntry, nextEntry]}
latestEntries={[firstEntry, similarEntry]}
myEntries={[]}
historyEntries={[]}
profileDashboard={null}
@@ -4350,7 +4386,7 @@ test('mobile recommend keeps runtime visual stable when active entry changes', a
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={vi.fn()}
recommendRuntimeContent={<div data-testid="recommend-runtime" />}
activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-next-1"
activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-similar-1"
isRecommendRuntimeReady
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={vi.fn()}
@@ -4364,7 +4400,7 @@ test('mobile recommend keeps runtime visual stable when active entry changes', a
) as HTMLElement | null;
expect(rail?.className).toContain('platform-recommend-swipe-rail--settled');
expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)');
expect(screen.getByLabelText('下一张拼图 作品信息')).toBeTruthy();
expect(screen.getByLabelText('相似拼图 作品信息')).toBeTruthy();
expect(
document.querySelector('.platform-recommend-runtime-cover')?.className,
).toContain('platform-recommend-runtime-cover--hidden');
@@ -4471,10 +4507,8 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(
document.querySelectorAll(
'.platform-recommend-runtime-preview:not(.platform-recommend-runtime-preview--cover)',
),
).toHaveLength(2);
document.querySelectorAll('.platform-recommend-runtime-preview'),
).toHaveLength(3);
expect(
document.querySelectorAll('.platform-recommend-swipe-card'),
).toHaveLength(3);
@@ -4524,10 +4558,6 @@ test('logged in recommend runtime preloads adjacent work previews and drag switc
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
expect(onSelectPreviousRecommendEntry).not.toHaveBeenCalled();
expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)');
expect(rail?.className).toContain(
'platform-recommend-swipe-rail--resetting',
);
vi.useRealTimers();
});
@@ -4738,7 +4768,6 @@ test('mobile discover recommend feed only rotates the card closest to screen cen
});
test('mobile discover recommend feed renders cover fallback for legacy browsers', async () => {
const user = userEvent.setup();
renderStatefulLoggedOutHomeView({
latestEntries: [
{
@@ -4748,7 +4777,6 @@ test('mobile discover recommend feed renders cover fallback for legacy browsers'
},
],
});
await user.click(screen.getByRole('button', { name: '发现' }));
const discoverPanel = document.getElementById('platform-tab-panel-category');
if (!discoverPanel) {