From 3aa940fdbe97e4abfc1b0e637ced6cc464b66d05 Mon Sep 17 00:00:00 2001 From: palmoni5 Date: Tue, 10 Nov 2026 12:32:39 +0200 Subject: [PATCH] fix(legacy): keep the event bridge compiling when nativeapi adds events nativeapi's WindowEvent is sealed and the dependency is ^0.5.5, so a 0.5.x release that adds a WindowEvent subclass would make the exhaustive switch in the legacy bridge a compile error for every app using it. Events without a 0.5.x counterpart (created, closed, property and occlusion changes) now go through a default branch instead of an explicit list. The branch stays reachable, so the analyzer does not flag it, and a future event is ignored instead of breaking the build. Adds a @visibleForTesting debugBindWindow so the bridge can be driven by a simulated nativeapi.Window in tests. --- lib/src/legacy/window_manager.dart | 20 +++++-- test/legacy_event_compat_test.dart | 86 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 test/legacy_event_compat_test.dart diff --git a/lib/src/legacy/window_manager.dart b/lib/src/legacy/window_manager.dart index 73396ab9..5de91946 100644 --- a/lib/src/legacy/window_manager.dart +++ b/lib/src/legacy/window_manager.dart @@ -4,7 +4,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' show Brightness, Color, Offset, PlatformDispatcher, Rect, Size; -import 'package:flutter/foundation.dart' show ObserverList, VoidCallback; +import 'package:flutter/foundation.dart' + show ObserverList, VoidCallback, visibleForTesting; import 'package:flutter/painting.dart' show Alignment; import 'package:nativeapi/nativeapi.dart' as nativeapi; import 'package:nativeapi_flutter/nativeapi_flutter.dart' @@ -66,6 +67,15 @@ class WindowManager { nativeapi.Window? _window; nativeapi.ListenerId? _nativeListenerId; + /// Addresses [window] instead of the platform's current window, or goes + /// back to the current window with null. For tests of this bridge only. + @visibleForTesting + void debugBindWindow(nativeapi.Window? window) { + _unwireNativeEvents(); + _window = window; + if (_listeners.isNotEmpty || _isPreventClose) _wireNativeEvents(); + } + bool _isPreventClose = false; bool _isDestroying = false; bool _isMaximized = false; @@ -791,10 +801,10 @@ class WindowManager { case nativeapi.WindowMovedEvent(): _emit(kWindowEventMove); _emit(kWindowEventMoved); - case nativeapi.WindowCreatedEvent(): - case nativeapi.WindowClosedEvent(): - case nativeapi.WindowPropertyChangedEvent(): - case nativeapi.WindowOcclusionChangedEvent(): + // Created, closed, property and occlusion changes have no 0.5.x + // counterpart. A default rather than a list keeps this switch compiling + // when a nativeapi 0.5.x release adds a WindowEvent subclass. + default: break; } } diff --git a/test/legacy_event_compat_test.dart b/test/legacy_event_compat_test.dart new file mode 100644 index 00000000..06be711e --- /dev/null +++ b/test/legacy_event_compat_test.dart @@ -0,0 +1,86 @@ +// How package:window_manager/legacy.dart treats nativeapi window events that +// 0.5.x had no counterpart for. +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nativeapi/nativeapi.dart' as nativeapi; +import 'package:window_manager/legacy.dart'; + +class _Recorder with WindowListener { + final List events = []; + + @override + void onWindowEvent(String eventName) => events.add(eventName); +} + +// A nativeapi window simulated in Dart; no native window is created. +class _FakeNativeWindow extends nativeapi.Window { + _FakeNativeWindow() : super.borrowed(0); + + FutureOr Function(nativeapi.WindowEvent)? _callback; + + @override + bool get isMinimized => false; + + @override + bool get isMaximized => false; + + @override + bool get isFullScreen => false; + + @override + nativeapi.ListenerId addListener( + FutureOr Function(nativeapi.WindowEvent) listener, + ) { + _callback = listener; + return 1; + } + + @override + bool removeListener(nativeapi.ListenerId id) { + _callback = null; + return true; + } + + Future fire(nativeapi.WindowEvent event) async { + await _callback?.call(event); + } +} + +void main() { + late _FakeNativeWindow fake; + late _Recorder listener; + + setUp(() { + fake = _FakeNativeWindow(); + listener = _Recorder(); + windowManager.debugBindWindow(fake); + windowManager.addListener(listener); + }); + + tearDown(() { + windowManager.removeListener(listener); + windowManager.debugBindWindow(null); + }); + + test('events without a 0.5.x counterpart are dropped', () async { + await fake.fire(const nativeapi.WindowCreatedEvent(windowId: 1)); + await fake.fire(const nativeapi.WindowClosedEvent(windowId: 1)); + await fake.fire( + const nativeapi.WindowOcclusionChangedEvent( + windowId: 1, + occlusionState: nativeapi.WindowOcclusionState.occluded, + ), + ); + expect(listener.events, isEmpty); + }); + + test('events with a 0.5.x counterpart still arrive', () async { + await fake.fire(const nativeapi.WindowFocusedEvent(windowId: 1)); + await fake.fire(const nativeapi.WindowCreatedEvent(windowId: 1)); + await fake.fire(const nativeapi.WindowBlurredEvent(windowId: 1)); + expect(listener.events, [kWindowEventFocus, kWindowEventBlur]); + }); +}