| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ios/chrome/common/string_util.h" |
| |
| #import <UIKit/UIKit.h> |
| |
| #include "base/ios/ns_range.h" |
| #include "base/test/gtest_util.h" |
| #import "ios/chrome/common/ui/colors/semantic_color_names.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/gtest_mac.h" |
| #include "testing/platform_test.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| 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]); |
| } |
| } |
| } |
| |
| TEST_F(StringUtilTest, CleanNSStringForDisplay) { |
| NSArray* const all_test_data = @[ |
| @{ |
| @"input" : @"Clean String", |
| @"remove_graphic_chars" : @NO, |
| @"expected" : @"Clean String" |
| }, |
| @{ |
| @"input" : @" \t String with leading and trailing whitespaces ", |
| @"remove_graphic_chars" : @NO, |
| @"expected" : @"String with leading and trailing whitespaces" |
| }, |
| @{ |
| @"input" : @" \n\n\r String with \n\n\r\n\r newline characters \n\n\r", |
| @"remove_graphic_chars" : @NO, |
| @"expected" : @"String with newline characters" |
| }, |
| @{ |
| @"input" : @"String with an arrow ⟰ that remains intact", |
| @"remove_graphic_chars" : @NO, |
| @"expected" : @"String with an arrow ⟰ that remains intact" |
| }, |
| @{ |
| @"input" : @"String with an arrow ⟰ that gets cleaned up", |
| @"remove_graphic_chars" : @YES, |
| @"expected" : @"String with an arrow that gets cleaned up" |
| }, |
| ]; |
| |
| for (NSDictionary* test_data : all_test_data) { |
| NSString* input_text = test_data[@"input"]; |
| NSString* expected_text = test_data[@"expected"]; |
| BOOL remove_graphic_chars = [test_data[@"remove_graphic_chars"] boolValue]; |
| |
| EXPECT_NSEQ(expected_text, |
| CleanNSStringForDisplay(input_text, remove_graphic_chars)); |
| } |
| } |
| |
| TEST_F(StringUtilTest, SubstringOfWidth) { |
| // returns nil for zero-length strings |
| EXPECT_NSEQ(SubstringOfWidth(@"", @{}, 100, NO), nil); |
| EXPECT_NSEQ(SubstringOfWidth(nil, @{}, 100, NO), nil); |
| |
| // This font should always exist |
| UIFont* sys_font = [UIFont systemFontOfSize:18.0f]; |
| NSDictionary* attributes = @{NSFontAttributeName : sys_font}; |
| |
| EXPECT_NSEQ(SubstringOfWidth(@"asdf", attributes, 100, NO), @"asdf"); |
| |
| // construct some string of known lengths |
| NSString* leading = @"some text"; |
| NSString* trailing = @"some other text"; |
| NSString* mid = @"some text for the method to do some actual work"; |
| NSString* long_string = |
| [[leading stringByAppendingString:mid] stringByAppendingString:trailing]; |
| |
| CGFloat leading_width = [leading sizeWithAttributes:attributes].width; |
| CGFloat trailing_width = [trailing sizeWithAttributes:attributes].width; |
| |
| NSString* leading_calculated = |
| SubstringOfWidth(long_string, attributes, leading_width, NO); |
| EXPECT_NSEQ(leading, leading_calculated); |
| |
| NSString* trailing_calculated = |
| SubstringOfWidth(long_string, attributes, trailing_width, YES); |
| EXPECT_NSEQ(trailing, trailing_calculated); |
| } |
| |
| // Verifies when it should return CGRectNull and when it shouldn't. |
| TEST_F(StringUtilTest, TextViewLinkBound) { |
| UITextView* text_view = [[UITextView alloc] init]; |
| 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 |