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