• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
22using base::ASCIIToUTF16;
23using base::SysNSStringToUTF8;
24using base::SysNSStringToUTF16;
25using 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
31namespace {
32
33// Empty range shortcut for readibility.
34NSRange EmptyRange() {
35  return NSMakeRange(NSNotFound, 0);
36}
37
38}  // namespace
39
40namespace views {
41namespace test {
42
43// Provides the |parent| argument to construct a BridgedNativeWidget.
44class 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.
71class 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
99class 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
125BridgedNativeWidgetTest::BridgedNativeWidgetTest() {
126}
127
128BridgedNativeWidgetTest::~BridgedNativeWidgetTest() {
129}
130
131void 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
138std::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
145void 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
162void 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.
170TEST_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
194TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewDisplay) {
195  [bridge()->ns_view() display];
196}
197
198// Test that resizing the window resizes the root view appropriately.
199TEST_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
218TEST_F(BridgedNativeWidgetTest, CreateInputMethodShouldNotReturnNull) {
219  scoped_ptr<views::InputMethod> input_method(bridge()->CreateInputMethod());
220  EXPECT_TRUE(input_method);
221}
222
223TEST_F(BridgedNativeWidgetTest, GetHostInputMethodShouldNotReturnNull) {
224  EXPECT_TRUE(bridge()->GetHostInputMethod());
225}
226
227// A simpler test harness for testing initialization flows.
228typedef BridgedNativeWidgetTestBase BridgedNativeWidgetInitTest;
229
230// Test that BridgedNativeWidget remains sane if Init() is never called.
231TEST_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*.
240TEST_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.
262TEST_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.
276TEST_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.
290TEST_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.
305TEST_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.
319TEST_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.
332TEST_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.
341TEST_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.
368TEST_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.
386TEST_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.
402TEST_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