// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/core/layout/ng/ng_base_layout_algorithm_test.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" #include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h" #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" #include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" namespace blink { String ToString(NGInlineItemResults line, NGInlineNode node) { StringBuilder builder; const String& text = node.ItemsData(false).text_content; for (const auto& item_result : line) { builder.Append( StringView(text, item_result.StartOffset(), item_result.Length())); } return builder.ToString(); } class NGLineBreakerTest : public NGLayoutTest { protected: NGInlineNode CreateInlineNode(const String& html_content) { SetBodyInnerHTML(html_content); LayoutBlockFlow* block_flow = To(GetLayoutObjectByElementId("container")); return NGInlineNode(block_flow); } // Break lines using the specified available width. Vector> BreakLines( NGInlineNode node, LayoutUnit available_width, void (*callback)(const NGLineBreaker&, const NGLineInfo&) = nullptr, bool fill_first_space_ = false) { DCHECK(node); node.PrepareLayoutIfNeeded(); NGConstraintSpaceBuilder builder( WritingMode::kHorizontalTb, {WritingMode::kHorizontalTb, TextDirection::kLtr}, /* is_new_fc */ false); builder.SetAvailableSize({available_width, kIndefiniteSize}); NGConstraintSpace space = builder.ToConstraintSpace(); scoped_refptr break_token; Vector> lines; trailing_whitespaces_.resize(0); NGExclusionSpace exclusion_space; NGPositionedFloatVector leading_floats; NGLineLayoutOpportunity line_opportunity(available_width); while (!break_token || !break_token->IsFinished()) { NGLineInfo line_info; NGLineBreaker line_breaker(node, NGLineBreakerMode::kContent, space, line_opportunity, leading_floats, 0u, break_token.get(), &exclusion_space); line_breaker.NextLine(&line_info); if (callback) callback(line_breaker, line_info); trailing_whitespaces_.push_back( line_breaker.TrailingWhitespaceForTesting()); if (line_info.Results().IsEmpty()) break; break_token = line_breaker.CreateBreakToken(line_info); if (fill_first_space_ && lines.IsEmpty()) { first_should_hang_trailing_space_ = line_info.ShouldHangTrailingSpaces(); first_hang_width_ = line_info.HangWidth(); } lines.push_back(std::make_pair(ToString(line_info.Results(), node), line_info.Results().back().item_index)); } return lines; } Vector trailing_whitespaces_; bool first_should_hang_trailing_space_; LayoutUnit first_hang_width_; }; namespace { TEST_F(NGLineBreakerTest, FitWithEpsilon) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
00000
)HTML"); auto lines = BreakLines( node, LayoutUnit::FromFloatRound(50 - LayoutUnit::Epsilon()), [](const NGLineBreaker& line_breaker, const NGLineInfo& line_info) { EXPECT_FALSE(line_info.HasOverflow()); }); EXPECT_EQ(1u, lines.size()); // Make sure ellipsizing code use the same |HasOverflow|. NGInlineCursor cursor(*node.GetLayoutBlockFlow()); for (; cursor; cursor.MoveToNext()) EXPECT_FALSE(cursor.Current().IsEllipsis()); } TEST_F(NGLineBreakerTest, SingleNode) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
123 456 789
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(80)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("123 456", lines[0].first); EXPECT_EQ("789", lines[1].first); lines = BreakLines(node, LayoutUnit(60)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("123", lines[0].first); EXPECT_EQ("456", lines[1].first); EXPECT_EQ("789", lines[2].first); } TEST_F(NGLineBreakerTest, OverflowWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345 678
)HTML"); // The first line overflows, but the last line does not. Vector> lines; lines = BreakLines(node, LayoutUnit(40)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("12345", lines[0].first); EXPECT_EQ("678", lines[1].first); // Both lines overflow. lines = BreakLines(node, LayoutUnit(20)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("12345", lines[0].first); EXPECT_EQ("678", lines[1].first); } TEST_F(NGLineBreakerTest, OverflowTab) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345 678
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("12345\t\t", lines[0].first); EXPECT_EQ("678", lines[1].first); } TEST_F(NGLineBreakerTest, OverflowTabBreakWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345 678
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("12345\t\t", lines[0].first); EXPECT_EQ("678", lines[1].first); } TEST_F(NGLineBreakerTest, OverflowAtomicInline) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345678
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(80)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ(String(u"12345\uFFFC"), lines[0].first); EXPECT_EQ("678", lines[1].first); lines = BreakLines(node, LayoutUnit(70)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("12345", lines[0].first); EXPECT_EQ(String(u"\uFFFC678"), lines[1].first); lines = BreakLines(node, LayoutUnit(40)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("12345", lines[0].first); EXPECT_EQ(String(u"\uFFFC"), lines[1].first); EXPECT_EQ("678", lines[2].first); lines = BreakLines(node, LayoutUnit(20)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("12345", lines[0].first); EXPECT_EQ(String(u"\uFFFC"), lines[1].first); EXPECT_EQ("678", lines[2].first); } TEST_F(NGLineBreakerTest, OverflowMargin) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
123 456 789
)HTML"); const Vector& items = node.ItemsData(false).items; // While "123 456" can fit in a line, "456" has a right margin that cannot // fit. Since "456" and its right margin is not breakable, "456" should be on // the next line. Vector> lines; lines = BreakLines(node, LayoutUnit(80)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("123", lines[0].first); EXPECT_EQ("456", lines[1].first); DCHECK_EQ(NGInlineItem::kCloseTag, items[lines[1].second].Type()); EXPECT_EQ("789", lines[2].first); // Same as above, but this time "456" overflows the line because it is 70px. lines = BreakLines(node, LayoutUnit(60)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("123", lines[0].first); EXPECT_EQ("456", lines[1].first); DCHECK_EQ(NGInlineItem::kCloseTag, items[lines[1].second].Type()); EXPECT_EQ("789", lines[2].first); } TEST_F(NGLineBreakerTest, OverflowAfterSpacesAcrossElements) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345 1234567890123
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("12345 ", lines[0].first); EXPECT_EQ("1234567890", lines[1].first); EXPECT_EQ("123", lines[2].first); } // Tests when the last word in a node wraps, and another node continues. TEST_F(NGLineBreakerTest, WrapLastWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
AAA AAA AAA BB CC
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("AAA AAA", lines[0].first); EXPECT_EQ("AAA BB CC", lines[1].first); } TEST_F(NGLineBreakerTest, WrapLetterSpacing) { NGInlineNode node = CreateInlineNode(R"HTML(
Star Wars
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("Star", lines[0].first); EXPECT_EQ("Wars", lines[1].first); } TEST_F(NGLineBreakerTest, BoundaryInWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
123 456789 abc
)HTML"); // The element boundary within "456789" should not cause a break. // Since "789" does not fit, it should go to the next line along with "456". Vector> lines; lines = BreakLines(node, LayoutUnit(80)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("123", lines[0].first); EXPECT_EQ("456789", lines[1].first); EXPECT_EQ("abc", lines[2].first); // Same as above, but this time "456789" overflows the line because it is // 60px. lines = BreakLines(node, LayoutUnit(50)); EXPECT_EQ(3u, lines.size()); EXPECT_EQ("123", lines[0].first); EXPECT_EQ("456789", lines[1].first); EXPECT_EQ("abc", lines[2].first); } TEST_F(NGLineBreakerTest, BoundaryInFirstWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
123456 789
)HTML"); Vector> lines; lines = BreakLines(node, LayoutUnit(80)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("123456", lines[0].first); EXPECT_EQ("789", lines[1].first); lines = BreakLines(node, LayoutUnit(50)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("123456", lines[0].first); EXPECT_EQ("789", lines[1].first); lines = BreakLines(node, LayoutUnit(20)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("123456", lines[0].first); EXPECT_EQ("789", lines[1].first); } struct WhitespaceStateTestData { const char* html; const char* white_space; NGLineBreaker::WhitespaceState expected; } whitespace_state_test_data[] = { // The most common cases. {"12", "normal", NGLineBreaker::WhitespaceState::kNone}, {"1234 5678", "normal", NGLineBreaker::WhitespaceState::kCollapsed}, // |NGInlineItemsBuilder| collapses trailing spaces of a block, so // |NGLineBreaker| computes to `none`. {"12 ", "normal", NGLineBreaker::WhitespaceState::kNone}, // pre/pre-wrap should preserve trailing spaces if exists. {"1234 5678", "pre-wrap", NGLineBreaker::WhitespaceState::kPreserved}, {"12 ", "pre", NGLineBreaker::WhitespaceState::kPreserved}, {"12 ", "pre-wrap", NGLineBreaker::WhitespaceState::kPreserved}, {"12", "pre", NGLineBreaker::WhitespaceState::kNone}, {"12", "pre-wrap", NGLineBreaker::WhitespaceState::kNone}, // Empty/space-only cases. {"", "normal", NGLineBreaker::WhitespaceState::kLeading}, {" ", "pre", NGLineBreaker::WhitespaceState::kPreserved}, {" ", "pre-wrap", NGLineBreaker::WhitespaceState::kPreserved}, // Cases needing to rewind. {"12 3456", "normal", NGLineBreaker::WhitespaceState::kCollapsed}, {"12 3456", "pre-wrap", NGLineBreaker::WhitespaceState::kPreserved}, // Atomic inlines. {"12 ", "normal", NGLineBreaker::WhitespaceState::kNone}, // fast/text/whitespace/inline-whitespace-wrapping-4.html {"1234 " " 5678", "pre", NGLineBreaker::WhitespaceState::kCollapsed}, }; std::ostream& operator<<(std::ostream& os, const WhitespaceStateTestData& data) { return os << static_cast(data.expected) << " for '" << data.html << "' with 'white-space: " << data.white_space << "'"; } class NGWhitespaceStateTest : public NGLineBreakerTest, public testing::WithParamInterface {}; INSTANTIATE_TEST_SUITE_P(NGLineBreakerTest, NGWhitespaceStateTest, testing::ValuesIn(whitespace_state_test_data)); TEST_P(NGWhitespaceStateTest, WhitespaceState) { const auto& data = GetParam(); LoadAhem(); NGInlineNode node = CreateInlineNode(String(R"HTML(
)HTML" + data.html + R"HTML(
)HTML"); BreakLines(node, LayoutUnit(50)); EXPECT_EQ(trailing_whitespaces_[0], data.expected); } struct TrailingSpaceWidthTestData { const char* html; const char* white_space; unsigned trailing_space_width; } trailing_space_width_test_data[] = { {" ", "pre", 1}, {" ", "pre", 3}, {"1 ", "pre", 1}, {"1 ", "pre", 2}, {"1 ", "pre", 1}, {"1 ", "pre", 2}, {"1 ", "pre", 2}, {"1 ", "pre", 3}, {"1 \t", "pre", 3}, {"1 \n", "pre", 2}, {"1
", "pre", 2}, {" ", "pre-wrap", 1}, {" ", "pre-wrap", 3}, {"1 ", "pre-wrap", 1}, {"1 ", "pre-wrap", 2}, {"1 ", "pre-wrap", 1}, {"1 ", "pre-wrap", 2}, {"1 ", "pre-wrap", 2}, {"1 ", "pre-wrap", 3}, {"1 \t", "pre-wrap", 3}, {"1
", "pre-wrap", 2}, {"12 1234", "pre-wrap", 1}, {"12 1234", "pre-wrap", 2}, }; class NGTrailingSpaceWidthTest : public NGLineBreakerTest, public testing::WithParamInterface {}; INSTANTIATE_TEST_SUITE_P(NGLineBreakerTest, NGTrailingSpaceWidthTest, testing::ValuesIn(trailing_space_width_test_data)); TEST_P(NGTrailingSpaceWidthTest, TrailingSpaceWidth) { const auto& data = GetParam(); LoadAhem(); NGInlineNode node = CreateInlineNode(String(R"HTML(
)HTML" + data.html + R"HTML(
)HTML"); BreakLines(node, LayoutUnit(50), nullptr, true); if (first_should_hang_trailing_space_) { EXPECT_EQ(first_hang_width_, LayoutUnit(10) * data.trailing_space_width); } else { EXPECT_EQ(first_hang_width_, LayoutUnit()); } } TEST_F(NGLineBreakerTest, MinMaxWithTrailingSpaces) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
12345 6789
)HTML"); auto sizes = node.ComputeMinMaxSizes( WritingMode::kHorizontalTb, MinMaxSizesInput(/* percentage_resolution_block_size */ LayoutUnit(), MinMaxSizesType::kContent)) .sizes; EXPECT_EQ(sizes.min_size, LayoutUnit(60)); EXPECT_EQ(sizes.max_size, LayoutUnit(110)); } // For http://crbug.com/1104534 TEST_F(NGLineBreakerTest, SplitTextZero) { // Note: |V8TestingScope| is needed for |Text::splitText()|. V8TestingScope scope; LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML(
0123456789 ab
)HTML"); To(GetElementById("target")->firstChild()) ->splitText(0, ASSERT_NO_EXCEPTION); UpdateAllLifecyclePhasesForTest(); Vector> lines; lines = BreakLines(node, LayoutUnit(100)); EXPECT_EQ(2u, lines.size()); EXPECT_EQ("0123456789", lines[0].first); EXPECT_EQ("ab", lines[1].first); } TEST_F(NGLineBreakerTest, TableCellWidthCalculationQuirkOutOfFlow) { NGInlineNode node = CreateInlineNode(R"HTML(
1234567
)HTML"); // |SetBodyInnerHTML| doesn't set compatibility mode. GetDocument().SetCompatibilityMode(Document::kQuirksMode); EXPECT_TRUE(node.GetDocument().InQuirksMode()); node.ComputeMinMaxSizes( WritingMode::kHorizontalTb, MinMaxSizesInput(/* percentage_resolution_block_size */ LayoutUnit(), MinMaxSizesType::kContent)); // Pass if |ComputeMinMaxSize| doesn't hit DCHECK failures. } TEST_F(NGLineBreakerTest, RewindPositionedFloat) { SetBodyInnerHTML(R"HTML(
oB﭅|ﻼ )0{r 6
)HTML"); UpdateAllLifecyclePhasesForTest(); } // crbug.com/1091359 TEST_F(NGLineBreakerTest, RewindRubyRun) { NGInlineNode node = CreateInlineNode(R"HTML(
a B AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA )HTML"); node.ComputeMinMaxSizes( WritingMode::kHorizontalTb, MinMaxSizesInput(/* percentage_resolution_block_size */ LayoutUnit(), MinMaxSizesType::kContent)); // This test passes if no CHECK failures. } #undef MAYBE_OverflowAtomicInline } // namespace } // namespace blink