Files
shanxia2.0/web-admin/scripts/mobile-quote-product-meta.test.ts
T
2026-10-22 16:24:46 +08:00

817 lines
27 KiB
TypeScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
import { computed, effectScope, reactive, ref, shallowRef, watch } from 'vue'
import ts from 'typescript'
import {
prepareLineAliasAndBrandForSubmit,
applyAliasDisplayMeta
} from '../src/utils/crm-product-brand'
const helper: any = await import('../src/views/m/crm/quote/quoteProductMeta').catch(() => ({}))
const product = {
productName: '标准名称',
aliases: [
{ id: 'a1', aliasName: '第一别名', orderNum: 7 },
{ id: 'a2', aliasName: '第二别名' }
],
brands: [
{ id: 'b1', brandName: '新品牌名', status: 'on', orderNum: 9 },
{ id: 'b2', brandName: '停用品牌', status: 'off' }
]
}
const line = (name = '第二别名'): any => ({
productId: 'p1',
productName: name,
brandMode: 'required',
brandId: 'b1',
brandName: '历史品牌名'
})
const deferred = () => {
let resolve!: (v: any) => void
let reject!: (v: any) => void
const promise = new Promise<any>((y, n) => {
resolve = y
reject = n
})
return { resolve, reject, promise }
}
function actualFunction(file: string, name: string, bindings: any) {
const source = parse(readFileSync(new URL(file, import.meta.url), 'utf8')).descriptor.scriptSetup!
.content
const ast = ts.createSourceFile('component.ts', source, ts.ScriptTarget.Latest, true)
const declaration = ast.statements.find(
(n) => ts.isFunctionDeclaration(n) && n.name?.text === name
)!
const code = ts.transpile(declaration.getText(ast), { target: ts.ScriptTarget.ESNext })
return new Function(...Object.keys(bindings), `${code}; return ${name}`)(
...Object.values(bindings)
)
}
for (const failure of [false, true])
test(`detail preserves saved names and brands (${failure ? 'GET failure' : 'success'})`, async () => {
const rows = [
line(),
line('标准名称'),
line('未知历史名称'),
{ ...line(), brandMode: 'any', brandId: undefined, brandName: undefined },
{ ...line(), brandMode: undefined, brandId: undefined, brandName: undefined },
{ ...line(), productId: '', aliasName: '错误旧别名' }
]
const enrich = actualFunction(
'../src/views/m/crm/quote/detail.vue',
'enrichQuoteProductAliasDisplay',
{
applyAliasDisplayMeta,
fetchProductDetail: async () => {
if (failure) throw Error('offline')
return { data: product }
},
enrichQuoteDetailProductMeta: helper.enrichQuoteDetailProductMeta
}
)
await enrich(rows)
assert.deepEqual(
rows.map((r) => r.aliasName),
failure
? Array(6).fill(undefined)
: ['第二别名', undefined, undefined, '第二别名', '第二别名', undefined]
)
assert.deepEqual(
rows.map((r) => r.productName),
['第二别名', '标准名称', '未知历史名称', '第二别名', '第二别名', '第二别名']
)
assert.equal(rows[0].brandName, '历史品牌名')
assert.equal(rows[3].brandMode, 'any')
assert.equal(rows[4].brandMode, undefined)
assert.equal(rows[5].standardProductName, '第二别名')
})
function editor(rows: any[], fetch = async () => ({ data: product })) {
assert.equal(
typeof helper.createQuoteProductMetaEditor,
'function',
'mobile metadata controller is missing'
)
return helper.createQuoteProductMetaEditor(() => rows, fetch)
}
test('one row selection is isolated; maintenance refreshes matching rows and clears removed aliases', async () => {
const rows = [line(), line(), { ...line(), productId: 'other' }]
const e = editor(rows)
await e.load(rows)
assert.equal(e.open(rows[0], 'alias'), true)
e.confirm({ id: '第一别名' })
assert.equal(rows[0].aliasName, '第一别名')
assert.equal(rows[1].aliasName, '第二别名')
const unrelated = JSON.stringify(rows[2])
e.managed({
productId: 'p1',
product: { ...product, aliases: [{ aliasName: '第二别名' }], brands: [] }
})
assert.equal(rows[0].aliasName, undefined)
assert.equal(rows[0].productName, '标准名称')
assert.equal(rows[1].aliasName, '第二别名')
assert.equal(rows[0].brandId, undefined)
assert.equal(JSON.stringify(rows[2]), unrelated)
})
test('explicit clear resets history while untouched history survives real submit helper', async () => {
const rows = [line('历史名称'), line('历史名称')]
const e = editor(rows)
await e.load(rows)
e.open(rows[0], 'alias')
e.confirm(null)
rows.forEach(prepareLineAliasAndBrandForSubmit)
assert.equal(rows[0].productName, '标准名称')
assert.equal(rows[1].productName, '历史名称')
})
test('GET failure preserves snapshots, blocks folding/save and retry becomes ready', async () => {
const rows = [{ ...line(), aliasName: '第二别名', brandOptions: [] }]
let fail = true
const e = editor(rows, async () => {
if (fail) throw Error('offline')
return product as any
})
const before = JSON.stringify(rows)
const loading = e.load(rows)
assert.match(e.blockingMessage(), /加载/)
await loading
assert.equal(JSON.stringify(rows), before)
assert.match(e.blockingMessage(), /失败/)
assert.equal(e.prepare(), false)
assert.equal(JSON.stringify(rows), before)
fail = false
await e.load(rows)
assert.equal(e.blockingMessage(), '')
assert.equal(e.prepare(), true)
assert.equal(rows[0].brandName, '新品牌名')
})
test('requests deduplicate and ignore invalidated or removed rows; deleted active row cannot confirm', async () => {
const rows = [line(), line()]
const pending = deferred()
let gets = 0
const e = editor(rows, async () => {
gets++
return pending.promise
})
const load = e.load(rows)
assert.equal(gets, 1)
e.invalidate()
pending.resolve(product)
await load
assert.equal(rows[0].standardProductName, undefined)
const removed = rows[0]
e.open(removed, 'alias')
rows.splice(0, 1)
e.confirm({ id: '第一别名' })
assert.equal(removed.aliasName, undefined)
const p = deferred()
const e2 = editor(rows, () => p.promise)
const second = e2.load(rows)
const original = rows.pop()
p.resolve(product)
await second
assert.equal(original.standardProductName, undefined)
})
function manager(
fetch = async () => product,
update: (payload: any) => Promise<any> = async () => {}
) {
assert.equal(
typeof helper.createQuoteProductMetaManager,
'function',
'mobile maintenance controller is missing'
)
const saved: any[] = []
const m = helper.createQuoteProductMetaManager(fetch, update, (event: any) => saved.push(event))
return { m, saved }
}
test('cancel zero writes, copied draft, blank/duplicate blocked, brand status/order/id preserved', async () => {
const writes: any[] = []
const { m } = manager(undefined, async (p) => {
writes.push(p)
})
await m.open('p1', 'alias')
m.rows.value[0].aliasName = 'changed'
m.close()
assert.equal(writes.length, 0)
assert.equal(product.aliases[0].aliasName, '第一别名')
await m.open('p1', 'alias')
m.rows.value.push({ aliasName: ' ' })
await m.save()
assert.match(m.error.value, /不能为空/)
assert.equal(writes.length, 0)
m.rows.value.pop()
m.rows.value[1].aliasName = ' 第一别名 '
await m.save()
assert.match(m.error.value, /重复/)
assert.equal(writes.length, 0)
await m.open('p1', 'brand')
assert.equal(m.rows.value[1].status, 'off')
m.add()
assert.equal(m.rows.value[2].status, 'on')
m.remove(2)
await m.save()
assert.deepEqual(Object.keys(writes[0]).sort(), ['brands', 'id'])
assert.equal(writes[0].brands[0].orderNum, 9)
assert.equal(writes[0].brands[1].status, 'off')
})
test('update failure retains draft, emits nothing, double save one write and busy cannot close', async () => {
const pending = deferred()
let writes = 0
const { m, saved } = manager(undefined, async () => {
writes++
await pending.promise
})
await m.open('p1', 'alias')
m.rows.value[0].aliasName = '改名'
const save = m.save()
await m.save()
assert.equal(writes, 1)
assert.equal(m.close(), false)
pending.reject(Error('403 无权限'))
await save
assert.match(m.error.value, /403/)
assert.equal(m.rows.value[0].aliasName, '改名')
assert.equal(saved.length, 0)
assert.equal(m.busy.value, false)
})
test('update success GET failure retries GET only, emits once with captured product id', async () => {
let gets = 0
let writes = 0
const { m, saved } = manager(
async () => {
gets++
if (gets === 2) throw Error('offline')
return product
},
async () => {
writes++
}
)
await m.open('p1', 'alias')
await m.save()
assert.equal(writes, 1)
assert.match(m.refreshError.value, /已保存.*刷新选项失败/)
assert.equal(saved.length, 0)
await m.save()
assert.equal(writes, 1)
assert.equal(gets, 3)
assert.equal(saved.length, 1)
assert.equal(saved[0].productId, 'p1')
await m.save()
assert.equal(saved.length, 1)
})
test('old success/error/finally cannot touch reopened manager session', async () => {
const first = deferred()
const next = deferred()
let gets = 0
const { m, saved } = manager(() => (++gets === 1 ? first.promise : next.promise))
const a = m.open('p1', 'alias')
m.close()
const b = m.open('p2', 'brand')
first.reject(Error('old error'))
await a
assert.equal(m.loading.value, true)
assert.equal(m.error.value, '')
next.resolve(product)
await b
assert.equal(m.productId.value, 'p2')
assert.equal(saved.length, 0)
const put = deferred()
const n = manager(undefined, () => put.promise)
await n.m.open('p1', 'alias')
const old = n.m.save()
n.m.invalidate()
await n.m.open('p2', 'brand')
put.resolve({})
await old
assert.equal(n.m.productId.value, 'p2')
assert.equal(n.saved.length, 0)
assert.equal(n.m.busy.value, false)
})
test('PAGE submit guard executes actual component function; desktop never calls mobile guard', async () => {
const rows = [line()]
const e = editor(rows, async () => {
throw Error('offline')
})
await e.load(rows)
const form = { value: { products: rows } }
const sync = actualFunction(
'../src/views/crm/quote/components/QuoteFormPanel.vue',
'syncQuoteProductsForSubmit',
{
isPageVariant: { value: true },
quoteMeta: { prepare: e.prepare, blockingMessage: e.blockingMessage },
form,
showFailToast: () => {},
prepareLineAliasAndBrandForSubmit
}
)
const before = JSON.stringify(rows)
assert.equal(sync(), false)
assert.equal(JSON.stringify(rows), before)
const desktop = actualFunction(
'../src/views/crm/quote/components/QuoteFormPanel.vue',
'syncQuoteProductsForSubmit',
{
isPageVariant: { value: false },
quoteMeta: new Proxy(
{},
{
get() {
throw Error('desktop used mobile guard')
}
}
),
form,
showFailToast: () => {},
prepareLineAliasAndBrandForSubmit
}
)
desktop()
assert.equal(rows[0].brandMode, undefined)
})
test('brand picking uses real IDs and existing change handler, empty lists still open maintenance', async () => {
const rows = [line(), line()]
let called: any
const e = helper.createQuoteProductMetaEditor(
() => rows,
async () => product,
(row: any, id: any) => {
called = { row, id }
}
)
await e.load(rows)
e.open(rows[0], 'brand')
e.confirm({ id: 'b1' })
assert.equal(called.row, rows[0])
assert.equal(called.id, 'b1')
assert.equal(rows[1].brandId, 'b1')
e.managed({ productId: 'p1', product: { productName: '标准名称', aliases: [], brands: [] } })
assert.equal(e.open(rows[0], 'alias'), true)
assert.deepEqual(e.options.value, [])
assert.equal(e.open(rows[0], 'brand'), true)
assert.deepEqual(e.options.value, [])
})
test('deleting an actually open row and changing product ID invalidate selection by object identity', async () => {
const rows = [line(), line()]
const e = editor(rows)
await e.load(rows)
const removed = rows[0]
e.open(removed, 'alias')
rows.splice(0, 1)
e.cancelRemovedRow()
assert.equal(e.pickerOpen.value, false)
e.confirm({ id: '第一别名' })
assert.equal(removed.aliasName, '第二别名')
assert.equal(rows[0].aliasName, '第二别名')
e.open(rows[0], 'alias')
rows[0].productId = 'changed'
e.confirm(null)
assert.equal(rows[0].aliasName, '第二别名')
})
test('manager trim payload only updates requested field and saved event does not autochoose new entries', async () => {
let payload: any
const { m, saved } = manager(undefined, async (p) => {
payload = p
})
await m.open('p1', 'alias')
m.rows.value[0].aliasName = ' 第一别名 '
m.remove(1)
await m.save()
assert.deepEqual(payload, {
id: 'p1',
aliases: [{ id: 'a1', aliasName: '第一别名', orderNum: 7 }]
})
const rows = [line('标准名称')]
const e = editor(rows)
await e.load(rows)
e.managed(saved[0])
assert.equal(rows[0].aliasName, undefined)
})
test('successful empty GET alone applies empty semantics; failed same-product retry preserves selected snapshot', async () => {
const rows = [line()]
let fail = false
const e = editor(rows, async () => {
if (fail) throw Error('offline')
return product as any
})
await e.load(rows)
e.open(rows[0], 'alias')
e.confirm({ id: '第一别名' })
const before = JSON.stringify(rows)
fail = true
await e.load(rows)
assert.equal(JSON.stringify(rows), before)
const empty = helper.createQuoteProductMetaEditor(
() => rows,
async () => ({ productName: '标准名称', aliases: [], brands: [] })
)
await empty.load(rows)
assert.equal(rows[0].brandMode, undefined)
assert.equal(rows[0].brandName, undefined)
assert.equal(rows[0].aliasName, undefined)
})
test('a concurrent metadata load cannot reuse pre-maintenance data for a new row', async () => {
const pending = deferred()
let calls = 0
const rows = [line()]
const e = helper.createQuoteProductMetaEditor(
() => rows,
async () => {
calls++
return calls === 1 ? pending.promise : { ...product, aliases: [{ aliasName: '新别名' }] }
}
)
const old = e.load(rows)
e.managed({ productId: 'p1', product: { ...product, aliases: [{ aliasName: '新别名' }] } })
const added = line('新别名')
rows.push(added)
const latest = e.load([added])
pending.resolve(product)
await Promise.all([old, latest])
assert.equal(added.aliasName, '新别名')
assert.deepEqual(added.aliasOptions, ['新别名'])
})
test('retrying same-product metadata retains an existing unsaved alias selection', async () => {
const rows = [line()]
let fail = false
const e = editor(rows, async () => {
if (fail) throw Error('offline')
return product as any
})
await e.load(rows)
e.open(rows[0], 'alias')
e.confirm({ id: '第一别名' })
fail = true
await e.load(rows)
fail = false
await e.load(rows)
assert.equal(rows[0].aliasName, '第一别名')
})
function pageStateHarness() {
const source = parse(
readFileSync(
new URL('../src/views/crm/quote/components/QuoteFormPanel.vue', import.meta.url),
'utf8'
)
).descriptor.scriptSetup!.content
const ast = ts.createSourceFile('component.ts', source, ts.ScriptTarget.Latest, true)
const names: string[] = []
const selected = ast.statements.filter((node) => {
if (ts.isVariableStatement(node)) {
const name = node.declarationList.declarations[0].name.getText(ast)
if (name.startsWith('quoteMeta') || name === 'handleBrandIdChange') {
names.push(name)
return true
}
}
if (ts.isFunctionDeclaration(node)) {
const name = node.name?.text || ''
if (
[
'openQuoteMetaPicker',
'openQuoteMetaManage',
'resetQuoteMetaSession',
'enrichQuoteProductBrandMeta'
].includes(name)
) {
names.push(name)
return true
}
}
return (
ts.isExpressionStatement(node) &&
/^(watch\(|onBeforeUnmount\()/.test(node.getText(ast)) &&
/quoteMeta|resetQuoteMetaSession/.test(node.getText(ast))
)
})
const cleanup: Array<() => void> = []
const bindings = {
ref,
shallowRef,
computed,
reactive,
watch,
onBeforeUnmount: (fn: () => void) => cleanup.push(fn),
isPageVariant: ref(true),
form: ref({ products: [line(), line()] }),
route: reactive({ fullPath: '/m/crm/quote/form?id=1' }),
createQuoteProductMetaEditor: helper.createQuoteProductMetaEditor,
fetchProductDetail: async () => product,
showFailToast: () => {},
applyBrandSelectChange: (row: any, id: any, options: any[]) => {
row.brandId = id
row.brandName = options.find((item) => item.id === id)?.brandName
row.brandMode = id ? 'required' : 'any'
}
}
const scope = effectScope()
const code = ts.transpile(selected.map((node) => node.getText(ast)).join('\n'), {
target: ts.ScriptTarget.ESNext
})
const state = scope.run(() =>
new Function(...Object.keys(bindings), `${code}; return {${names.join(',')}}`)(
...Object.values(bindings)
)
)
return {
...bindings,
...state,
dispose() {
cleanup.forEach((fn) => fn())
scope.stop()
}
}
}
test('actual PAGE wiring closes picker before maintenance; deletion/route/unmount cancel sessions', async () => {
const h = pageStateHarness()
await h.enrichQuoteProductBrandMeta(h.form.value.products)
h.openQuoteMetaPicker(h.form.value.products[0], 'alias')
assert.equal(h.quoteMeta.pickerOpen, true)
h.openQuoteMetaManage()
assert.equal(h.quoteMeta.pickerOpen, false)
assert.equal(h.quoteMetaManageVisible.value, true)
assert.equal(h.quoteMetaManageId.value, 'p1')
h.form.value.products.splice(0, 1)
assert.equal(h.quoteMetaManageVisible.value, false)
h.openQuoteMetaPicker(h.form.value.products[0], 'brand')
h.route.fullPath = '/m/crm/quote/form?id=2'
assert.equal(h.quoteMeta.pickerOpen, false)
assert.equal(h.quoteMetaManageVisible.value, false)
await h.enrichQuoteProductBrandMeta(h.form.value.products)
h.openQuoteMetaPicker(h.form.value.products[0], 'alias')
h.dispose()
assert.equal(h.quoteMeta.pickerOpen, false)
})
test('changed Vue files compile their actual scripts and templates', () => {
for (const file of [
'../src/views/crm/quote/components/QuoteFormPanel.vue',
'../src/views/m/crm/quote/detail.vue',
'../src/views/m/crm/quote/QuoteProductMetaManageSheet.vue'
]) {
const { descriptor, errors } = parse(readFileSync(new URL(file, import.meta.url), 'utf8'), {
filename: file
})
assert.deepEqual(errors, [])
const script = compileScript(descriptor, { id: 'quote-meta-test' })
const compiled = compileTemplate({
id: 'quote-meta-test',
filename: file,
source: descriptor.template!.content,
compilerOptions: { bindingMetadata: script.bindings }
})
assert.deepEqual(compiled.errors, [])
}
})
test('a missing product response is an error, never successful empty metadata', async () => {
const rows = [line()]
const before = JSON.stringify(rows)
const e = editor(rows, async () => ({ data: null }) as any)
await e.load(rows)
assert.match(e.blockingMessage(), /失败/)
assert.equal(JSON.stringify(rows), before)
})
for (const failed of [false, true]) {
test(`deleted newly added duplicate product settles surviving ready rows (${failed ? 'failure' : 'success'})`, async () => {
const rows = [line()]
const pending = deferred()
let calls = 0
const e = editor(rows, async () => (++calls === 1 ? product : pending.promise) as any)
await e.load(rows)
const existingSnapshot = JSON.stringify(rows[0])
const added = line()
rows.push(added)
const load = e.load([added])
assert.match(e.blockingMessage(), /加载/)
rows.splice(1, 1)
if (failed) pending.reject(Error('offline'))
else pending.resolve(product)
await load
assert.equal(e.states.value.p1, 'ready')
assert.equal(e.blockingMessage(), '')
assert.equal(JSON.stringify(rows[0]), existingSnapshot)
assert.equal(added.standardProductName, undefined)
assert.equal(e.prepare(), true)
})
}
test('duplicate deletion never marks surviving unresolved metadata ready and failed rows remain retryable', async () => {
const rows = [line()]
const pending = deferred()
let calls = 0
const e = editor(rows, async () => (++calls === 1 ? product : pending.promise) as any)
await e.load(rows)
const removed = line()
const retained = line()
rows.push(removed, retained)
const first = e.load([removed])
const second = e.load([retained])
rows.splice(1, 1)
assert.equal(e.prepare(), false)
pending.reject(Error('offline'))
await Promise.all([first, second])
assert.equal(e.states.value.p1, 'error')
assert.match(e.blockingMessage(), /失败/)
assert.equal(e.prepare(), false)
assert.equal(retained.standardProductName, undefined)
})
test('mobile maintenance uses brand palette and a full cell touch target with single toggle and busy locks', () => {
const file = '../src/views/m/crm/quote/QuoteProductMetaManageSheet.vue'
const descriptor = parse(readFileSync(new URL(file, import.meta.url), 'utf8')).descriptor
assert.match(descriptor.styles[0].content, /--van-primary-color:\s*#0a9f6d/)
assert.doesNotMatch(descriptor.styles[0].content, /#10b981/)
let target: any
function walk(node: any) {
if (node.tag === 'van-cell' && node.loc.source.includes('toggleBrandStatus')) target = node
node.children?.forEach(walk)
}
walk(descriptor.template!.ast)
assert.ok(target, 'brand switch needs a clickable cell target')
assert.match(target.loc.source, /@click="toggleBrandStatus\(row\)"/)
assert.match(target.loc.source, /@click\.stop/)
assert.match(
descriptor.styles[0].content,
/\.quote-meta-manage__brand-toggle\s*\{[^}]*min-height:\s*44px/s
)
const manager = { busy: false, persisted: false }
const toggle = actualFunction(file, 'toggleBrandStatus', { manager })
const row = { status: 'on' }
toggle(row)
assert.equal(row.status, 'off')
toggle(row)
assert.equal(row.status, 'on')
manager.busy = true
toggle(row)
assert.equal(row.status, 'on')
manager.busy = false
manager.persisted = true
toggle(row)
assert.equal(row.status, 'on')
})
for (const mode of ['alias', 'brand'] as const) {
for (const retryFrom of ['sheet', 'page'] as const) {
test(`persisted ${mode} changes block quote writes until ${retryFrom} GET retry synchronizes current choices`, async () => {
const rows = [line(), line()]
let serverProduct = JSON.parse(JSON.stringify(product))
let writes = 0
let reads = 0
let failRefresh = true
const fetch = async () => {
reads++
if (writes && failRefresh) throw Error('offline after PUT')
return JSON.parse(JSON.stringify(serverProduct))
}
const e = helper.createQuoteProductMetaEditor(() => rows, fetch)
await e.load(rows)
const snapshot = JSON.stringify(rows)
const saved: any[] = []
const persistedIds: string[] = []
const m = helper.createQuoteProductMetaManager(
fetch,
async (payload: any) => {
writes++
serverProduct = { ...serverProduct, ...payload }
},
(event: any) => {
saved.push(event)
e.managed(event)
},
(id: string) => {
persistedIds.push(id)
e.markPersisted(id)
}
)
await m.open('p1', mode)
if (mode === 'alias') m.remove(1)
else m.rows.value[0].status = 'off'
await m.save()
assert.match(m.refreshError.value, /已保存.*刷新选项失败/)
assert.equal(
e.prepare(),
false,
'persisted but unread product metadata must block quote write'
)
assert.equal(
JSON.stringify(rows),
snapshot,
'failure preserves line snapshots until refreshed'
)
assert.deepEqual(persistedIds, ['p1'])
assert.equal(saved.length, 0)
const readsBeforeRetry = reads
if (retryFrom === 'page') {
assert.equal(m.close(), true)
assert.equal(e.prepare(), false, 'closing maintenance must not bypass stale metadata')
await e.load(rows)
assert.equal(e.prepare(), false, 'failed page retry must retain the write block')
failRefresh = false
await e.load(rows)
} else {
failRefresh = false
await m.save()
}
assert.ok(reads > readsBeforeRetry)
assert.equal(writes, 1, 'refresh retries must never repeat product update')
assert.equal(e.blockingMessage(), '')
assert.equal(e.prepare(), true)
for (const row of rows) {
if (mode === 'alias') {
assert.equal(row.aliasName, undefined)
assert.equal(row.productName, '标准名称')
} else {
assert.equal(row.brandId, undefined)
assert.equal(row.brandMode, undefined)
assert.equal(row.brandName, undefined)
}
}
})
}
}
test('actual sheet persisted emit is wired to the PAGE metadata invalidation handler', async () => {
const rows = [line()]
let updated = false
const fetch = async () => {
if (updated) throw Error('refresh unavailable')
return product
}
const e = helper.createQuoteProductMetaEditor(() => rows, fetch)
await e.load(rows)
const sheet = parse(
readFileSync(
new URL('../src/views/m/crm/quote/QuoteProductMetaManageSheet.vue', import.meta.url),
'utf8'
)
).descriptor
const page = parse(
readFileSync(
new URL('../src/views/crm/quote/components/QuoteFormPanel.vue', import.meta.url),
'utf8'
)
).descriptor
let persistedHandler = ''
function visit(node: any) {
if (node.tag === 'QuoteProductMetaManageSheet')
persistedHandler =
node.props.find((prop: any) => prop.name === 'on' && prop.arg?.content === 'persisted')?.exp
?.content || ''
node.children?.forEach(visit)
}
visit(page.template!.ast)
assert.ok(persistedHandler, 'PAGE must receive the persisted event')
const ast = ts.createSourceFile(
'sheet.ts',
sheet.scriptSetup!.content,
ts.ScriptTarget.Latest,
true
)
const declaration = ast.statements.find(
(node) =>
ts.isVariableStatement(node) &&
node.declarationList.declarations[0].name.getText(ast) === 'manager'
)!
const code = ts.transpile(declaration.getText(ast), { target: ts.ScriptTarget.ESNext })
const events: string[] = []
const emit = (name: string, value: any) => {
events.push(name)
if (name === 'persisted')
new Function('quoteMeta', 'value', `return ${persistedHandler}(value)`)(e, value)
}
const m = new Function(
'reactive',
'createQuoteProductMetaManager',
'fetchProductDetail',
'fetchUpdateProduct',
'emit',
`${code}; return manager`
)(
reactive,
helper.createQuoteProductMetaManager,
fetch,
async () => {
updated = true
},
emit
)
await m.open('p1', 'alias')
m.remove(1)
await m.save()
assert.deepEqual(events, ['persisted'])
assert.equal(e.prepare(), false)
assert.match(e.blockingMessage(), /产品资料已保存.*刷新/)
})