Files
zPDF_package/test/usePdfApi.test.js
zikai 25bc00ac51 refactor: 清理死代码 + 提前失败 + 错误记录控制台
- downloadUrl 提升为顶层纯函数导出, JobList 不再为此实例化整个 api 对象
- createPdfApi 返回对象移除内部用的 getJob / downloadUrl (高内聚)
- useLocale 移除未用的 isZh/pick 返回值
- 移除 4 个死 i18n key (upload.uploading/submitted/failed/networkError)
- App.vue: import {watch} 提升至顶部; onDelete 补 try/catch + console.error
- usePdfApi: pollPending 单任务失败补 console.warn; uploadFile 响应解析失败补 console.error
- docs: 新增 error-handling.md / integration.md (嵌入细节从 README 移入)
- README: 修正 dev 端口 5173->5175; 补 apache2 部署说明
2026-07-28 11:12:37 +08:00

194 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// usePdfApi 单元测试:验证状态机、校验、轮询、格式化逻辑(与 DOM 解耦)。
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
createPdfApi,
isTerminal,
fmtBytes,
validateFile,
uploadFile,
downloadUrl,
} from '../src/composables/usePdfApi.js'
describe('isTerminal', () => {
it('done 与 failed 为终态', () => {
expect(isTerminal({ status: 'done' })).toBe(true)
expect(isTerminal({ status: 'failed' })).toBe(true)
})
it('pending / converting 非终态', () => {
expect(isTerminal({ status: 'pending' })).toBe(false)
expect(isTerminal({ status: 'converting' })).toBe(false)
})
it('空值安全', () => {
expect(isTerminal(null)).toBe(false)
expect(isTerminal(undefined)).toBe(false)
})
})
describe('fmtBytes', () => {
it('字节级显示 B', () => {
expect(fmtBytes(500)).toBe('500 B')
})
it('KB/MB 进位', () => {
expect(fmtBytes(1500)).toBe('1.5 KB')
expect(fmtBytes(5 * 1024 * 1024)).toBe('5.2 MB')
})
it('空/非法值返回 -', () => {
expect(fmtBytes(null)).toBe('-')
expect(fmtBytes(NaN)).toBe('-')
})
})
describe('validateFile', () => {
const mk = (name, size) => ({ name, size })
it('epub 且未超限通过', () => {
expect(validateFile(mk('a.epub', 1000))).toBeNull()
expect(validateFile(mk('A.EPUB', 1000))).toBeNull()
})
it('非 epub 拒绝', () => {
expect(validateFile(mk('a.txt', 1000))).toBe('upload.requireEpub')
expect(validateFile(mk('a.pdf', 1000))).toBe('upload.requireEpub')
})
it('超 250MB 拒绝', () => {
expect(validateFile(mk('a.epub', 250 * 1024 * 1024 + 1))).toBe('upload.oversize')
})
it('自定义上限', () => {
expect(validateFile(mk('a.epub', 200), 100)).toBe('upload.oversize')
})
})
describe('createPdfApi', () => {
it('load 拉取列表并归一化', async () => {
const fetcher = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ total: 2, items: [{ id: 1 }, { id: 2 }] }),
})
const api = createPdfApi({ fetch: fetcher })
const r = await api.load()
expect(fetcher).toHaveBeenCalledWith('/api/pdf/jobs', { credentials: 'same-origin' })
expect(r.total).toBe(2)
expect(r.items).toHaveLength(2)
})
it('load 处理空响应', async () => {
const fetcher = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })
const api = createPdfApi({ fetch: fetcher })
const r = await api.load()
expect(r.total).toBe(0)
expect(r.items).toEqual([])
})
it('load HTTP 错误抛异常', async () => {
const fetcher = vi.fn().mockResolvedValue({ ok: false, status: 500 })
const api = createPdfApi({ fetch: fetcher })
await expect(api.load()).rejects.toThrow('HTTP 500')
})
it('remove 发 DELETE', async () => {
const fetcher = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ deleted: true }) })
const api = createPdfApi({ fetch: fetcher })
const r = await api.remove(7)
expect(fetcher).toHaveBeenCalledWith('/api/pdf/jobs/7', {
method: 'DELETE',
credentials: 'same-origin',
})
expect(r.deleted).toBe(true)
})
it('downloadUrl 生成路径', () => {
expect(downloadUrl(3)).toBe('/api/pdf/jobs/3/download')
})
it('pollPending 无未完成任务立即 onDone', () => {
const api = createPdfApi({ fetch: vi.fn() })
const onDone = vi.fn()
const onUpdate = vi.fn()
const stop = api.pollPending(
[{ id: 1, status: 'done' }],
onUpdate,
onDone
)
expect(onDone).toHaveBeenCalledTimes(1)
expect(onUpdate).not.toHaveBeenCalled()
stop()
})
it('pollPending 轮询未完成项直至终态', async () => {
vi.useFakeTimers()
// getJob 第 1 次返回 converting第 2 次返回 done
const states = [{ status: 'converting', progress: 50 }, { status: 'done', progress: 100 }]
let call = 0
const fetcher = vi.fn().mockImplementation(async () => ({
ok: true,
json: async () => ({ id: 1, ...states[Math.min(call++, states.length - 1)] }),
}))
const api = createPdfApi({ fetch: fetcher })
const updated = []
const onDone = vi.fn()
const stop = api.pollPending(
[{ id: 1, status: 'pending', progress: 0 }],
(fresh) => updated.push(fresh),
onDone
)
// 第 1 次轮询converting
await vi.advanceTimersByTimeAsync(1500)
expect(updated[0][0].status).toBe('converting')
// 第 2 次轮询done -> 触发 onDone
await vi.advanceTimersByTimeAsync(1500)
expect(onDone).toHaveBeenCalledTimes(1)
expect(updated[1][0].status).toBe('done')
stop()
vi.useRealTimers()
})
it('pollPending stop 提前停止', async () => {
vi.useFakeTimers()
const fetcher = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 1, status: 'converting', progress: 30 }),
})
const api = createPdfApi({ fetch: fetcher })
const onUpdate = vi.fn()
const stop = api.pollPending([{ id: 1, status: 'pending' }], onUpdate)
stop()
await vi.advanceTimersByTimeAsync(3000)
expect(onUpdate).not.toHaveBeenCalled()
vi.useRealTimers()
})
})
describe('uploadFile', () => {
it('成功解析响应', async () => {
const fakeXhr = vi.fn().mockImplementation(() => {
const x = {
open: vi.fn(),
send: vi.fn(),
upload: {},
status: 200,
responseText: JSON.stringify({ job: { id: 9 } }),
}
// 模拟 send 后异步触发 onload
setTimeout(() => x.onload && x.onload(), 0)
return x
})
const res = await uploadFile({ name: 'a.epub', size: 1 }, () => {}, { xhr: fakeXhr })
expect(res.job.id).toBe(9)
})
it('HTTP 错误抛含 detail', async () => {
const fakeXhr = vi.fn().mockImplementation(() => {
const x = {
open: vi.fn(),
send: vi.fn(),
upload: {},
status: 413,
responseText: JSON.stringify({ detail: 'too big' }),
}
setTimeout(() => x.onload && x.onload(), 0)
return x
})
await expect(
uploadFile({ name: 'a.epub', size: 1 }, () => {}, { xhr: fakeXhr })
).rejects.toThrow('too big')
})
})