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 #include "base/run_loop.h"
10 #include "base/strings/utf_string_conversions.h"
11 #import "testing/gtest_mac.h"
12 #import "ui/events/test/cocoa_test_event_utils.h"
13 #include "ui/events/test/event_generator.h"
14 #import "ui/gfx/mac/coordinate_conversion.h"
15 #include "ui/views/controls/label.h"
16 #include "ui/views/native_cursor.h"
17 #include "ui/views/test/test_widget_observer.h"
18 #include "ui/views/test/widget_test.h"
23 // Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which
24 // need access to Cocoa APIs.
25 typedef WidgetTest NativeWidgetMacTest;
27 class WidgetChangeObserver : public TestWidgetObserver {
29 WidgetChangeObserver(Widget* widget)
30 : TestWidgetObserver(widget),
31 gained_visible_count_(0),
32 lost_visible_count_(0) {}
34 int gained_visible_count() const { return gained_visible_count_; }
35 int lost_visible_count() const { return lost_visible_count_; }
39 void OnWidgetVisibilityChanged(Widget* widget,
40 bool visible) override {
41 ++(visible ? gained_visible_count_ : lost_visible_count_);
44 int gained_visible_count_;
45 int lost_visible_count_;
47 DISALLOW_COPY_AND_ASSIGN(WidgetChangeObserver);
50 // Test visibility states triggered externally.
51 TEST_F(NativeWidgetMacTest, HideAndShowExternally) {
52 Widget* widget = CreateTopLevelPlatformWidget();
53 NSWindow* ns_window = widget->GetNativeWindow();
54 WidgetChangeObserver observer(widget);
56 // Should initially be hidden.
57 EXPECT_FALSE(widget->IsVisible());
58 EXPECT_FALSE([ns_window isVisible]);
59 EXPECT_EQ(0, observer.gained_visible_count());
60 EXPECT_EQ(0, observer.lost_visible_count());
63 EXPECT_TRUE(widget->IsVisible());
64 EXPECT_TRUE([ns_window isVisible]);
65 EXPECT_EQ(1, observer.gained_visible_count());
66 EXPECT_EQ(0, observer.lost_visible_count());
69 EXPECT_FALSE(widget->IsVisible());
70 EXPECT_FALSE([ns_window isVisible]);
71 EXPECT_EQ(1, observer.gained_visible_count());
72 EXPECT_EQ(1, observer.lost_visible_count());
75 EXPECT_TRUE(widget->IsVisible());
76 EXPECT_TRUE([ns_window isVisible]);
77 EXPECT_EQ(2, observer.gained_visible_count());
78 EXPECT_EQ(1, observer.lost_visible_count());
80 // Test when hiding individual windows.
81 [ns_window orderOut:nil];
82 EXPECT_FALSE(widget->IsVisible());
83 EXPECT_FALSE([ns_window isVisible]);
84 EXPECT_EQ(2, observer.gained_visible_count());
85 EXPECT_EQ(2, observer.lost_visible_count());
87 [ns_window orderFront:nil];
88 EXPECT_TRUE(widget->IsVisible());
89 EXPECT_TRUE([ns_window isVisible]);
90 EXPECT_EQ(3, observer.gained_visible_count());
91 EXPECT_EQ(2, observer.lost_visible_count());
93 // Test when hiding the entire application. This doesn't send an orderOut:
96 // When the activation policy is NSApplicationActivationPolicyRegular, the
97 // calls via NSApp are asynchronous, and the run loop needs to be flushed.
98 // With NSApplicationActivationPolicyProhibited, the following RunUntilIdle
99 // calls are superfluous, but don't hurt.
100 base::RunLoop().RunUntilIdle();
101 EXPECT_FALSE(widget->IsVisible());
102 EXPECT_FALSE([ns_window isVisible]);
103 EXPECT_EQ(3, observer.gained_visible_count());
104 EXPECT_EQ(3, observer.lost_visible_count());
106 [NSApp unhideWithoutActivation];
107 base::RunLoop().RunUntilIdle();
108 EXPECT_TRUE(widget->IsVisible());
109 EXPECT_TRUE([ns_window isVisible]);
110 EXPECT_EQ(4, observer.gained_visible_count());
111 EXPECT_EQ(3, observer.lost_visible_count());
113 // Hide again to test unhiding with an activation.
115 base::RunLoop().RunUntilIdle();
116 EXPECT_EQ(4, observer.lost_visible_count());
118 base::RunLoop().RunUntilIdle();
119 EXPECT_EQ(5, observer.gained_visible_count());
121 // Hide again to test makeKeyAndOrderFront:.
122 [ns_window orderOut:nil];
123 EXPECT_FALSE(widget->IsVisible());
124 EXPECT_FALSE([ns_window isVisible]);
125 EXPECT_EQ(5, observer.gained_visible_count());
126 EXPECT_EQ(5, observer.lost_visible_count());
128 [ns_window makeKeyAndOrderFront:nil];
129 EXPECT_TRUE(widget->IsVisible());
130 EXPECT_TRUE([ns_window isVisible]);
131 EXPECT_EQ(6, observer.gained_visible_count());
132 EXPECT_EQ(5, observer.lost_visible_count());
134 // No change when closing.
136 EXPECT_EQ(5, observer.lost_visible_count());
137 EXPECT_EQ(6, observer.gained_visible_count());
140 // A view that counts calls to OnPaint().
141 class PaintCountView : public View {
143 PaintCountView() : paint_count_(0) {
144 SetBounds(0, 0, 100, 100);
148 void OnPaint(gfx::Canvas* canvas) override {
149 EXPECT_TRUE(GetWidget()->IsVisible());
153 int paint_count() { return paint_count_; }
158 DISALLOW_COPY_AND_ASSIGN(PaintCountView);
161 // Test minimized states triggered externally, implied visibility and restored
162 // bounds whilst minimized.
163 TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
164 Widget* widget = new Widget;
165 Widget::InitParams init_params(Widget::InitParams::TYPE_WINDOW);
166 // Make the layer not drawn, so that calls to paint can be observed
168 init_params.layer_type = ui::LAYER_NOT_DRAWN;
169 widget->Init(init_params);
171 PaintCountView* view = new PaintCountView();
172 widget->GetContentsView()->AddChildView(view);
173 NSWindow* ns_window = widget->GetNativeWindow();
174 WidgetChangeObserver observer(widget);
176 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
178 EXPECT_TRUE(view->IsDrawn());
179 EXPECT_EQ(0, view->paint_count());
182 EXPECT_EQ(1, observer.gained_visible_count());
183 EXPECT_EQ(0, observer.lost_visible_count());
184 const gfx::Rect restored_bounds = widget->GetRestoredBounds();
185 EXPECT_FALSE(restored_bounds.IsEmpty());
186 EXPECT_FALSE(widget->IsMinimized());
187 EXPECT_TRUE(widget->IsVisible());
189 // Showing should paint.
190 EXPECT_EQ(1, view->paint_count());
192 // First try performMiniaturize:, which requires a minimize button. Note that
193 // Cocoa just blocks the UI thread during the animation, so no need to do
194 // anything fancy to wait for it finish.
195 [ns_window performMiniaturize:nil];
197 EXPECT_TRUE(widget->IsMinimized());
198 EXPECT_FALSE(widget->IsVisible()); // Minimizing also makes things invisible.
199 EXPECT_EQ(1, observer.gained_visible_count());
200 EXPECT_EQ(1, observer.lost_visible_count());
201 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
203 // No repaint when minimizing. But note that this is partly due to not calling
204 // [NSView setNeedsDisplay:YES] on the content view. The superview, which is
205 // an NSThemeFrame, would repaint |view| if we had, because the miniaturize
206 // button is highlighted for performMiniaturize.
207 EXPECT_EQ(1, view->paint_count());
209 [ns_window deminiaturize:nil];
211 EXPECT_FALSE(widget->IsMinimized());
212 EXPECT_TRUE(widget->IsVisible());
213 EXPECT_EQ(2, observer.gained_visible_count());
214 EXPECT_EQ(1, observer.lost_visible_count());
215 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
217 EXPECT_EQ(2, view->paint_count()); // A single paint when deminiaturizing.
218 EXPECT_FALSE([ns_window isMiniaturized]);
222 EXPECT_TRUE(widget->IsMinimized());
223 EXPECT_TRUE([ns_window isMiniaturized]);
224 EXPECT_EQ(2, observer.gained_visible_count());
225 EXPECT_EQ(2, observer.lost_visible_count());
226 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
227 EXPECT_EQ(2, view->paint_count()); // No paint when miniaturizing.
229 widget->Restore(); // If miniaturized, should deminiaturize.
231 EXPECT_FALSE(widget->IsMinimized());
232 EXPECT_FALSE([ns_window isMiniaturized]);
233 EXPECT_EQ(3, observer.gained_visible_count());
234 EXPECT_EQ(2, observer.lost_visible_count());
235 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
236 EXPECT_EQ(3, view->paint_count());
238 widget->Restore(); // If not miniaturized, does nothing.
240 EXPECT_FALSE(widget->IsMinimized());
241 EXPECT_FALSE([ns_window isMiniaturized]);
242 EXPECT_EQ(3, observer.gained_visible_count());
243 EXPECT_EQ(2, observer.lost_visible_count());
244 EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
245 EXPECT_EQ(3, view->paint_count());
249 // Create a widget without a minimize button.
250 widget = CreateTopLevelFramelessPlatformWidget();
251 ns_window = widget->GetNativeWindow();
252 widget->SetBounds(gfx::Rect(100, 100, 300, 300));
254 EXPECT_FALSE(widget->IsMinimized());
256 // This should fail, since performMiniaturize: requires a minimize button.
257 [ns_window performMiniaturize:nil];
258 EXPECT_FALSE(widget->IsMinimized());
260 // But this should work.
262 EXPECT_TRUE(widget->IsMinimized());
264 // Test closing while minimized.
268 // Simple view for the SetCursor test that overrides View::GetCursor().
269 class CursorView : public View {
271 CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
272 SetBounds(x, 0, 100, 300);
276 gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
283 DISALLOW_COPY_AND_ASSIGN(CursorView);
286 // Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
287 // -[NSCursor currentCursor] to validate expectations. Note that currentCursor
288 // is just "the top cursor on the application's cursor stack.", which is why it
289 // is safe to use this in a non-interactive UI test with the EventGenerator.
290 TEST_F(NativeWidgetMacTest, SetCursor) {
291 NSCursor* arrow = [NSCursor arrowCursor];
292 NSCursor* hand = GetNativeHandCursor();
293 NSCursor* ibeam = GetNativeIBeamCursor();
295 Widget* widget = CreateTopLevelPlatformWidget();
296 widget->SetBounds(gfx::Rect(0, 0, 300, 300));
297 widget->GetContentsView()->AddChildView(new CursorView(0, hand));
298 widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
301 // Events used to simulate tracking rectangle updates. These are not passed to
302 // toolkit-views, so it only matters whether they are inside or outside the
304 NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
305 NSMakePoint(100, 100), NSMouseMoved, 0);
306 NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
307 NSMakePoint(-50, -50), NSMouseMoved, 0);
309 EXPECT_NE(arrow, hand);
310 EXPECT_NE(arrow, ibeam);
312 // At the start of the test, the cursor stack should be empty.
313 EXPECT_FALSE([NSCursor currentCursor]);
315 // Use an event generator to ask views code to set the cursor. However, note
316 // that this does not cause Cocoa to generate tracking rectangle updates.
317 ui::test::EventGenerator event_generator(GetContext(),
318 widget->GetNativeWindow());
320 // Move the mouse over the first view, then simulate a tracking rectangle
322 event_generator.MoveMouseTo(gfx::Point(50, 50));
323 [widget->GetNativeWindow() cursorUpdate:event_in_content];
324 EXPECT_EQ(hand, [NSCursor currentCursor]);
326 // A tracking rectangle update not in the content area should forward to
327 // the native NSWindow implementation, which sets the arrow cursor.
328 [widget->GetNativeWindow() cursorUpdate:event_out_of_content];
329 EXPECT_EQ(arrow, [NSCursor currentCursor]);
331 // Now move to the second view.
332 event_generator.MoveMouseTo(gfx::Point(150, 50));
333 [widget->GetNativeWindow() cursorUpdate:event_in_content];
334 EXPECT_EQ(ibeam, [NSCursor currentCursor]);
336 // Moving to the third view (but remaining in the content area) should also
337 // forward to the native NSWindow implementation.
338 event_generator.MoveMouseTo(gfx::Point(250, 50));
339 [widget->GetNativeWindow() cursorUpdate:event_in_content];
340 EXPECT_EQ(arrow, [NSCursor currentCursor]);
345 // Tests that an accessibility request from the system makes its way through to
346 // a views::Label filling the window.
347 TEST_F(NativeWidgetMacTest, AccessibilityIntegration) {
348 Widget* widget = CreateTopLevelPlatformWidget();
349 gfx::Rect screen_rect(50, 50, 100, 100);
350 widget->SetBounds(screen_rect);
352 const base::string16 test_string = base::ASCIIToUTF16("Green");
353 views::Label* label = new views::Label(test_string);
354 label->SetBounds(0, 0, 100, 100);
355 widget->GetContentsView()->AddChildView(label);
358 // Accessibility hit tests come in Cocoa screen coordinates.
359 NSRect nsrect = gfx::ScreenRectToNSRect(screen_rect);
360 NSPoint midpoint = NSMakePoint(NSMidX(nsrect), NSMidY(nsrect));
362 id hit = [widget->GetNativeWindow() accessibilityHitTest:midpoint];
363 id title = [hit accessibilityAttributeValue:NSAccessibilityTitleAttribute];
364 EXPECT_NSEQ(title, @"Green");