1
This commit is contained in:
@@ -210,6 +210,12 @@ async function openExistingRpgDraft(
|
||||
await user.click(await screen.findByRole('button', { name: actionName }));
|
||||
}
|
||||
|
||||
const ISOLATED_RUNTIME_AUTH_OPTIONS = {
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
};
|
||||
|
||||
function getPlatformTabPanel(tab: string) {
|
||||
const panel = document.getElementById(`platform-tab-panel-${tab}`);
|
||||
if (!panel) {
|
||||
@@ -3054,11 +3060,7 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
{
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3673,10 +3675,13 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('puzzle-board')).toBeTruthy();
|
||||
});
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith({
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
});
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
|
||||
await user.click(document.querySelector('[data-piece-id="piece-0"]')!);
|
||||
await user.click(document.querySelector('[data-piece-id="piece-1"]')!);
|
||||
@@ -3695,6 +3700,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
elapsedMs: 18_000,
|
||||
nickname: '测试玩家',
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3711,6 +3717,8 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
await waitFor(() => {
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedFirstLevel.runId,
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -3875,6 +3883,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedThirdLevel.runId,
|
||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
|
||||
@@ -310,16 +310,6 @@ const originalMatchMedia = window.matchMedia;
|
||||
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
||||
const originalCancelAnimationFrame = window.cancelAnimationFrame;
|
||||
|
||||
function dispatchClientYPointerEvent(
|
||||
target: HTMLElement,
|
||||
type: string,
|
||||
clientY: number,
|
||||
) {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true });
|
||||
Object.assign(event, { clientY });
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
const puzzlePublicEntry = {
|
||||
sourceType: 'puzzle',
|
||||
workId: 'puzzle-work-public-1',
|
||||
@@ -534,6 +524,7 @@ function renderLoggedOutHomeView(
|
||||
| 'onSelectPreviousRecommendEntry'
|
||||
>
|
||||
> = {},
|
||||
activeTab: RpgEntryHomeViewProps['activeTab'] = 'home',
|
||||
) {
|
||||
return render(
|
||||
<AuthUiContext.Provider
|
||||
@@ -556,7 +547,7 @@ function renderLoggedOutHomeView(
|
||||
}}
|
||||
>
|
||||
<RpgEntryHomeView
|
||||
activeTab="home"
|
||||
activeTab={activeTab}
|
||||
onTabChange={vi.fn()}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
@@ -602,19 +593,27 @@ function renderStatefulLoggedOutHomeView(
|
||||
| 'latestEntries'
|
||||
| 'onOpenGalleryDetail'
|
||||
| 'onSearchPublicCode'
|
||||
| 'recommendRuntimeContent'
|
||||
| 'activeRecommendEntryKey'
|
||||
| 'onSelectNextRecommendEntry'
|
||||
| 'onSelectPreviousRecommendEntry'
|
||||
>
|
||||
> = {},
|
||||
) {
|
||||
const authSpies = {
|
||||
openLoginModal: vi.fn(),
|
||||
};
|
||||
|
||||
function StatefulLoggedOutHomeView() {
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<RpgEntryHomeViewProps['activeTab']>('home');
|
||||
useState<RpgEntryHomeViewProps['activeTab']>('category');
|
||||
|
||||
return (
|
||||
<AuthUiContext.Provider
|
||||
value={{
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: vi.fn(),
|
||||
openLoginModal: authSpies.openLoginModal,
|
||||
requireAuth: vi.fn(),
|
||||
openSettingsModal: vi.fn(),
|
||||
openAccountModal: vi.fn(),
|
||||
@@ -651,7 +650,14 @@ function renderStatefulLoggedOutHomeView(
|
||||
onOpenCreateWorld={vi.fn()}
|
||||
onOpenCreateTypePicker={vi.fn()}
|
||||
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
|
||||
recommendRuntimeContent={<div data-testid="recommend-runtime" />}
|
||||
recommendRuntimeContent={
|
||||
overrides.recommendRuntimeContent ?? (
|
||||
<div data-testid="recommend-runtime" />
|
||||
)
|
||||
}
|
||||
activeRecommendEntryKey={overrides.activeRecommendEntryKey}
|
||||
onSelectNextRecommendEntry={overrides.onSelectNextRecommendEntry}
|
||||
onSelectPreviousRecommendEntry={overrides.onSelectPreviousRecommendEntry}
|
||||
onOpenLibraryDetail={vi.fn()}
|
||||
onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
|
||||
/>
|
||||
@@ -659,7 +665,10 @@ function renderStatefulLoggedOutHomeView(
|
||||
);
|
||||
}
|
||||
|
||||
return render(<StatefulLoggedOutHomeView />);
|
||||
return {
|
||||
...render(<StatefulLoggedOutHomeView />),
|
||||
openLoginModal: authSpies.openLoginModal,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1111,35 +1120,80 @@ test('public gallery cards hide work code until detail is opened', async () => {
|
||||
expect(onOpenGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry);
|
||||
});
|
||||
|
||||
test('mobile recommend page renders runtime viewport without bottom work cards', () => {
|
||||
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-category');
|
||||
expect(screen.getByPlaceholderText('搜索作品号、名称、作者、描述')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged out recommend tab opens login modal and shows cover only', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
});
|
||||
const bottomNav = container.querySelector('.platform-bottom-nav');
|
||||
if (!bottomNav) {
|
||||
throw new Error('缺少底部导航');
|
||||
}
|
||||
|
||||
await user.click(
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector('.platform-recommend-cover-only')).toBeTruthy();
|
||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
||||
expect(screen.queryByLabelText('奇幻拼图 作品信息')).toBeNull();
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('logged out recommend cover opens login modal again', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
onOpenGalleryDetail,
|
||||
});
|
||||
const bottomNav = document.querySelector('.platform-bottom-nav');
|
||||
if (!bottomNav) {
|
||||
throw new Error('缺少底部导航');
|
||||
}
|
||||
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
const runtimePanel = document.querySelector('.platform-recommend-runtime-panel');
|
||||
expect(runtimePanel).toBeTruthy();
|
||||
expect(runtimePanel?.className).not.toContain('bg-black');
|
||||
expect(screen.queryByText('一张用于公开分享的拼图作品。')).toBeNull();
|
||||
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.any(Function));
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logged out mobile recommend page renders cover instead of runtime', () => {
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
renderLoggedOutHomeView(
|
||||
vi.fn(),
|
||||
{
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
onOpenGalleryDetail,
|
||||
},
|
||||
'home',
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
||||
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.platform-public-work-card__cover'),
|
||||
).toBeNull();
|
||||
expect(screen.getByText('拼图玩家')).toBeTruthy();
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('20').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('12').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole('button', { name: '切换到 奇幻拼图' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '查看 奇幻拼图 详情' }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '打开 奇幻拼图 详情' }),
|
||||
).toBeNull();
|
||||
expect(document.querySelector('.platform-recommend-switcher')).toBeNull();
|
||||
fireEvent.click(screen.getByLabelText('奇幻拼图 作品信息'));
|
||||
fireEvent.click(screen.getByRole('button', { name: /登录后游玩 奇幻拼图/u }));
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1151,38 +1205,15 @@ test('mobile recommend loading state is themed instead of hardcoded black', () =
|
||||
recommendRuntimeContent: null,
|
||||
});
|
||||
|
||||
const loadingState = screen.getByText('加载中...');
|
||||
expect(loadingState.className).toContain('platform-recommend-runtime-state');
|
||||
expect(loadingState.className).not.toContain('bg-black');
|
||||
expect(document.querySelector('.platform-recommend-cover-only')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('mobile recommend meta swipes between public works', () => {
|
||||
const onSelectNextRecommendEntry = vi.fn();
|
||||
const onSelectPreviousRecommendEntry = vi.fn();
|
||||
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
onSelectNextRecommendEntry,
|
||||
onSelectPreviousRecommendEntry,
|
||||
});
|
||||
|
||||
const meta = screen.getByLabelText('奇幻拼图 作品信息');
|
||||
dispatchClientYPointerEvent(meta, 'pointerdown', 240);
|
||||
dispatchClientYPointerEvent(meta, 'pointerup', 180);
|
||||
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
|
||||
expect(onSelectPreviousRecommendEntry).not.toHaveBeenCalled();
|
||||
|
||||
dispatchClientYPointerEvent(meta, 'pointerdown', 180);
|
||||
dispatchClientYPointerEvent(meta, 'pointerup', 240);
|
||||
expect(onSelectPreviousRecommendEntry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('active recommend bottom tab selects next work instead of navigating', async () => {
|
||||
test('logged out active recommend bottom tab selects next work without login', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelectNextRecommendEntry = vi.fn();
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
renderLoggedOutHomeView(openLoginModal, {
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
onSelectNextRecommendEntry,
|
||||
@@ -1191,6 +1222,7 @@ test('active recommend bottom tab selects next work instead of navigating', asyn
|
||||
await user.click(screen.getByRole('button', { name: '下一个' }));
|
||||
|
||||
expect(onSelectNextRecommendEntry).toHaveBeenCalledTimes(1);
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('mobile recommend meta loads real author avatar from public user summary', async () => {
|
||||
@@ -1210,7 +1242,7 @@ test('mobile recommend meta loads real author avatar from public user summary',
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.platform-recommend-work-meta__avatar img')
|
||||
.querySelector('.platform-recommend-cover-only__author img')
|
||||
?.getAttribute('src'),
|
||||
).toBe('data:image/png;base64,AUTHOR');
|
||||
});
|
||||
|
||||
@@ -561,6 +561,66 @@ function WorldCard({
|
||||
);
|
||||
}
|
||||
|
||||
function RecommendCoverOnlyCard({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
onClick,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const typeLabel = describePublicGalleryCardKind(entry);
|
||||
const authorName = entry.authorDisplayName.trim() || '玩家';
|
||||
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={`登录后游玩 ${entry.worldName}`}
|
||||
className="platform-recommend-cover-only"
|
||||
>
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_16%,rgba(255,255,255,0.28),transparent_30%),linear-gradient(135deg,rgba(255,118,117,0.42),rgba(89,164,255,0.34))]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(0,0,0,0.04),rgba(0,0,0,0.42))]" />
|
||||
<div className="platform-recommend-cover-only__body">
|
||||
<span className="platform-public-work-card__kind">{typeLabel}</span>
|
||||
<span className="platform-recommend-cover-only__title">
|
||||
{displayName}
|
||||
</span>
|
||||
<span className="platform-recommend-cover-only__author">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="platform-public-work-card__author-avatar"
|
||||
>
|
||||
{normalizedAuthorAvatarUrl ? (
|
||||
<img
|
||||
src={normalizedAuthorAvatarUrl}
|
||||
alt=""
|
||||
className="platform-public-work-card__author-avatar-image"
|
||||
/>
|
||||
) : (
|
||||
authorAvatarLabel
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate">{authorName}</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CreationLibraryCard({
|
||||
entry,
|
||||
onClick,
|
||||
@@ -3049,9 +3109,9 @@ export function RpgEntryHomeView({
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleTabs.includes(activeTab)) {
|
||||
onTabChange('home');
|
||||
onTabChange(isAuthenticated ? 'home' : 'category');
|
||||
}
|
||||
}, [activeTab, onTabChange, visibleTabs]);
|
||||
}, [activeTab, isAuthenticated, onTabChange, visibleTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisitedTabs((currentTabs) => {
|
||||
@@ -3705,6 +3765,18 @@ export function RpgEntryHomeView({
|
||||
) ??
|
||||
recommendedFeedEntries[0] ??
|
||||
null;
|
||||
const openActiveRecommendEntry = useCallback(() => {
|
||||
if (!activeRecommendEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
authUi?.openLoginModal(() => onOpenGalleryDetail(activeRecommendEntry));
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenGalleryDetail(activeRecommendEntry);
|
||||
}, [activeRecommendEntry, authUi, isAuthenticated, onOpenGalleryDetail]);
|
||||
const selectNextRecommendEntry = useCallback(() => {
|
||||
onSelectNextRecommendEntry?.();
|
||||
}, [onSelectNextRecommendEntry]);
|
||||
@@ -3787,6 +3859,12 @@ export function RpgEntryHomeView({
|
||||
<div className="platform-recommend-runtime-state">
|
||||
正在读取公开作品...
|
||||
</div>
|
||||
) : !isAuthenticated && activeRecommendEntry ? (
|
||||
<RecommendCoverOnlyCard
|
||||
entry={activeRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
|
||||
onClick={openActiveRecommendEntry}
|
||||
/>
|
||||
) : recommendRuntimeError ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3808,7 +3886,7 @@ export function RpgEntryHomeView({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{activeRecommendEntry ? (
|
||||
{activeRecommendEntry && isAuthenticated ? (
|
||||
<RecommendRuntimeMeta
|
||||
entry={activeRecommendEntry}
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
|
||||
@@ -4685,6 +4763,12 @@ export function RpgEntryHomeView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
@@ -4818,7 +4902,15 @@ export function RpgEntryHomeView({
|
||||
label={tabLabels[tab]}
|
||||
icon={tabIcons[tab]}
|
||||
emphasized={tab === 'create'}
|
||||
onClick={() => onTabChange(tab)}
|
||||
onClick={() => {
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
@@ -66,7 +66,8 @@ export function useRpgEntryBootstrap(
|
||||
PlatformBrowseHistoryEntry[]
|
||||
>([]);
|
||||
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
|
||||
const [platformTab, setPlatformTabState] = useState<PlatformHomeTab>('home');
|
||||
const [platformTab, setPlatformTabState] =
|
||||
useState<PlatformHomeTab>('category');
|
||||
const [platformError, setPlatformError] = useState<string | null>(null);
|
||||
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
@@ -329,8 +330,8 @@ export function useRpgEntryBootstrap(
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
// 中文注释:saves 现在承载草稿列表,存档入口已并入“我的-玩过”,默认仍回到推荐页。
|
||||
setPlatformTabState('home');
|
||||
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口。
|
||||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
|
||||
Reference in New Issue
Block a user