chromium/ios/chrome/browser/shared/ui/util/layout_guide_center_unittest.mm

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

#import "ios/chrome/browser/shared/ui/util/util_swift.h"

#import <UIKit/UIKit.h>

#import "base/apple/foundation_util.h"
#import "base/test/ios/wait_util.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

using base::test::ios::kWaitForUIElementTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

// Sets up layout guide center.
class LayoutGuideCenterTest : public PlatformTest {
 protected:
  LayoutGuideCenterTest() : center_([[LayoutGuideCenter alloc] init]) {}

  LayoutGuideCenter* center_;
};

// Checks that the correct view is referenced.
TEST_F(LayoutGuideCenterTest, TestReferenced) {
  UIView* reference_view = [[UIView alloc] init];

  [center_ referenceView:reference_view underName:@"view"];

  EXPECT_EQ([center_ referencedViewUnderName:@"view"], reference_view);
}

// Checks that the view is no longer referenced.
TEST_F(LayoutGuideCenterTest, TestDereferenced) {
  UIView* reference_view = [[UIView alloc] init];
  [center_ referenceView:reference_view underName:@"view"];

  [center_ referenceView:nil underName:@"view"];

  EXPECT_EQ([center_ referencedViewUnderName:@"view"], nil);
}

// Checks that a tracking layout guide is correctly updated to match the
// reference view's frame.
TEST_F(LayoutGuideCenterTest, LayoutGuideMatchesReferenceView) {
  CGRect rect = CGRectMake(10, 20, 30, 40);
  UIView* reference_view = [[UIView alloc] initWithFrame:rect];
  [center_ referenceView:reference_view underName:@"view"];
  // Set up the tracking layout guide.
  UILayoutGuide* layout_guide = [center_ makeLayoutGuideNamed:@"view"];
  UIView* view = [[UIView alloc] init];
  [view addLayoutGuide:layout_guide];

  UIWindow* window = [[UIWindow alloc] init];
  [window addSubview:reference_view];
  [window addSubview:view];

  EXPECT_TRUE(CGRectEqualToRect(layout_guide.layoutFrame, rect));
  EXPECT_EQ([center_ referencedViewUnderName:@"view"], reference_view);
}

// Checks that a tracking layout guide is correctly updated to track the
// reference view's frame.
TEST_F(LayoutGuideCenterTest, LayoutGuideTracksReferenceView) {
  UIView* reference_view = [[UIView alloc] init];
  [center_ referenceView:reference_view underName:@"view"];
  // Set up the tracking layout guide.
  UILayoutGuide* layout_guide = [center_ makeLayoutGuideNamed:@"view"];
  UIView* view = [[UIView alloc] init];
  [view addLayoutGuide:layout_guide];
  UIWindow* window = [[UIWindow alloc] init];
  [window addSubview:reference_view];
  [window addSubview:view];
  EXPECT_TRUE(CGRectEqualToRect(layout_guide.layoutFrame, CGRectZero));

  for (NSValue* rectValue in @[
         @(CGRectMake(0, -10, 20, 30)), @(CGRectMake(3, 14, 15, 93)),
         @(CGRectZero)
       ]) {
    CGRect rect = rectValue.CGRectValue;
    reference_view.frame = rect;
    [window setNeedsLayout];
    [window layoutIfNeeded];

    // Wait until the frame is updated.
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^{
      return CGRectEqualToRect(layout_guide.layoutFrame, rect);
    }));
  }
}

// Checks that a tracking layout guide is correctly updated to match the
// reference view's frame in a different window.
TEST_F(LayoutGuideCenterTest,
       LayoutGuideMatchesReferenceViewInDifferentWindow) {
  CGRect rect = CGRectMake(10, 20, 30, 40);
  UIView* reference_view = [[UIView alloc] initWithFrame:rect];
  [center_ referenceView:reference_view underName:@"view"];
  // Set up the tracking layout guide.
  UILayoutGuide* layout_guide = [center_ makeLayoutGuideNamed:@"view"];
  UIView* view = [[UIView alloc] init];
  [view addLayoutGuide:layout_guide];
  // Set up windows in the same scene.
  UIWindowScene* scene = base::apple::ObjCCastStrict<UIWindowScene>(
      [UIApplication.sharedApplication.connectedScenes anyObject]);
  UIWindow* reference_window = [[UIWindow alloc] init];
  reference_window.windowScene = scene;
  UIWindow* window = [[UIWindow alloc] init];
  window.windowScene = scene;

  [reference_window addSubview:reference_view];
  [window addSubview:view];

  EXPECT_TRUE(CGRectEqualToRect(layout_guide.layoutFrame, rect));
}

