blob: 499753309cbd6312ded3db776bbf5b331c417e88 [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import "mac/GoogleJapaneseInputController.h"
#import <Cocoa/Cocoa.h>
#import "mac/GoogleJapaneseInputControllerInterface.h"
#import "mac/KeyCodeMap.h"
#include <objc/objc-class.h>
#include "base/logging.h"
#include "base/mac_util.h"
#include "base/util.h"
#include "client/client_mock.h"
#include "renderer/renderer_interface.h"
#include "testing/base/public/googletest.h"
#include "testing/base/public/gunit.h"
@interface MockIMKServer : IMKServer <ServerCallback> {
// The controller which accepts user's clicks
id<ControllerCallback> expectedController_;
int setCurrentController_count_;
}
@property(readwrite, assign) id<ControllerCallback> expectedController;
@end
@implementation MockIMKServer
@synthesize expectedController = expectedController_;
- (MockIMKServer*)init {
self = [super init];
expectedController_ = nil;
setCurrentController_count_ = 0;
return self;
}
- (void)sendData:(NSData *)data {
// Do nothing
}
- (void)outputResult:(NSData *)data {
// Do nothing
}
- (void)setCurrentController:(id<ControllerCallback>)controller {
EXPECT_EQ(expectedController_, controller);
++setCurrentController_count_;
}
@end
@interface MockClient : NSObject {
NSString *bundleIdentifier;
NSRect expectedCursor;
NSRange expectedRange;
string *selectedMode_;
NSString *insertedText_;
NSString *overriddenLayout_;
NSAttributedString *attributedString_;
map<string, int> *counters_;
}
@property(readwrite, assign) NSString *bundleIdentifier;
@property(readwrite, assign) NSRect expectedCursor;
@property(readwrite, assign) NSRange expectedRange;
@property(readonly) string *selectedMode;
@property(readonly) NSString *insertedText;
@property(readonly) NSString *overriddenLayout;
@end
@implementation MockClient
@synthesize bundleIdentifier;
@synthesize expectedCursor;
@synthesize expectedRange;
@synthesize selectedMode = selectedMode_;
@synthesize insertedText = insertedText_;
@synthesize overriddenLayout = overriddenLayout_;
- (MockClient *)init {
self = [super init];
self.bundleIdentifier = @"com.google.exampleBundle";
expectedRange = NSMakeRange(NSNotFound, NSNotFound);
counters_ = new map<string, int>;
selectedMode_ = new string;
return self;
}
- (void)dealloc {
delete counters_;
delete selectedMode_;
[bundleIdentifier release];
[insertedText_ release];
[overriddenLayout_ release];
[attributedString_ release];
[super dealloc];
}
- (int)getCounter:(const char *)selector {
return (*counters_)[selector];
}
- (NSConnection *)connectionForProxy {
return nil;
}
- (NSDictionary *)attributesForCharacterIndex:(int)index
lineHeightRectangle:(NSRect *)rect {
(*counters_)["attributesForCharacterIndex:lineHeightRectangle:"]++;
*rect = expectedCursor;
return nil;
}
- (NSRange)selectedRange {
(*counters_)["selectedRange"]++;
return expectedRange;
}
- (NSRange)markedRange {
(*counters_)["markedRange"]++;
return expectedRange;
}
- (void)selectInputMode:(NSString *)mode {
(*counters_)["selectInputMode:"]++;
selectedMode_->assign([mode UTF8String]);
}
- (void)setAttributedString:(NSAttributedString *)newString {
[attributedString_ autorelease];
attributedString_ = [newString copy];
}
- (NSInteger)length {
return [attributedString_ length];
}
- (NSAttributedString*)attributedSubstringFromRange:(NSRange)range {
return [attributedString_ attributedSubstringFromRange:range];
}
// a dummy method which is necessary internally
- (void)setMarkedText:(id)string
selectionRange:(NSRange)selectionRange
replacementRange:(NSRange)replacementRange {
// Do nothing
}
- (void)insertText:(NSString *)result replacementRange:(NSRange)range {
(*counters_)["insertText:replacementRange:"]++;
[insertedText_ release];
insertedText_ = [result retain];
}
- (void)overrideKeyboardWithKeyboardNamed:(NSString *)newLayout {
(*counters_)["overrideKeyboardWithKeyboardNamed:"]++;
[overriddenLayout_ release];
overriddenLayout_ = [newLayout retain];
}
@end
@interface MockScreen : NSScreen
+ (NSRect)mockScreen;
@end
@implementation MockScreen
// a dummy frame of 1024x768. This method is used to share size of
// screen among the implementation and test case.
+ (NSRect)mockScreen {
return NSMakeRect(0, 0, 1024, 768);
}
- (NSRect)frame {
return [MockScreen mockScreen];
}
@end
namespace {
const NSTimeInterval kDoubleTapInterval = 0.5;
int openURL_count = 0;
BOOL openURL_test(id self, SEL selector, NSURL *url) {
openURL_count++;
return true;
}
NSArray *dummy_screens(id self, SEL selector) {
return [NSArray arrayWithObject:[[[MockScreen alloc] init] autorelease]];
}
} // anonymous namespace
class MockRenderer : public mozc::renderer::RendererInterface {
public:
MockRenderer()
: Activate_counter_(0),
IsAvailable_counter_(0),
ExecCommand_counter_(0) {
}
virtual bool Activate() {
Activate_counter_++;
return true;
}
int counter_Activate() const {
return Activate_counter_;
}
virtual bool ExecCommand(const mozc::commands::RendererCommand &command) {
ExecCommand_counter_++;
called_command_.CopyFrom(command);
return true;
}
int counter_ExecCommand() const {
return ExecCommand_counter_;
}
const mozc::commands::RendererCommand &CalledCommand() const {
return called_command_;
}
virtual bool IsAvailable() const {
IsAvailable_counter_++;
return true;
}
int counter_IsAvailable() const {
return IsAvailable_counter_;
}
#undef MockMethod
private:
int Activate_counter_;
mutable int IsAvailable_counter_;
int ExecCommand_counter_;
mozc::commands::RendererCommand called_command_;
};
class GoogleJapaneseInputControllerTest : public testing::Test {
protected:
void SetUp() {
pool_ = [[NSAutoreleasePool alloc] init];
mock_server_ = [[MockIMKServer alloc] init];
mock_client_ = [[MockClient alloc] init];
// setup workspace
Method method = class_getInstanceMethod(
[NSWorkspace class], @selector(openURL:));
method_setImplementation(method, reinterpret_cast<IMP>(openURL_test));
openURL_count = 0;
// setup NSScreen
method = class_getClassMethod([NSScreen class], @selector(screens));
method_setImplementation(method, reinterpret_cast<IMP>(dummy_screens));
[GoogleJapaneseInputController initializeConstants];
SetUpController();
}
void TearDown() {
[controller_ release];
[mock_server_ release];
[mock_client_ release];
// mock_renderer is released during the release of |controller_|.
[pool_ release];
}
void SetUpController() {
controller_ = [[GoogleJapaneseInputController alloc]
initWithServer:mock_server_
delegate:nil
client:mock_client_];
controller_.imkClientForTest = mock_client_;
mock_server_.expectedController = controller_;
mock_mozc_client_ = new mozc::client::ClientMock;
controller_.mozcClient = mock_mozc_client_;
mock_renderer_ = new MockRenderer;
controller_.renderer = mock_renderer_;
}
void ResetClientBundleIdentifier(NSString *new_bundle_id) {
[controller_ release];
mock_client_.bundleIdentifier = new_bundle_id;
SetUpController();
}
mozc::client::ClientMock *mock_mozc_client_;
MockClient *mock_client_;
MockRenderer *mock_renderer_;
GoogleJapaneseInputController *controller_;
private:
MockIMKServer *mock_server_;
NSAutoreleasePool *pool_;
};
// Because preedit has NSMarkedClauseSegment attribute which has to
// change for each call of creating preedit, we can't use simple
// isEqualToAttributedString method.
bool ComparePreedit(NSAttributedString *expected, NSAttributedString *actual) {
if ([expected length] != [actual length]) {
LOG(ERROR) << "length is different";
return false;
}
if (![[expected string] isEqualToString:[actual string]]) {
LOG(ERROR) << "content string is different";
return false;
}
// Check the attributes
int cursor = 0;
NSSet *knownAttributeNames =
[NSSet setWithObjects:NSUnderlineStyleAttributeName,
NSUnderlineColorAttributeName,
NSMarkedClauseSegmentAttributeName,
nil];
while (cursor < [expected length]) {
NSRange expectedRange = NSMakeRange(NSNotFound, NSNotFound);
NSRange actualRange = NSMakeRange(NSNotFound, NSNotFound);
NSDictionary *expectedAttributes =
[expected attributesAtIndex:cursor effectiveRange:&expectedRange];
NSDictionary *actualAttributes =
[actual attributesAtIndex:cursor effectiveRange:&actualRange];
// false if attribute over different range.
if (expectedRange.location != actualRange.location ||
expectedRange.length != actualRange.length) {
LOG(ERROR) << "range is different";
return false;
}
// Check attributes for underlines
NSNumber *expectedUnderlineStyle =
[expectedAttributes valueForKey:NSUnderlineStyleAttributeName];
NSNumber *actualUnderlineStyle =
[expectedAttributes valueForKey:NSUnderlineStyleAttributeName];
if (![expectedUnderlineStyle isEqualToNumber:actualUnderlineStyle] ||
actualUnderlineStyle == nil) {
LOG(ERROR) << "underline style is different at range ("
<< expectedRange.location << ", "
<< expectedRange.length << ")";
return false;
}
NSColor *expectedUnderlineColor =
[expectedAttributes valueForKey:NSUnderlineColorAttributeName];
NSColor *actualUnderlineColor =
[actualAttributes valueForKey:NSUnderlineColorAttributeName];
if (![[NSString stringWithFormat:@"%@", expectedUnderlineColor]
isEqualToString:[NSString stringWithFormat:@"%@",
actualUnderlineColor]]) {
LOG(ERROR) << "underline color is different at range ("
<< expectedRange.location << ", "
<< expectedRange.length << ")";
return false;
}
// Check if it contains unknown attributes
for (NSString *key in [expectedAttributes keyEnumerator]) {
if (![knownAttributeNames containsObject:key]) {
LOG(ERROR) << "expected value contains an unknown key: "
<< [key UTF8String];
return false;
}
}
for (NSString *key in [actualAttributes keyEnumerator]) {
if (![knownAttributeNames containsObject:key]) {
LOG(ERROR) << "actual value contains an unknown key: "
<< [key UTF8String];
return false;
}
}
cursor += expectedRange.length;
}
// true if it passes every check
return true;
}
NSTimeInterval GetDoubleTapInterval() {
// TODO(horo): This value should be get from system configuration.
return kDoubleTapInterval;
}
BOOL SendKeyEvent(unsigned short keyCode,
GoogleJapaneseInputController *controller,
MockClient *client) {
// tap Kana-key
NSEvent *kanaKeyEvent = [NSEvent keyEventWithType:NSKeyDown
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
characters:@" "
charactersIgnoringModifiers:@" "
isARepeat:NO
keyCode:keyCode];
return [controller handleEvent:kanaKeyEvent client:client];
}
TEST_F(GoogleJapaneseInputControllerTest, UpdateComposedString) {
// If preedit is NULL, it still calls setMarkedText, with an empty string.
NSMutableAttributedString *expected =
[[[NSMutableAttributedString alloc] initWithString:@""] autorelease];
[controller_ updateComposedString:NULL];
EXPECT_TRUE([expected
isEqualToAttributedString:[controller_ composedString:nil]]);
// If preedit has some data, it returns a data with highlighting information
mozc::commands::Preedit preedit;
mozc::commands::Preedit::Segment *new_segment = preedit.add_segment();
// UNDERLINE segment
new_segment->set_annotation(mozc::commands::Preedit::Segment::UNDERLINE);
new_segment->set_value("a");
new_segment->set_value_length(1);
new_segment = preedit.add_segment();
// Second segment is HIGHLIGHT
new_segment->set_annotation(mozc::commands::Preedit::Segment::HIGHLIGHT);
new_segment->set_value("bc");
new_segment->set_value_length(2);
// Third segment is NONE annotation but it seems same as UNDERLINE
// at this time
new_segment = preedit.add_segment();
new_segment->set_annotation(mozc::commands::Preedit::Segment::NONE);
new_segment->set_value("def");
new_segment->set_value_length(2);
NSDictionary *highlightAttributes =
[controller_ markForStyle:kTSMHiliteSelectedConvertedText
atRange:NSMakeRange(NSNotFound, 0)];
NSDictionary *underlineAttributes =
[controller_ markForStyle:kTSMHiliteConvertedText
atRange:NSMakeRange(NSNotFound, 0)];
expected =
[[[NSMutableAttributedString alloc] initWithString:@"abcdef"]
autorelease];
[expected addAttributes:underlineAttributes range:NSMakeRange(0, 1)];
[expected addAttributes:highlightAttributes range:NSMakeRange(1, 2)];
[expected addAttributes:underlineAttributes range:NSMakeRange(3, 3)];
[controller_ updateComposedString:&preedit];
NSAttributedString *actual = [controller_ composedString:nil];
EXPECT_TRUE(ComparePreedit(expected, actual))
<< [[NSString stringWithFormat:@"expected:%@ actual:%@", expected, actual]
UTF8String];
}
TEST_F(GoogleJapaneseInputControllerTest, ClearCandidates) {
[controller_ clearCandidates];
EXPECT_EQ(1, mock_renderer_->counter_ExecCommand());
// After clearing candidates, the candidate window has to be invisible.
EXPECT_FALSE(mock_renderer_->CalledCommand().visible());
}
TEST_F(GoogleJapaneseInputControllerTest, UpdateCandidates) {
// When output is null, same as ClearCandidate
[controller_ updateCandidates:NULL];
// Run the runloop so "delayedUpdateCandidates" can be called
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
EXPECT_EQ(1, mock_renderer_->counter_ExecCommand());
EXPECT_FALSE(mock_renderer_->CalledCommand().visible());
EXPECT_EQ(0,
[mock_client_
getCounter:"attributesForCharacterIndex:lineHeightRectangle:"]);
// create an output
mozc::commands::Output output;
// a candidate has to have at least a candidate
mozc::commands::Candidates *candidates = output.mutable_candidates();
candidates->set_focused_index(0);
candidates->set_size(1);
mozc::commands::Candidates::Candidate *candidate =
candidates->add_candidate();
candidate->set_index(0);
candidate->set_value("abc");
NSRect screen_rect = [MockScreen mockScreen];
// setup the cursor position
mock_client_.expectedCursor = NSMakeRect(50, 50, 1, 10);
[controller_ updateCandidates:&output];
// Run the runloop so "delayedUpdateCandidates" can be called
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
EXPECT_EQ(1,
[mock_client_
getCounter:"attributesForCharacterIndex:lineHeightRectangle:"]);
const mozc::commands::RendererCommand *rendererCommand =
controller_.rendererCommand;
EXPECT_EQ(2, mock_renderer_->counter_ExecCommand());
EXPECT_TRUE(rendererCommand->visible());
const mozc::commands::RendererCommand::Rectangle &preedit_rectangle =
rendererCommand->preedit_rectangle();
EXPECT_EQ(50, preedit_rectangle.left());
EXPECT_EQ(screen_rect.origin.y + screen_rect.size.height - 60,
preedit_rectangle.top());
EXPECT_EQ(51, preedit_rectangle.right());
EXPECT_EQ(screen_rect.origin.y + screen_rect.size.height - 50,
preedit_rectangle.bottom());
// reshow the candidate window again -- but cursor position has changed.
mock_client_.expectedCursor = NSMakeRect(60, 50, 1, 10);
[controller_ updateCandidates:&output];
// Run the runloop so "delayedUpdateCandidates" can be called
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
// Does not change: not call again
EXPECT_EQ(1,
[mock_client_
getCounter:"attributesForCharacterIndex:lineHeightRectangle:"]);
rendererCommand = controller_.rendererCommand;
EXPECT_EQ(3, mock_renderer_->counter_ExecCommand());
EXPECT_TRUE(rendererCommand->visible());
// Does not change the position
EXPECT_EQ(preedit_rectangle.DebugString(),
rendererCommand->preedit_rectangle().DebugString());
// Then update without candidate entries -- goes invisible.
output.Clear();
[controller_ updateCandidates:&output];
// Run the runloop so "delayedUpdateCandidates" can be called
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
// Does not change: not call again
EXPECT_EQ(1,
[mock_client_
getCounter:"attributesForCharacterIndex:lineHeightRectangle:"]);
EXPECT_EQ(4, mock_renderer_->counter_ExecCommand());
EXPECT_FALSE(rendererCommand->visible());
}
TEST_F(GoogleJapaneseInputControllerTest, OpenLink) {
EXPECT_EQ(0, openURL_count);
[controller_ openLink:[NSURL URLWithString:@"http://www.example.com/"]];
// openURL is invoked
EXPECT_EQ(1, openURL_count);
// Change to the unsecure bundle ID
ResetClientBundleIdentifier(@"com.apple.securityagent");
// The counter does not change during the initialization
EXPECT_EQ(1, openURL_count);
[controller_ openLink:[NSURL URLWithString:@"http://www.example.com/"]];
// openURL is not invoked
EXPECT_EQ(1, openURL_count);
}
TEST_F(GoogleJapaneseInputControllerTest, SwitchModeToDirect) {
// setup the IME status
controller_.mode = mozc::commands::HIRAGANA;
mozc::commands::Preedit preedit;
mozc::commands::Preedit::Segment *new_segment = preedit.add_segment();
new_segment->set_annotation(mozc::commands::Preedit::Segment::UNDERLINE);
new_segment->set_value("abcdef");
new_segment->set_value_length(6);
[controller_ updateComposedString:&preedit];
mozc::commands::Output output;
output.mutable_result()->set_type(mozc::commands::Result::STRING);
output.mutable_result()->set_value("foo");
mock_mozc_client_->set_output_SendKeyWithContext(output);
[controller_ switchModeToDirect:mock_client_];
EXPECT_EQ(mozc::commands::DIRECT, controller_.mode);
EXPECT_EQ(1, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
EXPECT_TRUE(mock_mozc_client_->called_SendKeyWithContext().has_special_key());
EXPECT_EQ(mozc::commands::KeyEvent::OFF,
mock_mozc_client_->called_SendKeyWithContext().special_key());
EXPECT_EQ(1, [mock_client_ getCounter:"insertText:replacementRange:"]);
EXPECT_TRUE([@"foo" isEqualToString:mock_client_.insertedText]);
EXPECT_EQ(0, [[controller_ composedString:nil] length]);
EXPECT_FALSE(controller_.rendererCommand->visible());
}
TEST_F(GoogleJapaneseInputControllerTest, SwitchModeInternal) {
// When a mode changes from DIRECT, it should invoke "ON" command beforehand.
controller_.mode = mozc::commands::DIRECT;
[controller_ switchModeInternal:mozc::commands::HIRAGANA];
EXPECT_EQ(mozc::commands::HIRAGANA, controller_.mode);
EXPECT_EQ(1, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
EXPECT_TRUE(mock_mozc_client_->called_SendKeyWithContext().has_special_key());
EXPECT_EQ(mozc::commands::KeyEvent::ON,
mock_mozc_client_->called_SendKeyWithContext().special_key());
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::SWITCH_INPUT_MODE,
mock_mozc_client_->called_SendCommandWithContext().type());
EXPECT_EQ(
mozc::commands::HIRAGANA,
mock_mozc_client_->called_SendCommandWithContext().composition_mode());
// Switch from HIRAGANA to KATAKANA. Just sending mode switching command.
controller_.mode = mozc::commands::HIRAGANA;
[controller_ switchModeInternal:mozc::commands::HALF_KATAKANA];
EXPECT_EQ(mozc::commands::HALF_KATAKANA, controller_.mode);
EXPECT_EQ(1, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
EXPECT_EQ(2,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::SWITCH_INPUT_MODE,
mock_mozc_client_->called_SendCommandWithContext().type());
EXPECT_EQ(
mozc::commands::HALF_KATAKANA,
mock_mozc_client_->called_SendCommandWithContext().composition_mode());
// going to same mode does not cause sendcommand
[controller_ switchModeInternal:mozc::commands::HALF_KATAKANA];
EXPECT_EQ(mozc::commands::HALF_KATAKANA, controller_.mode);
EXPECT_EQ(1, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
EXPECT_EQ(2,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
}
TEST_F(GoogleJapaneseInputControllerTest, SwitchDisplayMode) {
EXPECT_TRUE(mock_client_.selectedMode->empty());
EXPECT_EQ(mozc::commands::DIRECT, controller_.mode);
[controller_ switchDisplayMode];
EXPECT_EQ(1, [mock_client_ getCounter:"selectInputMode:"]);
string expected = mozc::MacUtil::GetLabelForSuffix("Roman");
EXPECT_EQ(expected, *(mock_client_.selectedMode));
// Does not change the display mode for MS Word. See
// GoogleJapaneseInputController.mm for the detailed information.
ResetClientBundleIdentifier(@"com.microsoft.Word");
[controller_ switchMode:mozc::commands::HIRAGANA client:mock_client_];
EXPECT_EQ(mozc::commands::HIRAGANA, controller_.mode);
[controller_ switchDisplayMode];
// still remains 1 and display mode does not change.
EXPECT_EQ(1, [mock_client_ getCounter:"selectInputMode:"]);
expected = mozc::MacUtil::GetLabelForSuffix("Roman");
EXPECT_EQ(expected, *(mock_client_.selectedMode));
}
TEST_F(GoogleJapaneseInputControllerTest, commitText) {
controller_.replacementRange = NSMakeRange(0, 1);
[controller_ commitText:"foo" client:mock_client_];
EXPECT_EQ(1, [mock_client_ getCounter:"insertText:replacementRange:"]);
EXPECT_TRUE([@"foo" isEqualToString:mock_client_.insertedText]);
// location has to be cleared after the commit.
// Do not use NSNotFound directly in EXPECT_EQ because type checker
// gets confused for the comparision of enums.
int expected = NSNotFound;
EXPECT_EQ(expected, [controller_ replacementRange].location);
}
TEST_F(GoogleJapaneseInputControllerTest, handleConfig) {
// Does not support multiple-calculation
mozc::config::Config config;
config.set_preedit_method(mozc::config::Config::KANA);
config.set_yen_sign_character(mozc::config::Config::BACKSLASH);
config.set_use_japanese_layout(true);
mock_mozc_client_->SetConfig(config);
mock_mozc_client_->SetBoolFunctionReturn("GetConfig", true);
[controller_ handleConfig];
EXPECT_EQ(1, mock_mozc_client_->GetFunctionCallCount("GetConfig"));
EXPECT_EQ(KANA, controller_.keyCodeMap.inputMode);
EXPECT_EQ(mozc::config::Config::BACKSLASH, controller_.yenSignCharacter);
EXPECT_EQ(1, [mock_client_ getCounter:"overrideKeyboardWithKeyboardNamed:"]);
EXPECT_TRUE([@"com.apple.keylayout.US"
isEqualToString:mock_client_.overriddenLayout])
<< [mock_client_.overriddenLayout UTF8String];
}
TEST_F(GoogleJapaneseInputControllerTest, DoubleTapKanaReconvert) {
// tap (short) tap -> emit undo command
controller_.mode = mozc::commands::HIRAGANA;
// set attributedString for Reconvert
[mock_client_ setAttributedString:
[[[NSAttributedString alloc] initWithString:@"abcde"] autorelease]];
mock_client_.expectedRange = NSMakeRange(1, 3);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::CONVERT_REVERSE,
mock_mozc_client_->called_SendCommandWithContext().type());
EXPECT_EQ("bcd",
mock_mozc_client_->called_SendCommandWithContext().text());
}
TEST_F(GoogleJapaneseInputControllerTest, DoubleTapKanaUndo) {
// tap (short) tap -> emit undo command
controller_.mode = mozc::commands::HIRAGANA;
// set expectedRange for Undo
mock_client_.expectedRange = NSMakeRange(1, 0);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
}
TEST_F(GoogleJapaneseInputControllerTest, DoubleTapKanaUndoTimeOver) {
// tap (long) tap -> don't emit undo command
controller_.mode = mozc::commands::HIRAGANA;
// set expectedRange for Undo
mock_client_.expectedRange = NSMakeRange(1, 0);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep more than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 * 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
}
TEST_F(GoogleJapaneseInputControllerTest, SingleAndDoubleTapKanaUndo) {
// tap (long) tap (short) tap -> emit once
controller_.mode = mozc::commands::HIRAGANA;
// set expectedRange for Undo
mock_client_.expectedRange = NSMakeRange(1, 0);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep more than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 * 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
}
TEST_F(GoogleJapaneseInputControllerTest, TripleTapKanaUndo) {
// tap (short) tap (short) tap -> emit twice
controller_.mode = mozc::commands::HIRAGANA;
// set expectedRange for Undo
mock_client_.expectedRange = NSMakeRange(1, 0);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(2,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
}
TEST_F(GoogleJapaneseInputControllerTest, QuadrupleTapKanaUndo) {
// tap (short) tap (short) tap (short) tap -> emit thrice
controller_.mode = mozc::commands::HIRAGANA;
// set expectedRange for Undo
mock_client_.expectedRange = NSMakeRange(1, 0);
// Send Kana-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(2,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Kana-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Kana, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(3,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::UNDO,
mock_mozc_client_->called_SendCommandWithContext().type());
}
TEST_F(GoogleJapaneseInputControllerTest, DoubleTapEisuCommitRawText) {
// Send Eisu-key.
// Because of special hack for Eisu/Kana keys, it returns YES.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Eisu, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it did not send command.
EXPECT_EQ(0,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
// Sleep less than DoubleTapInterval (sec)
mozc::Util::Sleep(GetDoubleTapInterval() * 1000.0 / 2.0);
// Send Eisu-key.
EXPECT_EQ(YES, SendKeyEvent(kVK_JIS_Eisu, controller_, mock_client_));
// Check if it did not send key.
EXPECT_EQ(0, mock_mozc_client_->GetFunctionCallCount("SendKeyWithContext"));
// Check if it sent an undo command.
EXPECT_EQ(1,
mock_mozc_client_->GetFunctionCallCount("SendCommandWithContext"));
EXPECT_EQ(mozc::commands::SessionCommand::COMMIT_RAW_TEXT,
mock_mozc_client_->called_SendCommandWithContext().type());
}
TEST_F(GoogleJapaneseInputControllerTest, fillSurroundingContext) {
[mock_client_ setAttributedString:
[[[NSAttributedString alloc] initWithString:@"abcde"] autorelease]];
mock_client_.expectedRange = NSMakeRange(2, 1);
mozc::commands::Context context;
[controller_ fillSurroundingContext:&context client:(id)mock_client_];
EXPECT_EQ("ab", context.preceding_text());
EXPECT_EQ(1, [mock_client_ getCounter:"selectedRange"]);
EXPECT_EQ(0, [mock_client_ getCounter:"markedRange"]);
[mock_client_ setAttributedString:
[[[NSAttributedString alloc] initWithString:@"012345678901234567890abcde"]
autorelease]];
mock_client_.expectedRange = NSMakeRange(1, 0);
[controller_ fillSurroundingContext:&context client:(id)mock_client_];
EXPECT_EQ("0", context.preceding_text());
EXPECT_EQ(2, [mock_client_ getCounter:"selectedRange"]);
EXPECT_EQ(0, [mock_client_ getCounter:"markedRange"]);
mock_client_.expectedRange = NSMakeRange(22, 1);
[controller_ fillSurroundingContext:&context client:(id)mock_client_];
EXPECT_EQ("2345678901234567890a", context.preceding_text());
EXPECT_EQ(3, [mock_client_ getCounter:"selectedRange"]);
EXPECT_EQ(0, [mock_client_ getCounter:"markedRange"]);
[mock_client_ setAttributedString:
[[[NSAttributedString alloc] initWithString:@"012abc345"] autorelease]];
mozc::commands::Preedit preedit;
mozc::commands::Preedit::Segment *new_segment = preedit.add_segment();
new_segment->set_annotation(mozc::commands::Preedit::Segment::HIGHLIGHT);
new_segment->set_value("abc");
new_segment->set_value_length(3);
[controller_ updateComposedString:&preedit];
mock_client_.expectedRange = NSMakeRange(3, 3);
[controller_ fillSurroundingContext:&context client:(id)mock_client_];
EXPECT_EQ("012", context.preceding_text());
EXPECT_EQ(4, [mock_client_ getCounter:"selectedRange"]);
EXPECT_EQ(0, [mock_client_ getCounter:"markedRange"]);
}