Flutter iOS Embedder
FlutterUndoManagerPluginTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #import <OCMock/OCMock.h>
8 #import <XCTest/XCTest.h>
9 
11 
13 
14 /// OCMock does not allow mocking both class and protocol. Use this to mock the methods used on
15 /// `UIView<UITextInput>*` in the plugin.
16 @interface TextInputViewTest : NSObject
17 
18 @property(nonatomic, weak) id<UITextInputDelegate> inputDelegate;
19 @property(nonatomic, readonly) UITextInputAssistantItem* inputAssistantItem;
20 
21 @end
22 
23 @implementation TextInputViewTest
24 @end
25 
27 
28 @property(readonly) NSUInteger undoCount;
29 @property(readonly) NSUInteger redoCount;
30 @property(nonatomic, nullable) NSUndoManager* undoManager;
31 
32 - (instancetype)initWithUndoManager:(NSUndoManager*)undoManager
33  activeTextInputView:(TextInputViewTest*)activeTextInputView;
34 
35 @end
36 
37 @implementation FakeFlutterUndoManagerDelegate
38 
39 @synthesize undoManager = _undoManager;
40 @synthesize activeTextInputView = _activeTextInputView;
41 
42 - (instancetype)initWithUndoManager:(NSUndoManager*)undoManager
43  activeTextInputView:(UIView<UITextInput>*)activeTextInputView {
44  self = [super init];
45  if (self) {
46  _undoManager = undoManager;
47  _activeTextInputView = activeTextInputView;
48  }
49  return self;
50 }
51 
52 - (void)handleUndoWithDirection:(FlutterUndoRedoDirection)direction {
53  if (direction == FlutterUndoRedoDirectionUndo) {
54  _undoCount++;
55  } else {
56  _redoCount++;
57  }
58 }
59 
60 @end
61 
62 @interface FlutterUndoManagerPluginTest : XCTestCase
63 @property(nonatomic) FakeFlutterUndoManagerDelegate* undoManagerDelegate;
64 @property(nonatomic) FlutterUndoManagerPlugin* undoManagerPlugin;
65 @property(nonatomic) TextInputViewTest* activeTextInputView;
66 @property(nonatomic) NSUndoManager* undoManager;
67 @end
68 
69 @implementation FlutterUndoManagerPluginTest
70 
71 - (void)setUp {
72  [super setUp];
73 
74  self.undoManager = OCMClassMock([NSUndoManager class]);
75  self.activeTextInputView = OCMClassMock([TextInputViewTest class]);
76 
77  self.undoManagerDelegate =
78  [[FakeFlutterUndoManagerDelegate alloc] initWithUndoManager:self.undoManager
79  activeTextInputView:self.activeTextInputView];
80 
81  self.undoManagerPlugin =
82  [[FlutterUndoManagerPlugin alloc] initWithDelegate:self.undoManagerDelegate];
83 }
84 
85 - (void)testSetUndoState {
86  __block int registerUndoCount = 0;
87  __block void (^undoHandler)(id target);
88  OCMStub([self.undoManager registerUndoWithTarget:self.undoManagerPlugin handler:[OCMArg any]])
89  .andDo(^(NSInvocation* invocation) {
90  registerUndoCount++;
91  __weak void (^handler)(id target);
92  [invocation retainArguments];
93  [invocation getArgument:&handler atIndex:3];
94  undoHandler = handler;
95  });
96  __block int removeAllActionsCount = 0;
97  OCMStub([self.undoManager removeAllActionsWithTarget:self.undoManagerPlugin])
98  .andDo(^(NSInvocation* invocation) {
99  removeAllActionsCount++;
100  });
101  __block int undoCount = 0;
102  OCMStub([self.undoManager undo]).andDo(^(NSInvocation* invocation) {
103  undoCount++;
104  undoHandler(self.undoManagerPlugin);
105  });
106 
107  // If canUndo and canRedo are false, only removeAllActionsWithTarget is called.
108  FlutterMethodCall* setUndoStateCall =
109  [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState"
110  arguments:@{@"canUndo" : @NO, @"canRedo" : @NO}];
111  [self.undoManagerPlugin handleMethodCall:setUndoStateCall
112  result:^(id _Nullable result){
113  }];
114  XCTAssertEqual(1, removeAllActionsCount);
115  XCTAssertEqual(0, registerUndoCount);
116 
117  // If canUndo is true, an undo will be registered.
118  setUndoStateCall =
119  [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState"
120  arguments:@{@"canUndo" : @YES, @"canRedo" : @NO}];
121  [self.undoManagerPlugin handleMethodCall:setUndoStateCall
122  result:^(id _Nullable result){
123  }];
124  XCTAssertEqual(2, removeAllActionsCount);
125  XCTAssertEqual(1, registerUndoCount);
126 
127  // Invoking the undo handler will invoke the handleUndo delegate method with "undo".
128  undoHandler(self.undoManagerPlugin);
129  XCTAssertEqual(1UL, self.undoManagerDelegate.undoCount);
130  XCTAssertEqual(0UL, self.undoManagerDelegate.redoCount);
131  XCTAssertEqual(2, registerUndoCount);
132 
133  // Invoking the redo handler will invoke the handleUndo delegate method with "redo".
134  undoHandler(self.undoManagerPlugin);
135  XCTAssertEqual(1UL, self.undoManagerDelegate.undoCount);
136  XCTAssertEqual(1UL, self.undoManagerDelegate.redoCount);
137  XCTAssertEqual(3, registerUndoCount);
138 
139  // If canRedo is true, an undo will be registered and undo will be called.
140  setUndoStateCall =
141  [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState"
142  arguments:@{@"canUndo" : @NO, @"canRedo" : @YES}];
143  [self.undoManagerPlugin handleMethodCall:setUndoStateCall
144  result:^(id _Nullable result){
145  }];
146  XCTAssertEqual(3, removeAllActionsCount);
147  XCTAssertEqual(5, registerUndoCount);
148  XCTAssertEqual(1, undoCount);
149 
150  // Invoking the redo handler will invoke the handleUndo delegate method with "redo".
151  undoHandler(self.undoManagerPlugin);
152  XCTAssertEqual(1UL, self.undoManagerDelegate.undoCount);
153  XCTAssertEqual(2UL, self.undoManagerDelegate.redoCount);
154 }
155 
156 - (void)testSetUndoStateDoesInteractWithInputDelegate {
157  // Regression test for https://github.com/flutter/flutter/issues/133424
158  FlutterMethodCall* setUndoStateCall =
159  [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState"
160  arguments:@{@"canUndo" : @NO, @"canRedo" : @NO}];
161  [self.undoManagerPlugin handleMethodCall:setUndoStateCall
162  result:^(id _Nullable result){
163  }];
164 
165  OCMVerify(never(), [self.activeTextInputView inputDelegate]);
166 }
167 
168 - (void)testDeallocRemovesAllUndoManagerActions {
169  __weak FlutterUndoManagerPlugin* weakUndoManagerPlugin;
170  // Use a real undo manager.
171  NSUndoManager* undoManager = [[NSUndoManager alloc] init];
172  @autoreleasepool {
173  id activeTextInputView = OCMClassMock([TextInputViewTest class]);
174 
176  [[FakeFlutterUndoManagerDelegate alloc] initWithUndoManager:undoManager
177  activeTextInputView:activeTextInputView];
178 
180  [[FlutterUndoManagerPlugin alloc] initWithDelegate:undoManagerDelegate];
181  weakUndoManagerPlugin = undoManagerPlugin;
182 
183  FlutterMethodCall* setUndoStateCall =
184  [FlutterMethodCall methodCallWithMethodName:@"UndoManager.setUndoState"
185  arguments:@{@"canUndo" : @YES, @"canRedo" : @YES}];
186  [undoManagerPlugin handleMethodCall:setUndoStateCall
187  result:^(id _Nullable result){
188  }];
189  XCTAssertTrue(undoManager.canUndo);
190  XCTAssertTrue(undoManager.canRedo);
191  // Fake out the undoManager being nil, which happens when the FlutterViewController deallocs and
192  // the undo manager can't be fetched from the FlutterEngine delegate.
194  }
195  XCTAssertNil(weakUndoManagerPlugin);
196  // Regression test for https://github.com/flutter/flutter/issues/150408.
197  // Undo manager undo and redo stack should be empty after the plugin deallocs.
198  XCTAssertFalse(undoManager.canUndo);
199  XCTAssertFalse(undoManager.canRedo);
200 }
201 
202 @end
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterUndoManagerPluginTest::undoManagerDelegate
FakeFlutterUndoManagerDelegate * undoManagerDelegate
Definition: FlutterUndoManagerPluginTest.mm:63
FakeFlutterUndoManagerDelegate::undoManager
NSUndoManager * undoManager
Definition: FlutterUndoManagerPluginTest.mm:30
FlutterUndoManagerPlugin.h
FakeFlutterUndoManagerDelegate::redoCount
NSUInteger redoCount
Definition: FlutterUndoManagerPluginTest.mm:29
FlutterMacros.h
FlutterUndoManagerPluginTest::undoManagerPlugin
FlutterUndoManagerPlugin * undoManagerPlugin
Definition: FlutterUndoManagerPluginTest.mm:64
FlutterUndoManagerPluginTest::undoManager
NSUndoManager * undoManager
Definition: FlutterUndoManagerPluginTest.mm:66
FlutterUndoManagerDelegate-p::activeTextInputView
UIView< UITextInput > * activeTextInputView
Definition: FlutterUndoManagerDelegate.h:35
FakeFlutterUndoManagerDelegate::undoCount
NSUInteger undoCount
Definition: FlutterUndoManagerPluginTest.mm:28
-[FlutterUndoManagerPlugin handleMethodCall:result:]
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
Definition: FlutterUndoManagerPlugin.mm:38
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterUndoManagerPluginTest
Definition: FlutterUndoManagerPluginTest.mm:62
FakeFlutterUndoManagerDelegate
Definition: FlutterUndoManagerPluginTest.mm:26
FlutterUndoManagerDelegate-p
Definition: FlutterUndoManagerDelegate.h:23
FlutterUndoManagerPlugin
Definition: FlutterUndoManagerPlugin.h:13
TextInputViewTest::inputAssistantItem
UITextInputAssistantItem * inputAssistantItem
Definition: FlutterUndoManagerPluginTest.mm:19
TextInputViewTest::inputDelegate
id< UITextInputDelegate > inputDelegate
Definition: FlutterUndoManagerPluginTest.mm:18
TextInputViewTest
Definition: FlutterUndoManagerPluginTest.mm:16
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
FlutterUndoManagerPluginTest::activeTextInputView
TextInputViewTest * activeTextInputView
Definition: FlutterUndoManagerPluginTest.mm:65