chromium/ios/chrome/browser/contextual_panel/ui/contextual_panel_egtest.mm

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

#import "base/strings/stringprintf.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "net/test/embedded_test_server/request_handler_util.h"

namespace {
// The page height of test pages. This must be big enough to triger fullscreen.
const int kPageHeightEM = 200;

// Provides test page long enough to allow fullscreen.
std::unique_ptr<net::test_server::HttpResponse> GetLongResponseForFullscreen(
    const net::test_server::HttpRequest& request) {
  auto result = std::make_unique<net::test_server::BasicHttpResponse>();
  result->set_code(net::HTTP_OK);
  result->set_content(base::StringPrintf(
      "<p style='height:%dem'>test1</p><p>test2</p>", kPageHeightEM));
  return result;
}
}  // namespace

@interface ContextualPanelTestCase : ChromeTestCase
@end

@implementation ContextualPanelTestCase

- (void)setUp {
  [super setUp];
  [ChromeEarlGrey resetDataForLocalStatePref:prefs::kBottomOmnibox];

  self.testServer->RegisterRequestHandler(base::BindRepeating(
      &net::test_server::HandlePrefixedRequest, "/long-fullscreen",
      base::BindRepeating(&GetLongResponseForFullscreen)));

  bool started = self.testServer->Start();
  GREYAssertTrue(started, @"Test server failed to start.");
}

- (void)tearDown {
  [super tearDown];
  [ChromeEarlGrey resetDataForLocalStatePref:prefs::kBottomOmnibox];
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config;

  config.relaunch_policy = ForceRelaunchByCleanShutdown;

  if ([self isRunningTest:@selector(testOpenContextualPanelFromNormalIPH)]) {
    config.features_enabled_and_params.push_back(
        {kContextualPanel, {{{"entrypoint-rich-iph", "false"}}}});
  } else {
    config.features_enabled_and_params.push_back({kContextualPanel, {}});
  }

  config.features_enabled_and_params.push_back(
      {kContextualPanelForceShowEntrypoint, {}});

  if ([self isRunningTest:@selector(testOpenContextualPanelFromNormalIPH)] ||
      [self isRunningTest:@selector(testOpenContextualPanelFromRichIPH)] ||
      [self isRunningTest:@selector(testOrientationChangeDismissesIPH)]) {
    config.iph_feature_enabled =
        feature_engagement::kIPHiOSContextualPanelSampleModelFeature.name;
  }

  return config;
}

// Tests that the contextual panel opens correctly.
- (void)testOpenContextualPanel {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Close panel
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Tests that the contextual panel opens correctly when tapping the rich IPH.
- (void)testOpenContextualPanelFromRichIPH {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  // Check that the IPH has appeared.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:grey_accessibilityID(
                                              @"BubbleViewLabelIdentifier")];

  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          @"BubbleViewLabelIdentifier")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Close panel
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Tests that the contextual panel opens correctly when tapping the normal IPH.
- (void)testOpenContextualPanelFromNormalIPH {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  // Check that the IPH has appeared.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:grey_accessibilityID(
                                              @"BubbleViewLabelIdentifier")];

  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
                                          @"BubbleViewLabelIdentifier")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Close panel
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Test that the Contextual Panel can still be closed after rotating to
// landscape.
- (void)testContextualPanelLandscape {
  // This test is not relevant on iPads as iPads aren't compact height in
  // landscape.
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Skipped for iPad");
  }

  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Switch to landscape.
  GREYAssert(
      [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft
                                    error:nil],
      @"Could not rotate device to Landscape Left");

  // Make sure that panel can still be closed.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Tests that closing the last tab with the panel open doesn't crash.
- (void)testCloseLastTabWithPanelOpen {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Close the tab.
  [ChromeEarlGrey closeTabAtIndex:0];
}

// Tests that closing the last tab before the large entrypoint callback is run
// doesn't crash.
- (void)testCloseLastTabBeforeLargeEntrypointAppears {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  // Close the tab.
  [ChromeEarlGrey closeTabAtIndex:0];
}

