// Characterization tests for CrudModel, they lock in the behavior of the model // so it can be refactored safely. #include "DataGridTest.h" #include "CustomDataRole.h" #include "Pgsql_oids.h" #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; }; /// Counts the emissions of a signal struct CountSpy { template CountSpy(Sender *sender, Func signal) { QObject::connect(sender, signal, sender, [this] { ++n; }); } int count() const { return n; } int n = 0; }; // crud_item columns constexpr int ColId = 0; constexpr int ColName = 1; constexpr int ColQty = 2; constexpr int ColFlag = 3; constexpr int ColBorn = 4; constexpr int ColTag = 5; class CrudModelBehaviorTest : public DataGridTest { protected: QModelIndex idx(int row, int resultCol) const { return model->index(row, resultCol + 1); } QVariant cell(int row, int resultCol, int role = Qt::DisplayRole) const { return model->data(idx(row, resultCol), role); } bool edit(int row, int resultCol, const QString &value) { return model->setData(idx(row, resultCol), value, Qt::EditRole); } /// Saves while auto closing any error dialog bool save() { QTimer::singleShot(0, [] { autoCloseModalDialogs(); }); return model->saveChanges(); } /// Row of crud_item with the given name, -1 if not shown int rowOfName(const QString &name) const { for (int r = 0; r < model->rowCount(); ++r) if (cell(r, ColName, Qt::EditRole).toString() == name) return r; return -1; } QStringList names() const { QStringList l; for (int r = 0; r < model->rowCount(); ++r) l << cell(r, ColName, Qt::EditRole).toString(); return l; } void reload() { bool reset = false; QObject::connect(model.get(), &QAbstractItemModel::modelReset, model.get(), [&reset] { reset = true; }); model->loadData(); QDeadlineTimer deadline(10000); while (!reset && !deadline.hasExpired()) QCoreApplication::processEvents(QEventLoop::AllEvents, 50); ASSERT_TRUE(reset); } }; } // namespace // ---- loading, headers, row number column TEST_F(CrudModelBehaviorTest, WholeTableLoadsRowsPlusEmptyNewRow) { ASSERT_TRUE(showTable("crud_item")); EXPECT_EQ(model->rowCount(), 6); EXPECT_EQ(model->columnCount(), 7); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", ""})); for (int c = 0; c <= ColTag; ++c) EXPECT_FALSE(cell(5, c).isValid()) << c; } TEST_F(CrudModelBehaviorTest, EmptyTableHasOnlyTheNewRow) { ASSERT_TRUE(showTable("crud_empty")); EXPECT_EQ(model->rowCount(), 1); } TEST_F(CrudModelBehaviorTest, QueryResultHasNoNewRow) { showQuery("SELECT id, name FROM grid_parent"); EXPECT_EQ(model->rowCount(), 3); } TEST_F(CrudModelBehaviorTest, HeaderShowsNameAndType) { ASSERT_TRUE(showTable("crud_item")); EXPECT_EQ(model->headerData(0, Qt::Horizontal).toString(), "#"); for (int c = 0; c <= ColTag; ++c) { auto text = model->headerData(c + 1, Qt::Horizontal, Qt::DisplayRole).toString(); EXPECT_EQ(text.count('\n'), 1) << c; EXPECT_FALSE(text.section('\n', 1).isEmpty()) << c; } EXPECT_EQ(columnName(ColId), "id"); EXPECT_EQ(columnName(ColName), "name"); EXPECT_EQ(columnName(ColTag), "tag"); EXPECT_FALSE(model->headerData(1, Qt::Horizontal, Qt::EditRole).isValid()); } TEST_F(CrudModelBehaviorTest, RowNumberColumnData) { ASSERT_TRUE(showTable("crud_item")); for (int r = 0; r < model->rowCount(); ++r) { auto i = model->index(r, 0); EXPECT_EQ(model->data(i, Qt::DisplayRole).toInt(), r + 1); EXPECT_EQ(model->data(i, CustomSortRole).toInt(), r + 1); EXPECT_EQ(model->data(i, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); EXPECT_FALSE(model->data(i, Qt::EditRole).isValid()); EXPECT_FALSE(model->data(i, Qt::ForegroundRole).isValid()); } EXPECT_FALSE(model->setData(model->index(0, 0), "5", Qt::EditRole)); EXPECT_FALSE(model->hasPendingChanges()); } // ---- data roles TEST_F(CrudModelBehaviorTest, DisplayAndEditRoles) { ASSERT_TRUE(showTable("crud_item")); EXPECT_EQ(cell(0, ColName).toString(), "a"); EXPECT_EQ(cell(0, ColQty).toString(), "1"); EXPECT_EQ(cell(0, ColQty, Qt::EditRole).toString(), "1"); EXPECT_EQ(cell(0, ColBorn).toString(), "2020-01-02"); EXPECT_EQ(cell(0, ColTag).toString(), "dflt"); // NULL EXPECT_FALSE(cell(1, ColBorn).isValid()); EXPECT_FALSE(cell(1, ColBorn, Qt::EditRole).isValid()); // Roles that are not handled EXPECT_FALSE(cell(0, ColName, Qt::ToolTipRole).isValid()); EXPECT_FALSE(cell(0, ColName, Qt::ForegroundRole).isValid()); } TEST_F(CrudModelBehaviorTest, BoolDisplaysAsBool) { ASSERT_TRUE(showTable("crud_item")); auto t = cell(0, ColFlag); EXPECT_EQ(t.typeId(), QMetaType::Bool); EXPECT_TRUE(t.toBool()); auto f = cell(1, ColFlag); EXPECT_EQ(f.typeId(), QMetaType::Bool); EXPECT_FALSE(f.toBool()); EXPECT_FALSE(cell(2, ColFlag).isValid()); EXPECT_EQ(cell(0, ColFlag, Qt::EditRole).toString(), "t"); } TEST_F(CrudModelBehaviorTest, DisplayTruncatesButEditDoesNot) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(0, ColName, QString(300, 'x'))); EXPECT_EQ(cell(0, ColName, Qt::DisplayRole).toString().size(), 256); EXPECT_EQ(cell(0, ColName, Qt::EditRole).toString().size(), 300); } TEST_F(CrudModelBehaviorTest, SortRoleGivesTypedValues) { ASSERT_TRUE(showTable("crud_item")); auto i = cell(0, ColQty, CustomSortRole); EXPECT_EQ(i.typeId(), QMetaType::Int); EXPECT_EQ(i.toInt(), 1); EXPECT_EQ(cell(0, ColBorn, CustomSortRole).toDate(), QDate(2020, 1, 2)); EXPECT_EQ(cell(0, ColBorn, CustomSortRole).typeId(), QMetaType::QDate); auto b = cell(0, ColFlag, CustomSortRole); EXPECT_EQ(b.typeId(), QMetaType::Bool); EXPECT_TRUE(b.toBool()); EXPECT_EQ(cell(0, ColName, CustomSortRole).toString(), "a"); EXPECT_FALSE(cell(1, ColBorn, CustomSortRole).isValid()); EXPECT_FALSE(cell(2, ColQty, CustomSortRole).isValid()); } TEST_F(CrudModelBehaviorTest, DataTypeRole) { ASSERT_TRUE(showTable("crud_item")); EXPECT_EQ(cell(0, ColId, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); EXPECT_EQ(cell(0, ColName, CustomDataTypeRole).toUInt(), static_cast(Pgsql::text_oid)); EXPECT_EQ(cell(0, ColFlag, CustomDataTypeRole).toUInt(), static_cast(Pgsql::bool_oid)); EXPECT_EQ(cell(0, ColBorn, CustomDataTypeRole).toUInt(), static_cast(Pgsql::date_oid)); // also for the new row EXPECT_EQ(cell(5, ColQty, CustomDataTypeRole).toUInt(), static_cast(Pgsql::int4_oid)); } TEST_F(CrudModelBehaviorTest, ReadOnlyColumnsAreGrayInQueryMode) { showQuery("SELECT id, name || 'x' AS e FROM grid_parent"); EXPECT_FALSE(cell(0, 0, Qt::ForegroundRole).isValid()); auto brush = cell(0, 1, Qt::ForegroundRole); ASSERT_TRUE(brush.isValid()); EXPECT_EQ(brush.value().color(), QColor(Qt::gray)); } // ---- flags TEST_F(CrudModelBehaviorTest, FlagsOfGeneratedAndIdentityColumns) { ASSERT_TRUE(showTable("crud_gen")); EXPECT_EQ(editableColumns(), (std::vector{false, true, false})); auto f = model->flags(model->index(0, 0)); EXPECT_TRUE(f & Qt::ItemIsSelectable); EXPECT_TRUE(f & Qt::ItemIsEnabled); EXPECT_FALSE(f & Qt::ItemIsEditable); } TEST_F(CrudModelBehaviorTest, FlagsOfWholeTable) { ASSERT_TRUE(showTable("crud_item")); EXPECT_EQ(editableColumns(), (std::vector(6, true))); } TEST_F(CrudModelBehaviorTest, ExpressionColumnIsNotEditable) { showQuery("SELECT id, name || 'x' AS e FROM grid_parent"); EXPECT_EQ(editableColumns(), (std::vector{true, false})); } TEST_F(CrudModelBehaviorTest, TableWithoutPkIsNotEditable) { ASSERT_TRUE(showTable("grid_nopk")); EXPECT_EQ(editableColumns(), (std::vector{false, false})); } // ---- setData TEST_F(CrudModelBehaviorTest, SetDataRejectsOtherRoles) { ASSERT_TRUE(showTable("crud_item")); EXPECT_FALSE(model->setData(idx(0, ColName), "x", Qt::DisplayRole)); EXPECT_FALSE(model->hasPendingChanges()); } TEST_F(CrudModelBehaviorTest, SetDataShowsPendingValueAndEmitsDataChanged) { ASSERT_TRUE(showTable("crud_item")); CountSpy spy(model.get(), &QAbstractItemModel::dataChanged); ASSERT_TRUE(edit(0, ColName, "changed")); EXPECT_EQ(spy.count(), 1); EXPECT_TRUE(model->hasPendingChanges()); EXPECT_EQ(cell(0, ColName).toString(), "changed"); EXPECT_EQ(cell(1, ColName).toString(), "b"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "a"); } TEST_F(CrudModelBehaviorTest, EmptyStringBecomesNullAndQuotesBecomeEmptyString) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(0, ColTag, "")); ASSERT_TRUE(edit(1, ColTag, "''")); EXPECT_FALSE(cell(0, ColTag).isValid()); EXPECT_TRUE(cell(1, ColTag).isValid()); EXPECT_EQ(cell(1, ColTag).toString(), ""); ASSERT_TRUE(save()); EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'a'"), std::nullopt); EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'b'"), ""); EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'c'"), "dflt"); } TEST_F(CrudModelBehaviorTest, RepeatedEditsOfSameCellKeepLastValue) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(0, ColName, "one")); ASSERT_TRUE(edit(0, ColName, "two")); ASSERT_TRUE(edit(1, ColName, "three")); EXPECT_EQ(spy.values, (std::vector{true})); ASSERT_TRUE(save()); EXPECT_EQ(spy.values, (std::vector{true, false})); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "two"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "three"); } // ---- saving updates TEST_F(CrudModelBehaviorTest, SavedUpdateIsShownAfterwards) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(2, ColQty, "42")); ASSERT_TRUE(save()); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(cell(2, ColQty).toString(), "42"); EXPECT_EQ(cell(2, ColName).toString(), "c"); EXPECT_EQ(cell(2, ColBorn).toString(), "2021-03-04"); EXPECT_EQ(model->rowCount(), 6); EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'c'"), "42"); } TEST_F(CrudModelBehaviorTest, SaveWithoutChangesIsOk) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); EXPECT_TRUE(model->saveChanges()); EXPECT_TRUE(spy.values.empty()); } TEST_F(CrudModelBehaviorTest, RevertThenEditAgain) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(0, ColName, "discarded")); model->revertChanges(); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(cell(0, ColName).toString(), "a"); model->revertChanges(); // nothing to revert, no signal ASSERT_TRUE(edit(0, ColName, "kept")); EXPECT_EQ(cell(0, ColName).toString(), "kept"); ASSERT_TRUE(save()); EXPECT_EQ(spy.values, (std::vector{true, false, true, false})); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "kept"); } TEST_F(CrudModelBehaviorTest, RevertDoesNotRemoveSavedValues) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(0, ColName, "saved")); ASSERT_TRUE(save()); ASSERT_TRUE(edit(0, ColName, "unsaved")); model->revertChanges(); EXPECT_EQ(cell(0, ColName).toString(), "saved"); } TEST_F(CrudModelBehaviorTest, PartialFailureKeepsOnlyFailingRowPending) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(0, ColName, "ok")); ASSERT_TRUE(edit(1, ColName, "")); // NULL violates NOT NULL ASSERT_TRUE(edit(2, ColName, "never saved")); EXPECT_FALSE(save()); EXPECT_TRUE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{true})); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 1"), "ok"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "b"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 3"), "c"); EXPECT_EQ(cell(0, ColName).toString(), "ok"); EXPECT_FALSE(cell(1, ColName).isValid()); // still shows the pending value // Fixing the failing row lets the rest be saved ASSERT_TRUE(edit(1, ColName, "fixed")); ASSERT_TRUE(save()); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{true, false})); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 2"), "fixed"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 3"), "never saved"); } TEST_F(CrudModelBehaviorTest, UpdateOfRowChangedElsewhereFails) { ASSERT_TRUE(showTable("crud_item")); conn().query("DELETE FROM crud_item WHERE id = 1"); ASSERT_TRUE(edit(0, ColName, "gone")); EXPECT_FALSE(save()); EXPECT_TRUE(model->hasPendingChanges()); } // ---- reload TEST_F(CrudModelBehaviorTest, ReloadClearsPendingChangesAndShowsDbState) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(0, ColName, "pending")); conn().query("INSERT INTO crud_item (name) VALUES ('f')"); PendingSpy spy(model.get()); reload(); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{false})); EXPECT_EQ(model->rowCount(), 7); EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "f", ""})); } TEST_F(CrudModelBehaviorTest, ReloadWithoutPendingChangesEmitsNoSignal) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); reload(); EXPECT_TRUE(spy.values.empty()); EXPECT_EQ(model->rowCount(), 6); } // ---- insert TEST_F(CrudModelBehaviorTest, InsertNewRow) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(5, ColName, "new")); ASSERT_TRUE(edit(5, ColQty, "7")); EXPECT_EQ(spy.values, (std::vector{true})); ASSERT_TRUE(save()); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{true, false})); EXPECT_EQ(model->rowCount(), 7); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "6"); EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'new'"), "7"); // RETURNING * brings the defaults into the model EXPECT_EQ(cell(5, ColName).toString(), "new"); EXPECT_EQ(cell(5, ColQty).toString(), "7"); EXPECT_EQ(cell(5, ColTag).toString(), "dflt"); EXPECT_EQ(cell(5, ColId).toString(), *dbValue("SELECT id FROM crud_item WHERE name = 'new'")); EXPECT_FALSE(cell(5, ColBorn).isValid()); // and a fresh empty row is appended EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "new", ""})); EXPECT_FALSE(cell(6, ColId).isValid()); } TEST_F(CrudModelBehaviorTest, InsertTwoRowsInARow) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(5, ColName, "n1")); ASSERT_TRUE(save()); ASSERT_TRUE(edit(6, ColName, "n2")); ASSERT_TRUE(save()); EXPECT_EQ(model->rowCount(), 8); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "7"); EXPECT_EQ(names(), (QStringList{"a", "b", "c", "d", "e", "n1", "n2", ""})); } TEST_F(CrudModelBehaviorTest, InsertIntoEmptyTable) { ASSERT_TRUE(showTable("crud_empty")); ASSERT_TRUE(edit(0, 1, "first")); ASSERT_TRUE(save()); EXPECT_EQ(model->rowCount(), 2); EXPECT_EQ(dbValue("SELECT v FROM crud_empty"), "first"); EXPECT_EQ(cell(0, 0).toString(), "1"); } TEST_F(CrudModelBehaviorTest, InsertWithExplicitNull) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(5, ColName, "nulltag")); ASSERT_TRUE(edit(5, ColTag, "")); ASSERT_TRUE(save()); EXPECT_EQ(dbValue("SELECT tag FROM crud_item WHERE name = 'nulltag'"), std::nullopt); EXPECT_FALSE(cell(5, ColTag).isValid()); } TEST_F(CrudModelBehaviorTest, FailedInsertKeepsRowPending) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(5, ColQty, "9")); // name is NOT NULL and missing EXPECT_FALSE(save()); EXPECT_TRUE(model->hasPendingChanges()); EXPECT_EQ(model->rowCount(), 6); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); EXPECT_EQ(cell(5, ColQty).toString(), "9"); ASSERT_TRUE(edit(5, ColName, "now valid")); ASSERT_TRUE(save()); EXPECT_EQ(model->rowCount(), 7); EXPECT_EQ(dbValue("SELECT qty FROM crud_item WHERE name = 'now valid'"), "9"); } TEST_F(CrudModelBehaviorTest, EditingOnlyThenRevertingNewRowDoesNotInsert) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(edit(5, ColName, "temp")); model->revertChanges(); EXPECT_FALSE(cell(5, ColName).isValid()); EXPECT_EQ(model->rowCount(), 6); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); } // ---- delete TEST_F(CrudModelBehaviorTest, RemoveSingleRow) { ASSERT_TRUE(showTable("crud_item")); CountSpy spy(model.get(), &QAbstractItemModel::rowsRemoved); auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); EXPECT_TRUE(ok) << msg.toStdString(); EXPECT_EQ(spy.count(), 1); EXPECT_EQ(names(), (QStringList{"a", "c", "d", "e", ""})); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "4"); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item WHERE name = 'b'"), "0"); EXPECT_EQ(model->data(model->index(4, 0), Qt::DisplayRole).toInt(), 5); } TEST_F(CrudModelBehaviorTest, RemoveContiguousRange) { ASSERT_TRUE(showTable("crud_item")); auto [ok, msg] = model->removeRows({IntegerRange(1, 3)}); EXPECT_TRUE(ok) << msg.toStdString(); EXPECT_EQ(names(), (QStringList{"a", "e", ""})); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "2"); } TEST_F(CrudModelBehaviorTest, RemoveNonContiguousRanges) { ASSERT_TRUE(showTable("crud_item")); auto [ok, msg] = model->removeRows({IntegerRange(0, 1), IntegerRange(2, 2)}); EXPECT_TRUE(ok) << msg.toStdString(); EXPECT_EQ(names(), (QStringList{"b", "e", ""})); EXPECT_EQ(dbValue("SELECT string_agg(name, ',' ORDER BY name) FROM crud_item"), "b,e"); } TEST_F(CrudModelBehaviorTest, RemoveRowsOverloadForViews) { ASSERT_TRUE(showTable("crud_item")); EXPECT_TRUE(model->removeRows(0, 2)); EXPECT_EQ(names(), (QStringList{"c", "d", "e", ""})); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "3"); } TEST_F(CrudModelBehaviorTest, RemoveNothingIsOk) { ASSERT_TRUE(showTable("crud_item")); auto [ok, msg] = model->removeRows({}); EXPECT_TRUE(ok); EXPECT_EQ(model->rowCount(), 6); } TEST_F(CrudModelBehaviorTest, RemoveDropsPendingEditsOfRemovedRows) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(1, ColName, "edited then removed")); auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); EXPECT_TRUE(ok); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{true, false})); } TEST_F(CrudModelBehaviorTest, PendingEditsOfOtherRowsSurviveRemoval) { ASSERT_TRUE(showTable("crud_item")); PendingSpy spy(model.get()); ASSERT_TRUE(edit(3, ColName, "d edited")); auto [ok, msg] = model->removeRows({IntegerRange(0, 3)}); EXPECT_TRUE(ok); EXPECT_TRUE(model->hasPendingChanges()); EXPECT_EQ(spy.values, (std::vector{true})); EXPECT_EQ(names(), (QStringList{"d edited", "e", ""})); ASSERT_TRUE(save()); EXPECT_FALSE(model->hasPendingChanges()); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 4"), "d edited"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 5"), "e"); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "2"); } TEST_F(CrudModelBehaviorTest, EditAfterRemovalUpdatesTheRightRow) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(std::get<0>(model->removeRows({IntegerRange(0, 3)}))); ASSERT_TRUE(edit(1, ColName, "e edited")); ASSERT_TRUE(save()); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 5"), "e edited"); EXPECT_EQ(dbValue("SELECT name FROM crud_item WHERE id = 4"), "d"); } TEST_F(CrudModelBehaviorTest, InsertAfterRemoval) { ASSERT_TRUE(showTable("crud_item")); ASSERT_TRUE(std::get<0>(model->removeRows({IntegerRange(0, 1)}))); ASSERT_TRUE(edit(model->rowCount() - 1, ColName, "after delete")); ASSERT_TRUE(save()); EXPECT_EQ(names(), (QStringList{"b", "c", "d", "e", "after delete", ""})); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); } TEST_F(CrudModelBehaviorTest, RemoveOutOfRangeFails) { ASSERT_TRUE(showTable("crud_item")); auto [ok, msg] = model->removeRows({IntegerRange(5, 5)}); EXPECT_FALSE(ok); EXPECT_FALSE(msg.isEmpty()); EXPECT_EQ(model->rowCount(), 6); EXPECT_EQ(dbValue("SELECT count(*) FROM crud_item"), "5"); } TEST_F(CrudModelBehaviorTest, RemoveFromQueryResultFails) { showQuery("SELECT id, name FROM grid_parent"); auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); EXPECT_FALSE(ok); EXPECT_FALSE(msg.isEmpty()); EXPECT_EQ(model->rowCount(), 3); EXPECT_EQ(dbValue("SELECT count(*) FROM grid_parent"), "3"); } TEST_F(CrudModelBehaviorTest, RemoveFromTableWithoutPkFails) { ASSERT_TRUE(showTable("grid_nopk")); auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); EXPECT_FALSE(ok); EXPECT_FALSE(msg.isEmpty()); EXPECT_EQ(model->rowCount(), 3); EXPECT_EQ(dbValue("SELECT count(*) FROM grid_nopk"), "2"); } TEST_F(CrudModelBehaviorTest, RemoveRowReferencedByForeignKeyFails) { conn().query("ALTER TABLE grid_child ADD FOREIGN KEY (parent_id) REFERENCES grid_parent (id)"); ASSERT_TRUE(showTable("grid_parent")); auto [ok, msg] = model->removeRows({IntegerRange(0, 1)}); EXPECT_FALSE(ok); EXPECT_EQ(model->rowCount(), 4); EXPECT_EQ(dbValue("SELECT count(*) FROM grid_parent"), "3"); } TEST_F(CrudModelBehaviorTest, RemoveFromCompositePkTable) { ASSERT_TRUE(showTable("grid_composite")); auto [ok, msg] = model->removeRows({IntegerRange(1, 1)}); EXPECT_TRUE(ok) << msg.toStdString(); EXPECT_EQ(dbValue("SELECT count(*) FROM grid_composite"), "2"); EXPECT_EQ(dbValue("SELECT count(*) FROM grid_composite WHERE a = 1 AND b = 2"), "0"); }