notifications + window: add Android native plugins
`ux/notifications/events` and `ux/window/events` only had macOS stream handlers, so on Android/iOS the unconditional Dart subscription threw MissingPluginException at startup (EventChannel reports activation failures straight to FlutterError.onError, bypassing the `onError:` callback). - Gate each Dart event-channel subscription to platforms that register a native handler (`defaultTargetPlatform`), silencing iOS. - `WindowPlugin`: report app foreground/background as host focus via `ProcessLifecycleOwner` ON_START/ON_STOP, so a backgrounded-but-alive process reports `focused = false`. - `NotificationsPlugin`: local notifications (show/cancel by thread/all), POST_NOTIFICATIONS request on 13+, and tap routing back over the event channel — a tap that cold-starts the process is buffered until Dart subscribes. - Regression tests for the subscription gate plus contract tests for the method/event payloads.
This commit is contained in:
123
test/notifications_test.dart
Normal file
123
test/notifications_test.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ux/ux.dart';
|
||||
|
||||
void main() {
|
||||
final messenger =
|
||||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger;
|
||||
const methods = MethodChannel('ux/notifications');
|
||||
const events = MethodChannel('ux/notifications/events');
|
||||
|
||||
final calls = <MethodCall>[];
|
||||
|
||||
setUp(() {
|
||||
calls.clear();
|
||||
messenger.setMockMethodCallHandler(methods, (call) async {
|
||||
calls.add(call);
|
||||
return call.method == 'requestPermission' ? true : null;
|
||||
});
|
||||
messenger.setMockMethodCallHandler(events, (_) async => null);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
messenger.setMockMethodCallHandler(methods, null);
|
||||
messenger.setMockMethodCallHandler(events, null);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
Future<void> sendEvent(Object? event) => messenger.handlePlatformMessage(
|
||||
'ux/notifications/events',
|
||||
const StandardMethodCodec().encodeSuccessEnvelope(event),
|
||||
(_) {},
|
||||
);
|
||||
|
||||
// Regression: iOS has no native handler, so activating the broadcast
|
||||
// stream throws MissingPluginException straight to FlutterError.onError.
|
||||
test('iOS does not activate the events channel', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
final eventCalls = <String>[];
|
||||
messenger.setMockMethodCallHandler(events, (call) async {
|
||||
eventCalls.add(call.method);
|
||||
return null;
|
||||
});
|
||||
MethodChannelXNotifications();
|
||||
await pumpEventQueue();
|
||||
expect(eventCalls, isEmpty);
|
||||
});
|
||||
|
||||
test('Android activates the events channel', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final eventCalls = <String>[];
|
||||
messenger.setMockMethodCallHandler(events, (call) async {
|
||||
eventCalls.add(call.method);
|
||||
return null;
|
||||
});
|
||||
MethodChannelXNotifications();
|
||||
await pumpEventQueue();
|
||||
expect(eventCalls, contains('listen'));
|
||||
});
|
||||
|
||||
test('show forwards the exact argument map', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final n = MethodChannelXNotifications();
|
||||
await n.show(XNotification(
|
||||
id: 'd1:7',
|
||||
title: 'Alice',
|
||||
body: 'hi',
|
||||
threadId: 'd1',
|
||||
data: const {'dialogId': 'd1', 'messageId': '7'},
|
||||
));
|
||||
final show = calls.firstWhere((c) => c.method == 'show');
|
||||
expect(show.arguments, {
|
||||
'id': 'd1:7',
|
||||
'title': 'Alice',
|
||||
'body': 'hi',
|
||||
'threadId': 'd1',
|
||||
'data': {'dialogId': 'd1', 'messageId': '7'},
|
||||
});
|
||||
});
|
||||
|
||||
test('requestPermission invokes the method and updates authorized', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final n = MethodChannelXNotifications();
|
||||
expect(await n.requestPermission(), isTrue);
|
||||
expect(calls.any((c) => c.method == 'requestPermission'), isTrue);
|
||||
expect(n.authorized.value, isTrue);
|
||||
});
|
||||
|
||||
test('cancelByThread and cancelAll invoke their methods', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final n = MethodChannelXNotifications();
|
||||
await n.cancelByThread('d1');
|
||||
await n.cancelAll();
|
||||
expect(
|
||||
calls.any((c) =>
|
||||
c.method == 'cancelByThread' &&
|
||||
(c.arguments as Map)['threadId'] == 'd1'),
|
||||
isTrue,
|
||||
);
|
||||
expect(calls.any((c) => c.method == 'cancelAll'), isTrue);
|
||||
});
|
||||
|
||||
test('authorization and tap events reach the Dart side', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final n = MethodChannelXNotifications();
|
||||
await pumpEventQueue();
|
||||
final taps = <Map<String, String>>[];
|
||||
n.onTap.listen(taps.add);
|
||||
|
||||
await sendEvent({'type': 'authorization', 'granted': true});
|
||||
await pumpEventQueue();
|
||||
expect(n.authorized.value, isTrue);
|
||||
|
||||
await sendEvent({
|
||||
'type': 'tap',
|
||||
'data': {'dialogId': 'd1', 'messageId': '7'},
|
||||
});
|
||||
await pumpEventQueue();
|
||||
expect(taps, [
|
||||
{'dialogId': 'd1', 'messageId': '7'},
|
||||
]);
|
||||
});
|
||||
}
|
||||
63
test/window_test.dart
Normal file
63
test/window_test.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ux/ux.dart';
|
||||
|
||||
void main() {
|
||||
final messenger =
|
||||
TestWidgetsFlutterBinding.ensureInitialized().defaultBinaryMessenger;
|
||||
const channel = MethodChannel('ux/window/events');
|
||||
|
||||
final listenCalls = <String>[];
|
||||
|
||||
setUp(() {
|
||||
listenCalls.clear();
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
listenCalls.add(call.method);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
messenger.setMockMethodCallHandler(channel, null);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
Future<void> sendEvent(Object? event) => messenger.handlePlatformMessage(
|
||||
'ux/window/events',
|
||||
const StandardMethodCodec().encodeSuccessEnvelope(event),
|
||||
(_) {},
|
||||
);
|
||||
|
||||
// Regression: iOS has no native handler, so activating the broadcast
|
||||
// stream throws MissingPluginException straight to FlutterError.onError.
|
||||
test('iOS does not activate the events channel', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
final w = MethodChannelXWindow();
|
||||
await pumpEventQueue();
|
||||
expect(listenCalls, isEmpty);
|
||||
expect(w.focused.value, isTrue);
|
||||
});
|
||||
|
||||
test('Android activates the events channel', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
MethodChannelXWindow();
|
||||
await pumpEventQueue();
|
||||
expect(listenCalls, contains('listen'));
|
||||
});
|
||||
|
||||
test('a focus event flips focused', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final w = MethodChannelXWindow();
|
||||
await pumpEventQueue();
|
||||
expect(w.focused.value, isTrue);
|
||||
|
||||
await sendEvent({'type': 'focus', 'focused': false});
|
||||
await pumpEventQueue();
|
||||
expect(w.focused.value, isFalse);
|
||||
|
||||
await sendEvent({'type': 'focus', 'focused': true});
|
||||
await pumpEventQueue();
|
||||
expect(w.focused.value, isTrue);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user