Browse Source
- 新增 fetchAbilityTypes / createAbilityAttempt / fetchAbilityAttempt / saveAbilityAnswers / submitAbilityAttempt / fetchAbilityResult / fetchMyAbilities - fetchAbilityAssessment 补真实分支:取最近一份 COMPLETED 结果并映射 - 实网实测契约(文档未标注): 题目 id 是 attemptQuestionId、答案字段是 selectedOptionId(写 optionId 报 9805); 答卷详情不带 version,保存与提交均无需乐观锁;未答完提交报 9806 - 四维 code 为 LOGIC/COMMUNICATION/EXECUTION/LEARNING,与 mock 阶段的 「专业能力/项目经验/技能广度」不是同一套口径,真实态以后端为准(产品口径待定夺) - 结果有效期 validUntil/validityStatus 由后端判定,前端不自行作废 新增 src/__tests__/ability-attempt.test.ts(5 用例) tsc 0 错误;jest 530 全过;eslint 0 errormaster
4 changed files with 364 additions and 6 deletions
@ -0,0 +1,170 @@ |
|||||
|
/** |
||||
|
* 四维能力测评(/abilities)真实分支测试 |
||||
|
* |
||||
|
* 与霍兰德答卷的两个关键差异(实网踩坑点): |
||||
|
* - 题目 id 是 attemptQuestionId、答案字段是 selectedOptionId,写错报 9805 |
||||
|
* - 答卷详情不带 version,保存/提交都不需要乐观锁 |
||||
|
*/ |
||||
|
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } })); |
||||
|
|
||||
|
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) => { |
||||
|
mem = { accessToken: a, refreshToken: r }; |
||||
|
}), |
||||
|
clearTokens: jest.fn(() => { |
||||
|
mem = null; |
||||
|
}), |
||||
|
hydrateTokens: jest.fn(async () => undefined), |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
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 () => undefined), |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
import { setMockMode, API_BASE_URL } from '../api/http'; |
||||
|
import { |
||||
|
fetchAbilityAssessment, |
||||
|
fetchAbilityTypes, |
||||
|
createAbilityAttempt, |
||||
|
saveAbilityAnswers, |
||||
|
submitAbilityAttempt, |
||||
|
} from '../api/api'; |
||||
|
|
||||
|
type Responder = (url: string) => { status: number; body: unknown }; |
||||
|
let responder: Responder | null = null; |
||||
|
let calls: Array<{ url: string; init: RequestInit }> = []; |
||||
|
|
||||
|
function okData(data: unknown) { |
||||
|
return { status: 200, body: { code: 200, message: 'ok', data, requestId: 'r', timestamp: 't' } }; |
||||
|
} |
||||
|
|
||||
|
const ABILITY_RESULT = { |
||||
|
attemptId: 1, |
||||
|
typeCode: 'WORKPLACE_ABILITY', |
||||
|
status: 'COMPLETED', |
||||
|
validityStatus: 'VALID', |
||||
|
overallScore: 44.79, |
||||
|
dimensionScores: [ |
||||
|
{ code: 'LOGIC', score: 37.5 }, |
||||
|
{ code: 'COMMUNICATION', score: 58.33 }, |
||||
|
], |
||||
|
improvementAreas: [ |
||||
|
{ |
||||
|
dimension: 'LEARNING', |
||||
|
currentScore: 12.5, |
||||
|
summary: '把反馈变成具体行动并通过再次验证确认改进。', |
||||
|
actions: ['把模糊反馈转化为具体例子'], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
calls = []; |
||||
|
responder = null; |
||||
|
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn(async (url: string, init: RequestInit) => { |
||||
|
calls.push({ url, init }); |
||||
|
const spec = responder ? responder(url) : okData(null); |
||||
|
return { |
||||
|
status: spec.status, |
||||
|
ok: spec.status >= 200 && spec.status < 300, |
||||
|
headers: { get: () => 'r' }, |
||||
|
text: async () => JSON.stringify(spec.body), |
||||
|
} as unknown as Response; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('能力测评真实态', () => { |
||||
|
beforeEach(() => setMockMode(false)); |
||||
|
|
||||
|
it('已完成时映射综合分/等级/四维与提升建议', async () => { |
||||
|
responder = (url) => { |
||||
|
if (url.includes('/abilities/me')) |
||||
|
return okData({ |
||||
|
items: [{ attemptId: 1, typeCode: 'WORKPLACE_ABILITY', status: 'COMPLETED' }], |
||||
|
total: 1, |
||||
|
}); |
||||
|
if (url.includes('/result')) return okData(ABILITY_RESULT); |
||||
|
return okData(null); |
||||
|
}; |
||||
|
const data = await fetchAbilityAssessment(); |
||||
|
expect(data.overallScore).toBe(44.79); |
||||
|
expect(data.level).toBe('medium'); // <75
|
||||
|
expect(data.dimensions).toEqual([ |
||||
|
{ label: '逻辑分析', value: 37.5 }, |
||||
|
{ label: '沟通协作', value: 58.33 }, |
||||
|
]); |
||||
|
expect(data.improvements[0].title).toBe('学习成长'); |
||||
|
// 后端不下发百分位与趋势:留空,不拿 mock 顶替
|
||||
|
expect(data.percentile).toBeUndefined(); |
||||
|
expect(data.trends).toEqual([]); |
||||
|
}); |
||||
|
|
||||
|
it('未完成时抛错,不静默返回 0 分', async () => { |
||||
|
responder = () => okData({ items: [], total: 0 }); |
||||
|
await expect(fetchAbilityAssessment()).rejects.toThrow('暂无能力测评结果'); |
||||
|
}); |
||||
|
|
||||
|
it('创建/答题/提交打真实路径,答案字段为 selectedOptionId', async () => { |
||||
|
responder = (url) => { |
||||
|
if (url.includes('/abilities/attempts') && !url.includes('/answers')) |
||||
|
return okData({ |
||||
|
attemptId: 7, |
||||
|
status: 'DRAFT', |
||||
|
totalQuestions: 24, |
||||
|
answeredCount: 0, |
||||
|
questions: [ |
||||
|
{ |
||||
|
attemptQuestionId: 1, |
||||
|
position: 1, |
||||
|
itemType: 'SITUATIONAL_JUDGMENT', |
||||
|
stem: '估算工期', |
||||
|
options: [{ optionId: 'A', text: '给大致天数' }], |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
return okData(null); |
||||
|
}; |
||||
|
await createAbilityAttempt(); |
||||
|
expect(calls[0].url).toBe(`${API_BASE_URL}/abilities/attempts`); |
||||
|
expect(JSON.parse(String(calls[0].init.body))).toEqual({ typeCode: 'WORKPLACE_ABILITY' }); |
||||
|
|
||||
|
await saveAbilityAnswers(7, [{ attemptQuestionId: 1, selectedOptionId: 'A' }]); |
||||
|
const patch = calls.find((c) => c.init.method === 'PATCH'); |
||||
|
expect(patch?.url).toBe(`${API_BASE_URL}/abilities/attempts/7/answers`); |
||||
|
expect(JSON.parse(String(patch?.init.body))).toEqual({ |
||||
|
answers: [{ attemptQuestionId: 1, selectedOptionId: 'A' }], |
||||
|
}); |
||||
|
|
||||
|
await submitAbilityAttempt(7); |
||||
|
const post = calls.find((c) => c.url.endsWith('/submit')); |
||||
|
expect(post?.url).toBe(`${API_BASE_URL}/abilities/attempts/7/submit`); |
||||
|
}); |
||||
|
|
||||
|
it('类型列表打 /abilities/types', async () => { |
||||
|
responder = () => okData([{ typeCode: 'WORKPLACE_ABILITY', displayName: '四维能力测评' }]); |
||||
|
await fetchAbilityTypes(); |
||||
|
expect(calls[0].url).toBe(`${API_BASE_URL}/abilities/types`); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('能力测评 mock 态', () => { |
||||
|
beforeEach(() => setMockMode(true)); |
||||
|
|
||||
|
it('mock 模式不发请求且保留原四维文案', async () => { |
||||
|
const data = await fetchAbilityAssessment(); |
||||
|
expect(calls.length).toBe(0); |
||||
|
expect(data.dimensions[0].label).toBe('专业能力'); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue