| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816 |
- 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(), /产品资料已保存.*刷新/)
- })
|