Merge remote-tracking branch 'origin/master' into feat/game-fork
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m31s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m3s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Backend tests (pull_request) Successful in 7m36s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m26s
Project CI / Repository checks (pull_request) Successful in 6m25s
Project CI / Native shell tests (pull_request) Successful in 9m34s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m31s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m20s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m3s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Backend tests (pull_request) Successful in 7m36s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m26s
Project CI / Repository checks (pull_request) Successful in 6m25s
Project CI / Native shell tests (pull_request) Successful in 9m34s
# Conflicts: # deploy/container/nginx.conf # deploy/nginx/genarrative-dev-http.conf # deploy/nginx/genarrative.conf # server-rs/crates/api-server/src/modules/game_distribution.rs # src/components/game-distribution/GameDetailPage.tsx # src/components/game-distribution/GameDistributionPages.test.tsx # src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
This commit is contained in:
@@ -62,3 +62,12 @@ describe('activeAppTitle', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('创作者主页和关系列表使用各自标题', () => {
|
||||
expect(resolveAppTitleForSelectionStage('creator-home')).toBe(
|
||||
'创作者主页 - 陶泥儿',
|
||||
);
|
||||
expect(resolveAppTitleForSelectionStage('creator-connections')).toBe(
|
||||
'关注与粉丝 - 陶泥儿',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ const APP_TITLE_BY_SELECTION_STAGE: Record<SelectionStage, string> = {
|
||||
platform: APP_HOME_TITLE,
|
||||
'creation-home': APP_HOME_TITLE,
|
||||
project: '项目 - 陶泥儿',
|
||||
'creator-home': '创作者主页 - 陶泥儿',
|
||||
'creator-connections': '关注与粉丝 - 陶泥儿',
|
||||
profile: '我的 - 陶泥儿',
|
||||
'profile-payment': '支付服务 - 陶泥儿',
|
||||
'payment-checkout': '扫码支付 - 陶泥儿',
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
changeCreatorRelationship,
|
||||
getCreator,
|
||||
getCreatorConnections,
|
||||
} from './creatorProfileClient';
|
||||
const request = vi.hoisted(() => vi.fn());
|
||||
vi.mock('./apiClient', () => ({ requestJson: request }));
|
||||
beforeEach(() => request.mockReset());
|
||||
it('公开主页请求独立于登录态', async () => {
|
||||
await getCreator('a/b');
|
||||
expect(request.mock.calls[0]?.[0]).toBe('/api/creators/a%2Fb');
|
||||
expect(request.mock.calls[0]?.[3]).toEqual({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
});
|
||||
});
|
||||
it('列表游标编码,游客不附带凭证', async () => {
|
||||
await getCreatorConnections('a', 'followers', 'x+y', false);
|
||||
expect(request.mock.calls[0]?.[0]).toBe(
|
||||
'/api/creators/a/followers?limit=20&cursor=x%2By',
|
||||
);
|
||||
expect(request.mock.calls[0]?.[3]).toEqual({
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
});
|
||||
});
|
||||
it.each([
|
||||
['follow', 'PUT', '/api/creators/b/follow'],
|
||||
['unfollow', 'DELETE', '/api/creators/b/follow'],
|
||||
['remove-follower', 'DELETE', '/api/creators/me/followers/b'],
|
||||
] as const)('%s 不自动重放写请求', async (action, method, path) => {
|
||||
await changeCreatorRelationship('b', action);
|
||||
expect(request.mock.calls[0]?.[0]).toBe(path);
|
||||
expect(request.mock.calls[0]?.[1]).toEqual({ method });
|
||||
expect(request.mock.calls[0]?.[3]).toEqual({
|
||||
skipRefresh: true,
|
||||
retry: { maxRetries: 0 },
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type {
|
||||
CreatorConnections,
|
||||
CreatorProfile,
|
||||
CreatorRelationshipResponse,
|
||||
} from '../../packages/shared/src/contracts/creator';
|
||||
import { requestJson as requestApiJson } from './apiClient';
|
||||
|
||||
export type CreatorConnectionKind = 'following' | 'followers';
|
||||
export type CreatorAction = 'follow' | 'unfollow' | 'remove-follower';
|
||||
export const creatorPath = (id: string) =>
|
||||
`/creators?id=${encodeURIComponent(id)}`;
|
||||
export const connectionsPath = (id: string, tab: CreatorConnectionKind) =>
|
||||
`/creators/connections?id=${encodeURIComponent(id)}&tab=${tab}`;
|
||||
const userPath = (id: string) => `/api/creators/${encodeURIComponent(id)}`;
|
||||
|
||||
export function getCreator(id: string) {
|
||||
return requestApiJson<CreatorProfile>(
|
||||
userPath(id),
|
||||
{ method: 'GET' },
|
||||
'读取创作者资料失败',
|
||||
{ skipAuth: true, skipRefresh: true },
|
||||
);
|
||||
}
|
||||
export function getCreatorRelationship(id: string) {
|
||||
return requestApiJson<CreatorRelationshipResponse>(
|
||||
`${userPath(id)}/relationship`,
|
||||
{ method: 'GET' },
|
||||
'读取关注状态失败',
|
||||
);
|
||||
}
|
||||
export function getCreatorConnections(
|
||||
id: string,
|
||||
kind: CreatorConnectionKind,
|
||||
cursor: string | null,
|
||||
authenticated: boolean,
|
||||
) {
|
||||
const query = new URLSearchParams({ limit: '20' });
|
||||
if (cursor) query.set('cursor', cursor);
|
||||
return requestApiJson<CreatorConnections>(
|
||||
`${userPath(id)}/${kind}?${query}`,
|
||||
{ method: 'GET' },
|
||||
'读取用户列表失败',
|
||||
authenticated ? undefined : { skipAuth: true, skipRefresh: true },
|
||||
);
|
||||
}
|
||||
export function changeCreatorRelationship(id: string, action: CreatorAction) {
|
||||
const path =
|
||||
action === 'remove-follower'
|
||||
? `/api/creators/me/followers/${encodeURIComponent(id)}`
|
||||
: `${userPath(id)}/follow`;
|
||||
return requestApiJson<CreatorRelationshipResponse>(
|
||||
path,
|
||||
{ method: action === 'follow' ? 'PUT' : 'DELETE' },
|
||||
'关注操作失败',
|
||||
{
|
||||
// 写入不自动重放,包括鉴权刷新后的重放;失败由业务层回读确认。
|
||||
skipRefresh: true,
|
||||
retry: { maxRetries: 0 },
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,14 @@ import {
|
||||
|
||||
describe('gameDistributionClient', () => {
|
||||
beforeEach(() => {
|
||||
// Node 新版提供的原生 localStorage 不等于浏览器存储;测试使用独立内存存储。
|
||||
const values = new Map<string, string>();
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => values.set(key, value),
|
||||
removeItem: (key: string) => values.delete(key),
|
||||
clear: () => values.clear(),
|
||||
});
|
||||
clearStoredAccessToken({ emit: false });
|
||||
});
|
||||
|
||||
@@ -83,6 +91,25 @@ describe('gameDistributionClient', () => {
|
||||
expect(games[0]?.id).toBe('game-1');
|
||||
});
|
||||
|
||||
it('作者过滤由查询参数交给后端,显式空值不退回全站游戏', async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ games: [] }), { status: 200 }),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
await listGames({ authorId: 'user/a' });
|
||||
expect(fetchMock.mock.calls[0][0]).toBe(
|
||||
'/api/game-distribution/games?authorId=user%2Fa',
|
||||
);
|
||||
await listGames({ authorId: '' });
|
||||
expect(fetchMock.mock.calls[1][0]).toBe(
|
||||
'/api/game-distribution/games?authorId=',
|
||||
);
|
||||
});
|
||||
|
||||
it('读取详情时拒绝空 id', async () => {
|
||||
expect(await getGame('')).toBeNull();
|
||||
});
|
||||
@@ -219,6 +246,14 @@ describe('gameDistributionClient', () => {
|
||||
|
||||
describe('gameDistributionClient 作者接口', () => {
|
||||
beforeEach(() => {
|
||||
// Node 新版提供的原生 localStorage 不等于浏览器存储;测试使用独立内存存储。
|
||||
const values = new Map<string, string>();
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => values.set(key, value),
|
||||
removeItem: (key: string) => values.delete(key),
|
||||
clear: () => values.clear(),
|
||||
});
|
||||
clearStoredAccessToken({ emit: false });
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export type GameAuthor = GameDistributionAuthor;
|
||||
export type GameVersionSummary = GameDistributionVersionSummary;
|
||||
|
||||
export type GameListQuery = {
|
||||
authorId?: string;
|
||||
keyword?: string;
|
||||
category?: GameCategory | '全部';
|
||||
};
|
||||
@@ -114,11 +115,13 @@ async function requestJson<T>(
|
||||
|
||||
export async function listGames(query: GameListQuery = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (query.authorId !== undefined) params.set('authorId', query.authorId);
|
||||
if (query.keyword?.trim()) params.set('keyword', query.keyword.trim());
|
||||
if (query.category && query.category !== '全部')
|
||||
params.set('category', query.category);
|
||||
const search = params.toString();
|
||||
const payload = await requestJson<unknown>(
|
||||
`/games${params.size ? `?${params.toString()}` : ''}`,
|
||||
`/games${search ? `?${search}` : ''}`,
|
||||
{ method: 'GET' },
|
||||
'读取游戏目录失败',
|
||||
PUBLIC_GAME_REQUEST_OPTIONS,
|
||||
|
||||
Reference in New Issue
Block a user