2024 lines
73 KiB
Dart
2024 lines
73 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
void main() {
|
|
group(LogicalKeySet, () {
|
|
test('LogicalKeySet passes parameters correctly.', () {
|
|
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
|
|
final LogicalKeySet set2 = LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
);
|
|
final LogicalKeySet set3 = LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
);
|
|
final LogicalKeySet set4 = LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
);
|
|
final LogicalKeySet setFromSet = LogicalKeySet.fromSet(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
});
|
|
expect(
|
|
set1.keys,
|
|
equals(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
}),
|
|
);
|
|
expect(
|
|
set2.keys,
|
|
equals(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
}),
|
|
);
|
|
expect(
|
|
set3.keys,
|
|
equals(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
}),
|
|
);
|
|
expect(
|
|
set4.keys,
|
|
equals(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
}),
|
|
);
|
|
expect(
|
|
setFromSet.keys,
|
|
equals(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('LogicalKeySet works as a map key.', () {
|
|
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
|
|
final LogicalKeySet set2 = LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
);
|
|
final LogicalKeySet set3 = LogicalKeySet(
|
|
LogicalKeyboardKey.keyD,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyA,
|
|
);
|
|
final LogicalKeySet set4 = LogicalKeySet.fromSet(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyD,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyA,
|
|
});
|
|
final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'};
|
|
expect(set2 == set3, isTrue);
|
|
expect(set2 == set4, isTrue);
|
|
expect(set2.hashCode, set3.hashCode);
|
|
expect(set2.hashCode, set4.hashCode);
|
|
expect(map.containsKey(set1), isTrue);
|
|
expect(map.containsKey(LogicalKeySet(LogicalKeyboardKey.keyA)), isTrue);
|
|
expect(
|
|
set2,
|
|
equals(LogicalKeySet.fromSet(<LogicalKeyboardKey>{
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.keyD,
|
|
})),
|
|
);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('handles two keys', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.keyC,
|
|
LogicalKeyboardKey.control,
|
|
),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// LCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// KeyC -> LCtrl: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// RCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> LShift -> KeyC: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 0);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> KeyA -> KeyC: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invoked, 0);
|
|
invoked = 0;
|
|
|
|
expect(RawKeyboard.instance.keysPressed, isEmpty);
|
|
});
|
|
|
|
test('LogicalKeySet.hashCode is stable', () {
|
|
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
|
|
expect(set1.hashCode, set1.hashCode);
|
|
|
|
final LogicalKeySet set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB);
|
|
expect(set2.hashCode, set2.hashCode);
|
|
|
|
final LogicalKeySet set3 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC);
|
|
expect(set3.hashCode, set3.hashCode);
|
|
|
|
final LogicalKeySet set4 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD);
|
|
expect(set4.hashCode, set4.hashCode);
|
|
});
|
|
|
|
test('LogicalKeySet.hashCode is order-independent', () {
|
|
expect(
|
|
LogicalKeySet(LogicalKeyboardKey.keyA).hashCode,
|
|
LogicalKeySet(LogicalKeyboardKey.keyA).hashCode,
|
|
);
|
|
expect(
|
|
LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB).hashCode,
|
|
LogicalKeySet(LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode,
|
|
);
|
|
expect(
|
|
LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC).hashCode,
|
|
LogicalKeySet(LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode,
|
|
);
|
|
expect(
|
|
LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD).hashCode,
|
|
LogicalKeySet(LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode,
|
|
);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
|
// Collect some key events to use for testing.
|
|
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
autofocus: true,
|
|
onKey: (FocusNode node, RawKeyEvent event) {
|
|
events.add(event);
|
|
return KeyEventResult.ignored;
|
|
},
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
|
|
final LogicalKeySet set = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.control);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(ShortcutActivator.isActivatedBy(set, events[0]), isTrue);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(ShortcutActivator.isActivatedBy(set, events[0]), isFalse);
|
|
});
|
|
|
|
test('LogicalKeySet diagnostics work.', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals('keys: Key A + Key B'));
|
|
});
|
|
});
|
|
|
|
group(SingleActivator, () {
|
|
testWidgetsWithLeakTracking('handles Ctrl-C', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyC,
|
|
control: true,
|
|
),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// LCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// KeyC -> LCtrl: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
invoked = 0;
|
|
|
|
// LShift -> LCtrl -> KeyC: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
invoked = 0;
|
|
|
|
// With Ctrl-C pressed, KeyA -> Release KeyA: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
invoked = 0;
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> KeyA -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
invoked = 0;
|
|
|
|
// RCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> RCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// While holding Ctrl-C, press KeyA: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
expect(RawKeyboard.instance.keysPressed, isEmpty);
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('handles repeated events', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyC,
|
|
control: true,
|
|
),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// LCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 2);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 2);
|
|
invoked = 0;
|
|
|
|
expect(RawKeyboard.instance.keysPressed, isEmpty);
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('rejects repeated events if requested', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyC,
|
|
control: true,
|
|
includeRepeats: false,
|
|
),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// LCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
expect(RawKeyboard.instance.keysPressed, isEmpty);
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('handles Shift-Ctrl-C', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyC,
|
|
shift: true,
|
|
control: true,
|
|
),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// LShift -> LCtrl -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> LShift -> KeyC: Accept
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// LCtrl -> KeyC -> LShift: Reject
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 0);
|
|
invoked = 0;
|
|
|
|
expect(RawKeyboard.instance.keysPressed, isEmpty);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
|
// Collect some key events to use for testing.
|
|
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
autofocus: true,
|
|
onKey: (FocusNode node, RawKeyEvent event) {
|
|
events.add(event);
|
|
return KeyEventResult.ignored;
|
|
},
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
|
|
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isTrue);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isFalse);
|
|
});
|
|
|
|
group('diagnostics.', () {
|
|
test('single key', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyA,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals('keys: Key A'));
|
|
});
|
|
|
|
test('no repeats', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyA,
|
|
includeRepeats: false,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(2));
|
|
expect(description[0], equals('keys: Key A'));
|
|
expect(description[1], equals('excluding repeats'));
|
|
});
|
|
|
|
test('combination', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const SingleActivator(
|
|
LogicalKeyboardKey.keyA,
|
|
control: true,
|
|
shift: true,
|
|
alt: true,
|
|
meta: true,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A'));
|
|
});
|
|
});
|
|
});
|
|
|
|
group(Shortcuts, () {
|
|
testWidgetsWithLeakTracking('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async {
|
|
const Shortcuts shortcuts = Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox());
|
|
await tester.pumpWidget(shortcuts);
|
|
expect(shortcuts.shortcuts, isNotNull);
|
|
expect(shortcuts.shortcuts, isEmpty);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Default constructed Shortcuts.manager has empty shortcuts', (WidgetTester tester) async {
|
|
final ShortcutManager manager = ShortcutManager();
|
|
addTearDown(manager.dispose);
|
|
expect(manager.shortcuts, isNotNull);
|
|
expect(manager.shortcuts, isEmpty);
|
|
final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox());
|
|
await tester.pumpWidget(shortcuts);
|
|
expect(shortcuts.shortcuts, isNotNull);
|
|
expect(shortcuts.shortcuts, isEmpty);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts.manager passes on shortcuts', (WidgetTester tester) async {
|
|
final Map<LogicalKeySet, Intent> testShortcuts = <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
};
|
|
final ShortcutManager manager = ShortcutManager(shortcuts: testShortcuts);
|
|
addTearDown(manager.dispose);
|
|
expect(manager.shortcuts, isNotNull);
|
|
expect(manager.shortcuts, equals(testShortcuts));
|
|
final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox());
|
|
await tester.pumpWidget(shortcuts);
|
|
expect(shortcuts.shortcuts, isNotNull);
|
|
expect(shortcuts.shortcuts, equals(testShortcuts));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('ShortcutManager handles shortcuts', (WidgetTester tester) async {
|
|
final GlobalKey containerKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return true;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Focus(
|
|
autofocus: true,
|
|
child: SizedBox(key: containerKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, isTrue);
|
|
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async {
|
|
final GlobalKey containerKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return true;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Focus(
|
|
autofocus: true,
|
|
child: SizedBox(key: containerKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, isTrue);
|
|
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('ShortcutManager ignores key presses with no primary focus', (WidgetTester tester) async {
|
|
final GlobalKey containerKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return true;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: SizedBox(key: containerKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
expect(primaryFocus, isNull);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, isFalse);
|
|
expect(pressedKeys, isEmpty);
|
|
});
|
|
|
|
test('$ShortcutManager dispatches object creation in constructor', () async {
|
|
await expectLater(
|
|
await memoryEvents(() => ShortcutManager().dispose(), ShortcutManager),
|
|
areCreateAndDispose,
|
|
);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async {
|
|
final GlobalKey containerKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return invoked;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing,
|
|
},
|
|
child: Focus(
|
|
autofocus: true,
|
|
child: SizedBox(key: containerKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, isTrue);
|
|
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async {
|
|
final GlobalKey containerKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return invoked;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing,
|
|
},
|
|
child: Focus(
|
|
autofocus: true,
|
|
child: SizedBox(key: containerKey, width: 100, height: 100),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, isFalse);
|
|
expect(pressedKeys, isEmpty);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async {
|
|
final GlobalKey textFieldKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: TextField(key: textFieldKey, autofocus: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
|
expect(handled, isFalse);
|
|
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA]));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async {
|
|
final GlobalKey textFieldKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return invoked;
|
|
},
|
|
),
|
|
},
|
|
child: TextField(key: textFieldKey, autofocus: true),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
|
expect(result, isTrue);
|
|
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA]));
|
|
expect(invoked, isTrue);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async {
|
|
final GlobalKey textFieldKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return invoked;
|
|
},
|
|
),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: DoNothingAction(consumesKey: false),
|
|
},
|
|
child: TextField(key: textFieldKey, autofocus: true),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
|
expect(result, isFalse);
|
|
expect(invoked, isFalse);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async {
|
|
final GlobalKey textFieldKey = GlobalKey();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
bool invoked = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Shortcuts.manager(
|
|
manager: testManager,
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invoked = true;
|
|
return invoked;
|
|
},
|
|
),
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingAndStopPropagationIntent(),
|
|
},
|
|
child: TextField(key: textFieldKey, autofocus: true),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
|
expect(result, isFalse);
|
|
expect(invoked, isFalse);
|
|
});
|
|
|
|
test('Shortcuts diagnostics work.', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.shift,
|
|
LogicalKeyboardKey.keyA,
|
|
): const ActivateIntent(),
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.shift,
|
|
LogicalKeyboardKey.arrowRight,
|
|
): const DirectionalFocusIntent(TraversalDirection.right),
|
|
},
|
|
child: const SizedBox(),
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(
|
|
description[0],
|
|
equalsIgnoringHashCodes(
|
|
'shortcuts: {{Shift + Key A}: ActivateIntent#00000, {Shift + Arrow Right}: DirectionalFocusIntent#00000(direction: right)}',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('Shortcuts diagnostics work when debugLabel specified.', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
Shortcuts(
|
|
debugLabel: '<Debug Label>',
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
): const ActivateIntent(),
|
|
},
|
|
child: const SizedBox(),
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals('shortcuts: <Debug Label>'));
|
|
});
|
|
|
|
test('Shortcuts diagnostics work when manager not specified.', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
): const ActivateIntent(),
|
|
},
|
|
child: const SizedBox(),
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
|
|
});
|
|
|
|
test('Shortcuts diagnostics work when manager specified.', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
|
|
final TestShortcutManager testManager = TestShortcutManager(
|
|
pressedKeys,
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(
|
|
LogicalKeyboardKey.keyA,
|
|
LogicalKeyboardKey.keyB,
|
|
): const ActivateIntent(),
|
|
},
|
|
);
|
|
addTearDown(testManager.dispose);
|
|
|
|
Shortcuts.manager(
|
|
manager: testManager,
|
|
child: const SizedBox(),
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(2));
|
|
expect(description[0], equalsIgnoringHashCodes('manager: TestShortcutManager#00000(shortcuts: {LogicalKeySet#00000(keys: Key A + Key B): ActivateIntent#00000})'));
|
|
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts support multiple intents', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
bool? value = true;
|
|
Widget buildApp() {
|
|
return MaterialApp(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents(
|
|
orderedIntents: <Intent>[
|
|
ActivateIntent(),
|
|
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
|
],
|
|
),
|
|
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
|
|
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
|
},
|
|
home: Material(
|
|
child: Center(
|
|
child: ListView(
|
|
primary: true,
|
|
children: <Widget> [
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Checkbox(
|
|
value: value,
|
|
onChanged: (bool? newValue) => setState(() { value = newValue; }),
|
|
focusColor: Colors.orange[500],
|
|
);
|
|
},
|
|
),
|
|
Container(
|
|
color: Colors.blue,
|
|
height: 1000,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
await tester.pumpWidget(buildApp());
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester.binding.focusManager.primaryFocus!.toStringShort(),
|
|
equalsIgnoringHashCodes('FocusScopeNode#00000(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])'),
|
|
);
|
|
final ScrollController controller = PrimaryScrollController.of(
|
|
tester.element(find.byType(ListView)),
|
|
);
|
|
expect(controller.position.pixels, 0.0);
|
|
expect(value, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pumpAndSettle();
|
|
// ScrollView scrolls
|
|
expect(controller.position.pixels, 448.0);
|
|
expect(value, isTrue);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
|
|
await tester.pumpAndSettle();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
|
await tester.pumpAndSettle();
|
|
// Focus is now on the checkbox.
|
|
expect(
|
|
tester.binding.focusManager.primaryFocus!.toStringShort(),
|
|
equalsIgnoringHashCodes('FocusNode#00000([PRIMARY FOCUS])'),
|
|
);
|
|
expect(value, isTrue);
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pumpAndSettle();
|
|
// Checkbox is toggled, scroll view does not scroll.
|
|
expect(value, isFalse);
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
|
await tester.pumpAndSettle();
|
|
expect(value, isTrue);
|
|
expect(controller.position.pixels, 0.0);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const DumbLogicalActivator(LogicalKeyboardKey.keyC),
|
|
(Intent intent) { invoked += 1; },
|
|
const SingleActivator(LogicalKeyboardKey.keyC, control: true),
|
|
(Intent intent) { invoked += 10; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// Press KeyC: Accepted by DumbLogicalActivator
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// Press ControlLeft + KeyC: Accepted by SingleActivator
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 10);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 10);
|
|
invoked = 0;
|
|
|
|
// Press ControlLeft + ShiftLeft + KeyC: Accepted by DumbLogicalActivator
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC);
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
});
|
|
});
|
|
|
|
group('CharacterActivator', () {
|
|
testWidgetsWithLeakTracking('is triggered on events with correct character', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const CharacterActivator('?'),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// Press Shift + /
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('handles repeated events', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const CharacterActivator('?'),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// Press KeyC: Accepted by DumbLogicalActivator
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 2);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 2);
|
|
invoked = 0;
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('rejects repeated events if requested', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const CharacterActivator('?', includeRepeats: false),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// Press Shift + /
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('handles Alt, Ctrl and Meta', (WidgetTester tester) async {
|
|
int invoked = 0;
|
|
await tester.pumpWidget(activatorTester(
|
|
const CharacterActivator('?', alt: true, meta: true, control: true),
|
|
(Intent intent) { invoked += 1; },
|
|
));
|
|
await tester.pump();
|
|
|
|
// Press Shift + /
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
expect(invoked, 0);
|
|
|
|
// Press Left Alt + Ctrl + Meta + Shift + /
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
|
|
// Press Right Alt + Ctrl + Meta + Shift + /
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftRight);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.metaRight);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 0);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?');
|
|
expect(invoked, 1);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.slash);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftRight);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.metaRight);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
|
|
expect(invoked, 1);
|
|
invoked = 0;
|
|
}, variant: KeySimulatorTransitModeVariant.all());
|
|
|
|
testWidgetsWithLeakTracking('isActivatedBy works as expected', (WidgetTester tester) async {
|
|
// Collect some key events to use for testing.
|
|
final List<RawKeyEvent> events = <RawKeyEvent>[];
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
autofocus: true,
|
|
onKey: (FocusNode node, RawKeyEvent event) {
|
|
events.add(event);
|
|
return KeyEventResult.ignored;
|
|
},
|
|
child: const SizedBox(),
|
|
),
|
|
);
|
|
|
|
const CharacterActivator characterActivator = CharacterActivator('a');
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue);
|
|
});
|
|
|
|
group('diagnostics.', () {
|
|
test('single key', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const CharacterActivator('A').debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals("character: 'A'"));
|
|
});
|
|
|
|
test('no repeats', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const CharacterActivator('A', includeRepeats: false)
|
|
.debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(2));
|
|
expect(description[0], equals("character: 'A'"));
|
|
expect(description[1], equals('excluding repeats'));
|
|
});
|
|
|
|
test('combination', () {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const CharacterActivator('A',
|
|
control: true,
|
|
meta: true,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties.where((DiagnosticsNode node) {
|
|
return !node.isFiltered(DiagnosticLevel.info);
|
|
}).map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description.length, equals(1));
|
|
expect(description[0], equals("character: Control + Meta + 'A'"));
|
|
});
|
|
});
|
|
});
|
|
|
|
group('CallbackShortcuts', () {
|
|
testWidgetsWithLeakTracking('trigger on key events', (WidgetTester tester) async {
|
|
int invokedA = 0;
|
|
int invokedB = 0;
|
|
await tester.pumpWidget(
|
|
CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): () {
|
|
invokedA += 1;
|
|
},
|
|
const SingleActivator(LogicalKeyboardKey.keyB): () {
|
|
invokedB += 1;
|
|
},
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.pump();
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
invokedA = 0;
|
|
invokedB = 0;
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('nested CallbackShortcuts stop propagation', (WidgetTester tester) async {
|
|
int invokedOuter = 0;
|
|
int invokedInner = 0;
|
|
await tester.pumpWidget(
|
|
CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): () {
|
|
invokedOuter += 1;
|
|
},
|
|
},
|
|
child: CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): () {
|
|
invokedInner += 1;
|
|
},
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('non-overlapping nested CallbackShortcuts fire appropriately', (WidgetTester tester) async {
|
|
int invokedOuter = 0;
|
|
int invokedInner = 0;
|
|
await tester.pumpWidget(
|
|
CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const CharacterActivator('b'): () {
|
|
invokedOuter += 1;
|
|
},
|
|
},
|
|
child: CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const CharacterActivator('a'): () {
|
|
invokedInner += 1;
|
|
},
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedOuter, equals(1));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedOuter, equals(1));
|
|
expect(invokedInner, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Works correctly with Shortcuts too', (WidgetTester tester) async {
|
|
int invokedCallbackA = 0;
|
|
int invokedCallbackB = 0;
|
|
int invokedActionA = 0;
|
|
int invokedActionB = 0;
|
|
|
|
void clear() {
|
|
invokedCallbackA = 0;
|
|
invokedCallbackB = 0;
|
|
invokedActionA = 0;
|
|
invokedActionB = 0;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invokedActionA += 1;
|
|
return true;
|
|
},
|
|
),
|
|
TestIntent2: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invokedActionB += 1;
|
|
return true;
|
|
},
|
|
),
|
|
},
|
|
child: CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const CharacterActivator('b'): () {
|
|
invokedCallbackB += 1;
|
|
},
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: <LogicalKeySet, Intent>{
|
|
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
|
|
LogicalKeySet(LogicalKeyboardKey.keyB): const TestIntent2(),
|
|
},
|
|
child: CallbackShortcuts(
|
|
bindings: <ShortcutActivator, VoidCallback>{
|
|
const CharacterActivator('a'): () {
|
|
invokedCallbackA += 1;
|
|
},
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedCallbackA, equals(1));
|
|
expect(invokedCallbackB, equals(0));
|
|
expect(invokedActionA, equals(0));
|
|
expect(invokedActionB, equals(0));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
clear();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedCallbackA, equals(0));
|
|
expect(invokedCallbackB, equals(0));
|
|
expect(invokedActionA, equals(0));
|
|
expect(invokedActionB, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
});
|
|
});
|
|
|
|
group('ShortcutRegistrar', () {
|
|
testWidgetsWithLeakTracking('trigger ShortcutRegistrar on key events', (WidgetTester tester) async {
|
|
int invokedA = 0;
|
|
int invokedB = 0;
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() {
|
|
invokedA += 1;
|
|
}),
|
|
const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() {
|
|
invokedB += 1;
|
|
}),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.pump();
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
invokedA = 0;
|
|
invokedB = 0;
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('MaterialApp has a ShortcutRegistrar listening', (WidgetTester tester) async {
|
|
int invokedA = 0;
|
|
int invokedB = 0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() {
|
|
invokedA += 1;
|
|
}),
|
|
const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() {
|
|
invokedB += 1;
|
|
}),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
await tester.pump();
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedA, equals(1));
|
|
expect(invokedB, equals(0));
|
|
invokedA = 0;
|
|
invokedB = 0;
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedA, equals(0));
|
|
expect(invokedB, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking("doesn't override text field shortcuts", (WidgetTester tester) async {
|
|
final TextEditingController controller = TextEditingController();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
|
},
|
|
child: TextField(
|
|
autofocus: true,
|
|
controller: controller,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
controller.text = 'Testing';
|
|
await tester.pump();
|
|
|
|
// Send a "Ctrl-A", which should be bound to select all by default.
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
|
await tester.pump();
|
|
|
|
expect(controller.selection.baseOffset, equals(0));
|
|
expect(controller.selection.extentOffset, equals(7));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('nested ShortcutRegistrars stop propagation', (WidgetTester tester) async {
|
|
int invokedOuter = 0;
|
|
int invokedInner = 0;
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() {
|
|
invokedOuter += 1;
|
|
}),
|
|
},
|
|
child: ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() {
|
|
invokedInner += 1;
|
|
}),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('non-overlapping nested ShortcutRegistrars fire appropriately', (WidgetTester tester) async {
|
|
int invokedOuter = 0;
|
|
int invokedInner = 0;
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const CharacterActivator('b'): VoidCallbackIntent(() {
|
|
invokedOuter += 1;
|
|
}),
|
|
},
|
|
child: ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const CharacterActivator('a'): VoidCallbackIntent(() {
|
|
invokedInner += 1;
|
|
}),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedOuter, equals(0));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedOuter, equals(1));
|
|
expect(invokedInner, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedOuter, equals(1));
|
|
expect(invokedInner, equals(1));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Works correctly with Shortcuts too', (WidgetTester tester) async {
|
|
int invokedCallbackA = 0;
|
|
int invokedCallbackB = 0;
|
|
int invokedActionA = 0;
|
|
int invokedActionB = 0;
|
|
|
|
void clear() {
|
|
invokedCallbackA = 0;
|
|
invokedCallbackB = 0;
|
|
invokedActionA = 0;
|
|
invokedActionB = 0;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invokedActionA += 1;
|
|
return true;
|
|
},
|
|
),
|
|
TestIntent2: TestAction(
|
|
onInvoke: (Intent intent) {
|
|
invokedActionB += 1;
|
|
return true;
|
|
},
|
|
),
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const CharacterActivator('b'): VoidCallbackIntent(() {
|
|
invokedCallbackB += 1;
|
|
}),
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): TestIntent(),
|
|
SingleActivator(LogicalKeyboardKey.keyB): TestIntent2(),
|
|
},
|
|
child: ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
const CharacterActivator('a'): VoidCallbackIntent(() {
|
|
invokedCallbackA += 1;
|
|
}),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
|
expect(invokedCallbackA, equals(1));
|
|
expect(invokedCallbackB, equals(0));
|
|
expect(invokedActionA, equals(0));
|
|
expect(invokedActionB, equals(0));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
|
clear();
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB);
|
|
expect(invokedCallbackA, equals(0));
|
|
expect(invokedCallbackB, equals(0));
|
|
expect(invokedActionA, equals(0));
|
|
expect(invokedActionB, equals(1));
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('Updating shortcuts triggers dependency rebuild', (WidgetTester tester) async {
|
|
final List<Map<ShortcutActivator, Intent>> shortcutsChanged = <Map<ShortcutActivator, Intent>>[];
|
|
void dependenciesUpdated(Map<ShortcutActivator, Intent> shortcuts) {
|
|
shortcutsChanged.add(shortcuts);
|
|
}
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
onDependencyUpdate: dependenciesUpdated,
|
|
shortcuts: const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
|
SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
onDependencyUpdate: dependenciesUpdated,
|
|
shortcuts: const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ShortcutRegistrar(
|
|
child: TestCallbackRegistration(
|
|
onDependencyUpdate: dependenciesUpdated,
|
|
shortcuts: const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
|
SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(),
|
|
},
|
|
child: Actions(
|
|
actions: <Type, Action<Intent>>{
|
|
VoidCallbackIntent: VoidCallbackAction(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: Placeholder(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(shortcutsChanged.length, equals(2));
|
|
expect(shortcutsChanged.last, equals(const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard),
|
|
SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(),
|
|
}));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('using a disposed token asserts', (WidgetTester tester) async {
|
|
final ShortcutRegistry registry = ShortcutRegistry();
|
|
addTearDown(registry.dispose);
|
|
final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(),
|
|
});
|
|
token.dispose();
|
|
expect(() {token.replaceAll(<ShortcutActivator, Intent>{}); }, throwsFlutterError);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('setting duplicate bindings asserts', (WidgetTester tester) async {
|
|
final ShortcutRegistry registry = ShortcutRegistry();
|
|
addTearDown(registry.dispose);
|
|
final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(),
|
|
});
|
|
expect(() {
|
|
final ShortcutRegistryEntry token2 = registry.addAll(const <ShortcutActivator, Intent>{
|
|
SingleActivator(LogicalKeyboardKey.keyA): ActivateIntent(),
|
|
});
|
|
token2.dispose();
|
|
}, throwsAssertionError);
|
|
token.dispose();
|
|
});
|
|
|
|
test('dispatches object creation in constructor', () async {
|
|
await expectLater(
|
|
await memoryEvents(() => ShortcutRegistry().dispose(), ShortcutRegistry),
|
|
areCreateAndDispose,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
class TestCallbackRegistration extends StatefulWidget {
|
|
const TestCallbackRegistration({super.key, required this.shortcuts, this.onDependencyUpdate, required this.child});
|
|
|
|
final Map<ShortcutActivator, Intent> shortcuts;
|
|
final void Function(Map<ShortcutActivator, Intent> shortcuts)? onDependencyUpdate;
|
|
final Widget child;
|
|
|
|
@override
|
|
State<TestCallbackRegistration> createState() => _TestCallbackRegistrationState();
|
|
}
|
|
|
|
class _TestCallbackRegistrationState extends State<TestCallbackRegistration> {
|
|
ShortcutRegistryEntry? _registryToken;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_registryToken?.dispose();
|
|
_registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(TestCallbackRegistration oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.shortcuts != oldWidget.shortcuts || _registryToken == null) {
|
|
_registryToken?.dispose();
|
|
_registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts);
|
|
}
|
|
widget.onDependencyUpdate?.call(ShortcutRegistry.of(context).shortcuts);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_registryToken?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return widget.child;
|
|
}
|
|
}
|
|
|
|
class TestAction extends CallbackAction<Intent> {
|
|
TestAction({
|
|
required super.onInvoke,
|
|
});
|
|
}
|
|
|
|
/// An activator that accepts down events that has [key] as the logical key.
|
|
///
|
|
/// This class is used only to tests. It is intentionally designed poorly by
|
|
/// returning null in [triggers], and checks [key] in [accepts].
|
|
class DumbLogicalActivator extends ShortcutActivator {
|
|
const DumbLogicalActivator(this.key);
|
|
|
|
final LogicalKeyboardKey key;
|
|
|
|
@override
|
|
Iterable<LogicalKeyboardKey>? get triggers => null;
|
|
|
|
@override
|
|
bool accepts(RawKeyEvent event, RawKeyboard state) {
|
|
return event is RawKeyDownEvent
|
|
&& event.logicalKey == key;
|
|
}
|
|
|
|
/// Returns a short and readable description of the key combination.
|
|
///
|
|
/// Intended to be used in debug mode for logging purposes. In release mode,
|
|
/// [debugDescribeKeys] returns an empty string.
|
|
@override
|
|
String debugDescribeKeys() {
|
|
String result = '';
|
|
assert(() {
|
|
result = key.keyLabel;
|
|
return true;
|
|
}());
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class TestIntent extends Intent {
|
|
const TestIntent();
|
|
}
|
|
|
|
class TestIntent2 extends Intent {
|
|
const TestIntent2();
|
|
}
|
|
|
|
class TestShortcutManager extends ShortcutManager {
|
|
TestShortcutManager(this.keys, { super.shortcuts });
|
|
|
|
List<LogicalKeyboardKey> keys;
|
|
|
|
@override
|
|
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
|
|
if (event is RawKeyDownEvent) {
|
|
keys.add(event.logicalKey);
|
|
}
|
|
return super.handleKeypress(context, event);
|
|
}
|
|
}
|
|
|
|
Widget activatorTester(
|
|
ShortcutActivator activator,
|
|
ValueSetter<Intent> onInvoke, [
|
|
ShortcutActivator? activator2,
|
|
ValueSetter<Intent>? onInvoke2,
|
|
]) {
|
|
final bool hasSecond = activator2 != null && onInvoke2 != null;
|
|
return Actions(
|
|
key: GlobalKey(),
|
|
actions: <Type, Action<Intent>>{
|
|
TestIntent: TestAction(onInvoke: (Intent intent) {
|
|
onInvoke(intent);
|
|
return true;
|
|
}),
|
|
if (hasSecond)
|
|
TestIntent2: TestAction(onInvoke: (Intent intent) {
|
|
onInvoke2(intent);
|
|
return null;
|
|
}),
|
|
},
|
|
child: Shortcuts(
|
|
shortcuts: <ShortcutActivator, Intent>{
|
|
activator: const TestIntent(),
|
|
if (hasSecond)
|
|
activator2: const TestIntent2(),
|
|
},
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: SizedBox(width: 100, height: 100),
|
|
),
|
|
),
|
|
);
|
|
}
|