Files
shanxia2.0/web-admin/scripts/stop-documents.test.ts
T

542 lines
18 KiB
TypeScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { parse } from '@vue/compiler-sfc'
import ts from 'typescript'
import * as vue from 'vue'
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
const flush = async () => {
for (let i = 0; i < 30; i++) await Promise.resolve()
}
function deferred() {
let resolve!: (value: any) => void, reject!: (error: any) => void
const promise = new Promise((a, b) => {
resolve = a
reject = b
})
return { promise, resolve, reject }
}
function harness(fixed?: string) {
const viewportWidth = vue.ref(1120)
const calls: any[] = [],
permissions: any[] = [],
unmount: (() => void)[] = []
const props = vue.reactive({
contractId: fixed,
active: true,
embedded: !!fixed
})
const user = vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } })
const queue = (method: string, args: any[]) => {
const pending = deferred()
calls.push({ method, args, ...pending })
return pending.promise
}
const api = Object.fromEntries(
['listStops', 'getStops', 'saveStops'].map((method) => [
method,
(...args: any[]) => queue(method, args)
])
)
const env = {
...Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
),
defineProps: () => props,
withDefaults: (v: any) => v,
useUserStore: () => user,
fetchGetMenuCodes: () => {
const p = deferred()
permissions.push(p)
return p.promise
},
useElementSize: () => ({ width: viewportWidth }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
usePersistedListColumns,
useServerColumnFilters,
legalDocumentApi: api,
ElMessage: { success() {}, warning() {} },
onBeforeUnmount: (fn: () => void) => unmount.push(fn)
}
const src = ts.createSourceFile(
'panel.ts',
parse(
readFileSync(
new URL('../src/views/pl/legal-use-document/StopDocuments.vue', import.meta.url),
'utf8'
)
).descriptor.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const code = ts.transpile(
src.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(src))
.join('\n'),
{ target: ts.ScriptTarget.ESNext }
)
const scope = vue.effectScope()
const state = scope.run(() =>
new Function(
...Object.keys(env),
`${code}; return { rows, details, load, loadDetail, allowed, canUpload, keyword, setFilterState, page, total, expansionStyle, open, save, visible, editing, upload, saving, editorLoading, status, search, statusLabel, reset, visibleColumns }`
)(...Object.values(env))
)
return {
state,
viewportWidth,
calls,
permissions,
props,
user,
stop: () => {
unmount.forEach((fn) => fn())
scope.stop()
}
}
}
test('global and fixed contract preserve server counts and scope every request', async () => {
for (const fixed of [undefined, 'contract-one']) {
const h = harness(fixed),
s = h.state
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list', 'pl:plan-cert:upload'] })
await flush()
assert.equal(h.calls[0].method, 'listStops')
assert.equal(h.calls[0].args[0].contractId, fixed)
h.calls[0].resolve({
list: [{ id: 'plan', uploadedCount: 1, requiredCount: fixed ? 1 : 3 }],
total: 45
})
await flush()
assert.equal(s.rows.value[0].requiredCount, fixed ? 1 : 3)
const pending = s.open({ id: 'plan' }, { stopType: 'delivery', stopProductId: 'd2' })
assert.equal(h.calls[1].args[1], fixed)
h.calls[1].resolve({
id: 'plan',
items: [
{ stopType: 'delivery', stopProductId: 'd2', productId: 'same', legalUseCertUrl: 'a,b' }
]
})
await pending
const saving = s.save()
assert.deepEqual(h.calls[2].args, [
'plan',
[{ stopType: 'delivery', stopProductId: 'd2', legalUseCertUrl: 'a,b' }],
fixed
])
h.calls[2].resolve({})
await saving
h.stop()
}
})
test('busy uploads refuse saves and failures preserve draft', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 0 })
await flush()
const opened = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' })
h.calls[1].resolve({
id: 'p',
items: [{ stopType: 'load', stopProductId: 'l', legalUseCertUrl: 'original' }]
})
await opened
s.editing.value.legalUseCertUrl = 'draft'
s.upload.value = { hasUploading: () => true }
await s.save()
assert.equal(h.calls.length, 2)
s.upload.value = { hasUploading: () => false }
const saving = s.save()
h.calls[2].reject(Error('failed'))
await saving
assert.equal(s.editing.value.legalUseCertUrl, 'draft')
assert.equal(s.visible.value, true)
h.stop()
})
for (const change of ['contract', 'tenant', 'close'])
test(`old editor response is discarded after ${change}`, async () => {
const h = harness('one'),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
const old = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' })
if (change === 'contract') h.props.contractId = 'two'
if (change === 'tenant') h.user.getUserInfo.tenantNo = 'b'
if (change === 'close') s.visible.value = false
h.calls[1].resolve({ id: 'p', items: [{ stopType: 'load', stopProductId: 'l' }] })
await old
assert.equal(s.editing.value, undefined)
h.stop()
})
test('search during permission initialization does not cancel it; filters are independent', async () => {
const h = harness(),
s = h.state
s.keyword.value = 'word'
s.setFilterState('planNo', { text: 'PL' })
await s.load()
h.permissions[0].resolve({ all: true })
await flush()
assert.equal(h.calls.length, 1)
assert.equal(h.calls[0].args[0].keyword, 'word')
assert.equal(h.calls[0].args[0].planNo, 'PL')
h.stop()
})
test('refresh preserves page and total; not_required is not uploaded', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [], total: 45 })
await flush()
s.page.value = 2
const pending = s.load()
assert.equal(s.total.value, 45)
assert.equal(h.calls[1].args[0].page, 2)
h.calls[1].reject(Error('failed'))
await pending
assert.equal(s.page.value, 2)
assert.equal(s.total.value, 45)
assert.equal(s.statusLabel('not_required'), '无需上传')
h.stop()
})
// Mount the real Element Plus pager. Its page-count watcher must remain active.
import ElPagination from 'element-plus/es/components/pagination/src/pagination.mjs'
test('real pagination keeps page two while loading and preserves count after request failure', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
h.calls[0].resolve({ list: [{ id: 'first' }], total: 45 })
await flush()
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText: () => {},
setElementText: () => {},
parentNode: () => null,
nextSibling: () => null,
insert: () => {},
remove: () => {},
patchProp: () => {}
})
const app = renderer.createApp({
render: () =>
vue.h(ElPagination, {
currentPage: s.page.value,
total: s.total.value,
pageSize: 20,
layout: '',
'onUpdate:current-page': (value: number) => {
s.page.value = value
},
onCurrentChange: () => {
void s.load()
}
})
})
app.mount({})
s.page.value = 2
const pending = s.load()
await vue.nextTick()
await flush()
assert.equal(s.page.value, 2)
assert.equal(h.calls.length, 2)
assert.equal(h.calls[1].args[0].page, 2)
h.calls[1].resolve({ list: [{ id: 'second' }], total: 45 })
await pending
await vue.nextTick()
assert.equal(s.page.value, 2)
assert.equal(s.rows.value[0].id, 'second')
const failed = s.load()
h.calls[2].reject(Error('failed'))
await failed
await vue.nextTick()
assert.equal(s.page.value, 2)
assert.equal(s.total.value, 45)
assert.deepEqual(s.rows.value, [])
h.user.getUserInfo.tenantNo = 'next'
await vue.nextTick()
assert.equal(s.page.value, 1)
assert.equal(s.total.value, 0)
app.unmount()
h.stop()
})
import { compileScript } from '@vue/compiler-sfc'
function compiledSfc(path: string, dependencies: Record<string, any>, inlineTemplate = true) {
const descriptor = parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor
const compiled = compileScript(descriptor, { id: path, inlineTemplate })
const source = ts.createSourceFile('compiled.ts', compiled.content, ts.ScriptTarget.Latest, true)
const env: Record<string, any> = Object.fromEntries(
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
)
Object.assign(env, dependencies)
for (const statement of source.statements) {
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly) continue
const clause = statement.importClause
if (clause?.name) env[clause.name.text] = dependencies[clause.name.text] || {}
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
for (const item of clause.namedBindings.elements) {
if (item.isTypeOnly) continue
const name = (item.propertyName || item.name).text
env[item.name.text] =
statement.moduleSpecifier.getText(source) === '"vue"' ||
statement.moduleSpecifier.getText(source) === "'vue'"
? (vue as any)[name]
: dependencies[name]
}
}
}
const code = ts.transpile(
source.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(source))
.join('\n')
.replace('export default', 'const component ='),
{ target: ts.ScriptTarget.ESNext }
)
return new Function(...Object.keys(env), `${code}; return component`)(...Object.values(env))
}
for (const active of [undefined, false])
test(`compiled proof component active=${active}`, async () => {
const calls: string[] = []
const component = compiledSfc(
'../src/views/pl/legal-use-document/StopDocuments.vue',
{
useUserStore: () => vue.reactive({ accessToken: 'token', getUserInfo: { tenantNo: 'a' } }),
fetchGetMenuCodes: async () => ({ menuCodes: ['pl:legal-use-document:list'] }),
legalDocumentApi: {
listStops: async () => {
calls.push('proof')
return { list: [], total: 0 }
}
},
usePersistedListColumns,
useServerColumnFilters,
useElementSize: () => ({ width: vue.ref(1000) }),
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) })
},
false
)
component.render = () => null
const renderer = vue.createRenderer<any, any>({
createElement: () => ({}),
createText: () => ({}),
createComment: () => ({}),
setText() {},
setElementText() {},
parentNode: () => null,
nextSibling: () => null,
insert() {},
remove() {},
patchProp() {}
})
const app = renderer.createApp(component, active === undefined ? {} : { active })
app.mount({})
await flush()
assert.deepEqual(calls, active === false ? [] : ['proof'])
app.unmount()
})
test('closing and reopening rejects old response and finally without clearing new loading', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
const first = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' })
s.visible.value = false
const second = s.open({ id: 'p' }, { stopType: 'delivery', stopProductId: 'd' })
h.calls[1].resolve({ items: [{ stopType: 'load', stopProductId: 'l' }] })
await first
assert.equal(s.editorLoading.value, true)
assert.equal(s.editing.value, undefined)
h.calls[2].resolve({ items: [{ stopType: 'delivery', stopProductId: 'd' }] })
await second
assert.equal(s.editorLoading.value, false)
assert.equal(s.editing.value.stopProductId, 'd')
h.stop()
})
test('historical attachments have a separate readonly field and viewport aligned expansion', () => {
const descriptor = parse(
readFileSync(
new URL('../src/views/pl/legal-use-document/StopDocuments.vue', import.meta.url),
'utf8'
)
).descriptor
assert.match(descriptor.template!.content, /历史计划级资料/)
assert.match(
descriptor.template!.content,
/:model-value="product.legalUseCertUrl \|\| ''"\s+disabled/
)
assert.match(descriptor.styles[0].content, /text-align: left/)
const h = harness()
assert.equal(h.state.expansionStyle.value.width, '1120px')
h.viewportWidth.value = 760
assert.equal(h.state.expansionStyle.value.width, '760px')
h.stop()
})
test('stop API preserves original legacy methods and scopes all three new endpoints', async () => {
const calls: any[] = []
const source = ts.createSourceFile(
'api.ts',
readFileSync(new URL('../src/api/pl/quota-cert.ts', import.meta.url), 'utf8'),
ts.ScriptTarget.Latest,
true
)
const code = ts.transpile(
source.statements
.filter((s) => !ts.isImportDeclaration(s))
.map((s) => s.getText(source).replace(/^export /, ''))
.join('\n'),
{ target: ts.ScriptTarget.ESNext }
)
const api = new Function('request', 'pickBxData', `${code};return legalDocumentApi`)(
{
get: (...args: any[]) => {
calls.push(args)
return Promise.resolve({})
},
patch: (...args: any[]) => {
calls.push(args)
return Promise.resolve({})
}
},
(v: any) => v
)
await api.listStops({ contractId: 'c', page: 2, status: 'pending', planNo: 'PL' })
await api.getStops('p', 'c')
await api.saveStops(
'p',
[{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'a,b' }],
'c'
)
assert.deepEqual(calls, [
[
'/transport/legal-use-documents/stops',
{ params: { contractId: 'c', page: 2, status: 'pending', planNo: 'PL' } }
],
['/transport/legal-use-documents/stops/p', { params: { contractId: 'c' } }],
[
'/transport/legal-use-documents/stops/p',
{ items: [{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'a,b' }] },
{ params: { contractId: 'c' } }
]
])
for (const method of ['list', 'get', 'save']) assert.equal(typeof api[method], 'function')
})
test('view permission loads proof only and prevents upload requests', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ menuCodes: ['pl:legal-use-document:list'] })
await flush()
assert.equal(s.allowed.value, true)
assert.equal(s.canUpload.value, false)
await s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' })
await s.save()
assert.deepEqual(
h.calls.map((c) => c.method),
['listStops']
)
h.stop()
})
test('old save completion cannot close or clear a reopened editor', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
const opened = s.open({ id: 'p' }, { stopType: 'load', stopProductId: 'l' })
h.calls[1].resolve({ items: [{ stopType: 'load', stopProductId: 'l', legalUseCertUrl: 'a' }] })
await opened
const oldSave = s.save()
s.visible.value = false
const reopened = s.open({ id: 'p' }, { stopType: 'delivery', stopProductId: 'd' })
h.calls[3].resolve({
items: [{ stopType: 'delivery', stopProductId: 'd', legalUseCertUrl: 'b' }]
})
await reopened
const newSave = s.save()
h.calls[2].resolve({})
await oldSave
assert.equal(s.visible.value, true)
assert.equal(s.saving.value, true)
assert.equal(s.editing.value.stopProductId, 'd')
assert.equal(h.calls.length, 5)
h.calls[4].reject(Error('retry'))
await newSave
assert.equal(s.editing.value.legalUseCertUrl, 'b')
assert.equal(s.saving.value, false)
h.stop()
})
test('old list and permission responses cannot reauthorize another tenant', async () => {
const h = harness(),
s = h.state
h.user.getUserInfo.tenantNo = 'b'
h.permissions[0].resolve({ all: true })
await flush()
assert.equal(h.calls.length, 0)
h.permissions[1].resolve({ all: true })
await flush()
h.user.getUserInfo.tenantNo = 'c'
h.calls[0].resolve({ list: [{ id: 'stale' }], total: 1 })
await flush()
assert.deepEqual(s.rows.value, [])
assert.equal(s.allowed.value, false)
h.permissions[2].resolve({ menuCodes: [] })
await flush()
assert.equal(h.calls.length, 1)
h.stop()
})
test('status is a server single filter and combines with the number filter', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
s.status.value = 'uploaded'
s.search()
assert.equal(h.calls.at(-1).args[0].status, 'uploaded')
s.setFilterState('planNo', { text: 'PL' })
assert.equal(h.calls.at(-1).args[0].planNo, 'PL')
assert.equal(h.calls.at(-1).args[0].status, 'uploaded')
h.calls.forEach((c) => c.resolve({ list: [], total: 0 }))
await flush()
h.stop()
})
test('latest refresh wins and hidden column/reset preserve server filter semantics', async () => {
const h = harness(),
s = h.state
h.permissions[0].resolve({ all: true })
await flush()
const fresh = s.load()
h.calls[1].resolve({ list: [{ id: 'new' }], total: 45 })
await fresh
h.calls[0].resolve({ list: [{ id: 'old' }], total: 1 })
await flush()
assert.equal(s.rows.value[0].id, 'new')
assert.equal(s.total.value, 45)
s.keyword.value = 'quick'
s.status.value = 'pending'
s.page.value = 2
s.setFilterState('planNo', { text: 'PL' })
assert.equal(s.page.value, 1)
assert.equal(h.calls.at(-1).args[0].planNo, 'PL')
s.visibleColumns.value = s.visibleColumns.value.filter((key: string) => key !== 'planNo')
await vue.nextTick()
assert.equal(h.calls.at(-1).args[0].planNo, undefined)
assert.equal(h.calls.at(-1).args[0].status, 'pending')
s.reset()
assert.equal(h.calls.at(-1).args[0].keyword, '')
assert.equal(h.calls.at(-1).args[0].status, undefined)
h.calls.forEach((c) => c.resolve({ list: [], total: 0 }))
await flush()
h.stop()
})