| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/javascript_dialogs/app_modal_dialog_queue.h" | |
| #include <memory> | |
| #include <utility> | |
| #include "base/functional/bind.h" | |
| #include "base/functional/callback_helpers.h" | |
| #include "components/javascript_dialogs/app_modal_dialog_controller.h" | |
| #include "components/javascript_dialogs/app_modal_dialog_manager.h" | |
| #include "components/javascript_dialogs/app_modal_dialog_view.h" | |
| #include "content/public/browser/javascript_dialog_manager.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| namespace javascript_dialogs { | |
| namespace { | |
| // A view, a la what is returned by AppModalDialogManager::AppModalViewFactory, | |
| // to own an AppModalDialogController. | |
| class FakeView : public AppModalDialogView { | |
| public: | |
| explicit FakeView(std::unique_ptr<AppModalDialogController> controller) | |
| : controller_(std::move(controller)) {} | |
| void ShowAppModalDialog() override {} | |
| void ActivateAppModalDialog() override {} | |
| // Real views notify their controller that the dialog was dismissed when they | |
| // are closed. Note that, unlike the real views, this one keeps `controller_` | |
| // alive so that tests control its lifetime. | |
| void CloseAppModalDialog() override { | |
| if (controller_) { | |
| controller_->OnCancel(/*suppress_js_messages=*/false); | |
| } | |
| } | |
| void AcceptAppModalDialog() override {} | |
| void CancelAppModalDialog() override {} | |
| bool IsShowing() const override { return true; } | |
| private: | |
| std::unique_ptr<AppModalDialogController> controller_; | |
| }; | |
| class TestDialogControllerBase : public AppModalDialogController { | |
| public: | |
| explicit TestDialogControllerBase( | |
| bool* was_destroyed, | |
| content::JavaScriptDialogManager::DialogClosedCallback callback = | |
| base::DoNothing()) | |
| : AppModalDialogController(nullptr, | |
| &extra_data_, | |
| u"test", | |
| content::JAVASCRIPT_DIALOG_TYPE_ALERT, | |
| u"msg", | |
| u"", | |
| false, | |
| false, | |
| false, | |
| std::move(callback)), | |
| was_destroyed_(was_destroyed) {} | |
| ~TestDialogControllerBase() override { | |
| if (was_destroyed_) { | |
| *was_destroyed_ = true; | |
| } | |
| } | |
| private: | |
| ExtraDataMap extra_data_; | |
| raw_ptr<bool> was_destroyed_ = nullptr; | |
| }; | |
| // A dialog controller whose ShowModalDialog() sets `view_` to the fake owning | |
| // view, allowing CompleteDialog() to trigger ShowNextDialog() on destruction. | |
| class TestDialogController : public TestDialogControllerBase { | |
| public: | |
| explicit TestDialogController(bool* was_shown, | |
| bool* was_destroyed, | |
| std::unique_ptr<FakeView>* owning_view) | |
| : TestDialogControllerBase(was_destroyed), | |
| was_shown_(was_shown), | |
| owning_view_(owning_view) {} | |
| void ShowModalDialog( | |
| std::unique_ptr<AppModalDialogController> controller) override { | |
| if (was_shown_) { | |
| *was_shown_ = true; | |
| } | |
| *owning_view_ = std::make_unique<FakeView>(std::move(controller)); | |
| view_ = owning_view_->get(); | |
| } | |
| void TriggerComplete() { CompleteDialog(); } | |
| private: | |
| raw_ptr<bool> was_shown_ = nullptr; | |
| raw_ptr<std::unique_ptr<FakeView>> owning_view_ = nullptr; | |
| }; | |
| // A dialog controller that fails the test if shown. Used to verify that | |
| // CancelAllDialogs() prevents queued dialogs from being displayed. | |
| class FailIfShownDialogController : public TestDialogControllerBase { | |
| public: | |
| using TestDialogControllerBase::TestDialogControllerBase; | |
| void ShowModalDialog( | |
| std::unique_ptr<AppModalDialogController> controller) override { | |
| ADD_FAILURE() << "Dialog must not be shown after CancelAllDialogs()"; | |
| } | |
| }; | |
| class AppModalDialogQueueTest : public testing::Test { | |
| protected: | |
| void TearDown() override { | |
| AppModalDialogQueue::GetInstance()->ResetForTesting(); | |
| } | |
| }; | |
| // Verifies that CancelAllDialogs() prevents queued dialogs from being shown | |
| // when the active dialog completes during shutdown. | |
| TEST_F(AppModalDialogQueueTest, CancelAllDialogsPreventsShowDuringShutdown) { | |
| auto* queue = AppModalDialogQueue::GetInstance(); | |
| bool dialog1_shown = false; | |
| std::unique_ptr<FakeView> dialog1_view; | |
| bool dialog2_destroyed = false; | |
| auto dialog1 = std::make_unique<TestDialogController>( | |
| &dialog1_shown, /*was_destroyed=*/nullptr, &dialog1_view); | |
| auto* dialog1_ptr = dialog1.get(); | |
| queue->AddDialog(std::move(dialog1)); | |
| ASSERT_TRUE(dialog1_shown); | |
| ASSERT_TRUE(queue->HasActiveDialog()); | |
| auto dialog2 = | |
| std::make_unique<FailIfShownDialogController>(&dialog2_destroyed); | |
| queue->AddDialog(std::move(dialog2)); | |
| ASSERT_FALSE(dialog2_destroyed); | |
| // Without this call, dialog1->TriggerComplete() would show dialog2. | |
| queue->CancelAllDialogs(); | |
| EXPECT_TRUE(dialog2_destroyed); | |
| dialog1_ptr->TriggerComplete(); | |
| EXPECT_FALSE(queue->HasActiveDialog()); | |
| dialog1_ptr->Invalidate(); | |
| } | |
| TEST_F(AppModalDialogQueueTest, AddDialogDeletesAfterShutdown) { | |
| auto* queue = AppModalDialogQueue::GetInstance(); | |
| queue->CancelAllDialogs(); | |
| bool destroyed = false; | |
| auto dialog = std::make_unique<FailIfShownDialogController>(&destroyed); | |
| queue->AddDialog(std::move(dialog)); | |
| EXPECT_TRUE(destroyed); | |
| EXPECT_FALSE(queue->HasActiveDialog()); | |
| } | |
| TEST_F(AppModalDialogQueueTest, ShowNextDialogDrainsQueueAfterShutdown) { | |
| auto* queue = AppModalDialogQueue::GetInstance(); | |
| bool dialog1_shown = false; | |
| std::unique_ptr<FakeView> dialog1_view; | |
| auto dialog1 = std::make_unique<TestDialogController>( | |
| &dialog1_shown, /*was_destroyed=*/nullptr, &dialog1_view); | |
| auto* dialog1_ptr = dialog1.get(); | |
| queue->AddDialog(std::move(dialog1)); | |
| ASSERT_TRUE(dialog1_shown); | |
| bool destroyed2 = false; | |
| bool destroyed3 = false; | |
| queue->AddDialog(std::make_unique<FailIfShownDialogController>(&destroyed2)); | |
| queue->AddDialog(std::make_unique<FailIfShownDialogController>(&destroyed3)); | |
| queue->CancelAllDialogs(); | |
| EXPECT_TRUE(destroyed2); | |
| EXPECT_TRUE(destroyed3); | |
| dialog1_ptr->TriggerComplete(); | |
| EXPECT_FALSE(queue->HasActiveDialog()); | |
| dialog1_ptr->Invalidate(); | |
| } | |
| // Returns a DialogClosedCallback that re-entrantly cancels all dialogs for the | |
| // test WebContents (nullptr), and records that it ran. | |
| content::JavaScriptDialogManager::DialogClosedCallback | |
| MakeReentrantCancelCallback(bool* callback_ran) { | |
| return base::BindOnce( | |
| [](bool* callback_ran, bool /*success*/, | |
| const std::u16string& /*user_input*/) { | |
| *callback_ran = true; | |
| // This invalidates the active dialog, whose view then dispatches | |
| // OnCancel -> CompleteDialog -> ShowNextDialog -> GetNextDialog, which | |
| // drains and destroys the invalidated dialogs still in the queue - one | |
| // of which is the dialog whose Invalidate() is running this callback. | |
| AppModalDialogManager::GetInstance()->CancelDialogs( | |
| /*web_contents=*/nullptr, /*reset_state=*/false); | |
| }, | |
| callback_ran); | |
| } | |
| // Regression test for https://crbug.com/560439699: a queued dialog's closed | |
| // callback re-entrantly cancels dialogs, which destroys the queued dialogs | |
| // while AppModalDialogController::Invalidate() and the CancelDialogs() loop | |
| // iterating the queue are still on the stack. | |
| TEST_F(AppModalDialogQueueTest, ReentrantCancelDialogsDuringInvalidate) { | |
| auto* queue = AppModalDialogQueue::GetInstance(); | |
| auto* manager = AppModalDialogManager::GetInstance(); | |
| bool dialog1_shown = false; | |
| bool dialog1_destroyed = false; | |
| bool dialog2_destroyed = false; | |
| bool dialog3_destroyed = false; | |
| bool callback_ran = false; | |
| std::unique_ptr<FakeView> dialog1_view; | |
| queue->AddDialog(std::make_unique<TestDialogController>( | |
| &dialog1_shown, &dialog1_destroyed, &dialog1_view)); | |
| ASSERT_TRUE(dialog1_shown); | |
| ASSERT_TRUE(queue->HasActiveDialog()); | |
| queue->AddDialog(std::make_unique<TestDialogControllerBase>( | |
| &dialog2_destroyed, MakeReentrantCancelCallback(&callback_ran))); | |
| queue->AddDialog( | |
| std::make_unique<TestDialogControllerBase>(&dialog3_destroyed)); | |
| manager->CancelDialogs(/*web_contents=*/nullptr, /*reset_state=*/false); | |
| EXPECT_TRUE(callback_ran); | |
| EXPECT_TRUE(dialog2_destroyed); | |
| EXPECT_TRUE(dialog3_destroyed); | |
| EXPECT_FALSE(queue->HasActiveDialog()); | |
| // The active dialog is owned by its view, so it outlives the cancellation. | |
| EXPECT_FALSE(dialog1_destroyed); | |
| } | |
| // Regression test for https://crbug.com/560439699: the same re-entrancy, but | |
| // entered through AppModalDialogQueue::InvalidateAndClearQueuedDialogs(). The | |
| // re-entrant ShowNextDialog() must not drain (and destroy) the dialogs that the | |
| // outer invalidation loop is still walking over. | |
| TEST_F(AppModalDialogQueueTest, ReentrantCancelDuringCancelAllDialogs) { | |
| auto* queue = AppModalDialogQueue::GetInstance(); | |
| bool dialog1_shown = false; | |
| bool dialog1_destroyed = false; | |
| bool dialog2_destroyed = false; | |
| bool dialog3_destroyed = false; | |
| bool callback_ran = false; | |
| std::unique_ptr<FakeView> dialog1_view; | |
| queue->AddDialog(std::make_unique<TestDialogController>( | |
| &dialog1_shown, &dialog1_destroyed, &dialog1_view)); | |
| ASSERT_TRUE(dialog1_shown); | |
| ASSERT_TRUE(queue->HasActiveDialog()); | |
| queue->AddDialog(std::make_unique<TestDialogControllerBase>( | |
| &dialog2_destroyed, MakeReentrantCancelCallback(&callback_ran))); | |
| queue->AddDialog( | |
| std::make_unique<TestDialogControllerBase>(&dialog3_destroyed)); | |
| queue->CancelAllDialogs(); | |
| EXPECT_TRUE(callback_ran); | |
| EXPECT_TRUE(dialog2_destroyed); | |
| EXPECT_TRUE(dialog3_destroyed); | |
| EXPECT_FALSE(queue->HasActiveDialog()); | |
| // The active dialog is owned by its view, so it outlives the cancellation. | |
| EXPECT_FALSE(dialog1_destroyed); | |
| } | |
| } // namespace | |
| } // namespace javascript_dialogs |