Files
zPDF_package/test/usePdfApi.test.js
zikai 30f5cee4a1 feat: PDF 转换前端(epub 上传/进度轮询/下载/用户软删,复用 zTools2 API)
便携式 Vue3 前端:上传 epub 转 PDF,显示上传与转换进度,完成后下载;
用户可软删任务(不再对自己展示,管理员仍可见)。凭 cookie 标识用户。
复用 zTools2 后端 /api/pdf/*(同源),独立 i18n(中英懒加载),
可独立运行或经 mainPage iframe 同源集成。含 vitest 单测 20 项。
2026-07-27 11:33:26 +08:00

194 lines
6.1 KiB
JavaScript
Raw 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,
} 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 生成路径', () => {
const api = createPdfApi({ fetch: vi.fn() })
expect(api.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')
})
})