chromium/ios/chrome/common/string_util_unittest.mm

// Copyright 2013 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/common/string_util.h"

#import <UIKit/UIKit.h>

#import "base/ios/ns_range.h"
#import "base/test/gtest_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/text_view_util.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace {

using StringUtilTest = PlatformTest;

TEST_F(StringUtilTest, AttributedStringFromStringWithLink) {
  struct TestCase {
    NSString* input;
    NSDictionary* textAttributes;
    NSDictionary* linkAttributes;
    NSString* expectedString;
    NSRange expectedTextRange;
    NSRange expectedLinkRange;
  };

  const TestCase kAllTestCases[] = {
      TestCase{@"Text with valid BEGIN_LINK link END_LINK and spaces.", @{},
               @{NSLinkAttributeName : @"google.com"},
               @"Text with valid link and spaces.", NSRange{0, 16},
               NSRange{16, 4}},
      TestCase{
          @"Text with valid BEGIN_LINK link END_LINK and spaces.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{}, @"Text with valid link and spaces.", NSRange{0, 32},
          NSRange{0, 32}},
      TestCase{
          @"Text with valid BEGIN_LINK link END_LINK and spaces.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
          @"Text with valid link and spaces.",
          NSRange{0, 16},
          NSRange{16, 4},
      },
      TestCase{
          @"Text with valid BEGIN_LINKlinkEND_LINK and no spaces.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
          @"Text with valid link and no spaces.",
          NSRange{0, 16},
          NSRange{16, 4},
      },
  };

  for (const TestCase& test_case : kAllTestCases) {
    const NSAttributedString* result = AttributedStringFromStringWithLink(
        test_case.input, test_case.textAttributes, test_case.linkAttributes);
    EXPECT_NSEQ(result.string, test_case.expectedString);

    // Text at index 0 has text attributes applied until the link location.
    NSRange textRange;
    NSDictionary* resultTextAttributes = [result attributesAtIndex:0
                                                    effectiveRange:&textRange];
    EXPECT_TRUE(NSEqualRanges(test_case.expectedTextRange, textRange));
    EXPECT_NSEQ(test_case.textAttributes, resultTextAttributes);

    // Text at index `expectedRange.location` has link attributes applied.
    NSRange linkRange;
    NSDictionary* resultLinkAttributes =
        [result attributesAtIndex:test_case.expectedLinkRange.location
                   effectiveRange:&linkRange];
    EXPECT_TRUE(NSEqualRanges(test_case.expectedLinkRange, linkRange));

    NSMutableDictionary* combinedAttributes =
        [[NSMutableDictionary alloc] init];
    [combinedAttributes addEntriesFromDictionary:test_case.textAttributes];
    [combinedAttributes addEntriesFromDictionary:test_case.linkAttributes];
    EXPECT_NSEQ(combinedAttributes, resultLinkAttributes);
  }
}

TEST_F(StringUtilTest, AttributedStringFromStringWithLinkFailures) {
  struct TestCase {
    NSString* input;
    NSDictionary* textAttributes;
    NSDictionary* linkAttributes;
  };

  const TestCase kAllTestCases[] = {
      TestCase{
          @"Text without link.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
      },
      TestCase{
          @"Text with BEGIN_LINK and no end link.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
      },
      TestCase{
          @"Text with no begin link and END_LINK.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
      },
      TestCase{
          @"Text with END_LINK before BEGIN_LINK.",
          @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
          @{NSLinkAttributeName : @"google.com"},
      },

  };

  for (const TestCase& test_case : kAllTestCases) {
    EXPECT_CHECK_DEATH(AttributedStringFromStringWithLink(
        test_case.input, test_case.textAttributes, test_case.linkAttributes));
  }
}

