blob: 3c43b94e41f8ace88c517ff991b11a9cc1ab496b [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package UnitTest.Tests
{
import UnitTest.ExtendedClasses.TestDescriptor;
import UnitTest.ExtendedClasses.TestSuiteExtended;
import UnitTest.ExtendedClasses.VellumTestCase;
import UnitTest.Fixtures.TestConfig;
import flash.display.DisplayObjectContainer;
import flash.geom.Rectangle;
import flash.text.engine.TextLine;
import flash.text.engine.TextLineValidity;
import flashx.textLayout.*;
import flashx.textLayout.compose.IFlowComposer;
import flashx.textLayout.compose.StandardFlowComposer;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.edit.TextScrap;
import flashx.textLayout.elements.*;
import flashx.textLayout.events.CompositionCompleteEvent;
import flashx.textLayout.factory.StringTextLineFactory;
import flashx.textLayout.factory.TruncationOptions;
import flashx.textLayout.formats.BlockProgression;
import flashx.textLayout.formats.LineBreak;
import flashx.textLayout.formats.TextAlign;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.formats.VerticalAlign;
use namespace tlf_internal;
import mx.utils.UIDUtil;
import flashx.textLayout.edit.EditManager;
import flashx.textLayout.factory.StringTextLineFactory;
import flashx.textLayout.compose.TextFlowLine;
import mx.core.Container;
import flash.display.Sprite;
import flashx.textLayout.formats.ITextLayoutFormat;
public class CompositionTest extends VellumTestCase
{
public function CompositionTest(methodName:String, testID:String, testConfig:TestConfig, testXML:XML = null)
{
super (methodName, testID, testConfig);
if (methodName != "resizeController2644361")
TestData.fileName = "asknot.xml";
else
addDefaultTestSettings = false;
// Note: These must correspond to a Watson product area (case-sensitive)
metaData.productArea = "Text Composition";
}
public static function suite(testConfig:TestConfig, ts:TestSuiteExtended):void
{
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "checkParagraphShufflingTest", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "partialCompositionTest", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "releasedLineTest", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "composeOneScreen", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "truncationTest", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "CompositionCompleteEventTest", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "scrolledRedrawPartialCompose", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "multipleContainersWithPadding", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "deleteAtContainerStart", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "resizeController2644361", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "resizeEmptyController", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "emptyController", testConfig ) );
ts.addTestDescriptor( new TestDescriptor ( CompositionTest, "contentBoundsOnComposeFromMiddle", testConfig ) );
}
/**
* First, find two back to back paragraphs. Second, record the first line of the
* second paragraph; if the first paragraph is changed and the second gets recomposed
* (i.e. what we don't want) this line will be re-created (also, the first line of
* the second paragraph is the easiest to find). Third, make an insertion
* point at the end of the first paragraph. Fourth, place a bunch of text at the end
* of the paragraph to force it to recompose. Finally, find the first line in the
* second paragraph again and see if it is the same as the line you recorded in step
* (using "===").
*/
public function checkParagraphShufflingTest():void
{
var startLength:int = TestFrame.rootElement.textLength;
var flow1:FlowElement;
var flow2:FlowElement;
//Look for two back to back paragraphs.
for(var i:int = 0; i < TestFrame.rootElement.numChildren-1; i++){
flow1 = TestFrame.rootElement.getChildAt(i);
flow2 = TestFrame.rootElement.getChildAt(i+1);
if(flow1 is ParagraphElement && flow2 is ParagraphElement){
break;
}
}
assertTrue("either flow1 or flow2 are null", flow1 != null && flow2 != null);
var para1:ParagraphElement = flow1 as ParagraphElement;
var para2:ParagraphElement = flow2 as ParagraphElement;
var lines:Array = StandardFlowComposer(SelManager.textFlow.flowComposer).lines;
var refLine:Object;
for each (var line:TextFlowLine in lines){
if(line.paragraph == para2){
refLine = line;
break;
}
}
var para1end:int = para1.textLength - 1;
SelManager.selectRange(para1end,para1end);
var longString:String = "Far be it from me to interrupt such an important " +
"discussion, but it's come to my attention that the behavior of " +
"line shuffling has yet to be fully investigated within this context. " +
"So please allow me but a few lines with which to test whether or not " +
"the aforementioned is indeed working. Thank you.";
SelManager.insertText(longString);
SelManager.flushPendingOperations();
lines = StandardFlowComposer(SelManager.textFlow.flowComposer).lines;
for each (var line2:TextFlowLine in lines){
if(line2.paragraph == para2){
assertTrue("the next paragraph got recomposed instead of shuffling", line2 === refLine);
break;
}
}
}
/**
* This very complicated test inserts some text in the middle of the flow after
* determining which lines will be affected by the change (in terms of which
* will need to recompose). It then checks to see if only those that should
* be effected by the change have been changed.
*/
public function partialCompositionTest():void
{
var lines:Array = StandardFlowComposer(SelManager.textFlow.flowComposer).lines;
var linenum:int = lines.length / 2;
var initLength:int = lines.length;
var good:Boolean = false;
for(var i:int = 0; i < lines.length - 1; i++){
if(
(lines[linenum + i] as TextFlowLine).paragraph ==
(lines[linenum + i + 1] as TextFlowLine).paragraph
){
good = true;
linenum = linenum + i;
break;
}
}
if(!good){
for(var j:int = 0; j > 1; j--){
if(
(lines[linenum - j] as TextFlowLine).paragraph ==
(lines[linenum - j - 1] as TextFlowLine).paragraph
){
good = true;
linenum = linenum - j;
break;
}
}
}
if(!good){
fail("No starting place could be found");
}
//Register all the lines that shouldn't be damaged.
var undamagedUIDs:Array = new Array();
for(var k:int = 0; k < linenum; k++){
undamagedUIDs[k] = UIDUtil.getUID(lines[k]);
}
for(var l:int = lines.length - 1;
l > linenum &&
(lines[l] as TextFlowLine).paragraph != (lines[linenum] as TextFlowLine).paragraph;
l--)
{
undamagedUIDs[l] = UIDUtil.getUID(lines[l]);
}
//Register all the lines that should be damaged.
var damagedUIDs:Array = new Array();
for(var n:int = linenum;
n < lines.length &&
(lines[n] as TextFlowLine).paragraph != null &&
(lines[n] as TextFlowLine).paragraph == (lines[linenum] as TextFlowLine).paragraph;
n++)
{
damagedUIDs[n] = UIDUtil.getUID(lines[n]);
}
var lineToDamage:TextFlowLine = lines[linenum] as TextFlowLine;
var ip:int = lineToDamage.absoluteStart + lineToDamage.textLength;
SelManager.selectRange(ip,ip+9);
var longString:String = "Line Break";
SelManager.insertText(longString);
SelManager.flushPendingOperations();
for(var m:int = 0; m < initLength; m++){
var UID:String = undamagedUIDs[m];
if(UID != null){
assertTrue("Expected line " + m + " not to recompose." +
" Break was at " + linenum + ".",
UID == UIDUtil.getUID(lines[m])
);
}else{
UID = damagedUIDs[m];
assertTrue("Expected line " + m + " to recompose." +
" Break was at " + linenum + ".",
UID != UIDUtil.getUID(lines[m])
);
}
}
}
private function createLineSummary(flowComposer:IFlowComposer):Object
{
// Lines that are referenced should go first
var releasedLineCount:int = 0;
var invalidLineCount:int = 0;
var validLineCount:int = 0;
var parentedLineCount:int = 0;
var nonexistentLineCount:int = 0;
var lineIndex:int = 0;
while (lineIndex < flowComposer.numLines)
{
var line:TextFlowLine = flowComposer.getLineAt(lineIndex);
if (line.validity == TextLineValidity.VALID)
{
assertTrue("Expecting valid referenced lines before invalid lines", invalidLineCount == 0);
var textLine:TextLine = line.peekTextLine();
assertTrue(!textLine || textLine.userData == line, "TextLine userData doesn't point back to TextFlowLine");
if (!textLine || !textLine.textBlock || textLine.textBlock.firstLine == null)
releasedLineCount++;
else if (textLine.parent)
parentedLineCount++;
else if (textLine.validity == TextLineValidity.VALID)
validLineCount++;
else assertTrue(false, "Found damaged unreleased TextLine for valid TextFlowLine");
}
else
invalidLineCount++;
lineIndex++;
}
var result:Object = new Object();
result["releasedLineCount"] = releasedLineCount;
result["invalidLineCount"] = invalidLineCount;
result["validLineCount"] = validLineCount;
result["parentedLineCount"] = parentedLineCount;
result["nonexistentLineCount"] = nonexistentLineCount;
return result;
}
// For benchmark: read in Alice and display one screenfull
public function composeOneScreen():void
{
loadTestFile("aliceExcerpt.xml");
}
// Tests that lines that aren't in view are released, and that composition didn't run to the end
public function releasedLineTest():void
{
loadTestFile("aliceExcerpt.xml");
var flowComposer:IFlowComposer = SelManager.textFlow.flowComposer;
assertTrue("Composed to the end, should leave text that is not in view uncomposed", flowComposer.damageAbsoluteStart < SelManager.textFlow.textLength);
var controller:ContainerController = flowComposer.getControllerAt(0);
var originalEstimatedHeight:Number = controller.contentHeight;
controller.verticalScrollPosition += 500; // scroll ahead so we have some lines generated that can be released
var lineSummary:Object = createLineSummary(flowComposer);
assertTrue("Expected some invalid lines -- composition not complete", lineSummary["invalidLineCount"] > 0);
// NOTE: Released lines not in view can be garbage collected. This assertion is not necessarily valid.
assertTrue("Expected some released lines -- not all lines in view", lineSummary["releasedLineCount"] > 0);
assertTrue("Expected some valid and parented lines", lineSummary["parentedLineCount"] > 0);
// This will force composition
flowComposer.composeToPosition();
var actualContentHeight:Number = controller.contentHeight;
assertTrue("Expected full compose", flowComposer.damageAbsoluteStart == SelManager.textFlow.textLength);
var afterFullCompose:Object = createLineSummary(flowComposer);
assertTrue("Expected no invalid lines -- composition complete", afterFullCompose["invalidLineCount"] == 0);
assertTrue("Expected estimated is correct after full composition!", flowComposer.getControllerAt(0).contentHeight == actualContentHeight);
/* Can't seem to get gc to release the textlines, although they get released when run through the profiler.
var eventCount:int = 0;
System.gc();System.gc();
var sprite:Sprite = Sprite(flowComposer.getControllerAt(0).container);
sprite.stage.addEventListener(Event.ENTER_FRAME, checkSummary);
// Wait for next enterFrame event, because gc is delayed
function checkSummary():void
{
if (eventCount > 50)
{
var afterGC:Object = createLineSummary(flowComposer);
// Test that lines are really getting gc'd
assertTrue("Expected lines to be gc'd!", afterGC["nonexistentLineCount"] > lineSummary["nonexistentLineCount"]);
assertTrue("Released lines expected 0", afterGC["releasedLineCount"] == 0);
sprite.stage.removeEventListener(Event.ENTER_FRAME, checkSummary);
}
System.gc();System.gc();
++eventCount;
} */
}
private var _lines:Array;
private var _textLen:int;
private function truncationTestCallback(textLine:TextLine):void
{
_textLen += textLine.rawTextLength;
_lines.push(textLine);
}
public function truncationTest():void
{
var bounds:Rectangle = new Rectangle();
var text:String = 'There are many such lime-kilns in that tract of country, for the purpose of burning the white marble which composes a large part of the substance of the hills. ' +
'Some of them, built years ago, and long deserted, with weeds growing in the vacant round of the interior, which is open to the sky, and grass and wild-flowers ' +
'rooting themselves into the chinks of the stones, look already like relics of antiquity, and may yet be overspread with the lichens of centuries to come.';
var rtlText:String ='مدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسة'+
'مدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسة'+
'مدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسة'+
'مدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسةمدرسة';
var accentedText:String = '\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A' +
'\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A' +
'\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A' +
'\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A' +
'\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A\u0041\u030A';
var formatForRtlTest:TextLayoutFormat = new TextLayoutFormat();
formatForRtlTest.fontFamily = 'Adobe Arabic';
// Get stats used later
_lines = new Array(); _textLen = 0;
bounds.width = 200; bounds.height = NaN;
var factory:StringTextLineFactory = new StringTextLineFactory();
factory.text = text;
factory.compositionBounds = bounds;
factory.createTextLines(truncationTestCallback);
bounds = factory.getContentBounds();
assertTrue("[Not a code bug] Fix test case so that text occupies at least three lines when composed in specified bounds.", _lines.length >= 3);
var line0:TextLine = _lines[0] as TextLine;
var line0Extent:Number = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line0.y - line0.ascent : line0.y + line0.descent;
var line0TextLen:int = line0.rawTextLength;
var line1:TextLine = _lines[1] as TextLine;
var line1Extent:Number = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line1.y - line1.ascent : line1.y + line1.descent;
var line2:TextLine = _lines[2] as TextLine;
var line2Extent:Number = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line2.y - line2.ascent : line2.y + line2.descent;
var contentHeight:Number = bounds.height;
var contentTextLength:int = _textLen;
_lines.splice(0); _textLen = 0; // reset
bounds.width = 200; bounds.height = NaN;
factory.compositionBounds = bounds;
factory.text = rtlText;
factory.spanFormat = formatForRtlTest;
factory.createTextLines(truncationTestCallback);
assertTrue("[Not a code bug] Fix test case so that RTL text occupies at least two lines when composed in specified bounds.", _lines.length >= 2);
var rtlLine0TextLen:int = _lines[0].rawTextLength;
_lines.splice(0); _textLen = 0; // Reset
bounds.width = 200; bounds.height = NaN;
factory.compositionBounds = bounds;
factory.text = accentedText;
factory.spanFormat = null;
factory.createTextLines(truncationTestCallback);
assertTrue("[Not a code bug] Fix test case so that accented text occupies at least two lines when composed in specified bounds.", _lines.length >= 2);
var line:TextLine;
var lineExtent:Number;
var truncationIndicatorIndex:int;
var originalContentPrefix:String;
var customTruncationIndicator:String;
var customFactory:StringTextLineFactory = new StringTextLineFactory();
// Verify that text is truncated even if width is not specified
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = NaN; bounds.height = NaN;
factory.text = "A\nB"; // has an explicit new line character to ensure two lines
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions(null, 1);
factory.createTextLines(truncationTestCallback);
assertTrue("Did not truncate when width is unspecified", factory.isTruncated);
// Verify that text is truncated even if explicit line breaking is used
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN;
var format:TextLayoutFormat = new TextLayoutFormat();
format.lineBreak = LineBreak.EXPLICIT;
factory.textFlowFormat = format;
factory.text = "A\nB"; // has an explicit new line character to ensure two lines
factory.compositionBounds = bounds;
factory.createTextLines(truncationTestCallback);
assertTrue("Did not truncate when explicit line breaking is used", factory.isTruncated);
// No lines case 1: compose height allows no line
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line0Extent/2; // less than what one line requires
factory.textFlowFormat = null;
factory.text = text;
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions();
factory.createTextLines(truncationTestCallback);
assertTrue("Composed one or more lines when compose height allows none", _lines.length == 0 && factory.isTruncated);
// No lines case 2: 0 line count limit
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = contentHeight; // enough to fit all content
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions(null, 0);
factory.createTextLines(truncationTestCallback);
assertTrue("Composed one or more lines when line count limit is 0", _lines.length == 0 && factory.isTruncated);
// No lines case 3: truncation indicator is too large
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = contentHeight -1; // just shy of what the truncation indicator (same as original text) requires
factory.compositionBounds = bounds;
factory.text = text;
factory.truncationOptions = new TruncationOptions(text);
factory.textFlowFormat = null;
factory.createTextLines(truncationTestCallback);
assertTrue("Composed one or more lines when compose height does not allow truncation indicator itself to fit", _lines.length == 0 && factory.isTruncated);
// Verify truncation if composing to fit in bounds
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line1Extent; // should fit two lines
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions();
factory.createTextLines(truncationTestCallback);
assertTrue("Invalid truncation results when composing to fit in bounds (lineCount)", _lines.length == 2 && factory.isTruncated);
line = _lines[1] as TextLine;
lineExtent = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line.y - line.ascent : line.y + line.descent;
assertTrue("Invalid truncation results when composing to fit in bounds", lineExtent <= line1Extent);
// Verify truncation if composing to fit in a line count limit
_lines.splice(0); _textLen = 0; // reset
bounds.width = 200; bounds.height = NaN;
bounds.left = 0; bounds.top = 0;
factory.text = text;
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions(null, 2);
factory.createTextLines(truncationTestCallback);
assertTrue("Invalid truncation results when composing to fit in a line count limit", _lines.length == 2 && factory.isTruncated);
// Verify truncation if composing to fit in bounds and a line count limit; the former dominates
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line0Extent; // should fit one line
factory.text = text;
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions(null, 2);
factory.createTextLines(truncationTestCallback); // line count limit of 2
assertTrue("Invalid truncation results when multiple truncation criteria provided", _lines.length == 1 && factory.isTruncated);
line = _lines[0] as TextLine;
lineExtent = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line.y - line.ascent : line.y + line.descent;
assertTrue("Invalid truncation results when multiple truncation criteria provided", lineExtent <= line0Extent);
// Verify truncation if composing to fit in bounds and a line count limit; the latter dominates
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line1Extent; // should fit two lines
factory.text = text;
factory.compositionBounds = bounds;
factory.truncationOptions = new TruncationOptions(null, 1);
factory.createTextLines(truncationTestCallback); // line count limit of 1
assertTrue("Invalid truncation results when multiple truncation criteria provided", _lines.length == 1 && factory.isTruncated);
line = _lines[0] as TextLine;
lineExtent = StringTextLineFactory.defaultConfiguration.overflowPolicy == OverflowPolicy.FIT_ANY ? line.y - line.ascent : line.y + line.descent;
assertTrue("Invalid truncation results when multiple truncation criteria provided", lineExtent <= line1Extent);
// Verify truncated text content with default truncation indicator (line count limit)
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN; customFactory.text = text;
customFactory.compositionBounds = bounds;
customFactory.truncationOptions = new TruncationOptions(null, 2);
customFactory.createTextLines(truncationTestCallback);
truncationIndicatorIndex = customFactory.truncatedText.lastIndexOf(TruncationOptions.HORIZONTAL_ELLIPSIS);
assertTrue("Default truncation indicator not present at the end of the truncated string", truncationIndicatorIndex+TruncationOptions.HORIZONTAL_ELLIPSIS.length == customFactory.truncatedText.length && factory.isTruncated);
originalContentPrefix = customFactory.truncatedText.slice(0, truncationIndicatorIndex);
assertTrue("Original content before truncation indicator mangled", text.indexOf(originalContentPrefix) == 0);
// Verify truncated text content with default truncation indicator (fit in bounds)
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line1Extent; // should fit two lines;
customFactory.text = text;
customFactory.compositionBounds = bounds;
customFactory.truncationOptions = new TruncationOptions();
customFactory.createTextLines(truncationTestCallback);
truncationIndicatorIndex = customFactory.truncatedText.lastIndexOf(TruncationOptions.HORIZONTAL_ELLIPSIS);
assertTrue("Default truncation indicator not present at the end of the truncated string", truncationIndicatorIndex+TruncationOptions.HORIZONTAL_ELLIPSIS.length == customFactory.truncatedText.length && factory.isTruncated);
originalContentPrefix = customFactory.truncatedText.slice(0, truncationIndicatorIndex);
assertTrue("Original content before truncation indicator mangled", text.indexOf(originalContentPrefix) == 0);
// Verify truncated text content with custom truncation indicator
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN; customFactory.text = text;
customTruncationIndicator = "<SNIP>";
customFactory.compositionBounds = bounds;
customFactory.truncationOptions = new TruncationOptions(customTruncationIndicator, 2);
customFactory.createTextLines(truncationTestCallback);
truncationIndicatorIndex = customFactory.truncatedText.lastIndexOf(customTruncationIndicator);
assertTrue("Truncation indicator not present at the end of the truncated string", truncationIndicatorIndex+customTruncationIndicator.length == customFactory.truncatedText.length && factory.isTruncated);
originalContentPrefix = customFactory.truncatedText.slice(0, truncationIndicatorIndex);
assertTrue("Original content before truncation indicator mangled", text.indexOf(originalContentPrefix) == 0);
// Verify original text replacement is optimal
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN; customFactory.text = text;
customFactory.text = text;
customFactory.compositionBounds = bounds;
customTruncationIndicator = '\u200B'; // Zero-width space : should not require *any* original content that fits to be replaced
customFactory.truncationOptions = new TruncationOptions(customTruncationIndicator, 1);
customFactory.createTextLines(truncationTestCallback);
assertTrue("Replacing more original content than is neccessary", customFactory.truncatedText.length == line0TextLen+customTruncationIndicator.length && factory.isTruncated);
// Verify original text replacement is optimal (RTL text)
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN; customFactory.text = rtlText;
customFactory.compositionBounds = bounds;
customTruncationIndicator = '\u200B'; // Zero-width space : should not require *any* original content that fits to be replaced
customFactory.spanFormat = formatForRtlTest;
customFactory.truncationOptions = new TruncationOptions(customTruncationIndicator, 1);
customFactory.createTextLines(truncationTestCallback);
assertTrue("Replacing more original content than is neccessary (RTL text)", customFactory.truncatedText.length == rtlLine0TextLen+customTruncationIndicator.length && factory.isTruncated);
customFactory.spanFormat = null;
// Verify truncation happens at atom boundaries
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = NaN; customFactory.text = accentedText;
customTruncationIndicator = '<' + '\u200A' /* Hair space */ + '>'; // what precedes and succeeds the hair space is irrelevant
customFactory.compositionBounds = bounds;
customFactory.truncationOptions = new TruncationOptions(customTruncationIndicator, 1);
customFactory.createTextLines(truncationTestCallback);
assertTrue("[Not a code bug] Fix test case so that truncation indicator itself fits", _lines.length == 1 && factory.isTruncated); // baseline
var initialTruncationPoint:int = customFactory.truncatedText.length - customTruncationIndicator.length;
assertTrue("[Not a code bug] Fix test case so that some of the original content is left behind on first truncation attempt", initialTruncationPoint > 0); // baseline
assertTrue("Truncation in the middle of an atom!", initialTruncationPoint % 2 == 0);
var nextTruncationPoint:int;
do
{
bounds.height = NaN;
customTruncationIndicator = customTruncationIndicator.replace('\u200A', '\u200A\u200A'); // add another hair space in each iteration, making truncation indicator wider (ever so slightly)
customFactory.compositionBounds = bounds;
customFactory.truncationOptions = new TruncationOptions(customTruncationIndicator, 1);
customFactory.createTextLines(truncationTestCallback);
nextTruncationPoint = customFactory.truncatedText.length - customTruncationIndicator.length;
if (nextTruncationPoint != initialTruncationPoint)
{
assertTrue("Truncation in the middle of an atom!", nextTruncationPoint % 2 == 0);
assertTrue("Sub-optimal replacement of original content?", nextTruncationPoint == initialTruncationPoint-2);
initialTruncationPoint = nextTruncationPoint;
}
} while (nextTruncationPoint);
// Verify scrolling behavior when truncation options are set
_lines.splice(0); _textLen = 0; // reset
bounds.left = 0; bounds.top = 0;
bounds.width = 200; bounds.height = line1Extent; // should fit two lines
factory.compositionBounds = bounds;
factory.verticalScrollPolicy = "on";
var vaFormat:TextLayoutFormat = new TextLayoutFormat();
vaFormat.verticalAlign = VerticalAlign.BOTTOM;
factory.textFlowFormat = vaFormat;
factory.truncationOptions = new TruncationOptions(); // should override scroll policy
factory.createTextLines(truncationTestCallback);
assertTrue("When verticalAlign is Bottom, and scrolling is on, but truncation options are set, only text that fits should be generated",
_textLen < contentTextLength && factory.isTruncated);
}
public function CompositionCompleteEventTest():void
{
var gotEvent:Boolean = false;
var textFlow:TextFlow = SelManager.textFlow;
textFlow.addEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE, completionHandler);
var charFormat:TextLayoutFormat = new TextLayoutFormat();
charFormat.fontSize=48;
SelManager.selectAll();
(SelManager as EditManager).applyLeafFormat(charFormat);
assertTrue("Didn't get the CompositionCompleteEvent", gotEvent == true);
function completionHandler(event:CompositionCompleteEvent): void
{
gotEvent = true;
textFlow.removeEventListener(CompositionCompleteEvent.COMPOSITION_COMPLETE, completionHandler);
}
}
private function setUpMultipleLinkedContainers(numberOfContainers:int):Sprite
{
var flexContainer:Container;
var textFlow:TextFlow = SelManager.textFlow;
var flowComposer:IFlowComposer = textFlow.flowComposer;
var firstController:ContainerController = textFlow.flowComposer.getControllerAt(0);
var totalWidth:Number = firstController.compositionWidth;
var containerWidth:Number = totalWidth / numberOfContainers;
var containerHeight:Number = firstController.compositionHeight;
firstController.setCompositionSize(containerWidth, firstController.compositionHeight);
var containerParent:Sprite = firstController.container.parent as Sprite;
if (containerParent is Container)
{
flexContainer = Container(containerParent);
var newContainerParent:Sprite = new Sprite();
flexContainer.rawChildren.addChild(newContainerParent);
flexContainer.rawChildren.removeChild(firstController.container);
newContainerParent.addChild(firstController.container);
containerParent = newContainerParent;
}
var pos:int = containerWidth;
while (flowComposer.numControllers < numberOfContainers)
{
var s:Sprite = new Sprite();
s.x = pos;
pos += containerWidth;
containerParent.addChild(s);
flowComposer.addController(new ContainerController(s, containerWidth, containerHeight));
}
return containerParent;
}
private function restoreToSingleContainer(containerParent:Sprite):void
{
var flexContainer:Container = containerParent.parent as Container;
if (flexContainer)
{
flexContainer.rawChildren.removeChild(containerParent);
flexContainer.rawChildren.addChild(containerParent.getChildAt(0));
}
var flowComposer:IFlowComposer = SelManager.textFlow.flowComposer;
while (flowComposer.numControllers > 1)
flowComposer.removeControllerAt(flowComposer.numControllers - 1);
}
// Test case with multiple containers, where the last container is scrolled down, and update will cause a scroll
// Watson 2583969
public function scrolledRedrawPartialCompose():void
{
var multiContainerParent:Sprite;
try
{
var textFlow:TextFlow = SelManager.textFlow;
var flowComposer:IFlowComposer = textFlow.flowComposer;
multiContainerParent = setUpMultipleLinkedContainers(5);
// Paste all the text again, so all containers are full, and there is text scrolled out
var textScrap:TextScrap = TextScrap.createTextScrap(new TextRange(textFlow, 0, textFlow.textLength));
EditManager(SelManager).pasteTextScrap(textScrap);
flowComposer.updateAllControllers();
// Set selection to the last two lines of the flow, and scroll to the new selection, and then delete the text
var lastController:ContainerController = flowComposer.getControllerAt(flowComposer.numControllers - 1);
flowComposer.composeToPosition(); // force all text to compose
var nextToLastLine:TextFlowLine = flowComposer.getLineAt(flowComposer.numLines - 2);
SelManager.selectRange(nextToLastLine.absoluteStart, textFlow.textLength);
lastController.scrollToRange(SelManager.absoluteStart, SelManager.absoluteEnd);
var firstVisibleChar:int = lastController.getFirstVisibleLine().absoluteStart; // save off the current scrolled-to text pos
flowComposer.updateAllControllers();
EditManager(SelManager).deleteText();
// The delete (and subsequent redraw) should have caused a scroll during the ContainerController updateCompositionShapes.
// Check that this happened correctly.
var firstVisibleCharAfterPaste:int = lastController.getFirstVisibleLine().absoluteStart;
assertTrue("Expected scroll during update", firstVisibleChar != firstVisibleCharAfterPaste);
}
finally
{
// restore how containers were set up before
restoreToSingleContainer(multiContainerParent);
}
}
// Test case with multiple containers, where the last container is scrolled down, and update will cause a scroll
// Watson 2583969
public function multipleContainersWithPadding():void
{
var multiContainerParent:Sprite;
try
{
var textFlow:TextFlow = SelManager.textFlow;
var flowComposer:IFlowComposer = textFlow.flowComposer;
multiContainerParent = setUpMultipleLinkedContainers(2);
var firstController:ContainerController = flowComposer.getControllerAt(0);
var format:TextLayoutFormat = new TextLayoutFormat(firstController.format);
format.paddingTop = firstController.compositionHeight;
firstController.format = format;
flowComposer.updateAllControllers();
assertTrue("Expected no lines in first container", firstController.getFirstVisibleLine() == null && firstController.getLastVisibleLine() == null);
}
finally
{
// restore how containers were set up before
restoreToSingleContainer(multiContainerParent);
}
}
private const numberOfLinesBack:int = 5;
public function deleteAtContainerStart():void
{
var multiContainerParent:Sprite;
try
{
var textFlow:TextFlow = SelManager.textFlow;
var flowComposer:IFlowComposer = textFlow.flowComposer;
multiContainerParent = setUpMultipleLinkedContainers(2);
flowComposer.composeToPosition();
var controller:ContainerController = flowComposer.getControllerAt(0);
var lastLineIndex:int = flowComposer.findLineIndexAtPosition(controller.absoluteStart + controller.textLength);
var startIndex:int = flowComposer.getLineAt(lastLineIndex - numberOfLinesBack).absoluteStart;
SelManager.selectRange(startIndex, startIndex);
for (var i:int = 0; i < numberOfLinesBack + 1; ++i)
SelManager.splitParagraph();
flowComposer.updateAllControllers();
var textLengthBefore:int = controller.textLength;
assertTrue("Selection should be at the start of the next container", SelManager.absoluteStart == controller.absoluteStart + controller.textLength);
SelManager.deletePreviousCharacter();
flowComposer.composeToPosition();
assertTrue("Expected first line of following container to be sucked in", controller.textLength > textLengthBefore);
}
finally
{
// restore how containers were set up before
restoreToSingleContainer(multiContainerParent);
}
}
public function resizeController2644361():void
{
var textFlow:TextFlow = SelManager.textFlow;
var controller:ContainerController = textFlow.flowComposer.getControllerAt(0);
var originalWidth:Number = controller.compositionWidth;
var originalHeight:Number = controller.compositionHeight;
var scrap:TextScrap = TextScrap.createTextScrap(new TextRange(textFlow, 0, textFlow.textLength));
SelManager.selectRange(textFlow.textLength - 1, textFlow.textLength - 1);
SelManager.splitParagraph();
SelManager.pasteTextScrap(scrap);
SelManager.pasteTextScrap(scrap);
textFlow.flowComposer.updateAllControllers();
controller.setCompositionSize( 825 , 471 )
SelManager.updateAllControllers();
controller.setCompositionSize( 808 , 464 )
SelManager.updateAllControllers();
controller.setCompositionSize( 791 , 462 )
SelManager.updateAllControllers();
controller.setCompositionSize( 768 , 461 )
SelManager.updateAllControllers();
}
public function resizeEmptyController():void
{
var textFlow:TextFlow = new TextFlow();
var p:ParagraphElement = new ParagraphElement();
textFlow.addChild(p);
var span:SpanElement = new SpanElement();
span.text = "Hello world";
span.fontSize = 40;
p.addChild(span);
var sprite1:Sprite = new Sprite();
var cc1:ContainerController = new ContainerController(sprite1,100,50);
sprite1.x = 100;
var sprite2:Sprite = new Sprite();
var cc2:ContainerController = new ContainerController(sprite2,100,50);
sprite2.x = 300;
// addChild(sprite1);
// addChild(sprite2);
textFlow.flowComposer.addController(cc1);
textFlow.flowComposer.addController(cc2);
textFlow.flowComposer.updateAllControllers();
var originalLength:int = cc1.textLength;
cc1.setCompositionSize(100,10);
textFlow.flowComposer.updateAllControllers();
cc1.setCompositionSize(100,50);
textFlow.flowComposer.updateAllControllers();
assertTrue("Expected text to recompose into controller", cc1.textLength == originalLength);
}
public function emptyController():void
{
var s:Sprite = new Sprite();
var textFlow:TextFlow = new TextFlow();
textFlow.flowComposer.addController(new ContainerController(s, 0, 0));
textFlow.flowComposer.updateAllControllers();
}
// Check that the content bounds includes all parcels when composition starts from a column that is not the first
// See Watson 2769670
public function contentBoundsOnComposeFromMiddle():void
{
TestFrame.rootElement.blockProgression = writingDirection[0];
TestFrame.rootElement.direction = writingDirection[1];
var textFlow:TextFlow = SelManager.textFlow;
var controller:ContainerController = textFlow.flowComposer.getControllerAt(0);
var composeSpace:Rectangle = new Rectangle(0, 0, controller.compositionWidth, controller.compositionHeight);
var lastLine:TextFlowLine = controller.getLastVisibleLine();
var lastVisiblePosition:int = lastLine.absoluteStart + lastLine.textLength - 1;
var charPos:int = lastVisiblePosition - 100;
// Trim off the unseen portion of the flow to a little before the end, so we aren't
// affected by content height estimation, and so we can check that height from previous
// columns is included.
SelManager.selectRange(charPos, textFlow.textLength - 1);
SelManager.deleteText();
// Change format to 3 columns justified text, and get the bounds. This time we composed from the start.
var format:TextLayoutFormat = new TextLayoutFormat(textFlow.format);
format.columnCount = 3;
format.textAlign = TextAlign.JUSTIFY;
textFlow.format = format;
textFlow.flowComposer.updateAllControllers();
var bounds:Rectangle = controller.getContentBounds();
// Force partial composition in the last column. The bounds may be slightly different in height because we aren't
// iterating all the lines to get height. If it doesn't match, it should be equal to the (logical) compositionHeight.
charPos = textFlow.textLength - 3;
var leafFormat:TextLayoutFormat = new TextLayoutFormat();
leafFormat.color = 0xFF0000;
SelManager.selectRange(charPos, charPos + 1);
SelManager.applyLeafFormat(leafFormat);
var boundsAfterPartialCompose:Rectangle = controller.getContentBounds();
var boundsMatch:Boolean = boundsAfterPartialCompose.equals(bounds);
if (!boundsMatch &&
bounds.y == boundsAfterPartialCompose.y)
{
if (controller.effectiveBlockProgression == BlockProgression.TB)
boundsMatch = Math.abs(boundsAfterPartialCompose.x - bounds.x) < 1 && Math.abs(boundsAfterPartialCompose.width - bounds.width) < 1 && boundsAfterPartialCompose.height == controller.compositionHeight;
else
boundsMatch = Math.abs(boundsAfterPartialCompose.x - -controller.compositionWidth) < 1 && Math.abs(boundsAfterPartialCompose.height - bounds.height) < 1 && boundsAfterPartialCompose.width == controller.compositionWidth;
}
assertTrue("Expected bounds after partial compose to match bounds from previous full compose", boundsMatch);
}
}
}