// Tests that the contextual panel transitions neatly between iOS sheet
// controller (full iPad layout) and the panel's custom sheet component (other
// window open/iPhone-style layout).
- (void)testContexutalPaneliPadMultiwindow {
  if (![ChromeEarlGrey areMultipleWindowsSupported]) {
    EARL_GREY_TEST_DISABLED(@"Multiple windows can't be opened.");
  }

  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Open a second window.
  [ChromeEarlGrey openNewWindow];
  [ChromeEarlGrey waitUntilReadyWindowWithNumber:1];
  [ChromeEarlGrey waitForForegroundWindowCount:2];

  // Check that the panel is still visible in the first window
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Close the second window.
  [ChromeEarlGrey closeWindowWithNumber:1];
  [ChromeEarlGrey waitForForegroundWindowCount:1];

  // Check that the panel is still visible in the first window
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Tests that fullscreen is disabled when the omnibox switches to bottom
// position.
- (void)testBottomOmniboxDisablesFullscreen {
  if (![ChromeEarlGrey isBottomOmniboxAvailable]) {
    EARL_GREY_TEST_SKIPPED(@"Test requires bottom omnibox");
  }

  [ChromeEarlGrey loadURL:self.testServer->GetURL("/long-fullscreen")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Scroll up in the webpage to enter fullscreen
  [[EarlGrey
      selectElementWithMatcher:chrome_test_util::WebStateScrollViewMatcher()]
      performAction:grey_swipeFastInDirection(kGREYDirectionUp)];
  [ChromeEarlGreyUI waitForToolbarVisible:NO];

  // Enable bottom omnibox.
  [ChromeEarlGrey setBoolValue:YES forLocalStatePref:prefs::kBottomOmnibox];
  [ChromeEarlGreyUI waitForToolbarVisible:YES];

  // Make sure that panel can still be closed.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelCloseButtonAXID")]
      performAction:grey_tap()];
}

// Test that the Contextual Panel entrypoint's large chip can be dismissed via
// swipe.
- (void)testContextualPanelEntrypointLargeChipDismissable {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  // Wait for large chip entrypoint to appear.
  [ChromeEarlGrey
      waitForSufficientlyVisibleElementWithMatcher:
          grey_accessibilityID(@"ContextualPanelEntrypointLabelAXID")];

  // Side swipe on the entrypoint.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointLabelAXID")]
      performAction:grey_swipeSlowInDirectionWithStartPoint(kGREYDirectionLeft,
                                                            0.9, 0.5)];

  // Check that the entrypoint is now back to default size.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointLabelAXID")]
      assertWithMatcher:grey_notVisible()];
}

// Test that the Contextual Panel entrypoint IPH is dismissed when the device
// orientation changes.
- (void)testOrientationChangeDismissesIPH {
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/defaultresponse")];

  // Check that the IPH has appeared.
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:grey_accessibilityID(
                                              @"BubbleViewLabelIdentifier")];

  // Switch to landscape.
  GREYAssert(
      [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft
                                    error:nil],
      @"Could not rotate device to Landscape Left");

  // Check that the IPH has disappeared.
  [ChromeEarlGrey
      waitForUIElementToDisappearWithMatcher:grey_accessibilityID(
                                                 @"BubbleViewLabelIdentifier")];
}

// Tests that opening the keyboard on iPhone closes the panel. On iPad, the
// panel is presented modally, so the panel wouldn't close.
- (void)testKeyboardOpenClosesPanelOniPhone {
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Test conditions don't happen on iPad.");
  }

  // Open a page wth a text field.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/simple_login_form.html")];

  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(
                                   @"ContextualPanelEntrypointImageViewAXID")]
      performAction:grey_tap()];

  // Check that the contextual panel opened up.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Open the keyboard by tapping a text field.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
      performAction:chrome_test_util::TapWebElementWithId("un")];

  // Check that the contextual panel is closed.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityID(@"PanelContentViewAXID")]
      assertWithMatcher:grey_nil()];
}

@end