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((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, 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() })