合并架构调整分支
合入 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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user