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) { 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 = {} 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/`)) } })