| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| |
| import 'common.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() { |
| setUpCanvasKitTest(); |
| |
| group('$fragmentUsingIntlSegmenter', () { |
| test('fragments text into words', () { |
| final Uint32List breaks = fragmentUsingIntlSegmenter( |
| 'Hello world 你好世界', |
| IntlSegmenterGranularity.word, |
| ); |
| expect( |
| breaks, |
| orderedEquals(<int>[0, 5, 6, 11, 12, 14, 16]), |
| ); |
| }); |
| |
| test('fragments multi-line text into words', () { |
| final Uint32List breaks = fragmentUsingIntlSegmenter( |
| 'Lorem ipsum\ndolor 你好世界 sit\namet', |
| IntlSegmenterGranularity.word, |
| ); |
| expect( |
| breaks, |
| orderedEquals(<int>[ |
| 0, 5, 6, 11, 12, // "Lorem ipsum\n" |
| 17, 18, 20, 22, 23, 26, 27, // "dolor 你好世界 sit\n" |
| 31, // "amet" |
| ]), |
| ); |
| }); |
| |
| test('fragments text into grapheme clusters', () { |
| // The smiley emoji has a length of 2. |
| // The family emoji has a length of 11. |
| final Uint32List breaks = fragmentUsingIntlSegmenter( |
| 'Lorem🙂ipsum👨👩👧👦', |
| IntlSegmenterGranularity.grapheme, |
| ); |
| expect( |
| breaks, |
| orderedEquals(<int>[ |
| 0, 1, 2, 3, 4, 5, 7, // "Lorem🙂" |
| 8, 9, 10, 11, 12, 23, // "ipsum👨👩👧👦" |
| ]), |
| ); |
| }); |
| |
| test('fragments multi-line text into grapheme clusters', () { |
| // The smiley emojis have a length of 2 each. |
| // The family emoji has a length of 11. |
| final Uint32List breaks = fragmentUsingIntlSegmenter( |
| 'Lorem🙂\nipsum👨👩👧👦dolor\n😄', |
| IntlSegmenterGranularity.grapheme, |
| ); |
| expect( |
| breaks, |
| orderedEquals(<int>[ |
| 0, 1, 2, 3, 4, 5, 7, 8, // "Lorem🙂\n" |
| 9, 10, 11, 12, 13, 24, // "ipsum👨👩👧👦" |
| 25, 26, 27, 28, 29, 30, 32, // "dolor😄\n" |
| ]), |
| ); |
| }); |
| }, skip: !browserSupportsCanvaskitChromium); |
| |
| group('$fragmentUsingV8LineBreaker', () { |
| const int kSoft = 0; |
| const int kHard = 1; |
| |
| test('fragments text into soft and hard line breaks', () { |
| final Uint32List breaks = fragmentUsingV8LineBreaker( |
| 'Lorem-ipsum 你好🙂\nDolor sit', |
| ); |
| expect( |
| breaks, |
| orderedEquals(<int>[ |
| 0, kSoft, |
| 6, kSoft, // "Lorem-" |
| 12, kSoft, // "ipsum " |
| 13, kSoft, // "你" |
| 14, kSoft, // "好" |
| 17, kHard, // "🙂\n" |
| 23, kSoft, // "Dolor " |
| 26, kSoft, // "sit" |
| ]), |
| ); |
| }); |
| }, skip: !browserSupportsCanvaskitChromium); |
| |
| group('segmentText', () { |
| setUp(() { |
| segmentationCache.clear(); |
| }); |
| |
| tearDown(() { |
| segmentationCache.clear(); |
| }); |
| |
| test('segments correctly', () { |
| const String text = 'Lorem-ipsum 你好🙂\nDolor sit'; |
| final SegmentationResult segmentation = segmentText(text); |
| expect( |
| segmentation.words, |
| fragmentUsingIntlSegmenter(text, IntlSegmenterGranularity.word), |
| ); |
| expect( |
| segmentation.graphemes, |
| fragmentUsingIntlSegmenter(text, IntlSegmenterGranularity.grapheme), |
| ); |
| expect( |
| segmentation.breaks, |
| fragmentUsingV8LineBreaker(text), |
| ); |
| }); |
| |
| test('caches segmentation results in LRU fashion', () { |
| const String text1 = 'hello'; |
| segmentText(text1); |
| expect(segmentationCache.small.debugItemQueue, hasLength(1)); |
| expect(segmentationCache.small[text1], isNotNull); |
| |
| const String text2 = 'world'; |
| segmentText(text2); |
| expect(segmentationCache.small.debugItemQueue, hasLength(2)); |
| expect(segmentationCache.small[text2], isNotNull); |
| |
| // "world" was segmented last, so it should be first, as in most recently used. |
| expect(segmentationCache.small.debugItemQueue.first.key, 'world'); |
| expect(segmentationCache.small.debugItemQueue.last.key, 'hello'); |
| }); |
| |
| test('puts segmentation results in the appropriate cache', () { |
| final String smallText = 'a' * (kSmallParagraphCacheSpec.maxTextLength - 1); |
| segmentText(smallText); |
| expect(segmentationCache.small.debugItemQueue, hasLength(1)); |
| expect(segmentationCache.medium.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.large.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.small[smallText], isNotNull); |
| segmentationCache.clear(); |
| |
| final String mediumText = 'a' * (kMediumParagraphCacheSpec.maxTextLength - 1); |
| segmentText(mediumText); |
| expect(segmentationCache.small.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.medium.debugItemQueue, hasLength(1)); |
| expect(segmentationCache.large.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.medium[mediumText], isNotNull); |
| segmentationCache.clear(); |
| |
| final String largeText = 'a' * (kLargeParagraphCacheSpec.maxTextLength - 1); |
| segmentText(largeText); |
| expect(segmentationCache.small.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.medium.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.large.debugItemQueue, hasLength(1)); |
| expect(segmentationCache.large[largeText], isNotNull); |
| segmentationCache.clear(); |
| |
| // Should not cache extremely large texts. |
| final String tooLargeText = 'a' * (kLargeParagraphCacheSpec.maxTextLength + 1); |
| segmentText(tooLargeText); |
| expect(segmentationCache.small.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.medium.debugItemQueue, hasLength(0)); |
| expect(segmentationCache.large.debugItemQueue, hasLength(0)); |
| segmentationCache.clear(); |
| }); |
| |
| test('has a limit on the number of entries', () { |
| testCacheCapacity(segmentationCache.small, kSmallParagraphCacheSpec); |
| testCacheCapacity(segmentationCache.medium, kMediumParagraphCacheSpec); |
| testCacheCapacity(segmentationCache.large, kLargeParagraphCacheSpec); |
| }, timeout: const Timeout.factor(4)); |
| }, skip: !browserSupportsCanvaskitChromium); |
| } |
| |
| void testCacheCapacity( |
| LruCache<String, SegmentationResult> cache, |
| SegmentationCacheSpec spec, |
| ) { |
| // 1. Fill the cache. |
| for (int i = 0; i < spec.cacheSize; i++) { |
| final String text = _randomString(spec.maxTextLength); |
| segmentText(text); |
| // The segmented text should have been added to the cache. |
| // TODO(mdebbar): This may fail if the random string generator generates |
| // the same string twice. |
| expect(cache.debugItemQueue, hasLength(i + 1)); |
| } |
| |
| // 2. Make sure the cache is full. |
| expect(cache.length, spec.cacheSize); |
| |
| // 3. Add more items to the cache. |
| for (int i = 0; i < 10; i++) { |
| final String text = _randomString(spec.maxTextLength); |
| segmentText(text); |
| // The cache size should remain the same. |
| expect(cache.debugItemQueue, hasLength(spec.cacheSize)); |
| } |
| |
| // 4. Clear the cache. |
| cache.clear(); |
| } |
| |
| int _seed = 0; |
| String _randomString(int length) { |
| const String allChars = ' 1234567890' |
| 'abcdefghijklmnopqrstuvwxyz' |
| 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
| |
| final String text = '*' * length; |
| return text.replaceAllMapped( |
| '*', |
| // Passing a seed so the results are reproducible. |
| (_) => allChars[Random(_seed++).nextInt(allChars.length)], |
| ); |
| } |