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