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(/