| // 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"]); |
| } |