| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770 |
- import fs from 'fs'
- import path from 'path'
- import * as babel from '@babel/core'
- import { parseComponent } from 'vue-template-compiler'
- import { ACCEPT, formatFileSize, validateBackgroundFile } from '@/utils/teamBackgroundFile'
- process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'
- const routerPath = path.resolve(__dirname, '../../src/router/index.js')
- const pagePath = path.resolve(__dirname, '../../src/views/pages/team-background-upload.vue')
- const queuePath = path.resolve(__dirname, '../../src/components/team-background/TeamBackgroundUploadQueue.vue')
- const readIfPresent = file => fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
- function deferred() {
- let resolve
- let reject
- const promise = new Promise((resolvePromise, rejectPromise) => {
- resolve = resolvePromise
- reject = rejectPromise
- })
- return { promise, resolve, reject }
- }
- function transformModule(source) {
- return babel.transformSync(source, {
- babelrc: false,
- configFile: false,
- plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
- }).code
- }
- function evaluateModule(source, mocks) {
- const code = transformModule(source)
- const module = { exports: {} }
- const localRequire = id => {
- if (Object.prototype.hasOwnProperty.call(mocks, id)) return mocks[id]
- throw new Error(`Missing test mock for ${id}`)
- }
- const evaluate = new Function('require', 'module', 'exports', code)
- evaluate(localRequire, module, module.exports)
- return module.exports
- }
- function loadRouter() {
- const http = { get: jest.fn(() => new Promise(() => {})) }
- const Vue = { use: jest.fn(), prototype: { $message: { error: jest.fn() } } }
- class Router {
- constructor(options) {
- this.options = options
- this.app = { $options: { store: { state: { sidebarMenuActiveName: [] } } } }
- }
- beforeEach(guard) {
- this.guard = guard
- }
- onError(handler) {
- this.errorHandler = handler
- }
- addRoutes() {}
- push() {
- return Promise.resolve()
- }
- }
- const previousWindow = global.window
- global.window = {
- SITE_CONFIG: {
- dynamicMenuRoutesHasAdded: false,
- dynamicRoutes: [],
- menuList: [],
- contentTabDefault: {}
- }
- }
- try {
- const exports = evaluateModule(readIfPresent(routerPath), {
- vue: Vue,
- 'vue-router': Router,
- '@/utils/request': http
- })
- return { ...exports, http }
- } finally {
- global.window = previousWindow
- }
- }
- function expectPage(source) {
- expect(source).not.toBe('')
- return source !== ''
- }
- function loadPageModule(api) {
- const source = readIfPresent(pagePath)
- const descriptor = parseComponent(source)
- return evaluateModule(descriptor.script.content, {
- '@/api/teamBackgroundFile': api,
- '@/utils/teamBackgroundFile': { ACCEPT, formatFileSize, validateBackgroundFile },
- '@/components/team-background/TeamBackgroundUploadQueue.vue': {}
- })
- }
- function loadPageOptions(api) {
- return loadPageModule(api).default
- }
- function loadExpiryParser(api) {
- const parser = loadPageModule(api).parseUploadExpiry
- expect(parser).toEqual(expect.any(Function))
- return parser
- }
- function createPageVm(api, overrides = {}) {
- const options = loadPageOptions(api)
- const vm = {
- ...options.data(),
- $route: { query: {} },
- $message: { error: jest.fn(), success: jest.fn() },
- $refs: { fileInput: { value: 'selected', click: jest.fn() } },
- ...options.methods,
- ...overrides
- }
- vm.$componentOptions = options
- return vm
- }
- function computed(vm, name) {
- return vm.$componentOptions.computed[name].call(vm)
- }
- function invokeWatcher(vm, name, value, oldValue) {
- const watcher = vm.$componentOptions.watch && vm.$componentOptions.watch[name]
- expect(watcher).toBeDefined()
- if (!watcher) return
- const handler = typeof watcher === 'function' ? watcher : watcher.handler
- return handler.call(vm, value, oldValue)
- }
- describe('public team background upload route', () => {
- test('is a page route outside the authenticated layout and short-circuits before menu requests', () => {
- const { default: router, pageRoutes, moduleRoutes, http } = loadRouter()
- const route = pageRoutes.find(candidate => candidate.path === '/team-background-upload')
- const next = jest.fn()
- expect(route).toMatchObject({
- path: '/team-background-upload',
- name: 'team-background-upload',
- meta: { title: '上传团队背景资料', public: true }
- })
- expect(moduleRoutes.children.some(candidate => candidate.path === route.path)).toBe(false)
- router.guard({ path: route.path, matched: [{ meta: route.meta }] }, {}, next)
- expect(next).toHaveBeenCalledTimes(1)
- expect(next).toHaveBeenCalledWith()
- expect(http.get).not.toHaveBeenCalled()
- })
- })
- describe('public team background upload page state machine', () => {
- const source = readIfPresent(pagePath)
- let api
- beforeEach(() => {
- api = {
- completePublicUpload: jest.fn(),
- getPublicUploadSession: jest.fn(),
- uploadPublicTeamBackgroundFile: jest.fn(),
- verifyPublicUploadCode: jest.fn()
- }
- })
- afterEach(() => {
- jest.useRealTimers()
- })
- test('parses the backend timestamp as GMT+8 at the countdown boundary', () => {
- if (!expectPage(source)) return
- const parseUploadExpiry = loadExpiryParser(api)
- if (typeof parseUploadExpiry !== 'function') return
- expect(parseUploadExpiry('2026-07-21 10:00:02')).toBe(Date.UTC(2026, 6, 21, 2, 0, 2))
- jest.useFakeTimers()
- jest.setSystemTime(new Date('2026-07-21T02:00:00.000Z'))
- const vm = createPageVm(api)
- vm.activateSession({ teamName: 'Alpha', uploadToken: 'upload-token', expiresAt: '2026-07-21 10:00:02' })
- expect(vm.remainingSeconds).toBe(2)
- jest.advanceTimersByTime(2000)
- expect(vm.state).toBe('expired')
- })
- test('retains valid ISO timestamps with explicit offsets', () => {
- if (!expectPage(source)) return
- const parseUploadExpiry = loadExpiryParser(api)
- if (typeof parseUploadExpiry !== 'function') return
- expect(parseUploadExpiry('2026-07-21T02:00:02.500Z')).toBe(Date.UTC(2026, 6, 21, 2, 0, 2, 500))
- expect(parseUploadExpiry('2026-07-21T10:00:02+08:00')).toBe(Date.UTC(2026, 6, 21, 2, 0, 2))
- })
- test('rejects malformed, timezone-free and impossible expiry values', () => {
- if (!expectPage(source)) return
- const parseUploadExpiry = loadExpiryParser(api)
- if (typeof parseUploadExpiry !== 'function') return
- for (const invalid of [
- '',
- '2026-02-29 10:00:00',
- '2026-13-01 10:00:00',
- '2026-07-21 24:00:00',
- '2026-02-30T10:00:00Z',
- '2026-07-21T10:00:00',
- 'not-a-date'
- ]) {
- expect(Number.isNaN(parseUploadExpiry(invalid))).toBe(true)
- }
- })
- test('preserves valid leap days and one-second calendar rollover', () => {
- if (!expectPage(source)) return
- const parseUploadExpiry = loadExpiryParser(api)
- if (typeof parseUploadExpiry !== 'function') return
- expect(parseUploadExpiry('2028-02-29 08:00:00')).toBe(Date.UTC(2028, 1, 29, 0, 0, 0))
- expect(parseUploadExpiry('2027-01-01 00:00:00') - parseUploadExpiry('2026-12-31 23:59:59')).toBe(1000)
- })
- test('auto-validates a token and ignores its deferred result after expiry', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- api.getPublicUploadSession.mockReturnValue(request.promise)
- const vm = createPageVm(api, { $route: { query: { token: 'link-token' } } })
- const loading = vm.$componentOptions.created.call(vm)
- expect(vm.state).toBe('verifying')
- expect(api.getPublicUploadSession).toHaveBeenCalledWith('link-token')
- vm.expireSession()
- request.resolve({ code: 0, data: { teamName: 'Alpha', uploadToken: 'upload-token', expiresAt: '2099-01-01T00:00:00Z' } })
- await loading
- expect(vm.state).toBe('expired')
- expect(vm.session).toBeNull()
- })
- test('renews an expired component for a changed token and ignores the old verification', async () => {
- if (!expectPage(source)) return
- const oldRequest = deferred()
- const newRequest = deferred()
- api.getPublicUploadSession
- .mockReturnValueOnce(oldRequest.promise)
- .mockReturnValueOnce(newRequest.promise)
- const vm = createPageVm(api, { $route: { query: { token: 'old-token' } } })
- const initial = vm.$componentOptions.created.call(vm)
- expect(api.getPublicUploadSession).toHaveBeenCalledTimes(1)
- vm.expireSession()
- vm.$route.query.token = ' new-token '
- const renewal = invokeWatcher(vm, '$route.query.token', ' new-token ', 'old-token')
- if (!vm.$componentOptions.watch || !vm.$componentOptions.watch['$route.query.token']) return
- expect(api.getPublicUploadSession.mock.calls).toEqual([['old-token'], ['new-token']])
- expect(vm.state).toBe('verifying')
- expect(vm.session).toBeNull()
- oldRequest.resolve({ code: 0, data: { teamName: 'Old', uploadToken: 'old-upload-token', expiresAt: '2099-01-01 08:00:00' } })
- await initial
- expect(vm.state).toBe('verifying')
- newRequest.resolve({ code: 0, data: { teamName: 'New', uploadToken: 'new-upload-token', expiresAt: '2099-01-01 08:00:00' } })
- await renewal
- expect(vm.state).toBe('ready')
- expect(vm.session.uploadToken).toBe('new-upload-token')
- vm.$componentOptions.beforeDestroy.call(vm)
- })
- test('resets a completed component to code entry for a blank token without duplicate initial verification', async () => {
- if (!expectPage(source)) return
- jest.useFakeTimers()
- const vm = createPageVm(api, {
- $route: { query: { token: 'finished-token' } },
- state: 'completed',
- session: { teamName: 'Old', uploadToken: 'old-upload-token', expiresAt: '2099-01-01 08:00:00' },
- queue: [{ id: 'old', status: 'success' }],
- successCount: 1,
- code: '123456',
- completing: true,
- remainingSeconds: 30,
- countdownTimer: setInterval(() => {}, 1000)
- })
- const generation = vm.asyncGeneration
- const requestId = vm.verificationRequestId
- vm.$route.query.token = ' '
- const resetting = invokeWatcher(vm, '$route.query.token', ' ', 'finished-token')
- if (!vm.$componentOptions.watch || !vm.$componentOptions.watch['$route.query.token']) return
- await resetting
- expect(vm).toMatchObject({
- state: 'code',
- session: null,
- queue: [],
- successCount: 0,
- code: '',
- completing: false,
- remainingSeconds: 0,
- countdownTimer: null
- })
- expect(vm.asyncGeneration).toBeGreaterThan(generation)
- expect(vm.verificationRequestId).toBeGreaterThan(requestId)
- expect(jest.getTimerCount()).toBe(0)
- expect(api.getPublicUploadSession).not.toHaveBeenCalled()
- })
- test('route renewal from ready ignores old upload progress and result', async () => {
- if (!expectPage(source)) return
- const uploadRequest = deferred()
- const renewalRequest = deferred()
- let oldProgress
- api.uploadPublicTeamBackgroundFile.mockImplementation((_token, _file, onProgress) => {
- oldProgress = onProgress
- return uploadRequest.promise
- })
- api.getPublicUploadSession.mockReturnValue(renewalRequest.promise)
- const item = { id: 'old', file: { name: 'old.pdf', size: 1 }, name: 'old.pdf', progress: 0, status: 'pending', error: '' }
- const vm = createPageVm(api, {
- $route: { query: { token: 'old-link-token' } },
- state: 'ready',
- session: { teamName: 'Old', uploadToken: 'old-upload-token', expiresAt: '2099-01-01 08:00:00' },
- queue: [item],
- code: '123456',
- successCount: 2
- })
- const uploading = vm.uploadOne(item)
- expect(item.status).toBe('uploading')
- vm.$route.query.token = 'new-link-token'
- const renewal = invokeWatcher(vm, '$route.query.token', 'new-link-token', 'old-link-token')
- if (!vm.$componentOptions.watch || !vm.$componentOptions.watch['$route.query.token']) return
- expect(vm).toMatchObject({ state: 'verifying', session: null, queue: [], code: '', successCount: 0 })
- oldProgress({ loaded: 4, total: 5 })
- uploadRequest.resolve({ code: 0, data: {} })
- await uploading
- expect(item).toMatchObject({ status: 'uploading', progress: 0 })
- expect(vm.state).toBe('verifying')
- renewalRequest.resolve({ code: 0, data: { teamName: 'New', uploadToken: 'new-upload-token', expiresAt: '2099-01-01 08:00:00' } })
- await renewal
- expect(vm.state).toBe('ready')
- expect(vm.session.uploadToken).toBe('new-upload-token')
- vm.$componentOptions.beforeDestroy.call(vm)
- })
- test('route renewal cannot resume remaining files from an old batch with the new token', async () => {
- if (!expectPage(source)) return
- const oldFirstUpload = deferred()
- const renewalRequest = deferred()
- api.uploadPublicTeamBackgroundFile
- .mockReturnValueOnce(oldFirstUpload.promise)
- .mockResolvedValue({ code: 0, data: {} })
- api.getPublicUploadSession.mockReturnValue(renewalRequest.promise)
- const vm = createPageVm(api, {
- $route: { query: { token: 'old-link-token' } },
- state: 'ready',
- session: { teamName: 'Old', uploadToken: 'old-upload-token', expiresAt: '2099-01-01 08:00:00' }
- })
- const oldBatch = vm.handleFileChange({
- target: { files: [{ name: 'old-one.pdf', size: 1 }, { name: 'old-two.pdf', size: 1 }] }
- })
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledWith('old-upload-token', expect.objectContaining({ name: 'old-one.pdf' }), expect.any(Function))
- vm.$route.query.token = 'new-link-token'
- const renewal = invokeWatcher(vm, '$route.query.token', 'new-link-token', 'old-link-token')
- renewalRequest.resolve({ code: 0, data: { teamName: 'New', uploadToken: 'new-upload-token', expiresAt: '2099-01-01 08:00:00' } })
- await renewal
- expect(vm.state).toBe('ready')
- expect(vm.session.uploadToken).toBe('new-upload-token')
- oldFirstUpload.resolve({ code: 0, data: {} })
- await oldBatch
- vm.$componentOptions.beforeDestroy.call(vm)
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(vm.queue).toEqual([])
- expect(vm.session.uploadToken).toBe('new-upload-token')
- })
- test('route renewal from ready ignores an old deferred completion', async () => {
- if (!expectPage(source)) return
- const completionRequest = deferred()
- const renewalRequest = deferred()
- api.completePublicUpload.mockReturnValue(completionRequest.promise)
- api.getPublicUploadSession.mockReturnValue(renewalRequest.promise)
- const vm = createPageVm(api, {
- $route: { query: { token: 'old-link-token' } },
- state: 'ready',
- session: { teamName: 'Old', uploadToken: 'old-upload-token', expiresAt: '2099-01-01 08:00:00' },
- queue: [{ id: 'old', status: 'success' }]
- })
- const completing = vm.completeUpload()
- expect(vm.completing).toBe(true)
- vm.$route.query.token = 'new-link-token'
- const renewal = invokeWatcher(vm, '$route.query.token', 'new-link-token', 'old-link-token')
- if (!vm.$componentOptions.watch || !vm.$componentOptions.watch['$route.query.token']) return
- expect(vm).toMatchObject({ state: 'verifying', session: null, queue: [], completing: false })
- completionRequest.resolve({ code: 0 })
- await completing
- expect(vm.state).toBe('verifying')
- expect(vm.successCount).toBe(0)
- renewalRequest.resolve({ code: 0, data: { teamName: 'New', uploadToken: 'new-upload-token', expiresAt: '2099-01-01 08:00:00' } })
- await renewal
- expect(vm.state).toBe('ready')
- expect(vm.session.uploadToken).toBe('new-upload-token')
- vm.$componentOptions.beforeDestroy.call(vm)
- })
- test('shows code entry without a token and accepts exactly six ASCII digits', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- api.verifyPublicUploadCode.mockReturnValue(request.promise)
- const vm = createPageVm(api)
- await vm.$componentOptions.created.call(vm)
- expect(vm.state).toBe('code')
- expect(api.getPublicUploadSession).not.toHaveBeenCalled()
- for (const invalidCode of ['12345', '1234567', '12345a', '123456']) {
- vm.code = invalidCode
- await vm.verifyCode()
- }
- expect(api.verifyPublicUploadCode).not.toHaveBeenCalled()
- expect(vm.$message.error).toHaveBeenCalledTimes(4)
- expect(vm.$message.error).toHaveBeenLastCalledWith('请输入6位数字验证码')
- vm.code = '123456'
- const verifying = vm.verifyCode()
- expect(vm.state).toBe('verifying')
- request.resolve({ code: 0, data: { teamName: 'Alpha', uploadToken: 'verified-token', expiresAt: '2099-01-01T00:00:00Z' } })
- await verifying
- expect(api.verifyPublicUploadCode).toHaveBeenCalledWith('123456')
- expect(vm.state).toBe('ready')
- expect(vm.session.uploadToken).toBe('verified-token')
- vm.$componentOptions.beforeDestroy.call(vm)
- })
- test('ignores a deferred code response after the page becomes expired', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- api.verifyPublicUploadCode.mockReturnValue(request.promise)
- const vm = createPageVm(api, { code: '123456', state: 'code' })
- const verifying = vm.verifyCode()
- vm.expireSession()
- request.resolve({ code: 0, data: { teamName: 'Late', uploadToken: 'late-token', expiresAt: '2099-01-01T00:00:00Z' } })
- await verifying
- expect(vm.state).toBe('expired')
- expect(vm.session).toBeNull()
- })
- test('uses the fixed expiry for a one-second countdown and clears timers', () => {
- if (!expectPage(source)) return
- jest.useFakeTimers()
- jest.setSystemTime(new Date('2026-07-21T10:00:00.000Z'))
- const vm = createPageVm(api)
- vm.activateSession({ teamName: 'Alpha', uploadToken: 'upload-token', expiresAt: '2026-07-21T10:00:02.500Z' })
- expect(vm.remainingSeconds).toBe(2)
- expect(jest.getTimerCount()).toBe(1)
- jest.advanceTimersByTime(1000)
- expect(vm.remainingSeconds).toBe(1)
- jest.advanceTimersByTime(1000)
- expect(vm.remainingSeconds).toBe(0)
- expect(vm.state).toBe('expired')
- expect(jest.getTimerCount()).toBe(0)
- vm.activateSession({ teamName: 'Beta', uploadToken: 'other-token', expiresAt: '2026-07-21T10:01:00.000Z' })
- expect(jest.getTimerCount()).toBe(1)
- vm.$componentOptions.beforeDestroy.call(vm)
- expect(jest.getTimerCount()).toBe(0)
- })
- test('creates the exact queue shape and rejects more than ten files before enqueueing', async () => {
- if (!expectPage(source)) return
- const vm = createPageVm(api, { state: 'ready', session: { uploadToken: 'upload-token' } })
- const files = [{ name: 'one.pdf', size: 1 }, { name: 'two.docx', size: 2 }]
- const now = jest.spyOn(Date, 'now').mockReturnValue(123456)
- expect(vm.createQueueItems(files)).toEqual([
- { id: '123456-0', file: files[0], name: 'one.pdf', progress: 0, status: 'pending', error: '' },
- { id: '123456-1', file: files[1], name: 'two.docx', progress: 0, status: 'pending', error: '' }
- ])
- now.mockRestore()
- await vm.handleFileChange({ target: { files: Array.from({ length: 11 }, (_, index) => ({ name: `${index}.pdf`, size: 1 })) } })
- expect(vm.queue).toEqual([])
- expect(api.uploadPublicTeamBackgroundFile).not.toHaveBeenCalled()
- expect(vm.$message.error).toHaveBeenCalledWith('一次最多选择 10 个文件')
- })
- test('reports progress, retains independent results and retries only the failed item', async () => {
- if (!expectPage(source)) return
- const vm = createPageVm(api, { state: 'ready', session: { uploadToken: 'upload-token' } })
- const success = { id: '1-0', file: { name: 'ok.pdf', size: 1 }, name: 'ok.pdf', progress: 0, status: 'pending', error: '' }
- const failure = { id: '1-1', file: { name: 'retry.docx', size: 1 }, name: 'retry.docx', progress: 0, status: 'pending', error: '' }
- vm.queue = [success, failure]
- api.uploadPublicTeamBackgroundFile
- .mockImplementationOnce((_token, _file, onProgress) => {
- onProgress({ loaded: 1, total: 2 })
- return Promise.resolve({ code: 0, data: {} })
- })
- .mockRejectedValueOnce({ msg: '网络失败' })
- await vm.uploadOne(success)
- await vm.uploadOne(failure)
- expect(success).toMatchObject({ status: 'success', progress: 100, error: '' })
- expect(failure).toMatchObject({ status: 'failed', progress: 0, error: '网络失败' })
- expect(vm.queue).toEqual([success, failure])
- expect(api.uploadPublicTeamBackgroundFile.mock.calls[0]).toEqual([
- 'upload-token',
- success.file,
- expect.any(Function)
- ])
- api.uploadPublicTeamBackgroundFile.mockImplementationOnce((_token, _file, onProgress) => {
- onProgress({ loaded: 3, total: 4 })
- expect(failure.progress).toBe(75)
- return Promise.resolve({ code: 0, data: {} })
- })
- await vm.retryUpload(failure)
- expect(success).toMatchObject({ status: 'success', progress: 100 })
- expect(failure).toMatchObject({ status: 'success', progress: 100, error: '' })
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenLastCalledWith('upload-token', failure.file, expect.any(Function))
- })
- test('rejects overlapping batches, retry and completion until the active upload settles', async () => {
- if (!expectPage(source)) return
- const activeRequest = deferred()
- api.uploadPublicTeamBackgroundFile
- .mockReturnValueOnce(activeRequest.promise)
- .mockResolvedValue({ code: 0, data: {} })
- api.completePublicUpload.mockResolvedValue({ code: 0 })
- const failed = { id: 'failed', file: { name: 'retry.pdf', size: 1 }, name: 'retry.pdf', progress: 0, status: 'failed', error: '网络失败' }
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' },
- queue: [failed]
- })
- const firstEvent = { target: { value: 'first', files: [{ name: 'first.pdf', size: 1 }] } }
- const firstBatch = vm.handleFileChange(firstEvent)
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(vm.queue[1]).toMatchObject({ name: 'first.pdf', status: 'uploading' })
- expect(computed(vm, 'uploadDisabled')).toBe(true)
- const secondEvent = { target: { value: 'second', files: [{ name: 'second.pdf', size: 1 }] } }
- await vm.handleFileChange(secondEvent)
- await vm.retryUpload(failed)
- await vm.completeUpload()
- expect(secondEvent.target.value).toBe('')
- expect(vm.queue).toHaveLength(2)
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(api.completePublicUpload).not.toHaveBeenCalled()
- expect(failed.status).toBe('failed')
- expect(vm.$message.error).toHaveBeenCalledWith('文件正在上传,请稍后再试')
- activeRequest.resolve({ code: 0, data: {} })
- await firstBatch
- expect(computed(vm, 'uploadDisabled')).toBe(false)
- await vm.retryUpload(failed)
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(2)
- expect(failed).toMatchObject({ status: 'success', progress: 100, error: '' })
- })
- test('does not upload a pending batch item removed while an earlier item is active', async () => {
- if (!expectPage(source)) return
- const firstRequest = deferred()
- api.uploadPublicTeamBackgroundFile
- .mockReturnValueOnce(firstRequest.promise)
- .mockResolvedValue({ code: 0, data: {} })
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' }
- })
- const batch = vm.handleFileChange({
- target: { files: [{ name: 'first.pdf', size: 1 }, { name: 'removed.pdf', size: 1 }] }
- })
- const removedItem = vm.queue[1]
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(removedItem).toMatchObject({ name: 'removed.pdf', status: 'pending' })
- vm.removeQueueItem(removedItem)
- expect(vm.queue).not.toContain(removedItem)
- firstRequest.resolve({ code: 0, data: {} })
- await batch
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(vm.queue).not.toContain(removedItem)
- })
- test('a session-level upload failure expires the page and stops later files in the batch', async () => {
- if (!expectPage(source)) return
- api.uploadPublicTeamBackgroundFile.mockRejectedValue({ msg: '上传会话已过期' })
- const vm = createPageVm(api, { state: 'ready', session: { uploadToken: 'upload-token' } })
- const files = [{ name: 'one.pdf', size: 1 }, { name: 'two.pdf', size: 1 }]
- await vm.handleFileChange({ target: { files } })
- expect(api.uploadPublicTeamBackgroundFile).toHaveBeenCalledTimes(1)
- expect(vm.queue[0]).toMatchObject({ status: 'failed', error: '上传会话已过期' })
- expect(vm.queue[1]).toMatchObject({ status: 'pending' })
- expect(vm.state).toBe('expired')
- })
- test('late upload progress and success cannot overwrite expiry', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- let onProgress
- api.uploadPublicTeamBackgroundFile.mockImplementation((_token, _file, callback) => {
- onProgress = callback
- return request.promise
- })
- const vm = createPageVm(api, { state: 'ready', session: { uploadToken: 'upload-token' } })
- const item = { id: '1-0', file: { name: 'slow.pdf', size: 1 }, name: 'slow.pdf', progress: 0, status: 'pending', error: '' }
- const uploading = vm.uploadOne(item)
- onProgress({ loaded: 1, total: 5 })
- expect(item.progress).toBe(20)
- vm.expireSession()
- onProgress({ loaded: 4, total: 5 })
- request.resolve({ code: 0, data: {} })
- await uploading
- expect(vm.state).toBe('expired')
- expect(item).toMatchObject({ status: 'uploading', progress: 20 })
- })
- test('completion uses only the upload token, counts successes and freezes further uploads', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- api.completePublicUpload.mockReturnValue(request.promise)
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' },
- queue: [
- { id: '1', status: 'success' },
- { id: '2', status: 'failed' },
- { id: '3', status: 'success' }
- ]
- })
- vm.uploadOne = jest.fn()
- expect(computed(vm, 'uploadDisabled')).toBe(false)
- const completing = vm.completeUpload()
- expect(vm.completing).toBe(true)
- expect(computed(vm, 'uploadDisabled')).toBe(true)
- request.resolve({ code: 0 })
- await completing
- expect(api.completePublicUpload).toHaveBeenCalledWith('upload-token')
- expect(vm.successCount).toBe(2)
- expect(vm.state).toBe('completed')
- expect(computed(vm, 'uploadDisabled')).toBe(true)
- await vm.handleFileChange({ target: { files: [{ name: 'late.pdf', size: 1 }] } })
- await vm.retryUpload(vm.queue[1])
- await vm.completeUpload()
- expect(vm.uploadOne).not.toHaveBeenCalled()
- expect(api.completePublicUpload).toHaveBeenCalledTimes(1)
- })
- test('a successful upload cannot be removed or omitted from the completion count', async () => {
- if (!expectPage(source)) return
- const successful = { id: 'success', status: 'success' }
- const failed = { id: 'failed', status: 'failed' }
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' },
- queue: [successful, failed]
- })
- api.completePublicUpload.mockResolvedValue({ code: 0 })
- vm.removeQueueItem(successful)
- expect(vm.queue).toContain(successful)
- await vm.completeUpload()
- expect(vm.successCount).toBe(1)
- expect(readIfPresent(queuePath)).toContain(
- 'v-if="item.status !== \'uploading\' && item.status !== \'success\'"'
- )
- })
- test('completion closes empty and all-failed queues with a zero success count', async () => {
- if (!expectPage(source)) return
- api.completePublicUpload.mockResolvedValue({ code: 0 })
- for (const queue of [[], [{ id: 'failed', status: 'failed' }]]) {
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' },
- queue
- })
- expect(computed({ ...vm, uploadDisabled: false }, 'completeDisabled')).toBe(false)
- await vm.completeUpload()
- expect(vm.state).toBe('completed')
- expect(vm.successCount).toBe(0)
- }
- expect(api.completePublicUpload).toHaveBeenCalledTimes(2)
- expect(api.completePublicUpload).toHaveBeenNthCalledWith(1, 'upload-token')
- expect(api.completePublicUpload).toHaveBeenNthCalledWith(2, 'upload-token')
- })
- test('late completion cannot overwrite timer expiry', async () => {
- if (!expectPage(source)) return
- const request = deferred()
- api.completePublicUpload.mockReturnValue(request.promise)
- const vm = createPageVm(api, {
- state: 'ready',
- session: { uploadToken: 'upload-token' },
- queue: [{ id: '1', status: 'success' }]
- })
- const completing = vm.completeUpload()
- vm.expireSession()
- request.resolve({ code: 0 })
- await completing
- expect(vm.state).toBe('expired')
- expect(vm.successCount).toBe(0)
- })
- test('ready and terminal templates expose only the approved public information', () => {
- if (!expectPage(source)) return
- const template = parseComponent(source).template.content
- const sessionFields = Array.from(template.matchAll(/session\.([A-Za-z_$][\w$]*)/g), match => match[1])
- expect(source).toContain('session.teamName')
- expect(source).toContain('session.expiresAt')
- expect(source).toContain('remainingSeconds')
- expect(source).toContain('TeamBackgroundUploadQueue')
- expect(source).toContain('请返回小程序或管理端重新生成上传链接')
- expect(source).toMatch(/state === 'expired'[\s\S]*请返回小程序或管理端重新生成上传链接/)
- expect(source).toMatch(/state === 'completed'[\s\S]*successCount/)
- expect(source).not.toMatch(/teamDescription|teamMembers|coachName|existingFiles|fileList|listTeamBackgroundFiles/)
- expect(new Set(sessionFields)).toEqual(new Set(['teamName', 'expiresAt']))
- expect(template).not.toMatch(/{{\s*session\s*}}/)
- })
- })
|