blob: 2f12a09fc930fadea3b58bcae97d61f320db01f1 [file] [log] [blame]
// 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<LayoutBlockFlow>(GetLayoutObjectByElementId("container"));
return NGInlineNode(block_flow);
}
// Break lines using the specified available width.
Vector<std::pair<String, unsigned>> 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<NGInlineBreakToken> break_token;
Vector<std::pair<String, unsigned>> lines;
trailing_whitespaces_.resize(0);
NGExclusionSpace exclusion_space;
NGPositionedFloatVector leading_floats;
NGLineLayoutOpportunity line_opportunity(available_width);
do {
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));
} while (break_token);
return lines;
}
Vector<NGLineBreaker::WhitespaceState> trailing_whitespaces_;
bool first_should_hang_trailing_space_;
LayoutUnit first_hang_width_;
};
namespace {
TEST_F(NGLineBreakerTest, FitWithEpsilon) {
LoadAhem();
NGInlineNode node = CreateInlineNode(R"HTML(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
width: 49.99px;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<div id=container>00000</div>
)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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
</style>
<div id=container>123 456 789</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
</style>
<div id=container>12345 678</div>
)HTML");
// The first line overflows, but the last line does not.
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
tab-size: 8;
white-space: pre-wrap;
width: 10ch;
}
</style>
<div id=container>12345&#9;&#9;678</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
tab-size: 8;
white-space: pre-wrap;
width: 10ch;
word-wrap: break-word;
}
</style>
<div id=container>12345&#9;&#9;678</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
span {
display: inline-block;
width: 30px;
height: 10px;
}
</style>
<div id=container>12345<span></span>678</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
span {
margin-right: 4em;
}
</style>
<div id=container><span>123 456</span> 789</div>
)HTML");
const Vector<NGInlineItem>& 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<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
div {
font: 10px/1 Ahem;
white-space: pre-wrap;
width: 10ch;
word-wrap: break-word;
}
</style>
<div id=container><span>12345 </span> 1234567890123</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
</style>
<div id=container>AAA AAA AAA <span>BB</span> CC</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Times;
letter-spacing: 10px;
width: 0px;
}
</style>
<div id=container>Star Wars</div>
)HTML");
Vector<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
</style>
<div id=container><span>123 456</span>789 abc</div>
)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<std::pair<String, unsigned>> 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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
}
</style>
<div id=container><span>123</span>456 789</div>
)HTML");
Vector<std::pair<String, unsigned>> 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 34<span>56</span>", "normal",
NGLineBreaker::WhitespaceState::kCollapsed},
{"12 34<span>56</span>", "pre-wrap",
NGLineBreaker::WhitespaceState::kPreserved},
// Atomic inlines.
{"12 <span style='display: inline-block'></span>", "normal",
NGLineBreaker::WhitespaceState::kNone},
// fast/text/whitespace/inline-whitespace-wrapping-4.html
{"<span style='white-space: nowrap'>1234 </span>"
"<span style='white-space: normal'> 5678</span>",
"pre", NGLineBreaker::WhitespaceState::kCollapsed},
};
std::ostream& operator<<(std::ostream& os,
const WhitespaceStateTestData& data) {
return os << static_cast<int>(data.expected) << " for '" << data.html
<< "' with 'white-space: " << data.white_space << "'";
}
class NGWhitespaceStateTest
: public NGLineBreakerTest,
public testing::WithParamInterface<WhitespaceStateTestData> {};
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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
width: 50px;
white-space: )HTML") + data.white_space +
R"HTML(
}
</style>
<div id=container>)HTML" + data.html +
R"HTML(</div>
)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<span> </span>", "pre", 1},
{"<span>1 </span> ", "pre", 2},
{"1<span> </span> ", "pre", 2},
{"1 <span> </span> ", "pre", 3},
{"1 \t", "pre", 3},
{"1 \n", "pre", 2},
{"1 <br>", "pre", 2},
{" ", "pre-wrap", 1},
{" ", "pre-wrap", 3},
{"1 ", "pre-wrap", 1},
{"1 ", "pre-wrap", 2},
{"1<span> </span>", "pre-wrap", 1},
{"<span>1 </span> ", "pre-wrap", 2},
{"1<span> </span> ", "pre-wrap", 2},
{"1 <span> </span> ", "pre-wrap", 3},
{"1 \t", "pre-wrap", 3},
{"1 <br>", "pre-wrap", 2},
{"12 1234", "pre-wrap", 1},
{"12 1234", "pre-wrap", 2},
};
class NGTrailingSpaceWidthTest
: public NGLineBreakerTest,
public testing::WithParamInterface<TrailingSpaceWidthTestData> {};
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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
width: 50px;
tab-size: 2;
white-space: )HTML") + data.white_space +
R"HTML(;
}
</style>
<div id=container>)HTML" + data.html +
R"HTML(</div>
)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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
white-space: pre-wrap;
}
</style>
<div id=container>12345 6789 </div>
)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(
<!DOCTYPE html>
<style>
#container {
font: 10px/1 Ahem;
overflow-wrap: break-word;
}
</style>
<div id=container>0123456789<b id=target> </b>ab</i></div>
)HTML");
To<Text>(GetElementById("target")->firstChild())
->splitText(0, ASSERT_NO_EXCEPTION);
UpdateAllLifecyclePhasesForTest();
Vector<std::pair<String, unsigned>> 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, ForcedBreakFollowedByCloseTag) {
SetBodyInnerHTML(R"HTML(
<!DOCTYPE html>
<div id="container">
<div><span>line<br></span></div>
<div>
<span>line<br></span>
</div>
<div>
<span>
line<br>
</span>
</div>
<div>
<span>line<br> </span>
</div>
<div>
<span>line<br> </span>&#32;&#32;
</div>
</div>
)HTML");
const LayoutObject* container = GetLayoutObjectByElementId("container");
for (const LayoutObject* child = container->SlowFirstChild(); child;
child = child->NextSibling()) {
NGInlineCursor cursor(*To<LayoutBlockFlow>(child));
wtf_size_t line_count = 0;
for (cursor.MoveToFirstLine(); cursor; cursor.MoveToNextLine())
++line_count;
EXPECT_EQ(line_count, 1u);
}
}
TEST_F(NGLineBreakerTest, TableCellWidthCalculationQuirkOutOfFlow) {
NGInlineNode node = CreateInlineNode(R"HTML(
<style>
table {
font-size: 10px;
width: 5ch;
}
</style>
<table><tr><td id=container>
1234567
<img style="position: absolute">
</td></tr></table>
)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(
<div style="float: left">
&#xe49d;oB&#xfb45;|&#xf237;&#xfefc;
)&#xe2c9;&#xea7a;0{r
6
<span style="float: left">
<span style="border-right: solid green 2.166621530302065e+19in"></span>
</span>
</div>
)HTML");
UpdateAllLifecyclePhasesForTest();
}
// crbug.com/1091359
TEST_F(NGLineBreakerTest, RewindRubyRun) {
NGInlineNode node = CreateInlineNode(R"HTML(
<div id="container">
<style>
* {
-webkit-text-security:square;
font-size:16px;
}
</style>
<big style="word-wrap: break-word">a
<ruby dir="rtl">
<rt>
B AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
<svg></svg>
<b>
</rt>
</ruby>
)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