- BridgeController 加 active 状态 + setActive/isActive: - 非激活:onLoadIntercept 拦截 yy:// 但不分发(入站忽略)、dispatch 直接返回(出站抑制) - 保证双 Web 下"同时只有一个 Web 执行桥接口",且去激活端不会收到 native 推送 - 去激活顺序约定:先 callHandler(appservice,'2')(彼时仍激活),再 setActive(false) - 单测新增 inactive_gate_suppresses_outbound_and_inbound;scripts/test.sh 通过 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
226 lines
9.0 KiB
Plaintext
226 lines
9.0 KiB
Plaintext
import { describe, it, expect } from '@ohos/hypium';
|
||
import {
|
||
BridgeController, FakeWebController, MessageCodec, Message, HandlerRegistry
|
||
} from 'feature_bridge';
|
||
|
||
/**
|
||
* 桥引擎单测(T-M1-08)。纯逻辑,用 FakeWebController 模拟 H5 收发,不依赖设备。
|
||
* 覆盖 MessageCodec(toJson/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, () => {
|
||
// data='a"b' → toJson 得 {"data":"a\"b"}(内部引号被 JSON 转义为 \")
|
||
const mm = new Message();
|
||
mm.data = 'a"b';
|
||
const json = MessageCodec.toJson(mm);
|
||
// toJson 内部:a 与 b 之间是 反斜杠+引号
|
||
expect(json.includes('a\\"b')).assertTrue();
|
||
const escaped = MessageCodec.escape(json);
|
||
// 第二步:结构引号(前面非反斜杠)被转义为 \"
|
||
expect(escaped.includes('\\"data\\"')).assertTrue();
|
||
// 第一步(命门):内部已转义的 \" 的反斜杠被加倍 → a 与 b 之间出现 \\\"(反斜杠×2+引号)
|
||
expect(escaped.includes('a\\\\\\"b')).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('');
|
||
});
|
||
|
||
it('set_default_and_remove', 0, () => {
|
||
const r = new HandlerRegistry();
|
||
let v = '';
|
||
r.setDefault((d, cb) => cb('D:' + d));
|
||
r.default()('x', (s) => { v = s; });
|
||
expect(v).assertEqual('D:x');
|
||
r.put('a', (_d, cb) => cb('A'));
|
||
expect(r.size()).assertEqual(1);
|
||
r.remove('a');
|
||
expect(r.has('a')).assertFalse();
|
||
});
|
||
});
|
||
|
||
describe('FakeWebController', () => {
|
||
it('records_and_clears', 0, () => {
|
||
const f = new FakeWebController();
|
||
expect(f.lastScript() === undefined).assertTrue();
|
||
f.runJavaScript('s1');
|
||
f.loadUrl('u1');
|
||
expect(f.scripts.length).assertEqual(1);
|
||
expect(f.loadedUrls.length).assertEqual(1);
|
||
expect(f.lastScript()).assertEqual('s1');
|
||
f.dispose();
|
||
f.clear();
|
||
expect(f.scripts.length).assertEqual(0);
|
||
expect(f.lastScript() === undefined).assertTrue();
|
||
});
|
||
});
|
||
|
||
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('send_no_handlerName', 0, () => {
|
||
const fake = new FakeWebController();
|
||
const bridge = new BridgeController(fake);
|
||
bridge.onPageEnd();
|
||
bridge.send('hello');
|
||
const last = fake.lastScript() ?? '';
|
||
expect(last.includes('_handleMessageFromNative')).assertTrue();
|
||
expect(last.includes('hello')).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() ?? '';
|
||
// 提取 callbackId(JAVA_CB_...)
|
||
const m = sent.match(/JAVA_CB_[0-9]+_[0-9]+/);
|
||
expect(m !== null).assertTrue();
|
||
const cbId = m![0];
|
||
// 先触发 flushMessageQueue 注册 _fetchQueue 回调,回执经队列机制路由
|
||
bridge.onLoadIntercept('yy://__queue__/');
|
||
bridge.onLoadIntercept('yy://return/_fetchQueue/' +
|
||
encodeURIComponent('[{"responseId":"' + cbId + '","responseData":"1700"}]'));
|
||
expect(resp).assertEqual('1700');
|
||
});
|
||
|
||
it('dispose_clears_registry_and_callbacks', 0, () => {
|
||
const fake = new FakeWebController();
|
||
const bridge = new BridgeController(fake);
|
||
bridge.registerHandler('a', (_d, cb) => cb('A'));
|
||
expect(bridge.getRegistry().has('a')).assertTrue();
|
||
bridge.dispose();
|
||
// dispose 清空注册表(默认兜底保留)
|
||
expect(bridge.getRegistry().has('a')).assertFalse();
|
||
});
|
||
|
||
it('inactive_gate_suppresses_outbound_and_inbound', 0, () => {
|
||
const fake = new FakeWebController();
|
||
const bridge = new BridgeController(fake);
|
||
bridge.onPageEnd();
|
||
let got = '';
|
||
bridge.registerHandler('echo', (data, cb) => { got = data; cb('R'); });
|
||
// 去激活:出站抑制
|
||
bridge.setActive(false);
|
||
bridge.callHandler('appservice', '2');
|
||
expect(fake.scripts.length).assertEqual(0);
|
||
// 去激活:入站忽略——yy:// 仍拦截(返回 true)但不 flush
|
||
const intercepted = bridge.onLoadIntercept('yy://__queue__/');
|
||
expect(intercepted).assertTrue();
|
||
expect(fake.scripts.length).assertEqual(0);
|
||
// 重新激活:恢复出站
|
||
bridge.setActive(true);
|
||
bridge.callHandler('appservice', '1');
|
||
expect(fake.scripts.some((s) => s.includes('appservice'))).assertTrue();
|
||
});
|
||
|
||
it('default_handler_unregistered_no_throw', 0, () => {
|
||
const fake = new FakeWebController();
|
||
const bridge = new BridgeController(fake);
|
||
bridge.onPageEnd();
|
||
// 先触发 flushMessageQueue 注册 _fetchQueue 回调
|
||
bridge.onLoadIntercept('yy://__queue__/');
|
||
// 未注册的 handler 名 → 默认兜底,不抛错
|
||
bridge.onLoadIntercept('yy://return/_fetchQueue/' +
|
||
encodeURIComponent('[{"handlerName":"finsh","data":"","callbackId":"cb9"}]'));
|
||
// 兜底回空串回执
|
||
const last = fake.lastScript() ?? '';
|
||
expect(last.includes('cb9')).assertTrue();
|
||
});
|
||
});
|
||
}
|