Files
shanxia2.0/web-admin/scripts/crm-child-identity.test.ts
T

209 lines
7.5 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 {
buildPackageSpecsPayload,
normalizeContractProductPackageFields
} from '../src/utils/crm-package-spec'
import { partitionQuoteLinesForCopy } from '../src/utils/crm-quote-copy-products'
// Execute the actual form handlers, with UI/network dependencies replaced at the boundary.
function handler(file: string, name: string, bindings: Record<string, any>) {
const source = parse(readFileSync(new URL(`../src/${file}`, import.meta.url), 'utf8')).descriptor
.scriptSetup!.content
const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
const statement = ast.statements.find(
(node) =>
(ts.isVariableStatement(node) &&
node.declarationList.declarations.some((d) => d.name.getText(ast) === name)) ||
(ts.isFunctionDeclaration(node) && node.name?.getText(ast) === name)
)
assert.ok(statement, name)
return new Function(
...Object.keys(bindings),
ts.transpile(statement.getText(ast), { target: ts.ScriptTarget.ESNext }) + `;return ${name}`
)(...Object.values(bindings))
}
const contractFile = 'views/crm/contract/components/ContractFormDialog.vue'
const quoteFile = 'views/crm/quote/components/QuoteFormPanel.vue'
const product = {
id: 'detail-id',
productId: 'catalog-id',
productName: 'Product',
unit: '吨',
packageSpec: '桶',
quantity: 2,
price: 3,
priceTiers: [
{
id: 'tier-id',
contractProductId: 'detail-id',
startDate: '2026-01-01',
endDate: '2026-02-01',
price: 4,
letterOssIds: ['file-id']
}
]
}
test('desktop and mobile contract mapper retain edit IDs and clear copied line/tier IDs', () => {
const map = handler(contractFile, 'mapContractProductsFromSource', {
normalizeContractProductPackageFields
})
const edit = map([product])[0]
assert.equal(edit.id, 'detail-id')
assert.equal(edit.productId, 'catalog-id')
assert.equal(edit.priceTiers[0].id, 'tier-id')
const copy = map([product], true)[0]
assert.equal(copy.id, undefined)
assert.equal(copy.productId, 'catalog-id')
assert.equal(copy.priceTiers[0].id, undefined)
assert.equal(copy.priceTiers[0].contractProductId, undefined)
assert.equal(product.id, 'detail-id')
})
test('shared desktop and H5 product editor preserves brand and alias IDs on save', async () => {
const file = 'components/business/product-form-dialog/index.vue'
const formData: Record<string, any> = {}
const source = {
id: 'product',
productName: 'Product',
brands: [{ id: 'brand', brandName: 'Brand', status: 'off' }],
aliases: [{ id: 'alias', aliasName: 'Alias' }]
}
const open = handler(file, 'handleOpen', {
formData,
dialogLoading: {},
resetFormData: () => {},
loadCategoryOptions: async () => {},
props: { productId: 'product' },
fetchProductDetail: async () => ({ data: source }),
isPageVariant: { value: false },
ElMessage: { error: assert.fail },
emit: () => {}
})
await open()
formData.brands.push({ brandName: 'New' })
formData.aliases.push({ aliasName: 'New' })
let sent: any
const submit = handler(file, 'submitForm', {
formData,
productImagesUploadRef: {},
formRef: { value: { validate: async () => true } },
submitLoading: {},
isEdit: { value: true },
fetchUpdateProduct: async (data: any) => {
sent = data
},
ElMessage: { success: () => {} },
visible: {},
emit: () => {}
})
await submit()
assert.deepEqual(
sent.brands.map((row: any) => row.id),
['brand', undefined]
)
assert.deepEqual(
sent.aliases.map((row: any) => row.id),
['alias', undefined]
)
assert.equal(sent.brands[0].status, 'off')
assert.ok(
readFileSync(new URL('../src/views/m/crm/product/form.vue', import.meta.url), 'utf8').includes(
'@/components/business/product-form-dialog/index.vue'
)
)
assert.ok(
readFileSync(new URL('../../miniprogram/config/api.js', import.meta.url), 'utf8').includes(
"encodeURIComponent('/m/crm/product')"
)
)
})
for (const file of [contractFile, 'views/crm/contract/form.vue']) {
test(`${file} sends detail and tier identities independently of catalog ID`, () => {
const form = { value: { id: 'contract', contractType: 'framework_sale', products: [product] } }
const build = handler(file, 'buildProductsPayload', {
form,
buildPackageSpecsPayload,
prepareLineAliasAndBrandForSubmit: () => {},
isFrameworkContractType: () => true,
prepareFrameworkProductForSubmit: (p: any) => ({ quantity: p.quantity, price: p.price })
})
const payload = build()[0]
assert.equal(payload.id, 'detail-id')
assert.equal(payload.productId, 'catalog-id')
assert.equal(payload.priceTiers[0].id, 'tier-id')
})
}
for (const mobile of [false, true]) {
test(`quote ${mobile ? 'mobile' : 'desktop'} load/copy retain only the correct identities`, async () => {
const form = { value: {} as any }
const bindings = {
form,
getQuote: async () => ({ data: { id: 'quote', productList: [product] } }),
isPageVariant: { value: mobile },
customerShortName: { value: '' },
confirmedCustomerId: { value: '' },
opportunityDisplayName: { value: '' },
attachmentList: { value: [] },
ensureCustomerOption: () => {},
ensureContactOption: () => {},
loadContactOptions: async () => {},
enrichQuoteProductBrandMeta: async () => {},
updateQuoteTitleAndNo: () => {},
fetchProductDetail: async () => ({ data: { id: 'catalog-id', status: 'on' } }),
partitionQuoteLinesForCopy,
finishClose: () => {},
$modal: { msgError: assert.fail, msgWarning: assert.fail }
}
await handler(quoteFile, 'getQuoteInfo', bindings)('quote')
assert.equal(form.value.products[0].id, 'detail-id')
assert.equal(form.value.products[0].productId, 'catalog-id')
await handler(quoteFile, 'getQuoteInfoForCopy', bindings)('quote')
assert.equal(form.value.products[0].id, undefined)
assert.equal(form.value.products[0].productId, 'catalog-id')
})
}
test('catalog picker does not put catalog IDs into child IDs', () => {
for (const [file, name, quote] of [
[quoteFile, 'handleProductConfirm', true],
[contractFile, 'handleProductSelected', false]
] as const) {
const form = { value: { products: [] as any[], contractType: 'sale' } }
const pick = handler(file, name, {
form,
isPageVariant: { value: false },
initLineBrandMetaFromProduct: () => ({}),
normalizeTaxRateDisplay: () => 13,
isFrameworkContractType: () => false,
calculateProductAmount: () => {},
calculateTotalAmount: () => {}
})
pick([{ id: 'catalog-id', productName: 'Product' }])
assert.equal(form.value.products[0].productId, 'catalog-id')
assert.equal(form.value.products[0].id, undefined, `${quote ? 'quote' : 'contract'} new row`)
}
})
test('native mini program uses the same verified CRM mobile forms', () => {
const config = readFileSync(new URL('../../miniprogram/config/api.js', import.meta.url), 'utf8')
for (const domain of ['quote', 'contract']) {
assert.match(
config,
new RegExp(`'crm-${domain}-webview':[\\s\\S]*?encodeURIComponent\\('/m/crm/${domain}'\\)`)
)
const mobile = readFileSync(
new URL(`../src/views/m/crm/${domain}/form.vue`, import.meta.url),
'utf8'
)
assert.ok(mobile.includes(`@/views/crm/${domain}/components/`))
}
})