LeechCraft 0.6.70-19424-g027e980fd1
Modular cross-platform feature rich live environment.
Loading...
Searching...
No Matches
tagseditoraddontest.cpp
Go to the documentation of this file.
1/**********************************************************************
2 * LeechCraft - modular cross-platform feature rich internet client.
3 * Copyright (C) 2006-2014 Georg Rudoy
4 *
5 * Distributed under the Boost Software License, Version 1.0.
6 * (See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
7 **********************************************************************/
8
10#include <algorithm>
11#include <stdexcept>
12#include <utility>
13#include <QAbstractItemView>
14#include <QAction>
15#include <QCompleter>
16#include <QContextMenuEvent>
17#include <QLineEdit>
18#include <QMenu>
19#include <QSignalSpy>
20#include <QStandardItemModel>
21#include <QTest>
22#include <QToolButton>
23#include <QTreeView>
24#include <util/sll/qtutil.h>
25#include "categoryselector.h"
27#include "tagschange.h"
28#include "tagseditoraddon.h"
29
31
32namespace LC::Util
33{
34 namespace
35 {
36 const auto A = "a"_qs;
37 const auto B = "b"_qs;
38 const auto C = "c"_qs;
39 const auto Z = "z"_qs;
40
41 struct Fixture
42 {
43 QStandardItemModel TagsModel_;
44 QLineEdit Edit_;
45 TagsEditorAddon& Addon_ = *new TagsEditorAddon { Edit_, TagsModel_, TagsEditorAddon::Flag::NoSelectorButton };
46 CategorySelector Selector_;
47 QList<TagsChange> Edits_;
48
49 Fixture ()
50 {
51 for (const auto& tag : { A, B, C })
52 TagsModel_.appendRow (new QStandardItem { tag });
53
54 Addon_.BindSelector (Selector_);
55 QObject::connect (&Addon_,
57 &Addon_,
58 [this] (const TagsChange& change) { Edits_ << change; });
59 }
60
61 static bool SetChecked (const CategorySelector& selector, const QString& tag, Qt::CheckState state)
62 {
63 const auto model = selector.findChild<QTreeView*> ()->model ();
64 const auto& found = model->match (model->index (0, 0), Qt::DisplayRole, tag, 1, Qt::MatchExactly);
65 return !found.isEmpty () && model->setData (found.front (), state, Qt::CheckStateRole);
66 }
67
68 bool SetChecked (const QString& tag, Qt::CheckState state) const
69 {
70 return SetChecked (Selector_, tag, state);
71 }
72
73 void FinishEditing ()
74 {
75 emit Edit_.editingFinished ();
76 }
77
78 QCompleter& Completer () const
79 {
80 return *Addon_.findChild<QCompleter*> ();
81 }
82
83 void Complete (const QString& completion)
84 {
85 emit Completer ().activated (completion);
86 }
87 };
88 }
89
91 {
92 qputenv ("QT_QPA_PLATFORM", "offscreen");
93 }
94
95 void TagsEditorAddonTest::testSelectorChoiceIsReported ()
96 {
97 Fixture f;
98 f.Addon_.SetTags ({ A, B });
99
100 QSignalSpy picked { &f.Addon_, &TagsEditorAddon::tagPicked };
101 QVERIFY (f.SetChecked (B, Qt::Unchecked));
102
103 QCOMPARE (f.Edit_.text (), A);
104 QCOMPARE (picked.size (), 1);
105 QCOMPARE (f.Edits_.size (), 1);
106 QCOMPARE (f.Edits_.last ().Before_, (QStringList { A, B }));
107 QCOMPARE (f.Edits_.last ().After_, QStringList { A });
108 }
109
110 void TagsEditorAddonTest::testSelectorFollowsNonCanonicalText ()
111 {
112 Fixture f;
113 f.Edit_.setText ("a;b"_qs);
114 QCOMPARE (f.Selector_.GetSelections (), (QStringList { A, B }));
115
116 QVERIFY (f.SetChecked (C, Qt::Checked));
117 QCOMPARE (f.Edit_.text (), "a; b; c"_qs);
118 }
119
120 void TagsEditorAddonTest::testSelectorChoiceKeepsUnknownTokens ()
121 {
122 Fixture f;
123 f.Edit_.setText ("a; zzz"_qs);
124 QCOMPARE (f.Selector_.GetSelections (), QStringList { A });
125
126 QVERIFY (f.SetChecked (B, Qt::Checked));
127 QCOMPARE (f.Edit_.text (), "a; b; zzz"_qs);
128
129 QVERIFY (f.SetChecked (A, Qt::Unchecked));
130 QCOMPARE (f.Edit_.text (), "b; zzz"_qs);
131 }
132
133 void TagsEditorAddonTest::testSetTagsSyncsTheSelectorWithBlockedSignals ()
134 {
135 Fixture f;
136 f.Addon_.SetTags ({ A });
137 {
138 const QSignalBlocker blocker { &f.Edit_ };
139 f.Addon_.SetTags ({ B });
140 }
141 QCOMPARE (f.Selector_.GetSelections (), QStringList { B });
142
143 QVERIFY (f.SetChecked (C, Qt::Checked));
144 QCOMPARE (f.Edit_.text (), "b; c"_qs);
145 QCOMPARE (f.Edits_.size (), 1);
146 QCOMPARE (f.Edits_.last ().Before_, QStringList { B });
147 QCOMPARE (f.Edits_.last ().After_, (QStringList { B, C }));
148 }
149
150 void TagsEditorAddonTest::testNestedSetTagsWins ()
151 {
152 Fixture f;
153
154 // Like a text change handler answering with tags of its own.
155 auto nested = false;
156 QObject::connect (&f.Edit_, &QLineEdit::textChanged, &f.Edit_, [&]
157 {
158 if (!std::exchange (nested, true))
159 f.Addon_.SetTags ({ B });
160 });
161 f.Addon_.SetTags ({ A });
162 QCOMPARE (f.Edit_.text (), B);
163 QCOMPARE (f.Selector_.GetSelections (), QStringList { B });
164
165 QVERIFY (f.SetChecked (C, Qt::Checked));
166 QCOMPARE (f.Edit_.text (), "b; c"_qs);
167 QCOMPARE (f.Edits_.size (), 1);
168 QCOMPARE (f.Edits_.last ().Before_, QStringList { B });
169 }
170
171 void TagsEditorAddonTest::testUnchangedTagsAreNotReported ()
172 {
173 Fixture f;
174 f.Addon_.SetTags ({ A, B });
175
176 f.FinishEditing ();
177 f.Edit_.setText ("b; a"_qs);
178 f.FinishEditing ();
179 f.Edit_.setText ("a;b"_qs);
180 f.FinishEditing ();
181 QVERIFY (f.Edits_.isEmpty ());
182 }
183
184 void TagsEditorAddonTest::testSetTagsIsNeverReported ()
185 {
186 Fixture f;
187 f.Addon_.SetTags ({ A });
188
189 // Like a text change handler taking the focus away.
190 QObject::connect (&f.Edit_, &QLineEdit::textChanged, &f.Edit_, [&f] { f.FinishEditing (); });
191 f.Addon_.SetTags ({ B });
192 QVERIFY (f.Edits_.isEmpty ());
193 }
194
195 void TagsEditorAddonTest::testHandEditIsReportedOnce ()
196 {
197 Fixture f;
198 f.Addon_.SetTags ({ A });
199
200 f.Edit_.setText ("a; c"_qs);
201 f.FinishEditing ();
202 QCOMPARE (f.Edits_.size (), 1);
203 QCOMPARE (f.Edits_.last ().Before_, QStringList { A });
204 QCOMPARE (f.Edits_.last ().After_, (QStringList { A, C }));
205
206 f.FinishEditing ();
207 QCOMPARE (f.Edits_.size (), 1);
208
209 f.Edit_.setText (A);
210 f.FinishEditing ();
211 QCOMPARE (f.Edits_.size (), 2);
212 QCOMPARE (f.Edits_.last ().Before_, (QStringList { A, C }));
213 QCOMPARE (f.Edits_.last ().After_, QStringList { A });
214 }
215
216 void TagsEditorAddonTest::testUpdateTagsKeepsAnEditInProgress ()
217 {
218 Fixture f;
219 f.Addon_.SetTags ({ A });
220
221 f.Addon_.UpdateTags ({ A, Z });
222 QCOMPARE (f.Edit_.text (), "a; z"_qs);
223 QVERIFY (f.Edits_.isEmpty ());
224
225 f.Edit_.setText ("a; z; c"_qs);
226 f.Addon_.UpdateTags ({ A });
227 QCOMPARE (f.Edit_.text (), "a; z; c"_qs);
228
229 f.FinishEditing ();
230 QCOMPARE (f.Edits_.size (), 1);
231 QCOMPARE (f.Edits_.last ().Before_, (QStringList { A, Z }));
232 QCOMPARE (f.Edits_.last ().After_, (QStringList { A, Z, C }));
233 }
234
235 void TagsEditorAddonTest::testCompletionReplacesTheTokenUnderCursor_data ()
236 {
237 QTest::addColumn<QString> ("separator");
238 QTest::addColumn<QString> ("text");
239 QTest::addColumn<int> ("cursor");
240 QTest::addColumn<QString> ("completion");
241 QTest::addColumn<QString> ("expected");
242 QTest::addColumn<int> ("expectedCursor");
243
244 const auto standard = "; "_qs;
245 const auto music = "music"_qs;
246 constexpr int atEnd = -1;
247
248 QTest::newRow ("appending") << standard << "a; b; mu"_qs << atEnd << music << "a; b; music"_qs << 11;
249 QTest::newRow ("appending without a space") << standard << "a; b;mu"_qs << atEnd << music << "a; b;music"_qs << 10;
250 QTest::newRow ("adjacent separators") << standard << "a;;mu"_qs << atEnd << music << "a;;music"_qs << 8;
251 QTest::newRow ("first token") << standard << "mu; b"_qs << 2 << music << "music; b"_qs << 5;
252 QTest::newRow ("middle token") << standard << "a; mu; c"_qs << 5 << music << "a; music; c"_qs << 8;
253 QTest::newRow ("the only token") << standard << "mu"_qs << atEnd << music << music << 5;
254 QTest::newRow ("after a separator") << standard << "a; b; "_qs << atEnd << C << "a; b; c"_qs << 7;
255 QTest::newRow ("empty") << standard << QString {} << atEnd << C << C << 1;
256 QTest::newRow ("space before the separator") << standard << "mu ; book"_qs << 3 << music << "music; book"_qs << 5;
257 QTest::newRow ("space before the separator of a middle token") << standard << "a; b ; c"_qs << 5 << music << "a; music; c"_qs << 8;
258 QTest::newRow ("between the separator and its space") << standard << "a; b"_qs << 2 << "bookstore"_qs << "a; bookstore"_qs << 12;
259 QTest::newRow ("regex metachar separator") << "|"_qs << "left|mu"_qs << atEnd << music << "left|music"_qs << 10;
260 QTest::newRow ("whitespace separator") << " "_qs << "a mu"_qs << atEnd << music << "a music"_qs << 7;
261 }
262
263 void TagsEditorAddonTest::testCompletionReplacesTheTokenUnderCursor ()
264 {
265 QFETCH (QString, separator);
266 QFETCH (QString, text);
267 QFETCH (int, cursor);
268 QFETCH (QString, completion);
269 QFETCH (QString, expected);
270 QFETCH (int, expectedCursor);
271
272 Fixture f;
273 f.Addon_.SetSeparator (separator);
274 f.Edit_.setText (text);
275 f.Edit_.setCursorPosition (cursor < 0 ? static_cast<int> (text.size ()) : cursor);
276
277 QSignalSpy picked { &f.Addon_, &TagsEditorAddon::tagPicked };
278 f.Complete (completion);
279
280 QCOMPARE (f.Edit_.text (), expected);
281 QCOMPARE (f.Edit_.cursorPosition (), expectedCursor);
282 QCOMPARE (picked.size (), 1);
283 }
284
285 void TagsEditorAddonTest::testEnterAcceptsTheCompletion ()
286 {
287 Fixture f;
288 f.Edit_.show ();
289 QSignalSpy finished { &f.Edit_, &QLineEdit::editingFinished };
290
291 QTest::keyClicks (&f.Edit_, C);
292 const auto popup = f.Completer ().popup ();
293 QVERIFY (popup->isVisible ());
294 QCOMPARE (popup->currentIndex ().data ().toString (), C);
295
296 QTest::keyClick (popup, Qt::Key_Return);
297 QVERIFY (!popup->isVisible ());
298 QCOMPARE (f.Edit_.text (), C);
299 QCOMPARE (finished.size (), 0);
300 QCOMPARE (f.Edits_.size (), 1);
301 QCOMPARE (f.Edits_.last ().After_, QStringList { C });
302
303 QTest::keyClicks (&f.Edit_, "; b"_qs);
304 QVERIFY (popup->isVisible ());
305 QTest::keyClick (popup, Qt::Key_Escape);
306 QVERIFY (!popup->isVisible ());
307 QCOMPARE (f.Edit_.text (), "c; b"_qs);
308 QCOMPARE (f.Edits_.size (), 1);
309 }
310
311 void TagsEditorAddonTest::testCursorMoveRefreshesCompletion ()
312 {
313 Fixture f;
314 f.Edit_.show ();
315
316 QTest::keyClicks (&f.Edit_, "b; c"_qs);
317 const auto popup = f.Completer ().popup ();
318 QVERIFY (popup->isVisible ());
319 QCOMPARE (popup->currentIndex ().data ().toString (), C);
320
321 QTest::keyClick (&f.Edit_, Qt::Key_Home);
322 QVERIFY (popup->isVisible ());
323 QCOMPARE (popup->currentIndex ().data ().toString (), B);
324
325 QTest::keyClick (popup, Qt::Key_Return);
326 QCOMPARE (f.Edit_.text (), "b; c"_qs);
327 }
328
329 void TagsEditorAddonTest::testCompletionIgnoresCase ()
330 {
331 const auto work = "Work"_qs;
332
333 Fixture f;
334 f.TagsModel_.appendRow (new QStandardItem { work });
335 f.Edit_.show ();
336
337 QTest::keyClicks (&f.Edit_, "wo"_qs);
338 const auto popup = f.Completer ().popup ();
339 QVERIFY (popup->isVisible ());
340 QCOMPARE (popup->currentIndex ().data ().toString (), work);
341 QTest::keyClick (popup, Qt::Key_Return);
342 QCOMPARE (f.Edit_.text (), work);
343 QCOMPARE (f.Edits_.last ().After_, QStringList { work });
344
345 QTest::keyClicks (&f.Edit_, "; B"_qs);
346 QVERIFY (popup->isVisible ());
347 QCOMPARE (popup->currentIndex ().data ().toString (), B);
348 QTest::keyClick (popup, Qt::Key_Return);
349 QCOMPARE (f.Edit_.text (), "Work; b"_qs);
350 }
351
352 void TagsEditorAddonTest::testInnerWhitespaceIsKept ()
353 {
354 const auto group = "Work friends"_qs;
355
356 Fixture f;
357 f.TagsModel_.appendRow (new QStandardItem { group });
358 f.Addon_.SetTags ({ group });
359 QCOMPARE (f.Addon_.GetTags (), QStringList { group });
360 QCOMPARE (f.Selector_.GetSelections (), QStringList { group });
361
362 QVERIFY (f.SetChecked (group, Qt::Unchecked));
363 QVERIFY (f.Edit_.text ().isEmpty ());
364 QVERIFY (f.SetChecked (group, Qt::Checked));
365 QCOMPARE (f.Edit_.text (), group);
366 QCOMPARE (f.Edits_.last ().After_, QStringList { group });
367 }
368
369 void TagsEditorAddonTest::testSelectorFollowsTheTagsModel ()
370 {
371 Fixture f;
372 f.Addon_.SetTags ({ A, "d"_qs });
373 QCOMPARE (f.Selector_.GetSelections (), QStringList { A });
374
375 QSignalSpy picked { &f.Addon_, &TagsEditorAddon::tagPicked };
376 f.TagsModel_.appendRow (new QStandardItem { "d"_qs });
377 QCOMPARE (f.Selector_.GetSelections (), (QStringList { A, "d"_qs }));
378 QCOMPARE (f.Edit_.text (), "a; d"_qs);
379 QCOMPARE (picked.size (), 0);
380
381 QVERIFY (f.SetChecked (B, Qt::Checked));
382 QCOMPARE (f.Edit_.text (), "a; b; d"_qs);
383 QCOMPARE (picked.size (), 1);
384 }
385
386 void TagsEditorAddonTest::testSelectorButtonBindsAPopup ()
387 {
388 Fixture f;
389 QVERIFY (!f.Edit_.findChild<QToolButton*> ());
390
391 QLineEdit edit;
392 auto& addon = *new TagsEditorAddon { edit, f.TagsModel_ };
393 addon.SetTags ({ A });
394
395 const auto popup = edit.findChild<CategorySelectorWindow*> ();
396 QVERIFY (popup);
397 QVERIFY (!popup->isVisible ());
398 QCOMPARE (popup->Selector ().GetSelections (), QStringList { A });
399
400 edit.findChild<QToolButton*> ()->click ();
401 QVERIFY (popup->isVisible ());
402
403 QVERIFY (Fixture::SetChecked (popup->Selector (), C, Qt::Checked));
404 QCOMPARE (edit.text (), "a; c"_qs);
405 }
406
407 void TagsEditorAddonTest::testOneLiveSelectorAtATime ()
408 {
409 Fixture f;
410 CategorySelector other;
411 QVERIFY_THROWS_EXCEPTION (std::logic_error, f.Addon_.BindSelector (other));
412 QVERIFY_THROWS_EXCEPTION (std::logic_error, f.Addon_.AddSelectorButton ());
413
414 QLineEdit edit;
415 auto& addon = *new TagsEditorAddon { edit, f.TagsModel_ };
416 QVERIFY_THROWS_EXCEPTION (std::logic_error, addon.BindSelector (other));
417 QVERIFY_THROWS_EXCEPTION (std::logic_error, addon.AddSelectorButton ());
418
419 delete edit.findChild<CategorySelectorWindow*> ();
420 addon.BindSelector (other);
421 QVERIFY (Fixture::SetChecked (other, A, Qt::Checked));
422 QCOMPARE (edit.text (), A);
423 }
424
425 void TagsEditorAddonTest::testContextMenuOffersTheSelector ()
426 {
427 Fixture f;
428 QLineEdit edit;
429 new TagsEditorAddon { edit, f.TagsModel_ };
430 edit.show ();
431
432 QContextMenuEvent event { QContextMenuEvent::Mouse, { 1, 1 }, edit.mapToGlobal (QPoint { 1, 1 }) };
433 QApplication::sendEvent (&edit, &event);
434
435 const auto menu = edit.findChild<QMenu*> (Qt::FindDirectChildrenOnly);
436 QVERIFY (menu);
437 QVERIFY (menu->isVisible ());
438 const auto& actions = menu->actions ();
439 QCOMPARE (actions.at (0)->text (), "Select tags..."_qs);
440 QVERIFY (actions.at (1)->isSeparator ());
441 QVERIFY (std::ranges::any_of (actions, [] (const QAction *action) { return action->text ().startsWith ("Select All"_qs); }));
442
443 const auto popup = edit.findChild<CategorySelectorWindow*> ();
444 QVERIFY (!popup->isVisible ());
445 actions.at (0)->trigger ();
446 QVERIFY (popup->isVisible ());
447 menu->close ();
448 }
449
450 void TagsEditorAddonTest::testContextMenuIsStandardWithoutAPopup ()
451 {
452 Fixture f;
453 f.Edit_.show ();
454
455 QContextMenuEvent event { QContextMenuEvent::Mouse, { 1, 1 }, f.Edit_.mapToGlobal (QPoint { 1, 1 }) };
456 QApplication::sendEvent (&f.Edit_, &event);
457
458 const auto menu = f.Edit_.findChild<QMenu*> (Qt::FindDirectChildrenOnly);
459 QVERIFY (menu);
460 QVERIFY (std::ranges::none_of (menu->actions (), [] (const QAction *action) { return action->text () == "Select tags..."_qs; }));
461 menu->close ();
462 }
463
464 void TagsEditorAddonTest::testAltDownOpensTheSelector ()
465 {
466 Fixture f;
467 QLineEdit edit;
468 new TagsEditorAddon { edit, f.TagsModel_ };
469 edit.show ();
470 QVERIFY (QTest::qWaitForWindowActive (&edit));
471 edit.setFocus ();
472
473 const auto popup = edit.findChild<CategorySelectorWindow*> ();
474 QVERIFY (!popup->isVisible ());
475 QTest::keyClick (&edit, Qt::Key_Down, Qt::AltModifier);
476 QVERIFY (popup->isVisible ());
477 }
478}
void tagsCommitted(const TagsChange &change)
Emitted when the user has changed the tags, by a pick or by editing the text and then pressing Enter ...
void tagPicked()
Emitted after the text has been updated with a pick made in the selector or the completer,...
constexpr detail::ExprTree< detail::ExprType::LeafStaticPlaceholder, detail::MemberPtrs< Ptr > > f
Definition oral.h:1079