import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:no_screenshot/constants.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot_method_channel.dart'; import 'package:no_screenshot/no_screenshot_platform_interface.dart'; import 'package:no_screenshot/screenshot_snapshot.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late MethodChannelNoScreenshot platform; setUp(() { platform = MethodChannelNoScreenshot(); }); group('MethodChannelNoScreenshot', () { const MethodChannel channel = MethodChannel(screenshotMethodChannel); test('screenshotOn', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenShotOnConst) { return expected; } return null; }); final result = await platform.screenshotOn(); expect(result, expected); }); test('screenshotOff', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenShotOffConst) { return expected; } return null; }); final result = await platform.screenshotOff(); expect(result, expected); }); test('toggleScreenshot', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == toggleScreenShotConst) { return expected; } return null; }); final result = await platform.toggleScreenshot(); expect(result, expected); }); test('startScreenshotListening', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == startScreenshotListeningConst) { return null; } return null; }); await platform.startScreenshotListening(); expect(true, true); // Add more specific expectations if needed }); test('stopScreenshotListening', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == stopScreenshotListeningConst) { return null; } return null; }); await platform.stopScreenshotListening(); expect(true, true); // Add more specific expectations if needed }); test('toggleScreenshotWithImage', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenSetImage) { return expected; } return null; }); final result = await platform.toggleScreenshotWithImage(); expect(result, expected); }); test('toggleScreenshotWithBlur', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenSetBlur) { expect(methodCall.arguments, {'radius': 30.0}); return expected; } return null; }); final result = await platform.toggleScreenshotWithBlur(); expect(result, expected); }); test('toggleScreenshotWithBlur with custom radius', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenSetBlur) { expect(methodCall.arguments, {'radius': 50.0}); return expected; } return null; }); final result = await platform.toggleScreenshotWithBlur(blurRadius: 50.0); expect(result, expected); }); test( 'toggleScreenshotWithBlur returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.toggleScreenshotWithBlur(); expect(result, false); }, ); test('toggleScreenshotWithColor', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenSetColor) { expect(methodCall.arguments, {'color': 0xFF000000}); return expected; } return null; }); final result = await platform.toggleScreenshotWithColor(); expect(result, expected); }); test('toggleScreenshotWithColor with custom color', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenSetColor) { expect(methodCall.arguments, {'color': 0xFFFF0000}); return expected; } return null; }); final result = await platform.toggleScreenshotWithColor( color: 0xFFFF0000, ); expect(result, expected); }); test( 'toggleScreenshotWithColor returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.toggleScreenshotWithColor(); expect(result, false); }, ); test( 'toggleScreenshotWithImage returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.toggleScreenshotWithImage(); expect(result, false); }, ); test('screenshotOn returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.screenshotOn(); expect(result, false); }); test('screenshotOff returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.screenshotOff(); expect(result, false); }); test('toggleScreenshot returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.toggleScreenshot(); expect(result, false); }); test('screenshotWithImage', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenEnableImage) { return expected; } return null; }); final result = await platform.screenshotWithImage(); expect(result, expected); }); test( 'screenshotWithImage returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.screenshotWithImage(); expect(result, false); }, ); test('screenshotWithBlur', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenEnableBlur) { expect(methodCall.arguments, {'radius': 30.0}); return expected; } return null; }); final result = await platform.screenshotWithBlur(); expect(result, expected); }); test('screenshotWithBlur with custom radius', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenEnableBlur) { expect(methodCall.arguments, {'radius': 50.0}); return expected; } return null; }); final result = await platform.screenshotWithBlur(blurRadius: 50.0); expect(result, expected); }); test( 'screenshotWithBlur returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.screenshotWithBlur(); expect(result, false); }, ); test('screenshotWithColor', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenEnableColor) { expect(methodCall.arguments, {'color': 0xFF000000}); return expected; } return null; }); final result = await platform.screenshotWithColor(); expect(result, expected); }); test('screenshotWithColor with custom color', () async { const bool expected = true; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == screenEnableColor) { expect(methodCall.arguments, {'color': 0xFFFF0000}); return expected; } return null; }); final result = await platform.screenshotWithColor(color: 0xFFFF0000); expect(result, expected); }); test( 'screenshotWithColor returns false when channel returns null', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return null; }); final result = await platform.screenshotWithColor(); expect(result, false); }, ); test('startScreenRecordingListening', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == startScreenRecordingListeningConst) { return null; } return null; }); await platform.startScreenRecordingListening(); expect(true, true); }); test('stopScreenRecordingListening', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == stopScreenRecordingListeningConst) { return null; } return null; }); await platform.stopScreenRecordingListening(); expect(true, true); }); test( 'screenshotStream returns a stream that emits ScreenshotSnapshot', () async { final snapshotMap = { 'screenshot_path': '/test/path', 'is_screenshot_on': true, 'was_screenshot_taken': true, 'is_screen_recording': false, 'timestamp': 0, 'source_app': '', }; final encoded = jsonEncode(snapshotMap); // Mock the event channel by handling the underlying method channel TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockStreamHandler( platform.eventChannel, MockStreamHandler.inline( onListen: (arguments, events) { events.success(encoded); }, ), ); final stream = platform.screenshotStream; final snapshot = await stream.first; expect(snapshot.screenshotPath, '/test/path'); expect(snapshot.isScreenshotProtectionOn, true); expect(snapshot.wasScreenshotTaken, true); }, ); test('screenshotStream caches and returns the same stream instance', () { final stream1 = platform.screenshotStream; final stream2 = platform.screenshotStream; expect(identical(stream1, stream2), true); }); }); group('ScreenshotSnapshot', () { test('fromMap', () { final map = { 'screenshot_path': '/example/path', 'is_screenshot_on': true, 'was_screenshot_taken': true, }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.screenshotPath, '/example/path'); expect(snapshot.isScreenshotProtectionOn, true); expect(snapshot.wasScreenshotTaken, true); expect(snapshot.isScreenRecording, false); }); test('fromMap with is_screen_recording', () { final map = { 'screenshot_path': '/example/path', 'is_screenshot_on': true, 'was_screenshot_taken': false, 'is_screen_recording': true, }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.screenshotPath, '/example/path'); expect(snapshot.isScreenshotProtectionOn, true); expect(snapshot.wasScreenshotTaken, false); expect(snapshot.isScreenRecording, true); }); test('fromMap without is_screen_recording defaults to false', () { final map = { 'screenshot_path': '/example/path', 'is_screenshot_on': true, 'was_screenshot_taken': true, }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.isScreenRecording, false); }); test('toMap', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, isScreenRecording: true, ); final map = snapshot.toMap(); expect(map['screenshot_path'], '/example/path'); expect(map['is_screenshot_on'], true); expect(map['was_screenshot_taken'], true); expect(map['is_screen_recording'], true); }); test('toMap with default isScreenRecording', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final map = snapshot.toMap(); expect(map['is_screen_recording'], false); }); test('equality operator', () { final snapshot1 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final snapshot2 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final snapshot3 = ScreenshotSnapshot( screenshotPath: '/different/path', isScreenshotProtectionOn: false, wasScreenshotTaken: false, ); final snapshot4 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, isScreenRecording: true, ); expect(snapshot1 == snapshot2, true); expect(snapshot1 == snapshot3, false); expect(snapshot1 == snapshot4, false); }); test('hashCode', () { final snapshot1 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final snapshot2 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final snapshot3 = ScreenshotSnapshot( screenshotPath: '/different/path', isScreenshotProtectionOn: false, wasScreenshotTaken: false, ); expect(snapshot1.hashCode, snapshot2.hashCode); expect(snapshot1.hashCode, isNot(snapshot3.hashCode)); }); test('fromMap with empty map uses defaults', () { final snapshot = ScreenshotSnapshot.fromMap({}); expect(snapshot.screenshotPath, ''); expect(snapshot.isScreenshotProtectionOn, false); expect(snapshot.wasScreenshotTaken, false); expect(snapshot.isScreenRecording, false); expect(snapshot.timestamp, 0); expect(snapshot.sourceApp, ''); }); test('fromMap with null values uses defaults', () { final map = { 'screenshot_path': null, 'is_screenshot_on': null, 'was_screenshot_taken': null, 'is_screen_recording': null, 'timestamp': null, 'source_app': null, }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.screenshotPath, ''); expect(snapshot.isScreenshotProtectionOn, false); expect(snapshot.wasScreenshotTaken, false); expect(snapshot.isScreenRecording, false); expect(snapshot.timestamp, 0); expect(snapshot.sourceApp, ''); }); test('fromMap with metadata', () { final map = { 'screenshot_path': '/example/path', 'is_screenshot_on': true, 'was_screenshot_taken': true, 'is_screen_recording': false, 'timestamp': 1700000000000, 'source_app': 'screencaptureui', }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.screenshotPath, '/example/path'); expect(snapshot.isScreenshotProtectionOn, true); expect(snapshot.wasScreenshotTaken, true); expect(snapshot.timestamp, 1700000000000); expect(snapshot.sourceApp, 'screencaptureui'); }); test('fromMap without metadata defaults timestamp and sourceApp', () { final map = { 'screenshot_path': '/example/path', 'is_screenshot_on': true, 'was_screenshot_taken': true, }; final snapshot = ScreenshotSnapshot.fromMap(map); expect(snapshot.timestamp, 0); expect(snapshot.sourceApp, ''); }); test('toMap includes metadata', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000000, sourceApp: 'GNOME Screenshot', ); final map = snapshot.toMap(); expect(map['timestamp'], 1700000000000); expect(map['source_app'], 'GNOME Screenshot'); }); test('equality with metadata', () { final snapshot1 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000000, sourceApp: 'screencaptureui', ); final snapshot2 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000000, sourceApp: 'screencaptureui', ); final snapshot3 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000001, sourceApp: 'screencaptureui', ); final snapshot4 = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000000, sourceApp: 'different_app', ); expect(snapshot1 == snapshot2, true); expect(snapshot1 == snapshot3, false); expect(snapshot1 == snapshot4, false); }); test('toString', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ); final string = snapshot.toString(); expect( string, 'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: false, \ntimestamp: 0, \nsourceApp: \n)', ); }); test('toString with isScreenRecording true', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, isScreenRecording: true, ); final string = snapshot.toString(); expect( string, 'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: true, \ntimestamp: 0, \nsourceApp: \n)', ); }); test('toString with metadata', () { final snapshot = ScreenshotSnapshot( screenshotPath: '/example/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, timestamp: 1700000000000, sourceApp: 'screencaptureui', ); final string = snapshot.toString(); expect(string, contains('timestamp: 1700000000000')); expect(string, contains('sourceApp: screencaptureui')); }); }); group('Granular Callbacks (P15)', () { late StreamController controller; late _MockNoScreenshotPlatform mockPlatform; late NoScreenshot noScreenshot; setUp(() { controller = StreamController.broadcast(); mockPlatform = _MockNoScreenshotPlatform(controller.stream); NoScreenshotPlatform.instance = mockPlatform; // Create a fresh instance for each test to avoid shared state. noScreenshot = NoScreenshot.instance; noScreenshot.removeAllCallbacks(); }); tearDown(() { noScreenshot.removeAllCallbacks(); controller.close(); }); test( 'onScreenshotDetected fires when wasScreenshotTaken is true', () async { final detected = []; noScreenshot.onScreenshotDetected = detected.add; noScreenshot.startCallbacks(); controller.add( ScreenshotSnapshot( screenshotPath: '/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ), ); await Future.delayed(Duration.zero); expect(detected, hasLength(1)); expect(detected.first.wasScreenshotTaken, true); }, ); test( 'onScreenshotDetected does NOT fire when wasScreenshotTaken is false', () async { final detected = []; noScreenshot.onScreenshotDetected = detected.add; noScreenshot.startCallbacks(); controller.add( ScreenshotSnapshot( screenshotPath: '', isScreenshotProtectionOn: true, wasScreenshotTaken: false, ), ); await Future.delayed(Duration.zero); expect(detected, isEmpty); }, ); test('onScreenRecordingStarted fires on false→true transition', () async { final started = []; noScreenshot.onScreenRecordingStarted = started.add; noScreenshot.startCallbacks(); // Initial state: not recording → recording starts controller.add( ScreenshotSnapshot( screenshotPath: '', isScreenshotProtectionOn: true, wasScreenshotTaken: false, isScreenRecording: true, ), ); await Future.delayed(Duration.zero); expect(started, hasLength(1)); expect(started.first.isScreenRecording, true); }); test('onScreenRecordingStopped fires on true→false transition', () async { final stopped = []; noScreenshot.onScreenRecordingStopped = stopped.add; noScreenshot.startCallbacks(); // First: recording starts controller.add( ScreenshotSnapshot( screenshotPath: '', isScreenshotProtectionOn: true, wasScreenshotTaken: false, isScreenRecording: true, ), ); await Future.delayed(Duration.zero); // Then: recording stops controller.add( ScreenshotSnapshot( screenshotPath: '', isScreenshotProtectionOn: true, wasScreenshotTaken: false, isScreenRecording: false, ), ); await Future.delayed(Duration.zero); expect(stopped, hasLength(1)); expect(stopped.first.isScreenRecording, false); }); test( 'removeAllCallbacks clears all callbacks and stops subscription', () async { final detected = []; noScreenshot.onScreenshotDetected = detected.add; noScreenshot.startCallbacks(); expect(noScreenshot.hasActiveCallbacks, true); noScreenshot.removeAllCallbacks(); expect(noScreenshot.hasActiveCallbacks, false); expect(noScreenshot.onScreenshotDetected, isNull); expect(noScreenshot.onScreenRecordingStarted, isNull); expect(noScreenshot.onScreenRecordingStopped, isNull); // Events after removal should not fire controller.add( ScreenshotSnapshot( screenshotPath: '/path', isScreenshotProtectionOn: true, wasScreenshotTaken: true, ), ); await Future.delayed(Duration.zero); expect(detected, isEmpty); }, ); test('hasActiveCallbacks reflects subscription state', () { expect(noScreenshot.hasActiveCallbacks, false); noScreenshot.onScreenshotDetected = (_) {}; noScreenshot.startCallbacks(); expect(noScreenshot.hasActiveCallbacks, true); noScreenshot.stopCallbacks(); expect(noScreenshot.hasActiveCallbacks, false); }); test('startCallbacks is idempotent', () { noScreenshot.onScreenshotDetected = (_) {}; noScreenshot.startCallbacks(); noScreenshot.startCallbacks(); // second call should be no-op expect(noScreenshot.hasActiveCallbacks, true); }); }); } class _MockNoScreenshotPlatform extends NoScreenshotPlatform { final Stream _stream; _MockNoScreenshotPlatform(this._stream); @override Stream get screenshotStream => _stream; @override Future screenshotOff() async => true; @override Future screenshotOn() async => true; @override Future toggleScreenshot() async => true; @override Future toggleScreenshotWithImage() async => true; @override Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async => true; @override Future toggleScreenshotWithColor({int color = 0xFF000000}) async => true; @override Future screenshotWithImage() async => true; @override Future screenshotWithBlur({double blurRadius = 30.0}) async => true; @override Future screenshotWithColor({int color = 0xFF000000}) async => true; @override Future startScreenshotListening() async {} @override Future stopScreenshotListening() async {} @override Future startScreenRecordingListening() async {} @override Future stopScreenRecordingListening() async {} }