blob: d56eb760e7973760c33eb25ea3e9487ca8946436 [file] [log] [blame]
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "cobalt/dom/element.h"
#include "cobalt/dom/mutation_observer_init.h"
#include "cobalt/dom/mutation_observer_task_manager.h"
#include "cobalt/dom/mutation_record.h"
#include "cobalt/dom/mutation_reporter.h"
#include "cobalt/dom/node_list.h"
#include "cobalt/dom/text.h"
#include "cobalt/script/sequence.h"
#include "cobalt/script/testing/mock_exception_state.h"
#include "cobalt/test/empty_document.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::SaveArg;
namespace cobalt {
namespace dom {
// Helper struct for childList mutations.
struct ChildListMutationArguments {
scoped_refptr<dom::Element> previous_sibling;
scoped_refptr<dom::Element> next_sibling;
scoped_refptr<dom::NodeList> added_nodes;
scoped_refptr<dom::NodeList> removed_nodes;
};
class MutationCallbackMock {
public:
MOCK_METHOD2(NativeMutationCallback,
void(const MutationObserver::MutationRecordSequence&,
const scoped_refptr<MutationObserver>&));
};
class MutationObserverTest : public ::testing::Test {
protected:
dom::Document* document() { return empty_document_.document(); }
MutationObserverTaskManager* task_manager() { return &task_manager_; }
MutationCallbackMock* callback_mock() { return &callback_mock_; }
scoped_refptr<Element> CreateDiv() {
scoped_refptr<Element> element = document()->CreateElement("div");
DCHECK(element);
document()->AppendChild(element);
return element;
}
scoped_refptr<MutationObserver> CreateObserver() {
return new MutationObserver(
base::Bind(&MutationCallbackMock::NativeMutationCallback,
base::Unretained(&callback_mock_)),
&task_manager_);
}
ChildListMutationArguments CreateChildListMutationArguments() {
// These nodes are not actually related in the way they are named
// (next_sibling, previous_sibling, etc) but that is not relevant for the
// purpose of these tests.
ChildListMutationArguments args;
args.previous_sibling = CreateDiv();
args.next_sibling = CreateDiv();
args.added_nodes = new NodeList();
args.added_nodes->AppendNode(CreateDiv());
args.removed_nodes = new NodeList();
args.removed_nodes->AppendNode(CreateDiv());
return args;
}
private:
MutationObserverTaskManager task_manager_;
test::EmptyDocument empty_document_;
MutationCallbackMock callback_mock_;
MessageLoop message_loop_;
};
TEST_F(MutationObserverTest, CreateAttributeMutationRecord) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationRecord> record =
MutationRecord::CreateAttributeMutationRecord(target, "attribute_name",
std::string("old_value"));
ASSERT_TRUE(record);
// "type" and attribute-related attributes are set as expected
EXPECT_STREQ("attributes", record->type().c_str());
EXPECT_TRUE(record->attribute_name());
EXPECT_STREQ("attribute_name", record->attribute_name()->c_str());
EXPECT_TRUE(record->old_value());
EXPECT_STREQ("old_value", record->old_value()->c_str());
// "target" is set as expected.
EXPECT_EQ(target, record->target());
// Namespaces are not supported.
EXPECT_FALSE(record->attribute_namespace());
// Unrelated attributes are not set.
ASSERT_TRUE(record->added_nodes());
EXPECT_EQ(record->added_nodes()->length(), 0);
ASSERT_TRUE(record->removed_nodes());
EXPECT_EQ(record->removed_nodes()->length(), 0);
EXPECT_FALSE(record->previous_sibling());
EXPECT_FALSE(record->next_sibling());
}
TEST_F(MutationObserverTest, CreateCharacterDataMutationRecord) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationRecord> record =
MutationRecord::CreateCharacterDataMutationRecord(
target, std::string("old_character_data"));
ASSERT_TRUE(record);
// "type" and attribute-related attributes are set as expected
EXPECT_STREQ("characterData", record->type().c_str());
EXPECT_TRUE(record->old_value());
EXPECT_STREQ("old_character_data", record->old_value()->c_str());
// "target" is set as expected.
EXPECT_EQ(target, record->target());
// Unrelated attributes are not set.
EXPECT_FALSE(record->attribute_name());
EXPECT_FALSE(record->attribute_namespace());
ASSERT_TRUE(record->added_nodes());
EXPECT_EQ(record->added_nodes()->length(), 0);
ASSERT_TRUE(record->removed_nodes());
EXPECT_EQ(record->removed_nodes()->length(), 0);
EXPECT_FALSE(record->previous_sibling());
EXPECT_FALSE(record->next_sibling());
}
TEST_F(MutationObserverTest, CreateChildListMutationRecord) {
scoped_refptr<dom::Element> target = CreateDiv();
ChildListMutationArguments args = CreateChildListMutationArguments();
scoped_refptr<MutationRecord> record =
MutationRecord::CreateChildListMutationRecord(
target, args.added_nodes, args.removed_nodes, args.previous_sibling,
args.next_sibling);
ASSERT_TRUE(record);
// "type" and attribute-related attributes are set as expected
EXPECT_STREQ("childList", record->type().c_str());
EXPECT_EQ(args.added_nodes, record->added_nodes());
EXPECT_EQ(args.removed_nodes, record->removed_nodes());
EXPECT_EQ(args.previous_sibling, record->previous_sibling());
EXPECT_EQ(args.next_sibling, record->next_sibling());
// "target" is set as expected.
EXPECT_EQ(target, record->target());
// Unrelated attributes are not set.
EXPECT_FALSE(record->attribute_name());
EXPECT_FALSE(record->attribute_namespace());
EXPECT_FALSE(record->old_value());
}
TEST_F(MutationObserverTest, MutationObserverInit) {
MutationObserverInit init;
// Default values are set according to spec.
EXPECT_FALSE(init.child_list());
EXPECT_FALSE(init.subtree());
// Other values are not set.
EXPECT_FALSE(init.has_attributes());
EXPECT_FALSE(init.has_character_data());
EXPECT_FALSE(init.has_attribute_old_value());
EXPECT_FALSE(init.has_character_data_old_value());
EXPECT_FALSE(init.has_attribute_filter());
// Set other values.
init.set_attributes(true);
init.set_character_data(true);
init.set_attribute_old_value(true);
init.set_character_data_old_value(true);
script::Sequence<std::string> attribute_filter;
attribute_filter.push_back("a_filter");
init.set_attribute_filter(attribute_filter);
// Other values are now set.
EXPECT_TRUE(init.has_attributes());
EXPECT_TRUE(init.attributes());
EXPECT_TRUE(init.has_character_data());
EXPECT_TRUE(init.character_data());
EXPECT_TRUE(init.has_attribute_old_value());
EXPECT_TRUE(init.attribute_old_value());
EXPECT_TRUE(init.has_character_data_old_value());
EXPECT_TRUE(init.character_data_old_value());
EXPECT_TRUE(init.has_attribute_filter());
EXPECT_EQ(init.attribute_filter().size(), attribute_filter.size());
EXPECT_EQ(init.attribute_filter().at(0), attribute_filter.at(0));
}
TEST_F(MutationObserverTest, TakeRecords) {
scoped_refptr<dom::Element> target = CreateDiv();
// Newly created observer shouldn't have records.
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserver::MutationRecordSequence records;
records = observer->TakeRecords();
EXPECT_TRUE(records.empty());
// Append a mutation records.
scoped_refptr<MutationRecord> record =
MutationRecord::CreateCharacterDataMutationRecord(
target, std::string("old_character_data"));
observer->QueueMutationRecord(record);
// The queued record can be taken once.
records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
ASSERT_EQ(records.at(0), record);
records = observer->TakeRecords();
EXPECT_TRUE(records.empty());
}
TEST_F(MutationObserverTest, Notify) {
scoped_refptr<dom::Element> target = CreateDiv();
// Create a new observer and queue a mutation record.
scoped_refptr<MutationObserver> observer = CreateObserver();
scoped_refptr<MutationRecord> record =
MutationRecord::CreateCharacterDataMutationRecord(
target, std::string("old_character_data"));
observer->QueueMutationRecord(record);
// Callback should be fired with the first argument being a sequence of the
// queued record, and the second argument being the observer.
MutationObserver::MutationRecordSequence records;
EXPECT_CALL(*callback_mock(), NativeMutationCallback(_, observer))
.WillOnce(SaveArg<0>(&records));
observer->Notify();
ASSERT_EQ(1, records.size());
EXPECT_EQ(record, records.at(0));
// There should be no more records queued up after the callback has been
// fired.
records = observer->TakeRecords();
EXPECT_TRUE(records.empty());
}
TEST_F(MutationObserverTest, ReportMutation) {
scoped_refptr<dom::Element> target = CreateDiv();
// Create a registered observer that cares about attribute and character data
// mutations.
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit init;
init.set_attributes(true);
init.set_child_list(false);
init.set_character_data(true);
// Create a MutationReporter for the list of registered observers.
scoped_ptr<std::vector<RegisteredObserver> > registered_observers(
new std::vector<RegisteredObserver>());
registered_observers->push_back(
RegisteredObserver(target.get(), observer, init));
MutationReporter reporter(target.get(), registered_observers.Pass());
// Report a few mutations.
reporter.ReportAttributesMutation("attribute_name", std::string("old_value"));
reporter.ReportCharacterDataMutation("old_character_data");
ChildListMutationArguments args = CreateChildListMutationArguments();
reporter.ReportChildListMutation(args.added_nodes, args.removed_nodes,
args.previous_sibling, args.next_sibling);
// Check that mutation records for the mutation types we care about have
// been queued.
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(2, records.size());
EXPECT_EQ(records.at(0)->type(), "attributes");
EXPECT_EQ(records.at(1)->type(), "characterData");
}
TEST_F(MutationObserverTest, AttributeFilter) {
scoped_refptr<dom::Element> target = CreateDiv();
// Create a registered observer that cares about attribute and character data
// mutations.
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit init;
script::Sequence<std::string> attribute_filter;
attribute_filter.push_back("banana");
attribute_filter.push_back("potato");
init.set_attribute_filter(attribute_filter);
init.set_attributes(true);
// Create a MutationReporter for the list of registered observers.
scoped_ptr<std::vector<RegisteredObserver> > registered_observers(
new std::vector<RegisteredObserver>());
registered_observers->push_back(
RegisteredObserver(target.get(), observer, init));
MutationReporter reporter(target.get(), registered_observers.Pass());
// Report a few attribute mutations.
reporter.ReportAttributesMutation("banana", std::string("rotten"));
reporter.ReportAttributesMutation("apple", std::string("wormy"));
reporter.ReportAttributesMutation("potato", std::string("mashed"));
// Check that mutation records for the filtered attrbiutes have been queued.
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(2, records.size());
EXPECT_STREQ(records.at(0)->attribute_name()->c_str(), "banana");
EXPECT_STREQ(records.at(1)->attribute_name()->c_str(), "potato");
}
TEST_F(MutationObserverTest, RegisteredObserverList) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationObserver> observer = CreateObserver();
RegisteredObserverList observer_list(target.get());
// Add an observer with options.
MutationObserverInit options;
options.set_attributes(true);
EXPECT_TRUE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(1, observer_list.registered_observers().size());
EXPECT_EQ(observer, observer_list.registered_observers()[0].observer());
EXPECT_TRUE(observer_list.registered_observers()[0].options().attributes());
EXPECT_FALSE(
observer_list.registered_observers()[0].options().has_character_data());
EXPECT_FALSE(observer_list.registered_observers()[0].options().child_list());
// Adding the same observer updates the options.
options = MutationObserverInit();
options.set_child_list(true);
EXPECT_TRUE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(1, observer_list.registered_observers().size());
EXPECT_EQ(observer, observer_list.registered_observers()[0].observer());
EXPECT_FALSE(
observer_list.registered_observers()[0].options().has_attributes());
EXPECT_FALSE(
observer_list.registered_observers()[0].options().has_character_data());
EXPECT_TRUE(observer_list.registered_observers()[0].options().child_list());
// Remove the observer.
observer_list.RemoveMutationObserver(observer);
EXPECT_EQ(0, observer_list.registered_observers().size());
}
TEST_F(MutationObserverTest, LazilySetOptions) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationObserver> observer = CreateObserver();
RegisteredObserverList observer_list(target.get());
// |attributes| gets set if an attribute-related option is set.
MutationObserverInit options;
options.set_attribute_old_value(true);
EXPECT_TRUE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(1, observer_list.registered_observers().size());
EXPECT_TRUE(observer_list.registered_observers()[0].options().attributes());
// |character_data| gets set if an attribute-related option is set.
options = MutationObserverInit();
options.set_character_data_old_value(true);
EXPECT_TRUE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(1, observer_list.registered_observers().size());
EXPECT_TRUE(
observer_list.registered_observers()[0].options().character_data());
}
TEST_F(MutationObserverTest, InvalidOptions) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationObserver> observer = CreateObserver();
RegisteredObserverList observer_list(target.get());
// No type of mutation is set.
MutationObserverInit options;
EXPECT_FALSE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(0, observer_list.registered_observers().size());
// |attributes| is set as false, but attribute old data is set.
options.set_attributes(false);
options.set_attribute_old_value(true);
EXPECT_FALSE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(0, observer_list.registered_observers().size());
// |character_data| is set as false, but attribute old data is set.
options = MutationObserverInit();
options.set_character_data(false);
options.set_character_data_old_value(true);
EXPECT_FALSE(observer_list.AddMutationObserver(observer, options));
EXPECT_EQ(0, observer_list.registered_observers().size());
}
TEST_F(MutationObserverTest, InvalidOptionsRaisesException) {
scoped_refptr<dom::Element> target = CreateDiv();
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit invalid_options;
script::testing::MockExceptionState exception_state;
EXPECT_CALL(exception_state, SetSimpleExceptionVA(script::kTypeError, _, _));
observer->Observe(target, invalid_options, &exception_state);
}
TEST_F(MutationObserverTest, AddChildNodes) {
scoped_refptr<Element> root = CreateDiv();
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_subtree(true);
options.set_child_list(true);
observer->Observe(root, options);
scoped_refptr<Element> child1 = document()->CreateElement("div");
scoped_refptr<Element> child2 = document()->CreateElement("div");
ASSERT_TRUE(child1);
ASSERT_TRUE(child2);
root->AppendChild(child1);
child1->AppendChild(child2);
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(2, records.size());
EXPECT_EQ("childList", records.at(0)->type());
EXPECT_EQ(root, records.at(0)->target());
ASSERT_TRUE(records.at(0)->removed_nodes());
ASSERT_EQ(0, records.at(0)->removed_nodes()->length());
ASSERT_TRUE(records.at(0)->added_nodes());
ASSERT_EQ(1, records.at(0)->added_nodes()->length());
EXPECT_EQ(child1, records.at(0)->added_nodes()->Item(0));
EXPECT_EQ("childList", records.at(1)->type());
EXPECT_EQ(child1, records.at(1)->target());
ASSERT_TRUE(records.at(1)->removed_nodes());
ASSERT_EQ(0, records.at(1)->removed_nodes()->length());
ASSERT_TRUE(records.at(1)->added_nodes());
ASSERT_EQ(1, records.at(1)->added_nodes()->length());
EXPECT_EQ(child2, records.at(1)->added_nodes()->Item(0));
}
TEST_F(MutationObserverTest, RemoveChildNode) {
scoped_refptr<Element> root = CreateDiv();
scoped_refptr<Element> child1 = document()->CreateElement("div");
scoped_refptr<Element> child2 = document()->CreateElement("div");
ASSERT_TRUE(child1);
ASSERT_TRUE(child2);
root->AppendChild(child1);
child1->AppendChild(child2);
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_subtree(true);
options.set_child_list(true);
observer->Observe(root, options);
child1->RemoveChild(child2);
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
EXPECT_EQ("childList", records.at(0)->type());
EXPECT_EQ(child1, records.at(0)->target());
ASSERT_TRUE(records.at(0)->removed_nodes());
ASSERT_EQ(1, records.at(0)->removed_nodes()->length());
EXPECT_EQ(child2, records.at(0)->removed_nodes()->Item(0));
}
TEST_F(MutationObserverTest, MutateCharacterData) {
scoped_refptr<Element> root = CreateDiv();
scoped_refptr<Text> text = document()->CreateTextNode("initial-data");
ASSERT_TRUE(text);
root->AppendChild(text);
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_subtree(true);
options.set_character_data(true);
observer->Observe(root, options);
text->set_data("new-data");
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
EXPECT_EQ("characterData", records.at(0)->type());
EXPECT_EQ(text, records.at(0)->target());
EXPECT_FALSE(records.at(0)->old_value());
}
TEST_F(MutationObserverTest, MutateCharacterDataWithOldValue) {
scoped_refptr<Element> root = CreateDiv();
scoped_refptr<Text> text = document()->CreateTextNode("initial-data");
ASSERT_TRUE(text);
root->AppendChild(text);
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_subtree(true);
options.set_character_data_old_value(true);
observer->Observe(root, options);
text->set_data("new-data");
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
EXPECT_EQ("characterData", records.at(0)->type());
EXPECT_EQ(text, records.at(0)->target());
ASSERT_TRUE(records.at(0)->old_value());
EXPECT_STREQ("initial-data", records.at(0)->old_value()->c_str());
}
TEST_F(MutationObserverTest, MutateAttribute) {
scoped_refptr<Element> root = CreateDiv();
root->SetAttribute("banana", "purple");
root->SetAttribute("apple", "green");
scoped_refptr<MutationObserver> observer = CreateObserver();
script::Sequence<std::string> filter;
filter.push_back("banana");
MutationObserverInit options;
options.set_attributes(true);
options.set_attribute_filter(filter);
observer->Observe(root, options);
root->SetAttribute("banana", "yellow");
root->SetAttribute("apple", "brown");
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
EXPECT_EQ("attributes", records.at(0)->type());
EXPECT_EQ(root, records.at(0)->target());
EXPECT_FALSE(records.at(0)->old_value());
ASSERT_TRUE(records.at(0)->attribute_name());
EXPECT_STREQ("banana", records.at(0)->attribute_name()->c_str());
}
TEST_F(MutationObserverTest, MutateAttributeWithOldValue) {
scoped_refptr<Element> root = CreateDiv();
root->SetAttribute("banana", "purple");
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_attribute_old_value(true);
observer->Observe(root, options);
root->SetAttribute("banana", "yellow");
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
ASSERT_EQ(1, records.size());
EXPECT_EQ("attributes", records.at(0)->type());
EXPECT_EQ(root, records.at(0)->target());
ASSERT_TRUE(records.at(0)->old_value());
ASSERT_TRUE(records.at(0)->attribute_name());
EXPECT_STREQ("banana", records.at(0)->attribute_name()->c_str());
EXPECT_STREQ("purple", records.at(0)->old_value()->c_str());
}
TEST_F(MutationObserverTest, Disconnect) {
scoped_refptr<Element> root = CreateDiv();
scoped_refptr<Text> text = document()->CreateTextNode("initial-data");
ASSERT_TRUE(text);
root->AppendChild(text);
scoped_refptr<MutationObserver> observer = CreateObserver();
MutationObserverInit options;
options.set_subtree(true);
options.set_character_data(true);
observer->Observe(root, options);
// This should queue up a mutation record.
text->set_data("new-data");
observer->Disconnect();
MutationObserver::MutationRecordSequence records = observer->TakeRecords();
// MutationObserver.disconnect() should clear any queued records.
EXPECT_EQ(0, records.size());
// This should not queue a mutation record.
text->set_data("more-new-data");
records = observer->TakeRecords();
EXPECT_EQ(0, records.size());
}
} // namespace dom
} // namespace cobalt