// Checks that LayoutGuideCenter only keeps weak references to referenced views.
TEST_F(LayoutGuideCenterTest, WeakReferenceView) {
  UIView* reference_view = [[UIView alloc] init];
  // Create a weak reference to the view, to check it gets nilled out.
  __weak UIView* weak_reference_view = reference_view;

  @autoreleasepool {
    [center_ referenceView:reference_view underName:@"view"];
    reference_view = nil;
  }

  // The center should not have kept the view around.
  EXPECT_EQ(weak_reference_view, nil);
}

// Checks that LayoutGuideCenter only keeps weak references to layout guides.
// Warning: this test may fail while debugging, because of intricacies of
// NSHashTable. See `HashTableWeakReference` below for more context.
TEST_F(LayoutGuideCenterTest, WeakLayoutGuide) {
  UILayoutGuide* layout_guide = [center_ makeLayoutGuideNamed:@"view"];
  EXPECT_NE(layout_guide, nil);
  // Create a weak reference to the layout guide, to check it gets nilled out.
  __weak UILayoutGuide* weak_layout_guide = layout_guide;

  @autoreleasepool {
    layout_guide = nil;
  }

  // This can fail if you are breaking in the debugger and inspecting the hash
  // table. This is due to NSHashTable not providing any guarantee as for when
  // the elements are released.
  EXPECT_EQ(weak_layout_guide, nil);
}

// Checks that NSHashTable references its content weakly.
// Warning: NSHashTable's behavior is not really deterministic (from a client
// perspective), in that it decides when it's removing the weak references. When
// breaking in the debugger in this test and poking around the hash table, the
// test fails, for example.
TEST_F(LayoutGuideCenterTest, HashTableWeakReference) {
  UILayoutGuide* layout_guide = [[UILayoutGuide alloc] init];
  __weak UILayoutGuide* weak_layout_guide = layout_guide;
  NSHashTable<UILayoutGuide*>* hash_table = [NSHashTable weakObjectsHashTable];
  [hash_table addObject:layout_guide];

  @autoreleasepool {
    layout_guide = nil;
  }

  // This can fail if you are breaking in the debugger and inspecting the hash
  // table. This is due to NSHashTable not providing any guarantee as for when
  // the elements are released.
  EXPECT_EQ(weak_layout_guide, nil);
}

// Checks that if `referenceView:underName:` is called twice with the same
// arguments, there are no changes
TEST_F(LayoutGuideCenterTest, TestReferenceViewNoChangesIfSameView) {
  CGRect rect = CGRectMake(10, 20, 30, 40);
  UIView* reference_view = [[UIView alloc] initWithFrame:rect];
  [center_ referenceView:reference_view underName:@"view"];

  // Override reference_view's cr_onWindowCoordinatesChanged to later verify
  // that it hasn't changed.
  __block BOOL windowCoordinatesChangedCalled = NO;
  reference_view.cr_onWindowCoordinatesChanged = ^(UIView* view) {
    windowCoordinatesChangedCalled = YES;
  };

  UIView* view = [[UIView alloc] init];
  reference_view.cr_onWindowCoordinatesChanged(view);
  EXPECT_TRUE(windowCoordinatesChangedCalled);
  windowCoordinatesChangedCalled = NO;

  // Re-reference reference_view. This should not change the view's
  // cr_onWindowCoordinatesChanged block.
  [center_ referenceView:reference_view underName:@"view"];
  reference_view.cr_onWindowCoordinatesChanged(view);
  EXPECT_TRUE(windowCoordinatesChangedCalled);
}