Add bidirectional host bridge
This commit is contained in:
@@ -11,7 +11,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('FlameLuaGame diagnostics debug access', () {
|
||||
test('exposes diagnostics entries, dump text and debug json', () {
|
||||
test('exposes diagnostics entries, dump text and debug json', () async {
|
||||
final diagnostics = RuntimeDiagnostics()
|
||||
..record(
|
||||
type: RuntimeDiagnosticType.commandError,
|
||||
@@ -29,6 +29,8 @@ void main() {
|
||||
expect(game.diagnosticsDumpText(), contains('command failed'));
|
||||
expect(game.diagnosticsDebugJson()['count'], 1);
|
||||
expect(game.resourcesDebugJson(), {'initialized': false});
|
||||
expect(game.notifyLua('host.ready'), isFalse);
|
||||
await expectLater(game.callLua('host.ready'), throwsA(isA<StateError>()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
93
test/runtime/host/runtime_host_bridge_test.dart
Normal file
93
test/runtime/host/runtime_host_bridge_test.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||
import 'package:flame_lua_runtime/runtime/host/runtime_host_bridge.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('RuntimeHostBridgeManager', () {
|
||||
test('calls registered host handler and emits result event', () async {
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeHostBridgeManager(
|
||||
bridge: RuntimeHostBridge(
|
||||
handlers: {
|
||||
'user.profile': (call) => {'name': 'Lua', 'id': call.data},
|
||||
},
|
||||
),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
await manager.callHost(
|
||||
const RuntimeHostCall(id: 'call_1', method: 'user.profile', data: 7),
|
||||
);
|
||||
|
||||
expect(events.single.type, RuntimeHostEventType.callResult);
|
||||
expect(events.single.data['ok'], isTrue);
|
||||
expect(events.single.data['result'], {'name': 'Lua', 'id': 7});
|
||||
});
|
||||
|
||||
test(
|
||||
'emits failed result and diagnostics when host handler throws',
|
||||
() async {
|
||||
final diagnostics = RuntimeDiagnostics();
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeHostBridgeManager(
|
||||
bridge: RuntimeHostBridge(
|
||||
handlers: {'boom': (_) => throw StateError('boom')},
|
||||
),
|
||||
eventSink: events.add,
|
||||
diagnostics: diagnostics,
|
||||
);
|
||||
|
||||
await manager.callHost(
|
||||
const RuntimeHostCall(id: 'call_1', method: 'boom'),
|
||||
);
|
||||
|
||||
expect(events.single.data['ok'], isFalse);
|
||||
expect(events.single.data['error'], contains('boom'));
|
||||
expect(
|
||||
diagnostics.entries.single.type,
|
||||
RuntimeDiagnosticType.hostBridgeError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('notifies host and emits Lua calls', () async {
|
||||
RuntimeHostNotification? notification;
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeHostBridgeManager(
|
||||
bridge: RuntimeHostBridge(onNotify: (value) => notification = value),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
expect(
|
||||
manager.notifyHost(
|
||||
const RuntimeHostNotification(
|
||||
method: 'analytics',
|
||||
data: {'level': 2},
|
||||
),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(notification?.method, 'analytics');
|
||||
expect(manager.notifyLua('pause', data: {'reason': 'host'}), isTrue);
|
||||
|
||||
expect(events.single.type, RuntimeHostEventType.notify);
|
||||
expect(events.single.data['method'], 'pause');
|
||||
});
|
||||
|
||||
test('completes Flutter-to-Lua call through host response', () async {
|
||||
final events = <RuntimeEvent>[];
|
||||
final manager = RuntimeHostBridgeManager(
|
||||
bridge: const RuntimeHostBridge(),
|
||||
eventSink: events.add,
|
||||
);
|
||||
|
||||
final future = manager.callLua('select_avatar', data: {'current': 1});
|
||||
final id = events.single.data['id']! as String;
|
||||
expect(events.single.type, RuntimeHostEventType.call);
|
||||
expect(manager.completeLuaCall(id, result: {'selected': 3}), isTrue);
|
||||
|
||||
expect(await future, {'selected': 3});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/host/runtime_host_bridge.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
|
||||
import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
|
||||
@@ -1021,6 +1022,57 @@ function on_event(event) return {} end
|
||||
expect(network.closedWebSockets, ['chat']);
|
||||
});
|
||||
|
||||
test('exposes host bridge runtime API to Lua', () async {
|
||||
final package = await _createPackage(
|
||||
mainScript: '''
|
||||
function smoke_test(ctx) return true end
|
||||
function init(ctx)
|
||||
local id = runtime.host_call({
|
||||
id = "profile",
|
||||
method = "user.profile",
|
||||
data = { userId = 9 },
|
||||
})
|
||||
local notified = runtime.host_notify({
|
||||
method = "analytics",
|
||||
data = { event = "open" },
|
||||
})
|
||||
return {
|
||||
commands = {
|
||||
{ type = "toast", text = id .. ":" .. tostring(notified) },
|
||||
},
|
||||
}
|
||||
end
|
||||
function on_event(event)
|
||||
if event.type == "host_call" then
|
||||
runtime.host_respond({ id = event.data.id, result = { handled = event.data.method } })
|
||||
end
|
||||
return {}
|
||||
end
|
||||
''',
|
||||
);
|
||||
final hostBridge = _RecordingHostBridgeManager();
|
||||
final engine = LuaDardoScriptEngine();
|
||||
|
||||
await engine.loadPackage(
|
||||
package,
|
||||
services: RuntimeScriptServices(hostBridge: hostBridge),
|
||||
);
|
||||
final diff = engine.init({'runtimeApiVersion': 1});
|
||||
|
||||
expect(diff.commands.single.payload['text'], 'profile:true');
|
||||
expect(hostBridge.calls.single.method, 'user.profile');
|
||||
expect(hostBridge.calls.single.data, {'userId': 9});
|
||||
expect(hostBridge.notifications.single.method, 'analytics');
|
||||
expect(hostBridge.notifications.single.data, {'event': 'open'});
|
||||
|
||||
final callFuture = hostBridge.callLua('flutter.request', data: {'id': 2});
|
||||
final event = _RecordingHostBridgeManager.events.singleWhere(
|
||||
(item) => item.type == RuntimeHostEventType.call,
|
||||
);
|
||||
engine.dispatchEvent(event);
|
||||
expect(await callFuture, {'handled': 'flutter.request'});
|
||||
});
|
||||
|
||||
test('rejects undeclared module imports', () async {
|
||||
final package = await _createPackage(
|
||||
mainScript: '''
|
||||
@@ -1052,6 +1104,26 @@ function on_event(event) return {} end
|
||||
});
|
||||
}
|
||||
|
||||
class _RecordingHostBridgeManager extends RuntimeHostBridgeManager {
|
||||
_RecordingHostBridgeManager()
|
||||
: super(bridge: const RuntimeHostBridge(), eventSink: events.add);
|
||||
|
||||
static final events = <RuntimeEvent>[];
|
||||
final calls = <RuntimeHostCall>[];
|
||||
final notifications = <RuntimeHostNotification>[];
|
||||
|
||||
@override
|
||||
Future<void> callHost(RuntimeHostCall call) async {
|
||||
calls.add(call);
|
||||
}
|
||||
|
||||
@override
|
||||
bool notifyHost(RuntimeHostNotification notification) {
|
||||
notifications.add(notification);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingNetworkManager extends RuntimeNetworkManager {
|
||||
_RecordingNetworkManager()
|
||||
: super(
|
||||
|
||||
Reference in New Issue
Block a user