| // Copyright 2016 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/inline/ng_inline_node.h" |
| |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/core/dom/dom_token_list.h" |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/html_names.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.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_layout_algorithm.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" |
| #include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" |
| #include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/core/svg_names.h" |
| |
| namespace blink { |
| |
| // The spec turned into a discussion that may change. Put this logic on hold |
| // until CSSWG resolves the issue. |
| // https://github.com/w3c/csswg-drafts/issues/337 |
| #define SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH 0 |
| |
| using ::testing::ElementsAre; |
| |
| class NGInlineNodeForTest : public NGInlineNode { |
| public: |
| using NGInlineNode::NGInlineNode; |
| |
| std::string Text() const { return Data().text_content.Utf8(); } |
| Vector<NGInlineItem>& Items() { return MutableData()->items; } |
| static Vector<NGInlineItem>& Items(NGInlineNodeData& data) { |
| return data.items; |
| } |
| |
| void Append(const String& text, LayoutObject* layout_object) { |
| NGInlineNodeData* data = MutableData(); |
| unsigned start = data->text_content.length(); |
| data->text_content = data->text_content + text; |
| data->items.push_back(NGInlineItem(NGInlineItem::kText, start, |
| start + text.length(), layout_object)); |
| data->is_empty_inline_ = false; |
| } |
| |
| void Append(UChar character) { |
| NGInlineNodeData* data = MutableData(); |
| data->text_content = data->text_content + character; |
| unsigned end = data->text_content.length(); |
| data->items.push_back( |
| NGInlineItem(NGInlineItem::kBidiControl, end - 1, end, nullptr)); |
| data->is_bidi_enabled_ = true; |
| data->is_empty_inline_ = false; |
| } |
| |
| void ClearText() { |
| NGInlineNodeData* data = MutableData(); |
| data->text_content = String(); |
| data->items.clear(); |
| data->is_empty_inline_ = true; |
| } |
| |
| void SegmentText() { |
| NGInlineNodeData* data = MutableData(); |
| data->is_bidi_enabled_ = true; |
| NGInlineNode::SegmentText(data); |
| } |
| |
| void CollectInlines() { NGInlineNode::CollectInlines(MutableData()); } |
| void ShapeText() { NGInlineNode::ShapeText(MutableData()); } |
| }; |
| |
| class NGInlineNodeTest : public NGLayoutTest { |
| protected: |
| void SetUp() override { |
| NGLayoutTest::SetUp(); |
| style_ = ComputedStyle::Create(); |
| } |
| |
| void SetupHtml(const char* id, String html) { |
| SetBodyInnerHTML(html); |
| layout_block_flow_ = ToLayoutNGBlockFlow(GetLayoutObjectByElementId(id)); |
| layout_object_ = layout_block_flow_->FirstChild(); |
| style_ = layout_object_ ? layout_object_->Style() : nullptr; |
| } |
| |
| void UseLayoutObjectAndAhem() { |
| // Get Ahem from document. Loading "Ahem.woff" using |createTestFont| fails |
| // on linux_chromium_asan_rel_ng. |
| LoadAhem(); |
| SetupHtml("t", "<div id=t style='font:10px Ahem'>test</div>"); |
| } |
| |
| NGInlineNodeForTest CreateInlineNode() { |
| if (!layout_block_flow_) |
| SetupHtml("t", "<div id=t style='font:10px'>test</div>"); |
| NGInlineNodeForTest node(layout_block_flow_); |
| node.InvalidatePrepareLayoutForTest(); |
| return node; |
| } |
| |
| MinMaxSizes ComputeMinMaxSizes(NGInlineNode node) { |
| return node |
| .ComputeMinMaxSizes( |
| node.Style().GetWritingMode(), |
| MinMaxSizesInput( |
| /* percentage_resolution_block_size */ LayoutUnit(), |
| MinMaxSizesType::kContent)) |
| .sizes; |
| } |
| |
| const String& GetText() const { |
| NGInlineNodeData* data = layout_block_flow_->GetNGInlineNodeData(); |
| CHECK(data); |
| return data->text_content; |
| } |
| |
| Vector<NGInlineItem>& Items() { |
| NGInlineNodeData* data = layout_block_flow_->GetNGInlineNodeData(); |
| CHECK(data); |
| return NGInlineNodeForTest::Items(*data); |
| } |
| |
| void ForceLayout() { GetDocument().body()->OffsetTop(); } |
| |
| Vector<unsigned> ToEndOffsetList( |
| NGInlineItemSegments::const_iterator segments) { |
| Vector<unsigned> end_offsets; |
| for (const RunSegmenter::RunSegmenterRange& segment : segments) |
| end_offsets.push_back(segment.end); |
| return end_offsets; |
| } |
| |
| void TestAnyItrermsAreDirty(LayoutBlockFlow* block_flow, bool expected) { |
| const NGFragmentItems* items = block_flow->FragmentItems(); |
| items->DirtyLinesFromNeedsLayout(block_flow); |
| // Check |NGFragmentItem::IsDirty| directly without using |
| // |EndOfReusableItems|. This is different from the line cache logic, but |
| // some items may not be reusable even if |!IsDirty()|. |
| const bool is_any_items_dirty = |
| std::any_of(items->Items().begin(), items->Items().end(), |
| [](const NGFragmentItem& item) { return item.IsDirty(); }); |
| EXPECT_EQ(is_any_items_dirty, expected); |
| } |
| |
| scoped_refptr<const ComputedStyle> style_; |
| LayoutNGBlockFlow* layout_block_flow_ = nullptr; |
| LayoutObject* layout_object_ = nullptr; |
| FontCachePurgePreventer purge_preventer_; |
| }; |
| |
| #define TEST_ITEM_TYPE_OFFSET(item, type, start, end) \ |
| EXPECT_EQ(NGInlineItem::type, item.Type()); \ |
| EXPECT_EQ(start, item.StartOffset()); \ |
| EXPECT_EQ(end, item.EndOffset()) |
| |
| #define TEST_ITEM_TYPE_OFFSET_LEVEL(item, type, start, end, level) \ |
| EXPECT_EQ(NGInlineItem::type, item.Type()); \ |
| EXPECT_EQ(start, item.StartOffset()); \ |
| EXPECT_EQ(end, item.EndOffset()); \ |
| EXPECT_EQ(level, item.BidiLevel()) |
| |
| #define TEST_ITEM_OFFSET_DIR(item, start, end, direction) \ |
| EXPECT_EQ(start, item.StartOffset()); \ |
| EXPECT_EQ(end, item.EndOffset()); \ |
| EXPECT_EQ(direction, item.Direction()) |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesText) { |
| SetupHtml("t", "<div id=t>Hello <span>inline</span> world.</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_FALSE(node.IsBidiEnabled()); |
| Vector<NGInlineItem>& items = node.Items(); |
| TEST_ITEM_TYPE_OFFSET(items[0], kText, 0u, 6u); |
| TEST_ITEM_TYPE_OFFSET(items[1], kOpenTag, 6u, 6u); |
| TEST_ITEM_TYPE_OFFSET(items[2], kText, 6u, 12u); |
| TEST_ITEM_TYPE_OFFSET(items[3], kCloseTag, 12u, 12u); |
| TEST_ITEM_TYPE_OFFSET(items[4], kText, 12u, 19u); |
| EXPECT_EQ(5u, items.size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesBR) { |
| SetupHtml("t", u"<div id=t>Hello<br>World</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_EQ("Hello\nWorld", node.Text()); |
| EXPECT_FALSE(node.IsBidiEnabled()); |
| Vector<NGInlineItem>& items = node.Items(); |
| TEST_ITEM_TYPE_OFFSET(items[0], kText, 0u, 5u); |
| TEST_ITEM_TYPE_OFFSET(items[1], kControl, 5u, 6u); |
| TEST_ITEM_TYPE_OFFSET(items[2], kText, 6u, 11u); |
| EXPECT_EQ(3u, items.size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesFloat) { |
| SetupHtml("t", |
| "<div id=t>" |
| "abc" |
| "<span style='float:right'>DEF</span>" |
| "ghi" |
| "<span style='float:left'>JKL</span>" |
| "mno" |
| "</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_EQ(u8"abc\uFFFCghi\uFFFCmno", node.Text()) |
| << "floats are appeared as an object replacement character"; |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(5u, items.size()); |
| TEST_ITEM_TYPE_OFFSET(items[0], kText, 0u, 3u); |
| TEST_ITEM_TYPE_OFFSET(items[1], kFloating, 3u, 4u); |
| TEST_ITEM_TYPE_OFFSET(items[2], kText, 4u, 7u); |
| TEST_ITEM_TYPE_OFFSET(items[3], kFloating, 7u, 8u); |
| TEST_ITEM_TYPE_OFFSET(items[4], kText, 8u, 11u); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesInlineBlock) { |
| SetupHtml("t", |
| "<div id=t>" |
| "abc<span style='display:inline-block'>DEF</span>jkl" |
| "</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_EQ(u8"abc\uFFFCjkl", node.Text()) |
| << "inline-block is appeared as an object replacement character"; |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(3u, items.size()); |
| TEST_ITEM_TYPE_OFFSET(items[0], kText, 0u, 3u); |
| TEST_ITEM_TYPE_OFFSET(items[1], kAtomicInline, 3u, 4u); |
| TEST_ITEM_TYPE_OFFSET(items[2], kText, 4u, 7u); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesUTF16) { |
| SetupHtml("t", u"<div id=t>Hello \u3042</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| // |CollectInlines()| sets |IsBidiEnabled()| for any UTF-16 strings. |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| // |SegmentText()| analyzes the string and resets |IsBidiEnabled()| if all |
| // characters are LTR. |
| node.SegmentText(); |
| EXPECT_FALSE(node.IsBidiEnabled()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesRtl) { |
| SetupHtml("t", u"<div id=t>Hello \u05E2</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| node.SegmentText(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesRtlWithSpan) { |
| SetupHtml("t", u"<div id=t dir=rtl>\u05E2 <span>\u05E2</span> \u05E2</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| node.SegmentText(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| Vector<NGInlineItem>& items = node.Items(); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[0], kText, 0u, 2u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[1], kOpenTag, 2u, 2u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[2], kText, 2u, 3u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[3], kCloseTag, 3u, 3u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[4], kText, 3u, 5u, 1u); |
| EXPECT_EQ(5u, items.size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesMixedText) { |
| SetupHtml("t", u"<div id=t>Hello, \u05E2 <span>\u05E2</span></div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| node.SegmentText(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| Vector<NGInlineItem>& items = node.Items(); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[0], kText, 0u, 7u, 0u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[1], kText, 7u, 9u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[2], kOpenTag, 9u, 9u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[3], kText, 9u, 10u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[4], kCloseTag, 10u, 10u, 1u); |
| EXPECT_EQ(5u, items.size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesMixedTextEndWithON) { |
| SetupHtml("t", u"<div id=t>Hello, \u05E2 <span>\u05E2!</span></div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.CollectInlines(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| node.SegmentText(); |
| EXPECT_TRUE(node.IsBidiEnabled()); |
| Vector<NGInlineItem>& items = node.Items(); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[0], kText, 0u, 7u, 0u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[1], kText, 7u, 9u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[2], kOpenTag, 9u, 9u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[3], kText, 9u, 10u, 1u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[4], kText, 10u, 11u, 0u); |
| TEST_ITEM_TYPE_OFFSET_LEVEL(items[5], kCloseTag, 11u, 11u, 0u); |
| EXPECT_EQ(6u, items.size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentASCII) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.Append("Hello", layout_object_); |
| node.SegmentText(); |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(1u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 5u, TextDirection::kLtr); |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentHebrew) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.Append(u"\u05E2\u05D1\u05E8\u05D9\u05EA", layout_object_); |
| node.SegmentText(); |
| ASSERT_EQ(1u, node.Items().size()); |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(1u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 5u, TextDirection::kRtl); |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentSplit1To2) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.Append(u"Hello \u05E2\u05D1\u05E8\u05D9\u05EA", layout_object_); |
| node.SegmentText(); |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(2u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 6u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[1], 6u, 11u, TextDirection::kRtl); |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentSplit3To4) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.Append("Hel", layout_object_); |
| node.Append(u"lo \u05E2", layout_object_); |
| node.Append(u"\u05D1\u05E8\u05D9\u05EA", layout_object_); |
| node.SegmentText(); |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(4u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 3u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[1], 3u, 6u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[2], 6u, 7u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[3], 7u, 11u, TextDirection::kRtl); |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentBidiOverride) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node.Append("Hello ", layout_object_); |
| node.Append(kRightToLeftOverrideCharacter); |
| node.Append("ABC", layout_object_); |
| node.Append(kPopDirectionalFormattingCharacter); |
| node.SegmentText(); |
| Vector<NGInlineItem>& items = node.Items(); |
| ASSERT_EQ(4u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 6u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[1], 6u, 7u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[2], 7u, 10u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[3], 10u, 11u, TextDirection::kLtr); |
| } |
| |
| static NGInlineNodeForTest CreateBidiIsolateNode(NGInlineNodeForTest node, |
| LayoutObject* layout_object) { |
| node.Append("Hello ", layout_object); |
| node.Append(kRightToLeftIsolateCharacter); |
| node.Append(u"\u05E2\u05D1\u05E8\u05D9\u05EA ", layout_object); |
| node.Append(kLeftToRightIsolateCharacter); |
| node.Append("A", layout_object); |
| node.Append(kPopDirectionalIsolateCharacter); |
| node.Append(u"\u05E2\u05D1\u05E8\u05D9\u05EA", layout_object); |
| node.Append(kPopDirectionalIsolateCharacter); |
| node.Append(" World", layout_object); |
| node.SegmentText(); |
| return node; |
| } |
| |
| TEST_F(NGInlineNodeTest, SegmentBidiIsolate) { |
| NGInlineNodeForTest node = CreateInlineNode(); |
| node = CreateBidiIsolateNode(node, layout_object_); |
| Vector<NGInlineItem>& items = node.Items(); |
| EXPECT_EQ(9u, items.size()); |
| TEST_ITEM_OFFSET_DIR(items[0], 0u, 6u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[1], 6u, 7u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[2], 7u, 13u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[3], 13u, 14u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[4], 14u, 15u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[5], 15u, 16u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[6], 16u, 21u, TextDirection::kRtl); |
| TEST_ITEM_OFFSET_DIR(items[7], 21u, 22u, TextDirection::kLtr); |
| TEST_ITEM_OFFSET_DIR(items[8], 22u, 28u, TextDirection::kLtr); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizes) { |
| LoadAhem(); |
| SetupHtml("t", "<div id=t style='font:10px Ahem'>AB CDEF</div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| EXPECT_EQ(40, sizes.min_size); |
| EXPECT_EQ(70, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizesElementBoundary) { |
| LoadAhem(); |
| SetupHtml("t", "<div id=t style='font:10px Ahem'>A B<span>C D</span></div>"); |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| // |min_content| should be the width of "BC" because there is an element |
| // boundary between "B" and "C" but no break opportunities. |
| EXPECT_EQ(20, sizes.min_size); |
| EXPECT_EQ(60, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizesFloats) { |
| LoadAhem(); |
| SetupHtml("t", R"HTML( |
| <style> |
| #left { float: left; width: 50px; } |
| </style> |
| <div id=t style="font: 10px Ahem"> |
| XXX <div id="left"></div> XXXX |
| </div> |
| )HTML"); |
| |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| |
| EXPECT_EQ(50, sizes.min_size); |
| EXPECT_EQ(130, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizesCloseTagAfterForcedBreak) { |
| LoadAhem(); |
| SetupHtml("t", R"HTML( |
| <style> |
| span { border: 30px solid blue; } |
| </style> |
| <div id=t style="font: 10px Ahem"> |
| <span>12<br></span> |
| </div> |
| )HTML"); |
| |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| // The right border of the `</span>` is included in the line even if it |
| // appears after `<br>`. crbug.com/991320. |
| EXPECT_EQ(80, sizes.min_size); |
| EXPECT_EQ(80, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizesFloatsClearance) { |
| LoadAhem(); |
| SetupHtml("t", R"HTML( |
| <style> |
| #left { float: left; width: 40px; } |
| #right { float: right; clear: left; width: 50px; } |
| </style> |
| <div id=t style="font: 10px Ahem"> |
| XXX <div id="left"></div><div id="right"></div><div id="left"></div> XXX |
| </div> |
| )HTML"); |
| |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| |
| EXPECT_EQ(50, sizes.min_size); |
| EXPECT_EQ(160, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, MinMaxSizesTabulationWithBreakWord) { |
| LoadAhem(); |
| SetupHtml("t", R"HTML( |
| <style> |
| #t { |
| font: 10px Ahem; |
| white-space: pre-wrap; |
| word-break: break-word; |
| } |
| </style> |
| <div id=t>		<span>X</span></div> |
| )HTML"); |
| |
| NGInlineNodeForTest node = CreateInlineNode(); |
| MinMaxSizes sizes = ComputeMinMaxSizes(node); |
| EXPECT_EQ(160, sizes.min_size); |
| EXPECT_EQ(170, sizes.max_size); |
| } |
| |
| TEST_F(NGInlineNodeTest, AssociatedItemsWithControlItem) { |
| SetBodyInnerHTML( |
| "<pre id=t style='-webkit-rtl-ordering:visual'>ab\nde</pre>"); |
| LayoutText* const layout_text = ToLayoutText( |
| GetDocument().getElementById("t")->firstChild()->GetLayoutObject()); |
| ASSERT_TRUE(layout_text->HasValidInlineItems()); |
| Vector<const NGInlineItem*> items; |
| for (const NGInlineItem& item : layout_text->InlineItems()) |
| items.push_back(&item); |
| ASSERT_EQ(5u, items.size()); |
| TEST_ITEM_TYPE_OFFSET((*items[0]), kText, 1u, 3u); |
| TEST_ITEM_TYPE_OFFSET((*items[1]), kBidiControl, 3u, 4u); |
| TEST_ITEM_TYPE_OFFSET((*items[2]), kControl, 4u, 5u); |
| TEST_ITEM_TYPE_OFFSET((*items[3]), kBidiControl, 5u, 6u); |
| TEST_ITEM_TYPE_OFFSET((*items[4]), kText, 6u, 8u); |
| } |
| |
| TEST_F(NGInlineNodeTest, NeedsCollectInlinesOnSetText) { |
| SetBodyInnerHTML(R"HTML( |
| <div id="container"> |
| <span id="previous"></span> |
| <span id="parent">old</span> |
| <span id="next"></span> |
| </div> |
| )HTML"); |
| |
| Element* container = GetElementById("container"); |
| Element* parent = GetElementById("parent"); |
| auto* text = To<Text>(parent->firstChild()); |
| EXPECT_FALSE(text->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| text->setData("new"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| // The text and ancestors up to the container should be marked. |
| EXPECT_TRUE(text->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_TRUE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_TRUE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| // Siblings of |parent| should stay clean. |
| Element* previous = GetElementById("previous"); |
| Element* next = GetElementById("next"); |
| EXPECT_FALSE(previous->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(next->GetLayoutObject()->NeedsCollectInlines()); |
| } |
| |
| struct StyleChangeData { |
| const char* css; |
| enum ChangedElements { |
| kText = 1, |
| kParent = 2, |
| kContainer = 4, |
| |
| kNone = 0, |
| kTextAndParent = kText | kParent, |
| kParentAndAbove = kParent | kContainer, |
| kAll = kText | kParentAndAbove, |
| }; |
| unsigned needs_collect_inlines; |
| base::Optional<bool> is_line_dirty; |
| } style_change_data[] = { |
| // Changing color, text-decoration, etc. should not re-run |
| // |CollectInlines()|. |
| {"#parent.after { color: red; }", StyleChangeData::kNone, false}, |
| {"#parent.after { text-decoration-line: underline; }", |
| StyleChangeData::kNone, false}, |
| // Changing fonts should re-run |CollectInlines()|. |
| {"#parent.after { font-size: 200%; }", StyleChangeData::kAll, true}, |
| // Changing from/to out-of-flow should re-rerun |CollectInlines()|. |
| {"#parent.after { position: absolute; }", StyleChangeData::kContainer, |
| true}, |
| {"#parent { position: absolute; }" |
| "#parent.after { position: initial; }", |
| StyleChangeData::kContainer, true}, |
| // List markers are captured in |NGInlineItem|. |
| {"#parent.after { display: list-item; }", StyleChangeData::kContainer}, |
| {"#parent { display: list-item; list-style-type: none; }" |
| "#parent.after { list-style-type: disc; }", |
| StyleChangeData::kParent}, |
| {"#parent { display: list-item; }" |
| "#container.after { list-style-type: none; }", |
| StyleChangeData::kParent}, |
| // Changing properties related with bidi resolution should re-run |
| // |CollectInlines()|. |
| {"#parent.after { unicode-bidi: bidi-override; }", |
| StyleChangeData::kParentAndAbove, true}, |
| {"#container.after { unicode-bidi: bidi-override; }", |
| StyleChangeData::kContainer, false}, |
| }; |
| |
| std::ostream& operator<<(std::ostream& os, const StyleChangeData& data) { |
| return os << data.css; |
| } |
| |
| class StyleChangeTest : public NGInlineNodeTest, |
| public testing::WithParamInterface<StyleChangeData> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(NGInlineNodeTest, |
| StyleChangeTest, |
| testing::ValuesIn(style_change_data)); |
| |
| TEST_P(StyleChangeTest, NeedsCollectInlinesOnStyle) { |
| auto data = GetParam(); |
| SetBodyInnerHTML(String(R"HTML( |
| <style> |
| )HTML") + data.css + |
| R"HTML( |
| </style> |
| <div id="container"> |
| <span id="previous"></span> |
| <span id="parent">text</span> |
| <span id="next"></span> |
| </div> |
| )HTML"); |
| |
| Element* container = GetElementById("container"); |
| Element* parent = GetElementById("parent"); |
| auto* text = To<Text>(parent->firstChild()); |
| EXPECT_FALSE(text->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| container->classList().Add("after"); |
| parent->classList().Add("after"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| // The text and ancestors up to the container should be marked. |
| unsigned changes = StyleChangeData::kNone; |
| if (text->GetLayoutObject()->NeedsCollectInlines()) |
| changes |= StyleChangeData::kText; |
| if (parent->GetLayoutObject()->NeedsCollectInlines()) |
| changes |= StyleChangeData::kParent; |
| if (container->GetLayoutObject()->NeedsCollectInlines()) |
| changes |= StyleChangeData::kContainer; |
| EXPECT_EQ(changes, data.needs_collect_inlines); |
| |
| // Siblings of |parent| should stay clean. |
| Element* previous = GetElementById("previous"); |
| Element* next = GetElementById("next"); |
| EXPECT_FALSE(previous->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(next->GetLayoutObject()->NeedsCollectInlines()); |
| |
| if (data.is_line_dirty && |
| RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()) { |
| TestAnyItrermsAreDirty(To<LayoutBlockFlow>(container->GetLayoutObject()), |
| *data.is_line_dirty); |
| } |
| |
| ForceLayout(); // Ensure running layout does not crash. |
| } |
| |
| using CreateNode = Node* (*)(Document&); |
| static CreateNode node_creators[] = { |
| [](Document& document) -> Node* { return document.createTextNode("new"); }, |
| [](Document& document) -> Node* { |
| return document.CreateRawElement(html_names::kSpanTag); |
| }, |
| [](Document& document) -> Node* { |
| Element* element = document.CreateRawElement(html_names::kSpanTag); |
| element->classList().Add("abspos"); |
| return element; |
| }, |
| [](Document& document) -> Node* { |
| Element* element = document.CreateRawElement(html_names::kSpanTag); |
| element->classList().Add("float"); |
| return element; |
| }}; |
| |
| class NodeInsertTest : public NGInlineNodeTest, |
| public testing::WithParamInterface<CreateNode> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(NGInlineNodeTest, |
| NodeInsertTest, |
| testing::ValuesIn(node_creators)); |
| |
| TEST_P(NodeInsertTest, NeedsCollectInlinesOnInsert) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| .abspos { position: absolute; } |
| .float { float: left; } |
| </style> |
| <div id="container"> |
| <span id="previous"></span> |
| <span id="parent"></span> |
| <span id="next"></span> |
| </div> |
| )HTML"); |
| |
| Element* container = GetElementById("container"); |
| Element* parent = GetElementById("parent"); |
| EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| Node* insert = (*GetParam())(GetDocument()); |
| parent->appendChild(insert); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| // Ancestors up to the container should be marked. |
| EXPECT_TRUE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_TRUE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| // Siblings of |parent| should stay clean. |
| Element* previous = GetElementById("previous"); |
| Element* next = GetElementById("next"); |
| EXPECT_FALSE(previous->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(next->GetLayoutObject()->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, NeedsCollectInlinesOnInsertToOutOfFlowButton) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| #xflex { display: flex; } |
| </style> |
| <div id="container"> |
| <button id="flex" style="position: absolute"></button> |
| </div> |
| )HTML"); |
| |
| Element* container = GetElementById("container"); |
| Element* parent = ElementTraversal::FirstChild(*container); |
| Element* child = GetDocument().CreateRawElement(html_names::kDivTag); |
| parent->appendChild(child); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); |
| } |
| |
| class NodeRemoveTest : public NGInlineNodeTest, |
| public testing::WithParamInterface<const char*> {}; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| NGInlineNodeTest, |
| NodeRemoveTest, |
| testing::Values(nullptr, "span", "abspos", "float", "inline-block", "img")); |
| |
| TEST_P(NodeRemoveTest, NeedsCollectInlinesOnRemove) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| .abspos { position: absolute; } |
| .float { float: left; } |
| .inline-block { display: inline-block; } |
| </style> |
| <div id="container"> |
| <span id="previous"></span> |
| <span id="parent"> |
| text |
| <span id="span">span</span> |
| <span id="abspos">abspos</span> |
| <span id="float">float</span> |
| <span id="inline-block">inline-block</span> |
| <img id="img"> |
| </span> |
| <span id="next"></span> |
| </div> |
| )HTML"); |
| |
| Element* container = GetElementById("container"); |
| Element* parent = GetElementById("parent"); |
| EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| const char* id = GetParam(); |
| if (id) { |
| Element* target = GetElementById(GetParam()); |
| target->remove(); |
| } else { |
| Node* target = parent->firstChild(); |
| target->remove(); |
| } |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| // Ancestors up to the container should be marked. |
| EXPECT_TRUE(parent->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_TRUE(container->GetLayoutObject()->NeedsCollectInlines()); |
| |
| // Siblings of |parent| should stay clean. |
| Element* previous = GetElementById("previous"); |
| Element* next = GetElementById("next"); |
| EXPECT_FALSE(previous->GetLayoutObject()->NeedsCollectInlines()); |
| EXPECT_FALSE(next->GetLayoutObject()->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, NeedsCollectInlinesOnForceLayout) { |
| SetBodyInnerHTML(R"HTML( |
| <div id="container"> |
| <span id="target"> |
| <span id="child" style="position: absolute">X</span> |
| </span> |
| </div> |
| )HTML"); |
| |
| LayoutObject* container = GetLayoutObjectByElementId("container"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| LayoutObject* child = GetLayoutObjectByElementId("child"); |
| child->ForceLayout(); |
| EXPECT_FALSE(container->NeedsCollectInlines()); |
| EXPECT_FALSE(target->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollectInlinesShouldNotClearFirstInlineFragment) { |
| SetBodyInnerHTML(R"HTML( |
| <div id="container"> |
| text |
| </div> |
| )HTML"); |
| |
| // Appending a child should set |NeedsCollectInlines|. |
| Element* container = GetElementById("container"); |
| container->appendChild(GetDocument().createTextNode("add")); |
| auto* block_flow = To<LayoutBlockFlow>(container->GetLayoutObject()); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| EXPECT_TRUE(block_flow->NeedsCollectInlines()); |
| |
| // |IsEmptyInline| should run |CollectInlines|. |
| NGInlineNode node(block_flow); |
| node.IsEmptyInline(); |
| EXPECT_FALSE(block_flow->NeedsCollectInlines()); |
| |
| // Running |CollectInlines| should not clear |FirstInlineFragment|. |
| LayoutObject* first_child = container->firstChild()->GetLayoutObject(); |
| if (RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()) { |
| // TODO(yosin): We should use |FirstInlineItemFragmentIndex()| once we |
| // implement it. |
| } else { |
| EXPECT_NE(first_child->FirstInlineFragment(), nullptr); |
| } |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateAddSpan) { |
| SetupHtml("t", "<div id=t>before</div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| auto* parent = To<Element>(layout_block_flow_->GetNode()); |
| Element* span = GetDocument().CreateRawElement(html_names::kSpanTag); |
| parent->appendChild(span); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, open/close items should be added. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before + 2, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateRemoveSpan) { |
| SetupHtml("t", "<div id=t><span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->remove(); |
| EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateAddInnerSpan) { |
| SetupHtml("t", "<div id=t><span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| Element* parent = GetElementById("x"); |
| ASSERT_TRUE(parent); |
| Element* span = GetDocument().CreateRawElement(html_names::kSpanTag); |
| parent->appendChild(span); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, open/close items should be added. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before + 2, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateRemoveInnerSpan) { |
| SetupHtml("t", "<div id=t><span><span id=x></span></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->remove(); |
| EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateSetText) { |
| SetupHtml("t", "<div id=t>before</div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| |
| LayoutText* text = ToLayoutText(layout_block_flow_->FirstChild()); |
| text->SetTextIfNeeded(String("after").Impl()); |
| EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateAddAbsolute) { |
| SetupHtml("t", |
| "<style>span { position: absolute; }</style>" |
| "<div id=t>before</div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| auto* parent = To<Element>(layout_block_flow_->GetNode()); |
| Element* span = GetDocument().CreateRawElement(html_names::kSpanTag); |
| parent->appendChild(span); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, an OOF item should be added. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before + 1, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateRemoveAbsolute) { |
| SetupHtml("t", |
| "<style>span { position: absolute; }</style>" |
| "<div id=t>before<span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->remove(); |
| EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateChangeToAbsolute) { |
| SetupHtml("t", |
| "<style>#y { position: absolute; }</style>" |
| "<div id=t>before<span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->SetIdAttribute("y"); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, an open/close items should be replaced with an |
| // OOF item. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before - 1, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateChangeFromAbsolute) { |
| SetupHtml("t", |
| "<style>#x { position: absolute; }</style>" |
| "<div id=t>before<span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->SetIdAttribute("y"); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, an OOF item should be replaced with open/close |
| // items.. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before + 1, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateAddFloat) { |
| SetupHtml("t", |
| "<style>span { float: left; }</style>" |
| "<div id=t>before</div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| unsigned item_count_before = Items().size(); |
| |
| auto* parent = To<Element>(layout_block_flow_->GetNode()); |
| Element* span = GetDocument().CreateRawElement(html_names::kSpanTag); |
| parent->appendChild(span); |
| |
| // NeedsCollectInlines() is marked during the layout. |
| // By re-collecting inlines, an float item should be added. |
| ForceLayout(); |
| EXPECT_EQ(item_count_before + 1, Items().size()); |
| } |
| |
| TEST_F(NGInlineNodeTest, InvalidateRemoveFloat) { |
| SetupHtml("t", |
| "<style>span { float: left; }</style>" |
| "<div id=t>before<span id=x></span></div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| span->remove(); |
| EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| } |
| |
| TEST_F(NGInlineNodeTest, SpaceRestoredByInsertingWord) { |
| SetupHtml("t", "<div id=t>before <span id=x></span> after</div>"); |
| EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); |
| EXPECT_EQ(String("before after"), GetText()); |
| |
| Element* span = GetElementById("x"); |
| ASSERT_TRUE(span); |
| Text* text = Text::Create(GetDocument(), "mid"); |
| span->appendChild(text); |
| // EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); |
| |
| ForceLayout(); |
| EXPECT_EQ(String("before mid after"), GetText()); |
| } |
| |
| TEST_F(NGInlineNodeTest, RemoveInlineNodeDataIfBlockBecomesEmpty1) { |
| SetupHtml("container", "<div id=container><b id=remove><i>foo</i></b></div>"); |
| ASSERT_TRUE(layout_block_flow_->HasNGInlineNodeData()); |
| |
| Element* to_remove = GetElementById("remove"); |
| to_remove->remove(); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_FALSE(layout_block_flow_->HasNGInlineNodeData()); |
| } |
| |
| TEST_F(NGInlineNodeTest, RemoveInlineNodeDataIfBlockBecomesEmpty2) { |
| SetupHtml("container", "<div id=container><b><i>foo</i></b></div>"); |
| ASSERT_TRUE(layout_block_flow_->HasNGInlineNodeData()); |
| |
| GetElementById("container")->setInnerHTML(""); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_FALSE(layout_block_flow_->HasNGInlineNodeData()); |
| } |
| |
| TEST_F(NGInlineNodeTest, RemoveInlineNodeDataIfBlockObtainsBlockChild) { |
| SetupHtml("container", |
| "<div id=container><b id=blockify><i>foo</i></b></div>"); |
| ASSERT_TRUE(layout_block_flow_->HasNGInlineNodeData()); |
| |
| GetElementById("blockify") |
| ->SetInlineStyleProperty(CSSPropertyID::kDisplay, CSSValueID::kBlock); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_FALSE(layout_block_flow_->HasNGInlineNodeData()); |
| } |
| |
| // Test inline objects are initialized when |SplitFlow()| moves them. |
| TEST_F(NGInlineNodeTest, ClearFirstInlineFragmentOnSplitFlow) { |
| SetBodyInnerHTML(R"HTML( |
| <div> |
| <span id=outer_span> |
| <span id=inner_span>1234</span> |
| </span> |
| </div> |
| )HTML"); |
| |
| // Keep the text fragment to compare later. |
| Element* inner_span = GetElementById("inner_span"); |
| Node* text = inner_span->firstChild(); |
| NGInlineCursor before_split; |
| before_split.MoveTo(*text->GetLayoutObject()); |
| EXPECT_TRUE(before_split); |
| scoped_refptr<const NGPaintFragment> text_fragment_before_split = |
| before_split.Current().PaintFragment(); |
| |
| // Append <div> to <span>. causing SplitFlow(). |
| Element* outer_span = GetElementById("outer_span"); |
| Element* div = GetDocument().CreateRawElement(html_names::kDivTag); |
| outer_span->appendChild(div); |
| |
| // Update tree but do NOT update layout. At this point, there's no guarantee, |
| // but there are some clients (e.g., Scroll Anchor) who try to read |
| // associated fragments. |
| // |
| // NGPaintFragment is owned by LayoutNGBlockFlow. Because the original owner |
| // no longer has an inline formatting context, the NGPaintFragment subtree is |
| // destroyed, and should not be accessible. |
| GetDocument().UpdateStyleAndLayoutTree(); |
| EXPECT_FALSE(text->GetLayoutObject()->IsInLayoutNGInlineFormattingContext()); |
| EXPECT_FALSE(text->GetLayoutObject()->HasInlineFragments()); |
| |
| // Update layout. There should be a different instance of the text fragment. |
| UpdateAllLifecyclePhasesForTest(); |
| NGInlineCursor after_layout; |
| after_layout.MoveTo(*text->GetLayoutObject()); |
| EXPECT_TRUE(after_layout); |
| if (!RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()) { |
| EXPECT_NE(text_fragment_before_split.get(), |
| after_layout.Current().PaintFragment()); |
| } |
| |
| // Check it is the one owned by the new root inline formatting context. |
| LayoutBlock* anonymous_block = |
| inner_span->GetLayoutObject()->ContainingBlock(); |
| EXPECT_TRUE(anonymous_block->IsAnonymous()); |
| NGInlineCursor anonymous_block_cursor(*To<LayoutBlockFlow>(anonymous_block)); |
| anonymous_block_cursor.MoveToFirstLine(); |
| anonymous_block_cursor.MoveToFirstChild(); |
| EXPECT_TRUE(anonymous_block_cursor); |
| EXPECT_EQ(anonymous_block_cursor.Current().GetLayoutObject(), |
| text->GetLayoutObject()); |
| if (!RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()) { |
| EXPECT_EQ(anonymous_block_cursor.Current().PaintFragment(), |
| after_layout.Current().PaintFragment()); |
| } |
| } |
| |
| TEST_F(NGInlineNodeTest, AddChildToSVGRoot) { |
| SetBodyInnerHTML(R"HTML( |
| <div id="container"> |
| text |
| <svg id="svg"></svg> |
| </div> |
| )HTML"); |
| |
| Element* svg = GetElementById("svg"); |
| svg->appendChild(GetDocument().CreateRawElement(svg_names::kTextTag)); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| |
| LayoutObject* container = GetLayoutObjectByElementId("container"); |
| EXPECT_FALSE(container->NeedsCollectInlines()); |
| } |
| |
| // https://crbug.com/911220 |
| TEST_F(NGInlineNodeTest, PreservedNewlineWithBidiAndRelayout) { |
| SetupHtml("container", |
| "<style>span{unicode-bidi:isolate}</style>" |
| "<pre id=container>foo<span>\n</span>bar<br></pre>"); |
| EXPECT_EQ(String(u"foo\u2066\u2069\n\u2066\u2069bar\n"), GetText()); |
| |
| Node* new_text = Text::Create(GetDocument(), "baz"); |
| GetElementById("container")->appendChild(new_text); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // The bidi context popping and re-entering should be preserved around '\n'. |
| EXPECT_EQ(String(u"foo\u2066\u2069\n\u2066\u2069bar\nbaz"), GetText()); |
| } |
| |
| TEST_F(NGInlineNodeTest, PreservedNewlineWithRemovedBidiAndRelayout) { |
| SetupHtml("container", |
| "<pre id=container>foo<span dir=rtl>\nbar</span></pre>"); |
| EXPECT_EQ(String(u"foo\u2067\u2069\n\u2067bar\u2069"), GetText()); |
| |
| GetDocument().QuerySelector("span")->removeAttribute(html_names::kDirAttr); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // The bidi control characters around '\n' should not preserve |
| EXPECT_EQ("foo\nbar", GetText()); |
| } |
| |
| TEST_F(NGInlineNodeTest, PreservedNewlineWithRemovedLtrDirAndRelayout) { |
| SetupHtml("container", |
| "<pre id=container>foo<span dir=ltr>\nbar</span></pre>"); |
| EXPECT_EQ(String(u"foo\u2066\u2069\n\u2066bar\u2069"), GetText()); |
| |
| GetDocument().QuerySelector("span")->removeAttribute(html_names::kDirAttr); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // The bidi control characters around '\n' should not preserve |
| EXPECT_EQ("foo\nbar", GetText()); |
| } |
| |
| // https://crbug.com/969089 |
| TEST_F(NGInlineNodeTest, InsertedWBRWithLineBreakInRelayout) { |
| SetupHtml("container", "<div id=container><span>foo</span>\nbar</div>"); |
| EXPECT_EQ("foo bar", GetText()); |
| |
| Element* div = GetElementById("container"); |
| Element* wbr = GetDocument().CreateElementForBinding("wbr"); |
| div->insertBefore(wbr, div->lastChild()); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // The '\n' should be collapsed by the inserted <wbr> |
| EXPECT_EQ(String(u"foo\u200Bbar"), GetText()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollapsibleSpaceFollowingBRWithNoWrapStyle) { |
| SetupHtml("t", "<div id=t><span style=white-space:pre><br></span> </div>"); |
| EXPECT_EQ("\n", GetText()); |
| |
| GetDocument().QuerySelector("span")->removeAttribute(html_names::kStyleAttr); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ("\n", GetText()); |
| } |
| |
| TEST_F(NGInlineNodeTest, CollapsibleSpaceFollowingNewlineWithPreStyle) { |
| SetupHtml("t", "<div id=t><span style=white-space:pre>\n</span> </div>"); |
| EXPECT_EQ("\n", GetText()); |
| |
| GetDocument().QuerySelector("span")->removeAttribute(html_names::kStyleAttr); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ("", GetText()); |
| } |
| |
| #if SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH |
| // https://crbug.com/879088 |
| TEST_F(NGInlineNodeTest, RemoveSegmentBreakFromJapaneseInRelayout) { |
| SetupHtml("container", |
| u"<div id=container>" |
| u"<span>\u30ED\u30B0\u30A4\u30F3</span>" |
| u"\n" |
| u"<span>\u767B\u9332</span>" |
| u"<br></div>"); |
| EXPECT_EQ(String(u"\u30ED\u30B0\u30A4\u30F3\u767B\u9332\n"), GetText()); |
| |
| Node* new_text = Text::Create(GetDocument(), "foo"); |
| GetElementById("container")->appendChild(new_text); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(String(u"\u30ED\u30B0\u30A4\u30F3\u767B\u9332\nfoo"), GetText()); |
| } |
| |
| // https://crbug.com/879088 |
| TEST_F(NGInlineNodeTest, RemoveSegmentBreakFromJapaneseInRelayout2) { |
| SetupHtml("container", |
| u"<div id=container>" |
| u"<span>\u30ED\u30B0\u30A4\u30F3</span>" |
| u"\n" |
| u"<span> \u767B\u9332</span>" |
| u"<br></div>"); |
| EXPECT_EQ(String(u"\u30ED\u30B0\u30A4\u30F3\u767B\u9332\n"), GetText()); |
| |
| Node* new_text = Text::Create(GetDocument(), "foo"); |
| GetElementById("container")->appendChild(new_text); |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(String(u"\u30ED\u30B0\u30A4\u30F3\u767B\u9332\nfoo"), GetText()); |
| } |
| #endif |
| |
| TEST_F(NGInlineNodeTest, SegmentRanges) { |
| SetupHtml("container", |
| "<div id=container>" |
| u"\u306Forange\u304C" |
| "<span>text</span>" |
| "</div>"); |
| |
| NGInlineItemsData* items_data = layout_block_flow_->GetNGInlineNodeData(); |
| ASSERT_TRUE(items_data); |
| NGInlineItemSegments* segments = items_data->segments.get(); |
| ASSERT_TRUE(segments); |
| |
| // Test EndOffset for the full text. All segment boundaries including the end |
| // of the text content should be returned. |
| Vector<unsigned> expect_0_12 = {1u, 7u, 8u, 12u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(0, 12, 0)), expect_0_12); |
| |
| // Test ranges for each segment that start with 1st item. |
| Vector<unsigned> expect_0_1 = {1u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(0, 1, 0)), expect_0_1); |
| Vector<unsigned> expect_2_3 = {3u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(2, 3, 0)), expect_2_3); |
| Vector<unsigned> expect_7_8 = {8u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(7, 8, 0)), expect_7_8); |
| |
| // Test ranges that acrosses multiple segments. |
| Vector<unsigned> expect_0_8 = {1u, 7u, 8u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(0, 8, 0)), expect_0_8); |
| Vector<unsigned> expect_2_8 = {7u, 8u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(2, 8, 0)), expect_2_8); |
| Vector<unsigned> expect_2_10 = {7u, 8u, 10u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(2, 10, 0)), expect_2_10); |
| Vector<unsigned> expect_7_10 = {8u, 10u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(7, 10, 0)), expect_7_10); |
| |
| // Test ranges that starts with 2nd item. |
| Vector<unsigned> expect_8_9 = {9u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(8, 9, 1)), expect_8_9); |
| Vector<unsigned> expect_8_10 = {10u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(8, 10, 1)), expect_8_10); |
| Vector<unsigned> expect_9_12 = {12u}; |
| EXPECT_EQ(ToEndOffsetList(segments->Ranges(9, 12, 1)), expect_9_12); |
| } |
| |
| // https://crbug.com/1021677 |
| TEST_F(NGInlineNodeTest, ReusingWithCollapsed) { |
| SetupHtml("container", |
| "<div id=container>" |
| "abc " |
| "<img style='float:right'>" |
| "<br id='remove'>" |
| "<b style='white-space:pre'>x</b>" |
| "</div>"); |
| GetElementById("remove")->remove(); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(String(u"abc \uFFFCx"), GetText()); |
| } |
| |
| } // namespace blink |