Browse Source
- 以 job.youthome.com.cn OpenAPI 为唯一准绳,实机验证 89 个 request 函数
- ALIGNED 6 / WRONG→REAL 14(/messages/conversations→/conversations、/resume→/profile/me/resume、/jobs/rank→/jobs/{id}/competition-ranking、/enterprise/candidates→/company/candidates 等)
- PHANTOM 69:所属域后端无契约,真实态不再调用 request,翻开关也不触网
- 清除全部【待确认】标记(含【待确认:…】形式)
- 修复 fetchResume 双前缀、fetchRankData ${jobId} 未插值两处转换 bug
- 4 个桥接测试 + job-api/password 测试改为断言真实路径
master
7 changed files with 1437 additions and 268 deletions
@ -0,0 +1,249 @@ |
|||
/** |
|||
* 后端桥接(剩余域)consolidated 测试 |
|||
* 覆盖第三轮之后批量转换的 62 个 fetch 函数: |
|||
* - 真实态(setMockMode(false)):仅对已与后端 OpenAPI 对齐的函数断言 request 命中真实路径 |
|||
* (见 REAL_FUNCS;其余为幽灵域,已退回 mock-only,翻真实开关也不触网,不在此断言) |
|||
* - mock 态(默认):断言返回非 undefined(防止 mock 分支被意外破坏) |
|||
* 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。 |
|||
*/ |
|||
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
|
|||
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } })); |
|||
|
|||
// token-store:内存双 Token,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/token-store', () => { |
|||
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null; |
|||
return { |
|||
getAccessToken: jest.fn(() => mem?.accessToken ?? null), |
|||
getTokens: jest.fn(() => mem), |
|||
setTokens: jest.fn((a: string, r: string, e?: number) => { |
|||
mem = { accessToken: a, refreshToken: r, expiresAt: e }; |
|||
}), |
|||
clearTokens: jest.fn(() => { |
|||
mem = null; |
|||
}), |
|||
hydrateTokens: jest.fn(async () => undefined), |
|||
}; |
|||
}); |
|||
|
|||
// storage:内存实现,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/storage', () => { |
|||
const m = new Map<string, string>(); |
|||
return { |
|||
storageGet: jest.fn(async (k: string) => m.get(k) ?? null), |
|||
storageSet: jest.fn(async (k: string, v: string) => { |
|||
m.set(k, v); |
|||
}), |
|||
storageRemove: jest.fn(async (k: string) => { |
|||
m.delete(k); |
|||
}), |
|||
}; |
|||
}); |
|||
|
|||
import { |
|||
fetchArticles, |
|||
fetchMenuItems, |
|||
fetchEnterpriseEntry, |
|||
fetchRankData, |
|||
fetchSkillCloud, |
|||
fetchSkillLearningPath, |
|||
fetchSalaryToolLinks, |
|||
fetchSalaryBrackets, |
|||
fetchOperationRules, |
|||
fetchHollandIntro, |
|||
fetchIdentityOptions, |
|||
fetchGameLevels, |
|||
fetchLegalBrand, |
|||
fetchLegalServices, |
|||
fetchLegalFAQs, |
|||
fetchLegalTemplates, |
|||
fetchLegalPlans, |
|||
fetchAboutInfo, |
|||
fetchQualifications, |
|||
fetchAITools, |
|||
fetchHistory, |
|||
fetchHollandAssessment, |
|||
fetchHollandResult, |
|||
fetchAbilityAssessment, |
|||
fetchUserStats, |
|||
fetchOverallScore, |
|||
fetchDimensionScores, |
|||
fetchBadges, |
|||
fetchOfferCompareData, |
|||
fetchPendingOfferCount, |
|||
fetchWeightSettings, |
|||
fetchSalaryRecords, |
|||
fetchAIInterviewData, |
|||
fetchHREvaluation, |
|||
fetchAppealForm, |
|||
fetchEnterpriseOverview, |
|||
fetchJobPublishForm, |
|||
fetchJobEvaluationManagement, |
|||
fetchCandidateDetail, |
|||
fetchInterviewScore, |
|||
fetchInterviewScoreDraft, |
|||
fetchAuditReport, |
|||
fetchEnterpriseCertification, |
|||
fetchJobManagement, |
|||
fetchTalentRecommendation, |
|||
fetchEnterpriseCommunication, |
|||
fetchJobEvaluationEmptyState, |
|||
fetchGameProgress, |
|||
fetchLegalMembership, |
|||
fetchLegalOrders, |
|||
fetchLegalServiceRequests, |
|||
fetchLegalConversation, |
|||
fetchPrivacySettings, |
|||
fetchPrivacyCenter, |
|||
fetchPrivacyDetailSetting, |
|||
fetchDndSetting, |
|||
fetchKeywordBlock, |
|||
fetchAccountCancel, |
|||
fetchLegalDocument, |
|||
fetchPersonalDataExport, |
|||
fetchNotificationSettings, |
|||
fetchAccountSecurity, |
|||
} from '../api/api'; |
|||
import { setMockMode } from '../api/http'; |
|||
|
|||
// [fn, args, expectedPath, mockSmoke]
|
|||
type Entry = [ |
|||
(...a: any[]) => Promise<any>, |
|||
any[], |
|||
string, |
|||
boolean, |
|||
]; |
|||
|
|||
const FUNCS: Entry[] = [ |
|||
// 公开读取(auth:false)
|
|||
[fetchArticles, [], '/growth/articles', true], |
|||
[fetchMenuItems, ['candidate'], '/user/menus', true], |
|||
[fetchEnterpriseEntry, [], '/user/enterprise-entry', true], |
|||
[fetchRankData, ['1'], '/jobs/rank', false], |
|||
[fetchSkillCloud, ['it'], '/tools/skill-cloud', false], |
|||
[fetchSkillLearningPath, ['it'], '/tools/skill-learning-path', false], |
|||
[fetchSalaryToolLinks, [], '/tools/salary-related', true], |
|||
[fetchSalaryBrackets, [], '/tools/salary-brackets', true], |
|||
[fetchOperationRules, [], '/operation/rules', true], |
|||
[fetchHollandIntro, [], '/growth/holland-intro', true], |
|||
[fetchIdentityOptions, [], '/tools/identity-options', true], |
|||
[fetchGameLevels, [], '/game/levels', true], |
|||
[fetchLegalBrand, [], '/legal/brand', true], |
|||
[fetchLegalServices, [], '/legal/services', true], |
|||
[fetchLegalFAQs, [], '/legal/faqs', true], |
|||
[fetchLegalTemplates, [], '/legal/templates', true], |
|||
[fetchLegalPlans, [], '/legal/plans', true], |
|||
[fetchAboutInfo, [], '/settings/about', true], |
|||
[fetchQualifications, [], '/settings/qualifications', true], |
|||
[fetchAITools, [], '/growth/ai-tools', true], |
|||
// 用户/企业私有(auth:true)
|
|||
[fetchHistory, [], '/user/history', true], |
|||
[fetchHollandAssessment, [], '/growth/holland', true], |
|||
[fetchHollandResult, [], '/growth/holland/result', true], |
|||
[fetchAbilityAssessment, [], '/growth/ability', true], |
|||
[fetchUserStats, [], '/user/stats', true], |
|||
[fetchOverallScore, [], '/user/overall-score', true], |
|||
[fetchDimensionScores, [], '/user/dimension-scores', true], |
|||
[fetchBadges, [], '/user/badges', true], |
|||
[fetchOfferCompareData, [undefined], '/offers/compare', true], |
|||
[fetchPendingOfferCount, [], '/offers/pending/count', true], |
|||
[fetchWeightSettings, [], '/jobs/weight-settings', true], |
|||
[fetchSalaryRecords, [], '/salary/records', true], |
|||
[fetchAIInterviewData, [], '/interview/config', true], |
|||
[fetchHREvaluation, [], '/evaluation/hr', true], |
|||
[fetchAppealForm, [], '/evaluation/appeal', true], |
|||
[fetchEnterpriseOverview, [], '/enterprise/overview', true], |
|||
[fetchJobPublishForm, [], '/enterprise/jobs/publish-form', true], |
|||
[fetchJobEvaluationManagement, [], '/enterprise/job-evaluations', true], |
|||
[fetchCandidateDetail, ['1'], '/enterprise/candidates/1', false], |
|||
[fetchInterviewScore, ['1'], '/enterprise/interviews/1/score', false], |
|||
[fetchInterviewScoreDraft, ['1'], '/enterprise/interviews/1/score/draft', false], |
|||
[fetchAuditReport, [], '/operation/audit-report', true], |
|||
[fetchEnterpriseCertification, [], '/enterprise/certification', true], |
|||
[fetchJobManagement, [], '/enterprise/job-management', true], |
|||
[fetchTalentRecommendation, [], '/enterprise/talent-recommendation', true], |
|||
[fetchEnterpriseCommunication, [], '/enterprise/communication', true], |
|||
[fetchJobEvaluationEmptyState, [], '/enterprise/job-evaluation-empty', true], |
|||
[fetchGameProgress, [], '/game/progress', true], |
|||
[fetchLegalMembership, [], '/legal/membership', true], |
|||
[fetchLegalOrders, [], '/legal/orders', true], |
|||
[fetchLegalServiceRequests, [], '/legal/service-requests', true], |
|||
[fetchLegalConversation, [], '/legal/conversations/default', true], |
|||
[fetchPrivacySettings, [], '/settings/privacy', true], |
|||
[fetchPrivacyCenter, [], '/settings/privacy-center', true], |
|||
[fetchPrivacyDetailSetting, ['1'], '/settings/privacy/1', false], |
|||
[fetchDndSetting, [], '/settings/privacy/dnd', true], |
|||
[fetchKeywordBlock, [], '/settings/privacy/keywords', true], |
|||
[fetchAccountCancel, [], '/settings/account-cancel', true], |
|||
[fetchLegalDocument, ['1'], '/settings/legal-document/1', false], |
|||
[fetchPersonalDataExport, [], '/settings/personal-data-export', true], |
|||
[fetchNotificationSettings, [], '/settings/notification', true], |
|||
[fetchAccountSecurity, [], '/settings/account-security', true], |
|||
]; |
|||
|
|||
const ENVELOPE = { |
|||
code: 0, |
|||
message: '', |
|||
data: null, |
|||
requestId: 'r', |
|||
timestamp: '', |
|||
}; |
|||
|
|||
// 已与后端 OpenAPI 对齐(ALIGNED + WRONG→REAL)的函数:真实态打真实路径
|
|||
// 幽灵域(PHANTOM,后端无契约)已退回 mock-only,翻真实开关也不触网,不在此断言路径
|
|||
const REAL_FUNCS: Array<[(...a: any[]) => Promise<any>, any[], string]> = [ |
|||
[fetchRankData, ['1'], '/jobs/1/competition-ranking'], |
|||
[fetchCandidateDetail, ['1'], '/company/candidates/1'], |
|||
[fetchInterviewScore, ['1'], '/interviews/1/score'], |
|||
[fetchInterviewScoreDraft, ['1'], '/interviews/1/score'], |
|||
[fetchOfferCompareData, [undefined], '/offers/compare'], |
|||
]; |
|||
|
|||
describe('后端桥接 - 真实态路径断言(已对齐函数)', () => { |
|||
const originalFetch = (global as any).fetch; |
|||
|
|||
beforeEach(() => { |
|||
(global as any).fetch = jest.fn(async () => ({ |
|||
ok: true, |
|||
status: 200, |
|||
headers: { get: () => null }, |
|||
json: async () => ENVELOPE, |
|||
text: async () => '', |
|||
})); |
|||
setMockMode(false); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
setMockMode(true); |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
(global as any).fetch = originalFetch; |
|||
}); |
|||
|
|||
it('已对齐函数数量正确(5 个)', () => { |
|||
expect(REAL_FUNCS.length).toBe(5); |
|||
}); |
|||
|
|||
REAL_FUNCS.forEach(([fn, args, path], i) => { |
|||
it(`#${i} 命中真实路径 ${path}`, async () => { |
|||
await fn(...args); |
|||
const calls = ((global as any).fetch as jest.Mock).mock.calls; |
|||
expect(calls.length).toBeGreaterThan(0); |
|||
const calledUrl: string = calls[calls.length - 1][0]; |
|||
expect(calledUrl).toContain(path); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('后端桥接 - mock 态冒烟(无回归)', () => { |
|||
beforeEach(() => { |
|||
setMockMode(true); |
|||
}); |
|||
|
|||
FUNCS.filter((e) => e[3]).forEach(([fn, args, path], idx) => { |
|||
it(`mock 态 ${path} 返回非 undefined`, async () => { |
|||
const result = await fn(...args); |
|||
expect(result).not.toBeUndefined(); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,141 @@ |
|||
/** |
|||
* 后端桥接(写入类)测试 |
|||
* 覆盖本轮转换的 12 个写函数(简历 8 + 求职偏好 + 权重 + 薪资 2): |
|||
* - 真实态(setMockMode(false)):断言 request 命中预期方法 + 相对路径 + body |
|||
* - mock 态(默认):断言返回本地派生/生成值(证明 mock 分支未被破坏,本地 storage 真写入仍可用) |
|||
* 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。 |
|||
* 幽灵域写函数已退回 mock-only,真实态不在此断言网络路径。 |
|||
*/ |
|||
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
|
|||
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } })); |
|||
|
|||
// token-store:内存双 Token,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/token-store', () => { |
|||
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null; |
|||
return { |
|||
getAccessToken: jest.fn(() => mem?.accessToken ?? null), |
|||
getTokens: jest.fn(() => mem), |
|||
setTokens: jest.fn((a: string, r: string, e?: number) => { |
|||
mem = { accessToken: a, refreshToken: r, expiresAt: e }; |
|||
}), |
|||
clearTokens: jest.fn(() => { |
|||
mem = null; |
|||
}), |
|||
hydrateTokens: jest.fn(async () => undefined), |
|||
}; |
|||
}); |
|||
|
|||
// storage:内存实现,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/storage', () => { |
|||
const m = new Map<string, string>(); |
|||
return { |
|||
storageGet: jest.fn(async (k: string) => m.get(k) ?? null), |
|||
storageSet: jest.fn(async (k: string, v: string) => { |
|||
m.set(k, v); |
|||
}), |
|||
storageRemove: jest.fn(async (k: string) => { |
|||
m.delete(k); |
|||
}), |
|||
}; |
|||
}); |
|||
|
|||
import { setMockMode } from '../api/http'; |
|||
import { |
|||
saveResumeBasicInfo, |
|||
saveResumeSelfEvaluation, |
|||
saveResumeSkills, |
|||
saveResumeEducation, |
|||
saveResumeExperience, |
|||
saveResumeProjects, |
|||
saveResumeSnapshot, |
|||
saveResumeExportSettings, |
|||
saveJobPreference, |
|||
saveWeightSettings, |
|||
saveSalaryRecord, |
|||
deleteSalaryRecord, |
|||
} from '../api/api'; |
|||
|
|||
const ENVELOPE = { code: 0, message: 'ok', data: null, requestId: 'r', timestamp: 0 }; |
|||
|
|||
let fetchMock: jest.Mock; |
|||
|
|||
beforeEach(() => { |
|||
fetchMock = jest.fn(async () => ({ |
|||
ok: true, |
|||
status: 200, |
|||
json: async () => ENVELOPE, |
|||
text: async () => '', |
|||
headers: { get: () => 'x' }, |
|||
})); |
|||
(global as any).fetch = fetchMock; |
|||
setMockMode(false); // 默认跑真实态
|
|||
}); |
|||
|
|||
/** 触发一次真实请求,返回最后一个 fetch 调用的 options */ |
|||
async function expectRequest( |
|||
call: () => Promise<unknown>, |
|||
path: string, |
|||
method: string, |
|||
): Promise<any> { |
|||
await call(); |
|||
const last = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]; |
|||
expect(last[0]).toContain(path); |
|||
expect((last[1] as any).method).toBe(method); |
|||
return last[1] as any; |
|||
} |
|||
|
|||
describe('写入函数真实态:方法 + 相对路径 + body(仅已对齐函数)', () => { |
|||
beforeEach(() => setMockMode(false)); |
|||
|
|||
test('saveJobPreference → PUT /profile/me/preferences', async () => { |
|||
const preference = { city: '北京', salary: '15k' } as any; |
|||
const opts = await expectRequest( |
|||
() => saveJobPreference(preference), |
|||
'/profile/me/preferences', |
|||
'PUT', |
|||
); |
|||
expect(JSON.parse(opts.body)).toEqual(preference); |
|||
}); |
|||
}); |
|||
|
|||
/** |
|||
* 以下写函数属幽灵域(后端无 OpenAPI 契约),已退回 mock-only,翻真实开关也不触网, |
|||
* 故真实态不再断言网络路径;其 mock 态本地派生/生成行为见下方 describe。 |
|||
*/ |
|||
|
|||
describe('写入函数 mock 态:本地派生/生成行为不变(开发期真写入仍可用)', () => { |
|||
beforeEach(() => setMockMode(true)); |
|||
|
|||
test('saveSalaryRecord 本地生成 id 与时间戳', async () => { |
|||
const rec = await saveSalaryRecord({} as any, {} as any, 'note'); |
|||
expect(rec.id.startsWith('salary-')).toBe(true); |
|||
expect(typeof rec.createdAt).toBe('number'); |
|||
expect(rec.note).toBe('note'); |
|||
}); |
|||
|
|||
test('saveResumeBasicInfo 本地派生 avatar', async () => { |
|||
const info = await saveResumeBasicInfo({ name: '张三' } as any); |
|||
expect(info.avatar).toBeTruthy(); |
|||
}); |
|||
|
|||
test('saveJobPreference 原样返回入参', async () => { |
|||
const p = { city: '北京' } as any; |
|||
expect(await saveJobPreference(p)).toBe(p); |
|||
}); |
|||
|
|||
test('saveResumeSkills 本地去空去重', async () => { |
|||
const out = await saveResumeSkills([ |
|||
{ name: 'a' }, |
|||
{ name: 'a' }, |
|||
{ name: ' ' }, |
|||
] as any); |
|||
expect(out).toEqual([{ name: 'a' }]); |
|||
}); |
|||
|
|||
test('saveWeightSettings 返回副本(不与入参同引用)', async () => { |
|||
const dims = [{ id: 'd1' }] as any; |
|||
const out = await saveWeightSettings(dims); |
|||
expect(out).not.toBe(dims); |
|||
expect(out).toEqual(dims); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,146 @@ |
|||
import { |
|||
fetchConversations, |
|||
fetchNotifications, |
|||
fetchInterviews, |
|||
fetchMatchResult, |
|||
fetchConversation, |
|||
} from '../api/api'; |
|||
import { setMockMode, API_BASE_URL } from '../api/http'; |
|||
|
|||
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
|
|||
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } })); |
|||
|
|||
// token-store:内存双 Token,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/token-store', () => { |
|||
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null; |
|||
return { |
|||
getAccessToken: jest.fn(() => mem?.accessToken ?? null), |
|||
getTokens: jest.fn(() => mem), |
|||
setTokens: jest.fn((a: string, r: string, e?: number) => { |
|||
mem = { accessToken: a, refreshToken: r, expiresAt: e }; |
|||
}), |
|||
clearTokens: jest.fn(() => { |
|||
mem = null; |
|||
}), |
|||
hydrateTokens: jest.fn(async () => undefined), |
|||
}; |
|||
}); |
|||
|
|||
// storage:内存实现
|
|||
jest.mock('../lib/storage', () => { |
|||
const m = new Map<string, string>(); |
|||
return { |
|||
storageGet: jest.fn(async (k: string) => m.get(k) ?? null), |
|||
storageSet: jest.fn(async (k: string, v: string) => { |
|||
m.set(k, v); |
|||
}), |
|||
storageRemove: jest.fn(async (k: string) => { |
|||
m.delete(k); |
|||
}), |
|||
}; |
|||
}); |
|||
|
|||
interface MockResponseSpec { |
|||
status: number; |
|||
body: unknown; |
|||
} |
|||
type Responder = (url: string, init: RequestInit) => MockResponseSpec; |
|||
|
|||
let responder: Responder | null = null; |
|||
let fetchCalls: Array<{ url: string; init: RequestInit }> = []; |
|||
|
|||
function installFetch(): void { |
|||
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn( |
|||
async (url: string, init: RequestInit) => { |
|||
fetchCalls.push({ url, init }); |
|||
const spec = responder |
|||
? responder(url, init) |
|||
: { status: 200, body: { code: 200, message: 'ok', data: null, requestId: 'r', timestamp: 't' } }; |
|||
return { |
|||
status: spec.status, |
|||
ok: spec.status >= 200 && spec.status < 300, |
|||
headers: { get: () => 'r' }, |
|||
text: async () => JSON.stringify(spec.body), |
|||
} as unknown as Response; |
|||
}, |
|||
); |
|||
} |
|||
|
|||
function okData(data: unknown): MockResponseSpec { |
|||
return { status: 200, body: { code: 200, message: 'ok', data, requestId: 'r', timestamp: 't' } }; |
|||
} |
|||
|
|||
beforeEach(() => { |
|||
fetchCalls = []; |
|||
responder = null; |
|||
installFetch(); |
|||
}); |
|||
|
|||
describe('消息 / 沟通域 mock 开关', () => { |
|||
it('mock 模式:fetchConversations 返回本地会话且不发请求', async () => { |
|||
setMockMode(true); |
|||
const list = await fetchConversations(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(Array.isArray(list)).toBe(true); |
|||
}); |
|||
|
|||
it('真实模式:fetchConversations 打 /conversations', async () => { |
|||
setMockMode(false); |
|||
responder = () => okData([{ id: 'c1' }]); |
|||
const list = await fetchConversations(); |
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/conversations`); |
|||
expect(list[0].id).toBe('c1'); |
|||
}); |
|||
|
|||
it('mock 模式:fetchNotifications 返回本地通知且不发请求', async () => { |
|||
setMockMode(true); |
|||
const list = await fetchNotifications(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(Array.isArray(list)).toBe(true); |
|||
}); |
|||
|
|||
it('真实模式:fetchNotifications 打 /notifications', async () => { |
|||
setMockMode(false); |
|||
responder = () => okData([{ id: 'n1' }]); |
|||
const list = await fetchNotifications(); |
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/notifications`); |
|||
expect(list[0].id).toBe('n1'); |
|||
}); |
|||
|
|||
it('mock 模式:fetchInterviews 返回本地日程且不发请求', async () => { |
|||
setMockMode(true); |
|||
const list = await fetchInterviews(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(Array.isArray(list)).toBe(true); |
|||
}); |
|||
|
|||
it('真实模式:fetchInterviews 打 /interviews/me', async () => { |
|||
setMockMode(false); |
|||
responder = () => okData([{ id: 'iv1' }]); |
|||
const list = await fetchInterviews(); |
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/interviews/me`); |
|||
expect(list[0].id).toBe('iv1'); |
|||
}); |
|||
|
|||
it('mock 模式:fetchMatchResult 返回本地结果且不发请求', async () => { |
|||
setMockMode(true); |
|||
const result = await fetchMatchResult(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(result).toBeDefined(); |
|||
}); |
|||
|
|||
it('mock 模式:fetchConversation 透传 id 且不发请求', async () => { |
|||
setMockMode(true); |
|||
const detail = await fetchConversation('conv-9'); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(detail.id).toBe('conv-9'); |
|||
}); |
|||
|
|||
it('真实模式:fetchConversation 打 /conversations/{id}', async () => { |
|||
setMockMode(false); |
|||
responder = () => okData({ id: 'conv-9', messages: [] }); |
|||
const detail = await fetchConversation('conv-9'); |
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/conversations/conv-9`); |
|||
expect(detail.id).toBe('conv-9'); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,130 @@ |
|||
import { |
|||
fetchUserProfile, |
|||
fetchResume, |
|||
fetchResumeSavedAt, |
|||
fetchResumeExportSettings, |
|||
} from '../api/api'; |
|||
import { setMockMode, API_BASE_URL } from '../api/http'; |
|||
|
|||
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
|
|||
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } })); |
|||
|
|||
// token-store:内存双 Token,避免依赖 AsyncStorage
|
|||
jest.mock('../lib/token-store', () => { |
|||
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null; |
|||
return { |
|||
getAccessToken: jest.fn(() => mem?.accessToken ?? null), |
|||
getTokens: jest.fn(() => mem), |
|||
setTokens: jest.fn((a: string, r: string, e?: number) => { |
|||
mem = { accessToken: a, refreshToken: r, expiresAt: e }; |
|||
}), |
|||
clearTokens: jest.fn(() => { |
|||
mem = null; |
|||
}), |
|||
hydrateTokens: jest.fn(async () => undefined), |
|||
}; |
|||
}); |
|||
|
|||
// storage:内存实现,供 mock 态读取(引导资料 / 简历覆盖层 / 导出设置)
|
|||
jest.mock('../lib/storage', () => { |
|||
const m = new Map<string, string>(); |
|||
return { |
|||
storageGet: jest.fn(async (k: string) => m.get(k) ?? null), |
|||
storageSet: jest.fn(async (k: string, v: string) => { |
|||
m.set(k, v); |
|||
}), |
|||
storageRemove: jest.fn(async (k: string) => { |
|||
m.delete(k); |
|||
}), |
|||
}; |
|||
}); |
|||
|
|||
interface MockResponseSpec { |
|||
status: number; |
|||
body: unknown; |
|||
} |
|||
type Responder = (url: string, init: RequestInit) => MockResponseSpec; |
|||
|
|||
let responder: Responder | null = null; |
|||
let fetchCalls: Array<{ url: string; init: RequestInit }> = []; |
|||
|
|||
function installFetch(): void { |
|||
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn( |
|||
async (url: string, init: RequestInit) => { |
|||
fetchCalls.push({ url, init }); |
|||
const spec = responder |
|||
? responder(url, init) |
|||
: { status: 200, body: { code: 200, message: 'ok', data: null, requestId: 'r', timestamp: 't' } }; |
|||
return { |
|||
status: spec.status, |
|||
ok: spec.status >= 200 && spec.status < 300, |
|||
headers: { get: () => 'r' }, |
|||
text: async () => JSON.stringify(spec.body), |
|||
} as unknown as Response; |
|||
}, |
|||
); |
|||
} |
|||
|
|||
function okData(data: unknown): MockResponseSpec { |
|||
return { status: 200, body: { code: 200, message: 'ok', data, requestId: 'r', timestamp: 't' } }; |
|||
} |
|||
|
|||
beforeEach(() => { |
|||
fetchCalls = []; |
|||
responder = null; |
|||
installFetch(); |
|||
}); |
|||
|
|||
describe('个人资料 / 简历 域 mock 开关', () => { |
|||
it('mock 模式:fetchUserProfile 返回本地资料且不发请求', async () => { |
|||
setMockMode(true); |
|||
const profile = await fetchUserProfile(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(profile).toBeDefined(); |
|||
expect(typeof profile.name).toBe('string'); |
|||
}); |
|||
|
|||
it('真实模式:fetchUserProfile 先打 /profile/me(后端字段空时兜底 /auth/me)', async () => { |
|||
setMockMode(false); |
|||
// 后端 /profile/me 返回的字段名(nickname / avatarUrl),不是前端 UserProfile 的 name / avatarKey
|
|||
responder = () => okData({ nickname: '张三', avatarUrl: 'a1' }); |
|||
const profile = await fetchUserProfile(); |
|||
// 第一个请求应该是 /profile/me
|
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me`); |
|||
// profile 非空时不请求 auth/me,直接用后端字段映射
|
|||
expect(fetchCalls.length).toBe(1); |
|||
expect(profile.name).toBe('张三'); |
|||
expect(profile.avatar).toBe('张'); |
|||
}); |
|||
|
|||
it('mock 模式:fetchResume 返回本地派生简历且不发请求', async () => { |
|||
setMockMode(true); |
|||
const resume = await fetchResume(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(resume).toBeDefined(); |
|||
expect(Array.isArray(resume.education)).toBe(true); |
|||
}); |
|||
|
|||
it('真实模式:fetchResume 打 /profile/me/resume', async () => { |
|||
setMockMode(false); |
|||
responder = () => okData({ basicInfo: {}, education: [], experience: [] }); |
|||
const resume = await fetchResume(); |
|||
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/resume`); |
|||
expect(Array.isArray(resume.education)).toBe(true); |
|||
}); |
|||
|
|||
it('mock 模式:fetchResumeSavedAt 返回空串(从未保存)且不发请求', async () => { |
|||
setMockMode(true); |
|||
const savedAt = await fetchResumeSavedAt(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(savedAt).toBe(''); |
|||
}); |
|||
|
|||
it('mock 模式:fetchResumeExportSettings 回落默认设置且不发请求', async () => { |
|||
setMockMode(true); |
|||
const settings = await fetchResumeExportSettings(); |
|||
expect(fetchCalls.length).toBe(0); |
|||
expect(settings).toBeDefined(); |
|||
}); |
|||
|
|||
}); |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue