From 82cc9f14bcdc6354ed89956364a1485db559213a Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 17:40:59 +0200 Subject: [PATCH] Add test to ensure correct operation of DataGridWidget --- tests/integration/CMakeLists.txt | 17 ++ tests/integration/DataGridTest.h | 72 ++++++++ tests/integration/fixtures/schema.sql | 27 +++ tests/integration/main.cpp | 6 + tests/integration/tst_DataGridMapping.cpp | 112 +++++++++++++ tests/integration/tst_DataGridUpdate.cpp | 190 ++++++++++++++++++++++ 6 files changed, 424 insertions(+) create mode 100644 tests/integration/DataGridTest.h create mode 100644 tests/integration/tst_DataGridMapping.cpp create mode 100644 tests/integration/tst_DataGridUpdate.cpp diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index 1284cdb..fbff992 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -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 diff --git a/tests/integration/DataGridTest.h b/tests/integration/DataGridTest.h new file mode 100644 index 0000000..1d4a0a6 --- /dev/null +++ b/tests/integration/DataGridTest.h @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include "DbTest.h" +#include "ConnectionConfig.h" +#include "OpenDatabase.h" +#include "crud/CrudModel.h" +#include "crud/DataGridWidget.h" +#include "Pgsql_Connection.h" + +#include +#include +#include +#include +#include +#include + +class DataGridTest : public pgtest::DbTest { +protected: + std::shared_ptr openDb() + { + QUrl url(dbUrl()); + ConnectionConfig cfg; + cfg.setHost(url.host()); + cfg.setPort(static_cast(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(conn().query(sql)); + model = std::make_unique(); + widget = std::make_unique(); + widget->setSourceModel(model.get()); + model->setResult(db, result); + } + + /// Editable flag of every result column, as seen through the widget's proxy + std::vector editableColumns() + { + auto proxy = widget->proxy(); + std::vector 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 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 db; + std::unique_ptr model; + std::unique_ptr widget; +}; diff --git a/tests/integration/fixtures/schema.sql b/tests/integration/fixtures/schema.sql index ee8554f..28d7d1a 100644 --- a/tests/integration/fixtures/schema.sql +++ b/tests/integration/fixtures/schema.sql @@ -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'); diff --git a/tests/integration/main.cpp b/tests/integration/main.cpp index 60bed8d..e3214e3 100644 --- a/tests/integration/main.cpp +++ b/tests/integration/main.cpp @@ -1,9 +1,15 @@ #include +#include +#include + #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(); diff --git a/tests/integration/tst_DataGridMapping.cpp b/tests/integration/tst_DataGridMapping.cpp new file mode 100644 index 0000000..d1dc58a --- /dev/null +++ b/tests/integration/tst_DataGridMapping.cpp @@ -0,0 +1,112 @@ +#include + +#include "catalog/PgClassContainer.h" +#include "catalog/PgDatabaseCatalog.h" + +#include +#include +#include +#include + +#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{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{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{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{true, true, false})); +} + +TEST_F(DataGridTest, AliasedColumnStaysEditable) +{ + showQuery("SELECT id, name AS title FROM grid_parent"); + EXPECT_EQ(editableColumns(), (std::vector{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{true, true, false, false})); +} + +TEST_F(DataGridTest, TableWithoutPkIsReadOnly) +{ + showQuery("SELECT x, y FROM grid_nopk"); + EXPECT_EQ(editableColumns(), (std::vector{false, false})); +} + +TEST_F(DataGridTest, PkNotSelectedIsReadOnly) +{ + showQuery("SELECT name FROM grid_parent"); + EXPECT_EQ(editableColumns(), (std::vector{false})); +} + +TEST_F(DataGridTest, CompositePkFullyPresentIsEditable) +{ + showQuery("SELECT a, b, val FROM grid_composite"); + EXPECT_EQ(editableColumns(), (std::vector{true, true, true})); +} + +TEST_F(DataGridTest, CompositePkPartiallyPresentIsReadOnly) +{ + showQuery("SELECT a, val FROM grid_composite"); + EXPECT_EQ(editableColumns(), (std::vector{false, false})); +} + +TEST_F(DataGridTest, WholeTableViaSetConfigLoadsAsynchronously) +{ + db = openDb(); + auto cls = db->catalog()->classes()->getByName("grid_parent"); + ASSERT_NE(cls, nullptr); + model = std::make_unique(); + widget = std::make_unique(); + 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{true, true})); +} diff --git a/tests/integration/tst_DataGridUpdate.cpp b/tests/integration/tst_DataGridUpdate.cpp new file mode 100644 index 0000000..e225abc --- /dev/null +++ b/tests/integration/tst_DataGridUpdate.cpp @@ -0,0 +1,190 @@ +#include "DataGridTest.h" + +#include +#include +#include +#include +#include + +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 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()) + 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{true})); + + save->trigger(); + EXPECT_FALSE(save->isEnabled()); + EXPECT_FALSE(revert->isEnabled()); + EXPECT_EQ(spy.values, (std::vector{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{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(); + 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); +}