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.
5 #import "ui/views/cocoa/bridged_native_widget.h"
7 #import <Cocoa/Cocoa.h>
9 #import "base/mac/foundation_util.h"
10 #import "base/mac/mac_util.h"
11 #import "base/mac/sdk_forward_declarations.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/strings/utf_string_conversions.h"
16 #import "testing/gtest_mac.h"
17 #import "ui/gfx/test/ui_cocoa_test_helper.h"
18 #import "ui/views/cocoa/bridged_content_view.h"
19 #import "ui/views/cocoa/native_widget_mac_nswindow.h"
20 #include "ui/views/controls/textfield/textfield.h"
21 #include "ui/views/ime/input_method.h"
22 #include "ui/views/view.h"
23 #include "ui/views/widget/native_widget_mac.h"
24 #include "ui/views/widget/root_view.h"
25 #include "ui/views/widget/widget.h"
26 #include "ui/views/widget/widget_observer.h"
28 using base::ASCIIToUTF16;
29 using base::SysNSStringToUTF8;
30 using base::SysNSStringToUTF16;
31 using base::SysUTF8ToNSString;
33 #define EXPECT_EQ_RANGE(a, b) \
34 EXPECT_EQ(a.location, b.location); \
35 EXPECT_EQ(a.length, b.length);
39 // Empty range shortcut for readibility.
40 NSRange EmptyRange() {
41 return NSMakeRange(NSNotFound, 0);
46 // Class to override -[NSWindow toggleFullScreen:] to a no-op. This simulates
47 // NSWindow's behavior when attempting to toggle fullscreen state again, when
48 // the last attempt failed but Cocoa has not yet sent
49 // windowDidFailToEnterFullScreen:.
50 @interface BridgedNativeWidgetTestFullScreenWindow : NativeWidgetMacNSWindow {
52 int ignoredToggleFullScreenCount_;
54 @property(readonly, nonatomic) int ignoredToggleFullScreenCount;
57 @implementation BridgedNativeWidgetTestFullScreenWindow
59 @synthesize ignoredToggleFullScreenCount = ignoredToggleFullScreenCount_;
61 - (void)toggleFullScreen:(id)sender {
62 ++ignoredToggleFullScreenCount_;
70 // Provides the |parent| argument to construct a BridgedNativeWidget.
71 class MockNativeWidgetMac : public NativeWidgetMac {
73 MockNativeWidgetMac(Widget* delegate) : NativeWidgetMac(delegate) {}
75 // Expose a reference, so that it can be reset() independently.
76 scoped_ptr<BridgedNativeWidget>& bridge() {
80 // internal::NativeWidgetPrivate:
81 void InitNativeWidget(const Widget::InitParams& params) override {
82 ownership_ = params.ownership;
84 // Usually the bridge gets initialized here. It is skipped to run extra
85 // checks in tests, and so that a second window isn't created.
86 delegate()->OnNativeWidgetCreated(true);
89 void ReorderNativeViews() override {
90 // Called via Widget::Init to set the content view. No-op in these tests.
94 DISALLOW_COPY_AND_ASSIGN(MockNativeWidgetMac);
97 // Helper test base to construct a BridgedNativeWidget with a valid parent.
98 class BridgedNativeWidgetTestBase : public ui::CocoaTest {
100 BridgedNativeWidgetTestBase()
101 : widget_(new Widget),
102 native_widget_mac_(new MockNativeWidgetMac(widget_.get())) {
105 scoped_ptr<BridgedNativeWidget>& bridge() {
106 return native_widget_mac_->bridge();
109 // Overridden from testing::Test:
110 void SetUp() override {
111 ui::CocoaTest::SetUp();
113 init_params_.native_widget = native_widget_mac_;
115 // To control the lifetime without an actual window that must be closed,
116 // tests in this file need to use WIDGET_OWNS_NATIVE_WIDGET.
117 init_params_.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
119 // Opacity defaults to "infer" which is usually updated by ViewsDelegate.
120 init_params_.opacity = Widget::InitParams::OPAQUE_WINDOW;
122 native_widget_mac_->GetWidget()->Init(init_params_);
126 scoped_ptr<Widget> widget_;
127 MockNativeWidgetMac* native_widget_mac_; // Weak. Owned by |widget_|.
129 // Make the InitParams available to tests to cover initialization codepaths.
130 Widget::InitParams init_params_;
133 class BridgedNativeWidgetTest : public BridgedNativeWidgetTestBase {
135 BridgedNativeWidgetTest();
136 ~BridgedNativeWidgetTest() override;
138 // Install a textfield in the view hierarchy and make it the text input
140 void InstallTextField(const std::string& text);
142 // Returns the current text as std::string.
143 std::string GetText();
146 void SetUp() override;
147 void TearDown() override;
150 scoped_ptr<views::View> view_;
151 scoped_ptr<BridgedNativeWidget> bridge_;
152 BridgedContentView* ns_view_; // Weak. Owned by bridge_.
155 DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTest);
158 BridgedNativeWidgetTest::BridgedNativeWidgetTest() {
161 BridgedNativeWidgetTest::~BridgedNativeWidgetTest() {
164 void BridgedNativeWidgetTest::InstallTextField(const std::string& text) {
165 Textfield* textfield = new Textfield();
166 textfield->SetText(ASCIIToUTF16(text));
167 view_->AddChildView(textfield);
168 [ns_view_ setTextInputClient:textfield];
171 std::string BridgedNativeWidgetTest::GetText() {
172 NSRange range = NSMakeRange(0, NSUIntegerMax);
173 NSAttributedString* text =
174 [ns_view_ attributedSubstringForProposedRange:range actualRange:NULL];
175 return SysNSStringToUTF8([text string]);
178 void BridgedNativeWidgetTest::SetUp() {
179 BridgedNativeWidgetTestBase::SetUp();
181 view_.reset(new views::internal::RootView(widget_.get()));
182 base::scoped_nsobject<NSWindow> window([test_window() retain]);
184 // BridgedNativeWidget expects to be initialized with a hidden (deferred)
186 [window orderOut:nil];
187 EXPECT_FALSE([window delegate]);
188 bridge()->Init(window, init_params_);
190 // The delegate should exist before setting the root view.
191 EXPECT_TRUE([window delegate]);
192 bridge()->SetRootView(view_.get());
193 ns_view_ = bridge()->ns_view();
195 // Pretend it has been shown via NativeWidgetMac::Show().
196 [window orderFront:nil];
197 [test_window() makePretendKeyWindowAndSetFirstResponder:bridge()->ns_view()];
200 void BridgedNativeWidgetTest::TearDown() {
202 BridgedNativeWidgetTestBase::TearDown();
205 // The TEST_VIEW macro expects the view it's testing to have a superview. In
206 // these tests, the NSView bridge is a contentView, at the root. These mimic
207 // what TEST_VIEW usually does.
208 TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewAddRemove) {
209 base::scoped_nsobject<BridgedContentView> view([bridge()->ns_view() retain]);
210 EXPECT_NSEQ([test_window() contentView], view);
211 EXPECT_NSEQ(test_window(), [view window]);
213 // The superview of a contentView is an NSNextStepFrame.
214 EXPECT_TRUE([view superview]);
215 EXPECT_TRUE([view hostedView]);
217 // Ensure the tracking area to propagate mouseMoved: events to the RootView is
219 EXPECT_EQ(1u, [[view trackingAreas] count]);
221 // Destroying the C++ bridge should remove references to any C++ objects in
222 // the ObjectiveC object, and remove it from the hierarchy.
224 EXPECT_FALSE([view hostedView]);
225 EXPECT_FALSE([view superview]);
226 EXPECT_FALSE([view window]);
227 EXPECT_EQ(0u, [[view trackingAreas] count]);
228 EXPECT_FALSE([test_window() contentView]);
229 EXPECT_FALSE([test_window() delegate]);
232 TEST_F(BridgedNativeWidgetTest, BridgedNativeWidgetTest_TestViewDisplay) {
233 [bridge()->ns_view() display];
236 // Test that resizing the window resizes the root view appropriately.
237 TEST_F(BridgedNativeWidgetTest, ViewSizeTracksWindow) {
238 const int kTestNewWidth = 400;
239 const int kTestNewHeight = 300;
241 // |test_window()| is borderless, so these should align.
242 NSSize window_size = [test_window() frame].size;
243 EXPECT_EQ(view_->width(), static_cast<int>(window_size.width));
244 EXPECT_EQ(view_->height(), static_cast<int>(window_size.height));
246 // Make sure a resize actually occurs.
247 EXPECT_NE(kTestNewWidth, view_->width());
248 EXPECT_NE(kTestNewHeight, view_->height());
250 [test_window() setFrame:NSMakeRect(0, 0, kTestNewWidth, kTestNewHeight)
252 EXPECT_EQ(kTestNewWidth, view_->width());
253 EXPECT_EQ(kTestNewHeight, view_->height());
256 TEST_F(BridgedNativeWidgetTest, CreateInputMethodShouldNotReturnNull) {
257 scoped_ptr<views::InputMethod> input_method(bridge()->CreateInputMethod());
258 EXPECT_TRUE(input_method);
261 TEST_F(BridgedNativeWidgetTest, GetHostInputMethodShouldNotReturnNull) {
262 EXPECT_TRUE(bridge()->GetHostInputMethod());
265 // A simpler test harness for testing initialization flows.
266 typedef BridgedNativeWidgetTestBase BridgedNativeWidgetInitTest;
268 // Test that BridgedNativeWidget remains sane if Init() is never called.
269 TEST_F(BridgedNativeWidgetInitTest, InitNotCalled) {
270 EXPECT_FALSE(bridge()->ns_view());
271 EXPECT_FALSE(bridge()->ns_window());
275 // Test getting complete string using text input protocol.
276 TEST_F(BridgedNativeWidgetTest, TextInput_GetCompleteString) {
277 const std::string kTestString = "foo bar baz";
278 InstallTextField(kTestString);
280 NSRange range = NSMakeRange(0, kTestString.size());
281 NSRange actual_range;
282 NSAttributedString* text =
283 [ns_view_ attributedSubstringForProposedRange:range
284 actualRange:&actual_range];
285 EXPECT_EQ(kTestString, SysNSStringToUTF8([text string]));
286 EXPECT_EQ_RANGE(range, actual_range);
289 // Test getting middle substring using text input protocol.
290 TEST_F(BridgedNativeWidgetTest, TextInput_GetMiddleSubstring) {
291 const std::string kTestString = "foo bar baz";
292 InstallTextField(kTestString);
294 NSRange range = NSMakeRange(4, 3);
295 NSRange actual_range;
296 NSAttributedString* text =
297 [ns_view_ attributedSubstringForProposedRange:range
298 actualRange:&actual_range];
299 EXPECT_EQ("bar", SysNSStringToUTF8([text string]));
300 EXPECT_EQ_RANGE(range, actual_range);
303 // Test getting ending substring using text input protocol.
304 TEST_F(BridgedNativeWidgetTest, TextInput_GetEndingSubstring) {
305 const std::string kTestString = "foo bar baz";
306 InstallTextField(kTestString);
308 NSRange range = NSMakeRange(8, 100);
309 NSRange actual_range;
310 NSAttributedString* text =
311 [ns_view_ attributedSubstringForProposedRange:range
312 actualRange:&actual_range];
313 EXPECT_EQ("baz", SysNSStringToUTF8([text string]));
314 EXPECT_EQ(range.location, actual_range.location);
315 EXPECT_EQ(3U, actual_range.length);
318 // Test getting empty substring using text input protocol.
319 TEST_F(BridgedNativeWidgetTest, TextInput_GetEmptySubstring) {
320 const std::string kTestString = "foo bar baz";
321 InstallTextField(kTestString);
323 NSRange range = EmptyRange();
324 NSRange actual_range;
325 NSAttributedString* text =
326 [ns_view_ attributedSubstringForProposedRange:range
327 actualRange:&actual_range];
328 EXPECT_EQ("", SysNSStringToUTF8([text string]));
329 EXPECT_EQ_RANGE(range, actual_range);
332 // Test inserting text using text input protocol.
333 TEST_F(BridgedNativeWidgetTest, TextInput_InsertText) {
334 const std::string kTestString = "foo";
335 InstallTextField(kTestString);
337 [ns_view_ insertText:SysUTF8ToNSString(kTestString)
338 replacementRange:EmptyRange()];
339 gfx::Range range(0, kTestString.size());
341 EXPECT_TRUE([ns_view_ textInputClient]->GetTextFromRange(range, &text));
342 EXPECT_EQ(ASCIIToUTF16(kTestString), text);
345 // Test replacing text using text input protocol.
346 TEST_F(BridgedNativeWidgetTest, TextInput_ReplaceText) {
347 const std::string kTestString = "foo bar";
348 InstallTextField(kTestString);
350 [ns_view_ insertText:@"baz" replacementRange:NSMakeRange(4, 3)];
351 EXPECT_EQ("foo baz", GetText());
354 // Test IME composition using text input protocol.
355 TEST_F(BridgedNativeWidgetTest, TextInput_Compose) {
356 const std::string kTestString = "foo ";
357 InstallTextField(kTestString);
359 EXPECT_FALSE([ns_view_ hasMarkedText]);
360 EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
362 // Start composition.
363 NSString* compositionText = @"bar";
364 NSUInteger compositionLength = [compositionText length];
365 [ns_view_ setMarkedText:compositionText
366 selectedRange:NSMakeRange(0, 2)
367 replacementRange:EmptyRange()];
368 EXPECT_TRUE([ns_view_ hasMarkedText]);
369 EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), compositionLength),
370 [ns_view_ markedRange]);
371 EXPECT_EQ_RANGE(NSMakeRange(kTestString.size(), 2), [ns_view_ selectedRange]);
373 // Confirm composition.
374 [ns_view_ unmarkText];
375 EXPECT_FALSE([ns_view_ hasMarkedText]);
376 EXPECT_EQ_RANGE(EmptyRange(), [ns_view_ markedRange]);
377 EXPECT_EQ("foo bar", GetText());
378 EXPECT_EQ_RANGE(NSMakeRange(GetText().size(), 0), [ns_view_ selectedRange]);
381 // Test moving the caret left and right using text input protocol.
382 TEST_F(BridgedNativeWidgetTest, TextInput_MoveLeftRight) {
383 InstallTextField("foo");
384 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
386 // Move right not allowed, out of range.
387 [ns_view_ doCommandBySelector:@selector(moveRight:)];
388 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
391 [ns_view_ doCommandBySelector:@selector(moveLeft:)];
392 EXPECT_EQ_RANGE(NSMakeRange(2, 0), [ns_view_ selectedRange]);
395 [ns_view_ doCommandBySelector:@selector(moveRight:)];
396 EXPECT_EQ_RANGE(NSMakeRange(3, 0), [ns_view_ selectedRange]);
399 // Test backward delete using text input protocol.
400 TEST_F(BridgedNativeWidgetTest, TextInput_DeleteBackward) {
401 InstallTextField("a");
402 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
404 // Delete one character.
405 [ns_view_ doCommandBySelector:@selector(deleteBackward:)];
406 EXPECT_EQ("", GetText());
407 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
409 // Try to delete again on an empty string.
410 [ns_view_ doCommandBySelector:@selector(deleteBackward:)];
411 EXPECT_EQ("", GetText());
412 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
415 // Test forward delete using text input protocol.
416 TEST_F(BridgedNativeWidgetTest, TextInput_DeleteForward) {
417 InstallTextField("a");
418 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
420 // At the end of the string, can't delete forward.
421 [ns_view_ doCommandBySelector:@selector(deleteForward:)];
422 EXPECT_EQ("a", GetText());
423 EXPECT_EQ_RANGE(NSMakeRange(1, 0), [ns_view_ selectedRange]);
425 // Should succeed after moving left first.
426 [ns_view_ doCommandBySelector:@selector(moveLeft:)];
427 [ns_view_ doCommandBySelector:@selector(deleteForward:)];
428 EXPECT_EQ("", GetText());
429 EXPECT_EQ_RANGE(NSMakeRange(0, 0), [ns_view_ selectedRange]);
432 typedef BridgedNativeWidgetTestBase BridgedNativeWidgetSimulateFullscreenTest;
434 // Simulate the notifications that AppKit would send out if a fullscreen
435 // operation begins, and then fails and must abort. This notification sequence
436 // was determined by posting delayed tasks to toggle fullscreen state and then
437 // mashing Ctrl+Left/Right to keep OSX in a transition between Spaces to cause
438 // the fullscreen transition to fail.
439 TEST_F(BridgedNativeWidgetSimulateFullscreenTest, FailToEnterAndExit) {
440 if (base::mac::IsOSSnowLeopard())
443 base::scoped_nsobject<NSWindow> owned_window(
444 [[BridgedNativeWidgetTestFullScreenWindow alloc]
445 initWithContentRect:NSMakeRect(50, 50, 400, 300)
446 styleMask:NSBorderlessWindowMask
447 backing:NSBackingStoreBuffered
449 [owned_window setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
450 bridge()->Init(owned_window, init_params_); // Transfers ownership.
452 BridgedNativeWidgetTestFullScreenWindow* window =
453 base::mac::ObjCCastStrict<BridgedNativeWidgetTestFullScreenWindow>(
454 widget_->GetNativeWindow());
457 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
459 EXPECT_FALSE(bridge()->target_fullscreen_state());
461 // Simulate an initial toggleFullScreen: (user- or Widget-initiated).
462 [center postNotificationName:NSWindowWillEnterFullScreenNotification
465 // On a failure, Cocoa starts by sending an unexpected *exit* fullscreen, and
466 // BridgedNativeWidget will think it's just a delayed transition and try to go
467 // back into fullscreen but get ignored by Cocoa.
468 EXPECT_EQ(0, [window ignoredToggleFullScreenCount]);
469 EXPECT_TRUE(bridge()->target_fullscreen_state());
470 [center postNotificationName:NSWindowDidExitFullScreenNotification
472 EXPECT_EQ(1, [window ignoredToggleFullScreenCount]);
473 EXPECT_FALSE(bridge()->target_fullscreen_state());
475 // Cocoa follows up with a failure message sent to the NSWindowDelegate (there
476 // is no equivalent notification for failure). Called via id so that this
478 id window_delegate = [window delegate];
479 [window_delegate windowDidFailToEnterFullScreen:window];
480 EXPECT_FALSE(bridge()->target_fullscreen_state());
482 // Now perform a successful fullscreen operation.
483 [center postNotificationName:NSWindowWillEnterFullScreenNotification
485 EXPECT_TRUE(bridge()->target_fullscreen_state());
486 [center postNotificationName:NSWindowDidEnterFullScreenNotification
488 EXPECT_TRUE(bridge()->target_fullscreen_state());
490 // And try to get out.
491 [center postNotificationName:NSWindowWillExitFullScreenNotification
493 EXPECT_FALSE(bridge()->target_fullscreen_state());
495 // On a failure, Cocoa sends a failure message, but then just dumps the window
496 // out of fullscreen anyway (in that order).
497 [window_delegate windowDidFailToExitFullScreen:window];
498 EXPECT_FALSE(bridge()->target_fullscreen_state());
499 [center postNotificationName:NSWindowDidExitFullScreenNotification
501 EXPECT_EQ(1, [window ignoredToggleFullScreenCount]); // No change.
502 EXPECT_FALSE(bridge()->target_fullscreen_state());