外部契约逐字不变(与原生 app 零改动对接): - handler 名白名单 14 个字符串(getVideoinfo/sharelogin/...) - window.WVJBCallbacks / WebViewJavascriptBridgeReady 事件名 - window.settings.getothername API - 回调签名 (data, responseCallback) 与数据结构 内部命名走现代专业风格: - WVJB → NativeBridgeChannel(双向通信通道语义清晰) - WVJBHandler → NativeHandler - WVJBCallback → NativeResponseCallback - WVJB_HANDLER_NAMES → NATIVE_HANDLER_NAMES - WVJBHandlerName → NativeHandlerName - createNativeBridge → createBridge - getSetting → getNativeSetting(明确是原生注入) - NativeBridge interface → Bridge(隐含 Native) - isKnownHandler → isNativeHandlerName - __nbBridge → __activeChannel - __nbHandlers → __registeredHandlers 144/144 tests pass, typecheck exit 0。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createBridge,
|
|
getNativeSetting,
|
|
type NativeHandler,
|
|
type NativeResponseCallback,
|
|
type NativeBridgeChannel,
|
|
} from '../../YouleNexus/assets/framework/platform/native-bridge.ts';
|
|
|
|
/**
|
|
* Mock 浏览器侧 globals。Node 单测无 window,需 stub。
|
|
*/
|
|
function makeEnv() {
|
|
const calls: { handler: string; data: unknown }[] = [];
|
|
const responses: Map<string, (data: any) => void> = new Map();
|
|
const handlers: Map<string, NativeHandler> = new Map();
|
|
|
|
const channel: NativeBridgeChannel = {
|
|
registerHandler(name, h) { handlers.set(name, h); },
|
|
callHandler(name, data, cb) {
|
|
calls.push({ handler: name, data });
|
|
if (cb) responses.set(name, cb);
|
|
},
|
|
};
|
|
|
|
const g = globalThis as any;
|
|
const oldWVJB = g.WVJBCallbacks;
|
|
g.WVJBCallbacks = [];
|
|
// mock 原生侧 WebView bridge 就绪:遍历 WVJBCallbacks 全调用
|
|
const triggerReady = () => {
|
|
for (const cb of [...g.WVJBCallbacks]) cb(channel);
|
|
};
|
|
|
|
return {
|
|
channel,
|
|
restore: () => { g.WVJBCallbacks = oldWVJB; },
|
|
calls,
|
|
handlers,
|
|
responses,
|
|
triggerReady,
|
|
};
|
|
}
|
|
|
|
test('createBridge: 通道就绪时 onReady 触发', () => {
|
|
const env = makeEnv();
|
|
let got: any = null;
|
|
const bridge = createBridge({
|
|
onReady(c) { got = c; },
|
|
});
|
|
env.triggerReady();
|
|
assert.equal(got, env.channel, 'onReady 应收到 mock channel');
|
|
env.restore();
|
|
});
|
|
|
|
test('registerHandler: H5 注册供原生调用', () => {
|
|
const env = makeEnv();
|
|
let received: any = null;
|
|
const bridge = createBridge({
|
|
onReady(c) {
|
|
(globalThis as any).__activeChannel = c;
|
|
bridge.registerHandler('getVideoinfo', (data) => {
|
|
received = data;
|
|
return { ok: true };
|
|
});
|
|
},
|
|
});
|
|
env.triggerReady();
|
|
// 原生侧调用
|
|
const handler = env.handlers.get('getVideoinfo');
|
|
assert.ok(handler, 'handler 应已注册');
|
|
handler!({ vid: 'abc' }, (() => {}) as NativeResponseCallback);
|
|
assert.deepEqual(received, { vid: 'abc' });
|
|
env.restore();
|
|
});
|
|
|
|
test('callHandler: H5 调用原生,响应通过 cb 接收', () => {
|
|
const env = makeEnv();
|
|
const bridge = createBridge({
|
|
onReady(c) { (globalThis as any).__activeChannel = c; },
|
|
});
|
|
env.triggerReady();
|
|
let resp: unknown = null;
|
|
bridge.callHandler('getBattery', {}, (r) => { resp = r; });
|
|
// 模拟原生侧响应
|
|
const cb = env.responses.get('getBattery');
|
|
assert.ok(cb, 'callHandler 应记录 cb');
|
|
cb!({ level: 80, charging: false });
|
|
assert.deepEqual(resp, { level: 80, charging: false });
|
|
env.restore();
|
|
});
|
|
|
|
test('callHandler: handler 名不在白名单 → 显式抛错(第二准则)', () => {
|
|
const env = makeEnv();
|
|
const bridge = createBridge({ onReady: () => {} });
|
|
env.triggerReady();
|
|
assert.throws(
|
|
() => bridge.callHandler('unknown_handler' as any, {}, () => {}),
|
|
/unknown_handler/,
|
|
);
|
|
env.restore();
|
|
});
|
|
|
|
test('registerHandler: handler 名不在白名单 → 显式抛错', () => {
|
|
const env = makeEnv();
|
|
const bridge = createBridge({ onReady: () => {} });
|
|
env.triggerReady();
|
|
assert.throws(
|
|
() => bridge.registerHandler('unknown_handler' as any, () => undefined),
|
|
/unknown_handler/,
|
|
);
|
|
env.restore();
|
|
});
|
|
|
|
test('bridge 未就绪时 callHandler/registerHandler 显式抛错', () => {
|
|
const g = globalThis as any;
|
|
const old = g.WVJBCallbacks;
|
|
g.WVJBCallbacks = []; // 空队列,triggerReady 不被调用
|
|
const bridge = createBridge({ onReady: () => {} });
|
|
assert.throws(() => bridge.callHandler('getBattery', {}, () => {}), /not ready/i);
|
|
assert.throws(() => bridge.registerHandler('getVideoinfo', (() => undefined) as any), /not ready/i);
|
|
g.WVJBCallbacks = old;
|
|
});
|
|
|
|
test('重复 createBridge: 多个 instance 各自注册独立回调', () => {
|
|
const g = globalThis as any;
|
|
const old = g.WVJBCallbacks;
|
|
g.WVJBCallbacks = [];
|
|
createBridge({ onReady: () => {} });
|
|
createBridge({ onReady: () => {} });
|
|
assert.equal(g.WVJBCallbacks.length, 2, '每个 instance 独立注册一个 cb');
|
|
g.WVJBCallbacks = old;
|
|
});
|
|
|
|
test('getNativeSetting: 同步取 window.settings.getothername', () => {
|
|
const g = globalThis as any;
|
|
const oldWindow = g.window;
|
|
g.window = { settings: { getothername: (n: string) => n === 'agentid' ? 'A' : '' } };
|
|
assert.equal(getNativeSetting('agentid'), 'A');
|
|
assert.equal(getNativeSetting('missing'), '');
|
|
g.window = oldWindow;
|
|
});
|
|
|
|
test('getNativeSetting: window.settings 未注入 → 显式抛错(不静默兜底)', () => {
|
|
const g = globalThis as any;
|
|
const oldWindow = g.window;
|
|
g.window = {}; // 无 settings
|
|
assert.throws(() => getNativeSetting('agentid'), /window\.settings 未注入/);
|
|
g.window = oldWindow;
|
|
}); |