blob: 12cb14b36c6798f7151b7bb67841db53a30e29fb [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/table/table_view.h"
#include <stddef.h>
#include <string>
#include <utility>
#include "base/macros.h"
#include "base/numerics/ranges.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/ax_virtual_view.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/table/table_grouper.h"
#include "ui/views/controls/table/table_header.h"
#include "ui/views/controls/table/table_view_observer.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/test/focus_manager_test.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/widget/widget_utils.h"
// Put the tests in the views namespace to make it easier to declare them as
// friend classes.
namespace views {
class TableViewTestHelper {
public:
explicit TableViewTestHelper(TableView* table) : table_(table) {}
std::string GetPaintRegion(const gfx::Rect& bounds) {
TableView::PaintRegion region(table_->GetPaintRegion(bounds));
return "rows=" + base::NumberToString(region.min_row) + " " +
base::NumberToString(region.max_row) +
" cols=" + base::NumberToString(region.min_column) + " " +
base::NumberToString(region.max_column);
}
size_t visible_col_count() { return table_->visible_columns().size(); }
int GetActiveVisibleColumnIndex() {
return table_->GetActiveVisibleColumnIndex();
}
TableHeader* header() { return table_->header_; }
void SetSelectionModel(const ui::ListSelectionModel& new_selection) {
table_->SetSelectionModel(new_selection);
}
const gfx::FontList& font_list() { return table_->font_list_; }
AXVirtualView* GetVirtualAccessibilityRow(int row) {
return table_->GetVirtualAccessibilityRow(row);
}
AXVirtualView* GetVirtualAccessibilityCell(int row,
int visible_column_index) {
return table_->GetVirtualAccessibilityCell(row, visible_column_index);
}
gfx::Rect GetCellBounds(int row, int visible_column_index) const {
return table_->GetCellBounds(row, visible_column_index);
}
gfx::Rect GetActiveCellBounds() const {
return table_->GetActiveCellBounds();
}
std::vector<std::vector<gfx::Rect>> GenerateExpectedBounds() {
// Generates the expected bounds for |table_|'s rows and cells. Each vector
// represents a row. The first entry in each child vector is the bounds for
// the entire row. The following entries in that vector are the bounds for
// each individual cell contained in that row.
auto expected_bounds = std::vector<std::vector<gfx::Rect>>();
// Generate the bounds for the header row and cells.
auto header_row = std::vector<gfx::Rect>();
gfx::Rect header_row_bounds =
table_->CalculateHeaderRowAccessibilityBounds();
View::ConvertRectToScreen(table_, &header_row_bounds);
header_row.push_back(header_row_bounds);
for (size_t column_index = 0; column_index < visible_col_count();
column_index++) {
gfx::Rect header_cell_bounds =
table_->CalculateHeaderCellAccessibilityBounds(column_index);
View::ConvertRectToScreen(table_, &header_cell_bounds);
header_row.push_back(header_cell_bounds);
}
expected_bounds.push_back(header_row);
// Generate the bounds for the table rows and cells.
for (int row_index = 0; row_index < table_->GetRowCount(); row_index++) {
auto table_row = std::vector<gfx::Rect>();
gfx::Rect table_row_bounds =
table_->CalculateTableRowAccessibilityBounds(row_index);
View::ConvertRectToScreen(table_, &table_row_bounds);
table_row.push_back(table_row_bounds);
for (size_t column_index = 0; column_index < visible_col_count();
column_index++) {
gfx::Rect table_cell_bounds =
table_->CalculateTableCellAccessibilityBounds(row_index,
column_index);
View::ConvertRectToScreen(table_, &table_cell_bounds);
table_row.push_back(table_cell_bounds);
}
expected_bounds.push_back(table_row);
}
return expected_bounds;
}
private:
TableView* table_;
DISALLOW_COPY_AND_ASSIGN(TableViewTestHelper);
};
namespace {
#if defined(OS_APPLE)
constexpr int kCtrlOrCmdMask = ui::EF_COMMAND_DOWN;
#else
constexpr int kCtrlOrCmdMask = ui::EF_CONTROL_DOWN;
#endif
// TestTableModel2 -------------------------------------------------------------
// Trivial TableModel implementation that is backed by a vector of vectors.
// Provides methods for adding/removing/changing the contents that notify the
// observer appropriately.
//
// Initial contents are:
// 0, 1
// 1, 1
// 2, 2
// 3, 0
class TestTableModel2 : public ui::TableModel {
public:
TestTableModel2();
// Adds a new row at index |row| with values |c1_value| and |c2_value|.
void AddRow(int row, int c1_value, int c2_value);
// Adds new rows starting from |row| to |row| + |length| with the value
// of |row| times the |value_multiplier|. The |value_multiplier| can be used
// to distinguish these rows from the rest.
void AddRows(int row, int length, int value_multiplier);
// Removes the row at index |row|.
void RemoveRow(int row);
// Removes all the rows starting from |row| to |row| + |length|.
void RemoveRows(int row, int length);
// Changes the values of the row at |row|.
void ChangeRow(int row, int c1_value, int c2_value);
// Reorders rows in the model.
void MoveRows(int row_from, int length, int row_to);
// Allows overriding the tooltip for testing.
void SetTooltip(const std::u16string& tooltip);
// ui::TableModel:
int RowCount() override;
std::u16string GetText(int row, int column_id) override;
std::u16string GetTooltip(int row) override;
void SetObserver(ui::TableModelObserver* observer) override;
int CompareValues(int row1, int row2, int column_id) override;
private:
ui::TableModelObserver* observer_ = nullptr;
base::Optional<std::u16string> tooltip_;
// The data.
std::vector<std::vector<int>> rows_;
DISALLOW_COPY_AND_ASSIGN(TestTableModel2);
};
TestTableModel2::TestTableModel2() {
AddRow(0, 0, 1);
AddRow(1, 1, 1);
AddRow(2, 2, 2);
AddRow(3, 3, 0);
}
void TestTableModel2::AddRow(int row, int c1_value, int c2_value) {
DCHECK(row >= 0 && row <= static_cast<int>(rows_.size()));
std::vector<int> new_row;
new_row.push_back(c1_value);
new_row.push_back(c2_value);
rows_.insert(rows_.begin() + row, new_row);
if (observer_)
observer_->OnItemsAdded(row, 1);
}
void TestTableModel2::AddRows(int row, int length, int value_multiplier) {
DCHECK(row >= 0 && length >= 0);
// Do not DCHECK here since we are testing the OnItemsAdded callback.
if (row >= 0 && row <= static_cast<int>(rows_.size())) {
for (int i = row; i < row + length; i++) {
std::vector<int> new_row;
new_row.push_back(i + value_multiplier);
new_row.push_back(i + value_multiplier);
rows_.insert(rows_.begin() + i, new_row);
}
}
if (observer_ && length > 0)
observer_->OnItemsAdded(row, length);
}
void TestTableModel2::RemoveRow(int row) {
DCHECK(row >= 0 && row < static_cast<int>(rows_.size()));
rows_.erase(rows_.begin() + row);
if (observer_)
observer_->OnItemsRemoved(row, 1);
}
void TestTableModel2::RemoveRows(int row, int length) {
DCHECK(row >= 0 && length >= 0);
if (row >= 0 && row <= static_cast<int>(rows_.size())) {
rows_.erase(
rows_.begin() + row,
rows_.begin() + base::ClampToRange(row + length, 0,
static_cast<int>(rows_.size())));
}
if (observer_ && length > 0)
observer_->OnItemsRemoved(row, length);
}
void TestTableModel2::ChangeRow(int row, int c1_value, int c2_value) {
DCHECK(row >= 0 && row < static_cast<int>(rows_.size()));
rows_[row][0] = c1_value;
rows_[row][1] = c2_value;
if (observer_)
observer_->OnItemsChanged(row, 1);
}
void TestTableModel2::MoveRows(int row_from, int length, int row_to) {
DCHECK_GT(length, 0);
DCHECK_GE(row_from, 0);
DCHECK_LE(row_from + length, static_cast<int>(rows_.size()));
DCHECK_GE(row_to, 0);
DCHECK_LE(row_to + length, static_cast<int>(rows_.size()));
auto old_start = rows_.begin() + row_from;
std::vector<std::vector<int>> temp(old_start, old_start + length);
rows_.erase(old_start, old_start + length);
rows_.insert(rows_.begin() + row_to, temp.begin(), temp.end());
if (observer_)
observer_->OnItemsMoved(row_from, length, row_to);
}
void TestTableModel2::SetTooltip(const std::u16string& tooltip) {
tooltip_ = tooltip;
}
int TestTableModel2::RowCount() {
return static_cast<int>(rows_.size());
}
std::u16string TestTableModel2::GetText(int row, int column_id) {
return base::NumberToString16(rows_[row][column_id]);
}
std::u16string TestTableModel2::GetTooltip(int row) {
return tooltip_ ? *tooltip_ : u"Tooltip" + base::NumberToString16(row);
}
void TestTableModel2::SetObserver(ui::TableModelObserver* observer) {
observer_ = observer;
}
int TestTableModel2::CompareValues(int row1, int row2, int column_id) {
return rows_[row1][column_id] - rows_[row2][column_id];
}
// Returns the view to model mapping as a string.
std::string GetViewToModelAsString(TableView* table) {
std::string result;
for (int i = 0; i < table->GetRowCount(); ++i) {
if (i != 0)
result += " ";
result += base::NumberToString(table->ViewToModel(i));
}
return result;
}
// Returns the model to view mapping as a string.
std::string GetModelToViewAsString(TableView* table) {
std::string result;
for (int i = 0; i < table->GetRowCount(); ++i) {
if (i != 0)
result += " ";
result += base::NumberToString(table->ModelToView(i));
}
return result;
}
// Formats the whole table as a string, like: "[a, b, c], [d, e, f]". Rows
// scrolled out of view are included; hidden columns are excluded.
std::string GetRowsInViewOrderAsString(TableView* table) {
std::string result;
for (int i = 0; i < table->GetRowCount(); ++i) {
if (i != 0)
result += ", "; // Comma between each row.
// Format row |i| like this: "[value1, value2, value3]"
result += "[";
for (size_t j = 0; j < table->visible_columns().size(); ++j) {
const ui::TableColumn& column = table->GetVisibleColumn(j).column;
if (j != 0)
result += ", "; // Comma between each value in the row.
result += base::UTF16ToUTF8(
table->model()->GetText(table->ViewToModel(i), column.id));
}
result += "]";
}
return result;
}
// Formats the whole accessibility views as a string.
// Like: "[a, b, c], [d, e, f]".
std::string GetRowsInVirtualViewAsString(TableView* table) {
auto& virtual_children = table->GetViewAccessibility().virtual_children();
std::string result;
for (size_t row_index = 0; row_index < virtual_children.size(); row_index++) {
if (row_index != 0)
result += ", "; // Comma between each row.
const auto& row = virtual_children[row_index];
result += "[";
for (size_t cell_index = 0; cell_index < row->children().size();
cell_index++) {
if (cell_index != 0)
result += ", "; // Comma between each value in the row.
const auto& cell = row->children()[cell_index];
const ui::AXNodeData& cell_data = cell->GetData();
result += cell_data.GetStringAttribute(ax::mojom::StringAttribute::kName);
}
result += "]";
}
return result;
}
std::string GetHeaderRowAsString(TableView* table) {
std::string result = "[";
for (size_t col_index = 0; col_index < table->visible_columns().size();
++col_index) {
if (col_index != 0)
result += ", "; // Comma between each column.
result +=
base::UTF16ToUTF8(table->GetVisibleColumn(col_index).column.title);
}
result += "]";
return result;
}
bool PressLeftMouseAt(views::View* target, const gfx::Point& point) {
const ui::MouseEvent pressed(ui::ET_MOUSE_PRESSED, point, point,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
ui::EF_LEFT_MOUSE_BUTTON);
return target->OnMousePressed(pressed);
}
void ReleaseLeftMouseAt(views::View* target, const gfx::Point& point) {
const ui::MouseEvent release(ui::ET_MOUSE_RELEASED, point, point,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
ui::EF_LEFT_MOUSE_BUTTON);
target->OnMouseReleased(release);
}
bool DragLeftMouseTo(views::View* target, const gfx::Point& point) {
const ui::MouseEvent dragged(ui::ET_MOUSE_DRAGGED, point, point,
ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
0);
return target->OnMouseDragged(dragged);
}
} // namespace
// The test parameter is used to control whether or not to test the TableView
// using the default construction path.
class TableViewTest : public ViewsTestBase,
public ::testing::WithParamInterface<bool> {
public:
TableViewTest() = default;
void SetUp() override {
ViewsTestBase::SetUp();
model_ = std::make_unique<TestTableModel2>();
std::vector<ui::TableColumn> columns(2);
columns[0].title = u"Title Column 0";
columns[0].sortable = true;
columns[1].title = u"Title Column 1";
columns[1].id = 1;
columns[1].sortable = true;
std::unique_ptr<TableView> table;
// Run the tests using both default and non-default TableView construction.
if (GetParam()) {
table = std::make_unique<TableView>();
table->Init(model_.get(), columns, TEXT_ONLY, false);
} else {
table =
std::make_unique<TableView>(model_.get(), columns, TEXT_ONLY, false);
}
table_ = table.get();
auto scroll_view = TableView::CreateScrollViewWithTable(std::move(table));
scroll_view->SetBounds(0, 0, 10000, 10000);
scroll_view->Layout();
helper_ = std::make_unique<TableViewTestHelper>(table_);
widget_ = std::make_unique<Widget>();
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.bounds = gfx::Rect(0, 0, 650, 650);
params.delegate = GetWidgetDelegate(widget_.get());
widget_->Init(std::move(params));
widget_->GetContentsView()->AddChildView(std::move(scroll_view));
widget_->Show();
}
void TearDown() override {
widget_.reset();
ViewsTestBase::TearDown();
}
void ClickOnRow(int row, int flags) {
ui::test::EventGenerator generator(GetRootWindow(widget_.get()));
generator.set_assume_window_at_origin(false);
generator.set_flags(flags);
generator.set_current_screen_location(GetPointForRow(row));
generator.PressLeftButton();
}
void TapOnRow(int row) {
ui::test::EventGenerator generator(GetRootWindow(widget_.get()));
generator.GestureTapAt(GetPointForRow(row));
}
// Returns the state of the selection model as a string. The format is:
// 'active=X anchor=X selection=X X X...'.
std::string SelectionStateAsString() const {
const ui::ListSelectionModel& model(table_->selection_model());
std::string result = "active=" + base::NumberToString(model.active()) +
" anchor=" + base::NumberToString(model.anchor()) +
" selection=";
const ui::ListSelectionModel::SelectedIndices& selection(
model.selected_indices());
bool first = true;
for (int index : selection) {
if (first) {
first = false;
} else {
result += " ";
}
result += base::NumberToString(index);
}
return result;
}
void PressKey(ui::KeyboardCode code) { PressKey(code, ui::EF_NONE); }
void PressKey(ui::KeyboardCode code, int flags) {
ui::test::EventGenerator generator(GetRootWindow(widget_.get()));
generator.PressKey(code, flags);
}
void VerifyTableViewAndAXOrder(std::string expected_view_order) {
VerifyAXRowIndexes();
// The table views should match the expected view order.
EXPECT_EQ(expected_view_order, GetRowsInViewOrderAsString(table_));
// Update the expected view order to have header information if exists.
if (helper_->header()) {
expected_view_order =
GetHeaderRowAsString(table_) + ", " + expected_view_order;
}
EXPECT_EQ(expected_view_order, GetRowsInVirtualViewAsString(table_));
}
// Verifies that there is an unique, properly-indexed virtual row for every
// row.
void VerifyAXRowIndexes() {
auto& virtual_children = table_->GetViewAccessibility().virtual_children();
// Makes sure the virtual row count factors in the presence of the header.
const int first_row_index = helper_->header() ? 1 : 0;
const int virtual_row_count = table_->GetRowCount() + first_row_index;
EXPECT_EQ(virtual_row_count, int{virtual_children.size()});
// Make sure every virtual row is valid.
for (int index = first_row_index; index < virtual_row_count; index++) {
const auto& row = virtual_children[index];
ASSERT_TRUE(row);
// Normalize the row index to account for the presence of a header if
// necessary.
const int normalized_index = index - first_row_index;
// Make sure the stored row index matches the row index in the table.
const ui::AXNodeData& row_data = row->GetCustomData();
const int stored_index =
row_data.GetIntAttribute(ax::mojom::IntAttribute::kTableRowIndex);
EXPECT_EQ(stored_index, normalized_index);
}
}
// Helper function for comparing the bounds of |table_|'s virtual
// accessibility child rows and cells with a set of expected bounds.
void VerifyTableAccChildrenBounds(
const ViewAccessibility& view_accessibility,
const std::vector<std::vector<gfx::Rect>>& expected_bounds) {
auto& virtual_children = view_accessibility.virtual_children();
EXPECT_EQ(virtual_children.size(), expected_bounds.size());
EXPECT_EQ((size_t)(table_->GetRowCount()) + 1U, expected_bounds.size());
for (size_t row_index = 0; row_index < virtual_children.size();
row_index++) {
const auto& row = virtual_children[row_index];
ASSERT_TRUE(row);
const ui::AXNodeData& row_data = row->GetData();
EXPECT_EQ(ax::mojom::Role::kRow, row_data.role);
ui::AXOffscreenResult offscreen_result = ui::AXOffscreenResult();
gfx::Rect row_custom_bounds = row->GetBoundsRect(
ui::AXCoordinateSystem::kScreenDIPs,
ui::AXClippingBehavior::kUnclipped, &offscreen_result);
EXPECT_EQ(row_custom_bounds, expected_bounds[row_index][0]);
EXPECT_EQ(row->children().size(), expected_bounds[row_index].size() - 1U);
EXPECT_EQ(row->children().size(), helper_->visible_col_count());
for (size_t cell_index = 0; cell_index < row->children().size();
cell_index++) {
const auto& cell = row->children()[cell_index];
ASSERT_TRUE(cell);
const ui::AXNodeData& cell_data = cell->GetData();
if (row_index == 0)
EXPECT_EQ(ax::mojom::Role::kColumnHeader, cell_data.role);
else
EXPECT_EQ(ax::mojom::Role::kCell, cell_data.role);
// Add 1 to get the cell's index into |expected_bounds| since the first
// entry is the row's bounds.
const int expected_bounds_index = cell_index + 1;
gfx::Rect cell_custom_bounds = cell->GetBoundsRect(
ui::AXCoordinateSystem::kScreenDIPs,
ui::AXClippingBehavior::kUnclipped, &offscreen_result);
EXPECT_EQ(cell_custom_bounds,
expected_bounds[row_index][expected_bounds_index]);
}
}
}
protected:
virtual WidgetDelegate* GetWidgetDelegate(Widget* widget) { return nullptr; }
std::unique_ptr<TestTableModel2> model_;
// Owned by |parent_|.
TableView* table_ = nullptr;
std::unique_ptr<TableViewTestHelper> helper_;
std::unique_ptr<Widget> widget_;
private:
gfx::Point GetPointForRow(int row) {
const int y = (row + 0.5) * table_->GetRowHeight();
return table_->GetBoundsInScreen().origin() + gfx::Vector2d(5, y);
}
DISALLOW_COPY_AND_ASSIGN(TableViewTest);
};
INSTANTIATE_TEST_SUITE_P(All, TableViewTest, testing::Values(false, true));
// Verifies GetPaintRegion.
TEST_P(TableViewTest, GetPaintRegion) {
// Two columns should be visible.
EXPECT_EQ(2u, helper_->visible_col_count());
EXPECT_EQ("rows=0 4 cols=0 2", helper_->GetPaintRegion(table_->bounds()));
EXPECT_EQ("rows=0 4 cols=0 1",
helper_->GetPaintRegion(gfx::Rect(0, 0, 1, table_->height())));
}
TEST_P(TableViewTest, RebuildVirtualAccessibilityChildren) {
const ViewAccessibility& view_accessibility = table_->GetViewAccessibility();
ui::AXNodeData data;
view_accessibility.GetAccessibleNodeData(&data);
EXPECT_EQ(ax::mojom::Role::kListGrid, data.role);
EXPECT_TRUE(data.HasState(ax::mojom::State::kFocusable));
EXPECT_EQ(ax::mojom::Restriction::kReadOnly, data.GetRestriction());
EXPECT_EQ(table_->GetRowCount(),
static_cast<int>(
data.GetIntAttribute(ax::mojom::IntAttribute::kTableRowCount)));
EXPECT_EQ(helper_->visible_col_count(),
static_cast<size_t>(data.GetIntAttribute(
ax::mojom::IntAttribute::kTableColumnCount)));
// The header takes up another row.
ASSERT_EQ(size_t{table_->GetRowCount() + 1},
view_accessibility.virtual_children().size());
const auto& header = view_accessibility.virtual_children().front();
ASSERT_TRUE(header);
EXPECT_EQ(ax::mojom::Role::kRow, header->GetData().role);
ASSERT_EQ(helper_->visible_col_count(), header->children().size());
int j = 0;
for (const auto& header_cell : header->children()) {
ASSERT_TRUE(header_cell);
const ui::AXNodeData& header_cell_data = header_cell->GetData();
EXPECT_EQ(ax::mojom::Role::kColumnHeader, header_cell_data.role);
EXPECT_EQ(j++, header_cell_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableCellColumnIndex));
}
int i = 0;
for (auto child_iter = view_accessibility.virtual_children().begin() + 1;
i < table_->GetRowCount(); ++child_iter, ++i) {
const auto& row = *child_iter;
ASSERT_TRUE(row);
const ui::AXNodeData& row_data = row->GetData();
EXPECT_EQ(ax::mojom::Role::kRow, row_data.role);
EXPECT_EQ(
i, row_data.GetIntAttribute(ax::mojom::IntAttribute::kTableRowIndex));
ASSERT_FALSE(row_data.HasState(ax::mojom::State::kInvisible));
ASSERT_EQ(helper_->visible_col_count(), row->children().size());
j = 0;
for (const auto& cell : row->children()) {
ASSERT_TRUE(cell);
const ui::AXNodeData& cell_data = cell->GetData();
EXPECT_EQ(ax::mojom::Role::kCell, cell_data.role);
EXPECT_EQ(i, cell_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableCellRowIndex));
EXPECT_EQ(j++, cell_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableCellColumnIndex));
ASSERT_FALSE(cell_data.HasState(ax::mojom::State::kInvisible));
}
}
}
// Verifies the bounding rect of each virtual accessibility child of the
// TableView (rows and cells) is updated appropriately as the table changes. For
// example, verifies that if a column is resized or hidden, the bounds are
// updated.
TEST_P(TableViewTest, UpdateVirtualAccessibilityChildrenBounds) {
// Verify the bounds are updated correctly when the TableView and its widget
// have been shown. Initially some widths would be 0 until the TableView's
// bounds are fully set up, so make sure the virtual children bounds have been
// updated and now match the expected bounds.
auto expected_bounds = helper_->GenerateExpectedBounds();
VerifyTableAccChildrenBounds(table_->GetViewAccessibility(), expected_bounds);
}
TEST_P(TableViewTest, UpdateVirtualAccessibilityChildrenBoundsWithResize) {
// Resize the first column 10 pixels smaller and check the bounds are updated.
int x = table_->GetVisibleColumn(0).width;
PressLeftMouseAt(helper_->header(), gfx::Point(x, 0));
DragLeftMouseTo(helper_->header(), gfx::Point(x - 10, 0));
auto expected_bounds_after_resize = helper_->GenerateExpectedBounds();
VerifyTableAccChildrenBounds(table_->GetViewAccessibility(),
expected_bounds_after_resize);
}
TEST_P(TableViewTest, UpdateVirtualAccessibilityChildrenBoundsHideColumn) {
// Hide 1 column and check the bounds are updated.
table_->SetColumnVisibility(1, false);
auto expected_bounds_after_hiding = helper_->GenerateExpectedBounds();
VerifyTableAccChildrenBounds(table_->GetViewAccessibility(),
expected_bounds_after_hiding);
}
TEST_P(TableViewTest, GetVirtualAccessibilityRow) {
for (int i = 0; i < table_->GetRowCount(); ++i) {
const AXVirtualView* row = helper_->GetVirtualAccessibilityRow(i);
ASSERT_TRUE(row);
const ui::AXNodeData& row_data = row->GetData();
EXPECT_EQ(ax::mojom::Role::kRow, row_data.role);
EXPECT_EQ(i, static_cast<int>(row_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableRowIndex)));
}
}
TEST_P(TableViewTest, GetVirtualAccessibilityCell) {
for (int i = 0; i < table_->GetRowCount(); ++i) {
for (int j = 0; j < static_cast<int>(helper_->visible_col_count()); ++j) {
const AXVirtualView* cell = helper_->GetVirtualAccessibilityCell(i, j);
ASSERT_TRUE(cell);
const ui::AXNodeData& cell_data = cell->GetData();
EXPECT_EQ(ax::mojom::Role::kCell, cell_data.role);
EXPECT_EQ(i, static_cast<int>(cell_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableCellRowIndex)));
EXPECT_EQ(j, static_cast<int>(cell_data.GetIntAttribute(
ax::mojom::IntAttribute::kTableCellColumnIndex)));
}
}
}
// Verifies SetColumnVisibility().
TEST_P(TableViewTest, ColumnVisibility) {
// Two columns should be visible.
EXPECT_EQ(2u, helper_->visible_col_count());
// Should do nothing (column already visible).
table_->SetColumnVisibility(0, true);
EXPECT_EQ(2u, helper_->visible_col_count());
// Hide the first column.
table_->SetColumnVisibility(0, false);
ASSERT_EQ(1u, helper_->visible_col_count());
EXPECT_EQ(1, table_->GetVisibleColumn(0).column.id);
EXPECT_EQ("rows=0 4 cols=0 1", helper_->GetPaintRegion(table_->bounds()));
// Hide the second column.
table_->SetColumnVisibility(1, false);
EXPECT_EQ(0u, helper_->visible_col_count());
// Show the second column.
table_->SetColumnVisibility(1, true);
ASSERT_EQ(1u, helper_->visible_col_count());
EXPECT_EQ(1, table_->GetVisibleColumn(0).column.id);
EXPECT_EQ("rows=0 4 cols=0 1", helper_->GetPaintRegion(table_->bounds()));
// Show the first column.
table_->SetColumnVisibility(0, true);
ASSERT_EQ(2u, helper_->visible_col_count());
EXPECT_EQ(1, table_->GetVisibleColumn(0).column.id);
EXPECT_EQ(0, table_->GetVisibleColumn(1).column.id);
EXPECT_EQ("rows=0 4 cols=0 2", helper_->GetPaintRegion(table_->bounds()));
}
// Verifies resizing a column using the mouse works.
TEST_P(TableViewTest, Resize) {
const int x = table_->GetVisibleColumn(0).width;
EXPECT_NE(0, x);
// Drag the mouse 1 pixel to the left.
PressLeftMouseAt(helper_->header(), gfx::Point(x, 0));
DragLeftMouseTo(helper_->header(), gfx::Point(x - 1, 0));
// This should shrink the first column and pull the second column in.
EXPECT_EQ(x - 1, table_->GetVisibleColumn(0).width);
EXPECT_EQ(x - 1, table_->GetVisibleColumn(1).x);
}
// Verifies resizing a column works with a gesture.
TEST_P(TableViewTest, ResizeViaGesture) {
const int x = table_->GetVisibleColumn(0).width;
EXPECT_NE(0, x);
// Drag the mouse 1 pixel to the left.
ui::GestureEvent scroll_begin(
x, 0, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
helper_->header()->OnGestureEvent(&scroll_begin);
ui::GestureEvent scroll_update(
x - 1, 0, 0, base::TimeTicks(),
ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE));
helper_->header()->OnGestureEvent(&scroll_update);
// This should shrink the first column and pull the second column in.
EXPECT_EQ(x - 1, table_->GetVisibleColumn(0).width);
EXPECT_EQ(x - 1, table_->GetVisibleColumn(1).x);
}
// Verifies resizing a column works with the keyboard.
// The resize keyboard amount is 5 pixels.
TEST_P(TableViewTest, ResizeViaKeyboard) {
if (!PlatformStyle::kTableViewSupportsKeyboardNavigationByCell)
return;
table_->RequestFocus();
const int x = table_->GetVisibleColumn(0).width;
EXPECT_NE(0, x);
// Table starts off with no visible column being active.
ASSERT_EQ(-1, helper_->GetActiveVisibleColumnIndex());
ui::ListSelectionModel new_selection;
new_selection.SetSelectedIndex(1);
helper_->SetSelectionModel(new_selection);
ASSERT_EQ(0, helper_->GetActiveVisibleColumnIndex());
PressKey(ui::VKEY_LEFT, ui::EF_CONTROL_DOWN);
// This should shrink the first column and pull the second column in.
EXPECT_EQ(x - 5, table_->GetVisibleColumn(0).width);
EXPECT_EQ(x - 5, table_->GetVisibleColumn(1).x);
PressKey(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
// This should restore the columns to their original sizes.
EXPECT_EQ(x, table_->GetVisibleColumn(0).width);
EXPECT_EQ(x, table_->GetVisibleColumn(1).x);
PressKey(ui::VKEY_RIGHT, ui::EF_CONTROL_DOWN);
// This should expand the first column and push the second column out.
EXPECT_EQ(x + 5, table_->GetVisibleColumn(0).width);
EXPECT_EQ(x + 5, table_->GetVisibleColumn(1).x);
}
// Verifies resizing a column won't reduce the column width below the width of
// the column's title text.
TEST_P(TableViewTest, ResizeHonorsMinimum) {
TableViewTestHelper helper(table_);
const int x = table_->GetVisibleColumn(0).width;
EXPECT_NE(0, x);
PressLeftMouseAt(helper_->header(), gfx::Point(x, 0));
DragLeftMouseTo(helper_->header(), gfx::Point(20, 0));
int title_width = gfx::GetStringWidth(
table_->GetVisibleColumn(0).column.title, helper.font_list());
EXPECT_LT(title_width, table_->GetVisibleColumn(0).width);
int old_width = table_->GetVisibleColumn(0).width;
DragLeftMouseTo(helper_->header(), gfx::Point(old_width + 10, 0));
EXPECT_EQ(old_width + 10, table_->GetVisibleColumn(0).width);
}
// Assertions for table sorting.
TEST_P(TableViewTest, Sort) {
// Initial ordering.
EXPECT_TRUE(table_->sort_descriptors().empty());
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[0, 1], [1, 1], [2, 2], [3, 0]");
// Toggle the sort order of the first column, shouldn't change anything.
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[0, 1], [1, 1], [2, 2], [3, 0]");
// Toggle the sort (first column descending).
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("3 2 1 0", GetViewToModelAsString(table_));
EXPECT_EQ("3 2 1 0", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[3, 0], [2, 2], [1, 1], [0, 1]");
// Change the [3, 0] cell to [-1, 0]. This should move it to the back of
// the current sort order.
model_->ChangeRow(3, -1, 0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("2 1 0 3", GetViewToModelAsString(table_));
EXPECT_EQ("2 1 0 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[2, 2], [1, 1], [0, 1], [-1, 0]");
// Toggle the sort again, to clear the sort and restore the model ordering.
table_->ToggleSortOrder(0);
EXPECT_TRUE(table_->sort_descriptors().empty());
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[0, 1], [1, 1], [2, 2], [-1, 0]");
// Toggle the sort again (first column ascending).
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("3 0 1 2", GetViewToModelAsString(table_));
EXPECT_EQ("1 2 3 0", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[-1, 0], [0, 1], [1, 1], [2, 2]");
// Add two rows that's second in the model order, but last in the active sort
// order.
model_->AddRows(1, 2, 10 /* Multiplier */);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("5 0 3 4 1 2", GetViewToModelAsString(table_));
EXPECT_EQ("1 4 5 2 3 0", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder(
"[-1, 0], [0, 1], [1, 1], [2, 2], [11, 11], [12, 12]");
// Add a row that's last in the model order but second in the the active sort
// order.
model_->AddRow(5, -1, 20);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("5 6 0 3 4 1 2", GetViewToModelAsString(table_));
EXPECT_EQ("2 5 6 3 4 0 1", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder(
"[-1, 20], [-1, 0], [0, 1], [1, 1], [2, 2], [11, 11], [12, 12]");
// Click the first column again, then click the second column. This should
// yield an ordering of second column ascending, with the first column
// descending as a tiebreaker.
table_->ToggleSortOrder(0);
table_->ToggleSortOrder(1);
ASSERT_EQ(2u, table_->sort_descriptors().size());
EXPECT_EQ(1, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ(0, table_->sort_descriptors()[1].column_id);
EXPECT_FALSE(table_->sort_descriptors()[1].ascending);
EXPECT_EQ("6 3 0 4 1 2 5", GetViewToModelAsString(table_));
EXPECT_EQ("2 4 5 1 3 6 0", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder(
"[-1, 0], [1, 1], [0, 1], [2, 2], [11, 11], [12, 12], [-1, 20]");
// Toggle the current column to change from ascending to descending. This
// should result in an almost-reversal of the previous order, except for the
// two rows with the same value for the second column.
table_->ToggleSortOrder(1);
ASSERT_EQ(2u, table_->sort_descriptors().size());
EXPECT_EQ(1, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ(0, table_->sort_descriptors()[1].column_id);
EXPECT_FALSE(table_->sort_descriptors()[1].ascending);
EXPECT_EQ("5 2 1 4 3 0 6", GetViewToModelAsString(table_));
EXPECT_EQ("5 2 1 4 3 0 6", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder(
"[-1, 20], [12, 12], [11, 11], [2, 2], [1, 1], [0, 1], [-1, 0]");
// Delete the [0, 1] row from the model. It's at model index zero.
model_->RemoveRow(0);
ASSERT_EQ(2u, table_->sort_descriptors().size());
EXPECT_EQ(1, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ(0, table_->sort_descriptors()[1].column_id);
EXPECT_FALSE(table_->sort_descriptors()[1].ascending);
EXPECT_EQ("4 1 0 3 2 5", GetViewToModelAsString(table_));
EXPECT_EQ("2 1 4 3 0 5", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder(
"[-1, 20], [12, 12], [11, 11], [2, 2], [1, 1], [-1, 0]");
// Delete [-1, 20] and [10, 11] from the model.
model_->RemoveRows(1, 2);
ASSERT_EQ(2u, table_->sort_descriptors().size());
EXPECT_EQ(1, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ(0, table_->sort_descriptors()[1].column_id);
EXPECT_FALSE(table_->sort_descriptors()[1].ascending);
EXPECT_EQ("2 0 1 3", GetViewToModelAsString(table_));
EXPECT_EQ("1 2 0 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[-1, 20], [11, 11], [2, 2], [-1, 0]");
// Toggle the current sort column again. This should clear both the primary
// and secondary sort descriptor.
table_->ToggleSortOrder(1);
EXPECT_TRUE(table_->sort_descriptors().empty());
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
VerifyTableViewAndAXOrder("[11, 11], [2, 2], [-1, 20], [-1, 0]");
}
// Verifies clicking on the header sorts.
TEST_P(TableViewTest, SortOnMouse) {
EXPECT_TRUE(table_->sort_descriptors().empty());
const int x = table_->GetVisibleColumn(0).width / 2;
EXPECT_NE(0, x);
// Press and release the mouse.
// The header must return true, else it won't normally get the release.
EXPECT_TRUE(PressLeftMouseAt(helper_->header(), gfx::Point(x, 0)));
ReleaseLeftMouseAt(helper_->header(), gfx::Point(x, 0));
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
}
// Verifies that pressing the space bar when a particular visible column is
// active will sort by that column.
TEST_P(TableViewTest, SortOnSpaceBar) {
if (!PlatformStyle::kTableViewSupportsKeyboardNavigationByCell)
return;
table_->RequestFocus();
ASSERT_TRUE(table_->sort_descriptors().empty());
// Table starts off with no visible column being active.
ASSERT_EQ(-1, helper_->GetActiveVisibleColumnIndex());
ui::ListSelectionModel new_selection;
new_selection.SetSelectedIndex(1);
helper_->SetSelectionModel(new_selection);
ASSERT_EQ(0, helper_->GetActiveVisibleColumnIndex());
PressKey(ui::VKEY_SPACE);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
PressKey(ui::VKEY_SPACE);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
PressKey(ui::VKEY_RIGHT);
ASSERT_EQ(1, helper_->GetActiveVisibleColumnIndex());
PressKey(ui::VKEY_SPACE);
ASSERT_EQ(2u, table_->sort_descriptors().size());
EXPECT_EQ(1, table_->sort_descriptors()[0].column_id);
EXPECT_EQ(0, table_->sort_descriptors()[1].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_FALSE(table_->sort_descriptors()[1].ascending);
}
TEST_P(TableViewTest, ActiveCellBoundsFollowColumnSorting) {
table_->RequestFocus();
ASSERT_TRUE(table_->sort_descriptors().empty());
ui::ListSelectionModel new_selection;
new_selection.SetSelectedIndex(1);
helper_->SetSelectionModel(new_selection);
// Toggle the sort order of the first column. Shouldn't change the order.
table_->ToggleSortOrder(0);
ClickOnRow(0, 0);
EXPECT_EQ(helper_->GetCellBounds(0, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(0, table_->ViewToModel(0));
ClickOnRow(1, 0);
EXPECT_EQ(helper_->GetCellBounds(1, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(1, table_->ViewToModel(1));
ClickOnRow(2, 0);
EXPECT_EQ(helper_->GetCellBounds(2, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(2, table_->ViewToModel(2));
// Toggle the sort order of the second column. The active row will stay in
// sync with the view index, meanwhile the model's change which shows that
// the list order has changed.
table_->ToggleSortOrder(1);
ClickOnRow(0, 0);
EXPECT_EQ(helper_->GetCellBounds(0, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(3, table_->ViewToModel(0));
ClickOnRow(1, 0);
EXPECT_EQ(helper_->GetCellBounds(1, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(0, table_->ViewToModel(1));
ClickOnRow(2, 0);
EXPECT_EQ(helper_->GetCellBounds(2, 0), helper_->GetActiveCellBounds());
EXPECT_EQ(1, table_->ViewToModel(2));
// Verifying invalid active indexes return an empty rect.
new_selection.Clear();
helper_->SetSelectionModel(new_selection);
EXPECT_EQ(gfx::Rect(), helper_->GetActiveCellBounds());
}
TEST_P(TableViewTest, Tooltip) {
// Column 0 uses the TableModel's GetTooltipText override for tooltips.
table_->SetVisibleColumnWidth(0, 10);
auto local_point_for_row = [&](int row) {
return gfx::Point(5, (row + 0.5) * table_->GetRowHeight());
};
auto expected = [](int row) {
return u"Tooltip" + base::NumberToString16(row);
};
EXPECT_EQ(expected(0), table_->GetTooltipText(local_point_for_row(0)));
EXPECT_EQ(expected(1), table_->GetTooltipText(local_point_for_row(1)));
EXPECT_EQ(expected(2), table_->GetTooltipText(local_point_for_row(2)));
// Hovering another column will return that cell's text instead.
const gfx::Point point(15, local_point_for_row(0).y());
EXPECT_EQ(model_->GetText(0, 1), table_->GetTooltipText(point));
}
namespace {
class TableGrouperImpl : public TableGrouper {
public:
TableGrouperImpl() = default;
void SetRanges(const std::vector<int>& ranges) { ranges_ = ranges; }
// TableGrouper overrides:
void GetGroupRange(int model_index, GroupRange* range) override {
int offset = 0;
size_t range_index = 0;
for (; range_index < ranges_.size() && offset < model_index; ++range_index)
offset += ranges_[range_index];
if (offset == model_index) {
range->start = model_index;
range->length = ranges_[range_index];
} else {
range->start = offset - ranges_[range_index - 1];
range->length = ranges_[range_index - 1];
}
}
private:
std::vector<int> ranges_;
DISALLOW_COPY_AND_ASSIGN(TableGrouperImpl);
};
} // namespace
// Assertions around grouping.
TEST_P(TableViewTest, Grouping) {
// Configure the grouper so that there are two groups:
// A 0
// 1
// B 2
// 3
TableGrouperImpl grouper;
std::vector<int> ranges;
ranges.push_back(2);
ranges.push_back(2);
grouper.SetRanges(ranges);
table_->SetGrouper(&grouper);
// Toggle the sort order of the first column, shouldn't change anything.
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
// Sort descending, resulting:
// B 2
// 3
// A 0
// 1
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_));
EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_));
// Change the entry in the 4th row to -1. The model now becomes:
// A 0
// 1
// B 2
// -1
// Since the first entry in the range didn't change the sort isn't impacted.
model_->ChangeRow(3, -1, 0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_));
EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_));
// Change the entry in the 3rd row to -1. The model now becomes:
// A 0
// 1
// B -1
// -1
model_->ChangeRow(2, -1, 0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
// Toggle to clear the sort.
table_->ToggleSortOrder(0);
EXPECT_TRUE(table_->sort_descriptors().empty());
EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_));
EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_));
// Toggle again to effect an ascending sort.
table_->ToggleSortOrder(0);
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_TRUE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_));
EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_));
}
namespace {
class TableViewObserverImpl : public TableViewObserver {
public:
TableViewObserverImpl() = default;
int GetChangedCountAndClear() {
const int count = selection_changed_count_;
selection_changed_count_ = 0;
return count;
}
// TableViewObserver overrides:
void OnSelectionChanged() override { selection_changed_count_++; }
private:
int selection_changed_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(TableViewObserverImpl);
};
} // namespace
// Assertions around changing the selection.
TEST_P(TableViewTest, Selection) {
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
// Select the last row.
table_->Select(3);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Change sort, shouldn't notify of change (toggle twice so that order
// actually changes).
table_->ToggleSortOrder(0);
table_->ToggleSortOrder(0);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Remove the selected row, this should notify of a change and update the
// selection.
model_->RemoveRow(3);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Insert a row, since the selection in terms of the original model hasn't
// changed the observer is not notified.
model_->AddRow(0, 1, 2);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Swap the first two rows. This shouldn't affect selection.
model_->MoveRows(0, 1, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Move the first row to after the selection. This will change the selection
// state.
model_->MoveRows(0, 1, 3);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Move the first two rows to be after the selection. This will change the
// selection state.
model_->MoveRows(0, 2, 2);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
// Move some rows after the selection.
model_->MoveRows(2, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
// Move the selection itself.
model_->MoveRows(0, 1, 3);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Move-left a range that ends at the selection
model_->MoveRows(2, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Move-right a range that ends at the selection
model_->MoveRows(1, 2, 2);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Add a row at the end.
model_->AddRow(4, 7, 9);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Move-left a range that includes the selection.
model_->MoveRows(2, 3, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Move-right a range that includes the selection.
model_->MoveRows(0, 4, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Insert two rows to the selection.
model_->AddRows(2, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=5 anchor=5 selection=5", SelectionStateAsString());
// Remove two rows which include the selection.
model_->RemoveRows(4, 2);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
table_->set_observer(nullptr);
}
TEST_P(TableViewTest, SelectAll) {
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
table_->SetSelectionAll(/*select=*/true);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=-1 anchor=-1 selection=0 1 2 3", SelectionStateAsString());
table_->Select(2);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
table_->SetSelectionAll(/*select=*/true);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=0 1 2 3", SelectionStateAsString());
table_->SetSelectionAll(/*select=*/false);
EXPECT_EQ("active=2 anchor=2 selection=", SelectionStateAsString());
}
TEST_P(TableViewTest, RemoveUnselectedRows) {
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Select a middle row.
table_->Select(2);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Remove the last row. This should notify of a change.
model_->RemoveRow(3);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
// Remove the first row. This should also notify of a change.
model_->RemoveRow(0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
}
TEST_P(TableViewTest, AddingRemovingMultipleRows) {
TableViewObserverImpl observer;
table_->set_observer(&observer);
VerifyTableViewAndAXOrder("[0, 1], [1, 1], [2, 2], [3, 0]");
model_->AddRows(0, 3, 10);
VerifyTableViewAndAXOrder(
"[10, 10], [11, 11], [12, 12], [0, 1], [1, 1], [2, 2], [3, 0]");
model_->RemoveRows(4, 3);
VerifyTableViewAndAXOrder("[10, 10], [11, 11], [12, 12], [0, 1]");
}
// 0 1 2 3:
// select 3 -> 0 1 2 [3]
// remove 3 -> 0 1 2 (none selected)
// select 1 -> 0 [1] 2
// remove 1 -> 0 1 (none selected)
// select 0 -> [0] 1
// remove 0 -> 0 (none selected)
TEST_P(TableViewTest, SelectionNoSelectOnRemove) {
TableViewObserverImpl observer;
table_->set_observer(&observer);
table_->SetSelectOnRemove(false);
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
// Select row 3.
table_->Select(3);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString());
// Remove the selected row, this should notify of a change and since the
// select_on_remove_ is set false, and the removed item is the previously
// selected item, so no item is selected.
model_->RemoveRow(3);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
// Select row 1.
table_->Select(1);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
// Remove the selected row.
model_->RemoveRow(1);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
// Select row 0.
table_->Select(0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
// Remove the selected row.
model_->RemoveRow(0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
table_->set_observer(nullptr);
}
// No touch on desktop Mac. Tracked in http://crbug.com/445520.
#if !defined(OS_APPLE)
// Verifies selection works by way of a gesture.
TEST_P(TableViewTest, SelectOnTap) {
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Tap on the first row, should select it and focus the table.
EXPECT_FALSE(table_->HasFocus());
TapOnRow(0);
EXPECT_TRUE(table_->HasFocus());
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
table_->set_observer(nullptr);
}
#endif
// Verifies up/down correctly navigate through groups.
TEST_P(TableViewTest, KeyUpDown) {
// Configure the grouper so that there are three groups:
// A 0
// 1
// B 5
// C 2
// 3
model_->AddRow(2, 5, 0);
TableGrouperImpl grouper;
std::vector<int> ranges;
ranges.push_back(2);
ranges.push_back(1);
ranges.push_back(2);
grouper.SetRanges(ranges);
table_->SetGrouper(&grouper);
TableViewObserverImpl observer;
table_->set_observer(&observer);
table_->RequestFocus();
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
// Sort the table descending by column 1, view now looks like:
// B 5 model: 2
// C 2 3
// 3 4
// A 0 0
// 1 1
table_->ToggleSortOrder(0);
table_->ToggleSortOrder(0);
EXPECT_EQ("2 3 4 0 1", GetViewToModelAsString(table_));
table_->Select(-1);
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
observer.GetChangedCountAndClear();
// Up with nothing selected selects the first row.
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_DOWN);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
PressKey(ui::VKEY_UP);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString());
table_->set_observer(nullptr);
}
// Verifies left/right correctly navigate through visible columns.
TEST_P(TableViewTest, KeyLeftRight) {
if (!PlatformStyle::kTableViewSupportsKeyboardNavigationByCell)
return;
TableViewObserverImpl observer;
table_->set_observer(&observer);
table_->RequestFocus();
// Initially no active visible column.
EXPECT_EQ(-1, helper_->GetActiveVisibleColumnIndex());
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
helper_->SetSelectionModel(ui::ListSelectionModel());
EXPECT_EQ(-1, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(1, observer.GetChangedCountAndClear());
PressKey(ui::VKEY_LEFT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(1, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(1, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0", SelectionStateAsString());
ui::ListSelectionModel new_selection;
new_selection.SetSelectedIndex(1);
helper_->SetSelectionModel(new_selection);
EXPECT_EQ(1, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
PressKey(ui::VKEY_LEFT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
PressKey(ui::VKEY_LEFT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
table_->SetColumnVisibility(0, false);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
// Since the first column was hidden, the active visible column should not
// advance.
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
// If visibility to the first column is restored, the active visible column
// should be unchanged because columns are always added to the end.
table_->SetColumnVisibility(0, true);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(1, helper_->GetActiveVisibleColumnIndex());
// If visibility to the first column is removed, the active visible column
// should be decreased by one.
table_->SetColumnVisibility(0, false);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
PressKey(ui::VKEY_LEFT);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
table_->SetColumnVisibility(0, true);
EXPECT_EQ(0, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
PressKey(ui::VKEY_RIGHT);
EXPECT_EQ(1, helper_->GetActiveVisibleColumnIndex());
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=1 selection=1", SelectionStateAsString());
table_->set_observer(nullptr);
}
// Verifies home/end do the right thing.
TEST_P(TableViewTest, HomeEnd) {
// Configure the grouper so that there are three groups:
// A 0
// 1
// B 5
// C 2
// 3
model_->AddRow(2, 5, 0);
TableGrouperImpl grouper;
std::vector<int> ranges{2, 1, 2};
grouper.SetRanges(ranges);
table_->SetGrouper(&grouper);
TableViewObserverImpl observer;
table_->set_observer(&observer);
table_->RequestFocus();
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
PressKey(ui::VKEY_HOME);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
PressKey(ui::VKEY_END);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
PressKey(ui::VKEY_HOME);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
table_->set_observer(nullptr);
}
// Verifies multiple selection gestures work (control-click, shift-click ...).
TEST_P(TableViewTest, Multiselection) {
// Configure the grouper so that there are three groups:
// A 0
// 1
// B 5
// C 2
// 3
model_->AddRow(2, 5, 0);
TableGrouperImpl grouper;
std::vector<int> ranges;
ranges.push_back(2);
ranges.push_back(1);
ranges.push_back(2);
grouper.SetRanges(ranges);
table_->SetGrouper(&grouper);
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Click on the first row, should select it and the second row.
ClickOnRow(0, 0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString());
// Click on the last row, should select it and the row before it.
ClickOnRow(4, 0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
// Shift click on the third row, should extend selection to it.
ClickOnRow(2, ui::EF_SHIFT_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=4 selection=2 3 4", SelectionStateAsString());
// Control click on third row, should toggle it.
ClickOnRow(2, kCtrlOrCmdMask);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=2 selection=3 4", SelectionStateAsString());
// Control-shift click on second row, should extend selection to it.
ClickOnRow(1, kCtrlOrCmdMask | ui::EF_SHIFT_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=2 selection=0 1 2 3 4", SelectionStateAsString());
// Click on last row again.
ClickOnRow(4, 0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
table_->set_observer(nullptr);
}
// Verifies multiple selection gestures work when sorted.
TEST_P(TableViewTest, MultiselectionWithSort) {
// Configure the grouper so that there are three groups:
// A 0
// 1
// B 5
// C 2
// 3
model_->AddRow(2, 5, 0);
TableGrouperImpl grouper;
std::vector<int> ranges;
ranges.push_back(2);
ranges.push_back(1);
ranges.push_back(2);
grouper.SetRanges(ranges);
table_->SetGrouper(&grouper);
// Sort the table descending by column 1, view now looks like:
// B 5 model: 2
// C 2 3
// 3 4
// A 0 0
// 1 1
table_->ToggleSortOrder(0);
table_->ToggleSortOrder(0);
// Initially no selection.
EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString());
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Click on the third row, should select it and the second row.
ClickOnRow(2, 0);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString());
// Extend selection to first row.
ClickOnRow(0, ui::EF_SHIFT_DOWN);
EXPECT_EQ(1, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=4 selection=2 3 4", SelectionStateAsString());
}
TEST_P(TableViewTest, MoveRowsWithMultipleSelection) {
model_->AddRow(3, 77, 0);
// Hide column 1.
table_->SetColumnVisibility(1, false);
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Select three rows.
ClickOnRow(2, 0);
ClickOnRow(4, ui::EF_SHIFT_DOWN);
EXPECT_EQ(2, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=2 selection=2 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[0], [1], [2], [77], [3]");
// Move the unselected rows to the middle of the current selection. None of
// the move operations should affect the view order.
model_->MoveRows(0, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[2], [0], [1], [77], [3]");
// Move the unselected rows to the end of the current selection.
model_->MoveRows(1, 2, 3);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=2 anchor=0 selection=0 1 2", SelectionStateAsString());
VerifyTableViewAndAXOrder("[2], [77], [3], [0], [1]");
// Move the unselected rows back to the middle of the selection.
model_->MoveRows(3, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[2], [0], [1], [77], [3]");
// Swap the unselected rows.
model_->MoveRows(1, 1, 2);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[2], [1], [0], [77], [3]");
// Move the second unselected row to be between two selected rows.
model_->MoveRows(2, 1, 3);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=0 selection=0 2 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[2], [1], [77], [0], [3]");
// Move the three middle rows to the beginning, including one selected row.
model_->MoveRows(1, 3, 0);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=4 anchor=3 selection=1 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder("[1], [77], [0], [2], [3]");
table_->set_observer(nullptr);
}
TEST_P(TableViewTest, MoveRowsWithMultipleSelectionAndSort) {
model_->AddRow(3, 77, 0);
// Sort ascending by column 0, and hide column 1. The view order should not
// change during this test.
table_->ToggleSortOrder(0);
table_->SetColumnVisibility(1, false);
const char* kViewOrder = "[0], [1], [2], [3], [77]";
VerifyTableViewAndAXOrder(kViewOrder);
TableViewObserverImpl observer;
table_->set_observer(&observer);
// Select three rows.
ClickOnRow(2, 0);
ClickOnRow(4, ui::EF_SHIFT_DOWN);
EXPECT_EQ(2, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=2 selection=2 3 4", SelectionStateAsString());
// Move the unselected rows to the middle of the current selection. None of
// the move operations should affect the view order.
model_->MoveRows(0, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
// Move the unselected rows to the end of the current selection.
model_->MoveRows(1, 2, 3);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=1 anchor=0 selection=0 1 2", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
// Move the unselected rows back to the middle of the selection.
model_->MoveRows(3, 2, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
// Swap the unselected rows.
model_->MoveRows(1, 1, 2);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
// Swap the unselected rows again.
model_->MoveRows(2, 1, 1);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=0 selection=0 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
// Move the unselected rows back to the beginning.
model_->MoveRows(1, 2, 0);
EXPECT_EQ(0, observer.GetChangedCountAndClear());
EXPECT_EQ("active=3 anchor=2 selection=2 3 4", SelectionStateAsString());
VerifyTableViewAndAXOrder(kViewOrder);
table_->set_observer(nullptr);
}
// Verifies we don't crash after removing the selected row when there is
// sorting and the anchor/active index also match the selected row.
TEST_P(TableViewTest, FocusAfterRemovingAnchor) {
table_->ToggleSortOrder(0);
ui::ListSelectionModel new_selection;
new_selection.AddIndexToSelection(0);
new_selection.AddIndexToSelection(1);
new_selection.set_active(0);
new_selection.set_anchor(0);
helper_->SetSelectionModel(new_selection);
model_->RemoveRow(0);
table_->RequestFocus();
}
// OnItemsRemoved() should ensure view-model mappings are updated in response to
// the table model change before these view-model mappings are used.
// Test for (https://crbug.com/1173373).
TEST_P(TableViewTest, RemovingSortedRowsDoesNotCauseOverflow) {
// Ensure the table has a sort descriptor set so that `view_to_model_` and
// `model_to_view_` mappings are established and are in descending order. Do
// this so the first view row maps to the last model row.
table_->ToggleSortOrder(0);
table_->ToggleSortOrder(0);
ASSERT_TRUE(table_->GetIsSorted());
ASSERT_EQ(1u, table_->sort_descriptors().size());
EXPECT_EQ(0, table_->sort_descriptors()[0].column_id);
EXPECT_FALSE(table_->sort_descriptors()[0].ascending);
EXPECT_EQ("3 2 1 0", GetViewToModelAsString(table_));
EXPECT_EQ("3 2 1 0", GetModelToViewAsString(table_));
// Removing rows can result in callbacks to GetTooltipText(). Above mappings
// will cause TableView to try to access the text for the first view row and
// consequently attempt to access the last element in the model via the
// `view_to_model_` mapping. This will result in a crash if the view-model
// mappings have not been appropriately updated.
model_->SetTooltip(u"");
model_->RemoveRow(0);
model_->RemoveRow(0);
model_->RemoveRow(0);
model_->RemoveRow(0);
}
namespace {
class RemoveFocusChangeListenerDelegate : public WidgetDelegate {
public:
explicit RemoveFocusChangeListenerDelegate(Widget* widget)
: listener_(nullptr) {
RegisterDeleteDelegateCallback(base::BindOnce(
[](Widget* widget, RemoveFocusChangeListenerDelegate* delegate) {
widget->GetFocusManager()->RemoveFocusChangeListener(
delegate->listener_);
},
base::Unretained(widget), base::Unretained(this)));
}
~RemoveFocusChangeListenerDelegate() override = default;
void SetFocusChangeListener(FocusChangeListener* listener);
private:
FocusChangeListener* listener_;
DISALLOW_COPY_AND_ASSIGN(RemoveFocusChangeListenerDelegate);
};
void RemoveFocusChangeListenerDelegate::SetFocusChangeListener(
FocusChangeListener* listener) {
listener_ = listener;
}
} // namespace
class TableViewFocusTest : public TableViewTest {
public:
TableViewFocusTest() = default;
protected:
WidgetDelegate* GetWidgetDelegate(Widget* widget) override;
RemoveFocusChangeListenerDelegate* GetFocusChangeListenerDelegate() {
return delegate_.get();
}
private:
std::unique_ptr<RemoveFocusChangeListenerDelegate> delegate_;
DISALLOW_COPY_AND_ASSIGN(TableViewFocusTest);
};
WidgetDelegate* TableViewFocusTest::GetWidgetDelegate(Widget* widget) {
delegate_ = std::make_unique<RemoveFocusChangeListenerDelegate>(widget);
return delegate_.get();
}
INSTANTIATE_TEST_SUITE_P(All, TableViewFocusTest, testing::Values(false, true));
// Verifies that the active focus is cleared when the widget is destroyed.
// In MD mode, if that doesn't happen a DCHECK in View::DoRemoveChildView(...)
// will trigger due to an attempt to modify the child view list while iterating.
TEST_P(TableViewFocusTest, FocusClearedDuringWidgetDestruction) {
TestFocusChangeListener listener;
GetFocusChangeListenerDelegate()->SetFocusChangeListener(&listener);
widget_->GetFocusManager()->AddFocusChangeListener(&listener);
table_->RequestFocus();
ASSERT_EQ(1u, listener.focus_changes().size());
EXPECT_EQ(listener.focus_changes()[0], ViewPair(nullptr, table_));
listener.ClearFocusChanges();
// Now destroy the widget. This should *not* cause a DCHECK in
// View::DoRemoveChildView(...).
widget_.reset();
ASSERT_EQ(1u, listener.focus_changes().size());
EXPECT_EQ(listener.focus_changes()[0], ViewPair(table_, nullptr));
}
class TableViewDefaultConstructabilityTest : public ViewsTestBase {
public:
TableViewDefaultConstructabilityTest() = default;
TableViewDefaultConstructabilityTest(
const TableViewDefaultConstructabilityTest&) = delete;
TableViewDefaultConstructabilityTest& operator=(
const TableViewDefaultConstructabilityTest&) = delete;
~TableViewDefaultConstructabilityTest() override = default;
void SetUp() override {
ViewsTestBase::SetUp();
widget_ = std::make_unique<Widget>();
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.bounds = gfx::Rect(0, 0, 650, 650);
widget_->Init(std::move(params));
widget_->Show();
}
void TearDown() override {
widget_.reset();
ViewsTestBase::TearDown();
}
Widget* widget() { return widget_.get(); }
private:
std::unique_ptr<Widget> widget_;
};
TEST_F(TableViewDefaultConstructabilityTest, TestFunctionalWithoutModel) {
auto scroll_view =
TableView::CreateScrollViewWithTable(std::make_unique<TableView>());
scroll_view->SetBounds(0, 0, 10000, 10000);
scroll_view->Layout();
widget()->GetContentsView()->AddChildView(std::move(scroll_view));
}
} // namespace views