| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738 |
- 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')
- const backgroundSurveyRules = require('../utils/backgroundSurvey')
- const surveyQuestion = {
- detailId: 101,
- options: [
- { code:'A', text:'固定选项', customAllowed:false },
- { code:'F', text:'其他', customAllowed:true }
- ]
- }
- assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, '', ''), '请选择当前题目的答案')
- assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, 'F', ''), '请填写其他选项的具体内容')
- assert.equal(backgroundSurveyRules.validateBackgroundAnswer(surveyQuestion, 'A', '忽略内容'), '')
- assert.deepEqual(
- backgroundSurveyRules.createBackgroundAnswerPayload(surveyQuestion, 'A', '忽略内容'),
- { detailId:101, optionCode:'A', customText:'' }
- )
- assert.deepEqual(
- backgroundSurveyRules.createBackgroundAnswerPayload(surveyQuestion, 'F', ' 自定义答案 '),
- { detailId:101, optionCode:'F', customText:'自定义答案' }
- )
- assert.equal(
- backgroundSurveyRules.backgroundIntroUrl({ teamQuestionnaireId:12, title:'团队 A' }),
- '/pagesPublish/backgroundSurveyIntro?stage=background&teamQuestionnaireId=12&title=%E5%9B%A2%E9%98%9F%20A'
- )
- assert.equal(backgroundSurveyRules.isPublishablePerillQuestionnaire({ purpose:'PERILL' }), true)
- assert.equal(backgroundSurveyRules.isPublishablePerillQuestionnaire({}), true)
- assert.equal(backgroundSurveyRules.isPublishablePerillQuestionnaire({ purpose:'BACKGROUND_COACH' }), false)
- assert.equal(backgroundSurveyRules.isPublishablePerillQuestionnaire({ purpose:'BACKGROUND_MEMBER' }), false)
- assert.equal(rules.validateEnterpriseWebsite(' 无 '), '')
- assert.equal(rules.validateEnterpriseWebsite('https://example.com'), '')
- assert.match(rules.validateEnterpriseWebsite('ftp://example.com'), /http/)
- const websiteWithLength = length => `https://example.com/${'a'.repeat(length - 20)}`
- assert.equal(rules.validateEnterpriseWebsite(websiteWithLength(499)), '')
- assert.equal(rules.validateEnterpriseWebsite(websiteWithLength(500)), '')
- assert.match(rules.validateEnterpriseWebsite(websiteWithLength(501)), /500/)
- 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: '',
- backgroundSurveyId: '',
- backgroundSurveyStatus: 'DRAFT',
- teamQuestionnaireId: '',
- publishResult: null,
- resumeStage: ''
- })
- 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')
- const httpInterfacePath = path.join(__dirname, '../http/interface.js')
- const httpIndexPath = path.join(__dirname, '../http/index.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
- }
- async function loadHttpApi(uni) {
- const context = vm.createContext({
- uni,
- process: { env: { NODE_ENV: 'test' } }
- })
- const baseApiModule = new vm.SyntheticModule(['BaseApi'], function () {
- this.setExport('BaseApi', 'https://api.example.test/app')
- }, { context, identifier: 'test:httpBaseApi' })
- const interfaceModule = new vm.SourceTextModule(
- fs.readFileSync(httpInterfacePath, 'utf8'),
- { context, identifier: pathToFileURL(httpInterfacePath).href }
- )
- const indexModule = new vm.SourceTextModule(
- fs.readFileSync(httpIndexPath, 'utf8'),
- { context, identifier: pathToFileURL(httpIndexPath).href }
- )
- await indexModule.link((specifier, referencingModule) => {
- if (referencingModule === indexModule && specifier === './interface') return interfaceModule
- if (referencingModule === interfaceModule && specifier === './baseApi.js') return baseApiModule
- throw new Error(`unexpected http import: ${specifier}`)
- })
- await indexModule.evaluate()
- return indexModule.namespace.default
- }
- async function testConcurrentSilentRequestDoesNotStrandLoadingMask() {
- const requests = []
- let shown = 0
- let hidden = 0
- const api = await loadHttpApi({
- showLoading() { shown += 1 },
- hideLoading() { hidden += 1 },
- getStorageSync() { return '' },
- request(options) { requests.push(options) }
- })
- const visibleRequest = api.get('/visible', {}, true)
- const silentRequest = api.get('/silent', {}, false)
- assert.equal(shown, 1)
- assert.equal(hidden, 0)
- assert.equal(requests.length, 2)
- requests[1].complete({ statusCode: 200, data: { code: 0, data: [] } })
- await silentRequest
- assert.equal(hidden, 0, 'a silent request must not hide another request loading mask')
- requests[0].complete({ statusCode: 200, data: { code: 0, data: {} } })
- await visibleRequest
- assert.equal(hidden, 1, 'the request that displayed loading must always hide it on completion')
- }
- 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: '<invalid-json>' })
- 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 = '<script>'
- const start = source.indexOf(openingTag)
- const end = source.indexOf('</script>', 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,
- backgroundRules = backgroundSurveyRules, 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 backgroundRulesStub = new vm.SyntheticModule(['default'], function () {
- this.setExport('default', backgroundRules)
- }, { context, identifier: 'test:background-survey-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 === '@/utils/backgroundSurvey.js') return backgroundRulesStub
- 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')
- const backgroundSurveyPagePath = path.join(__dirname, '../pagesPublish/backgroundSurvey.vue')
- const backgroundSurveyIntroPagePath = path.join(__dirname, '../pagesPublish/backgroundSurveyIntro.vue')
- async function testBackgroundSurveyPagesLoad() {
- const survey = await loadVueComponent(backgroundSurveyPagePath)
- const intro = await loadVueComponent(backgroundSurveyIntroPagePath)
- assert.equal(typeof survey.methods.loadSurvey, 'function')
- assert.equal(typeof survey.methods.saveCurrent, 'function')
- assert.equal(typeof survey.methods.next, 'function')
- assert.equal(typeof intro.methods.start, 'function')
- }
- async function testDeferredPcSelectionSurvivesSessionFailure() {
- let createCalls = 0
- const firstRequest = createDeferred()
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession() {
- createCalls += 1
- if (createCalls === 1) return firstRequest.promise
- return Promise.resolve({
- code: '123456', uploadUrl: 'https://upload.example/retry', expiresAt: '2099-01-01 00:00:00'
- })
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '',
- editable: true,
- boundTeamId: 12,
- pcUploadDeferred: true,
- $showToast() {},
- $emit() {}
- })
- const first = instance.waitForDeferredPcUpload(12)
- const second = instance.waitForDeferredPcUpload(12)
- assert.equal(first, second, 'the whole deferred flow must expose one shared waiter')
- assert.equal(createCalls, 1)
- firstRequest.reject(new Error('session unavailable'))
- const failed = await Promise.allSettled([first, second])
- assert.deepEqual(failed.map(result => result.status), ['rejected', 'rejected'])
- assert.match(failed[0].reason.message, /session unavailable/)
- assert.equal(instance.pcUploadDeferred, true)
- assert.equal(instance.pcDialogVisible, false)
- assert.equal(instance.deferredPcResolve, null)
- assert.equal(instance.deferredPcWaitPromise, null)
- const retry = instance.waitForDeferredPcUpload(12)
- await flushPromises()
- assert.equal(createCalls, 2, 'a failed shared waiter must be retryable')
- assert.equal(typeof instance.deferredPcResolve, 'function')
- instance.skipDeferredPcUpload()
- await retry
- }
- 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 testDeferredPcWaiterSingleFlightFinishesAllCallers() {
- let createCalls = 0
- const sessionRequest = createDeferred()
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession() {
- createCalls += 1
- return sessionRequest.promise
- },
- listTeamBackgroundFiles: () => Promise.resolve([])
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, boundTeamId: 12, pcUploadDeferred: true,
- $showToast() {}, $emit() {}
- })
- const first = instance.waitForDeferredPcUpload(12)
- const second = instance.waitForDeferredPcUpload(12)
- assert.equal(first, second)
- assert.equal(createCalls, 1)
- let settled = false
- first.then(() => { settled = true })
- sessionRequest.resolve({
- code: '654321', uploadUrl: 'https://upload.example/', expiresAt: '2099-01-01 00:00:00'
- })
- await flushPromises()
- assert.equal(settled, false, 'session creation must not settle the dialog waiter')
- await instance.finishDeferredPcUpload()
- await Promise.all([first, second])
- assert.equal(settled, true)
- assert.equal(instance.deferredPcWaitPromise, null)
- }
- 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 testUploadedFileUsesDeleteWordingAndRefreshesAfterDelete() {
- let modalOptions
- const disableCalls = []
- const toasts = []
- const component = await loadVueComponent(backgroundComponentPath, {
- uni: {
- showModal(options) {
- modalOptions = options
- }
- },
- http: {
- disableTeamBackgroundFile(teamId, fileId) {
- disableCalls.push([teamId, fileId])
- return Promise.resolve(true)
- },
- listTeamBackgroundFiles() {
- return Promise.resolve([])
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '',
- editable: true,
- $showToast(message) { toasts.push(message) },
- $emit() {}
- })
- instance.resetTeamContext(5)
- const file = { id: 51, fileName: '背景资料.pdf' }
- instance.files = [file]
- instance.filesReady = true
- instance.removeFile(file)
- assert.equal(modalOptions.title, '删除资料')
- assert.match(modalOptions.content, /确定删除/)
- modalOptions.success({ confirm: true })
- await flushPromises()
- assert.deepEqual(disableCalls, [[5, 51]])
- assert.deepEqual(instance.files, [])
- assert.ok(toasts.includes('已删除'))
- const source = fs.readFileSync(backgroundComponentPath, 'utf8')
- assert.match(source, />删除<\/view>/)
- assert.doesNotMatch(source, /停用资料|已停用|停用失败/)
- }
- async function testSameTeamRefreshFailurePreservesExistingFilesAndCount() {
- const listRequest = createDeferred()
- const emittedCounts = []
- const component = await loadVueComponent(backgroundComponentPath, {
- http: { listTeamBackgroundFiles: () => listRequest.promise }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, $showToast() {},
- $emit(name, value) { if (name === 'countChange') emittedCounts.push(value) }
- })
- instance.resetTeamContext(30)
- const existingFiles = [{ id: 301, fileName: 'existing.pdf' }]
- instance.files = existingFiles
- instance.count = 1
- instance.filesReady = true
- instance.hasLoadedFiles = true
- emittedCounts.length = 0
- const refresh = instance.refreshFiles(30)
- assert.equal(instance.files, existingFiles, 'same-team refresh must retain the visible list while loading')
- assert.equal(instance.count, 1)
- assert.deepEqual(emittedCounts, [], 'same-team refresh must not transiently emit zero')
- listRequest.reject(new Error('refresh failed'))
- await assert.rejects(refresh, /refresh failed/)
- assert.equal(instance.files, existingFiles)
- assert.equal(instance.count, 1)
- assert.deepEqual(emittedCounts, [])
- instance.resetTeamContext(31)
- assert.equal(instance.files.length, 0)
- assert.equal(instance.count, 0)
- assert.deepEqual(emittedCounts, [0], 'switching teams must still clear the old count immediately')
- }
- async function testTeamWebsiteInputExposesTheAuthoritativeLengthLimit() {
- const source = fs.readFileSync(teamFillComponentPath, 'utf8')
- assert.match(source, /v-model="teamInfo\.enterpriseWebsite"[^>]*maxlength="500"/)
- }
- async function testTeamFillRendersBackgroundFilesAndBusyState() {
- const source = fs.readFileSync(teamFillComponentPath, 'utf8')
- assert.match(
- source,
- /<cus-team-background-files[^>]*ref="backgroundRef"[^>]*>/,
- 'the team form must render the registered background-file component'
- )
- assert.match(
- source,
- /:class="\{ disabled: confirming \|\| submitting \}"/,
- 'the submit button must expose its busy state'
- )
- const component = await loadVueComponent(teamFillComponentPath)
- const state = component.data()
- assert.equal(state.confirming, false, 'the submit lock must be reactive from initialization')
- assert.equal(component.props.confirmText.type, String)
- assert.equal(component.props.qtype.type, Boolean)
- }
- 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', 'backgroundSurveyId', 'coachId', 'endTime', 'questionnaireId',
- 'startTime', 'teamId', 'type'
- ].sort())
- assert.equal(payload.backgroundSurveyId, 456)
- 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')
- if (teamPutCalls === 1) {
- return Promise.resolve({ data: { code: 9, msg: '官网更新失败' } })
- }
- 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
- }
- }
- })
- instance.creationState.backgroundSurveyId = 456
- instance.creationState.backgroundSurveyStatus = 'COMPLETED'
- 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 failedUpdate = instance.handleConfirm({
- teamName: '团队', enterpriseWebsite: 'https://new.example'
- })
- await failedUpdate
- assert.equal(teamPostCalls, 1)
- assert.equal(publishCalls, 1)
- assert.equal(flushCalls, 1)
- assert.equal(teamPutCalls, 1)
- assert.equal(instance.creationState.stage, 'update')
- assert.ok(toasts.some(message => String(message).includes('团队更新失败:官网更新失败')))
- assert.equal(timers.count(), 0)
- 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, 2, '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 testCreatedTeamRequiresCoachSurveyBeforePublish() {
- let navigationOptions
- let publishCalls = 0
- const uni = {
- getStorageSync: () => JSON.stringify({ id: 7 }),
- navigateTo(options) {
- navigationOptions = options
- }
- }
- const component = await loadVueComponent(createTeamPagePath, { uni })
- const instance = instantiateComponent(component, {
- next:'1',
- type:'1',
- title:'PERILL',
- questionnaireId:66,
- $api:{
- post(url) {
- assert.equal(url, '/core/user/team')
- return Promise.resolve({ data:{ code:0, data:{ teamId:88 } } })
- }
- },
- $showToast() {},
- $showModal: () => Promise.resolve(),
- $refs:{
- teamRef:{
- flushBackgroundFiles: () => Promise.resolve({ success:0, failed:0 }),
- hasUnresolvedBackgroundFiles: () => false,
- hasDeferredPcUpload: () => false
- }
- }
- })
- let continuedTeamId
- instance.continueAfterTeamCreated = async teamId => {
- continuedTeamId = teamId
- }
- instance.publishQuestionnaire = async teamId => {
- assert.equal(teamId, 88)
- assert.equal(instance.creationState.backgroundSurveyId, 456)
- publishCalls += 1
- return { code:0, data:321 }
- }
- await instance.handleConfirm({ teamName:'团队', enterpriseWebsite:'无' })
- assert.equal(publishCalls, 0, 'publishing must wait for the coach background survey')
- assert.match(navigationOptions.url, /backgroundSurvey\?mode=coach/)
- assert.equal(instance.creationState.stage, 'coachSurvey')
- navigationOptions.events.backgroundSurveyReady({ surveyId:456, status:'DRAFT' })
- assert.equal(instance.creationState.backgroundSurveyId, 456)
- navigationOptions.events.backgroundSurveyCompleted({ surveyId:456, status:'COMPLETED' })
- component.onShow.call(instance)
- await flushPromises()
- assert.equal(publishCalls, 1)
- assert.equal(instance.creationState.teamQuestionnaireId, 321)
- assert.equal(continuedTeamId, 88)
- assert.equal(instance.creationState.completed, true)
- }
- 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: '团队', enterpriseWebsite: 'https://old.example' },
- show: true,
- $api: {
- put(url, payload) {
- putCalls += 1
- assert.equal(url, '/core/user/team')
- assert.equal(payload.id, 12)
- assert.equal(Object.hasOwn(payload, 'coachId'), false)
- assert.equal(Object.hasOwn(payload, 'uploaderId'), false)
- assert.equal(Object.hasOwn(payload, 'source'), false)
- assert.equal(payload.enterpriseWebsite,
- putCalls === 1 ? 'https://old.example' : 'https://new.example')
- 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.fail({ errMsg: 'redirectTo:fail still missing' })
- await retry
- assert.equal(putCalls, 1)
- redirectOptions = null
- instance.handleConfirm({
- id: 12,
- teamName: '团队',
- enterpriseWebsite: ' https://new.example ',
- coachId: 7,
- uploaderId: 8,
- source: 'WECHAT'
- })
- const changedRetry = instance.editConfirm()
- await flushPromises()
- assert.equal(putCalls, 2, 'a changed payload must update the saved team before redirect')
- assert.equal(instance.saving, true)
- assert.equal(timers.count(), 1)
- timers.runNext()
- await flushPromises()
- assert.ok(redirectOptions)
- redirectOptions.success({})
- await changedRetry
- assert.equal(putCalls, 2)
- 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 testConcurrentSilentRequestDoesNotStrandLoadingMask()
- await testUploadTransport()
- await testApiTransport()
- await testDownloadTransport()
- await testBackgroundSurveyPagesLoad()
- await testDeferredPcSelectionSurvivesSessionFailure()
- await testSessionRefreshFailureKeepsOldTimer()
- await testPcSessionCreationIsSingleFlightPerTeam()
- await testPcSessionSingleFlightSurvivesSameTeamReset()
- await testPcSessionFailureClearsSingleFlightForRetry()
- await testDeferredPcFinishWaitsForListRefresh()
- await testDeferredPcWaiterSingleFlightFinishesAllCallers()
- await testTeamContextResetAndUploadIsolation()
- await testFileActionsWaitForCurrentListAndDocumentContext()
- await testUploadedFileUsesDeleteWordingAndRefreshesAfterDelete()
- await testSameTeamRefreshFailurePreservesExistingFilesAndCount()
- await testTeamWebsiteInputExposesTheAuthoritativeLengthLimit()
- await testTeamFillRendersBackgroundFilesAndBusyState()
- await testTeamFillFlushesBeforeEmitAndLocks()
- await testCreatedTeamResumeAndSubmitLock()
- await testCreatedTeamNavigationIsAwaitableAndResumable()
- await testCreatedTeamRequiresCoachSurveyBeforePublish()
- 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
- })
|