Commit 52d8ec7a authored by Tao Bai's avatar Tao Bai Committed by Commit Bot

ContentCapture: Support content change

The some changes of element will not cause remove/add LayoutText,
instead, LayoutText is changed.

This patch supports the content change in this case, DidUpdateContent()
is added and invoked after new content is sent.

Bug: 952973
Change-Id: Ia530e7e71570801f1f494bd584526cc7dc3ccba8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1598310
Commit-Queue: Tao Bai <michaelbai@chromium.org>
Reviewed-by: default avatarTobias Sargeant <tobiasjs@chromium.org>
Reviewed-by: default avatarXianzhu Wang <wangxianzhu@chromium.org>
Reviewed-by: default avatarPhilip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#659145}
parent 2611e35d
......@@ -69,6 +69,9 @@ void ContentCaptureSender::DidCaptureContent(
GetContentCaptureReceiver()->DidCaptureContent(frame_data, first_data);
}
void ContentCaptureSender::DidUpdateContent(
const std::vector<scoped_refptr<blink::WebContentHolder>>& data) {}
void ContentCaptureSender::DidRemoveContent(const std::vector<int64_t>& data) {
GetContentCaptureReceiver()->DidRemoveContent(data);
}
......
......@@ -42,9 +42,11 @@ class ContentCaptureSender : public content::RenderFrameObserver,
void DidCaptureContent(
const std::vector<scoped_refptr<blink::WebContentHolder>>& data,
bool first_data) override;
void DidUpdateContent(
const std::vector<scoped_refptr<blink::WebContentHolder>>& data) override;
void DidRemoveContent(const std::vector<int64_t>& data) override;
// mojom::ContentCaptureSender
// mojom::ContentCaptureSender:
void StartCapture() override;
void StopCapture() override;
......
......@@ -31,6 +31,10 @@ class WebContentCaptureClient {
const std::vector<scoped_refptr<WebContentHolder>>& content,
bool first_data) = 0;
// Invoked when a list of |content| is updated.
virtual void DidUpdateContent(
const std::vector<scoped_refptr<WebContentHolder>>& content) = 0;
// Invoked when the previously captured content is removed, |content_ids| is a
// list of removed content id.
virtual void DidRemoveContent(const std::vector<int64_t>& content_ids) = 0;
......
......@@ -69,6 +69,11 @@ void ContentCaptureManager::OnScrollPositionChanged() {
ScheduleTask(ContentCaptureTask::ScheduleReason::kScrolling);
}
void ContentCaptureManager::OnNodeTextChanged(const NodeHolder& node_holder) {
task_session_->OnNodeChanged(node_holder);
ScheduleTask(ContentCaptureTask::ScheduleReason::kContentChange);
}
void ContentCaptureManager::Trace(Visitor* visitor) {
visitor->Trace(local_frame_root_);
visitor->Trace(task_session_);
......
......@@ -36,6 +36,9 @@ class CORE_EXPORT ContentCaptureManager
// Invokes when scroll position was changed.
void OnScrollPositionChanged();
// Invokes when text node content was changed.
void OnNodeTextChanged(const NodeHolder& node_holder);
// Invokes when the local_frame_root shutdown.
void Shutdown();
......
......@@ -77,24 +77,34 @@ void ContentCaptureTask::SendContent(
TaskSession::DocumentSession& doc_session) {
auto* document = doc_session.GetDocument();
DCHECK(document);
auto* client = GetWebContentCaptureClient(*document);
DCHECK(client);
if (histogram_reporter_)
histogram_reporter_->OnSendContentStarted();
std::vector<scoped_refptr<WebContentHolder>> content_batch;
content_batch.reserve(kBatchSize);
// Only send changed content after the new content was sent.
bool sending_changed_content = !doc_session.HasUnsentCapturedContent();
while (content_batch.size() < kBatchSize) {
scoped_refptr<ContentHolder> content_holder =
doc_session.GetNextUnsentContentHolder();
scoped_refptr<ContentHolder> content_holder;
if (sending_changed_content)
content_holder = doc_session.GetNextChangedContentHolder();
else
content_holder = doc_session.GetNextUnsentContentHolder();
if (!content_holder)
break;
content_batch.push_back(
base::MakeRefCounted<WebContentHolder>(content_holder));
}
if (!content_batch.empty()) {
DCHECK(GetWebContentCaptureClient(*document));
GetWebContentCaptureClient(*document)->DidCaptureContent(
content_batch, !doc_session.FirstDataHasSent());
if (sending_changed_content) {
client->DidUpdateContent(content_batch);
} else {
client->DidCaptureContent(content_batch, !doc_session.FirstDataHasSent());
doc_session.SetFirstDataHasSent();
}
}
if (histogram_reporter_)
histogram_reporter_->OnSendContentEnded(content_batch.size());
}
......@@ -128,7 +138,8 @@ bool ContentCaptureTask::ProcessDocumentSession(
return true;
}
while (doc_session.HasUnsentCapturedContent()) {
while (doc_session.HasUnsentCapturedContent() ||
doc_session.HasUnsentChangedContent()) {
SendContent(doc_session);
if (ShouldPause()) {
return !doc_session.HasUnsentData();
......
......@@ -55,6 +55,13 @@ class WebContentCaptureClientTestHelper : public WebContentCaptureClient {
all_text_.push_back(d->GetValue().Utf8());
}
void DidUpdateContent(
const std::vector<scoped_refptr<WebContentHolder>>& data) override {
updated_data_ = data;
for (auto d : data)
updated_text_.push_back(d->GetValue().Utf8());
}
void DidRemoveContent(const std::vector<int64_t>& data) override {
removed_data_ = data;
}
......@@ -65,22 +72,31 @@ class WebContentCaptureClientTestHelper : public WebContentCaptureClient {
return data_;
}
const std::vector<scoped_refptr<WebContentHolder>>& UpdatedData() const {
return updated_data_;
}
const std::vector<std::string>& AllText() const { return all_text_; }
const std::vector<std::string>& UpdatedText() const { return updated_text_; }
const std::vector<int64_t>& RemovedData() const { return removed_data_; }
void ResetResults() {
first_data_ = false;
data_.clear();
updated_data_.clear();
removed_data_.clear();
}
private:
bool first_data_ = false;
std::vector<scoped_refptr<WebContentHolder>> data_;
std::vector<scoped_refptr<WebContentHolder>> updated_data_;
std::vector<int64_t> removed_data_;
NodeHolder::Type node_holder_type_;
std::vector<std::string> all_text_;
std::vector<std::string> updated_text_;
};
class ContentCaptureTaskTestHelper : public ContentCaptureTask {
......@@ -525,6 +541,8 @@ class ContentCaptureSimTest
: public SimTest,
public ::testing::WithParamInterface<NodeHolder::Type> {
public:
static const char* kEditableContent;
ContentCaptureSimTest() : client_(GetParam()), child_client_(GetParam()) {}
void SetUp() override {
SimTest::SetUp();
......@@ -570,6 +588,15 @@ class ContentCaptureSimTest
child_frame_expected_text_.push_back("New Text");
}
void InsertMainFrameEditableContent(const std::string& content,
unsigned offset) {
InsertNodeContent(GetDocument(), "editable_id", content, offset);
}
void DeleteMainFrameEditableContent(unsigned offset, unsigned length) {
DeleteNodeContent(GetDocument(), "editable_id", offset, length);
}
const std::vector<std::string>& MainFrameExpectedText() const {
return main_frame_expected_text_;
}
......@@ -578,6 +605,12 @@ class ContentCaptureSimTest
return child_frame_expected_text_;
}
void ReplaceMainFrameExpectedText(const std::string& old_text,
const std::string& new_text) {
std::replace(main_frame_expected_text_.begin(),
main_frame_expected_text_.end(), old_text, new_text);
}
private:
void SetupPage() {
SimRequest main_resource("https://example.com/test.html", "text/html");
......@@ -595,6 +628,7 @@ class ContentCaptureSimTest
<p id='p5'>Hello World5</p>
<p id='p6'>Hello World6</p>
<p id='p7'>Hello World7</p>
<div id='editable_id'>editable</div>
<svg>
<text id="s8">Hello World8</text>
</svg>
......@@ -620,13 +654,14 @@ class ContentCaptureSimTest
}
void InitMainFrameNodeHolders() {
std::vector<std::string> ids = {"p1", "p2", "p3", "p4",
"p5", "p6", "p7", "s8"};
main_frame_expected_text_ = {"Hello World1", "Hello World2", "Hello World3",
std::vector<std::string> ids = {"p1", "p2", "p3", "p4", "p5",
"p6", "p7", "s8", "editable_id"};
main_frame_expected_text_ = {
"Hello World1", "Hello World2", "Hello World3",
"Hello World4", "Hello World5", "Hello World6",
"Hello World7", "Hello World8"};
"Hello World7", "Hello World8", kEditableContent};
InitNodeHolders(main_frame_content_, ids, GetDocument());
EXPECT_EQ(8u, main_frame_content_.size());
EXPECT_EQ(9u, main_frame_content_.size());
}
void InitChildFrameNodeHolders(const Document& doc) {
......@@ -659,6 +694,25 @@ class ContentCaptureSimTest
buffer.insert(buffer.begin(), layout_text->EnsureNodeHolder());
}
void InsertNodeContent(Document& doc,
const std::string& id,
const std::string& content,
unsigned offset) {
To<Text>(doc.getElementById(id.c_str())->firstChild())
->insertData(offset, String(content.c_str()),
IGNORE_EXCEPTION_FOR_TESTING);
Compositor().BeginFrame();
}
void DeleteNodeContent(Document& doc,
const std::string& id,
unsigned offset,
unsigned length) {
To<Text>(doc.getElementById(id.c_str())->firstChild())
->deleteData(offset, length, IGNORE_EXCEPTION_FOR_TESTING);
Compositor().BeginFrame();
}
void SetCapturedContent(const std::vector<NodeHolder>& captured_content) {
GetDocument()
.GetFrame()
......@@ -677,6 +731,8 @@ class ContentCaptureSimTest
Persistent<Document> child_document_;
};
const char* ContentCaptureSimTest::kEditableContent = "editable";
INSTANTIATE_TEST_SUITE_P(,
ContentCaptureSimTest,
testing::Values(NodeHolder::Type::kID,
......@@ -685,7 +741,7 @@ INSTANTIATE_TEST_SUITE_P(,
TEST_P(ContentCaptureSimTest, MultiFrame) {
SetCapturedContent(ContentType::kAll);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(3u, Client().Data().size());
EXPECT_EQ(4u, Client().Data().size());
EXPECT_EQ(2u, ChildClient().Data().size());
EXPECT_THAT(Client().AllText(),
testing::UnorderedElementsAreArray(MainFrameExpectedText()));
......@@ -710,7 +766,7 @@ TEST_P(ContentCaptureSimTest, AddNodeToMultiFrame) {
// Sends the reset of data
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kProcessRetryTask);
EXPECT_EQ(3u, Client().Data().size());
EXPECT_EQ(4u, Client().Data().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
EXPECT_THAT(Client().AllText(),
......@@ -735,4 +791,104 @@ TEST_P(ContentCaptureSimTest, AddNodeToMultiFrame) {
EXPECT_TRUE(ChildClient().FirstData());
}
TEST_P(ContentCaptureSimTest, ChangeNode) {
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(4u, Client().Data().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
EXPECT_THAT(Client().AllText(),
testing::UnorderedElementsAreArray(MainFrameExpectedText()));
std::vector<std::string> expected_text_update;
std::string insert_text = "content ";
// Changed content to 'content editable'.
InsertMainFrameEditableContent(insert_text, 0);
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(1u, Client().UpdatedData().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
expected_text_update.push_back(insert_text + kEditableContent);
EXPECT_THAT(Client().UpdatedText(),
testing::UnorderedElementsAreArray(expected_text_update));
// Changing content multiple times before capturing.
std::string insert_text1 = "i";
// Changed content to 'content ieditable'.
InsertMainFrameEditableContent(insert_text1, insert_text.size());
std::string insert_text2 = "s ";
// Changed content to 'content is editable'.
InsertMainFrameEditableContent(insert_text2,
insert_text.size() + insert_text1.size());
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(1u, Client().UpdatedData().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
expected_text_update.push_back(insert_text + insert_text1 + insert_text2 +
kEditableContent);
EXPECT_THAT(Client().UpdatedText(),
testing::UnorderedElementsAreArray(expected_text_update));
}
TEST_P(ContentCaptureSimTest, ChangeNodeBeforeCapture) {
// Changed content to 'content editable' before capture.
std::string insert_text = "content ";
InsertMainFrameEditableContent(insert_text, 0);
// Changing content multiple times before capturing.
std::string insert_text1 = "i";
// Changed content to 'content ieditable'.
InsertMainFrameEditableContent(insert_text1, insert_text.size());
std::string insert_text2 = "s ";
// Changed content to 'content is editable'.
InsertMainFrameEditableContent(insert_text2,
insert_text.size() + insert_text1.size());
// The changed content shall be captured as new content.
ReplaceMainFrameExpectedText(
kEditableContent,
insert_text + insert_text1 + insert_text2 + kEditableContent);
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(4u, Client().Data().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
EXPECT_TRUE(ChildClient().UpdatedData().empty());
EXPECT_THAT(Client().AllText(),
testing::UnorderedElementsAreArray(MainFrameExpectedText()));
}
TEST_P(ContentCaptureSimTest, DeleteNodeContent) {
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(4u, Client().Data().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
EXPECT_THAT(Client().AllText(),
testing::UnorderedElementsAreArray(MainFrameExpectedText()));
// Deleted 4 char, changed content to 'edit'.
DeleteMainFrameEditableContent(4, 4);
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_EQ(1u, Client().UpdatedData().size());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
std::vector<std::string> expected_text_update;
expected_text_update.push_back("edit");
EXPECT_THAT(Client().UpdatedText(),
testing::UnorderedElementsAreArray(expected_text_update));
// Emptied content, the node shall be removed.
DeleteMainFrameEditableContent(0, 4);
SetCapturedContent(ContentType::kMainFrame);
RunContentCaptureTaskUntil(ContentCaptureTask::TaskState::kStop);
EXPECT_TRUE(Client().UpdatedData().empty());
EXPECT_FALSE(Client().FirstData());
EXPECT_TRUE(ChildClient().Data().empty());
EXPECT_EQ(1u, Client().RemovedData().size());
}
} // namespace blink
......@@ -31,6 +31,11 @@ void TaskSession::DocumentSession::AddDetachedNode(int64_t id) {
detached_nodes_.push_back(id);
}
void TaskSession::DocumentSession::AddChangedNodeHolder(
cc::NodeHolder node_holder) {
changed_content_.push_back(node_holder);
}
std::vector<int64_t> TaskSession::DocumentSession::MoveDetachedNodes() {
return std::move(detached_nodes_);
}
......@@ -64,12 +69,36 @@ TaskSession::DocumentSession::GetNextUnsentContentHolder() {
return content_holder;
}
scoped_refptr<blink::ContentHolder>
TaskSession::DocumentSession::GetNextChangedContentHolder() {
scoped_refptr<ContentHolder> content_holder;
while (!changed_content_.empty() && !content_holder) {
auto node_holder = changed_content_.back();
if (node_holder.type == cc::NodeHolder::Type::kID) {
Node* node = DOMNodeIds::NodeForId(node_holder.id);
if (node && node->GetLayoutObject())
content_holder = base::MakeRefCounted<ContentHolder>(*node);
} else if (node_holder.type == cc::NodeHolder::Type::kTextHolder &&
node_holder.text_holder) {
content_holder = scoped_refptr<ContentHolder>(
static_cast<ContentHolder*>(node_holder.text_holder.get()));
if (content_holder && !content_holder->IsValid())
content_holder.reset();
}
changed_content_.pop_back();
}
if (content_holder)
total_sent_nodes_++;
return content_holder;
}
void TaskSession::DocumentSession::Trace(blink::Visitor* visitor) {
visitor->Trace(sent_nodes_);
visitor->Trace(document_);
}
void TaskSession::DocumentSession::Reset() {
changed_content_.clear();
captured_content_.clear();
detached_nodes_.clear();
}
......@@ -97,6 +126,19 @@ void TaskSession::SetCapturedContent(
void TaskSession::GroupCapturedContentByDocument(
const std::vector<cc::NodeHolder>& captured_content) {
for (const cc::NodeHolder& node_holder : captured_content) {
if (const Node* node = GetNode(node_holder)) {
node = changed_nodes_.Take(node);
if (node) {
// The changed node might not be sent.
if (GetNodeIf(true, node_holder)) {
EnsureDocumentSession(node->GetDocument())
.AddChangedNodeHolder(node_holder);
} else {
EnsureDocumentSession(node->GetDocument()).AddNodeHolder(node_holder);
}
continue;
}
}
if (const Node* node = GetNodeIf(false /* sent */, node_holder)) {
EnsureDocumentSession(node->GetDocument()).AddNodeHolder(node_holder);
}
......@@ -111,6 +153,12 @@ void TaskSession::OnNodeDetached(const cc::NodeHolder& node_holder) {
}
}
void TaskSession::OnNodeChanged(const cc::NodeHolder& node_holder) {
if (const Node* node = GetNode(node_holder)) {
changed_nodes_.insert(WeakMember<const Node>(node));
}
}
const Node* TaskSession::GetNodeIf(bool sent,
const cc::NodeHolder& node_holder) const {
Node* node = nullptr;
......@@ -129,6 +177,19 @@ const Node* TaskSession::GetNodeIf(bool sent,
return nullptr;
}
const Node* TaskSession::GetNode(const cc::NodeHolder& node_holder) const {
if (node_holder.type == cc::NodeHolder::Type::kID)
return DOMNodeIds::NodeForId(node_holder.id);
if (node_holder.type == cc::NodeHolder::Type::kTextHolder) {
ContentHolder* content_holder =
static_cast<ContentHolder*>(node_holder.text_holder.get());
if (content_holder)
return content_holder->GetNode();
}
return nullptr;
}
TaskSession::DocumentSession& TaskSession::EnsureDocumentSession(
const Document& doc) {
DocumentSession* doc_session = GetDocumentSession(doc);
......@@ -150,6 +211,7 @@ TaskSession::DocumentSession* TaskSession::GetDocumentSession(
void TaskSession::Trace(blink::Visitor* visitor) {
visitor->Trace(sent_nodes_);
visitor->Trace(changed_nodes_);
visitor->Trace(to_document_session_);
}
......
......@@ -54,10 +54,13 @@ class TaskSession : public GarbageCollectedFinalized<TaskSession> {
~DocumentSession();
void AddNodeHolder(cc::NodeHolder node_holder);
void AddDetachedNode(int64_t id);
void AddChangedNodeHolder(cc::NodeHolder node);
bool HasUnsentData() const {
return HasUnsentCapturedContent() || HasUnsentDetachedNodes();
return HasUnsentCapturedContent() || HasUnsentChangedContent() ||
HasUnsentDetachedNodes();
}
bool HasUnsentCapturedContent() const { return !captured_content_.empty(); }
bool HasUnsentChangedContent() const { return !changed_content_.empty(); }
bool HasUnsentDetachedNodes() const { return !detached_nodes_.empty(); }
std::vector<int64_t> MoveDetachedNodes();
const Document* GetDocument() const { return document_; }
......@@ -68,6 +71,8 @@ class TaskSession : public GarbageCollectedFinalized<TaskSession> {
// ContentHolder.
scoped_refptr<ContentHolder> GetNextUnsentContentHolder();
scoped_refptr<ContentHolder> GetNextChangedContentHolder();
// Resets the |captured_content_| and the |detached_nodes_|, shall only be
// used if those data doesn't need to be sent, e.g. there is no
// WebContentCaptureClient for this document.
......@@ -83,6 +88,9 @@ class TaskSession : public GarbageCollectedFinalized<TaskSession> {
std::vector<int64_t> detached_nodes_;
WeakMember<const Document> document_;
Member<SentNodes> sent_nodes_;
// The list of changed nodes that needs to be sent.
std::vector<cc::NodeHolder> changed_content_;
bool first_data_has_sent_ = false;
// This is for the metrics to record the total node that has been sent.
size_t total_sent_nodes_ = 0;
......@@ -102,6 +110,8 @@ class TaskSession : public GarbageCollectedFinalized<TaskSession> {
void OnNodeDetached(const cc::NodeHolder& node_holder);
void OnNodeChanged(const cc::NodeHolder& node_holder);
bool HasUnsentData() const { return has_unsent_data_; }
void SetSentNodeCountCallback(
......@@ -119,9 +129,13 @@ class TaskSession : public GarbageCollectedFinalized<TaskSession> {
DocumentSession& EnsureDocumentSession(const Document& doc);
DocumentSession* GetDocumentSession(const Document& document) const;
const Node* GetNodeIf(bool sent, const cc::NodeHolder& node_holder) const;
const Node* GetNode(const cc::NodeHolder& node_holder) const;
Member<SentNodes> sent_nodes_;
// The list of node whose value has changed.
HeapHashSet<WeakMember<const Node>> changed_nodes_;
// This owns the DocumentSession which is released along with Document.
HeapHashMap<WeakMember<const Document>, Member<DocumentSession>>
to_document_session_;
......
......@@ -1873,6 +1873,11 @@ void LayoutText::SetText(scoped_refptr<StringImpl> text,
if (text_autosizer)
text_autosizer->Record(this);
if (HasNodeHolder()) {
if (auto* content_capture_manager = GetContentCaptureManager())
content_capture_manager->OnNodeTextChanged(node_holder_);
}
valid_ng_items_ = false;
SetNeedsCollectInlines();
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment