diff --git a/flutter/test/cursor_dpi_policy_test.dart b/flutter/test/cursor_dpi_policy_test.dart index ab233366b..6c61eb5ff 100644 --- a/flutter/test/cursor_dpi_policy_test.dart +++ b/flutter/test/cursor_dpi_policy_test.dart @@ -58,8 +58,8 @@ class _Canvas extends ChangeNotifier implements CanvasModel { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } -class _Cursor extends ChangeNotifier implements CursorModel { - _Cursor(this.cache, this._ffi); +class _Cursor extends CursorModel { + _Cursor(this.cache, this._ffi) : super(WeakReference(_ffi)); final FFI _ffi; @override @@ -68,17 +68,9 @@ class _Cursor extends ChangeNotifier implements CursorModel { @override CursorData cache; @override - ui.Image? get image => null; - @override double get hotx => cache.hotxOrigin; @override double get hoty => cache.hotyOrigin; - @override - final Set cachedKeys = {}; - @override - void addKey(String key) => cachedKeys.add(key); - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _Input extends Fake implements InputModel { diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart index 1a5c84827..389998303 100644 --- a/flutter/test/cursor_paint_scale_test.dart +++ b/flutter/test/cursor_paint_scale_test.dart @@ -200,48 +200,31 @@ void _linuxDisplayTests() { } } -void _minimumTests() { - for (final (style, dpr, source, viewScale, expected) in [ - (kRemoteViewStyleAdaptive, 1.0, (48, 64), 0.05, 0.1875), - (kRemoteViewStyleAdaptive, 2.0, (48, 64), 0.05, 0.1875), - (kRemoteViewStyleCustom, 1.0, (48, 64), 0.05, 0.1875), - (kRemoteViewStyleCustom, 2.0, (48, 64), 0.05, 0.1875), - (kRemoteViewStyleCustom, 2.0, (8, 10), 1.0, 1.2), - (kRemoteViewStyleOriginal, 1.0, (48, 64), 0.05, 0.25), - (kRemoteViewStyleOriginal, 2.0, (48, 64), 0.05, Platform.isWindows ? 0.125 : 0.25), - ]) { - testWidgets('density-aware $style DPR=$dpr source=$source minimum', - (tester) async { - final painter = await _paintCursor(tester, - _CanvasModel(style, viewScale, true), - dpr: dpr, source: source, density: 2); - expect(painter.scale, expected); - final hotspot = (Offset(painter.x, painter.y) + _hotspot) * painter.scale; - final target = _remotePosition * viewScale + _canvasOffset; - expect(hotspot.dx, closeTo(target.dx, 1e-9)); - expect(hotspot.dy, closeTo(target.dy, 1e-9)); - }); - } -} - void main() { _linuxDisplayTests(); - _minimumTests(); final minimumScale = Platform.isWindows ? 1 / 3 : 2 / 3; - for (final (style, zoom, dpr, source, canvasScale, scale, texture) in [ - (kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true), - (kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true), - (kRemoteViewStyleCustom, false, 2.0, (48, 64), 0.25, 0.25, false), - (kRemoteViewStyleCustom, true, 2.0, (48, 64), 2.0, 2.0, false), - (kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false), - (kRemoteViewStyleAdaptive, true, 2.0, (9, 18), 0.1, minimumScale, true), + for (final (style, zoom, dpr, source, canvasScale, scale, texture, density) in [ + (kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true, 0.0), + (kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true, 0.0), + (kRemoteViewStyleCustom, false, 2.0, (48, 64), 0.25, 0.25, false, 0.0), + (kRemoteViewStyleCustom, true, 2.0, (48, 64), 2.0, 2.0, false, 0.0), + (kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false, 0.0), + (kRemoteViewStyleAdaptive, true, 2.0, (9, 18), 0.1, minimumScale, true, 0.0), + (kRemoteViewStyleAdaptive, true, 1.0, (48, 64), 0.05, 0.1875, true, 2.0), + (kRemoteViewStyleAdaptive, true, 2.0, (48, 64), 0.05, 0.1875, true, 2.0), + (kRemoteViewStyleCustom, true, 1.0, (48, 64), 0.05, 0.1875, true, 2.0), + (kRemoteViewStyleCustom, true, 2.0, (48, 64), 0.05, 0.1875, true, 2.0), + (kRemoteViewStyleCustom, true, 2.0, (8, 10), 1.0, 1.2, true, 2.0), + (kRemoteViewStyleOriginal, true, 1.0, (48, 64), 0.05, 0.25, true, 2.0), + (kRemoteViewStyleOriginal, true, 2.0, (48, 64), 0.05, + Platform.isWindows ? 0.125 : 0.25, true, 2.0), ]) { testWidgets( - '$style zoom=$zoom dpr=$dpr source=$source texture=$texture keeps remote geometry', + '$style zoom=$zoom dpr=$dpr source=$source texture=$texture density=$density keeps remote geometry', (tester) async { final painter = await _paintCursor( tester, _CanvasModel(style, canvasScale, texture), - dpr: dpr, zoom: zoom, source: source); + dpr: dpr, zoom: zoom, source: source, density: density); expect(painter.scale, scale); var imageOrigin = _canvasOffset; if (!texture) { @@ -255,7 +238,9 @@ void main() { imageOrigin = background.position!; } final target = _remotePosition * canvasScale + imageOrigin; - expect((Offset(painter.x, painter.y) + _hotspot) * scale, target); + final hotspot = (Offset(painter.x, painter.y) + _hotspot) * scale; + expect(hotspot.dx, closeTo(target.dx, 1e-9)); + expect(hotspot.dy, closeTo(target.dy, 1e-9)); final canvas = _Canvas(); painter.paint(canvas, _viewport); final position = canvas.position! + _hotspot * canvas.factor; diff --git a/flutter/test/cursor_web_test.dart b/flutter/test/cursor_web_test.dart index e85207bb9..a32d0b78c 100644 --- a/flutter/test/cursor_web_test.dart +++ b/flutter/test/cursor_web_test.dart @@ -6,7 +6,6 @@ library; import 'dart:convert'; import 'dart:js' as js; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; @@ -20,13 +19,6 @@ import 'package:get/get.dart'; import 'package:image/image.dart' as img; import 'package:provider/provider.dart'; -class _CursorModel extends Fake implements model.CursorModel { - @override - final Set cachedKeys = {}; - @override - void addKey(String key) => cachedKeys.add(key); -} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); _alphaTests(); @@ -37,15 +29,10 @@ void main() { } } test('Web cursor aligns CSS hotspots with rounded PNG dimensions', () async { - Map? registered; - final original = js.context['setByName']; - js.context['setByName'] = js.allowInterop((String name, String value) { - expect(name, 'cursor'); - registered = jsonDecode(value) as Map; - }); - addTearDown(() => js.context['setByName'] = original); - final nativeImage = await createTestImage(width: 48, height: 48); - addTearDown(nativeImage.dispose); + final registered = _captureCursor(); + final canvas = _Canvas(kRemoteViewStyleAdaptive); + addTearDown(canvas.dispose); + final ffi = _FFI(canvas); for (final (hotspot, scale, side, expected) in [ ((7.0, 7.0), 633 / 1600, 19, (3, 3)), ((21.0, 23.0), 633 / 1600, 19, (8, 9)), @@ -54,37 +41,20 @@ void main() { ((21.0, 23.0), 0.5, 24, (11, 12)), ((7.0, 7.0), 1.0, 48, (7, 7)), ]) { - final cache = _data(nativeImage, hotspot); + final cursor = await _loadCursor(ffi, '$hotspot-$scale', + hotspot: hotspot, pixelRatio: null); final session = - buildCursorOfCache(_CursorModel(), scale, cache).createSession(1); + buildCursorOfCache(cursor, scale, cursor.cache).createSession(1); await session.activate(); - final uri = Uri.parse(registered!['url'] as String); + final uri = Uri.parse(registered['url'] as String); final bitmap = img.decodePng(uri.data!.contentAsBytes())!; expect((bitmap.width, bitmap.height), (side, side)); - expect((registered!['hotx'], registered!['hoty']), expected); + expect((registered['hotx'], registered['hoty']), expected); session.dispose(); - await deleteCustomCursor(cache.updateGetKey(scale)); } }); } -model.CursorData _data(ui.Image nativeImage, (double, double) hotspot) { - final image = img.Image(width: 48, height: 48, numChannels: 4); - img.fill(image, color: img.ColorRgba8(255, 255, 255, 255)); - return model.CursorData( - peerId: 'web-cursor-test', - id: '$hotspot', - image: image, - nativeImage: nativeImage, - scale: 1, - data: Uint8List.fromList(img.encodePng(image)), - hotxOrigin: hotspot.$1, - hotyOrigin: hotspot.$2, - width: 48, - height: 48, - ); -} - class _Image extends ChangeNotifier implements model.ImageModel { @override bool get useTextureRender => false; @@ -144,18 +114,21 @@ class _FFI extends Fake implements model.FFI { Future _loadCursor(model.FFI ffi, String id, {List pixel = const [255, 255, 255, 255], + (double, double) hotspot = (7, 9), double? pixelRatio = 1}) async { final cursor = model.CursorModel(WeakReference(ffi))..id = id; await cursor.updateCursorData({ 'id': id, 'width': '48', 'height': '48', - 'hotx': '7', - 'hoty': '9', + 'hotx': '${hotspot.$1}', + 'hoty': '${hotspot.$2}', if (pixelRatio != null) 'scale': '$pixelRatio', 'colors': jsonEncode([for (var i = 0; i < 48 * 48; i++) ...pixel]), }); addTearDown(() async { + // Keep the session owner alive across asynchronous image decoding. + expect(cursor.parent.target, same(ffi)); for (final key in cursor.cachedKeys) { await deleteCustomCursor(key); } @@ -224,23 +197,22 @@ Future _checkPolicy(WidgetTester tester, String style, double dpr) async { } void _alphaTests() { - for (final (density, pixel, expected) in [ - (1.0, [255, 255, 255, 128], [255, 255, 255, 128]), - (2.0, [255, 128, 64, 128], [255, 128, 64, 128]), - (1.0, [255, 128, 64, 112], [255, 128, 64, 112]), - (1.0, [0, 0, 0, 0], [0, 0, 0, 0]), - (1.0, [255, 255, 255, 255], [255, 255, 255, 255]), + for (final (density, pixel) in [ + (1.0, [255, 255, 255, 128]), + (2.0, [255, 128, 64, 128]), + (1.0, [255, 128, 64, 112]), + (1.0, [0, 0, 0, 0]), + (1.0, [255, 255, 255, 255]), // Old macOS hosts send straight alpha without density metadata. - (0.0, [255, 128, 64, 128], [255, 128, 64, 128]), - (null, [80, 40, 20, 128], [80, 40, 20, 128]), + (0.0, [255, 128, 64, 128]), + (null, [80, 40, 20, 128]), ]) { - test('Web macOS cursor density $density preserves RGBA $expected', - () => _checkAlpha(density, pixel, expected)); + test('Web macOS cursor density $density preserves RGBA $pixel', + () => _checkAlpha(density, pixel)); } } -Future _checkAlpha( - double? density, List pixel, List expected) async { +Future _checkAlpha(double? density, List pixel) async { final registered = _captureCursor(); final canvas = _Canvas(kRemoteViewStyleAdaptive); addTearDown(canvas.dispose); @@ -250,7 +222,7 @@ Future _checkAlpha( // Cover both the painted remote cursor and the initial CSS cursor PNG. final straight = await cursor.image! .toByteData(format: ui.ImageByteFormat.rawStraightRgba); - expect(straight!.buffer.asUint8List(0, 4), expected); + expect(straight!.buffer.asUint8List(0, 4), pixel); for (final scale in [1.0, 0.5, 1.0]) { final session = buildCursorOfCache(cursor, scale, cursor.cache).createSession(1); @@ -259,6 +231,6 @@ Future _checkAlpha( final png = img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!; final color = png.getPixel(png.width ~/ 2, png.height ~/ 2); - expect([color.r, color.g, color.b, color.a], expected); + expect([color.r, color.g, color.b, color.a], pixel); } }