259 lines
7.4 KiB
TypeScript
259 lines
7.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { requestJsonMock } = vi.hoisted(() => ({
|
|
requestJsonMock: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
deleteRpgEntryWorldProfile,
|
|
getRpgEntryWorldGalleryDetail,
|
|
getRpgEntryWorldLibraryDetail,
|
|
listRpgEntryWorldGallery,
|
|
listRpgEntryWorldLibrary,
|
|
publishRpgEntryWorldProfile,
|
|
unpublishRpgEntryWorldProfile,
|
|
upsertRpgEntryWorldProfile,
|
|
} from './rpgEntryLibraryClient';
|
|
|
|
vi.mock('../apiClient', () => ({
|
|
BACKGROUND_AUTH_REQUEST_OPTIONS: {
|
|
authImpact: 'local',
|
|
skipRefresh: true,
|
|
notifyAuthStateChange: false,
|
|
clearAuthOnUnauthorized: false,
|
|
},
|
|
requestJson: requestJsonMock,
|
|
}));
|
|
|
|
describe('rpgEntryLibraryClient world library routes', () => {
|
|
beforeEach(() => {
|
|
requestJsonMock.mockReset();
|
|
requestJsonMock.mockResolvedValue({ entries: [] });
|
|
});
|
|
|
|
it('reads world library from the runtime entry route', async () => {
|
|
await listRpgEntryWorldLibrary();
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library',
|
|
expect.objectContaining({ method: 'GET' }),
|
|
'读取自定义世界库失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({ maxRetries: 1 }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('reads world gallery from the public runtime entry route', async () => {
|
|
await listRpgEntryWorldGallery();
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-gallery',
|
|
expect.objectContaining({ method: 'GET' }),
|
|
'读取作品广场失败',
|
|
expect.objectContaining({
|
|
skipAuth: true,
|
|
skipRefresh: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('reads gallery detail from the public runtime entry route', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
ownerUserId: 'owner-1',
|
|
profileId: 'profile-1',
|
|
},
|
|
});
|
|
|
|
await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-gallery/owner-1/profile-1',
|
|
expect.objectContaining({ method: 'GET' }),
|
|
'读取作品详情失败',
|
|
expect.objectContaining({
|
|
skipAuth: true,
|
|
skipRefresh: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('normalizes detail profiles before runtime launch consumes them', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
ownerUserId: 'owner-1',
|
|
profileId: 'profile-1',
|
|
publicWorkCode: 'CW-1',
|
|
authorPublicUserCode: 'U-1',
|
|
profile: {
|
|
id: 'profile-1',
|
|
name: '旧数据世界',
|
|
summary: '只有摘要字段的旧 profile。',
|
|
},
|
|
visibility: 'published',
|
|
publishedAt: '2026-05-21T00:00:00.000Z',
|
|
updatedAt: '2026-05-21T00:00:00.000Z',
|
|
authorDisplayName: '作者',
|
|
worldName: '旧数据世界',
|
|
subtitle: '旧数据',
|
|
summaryText: '只有摘要字段的旧 profile。',
|
|
coverImageSrc: null,
|
|
themeMode: 'martial',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
},
|
|
});
|
|
|
|
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
|
|
|
|
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
|
|
expect(Array.isArray(entry.profile.storyNpcs)).toBe(true);
|
|
expect(Array.isArray(entry.profile.landmarks)).toBe(true);
|
|
expect(entry.profile.attributeSchema.schemaVersion).toBe(1);
|
|
});
|
|
|
|
it('falls back to entry summary when old detail profile cannot be normalized', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
ownerUserId: 'owner-1',
|
|
profileId: 'profile-1',
|
|
publicWorkCode: 'CW-1',
|
|
authorPublicUserCode: 'U-1',
|
|
profile: {
|
|
id: 'profile-1',
|
|
summary: '缺少 name 的旧 profile。',
|
|
},
|
|
visibility: 'published',
|
|
publishedAt: '2026-05-21T00:00:00.000Z',
|
|
updatedAt: '2026-05-21T00:00:00.000Z',
|
|
authorDisplayName: '作者',
|
|
worldName: '摘要兜底世界',
|
|
subtitle: '旧数据',
|
|
summaryText: '缺少 name 的旧 profile。',
|
|
coverImageSrc: null,
|
|
themeMode: 'martial',
|
|
playableNpcCount: 0,
|
|
landmarkCount: 0,
|
|
},
|
|
});
|
|
|
|
const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1');
|
|
|
|
expect(entry.profile.id).toBe('profile-1');
|
|
expect(entry.profile.name).toBe('摘要兜底世界');
|
|
expect(Array.isArray(entry.profile.playableNpcs)).toBe(true);
|
|
});
|
|
|
|
it('reads owned library detail from the runtime entry route', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
ownerUserId: 'owner-1',
|
|
profileId: 'profile-1',
|
|
},
|
|
});
|
|
|
|
await getRpgEntryWorldLibraryDetail('profile-1');
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library/profile-1',
|
|
expect.objectContaining({ method: 'GET' }),
|
|
'读取作品详情失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({ maxRetries: 1 }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('writes world profile through the runtime entry route', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
profileId: 'profile-1',
|
|
},
|
|
entries: [],
|
|
});
|
|
|
|
await upsertRpgEntryWorldProfile({
|
|
id: 'profile-1',
|
|
name: '测试世界',
|
|
} as never);
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library/profile-1',
|
|
expect.objectContaining({
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
'保存自定义世界失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({
|
|
maxRetries: 1,
|
|
retryUnsafeMethods: true,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('deletes world profile through the runtime entry route', async () => {
|
|
await deleteRpgEntryWorldProfile('profile-1');
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library/profile-1',
|
|
expect.objectContaining({ method: 'DELETE' }),
|
|
'删除自定义世界失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({
|
|
maxRetries: 1,
|
|
retryUnsafeMethods: true,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('publishes world profile through the runtime entry route', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
profileId: 'profile-1',
|
|
},
|
|
entries: [],
|
|
});
|
|
|
|
await publishRpgEntryWorldProfile('profile-1');
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library/profile-1/publish',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
'发布自定义世界失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({
|
|
maxRetries: 1,
|
|
retryUnsafeMethods: true,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('unpublishes world profile through the runtime entry route', async () => {
|
|
requestJsonMock.mockResolvedValueOnce({
|
|
entry: {
|
|
profileId: 'profile-1',
|
|
},
|
|
entries: [],
|
|
});
|
|
|
|
await unpublishRpgEntryWorldProfile('profile-1');
|
|
|
|
expect(requestJsonMock).toHaveBeenCalledWith(
|
|
'/api/runtime/custom-world-library/profile-1/unpublish',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
'下架自定义世界失败',
|
|
expect.objectContaining({
|
|
retry: expect.objectContaining({
|
|
maxRetries: 1,
|
|
retryUnsafeMethods: true,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|