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 NativeWidgetMacTestWindow : 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 NativeWidgetMacNSWindow* CreateNSWindow(
91 const Widget::InitParams& params) override {
92 NSUInteger style_mask = NSBorderlessWindowMask;
93 if (params.type == Widget::InitParams::TYPE_WINDOW) {
94 style_mask = NSTexturedBackgroundWindowMask | NSTitledWindowMask |
95 NSClosableWindowMask | NSMiniaturizableWindowMask |
96 NSResizableWindowMask;
98 return [[[NativeWidgetMacTestWindow alloc]
99 initWithContentRect:ui::kWindowSizeDeterminedLater
101 backing:NSBackingStoreBuffered
102 defer:NO] autorelease];
106 DISALLOW_COPY_AND_ASSIGN(TestWindowNativeWidgetMac);
109 // Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which
110 // need access to Cocoa APIs.
111 class NativeWidgetMacTest : public WidgetTest {
113 NativeWidgetMacTest() {}
115 // The content size of NSWindows made by MakeNativeParent().
116 NSRect ParentRect() const { return NSMakeRect(100, 100, 300, 200); }
118 // Make a native NSWindow to use as a parent.
119 NSWindow* MakeNativeParent() {
120 native_parent_.reset(
121 [[NSWindow alloc] initWithContentRect:ParentRect()
122 styleMask:NSBorderlessWindowMask
123 backing:NSBackingStoreBuffered
125 [native_parent_ setReleasedWhenClosed:NO]; // Owned by scoped_nsobject.
126 [native_parent_ makeKeyAndOrderFront:nil];
127 return native_parent_;
130 // Create a Widget backed by the NativeWidgetMacTestWindow NSWindow subclass.
131 Widget* CreateWidgetWithTestWindow(Widget::InitParams params,
132 NativeWidgetMacTestWindow** window) {
133 Widget* widget = new Widget;
134 params.native_widget = new TestWindowNativeWidgetMac(widget);
135 widget->Init(params);
137 *window = base::mac::ObjCCastStrict<NativeWidgetMacTestWindow>(
138 widget->GetNativeWindow());
139 EXPECT_TRUE(*window);
144 base::scoped_nsobject<NSWindow> native_parent_;
146 DISALLOW_COPY_AND_ASSIGN(NativeWidgetMacTest);
149 class WidgetChangeObserver : public TestWidgetObserver {
151 WidgetChangeObserver(Widget* widget) : TestWidgetObserver(widget) {}
153 void WaitForVisibleCounts(int gained, int lost) {
154 if (gained_visible_count_ >= gained && lost_visible_count_ >= lost)
157 target_gained_visible_count_ = gained;
158 target_lost_visible_count_ = lost;
160 base::RunLoop run_loop;
161 run_loop_ = &run_loop;
162 base::MessageLoop::current()->task_runner()->PostDelayedTask(
163 FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
168 int gained_visible_count() const { return gained_visible_count_; }
169 int lost_visible_count() const { return lost_visible_count_; }
173 void OnWidgetVisibilityChanged(Widget* widget,
174 bool visible) override {
175 ++(visible ? gained_visible_count_ : lost_visible_count_);
176 if (run_loop_ && gained_visible_count_ >= target_gained_visible_count_ &&
177 lost_visible_count_ >= target_lost_visible_count_)
181 int gained_visible_count_ = 0;
182 int lost_visible_count_ = 0;
183 int target_gained_visible_count_ = 0;
184 int target_lost_visible_count_ = 0;
185 base::RunLoop* run_loop_ = nullptr;
187 DISALLOW_COPY_AND_ASSIGN(WidgetChangeObserver);
190 // Test visibility states triggered externally.
191 TEST_F(NativeWidgetMacTest, HideAndShowExternally) {
192 Widget* widget = CreateTopLevelPlatformWidget();
193 NSWindow* ns_window = widget->GetNativeWindow();
194 WidgetChangeObserver observer(widget);
196 // Should initially be hidden.
197 EXPECT_FALSE(widget->IsVisible());
198 EXPECT_FALSE([ns_window isVisible]);
199 EXPECT_EQ(0, observer.gained_visible_count());
200 EXPECT_EQ(0, observer.lost_visible_count());
203 EXPECT_TRUE(widget->IsVisible());
204 EXPECT_TRUE([ns_window isVisible]);
205 EXPECT_EQ(1, observer.gained_visible_count());
206 EXPECT_EQ(0, observer.lost_visible_count());
209 EXPECT_FALSE(widget->IsVisible());
210 EXPECT_FALSE([ns_window isVisible]);
211 EXPECT_EQ(1, observer.gained_visible_count());
212 EXPECT_EQ(1, observer.lost_visible_count());
215 EXPECT_TRUE(widget->IsVisible());
216 EXPECT_TRUE([ns_window isVisible]);
217 EXPECT_EQ(2, observer.gained_visible_count());
218 EXPECT_EQ(1, observer.lost_visible_count());
220 // Test when hiding individual windows.
221 [ns_window orderOut:nil];
222 EXPECT_FALSE(widget->IsVisible());
223 EXPECT_FALSE([ns_window isVisible]);
224 EXPECT_EQ(2, observer.gained_visible_count());
225 EXPECT_EQ(2, observer.lost_visible_count());
227 [ns_window orderFront:nil];
228 EXPECT_TRUE(widget->IsVisible());
229 EXPECT_TRUE([ns_window isVisible]);
230 EXPECT_EQ(3, observer.gained_visible_count());
231 EXPECT_EQ(2, observer.lost_visible_count());
233 // Test when hiding the entire application. This doesn't send an orderOut:
236 // When the activation policy is NSApplicationActivationPolicyRegular, the
237 // calls via NSApp are asynchronous, and the run loop needs to be flushed.
238 // With NSApplicationActivationPolicyProhibited, the following
239 // WaitForVisibleCounts calls are superfluous, but don't hurt.
240 observer.WaitForVisibleCounts(3, 3);
241 EXPECT_FALSE(widget->IsVisible());
242 EXPECT_FALSE([ns_window isVisible]);
243 EXPECT_EQ(3, observer.gained_visible_count());
244 EXPECT_EQ(3, observer.lost_visible_count());
246 [NSApp unhideWithoutActivation];
247 observer.WaitForVisibleCounts(4, 3);
248 EXPECT_TRUE(widget->IsVisible());
249 EXPECT_TRUE([ns_window isVisible]);
250 EXPECT_EQ(4, observer.gained_visible_count());
251 EXPECT_EQ(3, observer.lost_visible_count());
253 // Hide again to test unhiding with an activation.
255 observer.WaitForVisibleCounts(4, 4);
256 EXPECT_EQ(4, observer.lost_visible_count());
258 observer.WaitForVisibleCounts(5, 4);
259 EXPECT_EQ(5, observer.gained_visible_count());
261 // Hide again to test makeKeyAndOrderFront:.
262 [ns_window orderOut:nil];
263 EXPECT_FALSE(widget->IsVisible());
264 EXPECT_FALSE([ns_window isVisible]);
265 EXPECT_EQ(5, observer.gained_visible_count());
266 EXPECT_EQ(5, observer.lost_visible_count());
268 [ns_window makeKeyAndOrderFront:nil];
269 EXPECT_TRUE(widget->IsVisible());
270 EXPECT_TRUE([ns_window isVisible]);
271 EXPECT_EQ(6, observer.gained_visible_count());
272 EXPECT_EQ(5, observer.lost_visible_count());
274 // No change when closing.
276 EXPECT_EQ(5, observer.lost_visible_count());
277 EXPECT_EQ(6, observer.gained_visible_count());
280 // A view that counts calls to OnPaint().
281 class PaintCountView : public View {
283 PaintCountView() { SetBounds(0, 0, 100, 100); }
286 void OnPaint(gfx::Canvas* canvas) override {
287 EXPECT_TRUE(GetWidget()->IsVisible());
289 if (run_loop_ && paint_count_ == target_paint_count_)
293 void WaitForPaintCount(int target) {
294 if (paint_count_ == target)
297 target_paint_count_ = target;
298 base::RunLoop run_loop;
299 run_loop_ = &run_loop;
304 int paint_count() { return paint_count_; }
307 int paint_count_ = 0;
308 int target_paint_count_ = 0;
309 base::RunLoop* run_loop_ = nullptr;
311 DISALLOW_COPY_AND_ASSIGN(PaintCountView);
314 // Test minimized states triggered externally, implied visibility and restored
315 // bounds whilst minimized.
316 TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
317 Widget* widget = new Widget;
318 Widget::InitParams init_params(Widget::InitParams::TYPE_WINDOW);
319 widget->Init(init_params);
321 PaintCountView* view = new PaintCountView();
322 widget->GetContentsView()->AddChildView(view);
323 NSWindow* ns_window = widget->GetNativeWindow();
324 WidgetChangeObserver observer(widget);
326 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
328 EXPECT_TRUE(view->IsDrawn());
329 EXPECT_EQ(0, view->paint_count());
331 base::RunLoop().RunUntilIdle();
333 EXPECT_EQ(1, observer.gained_visible_count());
334 EXPECT_EQ(0, observer.lost_visible_count());
335 const gfx::Rect restored_bounds = widget->GetRestoredBounds();
336 EXPECT_FALSE(restored_bounds.IsEmpty());
337 EXPECT_FALSE(widget->IsMinimized());
338 EXPECT_TRUE(widget->IsVisible());
340 // Showing should paint.
341 view->WaitForPaintCount(1);
343 // First try performMiniaturize:, which requires a minimize button. Note that
344 // Cocoa just blocks the UI thread during the animation, so no need to do
345 // anything fancy to wait for it finish.
346 [ns_window performMiniaturize:nil];
347 base::RunLoop().RunUntilIdle();
349 EXPECT_TRUE(widget->IsMinimized());
350 EXPECT_FALSE(widget->IsVisible()); // Minimizing also makes things invisible.
351 EXPECT_EQ(1, observer.gained_visible_count());
352 EXPECT_EQ(1, observer.lost_visible_count());
353 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
355 // No repaint when minimizing. But note that this is partly due to not calling
356 // [NSView setNeedsDisplay:YES] on the content view. The superview, which is
357 // an NSThemeFrame, would repaint |view| if we had, because the miniaturize
358 // button is highlighted for performMiniaturize.
359 EXPECT_EQ(1, view->paint_count());
361 [ns_window deminiaturize:nil];
362 base::RunLoop().RunUntilIdle();
364 EXPECT_FALSE(widget->IsMinimized());
365 EXPECT_TRUE(widget->IsVisible());
366 EXPECT_EQ(2, observer.gained_visible_count());
367 EXPECT_EQ(1, observer.lost_visible_count());
368 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
370 view->WaitForPaintCount(2); // A single paint when deminiaturizing.
371 EXPECT_FALSE([ns_window isMiniaturized]);
374 base::RunLoop().RunUntilIdle();
376 EXPECT_TRUE(widget->IsMinimized());
377 EXPECT_TRUE([ns_window isMiniaturized]);
378 EXPECT_EQ(2, observer.gained_visible_count());
379 EXPECT_EQ(2, observer.lost_visible_count());
380 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
381 EXPECT_EQ(2, view->paint_count()); // No paint when miniaturizing.
383 widget->Restore(); // If miniaturized, should deminiaturize.
384 base::RunLoop().RunUntilIdle();
386 EXPECT_FALSE(widget->IsMinimized());
387 EXPECT_FALSE([ns_window isMiniaturized]);
388 EXPECT_EQ(3, observer.gained_visible_count());
389 EXPECT_EQ(2, observer.lost_visible_count());
390 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
391 view->WaitForPaintCount(3);
393 widget->Restore(); // If not miniaturized, does nothing.
394 base::RunLoop().RunUntilIdle();
396 EXPECT_FALSE(widget->IsMinimized());
397 EXPECT_FALSE([ns_window isMiniaturized]);
398 EXPECT_EQ(3, observer.gained_visible_count());
399 EXPECT_EQ(2, observer.lost_visible_count());
400 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
401 EXPECT_EQ(3, view->paint_count());
405 // Create a widget without a minimize button.
406 widget = CreateTopLevelFramelessPlatformWidget();
407 ns_window = widget->GetNativeWindow();
408 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
410 EXPECT_FALSE(widget->IsMinimized());
412 // This should fail, since performMiniaturize: requires a minimize button.
413 [ns_window performMiniaturize:nil];
414 EXPECT_FALSE(widget->IsMinimized());
416 // But this should work.
418 EXPECT_TRUE(widget->IsMinimized());
420 // Test closing while minimized.
424 // Simple view for the SetCursor test that overrides View::GetCursor().
425 class CursorView : public View {
427 CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
428 SetBounds(x, 0, 100, 300);
432 gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
439 DISALLOW_COPY_AND_ASSIGN(CursorView);
442 // Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
443 // -[NSCursor currentCursor] to validate expectations. Note that currentCursor
444 // is just "the top cursor on the application's cursor stack.", which is why it
445 // is safe to use this in a non-interactive UI test with the EventGenerator.
446 TEST_F(NativeWidgetMacTest, SetCursor) {
447 NSCursor* arrow = [NSCursor arrowCursor];
448 NSCursor* hand = GetNativeHandCursor();
449 NSCursor* ibeam = GetNativeIBeamCursor();
451 Widget* widget = CreateTopLevelPlatformWidget();
452 widget->SetBounds(gfx::Rect(0, 0, 300, 300));
453 widget->GetContentsView()->AddChildView(new CursorView(0, hand));
454 widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
457 // Events used to simulate tracking rectangle updates. These are not passed to
458 // toolkit-views, so it only matters whether they are inside or outside the
460 NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
461 NSMakePoint(100, 100), NSMouseMoved, 0);
462 NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
463 NSMakePoint(-50, -50), NSMouseMoved, 0);
465 EXPECT_NE(arrow, hand);
466 EXPECT_NE(arrow, ibeam);
468 // At the start of the test, the cursor stack should be empty.
469 EXPECT_FALSE([NSCursor currentCursor]);
471 // Use an event generator to ask views code to set the cursor. However, note
472 // that this does not cause Cocoa to generate tracking rectangle updates.
473 ui::test::EventGenerator event_generator(GetContext(),
474 widget->GetNativeWindow());
476 // Move the mouse over the first view, then simulate a tracking rectangle
478 event_generator.MoveMouseTo(gfx::Point(50, 50));
479 [widget->GetNativeWindow() cursorUpdate:event_in_content];
480 EXPECT_EQ(hand, [NSCursor currentCursor]);
482 // A tracking rectangle update not in the content area should forward to
483 // the native NSWindow implementation, which sets the arrow cursor.
484 [widget->GetNativeWindow() cursorUpdate:event_out_of_content];
485 EXPECT_EQ(arrow, [NSCursor currentCursor]);
487 // Now move to the second view.
488 event_generator.MoveMouseTo(gfx::Point(150, 50));
489 [widget->GetNativeWindow() cursorUpdate:event_in_content];
490 EXPECT_EQ(ibeam, [NSCursor currentCursor]);
492 // Moving to the third view (but remaining in the content area) should also
493 // forward to the native NSWindow implementation.
494 event_generator.MoveMouseTo(gfx::Point(250, 50));
495 [widget->GetNativeWindow() cursorUpdate:event_in_content];
496 EXPECT_EQ(arrow, [NSCursor currentCursor]);
501 // Tests that an accessibility request from the system makes its way through to
502 // a views::Label filling the window.
503 TEST_F(NativeWidgetMacTest, AccessibilityIntegration) {
504 Widget* widget = CreateTopLevelPlatformWidget();
505 gfx::Rect screen_rect(50, 50, 100, 100);
506 widget->SetBounds(screen_rect);
508 const base::string16 test_string = base::ASCIIToUTF16("Green");
509 views::Label* label = new views::Label(test_string);
510 label->SetBounds(0, 0, 100, 100);
511 widget->GetContentsView()->AddChildView(label);
514 // Accessibility hit tests come in Cocoa screen coordinates.
515 NSRect nsrect = gfx::ScreenRectToNSRect(screen_rect);
516 NSPoint midpoint = NSMakePoint(NSMidX(nsrect), NSMidY(nsrect));
518 id hit = [widget->GetNativeWindow() accessibilityHitTest:midpoint];
519 id title = [hit accessibilityAttributeValue:NSAccessibilityTitleAttribute];
520 EXPECT_NSEQ(title, @"Green");
525 // Tests creating a views::Widget parented off a native NSWindow.
526 TEST_F(NativeWidgetMacTest, NonWidgetParent) {
527 NSWindow* native_parent = MakeNativeParent();
529 base::scoped_nsobject<NSView> anchor_view(
530 [[NSView alloc] initWithFrame:[[native_parent contentView] bounds]]);
531 [[native_parent contentView] addSubview:anchor_view];
533 // Note: Don't use WidgetTest::CreateChildPlatformWidget because that makes
534 // windows of TYPE_CONTROL which are automatically made visible. But still
535 // mark it as a child to test window positioning.
536 Widget* child = new Widget;
537 Widget::InitParams init_params;
538 init_params.parent = anchor_view;
539 init_params.child = true;
540 child->Init(init_params);
542 TestWidgetObserver child_observer(child);
544 // GetTopLevelNativeWidget() only goes as far as there exists a Widget (i.e.
545 // must stop at |child|.
546 internal::NativeWidgetPrivate* top_level_widget =
547 internal::NativeWidgetPrivate::GetTopLevelNativeWidget(
548 child->GetNativeView());
549 EXPECT_EQ(child, top_level_widget->GetWidget());
551 // To verify the parent, we need to use NativeWidgetMac APIs.
552 BridgedNativeWidget* bridged_native_widget =
553 NativeWidgetMac::GetBridgeForNativeWindow(child->GetNativeWindow());
554 EXPECT_EQ(native_parent, bridged_native_widget->parent()->GetNSWindow());
556 child->SetBounds(gfx::Rect(50, 50, 200, 100));
557 EXPECT_FALSE(child->IsVisible());
558 EXPECT_EQ(0u, [[native_parent childWindows] count]);
561 EXPECT_TRUE(child->IsVisible());
562 EXPECT_EQ(1u, [[native_parent childWindows] count]);
563 EXPECT_EQ(child->GetNativeWindow(),
564 [[native_parent childWindows] objectAtIndex:0]);
565 EXPECT_EQ(native_parent, [child->GetNativeWindow() parentWindow]);
567 // Child should be positioned on screen relative to the parent, but note we
568 // positioned the parent in Cocoa coordinates, so we need to convert.
569 gfx::Point parent_origin = gfx::ScreenRectFromNSRect(ParentRect()).origin();
570 EXPECT_EQ(gfx::Rect(150, parent_origin.y() + 50, 200, 100),
571 child->GetWindowBoundsInScreen());
573 // Removing the anchor_view from its view hierarchy is permitted. This should
574 // not break the relationship between the two windows.
575 [anchor_view removeFromSuperview];
577 EXPECT_EQ(native_parent, bridged_native_widget->parent()->GetNSWindow());
579 // Closing the parent should close and destroy the child.
580 EXPECT_FALSE(child_observer.widget_closed());
581 [native_parent close];
582 EXPECT_TRUE(child_observer.widget_closed());
584 EXPECT_EQ(0u, [[native_parent childWindows] count]);
587 // Use Native APIs to query the tooltip text that would be shown once the
588 // tooltip delay had elapsed.
589 base::string16 TooltipTextForWidget(Widget* widget) {
590 // For Mac, the actual location doesn't matter, since there is only one native
591 // view and it fills the window. This just assumes the window is at least big
592 // big enough for a constant coordinate to be within it.
593 NSPoint point = NSMakePoint(30, 30);
594 NSView* view = [widget->GetNativeView() hitTest:point];
596 [view view:view stringForToolTip:0 point:point userData:nullptr];
597 return base::SysNSStringToUTF16(text);
600 // Tests tooltips. The test doesn't wait for tooltips to appear. That is, the
601 // test assumes Cocoa calls stringForToolTip: at appropriate times and that,
602 // when a tooltip is already visible, changing it causes an update. These were
603 // tested manually by inserting a base::RunLoop.Run().
604 TEST_F(NativeWidgetMacTest, Tooltips) {
605 Widget* widget = CreateTopLevelPlatformWidget();
606 gfx::Rect screen_rect(50, 50, 100, 100);
607 widget->SetBounds(screen_rect);
609 const base::string16 tooltip_back = base::ASCIIToUTF16("Back");
610 const base::string16 tooltip_front = base::ASCIIToUTF16("Front");
611 const base::string16 long_tooltip(2000, 'W');
613 // Create a nested layout to test corner cases.
614 LabelButton* back = new LabelButton(nullptr, base::string16());
615 back->SetBounds(10, 10, 80, 80);
616 widget->GetContentsView()->AddChildView(back);
619 ui::test::EventGenerator event_generator(GetContext(),
620 widget->GetNativeWindow());
622 // Initially, there should be no tooltip.
623 event_generator.MoveMouseTo(gfx::Point(50, 50));
624 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
626 // Create a new button for the "front", and set the tooltip, but don't add it
627 // to the view hierarchy yet.
628 LabelButton* front = new LabelButton(nullptr, base::string16());
629 front->SetBounds(20, 20, 40, 40);
630 front->SetTooltipText(tooltip_front);
632 // Changing the tooltip text shouldn't require an additional mousemove to take
634 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
635 back->SetTooltipText(tooltip_back);
636 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
638 // Adding a new view under the mouse should also take immediate effect.
639 back->AddChildView(front);
640 EXPECT_EQ(tooltip_front, TooltipTextForWidget(widget));
642 // A long tooltip will be wrapped by Cocoa, but the full string should appear.
643 // Note that render widget hosts clip at 1024 to prevent DOS, but in toolkit-
644 // views the UI is more trusted.
645 front->SetTooltipText(long_tooltip);
646 EXPECT_EQ(long_tooltip, TooltipTextForWidget(widget));
648 // Move the mouse to a different view - tooltip should change.
649 event_generator.MoveMouseTo(gfx::Point(15, 15));
650 EXPECT_EQ(tooltip_back, TooltipTextForWidget(widget));
652 // Move the mouse off of any view, tooltip should clear.
653 event_generator.MoveMouseTo(gfx::Point(5, 5));
654 EXPECT_TRUE(TooltipTextForWidget(widget).empty());
661 // Delegate to make Widgets of MODAL_TYPE_CHILD.
662 class ChildModalDialogDelegate : public DialogDelegateView {
664 ChildModalDialogDelegate() {}
667 ui::ModalType GetModalType() const override { return ui::MODAL_TYPE_CHILD; }
670 DISALLOW_COPY_AND_ASSIGN(ChildModalDialogDelegate);
673 // While in scope, waits for a call to a swizzled objective C method, then quits
674 // a nested run loop.
675 class ScopedSwizzleWaiter {
677 explicit ScopedSwizzleWaiter(Class target)
679 [TestStopAnimationWaiter class],
680 @selector(setWindowStateForEnd)) {
685 ~ScopedSwizzleWaiter() { instance_ = nullptr; }
687 static IMP GetMethodAndMarkCalled() {
688 return instance_->GetMethodInternal();
691 void WaitForMethod() {
695 base::RunLoop run_loop;
696 base::MessageLoop::current()->task_runner()->PostDelayedTask(
697 FROM_HERE, run_loop.QuitClosure(), TestTimeouts::action_timeout());
698 run_loop_ = &run_loop;
703 bool method_called() const { return method_called_; }
706 IMP GetMethodInternal() {
707 DCHECK(!method_called_);
708 method_called_ = true;
711 return swizzler_.GetOriginalImplementation();
714 static ScopedSwizzleWaiter* instance_;
716 base::mac::ScopedObjCClassSwizzler swizzler_;
717 base::RunLoop* run_loop_ = nullptr;
718 bool method_called_ = false;
720 DISALLOW_COPY_AND_ASSIGN(ScopedSwizzleWaiter);
723 ScopedSwizzleWaiter* ScopedSwizzleWaiter::instance_ = nullptr;
725 // Shows a modal widget and waits for the show animation to complete. Waiting is
726 // not compulsory (calling Close() while animating the show will cancel the show
727 // animation). However, testing with overlapping swizzlers is tricky.
728 Widget* ShowChildModalWidgetAndWait(NSWindow* native_parent) {
729 Widget* modal_dialog_widget = views::DialogDelegate::CreateDialogWidget(
730 new ChildModalDialogDelegate, nullptr, [native_parent contentView]);
732 modal_dialog_widget->SetBounds(gfx::Rect(50, 50, 200, 150));
733 EXPECT_FALSE(modal_dialog_widget->IsVisible());
734 ScopedSwizzleWaiter show_waiter([ConstrainedWindowAnimationShow class]);
736 modal_dialog_widget->Show();
737 // Visible immediately (although it animates from transparent).
738 EXPECT_TRUE(modal_dialog_widget->IsVisible());
740 // Run the animation.
741 show_waiter.WaitForMethod();
742 EXPECT_TRUE(modal_dialog_widget->IsVisible());
743 EXPECT_TRUE(show_waiter.method_called());
744 return modal_dialog_widget;
749 // Tests object lifetime for the show/hide animations used for child-modal
750 // windows. Parents the dialog off a native parent window (not a views::Widget).
751 TEST_F(NativeWidgetMacTest, NativeWindowChildModalShowHide) {
752 NSWindow* native_parent = MakeNativeParent();
754 Widget* modal_dialog_widget = ShowChildModalWidgetAndWait(native_parent);
755 TestWidgetObserver widget_observer(modal_dialog_widget);
757 ScopedSwizzleWaiter hide_waiter([ConstrainedWindowAnimationHide class]);
758 EXPECT_TRUE(modal_dialog_widget->IsVisible());
759 EXPECT_FALSE(widget_observer.widget_closed());
761 // Widget::Close() is always asynchronous, so we can check that the widget
762 // is initially visible, but then it's destroyed.
763 modal_dialog_widget->Close();
764 EXPECT_TRUE(modal_dialog_widget->IsVisible());
765 EXPECT_FALSE(hide_waiter.method_called());
766 EXPECT_FALSE(widget_observer.widget_closed());
768 // Wait for a hide to finish.
769 hide_waiter.WaitForMethod();
770 EXPECT_TRUE(hide_waiter.method_called());
772 // The animation finishing should also mean it has closed the window.
773 EXPECT_TRUE(widget_observer.widget_closed());
777 // Make a new dialog to test another lifetime flow.
778 Widget* modal_dialog_widget = ShowChildModalWidgetAndWait(native_parent);
779 TestWidgetObserver widget_observer(modal_dialog_widget);
781 // Start an asynchronous close as above.
782 ScopedSwizzleWaiter hide_waiter([ConstrainedWindowAnimationHide class]);
783 modal_dialog_widget->Close();
784 EXPECT_FALSE(widget_observer.widget_closed());
785 EXPECT_FALSE(hide_waiter.method_called());
787 // Now close the _parent_ window to force a synchronous close of the child.
788 [native_parent close];
790 // Widget is destroyed immediately. No longer paints, but the animation is
792 EXPECT_TRUE(widget_observer.widget_closed());
793 EXPECT_FALSE(hide_waiter.method_called());
795 // Wait for the hide again. It will call close on its retained copy of the
796 // child NSWindow, but that's fine since all the C++ objects are detached.
797 hide_waiter.WaitForMethod();
798 EXPECT_TRUE(hide_waiter.method_called());
802 // Test calls to Widget::ReparentNativeView() that result in a no-op on Mac.
803 // Tests with both native and non-native parents.
804 TEST_F(NativeWidgetMacTest, NoopReparentNativeView) {
805 NSWindow* parent = MakeNativeParent();
806 Widget* dialog = views::DialogDelegate::CreateDialogWidget(
807 new DialogDelegateView, nullptr, [parent contentView]);
808 BridgedNativeWidget* bridge =
809 NativeWidgetMac::GetBridgeForNativeWindow(dialog->GetNativeWindow());
811 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
812 Widget::ReparentNativeView(dialog->GetNativeView(), [parent contentView]);
813 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
817 Widget* parent_widget = CreateNativeDesktopWidget();
818 parent = parent_widget->GetNativeWindow();
819 dialog = views::DialogDelegate::CreateDialogWidget(
820 new DialogDelegateView, nullptr, [parent contentView]);
821 bridge = NativeWidgetMac::GetBridgeForNativeWindow(dialog->GetNativeWindow());
823 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
824 Widget::ReparentNativeView(dialog->GetNativeView(), [parent contentView]);
825 EXPECT_EQ(bridge->parent()->GetNSWindow(), parent);
827 parent_widget->CloseNow();
830 // Tests Cocoa properties that should be given to particular widget types.
831 TEST_F(NativeWidgetMacTest, NativeProperties) {
832 // Create a regular widget (TYPE_WINDOW).
833 Widget* regular_widget = CreateNativeDesktopWidget();
834 EXPECT_TRUE([regular_widget->GetNativeWindow() canBecomeKeyWindow]);
835 EXPECT_TRUE([regular_widget->GetNativeWindow() canBecomeMainWindow]);
837 // Disabling activation should prevent key and main status.
838 regular_widget->widget_delegate()->set_can_activate(false);
839 EXPECT_FALSE([regular_widget->GetNativeWindow() canBecomeKeyWindow]);
840 EXPECT_FALSE([regular_widget->GetNativeWindow() canBecomeMainWindow]);
842 // Create a dialog widget (also TYPE_WINDOW), but with a DialogDelegate.
843 Widget* dialog_widget = views::DialogDelegate::CreateDialogWidget(
844 new ChildModalDialogDelegate, nullptr, regular_widget->GetNativeView());
845 EXPECT_TRUE([dialog_widget->GetNativeWindow() canBecomeKeyWindow]);
846 // Dialogs shouldn't take main status away from their parent.
847 EXPECT_FALSE([dialog_widget->GetNativeWindow() canBecomeMainWindow]);
849 regular_widget->CloseNow();
852 NSData* WindowContentsAsTIFF(NSWindow* window) {
853 NSView* frame_view = [[window contentView] superview];
854 EXPECT_TRUE(frame_view);
856 // Inset to mask off left and right edges which vary in HighDPI.
857 NSRect bounds = NSInsetRect([frame_view bounds], 4, 0);
859 // On 10.6, the grippy changes appearance slightly when painted the second
860 // time in a textured window. Since this test cares about the window title,
861 // cut off the bottom of the window.
862 bounds.size.height -= 40;
863 bounds.origin.y += 40;
865 NSBitmapImageRep* bitmap =
866 [frame_view bitmapImageRepForCachingDisplayInRect:bounds];
869 [frame_view cacheDisplayInRect:bounds toBitmapImageRep:bitmap];
870 NSData* tiff = [bitmap TIFFRepresentation];
875 class CustomTitleWidgetDelegate : public WidgetDelegate {
877 CustomTitleWidgetDelegate(Widget* widget)
878 : widget_(widget), should_show_title_(true) {}
880 void set_title(const base::string16& title) { title_ = title; }
881 void set_should_show_title(bool show) { should_show_title_ = show; }
884 base::string16 GetWindowTitle() const override { return title_; }
885 bool ShouldShowWindowTitle() const override { return should_show_title_; }
886 Widget* GetWidget() override { return widget_; };
887 const Widget* GetWidget() const override { return widget_; };
891 base::string16 title_;
892 bool should_show_title_;
894 DISALLOW_COPY_AND_ASSIGN(CustomTitleWidgetDelegate);
897 // Test that undocumented title-hiding API we're using does the job.
898 TEST_F(NativeWidgetMacTest, DoesHideTitle) {
899 // Same as CreateTopLevelPlatformWidget but with a custom delegate.
900 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
901 Widget* widget = new Widget;
902 params.native_widget = new NativeWidgetCapture(widget);
903 CustomTitleWidgetDelegate delegate(widget);
904 params.delegate = &delegate;
905 params.bounds = gfx::Rect(0, 0, 800, 600);
906 widget->Init(params);
909 NSWindow* ns_window = widget->GetNativeWindow();
910 // Disable color correction so we can read unmodified values from the bitmap.
911 [ns_window setColorSpace:[NSColorSpace sRGBColorSpace]];
913 EXPECT_EQ(base::string16(), delegate.GetWindowTitle());
914 EXPECT_NSEQ(@"", [ns_window title]);
915 NSData* empty_title_data = WindowContentsAsTIFF(ns_window);
917 delegate.set_title(base::ASCIIToUTF16("This is a title"));
918 widget->UpdateWindowTitle();
919 NSData* this_title_data = WindowContentsAsTIFF(ns_window);
921 // The default window with a title should look different from the
922 // window with an empty title.
923 EXPECT_FALSE([empty_title_data isEqualToData:this_title_data]);
925 delegate.set_should_show_title(false);
926 delegate.set_title(base::ASCIIToUTF16("This is another title"));
927 widget->UpdateWindowTitle();
928 NSData* hidden_title_data = WindowContentsAsTIFF(ns_window);
930 // With our magic setting, the window with a title should look the
931 // same as the window with an empty title.
932 EXPECT_TRUE([ns_window _isTitleHidden]);
933 EXPECT_TRUE([empty_title_data isEqualToData:hidden_title_data]);
938 // Test calls to invalidate the shadow when composited frames arrive.
939 TEST_F(NativeWidgetMacTest, InvalidateShadow) {
940 NativeWidgetMacTestWindow* window;
941 const gfx::Rect rect(0, 0, 100, 200);
942 Widget::InitParams init_params =
943 CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS);
944 init_params.bounds = rect;
945 Widget* widget = CreateWidgetWithTestWindow(init_params, &window);
947 // Simulate the initial paint.
948 BridgedNativeWidgetTestApi(window).SimulateFrameSwap(rect.size());
950 // Default is an opaque window, so shadow doesn't need to be invalidated.
951 EXPECT_EQ(0, [window invalidateShadowCount]);
954 init_params.opacity = Widget::InitParams::TRANSLUCENT_WINDOW;
955 widget = CreateWidgetWithTestWindow(init_params, &window);
956 BridgedNativeWidgetTestApi test_api(window);
958 // First paint on a translucent window needs to invalidate the shadow. Once.
959 EXPECT_EQ(0, [window invalidateShadowCount]);
960 test_api.SimulateFrameSwap(rect.size());
961 EXPECT_EQ(1, [window invalidateShadowCount]);
962 test_api.SimulateFrameSwap(rect.size());
963 EXPECT_EQ(1, [window invalidateShadowCount]);
965 // Resizing the window also needs to trigger a shadow invalidation.
966 [window setContentSize:NSMakeSize(123, 456)];
967 // A "late" frame swap at the old size should do nothing.
968 test_api.SimulateFrameSwap(rect.size());
969 EXPECT_EQ(1, [window invalidateShadowCount]);
971 test_api.SimulateFrameSwap(gfx::Size(123, 456));
972 EXPECT_EQ(2, [window invalidateShadowCount]);
973 test_api.SimulateFrameSwap(gfx::Size(123, 456));
974 EXPECT_EQ(2, [window invalidateShadowCount]);
979 // Test the expected result of GetWorkAreaBoundsInScreen().
980 TEST_F(NativeWidgetMacTest, GetWorkAreaBoundsInScreen) {
982 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_POPUP);
983 params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
985 // This is relative to the top-left of the primary screen, so unless the bot's
986 // display is smaller than 400x300, the window will be wholly contained there.
987 params.bounds = gfx::Rect(100, 100, 300, 200);
990 NSRect expected = [[[NSScreen screens] objectAtIndex:0] visibleFrame];
991 NSRect actual = gfx::ScreenRectToNSRect(widget.GetWorkAreaBoundsInScreen());
992 EXPECT_FALSE(NSIsEmptyRect(actual));
993 EXPECT_NSEQ(expected, actual);
995 [widget.GetNativeWindow() close];
996 actual = gfx::ScreenRectToNSRect(widget.GetWorkAreaBoundsInScreen());
997 EXPECT_TRUE(NSIsEmptyRect(actual));
1001 } // namespace views
1003 @implementation TestStopAnimationWaiter
1004 - (void)setWindowStateForEnd {
1005 views::test::ScopedSwizzleWaiter::GetMethodAndMarkCalled()(self, _cmd);
1009 @implementation NativeWidgetMacTestWindow
1011 @synthesize invalidateShadowCount = invalidateShadowCount_;
1013 - (void)invalidateShadow {
1014 ++invalidateShadowCount_;
1015 [super invalidateShadow];