diff --git a/flutter/test/cursor_dpi_policy_test.dart b/flutter/test/cursor_dpi_policy_test.dart index 3a3cfe029..f3877a852 100644 --- a/flutter/test/cursor_dpi_policy_test.dart +++ b/flutter/test/cursor_dpi_policy_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/native/custom_cursor.dart' show deleteCustomCursor; +import 'package:flutter_hbb/native/custom_cursor.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:image/image.dart' as img; @@ -111,7 +111,7 @@ Future _data(int density, String id) async { image: img.Image(width: image.width, height: image.height, numChannels: 4), nativeImage: image, scale: 1, - data: null, + data: Uint8List.fromList([1, 2]), hotxOrigin: 4.0 * bitmapDensity, hotyOrigin: 9.0 * bitmapDensity, width: image.width, @@ -121,64 +121,107 @@ Future _data(int density, String id) async { } void main() { - for (final (style, zoom, density, viewScale, expectedScale) in [ - (kRemoteViewStyleAdaptive, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3), - (kRemoteViewStyleCustom, false, 0, 0.25, Platform.isWindows ? 1.0 : 4 / 3), - (kRemoteViewStyleAdaptive, false, 1, 0.25, Platform.isWindows ? 2.0 : 1.0), - (kRemoteViewStyleAdaptive, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5), - (kRemoteViewStyleCustom, false, 2, 0.25, Platform.isWindows ? 1.0 : 0.5), - (kRemoteViewStyleAdaptive, true, 2, 0.25, Platform.isWindows ? 2 / 3 : 1 / 3), - (kRemoteViewStyleCustom, true, 2, 0.25, Platform.isWindows ? 2 / 3 : 1 / 3), - (kRemoteViewStyleOriginal, false, 2, 0.5, Platform.isWindows ? 1.0 : 2 / 3), + final binding = TestWidgetsFlutterBinding.ensureInitialized(); + final view = binding.platformDispatcher.views.single; + final channel = Platform.isWindows + ? SystemChannels.mouseCursor + : const MethodChannel('flutter_custom_cursor'); + final windows = Platform.isWindows; + final registrations = >[]; + setUp(() { + registrations.clear(); + binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, + (call) async { + if (!call.method.startsWith('createCustomCursor')) return null; + final args = call.arguments as Map; + registrations.add(args); + return args['name']; + }); + }); + tearDown(() { + view.resetDevicePixelRatio(); + binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + for (final testCase in [ + (kRemoteViewStyleAdaptive, false, 0, 0.25, windows ? 1.0 : 4 / 3), + (kRemoteViewStyleCustom, false, 0, 0.25, windows ? 1.0 : 4 / 3), + (kRemoteViewStyleAdaptive, false, 1, 0.25, windows ? 2.0 : 1.0), + (kRemoteViewStyleAdaptive, false, 2, 0.25, windows ? 1.0 : 0.5), + (kRemoteViewStyleCustom, false, 2, 0.25, windows ? 1.0 : 0.5), + (kRemoteViewStyleAdaptive, true, 2, 0.25, windows ? 2 / 3 : 1 / 3), + (kRemoteViewStyleCustom, true, 2, 0.25, windows ? 2 / 3 : 1 / 3), + (kRemoteViewStyleOriginal, false, 2, 0.5, windows ? 1.0 : 2 / 3), ]) { testWidgets( - '$style zoom=$zoom peerDPR=$density keeps cursor units', - (tester) => tester.runAsync(() async { - tester.view.devicePixelRatio = 2; - addTearDown(tester.view.resetDevicePixelRatio); - final channel = Platform.isWindows - ? SystemChannels.mouseCursor - : const MethodChannel('flutter_custom_cursor'); - tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (call) async => call.method.startsWith('createCustomCursor') - ? (call.arguments as Map)['name'] - : null); - final data = await _data(density, '$style-$zoom-$density'); - // A stale cached DPR must not affect the cursor when the window moves. - final canvas = _Canvas(1, style: style, scale: viewScale); - final ffi = _FFI(canvas); - final cursor = _Cursor(data, ffi); - await tester.pumpWidget(MediaQuery( - data: const MediaQueryData(devicePixelRatio: 2), - child: MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => _Image()), - ChangeNotifierProvider.value(value: canvas), - ChangeNotifierProvider.value(value: cursor), - ], - child: ImagePaint( - ffi: ffi, - id: 'dpi-policy', - zoomCursor: zoom.obs, - cursorOverImage: true.obs, - keyboardEnabled: true.obs, - remoteCursorMoved: false.obs, - )), - )); - await tester.pumpWidget(const SizedBox.shrink()); - await Future.wait(cursor.cachedKeys.map((key) async { - await deleteCustomCursor(key); - })); - data.nativeImage.dispose(); - cursor.dispose(); - canvas.dispose(); - tester.binding.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); - expect(data.scale, closeTo(expectedScale, 1e-9)); - expect(data.hotx, closeTo(data.hotxOrigin * expectedScale, 1e-9)); - expect(data.hoty, closeTo(data.hotyOrigin * expectedScale, 1e-9)); - })); + '${testCase.$1} zoom=${testCase.$2} peerDPR=${testCase.$3}', + (tester) => tester + .runAsync(() => _checkPolicy(tester, testCase, registrations))); + } + test('live DPR changes invalidate a cached native cursor', + () => _checkDprChange(view, registrations)); +} + +Future _checkPolicy( + WidgetTester tester, + (String, bool, int, double, double) testCase, + List> registrations) async { + final (style, zoom, density, viewScale, expectedScale) = testCase; + tester.view.devicePixelRatio = 2; + final data = await _data(density, '$style-$zoom-$density'); + final originalBytes = data.data; + // A stale cached DPR must not affect the cursor when the window moves. + final canvas = _Canvas(1, style: style, scale: viewScale); + final ffi = _FFI(canvas); + final cursor = _Cursor(data, ffi); + await tester.pumpWidget(MediaQuery( + data: const MediaQueryData(devicePixelRatio: 2), + child: MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => _Image()), + ChangeNotifierProvider.value(value: canvas), + ChangeNotifierProvider.value(value: cursor), + ], + child: ImagePaint( + ffi: ffi, + id: 'dpi-policy', + zoomCursor: zoom.obs, + cursorOverImage: true.obs, + keyboardEnabled: true.obs, + remoteCursorMoved: false.obs, + )), + )); + await tester.pumpWidget(const SizedBox.shrink()); + for (final key in cursor.cachedKeys) { + await deleteCustomCursor(key); + } + data.nativeImage.dispose(); + cursor.dispose(); + canvas.dispose(); + expect(data.scale, closeTo(expectedScale, 1e-9)); + expect(data.hotx, closeTo(data.hotxOrigin * expectedScale, 1e-9)); + expect(data.hoty, closeTo(data.hotyOrigin * expectedScale, 1e-9)); + expect(data.data, same(originalBytes)); + if (style == kRemoteViewStyleAdaptive && !zoom && density > 0) { + final args = registrations.single; + expect((args['width'], args['height']), (Platform.isLinux ? 36 : 18, 36)); + expect((args['hotX'], args['hotY']), (8.0, 18.0)); + expect(args['imagePixelRatio'], 2.0); } } + +Future _checkDprChange( + TestFlutterView view, List> registrations) async { + final data = await _data(1, 'dpr-cache'); + final canvas = _Canvas(1, style: kRemoteViewStyleAdaptive, scale: 1); + final cursor = _Cursor(data, _FFI(canvas)); + for (final dpr in [2.0, 1.0]) { + view.devicePixelRatio = dpr; + buildCursorOfCache(cursor, 1, data); + await deleteCustomCursor(cursor.cachedKeys.last); + } + expect(registrations, hasLength(2)); + expect(registrations[0]['name'], isNot(registrations[1]['name'])); + data.nativeImage.dispose(); + cursor.dispose(); + canvas.dispose(); +} diff --git a/flutter/test/cursor_plugin_test.dart b/flutter/test/cursor_plugin_test.dart deleted file mode 100644 index b9fee90dd..000000000 --- a/flutter/test/cursor_plugin_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// These tests drive the lifecycle normally owned by MouseTracker. -// ignore_for_file: invalid_use_of_protected_member - -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/native/custom_cursor.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image/image.dart' as img; - -class _CursorModel implements CursorModel { - @override - final Set cachedKeys = {}; - @override - void addKey(String key) => cachedKeys.add(key); - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -Future _data() async { - final recorder = ui.PictureRecorder(); - ui.Canvas(recorder).drawColor(const ui.Color(0xff123456), ui.BlendMode.src); - final picture = recorder.endRecording(); - final nativeImage = await picture.toImage(20, 30); - picture.dispose(); - addTearDown(nativeImage.dispose); - return CursorData( - peerId: 'plugin', - id: 'edit', - image: img.Image(width: 20, height: 30, numChannels: 4), - nativeImage: nativeImage, - scale: 1, - data: Uint8List.fromList([1, 2]), - hotxOrigin: 4, - hotyOrigin: 16, - width: 20, - height: 30); -} - -void main() { - final binding = TestWidgetsFlutterBinding.ensureInitialized(); - final view = binding.platformDispatcher.views.single; - final channel = Platform.isWindows - ? SystemChannels.mouseCursor - : const MethodChannel('flutter_custom_cursor'); - late List> registrations; - setUp(() { - registrations = []; - binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, - (call) async { - if (call.method.startsWith('createCustomCursor')) { - final args = call.arguments as Map; - registrations.add(args); - return args['name']; - } - return null; - }); - }); - tearDown(() { - view.resetDevicePixelRatio(); - binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('native bridge delegates rasterization and retains the minimum policy', - () => _checkRasterization(view, registrations)); - test('live DPR changes invalidate a cached native cursor', - () => _checkDprChange(view, registrations)); -} - -Future _checkRasterization( - TestFlutterView view, List> registrations) async { - view.devicePixelRatio = 2; - final data = await _data(); - final originalBytes = data.data; - final cursor = _CursorModel(); - final session = buildCursorOfCache(cursor, 0.5, data).createSession(1); - await session.activate(); - session.dispose(); - final args = registrations.single; - final pixelsPerUnit = Platform.isWindows ? 1 : 2; - expect(data.scale, 0.6); - expect(identical(data.data, originalBytes), isTrue); - expect(args['height'], 18 * pixelsPerUnit); - expect(args['width'], (Platform.isLinux ? 18 : 12) * pixelsPerUnit); - expect(args['imagePixelRatio'], 2.0); - expect(args['hotX'], Platform.isMacOS ? 4.8 : 2 * pixelsPerUnit); - expect(args['hotY'], Platform.isMacOS ? 19.2 : 10 * pixelsPerUnit); - await deleteCustomCursor(args['name'] as String); -} - -Future _checkDprChange( - TestFlutterView view, List> registrations) async { - final data = await _data(); - final cursor = _CursorModel(); - for (final dpr in [2.0, 1.0]) { - view.devicePixelRatio = dpr; - final session = buildCursorOfCache(cursor, 1, data).createSession(1); - await session.activate(); - session.dispose(); - } - expect(registrations, hasLength(2)); - expect(registrations[0]['name'], isNot(registrations[1]['name'])); - for (final args in registrations) { - await deleteCustomCursor(args['name'] as String); - } -}