fix(swaptrade): 修复重输/重贴原值后回车不触发计算、粘贴不标记REV的静默失效
- swapPricePrecisionHelper: onKeydown 回车时无条件派发 input+enter, 不再依赖仅值变化才触发的原生 change;onPaste 粘贴即派发 input 标 REV; onChange 不再派发 enter(改由 onKeydown 唯一派发,避免重复计算) - 新增 fe-tests/swapPriceInput.component.test.js 回归守卫(6 用例, 回退旧版即变红,锁定该 QA 高优 Bug 修复) - bundle/bundleV2: otcDebug 调试开关改为 localStorage 持久化 + URL ?otcdebug=1 触发,便于排查价格计算链路
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* swapPriceInput.component.test.js — vue-swap-price-input 事件派发回归守卫
|
||||
* ============================================================================
|
||||
* 目的:锁定债券三字段输入组件「粘贴 / 重输原值 不派发 input/enter」的 bug。
|
||||
* 症状:粘贴或重输原值后,点空白(标REV/清其他标识)与回车(触发计算)均无反应。
|
||||
* 根因:组件把 input/enter 全部锁在原生 change 事件里,而 change 仅在「值相对聚焦时
|
||||
* 变动」才触发;onPaste 甚至完全不派发任何事件。
|
||||
*
|
||||
* 做法:直接实例化组件 options 对象(createVueInputComponent),用 mock this + $emit 间谍
|
||||
* 驱动 onPaste/onKeydown/onChange,断言事件派发。不依赖 Vue 运行时挂载、不改动源文件。
|
||||
*
|
||||
* 运行:cd YLErpWeb/fe-tests && npx jest swapPriceInput.component
|
||||
* 设 SWAP_PRICE_HELPER 指向其它版本文件可做对照(如改之前的旧代码应使用例变红)。
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function loadComponent() {
|
||||
const rel = process.env.SWAP_PRICE_HELPER
|
||||
|| '../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js';
|
||||
const file = path.resolve(__dirname, rel);
|
||||
const code = fs.readFileSync(file, 'utf8');
|
||||
// swapPricePrecisionHelper.js: var swapPricePrecision = (function(global){...})(window);
|
||||
// 在受控作用域求值并取出 createVueInputComponent(IIFE 不挂 window,故用 new Function 取返回值)。
|
||||
const factory = new Function('window', 'global', 'console', code + '\n;return swapPricePrecision;');
|
||||
const sandbox = {};
|
||||
const swapPricePrecision = factory(sandbox, sandbox, console);
|
||||
return swapPricePrecision.createVueInputComponent();
|
||||
}
|
||||
|
||||
function makeCtx(component, opts) {
|
||||
opts = opts || {};
|
||||
const emitted = [];
|
||||
const ctx = {
|
||||
text: opts.text != null ? String(opts.text) : '',
|
||||
enterPressed: false,
|
||||
formatSnapshot: '{}',
|
||||
format: opts.format || { integerDigits: 2, precision: 4, percent: true },
|
||||
$emit: function (name, payload) { emitted.push({ name: name, payload: payload }); },
|
||||
};
|
||||
Object.keys(component.methods).forEach(function (m) {
|
||||
ctx[m] = component.methods[m].bind(ctx);
|
||||
});
|
||||
return { ctx: ctx, emitted: emitted };
|
||||
}
|
||||
|
||||
describe('vue-swap-price-input 事件派发(粘贴 / 重输原值场景)', () => {
|
||||
const component = loadComponent();
|
||||
|
||||
test('粘贴原值应派发 input(点空白标REV / 清其他标识)', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: true } });
|
||||
const evt = { preventDefault() {}, clipboardData: { getData: () => '3.5' }, target: { value: '' } };
|
||||
ctx.onPaste(evt);
|
||||
const inputs = emitted.filter(e => e.name === 'input');
|
||||
expect(inputs.length).toBe(1);
|
||||
expect(inputs[0].payload).toBe('0.035'); // percent:true → ÷100
|
||||
});
|
||||
|
||||
test('回车(原值未变)应派发 enter(触发自动计算)', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: true } });
|
||||
const evt = { which: 13, keyCode: 13, key: 'Enter', target: { blur() {}, value: '3.5' } };
|
||||
ctx.onKeydown(evt);
|
||||
const enters = emitted.filter(e => e.name === 'enter');
|
||||
expect(enters.length).toBe(1);
|
||||
expect(enters[0].payload).toBe('0.035');
|
||||
});
|
||||
|
||||
test('回车(值已改)应派发 input+enter 各一次,且不重复派发 enter', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.6', format: { percent: true } });
|
||||
const evt = { which: 13, keyCode: 13, key: 'Enter', target: { blur() {}, value: '3.6' } };
|
||||
ctx.onKeydown(evt);
|
||||
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
|
||||
expect(emitted.filter(e => e.name === 'enter').length).toBe(1);
|
||||
});
|
||||
|
||||
test('非 Enter 按键不应派发 enter / input', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { percent: true } });
|
||||
const evt = { which: 65, keyCode: 65, key: 'a', target: { blur() {} } };
|
||||
ctx.onKeydown(evt);
|
||||
expect(emitted.filter(e => e.name === 'enter').length).toBe(0);
|
||||
expect(emitted.filter(e => e.name === 'input').length).toBe(0);
|
||||
});
|
||||
|
||||
test('失焦(onChange)即使值未变也应派发 input(点空白标REV),且不再派发 enter', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { percent: true } });
|
||||
const evt = { target: { value: '3.5' } };
|
||||
ctx.onChange(evt);
|
||||
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
|
||||
expect(emitted.filter(e => e.name === 'enter').length).toBe(0);
|
||||
});
|
||||
|
||||
test('粘贴但格式为 percent:false 也应派发 input(原值透传)', () => {
|
||||
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: false } });
|
||||
const evt = { preventDefault() {}, clipboardData: { getData: () => '3.5' }, target: { value: '' } };
|
||||
ctx.onPaste(evt);
|
||||
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
|
||||
expect(emitted.filter(e => e.name === 'input')[0].payload).toBe('3.5');
|
||||
});
|
||||
});
|
||||
@@ -208,22 +208,32 @@ var swapPricePrecision = (function (global) {
|
||||
event.preventDefault();
|
||||
this.updateValue(clipboard.getData('text'), true, false);
|
||||
event.target.value = this.text;
|
||||
// 粘贴即显式赋值意图:直接派发 input,使「单击空白处标REV/其余清空」「回车前已标记」等行为生效。
|
||||
// 原实现只把粘贴内容写回DOM、等失焦时的原生change才派发,而原值粘贴时change不触发→静默无反应。
|
||||
this.$emit('input', this.toModel(this.text));
|
||||
},
|
||||
onKeydown: function (event) {
|
||||
this.enterPressed = false;
|
||||
this.$emit('keydown', event);
|
||||
const keyCode = event.which || event.keyCode;
|
||||
if (event.key !== 'Enter' && keyCode !== 13 && keyCode !== 108) return;
|
||||
this.enterPressed = true;
|
||||
event.target.blur();
|
||||
// 回车=用户显式提交意图:无论值是否相对聚焦时变化,都直接派发 input(标REV) + enter(触发计算)。
|
||||
// 原实现把 enter 锁在原生 change 里,而 change 仅在值变化时触发,
|
||||
// 导致「重输/重贴原值 + 回车」静默不计算(QA 高优 Bug)。
|
||||
this.updateValue(this.text, true, false);
|
||||
const value = this.toModel(this.text);
|
||||
event.target.blur(); // 同步触发原生 change(值变化时再派发一次 input,幂等无害)
|
||||
this.$emit('input', value);
|
||||
this.$emit('enter', value);
|
||||
this.enterPressed = false;
|
||||
},
|
||||
onChange: function (event) {
|
||||
if (this.text.endsWith('.')) this.text = this.text.substring(0, this.text.length - 1);
|
||||
this.updateValue(this.text, true, false);
|
||||
const value = this.toModel(this.text);
|
||||
// 失焦(非回车)→ 仅派发 input 标 REV;enter(计算) 改由 onKeydown 直接派发,
|
||||
// 避免 change 不触发时丢失,也避免与 onKeydown 重复派发 enter 造成重复计算。
|
||||
this.$emit('input', value);
|
||||
if (this.enterPressed) this.$emit('enter', value);
|
||||
this.enterPressed = false;
|
||||
event.target.value = this.text;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14667,6 +14667,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
};
|
||||
|
||||
}(window.FastVue));
|
||||
|
||||
//日期选择组件FastVue.DatePicker
|
||||
(function (global) {
|
||||
global.vueDatePicker = function () {
|
||||
@@ -15009,21 +15010,35 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
//依赖类库:lodash.js + jquery + jquery.validate + bootstrap.emodal.js
|
||||
|
||||
// 统一调试日志工具:仅在 otcdebug 开启时输出,默认静默(生产零噪音)。
|
||||
// 开启方式:URL 加 ?otcdebug=1,或控制台 localStorage.setItem('otcdebug','1')。
|
||||
// 业务脚本逐步改用 otcDebug.banner / otcDebug.log 替代裸 console.log,
|
||||
// 既能在排查时一键全开,又能避免版本标记散落硬编码(见 swapTradeEdit.js / fastVue.base.js 用法)。
|
||||
var __otcDiag = (window.ylotc && window.ylotc.__diag) || { debug: false };
|
||||
// 开启:URL 加 ?otcdebug=1(会自动写入 localStorage,同源 iframe/弹窗自动继承);
|
||||
// 或控制台 localStorage.setItem('otcdebug','1')。关闭:localStorage.removeItem('otcdebug')。
|
||||
// 业务脚本用 otcDebug.banner / otcDebug.log 替代裸 console.log(见 swapTradeEdit.js / fastVue.base.js)。
|
||||
//
|
||||
// 命名约定(避免 __私有对象 + otcDebug.API 大小写混搭):
|
||||
// __diag —— 版本元数据对象(仅 _MainLayout 灌入 ylotc.__diag 时存在,仅供 banner 展示,不参与开关判定)
|
||||
// __debug —— 调试开关布尔值(唯一真相来源:localStorage 优先 → URL 参数 → 兜底 __diag.debug)
|
||||
var __diag = (window.ylotc && window.ylotc.__diag) || {};
|
||||
var __debug = (function () {
|
||||
try {
|
||||
// 1) URL 参数优先(排查最直观);命中即写入 localStorage,供同源 iframe/弹窗继承
|
||||
if (/[?&]otcdebug=1/.test(location.search)) { localStorage.setItem('otcdebug', '1'); return true; }
|
||||
// 2) 已持久化的开关(父页面开过一次,_InfoLayout 弹窗自动生效)
|
||||
if (localStorage.getItem('otcdebug') === '1') return true;
|
||||
} catch (e) { /* localStorage 不可用时忽略,继续走兜底 */ }
|
||||
// 3) 兜底:layout 灌入的 ylotc.__diag.debug(仅 _MainLayout 有)
|
||||
return !!__diag.debug;
|
||||
})();
|
||||
window.otcDebug = {
|
||||
// 模块加载横幅:F12 一眼看到模块名+bundle版本+git sha,用于排查"是不是加载了旧代码/旧缓存"
|
||||
// 模块加载横幅:F12 一眼看到模块名+版本,用于排查"是不是加载了旧代码/旧缓存"
|
||||
banner: function (name, ver) {
|
||||
if (!__otcDiag.debug || !window.console) return;
|
||||
var git = (__otcDiag.git || '').slice(0, 7);
|
||||
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __otcDiag.jsVersion + ', git=' + git + ', built=' + __otcDiag.built + ')',
|
||||
if (!__debug || !window.console) return;
|
||||
var git = (__diag.git || '').slice(0, 7);
|
||||
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __diag.jsVersion + ', git=' + git + ', built=' + __diag.built + ')',
|
||||
'color:#06c;font-weight:bold');
|
||||
},
|
||||
// 普通调试日志:透传参数,仅 debug 开启时输出
|
||||
log: function () {
|
||||
if (!__otcDiag.debug || !window.console) return;
|
||||
if (!__debug || !window.console) return;
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -802,21 +802,35 @@ An}();typeof define=="function"&&typeof define.amd=="object"&&define.amd?($n._=r
|
||||
//依赖类库:lodash.js + jquery + jquery.validate + bootstrap.emodal.js
|
||||
|
||||
// 统一调试日志工具:仅在 otcdebug 开启时输出,默认静默(生产零噪音)。
|
||||
// 开启方式:URL 加 ?otcdebug=1,或控制台 localStorage.setItem('otcdebug','1')。
|
||||
// 业务脚本逐步改用 otcDebug.banner / otcDebug.log 替代裸 console.log,
|
||||
// 既能在排查时一键全开,又能避免版本标记散落硬编码(见 swapTradeEdit.js / fastVue.base.js 用法)。
|
||||
var __otcDiag = (window.ylotc && window.ylotc.__diag) || { debug: false };
|
||||
// 开启:URL 加 ?otcdebug=1(会自动写入 localStorage,同源 iframe/弹窗自动继承);
|
||||
// 或控制台 localStorage.setItem('otcdebug','1')。关闭:localStorage.removeItem('otcdebug')。
|
||||
// 业务脚本用 otcDebug.banner / otcDebug.log 替代裸 console.log(见 swapTradeEdit.js / fastVue.base.js)。
|
||||
//
|
||||
// 命名约定(避免 __私有对象 + otcDebug.API 大小写混搭):
|
||||
// __diag —— 版本元数据对象(仅 _MainLayout 灌入 ylotc.__diag 时存在,仅供 banner 展示,不参与开关判定)
|
||||
// __debug —— 调试开关布尔值(唯一真相来源:localStorage 优先 → URL 参数 → 兜底 __diag.debug)
|
||||
var __diag = (window.ylotc && window.ylotc.__diag) || {};
|
||||
var __debug = (function () {
|
||||
try {
|
||||
// 1) URL 参数优先(排查最直观);命中即写入 localStorage,供同源 iframe/弹窗继承
|
||||
if (/[?&]otcdebug=1/.test(location.search)) { localStorage.setItem('otcdebug', '1'); return true; }
|
||||
// 2) 已持久化的开关(父页面开过一次,_InfoLayout 弹窗自动生效)
|
||||
if (localStorage.getItem('otcdebug') === '1') return true;
|
||||
} catch (e) { /* localStorage 不可用时忽略,继续走兜底 */ }
|
||||
// 3) 兜底:layout 灌入的 ylotc.__diag.debug(仅 _MainLayout 有)
|
||||
return !!__diag.debug;
|
||||
})();
|
||||
window.otcDebug = {
|
||||
// 模块加载横幅:F12 一眼看到模块名+bundle版本+git sha,用于排查"是不是加载了旧代码/旧缓存"
|
||||
// 模块加载横幅:F12 一眼看到模块名+版本,用于排查"是不是加载了旧代码/旧缓存"
|
||||
banner: function (name, ver) {
|
||||
if (!__otcDiag.debug || !window.console) return;
|
||||
var git = (__otcDiag.git || '').slice(0, 7);
|
||||
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __otcDiag.jsVersion + ', git=' + git + ', built=' + __otcDiag.built + ')',
|
||||
if (!__debug || !window.console) return;
|
||||
var git = (__diag.git || '').slice(0, 7);
|
||||
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __diag.jsVersion + ', git=' + git + ', built=' + __diag.built + ')',
|
||||
'color:#06c;font-weight:bold');
|
||||
},
|
||||
// 普通调试日志:透传参数,仅 debug 开启时输出
|
||||
log: function () {
|
||||
if (!__otcDiag.debug || !window.console) return;
|
||||
if (!__debug || !window.console) return;
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user