Files
shanxia2.0/web-admin/scripts/quota-mobile-state.test.ts
T

378 lines
13 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { parse } from '@vue/compiler-sfc'
import ts from 'typescript'
import * as vue from 'vue'
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
const flush = async () => {
for (let i = 0; i < 20; i++) await Promise.resolve()
}
function deferred() {
let resolve!: (value?: any) => void
let reject!: (error: any) => void
const promise = new Promise<any>((a, b) => {
resolve = a
reject = b
})
return { promise, resolve, reject }
}
// Run production setup, including its real synchronous session watchers. Only I/O is substituted.
function pageHarness(kind: 'quota' | 'legal', props: any = { mobile: true }) {
const lists: any[] = [],
gets: any[] = [],
writes: any[] = [],
permissions: any[] = []
const events: string[] = [],
mounted: Function[] = [],
unmount: Function[] = []
const user = vue.reactive({ accessToken: 'a', getUserInfo: { tenantNo: 'a', roles: ['sales'] } })
const queue = (items: any[], params?: any) => {
const pending = deferred()
items.push({ ...pending, params })
return pending.promise
}
const env = {
...Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
),
useElementSize: () => ({ width: vue.ref(900) }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
useServerColumnFilters,
usePersistedListColumns,
useUserStore: () => user,
defineProps: () => props,
onMounted: (fn: Function) => mounted.push(fn),
onBeforeUnmount: (fn: Function) => unmount.push(fn),
request: { get: (url: string, options: any) => queue(lists, { url, ...options.params }) },
pickBxData: (value: any) => value,
planQuotaApi: { get: (id: string) => queue(gets, id) },
legalDocumentApi: {
list: (params: any) => queue(lists, params),
get: (id: string) => queue(gets, id),
save: (id: string, products: any) => queue(writes, { id, products })
},
fetchGetMenuCodes: () => queue(permissions),
ElMessage: {
warning: (msg: string) => events.push(msg),
success: (msg: string) => events.push(msg)
}
}
const path =
kind === 'quota'
? '../src/views/pl/plan-quota/index.vue'
: '../src/views/m/pl/legal-use-document/index.vue'
const source = ts.createSourceFile(
'page.ts',
parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const statements = source.statements.filter((s) => !ts.isImportDeclaration(s))
const names = statements.flatMap((s) =>
ts.isVariableStatement(s)
? s.declarationList.declarations
.filter((d) => ts.isIdentifier(d.name))
.map((d) => d.name.getText(source))
: ts.isFunctionDeclaration(s) && s.name
? [s.name.text]
: []
)
const code = ts.transpile(statements.map((s) => s.getText(source)).join('\n'), {
target: ts.ScriptTarget.ESNext
})
const scope = vue.effectScope()
const state = scope.run(() =>
new Function(...Object.keys(env), `${code}; return {${names.join(',')}}`)(...Object.values(env))
)
return {
state,
user,
lists,
gets,
writes,
permissions,
events,
mounted,
stop() {
unmount.forEach((fn) => fn())
scope.stop()
}
}
}
const response = (kind: string, id: string) =>
kind === 'quota' ? { list: [{ id }], total: 1 } : [{ id }]
const changeScope = (h: ReturnType<typeof pageHarness>, change: string) => {
if (change === 'token') h.user.accessToken = 'b'
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b'
if (change === 'roles') h.user.getUserInfo.roles = ['viewer']
}
for (const kind of ['quota', 'legal'] as const) {
test(`${kind}: real query sends keyword/status and latest response wins`, async () => {
const h = pageHarness(kind),
s = h.state
s.keyword.value = 'PLAN-A'
s.status.value = kind === 'quota' ? 'reserved' : 'pending'
const first = s.load()
assert.equal(h.lists[0].params.keyword, 'PLAN-A')
assert.equal(h.lists[0].params.status, s.status.value)
s.keyword.value = 'PLAN-B'
const second = s.load()
h.lists[1].resolve(response(kind, 'B'))
await second
h.lists[0].resolve(response(kind, 'A'))
await first
assert.deepEqual(s.rows.value, [{ id: 'B' }])
assert.equal(s.loading.value, false)
h.stop()
})
for (const change of ['token', 'tenant', 'roles']) {
test(`${kind}: ${change} clears private rows and rejects a pending old response`, async () => {
const h = pageHarness(kind),
s = h.state
if (kind === 'legal') {
const init = s.init()
h.permissions[0].resolve({ menuCodes: ['pl:plan-cert:upload'] })
await flush()
h.lists[0].resolve([{ id: 'private' }])
await init
} else s.rows.value = [{ id: 'private' }]
const old = s.load(),
oldRequest = h.lists.at(-1)
if (kind === 'legal') {
s.detail.value = { id: 'private' }
s.visible.value = true
}
changeScope(h, change)
assert.deepEqual(s.rows.value, [])
if (kind === 'legal') {
assert.equal(s.detail.value, undefined)
assert.equal(s.visible.value, false)
assert.equal(s.canUpload.value, false)
h.permissions.at(-1).resolve({ menuCodes: [] })
await flush()
}
const fresh = h.lists.at(-1)
assert.notEqual(fresh, oldRequest)
oldRequest.resolve(response(kind, 'private'))
await old
assert.deepEqual(s.rows.value, [])
assert.equal(s.loading.value, true, 'old finally cannot release current request')
fresh.resolve(response(kind, 'new-scope'))
await flush()
assert.deepEqual(s.rows.value, [{ id: 'new-scope' }])
h.stop()
})
}
test(`${kind}: unmount invalidates pending list response`, async () => {
const h = pageHarness(kind),
pending = h.state.load()
h.stop()
h.lists[0].resolve(response(kind, 'late'))
await pending
assert.deepEqual(h.state.rows.value, [])
})
}
test('legal: stale list failure cannot overwrite successful newer search', async () => {
const h = pageHarness('legal'),
s = h.state
const old = s.load(),
fresh = s.load()
h.lists[1].resolve([{ id: 'current' }])
await fresh
h.lists[0].reject(new Error('stale network error'))
await old
assert.equal(s.error.value, '')
assert.deepEqual(s.rows.value, [{ id: 'current' }])
h.stop()
})
test('legal: failed list can be retried and empty success is distinct from failure', async () => {
const h = pageHarness('legal'),
s = h.state
const failed = s.load()
h.lists[0].reject(new Error('offline'))
await failed
assert.ok(s.error.value)
assert.equal(s.loading.value, false)
const retry = s.load()
h.lists[1].resolve([])
await retry
assert.equal(s.error.value, '')
assert.deepEqual(s.rows.value, [])
h.stop()
})
test('legal: switching detail or closing rejects older detail response', async () => {
const h = pageHarness('legal'),
s = h.state
const first = s.open('A'),
second = s.open('B')
h.gets[1].resolve({ id: 'B' })
await second
h.gets[0].resolve({ id: 'A' })
await first
assert.equal(s.detail.value.id, 'B')
const third = s.open('C')
s.close(false)
h.gets[2].resolve({ id: 'C' })
await third
assert.equal(s.detail.value, undefined)
assert.equal(s.visible.value, false)
h.stop()
})
for (const change of ['token', 'tenant', 'roles']) {
test(`legal: ${change} invalidates permission, detail and save callbacks`, async () => {
const h = pageHarness('legal'),
s = h.state
const init = s.init(),
detail = s.open('private')
s.canUpload.value = true
s.detail.value = { id: 'private', products: [{ id: 'line', legalUseCertUrl: 'attachment' }] }
const save = s.save()
assert.deepEqual(h.writes[0].params, {
id: 'private',
products: [{ id: 'line', legalUseCertUrl: 'attachment' }]
})
changeScope(h, change)
h.permissions[0].resolve({ all: true })
await init
h.gets[0].resolve({ id: 'private' })
await detail
s.saving.value = true // another action in the new scope
h.writes[0].resolve()
await save
assert.equal(s.canUpload.value, false)
assert.equal(s.detail.value, undefined)
assert.equal(s.saving.value, true)
assert.deepEqual(h.events, [])
assert.equal(h.lists.length, 0)
h.stop()
})
}
test('legal: duplicate save and active uploads are guarded; session clears old uploader handles', async () => {
const h = pageHarness('legal'),
s = h.state
s.canUpload.value = true
s.detail.value = { id: 'A', products: [{ id: 'line', legalUseCertUrl: 'url' }] }
s.setUpload('line', { hasUploading: () => true })
await s.save()
assert.equal(h.writes.length, 0)
assert.equal(h.events.length, 1)
changeScope(h, 'tenant')
s.canUpload.value = true
s.detail.value = { id: 'B', products: [{ id: 'new-line', legalUseCertUrl: 'new-url' }] }
const save = s.save()
await s.save()
assert.equal(h.writes.length, 1)
h.writes[0].resolve()
await flush()
h.lists[0].resolve([])
await save
assert.equal(s.visible.value, false)
assert.equal(s.detail.value, undefined)
assert.equal(s.saving.value, false)
h.stop()
})
test('quota: finishing an old dialog after session change cannot reload the current list', async () => {
const h = pageHarness('quota'),
s = h.state,
result = deferred()
s.dialog.value = { open: () => result.promise, invalidateSession() {} }
s.open('A')
changeScope(h, 'tenant')
assert.equal(h.lists.length, 1)
result.resolve()
await flush()
assert.equal(h.lists.length, 1)
h.stop()
})
for (const kind of ['quota', 'legal'] as const) {
test(`${kind}: equivalent user snapshot does not clear state or restart requests`, () => {
const h = pageHarness(kind),
s = h.state
s.rows.value = [{ id: 'current' }]
h.user.getUserInfo = { tenantNo: 'a', roles: ['sales'] }
assert.deepEqual(s.rows.value, [{ id: 'current' }])
assert.equal(h.lists.length, 0)
assert.equal(h.permissions.length, 0)
h.stop()
})
}
test('quota: old mounted load cannot open a plan from the previous session', async () => {
const h = pageHarness('quota', { mobile: true, planId: 'private' }),
opened: string[] = []
h.state.dialog.value = {
open: (id: string) => {
opened.push(id)
return Promise.resolve()
},
invalidateSession() {}
}
const mount = h.mounted[0]()
changeScope(h, 'token')
h.lists[0].resolve({ list: [], total: 0 })
await mount
assert.deepEqual(opened, [])
h.stop()
})
test('legal: read-only permission prevents writes even when detail contains attachments', async () => {
const h = pageHarness('legal'),
s = h.state
const init = s.init()
h.permissions[0].resolve({ menuCodes: [] })
await flush()
h.lists[0].resolve([])
await init
const detail = s.open('A')
h.gets[0].resolve({ id: 'A', products: [{ id: 'line', legalUseCertUrl: 'url' }] })
await detail
assert.equal(s.visible.value, true)
await s.save()
assert.equal(h.writes.length, 0)
h.stop()
})
test('plan expansion loads on demand, preserves total during refresh and ignores previous tenant', async () => {
const h = pageHarness('quota', { mobile: false }),
s = h.state
s.total.value = 45
const refresh = s.load()
assert.equal(s.total.value, 45)
h.lists[0].resolve({ list: [{ id: 'plan' }], total: 45 })
await refresh
s.expand({ id: 'plan' }, [{ id: 'plan' }])
assert.equal(h.gets.length, 1)
assert.deepEqual(s.expandedIds.value, ['plan'])
h.user.getUserInfo.tenantNo = 'other'
h.gets[0].resolve({ planId: 'plan', sourceOptions: [], usages: [] })
await flush()
assert.deepEqual(Object.keys(s.details), [])
h.stop()
})
for (const status of [403, 404, 500, undefined])
test(`plan expansion reports ${status || 'network'} failure without showing partial usage`, async () => {
const h = pageHarness('quota', { mobile: false }),
s = h.state
const pending = s.loadDetail('plan')
h.gets[0].reject(status ? { response: { status } } : new Error('network'))
await pending
assert.equal(
s.details.plan.error,
[403, 404].includes(status as number)
? '计划或证件不可读取,无法展示完整用证详情'
: '段次用证加载失败,请重试'
)
assert.equal(s.details.plan.data, undefined)
assert.equal(s.details.plan.loading, false)
if (status === 500) {
const retry = s.loadDetail('plan')
h.gets[1].resolve({ planId: 'plan', sourceOptions: [], usages: [] })
await retry
assert.equal(s.details.plan.error, undefined)
assert.equal(s.details.plan.data.planId, 'plan')
}
h.stop()
})