chromium/ui/views/controls/native/native_view_host_mac_unittest.mm

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/native/native_view_host_mac.h"

#import <Cocoa/Cocoa.h>

#include <memory>

#include "base/mac/mac_util.h"
#import "testing/gtest_mac.h"
#import "ui/base/cocoa/views_hostable.h"
#import "ui/views/cocoa/native_widget_mac_ns_window_host.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/controls/native/native_view_host_test_base.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

class TestViewsHostable : public ui::ViewsHostableView {
 public:
  id parent_accessibility_element() const {
    return parent_accessibility_element_;
  }

 private:
  // ui::ViewsHostableView:
  void ViewsHostableAttach(ui::ViewsHostableView::Host* host) override {
  }
  void ViewsHostableDetach() override { parent_accessibility_element_ = nil; }
  void ViewsHostableSetBounds(const gfx::Rect& bounds_in_window) override {}
  void ViewsHostableSetVisible(bool visible) override {}
  void ViewsHostableMakeFirstResponder() override {}
  void ViewsHostableSetParentAccessible(
      gfx::NativeViewAccessible parent_accessibility_element) override {
    parent_accessibility_element_ = parent_accessibility_element;
  }
  gfx::NativeViewAccessible ViewsHostableGetParentAccessible() override {
    return parent_accessibility_element_;
  }
  gfx::NativeViewAccessible ViewsHostableGetAccessibilityElement() override {
    return nil;
  }

  id parent_accessibility_element_ = nil;
};

@interface TestViewsHostableView : NSView<ViewsHostable>
@property(nonatomic, assign) ui::ViewsHostableView* viewsHostableView;
@end
@implementation TestViewsHostableView
@synthesize viewsHostableView = _viewsHostableView;
@end

namespace views {

class NativeViewHostMacTest : public test::NativeViewHostTestBase {
 public:
  NativeViewHostMacTest() = default;

  NativeViewHostMacTest(const NativeViewHostMacTest&) = delete;
  NativeViewHostMacTest& operator=(const NativeViewHostMacTest&) = delete;

  // testing::Test:
  void TearDown() override {
    // On Aura, the compositor is destroyed when the WindowTreeHost provided by
    // AuraTestHelper is destroyed. On Mac, the Widget is the host, so it must
    // be closed before the ContextFactory is torn down by ViewsTestBase.
    DestroyTopLevel();
    NativeViewHostTestBase::TearDown();
  }

  NativeViewHostMac* native_host() {
    return static_cast<NativeViewHostMac*>(GetNativeWrapper());
  }

  void CreateHost() {
    CreateTopLevel();
    CreateTestingHost();
    native_view_ = [[NSView alloc] initWithFrame:NSZeroRect];

    // Verify the expectation that the NativeViewHostWrapper is only created
    // after the NativeViewHost is added to a widget.
    EXPECT_FALSE(native_host());
    toplevel()->GetRootView()->AddChildView(host());
    EXPECT_TRUE(native_host());

    host()->Attach(native_view_);
  }

  NSView* GetMovedContentViewForWidget(const std::unique_ptr<Widget>& widget) {
    return (__bridge NSView*)widget->GetNativeWindowProperty(
        views::NativeWidgetMacNSWindowHost::kMovedContentNSView);
  }

