13#include <QAbstractItemView>
16#include <QContextMenuEvent>
20#include <QStandardItemModel>
36 const auto A =
"a"_qs;
37 const auto B =
"b"_qs;
38 const auto C =
"c"_qs;
39 const auto Z =
"z"_qs;
43 QStandardItemModel TagsModel_;
46 CategorySelector Selector_;
47 QList<TagsChange> Edits_;
51 for (
const auto& tag : { A, B, C })
52 TagsModel_.appendRow (
new QStandardItem { tag });
54 Addon_.BindSelector (Selector_);
55 QObject::connect (&Addon_,
58 [
this] (
const TagsChange& change) { Edits_ << change; });
61 static bool SetChecked (
const CategorySelector& selector,
const QString& tag, Qt::CheckState state)
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);
68 bool SetChecked (
const QString& tag, Qt::CheckState state)
const
70 return SetChecked (Selector_, tag, state);
75 emit Edit_.editingFinished ();
78 QCompleter& Completer ()
const
80 return *Addon_.findChild<QCompleter*> ();
83 void Complete (
const QString& completion)
85 emit Completer ().activated (completion);
92 qputenv (
"QT_QPA_PLATFORM",
"offscreen");
95 void TagsEditorAddonTest::testSelectorChoiceIsReported ()
98 f.Addon_.SetTags ({ A, B });
101 QVERIFY (f.SetChecked (B, Qt::Unchecked));
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 });
110 void TagsEditorAddonTest::testSelectorFollowsNonCanonicalText ()
113 f.Edit_.setText (
"a;b"_qs);
114 QCOMPARE (
f.Selector_.GetSelections (), (QStringList { A, B }));
116 QVERIFY (
f.SetChecked (C, Qt::Checked));
117 QCOMPARE (
f.Edit_.text (),
"a; b; c"_qs);
120 void TagsEditorAddonTest::testSelectorChoiceKeepsUnknownTokens ()
123 f.Edit_.setText (
"a; zzz"_qs);
124 QCOMPARE (
f.Selector_.GetSelections (), QStringList { A });
126 QVERIFY (
f.SetChecked (B, Qt::Checked));
127 QCOMPARE (
f.Edit_.text (),
"a; b; zzz"_qs);
129 QVERIFY (
f.SetChecked (A, Qt::Unchecked));
130 QCOMPARE (
f.Edit_.text (),
"b; zzz"_qs);
133 void TagsEditorAddonTest::testSetTagsSyncsTheSelectorWithBlockedSignals ()
136 f.Addon_.SetTags ({ A });
138 const QSignalBlocker blocker { &
f.Edit_ };
139 f.Addon_.SetTags ({ B });
141 QCOMPARE (
f.Selector_.GetSelections (), QStringList { B });
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 }));
150 void TagsEditorAddonTest::testNestedSetTagsWins ()
156 QObject::connect (&
f.Edit_, &QLineEdit::textChanged, &
f.Edit_, [&]
158 if (!std::exchange (nested, true))
159 f.Addon_.SetTags ({ B });
161 f.Addon_.SetTags ({ A });
162 QCOMPARE (
f.Edit_.text (), B);
163 QCOMPARE (
f.Selector_.GetSelections (), QStringList { B });
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 });
171 void TagsEditorAddonTest::testUnchangedTagsAreNotReported ()
174 f.Addon_.SetTags ({ A, B });
177 f.Edit_.setText (
"b; a"_qs);
179 f.Edit_.setText (
"a;b"_qs);
181 QVERIFY (
f.Edits_.isEmpty ());
184 void TagsEditorAddonTest::testSetTagsIsNeverReported ()
187 f.Addon_.SetTags ({ A });
190 QObject::connect (&
f.Edit_, &QLineEdit::textChanged, &
f.Edit_, [&f] { f.FinishEditing (); });
191 f.Addon_.SetTags ({ B });
192 QVERIFY (
f.Edits_.isEmpty ());
195 void TagsEditorAddonTest::testHandEditIsReportedOnce ()
198 f.Addon_.SetTags ({ A });
200 f.Edit_.setText (
"a; c"_qs);
202 QCOMPARE (
f.Edits_.size (), 1);
203 QCOMPARE (
f.Edits_.last ().Before_, QStringList { A });
204 QCOMPARE (
f.Edits_.last ().After_, (QStringList { A, C }));
207 QCOMPARE (
f.Edits_.size (), 1);
211 QCOMPARE (
f.Edits_.size (), 2);
212 QCOMPARE (
f.Edits_.last ().Before_, (QStringList { A, C }));
213 QCOMPARE (
f.Edits_.last ().After_, QStringList { A });
216 void TagsEditorAddonTest::testUpdateTagsKeepsAnEditInProgress ()
219 f.Addon_.SetTags ({ A });
221 f.Addon_.UpdateTags ({ A, Z });
222 QCOMPARE (
f.Edit_.text (),
"a; z"_qs);
223 QVERIFY (
f.Edits_.isEmpty ());
225 f.Edit_.setText (
"a; z; c"_qs);
226 f.Addon_.UpdateTags ({ A });
227 QCOMPARE (
f.Edit_.text (),
"a; z; c"_qs);
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 }));
235 void TagsEditorAddonTest::testCompletionReplacesTheTokenUnderCursor_data ()
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");
244 const auto standard =
"; "_qs;
245 const auto music =
"music"_qs;
246 constexpr int atEnd = -1;
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;
263 void TagsEditorAddonTest::testCompletionReplacesTheTokenUnderCursor ()
265 QFETCH (QString, separator);
266 QFETCH (QString, text);
267 QFETCH (
int, cursor);
268 QFETCH (QString, completion);
269 QFETCH (QString, expected);
270 QFETCH (
int, expectedCursor);
273 f.Addon_.SetSeparator (separator);
274 f.Edit_.setText (text);
275 f.Edit_.setCursorPosition (cursor < 0 ?
static_cast<int> (text.size ()) : cursor);
277 QSignalSpy picked { &
f.Addon_, &TagsEditorAddon::tagPicked };
278 f.Complete (completion);
280 QCOMPARE (
f.Edit_.text (), expected);
281 QCOMPARE (
f.Edit_.cursorPosition (), expectedCursor);
282 QCOMPARE (picked.size (), 1);
285 void TagsEditorAddonTest::testEnterAcceptsTheCompletion ()
289 QSignalSpy finished { &
f.Edit_, &QLineEdit::editingFinished };
291 QTest::keyClicks (&
f.Edit_, C);
292 const auto popup =
f.Completer ().popup ();
293 QVERIFY (popup->isVisible ());
294 QCOMPARE (popup->currentIndex ().data ().toString (), C);
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 });
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);
311 void TagsEditorAddonTest::testCursorMoveRefreshesCompletion ()
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);
321 QTest::keyClick (&
f.Edit_, Qt::Key_Home);
322 QVERIFY (popup->isVisible ());
323 QCOMPARE (popup->currentIndex ().data ().toString (), B);
325 QTest::keyClick (popup, Qt::Key_Return);
326 QCOMPARE (
f.Edit_.text (),
"b; c"_qs);
329 void TagsEditorAddonTest::testCompletionIgnoresCase ()
331 const auto work =
"Work"_qs;
334 f.TagsModel_.appendRow (
new QStandardItem { work });
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 });
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);
352 void TagsEditorAddonTest::testInnerWhitespaceIsKept ()
354 const auto group =
"Work friends"_qs;
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 });
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 });
369 void TagsEditorAddonTest::testSelectorFollowsTheTagsModel ()
372 f.Addon_.SetTags ({ A,
"d"_qs });
373 QCOMPARE (
f.Selector_.GetSelections (), QStringList { A });
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);
381 QVERIFY (
f.SetChecked (B, Qt::Checked));
382 QCOMPARE (
f.Edit_.text (),
"a; b; d"_qs);
383 QCOMPARE (picked.size (), 1);
386 void TagsEditorAddonTest::testSelectorButtonBindsAPopup ()
389 QVERIFY (!
f.Edit_.findChild<QToolButton*> ());
392 auto& addon = *
new TagsEditorAddon { edit,
f.TagsModel_ };
393 addon.SetTags ({ A });
395 const auto popup = edit.findChild<CategorySelectorWindow*> ();
397 QVERIFY (!popup->isVisible ());
398 QCOMPARE (popup->Selector ().GetSelections (), QStringList { A });
400 edit.findChild<QToolButton*> ()->click ();
401 QVERIFY (popup->isVisible ());
403 QVERIFY (Fixture::SetChecked (popup->Selector (), C, Qt::Checked));
404 QCOMPARE (edit.text (),
"a; c"_qs);
407 void TagsEditorAddonTest::testOneLiveSelectorAtATime ()
410 CategorySelector other;
411 QVERIFY_THROWS_EXCEPTION (std::logic_error,
f.Addon_.BindSelector (other));
412 QVERIFY_THROWS_EXCEPTION (std::logic_error,
f.Addon_.AddSelectorButton ());
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 ());
419 delete edit.findChild<CategorySelectorWindow*> ();
420 addon.BindSelector (other);
421 QVERIFY (Fixture::SetChecked (other, A, Qt::Checked));
422 QCOMPARE (edit.text (), A);
425 void TagsEditorAddonTest::testContextMenuOffersTheSelector ()
429 new TagsEditorAddon { edit,
f.TagsModel_ };
432 QContextMenuEvent
event { QContextMenuEvent::Mouse, { 1, 1 }, edit.mapToGlobal (QPoint { 1, 1 }) };
433 QApplication::sendEvent (&edit, &event);
435 const auto menu = edit.findChild<QMenu*> (Qt::FindDirectChildrenOnly);
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); }));
443 const auto popup = edit.findChild<CategorySelectorWindow*> ();
444 QVERIFY (!popup->isVisible ());
445 actions.at (0)->trigger ();
446 QVERIFY (popup->isVisible ());
450 void TagsEditorAddonTest::testContextMenuIsStandardWithoutAPopup ()
455 QContextMenuEvent
event { QContextMenuEvent::Mouse, { 1, 1 },
f.Edit_.mapToGlobal (QPoint { 1, 1 }) };
456 QApplication::sendEvent (&
f.Edit_, &event);
458 const auto menu =
f.Edit_.findChild<QMenu*> (Qt::FindDirectChildrenOnly);
460 QVERIFY (std::ranges::none_of (menu->actions (), [] (
const QAction *action) { return action->text () ==
"Select tags..."_qs; }));
464 void TagsEditorAddonTest::testAltDownOpensTheSelector ()
468 new TagsEditorAddon { edit,
f.TagsModel_ };
470 QVERIFY (QTest::qWaitForWindowActive (&edit));
473 const auto popup = edit.findChild<CategorySelectorWindow*> ();
474 QVERIFY (!popup->isVisible ());
475 QTest::keyClick (&edit, Qt::Key_Down, Qt::AltModifier);
476 QVERIFY (popup->isVisible ());
constexpr detail::ExprTree< detail::ExprType::LeafStaticPlaceholder, detail::MemberPtrs< Ptr > > f