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*}}/) }) })