chromium/ios/chrome/browser/ui/omnibox/omnibox_text_field_ios_unittest.mm

// Copyright 2012 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/ui/omnibox/omnibox_text_field_ios.h"

#import "base/command_line.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/path_service.h"
#import "base/strings/string_split.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/common/uikit_ui_util.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

class OmniboxTextFieldIOSTest : public PlatformTest {
 protected:
  void SetUp() override {
    PlatformTest::SetUp();
    // This rect is fairly arbitrary. The text field just needs a non-zero width
    // so that the pre-edit label's text alignment can be tested.
    CGRect rect = CGRectMake(0, 0, 100, 20);
    textfield_ = [[OmniboxTextFieldIOS alloc] initWithFrame:rect];
    [GetAnyKeyWindow() addSubview:textfield_];
  }

  void TearDown() override { [textfield_ removeFromSuperview]; }

  void ExpectRectEqual(CGRect expectedRect, CGRect actualRect) {
    EXPECT_EQ(expectedRect.origin.x, actualRect.origin.x);
    EXPECT_EQ(expectedRect.origin.y, actualRect.origin.y);
    EXPECT_EQ(expectedRect.size.width, actualRect.size.width);
    EXPECT_EQ(expectedRect.size.height, actualRect.size.height);
  }

  // Verifies that the `selectedNSRange` function properly converts from opaque
  // UITextRanges to NSRanges.  This function selects blocks of text in the text
  // field and compares the field's actual selected text to the converted
  // NSRange.
  void VerifySelectedNSRanges(NSString* text) {
    // The NSRange conversion mechanism only works when the field is first
    // responder.
    [textfield_ setText:text];
    [textfield_ becomeFirstResponder];
    ASSERT_TRUE([textfield_ isFirstResponder]);

    // `i` and `j` hold the start and end offsets of the range that is currently
    // being tested.  This function iterates through all possible combinations
    // of `i` and `j`.
    NSInteger i = 0;
    NSInteger j = i + 1;
    UITextPosition* beginning = [textfield_ beginningOfDocument];
    UITextPosition* start =
        [textfield_ positionFromPosition:[textfield_ beginningOfDocument]
                                  offset:i];

    // In order to avoid making any assumptions about the length of the text in
    // the field, this test operates by incrementing the `i` and `j` offsets and
    // converting them to opaque UITextPositions.  If either `i` or `j` are
    // invalid offsets for the current field text,
    // `positionFromPosition:offset:` is documented to return nil.  This is used
    // as a signal to stop incrementing that offset and reset (or end the test).
    while (start) {
      UITextPosition* end = [textfield_ positionFromPosition:beginning
                                                      offset:j];
      while (end) {
        [textfield_
            setSelectedTextRange:[textfield_ textRangeFromPosition:start
                                                        toPosition:end]];

        // There are two ways to get the selected text:
        // 1) Ask the field for it directly.
        // 2) Compute the selected NSRange and use that to extract a substring
        //    from the field's text.
        // This block of code ensures that the two methods give identical text.
        NSRange nsrange = [textfield_ selectedNSRange];
        NSString* nstext = [[textfield_ text] substringWithRange:nsrange];
        UITextRange* uirange = [textfield_ selectedTextRange];
        NSString* uitext = [textfield_ textInRange:uirange];
        EXPECT_NSEQ(nstext, uitext);

        // Increment `j` and `end` for the next iteration of the inner while
        // loop.
        ++j;
        end = [textfield_ positionFromPosition:beginning offset:j];
      }

      // Increment `i` and `start` for the next iteration of the outer while
      // loop.  This also requires `j` to be reset.
      ++i;
      j = i + 1;
      start = [textfield_ positionFromPosition:beginning offset:i];
    }

    [textfield_ resignFirstResponder];
  }

  OmniboxTextFieldIOS* textfield_;
};

// TODO:(crbug.com/1156541): Re-enable this test on devices.
#if TARGET_OS_SIMULATOR
#define MAYBE_SelectedRanges SelectedRanges
#else
#define MAYBE_SelectedRanges FLAKY_SelectedRanges
#endif
TEST_F(OmniboxTextFieldIOSTest, MAYBE_SelectedRanges) {
  if (@available(iOS 17, *)) {
    // TODO:(crbug.com/1468176): Failing on iOS17 beta 5.
    return;
  }
  base::FilePath test_data_directory;
  ASSERT_TRUE(base::PathService::Get(ios::DIR_TEST_DATA, &test_data_directory));
  base::FilePath test_file = test_data_directory.Append(
      FILE_PATH_LITERAL("omnibox/selected_ranges.txt"));
  ASSERT_TRUE(base::PathExists(test_file));

  std::string contents;
  ASSERT_TRUE(base::ReadFileToString(test_file, &contents));
  std::vector<std::string> test_strings = base::SplitString(
      contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  for (size_t i = 0; i < test_strings.size(); ++i) {
    if (test_strings[i].size() > 0) {
      VerifySelectedNSRanges(base::SysUTF8ToNSString(test_strings[i]));
    }
  }
}

TEST_F(OmniboxTextFieldIOSTest, SelectExitsPreEditState) {
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ isPreEditing]);
  [textfield_ select:nil];
  EXPECT_FALSE([textfield_ isPreEditing]);
}

TEST_F(OmniboxTextFieldIOSTest, SelectAllExitsPreEditState) {
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ isPreEditing]);
  [textfield_ selectAll:nil];
  EXPECT_FALSE([textfield_ isPreEditing]);
}

TEST_F(OmniboxTextFieldIOSTest, CopyInPreedit) {
  id delegateMock = OCMProtocolMock(@protocol(OmniboxTextFieldDelegate));
  NSString* testString = @"omnibox test string";
  [textfield_ setText:testString];
  textfield_.delegate = delegateMock;
  [textfield_ becomeFirstResponder];
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ canPerformAction:@selector(copy:) withSender:nil]);
  [delegateMock onCopy];
  [textfield_ copy:nil];
  EXPECT_TRUE([textfield_.text isEqualToString:testString]);
  [delegateMock verify];
}

TEST_F(OmniboxTextFieldIOSTest, CutInPreedit) {
  id delegateMock = OCMProtocolMock(@protocol(OmniboxTextFieldDelegate));
  NSString* testString = @"omnibox test string";
  [textfield_ setText:testString];
  textfield_.delegate = delegateMock;
  [textfield_ becomeFirstResponder];
  [textfield_ enterPreEditState];
  EXPECT_TRUE([textfield_ canPerformAction:@selector(cut:) withSender:nil]);
  [delegateMock onCopy];
  [textfield_ cut:nil];
  EXPECT_TRUE([textfield_.text isEqualToString:@""]);
  [delegateMock verify];
}

}  // namespace