blob: 6cbb3a377c1c9fb627683cd18aaa615319031263 [file] [log] [blame]
// 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 'editable_text_utils.dart';
final FocusNode _focusNode = FocusNode(debugLabel: 'UndoHistory Node');
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('UndoHistory', () {
Future<void> sendUndoRedo(WidgetTester tester, [bool redo = false]) {
return sendKeys(
tester,
<LogicalKeyboardKey>[
LogicalKeyboardKey.keyZ,
],
shortcutModifier: true,
shift: redo,
targetPlatform: defaultTargetPlatform,
);
}
Future<void> sendUndo(WidgetTester tester) => sendUndoRedo(tester);
Future<void> sendRedo(WidgetTester tester) => sendUndoRedo(tester, true);
testWidgets('allows undo and redo to be called programmatically from the UndoHistoryController', (WidgetTester tester) async {
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
value: value,
controller: controller,
onTriggered: (int newValue) {
value.value = newValue;
},
focusNode: _focusNode,
child: Container(),
),
),
);
await tester.pump(const Duration(milliseconds: 500));
// Undo/redo have no effect if the value has never changed.
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
controller.redo();
expect(value.value, 0);
_focusNode.requestFocus();
await tester.pump();
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
controller.redo();
expect(value.value, 0);
value.value = 1;
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Can undo/redo a single change.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
controller.redo();
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
value.value = 2;
await tester.pump(const Duration(milliseconds: 500));
// And can undo/redo multiple changes.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
controller.undo();
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
controller.redo();
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
controller.redo();
expect(value.value, 2);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
// Changing the value again clears the redo stack.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
value.value = 3;
await tester.pump(const Duration(milliseconds: 500));
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
}, variant: TargetPlatformVariant.all());
testWidgets('allows undo and redo to be called using the keyboard', (WidgetTester tester) async {
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
controller: controller,
value: value,
onTriggered: (int newValue) {
value.value = newValue;
},
focusNode: _focusNode,
child: Focus(
focusNode: _focusNode,
child: Container(),
),
),
),
);
await tester.pump(const Duration(milliseconds: 500));
// Undo/redo have no effect if the value has never changed.
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
await sendUndo(tester);
expect(value.value, 0);
await sendRedo(tester);
expect(value.value, 0);
_focusNode.requestFocus();
await tester.pump();
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
await sendUndo(tester);
expect(value.value, 0);
await sendRedo(tester);
expect(value.value, 0);
value.value = 1;
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Can undo/redo a single change.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
await sendUndo(tester);
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
await sendRedo(tester);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
value.value = 2;
await tester.pump(const Duration(milliseconds: 500));
// And can undo/redo multiple changes.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
await sendUndo(tester);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
await sendUndo(tester);
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
await sendRedo(tester);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
await sendRedo(tester);
expect(value.value, 2);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
// Changing the value again clears the redo stack.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
await sendUndo(tester);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
value.value = 3;
await tester.pump(const Duration(milliseconds: 500));
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
testWidgets('duplicate changes do not affect the undo history', (WidgetTester tester) async {
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
controller: controller,
value: value,
onTriggered: (int newValue) {
value.value = newValue;
},
focusNode: _focusNode,
child: Container(),
),
),
);
_focusNode.requestFocus();
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
value.value = 1;
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Can undo/redo a single change.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
controller.redo();
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
// Changes that result in the same state won't be saved on the undo stack.
value.value = 1;
await tester.pump(const Duration(milliseconds: 500));
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
}, variant: TargetPlatformVariant.all());
testWidgets('ignores value changes pushed during onTriggered', (WidgetTester tester) async {
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
int Function(int newValue) valueToUse = (int value) => value;
final GlobalKey<UndoHistoryState<int>> key = GlobalKey<UndoHistoryState<int>>();
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
key: key,
value: value,
controller: controller,
onTriggered: (int newValue) {
value.value = valueToUse(newValue);
},
focusNode: _focusNode,
child: Container(),
),
),
);
await tester.pump(const Duration(milliseconds: 500));
// Undo/redo have no effect if the value has never changed.
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
controller.redo();
expect(value.value, 0);
_focusNode.requestFocus();
await tester.pump();
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
controller.undo();
expect(value.value, 0);
controller.redo();
expect(value.value, 0);
value.value = 1;
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
valueToUse = (int value) => 3;
expect(() => key.currentState!.undo(), throwsAssertionError);
}, variant: TargetPlatformVariant.all());
testWidgets('changes should send setUndoState to the UndoManagerConnection on iOS', (WidgetTester tester) async {
final List<MethodCall> log = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.undoManager, (MethodCall methodCall) async {
log.add(methodCall);
return null;
});
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
controller: controller,
value: value,
onTriggered: (int newValue) {
value.value = newValue;
},
focusNode: focusNode,
child: Focus(
focusNode: focusNode,
child: Container(),
),
),
),
);
await tester.pump();
focusNode.requestFocus();
await tester.pump();
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Undo and redo should both be disabled.
MethodCall methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
expect(methodCall.method, 'UndoManager.setUndoState');
expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': false, 'canRedo': false});
// Making a change should enable undo.
value.value = 1;
await tester.pump(const Duration(milliseconds: 500));
methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
expect(methodCall.method, 'UndoManager.setUndoState');
expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': false});
// Undo should remain enabled after another change.
value.value = 2;
await tester.pump(const Duration(milliseconds: 500));
methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
expect(methodCall.method, 'UndoManager.setUndoState');
expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': false});
// Undo and redo should be enabled after one undo.
controller.undo();
methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
expect(methodCall.method, 'UndoManager.setUndoState');
expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': true});
// Only redo should be enabled after a second undo.
controller.undo();
methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
expect(methodCall.method, 'UndoManager.setUndoState');
expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': false, 'canRedo': true});
}, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), skip: kIsWeb); // [intended]
testWidgets('handlePlatformUndo should undo or redo appropriately on iOS', (WidgetTester tester) async {
final ValueNotifier<int> value = ValueNotifier<int>(0);
addTearDown(value.dispose);
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: UndoHistory<int>(
controller: controller,
value: value,
onTriggered: (int newValue) {
value.value = newValue;
},
focusNode: _focusNode,
child: Focus(
focusNode: _focusNode,
child: Container(),
),
),
),
);
await tester.pump(const Duration(milliseconds: 500));
_focusNode.requestFocus();
await tester.pump();
// Undo/redo have no effect if the value has never changed.
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, false);
UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
expect(value.value, 0);
UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
expect(value.value, 0);
value.value = 1;
// Wait for the throttling.
await tester.pump(const Duration(milliseconds: 500));
// Can undo/redo a single change.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
value.value = 2;
await tester.pump(const Duration(milliseconds: 500));
// And can undo/redo multiple changes.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
expect(value.value, 0);
expect(controller.value.canUndo, false);
expect(controller.value.canRedo, true);
UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
expect(value.value, 2);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
// Changing the value again clears the redo stack.
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
expect(value.value, 1);
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, true);
value.value = 3;
await tester.pump(const Duration(milliseconds: 500));
expect(controller.value.canUndo, true);
expect(controller.value.canRedo, false);
}, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), skip: kIsWeb); // [intended]
});
group('UndoHistoryController', () {
testWidgets('UndoHistoryController notifies onUndo listeners onUndo', (WidgetTester tester) async {
int calls = 0;
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
controller.onUndo.addListener(() {
calls++;
});
// Does not notify the listener if canUndo is false.
controller.undo();
expect(calls, 0);
// Does notify the listener if canUndo is true.
controller.value = const UndoHistoryValue(canUndo: true);
controller.undo();
expect(calls, 1);
});
testWidgets('UndoHistoryController notifies onRedo listeners onRedo', (WidgetTester tester) async {
int calls = 0;
final UndoHistoryController controller = UndoHistoryController();
addTearDown(controller.dispose);
controller.onRedo.addListener(() {
calls++;
});
// Does not notify the listener if canUndo is false.
controller.redo();
expect(calls, 0);
// Does notify the listener if canRedo is true.
controller.value = const UndoHistoryValue(canRedo: true);
controller.redo();
expect(calls, 1);
});
testWidgets('UndoHistoryController notifies listeners on value change', (WidgetTester tester) async {
int calls = 0;
final UndoHistoryController controller = UndoHistoryController(value: const UndoHistoryValue(canUndo: true));
addTearDown(controller.dispose);
controller.addListener(() {
calls++;
});
// Does not notify if the value is the same.
controller.value = const UndoHistoryValue(canUndo: true);
expect(calls, 0);
// Does notify if the value has changed.
controller.value = const UndoHistoryValue(canRedo: true);
expect(calls, 1);
});
});
}