 protected:
  NSView* __strong native_view_;
};

// Test destroying the top level widget before destroying the NativeViewHost.
// On Mac, also ensure that the native view is removed from its superview when
// the Widget containing its host is destroyed.
TEST_F(NativeViewHostMacTest, DestroyWidget) {
  ResetHostDestroyedCount();
  CreateHost();
  ReleaseHost();
  EXPECT_EQ(0, host_destroyed_count());
  EXPECT_TRUE([native_view_ superview]);
  DestroyTopLevel();
  EXPECT_FALSE([native_view_ superview]);
  EXPECT_EQ(1, host_destroyed_count());
}

// Ensure the native view receives the correct bounds when it is attached. On
// Mac, the bounds of the native view is relative to the NSWindow it is in, not
// the screen, and the coordinates have to be flipped.
TEST_F(NativeViewHostMacTest, Attach) {
  CreateHost();
  EXPECT_TRUE([native_view_ superview]);
  EXPECT_TRUE([native_view_ window]);

  host()->Detach();

  [native_view_ setFrame:NSZeroRect];
  toplevel()->SetBounds(gfx::Rect(64, 48, 100, 200));
  host()->SetBounds(10, 10, 80, 60);

  EXPECT_FALSE([native_view_ superview]);
  EXPECT_FALSE([native_view_ window]);
  EXPECT_NSEQ(NSZeroRect, [native_view_ frame]);

  host()->Attach(native_view_);
  EXPECT_TRUE([native_view_ superview]);
  EXPECT_TRUE([native_view_ window]);

  // Layout is normally async, trigger it now to ensure bounds have been
  // applied.
  host()->DeprecatedLayoutImmediately();
  // Expect the top-left to be 10 pixels below the titlebar.
  int bottom = toplevel()->GetClientAreaBoundsInScreen().height() - 10 - 60;
  EXPECT_NSEQ(NSMakeRect(10, bottom, 80, 60), [native_view_ frame]);

  DestroyHost();
}

// If Widget A has been attached to Widget B, ensure Widget A maintains a
// reference to its native view.
TEST_F(NativeViewHostMacTest, CheckNativeViewReferenceOnAttach) {
  CreateTopLevel();
  CreateTestingHost();
  toplevel()->GetRootView()->AddChildView(host());

  // Create a second widget.
  auto second_widget = std::make_unique<Widget>();
  Widget::InitParams params =
      CreateParams(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
                   Widget::InitParams::TYPE_WINDOW);
  params.delegate = nullptr;
  second_widget->Init(std::move(params));

  // No reference to its native view should exist currently.
  EXPECT_EQ(GetMovedContentViewForWidget(second_widget), nullptr);

  NSView* view = second_widget->GetNativeView().GetNativeNSView();
  NSWindow* native_window = [view window];
  host()->Attach(second_widget->GetNativeView());

  // On Ventura, the attach rips Widget A's contentView from its window.
  // NativeViewHostMac::AttachNativeView() should have stored a reference.
  if (base::mac::MacOSMajorVersion() >= 13) {
    EXPECT_EQ([native_window contentView], nullptr);
    EXPECT_EQ(GetMovedContentViewForWidget(second_widget), view);
  } else {
    EXPECT_EQ([native_window contentView], view);
  }

  // After detaching, there should be no reference, and the native view should
  // be restored to its widget's window.
  host()->Detach();
  EXPECT_EQ(GetMovedContentViewForWidget(second_widget), nullptr);
  EXPECT_EQ([native_window contentView], view);

  DestroyHost();
}

// On macOS13, if Widget A has been attached to Widget B, ensure Widget A's
// reference to its native view disappears when the native view is freed.
TEST_F(NativeViewHostMacTest, CheckNoNativeViewReferenceOnDestruct) {
  if (base::mac::MacOSMajorVersion() < 13) {
    return;
  }

  CreateTopLevel();
  CreateTestingHost();
  toplevel()->GetRootView()->AddChildView(host());

  // Create a second widget.
  auto second_widget = std::make_unique<Widget>();
  Widget::InitParams params =
      CreateParams(Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
                   Widget::InitParams::TYPE_WINDOW);
  params.delegate = nullptr;
  second_widget->Init(std::move(params));

  // No reference should to the native view should exist currently.
  EXPECT_EQ(GetMovedContentViewForWidget(second_widget), nullptr);

  // Attaching the widget's native view should store a reference.
  NSView* view = second_widget->GetNativeView().GetNativeNSView();
  host()->Attach(second_widget->GetNativeView());
  EXPECT_EQ(GetMovedContentViewForWidget(second_widget), view);

  // Tearing down the destination widget (where we attached the second widget)
  // will free the native view and should remove the reference.
  DestroyHost();

  EXPECT_EQ(GetMovedContentViewForWidget(second_widget), nullptr);
}

// Ensure the native view is integrated into the views accessibility
// hierarchy if the native view conforms to the AccessibilityParent
// protocol.
TEST_F(NativeViewHostMacTest, AccessibilityParent) {
  CreateHost();
  host()->Detach();

  TestViewsHostableView* view = [[TestViewsHostableView alloc] init];
  TestViewsHostable views_hostable;
  [view setViewsHostableView:&views_hostable];

  host()->Attach(view);
  EXPECT_NSEQ(views_hostable.parent_accessibility_element(),
              toplevel()->GetRootView()->GetNativeViewAccessible());

  host()->Detach();
  DestroyHost();
  EXPECT_FALSE(views_hostable.parent_accessibility_element());
}

// Test that the content windows' bounds are set to the correct values while the
// native size is equal or not equal to the View size.
TEST_F(NativeViewHostMacTest, ContentViewPositionAndSize) {
  CreateHost();
  toplevel()->SetBounds(gfx::Rect(0, 0, 100, 100));

  native_host()->ShowWidget(5, 10, 100, 100, 200, 200);
  EXPECT_NSEQ(NSMakeRect(5, -38, 100, 100), native_view_.frame);

  native_host()->ShowWidget(10, 25, 50, 50, 50, 50);
  EXPECT_NSEQ(NSMakeRect(10, -3, 50, 50), native_view_.frame);

  DestroyHost();
}

// Ensure the native view is hidden along with its host, and when detaching, or
// when attaching to a host that is already hidden.
TEST_F(NativeViewHostMacTest, NativeViewHidden) {
  CreateHost();
  toplevel()->SetBounds(gfx::Rect(0, 0, 100, 100));
  host()->SetBounds(10, 10, 80, 60);

  EXPECT_FALSE([native_view_ isHidden]);
  host()->SetVisible(false);
  EXPECT_TRUE([native_view_ isHidden]);
  host()->SetVisible(true);
  EXPECT_FALSE([native_view_ isHidden]);

  host()->Detach();
  EXPECT_TRUE([native_view_ isHidden]);  // Hidden when detached.
  [native_view_ setHidden:NO];

  host()->SetVisible(false);
  EXPECT_FALSE([native_view_ isHidden]);  // Stays visible.
  host()->Attach(native_view_);
  EXPECT_TRUE([native_view_ isHidden]);  // Hidden when attached.

  host()->Detach();
  [native_view_ setHidden:YES];
  host()->SetVisible(true);
  EXPECT_TRUE([native_view_ isHidden]);  // Stays hidden.
  host()->Attach(native_view_);
  // Layout updates visibility, and is normally async, trigger it now to ensure
  // visibility updated.
  host()->DeprecatedLayoutImmediately();
  EXPECT_FALSE([native_view_ isHidden]);  // Made visible when attached.

  EXPECT_TRUE([native_view_ superview]);
  toplevel()->GetRootView()->RemoveChildView(host());
  EXPECT_TRUE([native_view_ isHidden]);  // Hidden when removed from Widget.
  EXPECT_FALSE([native_view_ superview]);

  toplevel()->GetRootView()->AddChildView(host());
  EXPECT_FALSE([native_view_ isHidden]);  // And visible when added.
  EXPECT_TRUE([native_view_ superview]);

  DestroyHost();
}

// Check that we can destroy cleanly even if the native view has already been
// released.
TEST_F(NativeViewHostMacTest, NativeViewReleased) {
  @autoreleasepool {
    CreateHost();
    // In practice the native view is a WebContentsViewCocoa which is retained
    // by its superview (a TabContentsContainerView) and by WebContentsViewMac.
    // It's possible for both of them to be destroyed without calling
    // NativeHostView::Detach().
    [native_view_ removeFromSuperview];
    native_view_ = nil;
  }

  // During teardown, NativeViewDetaching() is called in RemovedFromWidget().
  // Just trigger it with Detach().
  host()->Detach();

  DestroyHost();
}

}  // namespace views