blob: b2e6981d2b99a24ba718e60b946d1272d5e0d2ab [file] [log] [blame]
// Copyright 2013 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 <XCTest/XCTest.h>
#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
#import "third_party/ocmock/Source/OCMock/OCMock.h"
FLUTTER_ASSERT_ARC
@interface FlutterTextInputPluginTest : XCTestCase
@end
@interface FlutterTextInputView ()
- (void)setTextInputState:(NSDictionary*)state;
@end
@implementation FlutterTextInputPluginTest {
id engine;
FlutterTextInputPlugin* textInputPlugin;
}
- (void)setUp {
[super setUp];
engine = OCMClassMock([FlutterEngine class]);
textInputPlugin = [[FlutterTextInputPlugin alloc] init];
textInputPlugin.textInputDelegate = engine;
}
- (void)tearDown {
[engine stopMocking];
[[[[textInputPlugin textInputView] superview] subviews]
makeObjectsPerformSelector:@selector(removeFromSuperview)];
[super tearDown];
}
- (void)testSecureInput {
NSDictionary* config = @{
@"inputType" : @{@"name" : @"TextInuptType.text"},
@"keyboardAppearance" : @"Brightness.light",
@"obscureText" : @YES,
@"inputAction" : @"TextInputAction.unspecified",
@"smartDashesType" : @"0",
@"smartQuotesType" : @"0",
@"autocorrect" : @YES
};
FlutterMethodCall* setClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @123, config ]];
[textInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];
// Find all the FlutterTextInputViews we created.
NSArray<FlutterTextInputView*>* inputFields = [[[[textInputPlugin textInputView] superview]
subviews]
filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@",
[FlutterTextInputView class]]];
// There are no autofill and the mock framework requested a secure entry. The first and only
// inserted FlutterTextInputView should be a secure text entry one.
FlutterTextInputView* inputView = inputFields[0];
// Verify secureTextEntry is set to the correct value.
XCTAssertTrue(inputView.secureTextEntry);
// We should have only ever created one FlutterTextInputView.
XCTAssertEqual(inputFields.count, 1);
// The one FlutterTextInputView we inserted into the view hierarchy should be the text input
// plugin's active text input view.
XCTAssertEqual(inputView, textInputPlugin.textInputView);
}
- (void)testTextChangesTriggerUpdateEditingClient {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
[inputView.text setString:@"BEFORE"];
inputView.markedTextRange = nil;
inputView.selectedTextRange = nil;
// Text changes trigger update.
[inputView setTextInputState:@{@"text" : @"AFTER"}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
// Don't send anything if there's nothing new.
[inputView setTextInputState:@{@"text" : @"AFTER"}];
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
}
- (void)testSelectionChangeTriggersUpdateEditingClient {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
[inputView.text setString:@"SELECTION"];
inputView.markedTextRange = nil;
inputView.selectedTextRange = nil;
[inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
[inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
[inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
// Don't send anything if there's nothing new.
[inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}];
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
}
- (void)testComposingChangeTriggersUpdateEditingClient {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
// Reset to test marked text.
[inputView.text setString:@"COMPOSING"];
inputView.markedTextRange = nil;
inputView.selectedTextRange = nil;
[inputView
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @0, @"composingExtent" : @3}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
[inputView
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
[inputView
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil]]);
// Don't send anything if there's nothing new.
[inputView
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
}
- (void)testUpdateEditingClientNegativeSelection {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
[inputView.text setString:@"SELECTION"];
inputView.markedTextRange = nil;
inputView.selectedTextRange = nil;
[inputView setTextInputState:@{
@"text" : @"SELECTION",
@"selectionBase" : @-1,
@"selectionExtent" : @-1
}];
OCMVerify([engine updateEditingClient:0
withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
return ([state[@"selectionBase"] intValue]) == 0 &&
([state[@"selectionExtent"] intValue] == 0);
}]]);
}
- (void)testAutofillInputViews {
NSDictionary* template = @{
@"inputType" : @{@"name" : @"TextInuptType.text"},
@"keyboardAppearance" : @"Brightness.light",
@"obscureText" : @NO,
@"inputAction" : @"TextInputAction.unspecified",
@"smartDashesType" : @"0",
@"smartQuotesType" : @"0",
@"autocorrect" : @YES
};
NSMutableDictionary* field1 = [template mutableCopy];
[field1 setValue:@{
@"uniqueIdentifier" : @"field1",
@"hints" : @[ @"hint1" ],
@"editingValue" : @{@"text" : @""}
}
forKey:@"autofill"];
NSMutableDictionary* field2 = [template mutableCopy];
[field2 setValue:@{
@"uniqueIdentifier" : @"field2",
@"hints" : @[ @"hint2" ],
@"editingValue" : @{@"text" : @""}
}
forKey:@"autofill"];
NSMutableDictionary* config = [field1 mutableCopy];
[config setValue:@[ field1, field2 ] forKey:@"fields"];
FlutterMethodCall* setClientCall =
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @123, config ]];
[textInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];
// Find all the FlutterTextInputViews we created.
NSArray<FlutterTextInputView*>* inputFields = [[[[textInputPlugin textInputView] superview]
subviews]
filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@",
[FlutterTextInputView class]]];
XCTAssertEqual(inputFields.count, 2);
// Find the inactive autofillable input field.
FlutterTextInputView* inactiveView = inputFields[1];
[inactiveView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]
withText:@"Autofilled!"];
// Verify behavior.
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil] withTag:@"field2"]);
}
- (void)testAutocorrectionPromptRectAppears {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero];
inputView.textInputDelegate = engine;
[inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
// Verify behavior.
OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]);
}
- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero];
FlutterTextPosition* fromPosition = [[FlutterTextPosition alloc] initWithIndex:2];
FlutterTextPosition* toPosition = [[FlutterTextPosition alloc] initWithIndex:0];
FlutterTextRange* flutterRange = (FlutterTextRange*)[inputView textRangeFromPosition:fromPosition
toPosition:toPosition];
NSRange range = flutterRange.range;
XCTAssertEqual(range.location, 0);
XCTAssertEqual(range.length, 2);
}
- (void)testUITextInputCallsUpdateEditingStateOnce {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
__block int updateCount = 0;
OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]])
.andDo(^(NSInvocation* invocation) {
updateCount++;
});
[inputView insertText:@"text to insert"];
// Update the framework exactly once.
XCTAssertEqual(updateCount, 1);
[inputView deleteBackward];
XCTAssertEqual(updateCount, 2);
inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
XCTAssertEqual(updateCount, 3);
[inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]
withText:@"replace text"];
XCTAssertEqual(updateCount, 4);
[inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
XCTAssertEqual(updateCount, 5);
[inputView unmarkText];
XCTAssertEqual(updateCount, 6);
}
@end