M1: 桥引擎单测代码(T-M1-08,运行待工具链)

- entry/src/test/Bridge.test.ets:13 用例覆盖
  MessageCodec(toJson省略/response/escape转义/toArray)、
  HandlerRegistry(put/get/默认兜底不抛错)、
  BridgeController(入站分发+回执/出站callHandler/启动队列补发/
  responseId回执路由/未注册名默认兜底) —— 用 FakeWebController 纯逻辑、不依赖设备
- 接入本地单测入口 List.test.ets
- ⚠ 运行受阻:devecocli 无 test 子命令,本地单测 hvigor task 未常规暴露,
  待定运行方式(DevEco IDE / CI / 其他)后跑覆盖率(目标 ≥80%)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-25 09:06:55 +08:00
co-authored by Claude Opus 4.8
parent 3e2b1282ba
commit 5587df0562
2 changed files with 151 additions and 1 deletions
+148
View File
@@ -0,0 +1,148 @@
import { describe, it, expect } from '@ohos/hypium';
import {
BridgeController, FakeWebController, MessageCodec, Message, HandlerRegistry
} from 'feature_bridge';
/**
* 桥引擎单测(T-M1-08)。纯逻辑,用 FakeWebController 模拟 H5 收发,不依赖设备。
* 覆盖 MessageCodectoJson/escape/toArray)、HandlerRegistry(兜底)、
* BridgeController(入站分发/出站下发/回执路由/启动队列/默认兜底)。
*/
export default function bridgeTest() {
describe('MessageCodec', () => {
it('toJson_omits_undefined', 0, () => {
const m = new Message();
m.handlerName = 'h';
m.data = 'd';
const json = MessageCodec.toJson(m);
expect(json.includes('"handlerName":"h"')).assertTrue();
expect(json.includes('"data":"d"')).assertTrue();
expect(json.includes('callbackId')).assertFalse();
expect(json.includes('responseId')).assertFalse();
});
it('toJson_response', 0, () => {
const json = MessageCodec.toJson(Message.response('cb1', 'ok'));
expect(json.includes('"responseId":"cb1"')).assertTrue();
expect(json.includes('"responseData":"ok"')).assertTrue();
});
it('escape_plain_unchanged', 0, () => {
// 无双引号/反斜杠的纯文本不应被第一步改变(仅可能被第二步处理引号,这里无引号)
const s = MessageCodec.escape('1782349257911');
expect(s).assertEqual('1782349257911');
});
it('escape_quotes_backslash', 0, () => {
// JSON {"data":"a\"b"} → 结构引号转义、内部 \" 加倍
const mm = new Message();
mm.data = 'a"b';
const escaped = MessageCodec.escape(MessageCodec.toJson(mm));
// 结构引号被转义为 \"(前面非反斜杠)
expect(escaped.includes('\\"data\\"')).assertTrue();
});
it('toArray_parses_queue', 0, () => {
const list = MessageCodec.toArray('[{"handlerName":"echo","data":"hi","callbackId":"cb1"}]');
expect(list.length).assertEqual(1);
expect(list[0].handlerName).assertEqual('echo');
expect(list[0].data).assertEqual('hi');
expect(list[0].callbackId).assertEqual('cb1');
});
});
describe('HandlerRegistry', () => {
it('put_get_has', 0, () => {
const r = new HandlerRegistry();
r.put('a', (_d, cb) => cb('A'));
expect(r.has('a')).assertTrue();
expect(r.has('b')).assertFalse();
expect(r.size()).assertEqual(1);
});
it('default_handler_no_throw', 0, () => {
const r = new HandlerRegistry();
let resp = 'none';
// 默认兜底:未注册名不抛错,回空串
r.default()('x', (d) => { resp = d; });
expect(resp).assertEqual('');
});
});
describe('BridgeController', () => {
it('inbound_dispatch_and_callback', 0, () => {
const fake = new FakeWebController();
const bridge = new BridgeController(fake);
let got = '';
bridge.registerHandler('echo', (data, cb) => { got = data; cb('R:' + data); });
bridge.onPageEnd(); // 清空启动队列,进入直发模式
// 模拟 H5 触发取队列
const intercepted = bridge.onLoadIntercept('yy://__QUEUE__/');
expect(intercepted).assertTrue();
// FakeWebController 应收到 _fetchQueue 触发脚本
expect(fake.scripts.some((s) => s.includes('_fetchQueue'))).assertTrue();
// 模拟 H5 交回队列:一条 echo 调用
fake.clear();
bridge.onLoadIntercept('yy://return/_fetchQueue/' +
encodeURIComponent('[{"handlerName":"echo","data":"hi","callbackId":"cb1"}]'));
expect(got).assertEqual('hi');
// 回执经 dispatch 下发,脚本含 _handleMessageFromNative 与 responseId cb1
const last = fake.lastScript() ?? '';
expect(last.includes('_handleMessageFromNative')).assertTrue();
expect(last.includes('cb1')).assertTrue();
});
it('outbound_callHandler', 0, () => {
const fake = new FakeWebController();
const bridge = new BridgeController(fake);
bridge.onPageEnd();
bridge.callHandler('getlocationinfo', '{"city":"x"}');
const last = fake.lastScript() ?? '';
expect(last.includes('_handleMessageFromNative')).assertTrue();
expect(last.includes('getlocationinfo')).assertTrue();
});
it('startup_queue_flush_after_onPageEnd', 0, () => {
const fake = new FakeWebController();
const bridge = new BridgeController(fake);
// onPageEnd 之前 callHandler → 入启动队列,暂不下发
bridge.callHandler('appservice', '1');
expect(fake.scripts.length).assertEqual(0);
// onPageEnd → 补发
bridge.onPageEnd();
expect(fake.scripts.some((s) => s.includes('appservice'))).assertTrue();
});
it('response_route_by_responseId', 0, () => {
const fake = new FakeWebController();
const bridge = new BridgeController(fake);
bridge.onPageEnd();
let resp = '';
// 原生 callHandler 带回调 → 生成 callbackId
bridge.callHandler('getTime', '', (r) => { resp = r; });
// H5 回执:responseId 对应刚才的 callbackId(从下发脚本里取)
const sent = fake.lastScript() ?? '';
// 提取 callbackIdJAVA_CB_...
const m = sent.match(/JAVA_CB_[0-9]+_[0-9]+/);
expect(m !== null).assertTrue();
const cbId = m![0];
bridge.onLoadIntercept('yy://return/_fetchQueue/' +
encodeURIComponent('[{"responseId":"' + cbId + '","responseData":"1700"}]'));
expect(resp).assertEqual('1700');
});
it('default_handler_unregistered_no_throw', 0, () => {
const fake = new FakeWebController();
const bridge = new BridgeController(fake);
bridge.onPageEnd();
// 未注册的 handler 名 → 默认兜底,不抛错
bridge.onLoadIntercept('yy://return/_fetchQueue/' +
encodeURIComponent('[{"handlerName":"finsh","data":"","callbackId":"cb9"}]'));
// 兜底回空串回执
const last = fake.lastScript() ?? '';
expect(last.includes('cb9')).assertTrue();
});
});
}
+3 -1
View File
@@ -1,5 +1,7 @@
import localUnitTest from './LocalUnit.test';
import bridgeTest from './Bridge.test';
export default function testsuite() {
localUnitTest();
}
bridgeTest();
}