mobile-pl-contract-line-picker.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import { mobileStopIdentity } from '../src/views/m/pl/plan/mobileStopIdentity'
  2. import assert from 'node:assert/strict'
  3. import { existsSync, readFileSync } from 'node:fs'
  4. import test from 'node:test'
  5. import { computed, ref } from 'vue'
  6. import * as vue from 'vue'
  7. import { renderToString } from 'vue/server-renderer'
  8. import { compileScript, parse } from '@vue/compiler-sfc'
  9. import ts from 'typescript'
  10. const parentPath = new URL(
  11. '../src/components/business/pl-plan-form-dialog/index.vue',
  12. import.meta.url
  13. )
  14. const sheetPath = new URL('../src/views/m/pl/plan/ContractLinePickerSheet.vue', import.meta.url)
  15. const parent = readFileSync(parentPath, 'utf8')
  16. const sheet = existsSync(sheetPath) ? readFileSync(sheetPath, 'utf8') : ''
  17. function execute(source: string, names: string[], bindings: Record<string, any>) {
  18. const ast = ts.createSourceFile(
  19. 'component.ts',
  20. parse(source).descriptor.scriptSetup?.content || '',
  21. ts.ScriptTarget.Latest,
  22. true
  23. )
  24. const code = names
  25. .map((name) => {
  26. const node = ast.statements.find(
  27. (node) =>
  28. (ts.isFunctionDeclaration(node) && node.name?.text === name) ||
  29. (ts.isVariableStatement(node) &&
  30. node.declarationList.declarations.some((d) => d.name.getText(ast) === name))
  31. )
  32. assert.ok(node, `production declaration ${name} must exist`)
  33. return node.getText(ast)
  34. })
  35. .join('\n')
  36. return new Function(
  37. ...Object.keys(bindings),
  38. ts.transpile(code, { target: ts.ScriptTarget.ESNext }) + `;return {${names.join(',')}}`
  39. )(...Object.values(bindings))
  40. }
  41. const rows = [
  42. {
  43. id: 'line-a',
  44. contractId: 'a',
  45. contractNo: 'HT-A',
  46. productId: 'same',
  47. productName: '长名称液体',
  48. standardProductName: '标准名称',
  49. productNo: 'P001',
  50. model: 'M-A',
  51. brandName: '品牌甲',
  52. aliasName: '别名甲',
  53. quantity: 0,
  54. remainingQty: 0,
  55. taxRate: 0
  56. },
  57. {
  58. id: 'line-b',
  59. contractId: 'b',
  60. contractNo: 'HT-B',
  61. productId: 'same',
  62. productName: '液体乙',
  63. productNo: 'P002',
  64. productModel: 'M-B'
  65. }
  66. ]
  67. function sheetHarness() {
  68. const events: any[] = []
  69. const props = { rows, busy: false, loading: false }
  70. return {
  71. props,
  72. events,
  73. ...execute(
  74. sheet,
  75. [
  76. 'keyword',
  77. 'selectedRows',
  78. 'filteredRows',
  79. 'rowKey',
  80. 'isSelected',
  81. 'toggleRow',
  82. 'resetSelection',
  83. 'confirmSelection',
  84. 'displayNumber'
  85. ],
  86. { ref, computed, props, emit: (...args: any[]) => events.push(args) }
  87. )
  88. }
  89. }
  90. test('mobile picker replaces desktop dialog only for page variant', () => {
  91. assert.match(parent, /<ContractLinePickerSheet\s+v-if="isPageVariant"/)
  92. assert.match(parent, /<el-dialog\s+v-else\s+v-model="linePickerVisible"/)
  93. assert.doesNotMatch(sheet, /el-table|el-dialog/)
  94. })
  95. test('search trims and ignores case across product, contract, model, brand and alias', () => {
  96. const h = sheetHarness()
  97. for (const query of [' 标准名称 ', '长名称', ' p001 ', 'm-a', 'ht-a', '品牌甲', '别名甲']) {
  98. h.keyword.value = query
  99. assert.deepEqual(h.filteredRows.value, [rows[0]], query)
  100. }
  101. h.keyword.value = 'm-b'
  102. assert.deepEqual(h.filteredRows.value, [rows[1]])
  103. })
  104. test('selection survives search and keeps same product across distinct contract lines', () => {
  105. const h = sheetHarness()
  106. h.toggleRow(rows[0])
  107. h.keyword.value = 'HT-B'
  108. h.toggleRow(rows[1])
  109. assert.equal(h.selectedRows.value.length, 2)
  110. assert.notEqual(h.rowKey(rows[0]), h.rowKey(rows[1]))
  111. h.confirmSelection()
  112. assert.equal(h.events[0][0], 'confirm')
  113. assert.deepEqual(h.events[0][1], rows)
  114. h.resetSelection()
  115. assert.equal(h.keyword.value, '')
  116. assert.deepEqual(h.selectedRows.value, [])
  117. h.confirmSelection()
  118. assert.equal(h.events.length, 1)
  119. })
  120. test('zero quantities and tax are visible, missing numbers are placeholders', () => {
  121. const h = sheetHarness()
  122. assert.equal(h.displayNumber(0), '0')
  123. for (const value of [undefined, null, '', NaN]) assert.equal(h.displayNumber(value), '—')
  124. })
  125. const detail = readFileSync(new URL('../src/views/m/pl/plan/detail.vue', import.meta.url), 'utf8')
  126. function scrollHarness({ y = 600, height = 1200, locked = false, missingLast = false } = {}) {
  127. const activeTab = ref('hazard')
  128. const sections: Record<string, number> = { base: -600, hazard: -30, remark: 500 }
  129. const h = execute(detail, ['syncActiveTabFromScroll'], {
  130. activeTab,
  131. scrollLockUntil: locked ? Date.now() + 10000 : 0,
  132. plan: ref({}),
  133. scrollMargin: ref(100),
  134. detailTabs: Object.keys(sections).map((key) => ({ key, anchor: key })),
  135. window: { innerHeight: 600, scrollY: y },
  136. document: {
  137. scrollingElement: { scrollHeight: height },
  138. getElementById: (key: string) =>
  139. missingLast && key === 'remark'
  140. ? null
  141. : { getBoundingClientRect: () => ({ top: sections[key] - (y - 600) }) }
  142. }
  143. })
  144. return { ...h, activeTab }
  145. }
  146. test('short final remark remains highlighted at document bottom', () => {
  147. const h = scrollHarness()
  148. h.syncActiveTabFromScroll()
  149. assert.equal(h.activeTab.value, 'remark')
  150. })
  151. for (const [name, options, expected] of [
  152. ['Home restores base', { y: 0 }, 'base'],
  153. ['short unscrolled page avoids bottom override', { y: 0, height: 500 }, 'base'],
  154. ['click lock is respected', { locked: true }, 'hazard'],
  155. ['missing last section falls back to top probe', { missingLast: true }, 'hazard']
  156. ] as const)
  157. test(name, () => {
  158. const h = scrollHarness(options)
  159. h.syncActiveTabFromScroll()
  160. assert.equal(h.activeTab.value, expected)
  161. })
  162. function deferred() {
  163. let resolve!: (value: any) => void
  164. const promise = new Promise((yes) => {
  165. resolve = yes
  166. })
  167. return { resolve, promise }
  168. }
  169. function parentHarness({
  170. enrich = async (rows: any) => rows,
  171. fetch = async () => {},
  172. page = true
  173. } = {}) {
  174. const loadForm = ref({
  175. customerName: '客户甲',
  176. contracts: [{ contractId: 'a' }],
  177. products: [] as any[]
  178. })
  179. const deliveryForms = ref([
  180. { customerName: '客户乙', contracts: [{ contractId: 'b' }], products: [] as any[] },
  181. { customerName: '客户丙', contracts: [{ contractId: 'c' }], products: [] as any[] }
  182. ])
  183. const bindings = {
  184. mobileStopIdentity,
  185. initLineBrandMetaFromProduct: () => ({}),
  186. packageSpecsForProduct: () => [],
  187. normalizePackageSpec: (value: any) => String(value || '').trim(),
  188. productPackageKey: (id: any, pkg: any) => `${id}::${pkg || ''}`,
  189. unionContractProductRows: () => [],
  190. loadContractProductRows: ref([]),
  191. contractProductRowsCache: ref({}),
  192. ref,
  193. isPageVariant: ref(page),
  194. loadForm,
  195. deliveryForms,
  196. productSelectorTarget: ref({ side: 'load', deliveryIndex: 0 }),
  197. productSelectorVisible: ref(false),
  198. isPlInternalCustomerName: () => false,
  199. collectStopContractIds: (contracts: any[]) => contracts.map((row) => row.contractId),
  200. fetchContractProductIds: fetch,
  201. enrichContractLinesForPicker: async (rows: any) => rows,
  202. enrichPlanProductsHazardFromCrm: enrich,
  203. enrichPlanProductLinesAlias: async (rows: any) => rows,
  204. createStopProductsFromPlanProducts: (rows: any) => rows.map((row: any) => ({ ...row })),
  205. ElMessage: { warning() {}, info() {}, error() {} },
  206. freightCalc: { resetFreightCalc() {} }
  207. }
  208. const h = execute(
  209. parent,
  210. [
  211. 'pageReady',
  212. 'pageDisposed',
  213. 'pageLoadGeneration',
  214. 'isPageLoadCurrent',
  215. 'isPageSubmitCurrent',
  216. 'invalidatePageLoad',
  217. 'linePickerVisible',
  218. 'linePickerRows',
  219. 'linePickerSelected',
  220. 'contractLinesCache',
  221. 'mobileLinePickerSession',
  222. 'mobileLinePickerGeneration',
  223. 'mobileLinePickerBusy',
  224. 'mobileLinePickerLoading',
  225. 'mobileLinePickerError',
  226. 'setMobileLinePickerVisible',
  227. 'handleMobileLinePickerConfirm',
  228. 'openProductSelector',
  229. 'handleLinePickerConfirm',
  230. 'handleProductConfirm'
  231. ],
  232. bindings
  233. )
  234. h.pageReady.value = true
  235. h.contractLinesCache.value = {
  236. a: [rows[0]],
  237. b: [rows[1]],
  238. c: [{ ...rows[1], id: 'line-c', contractId: 'c' }]
  239. }
  240. return { ...bindings, ...h }
  241. }
  242. test('parent delivers exact selected contract line identities into requested delivery segment', async () => {
  243. const h = parentHarness()
  244. await h.openProductSelector('delivery', 1)
  245. await h.handleMobileLinePickerConfirm([rows[0], rows[1]])
  246. assert.equal(h.deliveryForms.value[0].products.length, 0)
  247. assert.deepEqual(
  248. h.deliveryForms.value[1].products.map((r: any) => [r.contractId, r.contractProductId]),
  249. [
  250. ['a', 'line-a'],
  251. ['b', 'line-b']
  252. ]
  253. )
  254. assert.equal(h.loadForm.value.products.length, 0)
  255. })
  256. test('parent busy gate prevents a double confirm while enrichment is pending', async () => {
  257. const d = deferred()
  258. let calls = 0
  259. const h = parentHarness({
  260. enrich: async (rows: any) => {
  261. calls++
  262. await d.promise
  263. return rows
  264. }
  265. })
  266. await h.openProductSelector('load')
  267. const first = h.handleMobileLinePickerConfirm([rows[0]])
  268. assert.equal(h.mobileLinePickerBusy.value, true)
  269. await h.handleMobileLinePickerConfirm([rows[0]])
  270. assert.equal(calls, 1)
  271. d.resolve(null)
  272. await first
  273. assert.equal(h.loadForm.value.products.length, 1)
  274. })
  275. for (const action of ['close', 'switch', 'unmount'] as const)
  276. test(`old confirm cannot append after ${action}`, async () => {
  277. const d = deferred()
  278. const h = parentHarness({
  279. enrich: async (rows: any) => {
  280. await d.promise
  281. return rows
  282. }
  283. })
  284. await h.openProductSelector('load')
  285. const pending = h.handleMobileLinePickerConfirm([rows[0]])
  286. if (action === 'close') {
  287. h.setMobileLinePickerVisible(false)
  288. await h.openProductSelector('load')
  289. }
  290. if (action === 'switch') await h.openProductSelector('delivery', 1)
  291. if (action === 'unmount') h.invalidatePageLoad()
  292. d.resolve(null)
  293. await pending
  294. assert.equal(h.loadForm.value.products.length, 0)
  295. assert.equal(h.deliveryForms.value[1].products.length, 0)
  296. if (action !== 'unmount') assert.equal(h.linePickerVisible.value, true)
  297. })
  298. test('out-of-order opening does not apply old rows to a new segment', async () => {
  299. const d = deferred()
  300. const h = parentHarness({
  301. fetch: async (id?: string) => {
  302. if (id === 'a') await d.promise
  303. }
  304. })
  305. const old = h.openProductSelector('load')
  306. await h.openProductSelector('delivery', 1)
  307. d.resolve(null)
  308. await old
  309. assert.equal(h.linePickerRows.value[0].contractId, 'c')
  310. assert.equal(h.productSelectorTarget.value.deliveryIndex, 1)
  311. })
  312. test('desktop confirm remains usable without mobile page readiness or sheet state', async () => {
  313. const h = parentHarness({ page: false })
  314. h.pageReady.value = false
  315. h.linePickerSelected.value = [rows[0]]
  316. await h.handleLinePickerConfirm()
  317. assert.equal(h.loadForm.value.products[0].contractProductId, 'line-a')
  318. })
  319. test('mobile confirmation deduplicates by contract plus line id, not line id alone', async () => {
  320. const h = parentHarness()
  321. await h.openProductSelector('load')
  322. await h.handleMobileLinePickerConfirm([rows[0], { ...rows[1], id: rows[0].id }])
  323. assert.equal(h.loadForm.value.products.length, 2)
  324. })
  325. test('failed mobile opening remains visible with retry and clears loading', async () => {
  326. let fail = true
  327. const h = parentHarness({
  328. fetch: async () => {
  329. if (fail) throw new Error('offline')
  330. }
  331. })
  332. await h.openProductSelector('load')
  333. assert.equal(h.linePickerVisible.value, true)
  334. assert.equal(h.mobileLinePickerLoading.value, false)
  335. assert.match(h.mobileLinePickerError.value, /重新加载/)
  336. fail = false
  337. await h.openProductSelector('load')
  338. assert.equal(h.mobileLinePickerError.value, '')
  339. assert.equal(h.linePickerRows.value.length, 1)
  340. })
  341. test('cancel pending opening cannot reopen or populate the closed sheet', async () => {
  342. const d = deferred()
  343. const h = parentHarness({
  344. fetch: async () => {
  345. await d.promise
  346. }
  347. })
  348. const pending = h.openProductSelector('load')
  349. h.setMobileLinePickerVisible(false)
  350. d.resolve(null)
  351. await pending
  352. assert.equal(h.linePickerVisible.value, false)
  353. assert.equal(h.linePickerRows.value.length, 0)
  354. })
  355. test('old confirm finally cannot clear a new session busy flag', async () => {
  356. const old = deferred()
  357. const fresh = deferred()
  358. let count = 0
  359. const h = parentHarness({
  360. enrich: async (rows: any) => {
  361. await (++count === 1 ? old.promise : fresh.promise)
  362. return rows
  363. }
  364. })
  365. await h.openProductSelector('load')
  366. const first = h.handleMobileLinePickerConfirm([rows[0]])
  367. h.setMobileLinePickerVisible(false)
  368. await h.openProductSelector('delivery', 1)
  369. const second = h.handleMobileLinePickerConfirm([rows[1]])
  370. old.resolve(null)
  371. await first
  372. assert.equal(h.mobileLinePickerBusy.value, true)
  373. fresh.resolve(null)
  374. await second
  375. assert.equal(h.mobileLinePickerBusy.value, false)
  376. assert.equal(h.deliveryForms.value[1].products.length, 1)
  377. })
  378. test('sheet session records the page generation and rejects confirmation from an older page', async () => {
  379. const h = parentHarness()
  380. await h.openProductSelector('load')
  381. h.mobileLinePickerGeneration.value -= 1
  382. await h.handleMobileLinePickerConfirm([rows[0]])
  383. assert.equal(h.loadForm.value.products.length, 0)
  384. })
  385. function compiledSheet() {
  386. const script = compileScript(parse(sheet).descriptor, { id: 'picker-test', inlineTemplate: true })
  387. const code = ts.transpile(script.content, {
  388. target: ts.ScriptTarget.ESNext,
  389. module: ts.ModuleKind.ESNext
  390. })
  391. const ast = ts.createSourceFile('sheet.js', code, ts.ScriptTarget.Latest, true)
  392. const bindings: Record<string, any> = Object.fromEntries(
  393. Object.entries(vue).filter(([name]) => /^[a-zA-Z_$][\w$]*$/.test(name) && name !== 'default')
  394. )
  395. const statements: string[] = []
  396. for (const node of ast.statements) {
  397. if (ts.isImportDeclaration(node)) {
  398. const imports = node.importClause?.namedBindings
  399. if (imports && ts.isNamedImports(imports))
  400. for (const item of imports.elements) {
  401. bindings[item.name.text] = (vue as any)[item.propertyName?.text || item.name.text]
  402. }
  403. } else statements.push(node.getText(ast))
  404. }
  405. return new Function(
  406. ...Object.keys(bindings),
  407. statements.join('\n').replace('export default', 'return')
  408. )(...Object.values(bindings))
  409. }
  410. test('real sheet renderer keeps all card fields, formatter results, zero values and no desktop table', async () => {
  411. const app = vue.createSSRApp(compiledSheet(), {
  412. modelValue: true,
  413. rows,
  414. busy: false,
  415. loading: false,
  416. error: '',
  417. formatPackage: () => '字典包装',
  418. formatBrand: () => '品牌规则结果',
  419. formatRemaining: (n: any) => (n == null ? '—' : String(n)),
  420. dictLabel: (_type: string, value: string) => '字典-' + value,
  421. dictTagType: () => 'success'
  422. })
  423. app.component('van-popup', {
  424. setup:
  425. (_props: any, { slots }: any) =>
  426. () =>
  427. vue.h('section', slots.default?.())
  428. })
  429. app.component('el-tag', {
  430. setup:
  431. (_props: any, { slots }: any) =>
  432. () =>
  433. vue.h('span', slots.default?.())
  434. })
  435. const html = await renderToString(app)
  436. for (const text of [
  437. '标准名称',
  438. 'HT-A',
  439. 'HT-B',
  440. 'P001',
  441. 'M-A',
  442. '字典包装',
  443. '品牌规则结果',
  444. '别名甲',
  445. '0%',
  446. '合同数量',
  447. '剩余数量',
  448. '计量单位',
  449. '状态'
  450. ])
  451. assert.ok(html.includes(text), text)
  452. assert.match(html, /<small>合同数量<\/small><span>0<\/span>/)
  453. assert.match(html, /<small>剩余数量<\/small><span>0<\/span>/)
  454. assert.match(html, /确认添加/)
  455. assert.match(html, /disabled/)
  456. assert.doesNotMatch(html, /<table|el-dialog/)
  457. })
  458. test('production visibility watcher resets search and temporary selection on close and reopen', () => {
  459. const h = sheetHarness()
  460. const props = vue.reactive({ modelValue: true })
  461. const ast = ts.createSourceFile(
  462. 'sheet.ts',
  463. parse(sheet).descriptor.scriptSetup!.content,
  464. ts.ScriptTarget.Latest,
  465. true
  466. )
  467. const watcher = ast.statements.find(
  468. (node) => ts.isExpressionStatement(node) && node.getText(ast).startsWith('watch(')
  469. )
  470. assert.ok(watcher)
  471. const scope = vue.effectScope()
  472. scope.run(() =>
  473. new Function('watch', 'props', 'resetSelection', ts.transpile(watcher.getText(ast)))(
  474. vue.watch,
  475. props,
  476. h.resetSelection
  477. )
  478. )
  479. h.keyword.value = 'HT-A'
  480. h.toggleRow(rows[0])
  481. props.modelValue = false
  482. assert.equal(h.keyword.value, '')
  483. assert.equal(h.selectedRows.value.length, 0)
  484. props.modelValue = true
  485. assert.equal(h.selectedRows.value.length, 0)
  486. assert.equal(h.events.length, 0)
  487. scope.stop()
  488. })
  489. function realFetchHarness(
  490. read: (id: string) => Promise<any> = async () => {
  491. throw new Error('offline')
  492. }
  493. ) {
  494. const h = parentHarness({ fetch: (...args: any[]) => fetchReal(...args) })
  495. const pageLoadError = ref('')
  496. const prod = execute(
  497. parent,
  498. ['fetchContractProductIds', 'assertPageLoadCurrent', 'failPageLoad', 'pageLoadErrorMessage'],
  499. {
  500. ...h,
  501. pageLoadGeneration: 0,
  502. pageLoadError,
  503. footerMoreOpen: ref(false),
  504. props: {},
  505. contractProductIdsCache: ref({}),
  506. contractProductRowsCache: ref({}),
  507. getContractDetailCached: read,
  508. invalidateContractDetailCache() {},
  509. collectContractProductRows: (rows: any) => rows,
  510. collectContractProductIds: (rows: any) => rows.map((row: any) => row.productId)
  511. }
  512. )
  513. const fetchReal = prod.fetchContractProductIds
  514. return { ...h, pageLoadError }
  515. }
  516. test('real contract failure blocks the page, releases loading and closes the sheet for page retry', async () => {
  517. const h = realFetchHarness()
  518. await h.openProductSelector('load')
  519. assert.equal(h.pageReady.value, false)
  520. assert.ok(h.pageLoadError.value)
  521. assert.equal(h.mobileLinePickerLoading.value, false)
  522. assert.match(h.mobileLinePickerError.value, /重新加载/)
  523. assert.equal(h.linePickerVisible.value, false)
  524. })
  525. for (const side of ['load', 'delivery'] as const)
  526. test(`${side} stop replacement by the contract watcher releases loading and closes only its current sheet`, async () => {
  527. const d = deferred()
  528. const h = realFetchHarness(async (id) => {
  529. await d.promise
  530. return { id, products: rows }
  531. })
  532. const pending = h.openProductSelector(side)
  533. if (side === 'load') h.loadForm.value = { ...h.loadForm.value, products: [] }
  534. else h.deliveryForms.value = h.deliveryForms.value.map((stop) => ({ ...stop, products: [] }))
  535. d.resolve(null)
  536. await pending
  537. assert.equal(h.mobileLinePickerLoading.value, false)
  538. assert.equal(h.linePickerVisible.value, false)
  539. assert.equal(h.pageReady.value, true, 'obsolete selection must not invalidate the ready page')
  540. })
  541. for (const failure of [true, false])
  542. test(`old real fetch ${failure ? 'failure' : 'success'} cannot fail the page or close/reset a newer loading sheet`, async () => {
  543. const old = deferred(),
  544. fresh = deferred()
  545. const h = realFetchHarness(async (id) => {
  546. await (id === 'a' ? old.promise : fresh.promise)
  547. if (id === 'a' && failure) throw new Error('old request failed')
  548. return { id, products: id === 'a' ? [rows[0]] : [rows[1]] }
  549. })
  550. const previous = h.openProductSelector('load')
  551. const current = h.openProductSelector('delivery')
  552. old.resolve(null)
  553. await previous
  554. assert.equal(h.pageReady.value, true)
  555. assert.equal(h.linePickerVisible.value, true)
  556. assert.equal(h.mobileLinePickerLoading.value, true)
  557. assert.equal(h.mobileLinePickerError.value, '')
  558. fresh.resolve(null)
  559. await current
  560. assert.equal(h.mobileLinePickerLoading.value, false)
  561. assert.equal(h.linePickerVisible.value, true)
  562. assert.equal(h.linePickerRows.value[0].contractId, 'b')
  563. })
  564. test('mobile opened line picker follows segment identity through reorder before confirm',async()=>{
  565. const h=parentHarness()
  566. await h.openProductSelector('delivery',0)
  567. const identity=mobileStopIdentity(h.deliveryForms.value[0])
  568. h.deliveryForms.value=[{...h.deliveryForms.value[1]},{...h.deliveryForms.value[0]}]
  569. await h.handleMobileLinePickerConfirm([rows[0]])
  570. assert.equal(h.deliveryForms.value[0].products.length,0)
  571. assert.equal(mobileStopIdentity(h.deliveryForms.value[1]),identity)
  572. assert.equal(h.deliveryForms.value[1].products.length,1)
  573. })
  574. test('mobile opened line picker discards confirmation after target segment deletion',async()=>{
  575. const h=parentHarness()
  576. await h.openProductSelector('delivery',0)
  577. h.deliveryForms.value=[h.deliveryForms.value[1]]
  578. await h.handleMobileLinePickerConfirm([rows[0]])
  579. assert.equal(h.deliveryForms.value[0].products.length,0)
  580. })
  581. test('mobile pending line enrichment follows original segment after reorder',async()=>{
  582. const pending=deferred()
  583. const h=parentHarness({enrich:async(input:any)=>{await pending.promise;return input}})
  584. await h.openProductSelector('delivery',0)
  585. const task=h.handleMobileLinePickerConfirm([rows[0]])
  586. h.deliveryForms.value=[{...h.deliveryForms.value[1]},{...h.deliveryForms.value[0]}]
  587. pending.resolve();await task
  588. assert.equal(h.deliveryForms.value[0].products.length,0)
  589. assert.equal(h.deliveryForms.value[1].products.length,1)
  590. })
  591. test('mobile pending internal catalog enrichment follows original segment after reorder',async()=>{
  592. const pending=deferred()
  593. const h=parentHarness({enrich:async(input:any)=>{await pending.promise;return input}})
  594. await h.openProductSelector('delivery',0)
  595. const task=h.handleProductConfirm([{id:'catalog-product',productName:'目录产品'}])
  596. h.deliveryForms.value=[{...h.deliveryForms.value[1]},{...h.deliveryForms.value[0]}]
  597. pending.resolve();await task
  598. assert.equal(h.deliveryForms.value[0].products.length,0)
  599. assert.equal(h.deliveryForms.value[1].products[0].productId,'catalog-product')
  600. })
  601. test('mobile pending contract list opening follows original segment after reorder',async()=>{
  602. const pending=deferred()
  603. const h=parentHarness({fetch:async()=>{await pending.promise;return []}})
  604. const task=h.openProductSelector('delivery',0)
  605. h.deliveryForms.value=[{...h.deliveryForms.value[1]},{...h.deliveryForms.value[0]}]
  606. pending.resolve();await task
  607. assert.equal(h.linePickerVisible.value,true)
  608. await h.handleMobileLinePickerConfirm([rows[0]])
  609. assert.equal(h.deliveryForms.value[0].products.length,0)
  610. assert.equal(h.deliveryForms.value[1].products.length,1)
  611. })