http://git-wip-us.apache.org/repos/asf/flex-tlf/blob/0d5ac0c1/automation_tests/src/UnitTest/Tests/CompositionTest.as ---------------------------------------------------------------------- diff --git a/automation_tests/src/UnitTest/Tests/CompositionTest.as b/automation_tests/src/UnitTest/Tests/CompositionTest.as index 5ee155a..fdda766 100644 --- a/automation_tests/src/UnitTest/Tests/CompositionTest.as +++ b/automation_tests/src/UnitTest/Tests/CompositionTest.as @@ -18,888 +18,975 @@ //////////////////////////////////////////////////////////////////////////////// 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; + import UnitTest.ExtendedClasses.TestDescriptor; + import UnitTest.ExtendedClasses.TestSuiteExtended; + import UnitTest.ExtendedClasses.VellumTestCase; + import UnitTest.Fixtures.TestConfig; + + import flash.display.Sprite; + 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.compose.TextFlowLine; + import flashx.textLayout.container.ContainerController; + import flashx.textLayout.edit.EditManager; + 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; + + import mx.core.Container; + import mx.utils.UIDUtil; import org.flexunit.asserts.assertTrue; import org.flexunit.asserts.fail; 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); - } - } + + [RunWith("org.flexunit.runners.Parameterized")] + public class CompositionTest extends VellumTestCase + { + [Parameters] + public static var parameters:Array = [ + ["simple.xml", false], + ["asknot.xml", true] + ]; + + private var _lines:Array; + private var _textLen:int; + private const numberOfLinesBack:int = 5; + + public function CompositionTest(fileName:String, testSettings:Boolean) + { + super("", "CompositionTest", TestConfig.getInstance()); + + TestData.fileName = fileName; + addDefaultTestSettings = testSettings; + + metaData = {}; + // Note: These must correspond to a Watson product area (case-sensitive) + metaData.productArea = "Text Composition"; + } + + [Before] + override public function setUpTest():void + { + super.setUpTest(); + } + + [After] + override public function tearDownTest():void + { + super.tearDownTest(); + } + + /** + * 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 "==="). + */ + [Test] + 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. + */ + [Test] + [Ignore] + 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]) + ); + } + } + } + + // For benchmark: read in Alice and display one screenfull + [Test] + 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; + } */ + } + + [Test] + 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); + } + + [Test] + 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 + [Test] + 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 + [Test] + 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); + } + } + + [Test] + [Ignore] + 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); + } + } + + [Test] + public function resizeController2644361():void + { + var textFlow:TextFlow = SelManager.textFlow; + var controller:ContainerController = textFlow.flowComposer.getControllerAt(0); + + 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(); + } + + [Test] + 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); + } + + [Test] + 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 + [Test] + 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
<TRUNCATED>
