| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365 |
- const assert = require('node:assert/strict')
- const fs = require('node:fs')
- const path = require('node:path')
- const { pathToFileURL } = require('node:url')
- const vm = require('node:vm')
- const rules = require('../utils/teamBackgroundFile')
- assert.equal(rules.validateEnterpriseWebsite(' 无 '), '')
- assert.equal(rules.validateEnterpriseWebsite('https://example.com'), '')
- assert.match(rules.validateEnterpriseWebsite('ftp://example.com'), /http/)
- assert.equal(rules.validateBackgroundFile({ name: 'brief.PDF', size: 1 }), '')
- assert.match(rules.validateBackgroundFile({ name: 'brief.zip', size: 1 }), /类型/)
- assert.match(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 + 1 }), /50MB/)
- assert.equal(rules.validateBackgroundFile({ name: 'brief.pdf', size: 50 * 1024 * 1024 }), '')
- assert.equal(rules.validateBackgroundFile({ name: `${'a'.repeat(251)}.pdf`, size: 1 }), '')
- assert.match(rules.validateBackgroundFile({ name: `${'a'.repeat(252)}.pdf`, size: 1 }), /255/)
- assert.match(rules.validateBackgroundFile({ name: 'empty.pdf', size: 0 }), /非空/)
- const backendExpiry = '2026-07-21 15:30:00'
- const explicitOffsetExpiry = '2026-07-21T15:30:00+08:00'
- const utcExpiry = '2026-07-21T07:30:00.250Z'
- assert.equal(rules.parseSessionExpiresAt(backendExpiry), Date.UTC(2026, 6, 21, 7, 30, 0))
- assert.equal(rules.parseSessionExpiresAt(explicitOffsetExpiry), Date.UTC(2026, 6, 21, 7, 30, 0))
- assert.equal(rules.parseSessionExpiresAt(utcExpiry), Date.UTC(2026, 6, 21, 7, 30, 0, 250))
- assert.equal(rules.getSessionRemainingSeconds(backendExpiry, Date.UTC(2026, 6, 21, 7, 29, 59, 1)), 1)
- assert.equal(rules.getSessionRemainingSeconds(backendExpiry, Date.UTC(2026, 6, 21, 7, 30, 1)), 0)
- assert.equal(rules.getSessionRemainingSeconds('2026-07-21T15:30:00', 0), 0)
- assert.equal(rules.getSessionRemainingSeconds('not-a-date', 0), 0)
- const validQueueItem = rules.createBackgroundQueueItem(
- { name: 'brief.pdf', path: '/tmp/brief.pdf', size: 1 },
- 'valid-file'
- )
- const invalidQueueItem = rules.createBackgroundQueueItem(
- { name: 'archive.zip', path: '/tmp/archive.zip', size: 1 },
- 'invalid-file'
- )
- assert.equal(validQueueItem.status, 'pending')
- assert.equal(validQueueItem.progress, 0)
- assert.equal(validQueueItem.validationError, '')
- assert.equal(invalidQueueItem.status, 'failed')
- assert.match(invalidQueueItem.validationError, /类型/)
- validQueueItem.status = 'success'
- const uploadFailure = {
- id: 'upload-failure',
- status: 'failed',
- validationError: '',
- error: '上传失败'
- }
- assert.deepEqual(
- rules.getUploadableBackgroundFiles([validQueueItem, invalidQueueItem, uploadFailure]),
- [uploadFailure]
- )
- const originalTeam = {
- id: 12,
- teamName: '研发团队',
- enterpriseWebsite: ' https://example.com/team ',
- coachId: 3,
- uploaderId: 4,
- source: 'WECHAT'
- }
- const teamSubmitPayload = rules.createTeamSubmitPayload(originalTeam)
- assert.equal(teamSubmitPayload.id, 12)
- assert.equal(teamSubmitPayload.enterpriseWebsite, 'https://example.com/team')
- assert.equal(Object.hasOwn(teamSubmitPayload, 'coachId'), false)
- assert.equal(Object.hasOwn(teamSubmitPayload, 'uploaderId'), false)
- assert.equal(Object.hasOwn(teamSubmitPayload, 'source'), false)
- assert.equal(originalTeam.coachId, 3, 'payload creation must not mutate the displayed detail')
- const textPreviewState = rules.createTextPreviewState({
- mode: 'text',
- fileName: '背景说明.md',
- content: '# 团队背景',
- truncated: true
- }, 'fallback.md')
- assert.deepEqual(textPreviewState, {
- visible: true,
- fileName: '背景说明.md',
- content: '# 团队背景',
- truncated: true
- })
- assert.deepEqual(rules.createEmptyTextPreviewState(), {
- visible: false,
- fileName: '',
- content: '',
- truncated: false
- })
- assert.notEqual(rules.createEmptyTextPreviewState(), rules.createEmptyTextPreviewState())
- const creationState = rules.createTeamCreationState()
- assert.deepEqual(creationState, {
- createdTeamId: '',
- submitting: false,
- stage: 'idle',
- completed: false,
- teamPayloadSnapshot: '',
- teamQuestionnaireId: '',
- publishResult: null
- })
- const firstPayloadSnapshot = rules.createTeamPayloadSnapshot({
- teamName: '团队', enterpriseWebsite: ' https://example.com ', coachId: 7
- })
- const equivalentPayloadSnapshot = rules.createTeamPayloadSnapshot({
- enterpriseWebsite: 'https://example.com', teamName: '团队'
- })
- assert.equal(firstPayloadSnapshot, equivalentPayloadSnapshot)
- assert.notEqual(firstPayloadSnapshot, rules.createTeamPayloadSnapshot({
- teamName: '团队', enterpriseWebsite: 'https://changed.example.com'
- }))
- assert.equal(rules.beginTeamSubmission(creationState), true)
- assert.equal(rules.beginTeamSubmission(creationState), false, 'a second submission must be locked')
- assert.equal(rules.rememberCreatedTeam(creationState, 88), 88)
- assert.equal(rules.rememberCreatedTeam(creationState, 99), 88, 'the first server team id must be retained')
- rules.setTeamSubmissionStage(creationState, 'upload')
- assert.equal(creationState.stage, 'upload')
- rules.finishTeamSubmission(creationState)
- assert.equal(creationState.submitting, false)
- assert.equal(rules.beginTeamSubmission(creationState), true, 'a failed later stage can resume')
- rules.finishTeamSubmission(creationState, true)
- assert.equal(creationState.completed, true)
- assert.equal(rules.beginTeamSubmission(creationState), false, 'a completed flow cannot be scheduled twice')
- assert.equal(rules.hasUnresolvedBackgroundFiles([
- { status: 'success', validationError: '' },
- { status: 'failed', validationError: '文件类型不支持' }
- ]), false)
- for (const item of [
- { status: 'pending', validationError: '' },
- { status: 'uploading', validationError: '' },
- { status: 'failed', validationError: '' }
- ]) {
- assert.equal(rules.hasUnresolvedBackgroundFiles([item]), true)
- }
- console.log('team background rules: PASS')
- const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js')
- async function loadTransport({ api = {}, uni = {}, wx = {} } = {}) {
- const source = fs.readFileSync(transportPath, 'utf8')
- const context = vm.createContext({ uni, wx })
- const baseApiModule = new vm.SyntheticModule(['BaseApi'], function () {
- this.setExport('BaseApi', 'https://api.example.test/app')
- }, { context, identifier: 'test:baseApi' })
- const apiModule = new vm.SyntheticModule(['default'], function () {
- this.setExport('default', api)
- }, { context, identifier: 'test:api' })
- const transportModule = new vm.SourceTextModule(source, {
- context,
- identifier: pathToFileURL(transportPath).href
- })
- await transportModule.link(specifier => {
- if (specifier === './baseApi.js') return baseApiModule
- if (specifier === './index.js') return apiModule
- throw new Error(`unexpected transport import: ${specifier}`)
- })
- await transportModule.evaluate()
- return transportModule.namespace
- }
- function assertNoIdentityFields(value) {
- if (!value || typeof value !== 'object') return
- for (const [key, child] of Object.entries(value)) {
- assert.ok(!['source', 'coachId', 'uploaderId'].includes(key), `unexpected identity field: ${key}`)
- assertNoIdentityFields(child)
- }
- }
- async function createUploadHarness(response, progress = 37) {
- let request
- const progressValues = []
- const uni = {
- getStorageSync(key) {
- assert.equal(key, 'token')
- return 'token-123'
- },
- uploadFile(options) {
- request = options
- return {
- onProgressUpdate(callback) {
- callback({ progress })
- setImmediate(() => options.success(response))
- }
- }
- }
- }
- const transport = await loadTransport({ uni })
- return {
- request: () => request,
- progressValues,
- promise: transport.uploadTeamBackgroundFile(12, '/tmp/brief.pdf', value => progressValues.push(value))
- }
- }
- async function testUploadTransport() {
- const stringHarness = await createUploadHarness({
- statusCode: 200,
- data: JSON.stringify({ code: 0, data: { id: 7 } })
- })
- assert.equal((await stringHarness.promise).id, 7)
- assert.equal(stringHarness.request().url, 'https://api.example.test/app/core/user/team/12/background-files')
- assert.equal(stringHarness.request().filePath, '/tmp/brief.pdf')
- assert.equal(stringHarness.request().name, 'file')
- assert.equal(stringHarness.request().header.token, 'token-123')
- assert.deepEqual(stringHarness.progressValues, [37])
- assertNoIdentityFields(stringHarness.request())
- const objectHarness = await createUploadHarness({ statusCode: 200, data: { code: 0, data: { id: 8 } } })
- assert.equal((await objectHarness.promise).id, 8)
- const statusHarness = await createUploadHarness({ statusCode: 500, data: { code: 0 } })
- await assert.rejects(statusHarness.promise, /上传失败,请重试/)
- const codeHarness = await createUploadHarness({ statusCode: 200, data: { code: 9, msg: '上传业务失败' } })
- await assert.rejects(codeHarness.promise, /上传业务失败/)
- const invalidJsonHarness = await createUploadHarness({ statusCode: 200, data: '<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, timers = {}
- } = {}) {
- const context = vm.createContext({
- uni,
- wx,
- Date,
- Promise,
- String,
- Number,
- Boolean,
- Object,
- Array,
- setInterval: timers.setInterval || setInterval,
- clearInterval: timers.clearInterval || clearInterval,
- setTimeout: timers.setTimeout || setTimeout,
- clearTimeout: timers.clearTimeout || clearTimeout
- })
- const vueStub = new vm.SyntheticModule(['default'], function () {
- this.setExport('default', {})
- }, { context, identifier: 'test:vue-component' })
- const rulesStub = new vm.SyntheticModule(['default'], function () {
- this.setExport('default', apiRules)
- }, { context, identifier: 'test:team-background-rules' })
- const httpNames = [
- 'createTeamBackgroundSession',
- 'disableTeamBackgroundFile',
- 'downloadTeamBackgroundFile',
- 'listTeamBackgroundFiles',
- 'uploadTeamBackgroundFile'
- ]
- const httpStub = new vm.SyntheticModule(httpNames, function () {
- for (const name of httpNames) {
- this.setExport(name, http[name] || (() => Promise.resolve()))
- }
- }, { context, identifier: 'test:team-background-http' })
- const componentModule = new vm.SourceTextModule(readSfcScript(filePath), {
- context,
- identifier: pathToFileURL(filePath).href
- })
- await componentModule.link(specifier => {
- if (specifier === '@/http/teamBackgroundFile.js') return httpStub
- if (specifier === '@/utils/teamBackgroundFile.js') return rulesStub
- if (specifier.endsWith('.vue')) return vueStub
- throw new Error(`unexpected component import: ${specifier}`)
- })
- await componentModule.evaluate()
- return componentModule.namespace.default
- }
- function instantiateComponent(component, overrides = {}) {
- const base = Object.assign({ initialCount: 0 }, overrides)
- const data = typeof component.data === 'function' ? component.data.call(base) : {}
- const instance = Object.assign(base, data, overrides)
- for (const [name, method] of Object.entries(component.methods || {})) {
- instance[name] = method.bind(instance)
- }
- return instance
- }
- function createDeferred() {
- let resolve
- let reject
- const promise = new Promise((resolvePromise, rejectPromise) => {
- resolve = resolvePromise
- reject = rejectPromise
- })
- return { promise, resolve, reject }
- }
- function createFakeTimers() {
- let nextId = 1
- const pending = new Map()
- return {
- setTimeout(callback) {
- const id = nextId++
- pending.set(id, callback)
- return id
- },
- clearTimeout(id) {
- pending.delete(id)
- },
- count() {
- return pending.size
- },
- runNext() {
- const entry = pending.entries().next().value
- if (!entry) return false
- pending.delete(entry[0])
- entry[1]()
- return true
- },
- runAll() {
- while (this.runNext()) {}
- }
- }
- }
- async function flushPromises() {
- await Promise.resolve()
- await new Promise(resolve => setImmediate(resolve))
- }
- const backgroundComponentPath = path.join(__dirname, '../components/CusTeamBackgroundFiles/index.vue')
- const teamFillComponentPath = path.join(__dirname, '../components/CusTeamInfoFill/index.vue')
- const createTeamPagePath = path.join(__dirname, '../pagesPublish/fillTeamInfo.vue')
- const createListComponentPath = path.join(__dirname, '../pagesHome/components/createList.vue')
- const teamEditPagePath = path.join(__dirname, '../pagesMy/teamEdit.vue')
- const headerComponentPath = path.join(__dirname, '../components/CusHeader/index.vue')
- async function testDeferredPcSelectionSurvivesSessionFailure() {
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession: () => Promise.reject(new Error('session unavailable'))
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '',
- editable: true,
- boundTeamId: 12,
- pcUploadDeferred: true,
- $showToast() {},
- $emit() {}
- })
- await assert.rejects(instance.waitForDeferredPcUpload(12), /session unavailable/)
- assert.equal(instance.pcUploadDeferred, true)
- assert.equal(instance.pcDialogVisible, false)
- assert.equal(instance.deferredPcResolve, null)
- }
- async function testSessionRefreshFailureKeepsOldTimer() {
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession: () => Promise.reject(new Error('refresh failed'))
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, boundTeamId: 12, $showToast() {}, $emit() {}
- })
- const oldSession = { code: '111111', expiresAt: '2099-01-01 00:00:00' }
- const oldTimer = setInterval(() => {}, 60 * 1000)
- instance.session = oldSession
- instance.sessionTimer = oldTimer
- try {
- await assert.rejects(instance.openPcSession(12), /refresh failed/)
- assert.equal(instance.session, oldSession)
- assert.equal(instance.sessionTimer, oldTimer)
- } finally {
- clearInterval(oldTimer)
- instance.sessionTimer = null
- }
- }
- async function testPcSessionCreationIsSingleFlightPerTeam() {
- const firstRequest = createDeferred()
- let createCalls = 0
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession() {
- createCalls += 1
- return firstRequest.promise
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, $showToast() {}, $emit() {}
- })
- instance.resetTeamContext(12)
- const first = instance.openPcSession(12)
- const second = instance.openPcSession(12)
- assert.equal(createCalls, 1, 'same-team callers must share one session POST')
- assert.equal(instance.sessionCreating, true)
- instance.refreshPcSession()
- assert.equal(createCalls, 1, 'disabled regenerate must not create another POST')
- instance.resetTeamContext(13)
- assert.equal(instance.sessionCreating, false, 'creating state follows the active team')
- firstRequest.resolve({
- code: '123456', uploadUrl: 'https://upload.example/', expiresAt: '2099-01-01 00:00:00'
- })
- assert.equal(await first, null)
- assert.equal(await second, null)
- assert.equal(instance.session, null, 'a stale context must not display the resolved session')
- assert.equal(instance.pcDialogVisible, false)
- }
- async function testPcSessionSingleFlightSurvivesSameTeamReset() {
- const request = createDeferred()
- let createCalls = 0
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession() {
- createCalls += 1
- return request.promise
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, $showToast() {}, $emit() {}
- })
- instance.resetTeamContext(21)
- const staleCaller = instance.openPcSession(21)
- instance.resetTeamContext(21)
- assert.equal(instance.sessionCreating, true, 'same-team reset must retain the network lock')
- const currentCaller = instance.openPcSession(21)
- assert.equal(createCalls, 1, 'same-team reset must reuse the raw network promise')
- const rawSession = {
- code: '654321', uploadUrl: 'https://upload.example/current', expiresAt: '2099-01-01 00:00:00'
- }
- request.resolve(rawSession)
- assert.equal(await staleCaller, null)
- assert.equal(await currentCaller, rawSession)
- assert.equal(instance.session, rawSession)
- assert.equal(instance.pcDialogVisible, true)
- assert.equal(instance.sessionCreating, false)
- instance.closePcDialog()
- }
- async function testPcSessionFailureClearsSingleFlightForRetry() {
- let createCalls = 0
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession() {
- createCalls += 1
- if (createCalls === 1) return Promise.reject(new Error('session unavailable'))
- return Promise.resolve({
- code: '222222', uploadUrl: 'https://upload.example/retry', expiresAt: '2099-01-01 00:00:00'
- })
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, $showToast() {}, $emit() {}
- })
- instance.resetTeamContext(22)
- await assert.rejects(instance.openPcSession(22), /session unavailable/)
- assert.equal(instance.sessionCreating, false)
- const session = await instance.openPcSession(22)
- assert.equal(createCalls, 2)
- assert.equal(session.code, '222222')
- assert.equal(instance.sessionCreating, false)
- instance.closePcDialog()
- }
- async function testDeferredPcFinishWaitsForListRefresh() {
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- createTeamBackgroundSession: () => Promise.resolve({
- code: '123456',
- uploadUrl: 'https://upload.example/',
- expiresAt: '2099-01-01 00:00:00'
- }),
- listTeamBackgroundFiles: () => Promise.reject(new Error('list refresh failed'))
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '', editable: true, boundTeamId: 12, pcUploadDeferred: true,
- $showToast() {}, $emit() {}
- })
- const waiting = instance.waitForDeferredPcUpload(12)
- await new Promise(resolve => setImmediate(resolve))
- assert.equal(typeof instance.deferredPcResolve, 'function')
- await instance.finishDeferredPcUpload()
- await assert.rejects(waiting, /list refresh failed/)
- }
- async function testTeamContextResetAndUploadIsolation() {
- const uploads = []
- const component = await loadVueComponent(backgroundComponentPath, {
- http: {
- uploadTeamBackgroundFile(teamId, filePath, onProgress) {
- const deferred = createDeferred()
- uploads.push({ teamId, filePath, onProgress, deferred })
- return deferred.promise
- },
- listTeamBackgroundFiles: () => Promise.resolve([])
- }
- })
- let resolverCalls = 0
- const instance = instantiateComponent(component, {
- teamId: '',
- editable: true,
- $showToast() {},
- $emit() {}
- })
- instance.resetTeamContext(1)
- const oldItem = rules.createBackgroundQueueItem(
- { name: 'old.pdf', path: '/tmp/old.pdf', size: 1 }, 'old'
- )
- instance.queue.push(oldItem)
- instance.deferredPcResolve = () => { resolverCalls += 1 }
- const oldFlush = instance.flushPendingFiles(1)
- await new Promise(resolve => setImmediate(resolve))
- assert.equal(uploads.length, 1)
- instance.files = [{ id: 11, fileName: 'old.pdf' }]
- instance.count = 1
- instance.filesReady = true
- instance.session = { code: '111111' }
- instance.pcDialogVisible = true
- instance.textPreview = rules.createTextPreviewState({ fileName: 'old.txt', content: 'old' })
- instance.resetTeamContext(2)
- assert.equal(instance.boundTeamId, 2)
- assert.equal(instance.files.length, 0)
- assert.equal(instance.count, 0)
- assert.equal(instance.queue.length, 0)
- assert.equal(instance.filesReady, false)
- assert.equal(instance.session, null)
- assert.equal(instance.pcDialogVisible, false)
- assert.equal(instance.textPreview.content, '')
- assert.equal(resolverCalls, 1)
- const newItem = rules.createBackgroundQueueItem(
- { name: 'new.pdf', path: '/tmp/new.pdf', size: 1 }, 'new'
- )
- instance.queue.push(newItem)
- const newFlush = instance.flushPendingFiles(2)
- await new Promise(resolve => setImmediate(resolve))
- assert.equal(uploads.length, 2, 'the new team must not reuse the old upload promise')
- assert.deepEqual(uploads.map(item => item.teamId), [1, 2])
- uploads[0].onProgress(75)
- assert.equal(oldItem.progress, 0, 'stale upload progress must not mutate the old item after reset')
- uploads[0].deferred.resolve({ id: 101 })
- await oldFlush
- assert.equal(newItem.status, 'uploading')
- uploads[1].deferred.resolve({ id: 202 })
- await newFlush
- assert.equal(newItem.status, 'success')
- }
- async function testFileActionsWaitForCurrentListAndDocumentContext() {
- const listDeferred = createDeferred()
- const downloadDeferred = createDeferred()
- const opened = []
- const toasts = []
- let downloadCalls = 0
- const wx = {
- openDocument(options) {
- opened.push(options.filePath)
- setImmediate(() => options.success())
- }
- }
- const component = await loadVueComponent(backgroundComponentPath, {
- wx,
- http: {
- listTeamBackgroundFiles: () => listDeferred.promise,
- downloadTeamBackgroundFile: () => {
- downloadCalls += 1
- return downloadDeferred.promise
- }
- }
- })
- const instance = instantiateComponent(component, {
- teamId: '',
- editable: true,
- $showToast(message) { toasts.push(message) },
- $emit() {}
- })
- instance.resetTeamContext(3)
- const oldFile = { id: 31, fileName: 'old.pdf' }
- instance.files = [oldFile]
- instance.filesReady = true
- const refresh = instance.refreshFiles(3)
- await instance.previewFile(oldFile)
- assert.equal(downloadCalls, 0, 'old file ids must be disabled while the current list is loading')
- listDeferred.resolve([oldFile])
- await refresh
- const latePreview = instance.previewFile(oldFile)
- assert.equal(downloadCalls, 1)
- instance.resetTeamContext(4)
- downloadDeferred.resolve({ mode: 'document', fileName: 'old.pdf', filePath: '/tmp/old.pdf' })
- await latePreview
- assert.deepEqual(opened, [], 'a stale download must not open a native document')
- const validComponent = await loadVueComponent(backgroundComponentPath, {
- wx,
- http: {
- downloadTeamBackgroundFile: () => Promise.resolve({
- mode: 'document', fileName: 'current.pdf', filePath: '/tmp/current.pdf'
- })
- }
- })
- const validInstance = instantiateComponent(validComponent, {
- teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {}
- })
- validInstance.resetTeamContext(5)
- const currentFile = { id: 51, fileName: 'current.pdf' }
- validInstance.files = [currentFile]
- validInstance.filesReady = true
- await validInstance.previewFile(currentFile)
- assert.deepEqual(opened, ['/tmp/current.pdf'])
- const failingComponent = await loadVueComponent(backgroundComponentPath, {
- wx: {
- openDocument(options) { setImmediate(() => options.fail(new Error('open failed'))) }
- },
- http: {
- downloadTeamBackgroundFile: () => Promise.resolve({
- mode: 'document', fileName: 'broken.pdf', filePath: '/tmp/broken.pdf'
- })
- }
- })
- const failingInstance = instantiateComponent(failingComponent, {
- teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {}
- })
- failingInstance.resetTeamContext(6)
- const brokenFile = { id: 61, fileName: 'broken.pdf' }
- failingInstance.files = [brokenFile]
- failingInstance.filesReady = true
- await failingInstance.previewFile(brokenFile)
- assert.ok(toasts.some(message => String(message).includes('open failed')))
- }
- async function testTeamFillFlushesBeforeEmitAndLocks() {
- const component = await loadVueComponent(teamFillComponentPath)
- const flushDeferred = createDeferred()
- let flushCalls = 0
- let emitCalls = 0
- const instance = instantiateComponent(component, {
- teamId: 12,
- qtype: false,
- $props: { qtype: false },
- $showToast() {},
- $emit() { emitCalls += 1 },
- $refs: {
- backgroundRef: {
- flushPendingFiles() { flushCalls += 1; return flushDeferred.promise },
- hasUnresolvedFiles() { return false }
- }
- }
- })
- instance.teamInfo = {
- teamName: '团队', enterpriseName: '公司', enterpriseWebsite: '无',
- districtId: 1, industryId: 2, functionIds: [], orgIds: []
- }
- const first = instance.handleConfirm()
- const second = instance.handleConfirm()
- assert.equal(flushCalls, 1)
- assert.equal(emitCalls, 0)
- flushDeferred.resolve({ success: 1, failed: 0 })
- await Promise.all([first, second])
- assert.equal(emitCalls, 1)
- }
- async function testCreatedTeamResumeAndSubmitLock() {
- const component = await loadVueComponent(createTeamPagePath)
- let teamPostCalls = 0
- let flushCalls = 0
- const toasts = []
- const instance = instantiateComponent(component, {
- $api: {
- post(url) {
- assert.equal(url, '/core/user/team')
- teamPostCalls += 1
- return Promise.resolve({ data: { code: 0, data: { teamId: 88 } } })
- }
- },
- $showToast(message) { toasts.push(message) },
- $showModal: () => Promise.resolve(),
- $refs: {
- teamRef: {
- flushBackgroundFiles() {
- flushCalls += 1
- if (flushCalls === 1) return Promise.reject(new Error('列表刷新失败'))
- return Promise.resolve({ success: 0, failed: 0 })
- },
- hasUnresolvedBackgroundFiles: () => false,
- hasDeferredPcUpload: () => false
- }
- }
- })
- let continuations = 0
- instance.continueAfterTeamCreated = async teamId => {
- assert.equal(teamId, 88)
- continuations += 1
- }
- await instance.handleConfirm({ teamName: '团队' })
- assert.equal(teamPostCalls, 1)
- assert.equal(instance.creationState.createdTeamId, 88)
- assert.ok(toasts.some(message => String(message).includes('背景资料')))
- await instance.handleConfirm({ teamName: '团队' })
- assert.equal(teamPostCalls, 1, 'retry after a later-stage failure must not POST a second team')
- assert.equal(continuations, 1)
- const lockedPost = createDeferred()
- const lockedInstance = instantiateComponent(component, {
- $api: { post: () => { teamPostCalls += 1; return lockedPost.promise } },
- $showToast() {},
- $showModal: () => Promise.resolve(),
- $refs: { teamRef: {} }
- })
- const lockedFirst = lockedInstance.handleConfirm({ teamName: '团队' })
- const callsAfterFirst = teamPostCalls
- const lockedSecond = lockedInstance.handleConfirm({ teamName: '团队' })
- assert.equal(teamPostCalls, callsAfterFirst, 'concurrent submission must be ignored')
- lockedPost.resolve({ data: { code: 1, msg: '保存接口失败' } })
- await Promise.all([lockedFirst, lockedSecond])
- }
- async function testCreatedTeamNavigationIsAwaitableAndResumable() {
- const timers = createFakeTimers()
- let navigationOptions
- let teamPostCalls = 0
- let publishCalls = 0
- let teamPutCalls = 0
- let flushCalls = 0
- const toasts = []
- const uni = {
- getStorageSync(key) {
- assert.equal(key, 'userInfo')
- return JSON.stringify({ id: 7 })
- },
- removeStorageSync() {},
- navigateTo(options) { navigationOptions = options }
- }
- const component = await loadVueComponent(createTeamPagePath, { uni, timers })
- const originalFormat = Date.prototype.Format
- Date.prototype.Format = () => '2026-07-21 12:00:00'
- try {
- const instance = instantiateComponent(component, {
- next: '1', questionnaireId: 66, type: 'survey', title: '问卷',
- $api: {
- post(url, payload) {
- if (url === '/core/user/team') {
- teamPostCalls += 1
- assert.equal(payload.enterpriseWebsite, 'https://old.example')
- return Promise.resolve({ data: { code: 0, data: { teamId: 88 } } })
- }
- assert.equal(url, '/core/team/questionnaire/publish')
- assert.deepEqual(Object.keys(payload).sort(), [
- 'answerSetting', 'coachId', 'endTime', 'questionnaireId',
- 'startTime', 'teamId', 'type'
- ].sort())
- publishCalls += 1
- return Promise.resolve({ data: { code: 0, data: 321 } })
- },
- put(url, payload) {
- teamPutCalls += 1
- assert.equal(url, '/core/user/team')
- assert.equal(payload.id, 88)
- assert.equal(payload.enterpriseWebsite, 'https://new.example')
- return Promise.resolve({ data: { code: 0, data: true } })
- }
- },
- $showToast(message) { toasts.push(message) },
- $showModal: () => Promise.resolve(),
- $refs: {
- teamRef: {
- flushBackgroundFiles() { flushCalls += 1; return Promise.resolve({ success: 0, failed: 0 }) },
- hasUnresolvedBackgroundFiles: () => false,
- hasDeferredPcUpload: () => false
- }
- }
- })
- const first = instance.handleConfirm({
- teamName: '团队', enterpriseWebsite: 'https://old.example'
- })
- await flushPromises()
- assert.equal(teamPostCalls, 1)
- assert.equal(publishCalls, 1)
- assert.equal(flushCalls, 1)
- assert.equal(instance.creationState.teamQuestionnaireId, 321)
- assert.equal(instance.creationState.completed, false)
- assert.equal(instance.creationState.submitting, true)
- assert.equal(timers.count(), 1)
- timers.runNext()
- await flushPromises()
- assert.ok(navigationOptions)
- assert.equal(instance.creationState.completed, false, 'navigation callback must settle first')
- navigationOptions.fail({ errMsg: 'navigateTo:fail route missing' })
- await first
- assert.equal(instance.creationState.completed, false)
- assert.equal(instance.creationState.submitting, false)
- assert.equal(instance.creationState.stage, 'navigate')
- assert.ok(toasts.some(message => String(message).includes('页面跳转失败')))
- navigationOptions = null
- const retry = instance.handleConfirm({
- teamName: '团队', enterpriseWebsite: 'https://new.example'
- })
- await flushPromises()
- assert.equal(teamPostCalls, 1, 'retry must reuse the created team id')
- assert.equal(publishCalls, 1, 'retry after publish success must not publish again')
- assert.equal(flushCalls, 1, 'navigate-stage retry must not repeat file processing')
- assert.equal(teamPutCalls, 1, 'changed sanitized payload must update the same team')
- assert.equal(timers.count(), 1)
- timers.runNext()
- await flushPromises()
- assert.ok(navigationOptions)
- assert.equal(instance.creationState.completed, false)
- navigationOptions.success({})
- await retry
- assert.equal(instance.creationState.completed, true)
- assert.equal(instance.creationState.submitting, false)
- } finally {
- if (originalFormat) Date.prototype.Format = originalFormat
- else delete Date.prototype.Format
- }
- }
- async function testCreatedTeamUnloadCancelsLateNavigation() {
- const timers = createFakeTimers()
- let navigateBackCalls = 0
- const uni = {
- navigateBack() { navigateBackCalls += 1 }
- }
- const component = await loadVueComponent(createTeamPagePath, { uni, timers })
- const instance = instantiateComponent(component, {
- $api: {
- post: () => Promise.resolve({ data: { code: 0, data: { teamId: 89 } } })
- },
- $showToast() {},
- $showModal: () => Promise.resolve(),
- getOpenerEventChannel: () => ({ emit() {} }),
- $refs: {
- teamRef: {
- flushBackgroundFiles: () => Promise.resolve({ success: 0, failed: 0 }),
- hasUnresolvedBackgroundFiles: () => false,
- hasDeferredPcUpload: () => false
- }
- }
- })
- const submission = instance.handleConfirm({ teamName: '团队', enterpriseWebsite: '无' })
- await flushPromises()
- assert.equal(timers.count(), 1)
- component.onUnload.call(instance)
- assert.equal(timers.count(), 0)
- timers.runAll()
- await submission
- assert.equal(navigateBackCalls, 0)
- assert.equal(instance.creationState.completed, false)
- assert.equal(instance.creationState.submitting, false)
- }
- async function testTeamEditRedirectIsLockedAndResumable() {
- const timers = createFakeTimers()
- let redirectOptions
- let putCalls = 0
- let navigateBackCalls = 0
- const toasts = []
- const uni = {
- redirectTo(options) { redirectOptions = options },
- navigateBack() { navigateBackCalls += 1 }
- }
- const component = await loadVueComponent(teamEditPagePath, { uni, timers })
- const instance = instantiateComponent(component, {
- submitDto: { id: 12, teamName: '团队' },
- show: true,
- $api: {
- put(url, payload) {
- putCalls += 1
- assert.equal(url, '/core/user/team')
- assert.equal(payload.id, 12)
- return Promise.resolve({ data: { code: 0, data: true } })
- }
- },
- $showToast(message) { toasts.push(message) }
- })
- const first = instance.editConfirm()
- await flushPromises()
- assert.equal(putCalls, 1)
- assert.equal(instance.saveSucceeded, true)
- assert.equal(instance.saving, true, 'save lock must cover the redirect delay')
- assert.equal(timers.count(), 1)
- instance.editConfirm()
- instance.requestBack()
- assert.equal(putCalls, 1, 'confirm during redirect delay must not PUT again')
- assert.equal(navigateBackCalls, 0, 'back during redirect delay must stay locked')
- assert.ok(toasts.some(message => String(message).includes('正在保存')))
- timers.runNext()
- await flushPromises()
- assert.ok(redirectOptions)
- assert.equal(instance.saving, true, 'redirect callback must settle before unlock')
- redirectOptions.fail({ errMsg: 'redirectTo:fail route missing' })
- await first
- assert.equal(instance.saving, false)
- assert.equal(instance.saveSucceeded, true)
- assert.equal(instance.saveStage, 'navigate')
- redirectOptions = null
- const retry = instance.editConfirm()
- await flushPromises()
- assert.equal(putCalls, 1, 'redirect retry must reuse the successful PUT')
- assert.equal(instance.saving, true)
- assert.equal(timers.count(), 1)
- timers.runNext()
- await flushPromises()
- assert.ok(redirectOptions)
- redirectOptions.success({})
- await retry
- assert.equal(putCalls, 1)
- assert.equal(instance.saving, false)
- }
- async function testTeamEditUnloadCancelsLateRedirect() {
- const timers = createFakeTimers()
- let redirectCalls = 0
- const component = await loadVueComponent(teamEditPagePath, {
- timers,
- uni: { redirectTo() { redirectCalls += 1 } }
- })
- const instance = instantiateComponent(component, {
- submitDto: { id: 12 },
- $api: { put: () => Promise.resolve({ data: { code: 0, data: true } }) },
- $showToast() {}
- })
- const saving = instance.editConfirm()
- await flushPromises()
- assert.equal(timers.count(), 1)
- component.onUnload.call(instance)
- assert.equal(timers.count(), 0)
- timers.runAll()
- await saving
- assert.equal(redirectCalls, 0)
- assert.equal(instance.saving, false)
- }
- async function testCloseAndBackRequirePendingUploadConfirmation() {
- let modalOptions
- let navigateBackCalls = 0
- const uni = {
- showModal(options) { modalOptions = options },
- navigateBack() { navigateBackCalls += 1 }
- }
- const createList = await loadVueComponent(createListComponentPath, { uni })
- const createListInstance = instantiateComponent(createList, {
- $imgBase: 'https://image.example/',
- teamInfoShow: true,
- $refs: {
- teamRef: {
- hasUnresolvedBackgroundFiles: () => true,
- isBackgroundUploadActive: () => false
- }
- }
- })
- createListInstance.requestCloseTeamInfo()
- assert.equal(createListInstance.teamInfoShow, true)
- modalOptions.success({ confirm: false })
- assert.equal(createListInstance.teamInfoShow, true)
- createListInstance.requestCloseTeamInfo()
- modalOptions.success({ confirm: true })
- assert.equal(createListInstance.teamInfoShow, false)
- const teamEdit = await loadVueComponent(teamEditPagePath, { uni })
- const teamEditInstance = instantiateComponent(teamEdit, {
- $refs: {
- teamRef: {
- hasUnresolvedBackgroundFiles: () => true,
- isBackgroundUploadActive: () => true
- }
- }
- })
- teamEditInstance.requestBack()
- assert.equal(navigateBackCalls, 0)
- modalOptions.success({ confirm: false })
- assert.equal(navigateBackCalls, 0)
- teamEditInstance.requestBack()
- modalOptions.success({ confirm: true })
- assert.equal(navigateBackCalls, 1)
- let headerBackEvents = 0
- const header = await loadVueComponent(headerComponentPath, { uni })
- const headerInstance = instantiateComponent(header, {
- interceptBack: true,
- $emit(name) { if (name === 'back') headerBackEvents += 1 }
- })
- headerInstance.toBack('')
- assert.equal(headerBackEvents, 1)
- assert.equal(navigateBackCalls, 1, 'intercepted header back must not navigate directly')
- }
- async function main() {
- const loadedTransport = await loadTransport()
- assert.equal(typeof loadedTransport.downloadTeamBackgroundFile, 'function')
- await testUploadTransport()
- await testApiTransport()
- await testDownloadTransport()
- await testDeferredPcSelectionSurvivesSessionFailure()
- await testSessionRefreshFailureKeepsOldTimer()
- await testPcSessionCreationIsSingleFlightPerTeam()
- await testPcSessionSingleFlightSurvivesSameTeamReset()
- await testPcSessionFailureClearsSingleFlightForRetry()
- await testDeferredPcFinishWaitsForListRefresh()
- await testTeamContextResetAndUploadIsolation()
- await testFileActionsWaitForCurrentListAndDocumentContext()
- await testTeamFillFlushesBeforeEmitAndLocks()
- await testCreatedTeamResumeAndSubmitLock()
- await testCreatedTeamNavigationIsAwaitableAndResumable()
- await testCreatedTeamUnloadCancelsLateNavigation()
- await testTeamEditRedirectIsLockedAndResumable()
- await testTeamEditUnloadCancelsLateRedirect()
- await testCloseAndBackRequirePendingUploadConfirmation()
- console.log('team background transport: PASS')
- console.log('team background component behavior: PASS')
- }
- main().catch(error => {
- console.error(error)
- process.exitCode = 1
- })
|