|
@@ -88,6 +88,38 @@ assert.deepEqual(rules.createEmptyTextPreviewState(), {
|
|
|
})
|
|
})
|
|
|
assert.notEqual(rules.createEmptyTextPreviewState(), rules.createEmptyTextPreviewState())
|
|
assert.notEqual(rules.createEmptyTextPreviewState(), rules.createEmptyTextPreviewState())
|
|
|
|
|
|
|
|
|
|
+const creationState = rules.createTeamCreationState()
|
|
|
|
|
+assert.deepEqual(creationState, {
|
|
|
|
|
+ createdTeamId: '',
|
|
|
|
|
+ submitting: false,
|
|
|
|
|
+ stage: 'idle',
|
|
|
|
|
+ completed: false
|
|
|
|
|
+})
|
|
|
|
|
+assert.equal(rules.beginTeamSubmission(creationState), true)
|
|
|
|
|
+assert.equal(rules.beginTeamSubmission(creationState), false, 'a second submission must be locked')
|
|
|
|
|
+assert.equal(rules.rememberCreatedTeam(creationState, 88), 88)
|
|
|
|
|
+assert.equal(rules.rememberCreatedTeam(creationState, 99), 88, 'the first server team id must be retained')
|
|
|
|
|
+rules.setTeamSubmissionStage(creationState, 'upload')
|
|
|
|
|
+assert.equal(creationState.stage, 'upload')
|
|
|
|
|
+rules.finishTeamSubmission(creationState)
|
|
|
|
|
+assert.equal(creationState.submitting, false)
|
|
|
|
|
+assert.equal(rules.beginTeamSubmission(creationState), true, 'a failed later stage can resume')
|
|
|
|
|
+rules.finishTeamSubmission(creationState, true)
|
|
|
|
|
+assert.equal(creationState.completed, true)
|
|
|
|
|
+assert.equal(rules.beginTeamSubmission(creationState), false, 'a completed flow cannot be scheduled twice')
|
|
|
|
|
+
|
|
|
|
|
+assert.equal(rules.hasUnresolvedBackgroundFiles([
|
|
|
|
|
+ { status: 'success', validationError: '' },
|
|
|
|
|
+ { status: 'failed', validationError: '文件类型不支持' }
|
|
|
|
|
+]), false)
|
|
|
|
|
+for (const item of [
|
|
|
|
|
+ { status: 'pending', validationError: '' },
|
|
|
|
|
+ { status: 'uploading', validationError: '' },
|
|
|
|
|
+ { status: 'failed', validationError: '' }
|
|
|
|
|
+]) {
|
|
|
|
|
+ assert.equal(rules.hasUnresolvedBackgroundFiles([item]), true)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
console.log('team background rules: PASS')
|
|
console.log('team background rules: PASS')
|
|
|
|
|
|
|
|
const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js')
|
|
const transportPath = path.join(__dirname, '../http/teamBackgroundFile.js')
|
|
@@ -304,11 +336,13 @@ async function testDownloadTransport() {
|
|
|
'CONTENT-disPOSITION': 'attachment; filename="download.pdf"'
|
|
'CONTENT-disPOSITION': 'attachment; filename="download.pdf"'
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- assert.equal(await successHarness.promise, 'opened')
|
|
|
|
|
|
|
+ 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().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.request().header.token, 'token-456')
|
|
|
- assert.equal(successHarness.openRequest().filePath, '/tmp/download.pdf')
|
|
|
|
|
- assert.equal(successHarness.openRequest().showMenu, true)
|
|
|
|
|
|
|
+ assert.equal(successHarness.openRequest(), undefined, 'transport must not open a document before component context checks')
|
|
|
assertNoIdentityFields(successHarness.request())
|
|
assertNoIdentityFields(successHarness.request())
|
|
|
|
|
|
|
|
const resultHeaderHarness = await createDownloadHarness({
|
|
const resultHeaderHarness = await createDownloadHarness({
|
|
@@ -322,7 +356,11 @@ async function testDownloadTransport() {
|
|
|
},
|
|
},
|
|
|
withHeadersListener: false
|
|
withHeadersListener: false
|
|
|
})
|
|
})
|
|
|
- assert.equal(await resultHeaderHarness.promise, 'opened')
|
|
|
|
|
|
|
+ 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)
|
|
assert.equal(resultHeaderHarness.fileSystemManagerCalls(), 0)
|
|
|
|
|
|
|
|
const txtHarness = await createDownloadHarness({
|
|
const txtHarness = await createDownloadHarness({
|
|
@@ -351,6 +389,35 @@ async function testDownloadTransport() {
|
|
|
assert.equal(txtHarness.readRequest().position, 0)
|
|
assert.equal(txtHarness.readRequest().position, 0)
|
|
|
assert.equal(txtHarness.readRequest().length, 5)
|
|
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 mdContent = 'm'.repeat(TEXT_PREVIEW_LIMIT)
|
|
|
const mdHarness = await createDownloadHarness({
|
|
const mdHarness = await createDownloadHarness({
|
|
|
result: { statusCode: 200, tempFilePath: '/tmp/guide.md' },
|
|
result: { statusCode: 200, tempFilePath: '/tmp/guide.md' },
|
|
@@ -457,16 +524,451 @@ async function testDownloadTransport() {
|
|
|
const downloadFailureHarness = await createDownloadHarness({ downloadError })
|
|
const downloadFailureHarness = await createDownloadHarness({ downloadError })
|
|
|
await assert.rejects(downloadFailureHarness.promise, /download network error/)
|
|
await assert.rejects(downloadFailureHarness.promise, /download network error/)
|
|
|
|
|
|
|
|
- const openError = new Error('open document error')
|
|
|
|
|
- const openFailureHarness = await createDownloadHarness({
|
|
|
|
|
|
|
+ const noTransportOpenHarness = await createDownloadHarness({
|
|
|
result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
|
|
result: { statusCode: 200, tempFilePath: '/tmp/download.pdf' },
|
|
|
responseHeaders: {
|
|
responseHeaders: {
|
|
|
'Content-Type': 'application/pdf',
|
|
'Content-Type': 'application/pdf',
|
|
|
'Content-Disposition': 'attachment; filename="download.pdf"'
|
|
'Content-Disposition': 'attachment; filename="download.pdf"'
|
|
|
},
|
|
},
|
|
|
- openError
|
|
|
|
|
|
|
+ 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 } = {}) {
|
|
|
|
|
+ const context = vm.createContext({
|
|
|
|
|
+ uni,
|
|
|
|
|
+ wx,
|
|
|
|
|
+ Date,
|
|
|
|
|
+ Promise,
|
|
|
|
|
+ String,
|
|
|
|
|
+ Number,
|
|
|
|
|
+ Boolean,
|
|
|
|
|
+ Object,
|
|
|
|
|
+ Array,
|
|
|
|
|
+ setInterval,
|
|
|
|
|
+ clearInterval
|
|
|
|
|
+ })
|
|
|
|
|
+ const vueStub = new vm.SyntheticModule(['default'], function () {
|
|
|
|
|
+ this.setExport('default', {})
|
|
|
|
|
+ }, { context, identifier: 'test:vue-component' })
|
|
|
|
|
+ const rulesStub = new vm.SyntheticModule(['default'], function () {
|
|
|
|
|
+ this.setExport('default', apiRules)
|
|
|
|
|
+ }, { context, identifier: 'test:team-background-rules' })
|
|
|
|
|
+ const httpNames = [
|
|
|
|
|
+ 'createTeamBackgroundSession',
|
|
|
|
|
+ 'disableTeamBackgroundFile',
|
|
|
|
|
+ 'downloadTeamBackgroundFile',
|
|
|
|
|
+ 'listTeamBackgroundFiles',
|
|
|
|
|
+ 'uploadTeamBackgroundFile'
|
|
|
|
|
+ ]
|
|
|
|
|
+ const httpStub = new vm.SyntheticModule(httpNames, function () {
|
|
|
|
|
+ for (const name of httpNames) {
|
|
|
|
|
+ this.setExport(name, http[name] || (() => Promise.resolve()))
|
|
|
|
|
+ }
|
|
|
|
|
+ }, { context, identifier: 'test:team-background-http' })
|
|
|
|
|
+ const componentModule = new vm.SourceTextModule(readSfcScript(filePath), {
|
|
|
|
|
+ context,
|
|
|
|
|
+ identifier: pathToFileURL(filePath).href
|
|
|
|
|
+ })
|
|
|
|
|
+ await componentModule.link(specifier => {
|
|
|
|
|
+ if (specifier === '@/http/teamBackgroundFile.js') return httpStub
|
|
|
|
|
+ if (specifier === '@/utils/teamBackgroundFile.js') return rulesStub
|
|
|
|
|
+ if (specifier.endsWith('.vue')) return vueStub
|
|
|
|
|
+ throw new Error(`unexpected component import: ${specifier}`)
|
|
|
|
|
+ })
|
|
|
|
|
+ await componentModule.evaluate()
|
|
|
|
|
+ return componentModule.namespace.default
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function instantiateComponent(component, overrides = {}) {
|
|
|
|
|
+ const base = Object.assign({ initialCount: 0 }, overrides)
|
|
|
|
|
+ const data = typeof component.data === 'function' ? component.data.call(base) : {}
|
|
|
|
|
+ const instance = Object.assign(base, data, overrides)
|
|
|
|
|
+ for (const [name, method] of Object.entries(component.methods || {})) {
|
|
|
|
|
+ instance[name] = method.bind(instance)
|
|
|
|
|
+ }
|
|
|
|
|
+ return instance
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createDeferred() {
|
|
|
|
|
+ let resolve
|
|
|
|
|
+ let reject
|
|
|
|
|
+ const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
|
|
|
+ resolve = resolvePromise
|
|
|
|
|
+ reject = rejectPromise
|
|
|
|
|
+ })
|
|
|
|
|
+ return { promise, resolve, reject }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const backgroundComponentPath = path.join(__dirname, '../components/CusTeamBackgroundFiles/index.vue')
|
|
|
|
|
+const teamFillComponentPath = path.join(__dirname, '../components/CusTeamInfoFill/index.vue')
|
|
|
|
|
+const createTeamPagePath = path.join(__dirname, '../pagesPublish/fillTeamInfo.vue')
|
|
|
|
|
+const createListComponentPath = path.join(__dirname, '../pagesHome/components/createList.vue')
|
|
|
|
|
+const teamEditPagePath = path.join(__dirname, '../pagesMy/teamEdit.vue')
|
|
|
|
|
+const headerComponentPath = path.join(__dirname, '../components/CusHeader/index.vue')
|
|
|
|
|
+
|
|
|
|
|
+async function testDeferredPcSelectionSurvivesSessionFailure() {
|
|
|
|
|
+ const component = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ http: {
|
|
|
|
|
+ createTeamBackgroundSession: () => Promise.reject(new Error('session unavailable'))
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: '',
|
|
|
|
|
+ editable: true,
|
|
|
|
|
+ boundTeamId: 12,
|
|
|
|
|
+ pcUploadDeferred: true,
|
|
|
|
|
+ $showToast() {},
|
|
|
|
|
+ $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ await assert.rejects(instance.waitForDeferredPcUpload(12), /session unavailable/)
|
|
|
|
|
+ assert.equal(instance.pcUploadDeferred, true)
|
|
|
|
|
+ assert.equal(instance.pcDialogVisible, false)
|
|
|
|
|
+ assert.equal(instance.deferredPcResolve, null)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testSessionRefreshFailureKeepsOldTimer() {
|
|
|
|
|
+ const component = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ http: {
|
|
|
|
|
+ createTeamBackgroundSession: () => Promise.reject(new Error('refresh failed'))
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: '', editable: true, boundTeamId: 12, $showToast() {}, $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ const oldSession = { code: '111111', expiresAt: '2099-01-01 00:00:00' }
|
|
|
|
|
+ const oldTimer = setInterval(() => {}, 60 * 1000)
|
|
|
|
|
+ instance.session = oldSession
|
|
|
|
|
+ instance.sessionTimer = oldTimer
|
|
|
|
|
+ try {
|
|
|
|
|
+ await assert.rejects(instance.openPcSession(12), /refresh failed/)
|
|
|
|
|
+ assert.equal(instance.session, oldSession)
|
|
|
|
|
+ assert.equal(instance.sessionTimer, oldTimer)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ clearInterval(oldTimer)
|
|
|
|
|
+ instance.sessionTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testDeferredPcFinishWaitsForListRefresh() {
|
|
|
|
|
+ const component = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ http: {
|
|
|
|
|
+ createTeamBackgroundSession: () => Promise.resolve({
|
|
|
|
|
+ code: '123456',
|
|
|
|
|
+ uploadUrl: 'https://upload.example/',
|
|
|
|
|
+ expiresAt: '2099-01-01 00:00:00'
|
|
|
|
|
+ }),
|
|
|
|
|
+ listTeamBackgroundFiles: () => Promise.reject(new Error('list refresh failed'))
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: '', editable: true, boundTeamId: 12, pcUploadDeferred: true,
|
|
|
|
|
+ $showToast() {}, $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ const waiting = instance.waitForDeferredPcUpload(12)
|
|
|
|
|
+ await new Promise(resolve => setImmediate(resolve))
|
|
|
|
|
+ assert.equal(typeof instance.deferredPcResolve, 'function')
|
|
|
|
|
+ await instance.finishDeferredPcUpload()
|
|
|
|
|
+ await assert.rejects(waiting, /list refresh failed/)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testTeamContextResetAndUploadIsolation() {
|
|
|
|
|
+ const uploads = []
|
|
|
|
|
+ const component = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ http: {
|
|
|
|
|
+ uploadTeamBackgroundFile(teamId, filePath, onProgress) {
|
|
|
|
|
+ const deferred = createDeferred()
|
|
|
|
|
+ uploads.push({ teamId, filePath, onProgress, deferred })
|
|
|
|
|
+ return deferred.promise
|
|
|
|
|
+ },
|
|
|
|
|
+ listTeamBackgroundFiles: () => Promise.resolve([])
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ let resolverCalls = 0
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: '',
|
|
|
|
|
+ editable: true,
|
|
|
|
|
+ $showToast() {},
|
|
|
|
|
+ $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ instance.resetTeamContext(1)
|
|
|
|
|
+ const oldItem = rules.createBackgroundQueueItem(
|
|
|
|
|
+ { name: 'old.pdf', path: '/tmp/old.pdf', size: 1 }, 'old'
|
|
|
|
|
+ )
|
|
|
|
|
+ instance.queue.push(oldItem)
|
|
|
|
|
+ instance.deferredPcResolve = () => { resolverCalls += 1 }
|
|
|
|
|
+ const oldFlush = instance.flushPendingFiles(1)
|
|
|
|
|
+ await new Promise(resolve => setImmediate(resolve))
|
|
|
|
|
+ assert.equal(uploads.length, 1)
|
|
|
|
|
+
|
|
|
|
|
+ instance.files = [{ id: 11, fileName: 'old.pdf' }]
|
|
|
|
|
+ instance.count = 1
|
|
|
|
|
+ instance.filesReady = true
|
|
|
|
|
+ instance.session = { code: '111111' }
|
|
|
|
|
+ instance.pcDialogVisible = true
|
|
|
|
|
+ instance.textPreview = rules.createTextPreviewState({ fileName: 'old.txt', content: 'old' })
|
|
|
|
|
+ instance.resetTeamContext(2)
|
|
|
|
|
+ assert.equal(instance.boundTeamId, 2)
|
|
|
|
|
+ assert.equal(instance.files.length, 0)
|
|
|
|
|
+ assert.equal(instance.count, 0)
|
|
|
|
|
+ assert.equal(instance.queue.length, 0)
|
|
|
|
|
+ assert.equal(instance.filesReady, false)
|
|
|
|
|
+ assert.equal(instance.session, null)
|
|
|
|
|
+ assert.equal(instance.pcDialogVisible, false)
|
|
|
|
|
+ assert.equal(instance.textPreview.content, '')
|
|
|
|
|
+ assert.equal(resolverCalls, 1)
|
|
|
|
|
+
|
|
|
|
|
+ const newItem = rules.createBackgroundQueueItem(
|
|
|
|
|
+ { name: 'new.pdf', path: '/tmp/new.pdf', size: 1 }, 'new'
|
|
|
|
|
+ )
|
|
|
|
|
+ instance.queue.push(newItem)
|
|
|
|
|
+ const newFlush = instance.flushPendingFiles(2)
|
|
|
|
|
+ await new Promise(resolve => setImmediate(resolve))
|
|
|
|
|
+ assert.equal(uploads.length, 2, 'the new team must not reuse the old upload promise')
|
|
|
|
|
+ assert.deepEqual(uploads.map(item => item.teamId), [1, 2])
|
|
|
|
|
+ uploads[0].onProgress(75)
|
|
|
|
|
+ assert.equal(oldItem.progress, 0, 'stale upload progress must not mutate the old item after reset')
|
|
|
|
|
+ uploads[0].deferred.resolve({ id: 101 })
|
|
|
|
|
+ await oldFlush
|
|
|
|
|
+ assert.equal(newItem.status, 'uploading')
|
|
|
|
|
+ uploads[1].deferred.resolve({ id: 202 })
|
|
|
|
|
+ await newFlush
|
|
|
|
|
+ assert.equal(newItem.status, 'success')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testFileActionsWaitForCurrentListAndDocumentContext() {
|
|
|
|
|
+ const listDeferred = createDeferred()
|
|
|
|
|
+ const downloadDeferred = createDeferred()
|
|
|
|
|
+ const opened = []
|
|
|
|
|
+ const toasts = []
|
|
|
|
|
+ let downloadCalls = 0
|
|
|
|
|
+ const wx = {
|
|
|
|
|
+ openDocument(options) {
|
|
|
|
|
+ opened.push(options.filePath)
|
|
|
|
|
+ setImmediate(() => options.success())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const component = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ wx,
|
|
|
|
|
+ http: {
|
|
|
|
|
+ listTeamBackgroundFiles: () => listDeferred.promise,
|
|
|
|
|
+ downloadTeamBackgroundFile: () => {
|
|
|
|
|
+ downloadCalls += 1
|
|
|
|
|
+ return downloadDeferred.promise
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: '',
|
|
|
|
|
+ editable: true,
|
|
|
|
|
+ $showToast(message) { toasts.push(message) },
|
|
|
|
|
+ $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ instance.resetTeamContext(3)
|
|
|
|
|
+ const oldFile = { id: 31, fileName: 'old.pdf' }
|
|
|
|
|
+ instance.files = [oldFile]
|
|
|
|
|
+ instance.filesReady = true
|
|
|
|
|
+ const refresh = instance.refreshFiles(3)
|
|
|
|
|
+ await instance.previewFile(oldFile)
|
|
|
|
|
+ assert.equal(downloadCalls, 0, 'old file ids must be disabled while the current list is loading')
|
|
|
|
|
+ listDeferred.resolve([oldFile])
|
|
|
|
|
+ await refresh
|
|
|
|
|
+
|
|
|
|
|
+ const latePreview = instance.previewFile(oldFile)
|
|
|
|
|
+ assert.equal(downloadCalls, 1)
|
|
|
|
|
+ instance.resetTeamContext(4)
|
|
|
|
|
+ downloadDeferred.resolve({ mode: 'document', fileName: 'old.pdf', filePath: '/tmp/old.pdf' })
|
|
|
|
|
+ await latePreview
|
|
|
|
|
+ assert.deepEqual(opened, [], 'a stale download must not open a native document')
|
|
|
|
|
+
|
|
|
|
|
+ const validComponent = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ wx,
|
|
|
|
|
+ http: {
|
|
|
|
|
+ downloadTeamBackgroundFile: () => Promise.resolve({
|
|
|
|
|
+ mode: 'document', fileName: 'current.pdf', filePath: '/tmp/current.pdf'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const validInstance = instantiateComponent(validComponent, {
|
|
|
|
|
+ teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ validInstance.resetTeamContext(5)
|
|
|
|
|
+ const currentFile = { id: 51, fileName: 'current.pdf' }
|
|
|
|
|
+ validInstance.files = [currentFile]
|
|
|
|
|
+ validInstance.filesReady = true
|
|
|
|
|
+ await validInstance.previewFile(currentFile)
|
|
|
|
|
+ assert.deepEqual(opened, ['/tmp/current.pdf'])
|
|
|
|
|
+
|
|
|
|
|
+ const failingComponent = await loadVueComponent(backgroundComponentPath, {
|
|
|
|
|
+ wx: {
|
|
|
|
|
+ openDocument(options) { setImmediate(() => options.fail(new Error('open failed'))) }
|
|
|
|
|
+ },
|
|
|
|
|
+ http: {
|
|
|
|
|
+ downloadTeamBackgroundFile: () => Promise.resolve({
|
|
|
|
|
+ mode: 'document', fileName: 'broken.pdf', filePath: '/tmp/broken.pdf'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ const failingInstance = instantiateComponent(failingComponent, {
|
|
|
|
|
+ teamId: '', editable: true, $showToast(message) { toasts.push(message) }, $emit() {}
|
|
|
|
|
+ })
|
|
|
|
|
+ failingInstance.resetTeamContext(6)
|
|
|
|
|
+ const brokenFile = { id: 61, fileName: 'broken.pdf' }
|
|
|
|
|
+ failingInstance.files = [brokenFile]
|
|
|
|
|
+ failingInstance.filesReady = true
|
|
|
|
|
+ await failingInstance.previewFile(brokenFile)
|
|
|
|
|
+ assert.ok(toasts.some(message => String(message).includes('open failed')))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testTeamFillFlushesBeforeEmitAndLocks() {
|
|
|
|
|
+ const component = await loadVueComponent(teamFillComponentPath)
|
|
|
|
|
+ const flushDeferred = createDeferred()
|
|
|
|
|
+ let flushCalls = 0
|
|
|
|
|
+ let emitCalls = 0
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ teamId: 12,
|
|
|
|
|
+ qtype: false,
|
|
|
|
|
+ $props: { qtype: false },
|
|
|
|
|
+ $showToast() {},
|
|
|
|
|
+ $emit() { emitCalls += 1 },
|
|
|
|
|
+ $refs: {
|
|
|
|
|
+ backgroundRef: {
|
|
|
|
|
+ flushPendingFiles() { flushCalls += 1; return flushDeferred.promise },
|
|
|
|
|
+ hasUnresolvedFiles() { return false }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ instance.teamInfo = {
|
|
|
|
|
+ teamName: '团队', enterpriseName: '公司', enterpriseWebsite: '无',
|
|
|
|
|
+ districtId: 1, industryId: 2, functionIds: [], orgIds: []
|
|
|
|
|
+ }
|
|
|
|
|
+ const first = instance.handleConfirm()
|
|
|
|
|
+ const second = instance.handleConfirm()
|
|
|
|
|
+ assert.equal(flushCalls, 1)
|
|
|
|
|
+ assert.equal(emitCalls, 0)
|
|
|
|
|
+ flushDeferred.resolve({ success: 1, failed: 0 })
|
|
|
|
|
+ await Promise.all([first, second])
|
|
|
|
|
+ assert.equal(emitCalls, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testCreatedTeamResumeAndSubmitLock() {
|
|
|
|
|
+ const component = await loadVueComponent(createTeamPagePath)
|
|
|
|
|
+ let teamPostCalls = 0
|
|
|
|
|
+ let flushCalls = 0
|
|
|
|
|
+ const toasts = []
|
|
|
|
|
+ const instance = instantiateComponent(component, {
|
|
|
|
|
+ $api: {
|
|
|
|
|
+ post(url) {
|
|
|
|
|
+ assert.equal(url, '/core/user/team')
|
|
|
|
|
+ teamPostCalls += 1
|
|
|
|
|
+ return Promise.resolve({ data: { code: 0, data: { teamId: 88 } } })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ $showToast(message) { toasts.push(message) },
|
|
|
|
|
+ $showModal: () => Promise.resolve(),
|
|
|
|
|
+ $refs: {
|
|
|
|
|
+ teamRef: {
|
|
|
|
|
+ flushBackgroundFiles() {
|
|
|
|
|
+ flushCalls += 1
|
|
|
|
|
+ if (flushCalls === 1) return Promise.reject(new Error('列表刷新失败'))
|
|
|
|
|
+ return Promise.resolve({ success: 0, failed: 0 })
|
|
|
|
|
+ },
|
|
|
|
|
+ hasUnresolvedBackgroundFiles: () => false,
|
|
|
|
|
+ hasDeferredPcUpload: () => false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ let continuations = 0
|
|
|
|
|
+ instance.continueAfterTeamCreated = async teamId => {
|
|
|
|
|
+ assert.equal(teamId, 88)
|
|
|
|
|
+ continuations += 1
|
|
|
|
|
+ }
|
|
|
|
|
+ await instance.handleConfirm({ teamName: '团队' })
|
|
|
|
|
+ assert.equal(teamPostCalls, 1)
|
|
|
|
|
+ assert.equal(instance.creationState.createdTeamId, 88)
|
|
|
|
|
+ assert.ok(toasts.some(message => String(message).includes('背景资料')))
|
|
|
|
|
+ await instance.handleConfirm({ teamName: '团队' })
|
|
|
|
|
+ assert.equal(teamPostCalls, 1, 'retry after a later-stage failure must not POST a second team')
|
|
|
|
|
+ assert.equal(continuations, 1)
|
|
|
|
|
+
|
|
|
|
|
+ const lockedPost = createDeferred()
|
|
|
|
|
+ const lockedInstance = instantiateComponent(component, {
|
|
|
|
|
+ $api: { post: () => { teamPostCalls += 1; return lockedPost.promise } },
|
|
|
|
|
+ $showToast() {},
|
|
|
|
|
+ $showModal: () => Promise.resolve(),
|
|
|
|
|
+ $refs: { teamRef: {} }
|
|
|
|
|
+ })
|
|
|
|
|
+ const lockedFirst = lockedInstance.handleConfirm({ teamName: '团队' })
|
|
|
|
|
+ const callsAfterFirst = teamPostCalls
|
|
|
|
|
+ const lockedSecond = lockedInstance.handleConfirm({ teamName: '团队' })
|
|
|
|
|
+ assert.equal(teamPostCalls, callsAfterFirst, 'concurrent submission must be ignored')
|
|
|
|
|
+ lockedPost.resolve({ data: { code: 1, msg: '保存接口失败' } })
|
|
|
|
|
+ await Promise.all([lockedFirst, lockedSecond])
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function testCloseAndBackRequirePendingUploadConfirmation() {
|
|
|
|
|
+ let modalOptions
|
|
|
|
|
+ let navigateBackCalls = 0
|
|
|
|
|
+ const uni = {
|
|
|
|
|
+ showModal(options) { modalOptions = options },
|
|
|
|
|
+ navigateBack() { navigateBackCalls += 1 }
|
|
|
|
|
+ }
|
|
|
|
|
+ const createList = await loadVueComponent(createListComponentPath, { uni })
|
|
|
|
|
+ const createListInstance = instantiateComponent(createList, {
|
|
|
|
|
+ $imgBase: 'https://image.example/',
|
|
|
|
|
+ teamInfoShow: true,
|
|
|
|
|
+ $refs: {
|
|
|
|
|
+ teamRef: {
|
|
|
|
|
+ hasUnresolvedBackgroundFiles: () => true,
|
|
|
|
|
+ isBackgroundUploadActive: () => false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ createListInstance.requestCloseTeamInfo()
|
|
|
|
|
+ assert.equal(createListInstance.teamInfoShow, true)
|
|
|
|
|
+ modalOptions.success({ confirm: false })
|
|
|
|
|
+ assert.equal(createListInstance.teamInfoShow, true)
|
|
|
|
|
+ createListInstance.requestCloseTeamInfo()
|
|
|
|
|
+ modalOptions.success({ confirm: true })
|
|
|
|
|
+ assert.equal(createListInstance.teamInfoShow, false)
|
|
|
|
|
+
|
|
|
|
|
+ const teamEdit = await loadVueComponent(teamEditPagePath, { uni })
|
|
|
|
|
+ const teamEditInstance = instantiateComponent(teamEdit, {
|
|
|
|
|
+ $refs: {
|
|
|
|
|
+ teamRef: {
|
|
|
|
|
+ hasUnresolvedBackgroundFiles: () => true,
|
|
|
|
|
+ isBackgroundUploadActive: () => true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ teamEditInstance.requestBack()
|
|
|
|
|
+ assert.equal(navigateBackCalls, 0)
|
|
|
|
|
+ modalOptions.success({ confirm: false })
|
|
|
|
|
+ assert.equal(navigateBackCalls, 0)
|
|
|
|
|
+ teamEditInstance.requestBack()
|
|
|
|
|
+ modalOptions.success({ confirm: true })
|
|
|
|
|
+ assert.equal(navigateBackCalls, 1)
|
|
|
|
|
+
|
|
|
|
|
+ let headerBackEvents = 0
|
|
|
|
|
+ const header = await loadVueComponent(headerComponentPath, { uni })
|
|
|
|
|
+ const headerInstance = instantiateComponent(header, {
|
|
|
|
|
+ interceptBack: true,
|
|
|
|
|
+ $emit(name) { if (name === 'back') headerBackEvents += 1 }
|
|
|
})
|
|
})
|
|
|
- await assert.rejects(openFailureHarness.promise, /open document error/)
|
|
|
|
|
|
|
+ headerInstance.toBack('')
|
|
|
|
|
+ assert.equal(headerBackEvents, 1)
|
|
|
|
|
+ assert.equal(navigateBackCalls, 1, 'intercepted header back must not navigate directly')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function main() {
|
|
async function main() {
|
|
@@ -475,7 +977,16 @@ async function main() {
|
|
|
await testUploadTransport()
|
|
await testUploadTransport()
|
|
|
await testApiTransport()
|
|
await testApiTransport()
|
|
|
await testDownloadTransport()
|
|
await testDownloadTransport()
|
|
|
|
|
+ await testDeferredPcSelectionSurvivesSessionFailure()
|
|
|
|
|
+ await testSessionRefreshFailureKeepsOldTimer()
|
|
|
|
|
+ await testDeferredPcFinishWaitsForListRefresh()
|
|
|
|
|
+ await testTeamContextResetAndUploadIsolation()
|
|
|
|
|
+ await testFileActionsWaitForCurrentListAndDocumentContext()
|
|
|
|
|
+ await testTeamFillFlushesBeforeEmitAndLocks()
|
|
|
|
|
+ await testCreatedTeamResumeAndSubmitLock()
|
|
|
|
|
+ await testCloseAndBackRequirePendingUploadConfirmation()
|
|
|
console.log('team background transport: PASS')
|
|
console.log('team background transport: PASS')
|
|
|
|
|
+ console.log('team background component behavior: PASS')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
main().catch(error => {
|
|
main().catch(error => {
|