mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-15 00:41:01 +03:00
test: reduce duplicate cursor fixtures and geometry checks
This commit is contained in:
@@ -58,8 +58,8 @@ class _Canvas extends ChangeNotifier implements CanvasModel {
|
|||||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Cursor extends ChangeNotifier implements CursorModel {
|
class _Cursor extends CursorModel {
|
||||||
_Cursor(this.cache, this._ffi);
|
_Cursor(this.cache, this._ffi) : super(WeakReference(_ffi));
|
||||||
|
|
||||||
final FFI _ffi;
|
final FFI _ffi;
|
||||||
@override
|
@override
|
||||||
@@ -68,17 +68,9 @@ class _Cursor extends ChangeNotifier implements CursorModel {
|
|||||||
@override
|
@override
|
||||||
CursorData cache;
|
CursorData cache;
|
||||||
@override
|
@override
|
||||||
ui.Image? get image => null;
|
|
||||||
@override
|
|
||||||
double get hotx => cache.hotxOrigin;
|
double get hotx => cache.hotxOrigin;
|
||||||
@override
|
@override
|
||||||
double get hoty => cache.hotyOrigin;
|
double get hoty => cache.hotyOrigin;
|
||||||
@override
|
|
||||||
final Set<String> cachedKeys = {};
|
|
||||||
@override
|
|
||||||
void addKey(String key) => cachedKeys.add(key);
|
|
||||||
@override
|
|
||||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Input extends Fake implements InputModel {
|
class _Input extends Fake implements InputModel {
|
||||||
|
|||||||
@@ -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() {
|
void main() {
|
||||||
_linuxDisplayTests();
|
_linuxDisplayTests();
|
||||||
_minimumTests();
|
|
||||||
final minimumScale = Platform.isWindows ? 1 / 3 : 2 / 3;
|
final minimumScale = Platform.isWindows ? 1 / 3 : 2 / 3;
|
||||||
for (final (style, zoom, dpr, source, canvasScale, scale, texture) in [
|
for (final (style, zoom, dpr, source, canvasScale, scale, texture, density) in [
|
||||||
(kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true),
|
(kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true, 0.0),
|
||||||
(kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true),
|
(kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true, 0.0),
|
||||||
(kRemoteViewStyleCustom, false, 2.0, (48, 64), 0.25, 0.25, false),
|
(kRemoteViewStyleCustom, false, 2.0, (48, 64), 0.25, 0.25, false, 0.0),
|
||||||
(kRemoteViewStyleCustom, true, 2.0, (48, 64), 2.0, 2.0, false),
|
(kRemoteViewStyleCustom, true, 2.0, (48, 64), 2.0, 2.0, false, 0.0),
|
||||||
(kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false),
|
(kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false, 0.0),
|
||||||
(kRemoteViewStyleAdaptive, true, 2.0, (9, 18), 0.1, minimumScale, true),
|
(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(
|
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 {
|
(tester) async {
|
||||||
final painter = await _paintCursor(
|
final painter = await _paintCursor(
|
||||||
tester, _CanvasModel(style, canvasScale, texture),
|
tester, _CanvasModel(style, canvasScale, texture),
|
||||||
dpr: dpr, zoom: zoom, source: source);
|
dpr: dpr, zoom: zoom, source: source, density: density);
|
||||||
expect(painter.scale, scale);
|
expect(painter.scale, scale);
|
||||||
var imageOrigin = _canvasOffset;
|
var imageOrigin = _canvasOffset;
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
@@ -255,7 +238,9 @@ void main() {
|
|||||||
imageOrigin = background.position!;
|
imageOrigin = background.position!;
|
||||||
}
|
}
|
||||||
final target = _remotePosition * canvasScale + imageOrigin;
|
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();
|
final canvas = _Canvas();
|
||||||
painter.paint(canvas, _viewport);
|
painter.paint(canvas, _viewport);
|
||||||
final position = canvas.position! + _hotspot * canvas.factor;
|
final position = canvas.position! + _hotspot * canvas.factor;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ library;
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:js' as js;
|
import 'dart:js' as js;
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@@ -20,13 +19,6 @@ import 'package:get/get.dart';
|
|||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class _CursorModel extends Fake implements model.CursorModel {
|
|
||||||
@override
|
|
||||||
final Set<String> cachedKeys = {};
|
|
||||||
@override
|
|
||||||
void addKey(String key) => cachedKeys.add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
_alphaTests();
|
_alphaTests();
|
||||||
@@ -37,15 +29,10 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
test('Web cursor aligns CSS hotspots with rounded PNG dimensions', () async {
|
test('Web cursor aligns CSS hotspots with rounded PNG dimensions', () async {
|
||||||
Map<String, dynamic>? registered;
|
final registered = _captureCursor();
|
||||||
final original = js.context['setByName'];
|
final canvas = _Canvas(kRemoteViewStyleAdaptive);
|
||||||
js.context['setByName'] = js.allowInterop((String name, String value) {
|
addTearDown(canvas.dispose);
|
||||||
expect(name, 'cursor');
|
final ffi = _FFI(canvas);
|
||||||
registered = jsonDecode(value) as Map<String, dynamic>;
|
|
||||||
});
|
|
||||||
addTearDown(() => js.context['setByName'] = original);
|
|
||||||
final nativeImage = await createTestImage(width: 48, height: 48);
|
|
||||||
addTearDown(nativeImage.dispose);
|
|
||||||
for (final (hotspot, scale, side, expected) in [
|
for (final (hotspot, scale, side, expected) in [
|
||||||
((7.0, 7.0), 633 / 1600, 19, (3, 3)),
|
((7.0, 7.0), 633 / 1600, 19, (3, 3)),
|
||||||
((21.0, 23.0), 633 / 1600, 19, (8, 9)),
|
((21.0, 23.0), 633 / 1600, 19, (8, 9)),
|
||||||
@@ -54,37 +41,20 @@ void main() {
|
|||||||
((21.0, 23.0), 0.5, 24, (11, 12)),
|
((21.0, 23.0), 0.5, 24, (11, 12)),
|
||||||
((7.0, 7.0), 1.0, 48, (7, 7)),
|
((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 =
|
final session =
|
||||||
buildCursorOfCache(_CursorModel(), scale, cache).createSession(1);
|
buildCursorOfCache(cursor, scale, cursor.cache).createSession(1);
|
||||||
await session.activate();
|
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())!;
|
final bitmap = img.decodePng(uri.data!.contentAsBytes())!;
|
||||||
expect((bitmap.width, bitmap.height), (side, side));
|
expect((bitmap.width, bitmap.height), (side, side));
|
||||||
expect((registered!['hotx'], registered!['hoty']), expected);
|
expect((registered['hotx'], registered['hoty']), expected);
|
||||||
session.dispose();
|
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 {
|
class _Image extends ChangeNotifier implements model.ImageModel {
|
||||||
@override
|
@override
|
||||||
bool get useTextureRender => false;
|
bool get useTextureRender => false;
|
||||||
@@ -144,18 +114,21 @@ class _FFI extends Fake implements model.FFI {
|
|||||||
|
|
||||||
Future<model.CursorModel> _loadCursor(model.FFI ffi, String id,
|
Future<model.CursorModel> _loadCursor(model.FFI ffi, String id,
|
||||||
{List<int> pixel = const [255, 255, 255, 255],
|
{List<int> pixel = const [255, 255, 255, 255],
|
||||||
|
(double, double) hotspot = (7, 9),
|
||||||
double? pixelRatio = 1}) async {
|
double? pixelRatio = 1}) async {
|
||||||
final cursor = model.CursorModel(WeakReference(ffi))..id = id;
|
final cursor = model.CursorModel(WeakReference(ffi))..id = id;
|
||||||
await cursor.updateCursorData({
|
await cursor.updateCursorData({
|
||||||
'id': id,
|
'id': id,
|
||||||
'width': '48',
|
'width': '48',
|
||||||
'height': '48',
|
'height': '48',
|
||||||
'hotx': '7',
|
'hotx': '${hotspot.$1}',
|
||||||
'hoty': '9',
|
'hoty': '${hotspot.$2}',
|
||||||
if (pixelRatio != null) 'scale': '$pixelRatio',
|
if (pixelRatio != null) 'scale': '$pixelRatio',
|
||||||
'colors': jsonEncode([for (var i = 0; i < 48 * 48; i++) ...pixel]),
|
'colors': jsonEncode([for (var i = 0; i < 48 * 48; i++) ...pixel]),
|
||||||
});
|
});
|
||||||
addTearDown(() async {
|
addTearDown(() async {
|
||||||
|
// Keep the session owner alive across asynchronous image decoding.
|
||||||
|
expect(cursor.parent.target, same(ffi));
|
||||||
for (final key in cursor.cachedKeys) {
|
for (final key in cursor.cachedKeys) {
|
||||||
await deleteCustomCursor(key);
|
await deleteCustomCursor(key);
|
||||||
}
|
}
|
||||||
@@ -224,23 +197,22 @@ Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _alphaTests() {
|
void _alphaTests() {
|
||||||
for (final (density, pixel, expected) in [
|
for (final (density, pixel) in [
|
||||||
(1.0, [255, 255, 255, 128], [255, 255, 255, 128]),
|
(1.0, [255, 255, 255, 128]),
|
||||||
(2.0, [255, 128, 64, 128], [255, 128, 64, 128]),
|
(2.0, [255, 128, 64, 128]),
|
||||||
(1.0, [255, 128, 64, 112], [255, 128, 64, 112]),
|
(1.0, [255, 128, 64, 112]),
|
||||||
(1.0, [0, 0, 0, 0], [0, 0, 0, 0]),
|
(1.0, [0, 0, 0, 0]),
|
||||||
(1.0, [255, 255, 255, 255], [255, 255, 255, 255]),
|
(1.0, [255, 255, 255, 255]),
|
||||||
// Old macOS hosts send straight alpha without density metadata.
|
// Old macOS hosts send straight alpha without density metadata.
|
||||||
(0.0, [255, 128, 64, 128], [255, 128, 64, 128]),
|
(0.0, [255, 128, 64, 128]),
|
||||||
(null, [80, 40, 20, 128], [80, 40, 20, 128]),
|
(null, [80, 40, 20, 128]),
|
||||||
]) {
|
]) {
|
||||||
test('Web macOS cursor density $density preserves RGBA $expected',
|
test('Web macOS cursor density $density preserves RGBA $pixel',
|
||||||
() => _checkAlpha(density, pixel, expected));
|
() => _checkAlpha(density, pixel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkAlpha(
|
Future<void> _checkAlpha(double? density, List<int> pixel) async {
|
||||||
double? density, List<int> pixel, List<int> expected) async {
|
|
||||||
final registered = _captureCursor();
|
final registered = _captureCursor();
|
||||||
final canvas = _Canvas(kRemoteViewStyleAdaptive);
|
final canvas = _Canvas(kRemoteViewStyleAdaptive);
|
||||||
addTearDown(canvas.dispose);
|
addTearDown(canvas.dispose);
|
||||||
@@ -250,7 +222,7 @@ Future<void> _checkAlpha(
|
|||||||
// Cover both the painted remote cursor and the initial CSS cursor PNG.
|
// Cover both the painted remote cursor and the initial CSS cursor PNG.
|
||||||
final straight = await cursor.image!
|
final straight = await cursor.image!
|
||||||
.toByteData(format: ui.ImageByteFormat.rawStraightRgba);
|
.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]) {
|
for (final scale in [1.0, 0.5, 1.0]) {
|
||||||
final session =
|
final session =
|
||||||
buildCursorOfCache(cursor, scale, cursor.cache).createSession(1);
|
buildCursorOfCache(cursor, scale, cursor.cache).createSession(1);
|
||||||
@@ -259,6 +231,6 @@ Future<void> _checkAlpha(
|
|||||||
final png =
|
final png =
|
||||||
img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!;
|
img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!;
|
||||||
final color = png.getPixel(png.width ~/ 2, png.height ~/ 2);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user