| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734 |
- 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(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
- })
|