| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/display_lock/display_lock_context.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/core/display_lock/display_lock_options.h" |
| #include "third_party/blink/renderer/core/editing/finder/text_finder.h" |
| #include "third_party/blink/renderer/core/frame/find_in_page.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| |
| namespace blink { |
| namespace { |
| class DisplayLockTestFindInPageClient : public mojom::blink::FindInPageClient { |
| public: |
| DisplayLockTestFindInPageClient() |
| : find_results_are_ready_(false), count_(-1), binding_(this) {} |
| |
| ~DisplayLockTestFindInPageClient() override = default; |
| |
| void SetFrame(WebLocalFrameImpl* frame) { |
| mojom::blink::FindInPageClientPtr client; |
| binding_.Bind(MakeRequest(&client)); |
| frame->GetFindInPage()->SetClient(std::move(client)); |
| } |
| |
| void SetNumberOfMatches( |
| int request_id, |
| unsigned int current_number_of_matches, |
| mojom::blink::FindMatchUpdateType final_update) final { |
| count_ = current_number_of_matches; |
| find_results_are_ready_ = |
| (final_update == mojom::blink::FindMatchUpdateType::kFinalUpdate); |
| } |
| |
| void SetActiveMatch(int request_id, |
| const WebRect& active_match_rect, |
| int active_match_ordinal, |
| mojom::blink::FindMatchUpdateType final_update) final { |
| find_results_are_ready_ = |
| (final_update == mojom::blink::FindMatchUpdateType::kFinalUpdate); |
| } |
| |
| bool FindResultsAreReady() const { return find_results_are_ready_; } |
| int Count() const { return count_; } |
| void Reset() { |
| find_results_are_ready_ = false; |
| count_ = -1; |
| } |
| |
| private: |
| bool find_results_are_ready_; |
| int count_; |
| mojo::Binding<mojom::blink::FindInPageClient> binding_; |
| }; |
| |
| } // namespace |
| |
| class DisplayLockContextTest : public testing::Test { |
| public: |
| void SetUp() override { |
| features_backup_.emplace(); |
| RuntimeEnabledFeatures::SetDisplayLockingEnabled(true); |
| web_view_helper_.Initialize(); |
| } |
| |
| void TearDown() override { |
| if (features_backup_) { |
| features_backup_->Restore(); |
| features_backup_.reset(); |
| } |
| web_view_helper_.Reset(); |
| } |
| |
| Document& GetDocument() { |
| return *static_cast<Document*>( |
| web_view_helper_.LocalMainFrame()->GetDocument()); |
| } |
| TextFinder& GetTextFinder() { |
| return web_view_helper_.LocalMainFrame()->EnsureTextFinder(); |
| } |
| FindInPage* GetFindInPage() { |
| return web_view_helper_.LocalMainFrame()->GetFindInPage(); |
| } |
| WebLocalFrameImpl* LocalMainFrame() { |
| return web_view_helper_.LocalMainFrame(); |
| } |
| |
| void UpdateAllLifecyclePhasesForTest() { |
| GetDocument().View()->UpdateAllLifecyclePhases( |
| DocumentLifecycle::LifecycleUpdateReason::kTest); |
| } |
| |
| void SetHtmlInnerHTML(const char* content) { |
| GetDocument().documentElement()->SetInnerHTMLFromString( |
| String::FromUTF8(content)); |
| UpdateAllLifecyclePhasesForTest(); |
| } |
| |
| void ResizeAndFocus() { |
| web_view_helper_.Resize(WebSize(640, 480)); |
| web_view_helper_.GetWebView()->MainFrameWidget()->SetFocus(true); |
| test::RunPendingTasks(); |
| } |
| |
| private: |
| base::Optional<RuntimeEnabledFeatures::Backup> features_backup_; |
| frame_test_helpers::WebViewHelper web_view_helper_; |
| }; |
| |
| TEST_F(DisplayLockContextTest, LockedElementIsNotSearchableViaTextFinder) { |
| SetHtmlInnerHTML(R"HTML( |
| <style> |
| #container { |
| width: 100px; |
| height: 100px; |
| contain: content; |
| } |
| </style> |
| <body><div id="container">testing</div></body> |
| )HTML"); |
| |
| int identifier = 0; |
| WebString search_text(String("testing")); |
| auto& text_finder = GetTextFinder(); |
| auto find_options = mojom::blink::FindOptions::New(); |
| bool wrap_within_frame = true; |
| |
| ASSERT_TRUE(text_finder.Find(identifier, search_text, *find_options, |
| wrap_within_frame)); |
| text_finder.ClearActiveFindMatch(); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| auto* element = GetDocument().getElementById("container"); |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| // We should be in pending acquire state. In this mode, we're still |
| // technically not locked. |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Sanity checks to ensure the element is locked. |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| |
| EXPECT_FALSE(element->GetDisplayLockContext()->IsActivatable()); |
| |
| EXPECT_FALSE(text_finder.Find(identifier, search_text, *find_options, |
| wrap_within_frame)); |
| |
| // Now commit the lock and ensure we can find the text. |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldPaint()); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| EXPECT_TRUE(text_finder.Find(identifier, search_text, *find_options, |
| wrap_within_frame)); |
| } |
| |
| TEST_F(DisplayLockContextTest, LockedElementIsNotSearchableViaFindInPage) { |
| ResizeAndFocus(); |
| SetHtmlInnerHTML(R"HTML( |
| <style> |
| #container { |
| width: 100px; |
| height: 100px; |
| contain: content; |
| } |
| </style> |
| <body><div id="container">testing</div></body> |
| )HTML"); |
| |
| WebString search_text(String("testing")); |
| auto* find_in_page = GetFindInPage(); |
| ASSERT_TRUE(find_in_page); |
| |
| DisplayLockTestFindInPageClient client; |
| client.SetFrame(LocalMainFrame()); |
| |
| auto find_options = mojom::blink::FindOptions::New(); |
| find_options->run_synchronously_for_testing = true; |
| find_options->find_next = false; |
| find_options->forward = true; |
| |
| int current_id = 123; |
| find_in_page->Find(current_id++, "testing", find_options->Clone()); |
| EXPECT_FALSE(client.FindResultsAreReady()); |
| test::RunPendingTasks(); |
| EXPECT_TRUE(client.FindResultsAreReady()); |
| EXPECT_EQ(1, client.Count()); |
| client.Reset(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| auto* element = GetDocument().getElementById("container"); |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| // We should be in pending acquire state, which means we would allow things |
| // like style and layout but disallow paint. This is still considered an |
| // unlocked state. |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Sanity checks to ensure the element is locked. |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| |
| EXPECT_FALSE(element->GetDisplayLockContext()->IsActivatable()); |
| |
| find_in_page->Find(current_id++, "testing", find_options->Clone()); |
| EXPECT_FALSE(client.FindResultsAreReady()); |
| test::RunPendingTasks(); |
| EXPECT_TRUE(client.FindResultsAreReady()); |
| EXPECT_EQ(0, client.Count()); |
| client.Reset(); |
| |
| // Now commit the lock and ensure we can find the text. |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldPaint()); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| find_in_page->Find(current_id++, "testing", find_options->Clone()); |
| EXPECT_FALSE(client.FindResultsAreReady()); |
| test::RunPendingTasks(); |
| EXPECT_TRUE(client.FindResultsAreReady()); |
| EXPECT_EQ(1, client.Count()); |
| client.Reset(); |
| } |
| |
| TEST_F(DisplayLockContextTest, LockedElementAndDescendantsAreNotFocusable) { |
| ResizeAndFocus(); |
| SetHtmlInnerHTML(R"HTML( |
| <style> |
| #container { |
| width: 100px; |
| height: 100px; |
| contain: content; |
| } |
| </style> |
| <body> |
| <div id="container"> |
| <input id="textfield", type="text"> |
| </div> |
| </body> |
| )HTML"); |
| |
| // We start off as being focusable. |
| ASSERT_TRUE(GetDocument().getElementById("textfield")->IsKeyboardFocusable()); |
| ASSERT_TRUE(GetDocument().getElementById("textfield")->IsMouseFocusable()); |
| ASSERT_TRUE(GetDocument().getElementById("textfield")->IsFocusable()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| auto* element = GetDocument().getElementById("container"); |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| // We should be in pending acquire state, which means we would allow things |
| // like style and layout but disallow paint. This is sitll considered an |
| // unlocked state. |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Sanity checks to ensure the element is locked. |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_FALSE(element->GetDisplayLockContext()->ShouldPaint()); |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| |
| // The input should not be focusable now. |
| EXPECT_FALSE( |
| GetDocument().getElementById("textfield")->IsKeyboardFocusable()); |
| EXPECT_FALSE(GetDocument().getElementById("textfield")->IsMouseFocusable()); |
| EXPECT_FALSE(GetDocument().getElementById("textfield")->IsFocusable()); |
| |
| // Calling explicit focus() should also not focus the element. |
| GetDocument().getElementById("textfield")->focus(); |
| EXPECT_FALSE(GetDocument().FocusedElement()); |
| |
| // Now commit the lock and ensure we can focus the input |
| { |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| ScriptState::Scope scope(script_state); |
| element->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldStyle()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldLayout()); |
| EXPECT_TRUE(element->GetDisplayLockContext()->ShouldPaint()); |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| EXPECT_TRUE(GetDocument().getElementById("textfield")->IsKeyboardFocusable()); |
| EXPECT_TRUE(GetDocument().getElementById("textfield")->IsMouseFocusable()); |
| EXPECT_TRUE(GetDocument().getElementById("textfield")->IsFocusable()); |
| |
| // Calling explicit focus() should focus the element |
| GetDocument().getElementById("textfield")->focus(); |
| EXPECT_EQ(GetDocument().FocusedElement(), |
| GetDocument().getElementById("textfield")); |
| } |
| |
| TEST_F(DisplayLockContextTest, LockedCountsWithMultipleLocks) { |
| ResizeAndFocus(); |
| SetHtmlInnerHTML(R"HTML( |
| <style> |
| .container { |
| width: 100px; |
| height: 100px; |
| contain: content; |
| } |
| </style> |
| <body> |
| <div id="one" class="container"> |
| <div id="two" class="container"></div> |
| </div> |
| <div id="three" class="container"></div> |
| </body> |
| )HTML"); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| auto* one = GetDocument().getElementById("one"); |
| auto* two = GetDocument().getElementById("two"); |
| auto* three = GetDocument().getElementById("three"); |
| |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| { |
| ScriptState::Scope scope(script_state); |
| one->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| |
| { |
| ScriptState::Scope scope(script_state); |
| two->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 2); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 2); |
| |
| { |
| ScriptState::Scope scope(script_state); |
| three->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 3); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 3); |
| |
| // Now commit the inner lock. |
| { |
| ScriptState::Scope scope(script_state); |
| two->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 2); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 2); |
| |
| // Commit the outer lock. |
| { |
| ScriptState::Scope scope(script_state); |
| one->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Both inner and outer locks should have committed. |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| |
| // Commit the sibling lock. |
| { |
| ScriptState::Scope scope(script_state); |
| three->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| // Both inner and outer locks should have committed. |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| } |
| |
| TEST_F(DisplayLockContextTest, ActivatableNotCountedAsBlocking) { |
| ResizeAndFocus(); |
| SetHtmlInnerHTML(R"HTML( |
| <style> |
| .container { |
| width: 100px; |
| height: 100px; |
| contain: content; |
| } |
| </style> |
| <body> |
| <div id="activatable" class="container"></div> |
| <div id="nonActivatable" class="container"></div> |
| </body> |
| )HTML"); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 0); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| |
| auto* activatable = GetDocument().getElementById("activatable"); |
| auto* non_activatable = GetDocument().getElementById("nonActivatable"); |
| |
| DisplayLockOptions activatable_options; |
| activatable_options.setActivatable(true); |
| |
| auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| { |
| ScriptState::Scope scope(script_state); |
| activatable->getDisplayLockForBindings()->acquire(script_state, |
| &activatable_options); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| EXPECT_TRUE(activatable->GetDisplayLockContext()->IsActivatable()); |
| |
| { |
| ScriptState::Scope scope(script_state); |
| non_activatable->getDisplayLockForBindings()->acquire(script_state, |
| nullptr); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 2); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| EXPECT_FALSE(non_activatable->GetDisplayLockContext()->IsActivatable()); |
| |
| // Now commit the lock for |non_ctivatable|. |
| { |
| ScriptState::Scope scope(script_state); |
| non_activatable->getDisplayLockForBindings()->commit(script_state); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| EXPECT_TRUE(activatable->GetDisplayLockContext()->IsActivatable()); |
| EXPECT_TRUE(activatable->GetDisplayLockContext()->IsActivatable()); |
| |
| // Re-acquire the lock for |activatable|, but without the activatable flag. |
| { |
| ScriptState::Scope scope(script_state); |
| activatable->getDisplayLockForBindings()->acquire(script_state, nullptr); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 1); |
| EXPECT_FALSE(activatable->GetDisplayLockContext()->IsActivatable()); |
| |
| // Re-acquire the lock for |activatable| again with the activatable flag. |
| { |
| ScriptState::Scope scope(script_state); |
| activatable->getDisplayLockForBindings()->acquire(script_state, |
| &activatable_options); |
| } |
| |
| UpdateAllLifecyclePhasesForTest(); |
| |
| EXPECT_EQ(GetDocument().LockedDisplayLockCount(), 1); |
| EXPECT_EQ(GetDocument().ActivationBlockingDisplayLockCount(), 0); |
| EXPECT_TRUE(activatable->GetDisplayLockContext()->IsActivatable()); |
| } |
| } // namespace blink |