620 lines
21 KiB
TypeScript
620 lines
21 KiB
TypeScript
import * as packageSpec from '../src/utils/crm-package-spec'
|
|
import {
|
|
cleanQuoteQuery,
|
|
defaultQuoteColumns,
|
|
isQuoteListReturnRoute
|
|
} from '../src/views/m/crm/quote/listState'
|
|
import {
|
|
cleanContractQuery,
|
|
isContractListReturnRoute
|
|
} from '../src/views/m/crm/contract/listState'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import { compile, computed, createSSRApp, reactive, ref, watch } from 'vue'
|
|
import { renderToString } from 'vue/server-renderer'
|
|
import { parse } from '@vue/compiler-sfc'
|
|
import ts from 'typescript'
|
|
import { isFrameworkContractType } from '../src/views/crm/contract/contractType'
|
|
import {
|
|
displayFrameworkMoney,
|
|
displayFrameworkQuantity
|
|
} from '../src/views/crm/contract/frameworkProductPricing'
|
|
|
|
const root = new URL('../src/views/m/crm/', import.meta.url)
|
|
const detail = 'contract/detail.vue'
|
|
const lists = ['quote/MpQuoteList.vue', 'contract/MpContractList.vue']
|
|
|
|
function source(file: string) {
|
|
const { descriptor } = parse(readFileSync(new URL(file, root), 'utf8'))
|
|
return {
|
|
descriptor,
|
|
ast: ts.createSourceFile(file, descriptor.scriptSetup!.content, ts.ScriptTarget.Latest, true)
|
|
}
|
|
}
|
|
|
|
function functions(file: string, names: string[], bindings: Record<string, any>, prelude = '') {
|
|
const { ast } = source(file)
|
|
const declarations = ast.statements.filter(
|
|
(item) => ts.isFunctionDeclaration(item) && names.includes(item.name?.text || '')
|
|
)
|
|
const code = ts.transpile(declarations.map((item) => item.getText(ast)).join('\n'), {
|
|
target: ts.ScriptTarget.ESNext
|
|
})
|
|
return new Function(
|
|
...Object.keys(bindings),
|
|
`${prelude}\n${code}\nreturn typeof ${names[0]} === "undefined" ? undefined : ${names[0]}`
|
|
)(...Object.values(bindings))
|
|
}
|
|
|
|
function value(file: string, name: string, bindings: Record<string, any>) {
|
|
const { ast } = source(file)
|
|
const declaration = ast.statements
|
|
.filter(ts.isVariableStatement)
|
|
.flatMap((item) => item.declarationList.declarations)
|
|
.find((item) => item.name.getText(ast) === name)
|
|
assert.ok(declaration, `Missing ${name}`)
|
|
const code = ts.transpile(`const result = ${declaration.initializer!.getText(ast)}`, {
|
|
target: ts.ScriptTarget.ESNext
|
|
})
|
|
return new Function(...Object.keys(bindings), `${code}\nreturn result`)(
|
|
...Object.values(bindings)
|
|
)
|
|
}
|
|
|
|
async function render(fragment: string, bindings: Record<string, any>) {
|
|
const compiled = compile(fragment)
|
|
return renderToString(createSSRApp({ render: () => compiled.call(bindings, bindings, []) }))
|
|
}
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (result: T) => void
|
|
let reject!: (error: Error) => void
|
|
const promise = new Promise<T>((a, b) => {
|
|
resolve = a
|
|
reject = b
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
// Execute the installed Vant click handler so identical-value suppression stays covered.
|
|
function clickAmountOption(
|
|
modelValue: string,
|
|
nextValue: string,
|
|
emit: (event: string, value: string) => void
|
|
) {
|
|
const file = readFileSync(
|
|
new URL('../node_modules/vant/es/dropdown-item/DropdownItem.mjs', import.meta.url),
|
|
'utf8'
|
|
)
|
|
const ast = ts.createSourceFile('DropdownItem.mjs', file, ts.ScriptTarget.Latest, true)
|
|
let clickExpression = ''
|
|
const visit = (node: ts.Node) => {
|
|
if (ts.isVariableDeclaration(node) && node.name.getText(ast) === 'onClick') {
|
|
clickExpression = node.initializer!.getText(ast)
|
|
}
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
visit(ast)
|
|
assert.ok(clickExpression)
|
|
new Function('props', 'option', 'emit', 'state', 'disabled', `return ${clickExpression}`)(
|
|
{ modelValue },
|
|
{ value: nextValue },
|
|
emit,
|
|
{},
|
|
false
|
|
)()
|
|
}
|
|
|
|
function productBindings(contract: Record<string, any>, product: Record<string, any>) {
|
|
const formatContractMoney = (amount: any) => `¥${Number(amount || 0).toFixed(2)}`
|
|
const productMoney = functions(detail, ['productMoney'], {
|
|
contract: ref(contract),
|
|
isFrameworkContractType,
|
|
displayFrameworkMoney,
|
|
formatContractMoney
|
|
})
|
|
return {
|
|
props: { embedded: false, hideProductPrice: false },
|
|
...packageSpec,
|
|
isFrameworkContractType,
|
|
displayFrameworkQuantity,
|
|
formatProductPlanQty: functions(detail, ['formatProductPlanQty', 'formatPlanQty'], {
|
|
...packageSpec
|
|
}),
|
|
contract,
|
|
products: [product],
|
|
productMoney,
|
|
displayCategory: functions(detail, ['displayCategory'], { categoryPaths: ref(new Map()) }),
|
|
formatContractMoney,
|
|
unitLabel: () => '吨',
|
|
formatBrandConstraintLabel: () => ''
|
|
}
|
|
}
|
|
|
|
test('contract detail tabs select the requested pane and load transport plans only when needed', () => {
|
|
const tabs = value(detail, 'detailTabs', {})
|
|
assert.deepEqual(
|
|
tabs.map((t: any) => t.key),
|
|
['contract', 'plans', 'payment', 'invoice', 'approval', 'operation']
|
|
)
|
|
const activeTab = ref('contract'),
|
|
linkedPlans = ref<any[]>([]),
|
|
linkedPlansLoading = ref(false)
|
|
let loads = 0,
|
|
scrolls = 0
|
|
const change = functions(detail, ['onDetailTabChange'], {
|
|
activeTab,
|
|
linkedPlans,
|
|
linkedPlansLoading,
|
|
loadLinkedPlans: () => loads++,
|
|
window: { scrollTo: () => scrolls++ }
|
|
})
|
|
change('plans')
|
|
assert.equal(activeTab.value, 'plans')
|
|
assert.equal(loads, 1)
|
|
linkedPlans.value = [{ id: '1' }]
|
|
change('plans')
|
|
assert.equal(loads, 1)
|
|
change('approval')
|
|
assert.equal(activeTab.value, 'approval')
|
|
assert.equal(scrolls, 3)
|
|
})
|
|
|
|
function approvalState(overrides: Record<string, any> = {}) {
|
|
const contract = ref({ status: 'approving', ...overrides })
|
|
const approvalList = ref<any[]>([
|
|
{
|
|
id: 'a',
|
|
step: 1,
|
|
approverId: 'other',
|
|
approvalResult: 'approved',
|
|
isCurrent: 0,
|
|
createTime: '2026-10-21 10:00:00'
|
|
},
|
|
{
|
|
id: 'b',
|
|
step: 2,
|
|
approverId: 'me',
|
|
approvalResult: 'pending',
|
|
isCurrent: 1,
|
|
createTime: '2026-10-21 10:00:00'
|
|
}
|
|
])
|
|
const pending = value(detail, 'pendingApproval', {
|
|
computed,
|
|
contract,
|
|
approvalList,
|
|
approvalError: ref(''),
|
|
approvalLoading: ref(false),
|
|
resolveCurrentUserId: () => 'me'
|
|
})
|
|
return { contract, approvalList, pending }
|
|
}
|
|
|
|
test('contract approval allows the current pending node after all earlier steps passed', () => {
|
|
assert.equal(approvalState().pending.value?.id, 'b')
|
|
})
|
|
for (const status of ['draft', 'approved', 'executing', 'finished', 'invalid', '']) {
|
|
test(`contract approval denies status ${status || '(empty)'}`, () => {
|
|
assert.equal(approvalState({ status }).pending.value, null)
|
|
})
|
|
}
|
|
for (const prior of ['rejected', 'pending', undefined]) {
|
|
test(`contract approval denies earlier ${prior ?? '(missing)'} result`, () => {
|
|
const state = approvalState()
|
|
state.approvalList.value[0].approvalResult = prior
|
|
assert.equal(state.pending.value, null)
|
|
})
|
|
}
|
|
for (const current of [0, undefined]) {
|
|
test(`contract approval denies isCurrent=${current}`, () => {
|
|
const state = approvalState()
|
|
state.approvalList.value[1].isCurrent = current
|
|
assert.equal(state.pending.value, null)
|
|
})
|
|
}
|
|
for (const result of ['approved', 'rejected', undefined]) {
|
|
test(`contract approval denies own ${result ?? '(missing)'} result`, () => {
|
|
const state = approvalState()
|
|
state.approvalList.value[1].approvalResult = result
|
|
assert.equal(state.pending.value, null)
|
|
})
|
|
}
|
|
test('contract approval denies a node belonging to another user', () => {
|
|
const state = approvalState()
|
|
state.approvalList.value[1].approverId = 'someone'
|
|
assert.equal(state.pending.value, null)
|
|
})
|
|
test('contract approval ignores previous submission rounds after rejection and resubmission', () => {
|
|
const state = approvalState()
|
|
state.approvalList.value = [
|
|
{
|
|
id: 'old',
|
|
step: 1,
|
|
approverId: 'me',
|
|
approvalResult: 'rejected',
|
|
isCurrent: 0,
|
|
createTime: '2026-10-20 10:00:00'
|
|
},
|
|
{
|
|
id: 'first',
|
|
step: 1,
|
|
approverId: 'other',
|
|
approvalResult: 'approved',
|
|
isCurrent: 0,
|
|
createTime: '2026-10-21 10:00:00'
|
|
},
|
|
{
|
|
id: 'current',
|
|
step: 2,
|
|
approverId: 'me',
|
|
approvalResult: 'pending',
|
|
isCurrent: 1,
|
|
createTime: '2026-10-21 10:00:00'
|
|
}
|
|
]
|
|
assert.equal(state.pending.value?.id, 'current')
|
|
state.approvalList.value[1].approvalResult = 'rejected'
|
|
assert.equal(state.pending.value, null, 'a rejection in the current round still blocks approval')
|
|
})
|
|
|
|
for (const createTime of ['', undefined]) {
|
|
test(`contract approval without pending timestamps (${createTime === '' ? 'empty API value' : 'absent field'}) matches the backend fallback`, () => {
|
|
const state = approvalState()
|
|
state.approvalList.value = [
|
|
{
|
|
id: 'old',
|
|
step: 1,
|
|
approverId: 'other',
|
|
approvalResult: 'rejected',
|
|
isCurrent: 0,
|
|
createTime
|
|
},
|
|
{
|
|
id: 'new-first',
|
|
step: 1,
|
|
approverId: 'other',
|
|
approvalResult: 'approved',
|
|
isCurrent: 0,
|
|
createTime
|
|
},
|
|
{
|
|
id: 'current',
|
|
step: 2,
|
|
approverId: 'me',
|
|
approvalResult: 'pending',
|
|
isCurrent: 1,
|
|
createTime
|
|
}
|
|
]
|
|
assert.equal(state.pending.value?.id, 'current')
|
|
state.approvalList.value[1].approvalResult = 'pending'
|
|
assert.equal(
|
|
state.pending.value,
|
|
null,
|
|
'an earlier pending step still blocks approval without timestamps'
|
|
)
|
|
})
|
|
}
|
|
|
|
function detailLoader(api: Record<string, any> = {}) {
|
|
const bindings = {
|
|
props: { embedded: false },
|
|
route: reactive({
|
|
name: 'MpCrmContractDetail',
|
|
get path() {
|
|
return `/m/crm/contract/${this.params.id}`
|
|
},
|
|
params: { id: 'A' }
|
|
}),
|
|
contract: ref<any>(null),
|
|
products: ref<any[]>([]),
|
|
approvalList: ref<any[]>([]),
|
|
approvalError: ref(''),
|
|
approvalLoading: ref(false),
|
|
loading: ref(false),
|
|
errorText: ref(''),
|
|
getContract: async (id: string) => ({
|
|
data: { id, status: 'approving', quoteId: 'Q', products: [] }
|
|
}),
|
|
getQuote: async () => ({ data: { quoteNo: 'SXDD-20261009005' } }),
|
|
listContractApproval: async () => ({ data: [{ id: 'approval-A' }] }),
|
|
runRouteAction: async () => {},
|
|
categoryPaths: ref(new Map()),
|
|
loadCategoryPaths: async () => {},
|
|
...api
|
|
}
|
|
const load = functions(
|
|
detail,
|
|
['loadDetail', 'isCurrentLoad', 'currentRecordId', 'loadApprovalHistory'],
|
|
bindings,
|
|
'let detailRequestSeq = 0; let approvalRequestSeq = 0; const shellReady = true; const disposed = false'
|
|
)
|
|
return { ...bindings, load }
|
|
}
|
|
|
|
test('contract detail loads the linked quote number using quoteId', async () => {
|
|
const calls: string[] = []
|
|
const state = detailLoader({
|
|
getQuote: async (id: string) => {
|
|
calls.push(id)
|
|
return { data: { quoteNo: 'SXDD-20261009005' } }
|
|
}
|
|
})
|
|
await state.load()
|
|
assert.deepEqual(calls, ['Q'])
|
|
assert.equal(state.contract.value.quoteNo, 'SXDD-20261009005')
|
|
})
|
|
test('a failed quote lookup preserves contract and approval details', async () => {
|
|
const state = detailLoader({
|
|
getQuote: async () => {
|
|
throw new Error('quote unavailable')
|
|
}
|
|
})
|
|
await state.load()
|
|
assert.equal(state.contract.value.id, 'A')
|
|
assert.equal(state.approvalList.value[0].id, 'approval-A')
|
|
assert.equal(state.errorText.value, '')
|
|
})
|
|
test('a failed approval lookup still shows the linked quote and main detail', async () => {
|
|
const state = detailLoader({
|
|
listContractApproval: async () => {
|
|
throw new Error('approval unavailable')
|
|
}
|
|
})
|
|
await state.load()
|
|
assert.equal(state.contract.value.quoteNo, 'SXDD-20261009005')
|
|
assert.deepEqual(state.approvalList.value, [])
|
|
assert.equal(state.errorText.value, '')
|
|
})
|
|
test('older main detail responses cannot replace a newer route', async () => {
|
|
const old = deferred<any>()
|
|
const state = detailLoader({
|
|
getContract: (id: string) => (id === 'A' ? old.promise : Promise.resolve({ data: { id } }))
|
|
})
|
|
const first = state.load()
|
|
state.route.params.id = 'B'
|
|
await state.load()
|
|
old.resolve({ data: { id: 'A', products: [{ id: 'old' }] } })
|
|
await first
|
|
assert.equal(state.contract.value.id, 'B')
|
|
assert.deepEqual(state.products.value, [])
|
|
})
|
|
test('older quote and approval responses cannot alter the newer route', async () => {
|
|
const oldQuote = deferred<any>()
|
|
const oldApproval = deferred<any>()
|
|
const state = detailLoader({
|
|
getContract: async (id: string) => ({ data: { id, quoteId: `Q-${id}` } }),
|
|
getQuote: (id: string) => {
|
|
if (id === 'Q-A') return oldQuote.promise
|
|
return Promise.resolve({ data: { quoteNo: 'quote-B' } })
|
|
},
|
|
listContractApproval: (id: string) =>
|
|
id === 'A' ? oldApproval.promise : Promise.resolve({ data: [{ id: 'approval-B' }] })
|
|
})
|
|
const first = state.load()
|
|
// Flush microtasks without waiting forever when the old implementation never fetches a quote.
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
state.route.params.id = 'B'
|
|
await state.load()
|
|
oldQuote.resolve({ data: { quoteNo: 'quote-A' } })
|
|
oldApproval.resolve({ data: [{ id: 'approval-A' }] })
|
|
await first
|
|
assert.equal(state.contract.value.quoteNo, 'quote-B')
|
|
assert.equal(state.approvalList.value[0].id, 'approval-B')
|
|
})
|
|
|
|
test('contract mobile rendering matches quote number, unit dictionary, tax and payable total', async () => {
|
|
const template = source(detail).descriptor.template!.content
|
|
const section = (id: string) =>
|
|
template.match(new RegExp(`<section\\s+[^>]*id="contract-sec-${id}"[\\s\\S]*?<\\/section>`))![0]
|
|
const contract = { amount: 750, freight: 20, barrelDeposit: 30, amountWithTax: 795 }
|
|
const dictStore = {
|
|
getLabel: (type: string, unit: string) =>
|
|
type === 'crm_product_unit' && unit === 'dun' ? '吨' : unit
|
|
}
|
|
const unitLabel = functions(detail, ['unitLabel'], { dictStore }) || ((unit: string) => unit)
|
|
const bindings = {
|
|
...productBindings(contract, {}),
|
|
contract,
|
|
products: [{ id: 'p', unit: 'dun', taxAmount: 86.28, subtotal: 750 }],
|
|
formatContractMoney: (amount: any) => `¥${Number(amount || 0).toFixed(2)}`,
|
|
formatBrandConstraintLabel: () => '',
|
|
unitLabel,
|
|
dictStore
|
|
}
|
|
// Read the amount expression from the real template; no mirrored total calculation in this test.
|
|
const html = await render(section('amount') + section('products'), bindings)
|
|
assert.match(html, /吨/)
|
|
assert.match(html, /税费[\s\S]*¥86\.28/)
|
|
assert.match(html, /合计金额[\s\S]*¥800\.00/)
|
|
assert.doesNotMatch(html, /¥795\.00/)
|
|
})
|
|
|
|
for (const contractType of [
|
|
'framework_sale',
|
|
'framework_purchase',
|
|
'framework',
|
|
'sales',
|
|
'purchase'
|
|
]) {
|
|
for (const priceMode of ['market', 'fixed']) {
|
|
test(`contract ${contractType}/${priceMode} renders all three monetary fields consistently`, async () => {
|
|
const template = source(detail).descriptor.template!.content
|
|
const products = template.match(/<section id="contract-sec-products"[\s\S]*?<\/section>/)![0]
|
|
const row = { priceMode, price: 100, subtotal: 750, taxAmount: 86.28 }
|
|
const html = await render(products, productBindings({ contractType }, row))
|
|
const market = contractType.startsWith('framework') && priceMode === 'market'
|
|
for (const [label, money] of [
|
|
['含税单价(元)', '¥100.00'],
|
|
['含税总价(元)', '¥750.00'],
|
|
['税费(元)', '¥86.28']
|
|
]) {
|
|
assert.ok(
|
|
html.includes(`<span>${label}</span><span>${market ? '随行就市' : money}</span>`),
|
|
`${label} must follow ${contractType}/${priceMode}`
|
|
)
|
|
}
|
|
if (market) assert.doesNotMatch(html, /¥(?:100\.00|750\.00|86\.28)/)
|
|
})
|
|
}
|
|
}
|
|
test('framework market zero and legacy snake_case modes use the same Web semantics', async () => {
|
|
const products = source(detail).descriptor.template!.content.match(
|
|
/<section id="contract-sec-products"[\s\S]*?<\/section>/
|
|
)![0]
|
|
for (const row of [
|
|
{ priceMode: 'market', price: 0, subtotal: 0, taxAmount: 0 },
|
|
{ price_mode: 'market', price: 15, subtotal: 100, taxAmount: 13 },
|
|
{ price: 0, subtotal: 0, taxAmount: 0 }
|
|
]) {
|
|
const html = await render(products, productBindings({ contractType: 'framework_sale' }, row))
|
|
assert.equal(html.match(/随行就市/g)?.length, 3)
|
|
assert.doesNotMatch(html, /¥0\.00/)
|
|
}
|
|
})
|
|
test('fixed zero pricing remains numeric for framework contracts', async () => {
|
|
const products = source(detail).descriptor.template!.content.match(
|
|
/<section id="contract-sec-products"[\s\S]*?<\/section>/
|
|
)![0]
|
|
const html = await render(
|
|
products,
|
|
productBindings(
|
|
{ contractType: 'framework_purchase' },
|
|
{ priceMode: 'fixed', price: 0, subtotal: 0, taxAmount: 0 }
|
|
)
|
|
)
|
|
assert.equal(html.match(/¥0\.00/g)?.length, 3)
|
|
assert.doesNotMatch(html, /随行就市/)
|
|
})
|
|
|
|
function amountState(page: string) {
|
|
let reloads = 0
|
|
const state: Record<string, any> = {
|
|
keyword: ref('搜索词'),
|
|
view: ref('all'),
|
|
query: ref({}),
|
|
minAmount: ref(''),
|
|
maxAmount: ref(''),
|
|
amountRange: ref(''),
|
|
contractType: ref(''),
|
|
quoteType: ref(''),
|
|
moreOpen: ref(false),
|
|
visibleColumns: ref(defaultQuoteColumns),
|
|
cleanQuoteQuery,
|
|
cleanContractQuery,
|
|
reload: () => reloads++,
|
|
requestSnapshot: () => JSON.stringify(state.query.value)
|
|
}
|
|
const operation = (name: string) =>
|
|
functions(page, [name, 'syncQuickFilters', 'applyAmountRange'], state)
|
|
return { state, operation, reloads: () => reloads }
|
|
}
|
|
for (const page of lists) {
|
|
const field = page.startsWith('quote') ? 'finalAmount' : 'amount'
|
|
for (const [min, max, expected] of [
|
|
['10000', '100000', '1-10w'],
|
|
['10000.00', '100000.00', '1-10w'],
|
|
['', '10000', 'lt1w'],
|
|
['100000', '', 'gt10w'],
|
|
['12000', '23000', 'custom'],
|
|
['', '', '']
|
|
]) {
|
|
test(`${page}: applied query preserves amount preset ${min}..${max}`, () => {
|
|
const h = amountState(page)
|
|
const q =
|
|
min || max
|
|
? {
|
|
[field + 'Op']: 'between',
|
|
...(min ? { [field + 'Min']: min } : {}),
|
|
...(max ? { [field + 'Max']: max } : {})
|
|
}
|
|
: {}
|
|
h.operation('applyFilters')(q)
|
|
assert.equal(h.state.amountRange.value, expected)
|
|
assert.equal(h.state.minAmount.value, min)
|
|
assert.equal(h.state.maxAmount.value, max)
|
|
assert.equal(h.state.keyword.value, '搜索词')
|
|
})
|
|
}
|
|
test(`${page}: selecting all in Vant clears custom amount comparison and preserves other filters`, () => {
|
|
const h = amountState(page)
|
|
h.operation('applyFilters')({ [field + 'Op']: 'gte', [field]: '23000', statusIn: 'draft' })
|
|
assert.equal(h.state.amountRange.value, 'custom')
|
|
const before = h.reloads()
|
|
clickAmountOption(h.state.amountRange.value, '', (event, v) => {
|
|
if (event === 'update:modelValue') h.state.amountRange.value = v
|
|
if (event === 'change') h.operation('onAmountRangeChange')()
|
|
})
|
|
assert.deepEqual(h.state.query.value, { statusIn: 'draft' })
|
|
assert.equal(h.reloads(), before + 1)
|
|
})
|
|
test(`${page}: custom amount label is rendered and empty filters clear the active marker`, () => {
|
|
const h = amountState(page)
|
|
h.operation('applyFilters')({ [field + 'Min']: '12000', [field + 'Max']: '23000' })
|
|
const active = value(page, 'moreActive', { computed, query: h.state.query })
|
|
assert.equal(active.value, true)
|
|
const dropdown = source(page).descriptor.template!.content.match(
|
|
/<van-dropdown-item[^>]*v-model="amountRange"[^>]*\/>/
|
|
)![0]
|
|
const title = dropdown.match(/:title="([^"]+)"/)![1]
|
|
assert.equal(
|
|
new Function('amountRange', `return ${title}`)(h.state.amountRange.value),
|
|
'自定义金额'
|
|
)
|
|
h.operation('applyFilters')({})
|
|
assert.equal(active.value, false)
|
|
assert.equal(h.state.amountRange.value, '')
|
|
})
|
|
test(`${page}: detail return restores the query and derives the same custom amount label`, async () => {
|
|
const path = page.startsWith('quote') ? '/m/crm/quote' : '/m/crm/contract'
|
|
let leave: any
|
|
const runtime = {
|
|
ref,
|
|
watch,
|
|
nextTick: async () => {},
|
|
useRoute: () => ({ path }),
|
|
useRouter: () => ({ afterEach: () => () => {} }),
|
|
useUserStore: () => ({ accessToken: 'session', getUserInfo: { id: 'me' } }),
|
|
onBeforeRouteLeave: (fn: any) => {
|
|
leave = fn
|
|
},
|
|
onBeforeUnmount() {},
|
|
window: { scrollY: 0, scrollTo() {} }
|
|
}
|
|
const helper = readFileSync(new URL('useListReturnState.ts', root), 'utf8')
|
|
.replace(/^import .*$/gm, '')
|
|
.replace('export function', 'function')
|
|
const useListReturnState = new Function(
|
|
...Object.keys(runtime),
|
|
ts.transpile(helper, { target: ts.ScriptTarget.ESNext }) + ';return useListReturnState'
|
|
)(...Object.values(runtime))
|
|
const create = () => {
|
|
const h = amountState(page)
|
|
const returned = value(page, 'listReturnState', {
|
|
...h.state,
|
|
tabQueries: ref({}),
|
|
isQuoteListReturnRoute,
|
|
isContractListReturnRoute,
|
|
useListReturnState,
|
|
pageNum: ref(1),
|
|
expandedIds: ref(new Set()),
|
|
hasMore: ref(false),
|
|
fetchPage: async () => true
|
|
})
|
|
return { ...h, returned }
|
|
}
|
|
const first = create()
|
|
first.operation('applyFilters')({ [field + 'Min']: '12000', [field + 'Max']: '23000' })
|
|
leave({ path: `${path}/detail-id`, query: {} })
|
|
const back = create()
|
|
await back.returned.restore()
|
|
back.operation('syncQuickFilters')()
|
|
assert.equal(back.state.amountRange.value, 'custom')
|
|
assert.equal(back.state.minAmount.value, '12000')
|
|
assert.equal(back.state.maxAmount.value, '23000')
|
|
})
|
|
}
|