TEST_F(StringUtilTest, AttributedStringFromStringWithLinkWithEmptyLink) {
  struct TestCase {
    NSString* input;
    NSDictionary* textAttributes;
    NSDictionary* linkAttributes;
    NSString* expectedString;
  };
  const TestCase test_case = TestCase {
    @"Text with empty link BEGIN_LINK END_LINK.",
        @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
        @{NSLinkAttributeName : @"google.com"}, @"Text with empty link .",
  };

  const NSAttributedString* result = AttributedStringFromStringWithLink(
      test_case.input, test_case.textAttributes, test_case.linkAttributes);
  EXPECT_NSEQ(result.string, test_case.expectedString);

  // Text attributes apply to the full range of the result string.
  NSRange textRange;
  NSDictionary* resultTextAttributes = [result attributesAtIndex:0
                                                  effectiveRange:&textRange];
  EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, test_case.expectedString.length),
                            textRange));
  EXPECT_NSEQ(test_case.textAttributes, resultTextAttributes);
}

TEST_F(StringUtilTest, ParseStringWithLinks) {
  struct TestCase {
    NSString* input;
    StringWithTags expected;
  };

  const TestCase kAllTestCases[] = {
      TestCase{
          @"Text without link.",
          StringWithTags{
              @"Text without link.",
              {},
          },
      },
      TestCase{
          @"Text with empty link BEGIN_LINK END_LINK.",
          StringWithTags{
              @"Text with empty link .",
              {NSRange{21, 0}},
          },
      },
      TestCase{
          @"Text with BEGIN_LINK and no end link.",
          StringWithTags{
              @"Text with BEGIN_LINK and no end link.",
              {},
          },
      },
      TestCase{
          @"Text with no begin link and END_LINK.",
          StringWithTags{
              @"Text with no begin link and END_LINK.",
              {},
          },
      },
      TestCase{@"Text with END_LINK before BEGIN_LINK.",
               StringWithTags{
                   @"Text with END_LINK before BEGIN_LINK.",
                   {},
               }},
      TestCase{
          @"Text with valid BEGIN_LINK link END_LINK and spaces.",
          StringWithTags{
              @"Text with valid link and spaces.",
              {NSRange{16, 4}},
          },
      },
      TestCase{
          @"Text with valid BEGIN_LINKlinkEND_LINK and no spaces.",
          StringWithTags{
              @"Text with valid link and no spaces.",
              {NSRange{16, 4}},
          },
      },
      TestCase{
          @"Text with multiple tags: BEGIN_LINKlink1END_LINK, "
          @"BEGIN_LINKlink2END_LINK and BEGIN_LINKlink3END_LINK.",
          StringWithTags{
              @"Text with multiple tags: link1, link2 and link3.",
              {NSRange{25, 5}, NSRange{32, 5}, NSRange{42, 5}},
          },
      },
  };

  for (const TestCase& test_case : kAllTestCases) {
    const StringWithTags result = ParseStringWithLinks(test_case.input);
    EXPECT_NSEQ(result.string, test_case.expected.string);
    ASSERT_EQ(result.ranges.size(), test_case.expected.ranges.size());
    for (size_t i = 0; i < test_case.expected.ranges.size(); i++) {
      EXPECT_EQ(result.ranges[i], test_case.expected.ranges[i]);
    }
  }
}

TEST_F(StringUtilTest, ParseStringWithTag) {
  struct TestCase {
    NSString* input;
    StringWithTag expected;
  };

  const TestCase kAllTestCases[] = {
      TestCase{
          @"Text without tag.",
          StringWithTag{
              @"Text without tag.",
              NSRange{NSNotFound, 0},
          },
      },
      TestCase{
          @"Text with empty tag BEGIN_TAG END_TAG.",
          StringWithTag{
              @"Text with empty tag  .",
              NSRange{20, 1},
          },
      },
      TestCase{
          @"Text with BEGIN_TAG and no end tag.",
          StringWithTag{
              @"Text with BEGIN_TAG and no end tag.",
              NSRange{NSNotFound, 0},
          },
      },
      TestCase{
          @"Text with no begin tag and END_TAG.",
          StringWithTag{
              @"Text with no begin tag and END_TAG.",
              NSRange{NSNotFound, 0},
          },
      },
      TestCase{@"Text with END_TAG before BEGIN_TAG.",
               StringWithTag{
                   @"Text with END_TAG before BEGIN_TAG.",
                   NSRange{NSNotFound, 0},
               }},
      TestCase{
          @"Text with valid BEGIN_TAG tag END_TAG and spaces.",
          StringWithTag{
              @"Text with valid  tag  and spaces.",
              NSRange{16, 5},
          },
      },
      TestCase{
          @"Text with valid BEGIN_TAGtagEND_TAG and no spaces.",
          StringWithTag{
              @"Text with valid tag and no spaces.",
              NSRange{16, 3},
          },
      },
  };

  for (const TestCase& test_case : kAllTestCases) {
    const StringWithTag result =
        ParseStringWithTag(test_case.input, @"BEGIN_TAG", @"END_TAG");
    EXPECT_NSEQ(result.string, test_case.expected.string);
    EXPECT_EQ(result.range, test_case.expected.range);
  }
}

