blob: c783f1274b26a97bc05752785cc21617f3c8c328 [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 "core/editing/CaretDisplayItemClient.h"
#include "core/HTMLNames.h"
#include "core/editing/FrameSelection.h"
#include "core/frame/FrameView.h"
#include "core/layout/LayoutTestHelper.h"
#include "core/layout/LayoutView.h"
#include "core/page/FocusController.h"
#include "core/paint/PaintLayer.h"
#include "platform/graphics/GraphicsLayer.h"
#include "platform/graphics/paint/RasterInvalidationTracking.h"
namespace blink {
class CaretDisplayItemClientTest : public RenderingTest {
protected:
void SetUp() override {
RenderingTest::SetUp();
EnableCompositing();
Selection().SetCaretBlinkingSuspended(true);
}
const RasterInvalidationTracking* GetRasterInvalidationTracking() const {
// TODO(wangxianzhu): Test SPv2.
return GetLayoutView()
.Layer()
->GraphicsLayerBacking()
->GetRasterInvalidationTracking();
}
FrameSelection& Selection() const {
return GetDocument().View()->GetFrame().Selection();
}
const DisplayItemClient& GetCaretDisplayItemClient() const {
return Selection().CaretDisplayItemClientForTesting();
}
const LayoutBlock* CaretLayoutBlock() const {
return static_cast<const CaretDisplayItemClient&>(
GetCaretDisplayItemClient())
.layout_block_;
}
const LayoutBlock* PreviousCaretLayoutBlock() const {
return static_cast<const CaretDisplayItemClient&>(
GetCaretDisplayItemClient())
.previous_layout_block_;
}
Text* AppendTextNode(const String& data) {
Text* text = GetDocument().createTextNode(data);
GetDocument().body()->AppendChild(text);
return text;
}
Element* AppendBlock(const String& data) {
Element* block = GetDocument().createElement("div");
Text* text = GetDocument().createTextNode(data);
block->AppendChild(text);
GetDocument().body()->AppendChild(block);
return block;
}
void UpdateAllLifecyclePhases() {
// Partial lifecycle updates should not affect caret paint invalidation.
GetDocument().View()->UpdateLifecycleToLayoutClean();
GetDocument().View()->UpdateAllLifecyclePhases();
// Partial lifecycle updates should not affect caret paint invalidation.
GetDocument().View()->UpdateLifecycleToLayoutClean();
}
};
TEST_F(CaretDisplayItemClientTest, CaretPaintInvalidation) {
GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
GetDocument().GetPage()->GetFocusController().SetActive(true);
GetDocument().GetPage()->GetFocusController().SetFocused(true);
Text* text = AppendTextNode("Hello, World!");
UpdateAllLifecyclePhases();
const auto* block = ToLayoutBlock(GetDocument().body()->GetLayoutObject());
// Focus the body. Should invalidate the new caret.
GetDocument().View()->SetTracksPaintInvalidations(true);
GetDocument().body()->focus();
UpdateAllLifecyclePhases();
EXPECT_TRUE(block->ShouldPaintCursorCaret());
LayoutRect caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(1, caret_visual_rect.Width());
EXPECT_EQ(block->Location(), caret_visual_rect.Location());
const auto* raster_invalidations =
&GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(1u, raster_invalidations->size());
EXPECT_EQ(EnclosingIntRect(caret_visual_rect),
(*raster_invalidations)[0].rect);
EXPECT_EQ(block, (*raster_invalidations)[0].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[0].reason);
std::unique_ptr<JSONArray> object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(1u, object_invalidations->size());
String s;
JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
EXPECT_EQ("Caret", s);
GetDocument().View()->SetTracksPaintInvalidations(false);
// Move the caret to the end of the text. Should invalidate both the old and
// new carets.
GetDocument().View()->SetTracksPaintInvalidations(true);
Selection().SetSelection(
SelectionInDOMTree::Builder().Collapse(Position(text, 5)).Build());
UpdateAllLifecyclePhases();
EXPECT_TRUE(block->ShouldPaintCursorCaret());
LayoutRect new_caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(caret_visual_rect.Size(), new_caret_visual_rect.Size());
EXPECT_EQ(caret_visual_rect.Y(), new_caret_visual_rect.Y());
EXPECT_LT(caret_visual_rect.X(), new_caret_visual_rect.X());
raster_invalidations = &GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(2u, raster_invalidations->size());
EXPECT_EQ(EnclosingIntRect(caret_visual_rect),
(*raster_invalidations)[0].rect);
EXPECT_EQ(block, (*raster_invalidations)[0].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[0].reason);
EXPECT_EQ(EnclosingIntRect(new_caret_visual_rect),
(*raster_invalidations)[1].rect);
EXPECT_EQ(block, (*raster_invalidations)[1].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[1].reason);
object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(1u, object_invalidations->size());
JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
EXPECT_EQ("Caret", s);
GetDocument().View()->SetTracksPaintInvalidations(false);
// Remove selection. Should invalidate the old caret.
LayoutRect old_caret_visual_rect = new_caret_visual_rect;
GetDocument().View()->SetTracksPaintInvalidations(true);
Selection().SetSelection(SelectionInDOMTree());
UpdateAllLifecyclePhases();
EXPECT_FALSE(block->ShouldPaintCursorCaret());
EXPECT_EQ(LayoutRect(), GetCaretDisplayItemClient().VisualRect());
raster_invalidations = &GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(1u, raster_invalidations->size());
EXPECT_EQ(EnclosingIntRect(old_caret_visual_rect),
(*raster_invalidations)[0].rect);
EXPECT_EQ(block, (*raster_invalidations)[0].client);
object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(1u, object_invalidations->size());
JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
EXPECT_EQ("Caret", s);
GetDocument().View()->SetTracksPaintInvalidations(false);
}
TEST_F(CaretDisplayItemClientTest, CaretMovesBetweenBlocks) {
GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
GetDocument().GetPage()->GetFocusController().SetActive(true);
GetDocument().GetPage()->GetFocusController().SetFocused(true);
auto* block_element1 = AppendBlock("Block1");
auto* block_element2 = AppendBlock("Block2");
UpdateAllLifecyclePhases();
auto* block1 = ToLayoutBlock(block_element1->GetLayoutObject());
auto* block2 = ToLayoutBlock(block_element2->GetLayoutObject());
// Focus the body.
GetDocument().body()->focus();
UpdateAllLifecyclePhases();
LayoutRect caret_visual_rect1 = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(1, caret_visual_rect1.Width());
EXPECT_EQ(block1->VisualRect().Location(), caret_visual_rect1.Location());
EXPECT_TRUE(block1->ShouldPaintCursorCaret());
EXPECT_FALSE(block2->ShouldPaintCursorCaret());
// Move the caret into block2. Should invalidate both the old and new carets.
GetDocument().View()->SetTracksPaintInvalidations(true);
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element2, 0))
.Build());
UpdateAllLifecyclePhases();
LayoutRect caret_visual_rect2 = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(1, caret_visual_rect2.Width());
EXPECT_EQ(block2->VisualRect().Location(), caret_visual_rect2.Location());
EXPECT_FALSE(block1->ShouldPaintCursorCaret());
EXPECT_TRUE(block2->ShouldPaintCursorCaret());
const auto* raster_invalidations =
&GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(2u, raster_invalidations->size());
EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
(*raster_invalidations)[0].rect);
EXPECT_EQ(block1, (*raster_invalidations)[0].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[0].reason);
EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
(*raster_invalidations)[1].rect);
EXPECT_EQ(block2, (*raster_invalidations)[1].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[1].reason);
std::unique_ptr<JSONArray> object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(2u, object_invalidations->size());
GetDocument().View()->SetTracksPaintInvalidations(false);
// Move the caret back into block1.
GetDocument().View()->SetTracksPaintInvalidations(true);
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element1, 0))
.Build());
UpdateAllLifecyclePhases();
EXPECT_EQ(caret_visual_rect1, GetCaretDisplayItemClient().VisualRect());
EXPECT_TRUE(block1->ShouldPaintCursorCaret());
EXPECT_FALSE(block2->ShouldPaintCursorCaret());
raster_invalidations = &GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(2u, raster_invalidations->size());
EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
(*raster_invalidations)[0].rect);
EXPECT_EQ(block1, (*raster_invalidations)[0].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[0].reason);
EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
(*raster_invalidations)[1].rect);
EXPECT_EQ(block2, (*raster_invalidations)[1].client);
EXPECT_EQ(kPaintInvalidationCaret, (*raster_invalidations)[1].reason);
object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(2u, object_invalidations->size());
GetDocument().View()->SetTracksPaintInvalidations(false);
}
TEST_F(CaretDisplayItemClientTest, UpdatePreviousLayoutBlock) {
GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
GetDocument().GetPage()->GetFocusController().SetActive(true);
GetDocument().GetPage()->GetFocusController().SetFocused(true);
auto* block_element1 = AppendBlock("Block1");
auto* block_element2 = AppendBlock("Block2");
UpdateAllLifecyclePhases();
auto* block1 = ToLayoutBlock(block_element1->GetLayoutObject());
auto* block2 = ToLayoutBlock(block_element2->GetLayoutObject());
// Set caret into block2.
GetDocument().body()->focus();
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element2, 0))
.Build());
GetDocument().View()->UpdateLifecycleToLayoutClean();
EXPECT_TRUE(block2->ShouldPaintCursorCaret());
EXPECT_EQ(block2, CaretLayoutBlock());
EXPECT_FALSE(block1->ShouldPaintCursorCaret());
EXPECT_FALSE(PreviousCaretLayoutBlock());
// Move caret into block1. Should set previousCaretLayoutBlock to block2.
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element1, 0))
.Build());
GetDocument().View()->UpdateLifecycleToLayoutClean();
EXPECT_TRUE(block1->ShouldPaintCursorCaret());
EXPECT_EQ(block1, CaretLayoutBlock());
EXPECT_FALSE(block2->ShouldPaintCursorCaret());
EXPECT_EQ(block2, PreviousCaretLayoutBlock());
// Move caret into block2. Partial update should not change
// previousCaretLayoutBlock.
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element2, 0))
.Build());
GetDocument().View()->UpdateLifecycleToLayoutClean();
EXPECT_TRUE(block2->ShouldPaintCursorCaret());
EXPECT_EQ(block2, CaretLayoutBlock());
EXPECT_FALSE(block1->ShouldPaintCursorCaret());
EXPECT_EQ(block2, PreviousCaretLayoutBlock());
// Remove block2. Should clear caretLayoutBlock and previousCaretLayoutBlock.
block_element2->parentNode()->RemoveChild(block_element2);
EXPECT_FALSE(CaretLayoutBlock());
EXPECT_FALSE(PreviousCaretLayoutBlock());
// Set caret into block1.
Selection().SetSelection(SelectionInDOMTree::Builder()
.Collapse(Position(block_element1, 0))
.Build());
UpdateAllLifecyclePhases();
// Remove selection.
Selection().SetSelection(SelectionInDOMTree());
GetDocument().View()->UpdateLifecycleToLayoutClean();
EXPECT_EQ(block1, PreviousCaretLayoutBlock());
}
TEST_F(CaretDisplayItemClientTest, CaretHideMoveAndShow) {
GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
GetDocument().GetPage()->GetFocusController().SetActive(true);
GetDocument().GetPage()->GetFocusController().SetFocused(true);
Text* text = AppendTextNode("Hello, World!");
GetDocument().body()->focus();
UpdateAllLifecyclePhases();
const auto* block = ToLayoutBlock(GetDocument().body()->GetLayoutObject());
LayoutRect caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(1, caret_visual_rect.Width());
EXPECT_EQ(block->Location(), caret_visual_rect.Location());
// Simulate that the blinking cursor becomes invisible.
Selection().SetCaretVisible(false);
// Move the caret to the end of the text.
GetDocument().View()->SetTracksPaintInvalidations(true);
Selection().SetSelection(
SelectionInDOMTree::Builder().Collapse(Position(text, 5)).Build());
// Simulate that the cursor blinking is restarted.
Selection().SetCaretVisible(true);
UpdateAllLifecyclePhases();
LayoutRect new_caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
EXPECT_EQ(caret_visual_rect.Size(), new_caret_visual_rect.Size());
EXPECT_EQ(caret_visual_rect.Y(), new_caret_visual_rect.Y());
EXPECT_LT(caret_visual_rect.X(), new_caret_visual_rect.X());
const auto& raster_invalidations =
GetRasterInvalidationTracking()->invalidations;
ASSERT_EQ(2u, raster_invalidations.size());
EXPECT_EQ(EnclosingIntRect(caret_visual_rect), raster_invalidations[0].rect);
EXPECT_EQ(block, raster_invalidations[0].client);
EXPECT_EQ(kPaintInvalidationCaret, raster_invalidations[0].reason);
EXPECT_EQ(EnclosingIntRect(new_caret_visual_rect),
raster_invalidations[1].rect);
EXPECT_EQ(block, raster_invalidations[1].client);
EXPECT_EQ(kPaintInvalidationCaret, raster_invalidations[1].reason);
auto object_invalidations =
GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
ASSERT_EQ(1u, object_invalidations->size());
String s;
JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
EXPECT_EQ("Caret", s);
GetDocument().View()->SetTracksPaintInvalidations(false);
}
TEST_F(CaretDisplayItemClientTest, CompositingChange) {
EnableCompositing();
SetBodyInnerHTML(
"<style>"
" body { margin: 0 }"
" #container { position: absolute; top: 55px; left: 66px; }"
"</style>"
"<div id='container'>"
" <div id='editor' contenteditable style='padding: 50px'>ABCDE</div>"
"</div>");
GetDocument().GetPage()->GetFocusController().SetActive(true);
GetDocument().GetPage()->GetFocusController().SetFocused(true);
auto* container = GetDocument().getElementById("container");
auto* editor = GetDocument().getElementById("editor");
auto* editor_block = ToLayoutBlock(editor->GetLayoutObject());
Selection().SetSelection(
SelectionInDOMTree::Builder().Collapse(Position(editor, 0)).Build());
UpdateAllLifecyclePhases();
EXPECT_TRUE(editor_block->ShouldPaintCursorCaret());
EXPECT_EQ(editor_block, CaretLayoutBlock());
EXPECT_EQ(LayoutRect(116, 105, 1, 1),
GetCaretDisplayItemClient().VisualRect());
// Composite container.
container->setAttribute(HTMLNames::styleAttr, "will-change: transform");
UpdateAllLifecyclePhases();
EXPECT_EQ(LayoutRect(50, 50, 1, 1), GetCaretDisplayItemClient().VisualRect());
// Uncomposite container.
container->setAttribute(HTMLNames::styleAttr, "");
UpdateAllLifecyclePhases();
EXPECT_EQ(LayoutRect(116, 105, 1, 1),
GetCaretDisplayItemClient().VisualRect());
}
} // namespace blink