// 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. #include "ui/views/widget/native_widget_mac.h" #import #import "base/mac/mac_util.h" #import "base/mac/scoped_nsobject.h" #include "base/macros.h" #include "ui/base/test/ui_controls.h" #import "ui/base/test/windowed_nsnotification_observer.h" #import "ui/events/test/cocoa_test_event_utils.h" #include "ui/views/bubble/bubble_dialog_delegate_view.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/test/test_widget_observer.h" #include "ui/views/test/widget_test.h" namespace views { namespace test { // Tests for NativeWidgetMac that rely on global window manager state, and can // not be parallelized. class NativeWidgetMacInteractiveUITest : public WidgetTest, public ::testing::WithParamInterface { public: class Observer; NativeWidgetMacInteractiveUITest() = default; // WidgetTest: void SetUp() override { SetUpForInteractiveTests(); WidgetTest::SetUp(); } Widget* MakeWidget() { return GetParam() ? CreateTopLevelFramelessPlatformWidget() : CreateTopLevelPlatformWidget(); } protected: std::unique_ptr observer_; int activation_count_ = 0; int deactivation_count_ = 0; private: DISALLOW_COPY_AND_ASSIGN(NativeWidgetMacInteractiveUITest); }; class NativeWidgetMacInteractiveUITest::Observer : public TestWidgetObserver { public: Observer(NativeWidgetMacInteractiveUITest* parent, Widget* widget) : TestWidgetObserver(widget), parent_(parent) {} void OnWidgetActivationChanged(Widget* widget, bool active) override { if (active) parent_->activation_count_++; else parent_->deactivation_count_++; } private: NativeWidgetMacInteractiveUITest* parent_; DISALLOW_COPY_AND_ASSIGN(Observer); }; // Test that showing a window causes it to attain global keyWindow status. TEST_P(NativeWidgetMacInteractiveUITest, ShowAttainsKeyStatus) { Widget* widget = MakeWidget(); observer_ = std::make_unique(this, widget); EXPECT_FALSE(widget->IsActive()); EXPECT_EQ(0, activation_count_); { WidgetActivationWaiter wait_for_first_active(widget, true); widget->Show(); wait_for_first_active.Wait(); } EXPECT_TRUE(widget->IsActive()); EXPECT_TRUE([widget->GetNativeWindow().GetNativeNSWindow() isKeyWindow]); EXPECT_EQ(1, activation_count_); EXPECT_EQ(0, deactivation_count_); // Now check that losing and gaining key status due events outside of Widget // works correctly. Widget* widget2 = MakeWidget(); // Note: not observed. EXPECT_EQ(0, deactivation_count_); { WidgetActivationWaiter wait_for_deactivate(widget, false); widget2->Show(); wait_for_deactivate.Wait(); } EXPECT_EQ(1, deactivation_count_); EXPECT_FALSE(widget->IsActive()); EXPECT_EQ(1, activation_count_); { WidgetActivationWaiter wait_for_external_activate(widget, true); [widget->GetNativeWindow().GetNativeNSWindow() makeKeyAndOrderFront:nil]; wait_for_external_activate.Wait(); } EXPECT_TRUE(widget->IsActive()); EXPECT_EQ(1, deactivation_count_); EXPECT_EQ(2, activation_count_); widget2->CloseNow(); widget->CloseNow(); EXPECT_EQ(1, deactivation_count_); EXPECT_EQ(2, activation_count_); } // Test that ShowInactive does not take keyWindow status. TEST_P(NativeWidgetMacInteractiveUITest, ShowInactiveIgnoresKeyStatus) { WidgetTest::WaitForSystemAppActivation(); Widget* widget = MakeWidget(); NSWindow* widget_window = widget->GetNativeWindow().GetNativeNSWindow(); base::scoped_nsobject waiter( [[WindowedNSNotificationObserver alloc] initForNotification:NSWindowDidBecomeKeyNotification object:widget_window]); EXPECT_FALSE(widget->IsVisible()); EXPECT_FALSE([widget_window isVisible]); EXPECT_FALSE(widget->IsActive()); EXPECT_FALSE([widget_window isKeyWindow]); widget->ShowInactive(); EXPECT_TRUE(widget->IsVisible()); EXPECT_TRUE([widget_window isVisible]); EXPECT_FALSE(widget->IsActive()); EXPECT_FALSE([widget_window isKeyWindow]); // If the window were to become active, this would activate it. RunPendingMessages(); EXPECT_FALSE(widget->IsActive()); EXPECT_FALSE([widget_window isKeyWindow]); EXPECT_EQ(0, [waiter notificationCount]); // Activating the inactive widget should make it key, asynchronously. widget->Activate(); [waiter wait]; EXPECT_EQ(1, [waiter notificationCount]); EXPECT_TRUE(widget->IsActive()); EXPECT_TRUE([widget_window isKeyWindow]); widget->CloseNow(); } namespace { // Show |widget| and wait for it to become the key window. void ShowKeyWindow(Widget* widget) { NSWindow* widget_window = widget->GetNativeWindow().GetNativeNSWindow(); base::scoped_nsobject waiter( [[WindowedNSNotificationObserver alloc] initForNotification:NSWindowDidBecomeKeyNotification object:widget_window]); widget->Show(); EXPECT_TRUE([waiter wait]); EXPECT_TRUE([widget_window isKeyWindow]); } NSData* ViewAsTIFF(NSView* view) { NSBitmapImageRep* bitmap = [view bitmapImageRepForCachingDisplayInRect:[view bounds]]; [view cacheDisplayInRect:[view bounds] toBitmapImageRep:bitmap]; return [bitmap TIFFRepresentation]; } class TestBubbleView : public BubbleDialogDelegateView { public: explicit TestBubbleView(Widget* parent) { SetAnchorView(parent->GetContentsView()); } private: DISALLOW_COPY_AND_ASSIGN(TestBubbleView); }; } // namespace // Test that parent windows keep their traffic lights enabled when showing // dialogs. TEST_F(NativeWidgetMacInteractiveUITest, ParentWindowTrafficLights) { Widget* parent_widget = CreateTopLevelPlatformWidget(); parent_widget->SetBounds(gfx::Rect(100, 100, 100, 100)); ShowKeyWindow(parent_widget); NSWindow* parent = parent_widget->GetNativeWindow().GetNativeNSWindow(); EXPECT_TRUE([parent isMainWindow]); NSButton* button = [parent standardWindowButton:NSWindowCloseButton]; EXPECT_TRUE(button); NSData* active_button_image = ViewAsTIFF(button); EXPECT_TRUE(active_button_image); EXPECT_TRUE(parent_widget->ShouldPaintAsActive()); // If a child widget is key, the parent should paint as active. Widget* child_widget = new Widget; Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.parent = parent_widget->GetNativeView(); child_widget->Init(std::move(params)); child_widget->SetContentsView(new View); child_widget->Show(); NSWindow* child = child_widget->GetNativeWindow().GetNativeNSWindow(); // Ensure the button instance is still valid. EXPECT_EQ(button, [parent standardWindowButton:NSWindowCloseButton]); EXPECT_TRUE(parent_widget->ShouldPaintAsActive()); // Parent window should still be main, and have its traffic lights active. EXPECT_TRUE([parent isMainWindow]); EXPECT_FALSE([parent isKeyWindow]); // Enabled status doesn't actually change, but check anyway. EXPECT_TRUE([button isEnabled]); NSData* button_image_with_child = ViewAsTIFF(button); EXPECT_TRUE([active_button_image isEqualToData:button_image_with_child]); // Verify that activating some other random window does change the button. // When the bubble loses activation, it will dismiss itself and update // Widget::ShouldPaintAsActive(). Widget* other_widget = CreateTopLevelPlatformWidget(); other_widget->SetBounds(gfx::Rect(200, 200, 100, 100)); ShowKeyWindow(other_widget); EXPECT_FALSE([parent isMainWindow]); EXPECT_FALSE([parent isKeyWindow]); EXPECT_FALSE(parent_widget->ShouldPaintAsActive()); EXPECT_TRUE([button isEnabled]); NSData* inactive_button_image = ViewAsTIFF(button); EXPECT_FALSE([active_button_image isEqualToData:inactive_button_image]); // Focus the child again and assert the parent once again paints as active. [child makeKeyWindow]; EXPECT_TRUE(parent_widget->ShouldPaintAsActive()); EXPECT_TRUE([child isKeyWindow]); EXPECT_FALSE([parent isKeyWindow]); child_widget->CloseNow(); other_widget->CloseNow(); parent_widget->CloseNow(); } // Test that bubble widgets are dismissed on right mouse down. TEST_F(NativeWidgetMacInteractiveUITest, BubbleDismiss) { Widget* parent_widget = CreateTopLevelPlatformWidget(); parent_widget->SetBounds(gfx::Rect(100, 100, 100, 100)); ShowKeyWindow(parent_widget); Widget* bubble_widget = BubbleDialogDelegateView::CreateBubble(new TestBubbleView(parent_widget)); ShowKeyWindow(bubble_widget); // First, test with LeftMouseDown in the parent window. NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( NSMakePoint(50, 50), parent_widget->GetNativeWindow().GetNativeNSWindow()); [NSApp sendEvent:mouse_down]; EXPECT_TRUE(bubble_widget->IsClosed()); bubble_widget = BubbleDialogDelegateView::CreateBubble(new TestBubbleView(parent_widget)); ShowKeyWindow(bubble_widget); // Test with RightMouseDown in the parent window. mouse_down = cocoa_test_event_utils::RightMouseDownAtPointInWindow( NSMakePoint(50, 50), parent_widget->GetNativeWindow().GetNativeNSWindow()); [NSApp sendEvent:mouse_down]; EXPECT_TRUE(bubble_widget->IsClosed()); bubble_widget = BubbleDialogDelegateView::CreateBubble(new TestBubbleView(parent_widget)); ShowKeyWindow(bubble_widget); // Test with RightMouseDown in the bubble (bubble should stay open). mouse_down = cocoa_test_event_utils::RightMouseDownAtPointInWindow( NSMakePoint(50, 50), bubble_widget->GetNativeWindow().GetNativeNSWindow()); [NSApp sendEvent:mouse_down]; EXPECT_FALSE(bubble_widget->IsClosed()); bubble_widget->CloseNow(); // Test with RightMouseDown when set_close_on_deactivate(false). TestBubbleView* bubble_view = new TestBubbleView(parent_widget); bubble_view->set_close_on_deactivate(false); bubble_widget = BubbleDialogDelegateView::CreateBubble(bubble_view); ShowKeyWindow(bubble_widget); mouse_down = cocoa_test_event_utils::RightMouseDownAtPointInWindow( NSMakePoint(50, 50), parent_widget->GetNativeWindow().GetNativeNSWindow()); [NSApp sendEvent:mouse_down]; EXPECT_FALSE(bubble_widget->IsClosed()); parent_widget->CloseNow(); } // Ensure BridgedContentView's inputContext can handle its window being torn // away mid-way through event processing. Toolkit-views guarantees to move focus // away from any Widget when the window is torn down. This test ensures that // global references AppKit may have held on to are also updated. TEST_F(NativeWidgetMacInteractiveUITest, GlobalNSTextInputContextUpdates) { Widget* widget = CreateTopLevelNativeWidget(); Textfield* textfield = new Textfield; textfield->SetBounds(0, 0, 100, 100); widget->GetContentsView()->AddChildView(textfield); textfield->RequestFocus(); { WidgetActivationWaiter wait_for_first_active(widget, true); widget->Show(); wait_for_first_active.Wait(); } EXPECT_TRUE([widget->GetNativeView().GetNativeNSView() inputContext]); EXPECT_EQ([widget->GetNativeView().GetNativeNSView() inputContext], [NSTextInputContext currentInputContext]); widget->GetContentsView()->RemoveChildView(textfield); // NSTextInputContext usually only updates at the end of an AppKit event loop // iteration. We just tore out the inputContext, so ensure the raw, weak // global pointer that AppKit likes to keep around has been updated manually. EXPECT_EQ(nil, [NSTextInputContext currentInputContext]); EXPECT_FALSE([widget->GetNativeView().GetNativeNSView() inputContext]); // RemoveChildView() doesn't delete the view. delete textfield; widget->Close(); base::RunLoop().RunUntilIdle(); } INSTANTIATE_TEST_SUITE_P(NativeWidgetMacInteractiveUITestInstance, NativeWidgetMacInteractiveUITest, ::testing::Bool()); } // namespace test } // namespace views