string_util_unittest.mm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. // Copyright 2013 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "ios/chrome/common/string_util.h"
  5. #import <UIKit/UIKit.h>
  6. #include "base/ios/ns_range.h"
  7. #include "base/test/gtest_util.h"
  8. #import "ios/chrome/common/ui/colors/semantic_color_names.h"
  9. #import "ios/chrome/common/ui/util/text_view_util.h"
  10. #include "testing/gtest/include/gtest/gtest.h"
  11. #include "testing/gtest_mac.h"
  12. #include "testing/platform_test.h"
  13. #if !defined(__has_feature) || !__has_feature(objc_arc)
  14. #error "This file requires ARC support."
  15. #endif
  16. namespace {
  17. using StringUtilTest = PlatformTest;
  18. TEST_F(StringUtilTest, AttributedStringFromStringWithLink) {
  19. struct TestCase {
  20. NSString* input;
  21. NSDictionary* textAttributes;
  22. NSDictionary* linkAttributes;
  23. NSString* expectedString;
  24. NSRange expectedTextRange;
  25. NSRange expectedLinkRange;
  26. };
  27. const TestCase kAllTestCases[] = {
  28. TestCase{@"Text with valid BEGIN_LINK link END_LINK and spaces.", @{},
  29. @{NSLinkAttributeName : @"google.com"},
  30. @"Text with valid link and spaces.", NSRange{0, 16},
  31. NSRange{16, 4}},
  32. TestCase{
  33. @"Text with valid BEGIN_LINK link END_LINK and spaces.",
  34. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  35. @{}, @"Text with valid link and spaces.", NSRange{0, 32},
  36. NSRange{0, 32}},
  37. TestCase{
  38. @"Text with valid BEGIN_LINK link END_LINK and spaces.",
  39. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  40. @{NSLinkAttributeName : @"google.com"},
  41. @"Text with valid link and spaces.",
  42. NSRange{0, 16},
  43. NSRange{16, 4},
  44. },
  45. TestCase{
  46. @"Text with valid BEGIN_LINKlinkEND_LINK and no spaces.",
  47. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  48. @{NSLinkAttributeName : @"google.com"},
  49. @"Text with valid link and no spaces.",
  50. NSRange{0, 16},
  51. NSRange{16, 4},
  52. },
  53. };
  54. for (const TestCase& test_case : kAllTestCases) {
  55. const NSAttributedString* result = AttributedStringFromStringWithLink(
  56. test_case.input, test_case.textAttributes, test_case.linkAttributes);
  57. EXPECT_NSEQ(result.string, test_case.expectedString);
  58. // Text at index 0 has text attributes applied until the link location.
  59. NSRange textRange;
  60. NSDictionary* resultTextAttributes = [result attributesAtIndex:0
  61. effectiveRange:&textRange];
  62. EXPECT_TRUE(NSEqualRanges(test_case.expectedTextRange, textRange));
  63. EXPECT_NSEQ(test_case.textAttributes, resultTextAttributes);
  64. // Text at index |expectedRange.location| has link attributes applied.
  65. NSRange linkRange;
  66. NSDictionary* resultLinkAttributes =
  67. [result attributesAtIndex:test_case.expectedLinkRange.location
  68. effectiveRange:&linkRange];
  69. EXPECT_TRUE(NSEqualRanges(test_case.expectedLinkRange, linkRange));
  70. NSMutableDictionary* combinedAttributes =
  71. [[NSMutableDictionary alloc] init];
  72. [combinedAttributes addEntriesFromDictionary:test_case.textAttributes];
  73. [combinedAttributes addEntriesFromDictionary:test_case.linkAttributes];
  74. EXPECT_NSEQ(combinedAttributes, resultLinkAttributes);
  75. }
  76. }
  77. TEST_F(StringUtilTest, AttributedStringFromStringWithLinkFailures) {
  78. struct TestCase {
  79. NSString* input;
  80. NSDictionary* textAttributes;
  81. NSDictionary* linkAttributes;
  82. };
  83. const TestCase kAllTestCases[] = {
  84. TestCase{
  85. @"Text without link.",
  86. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  87. @{NSLinkAttributeName : @"google.com"},
  88. },
  89. TestCase{
  90. @"Text with BEGIN_LINK and no end link.",
  91. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  92. @{NSLinkAttributeName : @"google.com"},
  93. },
  94. TestCase{
  95. @"Text with no begin link and END_LINK.",
  96. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  97. @{NSLinkAttributeName : @"google.com"},
  98. },
  99. TestCase{
  100. @"Text with END_LINK before BEGIN_LINK.",
  101. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  102. @{NSLinkAttributeName : @"google.com"},
  103. },
  104. };
  105. for (const TestCase& test_case : kAllTestCases) {
  106. EXPECT_CHECK_DEATH(AttributedStringFromStringWithLink(
  107. test_case.input, test_case.textAttributes, test_case.linkAttributes));
  108. }
  109. }
  110. TEST_F(StringUtilTest, AttributedStringFromStringWithLinkWithEmptyLink) {
  111. struct TestCase {
  112. NSString* input;
  113. NSDictionary* textAttributes;
  114. NSDictionary* linkAttributes;
  115. NSString* expectedString;
  116. };
  117. const TestCase test_case = TestCase {
  118. @"Text with empty link BEGIN_LINK END_LINK.",
  119. @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]},
  120. @{NSLinkAttributeName : @"google.com"}, @"Text with empty link .",
  121. };
  122. const NSAttributedString* result = AttributedStringFromStringWithLink(
  123. test_case.input, test_case.textAttributes, test_case.linkAttributes);
  124. EXPECT_NSEQ(result.string, test_case.expectedString);
  125. // Text attributes apply to the full range of the result string.
  126. NSRange textRange;
  127. NSDictionary* resultTextAttributes = [result attributesAtIndex:0
  128. effectiveRange:&textRange];
  129. EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, test_case.expectedString.length),
  130. textRange));
  131. EXPECT_NSEQ(test_case.textAttributes, resultTextAttributes);
  132. }
  133. TEST_F(StringUtilTest, ParseStringWithLinks) {
  134. struct TestCase {
  135. NSString* input;
  136. StringWithTags expected;
  137. };
  138. const TestCase kAllTestCases[] = {
  139. TestCase{
  140. @"Text without link.",
  141. StringWithTags{
  142. @"Text without link.",
  143. {},
  144. },
  145. },
  146. TestCase{
  147. @"Text with empty link BEGIN_LINK END_LINK.",
  148. StringWithTags{
  149. @"Text with empty link .",
  150. {NSRange{21, 0}},
  151. },
  152. },
  153. TestCase{
  154. @"Text with BEGIN_LINK and no end link.",
  155. StringWithTags{
  156. @"Text with BEGIN_LINK and no end link.",
  157. {},
  158. },
  159. },
  160. TestCase{
  161. @"Text with no begin link and END_LINK.",
  162. StringWithTags{
  163. @"Text with no begin link and END_LINK.",
  164. {},
  165. },
  166. },
  167. TestCase{@"Text with END_LINK before BEGIN_LINK.",
  168. StringWithTags{
  169. @"Text with END_LINK before BEGIN_LINK.",
  170. {},
  171. }},
  172. TestCase{
  173. @"Text with valid BEGIN_LINK link END_LINK and spaces.",
  174. StringWithTags{
  175. @"Text with valid link and spaces.",
  176. {NSRange{16, 4}},
  177. },
  178. },
  179. TestCase{
  180. @"Text with valid BEGIN_LINKlinkEND_LINK and no spaces.",
  181. StringWithTags{
  182. @"Text with valid link and no spaces.",
  183. {NSRange{16, 4}},
  184. },
  185. },
  186. TestCase{
  187. @"Text with multiple tags: BEGIN_LINKlink1END_LINK, "
  188. @"BEGIN_LINKlink2END_LINK and BEGIN_LINKlink3END_LINK.",
  189. StringWithTags{
  190. @"Text with multiple tags: link1, link2 and link3.",
  191. {NSRange{25, 5}, NSRange{32, 5}, NSRange{42, 5}},
  192. },
  193. },
  194. };
  195. for (const TestCase& test_case : kAllTestCases) {
  196. const StringWithTags result = ParseStringWithLinks(test_case.input);
  197. EXPECT_NSEQ(result.string, test_case.expected.string);
  198. ASSERT_EQ(result.ranges.size(), test_case.expected.ranges.size());
  199. for (size_t i = 0; i < test_case.expected.ranges.size(); i++) {
  200. EXPECT_EQ(result.ranges[i], test_case.expected.ranges[i]);
  201. }
  202. }
  203. }
  204. TEST_F(StringUtilTest, ParseStringWithTag) {
  205. struct TestCase {
  206. NSString* input;
  207. StringWithTag expected;
  208. };
  209. const TestCase kAllTestCases[] = {
  210. TestCase{
  211. @"Text without tag.",
  212. StringWithTag{
  213. @"Text without tag.",
  214. NSRange{NSNotFound, 0},
  215. },
  216. },
  217. TestCase{
  218. @"Text with empty tag BEGIN_TAG END_TAG.",
  219. StringWithTag{
  220. @"Text with empty tag .",
  221. NSRange{20, 1},
  222. },
  223. },
  224. TestCase{
  225. @"Text with BEGIN_TAG and no end tag.",
  226. StringWithTag{
  227. @"Text with BEGIN_TAG and no end tag.",
  228. NSRange{NSNotFound, 0},
  229. },
  230. },
  231. TestCase{
  232. @"Text with no begin tag and END_TAG.",
  233. StringWithTag{
  234. @"Text with no begin tag and END_TAG.",
  235. NSRange{NSNotFound, 0},
  236. },
  237. },
  238. TestCase{@"Text with END_TAG before BEGIN_TAG.",
  239. StringWithTag{
  240. @"Text with END_TAG before BEGIN_TAG.",
  241. NSRange{NSNotFound, 0},
  242. }},
  243. TestCase{
  244. @"Text with valid BEGIN_TAG tag END_TAG and spaces.",
  245. StringWithTag{
  246. @"Text with valid tag and spaces.",
  247. NSRange{16, 5},
  248. },
  249. },
  250. TestCase{
  251. @"Text with valid BEGIN_TAGtagEND_TAG and no spaces.",
  252. StringWithTag{
  253. @"Text with valid tag and no spaces.",
  254. NSRange{16, 3},
  255. },
  256. },
  257. };
  258. for (const TestCase& test_case : kAllTestCases) {
  259. const StringWithTag result =
  260. ParseStringWithTag(test_case.input, @"BEGIN_TAG", @"END_TAG");
  261. EXPECT_NSEQ(result.string, test_case.expected.string);
  262. EXPECT_EQ(result.range, test_case.expected.range);
  263. }
  264. }
  265. TEST_F(StringUtilTest, ParseStringWithTags) {
  266. struct TestCase {
  267. NSString* input;
  268. StringWithTags expected;
  269. };
  270. const TestCase kAllTestCases[] = {
  271. TestCase{
  272. @"Text without tag.",
  273. StringWithTags{
  274. @"Text without tag.",
  275. {},
  276. },
  277. },
  278. TestCase{
  279. @"Text with empty tag BEGIN_TAG END_TAG.",
  280. StringWithTags{
  281. @"Text with empty tag .",
  282. {NSRange{20, 1}},
  283. },
  284. },
  285. TestCase{
  286. @"Text with BEGIN_TAG and no end tag.",
  287. StringWithTags{
  288. @"Text with BEGIN_TAG and no end tag.",
  289. {},
  290. },
  291. },
  292. TestCase{
  293. @"Text with no begin tag and END_TAG.",
  294. StringWithTags{
  295. @"Text with no begin tag and END_TAG.",
  296. {},
  297. },
  298. },
  299. TestCase{@"Text with END_TAG before BEGIN_TAG.",
  300. StringWithTags{
  301. @"Text with END_TAG before BEGIN_TAG.",
  302. {},
  303. }},
  304. TestCase{
  305. @"Text with valid BEGIN_TAG tag END_TAG and spaces.",
  306. StringWithTags{
  307. @"Text with valid tag and spaces.",
  308. {NSRange{16, 5}},
  309. },
  310. },
  311. TestCase{
  312. @"Text with valid BEGIN_TAGtagEND_TAG and no spaces.",
  313. StringWithTags{
  314. @"Text with valid tag and no spaces.",
  315. {NSRange{16, 3}},
  316. },
  317. },
  318. TestCase{
  319. @"Text with multiple tags: BEGIN_TAGtag1END_TAG, "
  320. @"BEGIN_TAGtag2END_TAG and BEGIN_TAGtag3END_TAG.",
  321. StringWithTags{
  322. @"Text with multiple tags: tag1, tag2 and tag3.",
  323. {NSRange{25, 4}, NSRange{31, 4}, NSRange{40, 4}},
  324. },
  325. },
  326. };
  327. for (const TestCase& test_case : kAllTestCases) {
  328. const StringWithTags result =
  329. ParseStringWithTags(test_case.input, @"BEGIN_TAG", @"END_TAG");
  330. EXPECT_NSEQ(result.string, test_case.expected.string);
  331. ASSERT_EQ(result.ranges.size(), test_case.expected.ranges.size());
  332. for (size_t i = 0; i < test_case.expected.ranges.size(); i++) {
  333. EXPECT_EQ(result.ranges[i], test_case.expected.ranges[i]);
  334. }
  335. }
  336. }
  337. TEST_F(StringUtilTest, CleanNSStringForDisplay) {
  338. NSArray* const all_test_data = @[
  339. @{
  340. @"input" : @"Clean String",
  341. @"remove_graphic_chars" : @NO,
  342. @"expected" : @"Clean String"
  343. },
  344. @{
  345. @"input" : @" \t String with leading and trailing whitespaces ",
  346. @"remove_graphic_chars" : @NO,
  347. @"expected" : @"String with leading and trailing whitespaces"
  348. },
  349. @{
  350. @"input" : @" \n\n\r String with \n\n\r\n\r newline characters \n\n\r",
  351. @"remove_graphic_chars" : @NO,
  352. @"expected" : @"String with newline characters"
  353. },
  354. @{
  355. @"input" : @"String with an arrow ⟰ that remains intact",
  356. @"remove_graphic_chars" : @NO,
  357. @"expected" : @"String with an arrow ⟰ that remains intact"
  358. },
  359. @{
  360. @"input" : @"String with an arrow ⟰ that gets cleaned up",
  361. @"remove_graphic_chars" : @YES,
  362. @"expected" : @"String with an arrow that gets cleaned up"
  363. },
  364. ];
  365. for (NSDictionary* test_data : all_test_data) {
  366. NSString* input_text = test_data[@"input"];
  367. NSString* expected_text = test_data[@"expected"];
  368. BOOL remove_graphic_chars = [test_data[@"remove_graphic_chars"] boolValue];
  369. EXPECT_NSEQ(expected_text,
  370. CleanNSStringForDisplay(input_text, remove_graphic_chars));
  371. }
  372. }
  373. TEST_F(StringUtilTest, SubstringOfWidth) {
  374. // returns nil for zero-length strings
  375. EXPECT_NSEQ(SubstringOfWidth(@"", @{}, 100, NO), nil);
  376. EXPECT_NSEQ(SubstringOfWidth(nil, @{}, 100, NO), nil);
  377. // This font should always exist
  378. UIFont* sys_font = [UIFont systemFontOfSize:18.0f];
  379. NSDictionary* attributes = @{NSFontAttributeName : sys_font};
  380. EXPECT_NSEQ(SubstringOfWidth(@"asdf", attributes, 100, NO), @"asdf");
  381. // construct some string of known lengths
  382. NSString* leading = @"some text";
  383. NSString* trailing = @"some other text";
  384. NSString* mid = @"some text for the method to do some actual work";
  385. NSString* long_string =
  386. [[leading stringByAppendingString:mid] stringByAppendingString:trailing];
  387. CGFloat leading_width = [leading sizeWithAttributes:attributes].width;
  388. CGFloat trailing_width = [trailing sizeWithAttributes:attributes].width;
  389. NSString* leading_calculated =
  390. SubstringOfWidth(long_string, attributes, leading_width, NO);
  391. EXPECT_NSEQ(leading, leading_calculated);
  392. NSString* trailing_calculated =
  393. SubstringOfWidth(long_string, attributes, trailing_width, YES);
  394. EXPECT_NSEQ(trailing, trailing_calculated);
  395. }
  396. // Verifies when it should return CGRectNull and when it shouldn't.
  397. TEST_F(StringUtilTest, TextViewLinkBound) {
  398. UITextView* text_view = CreateUITextViewWithTextKit1();
  399. text_view.text = @"Some text.";
  400. // Returns CGRectNull for empty NSRange.
  401. EXPECT_TRUE(CGRectEqualToRect(
  402. TextViewLinkBound(text_view, NSMakeRange(-1, -1)), CGRectNull));
  403. // Returns CGRectNull for a range out of bound.
  404. EXPECT_TRUE(CGRectEqualToRect(
  405. TextViewLinkBound(text_view, NSMakeRange(20, 5)), CGRectNull));
  406. // Returns a CGRect diffent from CGRectNull when there is a range in bound.
  407. EXPECT_FALSE(CGRectEqualToRect(
  408. TextViewLinkBound(text_view, NSMakeRange(0, 5)), CGRectNull));
  409. }
  410. } // namespace