|
@@ -0,0 +1,229 @@
|
|
|
|
|
+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 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
|
|
|
|
|
+ }
|
|
|
|
|
+ return vm
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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(/<input[^>]+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('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)"')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ test('mounts one dialog and updates only the matching row on changed(count)', () => {
|
|
|
|
|
+ const mounts = source.match(/<team-background-file-dialog\b/g) || []
|
|
|
|
|
+ expect(mounts).toHaveLength(1)
|
|
|
|
|
+ expect(source).toContain('@changed="handleBackgroundFilesChanged"')
|
|
|
|
|
+ expect(source).toMatch(/dataList\.value\.find\([^)]*activeTeam\.value\.id/)
|
|
|
|
|
+ expect(source).toMatch(/row\.backgroundFileCount\s*=\s*count/)
|
|
|
|
|
+ expect(source).not.toMatch(/handleBackgroundFilesChanged[\s\S]{0,180}getList\(/)
|
|
|
|
|
+ })
|
|
|
|
|
+})
|