Add test to ensure correct operation of DataGridWidget
This commit is contained in:
parent
c181fe99fa
commit
82cc9f14bc
6 changed files with 424 additions and 0 deletions
|
|
@ -3,6 +3,19 @@ add_executable(IntegrationTests
|
|||
tst_Smoke.cpp
|
||||
tst_CatalogLoad.cpp
|
||||
tst_DatabaseSizes.cpp
|
||||
tst_DataGridMapping.cpp
|
||||
tst_DataGridUpdate.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/crud/CrudModel.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/crud/DataGridWidget.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/OpenDatabase.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/util/PgLabItemDelegate.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/util/Colors.cpp
|
||||
${CMAKE_SOURCE_DIR}/pglab/ResultTableModelUtil.cpp
|
||||
)
|
||||
|
||||
target_include_directories(IntegrationTests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/pglab
|
||||
${CMAKE_SOURCE_DIR}/pglab/crud
|
||||
)
|
||||
|
||||
target_link_libraries(IntegrationTests PRIVATE
|
||||
|
|
@ -10,6 +23,10 @@ target_link_libraries(IntegrationTests PRIVATE
|
|||
pglablib
|
||||
GTest::gtest
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Sql
|
||||
Qt6::Concurrent
|
||||
)
|
||||
|
||||
target_compile_definitions(IntegrationTests PRIVATE
|
||||
|
|
|
|||
72
tests/integration/DataGridTest.h
Normal file
72
tests/integration/DataGridTest.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "DbTest.h"
|
||||
#include "ConnectionConfig.h"
|
||||
#include "OpenDatabase.h"
|
||||
#include "crud/CrudModel.h"
|
||||
#include "crud/DataGridWidget.h"
|
||||
#include "Pgsql_Connection.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class DataGridTest : public pgtest::DbTest {
|
||||
protected:
|
||||
std::shared_ptr<OpenDatabase> openDb()
|
||||
{
|
||||
QUrl url(dbUrl());
|
||||
ConnectionConfig cfg;
|
||||
cfg.setHost(url.host());
|
||||
cfg.setPort(static_cast<unsigned short>(url.port(5432)));
|
||||
cfg.setUser(url.userName());
|
||||
cfg.setPassword(url.password());
|
||||
cfg.setDbname(url.path().mid(1));
|
||||
return OpenDatabase::createOpenDatabase(cfg);
|
||||
}
|
||||
|
||||
/// Runs the query and shows its result in a grid
|
||||
void showQuery(const char *sql)
|
||||
{
|
||||
db = openDb();
|
||||
auto result = std::make_shared<Pgsql::Result>(conn().query(sql));
|
||||
model = std::make_unique<CrudModel>();
|
||||
widget = std::make_unique<DataGridWidget>();
|
||||
widget->setSourceModel(model.get());
|
||||
model->setResult(db, result);
|
||||
}
|
||||
|
||||
/// Editable flag of every result column, as seen through the widget's proxy
|
||||
std::vector<bool> editableColumns()
|
||||
{
|
||||
auto proxy = widget->proxy();
|
||||
std::vector<bool> r;
|
||||
for (int c = 1; c < proxy->columnCount(); ++c)
|
||||
r.push_back(proxy->flags(proxy->index(0, c)) & Qt::ItemIsEditable);
|
||||
return r;
|
||||
}
|
||||
|
||||
QString columnName(int resultCol)
|
||||
{
|
||||
return widget->proxy()->headerData(resultCol + 1, Qt::Horizontal).toString().section('\n', 0, 0);
|
||||
}
|
||||
|
||||
/// Reads a single cell with the test's own connection
|
||||
std::optional<std::string> dbValue(const char *sql)
|
||||
{
|
||||
auto res = conn().query(sql);
|
||||
auto v = res.get(0, 0);
|
||||
if (v.null())
|
||||
return std::nullopt;
|
||||
return std::string(v.c_str());
|
||||
}
|
||||
|
||||
std::shared_ptr<OpenDatabase> db;
|
||||
std::unique_ptr<CrudModel> model;
|
||||
std::unique_ptr<DataGridWidget> widget;
|
||||
};
|
||||
|
|
@ -4,3 +4,30 @@ CREATE TABLE fixture_item (
|
|||
);
|
||||
|
||||
INSERT INTO fixture_item (id, name) VALUES (1, 'one'), (2, 'two');
|
||||
|
||||
CREATE TABLE grid_parent (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL
|
||||
);
|
||||
INSERT INTO grid_parent (id, name) VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');
|
||||
|
||||
CREATE TABLE grid_child (
|
||||
id integer PRIMARY KEY,
|
||||
parent_id integer,
|
||||
note text
|
||||
);
|
||||
INSERT INTO grid_child (id, parent_id, note) VALUES (10, 1, 'c1'), (11, 2, 'c2'), (12, 3, 'c3');
|
||||
|
||||
CREATE TABLE grid_composite (
|
||||
a integer,
|
||||
b integer,
|
||||
val text,
|
||||
PRIMARY KEY (a, b)
|
||||
);
|
||||
INSERT INTO grid_composite (a, b, val) VALUES (1, 1, 'x'), (1, 2, 'y'), (2, 1, 'z');
|
||||
|
||||
CREATE TABLE grid_nopk (
|
||||
x integer,
|
||||
y text
|
||||
);
|
||||
INSERT INTO grid_nopk (x, y) VALUES (1, 'a'), (2, 'b');
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "PgTestEnvironment.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM"))
|
||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
||||
QApplication app(argc, argv);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
::testing::AddGlobalTestEnvironment(new pgtest::PgEnv(PGTEST_FIXTURE_DIR));
|
||||
return RUN_ALL_TESTS();
|
||||
|
|
|
|||
112
tests/integration/tst_DataGridMapping.cpp
Normal file
112
tests/integration/tst_DataGridMapping.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#include <gtest/gtest.h>
|
||||
|
||||
#include "catalog/PgClassContainer.h"
|
||||
#include "catalog/PgDatabaseCatalog.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDeadlineTimer>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QEventLoop>
|
||||
|
||||
#include "DataGridTest.h"
|
||||
|
||||
|
||||
TEST_F(DataGridTest, SingleTableLoadsRows)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent ORDER BY id");
|
||||
EXPECT_EQ(model->rowCount(), 3);
|
||||
ASSERT_NE(widget->proxy(), nullptr);
|
||||
EXPECT_EQ(widget->proxy()->rowCount(), 3);
|
||||
EXPECT_FALSE(model->hasPendingChanges());
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, RowNumberColumnIsNeverEditable)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
EXPECT_FALSE(widget->proxy()->flags(widget->proxy()->index(0, 0)) & Qt::ItemIsEditable);
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, SingleTableAllColumnsEditable)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
|
||||
EXPECT_EQ(columnName(0), "id");
|
||||
EXPECT_EQ(columnName(1), "name");
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, SingleTableSubsetWithPkIsEditable)
|
||||
{
|
||||
showQuery("SELECT name, id FROM grid_parent");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
|
||||
EXPECT_EQ(columnName(0), "name");
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, JoinOfTwoTablesWithPksIsEditable)
|
||||
{
|
||||
showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, true, true}));
|
||||
EXPECT_EQ(columnName(2), "child_id");
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, JoinWithoutOnePkOnlyOtherTableEditable)
|
||||
{
|
||||
showQuery("SELECT p.id, p.name, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, AliasedColumnStaysEditable)
|
||||
{
|
||||
showQuery("SELECT id, name AS title FROM grid_parent");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
|
||||
EXPECT_EQ(columnName(1), "title");
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, ExpressionColumnIsReadOnly)
|
||||
{
|
||||
showQuery("SELECT id, name, upper(name) AS shout, id + 1 AS next_id FROM grid_parent");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, false, false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, TableWithoutPkIsReadOnly)
|
||||
{
|
||||
showQuery("SELECT x, y FROM grid_nopk");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{false, false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, PkNotSelectedIsReadOnly)
|
||||
{
|
||||
showQuery("SELECT name FROM grid_parent");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, CompositePkFullyPresentIsEditable)
|
||||
{
|
||||
showQuery("SELECT a, b, val FROM grid_composite");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, true}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, CompositePkPartiallyPresentIsReadOnly)
|
||||
{
|
||||
showQuery("SELECT a, val FROM grid_composite");
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{false, false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridTest, WholeTableViaSetConfigLoadsAsynchronously)
|
||||
{
|
||||
db = openDb();
|
||||
auto cls = db->catalog()->classes()->getByName("grid_parent");
|
||||
ASSERT_NE(cls, nullptr);
|
||||
model = std::make_unique<CrudModel>();
|
||||
widget = std::make_unique<DataGridWidget>();
|
||||
widget->setSourceModel(model.get());
|
||||
model->setConfig(db, *cls);
|
||||
model->loadData();
|
||||
|
||||
QDeadlineTimer deadline(10000);
|
||||
while (model->rowCount() < 4 && !deadline.hasExpired())
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||
|
||||
// the whole table view appends an empty row for inserting
|
||||
ASSERT_EQ(model->rowCount(), 4);
|
||||
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
|
||||
}
|
||||
190
tests/integration/tst_DataGridUpdate.cpp
Normal file
190
tests/integration/tst_DataGridUpdate.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
#include "DataGridTest.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTimer>
|
||||
#include <QToolBar>
|
||||
|
||||
namespace {
|
||||
|
||||
/// Failed saves show a modal message box, this closes it so the test can continue
|
||||
void autoCloseModalDialogs(int triesLeft = 100)
|
||||
{
|
||||
if (auto w = QApplication::activeModalWidget())
|
||||
w->close();
|
||||
else if (triesLeft > 0)
|
||||
QTimer::singleShot(20, [triesLeft] { autoCloseModalDialogs(triesLeft - 1); });
|
||||
}
|
||||
|
||||
/// Records the values of CrudModel::pendingChangesChanged
|
||||
struct PendingSpy {
|
||||
explicit PendingSpy(CrudModel *m)
|
||||
{
|
||||
QObject::connect(m, &CrudModel::pendingChangesChanged, m, [this](bool b) { values.push_back(b); });
|
||||
}
|
||||
std::vector<bool> values;
|
||||
};
|
||||
|
||||
class DataGridUpdateTest : public DataGridTest {
|
||||
protected:
|
||||
void edit(int proxyRow, int resultCol, const QString &value)
|
||||
{
|
||||
auto proxy = widget->proxy();
|
||||
ASSERT_TRUE(proxy->setData(proxy->index(proxyRow, resultCol + 1), value, Qt::EditRole));
|
||||
}
|
||||
|
||||
QAction *action(const QString &text)
|
||||
{
|
||||
for (auto a : widget->findChildren<QAction *>())
|
||||
if (a->text() == text)
|
||||
return a;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DataGridUpdateTest, EditSingleCellAndSave)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
edit(0, 1, "renamed");
|
||||
EXPECT_TRUE(model->hasPendingChanges());
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_FALSE(model->hasPendingChanges());
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "renamed");
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, EditSeveralColumnsAndRows)
|
||||
{
|
||||
showQuery("SELECT id, parent_id, note FROM grid_child");
|
||||
edit(0, 1, "3");
|
||||
edit(0, 2, "first");
|
||||
edit(2, 2, "last");
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_FALSE(model->hasPendingChanges());
|
||||
EXPECT_EQ(dbValue("SELECT parent_id FROM grid_child WHERE id = 10"), "3");
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "first");
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2");
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 12"), "last");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, EmptyValueIsSavedAsNull)
|
||||
{
|
||||
showQuery("SELECT id, note FROM grid_child");
|
||||
edit(1, 1, "");
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), std::nullopt);
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "c1");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, JoinUpdatesTheOwningTable)
|
||||
{
|
||||
showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
|
||||
edit(0, 1, "parent edited");
|
||||
edit(0, 3, "child edited");
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_FALSE(model->hasPendingChanges());
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "parent edited");
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "child edited");
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta");
|
||||
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, CompositePkUpdatesOnlyMatchingRow)
|
||||
{
|
||||
showQuery("SELECT a, b, val FROM grid_composite");
|
||||
edit(1, 2, "changed");
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 2"), "changed");
|
||||
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 1"), "x");
|
||||
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 2 AND b = 1"), "z");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, EditAfterSortingUpdatesTheShownRow)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
widget->proxy()->sort(2, Qt::DescendingOrder);
|
||||
auto proxy = widget->proxy();
|
||||
ASSERT_EQ(proxy->index(0, 1).data(Qt::DisplayRole).toString(), "3");
|
||||
ASSERT_EQ(proxy->index(0, 2).data(Qt::DisplayRole).toString(), "gamma");
|
||||
|
||||
edit(0, 1, "sorted edit");
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 3"), "sorted edit");
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, SaveAndRevertFollowPendingChanges)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
auto save = action("Save");
|
||||
auto revert = action("Revert");
|
||||
ASSERT_NE(save, nullptr);
|
||||
ASSERT_NE(revert, nullptr);
|
||||
EXPECT_FALSE(save->isEnabled());
|
||||
EXPECT_FALSE(revert->isEnabled());
|
||||
|
||||
PendingSpy spy(model.get());
|
||||
edit(0, 1, "x");
|
||||
EXPECT_TRUE(save->isEnabled());
|
||||
EXPECT_TRUE(revert->isEnabled());
|
||||
EXPECT_EQ(spy.values, (std::vector<bool>{true}));
|
||||
|
||||
save->trigger();
|
||||
EXPECT_FALSE(save->isEnabled());
|
||||
EXPECT_FALSE(revert->isEnabled());
|
||||
EXPECT_EQ(spy.values, (std::vector<bool>{true, false}));
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, RevertDiscardsEdits)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
edit(0, 1, "discard me");
|
||||
EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "discard me");
|
||||
|
||||
PendingSpy spy(model.get());
|
||||
action("Revert")->trigger();
|
||||
|
||||
EXPECT_FALSE(model->hasPendingChanges());
|
||||
EXPECT_FALSE(action("Save")->isEnabled());
|
||||
EXPECT_EQ(spy.values, (std::vector<bool>{false}));
|
||||
EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "alpha");
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, FailedSaveKeepsRowPending)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
edit(0, 1, ""); // NULL violates NOT NULL on name
|
||||
QTimer::singleShot(0, [] { autoCloseModalDialogs(); });
|
||||
action("Save")->trigger();
|
||||
|
||||
EXPECT_TRUE(model->hasPendingChanges());
|
||||
EXPECT_TRUE(action("Save")->isEnabled());
|
||||
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
|
||||
}
|
||||
|
||||
TEST_F(DataGridUpdateTest, ToolbarHiddenForNonCrudModel)
|
||||
{
|
||||
showQuery("SELECT id, name FROM grid_parent");
|
||||
auto toolbar = widget->findChild<QToolBar *>();
|
||||
ASSERT_NE(toolbar, nullptr);
|
||||
EXPECT_FALSE(toolbar->isHidden());
|
||||
|
||||
QStandardItemModel other(1, 1);
|
||||
widget->setSourceModel(&other);
|
||||
EXPECT_TRUE(toolbar->isHidden());
|
||||
EXPECT_FALSE(action("Save")->isEnabled());
|
||||
|
||||
widget->setSourceModel(model.get());
|
||||
EXPECT_FALSE(toolbar->isHidden());
|
||||
widget->setSourceModel(nullptr);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue