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, teamPayloadSnapshot: '', teamQuestionnaireId: '', publishResult: null }) const firstPayloadSnapshot = rules.createTeamPayloadSnapshot({ teamName: '团队', enterpriseWebsite: ' https://example.com ', coachId: 7 }) const equivalentPayloadSnapshot = rules.createTeamPayloadSnapshot({ enterpriseWebsite: 'https://example.com', teamName: '团队' }) assert.equal(firstPayloadSnapshot, equivalentPayloadSnapshot) assert.notEqual(firstPayloadSnapshot, rules.createTeamPayloadSnapshot({ teamName: '团队', enterpriseWebsite: 'https://changed.example.com' })) 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, timers = {} } = {}) { const context = vm.createContext({ uni, wx, Date, Promise, String, Number, Boolean, Object, Array, setInterval: timers.setInterval || setInterval, clearInterval: timers.clearInterval || clearInterval, setTimeout: timers.setTimeout || setTimeout, clearTimeout: timers.clearTimeout || clearTimeout }) 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 } } function createFakeTimers() { let nextId = 1 const pending = new Map() return { setTimeout(callback) { const id = nextId++ pending.set(id, callback) return id }, clearTimeout(id) { pending.delete(id) }, count() { return pending.size }, runNext() { const entry = pending.entries().next().value if (!entry) return false pending.delete(entry[0]) entry[1]() return true }, runAll() { while (this.runNext()) {} } } } async function flushPromises() { await Promise.resolve() await new Promise(resolve => setImmediate(resolve)) } 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 testPcSessionCreationIsSingleFlightPerTeam() { const firstRequest = createDeferred() let createCalls = 0 const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession() { createCalls += 1 return firstRequest.promise } } }) const instance = instantiateComponent(component, { teamId: '', editable: true, $showToast() {}, $emit() {} }) instance.resetTeamContext(12) const first = instance.openPcSession(12) const second = instance.openPcSession(12) assert.equal(createCalls, 1, 'same-team callers must share one session POST') assert.equal(instance.sessionCreating, true) instance.refreshPcSession() assert.equal(createCalls, 1, 'disabled regenerate must not create another POST') instance.resetTeamContext(13) assert.equal(instance.sessionCreating, false, 'creating state follows the active team') firstRequest.resolve({ code: '123456', uploadUrl: 'https://upload.example/', expiresAt: '2099-01-01 00:00:00' }) assert.equal(await first, null) assert.equal(await second, null) assert.equal(instance.session, null, 'a stale context must not display the resolved session') assert.equal(instance.pcDialogVisible, false) } async function testPcSessionSingleFlightSurvivesSameTeamReset() { const request = createDeferred() let createCalls = 0 const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession() { createCalls += 1 return request.promise } } }) const instance = instantiateComponent(component, { teamId: '', editable: true, $showToast() {}, $emit() {} }) instance.resetTeamContext(21) const staleCaller = instance.openPcSession(21) instance.resetTeamContext(21) assert.equal(instance.sessionCreating, true, 'same-team reset must retain the network lock') const currentCaller = instance.openPcSession(21) assert.equal(createCalls, 1, 'same-team reset must reuse the raw network promise') const rawSession = { code: '654321', uploadUrl: 'https://upload.example/current', expiresAt: '2099-01-01 00:00:00' } request.resolve(rawSession) assert.equal(await staleCaller, null) assert.equal(await currentCaller, rawSession) assert.equal(instance.session, rawSession) assert.equal(instance.pcDialogVisible, true) assert.equal(instance.sessionCreating, false) instance.closePcDialog() } async function testPcSessionFailureClearsSingleFlightForRetry() { let createCalls = 0 const component = await loadVueComponent(backgroundComponentPath, { http: { createTeamBackgroundSession() { createCalls += 1 if (createCalls === 1) return Promise.reject(new Error('session unavailable')) return Promise.resolve({ code: '222222', uploadUrl: 'https://upload.example/retry', expiresAt: '2099-01-01 00:00:00' }) } } }) const instance = instantiateComponent(component, { teamId: '', editable: true, $showToast() {}, $emit() {} }) instance.resetTeamContext(22) await assert.rejects(instance.openPcSession(22), /session unavailable/) assert.equal(instance.sessionCreating, false) const session = await instance.openPcSession(22) assert.equal(createCalls, 2) assert.equal(session.code, '222222') assert.equal(instance.sessionCreating, false) instance.closePcDialog() } 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 testCreatedTeamNavigationIsAwaitableAndResumable() { const timers = createFakeTimers() let navigationOptions let teamPostCalls = 0 let publishCalls = 0 let teamPutCalls = 0 let flushCalls = 0 const toasts = [] const uni = { getStorageSync(key) { assert.equal(key, 'userInfo') return JSON.stringify({ id: 7 }) }, removeStorageSync() {}, navigateTo(options) { navigationOptions = options } } const component = await loadVueComponent(createTeamPagePath, { uni, timers }) const originalFormat = Date.prototype.Format Date.prototype.Format = () => '2026-07-21 12:00:00' try { const instance = instantiateComponent(component, { next: '1', questionnaireId: 66, type: 'survey', title: '问卷', $api: { post(url, payload) { if (url === '/core/user/team') { teamPostCalls += 1 assert.equal(payload.enterpriseWebsite, 'https://old.example') return Promise.resolve({ data: { code: 0, data: { teamId: 88 } } }) } assert.equal(url, '/core/team/questionnaire/publish') assert.deepEqual(Object.keys(payload).sort(), [ 'answerSetting', 'coachId', 'endTime', 'questionnaireId', 'startTime', 'teamId', 'type' ].sort()) publishCalls += 1 return Promise.resolve({ data: { code: 0, data: 321 } }) }, put(url, payload) { teamPutCalls += 1 assert.equal(url, '/core/user/team') assert.equal(payload.id, 88) assert.equal(payload.enterpriseWebsite, 'https://new.example') return Promise.resolve({ data: { code: 0, data: true } }) } }, $showToast(message) { toasts.push(message) }, $showModal: () => Promise.resolve(), $refs: { teamRef: { flushBackgroundFiles() { flushCalls += 1; return Promise.resolve({ success: 0, failed: 0 }) }, hasUnresolvedBackgroundFiles: () => false, hasDeferredPcUpload: () => false } } }) const first = instance.handleConfirm({ teamName: '团队', enterpriseWebsite: 'https://old.example' }) await flushPromises() assert.equal(teamPostCalls, 1) assert.equal(publishCalls, 1) assert.equal(flushCalls, 1) assert.equal(instance.creationState.teamQuestionnaireId, 321) assert.equal(instance.creationState.completed, false) assert.equal(instance.creationState.submitting, true) assert.equal(timers.count(), 1) timers.runNext() await flushPromises() assert.ok(navigationOptions) assert.equal(instance.creationState.completed, false, 'navigation callback must settle first') navigationOptions.fail({ errMsg: 'navigateTo:fail route missing' }) await first assert.equal(instance.creationState.completed, false) assert.equal(instance.creationState.submitting, false) assert.equal(instance.creationState.stage, 'navigate') assert.ok(toasts.some(message => String(message).includes('页面跳转失败'))) navigationOptions = null const retry = instance.handleConfirm({ teamName: '团队', enterpriseWebsite: 'https://new.example' }) await flushPromises() assert.equal(teamPostCalls, 1, 'retry must reuse the created team id') assert.equal(publishCalls, 1, 'retry after publish success must not publish again') assert.equal(flushCalls, 1, 'navigate-stage retry must not repeat file processing') assert.equal(teamPutCalls, 1, 'changed sanitized payload must update the same team') assert.equal(timers.count(), 1) timers.runNext() await flushPromises() assert.ok(navigationOptions) assert.equal(instance.creationState.completed, false) navigationOptions.success({}) await retry assert.equal(instance.creationState.completed, true) assert.equal(instance.creationState.submitting, false) } finally { if (originalFormat) Date.prototype.Format = originalFormat else delete Date.prototype.Format } } async function testCreatedTeamUnloadCancelsLateNavigation() { const timers = createFakeTimers() let navigateBackCalls = 0 const uni = { navigateBack() { navigateBackCalls += 1 } } const component = await loadVueComponent(createTeamPagePath, { uni, timers }) const instance = instantiateComponent(component, { $api: { post: () => Promise.resolve({ data: { code: 0, data: { teamId: 89 } } }) }, $showToast() {}, $showModal: () => Promise.resolve(), getOpenerEventChannel: () => ({ emit() {} }), $refs: { teamRef: { flushBackgroundFiles: () => Promise.resolve({ success: 0, failed: 0 }), hasUnresolvedBackgroundFiles: () => false, hasDeferredPcUpload: () => false } } }) const submission = instance.handleConfirm({ teamName: '团队', enterpriseWebsite: '无' }) await flushPromises() assert.equal(timers.count(), 1) component.onUnload.call(instance) assert.equal(timers.count(), 0) timers.runAll() await submission assert.equal(navigateBackCalls, 0) assert.equal(instance.creationState.completed, false) assert.equal(instance.creationState.submitting, false) } async function testTeamEditRedirectIsLockedAndResumable() { const timers = createFakeTimers() let redirectOptions let putCalls = 0 let navigateBackCalls = 0 const toasts = [] const uni = { redirectTo(options) { redirectOptions = options }, navigateBack() { navigateBackCalls += 1 } } const component = await loadVueComponent(teamEditPagePath, { uni, timers }) const instance = instantiateComponent(component, { submitDto: { id: 12, teamName: '团队' }, show: true, $api: { put(url, payload) { putCalls += 1 assert.equal(url, '/core/user/team') assert.equal(payload.id, 12) return Promise.resolve({ data: { code: 0, data: true } }) } }, $showToast(message) { toasts.push(message) } }) const first = instance.editConfirm() await flushPromises() assert.equal(putCalls, 1) assert.equal(instance.saveSucceeded, true) assert.equal(instance.saving, true, 'save lock must cover the redirect delay') assert.equal(timers.count(), 1) instance.editConfirm() instance.requestBack() assert.equal(putCalls, 1, 'confirm during redirect delay must not PUT again') assert.equal(navigateBackCalls, 0, 'back during redirect delay must stay locked') assert.ok(toasts.some(message => String(message).includes('正在保存'))) timers.runNext() await flushPromises() assert.ok(redirectOptions) assert.equal(instance.saving, true, 'redirect callback must settle before unlock') redirectOptions.fail({ errMsg: 'redirectTo:fail route missing' }) await first assert.equal(instance.saving, false) assert.equal(instance.saveSucceeded, true) assert.equal(instance.saveStage, 'navigate') redirectOptions = null const retry = instance.editConfirm() await flushPromises() assert.equal(putCalls, 1, 'redirect retry must reuse the successful PUT') assert.equal(instance.saving, true) assert.equal(timers.count(), 1) timers.runNext() await flushPromises() assert.ok(redirectOptions) redirectOptions.success({}) await retry assert.equal(putCalls, 1) assert.equal(instance.saving, false) } async function testTeamEditUnloadCancelsLateRedirect() { const timers = createFakeTimers() let redirectCalls = 0 const component = await loadVueComponent(teamEditPagePath, { timers, uni: { redirectTo() { redirectCalls += 1 } } }) const instance = instantiateComponent(component, { submitDto: { id: 12 }, $api: { put: () => Promise.resolve({ data: { code: 0, data: true } }) }, $showToast() {} }) const saving = instance.editConfirm() await flushPromises() assert.equal(timers.count(), 1) component.onUnload.call(instance) assert.equal(timers.count(), 0) timers.runAll() await saving assert.equal(redirectCalls, 0) assert.equal(instance.saving, false) } 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 testPcSessionCreationIsSingleFlightPerTeam() await testPcSessionSingleFlightSurvivesSameTeamReset() await testPcSessionFailureClearsSingleFlightForRetry() await testDeferredPcFinishWaitsForListRefresh() await testTeamContextResetAndUploadIsolation() await testFileActionsWaitForCurrentListAndDocumentContext() await testTeamFillFlushesBeforeEmitAndLocks() await testCreatedTeamResumeAndSubmitLock() await testCreatedTeamNavigationIsAwaitableAndResumable() await testCreatedTeamUnloadCancelsLateNavigation() await testTeamEditRedirectIsLockedAndResumable() await testTeamEditUnloadCancelsLateRedirect() await testCloseAndBackRequirePendingUploadConfirmation() console.log('team background transport: PASS') console.log('team background component behavior: PASS') } main().catch(error => { console.error(error) process.exitCode = 1 })