Files
flutter_lua_runtime/test/runtime/events/runtime_event_dispatcher_test.dart
2026-06-09 16:09:19 +08:00

175 lines
5.2 KiB
Dart

import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
import 'package:flame_lua_runtime/runtime/events/runtime_event_dispatcher.dart';
import 'package:flame_lua_runtime/runtime/lifecycle/runtime_session.dart';
import 'package:flame_lua_runtime/runtime/models/game_diff.dart';
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
import 'package:flame_lua_runtime/runtime/scripting/runtime_script_services.dart';
import 'package:flame_lua_runtime/runtime/scripting/script_engine.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RuntimeEventDispatcher', () {
test('dispatches queued events serially', () async {
final session = _activeSession();
final script = _FakeScriptEngine();
final applied = <GameDiff>[];
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => true,
applyDiff: applied.add,
);
dispatcher
..enqueue(const RuntimeEvent(type: 'tap', target: 'a'))
..enqueue(const RuntimeEvent(type: 'tap', target: 'b'));
await Future<void>.delayed(Duration.zero);
expect(script.events.map((event) => event.target), ['a', 'b']);
expect(applied, hasLength(2));
});
test('drops events for removed scope', () async {
final session = _activeSession();
final script = _FakeScriptEngine();
var alive = true;
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => alive,
applyDiff: (_) {},
);
dispatcher.enqueue(const RuntimeEvent(type: 'tap', scope: 'dialog'));
alive = false;
await Future<void>.delayed(Duration.zero);
expect(script.events, isEmpty);
});
test('drops events with stale target epoch', () async {
final session = _activeSession();
final script = _FakeScriptEngine();
var currentEpoch = 2;
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => true,
isNodeEpochAlive: (_, epoch) => epoch == currentEpoch,
applyDiff: (_) {},
);
dispatcher.enqueue(
const RuntimeEvent(type: 'tap', target: 'button', targetEpoch: 1),
);
currentEpoch = 3;
await Future<void>.delayed(Duration.zero);
expect(script.events, isEmpty);
});
test('drops queued events after dispose', () async {
final session = _activeSession();
final script = _FakeScriptEngine();
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => true,
applyDiff: (_) {},
);
dispatcher.enqueue(const RuntimeEvent(type: 'tap'));
dispatcher.dispose();
await Future<void>.delayed(Duration.zero);
expect(script.events, isEmpty);
});
test('drops queued events after session dispose', () async {
final session = _activeSession();
final script = _FakeScriptEngine();
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => true,
applyDiff: (_) {},
);
dispatcher.enqueue(const RuntimeEvent(type: 'tap'));
session.dispose();
await Future<void>.delayed(Duration.zero);
expect(script.events, isEmpty);
});
test('continues draining after a script error', () async {
final session = _activeSession();
final script = _FakeScriptEngine()..failNext = true;
final errors = <Object>[];
final diagnostics = RuntimeDiagnostics();
final dispatcher = RuntimeEventDispatcher(
session: session,
scriptEngine: script,
isScopeAlive: (_) => true,
applyDiff: (_) {},
diagnostics: diagnostics,
onError: errors.add,
);
dispatcher
..enqueue(const RuntimeEvent(type: 'tap', target: 'bad'))
..enqueue(const RuntimeEvent(type: 'tap', target: 'good'));
await Future<void>.delayed(Duration.zero);
expect(errors, hasLength(1));
expect(diagnostics.entries, hasLength(1));
expect(
diagnostics.entries.single.type,
RuntimeDiagnosticType.luaEventError,
);
expect(diagnostics.entries.single.context['target'], 'bad');
expect(script.events.map((event) => event.target), ['bad', 'good']);
});
});
}
RuntimeSession _activeSession() {
final session = RuntimeSession(gameId: 'test')..beginLoading();
session.activate();
return session;
}
class _FakeScriptEngine implements ScriptEngine {
final events = <RuntimeEvent>[];
bool failNext = false;
@override
Future<void> loadPackage(
GamePackage package, {
RuntimeScriptServices services = const RuntimeScriptServices(),
}) async {}
@override
bool smokeTest(Map<String, Object?> context) => true;
@override
GameDiff init(Map<String, Object?> context) => GameDiff.empty;
@override
GameDiff dispatchEvent(RuntimeEvent event) {
events.add(event);
if (failNext) {
failNext = false;
throw StateError('boom');
}
return GameDiff.empty;
}
}