blob: ccc1e4c48c56de2886e7939469c70ea04e32ed59 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/views/widget/native_widget_mac.h"
#import <Cocoa/Cocoa.h>
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#import "testing/gtest_mac.h"
#import "ui/events/test/cocoa_test_event_utils.h"
#include "ui/events/test/event_generator.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#include "ui/views/controls/label.h"
#include "ui/views/native_cursor.h"
#include "ui/views/test/test_widget_observer.h"
#include "ui/views/test/widget_test.h"
namespace views {
namespace test {
// Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which
// need access to Cocoa APIs.
typedef WidgetTest NativeWidgetMacTest;
class WidgetChangeObserver : public TestWidgetObserver {
public:
WidgetChangeObserver(Widget* widget)
: TestWidgetObserver(widget),
gained_visible_count_(0),
lost_visible_count_(0) {}
int gained_visible_count() const { return gained_visible_count_; }
int lost_visible_count() const { return lost_visible_count_; }
private:
// WidgetObserver:
void OnWidgetVisibilityChanged(Widget* widget,
bool visible) override {
++(visible ? gained_visible_count_ : lost_visible_count_);
}
int gained_visible_count_;
int lost_visible_count_;
DISALLOW_COPY_AND_ASSIGN(WidgetChangeObserver);
};
// Test visibility states triggered externally.
TEST_F(NativeWidgetMacTest, HideAndShowExternally) {
Widget* widget = CreateTopLevelPlatformWidget();
NSWindow* ns_window = widget->GetNativeWindow();
WidgetChangeObserver observer(widget);
// Should initially be hidden.
EXPECT_FALSE(widget->IsVisible());
EXPECT_FALSE([ns_window isVisible]);
EXPECT_EQ(0, observer.gained_visible_count());
EXPECT_EQ(0, observer.lost_visible_count());
widget->Show();
EXPECT_TRUE(widget->IsVisible());
EXPECT_TRUE([ns_window isVisible]);
EXPECT_EQ(1, observer.gained_visible_count());
EXPECT_EQ(0, observer.lost_visible_count());
widget->Hide();
EXPECT_FALSE(widget->IsVisible());
EXPECT_FALSE([ns_window isVisible]);
EXPECT_EQ(1, observer.gained_visible_count());
EXPECT_EQ(1, observer.lost_visible_count());
widget->Show();
EXPECT_TRUE(widget->IsVisible());
EXPECT_TRUE([ns_window isVisible]);
EXPECT_EQ(2, observer.gained_visible_count());
EXPECT_EQ(1, observer.lost_visible_count());
// Test when hiding individual windows.
[ns_window orderOut:nil];
EXPECT_FALSE(widget->IsVisible());
EXPECT_FALSE([ns_window isVisible]);
EXPECT_EQ(2, observer.gained_visible_count());
EXPECT_EQ(2, observer.lost_visible_count());
[ns_window orderFront:nil];
EXPECT_TRUE(widget->IsVisible());
EXPECT_TRUE([ns_window isVisible]);
EXPECT_EQ(3, observer.gained_visible_count());
EXPECT_EQ(2, observer.lost_visible_count());
// Test when hiding the entire application. This doesn't send an orderOut:
// to the NSWindow.
[NSApp hide:nil];
// When the activation policy is NSApplicationActivationPolicyRegular, the
// calls via NSApp are asynchronous, and the run loop needs to be flushed.
// With NSApplicationActivationPolicyProhibited, the following RunUntilIdle
// calls are superfluous, but don't hurt.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(widget->IsVisible());
EXPECT_FALSE([ns_window isVisible]);
EXPECT_EQ(3, observer.gained_visible_count());
EXPECT_EQ(3, observer.lost_visible_count());
[NSApp unhideWithoutActivation];
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(widget->IsVisible());
EXPECT_TRUE([ns_window isVisible]);
EXPECT_EQ(4, observer.gained_visible_count());
EXPECT_EQ(3, observer.lost_visible_count());
// Hide again to test unhiding with an activation.
[NSApp hide:nil];
base::RunLoop().RunUntilIdle();
EXPECT_EQ(4, observer.lost_visible_count());
[NSApp unhide:nil];
base::RunLoop().RunUntilIdle();
EXPECT_EQ(5, observer.gained_visible_count());
// Hide again to test makeKeyAndOrderFront:.
[ns_window orderOut:nil];
EXPECT_FALSE(widget->IsVisible());
EXPECT_FALSE([ns_window isVisible]);
EXPECT_EQ(5, observer.gained_visible_count());
EXPECT_EQ(5, observer.lost_visible_count());
[ns_window makeKeyAndOrderFront:nil];
EXPECT_TRUE(widget->IsVisible());
EXPECT_TRUE([ns_window isVisible]);
EXPECT_EQ(6, observer.gained_visible_count());
EXPECT_EQ(5, observer.lost_visible_count());
// No change when closing.
widget->CloseNow();
EXPECT_EQ(5, observer.lost_visible_count());
EXPECT_EQ(6, observer.gained_visible_count());
}
// A view that counts calls to OnPaint().
class PaintCountView : public View {
public:
PaintCountView() : paint_count_(0) {
SetBounds(0, 0, 100, 100);
}
// View:
void OnPaint(gfx::Canvas* canvas) override {
EXPECT_TRUE(GetWidget()->IsVisible());
++paint_count_;
}
int paint_count() { return paint_count_; }
private:
int paint_count_;
DISALLOW_COPY_AND_ASSIGN(PaintCountView);
};
// Test minimized states triggered externally, implied visibility and restored
// bounds whilst minimized.
TEST_F(NativeWidgetMacTest, MiniaturizeExternally) {
Widget* widget = new Widget;
Widget::InitParams init_params(Widget::InitParams::TYPE_WINDOW);
// Make the layer not drawn, so that calls to paint can be observed
// synchronously.
init_params.layer_type = ui::LAYER_NOT_DRAWN;
widget->Init(init_params);
PaintCountView* view = new PaintCountView();
widget->GetContentsView()->AddChildView(view);
NSWindow* ns_window = widget->GetNativeWindow();
WidgetChangeObserver observer(widget);
widget->SetBounds(gfx::Rect(100, 100, 300, 300));
EXPECT_TRUE(view->IsDrawn());
EXPECT_EQ(0, view->paint_count());
widget->Show();
EXPECT_EQ(1, observer.gained_visible_count());
EXPECT_EQ(0, observer.lost_visible_count());
const gfx::Rect restored_bounds = widget->GetRestoredBounds();
EXPECT_FALSE(restored_bounds.IsEmpty());
EXPECT_FALSE(widget->IsMinimized());
EXPECT_TRUE(widget->IsVisible());
// Showing should paint.
EXPECT_EQ(1, view->paint_count());
// First try performMiniaturize:, which requires a minimize button. Note that
// Cocoa just blocks the UI thread during the animation, so no need to do
// anything fancy to wait for it finish.
[ns_window performMiniaturize:nil];
EXPECT_TRUE(widget->IsMinimized());
EXPECT_FALSE(widget->IsVisible()); // Minimizing also makes things invisible.
EXPECT_EQ(1, observer.gained_visible_count());
EXPECT_EQ(1, observer.lost_visible_count());
EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
// No repaint when minimizing. But note that this is partly due to not calling
// [NSView setNeedsDisplay:YES] on the content view. The superview, which is
// an NSThemeFrame, would repaint |view| if we had, because the miniaturize
// button is highlighted for performMiniaturize.
EXPECT_EQ(1, view->paint_count());
[ns_window deminiaturize:nil];
EXPECT_FALSE(widget->IsMinimized());
EXPECT_TRUE(widget->IsVisible());
EXPECT_EQ(2, observer.gained_visible_count());
EXPECT_EQ(1, observer.lost_visible_count());
EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
EXPECT_EQ(2, view->paint_count()); // A single paint when deminiaturizing.
EXPECT_FALSE([ns_window isMiniaturized]);
widget->Minimize();
EXPECT_TRUE(widget->IsMinimized());
EXPECT_TRUE([ns_window isMiniaturized]);
EXPECT_EQ(2, observer.gained_visible_count());
EXPECT_EQ(2, observer.lost_visible_count());
EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
EXPECT_EQ(2, view->paint_count()); // No paint when miniaturizing.
widget->Restore(); // If miniaturized, should deminiaturize.
EXPECT_FALSE(widget->IsMinimized());
EXPECT_FALSE([ns_window isMiniaturized]);
EXPECT_EQ(3, observer.gained_visible_count());
EXPECT_EQ(2, observer.lost_visible_count());
EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
EXPECT_EQ(3, view->paint_count());
widget->Restore(); // If not miniaturized, does nothing.
EXPECT_FALSE(widget->IsMinimized());
EXPECT_FALSE([ns_window isMiniaturized]);
EXPECT_EQ(3, observer.gained_visible_count());
EXPECT_EQ(2, observer.lost_visible_count());
EXPECT_EQ(restored_bounds, widget->GetRestoredBounds());
EXPECT_EQ(3, view->paint_count());
widget->CloseNow();
// Create a widget without a minimize button.
widget = CreateTopLevelFramelessPlatformWidget();
ns_window = widget->GetNativeWindow();
widget->SetBounds(gfx::Rect(100, 100, 300, 300));
widget->Show();
EXPECT_FALSE(widget->IsMinimized());
// This should fail, since performMiniaturize: requires a minimize button.
[ns_window performMiniaturize:nil];
EXPECT_FALSE(widget->IsMinimized());
// But this should work.
widget->Minimize();
EXPECT_TRUE(widget->IsMinimized());
// Test closing while minimized.
widget->CloseNow();
}
// Simple view for the SetCursor test that overrides View::GetCursor().
class CursorView : public View {
public:
CursorView(int x, NSCursor* cursor) : cursor_(cursor) {
SetBounds(x, 0, 100, 300);
}
// View:
gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override {
return cursor_;
}
private:
NSCursor* cursor_;
DISALLOW_COPY_AND_ASSIGN(CursorView);
};
// Test for Widget::SetCursor(). There is no Widget::GetCursor(), so this uses
// -[NSCursor currentCursor] to validate expectations. Note that currentCursor
// is just "the top cursor on the application's cursor stack.", which is why it
// is safe to use this in a non-interactive UI test with the EventGenerator.
TEST_F(NativeWidgetMacTest, SetCursor) {
NSCursor* arrow = [NSCursor arrowCursor];
NSCursor* hand = GetNativeHandCursor();
NSCursor* ibeam = GetNativeIBeamCursor();
Widget* widget = CreateTopLevelPlatformWidget();
widget->SetBounds(gfx::Rect(0, 0, 300, 300));
widget->GetContentsView()->AddChildView(new CursorView(0, hand));
widget->GetContentsView()->AddChildView(new CursorView(100, ibeam));
widget->Show();
// Events used to simulate tracking rectangle updates. These are not passed to
// toolkit-views, so it only matters whether they are inside or outside the
// content area.
NSEvent* event_in_content = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(100, 100), NSMouseMoved, 0);
NSEvent* event_out_of_content = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(-50, -50), NSMouseMoved, 0);
EXPECT_NE(arrow, hand);
EXPECT_NE(arrow, ibeam);
// At the start of the test, the cursor stack should be empty.
EXPECT_FALSE([NSCursor currentCursor]);
// Use an event generator to ask views code to set the cursor. However, note
// that this does not cause Cocoa to generate tracking rectangle updates.
ui::test::EventGenerator event_generator(GetContext(),
widget->GetNativeWindow());
// Move the mouse over the first view, then simulate a tracking rectangle
// update.
event_generator.MoveMouseTo(gfx::Point(50, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(hand, [NSCursor currentCursor]);
// A tracking rectangle update not in the content area should forward to
// the native NSWindow implementation, which sets the arrow cursor.
[widget->GetNativeWindow() cursorUpdate:event_out_of_content];
EXPECT_EQ(arrow, [NSCursor currentCursor]);
// Now move to the second view.
event_generator.MoveMouseTo(gfx::Point(150, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(ibeam, [NSCursor currentCursor]);
// Moving to the third view (but remaining in the content area) should also
// forward to the native NSWindow implementation.
event_generator.MoveMouseTo(gfx::Point(250, 50));
[widget->GetNativeWindow() cursorUpdate:event_in_content];
EXPECT_EQ(arrow, [NSCursor currentCursor]);
widget->CloseNow();
}
// Tests that an accessibility request from the system makes its way through to
// a views::Label filling the window.
TEST_F(NativeWidgetMacTest, AccessibilityIntegration) {
Widget* widget = CreateTopLevelPlatformWidget();
gfx::Rect screen_rect(50, 50, 100, 100);
widget->SetBounds(screen_rect);
const base::string16 test_string = base::ASCIIToUTF16("Green");
views::Label* label = new views::Label(test_string);
label->SetBounds(0, 0, 100, 100);
widget->GetContentsView()->AddChildView(label);
widget->Show();
// Accessibility hit tests come in Cocoa screen coordinates.
NSRect nsrect = gfx::ScreenRectToNSRect(screen_rect);
NSPoint midpoint = NSMakePoint(NSMidX(nsrect), NSMidY(nsrect));
id hit = [widget->GetNativeWindow() accessibilityHitTest:midpoint];
id title = [hit accessibilityAttributeValue:NSAccessibilityTitleAttribute];
EXPECT_NSEQ(title, @"Green");
}
} // namespace test
} // namespace views