blob: d4951ff2469f1dde71f94e1fb37debfcf13dbbd6 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: avoid_as, dead_code, null_check_always_fails
import 'dart:async';
import 'dart:ui';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
// ignore: implementation_imports
import 'package:fuchsia_scenic_flutter/src/pointer_injector.dart';
import 'package:mockito/mockito.dart';
import 'package:pedantic/pedantic.dart';
import 'package:zircon/zircon.dart';
void main() {
const testViewId = 42;
Future<void> sendViewConnectedEvent(int viewId) {
return FuchsiaViewsService.instance.platformViewChannel.binaryMessenger
.handlePlatformMessage(
FuchsiaViewsService.instance.platformViewChannel.name,
FuchsiaViewsService.instance.platformViewChannel.codec
.encodeMethodCall(MethodCall(
'View.viewConnected', <String, dynamic>{'viewId': viewId})),
null);
}
Future<void> sendViewDisconnectedEvent(int viewId) {
return FuchsiaViewsService.instance.platformViewChannel.binaryMessenger
.handlePlatformMessage(
FuchsiaViewsService.instance.platformViewChannel.name,
FuchsiaViewsService.instance.platformViewChannel.codec
.encodeMethodCall(MethodCall('View.viewDisconnected',
<String, dynamic>{'viewId': viewId})),
null);
}
void expectPlatformViewChannelCall(
WidgetTester tester, String methodName, Map<String, dynamic> methodArgs,
{dynamic Function()? handler}) {
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
FuchsiaViewsService.instance.platformViewChannel, (call) async {
expect(call.method, methodName);
expect(call.arguments, methodArgs);
if (handler != null) {
return handler.call();
} else {
return null;
}
});
}
testWidgets('FuchsiaViewConnection', (WidgetTester tester) async {
bool connectedCalled = false;
bool disconnectedCalled = false;
final connection = TestFuchsiaViewConnection(
_mockViewHolderToken(testViewId),
onViewConnected: (_) => connectedCalled = true,
onViewDisconnected: (_) => disconnectedCalled = true,
);
expectPlatformViewChannelCall(tester, 'View.create', <String, dynamic>{
'viewId': testViewId,
'hitTestable': true,
'focusable': true,
'viewOcclusionHintLTRB': <double>[0, 0, 0, 0],
});
await connection.connect();
expect(connection.connected, false);
expect(connectedCalled, false);
expect(disconnectedCalled, false);
// signal that the platform view has connected successfully.
await sendViewConnectedEvent(testViewId);
expect(connection.connected, true);
expect(connectedCalled, true);
// signal that the platform view has disconnected.
await sendViewDisconnectedEvent(testViewId);
expect(connection.connected, false);
expect(disconnectedCalled, true);
});
testWidgets('FuchsiaViewConnection.usePointerInjection',
(WidgetTester tester) async {
bool connectedCalled = false;
bool disconnectedCalled = false;
final connection = TestFuchsiaViewConnection(
_mockViewHolderToken(testViewId),
viewRef: _mockViewRef(),
onViewConnected: (_) => connectedCalled = true,
onViewDisconnected: (_) => disconnectedCalled = true,
usePointerInjection: true,
);
expectPlatformViewChannelCall(tester, 'View.create', <String, dynamic>{
'viewId': testViewId,
'hitTestable': true,
'focusable': true,
'viewOcclusionHintLTRB': <double>[0, 0, 0, 0],
});
await connection.connect();
// Invoke all View.view* callbacks; disconnect() should dispose the pointer injector.
await sendViewConnectedEvent(testViewId);
await sendViewDisconnectedEvent(testViewId);
expect(connectedCalled, true);
expect(disconnectedCalled, true);
verify(connection.pointerInjector.dispose());
// Test pointer dispatch works.
when(connection.pointerInjector.registered).thenReturn(false);
await connection.dispatchPointerEvent(PointerDownEvent());
verify((connection.pointerInjector as MockPointerInjector).register(
hostViewRef: anyNamed('hostViewRef'),
viewRef: anyNamed('viewRef'),
viewport: anyNamed('viewport'),
));
when(connection.pointerInjector.registered).thenReturn(true);
await connection.dispatchPointerEvent(PointerDownEvent());
verify((connection.pointerInjector as MockPointerInjector).dispatchEvent(
pointer: anyNamed('pointer'), viewport: anyNamed('viewport')));
});
testWidgets('Recreate pointer injection on error',
(WidgetTester tester) async {
final connection = TestFuchsiaViewConnection(
_mockViewHolderToken(testViewId),
viewRef: _mockViewRef(),
usePointerInjection: true,
);
// Error handler for pointer injection should dispose current instance of
// PointerInjector.
final injector = connection.pointerInjector;
connection.onPointerInjectionError();
verify(injector.dispose());
// A new instance of PointerInjector should be created as part of register()
// during dispatchPointerEvent call.
connection._pointerInjector = MockPointerInjector();
final downEvent = PointerDownEvent();
when(connection.pointerInjector.registered).thenReturn(false);
await connection.dispatchPointerEvent(downEvent);
verify((connection.pointerInjector as MockPointerInjector).register(
hostViewRef: anyNamed('hostViewRef'),
viewRef: anyNamed('viewRef'),
viewport: anyNamed('viewport'),
));
// A new instance of PointerInjector should be used during register().
final newInjector = connection.pointerInjector;
expect(newInjector != injector, isTrue);
});
test('Multiple pointer events only register injector once', () async {
final FuchsiaViewConnection connection = TestFuchsiaViewConnection(
_mockViewHolderToken(testViewId),
viewRef: _mockViewRef(),
usePointerInjection: true,
);
// Inject multiple events which should trigger registration.
// Don't `await` the resulting `Future`s, since the real caller
// (_PlatformViewGestureRecognizer) doesn't do so.
//
// Switch the `registered` getter to return `true` after the
// first call to `dispatchPointerEvent()`, to emulate the
// case where the PointerInjector immediately updates
// the registration state.
final downEvent = PointerDownEvent();
when(connection.pointerInjector.registered).thenReturn(false);
unawaited(connection.dispatchPointerEvent(downEvent));
when(connection.pointerInjector.registered).thenReturn(true);
unawaited(connection.dispatchPointerEvent(downEvent));
// Because `registered` was true at the time of the second pointer event,
// FuchsiaViewConnection should not have called `register()` for the
// second pointer event.
verify((connection.pointerInjector as MockPointerInjector).register(
hostViewRef: anyNamed('hostViewRef'),
viewRef: anyNamed('viewRef'),
viewport: anyNamed('viewport'),
)).called(1);
});
test('Multiple pointer events preserve event order', () async {
// Create a FuchsiaViewConnection with a PointerInjector whose
// register() method never completes. This emulates the case
// where the FIDL call to the PointerInjector Registry doesn't
// complete immediately.
final FuchsiaViewConnection connection = TestFuchsiaViewConnection(
_mockViewHolderToken(testViewId),
viewRef: _mockViewRef(),
usePointerInjection: true,
).._pointerInjector = _hangingPointerInjector();
// Inject a pointer event in the unregistered state.
// Don't `await` the resulting `Future`s, since the real caller
// (_PlatformViewGestureRecognizer) doesn't do so.
final firstDownEvent = PointerDownEvent(position: Offset(1.0, 1.0));
when(connection.pointerInjector.registered).thenReturn(false);
unawaited(connection.dispatchPointerEvent(firstDownEvent));
// Update PointerInjector to indicate that the injector is
// registered. This emulates the case where PointerInjector
// optimistically updates registration state before the FIDL
// call completes.
when(connection.pointerInjector.registered).thenReturn(true);
// Inject a pointer event in the registered state.
// Don't `await` the resulting `Future`s, since the real caller
// (_PlatformViewGestureRecognizer) doesn't do so.
final secondDownEvent = PointerDownEvent(position: Offset(2.0, 2.0));
unawaited(connection.dispatchPointerEvent(secondDownEvent));
// Verify that the events were not reordered.
expect(
verify((connection.pointerInjector as MockPointerInjector)
.dispatchEvent(
pointer: captureAnyNamed('pointer'),
viewport: anyNamed('viewport')))
.captured,
anyOf(equals(null), equals(firstDownEvent),
equals([firstDownEvent, secondDownEvent])));
});
}
ViewRef _mockViewRef() {
final handle = MockHandle();
when(handle.isValid).thenReturn(true);
when(handle.duplicate(any)).thenReturn(handle);
final eventPair = MockEventPair();
when(eventPair.handle).thenReturn(handle);
when(eventPair.isValid).thenReturn(true);
final viewRef = MockViewRef();
when(viewRef.reference).thenReturn(eventPair);
return viewRef;
}
ViewHolderToken _mockViewHolderToken(int handleValue) {
final handle = MockHandle();
when(handle.handle).thenReturn(handleValue);
when(handle.isValid).thenReturn(true);
final eventPair = MockEventPair();
when(eventPair.handle).thenReturn(handle);
when(eventPair.isValid).thenReturn(true);
final viewHolderToken = MockViewHolderToken();
when(viewHolderToken.value).thenReturn(eventPair);
return viewHolderToken;
}
class TestFuchsiaViewConnection extends FuchsiaViewConnection {
// ignore: prefer_final_fields
var _pointerInjector = MockPointerInjector();
TestFuchsiaViewConnection(
ViewHolderToken viewHolderToken, {
ViewRef? viewRef,
FuchsiaViewConnectionCallback? onViewConnected,
FuchsiaViewConnectionCallback? onViewDisconnected,
FuchsiaViewConnectionStateCallback? onViewStateChanged,
bool usePointerInjection = false,
}) : super(
viewHolderToken,
viewRef: viewRef,
onViewConnected: onViewConnected,
onViewDisconnected: onViewDisconnected,
onViewStateChanged: onViewStateChanged,
usePointerInjection: usePointerInjection,
);
@override
PointerInjector get pointerInjector => _pointerInjector;
@override
ViewRef get hostViewRef => _mockViewRef();
}
class MockViewHolderToken extends Mock implements ViewHolderToken {
@override
int get hashCode => super.noSuchMethod(Invocation.method(#hashCode, []));
@override
bool operator ==(dynamic other) =>
super.noSuchMethod(Invocation.method(#==, [other]));
}
class MockViewRef extends Mock implements ViewRef {
@override
int get hashCode => super.noSuchMethod(Invocation.method(#hashCode, []));
@override
bool operator ==(dynamic other) =>
super.noSuchMethod(Invocation.method(#==, [other]));
}
class MockEventPair extends Mock implements EventPair {}
class MockHandle extends Mock implements Handle {
@override
int get hashCode => super.noSuchMethod(Invocation.method(#hashCode, []));
@override
bool operator ==(dynamic other) =>
super.noSuchMethod(Invocation.method(#==, [other]));
@override
Handle duplicate(int? rights) =>
super.noSuchMethod(Invocation.method(#==, [rights]));
}
class MockPointerInjector extends Mock implements PointerInjector {
@override
Future<void> register({
ViewRef? hostViewRef,
ViewRef? viewRef,
Rect? viewport,
}) async =>
super.noSuchMethod(Invocation.method(#register, [], {
#hostViewRef: hostViewRef,
#viewRef: viewRef,
#viewport: viewport,
}));
@override
Future<void> dispatchEvent({
PointerEvent? pointer,
Rect? viewport,
}) async =>
super.noSuchMethod(Invocation.method(#dispatchEvent, [], {
#pointer: pointer,
#viewport: viewport,
}));
}
// Returns a MockPointerInjector whose `register()` method returns a `Future`
// which never completes.
MockPointerInjector _hangingPointerInjector() {
final injector = MockPointerInjector();
when(injector.register(
hostViewRef: anyNamed('hostViewRef'),
viewRef: anyNamed('viewRef'),
viewport: anyNamed('viewport'),
)).thenAnswer((_) => Completer().future);
return injector;
}