Merge remote-tracking branch 'origin/master' into codex/wooden-fish-template

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	.hermes/shared-memory/pitfalls.md
#	docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
This commit is contained in:
2026-05-26 02:00:11 +08:00
140 changed files with 12176 additions and 3526 deletions

View File

@@ -413,6 +413,11 @@ const originalUserAgent = navigator.userAgent;
const originalMaxTouchPoints = navigator.maxTouchPoints;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame;
const DEFAULT_PROFILE_CREATED_AT = '2026-04-01T00:00:00.000Z';
function buildFreshProfileCreatedAt() {
return new Date().toISOString();
}
function dispatchPointerEvent(
target: HTMLElement,
@@ -670,6 +675,33 @@ function mockWechatMobileLayout() {
});
}
function mockNarrowMobileLayout() {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit Mobile',
});
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: vi.fn().mockImplementation((query: string) => {
const normalizedQuery = query.replace(/\s/g, '');
return {
matches:
normalizedQuery.includes('max-width:767px') ||
normalizedQuery.includes('max-width:768px'),
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
};
}),
});
}
function renderProfileView(
onRechargeSuccess = vi.fn(),
profileDashboardOverrides: Partial<
@@ -690,7 +722,7 @@ function renderProfileView(
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
createdAt: DEFAULT_PROFILE_CREATED_AT,
...userOverrides,
},
canAccessProtectedData: true,
@@ -772,6 +804,7 @@ function renderLoggedOutHomeView(
>
> = {},
activeTab: RpgEntryHomeViewProps['activeTab'] = 'home',
isDesktopLayout = false,
) {
return render(
<AuthUiContext.Provider
@@ -795,6 +828,7 @@ function renderLoggedOutHomeView(
>
<RpgEntryHomeView
activeTab={activeTab}
isDesktopLayout={isDesktopLayout}
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
@@ -922,6 +956,7 @@ function renderStatefulLoggedOutHomeView(
| 'onSelectPreviousRecommendEntry'
>
> = {},
isDesktopLayout = false,
) {
const authSpies = {
openLoginModal: vi.fn(),
@@ -953,6 +988,7 @@ function renderStatefulLoggedOutHomeView(
>
<RpgEntryHomeView
activeTab={activeTab}
isDesktopLayout={isDesktopLayout}
onTabChange={setActiveTab}
hasSavedGame={false}
savedSnapshot={null}
@@ -1003,6 +1039,15 @@ afterEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
configurable: true,
value: vi.fn(async () => undefined),
});
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: undefined,
});
Reflect.deleteProperty(globalThis as Record<string, unknown>, 'BarcodeDetector');
window.wx = undefined;
document
.querySelectorAll(
@@ -1056,7 +1101,7 @@ afterEach(() => {
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: new Date().toISOString(),
createdAt: DEFAULT_PROFILE_CREATED_AT,
});
mockQrCodeToDataUrl.mockResolvedValue('data:image/png;base64,QR');
mockRedirectToPaymentUrl.mockReset();
@@ -1094,7 +1139,9 @@ 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();
expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
@@ -1760,19 +1807,21 @@ test('profile native qr confirmation refreshes only after server reports paid',
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
});
test('non-wechat profile shows reward code instead of recharge entry', async () => {
test('non-wechat profile opens reward code from recharge-shaped entry', async () => {
const user = userEvent.setup();
renderProfileView();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
within(shortcutRegion).queryByRole('button', { name: //u }),
).toBeNull();
within(shortcutRegion).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
within(shortcutRegion).getByRole('button', { name: //u }),
).toBeTruthy();
await user.click(within(shortcutRegion).getByRole('button', { name: //u }));
await user.click(
within(shortcutRegion).getByRole('button', { name: //u }),
);
expect(await screen.findByPlaceholderText('输入兑换码')).toBeTruthy();
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
@@ -1796,10 +1845,68 @@ test('profile daily task shortcut opens task center and claims reward', async ()
});
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
expect(await screen.findByText('已领取 10 泥点')).toBeTruthy();
expect(
(screen.getByRole('button', { name: '已领取' }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(screen.queryByRole('button', { name: '已领取' })).toBeNull();
expect(screen.getByText('暂无任务')).toBeTruthy();
});
test('profile task center keeps only the highest priority actionable task', async () => {
const user = userEvent.setup();
mockGetRpgProfileTasks.mockResolvedValueOnce(
mockBuildTaskCenter({
tasks: [
{
taskId: 'claimed_low',
title: '低优先级已完成',
description: '',
eventKey: 'profile.task.claimed_low',
cycle: 'daily',
threshold: 1,
progressCount: 1,
rewardPoints: 5,
status: 'claimed',
dayKey: 20260503,
claimedAt: '2026-05-03T08:01:00Z',
updatedAt: '2026-05-03T08:01:00Z',
},
{
taskId: 'claimable_mid',
title: '中优先级可领取',
description: '',
eventKey: 'profile.task.claimable_mid',
cycle: 'daily',
threshold: 2,
progressCount: 2,
rewardPoints: 10,
status: 'claimable',
dayKey: 20260503,
claimedAt: null,
updatedAt: '2026-05-03T08:01:00Z',
},
{
taskId: 'incomplete_high',
title: '高优先级未完成',
description: '',
eventKey: 'profile.task.incomplete_high',
cycle: 'daily',
threshold: 3,
progressCount: 1,
rewardPoints: 20,
status: 'incomplete',
dayKey: 20260503,
claimedAt: null,
updatedAt: '2026-05-03T08:01:00Z',
},
],
}),
);
renderProfileView();
await user.click(screen.getByRole('button', { name: //u }));
expect(await screen.findByText('中优先级可领取')).toBeTruthy();
expect(screen.queryByText('高优先级未完成')).toBeNull();
expect(screen.queryByText('低优先级已完成')).toBeNull();
});
test('profile total play time card always uses hours', () => {
@@ -1821,7 +1928,7 @@ test('profile played works card shows count unit', () => {
});
const playedCard = screen.getByRole('button', {
name: /\s*1/u,
name: /\s*1/u,
});
expect(within(playedCard).getByText('1个')).toBeTruthy();
@@ -1832,18 +1939,168 @@ test('profile stats cards are centered without update timestamp', () => {
updatedAt: '2026-05-03T08:01:00Z',
});
const walletCard = screen.getByRole('button', { name: /\s*0/u });
const playTimeCard = screen.getByRole('button', { name: //u });
const playedCard = screen.getByRole('button', { name: /\s*0/u });
const walletCard = screen.getByRole('button', {
name: /\s*0/u,
});
const playTimeCard = screen.getByRole('button', { name: /|/u });
const playedCard = screen.getByRole('button', { name: /\s*0/u });
for (const card of [walletCard, playTimeCard, playedCard]) {
expect(card.className).toContain('items-center');
expect(card.className).toContain('justify-center');
expect(card.className).toContain('platform-profile-stat-card');
expect(card.className).toContain('text-center');
}
expect(screen.queryByText(//u)).toBeNull();
});
test('mobile profile page matches the reference layout sections', async () => {
mockNarrowMobileLayout();
const { container } = renderProfileView(vi.fn(), {
walletBalance: 70,
totalPlayTimeMs: 0,
playedWorldCount: 0,
});
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?.classList.contains('platform-profile-page')).toBe(true);
expect(profilePage?.getAttribute('style') ?? '').not.toContain('overflow: hidden');
const topbar = container.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
).toBeTruthy();
expect(
within(topbar as HTMLElement).getByRole('button', { name: '打开设置' }),
).toBeTruthy();
expect(
within(topbar as HTMLElement).queryByRole('button', {
name: //u,
}),
).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(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 }).className,
).toContain('platform-profile-stat-card');
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.textContent).toContain('完成任务可领取 10 泥点');
expect(within(dailyTask).getByText('0 / 1')).toBeTruthy();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
shortcutRegion.querySelector('.platform-profile-shortcut-grid'),
).toBeTruthy();
expect(
shortcutRegion.querySelectorAll('.platform-profile-shortcut-button'),
).toHaveLength(5);
expect(
shortcutRegion
.querySelector('.platform-profile-shortcut-grid')
?.classList.contains('platform-profile-shortcut-grid'),
).toBe(true);
for (const label of [
'泥点充值',
'邀请好友',
'兑换码',
'玩家社区',
'反馈与建议',
]) {
expect(
within(shortcutRegion).getByRole('button', { name: new RegExp(label, 'u') }),
).toBeTruthy();
}
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
for (const label of ['主题设置', '账号与安全', '通用设置']) {
expect(
within(settingsRegion).getByRole('button', { name: new RegExp(label, 'u') }),
).toBeTruthy();
}
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
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();
const legalRegion = screen.getByRole('region', { name: '法律信息' });
expect(legalRegion.className).toContain('platform-profile-legal-strip');
expect(legalRegion.textContent).toContain('用户协议');
expect(legalRegion.textContent).toContain('隐私政策');
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();
});
test('profile scan action opens camera scanner instead of recharge panel', async () => {
const user = userEvent.setup();
const stopTrack = vi.fn();
const stream = {
getTracks: () => [{ stop: stopTrack }],
} as unknown as MediaStream;
const getUserMedia = vi.fn(async () => stream);
mockNarrowMobileLayout();
Object.defineProperty(globalThis, 'BarcodeDetector', {
configurable: true,
value: class {
async detect() {
return [];
}
},
});
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: { getUserMedia },
});
renderProfileView();
const topbar = document.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
await user.click(
within(topbar as HTMLElement).getByRole('button', { name: '扫码' }),
);
expect(await screen.findByRole('dialog', { name: '扫码' })).toBeTruthy();
await waitFor(() => {
expect(getUserMedia).toHaveBeenCalledWith({
audio: false,
video: { facingMode: { ideal: 'environment' } },
});
});
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
test('desktop account entry uses saved avatar image when available', () => {
mockDesktopLayout();
const avatarUrl = 'data:image/png;base64,AAAA';
@@ -1886,27 +2143,33 @@ test('wallet ledger modal shows empty and error states', async () => {
mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] });
renderProfileView();
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('暂无账单记录')).toBeTruthy();
await user.click(screen.getByRole('button', { name: /\s*0/u }));
expect(await screen.findByText('泥点账单')).toBeTruthy();
await waitFor(() => {
expect(screen.getByText('暂无账单记录')).toBeTruthy();
});
await user.click(screen.getByLabelText('关闭泥点账单'));
mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败'));
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();
expect(await screen.findByText('泥点账单')).toBeTruthy();
await waitFor(() => {
expect(screen.getByText('加载失败')).toBeTruthy();
});
expect(screen.getByText('重新加载')).toBeTruthy();
});
test('profile invite shortcut shows reward subtitle and invited users', async () => {
const user = userEvent.setup();
renderProfileView();
renderProfileView(vi.fn(), {}, { createdAt: buildFreshProfileCreatedAt() });
const inviteButton = screen.getByRole('button', { name: //u });
expect(within(inviteButton).getByText('双方得30')).toBeTruthy();
expect(within(inviteButton).getByText('双方得 30 泥点')).toBeTruthy();
const communityButton = screen.getByRole('button', { name: //u });
expect(within(communityButton).getByText('每日领福利')).toBeTruthy();
expect(within(communityButton).getByText('交流心得 领取福利')).toBeTruthy();
await user.click(inviteButton);
@@ -1922,21 +2185,25 @@ test('profile invite shortcut shows reward subtitle and invited users', async ()
});
test('profile redeem invite shortcut sits between invite and community for fresh accounts', async () => {
renderProfileView();
renderProfileView(
vi.fn(),
{},
{ createdAt: buildFreshProfileCreatedAt() },
);
const inviteButton = screen.getByRole('button', { name: //u });
const redeemButton = await screen.findByRole('button', {
name: //u,
});
const communityButton = screen.getByRole('button', { name: //u });
const secondaryShortcuts = screen.getByRole('region', {
name: '次级入口',
});
expect(inviteButton).toBeTruthy();
expect(communityButton).toBeTruthy();
expect(
inviteButton.compareDocumentPosition(redeemButton) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
expect(
redeemButton.compareDocumentPosition(communityButton) &
Node.DOCUMENT_POSITION_FOLLOWING,
within(secondaryShortcuts).getByRole('button', { name: //u }),
).toBeTruthy();
expect(within(redeemButton).getByText('新用户奖励')).toBeTruthy();
});
@@ -2006,7 +2273,11 @@ test('profile redeem invite modal submits code and hides shortcut after success'
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
renderProfileView(onRechargeSuccess);
renderProfileView(
onRechargeSuccess,
{},
{ createdAt: buildFreshProfileCreatedAt() },
);
await user.click(await screen.findByRole('button', { name: //u }));
const input = await screen.findByLabelText('邀请码');
@@ -2043,18 +2314,17 @@ test('opens reward code modal from profile action on mobile', async () => {
expect(screen.getByLabelText('关闭兑换码')).toBeTruthy();
});
test('profile page shows legal entries and ICP record link', async () => {
test('profile page shows legal entries and hides archive shortcuts', async () => {
const user = userEvent.setup();
renderProfileView();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
shortcutRegion.querySelector('.grid')?.className.includes('grid-cols-3'),
shortcutRegion
.querySelector('.platform-profile-shortcut-grid')
?.classList.contains('platform-profile-shortcut-grid'),
).toBe(true);
expect(
within(shortcutRegion).getByRole('button', { name: //u }),
).toBeTruthy();
expect(
within(shortcutRegion).getByRole('button', { name: //u }),
).toBeTruthy();
@@ -2064,6 +2334,15 @@ test('profile page shows legal entries and ICP record link', async () => {
expect(
within(shortcutRegion).getByRole('button', { name: //u }),
).toBeTruthy();
const dailyTask = screen.getByRole('button', { name: //u });
expect(dailyTask).toBeTruthy();
expect(dailyTask.textContent).toContain('完成任务可领取 10 泥点');
const settingsRegion = screen.getByRole('region', { name: '设置入口' });
expect(
within(settingsRegion).queryByRole('button', { name: //u }),
).toBeNull();
expect(screen.queryByRole('region', { name: '次级入口' })).toBeNull();
const legalRegion = screen.getByRole('region', { name: '法律信息' });
expect(
@@ -2138,6 +2417,83 @@ test('logged in draft bottom tab shows unread marker', () => {
expect(draftButton.querySelector('.platform-nav-unread-dot')).toBeTruthy();
});
test('logged in create tab shows real wallet balance beside the brand', () => {
mockNarrowMobileLayout();
const { container } = render(
<AuthUiContext.Provider
value={{
user: {
id: 'user-1',
publicUserCode: '100001',
username: 'tester',
displayName: '测试玩家',
avatarUrl: null,
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
createdAt: DEFAULT_PROFILE_CREATED_AT,
},
canAccessProtectedData: true,
openLoginModal: vi.fn(),
requireAuth: (action) => action(),
openSettingsModal: vi.fn(),
openAccountModal: vi.fn(),
setCurrentUser: vi.fn(),
logout: vi.fn(async () => undefined),
musicVolume: 0.42,
setMusicVolume: vi.fn(),
platformTheme: 'light',
setPlatformTheme: vi.fn(),
isHydratingSettings: false,
isPersistingSettings: false,
settingsError: null,
}}
>
<RpgEntryHomeView
activeTab="create"
onTabChange={vi.fn()}
hasSavedGame={false}
savedSnapshot={null}
saveEntries={[]}
saveError={null}
featuredEntries={[]}
latestEntries={[]}
myEntries={[]}
historyEntries={[]}
profileDashboard={{
walletBalance: 1234,
totalPlayTimeMs: 0,
playedWorldCount: 0,
updatedAt: null,
}}
isLoadingPlatform={false}
isLoadingDashboard={false}
isResumingSaveWorldKey={null}
platformError={null}
dashboardError={null}
onContinueGame={vi.fn()}
onResumeSave={vi.fn()}
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={vi.fn()}
onOpenLibraryDetail={vi.fn()}
onSearchPublicCode={vi.fn()}
createTabContent={<div></div>}
/>
</AuthUiContext.Provider>,
);
const topbar = container.querySelector('.platform-mobile-topbar');
expect(topbar).toBeTruthy();
expect(
topbar?.querySelector('.platform-mobile-create-wallet-chip'),
).toBeTruthy();
expect(topbar?.textContent).toContain('陶泥儿');
expect(topbar?.textContent).toContain('1,234泥点');
});
test('mobile discover search submits public work code', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
@@ -2248,6 +2604,15 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
throw new Error('缺少发现面板');
}
const discoverStage = discoverPanel.querySelector(
'.platform-mobile-home-stage',
);
expect(discoverStage).toBeTruthy();
expect(discoverStage?.classList.contains('platform-remap-surface')).toBe(
true,
);
expect(discoverStage?.classList.contains('platform-page-stage')).toBe(false);
const channels = Array.from(
discoverPanel.querySelectorAll('.platform-mobile-home-channel'),
).map((button) => button.textContent);
@@ -2442,7 +2807,7 @@ test('logged out mobile shell defaults to discover tab', () => {
).toBeNull();
});
test('logged out recommend tab opens login modal and shows cover only', async () => {
test('logged out recommend tab opens embedded runtime without login modal', async () => {
const user = userEvent.setup();
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
latestEntries: [puzzlePublicEntry],
@@ -2457,20 +2822,18 @@ test('logged out recommend tab opens login modal and shows cover only', async ()
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(
container.querySelector('.platform-recommend-cover-only'),
).toBeTruthy();
expect(openLoginModal).not.toHaveBeenCalled();
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
expect(
container.querySelector('.platform-mobile-entry-shell--recommend'),
).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(screen.getByLabelText('奇幻拼图 作品信息')).toBeTruthy();
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
});
test('logged out recommend cover opens login modal again', async () => {
test('logged out recommend runtime keeps detail callback idle', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
const { openLoginModal } = renderStatefulLoggedOutHomeView({
@@ -2486,12 +2849,9 @@ test('logged out recommend cover opens login modal again', async () => {
await user.click(
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
);
await user.click(
screen.getByRole('button', { name: / /u }),
);
expect(openLoginModal).toHaveBeenCalledTimes(2);
expect(openLoginModal).toHaveBeenLastCalledWith();
expect(openLoginModal).not.toHaveBeenCalled();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
@@ -2500,16 +2860,15 @@ test('logged out desktop recommend page renders runtime directly', () => {
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.queryByText('今日游戏')).toBeNull();
expect(screen.queryByText('作品分类')).toBeNull();
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(screen.getByText('今日游戏')).toBeTruthy();
expect(screen.getByText('作品分类')).toBeTruthy();
});
test('logged out recommend page can enter runtime without login gate', () => {
mockDesktopLayout();
const openLoginModal = vi.fn();
const onOpenGalleryDetail = vi.fn();
renderLoggedOutHomeView(openLoginModal, {
@@ -2525,6 +2884,35 @@ test('logged out recommend page can enter runtime without login gate', () => {
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
});
test('logged out desktop recommend rail enters runtime without login modal', async () => {
mockDesktopLayout();
const user = userEvent.setup();
const openLoginModal = vi.fn();
const { container } = renderLoggedOutHomeView(
openLoginModal,
{
latestEntries: [puzzlePublicEntry],
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
},
'category',
true,
);
const desktopRail = container.querySelector('.platform-desktop-rail');
if (!desktopRail) {
throw new Error('缺少桌面侧边栏');
}
await user.click(
within(desktopRail as HTMLElement).getByRole('button', { name: '推荐' }),
);
expect(openLoginModal).not.toHaveBeenCalled();
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
expect(container.querySelector('.platform-desktop-shell')).toBeTruthy();
});
test('logged in recommend page uses gated recommend detail callback', async () => {
const user = userEvent.setup();
const onOpenGalleryDetail = vi.fn();
@@ -2827,7 +3215,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
await waitFor(() => {
expect(
document
.querySelector('.platform-recommend-cover-only__author img')
.querySelector('.platform-recommend-work-meta__avatar img')
?.getAttribute('src'),
).toBe('data:image/png;base64,AUTHOR');
});
@@ -3135,7 +3523,6 @@ test('desktop logged in home syncs mobile home modules without square or latest
expect(screen.queryByText('作品广场')).toBeNull();
expect(screen.queryByText('公开作品')).toBeNull();
expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
expect(screen.queryByText('1777110165.990127Z')).toBeNull();
});

File diff suppressed because it is too large Load Diff

View File

@@ -351,7 +351,7 @@ export function useRpgEntryBootstrap(
!hasInitialAgentSession &&
!hasExplicitPlatformTabSelectionRef.current
) {
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
setPlatformTabState(isAuthenticated ? 'home' : 'category');
}
} finally {