DriftLedger/tests/accessibilityParser.test.ts
fengmengqi 6767dd538a feat: 领域/组件/服务三层目录重构 + 微信无障碍绕过方案 + 新 UI 组件体系 + 账本增删改增强
### 架构重构:三层分层目录化
  - 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及文本模型测试 / 账单元识别及账户分类设计 / 账户分类模型测试
2026-07-28 20:57:37 +08:00

433 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 无障碍文本解析器测试。
*
* 重点验证:
* - 银行详情页(兴业/建行/招行等)通用解析器,按页面特征词分派
* - 金额格式兼容性:¥ 前缀、千分位逗号、符号前置/后置负号
* - 不破坏支付宝/微信现有解析行为
*
* 测试样本取自真实无障碍日志(兴业/建行/支付宝详情页节点文本)。
*/
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();
});
});