333 lines
11 KiB
TypeScript
333 lines
11 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')
|
|
),
|
|
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,
|
|
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()
|
|
})
|