har-status16-harness.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. /**
  2. * 「trade_status=16 超时关闭」前端转译的执行级验证台(2026-10-10)。
  3. *
  4. * 【要证的三句话】
  5. * A. 订单列表页:挂**真 SFC**(真模板),喂 trade_status="16" 的订单,状态列
  6. * 渲染出「超时关闭」;且整个页面文本里**不再出现裸 "16"**。
  7. * B. 订单详情弹层:驱动 detail / detailVisible 后,详情里的状态 tag 同样转译
  8. * (列表与详情是模板里两处独立的映射点,测一处不能代表另一处)。
  9. * C. 收银台 result computed:16 → 超时关闭(不再落进「表外状态原样显示」的兜底)。
  10. *
  11. * 【两向读数】表里既有「必须转译」的 16,也有正控:已知状态(1 / 8)行为不变;
  12. * 表外的 "99" 仍**原样显示** —— 那是「表外不猜」的既定哲学,谁顺手把它删掉,A4/C4 该红。
  13. *
  14. * 【配方来源】../2026-09-29-lsy-amount-floor-harness/amount-floor-harness.mjs:
  15. * 同一套「编译真 SFC + 打桩 import + typescript 转译 + new Function」。本台多两件:
  16. * ① 订单页要**渲染真模板**:compileScript(inlineTemplate:false) + 单独 compileTemplate
  17. * 出 render(**必须带 bindingMetadata**,script setup 的 __isScriptSetup 旗子会让
  18. * runtime 拒绝走 _ctx 代理),二者拼回一个组件再挂;
  19. * ② el-table / el-tag / el-dialog / el-option 用「会转发插槽或记录读数」的桩
  20. * (el-table 按 data 逐行把 row 塞进列插槽;el-tag 记 {type,text};el-option 记
  21. * {label,value}),其余 el-* 直通默认插槽或渲染空。
  22. * 模板里的 HAR_TRADE_STATUS_TEXT 等映射表来自**真 har.ts**(转译求值后取出),
  23. * 只有 HarAPI 本身换成桩(免得真发请求)。
  24. *
  25. * 【A5b 为什么直接钉表值】两处 el-tag 都写成 `TAG[st] || 'info'` —— 兜底把
  26. * 「TAG 表里 16 缺格」和「给了 info」渲染成同一个样子,变异体 M2(删掉这格)
  27. * 在渲染层分辨不出(2026-10-10 实测:只加渲染断言时 M2 全绿)。所以表值单独钉。
  28. *
  29. * 【不设 process.exitCode】失败也退 0,只认最后那行 SUMMARY(同目录各台惯例)。
  30. *
  31. * 用法:node har-status16-harness.mjs
  32. */
  33. import fs from "node:fs";
  34. import { createRequire } from "node:module";
  35. const FE = "D:/project2/payment-platform/frontend/";
  36. const require = createRequire(FE + "package.json");
  37. const vue = require("vue");
  38. const { parse, compileScript, compileTemplate } = require("@vue/compiler-sfc");
  39. const ts = require("typescript");
  40. const ORDER = FE + "src/views/module_payment/har/order/index.vue";
  41. const CASHIER = FE + "src/views/module_payment/har/cashier/index.vue";
  42. const HAR_TS = FE + "src/api/module_payment/har.ts";
  43. // 无 DOM 台子的垫片:页面里用的是 `window.setTimeout` / `window.clearTimeout`。
  44. if (typeof globalThis.window === "undefined") {
  45. globalThis.window = globalThis;
  46. }
  47. // ---------- 通用:翻 SFC 里的 import 成桩 ----------
  48. const braceImport = (spec) =>
  49. new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${spec}['"];?`, "g");
  50. const braceAsConst = (slot) => (m, names) => {
  51. const clean = names
  52. .replace(/\btype\s+[\w$.]+\s*,?/g, "")
  53. .replace(/\bas\b/g, ":");
  54. return `const {${clean}} = globalThis.__U.${slot};`;
  55. };
  56. const transpile = (code, fileName) =>
  57. ts.transpileModule(code, {
  58. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
  59. fileName,
  60. }).outputText;
  61. // ---------- 真 har.ts:取映射表(HarAPI 换成桩) ----------
  62. const HAR_NAMES = [
  63. "HAR_TRADE_STATUS_TEXT",
  64. "HAR_TRADE_STATUS_TAG",
  65. "HAR_TRADE_TYPE_OPTIONS",
  66. "HAR_ORDER_STATUS_PAID",
  67. "HAR_NOTIFY_STATUS_TEXT",
  68. "HAR_BIZ_UN_STAGE",
  69. ];
  70. function evalRealHarModule() {
  71. let code = fs.readFileSync(HAR_TS, "utf8");
  72. code = code.replace(
  73. /import\s+(\w+)\s+from\s*['"]@\/utils\/request['"];?/,
  74. "const $1 = globalThis.__U.request;"
  75. );
  76. code = code.replace(/export\s+default\s+/g, "const __har_default = ");
  77. code = code.replace(/^\s*export\s+/gm, "");
  78. if (/^\s*import\s/m.test(code) || /^\s*export\s/m.test(code)) {
  79. throw new Error("har.ts 还有没换掉的 import/export —— 配方要更新");
  80. }
  81. code = transpile(code, "har.ts");
  82. code +=
  83. "\nglobalThis.__U.realHar = { __default: __har_default, " +
  84. HAR_NAMES.map((n) => `${n}: (typeof ${n} !== "undefined" ? ${n} : undefined)`).join(", ") +
  85. " };";
  86. new Function(code)();
  87. for (const n of HAR_NAMES) {
  88. if (globalThis.__U.realHar[n] === undefined) throw new Error("har.ts 里没有导出 " + n);
  89. }
  90. }
  91. // ---------- 桩:请求 / UI 库 / store ----------
  92. const apiCalls = [];
  93. const apiStub = new Proxy(
  94. {},
  95. {
  96. get: (_t, name) =>
  97. name === "then"
  98. ? undefined
  99. : async (...args) => {
  100. apiCalls.push({ name: String(name), args });
  101. return { data: { code: 0, data: { items: [], total: 0 } } };
  102. },
  103. }
  104. );
  105. const callable = new Proxy(async () => undefined, {
  106. get: (_t, k) => (k === "then" ? undefined : callable),
  107. apply: () => Promise.resolve(),
  108. });
  109. const epStub = new Proxy({}, { get: () => callable });
  110. const noop = () => {};
  111. const enterpriseStoreStub = () => ({
  112. getEnterpriseList: [],
  113. loadEnterpriseList: async () => {},
  114. });
  115. // ---------- 无 DOM 渲染器:垫出足够的节点操作,让真模板能挂/能更新 ----------
  116. const noopNode = () => {};
  117. const renderer = vue.createRenderer({
  118. createElement: (tag) => ({ kind: "el", tag, children: [], parent: null }),
  119. createText: (text) => ({ kind: "text", text, parent: null }),
  120. createComment: (text) => ({ kind: "comment", text, parent: null }),
  121. setText: (n, text) => {
  122. n.text = text;
  123. },
  124. setElementText: (el, text) => {
  125. el.children = [{ kind: "text", text, parent: el }];
  126. },
  127. insert: (child, parent, anchor) => {
  128. child.parent = parent;
  129. if (anchor) {
  130. const i = parent.children.indexOf(anchor);
  131. parent.children.splice(i < 0 ? parent.children.length : i, 0, child);
  132. } else {
  133. parent.children.push(child);
  134. }
  135. },
  136. remove: (child) => {
  137. const p = child.parent;
  138. if (p) {
  139. const i = p.children.indexOf(child);
  140. if (i >= 0) p.children.splice(i, 1);
  141. }
  142. child.parent = null;
  143. },
  144. parentNode: (n) => n.parent ?? null,
  145. nextSibling: (n) => {
  146. const p = n.parent;
  147. if (!p) return null;
  148. const i = p.children.indexOf(n);
  149. return i >= 0 ? p.children[i + 1] ?? null : null;
  150. },
  151. patchProp: noopNode, // 属性/class/style 与本台断言无关(只读文本)
  152. insertStaticContent: (content, parent, anchor, ns, start, end) => {
  153. if (start && end) {
  154. // 缓存节点搬到新位置(同 DOM 语义)
  155. const move = (n) => {
  156. if (n.parent) renderer.remove ? null : null;
  157. if (n.parent) {
  158. const i = n.parent.children.indexOf(n);
  159. if (i >= 0) n.parent.children.splice(i, 1);
  160. }
  161. n.parent = parent;
  162. if (anchor) {
  163. const i = parent.children.indexOf(anchor);
  164. parent.children.splice(i < 0 ? parent.children.length : i, 0, n);
  165. } else {
  166. parent.children.push(n);
  167. }
  168. };
  169. move(start);
  170. if (end !== start) move(end);
  171. return [start, end];
  172. }
  173. const n = { kind: "static", html: String(content), children: [], parent: null };
  174. n.parent = parent;
  175. parent.children.push(n);
  176. return [n, n];
  177. },
  178. });
  179. // ---------- 桩组件:转发插槽的 el-table / el-tag / el-dialog / el-option ----------
  180. const tagPairs = []; // el-tag 每次渲染记一对 {type, text} —— TAG 表就是靠它钉
  181. const optionPairs = []; // el-option 每次渲染记一对 {label, value} —— 筛选下拉吃 TEXT 表
  182. function collectText(nodes, out = []) {
  183. for (const n of Array.isArray(nodes) ? nodes : [nodes]) {
  184. if (n == null || n === false) continue;
  185. if (typeof n === "string" || typeof n === "number") out.push(String(n));
  186. else if (Array.isArray(n)) collectText(n, out);
  187. else if (typeof n === "object" && n.children != null) collectText(n.children, out);
  188. }
  189. return out;
  190. }
  191. const ElTableStub = {
  192. name: "el-table",
  193. props: { data: { type: Array, default: () => [] } },
  194. setup(props, { slots }) {
  195. return () => {
  196. const cols = slots.default ? slots.default().flat(Infinity) : [];
  197. const out = [];
  198. for (const row of props.data || []) {
  199. for (const col of cols) {
  200. const colSlots = col && col.children;
  201. if (colSlots && typeof colSlots === "object" && typeof colSlots.default === "function") {
  202. out.push(vue.h("div", { class: "cell" }, colSlots.default({ row })));
  203. }
  204. }
  205. }
  206. return vue.h("div", out);
  207. };
  208. },
  209. };
  210. const ElTagStub = {
  211. name: "el-tag",
  212. props: { type: { type: String, default: "" } },
  213. setup(props, { slots }) {
  214. return () => {
  215. const kids = slots.default ? slots.default() : [];
  216. tagPairs.push({ type: props.type, text: collectText(kids).join("") });
  217. return vue.h("span", kids);
  218. };
  219. },
  220. };
  221. const ElOptionStub = {
  222. name: "el-option",
  223. props: { label: { type: String, default: "" }, value: { type: [String, Number], default: "" } },
  224. setup(props) {
  225. return () => {
  226. optionPairs.push({ label: props.label, value: String(props.value) });
  227. return null; // 选项的文字在真实组件里来自 label prop,槽里没有内容可渲染
  228. };
  229. },
  230. };
  231. const ElDialogStub = {
  232. name: "el-dialog",
  233. props: { modelValue: { type: Boolean, default: false } },
  234. setup(props, { slots }) {
  235. return () =>
  236. props.modelValue
  237. ? vue.h("div", [slots.default ? slots.default() : null, slots.footer ? slots.footer() : null])
  238. : null;
  239. },
  240. };
  241. function registerStubs(app) {
  242. const passthrough = (name) => ({
  243. name,
  244. setup: (_p, { slots }) => () => (slots.default ? slots.default() : null),
  245. });
  246. for (const n of [
  247. "el-alert",
  248. "el-button",
  249. "el-card",
  250. "el-date-picker",
  251. "el-descriptions",
  252. "el-descriptions-item",
  253. "el-empty",
  254. "el-form",
  255. "el-form-item",
  256. "el-input",
  257. "el-input-number",
  258. "el-pagination",
  259. "el-select",
  260. "el-table-column",
  261. ]) {
  262. app.component(n, passthrough(n));
  263. }
  264. app.component("el-table", ElTableStub);
  265. app.component("el-tag", ElTagStub);
  266. app.component("el-option", ElOptionStub);
  267. app.component("el-dialog", ElDialogStub);
  268. app.directive("hasPerm", {});
  269. app.directive("loading", {});
  270. }
  271. // ---------- 编译 + 挂载:订单页(真模板) ----------
  272. function compileOrder() {
  273. const src = fs.readFileSync(ORDER, "utf8");
  274. const { descriptor, errors } = parse(src, { filename: ORDER });
  275. if (errors && errors.length) throw new Error("SFC 解析失败: " + errors.map(String).join("; "));
  276. const script = compileScript(descriptor, { id: "s16order", inlineTemplate: false });
  277. let code = script.content;
  278. code = code
  279. .replace(braceImport("vue"), braceAsConst("vue"))
  280. .replace(braceImport("element-plus"), braceAsConst("ep"))
  281. .replace(/import\s+dayjs\s+from\s*['"]dayjs['"];?/, "const dayjs = globalThis.__U.dayjs;")
  282. .replace(
  283. /import\s+(\w+)\s*,\s*\{([^}]*)\}\s*from\s*['"]@\/api\/module_payment\/har['"];?/g,
  284. (_m, def, names) => {
  285. const clean = names
  286. .split(",")
  287. .map((s) => s.trim())
  288. .filter((s) => s && !/^type\s/.test(s));
  289. return (
  290. `const ${def} = globalThis.__U.api;` +
  291. (clean.length ? ` const { ${clean.join(", ")} } = globalThis.__U.realHar;` : "")
  292. );
  293. }
  294. )
  295. .replace(
  296. /import\s*\{\s*useEnterpriseStore\s*\}\s*from\s*['"]@\/store\/modules\/enterprise\.store['"];?/,
  297. "const useEnterpriseStore = globalThis.__U.enterpriseStore;"
  298. )
  299. .replace(
  300. /import\s*\{\s*useLiveRefetch\s*\}\s*from\s*['"]@\/composables['"];?/,
  301. "const useLiveRefetch = globalThis.__U.noop;"
  302. );
  303. if (/^\s*import\s/m.test(code)) throw new Error("order 还有没换掉的 import:\n" + code.slice(0, 500));
  304. code = transpile(code, "order.ts");
  305. code = code.replace(/export\s+default/, "const __sfc =");
  306. if (/^\s*export\s/m.test(code)) throw new Error("order 还有没换掉的 export");
  307. code += "\nglobalThis.__U.orderSfc = __sfc;";
  308. new Function(code)();
  309. const tpl = compileTemplate({
  310. source: descriptor.template.content,
  311. filename: ORDER,
  312. id: "s16order",
  313. // bindingMetadata 必须带:script setup 的 setup 结果带 __isScriptSetup 旗子,
  314. // runtime 的 hasSetupBinding 见到它就拒绝走 _ctx 代理(查不到 = undefined + 警告)。
  315. // 带上它,模板才编成 $setup.xx 直取。2026-10-10 实测踩过一次。
  316. compilerOptions: { bindingMetadata: script.bindings },
  317. });
  318. if (tpl.errors && tpl.errors.length) throw new Error("模板编译失败: " + tpl.errors.map(String).join("; "));
  319. let tcode = tpl.code.replace(braceImport("vue"), braceAsConst("vue"));
  320. tcode = tcode.replace(/export\s+function\s+render/, "function render");
  321. if (/^\s*(import|export)\s/m.test(tcode)) {
  322. throw new Error("模板码还有没换掉的 import/export:\n" + tcode.slice(0, 400));
  323. }
  324. tcode = transpile(tcode, "order.render.ts");
  325. tcode += "\nglobalThis.__U.orderRender = render;";
  326. new Function(tcode)();
  327. }
  328. function mountOrder() {
  329. const Real = globalThis.__U.orderSfc;
  330. let bindings = null;
  331. const Wrapped = {
  332. render: globalThis.__U.orderRender,
  333. setup(props, ctx) {
  334. bindings = Real.setup(props, ctx);
  335. return bindings;
  336. },
  337. };
  338. const root = { kind: "el", tag: "__root__", children: [], parent: null };
  339. const app = renderer.createApp({ render: () => vue.h(Wrapped) });
  340. registerStubs(app);
  341. app.mount(root);
  342. if (!bindings) throw new Error("setup 没有返回绑定");
  343. return { bindings, root };
  344. }
  345. // ---------- 编译 + 挂载:收银台(只驱动绑定,不渲染模板,同金额闸台) ----------
  346. function compileCashier() {
  347. const src = fs.readFileSync(CASHIER, "utf8");
  348. const { descriptor, errors } = parse(src, { filename: CASHIER });
  349. if (errors && errors.length) throw new Error("SFC 解析失败: " + errors.map(String).join("; "));
  350. let code = compileScript(descriptor, { id: "s16cashier", inlineTemplate: false }).content;
  351. code = code
  352. .replace(braceImport("vue"), braceAsConst("vue"))
  353. .replace(braceImport("element-plus"), braceAsConst("ep"))
  354. .replace(/import\s+(\w+)\s+from\s*['"]qrcode['"];?/, "const $1 = globalThis.__U.ep;")
  355. .replace(
  356. /import\s+(\w+)\s*,\s*\{([^}]*)\}\s*from\s*['"]@\/api\/module_payment\/har['"];?/g,
  357. (_m, def) => `const ${def} = globalThis.__U.api;`
  358. );
  359. if (/^\s*import\s/m.test(code)) throw new Error("cashier 还有没换掉的 import:\n" + code.slice(0, 500));
  360. code = transpile(code, "cashier.ts");
  361. code = code.replace(/export\s+default/, "const __sfc =");
  362. if (/^\s*export\s/m.test(code)) throw new Error("cashier 还有没换掉的 export");
  363. code += "\nglobalThis.__U.cashierSfc = __sfc;";
  364. new Function(code)();
  365. }
  366. function mountCashier() {
  367. const Real = globalThis.__U.cashierSfc;
  368. let bindings = null;
  369. const Wrapped = {
  370. ...Real,
  371. render: () => null, // 模板不渲染(同金额闸台):本段只驱动 result / apply
  372. setup(props, ctx) {
  373. bindings = Real.setup(props, ctx);
  374. return bindings;
  375. },
  376. };
  377. renderer.createApp({ render: () => vue.h(Wrapped) }).mount({ kind: "el", tag: "__root__", children: [], parent: null });
  378. if (!bindings) throw new Error("setup 没有返回绑定");
  379. return bindings;
  380. }
  381. // ---------- 读数工具 ----------
  382. function flattenText(node, out = []) {
  383. if (!node) return out;
  384. if (node.kind === "text") out.push(String(node.text));
  385. else if (node.kind === "static") out.push(String(node.html).replace(/<[^>]*>/g, ""));
  386. for (const c of node.children || []) flattenText(c, out);
  387. return out;
  388. }
  389. const flush = async (n = 4) => {
  390. for (let i = 0; i < n; i++) {
  391. await new Promise((r) => setTimeout(r, 0));
  392. await vue.nextTick();
  393. }
  394. };
  395. // ---------- 夹具(所有非状态字段都不含子串 "16",负向断言才干净) ----------
  396. const MK = (no, status, extra = {}) => ({
  397. id: "R-" + no,
  398. order_no: "APTEST-A" + no,
  399. out_biz_no: "BIZ-A" + no,
  400. price: "1.00",
  401. title: "样例单" + no,
  402. trade_status: status,
  403. trade_type: "alipay_un_stage",
  404. merchant_code: "M0001",
  405. created_time: "2026-10-10 09:00:00",
  406. notify_status: "FAILED",
  407. ...extra,
  408. });
  409. const ROW16 = MK("1", "16");
  410. const ROW8 = MK("2", "8");
  411. const ROW1 = MK("3", "1");
  412. const ROW99 = MK("4", "99");
  413. const ROWS = [ROW16, ROW8, ROW1, ROW99];
  414. // ---------- 跑 ----------
  415. globalThis.__U = {
  416. vue,
  417. api: apiStub,
  418. ep: epStub,
  419. request: noop,
  420. dayjs: require("dayjs"),
  421. enterpriseStore: enterpriseStoreStub,
  422. noop,
  423. realHar: null,
  424. orderSfc: null,
  425. orderRender: null,
  426. cashierSfc: null,
  427. };
  428. // orderList 要喂夹具行;其余接口空列表(merchantList / enterprise 等)
  429. const rawApi = apiStub;
  430. globalThis.__U.api = new Proxy(
  431. {},
  432. {
  433. get: (_t, name) =>
  434. name === "then"
  435. ? undefined
  436. : async (...args) => {
  437. apiCalls.push({ name: String(name), args });
  438. if (name === "orderList") {
  439. return { data: { code: 0, data: { items: ROWS, total: ROWS.length } } };
  440. }
  441. if (name === "orderDetail") {
  442. return { data: { code: 0, data: ROW16 } };
  443. }
  444. return { data: { code: 0, data: { items: [], total: 0 } } };
  445. },
  446. }
  447. );
  448. evalRealHarModule();
  449. compileOrder();
  450. compileCashier();
  451. const results = [];
  452. const check = (tag, ok, detail) => {
  453. results.push({ tag, ok });
  454. console.log(` ${tag.padEnd(4)} ${ok ? "PASS" : "FAIL"} ${detail}`);
  455. };
  456. console.log(`har.ts = ${HAR_TS}`);
  457. console.log(`order = ${ORDER}`);
  458. console.log(`cashier = ${CASHIER}`);
  459. console.log("");
  460. console.log("== A/B 订单列表页与详情弹层(真 SFC + 真模板) ==");
  461. const { bindings: bOrder, root: rootOrder } = mountOrder();
  462. await flush();
  463. const textsList = flattenText(rootOrder);
  464. const allList = textsList.join(" | ");
  465. check("A1", allList.includes("超时关闭"), "列表 16 行转译为「超时关闭」");
  466. check("A2", !textsList.some((t) => t.includes("16")), "全页文本不含裸 \"16\"");
  467. check("A3", allList.includes("交易关闭"), "正控:8 行「交易关闭」仍是老文案");
  468. check("A4", allList.includes("99"), "正控:表外值 99 仍原样显示(兜底没被删)");
  469. check(
  470. "A5",
  471. tagPairs.some((p) => p.text === "超时关闭" && p.type === "info"),
  472. "tag 配对 {type:info, text:超时关闭} 存在"
  473. );
  474. check(
  475. "A6",
  476. tagPairs.some((p) => p.text === "支付成功" && p.type === "success"),
  477. "正控:1 行仍是 {success, 支付成功}"
  478. );
  479. check(
  480. "A5b",
  481. globalThis.__U.realHar.HAR_TRADE_STATUS_TAG["16"] === "info",
  482. "TAG 表里 16 显式给了 info(模板 `|| 'info'` 兜底让渲染分辨不出,直接钉表值)"
  483. );
  484. check(
  485. "A7",
  486. optionPairs.some((o) => o.value === "16" && o.label === "超时关闭"),
  487. "筛选下拉(v-for 吃 TEXT 表)出现 {16 → 超时关闭}"
  488. );
  489. check(
  490. "A8",
  491. optionPairs.some((o) => o.value === "8" && o.label === "交易关闭"),
  492. "正控:下拉里 8 仍是「交易关闭」"
  493. );
  494. const tagCountBefore = tagPairs.length;
  495. bOrder.detail.value = ROW16;
  496. bOrder.detailVisible.value = true;
  497. await flush();
  498. const added = tagPairs.slice(tagCountBefore);
  499. const textsAfterDetail = flattenText(rootOrder);
  500. check(
  501. "B1",
  502. added.some((p) => p.text === "超时关闭" && p.type === "info"),
  503. "详情弹层里的状态 tag 同款转译"
  504. );
  505. check("B2", !textsAfterDetail.some((t) => t.includes("16")), "打开详情后全页文本仍不含裸 \"16\"");
  506. console.log("");
  507. console.log("== C 收银台 result / apply ==");
  508. const bCashier = mountCashier();
  509. await flush();
  510. const r16 = (() => {
  511. bCashier.order.value = { trade_status: "16", price: "1.00" };
  512. return bCashier.result.value;
  513. })();
  514. check(
  515. "C1",
  516. r16.title === "超时关闭" && r16.tone === "muted" && String(r16.detail || "").includes("没有收到钱"),
  517. `16 → ${JSON.stringify(r16.title)} / ${JSON.stringify(r16.detail)}`
  518. );
  519. bCashier.order.value = { trade_status: "1", rcv_amount: "1.00", price: "1.00" };
  520. const r1 = bCashier.result.value;
  521. check("C2", r1.title === "收款成功" && r1.money === "1.00", `正控:1 → ${JSON.stringify(r1.title)} / money=${r1.money}`);
  522. bCashier.order.value = { trade_status: "8" };
  523. const r8 = bCashier.result.value;
  524. check("C3", r8.title === "交易关闭", `正控:8 → ${JSON.stringify(r8.title)}`);
  525. bCashier.order.value = { trade_status: "99" };
  526. const r99 = bCashier.result.value;
  527. check("C4", r99.title === "99", `正控:表外值 99 仍原样(兜底没被删)`);
  528. bCashier.phase.value = "waiting";
  529. bCashier.apply = bCashier.apply; // 绑定即函数本身
  530. bCashier.apply({ trade_status: "16", price: "1.00" });
  531. check("C5", bCashier.phase.value === "done", "apply(16) 停轮询进 done(现状行为回归)");
  532. console.log("");
  533. const bad = results.filter((r) => !r.ok).length;
  534. console.log(`SUMMARY ${bad === 0 ? "PASS" : "FAIL"} 断言=${results.length} 不符=${bad}`);
  535. // 兜底退出:即使有残留定时器也不吊在事件循环上;失败也退 0,只认 SUMMARY。
  536. process.exit(0);