Browse Source
承接后端联调地基,开始接入真实业务端点:
- 请求层新增 mock 开关:http.ts 的 isMockMode()/setMockMode() 读 app.config.js 的
extra.apiUseMock(默认 true)。业务函数 if (isMockMode()) 返回本地数据不触网,
联调期置 false 直连后端,避免"全转真实破坏开发期 UI"。RequestOptions 加 query
支持(数组逗号拼接、undefined 跳过)。
- 岗位域 6 个候选侧读取接口改造成统一形态(保留签名与返回类型 T,仅换函数体):
fetchJobList→GET /jobs(pageSize→size 映射)、fetchJobDetail→/jobs/{id}、
fetchJobOverallScore→/jobs/{id}/score、fetchJobPreference→/profile/me/job-preference、
fetchFavorites→/profile/me/favorites、fetchJobProgress→/tools/job-progress。
旧 @url 多带 /api 前缀、未带 /v1,已按规范 §7 重映射。
- auth.ts 6 个鉴权函数同样挂 isMockMode() 分支(mock 返回本地双 Token/成功),
保开发期 parity——否则 dev 登录会直打真后端必崩。
- 新增 job-api.test.ts(8 用例:开关两面 + query 拼装);password.test.ts 鉴权断言改在
mock 关闭态校验真实契约。
- 文档 后端联调前端实现.md 补 §三 业务端点转换进度与路径重映射表。
OpenAPI 主机在本环境被网络封禁、无法取真实字段,故 T 沿用现有项目类型(AGENTS.md
"保留签名只换函数体"),真实结构待真机联调按 OpenAPI 校正。
tsc 0 错误;eslint 0 错误;jest 406 passed / 29 suites。
master
8 changed files with 328 additions and 55 deletions
@ -0,0 +1,155 @@ |
|||||
|
import { |
||||
|
fetchJobList, |
||||
|
fetchJobDetail, |
||||
|
fetchJobOverallScore, |
||||
|
fetchJobPreference, |
||||
|
fetchFavorites, |
||||
|
fetchJobProgress, |
||||
|
} 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:内存实现,供 resolveJobPreference 等读取引导资料
|
||||
|
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 模式:fetchJobDetail 返回本地详情且不发任何请求', async () => { |
||||
|
setMockMode(true); |
||||
|
const detail = await fetchJobDetail('1'); |
||||
|
expect(fetchCalls.length).toBe(0); |
||||
|
expect(detail.id).toBe('1'); |
||||
|
expect(typeof detail.level).toBe('string'); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchJobDetail 打 /jobs/{id} 并返回 data', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData({ id: '1', positionName: '后端工程师' }); |
||||
|
const detail = await fetchJobDetail('1'); |
||||
|
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/1`); |
||||
|
expect(detail.positionName).toBe('后端工程师'); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchJobOverallScore 打 /jobs/{id}/score', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData({ totalScore: 88 }); |
||||
|
const score = await fetchJobOverallScore('2'); |
||||
|
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/2/score`); |
||||
|
expect(score.totalScore).toBe(88); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchJobPreference 打 /profile/me/job-preference', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData({ city: '上海', position: '前端' }); |
||||
|
await fetchJobPreference(); |
||||
|
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/job-preference`); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchFavorites 打 /profile/me/favorites', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData([{ id: '1' }]); |
||||
|
const favs = await fetchFavorites(); |
||||
|
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/favorites`); |
||||
|
expect(favs[0].id).toBe('1'); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchJobProgress 打 /tools/job-progress', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData({ steps: [] }); |
||||
|
await fetchJobProgress(); |
||||
|
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/tools/job-progress`); |
||||
|
}); |
||||
|
|
||||
|
it('真实模式:fetchJobList 拼装 query(数组逗号、undefined 跳过、分页键 size)', async () => { |
||||
|
setMockMode(false); |
||||
|
responder = () => okData({ jobs: [], total: 0 }); |
||||
|
await fetchJobList({ |
||||
|
sortBy: 'comprehensive', |
||||
|
salary: ['10-15K'], |
||||
|
experience: ['3-5年'], |
||||
|
}); |
||||
|
const url = fetchCalls[0].url; |
||||
|
expect(url.startsWith(`${API_BASE_URL}/jobs?`)).toBe(true); |
||||
|
expect(url).toContain('sortBy=comprehensive'); |
||||
|
expect(url).toContain('page=1'); |
||||
|
expect(url).toContain('size=10'); |
||||
|
expect(url).toContain('salary=10-15K'); |
||||
|
// 中文被 URLSearchParams 编码,解码后断言
|
||||
|
expect(decodeURIComponent(url)).toContain('experience=3-5年'); |
||||
|
// keyword / industry / city / education 未传 → 不出现在 query 中
|
||||
|
expect(url).not.toContain('keyword='); |
||||
|
expect(url).not.toContain('industry='); |
||||
|
}); |
||||
|
|
||||
|
it('mock 模式:fetchJobList 返回本地 mock 且分页字段齐全', async () => { |
||||
|
setMockMode(true); |
||||
|
const res = await fetchJobList({ sortBy: 'comprehensive' }); |
||||
|
expect(fetchCalls.length).toBe(0); |
||||
|
expect(Array.isArray(res.jobs)).toBe(true); |
||||
|
expect(typeof res.total).toBe('number'); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue