11#include < gtest/gtest.h>
22#include < QAbstractItemModel>
3+ #include < QApplication>
4+ #include < QHeaderView>
5+ #include < QMouseEvent>
36#include < QStandardItemModel>
47#include < QTableView>
58#include < QWidget>
9+ #include < algorithm>
10+ #include < memory>
611
712#include " gui/queryresultpresenter.h"
813
14+ namespace {
15+ // What a view asked a model to sort by. A paged result answers a sort by
16+ // re-running its statement, so an unasked-for sort is both the wrong order
17+ // and a second execution of the query.
18+ //
19+ // The record is kept outside the model and shared with it, because binding
20+ // a model to a view deletes the one it replaces: a test that reads this
21+ // after the next result arrives would otherwise be reading freed memory.
22+ struct SortLog {
23+ QList<int > columns{};
24+ QList<Qt::SortOrder> orders{};
25+
26+ // Sorting by column -1 is Qt's "leave it in its natural order", so it
27+ // does not count as having sorted anything.
28+ [[nodiscard]] bool sorted () const {
29+ return std::any_of (columns.begin (),
30+ columns.end (),
31+ [](const int column) { return column >= 0 ; });
32+ }
33+ };
34+
35+ class SortSpyModel final : public QStandardItemModel {
36+ public:
37+ SortSpyModel (const int rows, const int columns, std::shared_ptr<SortLog> log)
38+ : QStandardItemModel(rows, columns), log(std::move(log)) {
39+ }
40+
41+ void sort (const int column, const Qt::SortOrder order) override {
42+ if (this ->log != nullptr ) {
43+ this ->log ->columns .append (column);
44+ this ->log ->orders .append (order);
45+ }
46+ // Column -1 is Qt asking for no particular order, which is not
47+ // something to hand to a model that orders rows for real.
48+ if (column >= 0 )
49+ QStandardItemModel::sort (column, order);
50+ }
51+
52+ private:
53+ std::shared_ptr<SortLog> log;
54+ };
55+
56+ void clickHeaderSection (const QTableView *view, const int section) {
57+ const auto *header = view->horizontalHeader ();
58+ const QPoint position (
59+ header->sectionViewportPosition (section) + header->sectionSize (section) / 2 ,
60+ header->viewport ()->height () / 2 );
61+ const QPoint global = header->viewport ()->mapToGlobal (position);
62+ QMouseEvent press (QEvent::MouseButtonPress, position, global,
63+ Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
64+ // A release reports the buttons still held afterwards, which is none.
65+ QMouseEvent release (QEvent::MouseButtonRelease, position, global,
66+ Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
67+ QApplication::sendEvent (header->viewport (), &press);
68+ QApplication::sendEvent (header->viewport (), &release);
69+ }
70+ }
71+
972class QueryResultPresenterTest : public ::testing::Test {
1073protected:
1174 void SetUp () override {
@@ -16,8 +79,9 @@ class QueryResultPresenterTest : public ::testing::Test {
1679 }
1780
1881 // Stands in for a PagedResult; the presenter only ever binds models.
19- static QAbstractItemModel *modelWithRows (const int rows) {
20- auto *model = new QStandardItemModel (rows, 2 );
82+ static SortSpyModel *modelWithRows (const int rows,
83+ std::shared_ptr<SortLog> log = nullptr ) {
84+ auto *model = new SortSpyModel (rows, 2 , std::move (log));
2185 for (int row = 0 ; row < rows; ++row) {
2286 model->setItem (row, 0 , new QStandardItem (QString::number (row)));
2387 model->setItem (row, 1 , new QStandardItem (" name" + QString::number (row)));
@@ -97,3 +161,60 @@ TEST_F(QueryResultPresenterTest, PresentToViewIgnoresMissingModel) {
97161 ASSERT_NE (view.model (), nullptr ) << " a failed preview cleared the view" ;
98162 EXPECT_EQ (view.model ()->rowCount (), 3 );
99163}
164+
165+ // Qt's header starts out on the first column in descending order, and enabling
166+ // sorting sorts the bound model straight away. A paged result answers that by
167+ // re-running its statement wrapped in an ORDER BY, which both reverses the rows
168+ // and makes the database order the whole result set before the first page can
169+ // be shown.
170+ TEST_F (QueryResultPresenterTest, PresentDoesNotSortTheResult) {
171+ const auto log = std::make_shared<SortLog>();
172+
173+ presenter->present ({modelWithRows (3 , log)});
174+
175+ EXPECT_FALSE (log->sorted ()) << " the result was sorted before anyone asked for an order" ;
176+
177+ const auto views = parent->findChildren <QTableView *>();
178+ ASSERT_EQ (views.size (), 1 );
179+ EXPECT_EQ (views.at (0 )->horizontalHeader ()->sortIndicatorSection (), -1 )
180+ << " the result opened claiming to be sorted by a column" ;
181+ }
182+
183+ TEST_F (QueryResultPresenterTest, PresentToViewDoesNotSortTheTable) {
184+ QTableView view (parent.get ());
185+
186+ const auto first = std::make_shared<SortLog>();
187+ const auto second = std::make_shared<SortLog>();
188+
189+ presenter->presentToView (&view, modelWithRows (3 , first));
190+ presenter->presentToView (&view, modelWithRows (4 , second));
191+
192+ EXPECT_FALSE (first->sorted ()) << " the outgoing table was sorted on its way out" ;
193+ EXPECT_FALSE (second->sorted ()) << " the table was sorted before anyone asked for an order" ;
194+ }
195+
196+ TEST_F (QueryResultPresenterTest, FirstClickOnAColumnSortsAscending) {
197+ const auto log = std::make_shared<SortLog>();
198+ presenter->present ({modelWithRows (3 , log)});
199+ const auto views = parent->findChildren <QTableView *>();
200+ ASSERT_EQ (views.size (), 1 );
201+
202+ clickHeaderSection (views.at (0 ), 0 );
203+
204+ ASSERT_FALSE (log->orders .isEmpty ()) << " clicking the header sorted nothing" ;
205+ EXPECT_EQ (log->columns .last (), 0 );
206+ EXPECT_EQ (log->orders .last (), Qt::AscendingOrder);
207+ }
208+
209+ TEST_F (QueryResultPresenterTest, SecondClickOnTheSameColumnSortsDescending) {
210+ const auto log = std::make_shared<SortLog>();
211+ presenter->present ({modelWithRows (3 , log)});
212+ const auto views = parent->findChildren <QTableView *>();
213+ ASSERT_EQ (views.size (), 1 );
214+
215+ clickHeaderSection (views.at (0 ), 0 );
216+ clickHeaderSection (views.at (0 ), 0 );
217+
218+ ASSERT_FALSE (log->orders .isEmpty ()) << " clicking the header sorted nothing" ;
219+ EXPECT_EQ (log->orders .last (), Qt::DescendingOrder);
220+ }
0 commit comments