const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { pathToFileURL } = require('node:url') const vm = require('node:vm') const rules = require('../utils/teamBackgroundFile') assert.equal(rules.validateEnterpriseWebsite(' 无 '), '') assert.equal(rules.validateEnterpriseWebsite('https://example.com'), '') assert.match(rules.validateEnterpriseWebsite('ftp://example.com'), /http/) assert.equal(rules.validateBackgroundFile({ name: 'brief.PDF', size: 1 }), '') assert.match(rules.validateBackgroundFile({ name: 'brief.zip', size: 1 }), /类型/) assert.match(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 + 1 }), /50MB/) assert.equal(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 }), '') assert.equal(rules.validateBackgroundFile({ name: `${'a'.repeat(251)}.pdf`, size: 1 }), '') assert.match(rules.validateBackgroundFile({ name: `${'a'.repeat(252)}.pdf`, size: 1 }), /255/) assert.match(rules.validateBackgroundFile({ name: 'empty.pdf', size: 0 }), /非空/) const backendExpiry = '2026-07-21 15:30:00' const explicitOffsetExpiry = '2026-07-21T15:30:00+08:00' const utcExpiry = '2026-07-21T07:30:00.250Z' assert.equal(rules.parseSessionExpiresAt(backendExpiry), Date.UTC(2026, 6, 21, 7, 30, 0)) assert.equal(rules.parseSessionExpiresAt(explicitOffsetExpiry), Date.UTC(2026, 6, 21, 7, 30, 0)) assert.equal(rules.parseSessionExpiresAt(utcExpiry), Date.UTC(2026, 6, 21, 7, 30, 0, 250)) assert.equal(rules.getSessionRemainingSeconds(backendExpiry, Date.UTC(2026, 6, 21, 7, 29, 59, 1)), 1) assert.equal(rules.getSessionRemainingSeconds(backendExpiry, Date.UTC(2026, 6, 21, 7, 30, 1)), 0) assert.equal(rules.getSessionRemainingSeconds('2026-07-21T15:30:00', 0), 0) assert.equal(rules.getSessionRemainingSeconds('not-a-date', 0), 0) const validQueueItem = rules.createBackgroundQueueItem( { name: 'brief.pdf', path: '/tmp/brief.pdf', size: 1 }, 'valid-file' ) const invalidQueueItem = rules.createBackgroundQueueItem( { name: 'archive.zip', path: '/tmp/archive.zip', size: 1 }, 'invalid-file' ) assert.equal(validQueueItem.status, 'pending') assert.equal(validQueueItem.progress, 0) assert.equal(validQueueItem.validationError, '') assert.equal(invalidQueueItem.status, 'failed') assert.match(invalidQueueItem.validationError, /类型/) validQueueItem.status = 'success' const uploadFailure = { id: 'upload-failure', status: 'failed', validationError: '', error: '上传失败' } assert.deepEqual( rules.getUploadableBackgroundFiles([validQueueItem, invalidQueueItem, uploadFailure]), [uploadFailure] ) const originalTeam = { id: 12, teamName: '研发团队', enterpriseWebsite: ' https://example.com/team ', coachId: 3, uploaderId: 4, source: 'WECHAT' } const teamSubmitPayload = rules.createTeamSubmitPayload(originalTeam) assert.equal(teamSubmitPayload.id, 12) assert.equal(teamSubmitPayload.enterpriseWebsite, 'https://example.com/team') assert.equal(Object.hasOwn(teamSubmitPayload, 'coachId'), false) assert.equal(Object.hasOwn(teamSubmitPayload, 'uploaderId'), false) assert.equal(Object.hasOwn(teamSubmitPayload, 'source'), false) assert.equal(originalTeam.coachId, 3, 'payload creation must not mutate the displayed detail') const textPreviewState = rules.createTextPreviewState({ mode: 'text', fileName: '背景说明.md', content: '# 团队背景', truncated: true }, 'fallback.md') assert.deepEqual(textPreviewState, { visible: true, fileName: '背景说明.md', content: '# 团队背景', truncated: true }) assert.deepEqual(rules.createEmptyTextPreviewState(), { visible: false, fileName: '', content: '', truncated: false }) assert.notEqual(rules.createEmptyTextPreviewState(), rules.createEmptyTextPreviewState()) const creationState = rules.createTeamCreationState() assert.deepEqual(creationState, { createdTeamId: '', submitting: false, stage: 'idle', completed: false }) assert.equal(rules.beginTeamSubmission(creationState), true) assert.equal(rules.beginTeamSubmission(creationState), false, 'a second submission must be locked') assert.equal(rules.rememberCreatedTeam(creationState, 88), 88) assert.equal(rules.rememberCreatedTeam(creationState, 99), 88, 'the first server team id must be retained') rules.setTeamSubmissionStage(creationState, 'upload') assert.equal(creationState.stage, 'upload') rules.finishTeamSubmission(creationState) assert.equal(creationState.submitting, false) assert.equal(rules.beginTeamSubmission(creationState), true, 'a failed later stage can resume') rules.finishTeamSubmission(creationState, true) assert.equal(creationState.completed, true) assert.equal(rules.beginTeamSubmission(creationState), false, 'a completed flow cannot be scheduled twice') assert.equal(rules.hasUnresolvedBackgroundFiles([ { status: 'success', validationError: '' }, { status: 'failed', validationError: '文件类型不支持' } ]), false) for (const item of [ { status: 'pending', validationError: '' }, { status: 'uploading', validationError: '' }, { status: 'failed', validationError: '' } ]) { assert.equal(rules.hasUnresolvedBackgroundFiles([item]), true) } console.log('team background rules: PASS') const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js') async function loadTransport({ api = {}, uni = {}, wx = {} } = {}) { const source = fs.readFileSync(transportPath, 'utf8') const context = vm.createContext({ uni, wx }) const baseApiModule = new vm.SyntheticModule(['BaseApi'], function () { this.setExport('BaseApi', 'https://api.example.test/app') }, { context, identifier: 'test:baseApi' }) const apiModule = new vm.SyntheticModule(['default'], function () { this.setExport('default', api) }, { context, identifier: 'test:api' }) const transportModule = new vm.SourceTextModule(source, { context, identifier: pathToFileURL(transportPath).href }) await transportModule.link(specifier => { if (specifier === './baseApi.js') return baseApiModule if (specifier === './index.js') return apiModule throw new Error(`unexpected transport import: ${specifier}`) }) await transportModule.evaluate() return transportModule.namespace } function assertNoIdentityFields(value) { if (!value || typeof value !== 'object') return for (const [key, child] of Object.entries(value)) { assert.ok(!['source', 'coachId', 'uploaderId'].includes(key), `unexpected identity field: ${key}`) assertNoIdentityFields(child) } } async function createUploadHarness(response, progress = 37) { let request const progressValues = [] const uni = { getStorageSync(key) { assert.equal(key, 'token') return 'token-123' }, uploadFile(options) { request = options return { onProgressUpdate(callback) { callback({ progress }) setImmediate(() => options.success(response)) } } } } const transport = await loadTransport({ uni }) return { request: () => request, progressValues, promise: transport.uploadTeamBackgroundFile(12, '/tmp/brief.pdf', value => progressValues.push(value)) } } async function testUploadTransport() { const stringHarness = await createUploadHarness({ statusCode: 200, data: JSON.stringify({ code: 0, data: { id: 7 } }) }) assert.equal((await stringHarness.promise).id, 7) assert.equal(stringHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files') assert.equal(stringHarness.request().filePath, '/tmp/brief.pdf') assert.equal(stringHarness.request().name, 'file') assert.equal(stringHarness.request().header.token, 'token-123') assert.deepEqual(stringHarness.progressValues, [37]) assertNoIdentityFields(stringHarness.request()) const objectHarness = await createUploadHarness({ statusCode: 200, data: { code: 0, data: { id: 8 } } }) assert.equal((await objectHarness.promise).id, 8) const statusHarness = await createUploadHarness({ statusCode: 500, data: { code: 0 } }) await assert.rejects(statusHarness.promise, /上传失败,请重试/) const codeHarness = await createUploadHarness({ statusCode: 200, data: { code: 9, msg: '上传业务失败' } }) await assert.rejects(codeHarness.promise, /上传业务失败/) const invalidJsonHarness = await createUploadHarness({ statusCode: 200, data: '' }) await assert.rejects(invalidJsonHarness.promise, /上传失败,请重试/) } async function testApiTransport() { const calls = [] const api = { get(...args) { calls.push(['get', ...args]) return Promise.resolve({ data: { code: 0, data: [{ id: 1 }] } }) }, del(...args) { calls.push(['del', ...args]) return Promise.resolve({ data: { code: 0, data: true } }) }, post(...args) { calls.push(['post', ...args]) return Promise.resolve({ data: { code: 0, data: { sessionId: 'session-1' } } }) } } const transport = await loadTransport({ api }) assert.deepEqual(await transport.listTeamBackgroundFiles(12), [{ id: 1 }]) assert.equal(await transport.disableTeamBackgroundFile(12, 34), true) assert.deepEqual(await transport.createTeamBackgroundSession(12), { sessionId: 'session-1' }) assert.equal(calls[0][0], 'get') assert.equal(calls[0][1], '/core/user/team/12/background-files') assert.equal(calls[1][0], 'del') assert.equal(calls[1][1], '/core/user/team/12/background-files/34') assert.equal(calls[2][0], 'post') assert.equal(calls[2][1], '/core/user/team/12/background-upload-session') for (const call of calls) { assert.equal(Object.keys(call[2]).length, 0) assert.equal(call[3], false) assertNoIdentityFields(call) } const failingTransport = await loadTransport({ api: { get: () => Promise.resolve({ data: { code: 3, msg: '列表业务失败' } }) } }) await assert.rejects(failingTransport.listTeamBackgroundFiles(12), /列表业务失败/) assert.equal(transport.uploadTeamBackgroundFile.length, 2) assert.equal(transport.listTeamBackgroundFiles.length, 1) assert.equal(transport.disableTeamBackgroundFile.length, 2) assert.equal(transport.createTeamBackgroundSession.length, 1) assert.equal(transport.downloadTeamBackgroundFile.length, 2) } const TEXT_PREVIEW_LIMIT = 256 * 1024 async function createDownloadHarness({ result, responseHeaders, downloadError, openError, fileSize, fileContent = '', fileInfoError, readError, withHeadersListener = true } = {}) { let request let openRequest let fileInfoRequest let readRequest let fileSystemManagerCalls = 0 const resolvedFileSize = fileSize === undefined ? Buffer.byteLength(fileContent, 'utf8') : fileSize const uni = { getStorageSync(key) { assert.equal(key, 'token') return 'token-456' }, downloadFile(options) { request = options setImmediate(() => { if (downloadError) options.fail(downloadError) else options.success(result) }) if (!withHeadersListener) return {} return { onHeadersReceived(callback) { if (responseHeaders) callback({ header: responseHeaders }) } } } } const wx = { openDocument(options) { openRequest = options setImmediate(() => { if (openError) options.fail(openError) else options.success('opened') }) }, getFileSystemManager() { fileSystemManagerCalls += 1 return { getFileInfo(options) { fileInfoRequest = options setImmediate(() => { if (fileInfoError) options.fail(fileInfoError) else options.success({ size: resolvedFileSize }) }) }, readFile(options) { readRequest = options setImmediate(() => { if (readError) options.fail(readError) else options.success({ data: fileContent }) }) } } } } const transport = await loadTransport({ uni, wx }) return { request: () => request, openRequest: () => openRequest, fileInfoRequest: () => fileInfoRequest, readRequest: () => readRequest, fileSystemManagerCalls: () => fileSystemManagerCalls, promise: transport.downloadTeamBackgroundFile(12, 34) } } async function testDownloadTransport() { const successHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' }, responseHeaders: { 'cOnTeNt-TyPe': 'application/pdf', 'CONTENT-disPOSITION': 'attachment; filename="download.pdf"' } }) const documentPreview = await successHarness.promise assert.equal(documentPreview.mode, 'document') assert.equal(documentPreview.fileName, 'download.pdf') assert.equal(documentPreview.filePath, '/tmp/download.pdf') assert.equal(successHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files/34/download') assert.equal(successHarness.request().header.token, 'token-456') assert.equal(successHarness.openRequest(), undefined, 'transport must not open a document before component context checks') assertNoIdentityFields(successHarness.request()) const resultHeaderHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/result-header.docx', header: { 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'content-disposition': 'attachment; filename="result-header.docx"' } }, withHeadersListener: false }) const resultHeaderPreview = await resultHeaderHarness.promise assert.equal(resultHeaderPreview.mode, 'document') assert.equal(resultHeaderPreview.fileName, 'result-header.docx') assert.equal(resultHeaderPreview.filePath, '/tmp/result-header.docx') assert.equal(resultHeaderHarness.openRequest(), undefined) assert.equal(resultHeaderHarness.fileSystemManagerCalls(), 0) const txtHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/text-preview.txt', headers: { 'Content-Type': 'text/plain; charset=UTF-8', 'Content-Disposition': 'attachment; filename="fallback.txt"; ' + "filename*=UTF-8''%E8%83%8C%E6%99%AF%20%E8%B5%84%E6%96%99.txt" } }, fileSize: 5, fileContent: 'hello', withHeadersListener: false }) const txtPreview = await txtHarness.promise assert.equal(txtHarness.openRequest(), undefined, 'txt must not call openDocument') assert.equal(txtPreview.mode, 'text') assert.equal(txtPreview.fileName, '背景 资料.txt') assert.equal(txtPreview.content, 'hello') assert.equal(txtPreview.truncated, false) assert.equal(txtHarness.fileInfoRequest().filePath, '/tmp/text-preview.txt') assert.equal(txtHarness.readRequest().filePath, '/tmp/text-preview.txt') assert.equal(txtHarness.readRequest().encoding, 'utf8') assert.equal(txtHarness.readRequest().position, 0) assert.equal(txtHarness.readRequest().length, 5) const emptyTextHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/empty.txt' }, responseHeaders: { 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="empty.txt"' }, fileSize: 0 }) const emptyTextPreview = await emptyTextHarness.promise assert.equal(emptyTextPreview.mode, 'text') assert.equal(emptyTextPreview.fileName, 'empty.txt') assert.equal(emptyTextPreview.content, '') assert.equal(emptyTextPreview.truncated, false) assert.equal(emptyTextHarness.readRequest(), undefined) const invalidEncodedNameHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/fallback.txt' }, responseHeaders: { 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="fallback.txt"; ' + "filename*=UTF-8''%E0%A4%A" }, fileSize: 8, fileContent: 'fallback' }) const invalidEncodedNamePreview = await invalidEncodedNameHarness.promise assert.equal(invalidEncodedNamePreview.fileName, 'fallback.txt') assert.equal(invalidEncodedNamePreview.content, 'fallback') const mdContent = 'm'.repeat(TEXT_PREVIEW_LIMIT) const mdHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/guide.md' }, responseHeaders: { 'Content-Type': 'text/markdown; charset=UTF-8', 'Content-Disposition': 'attachment; filename="guide.md"' }, fileSize: 50 * 1024 * 1024, fileContent: mdContent }) const mdPreview = await mdHarness.promise assert.equal(mdHarness.openRequest(), undefined) assert.equal(mdPreview.mode, 'text') assert.equal(mdPreview.fileName, 'guide.md') assert.equal(mdPreview.content.length, TEXT_PREVIEW_LIMIT) assert.equal(mdPreview.truncated, true) assert.equal(mdHarness.readRequest().length, TEXT_PREVIEW_LIMIT) const csvContent = 'c'.repeat(TEXT_PREVIEW_LIMIT) const csvHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/data.csv' }, responseHeaders: { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=data.csv' }, fileSize: TEXT_PREVIEW_LIMIT, fileContent: csvContent }) const csvPreview = await csvHarness.promise assert.equal(csvHarness.openRequest(), undefined) assert.equal(csvPreview.mode, 'text') assert.equal(csvPreview.fileName, 'data.csv') assert.equal(csvPreview.content.length, TEXT_PREVIEW_LIMIT) assert.equal(csvPreview.truncated, false) assert.equal(csvHarness.readRequest().length, TEXT_PREVIEW_LIMIT) const fileInfoFailureHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/file-info-failure.txt' }, responseHeaders: { 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="file-info-failure.txt"' }, fileInfoError: new Error('getFileInfo failed') }) await assert.rejects(fileInfoFailureHarness.promise, /文件读取失败,请重试/) assert.equal(fileInfoFailureHarness.openRequest(), undefined) assert.equal(fileInfoFailureHarness.readRequest(), undefined) const readFailureHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/read-failure.md' }, responseHeaders: { 'Content-Type': 'text/markdown', 'Content-Disposition': 'attachment; filename="read-failure.md"' }, fileSize: 10, readError: new Error('readFile failed') }) await assert.rejects(readFailureHarness.promise, /文件读取失败,请重试/) assert.equal(readFailureHarness.openRequest(), undefined) const unknownExtensionHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/unknown-file' }, responseHeaders: { 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="archive.zip"' } }) await assert.rejects(unknownExtensionHarness.promise, /文件类型不支持预览/) assert.equal(unknownExtensionHarness.openRequest(), undefined) assert.equal(unknownExtensionHarness.fileSystemManagerCalls(), 0) const jsonHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/error.json' }, responseHeaders: { 'Content-Type': 'application/json;charset=UTF-8', 'Content-Disposition': 'attachment; filename="error.json"' } }) await assert.rejects(jsonHarness.promise, /下载失败,请重试/) assert.equal(jsonHarness.openRequest(), undefined) const htmlHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/error.html' }, responseHeaders: { 'content-type': 'text/html; charset=UTF-8', 'content-disposition': 'attachment; filename="error.html"' } }) await assert.rejects(htmlHarness.promise, /下载失败,请重试/) assert.equal(htmlHarness.openRequest(), undefined) const missingHeadersHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/unknown.pdf' }, withHeadersListener: false }) await assert.rejects(missingHeadersHarness.promise, /下载失败,请重试/) assert.equal(missingHeadersHarness.openRequest(), undefined) const statusHarness = await createDownloadHarness({ result: { statusCode: 500 } }) await assert.rejects(statusHarness.promise, /下载失败,请重试/) assert.equal(statusHarness.openRequest(), undefined) const downloadError = new Error('download network error') const downloadFailureHarness = await createDownloadHarness({ downloadError }) await assert.rejects(downloadFailureHarness.promise, /download network error/) const noTransportOpenHarness = await createDownloadHarness({ result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' }, responseHeaders: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="download.pdf"' }, openError: new Error('must not be reached') }) assert.equal((await noTransportOpenHarness.promise).mode, 'document') assert.equal(noTransportOpenHarness.openRequest(), undefined) } function readSfcScript(filePath) { const source = fs.readFileSync(filePath, 'utf8') const openingTag = '', start + openingTag.length) if (start < 0 || end < 0) throw new Error(`missing script block: ${filePath}`) return source.slice(start + openingTag.length, end) } async function loadVueComponent(filePath, { http = {}, uni = {}, wx = {}, apiRules = rules } = {}) { const context = vm.createContext({ uni, wx, Date, Promise, String, Number, Boolean, Object, Array, setInterval, clearInterval }) const vueStub = new vm.SyntheticModule(['default'], function () { this.setExport('default', {}) }, { context, identifier: 'test:vue-component' }) const rulesStub = new vm.SyntheticModule(['default'], function () { this.setExport('default', apiRules) }, { context, identifier: 'test:team-background-rules' }) const httpNames = [ 'createTeamBackgroundSession', 'disableTeamBackgroundFile', 'downloadTeamBackgroundFile', 'listTeamBackgroundFiles', 'uploadTeamBackgroundFile' ] const httpStub = new vm.SyntheticModule(httpNames, function () { for (const name of httpNames) { this.setExport(name, http[name] || (() => Promise.resolve())) } }, { context, identifier: 'test:team-background-http' }) const componentModule = new vm.SourceTextModule(readSfcScript(filePath), { context, identifier: pathToFileURL(filePath).href }) await componentModule.link(specifier => { if (specifier === '@/http/teamBackgroundFile.js') return httpStub if (specifier === '@/utils/teamBackgroundFile.js') return rulesStub if (specifier.endsWith('.vue')) return vueStub throw new Error(`unexpected component import: ${specifier}`) }) await componentModule.evaluate() return componentModule.namespace.default } function instantiateComponent(component, overrides = {}) { const base = Object.assign({ initialCount: 0 }, overrides) const data = typeof component.data === 'function' ? component.data.call(base) : {} const instance = Object.assign(base, data, overrides) for (const [name, method] of Object.entries(component.methods || {})) { instance[name] = method.bind(instance) } return instance } function createDeferred() { let resolve let reject const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise reject = rejectPromise }) return { promise, resolve, reject } } const backgroundComponentPath = path.join(__dirname, '../components/CusTeamBackgroundFiles/index.vue') const teamFillComponentPath = path.join(__dirname, '../components/CusTeamInfoFill/index.vue') const createTeamPagePath = path.join(__dirname, '../pagesPublish/fillTeamInfo.vue') const createListComponentPath = path.join(__dirname, '../pagesHome/components/createList.vue') const teamEditPagePath = path.join(__dirname, '../pagesMy/teamEdit.vue') const headerComponentPath = path.join(__dirname, '../components/CusHeader/index.vue') async function testDeferredPcSelectionSurvivesSessionFailure() { const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession: () => Promise.reject(new Error('session unavailable')) } }) const instance = instantiateComponent(component, { teamId: '', editable: true, boundTeamId: 12, pcUploadDeferred: true, $showToast() {}, $emit() {} }) await assert.rejects(instance.waitForDeferredPcUpload(12), /session unavailable/) assert.equal(instance.pcUploadDeferred, true) assert.equal(instance.pcDialogVisible, false) assert.equal(instance.deferredPcResolve, null) } async function testSessionRefreshFailureKeepsOldTimer() { const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession: () => Promise.reject(new Error('refresh failed')) } }) const instance = instantiateComponent(component, { teamId: '', editable: true, boundTeamId: 12, $showToast() {}, $emit() {} }) const oldSession = { code: '111111', expiresAt: '2099-01-01 00:00:00' } const oldTimer = setInterval(() => {}, 60 * 1000) instance.session = oldSession instance.sessionTimer = oldTimer try { await assert.rejects(instance.openPcSession(12), /refresh failed/) assert.equal(instance.session, oldSession) assert.equal(instance.sessionTimer, oldTimer) } finally { clearInterval(oldTimer) instance.sessionTimer = null } } async function testDeferredPcFinishWaitsForListRefresh() { const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession: () => Promise.resolve({ code: '123456', uploadUrl: 'https://upload.example/', expiresAt: '2099-01-01 00:00:00' }), listTeamBackgroundFiles: () => Promise.reject(new Error('list refresh failed')) } }) const instance = instantiateComponent(component, { teamId: '', editable: true, boundTeamId: 12, pcUploadDeferred: true, $showToast() {}, $emit() {} }) const waiting = instance.waitForDeferredPcUpload(12) await new Promise(resolve => setImmediate(resolve)) assert.equal(typeof instance.deferredPcResolve, 'function') await instance.finishDeferredPcUpload() await assert.rejects(waiting, /list refresh failed/) } async function testTeamContextResetAndUploadIsolation() { const uploads = [] const component = await loadVueComponent(backgroundComponentPath, { http: { uploadTeamBackgroundFile(teamId, filePath, onProgress) { const deferred = createDeferred() uploads.push({ teamId, filePath, onProgress, deferred }) return deferred.promise }, listTeamBackgroundFiles: () => Promise.resolve([]) } }) let resolverCalls = 0 const instance = instantiateComponent(component, { teamId: '', editable: true, $showToast() {}, $emit() {} }) instance.resetTeamContext(1) const oldItem = rules.createBackgroundQueueItem( { name: 'old.pdf', path: '/tmp/old.pdf', size: 1 }, 'old' ) instance.queue.push(oldItem) instance.deferredPcResolve = () => { resolverCalls += 1 } const oldFlush = instance.flushPendingFiles(1) await new Promise(resolve => setImmediate(resolve)) assert.equal(uploads.length, 1) instance.files = [{ id: 11, fileName: 'old.pdf' }] instance.count = 1 instance.filesReady = true instance.session = { code: '111111' } instance.pcDialogVisible = true instance.textPreview = rules.createTextPreviewState({ fileName: 'old.txt', content: 'old' }) instance.resetTeamContext(2) assert.equal(instance.boundTeamId, 2) assert.equal(instance.files.length, 0) assert.equal(instance.count, 0) assert.equal(instance.queue.length, 0) assert.equal(instance.filesReady, false) assert.equal(instance.session, null) assert.equal(instance.pcDialogVisible, false) assert.equal(instance.textPreview.content, '') assert.equal(resolverCalls, 1) const newItem = rules.createBackgroundQueueItem( { name: 'new.pdf', path: '/tmp/new.pdf', size: 1 }, 'new' ) instance.queue.push(newItem) const newFlush = instance.flushPendingFiles(2) await new Promise(resolve => setImmediate(resolve)) assert.equal(uploads.length, 2, 'the new team must not reuse the old upload promise') assert.deepEqual(uploads.map(item => item.teamId), [1, 2]) uploads[0].onProgress(75) assert.equal(oldItem.progress, 0, 'stale upload progress must not mutate the old item after reset') uploads[0].deferred.resolve({ id: 101 }) await oldFlush assert.equal(newItem.status, 'uploading') uploads[1].deferred.resolve({ id: 202 }) await newFlush assert.equal(newItem.status, 'success') } async function testFileActionsWaitForCurrentListAndDocumentContext() { const listDeferred = createDeferred() const downloadDeferred = createDeferred() const opened = [] const toasts = [] let downloadCalls = 0 const wx = { openDocument(options) { opened.push(options.filePath) setImmediate(() => options.success()) } } const component = await loadVueComponent(backgroundComponentPath, { wx, http: { listTeamBackgroundFiles: () => listDeferred.promise, downloadTeamBackgroundFile: () => { downloadCalls += 1 return downloadDeferred.promise } } }) const instance = instantiateComponent(component, { teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {} }) instance.resetTeamContext(3) const oldFile = { id: 31, fileName: 'old.pdf' } instance.files = [oldFile] instance.filesReady = true const refresh = instance.refreshFiles(3) await instance.previewFile(oldFile) assert.equal(downloadCalls, 0, 'old file ids must be disabled while the current list is loading') listDeferred.resolve([oldFile]) await refresh const latePreview = instance.previewFile(oldFile) assert.equal(downloadCalls, 1) instance.resetTeamContext(4) downloadDeferred.resolve({ mode: 'document', fileName: 'old.pdf', filePath: '/tmp/old.pdf' }) await latePreview assert.deepEqual(opened, [], 'a stale download must not open a native document') const validComponent = await loadVueComponent(backgroundComponentPath, { wx, http: { downloadTeamBackgroundFile: () => Promise.resolve({ mode: 'document', fileName: 'current.pdf', filePath: '/tmp/current.pdf' }) } }) const validInstance = instantiateComponent(validComponent, { teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {} }) validInstance.resetTeamContext(5) const currentFile = { id: 51, fileName: 'current.pdf' } validInstance.files = [currentFile] validInstance.filesReady = true await validInstance.previewFile(currentFile) assert.deepEqual(opened, ['/tmp/current.pdf']) const failingComponent = await loadVueComponent(backgroundComponentPath, { wx: { openDocument(options) { setImmediate(() => options.fail(new Error('open failed'))) } }, http: { downloadTeamBackgroundFile: () => Promise.resolve({ mode: 'document', fileName: 'broken.pdf', filePath: '/tmp/broken.pdf' }) } }) const failingInstance = instantiateComponent(failingComponent, { teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {} }) failingInstance.resetTeamContext(6) const brokenFile = { id: 61, fileName: 'broken.pdf' } failingInstance.files = [brokenFile] failingInstance.filesReady = true await failingInstance.previewFile(brokenFile) assert.ok(toasts.some(message => String(message).includes('open failed'))) } async function testTeamFillFlushesBeforeEmitAndLocks() { const component = await loadVueComponent(teamFillComponentPath) const flushDeferred = createDeferred() let flushCalls = 0 let emitCalls = 0 const instance = instantiateComponent(component, { teamId: 12, qtype: false, $props: { qtype: false }, $showToast() {}, $emit() { emitCalls += 1 }, $refs: { backgroundRef: { flushPendingFiles() { flushCalls += 1; return flushDeferred.promise }, hasUnresolvedFiles() { return false } } } }) instance.teamInfo = { teamName: '团队', enterpriseName: '公司', enterpriseWebsite: '无', districtId: 1, industryId: 2, functionIds: [], orgIds: [] } const first = instance.handleConfirm() const second = instance.handleConfirm() assert.equal(flushCalls, 1) assert.equal(emitCalls, 0) flushDeferred.resolve({ success: 1, failed: 0 }) await Promise.all([first, second]) assert.equal(emitCalls, 1) } async function testCreatedTeamResumeAndSubmitLock() { const component = await loadVueComponent(createTeamPagePath) let teamPostCalls = 0 let flushCalls = 0 const toasts = [] const instance = instantiateComponent(component, { $api: { post(url) { assert.equal(url, '/core/user/team') teamPostCalls += 1 return Promise.resolve({ data: { code: 0, data: { teamId: 88 } } }) } }, $showToast(message) { toasts.push(message) }, $showModal: () => Promise.resolve(), $refs: { teamRef: { flushBackgroundFiles() { flushCalls += 1 if (flushCalls === 1) return Promise.reject(new Error('列表刷新失败')) return Promise.resolve({ success: 0, failed: 0 }) }, hasUnresolvedBackgroundFiles: () => false, hasDeferredPcUpload: () => false } } }) let continuations = 0 instance.continueAfterTeamCreated = async teamId => { assert.equal(teamId, 88) continuations += 1 } await instance.handleConfirm({ teamName: '团队' }) assert.equal(teamPostCalls, 1) assert.equal(instance.creationState.createdTeamId, 88) assert.ok(toasts.some(message => String(message).includes('背景资料'))) await instance.handleConfirm({ teamName: '团队' }) assert.equal(teamPostCalls, 1, 'retry after a later-stage failure must not POST a second team') assert.equal(continuations, 1) const lockedPost = createDeferred() const lockedInstance = instantiateComponent(component, { $api: { post: () => { teamPostCalls += 1; return lockedPost.promise } }, $showToast() {}, $showModal: () => Promise.resolve(), $refs: { teamRef: {} } }) const lockedFirst = lockedInstance.handleConfirm({ teamName: '团队' }) const callsAfterFirst = teamPostCalls const lockedSecond = lockedInstance.handleConfirm({ teamName: '团队' }) assert.equal(teamPostCalls, callsAfterFirst, 'concurrent submission must be ignored') lockedPost.resolve({ data: { code: 1, msg: '保存接口失败' } }) await Promise.all([lockedFirst, lockedSecond]) } async function testCloseAndBackRequirePendingUploadConfirmation() { let modalOptions let navigateBackCalls = 0 const uni = { showModal(options) { modalOptions = options }, navigateBack() { navigateBackCalls += 1 } } const createList = await loadVueComponent(createListComponentPath, { uni }) const createListInstance = instantiateComponent(createList, { $imgBase: 'https://image.example/', teamInfoShow: true, $refs: { teamRef: { hasUnresolvedBackgroundFiles: () => true, isBackgroundUploadActive: () => false } } }) createListInstance.requestCloseTeamInfo() assert.equal(createListInstance.teamInfoShow, true) modalOptions.success({ confirm: false }) assert.equal(createListInstance.teamInfoShow, true) createListInstance.requestCloseTeamInfo() modalOptions.success({ confirm: true }) assert.equal(createListInstance.teamInfoShow, false) const teamEdit = await loadVueComponent(teamEditPagePath, { uni }) const teamEditInstance = instantiateComponent(teamEdit, { $refs: { teamRef: { hasUnresolvedBackgroundFiles: () => true, isBackgroundUploadActive: () => true } } }) teamEditInstance.requestBack() assert.equal(navigateBackCalls, 0) modalOptions.success({ confirm: false }) assert.equal(navigateBackCalls, 0) teamEditInstance.requestBack() modalOptions.success({ confirm: true }) assert.equal(navigateBackCalls, 1) let headerBackEvents = 0 const header = await loadVueComponent(headerComponentPath, { uni }) const headerInstance = instantiateComponent(header, { interceptBack: true, $emit(name) { if (name === 'back') headerBackEvents += 1 } }) headerInstance.toBack('') assert.equal(headerBackEvents, 1) assert.equal(navigateBackCalls, 1, 'intercepted header back must not navigate directly') } async function main() { const loadedTransport = await loadTransport() assert.equal(typeof loadedTransport.downloadTeamBackgroundFile, 'function') await testUploadTransport() await testApiTransport() await testDownloadTransport() await testDeferredPcSelectionSurvivesSessionFailure() await testSessionRefreshFailureKeepsOldTimer() await testDeferredPcFinishWaitsForListRefresh() await testTeamContextResetAndUploadIsolation() await testFileActionsWaitForCurrentListAndDocumentContext() await testTeamFillFlushesBeforeEmitAndLocks() await testCreatedTeamResumeAndSubmitLock() await testCloseAndBackRequirePendingUploadConfirmation() console.log('team background transport: PASS') console.log('team background component behavior: PASS') } main().catch(error => { console.error(error) process.exitCode = 1 })