blob: 1d9dc837abef0432ee7477f32f53ba6c7332a328 [file] [log] [blame]
/*
* Copyright (c) 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/dom/document.h"
#include <memory>
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "components/ukm/test_ukm_recorder.h"
#include "services/network/public/mojom/referrer_policy.mojom-blink.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/common/feature_policy/document_policy_features.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom-blink.h"
#include "third_party/blink/public/web/web_print_page_description.h"
#include "third_party/blink/renderer/bindings/core/v8/isolated_world_csp.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/core/css/media_query_list_listener.h"
#include "third_party/blink/renderer/core/css/media_query_matcher.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/dom/dom_implementation.h"
#include "third_party/blink/renderer/core/dom/node_with_index.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/synchronous_mutation_observer.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/reporting_context.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/viewport_data.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/html/html_link_element.h"
#include "third_party/blink/renderer/core/loader/appcache/application_cache_host_for_frame.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/validation_message_client.h"
#include "third_party/blink/renderer/core/testing/color_scheme_helper.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/core/testing/scoped_mock_overlay_scrollbars.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
#include "third_party/blink/renderer/platform/weborigin/scheme_registry.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "url/url_util.h"
namespace blink {
using network::mojom::ContentSecurityPolicySource;
using network::mojom::ContentSecurityPolicyType;
class DocumentTest : public PageTestBase {
public:
static void SimulateHasTrustTokensAnswererConnectionError(
Document* document) {
document->HasTrustTokensAnswererConnectionError();
}
protected:
void TearDown() override {
ThreadState::Current()->CollectAllGarbageForTesting();
PageTestBase::TearDown();
}
void SetHtmlInnerHTML(const char*);
};
void DocumentTest::SetHtmlInnerHTML(const char* html_content) {
GetDocument().documentElement()->setInnerHTML(String::FromUTF8(html_content));
UpdateAllLifecyclePhasesForTest();
}
class DocumentSimTest : public SimTest {};
namespace {
class TestSynchronousMutationObserver
: public GarbageCollected<TestSynchronousMutationObserver>,
public SynchronousMutationObserver {
public:
struct MergeTextNodesRecord : GarbageCollected<MergeTextNodesRecord> {
Member<const Text> node_;
Member<Node> node_to_be_removed_;
unsigned offset_ = 0;
MergeTextNodesRecord(const Text* node,
const NodeWithIndex& node_with_index,
unsigned offset)
: node_(node),
node_to_be_removed_(node_with_index.GetNode()),
offset_(offset) {}
void Trace(Visitor* visitor) const {
visitor->Trace(node_);
visitor->Trace(node_to_be_removed_);
}
};
struct UpdateCharacterDataRecord
: GarbageCollected<UpdateCharacterDataRecord> {
Member<CharacterData> node_;
unsigned offset_ = 0;
unsigned old_length_ = 0;
unsigned new_length_ = 0;
UpdateCharacterDataRecord(CharacterData* node,
unsigned offset,
unsigned old_length,
unsigned new_length)
: node_(node),
offset_(offset),
old_length_(old_length),
new_length_(new_length) {}
void Trace(Visitor* visitor) const { visitor->Trace(node_); }
};
explicit TestSynchronousMutationObserver(Document&);
TestSynchronousMutationObserver(const TestSynchronousMutationObserver&) =
delete;
TestSynchronousMutationObserver& operator=(
const TestSynchronousMutationObserver&) = delete;
virtual ~TestSynchronousMutationObserver() = default;
int CountContextDestroyedCalled() const {
return on_document_shutdown_called_counter_;
}
const HeapVector<Member<const ContainerNode>>& ChildrenChangedNodes() const {
return children_changed_nodes_;
}
const HeapVector<Member<MergeTextNodesRecord>>& MergeTextNodesRecords()
const {
return merge_text_nodes_records_;
}
const HeapVector<Member<const Node>>& MoveTreeToNewDocumentNodes() const {
return move_tree_to_new_document_nodes_;
}
const HeapVector<Member<ContainerNode>>& RemovedChildrenNodes() const {
return removed_children_nodes_;
}
const HeapVector<Member<Node>>& RemovedNodes() const {
return removed_nodes_;
}
const HeapVector<Member<const Text>>& SplitTextNodes() const {
return split_text_nodes_;
}
const HeapVector<Member<UpdateCharacterDataRecord>>&
UpdatedCharacterDataRecords() const {
return updated_character_data_records_;
}
void Trace(Visitor*) const override;
private:
// Implement |SynchronousMutationObserver| member functions.
void ContextDestroyed() final;
void DidChangeChildren(const ContainerNode&) final;
void DidMergeTextNodes(const Text&, const NodeWithIndex&, unsigned) final;
void DidMoveTreeToNewDocument(const Node& root) final;
void DidSplitTextNode(const Text&) final;
void DidUpdateCharacterData(CharacterData*,
unsigned offset,
unsigned old_length,
unsigned new_length) final;
void NodeChildrenWillBeRemoved(ContainerNode&) final;
void NodeWillBeRemoved(Node&) final;
int on_document_shutdown_called_counter_ = 0;
HeapVector<Member<const ContainerNode>> children_changed_nodes_;
HeapVector<Member<MergeTextNodesRecord>> merge_text_nodes_records_;
HeapVector<Member<const Node>> move_tree_to_new_document_nodes_;
HeapVector<Member<ContainerNode>> removed_children_nodes_;
HeapVector<Member<Node>> removed_nodes_;
HeapVector<Member<const Text>> split_text_nodes_;
HeapVector<Member<UpdateCharacterDataRecord>> updated_character_data_records_;
};
TestSynchronousMutationObserver::TestSynchronousMutationObserver(
Document& document) {
SetDocument(&document);
}
void TestSynchronousMutationObserver::ContextDestroyed() {
++on_document_shutdown_called_counter_;
}
void TestSynchronousMutationObserver::DidChangeChildren(
const ContainerNode& container) {
children_changed_nodes_.push_back(&container);
}
void TestSynchronousMutationObserver::DidMergeTextNodes(
const Text& node,
const NodeWithIndex& node_with_index,
unsigned offset) {
merge_text_nodes_records_.push_back(
MakeGarbageCollected<MergeTextNodesRecord>(&node, node_with_index,
offset));
}
void TestSynchronousMutationObserver::DidMoveTreeToNewDocument(
const Node& root) {
move_tree_to_new_document_nodes_.push_back(&root);
}
void TestSynchronousMutationObserver::DidSplitTextNode(const Text& node) {
split_text_nodes_.push_back(&node);
}
void TestSynchronousMutationObserver::DidUpdateCharacterData(
CharacterData* character_data,
unsigned offset,
unsigned old_length,
unsigned new_length) {
updated_character_data_records_.push_back(
MakeGarbageCollected<UpdateCharacterDataRecord>(character_data, offset,
old_length, new_length));
}
void TestSynchronousMutationObserver::NodeChildrenWillBeRemoved(
ContainerNode& container) {
removed_children_nodes_.push_back(&container);
}
void TestSynchronousMutationObserver::NodeWillBeRemoved(Node& node) {
removed_nodes_.push_back(&node);
}
void TestSynchronousMutationObserver::Trace(Visitor* visitor) const {
visitor->Trace(children_changed_nodes_);
visitor->Trace(merge_text_nodes_records_);
visitor->Trace(move_tree_to_new_document_nodes_);
visitor->Trace(removed_children_nodes_);
visitor->Trace(removed_nodes_);
visitor->Trace(split_text_nodes_);
visitor->Trace(updated_character_data_records_);
SynchronousMutationObserver::Trace(visitor);
}
class MockDocumentValidationMessageClient
: public GarbageCollected<MockDocumentValidationMessageClient>,
public ValidationMessageClient {
public:
MockDocumentValidationMessageClient() { Reset(); }
void Reset() {
show_validation_message_was_called = false;
document_detached_was_called = false;
}
bool show_validation_message_was_called;
bool document_detached_was_called;
// ValidationMessageClient functions.
void ShowValidationMessage(const Element& anchor,
const String& main_message,
TextDirection,
const String& sub_message,
TextDirection) override {
show_validation_message_was_called = true;
}
void HideValidationMessage(const Element& anchor) override {}
bool IsValidationMessageVisible(const Element& anchor) override {
return true;
}
void DocumentDetached(const Document&) override {
document_detached_was_called = true;
}
void DidChangeFocusTo(const Element*) override {}
void WillBeDestroyed() override {}
// virtual void Trace(Visitor* visitor) const {
// ValidationMessageClient::trace(visitor); }
};
class MockApplicationCacheHost final : public ApplicationCacheHostForFrame {
public:
explicit MockApplicationCacheHost(DocumentLoader* loader)
: ApplicationCacheHostForFrame(loader,
GetEmptyBrowserInterfaceBroker(),
/*task_runner=*/nullptr,
base::UnguessableToken()) {}
~MockApplicationCacheHost() override = default;
void SelectCacheWithoutManifest() override {
without_manifest_was_called_ = true;
}
bool without_manifest_was_called_ = false;
};
class PrefersColorSchemeTestListener final : public MediaQueryListListener {
public:
void NotifyMediaQueryChanged() override { notified_ = true; }
bool IsNotified() const { return notified_; }
private:
bool notified_ = false;
};
bool IsDOMException(ScriptState* script_state,
ScriptValue value,
DOMExceptionCode code) {
auto* dom_exception = V8DOMException::ToImplWithTypeCheck(
script_state->GetIsolate(), value.V8Value());
if (!dom_exception)
return false;
// Unfortunately, it's not enough to check |dom_exception->code() == code|,
// as DOMException::code is only populated for the DOMExceptionCodes with
// "legacy code" numeric values.
return dom_exception->name() == DOMException(code).name();
}
} // anonymous namespace
TEST_F(DocumentTest, CreateRangeAdjustedToTreeScopeWithPositionInShadowTree) {
GetDocument().body()->setInnerHTML("<div><select><option>012</option></div>");
Element* const select_element = GetDocument().QuerySelector("select");
const Position& position =
Position(*select_element->UserAgentShadowRoot(),
select_element->UserAgentShadowRoot()->CountChildren());
Range* const range =
Document::CreateRangeAdjustedToTreeScope(GetDocument(), position);
EXPECT_EQ(range->startContainer(), select_element->parentNode());
EXPECT_EQ(static_cast<unsigned>(range->startOffset()),
select_element->NodeIndex());
EXPECT_TRUE(range->collapsed());
}
TEST_F(DocumentTest, DomTreeVersionForRemoval) {
// ContainerNode::CollectChildrenAndRemoveFromOldParentWithCheck assumes this
// behavior.
Document& doc = GetDocument();
{
DocumentFragment* fragment = DocumentFragment::Create(doc);
fragment->appendChild(
MakeGarbageCollected<Element>(html_names::kDivTag, &doc));
fragment->appendChild(
MakeGarbageCollected<Element>(html_names::kSpanTag, &doc));
uint64_t original_version = doc.DomTreeVersion();
fragment->RemoveChildren();
EXPECT_EQ(original_version + 1, doc.DomTreeVersion())
<< "RemoveChildren() should increase DomTreeVersion by 1.";
}
{
DocumentFragment* fragment = DocumentFragment::Create(doc);
Node* child = MakeGarbageCollected<Element>(html_names::kDivTag, &doc);
child->appendChild(
MakeGarbageCollected<Element>(html_names::kSpanTag, &doc));
fragment->appendChild(child);
uint64_t original_version = doc.DomTreeVersion();
fragment->removeChild(child);
EXPECT_EQ(original_version + 1, doc.DomTreeVersion())
<< "removeChild() should increase DomTreeVersion by 1.";
}
}
// This tests that we properly resize and re-layout pages for printing in the
// presence of media queries effecting elements in a subtree layout boundary
TEST_F(DocumentTest, PrintRelayout) {
SetHtmlInnerHTML(R"HTML(
<style>
div {
width: 100px;
height: 100px;
overflow: hidden;
}
span {
width: 50px;
height: 50px;
}
@media screen {
span {
width: 20px;
}
}
</style>
<p><div><span></span></div></p>
)HTML");
FloatSize page_size(400, 400);
float maximum_shrink_ratio = 1.6;
GetDocument().GetFrame()->StartPrinting(page_size, page_size,
maximum_shrink_ratio);
EXPECT_EQ(GetDocument().documentElement()->OffsetWidth(), 400);
GetDocument().GetFrame()->EndPrinting();
EXPECT_EQ(GetDocument().documentElement()->OffsetWidth(), 800);
}
// This test checks that Documunt::linkManifest() returns a value conform to the
// specification.
TEST_F(DocumentTest, LinkManifest) {
// Test the default result.
EXPECT_EQ(nullptr, GetDocument().LinkManifest());
// Check that we use the first manifest with <link rel=manifest>
auto* link = MakeGarbageCollected<HTMLLinkElement>(GetDocument(),
CreateElementFlags());
link->setAttribute(blink::html_names::kRelAttr, "manifest");
link->setAttribute(blink::html_names::kHrefAttr, "foo.json");
GetDocument().head()->AppendChild(link);
EXPECT_EQ(link, GetDocument().LinkManifest());
auto* link2 = MakeGarbageCollected<HTMLLinkElement>(GetDocument(),
CreateElementFlags());
link2->setAttribute(blink::html_names::kRelAttr, "manifest");
link2->setAttribute(blink::html_names::kHrefAttr, "bar.json");
GetDocument().head()->InsertBefore(link2, link);
EXPECT_EQ(link2, GetDocument().LinkManifest());
GetDocument().head()->AppendChild(link2);
EXPECT_EQ(link, GetDocument().LinkManifest());
// Check that crazy URLs are accepted.
link->setAttribute(blink::html_names::kHrefAttr, "http:foo.json");
EXPECT_EQ(link, GetDocument().LinkManifest());
// Check that empty URLs are accepted.
link->setAttribute(blink::html_names::kHrefAttr, "");
EXPECT_EQ(link, GetDocument().LinkManifest());
// Check that URLs from different origins are accepted.
link->setAttribute(blink::html_names::kHrefAttr,
"http://example.org/manifest.json");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kHrefAttr,
"http://foo.example.org/manifest.json");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kHrefAttr,
"http://foo.bar/manifest.json");
EXPECT_EQ(link, GetDocument().LinkManifest());
// More than one token in @rel is accepted.
link->setAttribute(blink::html_names::kRelAttr, "foo bar manifest");
EXPECT_EQ(link, GetDocument().LinkManifest());
// Such as spaces around the token.
link->setAttribute(blink::html_names::kRelAttr, " manifest ");
EXPECT_EQ(link, GetDocument().LinkManifest());
// Check that rel=manifest actually matters.
link->setAttribute(blink::html_names::kRelAttr, "");
EXPECT_EQ(link2, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kRelAttr, "manifest");
// Check that link outside of the <head> are ignored.
GetDocument().head()->RemoveChild(link);
GetDocument().head()->RemoveChild(link2);
EXPECT_EQ(nullptr, GetDocument().LinkManifest());
GetDocument().body()->AppendChild(link);
EXPECT_EQ(nullptr, GetDocument().LinkManifest());
GetDocument().head()->AppendChild(link);
GetDocument().head()->AppendChild(link2);
// Check that some attribute values do not have an effect.
link->setAttribute(blink::html_names::kCrossoriginAttr, "use-credentials");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kHreflangAttr, "klingon");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kTypeAttr, "image/gif");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kSizesAttr, "16x16");
EXPECT_EQ(link, GetDocument().LinkManifest());
link->setAttribute(blink::html_names::kMediaAttr, "print");
EXPECT_EQ(link, GetDocument().LinkManifest());
}
TEST_F(DocumentTest, StyleVersion) {
SetHtmlInnerHTML(R"HTML(
<style>
.a * { color: green }
.b .c { color: green }
</style>
<div id='x'><span class='c'></span></div>
)HTML");
Element* element = GetDocument().getElementById("x");
EXPECT_TRUE(element);
uint64_t previous_style_version = GetDocument().StyleVersion();
element->setAttribute(blink::html_names::kClassAttr, "notfound");
EXPECT_EQ(previous_style_version, GetDocument().StyleVersion());
UpdateAllLifecyclePhasesForTest();
previous_style_version = GetDocument().StyleVersion();
element->setAttribute(blink::html_names::kClassAttr, "a");
EXPECT_NE(previous_style_version, GetDocument().StyleVersion());
UpdateAllLifecyclePhasesForTest();
previous_style_version = GetDocument().StyleVersion();
element->setAttribute(blink::html_names::kClassAttr, "a b");
EXPECT_NE(previous_style_version, GetDocument().StyleVersion());
}
TEST_F(DocumentTest, SynchronousMutationNotifier) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
EXPECT_EQ(GetDocument(), observer.GetDocument());
EXPECT_EQ(0, observer.CountContextDestroyedCalled());
Element* div_node = GetDocument().CreateRawElement(html_names::kDivTag);
GetDocument().body()->AppendChild(div_node);
Element* bold_node = GetDocument().CreateRawElement(html_names::kBTag);
div_node->AppendChild(bold_node);
Element* italic_node = GetDocument().CreateRawElement(html_names::kITag);
div_node->AppendChild(italic_node);
Node* text_node = GetDocument().createTextNode("0123456789");
bold_node->AppendChild(text_node);
EXPECT_TRUE(observer.RemovedNodes().IsEmpty());
text_node->remove();
ASSERT_EQ(1u, observer.RemovedNodes().size());
EXPECT_EQ(text_node, observer.RemovedNodes()[0]);
div_node->RemoveChildren();
EXPECT_EQ(1u, observer.RemovedNodes().size())
<< "ContainerNode::removeChildren() doesn't call nodeWillBeRemoved()";
ASSERT_EQ(1u, observer.RemovedChildrenNodes().size());
EXPECT_EQ(div_node, observer.RemovedChildrenNodes()[0]);
GetDocument().Shutdown();
EXPECT_EQ(nullptr, observer.GetDocument());
EXPECT_EQ(1, observer.CountContextDestroyedCalled());
}
TEST_F(DocumentTest, SynchronousMutationNotifieAppendChild) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
GetDocument().body()->AppendChild(GetDocument().createTextNode("a123456789"));
ASSERT_EQ(1u, observer.ChildrenChangedNodes().size());
EXPECT_EQ(GetDocument().body(), observer.ChildrenChangedNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifieInsertBefore) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
GetDocument().documentElement()->InsertBefore(
GetDocument().createTextNode("a123456789"), GetDocument().body());
ASSERT_EQ(1u, observer.ChildrenChangedNodes().size());
EXPECT_EQ(GetDocument().documentElement(),
observer.ChildrenChangedNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifierMergeTextNodes) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
Text* merge_sample_a = GetDocument().createTextNode("a123456789");
GetDocument().body()->AppendChild(merge_sample_a);
Text* merge_sample_b = GetDocument().createTextNode("b123456789");
GetDocument().body()->AppendChild(merge_sample_b);
EXPECT_EQ(0u, observer.MergeTextNodesRecords().size());
GetDocument().body()->normalize();
ASSERT_EQ(1u, observer.MergeTextNodesRecords().size());
EXPECT_EQ(merge_sample_a, observer.MergeTextNodesRecords()[0]->node_);
EXPECT_EQ(merge_sample_b,
observer.MergeTextNodesRecords()[0]->node_to_be_removed_);
EXPECT_EQ(10u, observer.MergeTextNodesRecords()[0]->offset_);
}
TEST_F(DocumentTest, SynchronousMutationNotifierMoveTreeToNewDocument) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
Node* move_sample = GetDocument().CreateRawElement(html_names::kDivTag);
move_sample->appendChild(GetDocument().createTextNode("a123"));
move_sample->appendChild(GetDocument().createTextNode("b456"));
GetDocument().body()->AppendChild(move_sample);
Document& another_document = *Document::CreateForTest();
another_document.AppendChild(move_sample);
EXPECT_EQ(1u, observer.MoveTreeToNewDocumentNodes().size());
EXPECT_EQ(move_sample, observer.MoveTreeToNewDocumentNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifieRemoveChild) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
GetDocument().documentElement()->RemoveChild(GetDocument().body());
ASSERT_EQ(1u, observer.ChildrenChangedNodes().size());
EXPECT_EQ(GetDocument().documentElement(),
observer.ChildrenChangedNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifieReplaceChild) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
Element* const replaced_node = GetDocument().body();
GetDocument().documentElement()->ReplaceChild(
GetDocument().CreateRawElement(html_names::kDivTag),
GetDocument().body());
ASSERT_EQ(2u, observer.ChildrenChangedNodes().size());
EXPECT_EQ(GetDocument().documentElement(),
observer.ChildrenChangedNodes()[0]);
EXPECT_EQ(GetDocument().documentElement(),
observer.ChildrenChangedNodes()[1]);
ASSERT_EQ(1u, observer.RemovedNodes().size());
EXPECT_EQ(replaced_node, observer.RemovedNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifierSplitTextNode) {
V8TestingScope scope;
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
Text* split_sample = GetDocument().createTextNode("0123456789");
GetDocument().body()->AppendChild(split_sample);
split_sample->splitText(4, ASSERT_NO_EXCEPTION);
ASSERT_EQ(1u, observer.SplitTextNodes().size());
EXPECT_EQ(split_sample, observer.SplitTextNodes()[0]);
}
TEST_F(DocumentTest, SynchronousMutationNotifierUpdateCharacterData) {
auto& observer =
*MakeGarbageCollected<TestSynchronousMutationObserver>(GetDocument());
Text* append_sample = GetDocument().createTextNode("a123456789");
GetDocument().body()->AppendChild(append_sample);
Text* delete_sample = GetDocument().createTextNode("b123456789");
GetDocument().body()->AppendChild(delete_sample);
Text* insert_sample = GetDocument().createTextNode("c123456789");
GetDocument().body()->AppendChild(insert_sample);
Text* replace_sample = GetDocument().createTextNode("c123456789");
GetDocument().body()->AppendChild(replace_sample);
EXPECT_EQ(0u, observer.UpdatedCharacterDataRecords().size());
append_sample->appendData("abc");
ASSERT_EQ(1u, observer.UpdatedCharacterDataRecords().size());
EXPECT_EQ(append_sample, observer.UpdatedCharacterDataRecords()[0]->node_);
EXPECT_EQ(10u, observer.UpdatedCharacterDataRecords()[0]->offset_);
EXPECT_EQ(0u, observer.UpdatedCharacterDataRecords()[0]->old_length_);
EXPECT_EQ(3u, observer.UpdatedCharacterDataRecords()[0]->new_length_);
delete_sample->deleteData(3, 4, ASSERT_NO_EXCEPTION);
ASSERT_EQ(2u, observer.UpdatedCharacterDataRecords().size());
EXPECT_EQ(delete_sample, observer.UpdatedCharacterDataRecords()[1]->node_);
EXPECT_EQ(3u, observer.UpdatedCharacterDataRecords()[1]->offset_);
EXPECT_EQ(4u, observer.UpdatedCharacterDataRecords()[1]->old_length_);
EXPECT_EQ(0u, observer.UpdatedCharacterDataRecords()[1]->new_length_);
insert_sample->insertData(3, "def", ASSERT_NO_EXCEPTION);
ASSERT_EQ(3u, observer.UpdatedCharacterDataRecords().size());
EXPECT_EQ(insert_sample, observer.UpdatedCharacterDataRecords()[2]->node_);
EXPECT_EQ(3u, observer.UpdatedCharacterDataRecords()[2]->offset_);
EXPECT_EQ(0u, observer.UpdatedCharacterDataRecords()[2]->old_length_);
EXPECT_EQ(3u, observer.UpdatedCharacterDataRecords()[2]->new_length_);
replace_sample->replaceData(6, 4, "ghi", ASSERT_NO_EXCEPTION);
ASSERT_EQ(4u, observer.UpdatedCharacterDataRecords().size());
EXPECT_EQ(replace_sample, observer.UpdatedCharacterDataRecords()[3]->node_);
EXPECT_EQ(6u, observer.UpdatedCharacterDataRecords()[3]->offset_);
EXPECT_EQ(4u, observer.UpdatedCharacterDataRecords()[3]->old_length_);
EXPECT_EQ(3u, observer.UpdatedCharacterDataRecords()[3]->new_length_);
}
// This tests that meta-theme-color can be found correctly
TEST_F(DocumentTest, ThemeColor) {
{
SetHtmlInnerHTML(
"<meta name=\"theme-color\" content=\"#00ff00\">"
"<body>");
EXPECT_EQ(Color(0, 255, 0), GetDocument().ThemeColor())
<< "Theme color should be bright green.";
}
{
SetHtmlInnerHTML(
"<body>"
"<meta name=\"theme-color\" content=\"#00ff00\">");
EXPECT_EQ(Color(0, 255, 0), GetDocument().ThemeColor())
<< "Theme color should be bright green.";
}
}
TEST_F(DocumentTest, ValidationMessageCleanup) {
ValidationMessageClient* original_client =
&GetPage().GetValidationMessageClient();
MockDocumentValidationMessageClient* mock_client =
MakeGarbageCollected<MockDocumentValidationMessageClient>();
GetDocument().GetSettings()->SetScriptEnabled(true);
GetPage().SetValidationMessageClientForTesting(mock_client);
// ImplicitOpen()-CancelParsing() makes Document.loadEventFinished()
// true. It's necessary to kick unload process.
GetDocument().ImplicitOpen(kForceSynchronousParsing);
GetDocument().CancelParsing();
GetDocument().AppendChild(
GetDocument().CreateRawElement(html_names::kHTMLTag));
SetHtmlInnerHTML("<body><input required></body>");
Element* script = GetDocument().CreateRawElement(html_names::kScriptTag);
script->setTextContent(
"window.onunload = function() {"
"document.querySelector('input').reportValidity(); };");
GetDocument().body()->AppendChild(script);
auto* input = To<HTMLInputElement>(GetDocument().body()->firstChild());
DVLOG(0) << GetDocument().body()->outerHTML();
// Sanity check.
input->reportValidity();
EXPECT_TRUE(mock_client->show_validation_message_was_called);
mock_client->Reset();
// DetachDocument() unloads the document, and shutdowns.
GetDocument().GetFrame()->DetachDocument();
EXPECT_TRUE(mock_client->document_detached_was_called);
// Unload handler tried to show a validation message, but it should fail.
EXPECT_FALSE(mock_client->show_validation_message_was_called);
GetPage().SetValidationMessageClientForTesting(original_client);
}
TEST_F(DocumentTest, SandboxDisablesAppCache) {
NavigateTo(KURL("https://test.com/foobar/document"),
{{http_names::kContentSecurityPolicy, "sandbox"}});
GetDocument().Loader()->SetApplicationCacheHostForTesting(
MakeGarbageCollected<MockApplicationCacheHost>(GetDocument().Loader()));
ApplicationCacheHostForFrame* appcache_host =
GetDocument().Loader()->GetApplicationCacheHost();
appcache_host->SelectCacheWithManifest(
KURL("https://test.com/foobar/manifest"));
auto* mock_host = static_cast<MockApplicationCacheHost*>(appcache_host);
EXPECT_TRUE(mock_host->without_manifest_was_called_);
}
// Verifies that calling EnsurePaintLocationDataValidForNode cleans compositor
// inputs only when necessary. We generally want to avoid cleaning the inputs,
// as it is more expensive than just doing layout.
TEST_F(DocumentTest,
EnsurePaintLocationDataValidForNodeCompositingInputsOnlyWhenNecessary) {
GetDocument().body()->setInnerHTML(R"HTML(
<div id='ancestor'>
<div id='sticky' style='position:sticky;'>
<div id='stickyChild'></div>
</div>
<div id='nonSticky'></div>
</div>
)HTML");
GetDocument().UpdateStyleAndLayoutTree();
EXPECT_EQ(DocumentLifecycle::kStyleClean,
GetDocument().Lifecycle().GetState());
// Asking for any element that is not affected by a sticky element should only
// advance the lifecycle to layout clean.
GetDocument().EnsurePaintLocationDataValidForNode(
GetDocument().getElementById("ancestor"), DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kLayoutClean,
GetDocument().Lifecycle().GetState());
GetDocument().EnsurePaintLocationDataValidForNode(
GetDocument().getElementById("nonSticky"), DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kLayoutClean,
GetDocument().Lifecycle().GetState());
// However, asking for either the sticky element or it's descendents should
// clean compositing inputs as well.
GetDocument().EnsurePaintLocationDataValidForNode(
GetDocument().getElementById("sticky"), DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kLayoutClean,
GetDocument().Lifecycle().GetState());
// Dirty layout.
GetDocument().body()->setAttribute("style", "background: red;");
EXPECT_EQ(DocumentLifecycle::kVisualUpdatePending,
GetDocument().Lifecycle().GetState());
GetDocument().EnsurePaintLocationDataValidForNode(
GetDocument().getElementById("stickyChild"), DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kLayoutClean,
GetDocument().Lifecycle().GetState());
}
// Tests that the difference in computed style of direction on the html and body
// elements does not trigger a style recalc for viewport style propagation when
// the computed style for another element in the document is recalculated.
TEST_F(DocumentTest, ViewportPropagationNoRecalc) {
SetHtmlInnerHTML(R"HTML(
<body style='direction:rtl'>
<div id=recalc></div>
</body>
)HTML");
int old_element_count = GetDocument().GetStyleEngine().StyleForElementCount();
Element* div = GetDocument().getElementById("recalc");
div->setAttribute("style", "color:green");
GetDocument().UpdateStyleAndLayoutTree();
int new_element_count = GetDocument().GetStyleEngine().StyleForElementCount();
EXPECT_EQ(1, new_element_count - old_element_count);
}
TEST_F(DocumentTest, CanExecuteScriptsWithSandboxAndIsolatedWorld) {
NavigateTo(KURL("https://www.example.com/"),
{{http_names::kContentSecurityPolicy, "sandbox"}});
LocalFrame* frame = GetDocument().GetFrame();
frame->GetSettings()->SetScriptEnabled(true);
ScriptState* main_world_script_state = ToScriptStateForMainWorld(frame);
v8::Isolate* isolate = main_world_script_state->GetIsolate();
constexpr int kIsolatedWorldWithoutCSPId = 1;
scoped_refptr<DOMWrapperWorld> world_without_csp =
DOMWrapperWorld::EnsureIsolatedWorld(isolate, kIsolatedWorldWithoutCSPId);
ScriptState* isolated_world_without_csp_script_state =
ToScriptState(frame, *world_without_csp);
ASSERT_TRUE(world_without_csp->IsIsolatedWorld());
EXPECT_FALSE(IsolatedWorldCSP::Get().HasContentSecurityPolicy(
kIsolatedWorldWithoutCSPId));
constexpr int kIsolatedWorldWithCSPId = 2;
scoped_refptr<DOMWrapperWorld> world_with_csp =
DOMWrapperWorld::EnsureIsolatedWorld(isolate, kIsolatedWorldWithCSPId);
IsolatedWorldCSP::Get().SetContentSecurityPolicy(
kIsolatedWorldWithCSPId, String::FromUTF8("script-src *"),
SecurityOrigin::Create(KURL("chrome-extension://123")));
ScriptState* isolated_world_with_csp_script_state =
ToScriptState(frame, *world_with_csp);
ASSERT_TRUE(world_with_csp->IsIsolatedWorld());
EXPECT_TRUE(IsolatedWorldCSP::Get().HasContentSecurityPolicy(
kIsolatedWorldWithCSPId));
{
// Since the page is sandboxed, main world script execution shouldn't be
// allowed.
ScriptState::Scope scope(main_world_script_state);
EXPECT_FALSE(frame->DomWindow()->CanExecuteScripts(kAboutToExecuteScript));
}
{
// Isolated worlds without a dedicated CSP should also not be allowed to
// run scripts.
ScriptState::Scope scope(isolated_world_without_csp_script_state);
EXPECT_FALSE(frame->DomWindow()->CanExecuteScripts(kAboutToExecuteScript));
}
{
// An isolated world with a CSP should bypass the main world CSP, and be
// able to run scripts.
ScriptState::Scope scope(isolated_world_with_csp_script_state);
EXPECT_TRUE(frame->DomWindow()->CanExecuteScripts(kAboutToExecuteScript));
}
}
// Android does not support non-overlay top-level scrollbars.
#if !defined(OS_ANDROID)
TEST_F(DocumentTest, ElementFromPointOnScrollbar) {
GetDocument().SetCompatibilityMode(Document::kQuirksMode);
// This test requires that scrollbars take up space.
ScopedMockOverlayScrollbars no_overlay_scrollbars(false);
SetHtmlInnerHTML(R"HTML(
<style>
body { margin: 0; }
</style>
<div id='content'>content</div>
)HTML");
// A hit test close to the bottom of the page without scrollbars should hit
// the body element.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 590), GetDocument().body());
// Add width which will cause a horizontal scrollbar.
auto* content = GetDocument().getElementById("content");
content->setAttribute("style", "width: 101%;");
// A hit test on the horizontal scrollbar should not return an element because
// it is outside the viewport.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 590), nullptr);
// A hit test above the horizontal scrollbar should hit the body element.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 580), GetDocument().body());
}
#endif // defined(OS_ANDROID)
TEST_F(DocumentTest, ElementFromPointWithPageZoom) {
GetDocument().SetCompatibilityMode(Document::kQuirksMode);
// This test requires that scrollbars take up space.
ScopedMockOverlayScrollbars no_overlay_scrollbars(false);
SetHtmlInnerHTML(R"HTML(
<style>
body { margin: 0; }
</style>
<div id='content' style='height: 10px;'>content</div>
)HTML");
// A hit test on the content div should hit it.
auto* content = GetDocument().getElementById("content");
EXPECT_EQ(GetDocument().ElementFromPoint(1, 8), content);
// A hit test below the content div should not hit it.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 12), GetDocument().body());
// Zoom the page by 2x,
GetDocument().GetFrame()->SetPageZoomFactor(2);
// A hit test on the content div should hit it.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 8), content);
// A hit test below the content div should not hit it.
EXPECT_EQ(GetDocument().ElementFromPoint(1, 12), GetDocument().body());
}
TEST_F(DocumentTest, PrefersColorSchemeChanged) {
ColorSchemeHelper color_scheme_helper(GetDocument());
color_scheme_helper.SetPreferredColorScheme(
mojom::blink::PreferredColorScheme::kLight);
UpdateAllLifecyclePhasesForTest();
auto* list = GetDocument().GetMediaQueryMatcher().MatchMedia(
"(prefers-color-scheme: dark)");
auto* listener = MakeGarbageCollected<PrefersColorSchemeTestListener>();
list->AddListener(listener);
EXPECT_FALSE(listener->IsNotified());
color_scheme_helper.SetPreferredColorScheme(
mojom::blink::PreferredColorScheme::kDark);
UpdateAllLifecyclePhasesForTest();
GetDocument().ServiceScriptedAnimations(base::TimeTicks());
EXPECT_TRUE(listener->IsNotified());
}
TEST_F(DocumentTest, FindInPageUkm) {
GetDocument().ukm_recorder_ = std::make_unique<ukm::TestUkmRecorder>();
auto* recorder =
static_cast<ukm::TestUkmRecorder*>(GetDocument().UkmRecorder());
EXPECT_EQ(recorder->entries_count(), 0u);
GetDocument().MarkHasFindInPageRequest();
EXPECT_EQ(recorder->entries_count(), 1u);
GetDocument().MarkHasFindInPageRequest();
EXPECT_EQ(recorder->entries_count(), 1u);
auto entries = recorder->GetEntriesByName("Blink.FindInPage");
EXPECT_EQ(entries.size(), 1u);
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(entries[0], "DidSearch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[0], "DidSearch"), 1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(
entries[0], "DidHaveRenderSubtreeMatch"));
GetDocument().MarkHasFindInPageContentVisibilityActiveMatch();
EXPECT_EQ(recorder->entries_count(), 2u);
GetDocument().MarkHasFindInPageContentVisibilityActiveMatch();
EXPECT_EQ(recorder->entries_count(), 2u);
entries = recorder->GetEntriesByName("Blink.FindInPage");
EXPECT_EQ(entries.size(), 2u);
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(entries[0], "DidSearch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[0], "DidSearch"), 1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(
entries[0], "DidHaveRenderSubtreeMatch"));
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(
entries[1], "DidHaveRenderSubtreeMatch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[1],
"DidHaveRenderSubtreeMatch"),
1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(entries[1], "DidSearch"));
}
TEST_F(DocumentTest, FindInPageUkmInFrame) {
std::string base_url = "http://internal.test/";
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8(base_url), test::CoreTestDataPath(),
WebString::FromUTF8("visible_iframe.html"));
url_test_helpers::RegisterMockedURLLoadFromBase(
WebString::FromUTF8(base_url), test::CoreTestDataPath(),
WebString::FromUTF8("single_iframe.html"));
frame_test_helpers::WebViewHelper web_view_helper;
WebViewImpl* web_view_impl =
web_view_helper.InitializeAndLoad(base_url + "single_iframe.html");
web_view_impl->MainFrameWidget()->UpdateAllLifecyclePhases(
DocumentUpdateReason::kTest);
Document* top_doc = web_view_impl->MainFrameImpl()->GetFrame()->GetDocument();
auto* iframe = To<HTMLIFrameElement>(top_doc->QuerySelector("iframe"));
Document* document = iframe->contentDocument();
ASSERT_TRUE(document);
ASSERT_FALSE(document->IsInMainFrame());
// Save the old recorder and replace it with a test one.
auto old_recorder = std::move(document->ukm_recorder_);
document->ukm_recorder_ = std::make_unique<ukm::TestUkmRecorder>();
auto* recorder = static_cast<ukm::TestUkmRecorder*>(document->UkmRecorder());
EXPECT_EQ(recorder->entries_count(), 0u);
document->MarkHasFindInPageRequest();
EXPECT_EQ(recorder->entries_count(), 1u);
document->MarkHasFindInPageRequest();
EXPECT_EQ(recorder->entries_count(), 1u);
auto entries = recorder->GetEntriesByName("Blink.FindInPage");
EXPECT_EQ(entries.size(), 1u);
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(entries[0], "DidSearch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[0], "DidSearch"), 1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(
entries[0], "DidHaveRenderSubtreeMatch"));
document->MarkHasFindInPageContentVisibilityActiveMatch();
EXPECT_EQ(recorder->entries_count(), 2u);
document->MarkHasFindInPageContentVisibilityActiveMatch();
EXPECT_EQ(recorder->entries_count(), 2u);
entries = recorder->GetEntriesByName("Blink.FindInPage");
EXPECT_EQ(entries.size(), 2u);
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(entries[0], "DidSearch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[0], "DidSearch"), 1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(
entries[0], "DidHaveRenderSubtreeMatch"));
EXPECT_TRUE(ukm::TestUkmRecorder::EntryHasMetric(
entries[1], "DidHaveRenderSubtreeMatch"));
EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(entries[1],
"DidHaveRenderSubtreeMatch"),
1);
EXPECT_FALSE(ukm::TestUkmRecorder::EntryHasMetric(entries[1], "DidSearch"));
// Restore the old recorder, since some ukm metrics are recorded at shutdown.
document->ukm_recorder_ = std::move(old_recorder);
}
TEST_F(DocumentTest, AtPageMarginWithDeviceScaleFactor) {
GetDocument().GetFrame()->SetPageZoomFactor(2);
SetBodyInnerHTML("<style>@page { margin: 50px; size: 400px 10in; }</style>");
constexpr FloatSize initial_page_size(800, 600);
GetDocument().GetFrame()->StartPrinting(initial_page_size, initial_page_size);
GetDocument().View()->UpdateLifecyclePhasesForPrinting();
WebPrintPageDescription description;
GetDocument().GetPageDescription(0, &description);
EXPECT_EQ(50, description.margin_top);
EXPECT_EQ(50, description.margin_right);
EXPECT_EQ(50, description.margin_bottom);
EXPECT_EQ(50, description.margin_left);
EXPECT_EQ(WebDoubleSize(400, 960), description.size);
}
TEST(Document, HandlesDisconnectDuringHasTrustToken) {
// Check that a Mojo handle disconnecting during hasTrustToken operation
// execution results in the promise getting rejected with the proper
// exception.
V8TestingScope scope(KURL("https://trusttoken.example"));
Document& document = scope.GetDocument();
auto promise =
document.hasTrustToken(scope.GetScriptState(), "https://issuer.example",
scope.GetExceptionState());
DocumentTest::SimulateHasTrustTokensAnswererConnectionError(&document);
ASSERT_TRUE(promise.IsAssociatedWith(scope.GetScriptState()));
ScriptPromiseTester promise_tester(scope.GetScriptState(), promise);
promise_tester.WaitUntilSettled();
EXPECT_TRUE(promise_tester.IsRejected());
EXPECT_TRUE(IsDOMException(scope.GetScriptState(), promise_tester.Value(),
DOMExceptionCode::kOperationError));
}
TEST(Document, RejectsHasTrustTokenCallFromNonHttpNonHttpsDocument) {
// Check that hasTrustToken getting called from a secure, but
// non-http/non-https, document results in an exception being thrown.
V8TestingScope scope(KURL("file:///trusttoken.txt"));
Document& document = scope.GetDocument();
ScriptState* script_state = scope.GetScriptState();
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext, "Document",
"hasTrustToken");
auto promise = document.hasTrustToken(script_state, "https://issuer.example",
exception_state);
ScriptPromiseTester promise_tester(script_state, promise);
promise_tester.WaitUntilSettled();
EXPECT_TRUE(promise_tester.IsRejected());
EXPECT_TRUE(IsDOMException(script_state, promise_tester.Value(),
DOMExceptionCode::kNotAllowedError));
}
namespace {
class MockHasTrustTokensAnswerer
: public network::mojom::blink::HasTrustTokensAnswerer {
public:
enum Outcome { kError, kTrue, kFalse };
explicit MockHasTrustTokensAnswerer(Outcome outcome) : outcome_(outcome) {}
void HasTrustTokens(
const ::scoped_refptr<const ::blink::SecurityOrigin>& issuer,
HasTrustTokensCallback callback) override {
auto result = network::mojom::blink::HasTrustTokensResult::New();
result->status = network::mojom::blink::TrustTokenOperationStatus::kOk;
switch (outcome_) {
case kTrue: {
result->has_trust_tokens = true;
std::move(callback).Run(std::move(result));
return;
}
case kFalse: {
result->has_trust_tokens = false;
std::move(callback).Run(std::move(result));
return;
}
case kError: {
result->status =
network::mojom::blink::TrustTokenOperationStatus::kUnknownError;
std::move(callback).Run(std::move(result));
}
}
}
void Bind(mojo::ScopedMessagePipeHandle handle) {
receiver_.Bind(
mojo::PendingReceiver<network::mojom::blink::HasTrustTokensAnswerer>(
std::move(handle)));
}
private:
Outcome outcome_;
mojo::Receiver<network::mojom::blink::HasTrustTokensAnswerer> receiver_{this};
};
} // namespace
TEST(Document, HasTrustTokenSuccess) {
V8TestingScope scope(KURL("https://secure.example"));
MockHasTrustTokensAnswerer answerer(MockHasTrustTokensAnswerer::kTrue);
Document& document = scope.GetDocument();
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_,
WTF::BindRepeating(&MockHasTrustTokensAnswerer::Bind,
WTF::Unretained(&answerer)));
ScriptState* script_state = scope.GetScriptState();
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext, "Document",
"hasTrustToken");
auto promise = document.hasTrustToken(script_state, "https://issuer.example",
exception_state);
ScriptPromiseTester promise_tester(script_state, promise);
promise_tester.WaitUntilSettled();
EXPECT_TRUE(promise_tester.IsFulfilled());
EXPECT_TRUE(promise_tester.Value().V8Value()->IsTrue());
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_, {});
}
TEST(Document, HasTrustTokenSuccessWithFalseValue) {
V8TestingScope scope(KURL("https://secure.example"));
MockHasTrustTokensAnswerer answerer(MockHasTrustTokensAnswerer::kFalse);
Document& document = scope.GetDocument();
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_,
WTF::BindRepeating(&MockHasTrustTokensAnswerer::Bind,
WTF::Unretained(&answerer)));
ScriptState* script_state = scope.GetScriptState();
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext, "Document",
"hasTrustToken");
auto promise = document.hasTrustToken(script_state, "https://issuer.example",
exception_state);
ScriptPromiseTester promise_tester(script_state, promise);
promise_tester.WaitUntilSettled();
EXPECT_TRUE(promise_tester.IsFulfilled());
EXPECT_TRUE(promise_tester.Value().V8Value()->IsFalse());
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_, {});
}
TEST(Document, HasTrustTokenOperationError) {
V8TestingScope scope(KURL("https://secure.example"));
MockHasTrustTokensAnswerer answerer(MockHasTrustTokensAnswerer::kError);
Document& document = scope.GetDocument();
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_,
WTF::BindRepeating(&MockHasTrustTokensAnswerer::Bind,
WTF::Unretained(&answerer)));
ScriptState* script_state = scope.GetScriptState();
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kExecutionContext, "Document",
"hasTrustToken");
auto promise = document.hasTrustToken(script_state, "https://issuer.example",
exception_state);
ScriptPromiseTester promise_tester(script_state, promise);
promise_tester.WaitUntilSettled();
EXPECT_TRUE(promise_tester.IsRejected());
EXPECT_TRUE(IsDOMException(script_state, promise_tester.Value(),
DOMExceptionCode::kOperationError));
document.GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
network::mojom::blink::HasTrustTokensAnswerer::Name_, {});
}
/**
* Tests for viewport-fit propagation.
*/
class ViewportFitDocumentTest : public DocumentTest,
private ScopedDisplayCutoutAPIForTest {
public:
ViewportFitDocumentTest() : ScopedDisplayCutoutAPIForTest(true) {}
void SetUp() override {
DocumentTest::SetUp();
GetDocument().GetSettings()->SetViewportMetaEnabled(true);
}
mojom::ViewportFit GetViewportFit() const {
return GetDocument().GetViewportData().GetCurrentViewportFitForTests();
}
};
// Test meta viewport present but no viewport-fit.
TEST_F(ViewportFitDocumentTest, MetaViewportButNoFit) {
SetHtmlInnerHTML("<meta name='viewport' content='initial-scale=1'>");
EXPECT_EQ(mojom::ViewportFit::kAuto, GetViewportFit());
}
// Test overriding the viewport fit using SetExpandIntoDisplayCutout.
TEST_F(ViewportFitDocumentTest, ForceExpandIntoCutout) {
SetHtmlInnerHTML("<meta name='viewport' content='viewport-fit=contain'>");
EXPECT_EQ(mojom::ViewportFit::kContain, GetViewportFit());
// Now override the viewport fit value and expect it to be kCover.
GetDocument().GetViewportData().SetExpandIntoDisplayCutout(true);
EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, GetViewportFit());
// Test that even if we change the value we ignore it.
SetHtmlInnerHTML("<meta name='viewport' content='viewport-fit=auto'>");
EXPECT_EQ(mojom::ViewportFit::kCoverForcedByUserAgent, GetViewportFit());
// Now remove the override and check that it went back to the previous value.
GetDocument().GetViewportData().SetExpandIntoDisplayCutout(false);
EXPECT_EQ(mojom::ViewportFit::kAuto, GetViewportFit());
}
// This is a test case for testing a combination of viewport-fit meta value,
// viewport CSS value and the expected outcome.
using ViewportTestCase = std::tuple<const char*, mojom::ViewportFit>;
class ParameterizedViewportFitDocumentTest
: public ViewportFitDocumentTest,
public testing::WithParamInterface<ViewportTestCase> {
protected:
void LoadTestHTML() {
const char* kMetaValue = std::get<0>(GetParam());
StringBuilder html;
if (kMetaValue) {
html.Append("<meta name='viewport' content='viewport-fit=");
html.Append(kMetaValue);
html.Append("'>");
}
GetDocument().documentElement()->setInnerHTML(html.ToString());
UpdateAllLifecyclePhasesForTest();
}
};
TEST_P(ParameterizedViewportFitDocumentTest, EffectiveViewportFit) {
LoadTestHTML();
EXPECT_EQ(std::get<1>(GetParam()), GetViewportFit());
}
INSTANTIATE_TEST_SUITE_P(
All,
ParameterizedViewportFitDocumentTest,
testing::Values(
// Test the default case.
ViewportTestCase(nullptr, mojom::ViewportFit::kAuto),
// Test the different values set through the meta tag.
ViewportTestCase("auto", mojom::ViewportFit::kAuto),
ViewportTestCase("contain", mojom::ViewportFit::kContain),
ViewportTestCase("cover", mojom::ViewportFit::kCover),
ViewportTestCase("invalid", mojom::ViewportFit::kAuto)));
class BatterySavingsChromeClient : public EmptyChromeClient {
public:
MOCK_METHOD2(BatterySavingsChanged, void(LocalFrame&, BatterySavingsFlags));
};
class DocumentBatterySavingsTest : public PageTestBase,
private ScopedBatterySavingsMetaForTest {
protected:
DocumentBatterySavingsTest() : ScopedBatterySavingsMetaForTest(true) {}
void SetUp() override {
chrome_client_ = MakeGarbageCollected<BatterySavingsChromeClient>();
Page::PageClients page_clients;
FillWithEmptyClients(page_clients);
page_clients.chrome_client = chrome_client_;
SetupPageWithClients(&page_clients);
}
Persistent<BatterySavingsChromeClient> chrome_client_;
};
TEST_F(DocumentBatterySavingsTest, ChromeClientCalls) {
testing::InSequence s;
// The client is called twice, once for each meta element, but is called with
// the same parameter both times because the first meta in DOM order takes
// precedence.
EXPECT_CALL(*chrome_client_,
BatterySavingsChanged(testing::_, kAllowReducedFrameRate))
.Times(2);
EXPECT_FALSE(
GetDocument().Loader()->GetUseCounterHelper().HasRecordedMeasurement(
WebFeature::kBatterySavingsMeta));
SetHtmlInnerHTML(R"HTML(
<meta id="first" name="battery-savings" content="allow-reduced-framerate">
<meta id="second" name="battery-savings" content="allow-reduced-script-speed">
)HTML");
EXPECT_TRUE(
GetDocument().Loader()->GetUseCounterHelper().HasRecordedMeasurement(
WebFeature::kBatterySavingsMeta));
// Remove the first meta causing the second to apply.
EXPECT_CALL(*chrome_client_,
BatterySavingsChanged(testing::_, kAllowReducedScriptSpeed))
.Times(1);
GetDocument().getElementById("first")->remove();
// Change the content attribute to an unsupported value.
EXPECT_CALL(*chrome_client_, BatterySavingsChanged(testing::_, 0)).Times(1);
Element* second = GetDocument().getElementById("second");
second->setAttribute(html_names::kContentAttr, "allow-blah");
// Change the content attribute to both supported values.
EXPECT_CALL(*chrome_client_,
BatterySavingsChanged(testing::_, kAllowReducedFrameRate |
kAllowReducedScriptSpeed))
.Times(1);
second->setAttribute(html_names::kContentAttr,
"allow-reduced-framerate allow-reduced-script-speed");
// Change the meta name to "viewport".
EXPECT_CALL(*chrome_client_, BatterySavingsChanged(testing::_, 0)).Times(1);
second->setAttribute(html_names::kNameAttr, "viewport");
}
namespace {
class MockReportingContext final : public ReportingContext {
public:
explicit MockReportingContext(ExecutionContext& ec) : ReportingContext(ec) {}
void QueueReport(Report* report, const Vector<String>& endpoint) override {
report_count++;
}
unsigned report_count = 0;
};
} // namespace
TEST_F(DocumentSimTest, DuplicatedDocumentPolicyViolationsAreIgnored) {
blink::ScopedDocumentPolicyForTest scoped_document_policy(true);
SimRequest::Params params;
params.response_http_headers = {
{"Document-Policy", "lossless-images-max-bpp=1.0"}};
SimRequest main_resource("https://example.com", "text/html", params);
LoadURL("https://example.com");
main_resource.Finish();
ExecutionContext* execution_context = GetDocument().GetExecutionContext();
MockReportingContext* mock_reporting_context =
MakeGarbageCollected<MockReportingContext>(*execution_context);
Supplement<ExecutionContext>::ProvideTo(*execution_context,
mock_reporting_context);
EXPECT_FALSE(execution_context->IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
PolicyValue::CreateDecDouble(1.1), ReportOptions::kReportOnFailure));
EXPECT_EQ(mock_reporting_context->report_count, 1u);
EXPECT_FALSE(execution_context->IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
PolicyValue::CreateDecDouble(1.1), ReportOptions::kReportOnFailure));
EXPECT_EQ(mock_reporting_context->report_count, 1u);
}
} // namespace blink