Files
brave-core/browser/ui/sidebar/sidebar_browsertest.cc
Simon Hong 9c51b5e6e2 [Sidebar V2] Update panel contents border radius (#36834)
Applied rounded corners to panel's contents view.
Introduced brave::GetPanelContentsRoundedCorners(PrefService*, bool has_header)
which derives corners from the pref and current header presence.
Route GetRoundedCorners() through it via patch, reading header_view_ state
(set before AddChildView) instead of the stale coordinator entry ID.
Call UpdateContentWrapperChildCorners() from UpdateBorder() overrides so
corners are re-applied on panel open and pref change.

Resolves no issue - use umbrella issue (brave/brave-browser#51462)

TEST=SidePanelUtilsTest.*, SidebarBrowserTest.SidebarV2ContentCornersUpdateOnStateChange
2026-06-05 08:06:19 +09:00

2667 lines
107 KiB
C++

/* Copyright (c) 2021 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <algorithm>
#include <optional>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "brave/app/brave_command_ids.h"
#include "brave/browser/ui/browser_commands.h"
#include "brave/browser/ui/sidebar/buildflags/buildflags.h"
#include "brave/browser/ui/sidebar/features.h"
#include "brave/browser/ui/sidebar/sidebar_controller.h"
#include "brave/browser/ui/sidebar/sidebar_model.h"
#include "brave/browser/ui/sidebar/sidebar_service_factory.h"
#include "brave/browser/ui/sidebar/sidebar_utils.h"
#include "brave/browser/ui/sidebar/sidebar_web_panel_controller.h"
#include "brave/browser/ui/views/frame/brave_browser_view.h"
#include "brave/browser/ui/views/frame/brave_contents_view_util.h"
#include "brave/browser/ui/views/frame/split_view/brave_contents_container_view.h"
#include "brave/browser/ui/views/frame/split_view/brave_multi_contents_view.h"
#include "brave/browser/ui/views/frame/split_view/brave_multi_contents_view_mini_toolbar.h"
#include "brave/browser/ui/views/side_panel/side_panel_resize_widget.h"
#include "brave/browser/ui/views/sidebar/sidebar_container_view.h"
#include "brave/browser/ui/views/sidebar/sidebar_control_view.h"
#include "brave/browser/ui/views/sidebar/sidebar_items_contents_view.h"
#include "brave/browser/ui/views/sidebar/sidebar_items_scroll_view.h"
#include "brave/browser/ui/views/tabs/vertical_tab_utils.h"
#include "brave/browser/ui/views/toolbar/brave_toolbar_view.h"
#include "brave/browser/ui/views/toolbar/side_panel_button.h"
#include "brave/common/pref_names.h"
#include "brave/components/ai_chat/core/common/buildflags/buildflags.h"
#include "brave/components/brave_talk/buildflags/buildflags.h"
#include "brave/components/brave_wallet/common/buildflags/buildflags.h"
#include "brave/components/constants/brave_switches.h"
#include "brave/components/playlist/core/common/buildflags/buildflags.h"
#include "brave/components/sidebar/browser/constants.h"
#include "brave/components/sidebar/browser/pref_names.h"
#include "brave/components/sidebar/browser/sidebar_item.h"
#include "brave/components/sidebar/browser/sidebar_service.h"
#include "brave/components/sidebar/common/features.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/side_panel/side_panel_entry.h"
#include "chrome/browser/ui/side_panel/side_panel_registry.h"
#include "chrome/browser/ui/side_panel/side_panel_ui.h"
#include "chrome/browser/ui/tabs/features.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/frame/multi_contents_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/side_panel/side_panel.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/search_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/display/test/test_screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/widget/widget_utils.h"
#if BUILDFLAG(ENABLE_AI_CHAT)
#include "brave/components/ai_chat/core/common/features.h"
#endif
#if BUILDFLAG(ENABLE_PLAYLIST)
#include "brave/components/playlist/core/common/features.h"
#endif
#if BUILDFLAG(ENABLE_SIDEBAR_V2)
#include "brave/browser/ui/views/side_panel/brave_side_panel_header.h"
#include "brave/browser/ui/views/side_panel/brave_side_panel_resize_area.h"
#include "brave/browser/ui/views/side_panel/side_panel_utils.h"
#endif
using ::testing::Eq;
using ::testing::Ne;
using ::testing::Optional;
namespace {
ui::MouseEvent GetDummyEvent() {
return ui::MouseEvent(ui::EventType::kMouseMoved, gfx::PointF(),
gfx::PointF(), base::TimeTicks::Now(), 0, 0);
}
} // namespace
namespace sidebar {
class SidebarBrowserTest : public InProcessBrowserTest {
public:
SidebarBrowserTest() = default;
~SidebarBrowserTest() override = default;
void PreRunTestOnMainThread() override {
InProcessBrowserTest::PreRunTestOnMainThread();
auto* service = SidebarServiceFactory::GetForProfile(browser()->profile());
// Enable sidebar explicitely because sidebar option is different based on
// channel.
service->SetSidebarShowOption(
SidebarService::ShowSidebarOption::kShowAlways);
}
SidebarModel* model() const { return controller()->model(); }
TabStripModel* tab_model() const { return browser()->tab_strip_model(); }
SidebarController* controller() const {
return browser()->GetFeatures().sidebar_controller();
}
SidePanelButton* GetSidePanelToolbarButton() const {
return static_cast<BraveToolbarView*>(
BrowserView::GetBrowserViewForBrowser(browser())->toolbar())
->side_panel_button();
}
views::View* GetVerticalTabsContainer() const {
auto* view = BrowserView::GetBrowserViewForBrowser(browser());
return static_cast<BraveBrowserView*>(view)->vertical_tab_strip_host_view_;
}
views::Widget* GetSidePanelResizeWidget() {
#if BUILDFLAG(ENABLE_SIDEBAR_V2)
NOTREACHED() << "No resize widget in v2";
#else
return GetSidePanel()->resize_widget_->GetWidget();
#endif
}
raw_ptr<SidebarItemsContentsView> GetSidebarItemsContentsView(
SidebarController* controller) const {
auto* sidebar_container_view =
static_cast<SidebarContainerView*>(controller->sidebar());
auto sidebar_control_view = sidebar_container_view->sidebar_control_view_;
auto sidebar_scroll_view = sidebar_control_view->sidebar_items_view_;
auto sidebar_items_contents_view = sidebar_scroll_view->contents_view_;
DCHECK(sidebar_items_contents_view);
return sidebar_items_contents_view;
}
SidebarItemsScrollView* GetSidebarItemsScrollView(
SidebarController* controller) const {
auto* sidebar_container_view =
static_cast<SidebarContainerView*>(controller->sidebar());
auto sidebar_control_view = sidebar_container_view->sidebar_control_view_;
return sidebar_control_view->sidebar_items_view_;
}
// If the item at |index| is panel item, this will return after waiting
// model's active index is changed as active index could not be not updated
// synchronously. Panel activation is done via SidePanelCoordinator instead of
// asking activation to SidebarController directly.
void SimulateSidebarItemClickAt(size_t index) {
auto sidebar_items_contents_view =
GetSidebarItemsContentsView(controller());
auto* item = sidebar_items_contents_view->children()[index].get();
DCHECK(item);
const gfx::Point origin(0, 0);
ui::MouseEvent event(ui::EventType::kMousePressed, origin, origin,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON, 0);
sidebar_items_contents_view->OnItemPressed(item, event);
if (model()->GetAllSidebarItems()[index].open_in_panel) {
WaitUntil(base::BindLambdaForTesting(
[&]() { return model()->active_index() == index; }));
}
}
SidebarControlView* GetSidebarControlView() const {
return GetSidebarContainerView()->sidebar_control_view_;
}
SidebarContainerView* GetSidebarContainerView() const {
return static_cast<SidebarContainerView*>(controller()->sidebar());
}
void CheckOperationFromActiveTabChangedFlagCleared() const {
EXPECT_FALSE(GetSidebarContainerView()->operation_from_active_tab_change_);
}
SidePanel* GetSidePanel() const {
return GetSidebarContainerView()->side_panel_;
}
bool IsSidebarUIOnLeft() const {
if (IsV2Enabled()) {
return GetSidebarContainerView()->sidebar_on_left_ &&
GetSidebarControlView()->sidebar_on_left_;
}
return GetSidebarContainerView()->sidebar_on_left_ &&
!GetSidePanel()->IsRightAligned() &&
GetSidebarControlView()->sidebar_on_left_;
}
void ShowSidebar(bool show_side_panel) {
GetSidebarContainerView()->ShowSidebar(show_side_panel);
}
void HideSidebar(bool hide_sidebar_control) {
GetSidebarContainerView()->HideSidebar(hide_sidebar_control);
}
void WaitUntil(base::RepeatingCallback<bool()> condition) {
if (condition.Run()) {
return;
}
base::RepeatingTimer scheduler;
scheduler.Start(FROM_HERE, base::Milliseconds(100),
base::BindLambdaForTesting([this, &condition] {
if (condition.Run()) {
run_loop_->Quit();
}
}));
Run();
}
void Run() {
run_loop_ = std::make_unique<base::RunLoop>();
run_loop()->Run();
}
void SetItemAddedBubbleLaunchedCallback(
SidebarItemsContentsView* items_contents_view) {
items_contents_view->item_added_bubble_launched_for_test_ =
base::BindRepeating(
&SidebarBrowserTest::ItemAddedBubbleLaunchedCallback,
weak_factory_.GetWeakPtr());
}
void ItemAddedBubbleLaunchedCallback(views::View* anchor) {
item_added_bubble_anchor_ = anchor;
}
void AddItemsTillScrollable(SidebarItemsScrollView* scroll_view,
SidebarService* sidebar_service) {
int url_prefix = 0;
while (true) {
sidebar_service->AddItem(sidebar::SidebarItem::Create(
GURL(base::StrCat(
{"https://foo/bar_", base::NumberToString(url_prefix)})),
u"title", SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false));
url_prefix++;
base::RunLoop().RunUntilIdle();
// Add items till first item becomes invisible.
if (scroll_view->NeedScrollForItemAt(0)) {
break;
}
}
}
bool NeedScrollForItemAt(size_t index, SidebarItemsScrollView* scroll_view) {
return scroll_view->NeedScrollForItemAt(index);
}
void VerifyTargetDragIndicatorIndexCalc(const gfx::Point& screen_position) {
auto sidebar_items_contents_view =
GetSidebarItemsContentsView(controller());
EXPECT_NE(std::nullopt,
sidebar_items_contents_view->CalculateTargetDragIndicatorIndex(
screen_position));
}
base::RunLoop* run_loop() const { return run_loop_.get(); }
size_t GetDefaultItemCount() const {
// kDefaultBuiltInItemTypes.size() already accounts for buildflags
// (ENABLE_BRAVE_TALK, ENABLE_AI_CHAT) at compile time.
auto item_count =
std::size(SidebarServiceFactory::kDefaultBuiltInItemTypes) -
1 /* for history*/;
#if BUILDFLAG(ENABLE_PLAYLIST)
if (!base::FeatureList::IsEnabled(playlist::features::kPlaylist)) {
item_count -= 1;
}
#endif
#if BUILDFLAG(ENABLE_AI_CHAT)
if (!ai_chat::features::IsAIChatEnabled()) {
item_count -= 1;
}
#endif
#if !BUILDFLAG(ENABLE_BRAVE_WALLET)
item_count -= 1;
#endif
return item_count;
}
int GetFirstPanelItemIndex() {
auto const items = model()->GetAllSidebarItems();
auto const iter =
std::ranges::find(items, true, &SidebarItem::open_in_panel);
return std::distance(items.cbegin(), iter);
}
int GetFirstWebItemIndex() {
const auto items = model()->GetAllSidebarItems();
auto const iter =
std::ranges::find(items, false, &SidebarItem::open_in_panel);
return std::distance(items.cbegin(), iter);
}
// ===== V2-aware helpers =====
bool IsV2Enabled() const {
return base::FeatureList::IsEnabled(sidebar::features::kSidebarV2);
}
raw_ptr<views::View, DanglingUntriaged> item_added_bubble_anchor_ = nullptr;
std::unique_ptr<base::RunLoop> run_loop_;
base::WeakPtrFactory<SidebarBrowserTest> weak_factory_{this};
};
// Parameterized test fixture to test both Sidebar V1 and V2.
// Test parameter: bool - false = V1 (default), true = V2 (kSidebarV2 enabled)
//
// Tests using this fixture fall into three categories:
//
// Category A: Tests that work in both V1 and V2 without changes
// - Tests UI/model/controller logic that's identical across versions
// - No conditional logic needed (runs same assertions for both V1 and V2)
//
// Category B: Mixed tests with version-specific sections
// - Tests behavior that differs between V1 and V2
// - Uses if (IsV2Enabled()) conditionals to handle version-specific
// assertions
//
// Category C: V1-only tests (skip in V2)
// - Tests V1-specific functionality that doesn't exist in V2
// - Uses GTEST_SKIP() to skip when IsV2Enabled() is true
//
// This parameterized test will be removed when V2 is enabled by default.
class SidebarBrowserTestV1AndV2 : public SidebarBrowserTest,
public testing::WithParamInterface<bool> {
public:
SidebarBrowserTestV1AndV2() {
if (GetParam()) { // true = Enable V2
scoped_features_.InitAndEnableFeature(sidebar::features::kSidebarV2);
} else { // false = V1 (default behavior)
scoped_features_.InitAndDisableFeature(sidebar::features::kSidebarV2);
}
}
~SidebarBrowserTestV1AndV2() override = default;
private:
base::test::ScopedFeatureList scoped_features_;
};
// Category B: Mixed test (panel-specific sections guarded with conditionals)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, BasicTest) {
EXPECT_TRUE(!!GetSidePanelToolbarButton()->context_menu_controller());
// Initially, active index is not set.
EXPECT_THAT(model()->active_index(), Eq(std::nullopt));
// Check sidebar UI is initalized properly.
EXPECT_TRUE(!!controller()->sidebar());
// IDC_TOGGLE_SIDEBAR command doesn't work for V2 now.
// In V1, SidebarContainerView listens panel open/close event.
// In V2, SidebarController will do that.
if (!IsV2Enabled()) {
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
WaitUntil(base::BindLambdaForTesting(
[&]() { return !!model()->active_index(); }));
// Check active index is non-null.
EXPECT_THAT(model()->active_index(), Ne(std::nullopt));
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
WaitUntil(
base::BindLambdaForTesting([&]() { return !model()->active_index(); }));
// Check active index is null.
EXPECT_THAT(model()->active_index(), Eq(std::nullopt));
}
auto expected_count = GetDefaultItemCount();
EXPECT_EQ(expected_count, model()->GetAllSidebarItems().size());
// Activate item that opens in panel.
const size_t first_panel_item_index = GetFirstPanelItemIndex();
const auto& first_panel_item =
controller()->model()->GetAllSidebarItems()[first_panel_item_index];
if (IsV2Enabled()) {
controller()->ActivateItemAt(
model()->GetIndexOf(first_panel_item.built_in_item_type));
} else {
controller()->ActivatePanelItem(first_panel_item.built_in_item_type);
}
WaitUntil(
base::BindLambdaForTesting([&]() { return !!model()->active_index(); }));
EXPECT_THAT(model()->active_index(), Optional(first_panel_item_index));
EXPECT_TRUE(controller()->IsActiveIndex(first_panel_item_index));
// Get first index of item that opens in a new tab (not panel).
// Note: Web-type items (kBraveTalk, kWallet) may not exist if their
// respective features are disabled.
const size_t first_web_item_index = GetFirstWebItemIndex();
int active_item_index = first_panel_item_index;
if (first_web_item_index < model()->GetAllSidebarItems().size()) {
const auto item = model()->GetAllSidebarItems()[first_web_item_index];
EXPECT_FALSE(item.open_in_panel);
controller()->ActivateItemAt(first_web_item_index);
}
EXPECT_THAT(model()->active_index(), Optional(active_item_index));
// Setting std::nullopt means deactivate current active tab.
if (IsV2Enabled()) {
controller()->ActivateItemAt(std::nullopt);
} else {
controller()->DeactivateCurrentPanel();
}
WaitUntil(
base::BindLambdaForTesting([&]() { return !model()->active_index(); }));
EXPECT_THAT(model()->active_index(), Eq(std::nullopt));
if (IsV2Enabled()) {
controller()->ActivateItemAt(
model()->GetIndexOf(first_panel_item.built_in_item_type));
} else {
controller()->ActivatePanelItem(first_panel_item.built_in_item_type);
}
WaitUntil(
base::BindLambdaForTesting([&]() { return !!model()->active_index(); }));
EXPECT_THAT(model()->active_index(), Optional(active_item_index));
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
// Move active item to the next index to make sure it's not the first item.
sidebar_service->MoveItem(first_panel_item_index, first_panel_item_index + 1);
active_item_index++;
EXPECT_THAT(model()->active_index(), Eq(active_item_index));
// Remove Item at index 0 to change active index.
sidebar_service->RemoveItemAt(0);
active_item_index--;
--expected_count;
// Wait for sidebar item removal to complete before navigation.
WaitUntil(base::BindLambdaForTesting([&]() {
return model()->GetAllSidebarItems().size() == expected_count;
}));
EXPECT_EQ(expected_count, model()->GetAllSidebarItems().size());
EXPECT_THAT(model()->active_index(), Optional(active_item_index));
// Navigate to a non-NTP page first to ensure we can add it to sidebar
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("brave://settings/")));
// If current active tab is not NTP, we can add current url to sidebar.
EXPECT_TRUE(CanAddCurrentActiveTabToSidebar(browser()));
// If current active tab is NTP, we can't add current url to sidebar.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("brave://newtab/")));
EXPECT_FALSE(CanAddCurrentActiveTabToSidebar(browser()));
// Check |BrowserView::find_bar_host_view_| is the last child view.
// If not, findbar dialog is not positioned properly.
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto find_bar_host_view_index =
browser_view->GetIndexOf(browser_view->find_bar_host_view());
EXPECT_THAT(find_bar_host_view_index,
Optional(browser_view->children().size() - 1));
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, WebTypePanelTest) {
auto expected_count = GetDefaultItemCount();
EXPECT_EQ(expected_count, model()->GetAllSidebarItems().size());
// Add an item
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("brave://settings/")));
int current_tab_index = tab_model()->active_index();
EXPECT_EQ(0, current_tab_index);
EXPECT_TRUE(CanAddCurrentActiveTabToSidebar(browser()));
controller()->AddItemWithCurrentTab();
// Verify new size
EXPECT_EQ(++expected_count, model()->GetAllSidebarItems().size());
// Load NTP in a new tab and activate it. (tab index 1)
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
current_tab_index = tab_model()->active_index();
EXPECT_EQ(1, current_tab_index);
// Activate sidebar item(brave://settings) and check existing first tab is
// activated.
auto items = model()->GetAllSidebarItems();
auto iter =
std::ranges::find(items, GURL("chrome://settings/"), &SidebarItem::url);
EXPECT_NE(items.end(), iter);
controller()->ActivateItemAt(std::distance(items.begin(), iter));
EXPECT_EQ(0, tab_model()->active_index());
EXPECT_EQ(tab_model()->GetWebContentsAt(0)->GetVisibleURL(), iter->url);
#if BUILDFLAG(ENABLE_BRAVE_WALLET)
// Activate second sidebar item(wallet) and check it's loaded at current tab.
iter = std::ranges::find(items, SidebarItem::BuiltInItemType::kWallet,
&SidebarItem::built_in_item_type);
EXPECT_NE(items.end(), iter);
controller()->ActivateItemAt(std::distance(items.begin(), iter));
EXPECT_EQ(0, tab_model()->active_index());
EXPECT_EQ(tab_model()->GetWebContentsAt(0)->GetVisibleURL(), iter->url);
#endif
// New tab is not created.
EXPECT_EQ(2, tab_model()->count());
}
#if BUILDFLAG(ENABLE_BRAVE_WALLET)
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, IterateBuiltInWebTypeTest) {
// Click builtin wallet item and it's loaded at current active tab.
const auto items = model()->GetAllSidebarItems();
const auto wallet_item_iter =
std::ranges::find(items, SidebarItem::BuiltInItemType::kWallet,
&SidebarItem::built_in_item_type);
ASSERT_NE(wallet_item_iter, items.cend());
const int wallet_item_index = std::distance(items.cbegin(), wallet_item_iter);
auto wallet_item = model()->GetAllSidebarItems()[wallet_item_index];
EXPECT_FALSE(controller()->DoesBrowserHaveOpenedTabForItem(wallet_item));
SimulateSidebarItemClickAt(wallet_item_index);
EXPECT_TRUE(controller()->DoesBrowserHaveOpenedTabForItem(wallet_item));
EXPECT_EQ(0, tab_model()->active_index());
EXPECT_EQ(tab_model()->GetWebContentsAt(0)->GetVisibleURL().host(),
wallet_item.url.host());
// Create NTP and click wallet item. Then wallet tab(index 0) is activated.
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
// NTP is active tab.
EXPECT_EQ(1, tab_model()->active_index());
SimulateSidebarItemClickAt(wallet_item_index);
// Wallet tab is active tab.
EXPECT_EQ(0, tab_model()->active_index());
EXPECT_EQ(tab_model()->GetWebContentsAt(0)->GetVisibleURL().host(),
wallet_item.url.host());
// Create NTP.
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
// NTP is active tab and load wallet on it.
EXPECT_EQ(2, tab_model()->active_index());
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), wallet_item.url));
// Click wallet item and then first wallet tab(tab index 0) is activated.
SimulateSidebarItemClickAt(wallet_item_index);
EXPECT_EQ(0, tab_model()->active_index());
// Click wallet item and then second wallet tab(index 2) is activated.
SimulateSidebarItemClickAt(wallet_item_index);
EXPECT_EQ(2, tab_model()->active_index());
// Click wallet item and then first wallet tab(index 0) is activated.
SimulateSidebarItemClickAt(wallet_item_index);
EXPECT_EQ(0, tab_model()->active_index());
// Checking windows' activation state is flaky in browser tests.
#if !BUILDFLAG(IS_MAC)
auto* browser2 = CreateBrowser(browser()->profile());
WaitUntil(base::BindLambdaForTesting(
[&]() { return browser2->window()->IsActive(); }));
// |browser2| doesn't have any wallet tab. So, clicking wallet sidebar item
// activates other browser's first wallet tab.
browser2->GetFeatures().sidebar_controller()->ActivateItemAt(
wallet_item_index);
// Wait till browser() is activated.
WaitUntil(base::BindLambdaForTesting(
[&]() { return browser()->window()->IsActive(); }));
EXPECT_EQ(0, tab_model()->active_index());
#endif
}
#endif // BUILDFLAG(ENABLE_BRAVE_WALLET)
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
BookmarksPanelShownAfterReadingListTest) {
if (IsV2Enabled()) {
controller()->ActivateItemAt(
model()->GetIndexOf(SidebarItem::BuiltInItemType::kReadingList));
} else {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kReadingList);
}
// Check reading list panel is activated.
auto reading_list_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kReadingList);
ASSERT_TRUE(reading_list_item_index.has_value());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return controller()->IsActiveIndex(reading_list_item_index); }));
// Check bookmarks panel is activated.
if (IsV2Enabled()) {
controller()->ActivateItemAt(
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
} else {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kBookmarks);
}
auto bookmarks_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
ASSERT_TRUE(bookmarks_item_index.has_value());
ASSERT_TRUE(base::test::RunUntil(
[&]() { return controller()->IsActiveIndex(bookmarks_item_index); }));
}
// Category C:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
PRE_LastlyUsedSidePanelItemTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Panel state persistence is V1-specific";
}
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kBookmarks);
// Wait till panel UI opens.
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->GetVisible(); }));
// Check bookmarks panel is shown.
auto bookmark_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
ASSERT_TRUE(bookmark_item_index.has_value());
EXPECT_TRUE(controller()->IsActiveIndex(bookmark_item_index));
}
// Category C:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, LastlyUsedSidePanelItemTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Panel state persistence is V1-specific";
}
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Toggle();
// Wait till panel UI opens.
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->GetVisible(); }));
// Check bookmarks item is opened after toggle.
auto bookmark_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
ASSERT_TRUE(bookmark_item_index.has_value());
EXPECT_TRUE(controller()->IsActiveIndex(bookmark_item_index));
}
// Category C:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, DefaultEntryTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Default panel entry testing is V1-specific";
}
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
auto bookmark_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
panel_ui->Show(SidePanelEntryId::kBookmarks);
// Wait till bookmark panel is activated.
WaitUntil(base::BindLambdaForTesting(
[&]() { return controller()->IsActiveIndex(bookmark_item_index); }));
panel_ui->Close();
WaitUntil(base::BindLambdaForTesting(
[&]() { return !panel_ui->GetCurrentEntryId().has_value(); }));
// Remove bookmarks and check it's gone.
SidebarServiceFactory::GetForProfile(browser()->profile())
->RemoveItemAt(*bookmark_item_index);
EXPECT_FALSE(!!model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
// Open panel w/o entry id.
panel_ui->Toggle();
WaitUntil(base::BindLambdaForTesting(
[&]() { return panel_ui->GetCurrentEntryId().has_value(); }));
// Check bookmark panel is not opened again as it's deleted item.
EXPECT_NE(SidePanelEntryId::kBookmarks, panel_ui->GetCurrentEntryId());
}
// Category A:
// Test sidebar's initial horizontal option is set properly.
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
PRE_InitialHorizontalOptionTest) {
auto* prefs = browser()->profile()->GetPrefs();
// Check default horizontal option is right-sided.
EXPECT_TRUE(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
EXPECT_FALSE(IsSidebarUIOnLeft());
// Set left-sided for next testing.
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, InitialHorizontalOptionTest) {
auto* prefs = browser()->profile()->GetPrefs();
// Check horizontal option is right-sided.
EXPECT_FALSE(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
EXPECT_TRUE(IsSidebarUIOnLeft());
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, ItemDragIndicatorCalcTest) {
auto sidebar_items_contents_view = GetSidebarItemsContentsView(controller());
gfx::Rect contents_view_rect = sidebar_items_contents_view->GetLocalBounds();
views::View::ConvertRectToScreen(sidebar_items_contents_view,
&contents_view_rect);
gfx::Point screen_position = contents_view_rect.origin();
screen_position.Offset(5, 0);
// Any point from items contents view should have proper drag indicator index.
for (int i = 0; i < contents_view_rect.height(); ++i) {
gfx::Point simulated_mouse_drag_point = screen_position;
simulated_mouse_drag_point.Offset(0, i);
VerifyTargetDragIndicatorIndexCalc(simulated_mouse_drag_point);
}
}
class SidebarBrowserWithSplitViewTest
: public SidebarBrowserTest,
public testing::WithParamInterface<bool> {
public:
SidebarBrowserWithSplitViewTest() = default;
~SidebarBrowserWithSplitViewTest() override = default;
void SetUpOnMainThread() override {
SidebarBrowserTest::SetUpOnMainThread();
browser()->profile()->GetPrefs()->SetBoolean(kWebViewRoundedCorners,
GetParam());
}
void NewSplitTab() {
chrome::NewSplitTab(browser(),
split_tabs::SplitTabCreatedSource::kTabContextMenu);
}
// Use this when left split view is active.
views::View* GetStartSplitContentsView() {
return browser_view()
->GetBraveMultiContentsView()
->GetActiveContentsContainerView();
}
// Use this when left split view is active.
views::View* GetEndSplitContentsView() {
return browser_view()
->GetBraveMultiContentsView()
->GetInactiveContentsContainerView();
}
BraveBrowserView* browser_view() {
return BraveBrowserView::From(
BrowserView::GetBrowserViewForBrowser(browser()));
}
private:
display::test::TestScreen screen{/*create_display=*/true,
/*register_screen=*/true};
};
IN_PROC_BROWSER_TEST_P(SidebarBrowserWithSplitViewTest,
ShowSidebarOnMouseOverTest) {
auto scoped_mode = gfx::AnimationTestApi::SetRichAnimationRenderMode(
gfx::Animation::RichAnimationRenderMode::FORCE_DISABLED);
auto* service = SidebarServiceFactory::GetForProfile(browser()->profile());
service->SetSidebarShowOption(
SidebarService::ShowSidebarOption::kShowOnMouseOver);
// To put sidebar right position after changing show option.
browser_view()->DeprecatedLayoutImmediately();
auto* browser_view = static_cast<BraveBrowserView*>(
BrowserView::GetBrowserViewForBrowser(browser()));
auto* prefs = browser()->profile()->GetPrefs();
auto* sidebar_container = GetSidebarContainerView();
auto* screen = display::Screen::Get();
auto contents_area_view_rect =
browser_view->GetBoundingBoxInScreenForMouseOverHandling();
EXPECT_EQ(browser_view->width(), contents_area_view_rect.width());
// Check sidebar is not shown.
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
// Set mouse position inside the mouse hover area to check sidebar UI is shown
// with that mouse position when sidebar is on right side.
gfx::Point mouse_position = contents_area_view_rect.top_right();
mouse_position.Offset(-2, 2);
screen->SetCursorScreenPointForTesting(mouse_position);
browser_view->HandleBrowserWindowMouseEvent(GetDummyEvent());
EXPECT_TRUE(sidebar_container->IsSidebarVisible());
// Check when sidebar on left.
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
// Hide sidebar.
HideSidebar(true);
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
// Set mouse position inside the mouse hover area to check sidebar UI is shown
// with that mouse position when sidebar is on left side.
mouse_position = contents_area_view_rect.origin();
mouse_position.Offset(2, 2);
screen->SetCursorScreenPointForTesting(mouse_position);
browser_view->HandleBrowserWindowMouseEvent(GetDummyEvent());
EXPECT_TRUE(sidebar_container->IsSidebarVisible());
// Hide sidebar.
HideSidebar(true);
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
// Check with the space between window border and contents.
// We have that space with rounded corners.
// When mouse moves into that space, sidebar should be visible.
mouse_position = contents_area_view_rect.origin();
screen->SetCursorScreenPointForTesting(mouse_position);
browser_view->HandleBrowserWindowMouseEvent(GetDummyEvent());
EXPECT_TRUE(sidebar_container->IsSidebarVisible());
// Hide sidebar.
HideSidebar(true);
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
// Test with split view.
// With sidebar on left, only left split view contents' left side hot
// corner should trigger split view.
NewSplitTab();
auto* tab_strip_model = browser()->tab_strip_model();
EXPECT_EQ(2, tab_strip_model->count());
// Make left split view as active to make secondary container put
// at right side(end).
tab_strip_model->ActivateTabAt(0);
auto* left_split_view = GetStartSplitContentsView();
auto* right_split_view = GetEndSplitContentsView();
// Check left split view's left hot corner handles.
mouse_position = left_split_view->GetLocalBounds().origin();
mouse_position.Offset(2, 2);
views::View::ConvertPointToScreen(left_split_view, &mouse_position);
screen->SetCursorScreenPointForTesting(mouse_position);
browser_view->HandleBrowserWindowMouseEvent(GetDummyEvent());
EXPECT_TRUE(sidebar_container->IsSidebarVisible());
// Hide sidebar.
HideSidebar(true);
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
// Check right split view's left hot corner doesn't handle.
mouse_position = right_split_view->GetLocalBounds().origin();
mouse_position.Offset(2, 2);
views::View::ConvertPointToScreen(right_split_view, &mouse_position);
screen->SetCursorScreenPointForTesting(mouse_position);
browser_view->HandleBrowserWindowMouseEvent(GetDummyEvent());
EXPECT_FALSE(sidebar_container->IsSidebarVisible());
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
SidebarBrowserWithSplitViewTest,
::testing::Bool());
class SidebarBrowserWithWebPanelTest
: public SidebarBrowserTest,
public testing::WithParamInterface<bool> {
public:
SidebarBrowserWithWebPanelTest() {
if (GetParam()) {
scoped_features_.InitAndEnableFeature(
sidebar::features::kSidebarWebPanel);
}
}
~SidebarBrowserWithWebPanelTest() override = default;
BraveMultiContentsView* GetBraveMultiContentsView() {
return browser_view()->GetBraveMultiContentsView();
}
BraveBrowserView* browser_view() {
return BraveBrowserView::From(
BrowserView::GetBrowserViewForBrowser(browser()));
}
bool IsWebPanelEnabled() const {
return base::FeatureList::IsEnabled(features::kSidebarWebPanel);
}
SidebarWebPanelController* web_panel_controller() {
return controller()->GetWebPanelController();
}
private:
base::test::ScopedFeatureList scoped_features_;
};
IN_PROC_BROWSER_TEST_P(SidebarBrowserWithWebPanelTest, WebPanelTest) {
auto contents_container_view_for_web_panel =
GetBraveMultiContentsView()->contents_container_view_for_web_panel_;
if (!IsWebPanelEnabled()) {
EXPECT_FALSE(contents_container_view_for_web_panel);
// Even web panel feature is disabled, sidebar could have web panel
// type because it could be added when that feature enabled.
// In this sitaution, web panel type should work like web type that
// load its url in a tab.
// Test it works in that way after adding web panel type.
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
GURL item_url("http://foo.bar/");
sidebar_service->AddItem(sidebar::SidebarItem::Create(
item_url, u"title", SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, /*open_in_panel*/ true));
EXPECT_NE(tab_model()->GetActiveWebContents()->GetVisibleURL(), item_url);
// Above item is added at last.
controller()->ActivateItemAt(sidebar_service->items().size() - 1);
EXPECT_EQ(tab_model()->GetActiveWebContents()->GetVisibleURL(), item_url);
// Test toggle existing panel doesn't have any issue even web panel type
// exists.
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
ASSERT_TRUE(
base::test::RunUntil([&]() { return GetSidePanel()->GetVisible(); }));
panel_ui->Close();
ASSERT_TRUE(
base::test::RunUntil([&]() { return !GetSidePanel()->GetVisible(); }));
return;
}
EXPECT_TRUE(contents_container_view_for_web_panel);
EXPECT_FALSE(contents_container_view_for_web_panel->GetVisible());
EXPECT_GT(GetBraveMultiContentsView()->web_panel_width_, 0);
EXPECT_EQ(GetBraveMultiContentsView()->GetWebPanelWidth(), 0);
EXPECT_EQ(contents_container_view_for_web_panel->width(), 0);
EXPECT_FALSE(GetBraveMultiContentsView()->web_panel_on_left_);
EXPECT_EQ(
GetBraveMultiContentsView()->width(),
GetBraveMultiContentsView()->GetActiveContentsContainerView()->width());
contents_container_view_for_web_panel->SetVisible(true);
EXPECT_GT(GetBraveMultiContentsView()->GetWebPanelWidth(), 0);
GetBraveMultiContentsView()->InvalidateLayout();
ASSERT_TRUE(base::test::RunUntil([&]() {
return contents_container_view_for_web_panel->width() ==
GetBraveMultiContentsView()->web_panel_width_;
}));
EXPECT_EQ(contents_container_view_for_web_panel->bounds().origin(),
GetBraveMultiContentsView()
->GetActiveContentsContainerView()
->bounds()
.top_right());
GetBraveMultiContentsView()->SetWebPanelOnLeft(true);
ASSERT_TRUE(base::test::RunUntil([&]() {
return contents_container_view_for_web_panel->bounds().top_right() ==
GetBraveMultiContentsView()
->GetActiveContentsContainerView()
->bounds()
.origin();
}));
contents_container_view_for_web_panel->SetVisible(false);
// To prevent item added bubble launching.
auto* prefs = browser()->profile()->GetPrefs();
prefs->SetInteger(sidebar::kSidebarItemAddedFeedbackBubbleShowCount, 3);
// Add two web panel items and check panel is shown by activating it.
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("https://brave.com/")));
EXPECT_TRUE(CanAddCurrentActiveTabToSidebar(browser()));
controller()->AddItemWithCurrentTab();
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL("https://basicattentiontoken.com/")));
EXPECT_TRUE(CanAddCurrentActiveTabToSidebar(browser()));
controller()->AddItemWithCurrentTab();
EXPECT_FALSE(GetBraveMultiContentsView()->IsWebPanelVisible());
// Activate web panel item and check panel gets visible.
// We have 1 tabs now and will check another pinned tab is created for web
// panel.
auto* tab_strip_model = browser()->tab_strip_model();
EXPECT_EQ(1, tab_strip_model->count());
const int web_panel_item_index = model()->GetAllSidebarItems().size() - 1;
EXPECT_TRUE(
model()->GetAllSidebarItems()[web_panel_item_index].is_web_panel_type());
EXPECT_TRUE(model()
->GetAllSidebarItems()[web_panel_item_index - 1]
.is_web_panel_type());
controller()->ActivateItemAt(web_panel_item_index - 1);
EXPECT_TRUE(GetBraveMultiContentsView()->IsWebPanelVisible());
EXPECT_TRUE(
contents_container_view_for_web_panel->mini_toolbar()->GetVisible());
// Activate another web panel and check panel is still visible.
controller()->ActivateItemAt(web_panel_item_index);
EXPECT_TRUE(GetBraveMultiContentsView()->IsWebPanelVisible());
EXPECT_TRUE(
contents_container_view_for_web_panel->mini_toolbar()->GetVisible());
// Now we have another pinned tab for web panel at 0.
EXPECT_EQ(2, tab_strip_model->count());
auto* tab_for_web_panel = tab_strip_model->GetTabAtIndex(0);
EXPECT_TRUE(tab_for_web_panel->IsPinned());
EXPECT_FALSE(tab_for_web_panel->IsActivated());
EXPECT_EQ(tab_for_web_panel->GetContents(),
web_panel_controller()->panel_contents());
// Toggle web panel item and check panel gets hidden.
controller()->ActivateItemAt(web_panel_item_index);
EXPECT_FALSE(web_panel_controller()->panel_contents());
EXPECT_FALSE(GetBraveMultiContentsView()->IsWebPanelVisible());
EXPECT_EQ(1, tab_strip_model->count());
// Open web panel.
controller()->ActivateItemAt(web_panel_item_index);
EXPECT_TRUE(GetBraveMultiContentsView()->IsWebPanelVisible());
EXPECT_EQ(2, tab_strip_model->count());
tab_for_web_panel = tab_strip_model->GetTabAtIndex(0);
EXPECT_EQ(tab_for_web_panel->GetContents(),
web_panel_controller()->panel_contents());
// Web panel contents should be treated as active contents without
// explicitly forcing focus (avoids macOS focus teardown issues).
EXPECT_EQ(
web_panel_controller()->panel_contents(),
contents_container_view_for_web_panel->contents_view()->GetWebContents());
// Check tab contents test with split view.
// Create split view with tab at 1.
tab_strip_model->ActivateTabAt(1);
chrome::NewSplitTab(browser(),
split_tabs::SplitTabCreatedSource::kTabContextMenu);
EXPECT_EQ(2, tab_strip_model->active_index());
EXPECT_TRUE(tab_strip_model->GetTabAtIndex(1)->IsSplit());
EXPECT_TRUE(tab_strip_model->GetTabAtIndex(2)->IsSplit());
EXPECT_FALSE(GetBraveMultiContentsView()->is_web_panel_active_);
// Cache current inactive split tab contents and check it's not changed for
// inactive contents view when web panel is activated. Only active contents
// view is changed to panel's contents as
// BraveMultiContentsView::GetActiveContentsView() will give panel's contents.
auto* inactive_split_tab_contents =
GetBraveMultiContentsView()->GetInactiveContentsView()->GetWebContents();
tab_strip_model->ActivateTabAt(0);
EXPECT_TRUE(GetBraveMultiContentsView()->is_web_panel_active_);
EXPECT_EQ(0, tab_strip_model->active_index());
EXPECT_EQ(
web_panel_controller()->panel_contents(),
GetBraveMultiContentsView()->GetActiveContentsView()->GetWebContents());
EXPECT_EQ(
inactive_split_tab_contents,
GetBraveMultiContentsView()->GetInactiveContentsView()->GetWebContents());
// Check web panel is closed by closing its pinned tab.
tab_for_web_panel->Close();
EXPECT_FALSE(web_panel_controller()->panel_contents());
EXPECT_FALSE(GetBraveMultiContentsView()->IsWebPanelVisible());
EXPECT_EQ(2, tab_strip_model->count());
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
SidebarBrowserWithWebPanelTest,
::testing::Bool());
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, HideSidebarUITest) {
auto* service = SidebarServiceFactory::GetForProfile(browser()->profile());
auto* sidebar_container = GetSidebarContainerView();
// Set to on mouse over and check sidebar ui is not shown.
service->SetSidebarShowOption(
SidebarService::ShowSidebarOption::kShowOnMouseOver);
WaitUntil(base::BindLambdaForTesting(
[&]() { return sidebar_container->width() == 0; }));
// Ask to show sidebar ui and check it's shown.
ShowSidebar(false);
const int target_control_view_width =
GetSidebarControlView()->GetPreferredSize().width();
EXPECT_GT(target_control_view_width, 0);
WaitUntil(base::BindLambdaForTesting([&]() {
return sidebar_container->width() == target_control_view_width;
}));
// Ask to hide sidebar ui and check it's not shown.
HideSidebar(true);
WaitUntil(base::BindLambdaForTesting(
[&]() { return sidebar_container->width() == 0; }));
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
ItemAddedBubbleAnchorViewTest) {
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
auto sidebar_items_contents_view = GetSidebarItemsContentsView(controller());
SetItemAddedBubbleLaunchedCallback(sidebar_items_contents_view);
size_t lastly_added_item_index = 0;
// Add item at last.
item_added_bubble_anchor_ = nullptr;
sidebar_service->AddItem(sidebar::SidebarItem::Create(
GURL("http://foo.bar/"), u"title", SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false));
// Check item is added at last and check that bubble is anchored to
// it properly.
WaitUntil(base::BindLambdaForTesting(
[&]() { return !!item_added_bubble_anchor_; }));
lastly_added_item_index = sidebar_items_contents_view->children().size() - 1;
EXPECT_EQ(item_added_bubble_anchor_,
sidebar_items_contents_view->children()[lastly_added_item_index]);
// Add item at index 0.
item_added_bubble_anchor_ = nullptr;
sidebar_service->AddItemAtForTesting(
sidebar::SidebarItem::Create(GURL("http://foo.bar/"), u"title",
SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false),
0);
// Check item is added at first and check that bubble is anchored to
// it properly.
WaitUntil(base::BindLambdaForTesting(
[&]() { return !!item_added_bubble_anchor_; }));
lastly_added_item_index = 0;
EXPECT_EQ(item_added_bubble_anchor_,
sidebar_items_contents_view->children()[lastly_added_item_index]);
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, ItemActivatedScrollTest) {
// To prevent item added bubble launching.
auto* prefs = browser()->profile()->GetPrefs();
prefs->SetInteger(sidebar::kSidebarItemAddedFeedbackBubbleShowCount, 3);
auto bookmark_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
ASSERT_TRUE(bookmark_item_index.has_value());
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
auto* scroll_view = GetSidebarItemsScrollView(controller());
// Move bookmark item at zero index to make it hidden.
sidebar_service->MoveItem(*bookmark_item_index, 0);
bookmark_item_index = 0;
AddItemsTillScrollable(scroll_view, sidebar_service);
// Check bookmarks item is hidden.
EXPECT_TRUE(NeedScrollForItemAt(*bookmark_item_index, scroll_view));
// Open bookmark panel.
if (IsV2Enabled()) {
// Use panel->Show() when V2 is enabled by default.
// Item should be responded by panel activation.
controller()->ActivateItemAt(
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
} else {
browser()->GetFeatures().side_panel_ui()->Show(
SidePanelEntryId::kBookmarks);
}
// Wait till bookmarks item is visible.
WaitUntil(base::BindLambdaForTesting([&]() {
return !NeedScrollForItemAt(*bookmark_item_index, scroll_view);
}));
EXPECT_TRUE(controller()->IsActiveIndex(bookmark_item_index));
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, ItemAddedScrollTest) {
// To prevent item added bubble launching.
auto* prefs = browser()->profile()->GetPrefs();
prefs->SetInteger(sidebar::kSidebarItemAddedFeedbackBubbleShowCount, 3);
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
auto* scroll_view = GetSidebarItemsScrollView(controller());
auto sidebar_items_contents_view = GetSidebarItemsContentsView(controller());
AddItemsTillScrollable(scroll_view, sidebar_service);
// Check first item is not visible in scroll view.
EXPECT_TRUE(NeedScrollForItemAt(0, scroll_view));
// After inserting item at index 0, it should be visible as sidebar
// scrolls to make that new item visible. So last item becomes invisible.
sidebar_service->AddItemAtForTesting(
sidebar::SidebarItem::Create(GURL("https://abcd"), u"title",
SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false),
0);
WaitUntil(base::BindLambdaForTesting(
[&]() { return !NeedScrollForItemAt(0, scroll_view); }));
int last_item_index = sidebar_items_contents_view->children().size() - 1;
EXPECT_TRUE(NeedScrollForItemAt(last_item_index, scroll_view));
// After inserting item at last, it should be visible as sidebar
// scrolls to make that new item visible. So fisrt item becomes invisible.
last_item_index++;
sidebar_service->AddItem(sidebar::SidebarItem::Create(
GURL("https://abcdefg"), u"title", SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false));
WaitUntil(base::BindLambdaForTesting(
[&]() { return !NeedScrollForItemAt(last_item_index, scroll_view); }));
EXPECT_TRUE(NeedScrollForItemAt(0, scroll_view));
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, PRE_PrefsMigrationTest) {
// Prepare temporarily changed condition.
auto* prefs = browser()->profile()->GetPrefs();
prefs->SetBoolean(sidebar::kSidebarAlignmentChangedTemporarily, true);
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, true);
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, PrefsMigrationTest) {
// Check all prefs are changed to default.
auto* prefs = browser()->profile()->GetPrefs();
EXPECT_TRUE(prefs->FindPreference(prefs::kSidePanelHorizontalAlignment)
->IsDefaultValue());
EXPECT_TRUE(prefs->FindPreference(prefs::kSidePanelHorizontalAlignment)
->IsDefaultValue());
}
// Category C: V1-only test (skip in V2)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, SidePanelResizeTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Panel resize is V1-specific (V2 doesn't manage panel in "
"sidebar container)";
}
auto* prefs = browser()->profile()->GetPrefs();
EXPECT_EQ(kDefaultSidePanelWidth,
prefs->GetInteger(sidebar::kSidePanelWidth));
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
int expected_panel_width = kDefaultSidePanelWidth;
// Wait till sidebar animation ends.
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == expected_panel_width; }));
// Test smaller panel width than default(minimum) and check smaller than
// default is not applied. Positive offset value is for reducing width in
// right-sided sidebar.
GetSidePanel()->OnResize(30, true);
// Check panel width is not changed.
EXPECT_EQ(expected_panel_width, prefs->GetInteger(sidebar::kSidePanelWidth));
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == kDefaultSidePanelWidth; }));
// On right-side sidebar position, side panel's x and resize widget's x is
// same.
EXPECT_EQ(GetSidePanel()->GetBoundsInScreen().x(),
GetSidePanelResizeWidget()->GetWindowBoundsInScreen().x());
// Increase panel width and check resize handle widget's position.
// Negative offset value is for increasing width in right-sided
// sidebar.
GetSidePanel()->OnResize(-20, true);
expected_panel_width += 20;
EXPECT_EQ(expected_panel_width, prefs->GetInteger(sidebar::kSidePanelWidth));
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == expected_panel_width; }));
EXPECT_EQ(GetSidePanel()->GetBoundsInScreen().x(),
GetSidePanelResizeWidget()->GetWindowBoundsInScreen().x());
// Set sidebar on left side.
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
EXPECT_EQ(GetSidePanel()->GetBoundsInScreen().right(),
GetSidePanelResizeWidget()->GetWindowBoundsInScreen().right());
// Increase panel width and check width and resize handle position.
// Positive offset value is for increasing width in left-sided sidebar.
GetSidePanel()->OnResize(20, true);
expected_panel_width += 20;
EXPECT_EQ(expected_panel_width, prefs->GetInteger(sidebar::kSidePanelWidth));
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == expected_panel_width; }));
EXPECT_EQ(GetSidePanel()->GetBoundsInScreen().right(),
GetSidePanelResizeWidget()->GetWindowBoundsInScreen().right());
// Close side panel.
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
WaitUntil(base::BindLambdaForTesting(
[&]() { return !GetSidePanel()->GetVisible(); }));
// Re-open side panel and check it's opened as wide as lastly used width.
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == expected_panel_width; }));
}
// Category C: V1-only (unmanaged panel entry test)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, UnManagedPanelEntryTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Unmanaged panel entry testing is V1-specific";
}
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
// Show bookmarks entry and it has active index.
panel_ui->Show(SidePanelEntryId::kBookmarks);
// Wait till sidebar show ends.
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == kDefaultSidePanelWidth; }));
EXPECT_TRUE(model()->active_index().has_value());
// Cache bookmarks entry index to remove it later.
const auto bookmark_item_index = model()->active_index().value();
// Close panel and wait till panel closing animation ends. Panel is hidden
// when closing completes.
panel_ui->Close();
WaitUntil(base::BindLambdaForTesting(
[&]() { return !GetSidePanel()->GetVisible(); }));
EXPECT_FALSE(!!panel_ui->GetCurrentEntryId());
// Remove bookmarks and check it's gone.
SidebarServiceFactory::GetForProfile(browser()->profile())
->RemoveItemAt(bookmark_item_index);
EXPECT_FALSE(!!model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
// Show bookmarks entry again and wait till sidebar panel gets visible
panel_ui->Show(SidePanelEntryId::kBookmarks);
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->GetVisible(); }));
EXPECT_EQ(SidePanelEntryId::kBookmarks, panel_ui->GetCurrentEntryId());
}
#if BUILDFLAG(ENABLE_BRAVE_WALLET)
// Category C: V1-only (unmanaged panel after item deletion)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
OpenUnManagedPanelAfterDeletingDefaultWebTypeItem) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Unmanaged panel testing is V1-specific";
}
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
const auto items = model()->GetAllSidebarItems();
const auto wallet_item_iter =
std::ranges::find(items, SidebarItem::BuiltInItemType::kWallet,
&SidebarItem::built_in_item_type);
ASSERT_NE(wallet_item_iter, items.cend());
const int wallet_item_index = std::distance(items.cbegin(), wallet_item_iter);
SidebarServiceFactory::GetForProfile(browser()->profile())
->RemoveItemAt(wallet_item_index);
EXPECT_FALSE(!!model()->GetIndexOf(SidebarItem::BuiltInItemType::kWallet));
// Test with upstream's side panel that runs only with chrome://new-tab-page.
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("chrome://new-tab-page/")));
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->GetVisible(); }));
}
#endif // BUILDFLAG(ENABLE_BRAVE_WALLET)
// Category C: V1-only (tab-specific vs global panels)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
TabSpecificAndGlobalPanelsTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Tab-specific vs global panel testing is V1-specific";
}
// Create another tab.
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
// Open contextual panel to tab at 0.
tab_model()->ActivateTabAt(0);
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("chrome://new-tab-page/")));
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->GetVisible(); }));
// No panel when activated tab at 1 because we don't open any global panel.
tab_model()->ActivateTabAt(1);
EXPECT_FALSE(panel_ui->IsSidePanelShowing());
// Open global panel when active tab index is 1.
panel_ui->Show(SidePanelEntryId::kBookmarks);
WaitUntil(base::BindLambdaForTesting([&]() {
return panel_ui->GetCurrentEntryId() == SidePanelEntryId::kBookmarks;
}));
// Contextual panel should be set when activate tab at 0.
tab_model()->ActivateTabAt(0);
WaitUntil(base::BindLambdaForTesting([&]() {
return panel_ui->GetCurrentEntryId() == SidePanelEntryId::kCustomizeChrome;
}));
// Global panel should be set when activate tab at 1.
tab_model()->ActivateTabAt(1);
WaitUntil(base::BindLambdaForTesting([&]() {
return panel_ui->GetCurrentEntryId() == SidePanelEntryId::kBookmarks;
}));
}
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, DisabledItemsTest) {
auto* guest_browser = CreateGuestBrowser();
auto* controller = guest_browser->GetFeatures().sidebar_controller();
auto* model = controller->model();
for (const auto& item : model->GetAllSidebarItems()) {
// Check disabled builtin items are not included in guest browser's items
// list.
if (item.is_built_in_type()) {
EXPECT_FALSE(IsDisabledItemForGuest(item.built_in_item_type));
}
}
auto* private_browser = CreateIncognitoBrowser(browser()->profile());
controller = private_browser->GetFeatures().sidebar_controller();
model = controller->model();
for (const auto& item : model->GetAllSidebarItems()) {
// Check disabled builtin items are not included in private browser's items
// list.
if (item.is_built_in_type()) {
EXPECT_FALSE(IsDisabledItemForPrivate(item.built_in_item_type));
}
}
}
class MockSidebarModelObserver : public SidebarModel::Observer {
public:
MockSidebarModelObserver() = default;
~MockSidebarModelObserver() override = default;
MOCK_METHOD(void,
OnItemAdded,
(const SidebarItem& item, size_t index, bool user_gesture),
(override));
MOCK_METHOD(void,
OnItemMoved,
(const SidebarItem& item, size_t from, size_t to),
(override));
MOCK_METHOD(void, OnItemRemoved, (size_t index), (override));
MOCK_METHOD(void,
OnActiveIndexChanged,
(std::optional<size_t> old_index,
std::optional<size_t> new_index),
(override));
MOCK_METHOD(void,
OnItemUpdated,
(const SidebarItem& item, const SidebarItemUpdate& update),
(override));
MOCK_METHOD(void,
OnFaviconUpdatedForItem,
(const SidebarItem& item, const gfx::ImageSkia& image),
(override));
};
#if BUILDFLAG(ENABLE_AI_CHAT)
class SidebarBrowserTestWithkSidebarShowAlwaysOnStable
: public testing::WithParamInterface<bool>,
public SidebarBrowserTest {
public:
SidebarBrowserTestWithkSidebarShowAlwaysOnStable() {
if (GetParam()) {
feature_list_.InitAndEnableFeatureWithParameters(
sidebar::features::kSidebarShowAlwaysOnStable,
{{"open_one_shot_leo_panel", "true"}});
} else {
feature_list_.InitAndEnableFeature(
sidebar::features::kSidebarShowAlwaysOnStable);
}
}
~SidebarBrowserTestWithkSidebarShowAlwaysOnStable() override = default;
void SetUp() override { SidebarBrowserTest::SetUp(); }
void TearDown() override { SidebarBrowserTest::TearDown(); }
// For skipping SidebarBrowserTest's PreRunTestOnMainThread
// as show option is set explicitely there.
void PreRunTestOnMainThread() override {
InProcessBrowserTest::PreRunTestOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
SidebarBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDontShowSidebarOnNonStable);
command_line->AppendSwitch(switches::kForceFirstRun);
}
base::test::ScopedFeatureList feature_list_;
testing::NiceMock<MockSidebarModelObserver> observer_;
base::ScopedObservation<SidebarModel, SidebarModel::Observer> observation_{
&observer_};
};
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestWithkSidebarShowAlwaysOnStable,
SidebarShowAlwaysTest) {
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(browser()->profile());
EXPECT_EQ(SidebarService::ShowSidebarOption::kShowAlways,
sidebar_service->GetSidebarShowOption());
observation_.Observe(model());
// Check one shot Leo panel is opened or not based on test parameter.
if (GetParam()) {
// If Leo panel is opened, panel active index is changed.
EXPECT_CALL(observer_, OnActiveIndexChanged(testing::_, testing::_))
.Times(1);
} else {
EXPECT_CALL(observer_, OnActiveIndexChanged(testing::_, testing::_))
.Times(0);
}
// Need to make sure template url service is loaded before testing one-shot
// leo panel open. As we don't want to show one-shot leo panel open for
// SERP, SidebarTabHelper uses template url service for checking current
// url is SERP page or not. If template url service is not loaded, it just
// does early return w/o opening leo panel.
// See SidebarTabHelper::PrimaryPageChanged() for more details.
auto* service =
TemplateURLServiceFactory::GetForProfile(browser()->profile());
search_test_utils::WaitForTemplateURLServiceToLoad(service);
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("https://www.brave.com/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
if (GetParam()) {
// Wait till browser has active panel.
WaitUntil(base::BindLambdaForTesting(
[&]() { return !!panel_ui->GetCurrentEntryId(); }));
EXPECT_EQ(SidePanelEntryId::kChatUI, panel_ui->GetCurrentEntryId());
}
testing::Mock::VerifyAndClearExpectations(&observer_);
panel_ui->Close();
EXPECT_FALSE(panel_ui->IsSidePanelShowing());
// Check one shot panel is not opened anymore.
EXPECT_CALL(observer_, OnActiveIndexChanged(testing::_, testing::_)).Times(0);
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("https://www.brave.com/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
EXPECT_FALSE(panel_ui->IsSidePanelShowing());
testing::Mock::VerifyAndClearExpectations(&observer_);
observation_.Reset();
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
SidebarBrowserTestWithkSidebarShowAlwaysOnStable,
::testing::Bool());
#endif // BUILDFLAG(ENABLE_AI_CHAT)
#if BUILDFLAG(ENABLE_PLAYLIST)
class SidebarBrowserTestWithPlaylist : public SidebarBrowserTest {
public:
SidebarBrowserTestWithPlaylist() {
feature_list_.InitAndEnableFeature(playlist::features::kPlaylist);
}
~SidebarBrowserTestWithPlaylist() override = default;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SidebarBrowserTestWithPlaylist, Incognito) {
// There should be no crash with incognito.
auto* private_browser = CreateIncognitoBrowser(browser()->profile());
ASSERT_TRUE(private_browser);
auto* sidebar_service =
SidebarServiceFactory::GetForProfile(private_browser->profile());
const auto& items = sidebar_service->items();
auto iter = std::ranges::find_if(items, [](const auto& item) {
return item.type == SidebarItem::Type::kTypeBuiltIn &&
item.built_in_item_type == SidebarItem::BuiltInItemType::kPlaylist;
});
// Check playlist item is not included in private window.
EXPECT_EQ(iter, items.end());
// Try Adding an item
sidebar_service->AddItem(sidebar::SidebarItem::Create(
GURL("http://foo.bar/"), u"title", SidebarItem::Type::kTypeWeb,
SidebarItem::BuiltInItemType::kNone, false));
// Try moving an item
sidebar_service->MoveItem(sidebar_service->items().size() - 1, 0);
// Try Remove an item
sidebar_service->RemoveItemAt(0);
}
#endif // BUILDFLAG(ENABLE_PLAYLIST)
#if BUILDFLAG(ENABLE_AI_CHAT)
// Fixture for the tab-scoped (non-global) AI Chat sidebar panel. The
// TabSpecific tests below rely on the AI Chat sidebar item being a tab-specific
// entry, which only happens when kAIChatGlobalSidePanelEverywhere is disabled —
// with the global side panel feature on, AI Chat is registered as a global
// entry.
class SidebarBrowserTestWithTabSpecificAIChat : public SidebarBrowserTest {
public:
SidebarBrowserTestWithTabSpecificAIChat() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{ai_chat::features::kAIChat},
/*disabled_features=*/
{ai_chat::features::kAIChatGlobalSidePanelEverywhere});
}
~SidebarBrowserTestWithTabSpecificAIChat() override = default;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SidebarBrowserTestWithTabSpecificAIChat,
TabSpecificPanel) {
// Collect item indexes for test
constexpr auto kGlobalItemType = SidebarItem::BuiltInItemType::kBookmarks;
constexpr auto kTabSpecificItemType = SidebarItem::BuiltInItemType::kChatUI;
auto global_item_index = model()->GetIndexOf(kGlobalItemType);
ASSERT_TRUE(global_item_index.has_value());
auto tab_specific_item_index = model()->GetIndexOf(kTabSpecificItemType);
ASSERT_TRUE(tab_specific_item_index.has_value());
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_EQ(tab_model()->count(), 2);
// Open contextual panel from Tab 0.
tab_model()->ActivateTabAt(0);
SimulateSidebarItemClickAt(tab_specific_item_index.value());
EXPECT_EQ(model()->active_index(), tab_specific_item_index);
// Delete Tab 0 and check model doesn't have active index.
tab_model()->DetachAndDeleteWebContentsAt(0);
EXPECT_FALSE(!!model()->active_index());
ASSERT_EQ(tab_model()->count(), 1);
// Create two more tab for test below.
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_EQ(tab_model()->count(), 3);
// Open a "global" panel from Tab 0
tab_model()->ActivateTabAt(0);
// Tab changed flag should be cleared after ActivateTabAt() executed.
CheckOperationFromActiveTabChangedFlagCleared();
SimulateSidebarItemClickAt(global_item_index.value());
// Open a "tab specific" panel from Tab 1
tab_model()->ActivateTabAt(1);
SimulateSidebarItemClickAt(tab_specific_item_index.value());
// Tab Specific panel should be open when Tab 1 is active
EXPECT_EQ(model()->active_index(), tab_specific_item_index);
// Global panel should be open when Tab 0 is active
tab_model()->ActivateTabAt(0);
EXPECT_EQ(model()->active_index(), global_item_index);
// Global panel should be open when Tab 2 is active
tab_model()->ActivateTabAt(2);
EXPECT_EQ(model()->active_index(), global_item_index);
}
IN_PROC_BROWSER_TEST_F(SidebarBrowserTestWithTabSpecificAIChat,
TabSpecificPanelAndUnManagedPanel) {
// Collect item indexes for test and remove global item.
constexpr auto kGlobalItemType = SidebarItem::BuiltInItemType::kBookmarks;
constexpr auto kTabSpecificItemType = SidebarItem::BuiltInItemType::kChatUI;
auto global_item_index = model()->GetIndexOf(kGlobalItemType);
ASSERT_TRUE(global_item_index.has_value());
SidebarServiceFactory::GetForProfile(browser()->profile())
->RemoveItemAt(*global_item_index);
EXPECT_FALSE(!!model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
auto tab_specific_item_index = model()->GetIndexOf(kTabSpecificItemType);
ASSERT_TRUE(tab_specific_item_index.has_value());
// Open 2 more tabs
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_EQ(tab_model()->count(), 3);
// Open a unmanaged "global" panel from Tab 0
tab_model()->ActivateTabAt(0);
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kBookmarks);
// Wait till sidebar show ends.
WaitUntil(base::BindLambdaForTesting(
[&]() { return GetSidePanel()->width() == kDefaultSidePanelWidth; }));
// Unmanaged entry becomes managed when its panel is shown
// and it becomes active item.
EXPECT_TRUE(model()->active_index().has_value());
EXPECT_EQ(model()->active_index(),
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
// Open a "tab specific" panel from Tab 1
tab_model()->ActivateTabAt(1);
SimulateSidebarItemClickAt(tab_specific_item_index.value());
EXPECT_EQ(SidePanelEntryId::kChatUI, panel_ui->GetCurrentEntryId());
EXPECT_TRUE(GetSidePanel()->GetVisible());
// Tab Specific panel should be open when Tab 1 is active
EXPECT_EQ(model()->active_index(), tab_specific_item_index);
// Global panel should be open when Tab 0 is active
tab_model()->ActivateTabAt(0);
EXPECT_EQ(SidePanelEntryId::kBookmarks, panel_ui->GetCurrentEntryId());
EXPECT_TRUE(model()->active_index().has_value());
EXPECT_EQ(model()->active_index(),
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
// Global panel should be open when Tab 2 is active
tab_model()->ActivateTabAt(2);
EXPECT_EQ(SidePanelEntryId::kBookmarks, panel_ui->GetCurrentEntryId());
EXPECT_TRUE(model()->active_index().has_value());
EXPECT_EQ(model()->active_index(),
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks));
// Check tab specific panel item is still activated after moving to another
// browser.
tab_model()->ActivateTabAt(1);
EXPECT_EQ(model()->active_index(), tab_specific_item_index);
auto* browser2 = CreateBrowser(browser()->profile());
auto* browser2_model = browser2->GetFeatures().sidebar_controller()->model();
auto* browser2_tab_model = browser2->tab_strip_model();
auto detached_tab = tab_model()->DetachTabAtForInsertion(1);
browser2_tab_model->AppendTab(std::move(detached_tab), /* foreground */ true);
EXPECT_EQ(browser2_model->active_index(),
browser2_model->GetIndexOf(SidebarItem::BuiltInItemType::kChatUI));
}
IN_PROC_BROWSER_TEST_F(SidebarBrowserTestWithTabSpecificAIChat,
TabSpecificPanelIdxChange) {
// Collect item indexes for test
constexpr auto kGlobalItemType = SidebarItem::BuiltInItemType::kBookmarks;
constexpr auto kTabSpecificItemType = SidebarItem::BuiltInItemType::kChatUI;
auto global_item_index = model()->GetIndexOf(kGlobalItemType);
ASSERT_TRUE(global_item_index.has_value());
auto tab_specific_item_index = model()->GetIndexOf(kTabSpecificItemType);
ASSERT_TRUE(tab_specific_item_index.has_value());
// Open 2 more tabs
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("brave://newtab/"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
ASSERT_EQ(tab_model()->count(), 3);
// Open a "global" panel from Tab 0
tab_model()->ActivateTabAt(0);
SimulateSidebarItemClickAt(global_item_index.value());
// Open a "tab specific" panel from Tab 1
tab_model()->ActivateTabAt(1);
SimulateSidebarItemClickAt(tab_specific_item_index.value());
// Move global item
size_t new_global_item_index = (global_item_index > 0u) ? 0u : 1u;
SidebarServiceFactory::GetForProfile(browser()->profile())
->MoveItem(global_item_index.value(), new_global_item_index);
tab_specific_item_index = model()->GetIndexOf(kTabSpecificItemType);
// Tab Specific panel should be open when Tab 1 is active
EXPECT_EQ(model()->active_index(), tab_specific_item_index);
// Global panel should be open when Tab 0 is active
tab_model()->ActivateTabAt(0);
EXPECT_EQ(model()->active_index(), new_global_item_index);
// Global panel should be open when Tab 2 is active
tab_model()->ActivateTabAt(2);
EXPECT_EQ(model()->active_index(), new_global_item_index);
}
#endif // BUILDFLAG(ENABLE_AI_CHAT)
// Category A:
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, SidebarRightSideTest) {
// Sidebar is on right by default
EXPECT_FALSE(IsSidebarUIOnLeft());
brave::ToggleVerticalTabStrip(browser());
ASSERT_TRUE(tabs::utils::ShouldShowBraveVerticalTabs(browser()));
auto* prefs = browser()->profile()->GetPrefs();
auto* vertical_tabs_container = GetVerticalTabsContainer();
views::View* sidebar_container = GetSidebarContainerView();
// Check if vertical tabs is located at first and sidebar is located on the
// right side.
EXPECT_LT(vertical_tabs_container->GetBoundsInScreen().x(),
sidebar_container->GetBoundsInScreen().x());
// Changed to sidebar on left side.
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
EXPECT_TRUE(IsSidebarUIOnLeft());
int expected_sidebar_x = vertical_tabs_container->GetBoundsInScreen().right();
if (BraveContentsViewUtil::GetRoundedCornersWebViewMargin(browser())) {
expected_sidebar_x += 1;
}
// Check if vertical tabs is located first and sidebar is following it.
EXPECT_EQ(sidebar_container->GetBoundsInScreen().x(), expected_sidebar_x);
// Check sidebar position option is synced between normal and private window.
auto* private_browser = CreateIncognitoBrowser(browser()->profile());
auto* private_prefs = private_browser->profile()->GetPrefs();
EXPECT_EQ(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment),
private_prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
EXPECT_FALSE(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
private_prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, true);
EXPECT_TRUE(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
}
// Category C: V1-only (SidebarContainerView observation test)
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2,
SidebarContainerDoesNotObserveToolbarHeightEntry) {
if (IsV2Enabled()) {
GTEST_SKIP() << "SidebarContainerView observation is V1-specific";
}
// This test verifies that SidebarContainerView doesn't observe
// SidePanelEntry with kToolbar type, as it only observes kContent type
// entries per AddSidePanelEntryObservation() implementation.
auto* sidebar_container = GetSidebarContainerView();
// Get the tab registry and create a kToolbar type entry
auto* registry = SidePanelRegistry::From(browser()->GetActiveTabInterface());
ASSERT_TRUE(registry);
// Create a kToolbar type SidePanelEntry
std::unique_ptr<SidePanelEntry> toolbar_entry =
std::make_unique<SidePanelEntry>(
SidePanelType::kToolbar,
SidePanelEntry::Key(SidePanelEntry::Id::kAboutThisSite),
base::BindRepeating([](SidePanelEntryScope&) {
return std::make_unique<views::View>();
}),
/*default_content_width_callback=*/base::NullCallback());
auto* toolbar_entry_ptr = toolbar_entry.get();
registry->Register(std::move(toolbar_entry));
// Show the toolbar entry
auto* coordinator = SidePanelCoordinator::From(browser());
coordinator->SetNoDelaysForTesting(true);
coordinator->Show(SidePanelEntry::Id::kAboutThisSite);
// Verify that SidebarContainerView is NOT observing the kToolbar entry.
EXPECT_FALSE(toolbar_entry_ptr->IsBeingObservedBy(sidebar_container));
}
// Category B:
// Tests that sidebar container, side panel, control view, and contents
// container are positioned correctly in RTL mode.
//
// Three fixes work together:
// - SetMirrored(false) on SidebarContainerView/SidebarControlView prevents
// the Views framework from flipping internal layout in RTL.
// - SetFlipCanvasOnPaintForRTLUI(false) on buttons prevents icon flipping.
// - GetMirroredRect(contents_bounds) in BraveBrowserViewLayout ensures the
// contents container is placed correctly next to the sidebar in RTL.
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, SidebarLayoutInRTLTest) {
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* sidebar_container = GetSidebarContainerView();
auto* control_view = GetSidebarControlView();
auto* contents_view = browser_view->contents_container();
// Needs invalidation to apply RTL mode layout.
base::i18n::ScopedRTLForTesting scoped_rtl(/*rtl=*/true);
browser_view->InvalidateLayout();
RunScheduledLayouts();
// --- Right-aligned sidebar (default) ---
ASSERT_FALSE(IsSidebarUIOnLeft());
// As we set mirrored rect for sidebar and contents during the layout,
// Each views' mirrored bounds are what we're seeing in RTL mode.
EXPECT_LE(contents_view->GetMirroredBounds().right(),
sidebar_container->GetMirroredBounds().x());
if (IsV2Enabled()) {
GTEST_SKIP() << "Below panel position observation is V1-specific";
}
// Open the side panel to verify panel/control positioning within the
// sidebar container.
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
RunScheduledLayouts();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return GetSidePanel()->width() == kDefaultSidePanelWidth; }));
// As container disabled mirroring(SetMirrored(false)),
// their bounds are what we're seeing.
EXPECT_LE(GetSidePanel()->bounds().right(), control_view->bounds().x());
// --- Left-aligned sidebar in RTL mode ---
auto* prefs = browser()->profile()->GetPrefs();
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
ASSERT_TRUE(IsSidebarUIOnLeft());
RunScheduledLayouts();
// As container disabled mirroring(SetMirrored(false)),
// their bounds are what we're seeing.
EXPECT_LE(control_view->bounds().right(), GetSidePanel()->bounds().x());
// As we set mirrored rect for sidebar and contents during the layout,
// Each views' mirrored bounds are what we're seeing in RTL mode.
EXPECT_LE(sidebar_container->GetMirroredBounds().right(),
contents_view->GetMirroredBounds().x());
}
// Category C: V1-only (Panel test)
// Tests that side panel resize direction is correct in RTL mode.
// The OnResize sign convention is flipped in RTL compared to LTR because the
// drag direction on screen is reversed.
IN_PROC_BROWSER_TEST_P(SidebarBrowserTestV1AndV2, SidePanelResizeInRTLTest) {
if (IsV2Enabled()) {
GTEST_SKIP() << "Panel resize test is V1-specific";
}
// Open side panel (right-aligned by default).
browser()->command_controller()->ExecuteCommand(IDC_TOGGLE_SIDEBAR);
RunScheduledLayouts();
int expected_width = kDefaultSidePanelWidth;
ASSERT_TRUE(base::test::RunUntil(
[&]() { return GetSidePanel()->width() == expected_width; }));
// In LTR with right-aligned sidebar: negative resize_amount increases width.
GetSidePanel()->OnResize(-20, true);
expected_width += 20;
ASSERT_TRUE(base::test::RunUntil(
[&]() { return GetSidePanel()->width() == expected_width; }));
// Enable RTL mode.
base::i18n::ScopedRTLForTesting scoped_rtl(/*rtl=*/true);
BrowserView::GetBrowserViewForBrowser(browser())->InvalidateLayout();
RunScheduledLayouts();
// In RTL with right-aligned sidebar: positive resize_amount increases width
// (sign is flipped because screen drag direction reverses in RTL).
GetSidePanel()->OnResize(20, true);
expected_width += 20;
ASSERT_TRUE(base::test::RunUntil(
[&]() { return GetSidePanel()->width() == expected_width; }));
}
// Instantiate parameterized tests for both V1 and V2
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
SidebarBrowserTestV1AndV2,
::testing::Bool(), // false = V1, true = V2
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "V2" : "V1";
});
#if BUILDFLAG(ENABLE_SIDEBAR_V2)
// In V2 the upstream side panel is a direct child of browser_view, positioned
// by CalculateSideBarLayout. Verify that when the panel is open it sits
// between the contents container and the sidebar control, NOT outside it.
// Covers both the default right-side and the explicitly set left-side layouts.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest, SidebarV2PanelPositionTest) {
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* panel = browser_view->side_panel();
panel->DisableAnimationsForTesting();
SidebarContainerView* sidebar = GetSidebarContainerView();
auto* prefs = browser()->profile()->GetPrefs();
browser()->GetFeatures().side_panel_ui()->Toggle();
RunScheduledLayouts();
ASSERT_TRUE(panel->GetVisible());
ASSERT_TRUE(sidebar->IsSidebarVisible());
// --- Sidebar on right (default LTR: kSidePanelHorizontalAlignment = true)
ASSERT_FALSE(sidebar->sidebar_on_left());
auto* contents = browser_view->contents_container();
// Panel sits immediately left of the sidebar control:
// [contents] [panel] [sidebar_control]
EXPECT_EQ(panel->bounds().right(), sidebar->bounds().x())
<< "panel=" << panel->bounds().ToString()
<< " sidebar=" << sidebar->bounds().ToString();
// Panel top must align with the contents container — the upstream layout
// offsets the panel -1px to overlap the toolbar separator; Brave removes
// that offset so the separator is fully visible.
EXPECT_EQ(panel->bounds().y(), contents->bounds().y())
<< "panel y=" << panel->bounds().y()
<< " contents y=" << contents->bounds().y();
// --- Sidebar on left (kSidePanelHorizontalAlignment = false)
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
RunScheduledLayouts();
ASSERT_TRUE(sidebar->sidebar_on_left());
// Panel sits immediately right of the sidebar control:
// [sidebar_control] [panel] [contents]
EXPECT_EQ(sidebar->bounds().right(), panel->bounds().x())
<< "sidebar=" << sidebar->bounds().ToString()
<< " panel=" << panel->bounds().ToString();
EXPECT_EQ(panel->bounds().y(), contents->bounds().y())
<< "panel y=" << panel->bounds().y()
<< " contents y=" << contents->bounds().y();
}
// Verify that the sidebar item active state in SidebarModel is updated:
// - When clicking a panel item via the sidebar UI.
// - When the side panel is opened or closed via the side panel UI directly
// (e.g. toolbar toggle button), which bypasses SidebarController.
// In V1, SidebarContainerView monitors panel show/hide events and asks
// SidebarController to update the active state. In V2,
// SidebarContainerView does not do that, so BraveSidePanelCoordinator
// handles it in Show() and Close().
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest, SidebarV2ActiveItemStateSync) {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->DisableAnimationsForTesting();
const auto bookmark_item_index =
model()->GetIndexOf(SidebarItem::BuiltInItemType::kBookmarks);
ASSERT_TRUE(bookmark_item_index.has_value());
// Initially no item is active.
EXPECT_FALSE(model()->active_index());
// Clicking a panel item via the sidebar UI activates it in the model.
SimulateSidebarItemClickAt(*bookmark_item_index);
EXPECT_EQ(model()->active_index(), bookmark_item_index);
// Deactivate by closing the panel.
panel_ui->Close();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !model()->active_index().has_value(); }));
// Opening the side panel via the panel UI (e.g. toolbar toggle button path)
// also activates the corresponding sidebar item in the model.
panel_ui->Show(SidePanelEntryId::kBookmarks);
ASSERT_TRUE(base::test::RunUntil(
[&]() { return controller()->IsActiveIndex(bookmark_item_index); }));
// Closing the side panel via the panel UI deactivates the item in the model.
panel_ui->Close();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !model()->active_index().has_value(); }));
// Toggling the panel open (as the toolbar button does) activates the
// last-used sidebar item in the model.
panel_ui->Toggle();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return controller()->IsActiveIndex(bookmark_item_index); }));
// Wait for the panel to be fully shown before toggling closed, so that
// BraveSidePanelCoordinator::Toggle() sees IsSidePanelShowing() == true
// and takes the close branch instead of the show branch.
ASSERT_TRUE(
base::test::RunUntil([&]() { return panel_ui->IsSidePanelShowing(); }));
// Toggling the panel closed deactivates the item in the model.
panel_ui->Toggle();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !model()->active_index().has_value(); }));
}
// Verify the Brave-styled side panel header is attached for reading list and
// bookmarks and absent for other entries.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest, SidebarV2BraveHeaderTest) {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* side_panel = browser_view->side_panel();
side_panel->DisableAnimationsForTesting();
// Reading list: Brave header attached.
panel_ui->Show(SidePanelEntryId::kReadingList);
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(
SidePanelEntry::Key(SidePanelEntryId::kReadingList));
}));
EXPECT_NE(nullptr, side_panel->GetHeaderView<BraveSidePanelHeader>())
<< "BraveSidePanelHeader should be attached for the reading list panel";
// Bookmarks: Brave header attached.
panel_ui->Show(SidePanelEntryId::kBookmarks);
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(
SidePanelEntry::Key(SidePanelEntryId::kBookmarks));
}));
EXPECT_NE(nullptr, side_panel->GetHeaderView<BraveSidePanelHeader>())
<< "BraveSidePanelHeader should be attached for the bookmarks panel";
// CustomizeChrome: no Brave header (the previous one must be cleared when
// switching to an entry that doesn't request a Brave header).
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(
SidePanelEntry::Key(SidePanelEntryId::kCustomizeChrome));
}));
EXPECT_EQ(nullptr, side_panel->GetHeaderView<BraveSidePanelHeader>())
<< "BraveSidePanelHeader should not be attached for CustomizeChrome";
}
namespace {
// Returns the corners that GetRoundedCorners() inside ContentParentView would
// compute for the panel's current header and pref state.
gfx::RoundedCornersF ExpectedContentCorners(SidePanel* panel,
PrefService* prefs) {
return brave::GetPanelContentsRoundedCorners(
prefs, panel->GetHeaderView<views::View>() != nullptr);
}
// Asserts layer-backed content children carry the expected corner radii.
// Children without compositor layers (the typical WebView holder path) are
// silently skipped; their corners can't be observed via public API.
void ExpectContentChildLayerCorners(SidePanel* panel,
const gfx::RoundedCornersF& expected,
const base::Location& loc = FROM_HERE) {
SCOPED_TRACE(loc.ToString());
for (const auto child : panel->GetContentParentView()->children()) {
if (child->layer()) {
EXPECT_EQ(expected, child->layer()->rounded_corner_radii());
}
}
}
} // namespace
// Verify that content corner radii stay correct across three triggers:
// (a) panel type change (header ↔ no-header entry),
// (b) rounded-corners pref toggle while the panel is open,
// (c) panel reopened after the pref changed while it was closed.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest,
SidebarV2ContentCornersUpdateOnStateChange) {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
auto* side_panel =
BrowserView::GetBrowserViewForBrowser(browser())->side_panel();
auto* prefs = browser()->profile()->GetPrefs();
side_panel->DisableAnimationsForTesting();
prefs->SetBoolean(kWebViewRoundedCorners, true);
const int r = views::LayoutProvider::Get()->GetDistanceMetric(
ChromeDistanceMetric::DISTANCE_SIDE_PANEL_CONTENT_RADIUS);
const gfx::RoundedCornersF flat_top(0, 0, r, r);
const gfx::RoundedCornersF all_round(r);
const gfx::RoundedCornersF none;
auto wait_for_entry = [&](SidePanelEntryId id,
const base::Location& loc = FROM_HERE) {
SCOPED_TRACE(loc.ToString());
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(SidePanelEntry::Key(id));
}));
};
// (a) Panel type change -----------------------------------------------
// Bookmarks has a Brave header → top corners must be flat.
panel_ui->Show(SidePanelEntryId::kBookmarks);
wait_for_entry(SidePanelEntryId::kBookmarks);
EXPECT_NE(nullptr, side_panel->GetHeaderView<views::View>());
EXPECT_EQ(flat_top, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, flat_top);
// CustomizeChrome has no Brave header → all corners must be round.
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
wait_for_entry(SidePanelEntryId::kCustomizeChrome);
EXPECT_EQ(nullptr, side_panel->GetHeaderView<views::View>());
EXPECT_EQ(all_round, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, all_round);
// Back to bookmarks → flat top again.
panel_ui->Show(SidePanelEntryId::kBookmarks);
wait_for_entry(SidePanelEntryId::kBookmarks);
EXPECT_NE(nullptr, side_panel->GetHeaderView<views::View>());
EXPECT_EQ(flat_top, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, flat_top);
// (b) Pref change while panel is open ---------------------------------
// Pref OFF: no corners regardless of header (UpdateBorder() path).
prefs->SetBoolean(kWebViewRoundedCorners, false);
EXPECT_EQ(none, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, none);
// Pref ON again: flat top (bookmarks has header).
prefs->SetBoolean(kWebViewRoundedCorners, true);
EXPECT_EQ(flat_top, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, flat_top);
// (c) Panel reopened after pref changed while closed ------------------
panel_ui->Close();
ASSERT_TRUE(
base::test::RunUntil([&]() { return !side_panel->GetVisible(); }));
// Toggle pref while the panel is hidden.
prefs->SetBoolean(kWebViewRoundedCorners, false);
// Reopen — Open() must re-apply corners with the new pref value.
panel_ui->Show(SidePanelEntryId::kBookmarks);
wait_for_entry(SidePanelEntryId::kBookmarks);
EXPECT_EQ(none, ExpectedContentCorners(side_panel, prefs));
ExpectContentChildLayerCorners(side_panel, none);
}
// Verify that the resize area is positioned correctly for both border states.
// In both cases the strip starts at the panel's outer edge with width
// kResizeStripWidth. Border state only affects z-order: without rounded
// corners the strip is reordered to the top so it wins the hit-test over
// the overlapping content edge.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest,
SidebarV2ResizeAreaPositionMatchesBorderState) {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* side_panel = browser_view->side_panel();
side_panel->DisableAnimationsForTesting();
auto* prefs = browser()->profile()->GetPrefs();
// Default: sidebar on right.
ASSERT_TRUE(prefs->GetBoolean(prefs::kSidePanelHorizontalAlignment));
panel_ui->Toggle();
ASSERT_TRUE(base::test::RunUntil([&]() { return side_panel->GetVisible(); }));
auto* resize_area = side_panel->resize_area_for_testing();
ASSERT_TRUE(resize_area);
// Verifies resize strip position, width, z-order, and panel insets for the
// given border mode. Panel is on the right in LTR, so the content-facing
// edge is left and the outer edge is right.
auto verify_state = [&](bool rounded_corners,
const base::Location& loc = FROM_HERE) {
SCOPED_TRACE(loc.ToString());
if (rounded_corners) {
EXPECT_EQ(0, side_panel->GetInsets().left())
<< "rounded: no content-side inset (content view owns its margin)";
EXPECT_EQ(kRoundedCornersContentsViewMargin,
side_panel->GetInsets().right())
<< "rounded: outer-side gap for visual separation from window chrome";
} else {
EXPECT_EQ(1, side_panel->GetInsets().left())
<< "plain: 1px separator on the content-facing side";
EXPECT_EQ(0, side_panel->GetInsets().right())
<< "plain: no outer-side inset";
}
EXPECT_EQ(resize_area->GetBoundsInScreen().origin(),
side_panel->GetBoundsInScreen().origin())
<< "resize strip should sit at the panel's outer edge";
EXPECT_EQ(resize_area->width(),
views::BraveSidePanelResizeArea::kResizeStripWidth)
<< "resize strip width should equal kResizeStripWidth";
EXPECT_EQ(side_panel->GetIndexOf(resize_area),
side_panel->children().size() - 1)
<< "resize strip should be the top-most child view";
};
prefs->SetBoolean(kWebViewRoundedCorners, true);
RunScheduledLayouts();
verify_state(true);
prefs->SetBoolean(kWebViewRoundedCorners, false);
RunScheduledLayouts();
verify_state(false);
// Switch to another panel that has brave panel header.
panel_ui->Show(SidePanelEntryId::kBookmarks);
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(
SidePanelEntry::Key(SidePanelEntryId::kBookmarks));
}));
RunScheduledLayouts();
verify_state(false);
// Switch to another panel that doesn't have brave panel header.
panel_ui->Show(SidePanelEntryId::kCustomizeChrome);
ASSERT_TRUE(base::test::RunUntil([&]() {
return panel_ui->IsSidePanelEntryShowing(
SidePanelEntry::Key(SidePanelEntryId::kCustomizeChrome));
}));
RunScheduledLayouts();
verify_state(false);
}
// Verify that the panel border insets follow the sidebar's horizontal
// alignment. Flipping kSidePanelHorizontalAlignment runs through
// BraveBrowserView::UpdateSideBarHorizontalAlignment(), which calls
// SidePanel::UpdateBorder(). The content-facing edge owns the separator/margin
// and the outer edge owns the rounded-corner gap, so left alignment is the
// mirror image of the default right alignment.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest,
SidebarV2BorderInsetsFollowAlignment) {
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* side_panel = browser_view->side_panel();
side_panel->DisableAnimationsForTesting();
auto* prefs = browser()->profile()->GetPrefs();
panel_ui->Toggle();
ASSERT_TRUE(base::test::RunUntil([&]() { return side_panel->GetVisible(); }));
// When right-aligned the content-facing edge is the left and the outer edge
// is the right; when left-aligned the two are mirrored.
auto verify_state = [&](bool right_aligned, bool rounded_corners,
const base::Location& loc = FROM_HERE) {
SCOPED_TRACE(loc.ToString());
const gfx::Insets insets = side_panel->GetInsets();
const int content_side = right_aligned ? insets.left() : insets.right();
const int outer_side = right_aligned ? insets.right() : insets.left();
if (rounded_corners) {
EXPECT_EQ(0, content_side)
<< "rounded: no content-side inset (content view owns its margin)";
EXPECT_EQ(kRoundedCornersContentsViewMargin, outer_side)
<< "rounded: outer-side gap for visual separation from window chrome";
} else {
EXPECT_EQ(1, content_side)
<< "plain: 1px separator on the content-facing side";
EXPECT_EQ(0, outer_side) << "plain: no outer-side inset";
}
};
// Default: right-aligned.
ASSERT_TRUE(side_panel->IsRightAligned());
prefs->SetBoolean(kWebViewRoundedCorners, true);
RunScheduledLayouts();
verify_state(/*right_aligned=*/true, /*rounded_corners=*/true);
prefs->SetBoolean(kWebViewRoundedCorners, false);
RunScheduledLayouts();
verify_state(/*right_aligned=*/true, /*rounded_corners=*/false);
// Flip to left-aligned while visible. This drives
// UpdateSideBarHorizontalAlignment() -> SidePanel::UpdateBorder().
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, false);
ASSERT_FALSE(side_panel->IsRightAligned());
prefs->SetBoolean(kWebViewRoundedCorners, true);
RunScheduledLayouts();
verify_state(/*right_aligned=*/false, /*rounded_corners=*/true);
prefs->SetBoolean(kWebViewRoundedCorners, false);
RunScheduledLayouts();
verify_state(/*right_aligned=*/false, /*rounded_corners=*/false);
// Flip back to right-aligned while visible to exercise the reverse
// transition.
prefs->SetBoolean(prefs::kSidePanelHorizontalAlignment, true);
ASSERT_TRUE(side_panel->IsRightAligned());
RunScheduledLayouts();
verify_state(/*right_aligned=*/true, /*rounded_corners=*/false);
}
// Regression test: the top container separator must remain visible when a side
// panel opens. Upstream suppresses it (suppress_top_separator=true) when the
// side panel is shown and GetTopSeparatorType() returns kTopContainer.
// BraveBrowserViewTabbedLayoutImpl::CalculateTopContainerLayoutImpl resets the
// flag to false for that case so the separator stays shown.
// Rounded corners must be disabled: GetTopSeparatorType() only returns
// kTopContainer (instead of kNone) when rounded corners are off.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest,
TopContainerSeparatorVisibleWhenPanelOpens) {
browser()->profile()->GetPrefs()->SetBoolean(kWebViewRoundedCorners, false);
RunScheduledLayouts();
auto* brave_view =
BraveBrowserView::From(BrowserView::GetBrowserViewForBrowser(browser()));
auto* separator = brave_view->top_container_separator_for_testing();
ASSERT_TRUE(separator);
EXPECT_TRUE(separator->GetVisible());
auto* panel_ui = browser()->GetFeatures().side_panel_ui();
panel_ui->Show(SidePanelEntryId::kBookmarks);
ASSERT_TRUE(
base::test::RunUntil([&]() { return panel_ui->IsSidePanelShowing(); }));
EXPECT_TRUE(separator->GetVisible());
}
class ScopedSidePanelUIForTesting {
public:
ScopedSidePanelUIForTesting(SidebarController* controller, SidePanelUI* ui)
: controller_(controller) {
controller_->SetSidePanelUIForTesting(ui);
}
~ScopedSidePanelUIForTesting() {
controller_->SetSidePanelUIForTesting(nullptr);
}
private:
raw_ptr<SidebarController> controller_;
};
class MockSidePanelUI : public SidePanelUI {
public:
MOCK_METHOD(void,
Show,
(SidePanelEntryId, std::optional<SidePanelOpenTrigger>, bool),
(override));
MOCK_METHOD(void,
Show,
(SidePanelEntryKey, std::optional<SidePanelOpenTrigger>, bool),
(override));
MOCK_METHOD(void, ShowFrom, (SidePanelEntryKey, gfx::Rect), (override));
MOCK_METHOD(void, Close, (SidePanelEntryHideReason, bool), (override));
MOCK_METHOD(void,
Toggle,
(SidePanelEntryKey, SidePanelOpenTrigger),
(override));
MOCK_METHOD(std::optional<SidePanelEntryId>,
GetCurrentEntryId,
(),
(const, override));
MOCK_METHOD(int, GetCurrentEntryDefaultContentWidth, (), (const, override));
MOCK_METHOD(bool, IsSidePanelShowing, (), (const, override));
MOCK_METHOD(bool,
IsSidePanelEntryShowing,
(const SidePanelEntryKey&),
(const, override));
MOCK_METHOD(bool,
IsSidePanelEntryShowing,
(const SidePanelEntry::Key&, bool),
(const, override));
MOCK_METHOD(base::CallbackListSubscription,
RegisterSidePanelShown,
(ShownCallback),
(override));
MOCK_METHOD(void,
OnActiveTabChanged,
(content::WebContents*, content::WebContents*, bool),
(override));
MOCK_METHOD(content::WebContents*,
GetWebContentsForTest,
(SidePanelEntryId),
(override));
MOCK_METHOD(void, DisableAnimationsForTesting, (), (override));
MOCK_METHOD(void, SetNoDelaysForTesting, (bool), (override));
};
// Verify suppress_animations is false when opening from a closed state and
// true when switching panels while one is already active.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest,
SidebarV2ActivatePanelItemSuppressAnimation) {
MockSidePanelUI mock_ui;
ScopedSidePanelUIForTesting scoped_ui(controller(), &mock_ui);
// No active panel: opening should animate (suppress_animations=false).
ASSERT_FALSE(model()->active_index().has_value())
<< "Expected no active panel before first ActivatePanelItem call";
EXPECT_CALL(mock_ui,
Show(testing::An<SidePanelEntryId>(), testing::Eq(std::nullopt),
/*suppress_animations=*/false));
controller()->ActivatePanelItem(SidebarItem::BuiltInItemType::kBookmarks);
testing::Mock::VerifyAndClearExpectations(&mock_ui);
controller()->UpdateActiveItemState(SidebarItem::BuiltInItemType::kBookmarks);
// Active panel present: switching panels should suppress animations.
ASSERT_TRUE(model()->active_index().has_value())
<< "Expected active panel after UpdateActiveItemState";
EXPECT_CALL(mock_ui,
Show(testing::An<SidePanelEntryId>(), testing::Eq(std::nullopt),
/*suppress_animations=*/true));
controller()->ActivatePanelItem(SidebarItem::BuiltInItemType::kReadingList);
}
// In V2, the toolbar SidePanelButton acts as a "temporal pin" for the sidebar
// control view: clicking it toggles a session-only pinned state that forces
// the sidebar visible regardless of show option. The pinned state must reset
// when the show option changes. The button is hidden under kShowAlways
// because the sidebar is already always visible.
IN_PROC_BROWSER_TEST_F(SidebarBrowserTest, SidebarV2ToolbarButtonPinning) {
auto* service = SidebarServiceFactory::GetForProfile(browser()->profile());
auto* sidebar_container = GetSidebarContainerView();
auto* button = GetSidePanelToolbarButton();
ASSERT_TRUE(button);
auto button_highlighted = [&]() {
return views::InkDrop::Get(button)->GetHighlighted();
};
// Park the mouse at the top of the toolbar — well outside the sidebar
// bounds — so that kShowOnMouseOver doesn't keep the sidebar visible due
// to an incidental hover. Without this the test is sensitive to wherever
// the OS leaves the cursor between runs.
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
ui::test::EventGenerator event_generator(
views::GetRootWindow(browser_view->GetWidget()),
browser_view->GetNativeWindow());
event_generator.MoveMouseTo(
browser_view->toolbar()->GetBoundsInScreen().CenterPoint());
ASSERT_FALSE(sidebar_container->IsMouseHovered())
<< "Mouse should not be over the sidebar at test start";
// Pin → assert visible/highlighted; unpin → assert hidden/unhighlighted.
// Precondition: pinned=false, sidebar hidden. Used once per non-kShowAlways
// show option. SCOPED_TRACE makes failures point back to the caller.
auto verify_pin_cycle = [&]() {
ASSERT_FALSE(controller()->sidebar_pinned())
<< "Precondition: pinned=false before pin";
ASSERT_FALSE(sidebar_container->IsSidebarVisible())
<< "Precondition: sidebar hidden before pin";
ASSERT_FALSE(button_highlighted())
<< "Precondition: button not highlighted before pin";
// Pin: sidebar snaps visible, button highlights (fade-in).
controller()->ToggleSidebarPinning();
EXPECT_TRUE(controller()->sidebar_pinned()) << "Pin should set pinned=true";
EXPECT_TRUE(sidebar_container->IsSidebarVisible())
<< "Pin should force sidebar visible regardless of show option";
ASSERT_TRUE(base::test::RunUntil([&]() { return button_highlighted(); }))
<< "Pin should highlight the toolbar button";
// Unpin: pinned flips immediately; button highlight + sidebar visibility
// unwind asynchronously (animations).
controller()->ToggleSidebarPinning();
EXPECT_FALSE(controller()->sidebar_pinned())
<< "Unpin should set pinned=false";
ASSERT_TRUE(base::test::RunUntil([&]() { return !button_highlighted(); }))
<< "Unpin should clear the button highlight";
ASSERT_TRUE(base::test::RunUntil([&]() {
return !sidebar_container->IsSidebarVisible();
})) << "Unpin should hide the sidebar (current show option doesn't keep "
"it visible without pin)";
};
// kShowAlways (the fixture default) → button is hidden because the sidebar
// is always visible.
ASSERT_EQ(SidebarService::ShowSidebarOption::kShowAlways,
service->GetSidebarShowOption());
EXPECT_FALSE(button->GetVisible())
<< "Toolbar button must be hidden under kShowAlways";
// Switch to kShowNever so the button becomes visible and the sidebar hides.
service->SetSidebarShowOption(SidebarService::ShowSidebarOption::kShowNever);
EXPECT_TRUE(button->GetVisible())
<< "Toolbar button must be visible under kShowNever";
ASSERT_TRUE(base::test::RunUntil([&]() {
return !sidebar_container->IsSidebarVisible();
})) << "Sidebar should hide after switching to kShowNever";
{
SCOPED_TRACE("kShowNever");
verify_pin_cycle();
}
// Pin again, then change the show option → pinned must reset to false and
// the sidebar must follow the new option (kShowOnMouseOver, mouse parked
// outside sidebar → hidden).
controller()->ToggleSidebarPinning();
ASSERT_TRUE(controller()->sidebar_pinned())
<< "Pinning again should set pinned=true (precondition for reset test)";
ASSERT_TRUE(sidebar_container->IsSidebarVisible())
<< "Sidebar should be visible while pinned (precondition)";
ASSERT_TRUE(base::test::RunUntil([&]() { return button_highlighted(); }))
<< "Button should be highlighted while pinned (precondition)";
service->SetSidebarShowOption(
SidebarService::ShowSidebarOption::kShowOnMouseOver);
EXPECT_FALSE(controller()->sidebar_pinned())
<< "Changing show option must reset pinned state to false";
EXPECT_TRUE(button->GetVisible())
<< "Toolbar button must be visible under kShowOnMouseOver";
ASSERT_TRUE(base::test::RunUntil([&]() { return !button_highlighted(); }))
<< "Button highlight should fade off after pinned reset";
ASSERT_TRUE(base::test::RunUntil([&]() {
return !sidebar_container->IsSidebarVisible();
})) << "Sidebar should hide once pinned is reset under kShowOnMouseOver "
"(mouse is not over sidebar in tests)";
{
SCOPED_TRACE("kShowOnMouseOver");
verify_pin_cycle();
}
// Switching back to kShowAlways re-hides the toolbar button.
service->SetSidebarShowOption(SidebarService::ShowSidebarOption::kShowAlways);
EXPECT_FALSE(button->GetVisible())
<< "Toolbar button must be hidden again under kShowAlways";
EXPECT_FALSE(controller()->sidebar_pinned())
<< "Pinned state must remain false after returning to kShowAlways";
}
#endif // BUILDFLAG(ENABLE_SIDEBAR_V2)
} // namespace sidebar