### 架构重构:三层分层目录化 - domain 拆分为 8 个子目录(core/pipeline/rules/finance/stats/taxonomy/transaction/platform) - components 拆分为 6 个子目录(form/ui/layout/account/category/stats/transaction) - services 拆分为 5 个子目录(automation/data/ocr/security),accessibilityParser 从 automationPipeline 提取 - 新增 ruleConfig.ts — 规则配置唯一数据源(关键词/方向/OCR 模式),与业务逻辑解耦 ### 微信账单抓取:节点混淆绕过(核心突破) - BillingAccessibilityService 重命名为 SelectToSpeakService,完整伪装为系统服务 - 同时伪装包名+类名为 com.google.android.accessibility.selecttospeak,规避微信 8.0.52+ 白名单校验 - Config Plugin 重写:8 个 kt 文件整体复制+package 正则替换+Manifest/import 联动 - 支付宝/微信无障碍文本解析器全面增强(方向推断/账单分类提取/付款方式提取/容错) - 补充文档 accessibility-wechat-guide.md(伪装原理、踩坑全记录) ### 新 UI 组件体系 - Toast:全局轻量 toast(Context Provider + 入场动画 + 操作按钮 + 自动消失) - ErrorBoundary:React class 错误边界(降级 UI + 重试) - EmptyState / ConfirmDialog / SegmentedControl / Skeleton / TimePicker - BottomSheet 重写:SafeAreaProvider 修复、手势下滑关闭、键盘响应式避让 - FormModal 重构:拆出 FormFields 子组件(TextField/SelectField/DropdownField) - 新增 PeriodSwitcher、RangeStatsCard 独立组件 ### 新 Hooks & 工具 - useBottomInset — 统一底部安全区留白 - useKeyboardAvoiding — 键盘高度响应式 hook(替代 translateY 方案) - sanitize.ts — 日志脱敏工具提取 ### 账本增删改增强 - 写锁增加代际计数器(lockGeneration),reset 后旧链 pending 任务自动跳过 set - 新增 restoreTransaction — 撤销删除(重新追加 raw 文本到 mobile.bean) - 删除交易时清除去重缓存(buildTxKeyFromRaw 重建去重键),支持「删了重记」 - editTransaction/deleteTransaction 改用 dr-id 精确定位交易块(避免同名交易定位错误) - appendTransactionsBatch 改从存储直接读取,避免 zustand state 不一致 ### OCR 原生模块增强 - 异步 initEngine 增加 CountDownLatch 等待(最多 15s),解决竞态导致的「引擎未就绪」 - setModelDir 增加去重判断 + file:// 前缀剥离,避免冗余 reload - 推理链路增加分阶段耗时日志(det 推理/det 后处理/rec 识别) - 图片缩放策略重命名(scaleDownForOcr → capLongEdge) ### OCR 模型按需下载 - 移除了启动时自动下载 ~30MB OCR 模型的逻辑 - 改为首次使用 OCR 时才触发下载 ### 设置页重设计 - ScrollView → SectionList 分组卡片布局(iOS 风格分组圆角行+右侧箭头) - 移除 Card 组件包装,直接使用独立分组头+底部关于卡片 ### 首页优化 - ScrollView → FlatList(ListHeaderComponent 承载净资产卡片+待办条) - 日期/金额格式化增加 locale 感知(zh/en) ### 通知管道增强 - NotificationChannel MD5 去重改为批量淘汰(80% 阈值),替代逐个删除 - 增加 debug 日志输出(过滤原因/包名) ### ESLint - 新增 eslint.config.mjs(typescript-eslint + react-hooks + react-native 规则集) - package.json 新增 lint/lint:fix 脚本,引入 5 个 devDependencies ### 文档 - accessibility-wechat-guide.md — 微信无障碍伪装完整方案 - modal-keyboard-guide.md — 弹窗键盘避让方案 - ocr-pipeline-guide.md — OCR 三层层级管线 - OCR及文本模型测试 / 账单元识别及账户分类设计 / 账户分类模型测试
433 lines
20 KiB
TypeScript
433 lines
20 KiB
TypeScript
/**
|
||
* 无障碍文本解析器测试。
|
||
*
|
||
* 重点验证:
|
||
* - 银行详情页(兴业/建行/招行等)通用解析器,按页面特征词分派
|
||
* - 金额格式兼容性:¥ 前缀、千分位逗号、符号前置/后置负号
|
||
* - 不破坏支付宝/微信现有解析行为
|
||
*
|
||
* 测试样本取自真实无障碍日志(兴业/建行/支付宝详情页节点文本)。
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import { parseAccessibilityTexts } from '../src/services/automation/accessibilityParser';
|
||
|
||
// ─── 真实样本(取自日志) ───────────────────────────────────────
|
||
|
||
/** 兴业银行交易明细详情页(日志 14:45:49,支出 49.00)。 */
|
||
const CIB_TEXTS = [
|
||
'交易明细详情', '支出金额', '¥49.00', '对方信息', '1100000000', '对方银行',
|
||
'财付通支付科技有限公司', '交易摘要', '跨行无卡消费', '交易时间', '2026/07/17 20:29:48',
|
||
'记账日期', '2026/07/17', '用途', '财付通(银联云闪付)', '账户余额', '11,565.20 ', '',
|
||
];
|
||
|
||
/** 建设银行明细详情页(日志 14:47:57,还款 -12.32,京东白条)。 */
|
||
const CCB_TEXTS = [
|
||
'明细详情', 'png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8BAM', '还款', '¥-12.32',
|
||
'交易账户', '湘潭大学 6217***9959', '交易时间', '2026-07-22 11:32:06',
|
||
'交易地点/附言', '京东支付-京东金融-京东白条及金条业务网银平台', '对方账户',
|
||
'京***及金条业务网银平台 Z******0011', '记账日', '2026-07-22', '账户余额', '¥ 0.79', '备注',
|
||
];
|
||
|
||
/** 支付宝账单详情页(日志 14:48:23,花呗消费 66.8)。 */
|
||
const ALIPAY_TEXTS = [
|
||
'盛香亭新式热卤', '支出66.8元', '交易成功', '支付时间', '2026-07-24 21:53:15',
|
||
'2026-07-24 21:53:15', '付款方式', '花呗 ', '商品说明', '湘江新区闪亮云顶餐饮店',
|
||
'湘江新区闪亮云顶餐饮店', '账单分类', '餐饮美食', '账单详情',
|
||
];
|
||
|
||
// ─── 银行解析器 ─────────────────────────────────────────────
|
||
|
||
describe('parseBankTexts(通用银行解析器)', () => {
|
||
it('解析兴业银行详情页:金额带 ¥ 前缀,方向为支出', () => {
|
||
const event = parseAccessibilityTexts(CIB_TEXTS, 'com.cib.cibmb');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.amount).toBe('49.00');
|
||
expect(event!.currency).toBe('CNY');
|
||
expect(event!.direction).toBe('expense');
|
||
expect(event!.occurredAt).toBe('2026/07/17 20:29:48');
|
||
});
|
||
|
||
it('解析兴业银行详情页:提取对手方(被动扣款通知无持卡账户)', () => {
|
||
const event = parseAccessibilityTexts(CIB_TEXTS, 'com.cib.cibmb');
|
||
expect(event).not.toBeNull();
|
||
// 对手方:对方信息下一节点为手机号(纯数字被跳过),回退到对方银行或交易摘要
|
||
expect(event!.counterparty.length).toBeGreaterThan(0);
|
||
// 兴业这类被动扣款通知没有"交易账户"标签,付款方式从包名友好名兜底
|
||
// (com.cib.cibmb → "兴业银行"),避免落入 Assets:Unknown 兜底
|
||
expect(event!.raw['收/付款方式']).toBe('兴业银行');
|
||
});
|
||
|
||
it('解析建设银行还款页:金额 ¥-12.32(符号前置负号)', () => {
|
||
const event = parseAccessibilityTexts(CCB_TEXTS, 'com.chinamworld.main');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.amount).toBe('12.32');
|
||
expect(event!.direction).toBe('expense');
|
||
expect(event!.occurredAt).toBe('2026-07-22 11:32:06');
|
||
});
|
||
|
||
it('付款方式缺省时从包名友好名兜底(建行 → 建设银行)', () => {
|
||
// 建行这条有"交易账户"标签会优先提取,这里构造一个无任何付款方式标签的银行详情页
|
||
const texts = [
|
||
'明细详情', '支出金额', '¥100.00', '对方信息', '某商户',
|
||
'交易时间', '2026-07-24 10:00:00', '记账日', '2026-07-24', '账户余额', '¥ 500.00',
|
||
];
|
||
const event = parseAccessibilityTexts(texts, 'com.chinamworld.main');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.raw['收/付款方式']).toBe('建设银行');
|
||
});
|
||
|
||
it('解析建设银行还款页:备注包含"还款"文案(供还款识别规则命中)', () => {
|
||
const event = parseAccessibilityTexts(CCB_TEXTS, 'com.chinamworld.main');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.memo).toContain('还款');
|
||
});
|
||
|
||
it('解析建设银行还款页:交易账户作为付款方式写入 raw', () => {
|
||
const event = parseAccessibilityTexts(CCB_TEXTS, 'com.chinamworld.main');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.raw['收/付款方式']).toContain('湘潭大学');
|
||
});
|
||
|
||
it('跳过 base64 图片节点,不误识别为金额或对手方', () => {
|
||
const event = parseAccessibilityTexts(CCB_TEXTS, 'com.chinamworld.main');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.counterparty).not.toContain('base64');
|
||
expect(event!.amount).not.toContain('base64');
|
||
});
|
||
|
||
it('任意包名都能触发银行解析(按页面特征词分派,不依赖包名白名单)', () => {
|
||
// 用一个未登记的包名,但文本含银行详情页特征词,应能解析
|
||
const event = parseAccessibilityTexts(CIB_TEXTS, 'com.unknown.bank.app');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.amount).toBe('49.00');
|
||
});
|
||
|
||
it('招行掌银(交易明细 + 带冒号标签 + 无商户标签):不降级 OCR,商户取金额前节点', () => {
|
||
// 真机日志:招行掌银 ChatGPT 订阅,特征词"交易明细",标签带冒号,商户名裸露在金额前
|
||
const CMB_TEXTS = [
|
||
'图标', '交易明细', 'GOOGLE *ChatGPT', '¥143.97', '已入账',
|
||
'信用卡:', '3315', '交易时间:', '2026-07-23 00:00:00', '入账日期:', '2026-07-24',
|
||
'国家或地区:', '美国', '交易地:', '交易地金额:', '21.19', '我要分期',
|
||
];
|
||
const event = parseAccessibilityTexts(CMB_TEXTS, 'com.cmbchina.ccd.pluto.cmbActivity');
|
||
expect(event).not.toBeNull();
|
||
// 金额取带 ¥ 的 143.97(非交易地金额 21.19)
|
||
expect(event?.amount).toBe('143.97');
|
||
// 商户取金额前节点(GOOGLE *ChatGPT),跳过特征词"交易明细"
|
||
expect(event?.counterparty).toBe('GOOGLE *ChatGPT');
|
||
// 时间:含"交易时间"的节点("交易时间:")取下一节点
|
||
expect(event?.occurredAt).toContain('2026-07-23');
|
||
});
|
||
});
|
||
|
||
// ─── 金额格式兼容性 ─────────────────────────────────────────
|
||
|
||
describe('金额格式兼容性', () => {
|
||
const makeTexts = (amountNode: string) => [
|
||
'交易明细详情', '支出金额', amountNode, '对方信息', '某商户',
|
||
'交易时间', '2026-07-24 10:00:00', '记账日期', '2026-07-24',
|
||
];
|
||
|
||
it('支持 ¥ 前缀:¥49.00', () => {
|
||
const event = parseAccessibilityTexts(makeTexts('¥49.00'), 'com.cib.cibmb');
|
||
expect(event?.amount).toBe('49.00');
|
||
});
|
||
|
||
it('支持 ¥ 前缀:¥49.00', () => {
|
||
const event = parseAccessibilityTexts(makeTexts('¥49.00'), 'com.cib.cibmb');
|
||
expect(event?.amount).toBe('49.00');
|
||
});
|
||
|
||
it('支持符号前置负号:¥-12.32', () => {
|
||
const event = parseAccessibilityTexts(makeTexts('¥-12.32'), 'com.cib.cibmb');
|
||
expect(event?.amount).toBe('12.32');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('支持金额前带空格:¥ 0.79', () => {
|
||
const event = parseAccessibilityTexts(makeTexts('¥ 0.79'), 'com.cib.cibmb');
|
||
expect(event?.amount).toBe('0.79');
|
||
});
|
||
|
||
it('支持千分位逗号:11,565.20', () => {
|
||
const event = parseAccessibilityTexts(
|
||
['交易明细详情', '交易摘要', '某消费', '交易金额', '1,234.56', '交易时间', '2026-07-24 10:00:00', '记账日', '2026-07-24'],
|
||
'com.cib.cibmb',
|
||
);
|
||
expect(event?.amount).toBe('1234.56');
|
||
});
|
||
|
||
it('不匹配纯整数(避免误吞年份/卡号):2026', () => {
|
||
const event = parseAccessibilityTexts(
|
||
['交易明细详情', '支出金额', '2026', '对方信息', '商户', '交易时间', '2026-07-24 10:00:00', '记账日', '2026-07-24'],
|
||
'com.cib.cibmb',
|
||
);
|
||
// 金额无法解析(2026 无小数点),应返回 null
|
||
expect(event).toBeNull();
|
||
});
|
||
|
||
it('不误匹配非金额的纯数字小数(无 ¥ 也无千分位时,兜底分支不接管)', () => {
|
||
// "66.8" 是支付宝风格,但此处包名是银行,且无 ¥/千分位,兜底分支应跳过
|
||
// 只有当存在金额标签"支出金额"时才提取
|
||
const event = parseAccessibilityTexts(
|
||
['交易明细详情', '支出金额', '66.8', '对方信息', '商户', '交易时间', '2026-07-24 10:00:00', '记账日', '2026-07-24'],
|
||
'com.cib.cibmb',
|
||
);
|
||
expect(event?.amount).toBe('66.8');
|
||
});
|
||
});
|
||
|
||
// ─── 不破坏支付宝/微信现有行为 ───────────────────────────────
|
||
|
||
describe('支付宝/微信解析行为回归(不应被银行解析器干扰)', () => {
|
||
it('支付宝详情页正常解析(包名分派优先)', () => {
|
||
const event = parseAccessibilityTexts(ALIPAY_TEXTS, 'com.eg.android.AlipayGphone');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.amount).toBe('66.8');
|
||
expect(event!.direction).toBe('expense');
|
||
expect(event!.counterparty).toBe('盛香亭新式热卤');
|
||
expect(event!.raw['收/付款方式']).toBe('花呗');
|
||
});
|
||
|
||
it('微信详情页正常解析(包名分派优先)', () => {
|
||
const wechatTexts = [
|
||
'某商户', '12.93', '交易单号', '4200001234202407123456789',
|
||
'本服务由财付通提供', '2026-07-24 10:00:00',
|
||
];
|
||
const event = parseAccessibilityTexts(wechatTexts, 'com.tencent.mm');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.amount).toBe('12.93');
|
||
expect(event!.direction).toBe('expense');
|
||
});
|
||
|
||
it('支付宝包名不误触发银行解析器', () => {
|
||
// 支付宝文本不含银行特征词,即便强行走银行分支也应返回 null
|
||
// (实际上包名分派会先走支付宝,这里验证不冲突)
|
||
const event = parseAccessibilityTexts(ALIPAY_TEXTS, 'com.eg.android.AlipayGphone');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.counterparty).toBe('盛香亭新式热卤');
|
||
});
|
||
});
|
||
|
||
// ─── 边界情况 ─────────────────────────────────────────────
|
||
|
||
describe('边界情况', () => {
|
||
it('空文本返回 null', () => {
|
||
expect(parseAccessibilityTexts([], 'com.cib.cibmb')).toBeNull();
|
||
});
|
||
|
||
it('非详情页文本返回 null', () => {
|
||
expect(parseAccessibilityTexts(['随便一些', '不相关', '文本'], 'com.cib.cibmb')).toBeNull();
|
||
});
|
||
|
||
it('详情页但金额全为 0 返回 null', () => {
|
||
const event = parseAccessibilityTexts(
|
||
['交易明细详情', '支出金额', '¥0.00', '对方信息', '商户', '交易时间', '2026-07-24 10:00:00', '记账日', '2026-07-24'],
|
||
'com.cib.cibmb',
|
||
);
|
||
expect(event).toBeNull();
|
||
});
|
||
|
||
it('ImportedEvent 必填字段完整', () => {
|
||
const event = parseAccessibilityTexts(CIB_TEXTS, 'com.cib.cibmb');
|
||
expect(event).not.toBeNull();
|
||
expect(event!.id).toBeTruthy();
|
||
expect(event!.occurredAt).toBeTruthy();
|
||
expect(event!.amount).toBeTruthy();
|
||
expect(event!.currency).toBe('CNY');
|
||
expect(event!.raw.packageName).toBe('com.cib.cibmb');
|
||
});
|
||
});
|
||
|
||
describe('parseUnionPayTexts(云闪付交易详情页)', () => {
|
||
// 真机日志实测:云闪付乘车码交易详情页无障碍节点
|
||
// 商户名裸露在 -¥金额 上一节点,付款方式有标签,时间含"延迟扣费"前缀
|
||
const UNIONPAY_TEXTS = [
|
||
'返回', '云闪付交易详情', '客服按钮',
|
||
'长沙公交智慧大数据科技有限公司(乘车码)', '-¥1.16',
|
||
'优惠信息', '绿色出行-天天有优惠(公交地铁)-¥0.24',
|
||
'当前状态', '交易成功',
|
||
'订单金额', '¥1.40',
|
||
'付款方式', '招商银行银联信用卡[1589]',
|
||
'扣款时间', '延迟扣费|2026年6月27日 13:30:15',
|
||
'乘车信息', '8205路 2026年6月27日 13:28:46',
|
||
'交易流水号', '956893083417414766021', '商户订单号', '1388149433202380800',
|
||
'在此商户的交易',
|
||
];
|
||
|
||
it('云闪付详情页:识别金额 -1.16,方向支出', () => {
|
||
const event = parseAccessibilityTexts(UNIONPAY_TEXTS, 'com.unionpay');
|
||
expect(event).not.toBeNull();
|
||
expect(event?.amount).toBe('1.16');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('云闪付详情页:商户名取自金额上一节点(含全角括号归一化)', () => {
|
||
const event = parseAccessibilityTexts(UNIONPAY_TEXTS, 'com.unionpay');
|
||
expect(event).not.toBeNull();
|
||
// 全角()应归一化为半角 ()
|
||
expect(event?.counterparty).toBe('长沙公交智慧大数据科技有限公司(乘车码)');
|
||
});
|
||
|
||
it('云闪付详情页:付款方式提取到 raw', () => {
|
||
const event = parseAccessibilityTexts(UNIONPAY_TEXTS, 'com.unionpay');
|
||
expect(event?.raw['收/付款方式']).toBe('招商银行银联信用卡[1589]');
|
||
});
|
||
|
||
it('云闪付详情页:时间提取(含延迟扣费前缀清洗)', () => {
|
||
const event = parseAccessibilityTexts(UNIONPAY_TEXTS, 'com.unionpay');
|
||
expect(event).not.toBeNull();
|
||
// 原始 "延迟扣费|2026年6月27日 13:30:15" → occurredAt 含 2026-06-27
|
||
expect(event?.occurredAt).toContain('2026-06-27');
|
||
expect(event?.occurredAt).toContain('13:30');
|
||
});
|
||
|
||
it('云闪付列表页(非详情页)返回 null', () => {
|
||
const listTexts = ['返回', '交易记录', '2026年7月', '云闪付APP', '筛选', '07.17 周五', '-¥49.00'];
|
||
const event = parseAccessibilityTexts(listTexts, 'com.unionpay');
|
||
// 列表页无"云闪付交易详情"标题,也无乘车码+金额组合,不应误识别
|
||
expect(event).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('parseDidiTexts(滴滴出行订单详情页)', () => {
|
||
// 真机日志:节点含粘连长文本(创建时间、订单号等合并成一个节点)
|
||
const DIDO_TEXTS = [
|
||
'返回', '订单详情', '特惠快车打车-方师傅-04月25日行程', '10.80', '付款成功',
|
||
'消费场景特惠快车创建时间2026-04-25 16:24:03订单号212_202604254026406101466527订单编号XY0021308320260425162402GM3bd6付款手机号176****7202',
|
||
'订单金额10.80', '距离更近', '湘江科创基地1期(湘江科创', '园)8栋',
|
||
];
|
||
|
||
it('滴滴:商户取标题"-"前主体(特惠快车打车)', () => {
|
||
const event = parseAccessibilityTexts(DIDO_TEXTS, 'com.sdu.didi.psnger');
|
||
expect(event).not.toBeNull();
|
||
expect(event?.counterparty).toBe('特惠快车打车');
|
||
});
|
||
|
||
it('滴滴:金额提取(兼容粘连"订单金额10.80")', () => {
|
||
const event = parseAccessibilityTexts(DIDO_TEXTS, 'com.sdu.didi.psnger');
|
||
expect(event?.amount).toBe('10.80');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('滴滴:从粘连节点提取创建时间', () => {
|
||
const event = parseAccessibilityTexts(DIDO_TEXTS, 'com.sdu.didi.psnger');
|
||
expect(event?.occurredAt).toContain('2026-04-25');
|
||
expect(event?.occurredAt).toContain('16:24');
|
||
});
|
||
|
||
it('滴滴非订单页返回 null', () => {
|
||
expect(parseAccessibilityTexts(['返回', '我的行程', '进行中'], 'com.sdu.didi.psnger')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('parseMeituanTexts(美团消费详情页)', () => {
|
||
// 真机日志:节点含 base64 图片哈希(混在金额附近,需跳过)
|
||
const MEITUAN_TEXTS = [
|
||
'返回', '消费详情', '牛爷烧土家鲜肉饼', '9.36', '订单状态',
|
||
'3cfaedc4036dbc99467a0afda37ae64f1671', '支付成功', '2026-07-24 18:51:36',
|
||
'月付付款9.36元,计入2026年8月账单,还款日8月15日',
|
||
'468934a59ff36d2e29fd2db3796011bb1188', '美团商品订单', '查看',
|
||
'42846b1ee2ad163a01ba2c8e5462675722729', '牛爷烧土家鲜肉饼-【低GI】2个黑椒牛肉饼',
|
||
'支付方式', '美团月付(极速支付)', '入账情况', '已计入8月账单',
|
||
'支付时间', '2026-07-24 18:51:36', '下单平台', '美团', '消费类型', '美食团购',
|
||
];
|
||
|
||
it('美团:跳过 base64 哈希取商户(牛爷烧土家鲜肉饼)', () => {
|
||
const event = parseAccessibilityTexts(MEITUAN_TEXTS, 'com.sankuai.meituan');
|
||
expect(event).not.toBeNull();
|
||
expect(event?.counterparty).toBe('牛爷烧土家鲜肉饼');
|
||
});
|
||
|
||
it('美团:金额 9.36,方向 expense(非 income)', () => {
|
||
const event = parseAccessibilityTexts(MEITUAN_TEXTS, 'com.sankuai.meituan');
|
||
expect(event?.amount).toBe('9.36');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('美团:付款方式提取(美团月付极速支付)', () => {
|
||
const event = parseAccessibilityTexts(MEITUAN_TEXTS, 'com.sankuai.meituan');
|
||
expect(event?.raw['收/付款方式']).toBe('美团月付(极速支付)');
|
||
});
|
||
|
||
it('美团:时间提取(支付时间标签)', () => {
|
||
const event = parseAccessibilityTexts(MEITUAN_TEXTS, 'com.sankuai.meituan');
|
||
expect(event?.occurredAt).toBe('2026-07-24 18:51');
|
||
});
|
||
|
||
it('美团非消费页返回 null', () => {
|
||
expect(parseAccessibilityTexts(['返回', '我的订单', '待使用'], 'com.sankuai.meituan')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('parseJingdongTexts(京东账单详情页)', () => {
|
||
// 真机日志:金额带负号,商户在金额前
|
||
const JD_TEXTS = [
|
||
'dancing', 'dancing', '账单详情', '京东外卖', '-12.30', '交易成功',
|
||
'付款方式', '白条', 'dancing', '付款形式', '先享后付自动扣款',
|
||
'服务详情', '共1笔订单', '【新品推荐】江西炒粉+香辣猪耳+鸡蛋。', '0', '耳蛋炒',
|
||
'创建时间 2026-07-2418:54:58', '账单分类', '食品酒饮',
|
||
];
|
||
|
||
it('京东:带负号金额 -12.30,方向 expense', () => {
|
||
const event = parseAccessibilityTexts(JD_TEXTS, 'com.jingdong.app.mall');
|
||
expect(event).not.toBeNull();
|
||
expect(event?.amount).toBe('12.30');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('京东:商户取金额前节点(京东外卖)', () => {
|
||
const event = parseAccessibilityTexts(JD_TEXTS, 'com.jingdong.app.mall');
|
||
expect(event?.counterparty).toBe('京东外卖');
|
||
});
|
||
|
||
it('京东:付款方式提取(白条)', () => {
|
||
const event = parseAccessibilityTexts(JD_TEXTS, 'com.jingdong.app.mall');
|
||
expect(event?.raw['收/付款方式']).toBe('白条');
|
||
});
|
||
|
||
it('京东:时间提取(含日期时间粘连修复)', () => {
|
||
const event = parseAccessibilityTexts(JD_TEXTS, 'com.jingdong.app.mall');
|
||
expect(event?.occurredAt).toContain('2026-07-24');
|
||
expect(event?.occurredAt).toContain('18:54');
|
||
});
|
||
|
||
it('京东非账单页返回 null', () => {
|
||
expect(parseAccessibilityTexts(['返回', '我的京东', '待收货'], 'com.jingdong.app.mall')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('parseDouyinTexts(抖音月付还款页)', () => {
|
||
// 真机日志:标题即商户,金额纯数字
|
||
const DOUYIN_TEXTS = [
|
||
'加防', '抖音月付还款', '5.88', '支付成功', '还款时间', '2026-06-1320:18:44',
|
||
'支付方式', '支支付宝', '点击展开', '交易订单', '主动还款-抖音月付2026年6月账单',
|
||
];
|
||
|
||
it('抖音:商户固定为"抖音月付还款"', () => {
|
||
const event = parseAccessibilityTexts(DOUYIN_TEXTS, 'com.ss.android.ugc.aweme');
|
||
expect(event).not.toBeNull();
|
||
expect(event?.counterparty).toBe('抖音月付还款');
|
||
});
|
||
|
||
it('抖音:金额 5.88,方向 expense(还款是支出)', () => {
|
||
const event = parseAccessibilityTexts(DOUYIN_TEXTS, 'com.ss.android.ugc.aweme');
|
||
expect(event?.amount).toBe('5.88');
|
||
expect(event?.direction).toBe('expense');
|
||
});
|
||
|
||
it('抖音:还款时间提取(日期时间粘连 2026-06-1320:18:44)', () => {
|
||
const event = parseAccessibilityTexts(DOUYIN_TEXTS, 'com.ss.android.ugc.aweme');
|
||
expect(event?.occurredAt).toBe('2026-06-13 20:18');
|
||
});
|
||
|
||
it('抖音:付款方式提取(支支付宝)', () => {
|
||
const event = parseAccessibilityTexts(DOUYIN_TEXTS, 'com.ss.android.ugc.aweme');
|
||
expect(event?.raw['收/付款方式']).toBe('支支付宝');
|
||
});
|
||
|
||
it('抖音非还款页返回 null', () => {
|
||
expect(parseAccessibilityTexts(['返回', '首页', '推荐'], 'com.ss.android.ugc.aweme')).toBeNull();
|
||
});
|
||
});
|