TEST_F(StringUtilTest, ParseStringWithTags) {
  struct TestCase {
    NSString* input;
    StringWithTags expected;
  };

  const TestCase kAllTestCases[] = {
      TestCase{
          @"Text without tag.",
          StringWithTags{
              @"Text without tag.",
              {},
          },
      },
      TestCase{
          @"Text with empty tag BEGIN_TAG END_TAG.",
          StringWithTags{
              @"Text with empty tag  .",
              {NSRange{20, 1}},
          },
      },
      TestCase{
          @"Text with BEGIN_TAG and no end tag.",
          StringWithTags{
              @"Text with BEGIN_TAG and no end tag.",
              {},
          },
      },
      TestCase{
          @"Text with no begin tag and END_TAG.",
          StringWithTags{
              @"Text with no begin tag and END_TAG.",
              {},
          },
      },
      TestCase{@"Text with END_TAG before BEGIN_TAG.",
               StringWithTags{
                   @"Text with END_TAG before BEGIN_TAG.",
                   {},
               }},
      TestCase{
          @"Text with valid BEGIN_TAG tag END_TAG and spaces.",
          StringWithTags{
              @"Text with valid  tag  and spaces.",
              {NSRange{16, 5}},
          },
      },
      TestCase{
          @"Text with valid BEGIN_TAGtagEND_TAG and no spaces.",
          StringWithTags{
              @"Text with valid tag and no spaces.",
              {NSRange{16, 3}},
          },
      },
      TestCase{
          @"Text with multiple tags: BEGIN_TAGtag1END_TAG, "
          @"BEGIN_TAGtag2END_TAG and BEGIN_TAGtag3END_TAG.",
          StringWithTags{
              @"Text with multiple tags: tag1, tag2 and tag3.",
              {NSRange{25, 4}, NSRange{31, 4}, NSRange{40, 4}},
          },
      },
  };

  for (const TestCase& test_case : kAllTestCases) {
    const StringWithTags result =
        ParseStringWithTags(test_case.input, @"BEGIN_TAG", @"END_TAG");
    EXPECT_NSEQ(result.string, test_case.expected.string);
    ASSERT_EQ(result.ranges.size(), test_case.expected.ranges.size());
    for (size_t i = 0; i < test_case.expected.ranges.size(); i++) {
      EXPECT_EQ(result.ranges[i], test_case.expected.ranges[i]);
    }
  }
}

// Verifies when it should return CGRectNull and when it shouldn't.
TEST_F(StringUtilTest, TextViewLinkBound) {
  UITextView* text_view = CreateUITextViewWithTextKit1();
  text_view.text = @"Some text.";

  // Returns CGRectNull for empty NSRange.
  EXPECT_TRUE(CGRectEqualToRect(
      TextViewLinkBound(text_view, NSMakeRange(-1, -1)), CGRectNull));

  // Returns CGRectNull for a range out of bound.
  EXPECT_TRUE(CGRectEqualToRect(
      TextViewLinkBound(text_view, NSMakeRange(20, 5)), CGRectNull));

  // Returns a CGRect diffent from CGRectNull when there is a range in bound.
  EXPECT_FALSE(CGRectEqualToRect(
      TextViewLinkBound(text_view, NSMakeRange(0, 5)), CGRectNull));
}
}  // namespace