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