Files
flutter-go/packages/flutter_web_ui/test/text/measurement_test.dart
2019-08-13 20:38:46 +08:00

446 lines
16 KiB
Dart

// Copyright 2018 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.
import 'package:flutter_web_ui/ui.dart' as ui;
import 'package:flutter_web_ui/src/engine.dart';
import 'package:flutter_web_test/flutter_web_test.dart';
final ui.ParagraphStyle ahemStyle = ui.ParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
);
const ui.ParagraphConstraints constraints = ui.ParagraphConstraints(width: 50);
const ui.ParagraphConstraints infiniteConstraints =
ui.ParagraphConstraints(width: double.infinity);
ui.Paragraph build(ui.ParagraphStyle style, String text) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(style);
builder.addText(text);
return builder.build();
}
typedef MeasurementTestBody = void Function(TextMeasurementService instance);
/// Runs the same test twice - once with dom measurement and once with canvas
/// measurement.
void testMeasurements(String description, MeasurementTestBody body) {
test(
'$description (dom)',
() => body(TextMeasurementService.domInstance),
);
test(
'$description (canvas)',
() => body(TextMeasurementService.canvasInstance),
);
}
void main() {
group('$RulerManager', () {
final ui.ParagraphStyle s1 = ui.ParagraphStyle(fontFamily: 'sans-serif');
final ui.ParagraphStyle s2 = ui.ParagraphStyle(
fontWeight: ui.FontWeight.bold,
);
final ui.ParagraphStyle s3 = ui.ParagraphStyle(fontSize: 22.0);
ParagraphGeometricStyle style1, style2, style3;
ui.Paragraph style1Text1, style1Text2; // two paragraphs sharing style
ui.Paragraph style2Text1, style3Text3;
setUp(() {
style1Text1 = build(s1, '1');
style1Text2 = build(s1, '2');
style2Text1 = build(s2, '1');
style3Text3 = build(s3, '3');
style1 = style1Text1.webOnlyGetParagraphGeometricStyle();
style2 = style2Text1.webOnlyGetParagraphGeometricStyle();
style3 = style3Text3.webOnlyGetParagraphGeometricStyle();
final ParagraphGeometricStyle style1_2 =
style1Text2.webOnlyGetParagraphGeometricStyle();
expect(style1_2, style1); // styles must be equal despite different text
});
test('caches rulers', () {
final RulerManager rulerManager = RulerManager(rulerCacheCapacity: 2);
ParagraphRuler ruler1, ruler2, ruler3;
expect(rulerManager.rulerCacheCapacity, 2);
expect(rulerManager.rulers.length, 0);
// First ruler cached
ruler1 = rulerManager.findOrCreateRuler(style1);
expect(rulerManager.rulers.length, 1);
expect(ruler1.hitCount, 1);
// Increase hit count for style 1
ruler1 = rulerManager.findOrCreateRuler(style1);
expect(rulerManager.rulers.length, 1);
expect(ruler1.hitCount, 2);
// Previous ruler reused
rulerManager.findOrCreateRuler(style1);
expect(rulerManager.rulers.length, 1);
expect(ruler1.hitCount, 3);
// Second ruler created and cached
ruler2 = rulerManager.findOrCreateRuler(style2);
expect(rulerManager.rulers.length, 2);
expect(ruler1.hitCount, 3);
expect(ruler2.hitCount, 1);
// Increase hit count for style 2
rulerManager.findOrCreateRuler(style2);
rulerManager.findOrCreateRuler(style2);
rulerManager.findOrCreateRuler(style2);
expect(rulerManager.rulers.length, 2);
expect(ruler2.hitCount, 4);
// Third ruler cached: it is ok to store more rulers that the cache
// capacity because the cache is cleaned-up at the next microtask.
ruler3 = rulerManager.findOrCreateRuler(style3);
// Final ruler states
expect(rulerManager.rulers.length, 3);
expect(ruler1.hitCount, 3);
expect(ruler2.hitCount, 4);
expect(ruler3.hitCount, 1);
// The least hit ruler isn't disposed yet.
expect(ruler3.debugIsDisposed, isFalse);
// Cleaning up the cache should bring its size down to capacity limit.
rulerManager.cleanUpRulerCache();
expect(rulerManager.rulers.length, 2);
expect(rulerManager.rulers, containsValue(ruler1)); // retained
expect(rulerManager.rulers, containsValue(ruler2)); // retained
expect(rulerManager.rulers, isNot(containsValue(ruler3))); // evicted
expect(ruler1.debugIsDisposed, isFalse);
expect(ruler2.debugIsDisposed, isFalse);
expect(ruler3.debugIsDisposed, isTrue);
ruler1 = rulerManager.rulers[style1];
expect(ruler1.style, style1);
expect(ruler1.hitCount, 0); // hit counts are reset
ruler2 = rulerManager.rulers[style2];
expect(ruler2.style, style2);
expect(ruler2.hitCount, 0); // hit counts are reset
});
});
group('$TextMeasurementService', () {
setUp(() {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
});
tearDown(() {
TextMeasurementService.clearCache();
});
testMeasurements(
'preserves whitespace when measuring',
(TextMeasurementService instance) {
ui.Paragraph text;
MeasurementResult result;
// leading whitespaces
text = build(ahemStyle, ' abc');
result = instance.measure(text, infiniteConstraints);
expect(result.maxIntrinsicWidth, 60);
expect(result.minIntrinsicWidth, 30);
expect(result.height, 10);
// trailing whitespaces
text = build(ahemStyle, 'abc ');
result = instance.measure(text, infiniteConstraints);
expect(result.maxIntrinsicWidth, 60);
expect(result.minIntrinsicWidth, 30);
expect(result.height, 10);
// mixed whitespaces
text = build(ahemStyle, ' ab c ');
result = instance.measure(text, infiniteConstraints);
expect(result.maxIntrinsicWidth, 100);
expect(result.minIntrinsicWidth, 20);
expect(result.height, 10);
// single whitespace
text = build(ahemStyle, ' ');
result = instance.measure(text, infiniteConstraints);
expect(result.maxIntrinsicWidth, 10);
expect(result.minIntrinsicWidth, 0);
expect(result.height, 10);
// whitespace only
text = build(ahemStyle, ' ');
result = instance.measure(text, infiniteConstraints);
expect(result.maxIntrinsicWidth, 50);
expect(result.minIntrinsicWidth, 0);
expect(result.height, 10);
},
);
testMeasurements(
'uses single-line when text can fit without wrapping',
(TextMeasurementService instance) {
final MeasurementResult result =
instance.measure(build(ahemStyle, '12345'), constraints);
// Should fit on a single line.
expect(result.isSingleLine, true);
expect(result.maxIntrinsicWidth, 50);
expect(result.minIntrinsicWidth, 50);
expect(result.width, 50);
expect(result.height, 10);
},
);
testMeasurements(
'uses multi-line for long text',
(TextMeasurementService instance) {
final MeasurementResult result =
instance.measure(build(ahemStyle, '1234567890'), constraints);
// The long text doesn't fit in 50px of width, so it needs to wrap.
expect(result.isSingleLine, false);
expect(result.maxIntrinsicWidth, 100);
expect(result.minIntrinsicWidth, 100);
expect(result.width, 50);
expect(result.height, 20);
},
);
testMeasurements(
'uses multi-line for text that contains new-line',
(TextMeasurementService instance) {
final MeasurementResult result =
instance.measure(build(ahemStyle, '12\n34'), constraints);
// Text containing newlines should always be drawn in multi-line mode.
expect(result.isSingleLine, false);
expect(result.maxIntrinsicWidth, 20);
expect(result.minIntrinsicWidth, 20);
expect(result.width, 50);
expect(result.height, 20);
},
);
testMeasurements('empty lines', (TextMeasurementService instance) {
MeasurementResult result;
// Empty lines in the beginning.
result = instance.measure(build(ahemStyle, '\n\n1234'), constraints);
expect(result.maxIntrinsicWidth, 40);
expect(result.minIntrinsicWidth, 40);
expect(result.height, 30);
// Empty lines in the middle.
result = instance.measure(build(ahemStyle, '12\n\n345'), constraints);
expect(result.maxIntrinsicWidth, 30);
expect(result.minIntrinsicWidth, 30);
expect(result.height, 30);
// This can only be done correctly in the canvas-based implementation.
if (instance is CanvasTextMeasurementService) {
// Empty lines in the end.
result = instance.measure(build(ahemStyle, '1234\n\n'), constraints);
expect(result.maxIntrinsicWidth, 40);
expect(result.minIntrinsicWidth, 40);
expect(result.height, 30);
}
});
test('takes letter spacing into account', () {
const ui.ParagraphConstraints constraints =
ui.ParagraphConstraints(width: 100);
final ui.ParagraphBuilder normalBuilder = ui.ParagraphBuilder(ahemStyle);
normalBuilder.addText('abc');
final ui.Paragraph normalText = normalBuilder.build();
final ui.ParagraphBuilder spacedBuilder = ui.ParagraphBuilder(ahemStyle);
spacedBuilder.pushStyle(ui.TextStyle(letterSpacing: 1.5));
spacedBuilder.addText('abc');
final ui.Paragraph spacedText = spacedBuilder.build();
// Letter spacing is only supported via DOM measurement.
final TextMeasurementService instance =
TextMeasurementService.forParagraph(spacedText);
expect(instance, isInstanceOf<DomTextMeasurementService>());
final MeasurementResult normalResult =
instance.measure(normalText, constraints);
final MeasurementResult spacedResult =
instance.measure(spacedText, constraints);
expect(
normalResult.maxIntrinsicWidth < spacedResult.maxIntrinsicWidth,
isTrue,
);
});
test('takes word spacing into account', () {
const ui.ParagraphConstraints constraints =
ui.ParagraphConstraints(width: 100);
final ui.ParagraphBuilder normalBuilder = ui.ParagraphBuilder(ahemStyle);
normalBuilder.addText('a b c');
final ui.Paragraph normalText = normalBuilder.build();
final ui.ParagraphBuilder spacedBuilder = ui.ParagraphBuilder(ahemStyle);
spacedBuilder.pushStyle(ui.TextStyle(wordSpacing: 1.5));
spacedBuilder.addText('a b c');
final ui.Paragraph spacedText = spacedBuilder.build();
// Word spacing is only supported via DOM measurement.
final TextMeasurementService instance =
TextMeasurementService.forParagraph(spacedText);
expect(instance, isInstanceOf<DomTextMeasurementService>());
final MeasurementResult normalResult =
instance.measure(normalText, constraints);
final MeasurementResult spacedResult =
instance.measure(spacedText, constraints);
expect(
normalResult.maxIntrinsicWidth < spacedResult.maxIntrinsicWidth,
isTrue,
);
});
testMeasurements('minIntrinsicWidth', (TextMeasurementService instance) {
MeasurementResult result;
// Simple case.
result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints);
expect(result.minIntrinsicWidth, 40);
// With new lines.
result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints);
expect(result.minIntrinsicWidth, 40);
// With trailing whitespace.
result = instance.measure(build(ahemStyle, 'abcd efg'), constraints);
expect(result.minIntrinsicWidth, 40);
// With trailing whitespace and new lines.
result = instance.measure(build(ahemStyle, 'abc \ndefg'), constraints);
expect(result.minIntrinsicWidth, 40);
// Very long text.
result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints);
expect(result.minIntrinsicWidth, 120);
});
testMeasurements('maxIntrinsicWidth', (TextMeasurementService instance) {
MeasurementResult result;
// Simple case.
result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints);
expect(result.maxIntrinsicWidth, 110);
// With new lines.
result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints);
expect(result.maxIntrinsicWidth, 40);
// With long whitespace.
result = instance.measure(build(ahemStyle, 'abcd efg'), constraints);
expect(result.maxIntrinsicWidth, 100);
// With trailing whitespace.
result = instance.measure(build(ahemStyle, 'abc def '), constraints);
expect(result.maxIntrinsicWidth, 100);
// With trailing whitespace and new lines.
result = instance.measure(build(ahemStyle, 'abc \ndef '), constraints);
expect(result.maxIntrinsicWidth, 60);
// Very long text.
result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints);
expect(result.maxIntrinsicWidth, 120);
});
testMeasurements(
'respects text overflow',
(TextMeasurementService instance) {
final ui.ParagraphStyle overflowStyle = ui.ParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
ellipsis: '...',
);
MeasurementResult result;
// The text shouldn't be broken into multiple lines, so the height should
// be equal to a height of a single line.
final ui.Paragraph longText = build(
overflowStyle,
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
);
result = instance.measure(longText, constraints);
expect(result.minIntrinsicWidth, 480);
expect(result.maxIntrinsicWidth, 480);
expect(result.height, 10);
// The short prefix should make the text break into two lines, but the
// second line should remain unbroken.
final ui.Paragraph longTextShortPrefix = build(
overflowStyle,
'AAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
);
result = instance.measure(longTextShortPrefix, constraints);
expect(result.minIntrinsicWidth, 450);
expect(result.maxIntrinsicWidth, 450);
expect(result.height, 20);
// The first line is overflowing so we should stop the measurement there
// and there should be no second line (the short suffix shouldn't be rendered).
final ui.Paragraph longTextShortSuffix = build(
overflowStyle,
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAA',
);
result = instance.measure(longTextShortSuffix, constraints);
expect(result.minIntrinsicWidth, 450);
expect(result.maxIntrinsicWidth, 450);
// This can only be done correctly in the canvas-based implementation.
if (instance is CanvasTextMeasurementService) {
expect(result.height, 10);
}
},
);
testMeasurements('respects max lines', (TextMeasurementService instance) {
final ui.ParagraphStyle maxlinesStyle = ui.ParagraphStyle(
fontFamily: 'ahem',
fontSize: 10,
maxLines: 2,
);
MeasurementResult result;
// The height should be that of a single line.
final ui.Paragraph oneline = build(maxlinesStyle, 'One line');
result = instance.measure(oneline, infiniteConstraints);
expect(result.height, 10);
// The height should respect max lines and be limited to two lines here.
final ui.Paragraph threelines =
build(maxlinesStyle, 'First\nSecond\nThird');
result = instance.measure(threelines, infiniteConstraints);
expect(result.height, 20);
});
test('canvas line breaks', () {
// TODO(mdebbar): Add tests and make sure to cover the following edge cases:
// 1. First chunk already overflows the width constraint.
// 2. maxIntrinsicWidth in the presence of mandatory line breaks.
// 3. minIntrinsicWidth in the presence of optional line breaks.
// 4. empty lines in the middle.
// 5. long text with a short prefix.
// 6. long text with a short suffix.
// 7. whitespace at end of line (shouldn't count towards minIntWidth, but counts towards max).
// 8. long whitespace should still cause line break if not enough space for it.
});
});
}