Home | History | Annotate | Download | only in cocoa
      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