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/widget/native_widget_mac.h"
7 #import <Cocoa/Cocoa.h>
9 #import "base/mac/foundation_util.h"
10 #import "base/mac/scoped_nsobject.h"
11 #import "base/mac/scoped_objc_class_swizzler.h"
12 #include "base/run_loop.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/test/test_timeouts.h"
16 #import "testing/gtest_mac.h"
17 #include "third_party/skia/include/core/SkBitmap.h"
18 #include "third_party/skia/include/core/SkCanvas.h"
19 #import "ui/base/cocoa/constrained_window/constrained_window_animation.h"
20 #import "ui/base/cocoa/window_size_constants.h"
21 #import "ui/events/test/cocoa_test_event_utils.h"
22 #include "ui/events/test/event_generator.h"
23 #import "ui/gfx/mac/coordinate_conversion.h"
24 #import "ui/views/cocoa/bridged_native_widget.h"
25 #import "ui/views/cocoa/native_widget_mac_nswindow.h"
26 #include "ui/views/controls/button/label_button.h"
27 #include "ui/views/controls/label.h"
28 #include "ui/views/native_cursor.h"
29 #include "ui/views/test/test_widget_observer.h"
30 #include "ui/views/test/widget_test.h"
31 #include "ui/views/widget/native_widget_private.h"
32 #include "ui/views/window/dialog_delegate.h"
34 // Donates an implementation of -[NSAnimation stopAnimation] which calls the
35 // original implementation, then quits a nested run loop.
36 @interface TestStopAnimationWaiter : NSObject
39 @interface ConstrainedWindowAnimationBase (TestingAPI)
40 - (void)setWindowStateForEnd;
43 @interface NSWindow (PrivateAPI)
44 - (BOOL)_isTitleHidden;
47 // Test NSWindow that provides hooks via method overrides to verify behavior.
48 @interface NativeWidetMacTestWindow : NativeWidgetMacNSWindow {
50 int invalidateShadowCount_;
52 @property(readonly, nonatomic) int invalidateShadowCount;
58 // BridgedNativeWidget friend to access private members.
59 class BridgedNativeWidgetTestApi {
61 explicit BridgedNativeWidgetTestApi(NSWindow* window) {
62 bridge_ = NativeWidgetMac::GetBridgeForNativeWindow(window);
65 // Simulate a frame swap from the compositor. Assumes scale factor of 1.0f.
66 void SimulateFrameSwap(const gfx::Size& size) {
67 const float kScaleFactor = 1.0f;
69 bitmap.allocN32Pixels(size.width(), size.height());
70 SkCanvas canvas(bitmap);
71 bridge_->compositor_widget_->GotSoftwareFrame(kScaleFactor, &canvas);
72 std::vector<ui::LatencyInfo> latency_info;
73 bridge_->AcceleratedWidgetSwapCompleted(latency_info);
77 BridgedNativeWidget* bridge_;
79 DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTestApi);
82 // Custom native_widget to create a NativeWidgetMacTestWindow.
83 class TestWindowNativeWidgetMac : public NativeWidgetMac {
85 explicit TestWindowNativeWidgetMac(Widget* delegate)
86 : NativeWidgetMac(delegate) {}
90 gfx::NativeWindow CreateNSWindow(const Widget::InitParams& params) override {
91 NSUInteger style_mask = NSBorderlessWindowMask;
92 if (params.type == Widget::InitParams::TYPE_WINDOW) {
93 style_mask = NSTexturedBackgroundWindowMask | NSTitledWindowMask |
94 NSClosableWindowMask | NSMiniaturizableWindowMask |
95 NSResizableWindowMask;
97 return [[[NativeWidetMacTestWindow alloc]
98 initWithContentRect:ui::kWindowSizeDeterminedLater
100 backing:NSBackingStoreBuffered
101 defer:NO] autorelease];
105 DISALLOW_COPY_AND_ASSIGN(TestWindowNativeWidgetMac);
108 // Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which
109 // need access to Cocoa APIs.
110 class NativeWidgetMacTest : public WidgetTest {
112 NativeWidgetMacTest() {}
114 // The content size of NSWindows made by MakeNativeParent().
115 NSRect ParentRect() const { return NSMakeRect(100, 100, 300, 200); }
117 // Make a native NSWindow to use as a parent.
118 NSWindow* MakeNativeParent() {
119 native_parent_.reset(
120 [[NSWindow alloc] initWithContentRect:ParentRect()
121 styleMask:NSBorderlessWindowMask
122 backing:NSBackingStoreBuffered
124 [native_parent_ setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
125 [native_parent_ makeKeyAndOrderFront:nil];
126 return native_parent_;
129 // Create a Widget backed by the NativeWidetMacTestWindow NSWindow subclass.
130 Widget* CreateWidgetWithTestWindow(Widget::InitParams params,
131 NativeWidetMacTestWindow** window) {
132 Widget* widget = new Widget;
133 params.native_widget = new TestWindowNativeWidgetMac(widget);
134 widget->Init(params);
136 *window = base::mac::ObjCCastStrict<NativeWidetMacTestWindow>(
137 widget->GetNativeWindow());
138 EXPECT_TRUE(*window);
143 base::scoped_nsobject<NSWindow> native_parent_;
145 DISALLOW_COPY_AND_ASSIGN(NativeWidgetMacTest);
148 class WidgetChangeObserver : public TestWidgetObserver {
150 WidgetChangeObserver(Widget* widget) : TestWidgetObserver(widget) {}
152 void WaitForVisibleCounts(int gained, int lost) {
153 if (gained_visible_count_ >= gained && lost_visible_count_ >= lost)
156 target_gained_visible_count_ = gained;
157 target_lost_visible_count_ = lost;
159 base::RunLoop run_loop;
160 run_loop_ = &run_loop;
161 base::MessageLoop::current()->task_runner()->PostDelayedTask(
162 FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
167 int gained_visible_count() const { return gained_visible_count_; }
168 int lost_visible_count() const { return lost_visible_count_; }
172 void OnWidgetVisibilityChanged(Widget* widget,
173 bool visible) override {
174 ++(visible ? gained_visible_count_ : lost_visible_count_);
175 if (run_loop_ && gained_visible_count_ >= target_gained_visible_count_ &&
176 lost_visible_count_ >= target_lost_visible_count_)
180 int gained_visible_count_ = 0;
181 int lost_visible_count_ = 0;
182 int target_gained_visible_count_ = 0;
183 int target_lost_visible_count_ = 0;
184 base::RunLoop* run_loop_ = nullptr;
186 DISALLOW_COPY_AND_ASSIGN(WidgetChangeObserver);
189 // Test visibility states triggered externally.
190 TEST_F(NativeWidgetMacTest, HideAndShowExternally) {
191 Widget* widget = CreateTopLevelPlatformWidget();
192 NSWindow* ns_window = widget->GetNativeWindow();
193 WidgetChangeObserver observer(widget);
195 // Should initially be hidden.
196 EXPECT_FALSE(widget->IsVisible());
197 EXPECT_FALSE([ns_window isVisible]);
198 EXPECT_EQ(0, observer.gained_visible_count());
199 EXPECT_EQ(0, observer.lost_visible_count());
202 EXPECT_TRUE(widget->IsVisible());
203 EXPECT_TRUE([ns_window isVisible]);
204 EXPECT_EQ(1, observer.gained_visible_count());
205 EXPECT_EQ(0, observer.lost_visible_count());
208 EXPECT_FALSE(widget->IsVisible());
209 EXPECT_FALSE([ns_window isVisible]);
210 EXPECT_EQ(1, observer.gained_visible_count());
211 EXPECT_EQ(1, observer.lost_visible_count());
214 EXPECT_TRUE(widget->IsVisible());
215 EXPECT_TRUE([ns_window isVisible]);
216 EXPECT_EQ(2, observer.gained_visible_count());
217 EXPECT_EQ(1, observer.lost_visible_count());
219 // Test when hiding individual windows.
220 [ns_window orderOut:nil];
221 EXPECT_FALSE(widget->IsVisible());
222 EXPECT_FALSE([ns_window isVisible]);
223 EXPECT_EQ(2, observer.gained_visible_count());
224 EXPECT_EQ(2, observer.lost_visible_count());
226 [ns_window orderFront:nil];
227 EXPECT_TRUE(widget->IsVisible());
228 EXPECT_TRUE([ns_window isVisible]);
229 EXPECT_EQ(3, observer.gained_visible_count());
230 EXPECT_EQ(2, observer.lost_visible_count());
232 // Test when hiding the entire application. This doesn't send an orderOut:
235 // When the activation policy is NSApplicationActivationPolicyRegular, the
236 // calls via NSApp are asynchronous, and the run loop needs to be flushed.
237 // With NSApplicationActivationPolicyProhibited, the following
238 // WaitForVisibleCounts calls are superfluous, but don't hurt.
239 observer.WaitForVisibleCounts(3, 3);
240 EXPECT_FALSE(widget->IsVisible());
241 EXPECT_FALSE([ns_window isVisible]);
242 EXPECT_EQ(3, observer.gained_visible_count());
243 EXPECT_EQ(3, observer.lost_visible_count());
245 [NSApp unhideWithoutActivation];
246 observer.WaitForVisibleCounts(4, 3);
247 EXPECT_TRUE(widget->IsVisible());
248 EXPECT_TRUE([ns_window isVisible]);
249 EXPECT_EQ(4, observer.gained_visible_count());
250 EXPECT_EQ(3, observer.lost_visible_count());
252 // Hide again to test unhiding with an activation.
254 observer.WaitForVisibleCounts(4, 4);
255 EXPECT_EQ(4, observer.lost_visible_count());
257 observer.WaitForVisibleCounts(5, 4);
258 EXPECT_EQ(5, observer.gained_visible_count());
260 // Hide again to test makeKeyAndOrderFront:.
261 [ns_window orderOut:nil];
262 EXPECT_FALSE(widget->IsVisible());
263 EXPECT_FALSE([ns_window isVisible]);
264 EXPECT_EQ(5, observer.gained_visible_count());
265 EXPECT_EQ(5, observer.lost_visible_count());
267 [ns_window makeKeyAndOrderFront:nil];
268 EXPECT_TRUE(widget->IsVisible());
269 EXPECT_TRUE([ns_window isVisible]);
270 EXPECT_EQ(6, observer.gained_visible_count());
271 EXPECT_EQ(5, observer.lost_visible_count());
273 // No change when closing.
275 EXPECT_EQ(5, observer.lost_visible_count());
276 EXPECT_EQ(6, observer.gained_visible_count());
279 // A view that counts calls to OnPaint().
280 class PaintCountView : public View {
282 PaintCountView() : paint_count_(0) {
283 SetBounds(0, 0, 100, 100);
287 void OnPaint(gfx::Canvas* canvas) override {
288 EXPECT_TRUE(GetWidget()->IsVisible());
292 int paint_count() { return paint_count_; }
297 DISALLOW_COPY_AND_ASSIGN(PaintCountView);
300 // Test minimized states triggered externally, implied visibility and restored
301 // bounds whilst minimized.
302 TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
303 Widget* widget = new Widget;
304 Widget::InitParams init_params(Widget::InitParams::TYPE_WINDOW);
305 widget->Init(init_params);
307 PaintCountView* view = new PaintCountView();
308 widget->GetContentsView()->AddChildView(view);
309 NSWindow* ns_window = widget->GetNativeWindow();
310 WidgetChangeObserver observer(widget);
312 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
314 EXPECT_TRUE(view->IsDrawn());
315 EXPECT_EQ(0, view->paint_count());
317 base::RunLoop().RunUntilIdle();
319 EXPECT_EQ(1, observer.gained_visible_count());
320 EXPECT_EQ(0, observer.lost_visible_count());
321 const gfx::Rect restored_bounds = widget->GetRestoredBounds();
322 EXPECT_FALSE(restored_bounds.IsEmpty());
323 EXPECT_FALSE(widget->IsMinimized());
324 EXPECT_TRUE(widget->IsVisible());
326 // Showing should paint.
327 EXPECT_EQ(1, view->paint_count());
329 // First try performMiniaturize:, which requires a minimize button. Note that
330 // Cocoa just blocks the UI thread during the animation, so no need to do
331 // anything fancy to wait for it finish.
332 [ns_window performMiniaturize:nil];
333 base::RunLoop().RunUntilIdle();
335 EXPECT_TRUE(widget->IsMinimized());
336 EXPECT_FALSE(widget->IsVisible()); // Minimizing also makes things invisible.
337 EXPECT_EQ(1, observer.gained_visible_count());
338 EXPECT_EQ(1, observer.lost_visible_count());
339 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
341 // No repaint when minimizing. But note that this is partly due to not calling
342 // [NSView setNeedsDisplay:YES] on the content view. The superview, which is
343 // an NSThemeFrame, would repaint |view| if we had, because the miniaturize
344 // button is highlighted for performMiniaturize.
345 EXPECT_EQ(1, view->paint_count());
347 [ns_window deminiaturize:nil];
348 base::RunLoop().RunUntilIdle();
350 EXPECT_FALSE(widget->IsMinimized());
351 EXPECT_TRUE(widget->IsVisible());
352 EXPECT_EQ(2, observer.gained_visible_count());
353 EXPECT_EQ(1, observer.lost_visible_count());
354 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
356 EXPECT_EQ(2, view->paint_count()); // A single paint when deminiaturizing.
357 EXPECT_FALSE([ns_window isMiniaturized]);
360 base::RunLoop().RunUntilIdle();
362 EXPECT_TRUE(widget->IsMinimized());
363 EXPECT_TRUE([ns_window isMiniaturized]);
364 EXPECT_EQ(2, observer.gained_visible_count());
365 EXPECT_EQ(2, observer.lost_visible_count());
366 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
367 EXPECT_EQ(2, view->paint_count()); // No paint when miniaturizing.
369 widget->Restore(); // If miniaturized, should deminiaturize.
370 base::RunLoop().RunUntilIdle();
372 EXPECT_FALSE(widget->IsMinimized());
373 EXPECT_FALSE([ns_window isMiniaturized]);
374 EXPECT_EQ(3, observer.gained_visible_count());
375 EXPECT_EQ(2, observer.lost_visible_count());
376 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
377 EXPECT_EQ(3, view->paint_count());
379 widget->Restore(); // If not miniaturized, does nothing.
380 base::RunLoop().RunUntilIdle();
382 EXPECT_FALSE(widget->IsMinimized());
383 EXPECT_FALSE([ns_window isMiniaturized]);
384 EXPECT_EQ(3, observer.gained_visible_count());
385 EXPECT_EQ(2, observer.lost_visible_count());
386 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
387 EXPECT_EQ(3, view->paint_count());
391 // Create a widget without a minimize button.
392 widget = CreateTopLevelFramelessPlatformWidget();
393 ns_window = widget->GetNativeWindow();
394 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
396 EXPECT_FALSE(widget->IsMinimized());
398 // This should fail, since performMiniaturize: requires a minimize button.
399 [ns_window performMiniaturize:nil];
400 EXPECT_FALSE(widget->IsMinimized());
402 // But this should work.
404 EXPECT_TRUE(widget->IsMinimized());
406 // Test closing while minimized.
410 // Simple view for the SetCursor test that overrides View::GetCursor().
411 class CursorView : public View {
413 CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
414 SetBounds(x, 0, 100, 300);
418 gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
425 DISALLOW_COPY_AND_ASSIGN(CursorView);
428 // Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
429 // -[NSCursor currentCursor] to validate expectations. Note that currentCursor
430 // is just "the top cursor on the application's cursor stack.", which is why it
431 // is safe to use this in a non-interactive UI test with the EventGenerator.
432 TEST_F(NativeWidgetMacTest, SetCursor) {
433 NSCursor* arrow = [NSCursor arrowCursor];
434 NSCursor* hand = GetNativeHandCursor();
435 NSCursor* ibeam = GetNativeIBeamCursor();
437 Widget* widget = CreateTopLevelPlatformWidget();
438 widget->SetBounds(gfx::Rect(0, 0, 300, 300));
439 widget->GetContentsView()->AddChildView(new CursorView(0, hand));
440 widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
443 // Events used to simulate tracking rectangle updates. These are not passed to
444 // toolkit-views, so it only matters whether they are inside or outside the
446 NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
447 NSMakePoint(100, 100), NSMouseMoved, 0);
448 NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
449 NSMakePoint(-50, -50), NSMouseMoved, 0);
451 EXPECT_NE(arrow, hand);
452 EXPECT_NE(arrow, ibeam);
454 // At the start of the test, the cursor stack should be empty.
455 EXPECT_FALSE([NSCursor currentCursor]);
457 // Use an event generator to ask views code to set the cursor. However, note
458 // that this does not cause Cocoa to generate tracking rectangle updates.
459 ui::test::EventGenerator event_generator(GetContext(),
460 widget->GetNativeWindow());
462 // Move the mouse over the first view, then simulate a tracking rectangle
464 event_generator.MoveMouseTo(gfx::Point(50, 50));
465 [widget->GetNativeWindow() cursorUpdate:event_in_content];
466 EXPECT_EQ(hand, [NSCursor currentCursor]);
468 // A tracking rectangle update not in the content area should forward to
469 // the native NSWindow implementation, which sets the arrow cursor.
470 [widget->GetNativeWindow() cursorUpdate:event_out_of_content];
471 EXPECT_EQ(arrow, [NSCursor currentCursor]);
473 // Now move to the second view.
474 event_generator.MoveMouseTo(gfx::Point(150, 50));
475 [widget->GetNativeWindow() cursorUpdate:event_in_content];
476 EXPECT_EQ(ibeam, [NSCursor currentCursor]);
478 // Moving to the third view (but remaining in the content area) should also
479 // forward to the native NSWindow implementation.
480 event_generator.MoveMouseTo(gfx::Point(250, 50));
481 [widget->GetNativeWindow() cursorUpdate:event_in_content];
482 EXPECT_EQ(arrow, [NSCursor currentCursor]);
487 // Tests that an accessibility request from the system makes its way through to
488 // a views::Label filling the window.
489 TEST_F(NativeWidgetMacTest, AccessibilityIntegration) {
490 Widget* widget = CreateTopLevelPlatformWidget();
491 gfx::Rect screen_rect(50, 50, 100, 100);
492 widget->SetBounds(screen_rect);
494 const base::string16 test_string = base::ASCIIToUTF16("Green");
495 views::Label* label = new views::Label(test_string);
496 label->SetBounds(0, 0, 100, 100);
497 widget->GetContentsView()->AddChildView(label);
500 // Accessibility hit tests come in Cocoa screen coordinates.
501 NSRect nsrect = gfx::ScreenRectToNSRect(screen_rect);
502 NSPoint midpoint = NSMakePoint(NSMidX(nsrect), NSMidY(nsrect));
504 id hit = [widget->GetNativeWindow() accessibilityHitTest:midpoint];
505 id title = [hit accessibilityAttributeValue:NSAccessibilityTitleAttribute];
506 EXPECT_NSEQ(title, @"Green");
511 // Tests creating a views::Widget parented off a native NSWindow.
512 TEST_F(NativeWidgetMacTest, NonWidgetParent) {
513 NSWindow* native_parent = MakeNativeParent();
515 base::scoped_nsobject<NSView> anchor_view(
516 [[NSView alloc] initWithFrame:[[native_parent contentView] bounds]]);
517 [[native_parent contentView] addSubview:anchor_view];
519 // Note: Don't use WidgetTest::CreateChildPlatformWidget because that makes
520 // windows of TYPE_CONTROL which are automatically made visible. But still
521 // mark it as a child to test window positioning.
522 Widget* child = new Widget;
523 Widget::InitParams init_params;
524 init_params.parent = anchor_view;
525 init_params.child = true;
526 child->Init(init_params);
528 TestWidgetObserver child_observer(child);
530 // GetTopLevelNativeWidget() only goes as far as there exists a Widget (i.e.
531 // must stop at |child|.
532 internal::NativeWidgetPrivate* top_level_widget =
533 internal::NativeWidgetPrivate::GetTopLevelNativeWidget(
534 child->GetNativeView());
535 EXPECT_EQ(child, top_level_widget->GetWidget());
537 // To verify the parent, we need to use NativeWidgetMac APIs.
538 BridgedNativeWidget* bridged_native_widget =
539 NativeWidgetMac::GetBridgeForNativeWindow(child->GetNativeWindow());
540 EXPECT_EQ(native_parent, bridged_native_widget->parent()->GetNSWindow());
542 child->SetBounds(gfx::Rect(50, 50, 200, 100));
543 EXPECT_FALSE(child->IsVisible());
544 EXPECT_EQ(0u, [[native_parent childWindows] count]);
547 EXPECT_TRUE(child->IsVisible());
548 EXPECT_EQ(1u, [[native_parent childWindows] count]);
549 EXPECT_EQ(child->GetNativeWindow(),
550 [[native_parent childWindows] objectAtIndex:0]);
551 EXPECT_EQ(native_parent, [child->GetNativeWindow() parentWindow]);
553 // Child should be positioned on screen relative to the parent, but note we
554 // positioned the parent in Cocoa coordinates, so we need to convert.
555 gfx::Point parent_origin = gfx::ScreenRectFromNSRect(ParentRect()).origin();
556 EXPECT_EQ(gfx::Rect(150, parent_origin.y() + 50, 200, 100),
557 child->GetWindowBoundsInScreen());
559 // Removing the anchor_view from its view hierarchy is permitted. This should
560 // not break the relationship between the two windows.
561 [anchor_view removeFromSuperview];
563 EXPECT_EQ(native_parent, bridged_native_widget->parent()->GetNSWindow());
565 // Closing the parent should close and destroy the child.
566 EXPECT_FALSE(child_observer.widget_closed());
567 [native_parent close];
568 EXPECT_TRUE(child_observer.widget_closed());
570 EXPECT_EQ(0u, [[native_parent childWindows] count]);
573 // Use Native APIs to query the tooltip text that would be shown once the
574 // tooltip delay had elapsed.
575 base::string16 TooltipTextForWidget(Widget* widget) {
576 // For Mac, the actual location doesn't matter, since there is only one native
577 // view and it fills the window. This just assumes the window is at least big
578 // big enough for a constant coordinate to be within it.
579 NSPoint point = NSMakePoint(30, 30);
580 NSView* view = [widget->GetNativeView() hitTest:point];
582 [view view:view stringForToolTip:0 point:point userData:nullptr];
583 return base::SysNSStringToUTF16(text);
586 // Tests tooltips. The test doesn't wait for tooltips to appear. That is, the
587 // test assumes Cocoa calls stringForToolTip: at appropriate times and that,
588 // when a tooltip is already visible, changing it causes an update. These were
589 // tested manually by inserting a base::RunLoop.Run().
590 TEST_F(NativeWidgetMacTest, Tooltips) {
591 Widget* widget = CreateTopLevelPlatformWidget();
592 gfx::Rect screen_rect(50, 50, 100, 100);
593 widget->SetBounds(screen_rect);
595 const base::string16 tooltip_back = base::ASCIIToUTF16("Back");
596 const base::string16 tooltip_front = base::ASCIIToUTF16("Front");
597 const base::string16 long_tooltip(2000, 'W');
599 // Create a nested layout to test corner cases.
600 LabelButton* back = new LabelButton(nullptr, base::string16());
601 back->SetBounds(10, 10, 80, 80);
602 widget->GetContentsView()->AddChildView(back);
605 ui::test::EventGenerator event_generator(GetContext(),
606 widget->GetNativeWindow());
608 // Initially, there should be no tooltip.
609 event_generator.MoveMouseTo(gfx::Point(50, 50));
610 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
612 // Create a new button for the "front", and set the tooltip, but don't add it
613 // to the view hierarchy yet.
614 LabelButton* front = new LabelButton(nullptr, base::string16());
615 front->SetBounds(20, 20, 40, 40);
616 front->SetTooltipText(tooltip_front);
618 // Changing the tooltip text shouldn't require an additional mousemove to take
620 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
621 back->SetTooltipText(tooltip_back);
622 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
624 // Adding a new view under the mouse should also take immediate effect.
625 back->AddChildView(front);
626 EXPECT_EQ(tooltip_front, TooltipTextForWidget(widget));
628 // A long tooltip will be wrapped by Cocoa, but the full string should appear.
629 // Note that render widget hosts clip at 1024 to prevent DOS, but in toolkit-
630 // views the UI is more trusted.
631 front->SetTooltipText(long_tooltip);
632 EXPECT_EQ(long_tooltip, TooltipTextForWidget(widget));
634 // Move the mouse to a different view - tooltip should change.
635 event_generator.MoveMouseTo(gfx::Point(15, 15));
636 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
638 // Move the mouse off of any view, tooltip should clear.
639 event_generator.MoveMouseTo(gfx::Point(5, 5));
640 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
647 // Delegate to make Widgets of MODAL_TYPE_CHILD.
648 class ChildModalDialogDelegate : public DialogDelegateView {
650 ChildModalDialogDelegate() {}
653 ui::ModalType GetModalType() const override { return ui::MODAL_TYPE_CHILD; }
656 DISALLOW_COPY_AND_ASSIGN(ChildModalDialogDelegate);
659 // While in scope, waits for a call to a swizzled objective C method, then quits
660 // a nested run loop.
661 class ScopedSwizzleWaiter {
663 explicit ScopedSwizzleWaiter(Class target)
665 [TestStopAnimationWaiter class],
666 @selector(setWindowStateForEnd)) {
671 ~ScopedSwizzleWaiter() { instance_ = nullptr; }
673 static IMP GetMethodAndMarkCalled() {
674 return instance_->GetMethodInternal();
677 void WaitForMethod() {
681 base::RunLoop run_loop;
682 base::MessageLoop::current()->task_runner()->PostDelayedTask(
683 FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
684 run_loop_ = &run_loop;
689 bool method_called() const { return method_called_; }
692 IMP GetMethodInternal() {
693 DCHECK(!method_called_);
694 method_called_ = true;
697 return swizzler_.GetOriginalImplementation();
700 static ScopedSwizzleWaiter* instance_;
702 base::mac::ScopedObjCClassSwizzler swizzler_;
703 base::RunLoop* run_loop_ = nullptr;
704 bool method_called_ = false;
706 DISALLOW_COPY_AND_ASSIGN(ScopedSwizzleWaiter);
709 ScopedSwizzleWaiter* ScopedSwizzleWaiter::instance_ = nullptr;
711 // Shows a modal widget and waits for the show animation to complete. Waiting is
712 // not compulsory (calling Close() while animating the show will cancel the show
713 // animation). However, testing with overlapping swizzlers is tricky.
714 Widget* ShowChildModalWidgetAndWait(NSWindow* native_parent) {
715 Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget(
716 new ChildModalDialogDelegate, nullptr, [native_parent contentView]);
718 modal_dialog_widget->SetBounds(gfx::Rect(50, 50, 200, 150));
719 EXPECT_FALSE(modal_dialog_widget->IsVisible());
720 ScopedSwizzleWaiter show_waiter([ConstrainedWindowAnimationShow class]);
722 modal_dialog_widget->Show();
723 // Visible immediately (although it animates from transparent).
724 EXPECT_TRUE(modal_dialog_widget->IsVisible());
726 // Run the animation.
727 show_waiter.WaitForMethod();
728 EXPECT_TRUE(modal_dialog_widget->IsVisible());
729 EXPECT_TRUE(show_waiter.method_called());
730 return modal_dialog_widget;
735 // Tests object lifetime for the show/hide animations used for child-modal
736 // windows. Parents the dialog off a native parent window (not a views::Widget).
737 TEST_F(NativeWidgetMacTest, NativeWindowChildModalShowHide) {
738 NSWindow* native_parent = MakeNativeParent();
740 Widget* modal_dialog_widget = ShowChildModalWidgetAndWait(native_parent);
741 TestWidgetObserver widget_observer(modal_dialog_widget);
743 ScopedSwizzleWaiter hide_waiter([ConstrainedWindowAnimationHide class]);
744 EXPECT_TRUE(modal_dialog_widget->IsVisible());
745 EXPECT_FALSE(widget_observer.widget_closed());
747 // Widget::Close() is always asynchronous, so we can check that the widget
748 // is initially visible, but then it's destroyed.
749 modal_dialog_widget->Close();
750 EXPECT_TRUE(modal_dialog_widget->IsVisible());
751 EXPECT_FALSE(hide_waiter.method_called());
752 EXPECT_FALSE(widget_observer.widget_closed());
754 // Wait for a hide to finish.
755 hide_waiter.WaitForMethod();
756 EXPECT_TRUE(hide_waiter.method_called());
758 // The animation finishing should also mean it has closed the window.
759 EXPECT_TRUE(widget_observer.widget_closed());
763 // Make a new dialog to test another lifetime flow.
764 Widget* modal_dialog_widget = ShowChildModalWidgetAndWait(native_parent);
765 TestWidgetObserver widget_observer(modal_dialog_widget);
767 // Start an asynchronous close as above.
768 ScopedSwizzleWaiter hide_waiter([ConstrainedWindowAnimationHide class]);
769 modal_dialog_widget->Close();
770 EXPECT_FALSE(widget_observer.widget_closed());
771 EXPECT_FALSE(hide_waiter.method_called());
773 // Now close the _parent_ window to force a synchronous close of the child.
774 [native_parent close];
776 // Widget is destroyed immediately. No longer paints, but the animation is
778 EXPECT_TRUE(widget_observer.widget_closed());
779 EXPECT_FALSE(hide_waiter.method_called());
781 // Wait for the hide again. It will call close on its retained copy of the
782 // child NSWindow, but that's fine since all the C++ objects are detached.
783 hide_waiter.WaitForMethod();
784 EXPECT_TRUE(hide_waiter.method_called());
788 // Test calls to Widget::ReparentNativeView() that result in a no-op on Mac.
789 // Tests with both native and non-native parents.
790 TEST_F(NativeWidgetMacTest, NoopReparentNativeView) {
791 NSWindow* parent = MakeNativeParent();
792 Widget* dialog = views::DialogDelegate::CreateDialogWidget(
793 new DialogDelegateView, nullptr, [parent contentView]);
794 BridgedNativeWidget* bridge =
795 NativeWidgetMac::GetBridgeForNativeWindow(dialog->GetNativeWindow());
797 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
798 Widget::ReparentNativeView(dialog->GetNativeView(), [parent contentView]);
799 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
803 Widget* parent_widget = CreateNativeDesktopWidget();
804 parent = parent_widget->GetNativeWindow();
805 dialog = views::DialogDelegate::CreateDialogWidget(
806 new DialogDelegateView, nullptr, [parent contentView]);
807 bridge = NativeWidgetMac::GetBridgeForNativeWindow(dialog->GetNativeWindow());
809 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
810 Widget::ReparentNativeView(dialog->GetNativeView(), [parent contentView]);
811 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
813 parent_widget->CloseNow();
816 // Tests Cocoa properties that should be given to particular widget types.
817 TEST_F(NativeWidgetMacTest, NativeProperties) {
818 // Create a regular widget (TYPE_WINDOW).
819 Widget* regular_widget = CreateNativeDesktopWidget();
820 EXPECT_TRUE([regular_widget->GetNativeWindow() canBecomeKeyWindow]);
821 EXPECT_TRUE([regular_widget->GetNativeWindow() canBecomeMainWindow]);
823 // Disabling activation should prevent key and main status.
824 regular_widget->widget_delegate()->set_can_activate(false);
825 EXPECT_FALSE([regular_widget->GetNativeWindow() canBecomeKeyWindow]);
826 EXPECT_FALSE([regular_widget->GetNativeWindow() canBecomeMainWindow]);
828 // Create a dialog widget (also TYPE_WINDOW), but with a DialogDelegate.
829 Widget* dialog_widget = views::DialogDelegate::CreateDialogWidget(
830 new ChildModalDialogDelegate, nullptr, regular_widget->GetNativeView());
831 EXPECT_TRUE([dialog_widget->GetNativeWindow() canBecomeKeyWindow]);
832 // Dialogs shouldn't take main status away from their parent.
833 EXPECT_FALSE([dialog_widget->GetNativeWindow() canBecomeMainWindow]);
835 regular_widget->CloseNow();
838 NSData* WindowContentsAsTIFF(NSWindow* window) {
839 NSView* frame_view = [[window contentView] superview];
840 EXPECT_TRUE(frame_view);
842 // Inset to mask off left and right edges which vary in HighDPI.
843 NSRect bounds = NSInsetRect([frame_view bounds], 4, 0);
845 // On 10.6, the grippy changes appearance slightly when painted the second
846 // time in a textured window. Since this test cares about the window title,
847 // cut off the bottom of the window.
848 bounds.size.height -= 40;
849 bounds.origin.y += 40;
851 NSBitmapImageRep* bitmap =
852 [frame_view bitmapImageRepForCachingDisplayInRect:bounds];
855 [frame_view cacheDisplayInRect:bounds toBitmapImageRep:bitmap];
856 NSData* tiff = [bitmap TIFFRepresentation];
861 class CustomTitleWidgetDelegate : public WidgetDelegate {
863 CustomTitleWidgetDelegate(Widget* widget)
864 : widget_(widget), should_show_title_(true) {}
866 void set_title(const base::string16& title) { title_ = title; }
867 void set_should_show_title(bool show) { should_show_title_ = show; }
870 base::string16 GetWindowTitle() const override { return title_; }
871 bool ShouldShowWindowTitle() const override { return should_show_title_; }
872 Widget* GetWidget() override { return widget_; };
873 const Widget* GetWidget() const override { return widget_; };
877 base::string16 title_;
878 bool should_show_title_;
880 DISALLOW_COPY_AND_ASSIGN(CustomTitleWidgetDelegate);
883 // Test that undocumented title-hiding API we're using does the job.
884 TEST_F(NativeWidgetMacTest, DoesHideTitle) {
885 // Same as CreateTopLevelPlatformWidget but with a custom delegate.
886 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
887 Widget* widget = new Widget;
888 params.native_widget = new NativeWidgetCapture(widget);
889 CustomTitleWidgetDelegate delegate(widget);
890 params.delegate = &delegate;
891 params.bounds = gfx::Rect(0, 0, 800, 600);
892 widget->Init(params);
895 NSWindow* ns_window = widget->GetNativeWindow();
896 // Disable color correction so we can read unmodified values from the bitmap.
897 [ns_window setColorSpace:[NSColorSpace sRGBColorSpace]];
899 EXPECT_EQ(base::string16(), delegate.GetWindowTitle());
900 EXPECT_NSEQ(@"", [ns_window title]);
901 NSData* empty_title_data = WindowContentsAsTIFF(ns_window);
903 delegate.set_title(base::ASCIIToUTF16("This is a title"));
904 widget->UpdateWindowTitle();
905 NSData* this_title_data = WindowContentsAsTIFF(ns_window);
907 // The default window with a title should look different from the
908 // window with an empty title.
909 EXPECT_FALSE([empty_title_data isEqualToData:this_title_data]);
911 delegate.set_should_show_title(false);
912 delegate.set_title(base::ASCIIToUTF16("This is another title"));
913 widget->UpdateWindowTitle();
914 NSData* hidden_title_data = WindowContentsAsTIFF(ns_window);
916 // With our magic setting, the window with a title should look the
917 // same as the window with an empty title.
918 EXPECT_TRUE([ns_window _isTitleHidden]);
919 EXPECT_TRUE([empty_title_data isEqualToData:hidden_title_data]);
924 // Test calls to invalidate the shadow when composited frames arrive.
925 TEST_F(NativeWidgetMacTest, InvalidateShadow) {
926 NativeWidetMacTestWindow* window;
927 const gfx::Rect rect(0, 0, 100, 200);
928 Widget::InitParams init_params =
929 CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
930 init_params.bounds = rect;
931 Widget* widget = CreateWidgetWithTestWindow(init_params, &window);
933 // Simulate the initial paint.
934 BridgedNativeWidgetTestApi(window).SimulateFrameSwap(rect.size());
936 // Default is an opaque window, so shadow doesn't need to be invalidated.
937 EXPECT_EQ(0, [window invalidateShadowCount]);
940 init_params.opacity = Widget::InitParams::TRANSLUCENT_WINDOW;
941 widget = CreateWidgetWithTestWindow(init_params, &window);
942 BridgedNativeWidgetTestApi test_api(window);
944 // First paint on a translucent window needs to invalidate the shadow. Once.
945 EXPECT_EQ(0, [window invalidateShadowCount]);
946 test_api.SimulateFrameSwap(rect.size());
947 EXPECT_EQ(1, [window invalidateShadowCount]);
948 test_api.SimulateFrameSwap(rect.size());
949 EXPECT_EQ(1, [window invalidateShadowCount]);
951 // Resizing the window also needs to trigger a shadow invalidation.
952 [window setContentSize:NSMakeSize(123, 456)];
953 // A "late" frame swap at the old size should do nothing.
954 test_api.SimulateFrameSwap(rect.size());
955 EXPECT_EQ(1, [window invalidateShadowCount]);
957 test_api.SimulateFrameSwap(gfx::Size(123, 456));
958 EXPECT_EQ(2, [window invalidateShadowCount]);
959 test_api.SimulateFrameSwap(gfx::Size(123, 456));
960 EXPECT_EQ(2, [window invalidateShadowCount]);
968 @implementation TestStopAnimationWaiter
969 - (void)setWindowStateForEnd {
970 views::test::ScopedSwizzleWaiter::GetMethodAndMarkCalled()(self, _cmd);
974 @implementation NativeWidetMacTestWindow
976 @synthesize invalidateShadowCount = invalidateShadowCount_;
978 - (void)invalidateShadow {
979 ++invalidateShadowCount_;
980 [super invalidateShadow];