需求:小程序端代码要同时同步到服务器与 git 远程仓库,此前前端仅存在于本地目录(无 remote,从未推送), 服务器与 gitcode 上只有后端。现并入本仓库,复用现有「push 即部署」链路,一次推送双端落地。 - 新增 uniapp/:小程序端源码全量快照(150 文件 / 1.2M) pages(13 页面) · components(fc-navbar/fc-tabbar/fc-post-card/fc-empty) · common(api·utils·mock·assets·store·config) · static(74 图标 + 3 轮播插画 + LOGO) · docs(01-04 交付文档) · tests(零依赖全链路用例) · tools(图标/主题/轮播配图生成脚本) - 明确排除:AGENTS.md(含后台密码、服务器 root 密码、小程序 AppSecret,切勿入库)、 unpackage/(构建产物,由 uniapp/.gitignore 忽略)、node_modules、.DS_Store、*.log - .gitignore 新增 /uniapp/AGENTS.md 兜底(与根 /AGENTS.md、/fluniapp/ 同一约定) - README.md 增补「本项目结构 + 部署链路」章节,说明 uniapp/ 为源码快照、 小程序真正上线仍需微信开发者工具 cli upload 说明:本次为源码快照,未携带前端本地仓库历史——其历史提交中含 AGENTS.md 凭据, 若做 subtree 合并会把密码永久写入本仓库历史,故采用快照方式。
135 行
4.5 KiB
JavaScript
135 行
4.5 KiB
JavaScript
/**
|
||
* 请求层(全链路统一网关)测试
|
||
* 覆盖:URL 拼接 / 鉴权头 / 业务码解包 / 401 / 网络异常 / mock 降级
|
||
*/
|
||
import { describe, it, expect, expectReject } from './harness.mjs'
|
||
import config from '../common/config/index.js'
|
||
import storage from '../common/utils/storage.js'
|
||
import request, { get, post, buildUrl, buildHeaders, getRuntime } from '../common/utils/request.js'
|
||
|
||
/** 构造一个可控的 uni 运行时 */
|
||
function installUni(handler) {
|
||
const calls = { toasts: [], navigations: [], requests: [] }
|
||
globalThis.uni = {
|
||
request(opts) {
|
||
calls.requests.push(opts)
|
||
handler(opts, calls)
|
||
},
|
||
showToast(o) {
|
||
calls.toasts.push(o)
|
||
},
|
||
navigateTo(o) {
|
||
calls.navigations.push(o)
|
||
},
|
||
getStorageSync: () => '',
|
||
setStorageSync: () => {},
|
||
removeStorageSync: () => {},
|
||
}
|
||
return calls
|
||
}
|
||
|
||
function clearUni() {
|
||
delete globalThis.uni
|
||
}
|
||
|
||
describe('请求层 · 基础能力', () => {
|
||
it('buildUrl 拼接版本号与网关地址', () => {
|
||
expect(buildUrl('/home/feed')).toBe(`${config.baseURL}/${config.apiVersion}/home/feed`)
|
||
expect(buildUrl('post/create')).toContain('/v1/post/create')
|
||
expect(buildUrl('https://a.com/x')).toBe('https://a.com/x')
|
||
})
|
||
|
||
it('buildHeaders 注入 token 与客户端标识', () => {
|
||
storage.clear()
|
||
storage.set(config.storageKeys.token, 'T-123')
|
||
const h = buildHeaders()
|
||
expect(h.Authorization).toBe('Bearer T-123')
|
||
expect(h['X-Client']).toBe('fenleixinxi')
|
||
storage.remove(config.storageKeys.token)
|
||
expect(buildHeaders().Authorization).toBeFalsy()
|
||
})
|
||
|
||
it('无 uni.request 时自动走 mock(离线可运行)', async () => {
|
||
clearUni()
|
||
const data = await get('/home/categories')
|
||
expect(data).toBeArray()
|
||
expect(data.length).toBeGreaterThan(0)
|
||
})
|
||
})
|
||
|
||
describe('请求层 · 真实 HTTP 通道', () => {
|
||
it('业务码 0 时返回 data 字段', async () => {
|
||
const calls = installUni((opts) => {
|
||
opts.success({ statusCode: 200, data: { code: 0, message: 'ok', data: { total: 7 } } })
|
||
})
|
||
const res = await get('/home/feed', { page: 1 })
|
||
expect(res.total).toBe(7)
|
||
expect(calls.requests[0].url).toContain('/v1/home/feed')
|
||
clearUni()
|
||
})
|
||
|
||
it('业务码非 0 时 reject 并 toast 提示', async () => {
|
||
const calls = installUni((opts) => {
|
||
opts.success({ statusCode: 200, data: { code: 1001, message: '参数错误' } })
|
||
})
|
||
await expectReject(() => post('/post/create', { title: '' }), '参数错误')
|
||
expect(calls.toasts[0].title).toBe('参数错误')
|
||
clearUni()
|
||
})
|
||
|
||
it('HTTP 401 清空登录态并跳转登录页', async () => {
|
||
storage.set(config.storageKeys.token, 'EXPIRED')
|
||
const calls = installUni((opts) => opts.success({ statusCode: 401, data: {} }))
|
||
await expectReject(() => get('/auth/profile'), '登录已过期')
|
||
expect(storage.get(config.storageKeys.token, '')).toBe('')
|
||
expect(calls.navigations[0].url).toBe('/pages/auth/login')
|
||
clearUni()
|
||
})
|
||
|
||
it('显式开启 enableMockFallback 时,网络异常且存在 mock 路由则自动降级', async () => {
|
||
const saved = config.enableMockFallback
|
||
config.enableMockFallback = true
|
||
const calls = installUni((opts) => opts.fail({ errMsg: 'request:fail timeout' }))
|
||
const res = await get('/member/plans')
|
||
expect(res).toBeArray()
|
||
expect(res[0].planId).toBeDefined()
|
||
clearUni()
|
||
config.enableMockFallback = saved
|
||
})
|
||
|
||
// 生产配置下必须关闭降级:否则真实接口失败会静默返回 mock 假数据,
|
||
// 表现为「后台改了但前端不变」,且真实问题被完全掩盖。
|
||
it('生产配置(enableMockFallback=false)网络异常时不静默返回 mock 数据', async () => {
|
||
expect(config.forceMock).toBe(false)
|
||
expect(config.enableMockFallback).toBe(false)
|
||
installUni((opts) => opts.fail({ errMsg: 'request:fail timeout' }))
|
||
await expectReject(() => get('/member/plans'), 'request:fail')
|
||
clearUni()
|
||
})
|
||
|
||
it('网络异常且无 mock 路由时抛出异常', async () => {
|
||
installUni((opts) => opts.fail({ errMsg: 'request:fail' }))
|
||
await expectReject(() => get('/not/exist/endpoint'), 'request:fail')
|
||
clearUni()
|
||
})
|
||
|
||
it('getRuntime 在无 uni 环境返回安全降级函数', () => {
|
||
clearUni()
|
||
const rt = getRuntime()
|
||
expect(rt.request).toBe(null)
|
||
rt.showToast({ title: 'x' }) // 不应抛错
|
||
})
|
||
})
|
||
|
||
describe('请求层 · mock 强制模式', () => {
|
||
it('mock:true 时绕过网络直接返回 mock 数据', async () => {
|
||
const calls = installUni(() => {
|
||
throw new Error('不应发起真实请求')
|
||
})
|
||
const res = await request({ url: '/home/banners', mock: true })
|
||
expect(res).toBeArray()
|
||
expect(calls.requests).toHaveLength(0)
|
||
clearUni()
|
||
})
|
||
})
|