1 // Copyright 2014 The Chromium 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 5 #import "ui/views/cocoa/bridged_native_widget.h" 6 7 #import <Cocoa/Cocoa.h> 8 9 #include "base/memory/scoped_ptr.h" 10 #include "base/strings/sys_string_conversions.h" 11 #include "base/strings/utf_string_conversions.h" 12 #import "testing/gtest_mac.h" 13 #import "ui/gfx/test/ui_cocoa_test_helper.h" 14 #import "ui/views/cocoa/bridged_content_view.h" 15 #include "ui/views/controls/textfield/textfield.h" 16 #include "ui/views/ime/input_method.h" 17 #include "ui/views/view.h" 18 #include "ui/views/widget/native_widget_mac.h" 19 #include "ui/views/widget/widget.h" 20 #include "ui/views/widget/widget_observer.h" 21 22 using base::ASCIIToUTF16; 23 using base::SysNSStringToUTF8; 24 using base::SysNSStringToUTF16; 25 using base::SysUTF8ToNSString; 26 27 #define EXPECT_EQ_RANGE(a, b) \ 28 EXPECT_EQ(a.location, b.location); \ 29 EXPECT_EQ(a.length, b.length); 30 31 namespace { 32 33 // Empty range shortcut for readibility. 34 NSRange EmptyRange() { 35 return NSMakeRange(NSNotFound, 0); 36 } 37 38 } // namespace 39 40 namespace views { 41 namespace test { 42 43 // Provides the |parent| argument to construct a BridgedNativeWidget. 44 class MockNativeWidgetMac : public NativeWidgetMac { 45 public: 46 MockNativeWidgetMac(Widget* delegate) : NativeWidgetMac(delegate) {} 47 48 // Expose a reference, so that it can be reset() independently. 49 scoped_ptr<BridgedNativeWidget>& bridge() { 50 return bridge_; 51 } 52 53 // internal::NativeWidgetPrivate: 54 virtual void InitNativeWidget(const Widget::InitParams& params) OVERRIDE { 55 ownership_ = params.ownership; 56 57 // Usually the bridge gets initialized here. It is skipped to run extra 58 // checks in tests, and so that a second window isn't created. 59 delegate()->OnNativeWidgetCreated(true); 60 } 61 62 virtual void ReorderNativeViews() OVERRIDE { 63 // Called via Widget::Init to set the content view. No-op in these tests. 64 } 65 66 private: 67 DISALLOW_COPY_AND_ASSIGN(MockNativeWidgetMac); 68 }; 69 70 // Helper test base to construct a BridgedNativeWidget with a valid parent. 71 class BridgedNativeWidgetTestBase : public ui::CocoaTest { 72 public: 73 BridgedNativeWidgetTestBase() 74 : widget_(new Widget), 75 native_widget_mac_(new MockNativeWidgetMac(widget_.get())) { 76 } 77 78 scoped_ptr<BridgedNativeWidget>& bridge() { 79 return native_widget_mac_->bridge(); 80 } 81 82 // Overridden from testing::Test: 83 virtual void SetUp() OVERRIDE { 84 ui::CocoaTest::SetUp(); 85 86 Widget::InitParams params; 87 params.native_widget = native_widget_mac_; 88 // To control the lifetime without an actual window that must be closed, 89 // tests in this file need to use WIDGET_OWNS_NATIVE_WIDGET. 90 params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 91 native_widget_mac_->GetWidget()->Init(params); 92 } 93 94 protected: 95 scoped_ptr<Widget> widget_; 96 MockNativeWidgetMac* native_widget_mac_; // Weak. Owned by |widget_|. 97 }; 98 99 class BridgedNativeWidgetTest : public BridgedNativeWidgetTestBase { 100 public: 101 BridgedNativeWidgetTest(); 102 virtual ~BridgedNativeWidgetTest(); 103 104 // Install a textfield in the view hierarchy and make it the text input 105 // client. 106 void InstallTextField(const std::string& text); 107 108 // Returns the current text as std::string. 109 std::string GetText(); 110 111 // testing::Test: 112 virtual void SetUp() OVERRIDE; 113 virtual void TearDown() OVERRIDE; 114 115 protected: 116 // TODO(tapted): Make this a EventCountView from widget_unittest.cc. 117 scoped_ptr<views::View> view_; 118 scoped_ptr<BridgedNativeWidget> bridge_; 119 BridgedContentView* ns_view_; // Weak. Owned by bridge_. 120 121 private: 122 DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTest); 123 }; 124 125 BridgedNativeWidgetTest::BridgedNativeWidgetTest() { 126 } 127 128 BridgedNativeWidgetTest::~BridgedNativeWidgetTest() { 129 } 130 131 void BridgedNativeWidgetTest::InstallTextField(const std::string& text) { 132 Textfield* textfield = new Textfield(); 133 textfield->SetText(ASCIIToUTF16(text)); 134 view_->AddChildView(textfield); 135 [ns_view_ setTextInputClient:textfield]; 136 } 137 138 std::string BridgedNativeWidgetTest::GetText() { 139 NSRange range = NSMakeRange(0, NSUIntegerMax); 140 NSAttributedString* text = 141 [ns_view_ attributedSubstringForProposedRange:range actualRange:NULL]; 142 return SysNSStringToUTF8([text string]); 143 } 144 145 void BridgedNativeWidgetTest::SetUp() { 146 BridgedNativeWidgetTestBase::SetUp(); 147 148 view_.reset(new views::View); 149 base::scoped_nsobject<NSWindow> window([test_window() retain]); 150 151 EXPECT_FALSE([window delegate]); 152 bridge()->Init(window, Widget::InitParams()); 153 154 // The delegate should exist before setting the root view. 155 EXPECT_TRUE([window delegate]); 156 bridge()->SetRootView(view_.get()); 157 ns_view_ = bridge()->ns_view(); 158 159 [test_window() makePretendKeyWindowAndSetFirstResponder:bridge()->ns_view()]; 160 } 161 162 void BridgedNativeWidgetTest::TearDown() { 163 view_.reset(); 164 BridgedNativeWidgetTestBase::TearDown(); 165 } 166 167 // The TEST_VIEW macro expects the view it's testing to have a superview. In 168 // these tests, the NSView bridge is a contentView, at the root. These mimic 169 // what TEST_VIEW usually does. 170 TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewAddRemove) { 171 base::scoped_nsobject<BridgedContentView> view([bridge()->ns_view() retain]); 172 EXPECT_NSEQ([test_window() contentView], view); 173 EXPECT_NSEQ(test_window(), [view window]); 174 175 // The superview of a contentView is an NSNextStepFrame. 176 EXPECT_TRUE([view superview]); 177 EXPECT_TRUE([view hostedView]); 178 179 // Ensure the tracking area to propagate mouseMoved: events to the RootView is 180 // installed. 181 EXPECT_EQ(1u, [[view trackingAreas] count]); 182 183 // Destroying the C++ bridge should remove references to any C++ objects in 184 // the ObjectiveC object, and remove it from the hierarchy. 185 bridge().reset(); 186 EXPECT_FALSE([view hostedView]); 187 EXPECT_FALSE([view superview]); 188 EXPECT_FALSE([view window]); 189 EXPECT_EQ(0u, [[view trackingAreas] count]); 190 EXPECT_FALSE([test_window() contentView]); 191 EXPECT_FALSE([test_window() delegate]); 192 } 193 194 TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewDisplay) { 195 [bridge()->ns_view() display]; 196 } 197 198 // Test that resizing the window resizes the root view appropriately. 199 TEST_F(BridgedNativeWidgetTest, ViewSizeTracksWindow) { 200 const int kTestNewWidth = 400; 201 const int kTestNewHeight = 300; 202 203 // |test_window()| is borderless, so these should align. 204 NSSize window_size = [test_window() frame].size; 205 EXPECT_EQ(view_->width(), static_cast<int>(window_size.width)); 206 EXPECT_EQ(view_->height(), static_cast<int>(window_size.height)); 207 208 // Make sure a resize actually occurs. 209 EXPECT_NE(kTestNewWidth, view_->width()); 210 EXPECT_NE(kTestNewHeight, view_->height()); 211 212 [test_window() setFrame:NSMakeRect(0, 0, kTestNewWidth, kTestNewHeight) 213 display:NO]; 214 EXPECT_EQ(kTestNewWidth, view_->width()); 215 EXPECT_EQ(kTestNewHeight, view_->height()); 216 } 217 218 TEST_F(BridgedNativeWidgetTest, CreateInputMethodShouldNotReturnNull) { 219 scoped_ptr<views::InputMethod> input_method(bridge()->CreateInputMethod()); 220 EXPECT_TRUE(input_method); 221 } 222 223 TEST_F(BridgedNativeWidgetTest, GetHostInputMethodShouldNotReturnNull) { 224 EXPECT_TRUE(bridge()->GetHostInputMethod()); 225 } 226 227 // A simpler test harness for testing initialization flows. 228 typedef BridgedNativeWidgetTestBase BridgedNativeWidgetInitTest; 229 230 // Test that BridgedNativeWidget remains sane if Init() is never called. 231 TEST_F(BridgedNativeWidgetInitTest, InitNotCalled) { 232 EXPECT_FALSE(bridge()->ns_view()); 233 EXPECT_FALSE(bridge()->ns_window()); 234 bridge().reset(); 235 } 236 237 // Test attaching to a parent window that is not a NativeWidgetMac. When the 238 // parent is a NativeWidgetMac, that is covered in widget_unittest.cc by 239 // WidgetOwnershipTest.Ownership_ViewsNativeWidgetOwnsWidget*. 240 TEST_F(BridgedNativeWidgetInitTest, ParentWindowNotNativeWidgetMac) { 241 Widget::InitParams params; 242 params.parent = [test_window() contentView]; 243 EXPECT_EQ(0u, [[test_window() childWindows] count]); 244 245 base::scoped_nsobject<NSWindow> child_window( 246 [[NSWindow alloc] initWithContentRect:NSMakeRect(50, 50, 400, 300) 247 styleMask:NSBorderlessWindowMask 248 backing:NSBackingStoreBuffered 249 defer:NO]); 250 [child_window setReleasedWhenClosed:NO]; // Owned by scoped_nsobject. 251 252 EXPECT_FALSE([child_window parentWindow]); 253 bridge()->Init(child_window, params); 254 255 EXPECT_EQ(1u, [[test_window() childWindows] count]); 256 EXPECT_EQ(test_window(), [bridge()->ns_window() parentWindow]); 257 bridge().reset(); 258 EXPECT_EQ(0u, [[test_window() childWindows] count]); 259 } 260 261 // Test getting complete string using text input protocol. 262 TEST_F(BridgedNativeWidgetTest, TextInput_GetCompleteString) { 263 const std::string kTestString = "foo bar baz"; 264 InstallTextField(kTestString); 265 266 NSRange range = NSMakeRange(0, kTestString.size()); 267 NSRange actual_range; 268 NSAttributedString* text = 269 [ns_view_ attributedSubstringForProposedRange:range 270 actualRange:&actual_range]; 271 EXPECT_EQ(kTestString, SysNSStringToUTF8([text string])); 272 EXPECT_EQ_RANGE(range, actual_range); 273 } 274 275 // Test getting middle substring using text input protocol. 276 TEST_F(BridgedNativeWidgetTest, TextInput_GetMiddleSubstring) { 277 const std::string kTestString = "foo bar baz"; 278 InstallTextField(kTestString); 279 280 NSRange range = NSMakeRange(4, 3); 281 NSRange actual_range; 282 NSAttributedString* text = 283 [ns_view_ attributedSubstringForProposedRange:range 284 actualRange:&actual_range]; 285 EXPECT_EQ("bar", SysNSStringToUTF8([text string])); 286 EXPECT_EQ_RANGE(range, actual_range); 287 } 288 289 // Test getting ending substring using text input protocol. 290 TEST_F(BridgedNativeWidgetTest, TextInput_GetEndingSubstring) { 291 const std::string kTestString = "foo bar baz"; 292 InstallTextField(kTestString); 293 294 NSRange range = NSMakeRange(8, 100); 295 NSRange actual_range; 296 NSAttributedString* text = 297 [ns_view_ attributedSubstringForProposedRange:range 298 actualRange:&actual_range]; 299 EXPECT_EQ("baz", SysNSStringToUTF8([text string])); 300 EXPECT_EQ(range.location, actual_range.location); 301 EXPECT_EQ(3U, actual_range.length); 302 } 303 304 // Test getting empty substring using text input protocol. 305 TEST_F(BridgedNativeWidgetTest, TextInput_GetEmptySubstring) { 306 const std::string kTestString = "foo bar baz"; 307 InstallTextField(kTestString); 308 309 NSRange range = EmptyRange(); 310 NSRange actual_range; 311 NSAttributedString* text = 312 [ns_view_ attributedSubstringForProposedRange:range 313 actualRange:&actual_range]; 314 EXPECT_EQ("", SysNSStringToUTF8([text string])); 315 EXPECT_EQ_RANGE(range, actual_range); 316 } 317 318 // Test inserting text using text input protocol. 319 TEST_F(BridgedNativeWidgetTest, TextInput_InsertText) { 320 const std::string kTestString = "foo"; 321 InstallTextField(kTestString); 322 323 [ns_view_ insertText:SysUTF8ToNSString(kTestString) 324 replacementRange:EmptyRange()]; 325 gfx::Range range(0, kTestString.size()); 326 base::string16 text; 327 EXPECT_TRUE([ns_view_ textInputClient]->GetTextFromRange(range, &text)); 328 EXPECT_EQ(ASCIIToUTF16(kTestString), text); 329 } 330 331 // Test replacing text using text input protocol. 332 TEST_F(BridgedNativeWidgetTest, TextInput_ReplaceText) { 333 const std::string kTestString = "foo bar"; 334 InstallTextField(kTestString); 335 336 [ns_view_ insertText:@"baz" replacementRange:NSMakeRange(4, 3)]; 337 EXPECT_EQ("foo baz", GetText()); 338 } 339 340 // Test IME composition using text input protocol. 341 TEST_F(BridgedNativeWidgetTest, TextInput_Compose) { 342 const std::string kTestString = "foo "; 343 InstallTextField(kTestString); 344 345 EXPECT_FALSE([ns_view_ hasMarkedText]); 346 EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]); 347 348 // Start composition. 349 NSString* compositionText = @"bar"; 350 NSUInteger compositionLength = [compositionText length]; 351 [ns_view_ setMarkedText:compositionText 352 selectedRange:NSMakeRange(0, 2) 353 replacementRange:EmptyRange()]; 354 EXPECT_TRUE([ns_view_ hasMarkedText]); 355 EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), compositionLength), 356 [ns_view_ markedRange]); 357 EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), 2), [ns_view_ selectedRange]); 358 359 // Confirm composition. 360 [ns_view_ unmarkText]; 361 EXPECT_FALSE([ns_view_ hasMarkedText]); 362 EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]); 363 EXPECT_EQ("foo bar", GetText()); 364 EXPECT_EQ_RANGE(NSMakeRange(GetText().size(), 0), [ns_view_ selectedRange]); 365 } 366 367 // Test moving the caret left and right using text input protocol. 368 TEST_F(BridgedNativeWidgetTest, TextInput_MoveLeftRight) { 369 InstallTextField("foo"); 370 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]); 371 372 // Move right not allowed, out of range. 373 [ns_view_ doCommandBySelector:@selector(moveRight:)]; 374 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]); 375 376 // Move left. 377 [ns_view_ doCommandBySelector:@selector(moveLeft:)]; 378 EXPECT_EQ_RANGE(NSMakeRange(2, 0), [ns_view_ selectedRange]); 379 380 // Move right. 381 [ns_view_ doCommandBySelector:@selector(moveRight:)]; 382 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]); 383 } 384 385 // Test backward delete using text input protocol. 386 TEST_F(BridgedNativeWidgetTest, TextInput_DeleteBackward) { 387 InstallTextField("a"); 388 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]); 389 390 // Delete one character. 391 [ns_view_ doCommandBySelector:@selector(deleteBackward:)]; 392 EXPECT_EQ("", GetText()); 393 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]); 394 395 // Try to delete again on an empty string. 396 [ns_view_ doCommandBySelector:@selector(deleteBackward:)]; 397 EXPECT_EQ("", GetText()); 398 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]); 399 } 400 401 // Test forward delete using text input protocol. 402 TEST_F(BridgedNativeWidgetTest, TextInput_DeleteForward) { 403 InstallTextField("a"); 404 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]); 405 406 // At the end of the string, can't delete forward. 407 [ns_view_ doCommandBySelector:@selector(deleteForward:)]; 408 EXPECT_EQ("a", GetText()); 409 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]); 410 411 // Should succeed after moving left first. 412 [ns_view_ doCommandBySelector:@selector(moveLeft:)]; 413 [ns_view_ doCommandBySelector:@selector(deleteForward:)]; 414 EXPECT_EQ("", GetText()); 415 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]); 416 } 417 418 } // namespace test 419 } // namespace views 420