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 queuePath = path.resolve(__dirname, '../../src/components/team-background/TeamBackgroundUploadQueue.vue')
const dialogPath = path.resolve(__dirname, '../../src/components/team-background/TeamBackgroundFileDialog.vue')
const managerPath = path.resolve(__dirname, '../../src/views/modules/wechatUser/teamManager.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 loadVueOptions(file, mocks = {}) {
const source = readIfPresent(file)
const descriptor = parseComponent(source)
const code = babel.transformSync(descriptor.script.content, {
babelrc: false,
configFile: false,
plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')]
}).code
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.default
}
function expectComponent(source) {
expect(source).not.toBe('')
return source !== ''
}
describe('team background upload queue contract', () => {
const source = readIfPresent(queuePath)
test('accepts queue items and renders every required status without mutating the list', () => {
if (!expectComponent(source)) return
const options = loadVueOptions(queuePath)
expect(options.props.files).toBeDefined()
for (const state of ['pending', 'uploading', 'success', 'failed']) {
expect(source).toContain(state)
}
expect(source).not.toMatch(/files\.(?:splice|pop|shift)|files\s*=|filter\([^)]*status/)
})
test('exposes explicit retry and remove actions', () => {
if (!expectComponent(source)) return
expect(source).toMatch(/\$emit\(['"]retry['"],\s*item\)/)
expect(source).toMatch(/\$emit\(['"]remove['"],\s*item\)/)
})
})
describe('team background management dialog behavior', () => {
const source = readIfPresent(dialogPath)
const api = {
createTeamBackgroundSession: jest.fn(),
disableTeamBackgroundFile: jest.fn(),
downloadTeamBackgroundFile: jest.fn(),
listTeamBackgroundFiles: jest.fn(),
uploadTeamBackgroundFile: jest.fn()
}
const saveAs = jest.fn()
function createVm(overrides = {}) {
const options = loadVueOptions(dialogPath, {
'@/api/teamBackgroundFile': api,
'@/utils/teamBackgroundFile': { ACCEPT, formatFileSize, validateBackgroundFile },
'file-saver': { saveAs },
'./TeamBackgroundUploadQueue.vue': {}
})
const vm = {
...options.data(),
visible: true,
teamId: 7,
teamName: 'Alpha',
$emit: jest.fn(),
$message: { success: jest.fn(), error: jest.fn() },
$confirm: jest.fn().mockResolvedValue(),
$refs: { fileInput: { value: 'selected' } },
...options.methods,
...overrides
}
vm.$componentWatch = options.watch || {}
return vm
}
function invokeWatcher(vm, name, value, oldValue) {
const watcher = vm.$componentWatch[name]
if (!watcher) return
const handler = typeof watcher === 'function' ? watcher : watcher.handler
return handler.call(vm, value, oldValue)
}
beforeEach(() => {
Object.values(api).forEach(mock => mock.mockReset())
saveAs.mockReset()
})
test('creates the exact queue item shape and rejects more than ten files before enqueueing', async () => {
if (!expectComponent(source)) return
const vm = createVm()
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()
vm.uploadOne = jest.fn()
await vm.handleFileChange({ target: { files: Array.from({ length: 11 }, (_, index) => ({ name: `${index}.pdf`, size: 1 })) } })
expect(vm.queue).toEqual([])
expect(vm.uploadOne).not.toHaveBeenCalled()
expect(vm.$message.error).toHaveBeenCalledWith(expect.stringContaining('10'))
})
test('validates before upload and retains a successful item when another upload fails', async () => {
if (!expectComponent(source)) return
const vm = createVm()
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: 'bad.docx', size: 1 }, name: 'bad.docx', progress: 0, status: 'pending', error: '' }
const invalid = { id: '1-2', file: { name: 'bad.zip', size: 1 }, name: 'bad.zip', progress: 0, status: 'pending', error: '' }
vm.queue = [success, failure, invalid]
vm.refreshFiles = jest.fn().mockResolvedValue()
api.uploadTeamBackgroundFile
.mockResolvedValueOnce({ code: 0, data: {} })
.mockRejectedValueOnce({ msg: '网络失败' })
await vm.uploadOne(success)
await vm.uploadOne(failure)
await vm.uploadOne(invalid)
expect(success).toMatchObject({ status: 'success', progress: 100, error: '' })
expect(failure).toMatchObject({ status: 'failed', error: '网络失败' })
expect(invalid).toMatchObject({ status: 'failed', error: expect.stringContaining('文件类型') })
expect(vm.queue).toEqual([success, failure, invalid])
expect(api.uploadTeamBackgroundFile).toHaveBeenCalledTimes(2)
})
test('uses a multiple accept-filtered input and keeps the ten-file batch cap in source', () => {
if (!expectComponent(source)) return
expect(source).toMatch(/]+type="file"[^>]+multiple[^>]+:accept="ACCEPT"/s)
expect(source).toMatch(/selectedFiles\.length\s*>\s*10/)
})
test('refreshes active files and emits their count', async () => {
if (!expectComponent(source)) return
const rows = [{ id: 1 }, { id: 2 }]
api.listTeamBackgroundFiles.mockResolvedValue({ code: 0, data: rows })
const vm = createVm()
await vm.refreshFiles()
expect(vm.fileList).toBe(rows)
expect(vm.$emit).toHaveBeenCalledWith('changed', 2)
})
test('downloads only the authorized blob and disables through the team API', async () => {
if (!expectComponent(source)) return
const blob = { type: 'application/pdf' }
api.downloadTeamBackgroundFile.mockResolvedValue(blob)
api.disableTeamBackgroundFile.mockResolvedValue({ code: 0 })
const vm = createVm()
vm.refreshFiles = jest.fn().mockResolvedValue()
const row = { id: 12, fileName: 'brief.pdf', url: 'https://untrusted.example/file' }
await vm.downloadFile(row)
await vm.disableFile(row)
expect(api.downloadTeamBackgroundFile).toHaveBeenCalledWith(7, 12)
expect(saveAs).toHaveBeenCalledWith(blob, 'brief.pdf')
expect(api.disableTeamBackgroundFile).toHaveBeenCalledWith(7, 12)
expect(vm.refreshFiles).toHaveBeenCalledTimes(1)
})
test('generates and displays a copyable, refreshable PC upload session', async () => {
if (!expectComponent(source)) return
const session = { code: '123456', uploadUrl: 'https://upload.example/token', expiresAt: '2026-07-21 18:00:00' }
api.createTeamBackgroundSession.mockResolvedValue({ code: 0, data: session })
const vm = createVm()
await vm.createUploadSession()
expect(vm.session).toBe(session)
for (const field of ['session.code', 'session.expiresAt', 'session.uploadUrl', 'copyUploadUrl', 'refreshFiles']) {
expect(source).toContain(field)
}
})
test('visible false and closed invalidate pending work and reset both loading flags immediately', () => {
if (!expectComponent(source)) return
api.listTeamBackgroundFiles.mockReturnValue(new Promise(() => {}))
api.createTeamBackgroundSession.mockReturnValue(new Promise(() => {}))
const vm = createVm()
vm.refreshFiles()
vm.createUploadSession()
expect(vm.loading).toBe(true)
expect(vm.sessionLoading).toBe(true)
vm.visible = false
invokeWatcher(vm, 'visible', false, true)
expect(vm.loading).toBe(false)
expect(vm.sessionLoading).toBe(false)
vm.loading = true
vm.sessionLoading = true
vm.handleClosed()
expect(vm.loading).toBe(false)
expect(vm.sessionLoading).toBe(false)
})
test('ignores late team-A list and session responses after close and reopen for team B', async () => {
if (!expectComponent(source)) return
const teamAList = deferred()
const teamBList = deferred()
const teamASession = deferred()
const teamBSession = deferred()
api.listTeamBackgroundFiles
.mockReturnValueOnce(teamAList.promise)
.mockReturnValueOnce(teamBList.promise)
api.createTeamBackgroundSession
.mockReturnValueOnce(teamASession.promise)
.mockReturnValueOnce(teamBSession.promise)
const vm = createVm({ teamId: 7 })
const listA = vm.handleOpen()
const sessionA = vm.createUploadSession()
vm.visible = false
invokeWatcher(vm, 'visible', false, true)
vm.teamId = 8
invokeWatcher(vm, 'teamId', 8, 7)
vm.visible = true
const listB = vm.handleOpen()
const sessionB = vm.createUploadSession()
const filesB = [{ id: 81 }, { id: 82 }]
const currentSession = { code: '888888', uploadUrl: 'https://upload.example/b', expiresAt: 'later' }
teamBList.resolve({ code: 0, data: filesB })
teamBSession.resolve({ code: 0, data: currentSession })
await Promise.all([listB, sessionB])
teamAList.resolve({ code: 0, data: [{ id: 71 }] })
teamASession.resolve({ code: 0, data: { code: '777777', uploadUrl: 'https://upload.example/a', expiresAt: 'old' } })
await Promise.all([listA, sessionA])
expect(api.listTeamBackgroundFiles.mock.calls).toEqual([[7], [8]])
expect(api.createTeamBackgroundSession.mock.calls).toEqual([[7], [8]])
expect(vm.fileList).toBe(filesB)
expect(vm.session).toBe(currentSession)
expect(vm.$emit.mock.calls.filter(call => call[0] === 'changed')).toEqual([['changed', 2]])
expect(vm.loading).toBe(false)
expect(vm.sessionLoading).toBe(false)
})
test('only the newest same-team list and session requests may update state', async () => {
if (!expectComponent(source)) return
const firstList = deferred()
const secondList = deferred()
const firstSession = deferred()
const secondSession = deferred()
api.listTeamBackgroundFiles
.mockReturnValueOnce(firstList.promise)
.mockReturnValueOnce(secondList.promise)
api.createTeamBackgroundSession
.mockReturnValueOnce(firstSession.promise)
.mockReturnValueOnce(secondSession.promise)
const vm = createVm()
const listOne = vm.refreshFiles()
const listTwo = vm.refreshFiles()
const sessionOne = vm.createUploadSession()
const sessionTwo = vm.createUploadSession()
const newestFiles = [{ id: 2 }, { id: 3 }]
const newestSession = { code: '222222', uploadUrl: 'https://upload.example/new', expiresAt: 'new' }
secondList.resolve({ code: 0, data: newestFiles })
secondSession.resolve({ code: 0, data: newestSession })
await Promise.all([listTwo, sessionTwo])
firstList.resolve({ code: 0, data: [{ id: 1 }] })
firstSession.resolve({ code: 0, data: { code: '111111', uploadUrl: 'https://upload.example/old', expiresAt: 'old' } })
await Promise.all([listOne, sessionOne])
expect(vm.fileList).toBe(newestFiles)
expect(vm.session).toBe(newestSession)
expect(vm.$emit.mock.calls.filter(call => call[0] === 'changed')).toEqual([['changed', 2]])
expect(vm.loading).toBe(false)
expect(vm.sessionLoading).toBe(false)
})
test('an old disable confirmation cannot act on a new team context', async () => {
if (!expectComponent(source)) return
const confirmation = deferred()
const vm = createVm({ teamId: 7, $confirm: jest.fn(() => confirmation.promise) })
const disabling = vm.disableFile({ id: 12, fileName: 'old.pdf' })
vm.visible = false
invokeWatcher(vm, 'visible', false, true)
vm.teamId = 8
invokeWatcher(vm, 'teamId', 8, 7)
confirmation.resolve()
await disabling
expect(api.disableTeamBackgroundFile).not.toHaveBeenCalled()
})
test('reports a JSON blob download error and never saves it as a file', async () => {
if (!expectComponent(source)) return
const blob = {
type: 'application/json;charset=UTF-8',
text: jest.fn().mockResolvedValue(JSON.stringify({ code: 500, msg: '文件已停用' }))
}
api.downloadTeamBackgroundFile.mockResolvedValue(blob)
const vm = createVm()
await vm.downloadFile({ id: 12, fileName: 'brief.pdf' })
expect(blob.text).toHaveBeenCalled()
expect(saveAs).not.toHaveBeenCalled()
expect(vm.$message.error).toHaveBeenCalledWith('文件已停用')
})
test('shows complete file metadata and user-facing WECHAT/PC labels', () => {
if (!expectComponent(source)) return
for (const field of ['fileName', 'fileExt', 'fileSize', 'source', 'createDate']) {
expect(source).toContain(field)
}
expect(source).toContain("WECHAT: '微信端'")
expect(source).toContain("PC: '电脑端'")
expect(source).toContain('formatFileSize')
})
})
describe('team table integration contract', () => {
const source = readIfPresent(managerPath)
test('places enterprise website immediately after company and renders the document count link', () => {
expect(source).toContain('label="企业官网" prop="enterpriseWebsite"')
const company = source.indexOf('label="所在公司"')
const website = source.indexOf('label="企业官网"')
const region = source.indexOf('label="所属地区"')
const nextLabel = source.slice(company + 1).match(/label="([^"]+)"/)
expect(company).toBeGreaterThan(-1)
expect(website).toBeGreaterThan(company)
expect(region).toBeGreaterThan(website)
expect(nextLabel[1]).toBe('企业官网')
expect(source).toContain("row.enterpriseWebsite || '无'")
expect(source).toContain('row.backgroundFileCount || 0')
expect(source).toContain('@click="openBackgroundFiles(row)"')
expect(source).toMatch(/]+type="text"[^>]+@click="openBackgroundFiles\(row\)"/s)
expect(source).not.toMatch(/]+@click="openBackgroundFiles\(row\)"/s)
})
test('mounts one dialog and updates only the matching row on changed(count)', () => {
const mounts = source.match(/