SpinBoxTests.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
  9. #include <AzQtComponents/Components/Widgets/SpinBox.h>
  10. #include <QApplication>
  11. #include <QLineEdit>
  12. #include <QWheelEvent>
  13. namespace UnitTest
  14. {
  15. using namespace AzToolsFramework;
  16. // Expose the LineEdit functionality so selection behavior can be more easily tested.
  17. class DoubleSpinBoxWithLineEdit
  18. : public AzQtComponents::DoubleSpinBox
  19. {
  20. public:
  21. // const required as lineEdit() is const
  22. QLineEdit* GetLineEdit() const { return lineEdit(); }
  23. };
  24. // A fixture to help test the int and double spin boxes.
  25. class SpinBoxFixture
  26. : public ToolsApplicationFixture<>
  27. {
  28. public:
  29. void SetUpEditorFixtureImpl() override
  30. {
  31. // note: must set a widget as the active window and add widgets
  32. // as children to ensure focus in/out events fire correctly
  33. m_dummyWidget = AZStd::make_unique<QWidget>();
  34. // Give the test window a valid windowHandle. SpinBox code uses this to access the QScreen
  35. m_dummyWidget->winId();
  36. QApplication::setActiveWindow(m_dummyWidget.get());
  37. m_intSpinBox = AZStd::make_unique<AzQtComponents::SpinBox>();
  38. m_doubleSpinBox = AZStd::make_unique<AzQtComponents::DoubleSpinBox>();
  39. m_doubleSpinBoxWithLineEdit = AZStd::make_unique<DoubleSpinBoxWithLineEdit>();
  40. m_spinBoxes = { m_intSpinBox.get(), m_doubleSpinBox.get(), m_doubleSpinBoxWithLineEdit.get() };
  41. for (auto spinBox : m_spinBoxes)
  42. {
  43. // Polish is required to set up the SpinBoxWatcher event filter
  44. spinBox->ensurePolished();
  45. spinBox->setParent(m_dummyWidget.get());
  46. spinBox->setKeyboardTracking(false);
  47. spinBox->setFocusPolicy(Qt::StrongFocus);
  48. spinBox->clearFocus();
  49. }
  50. }
  51. void TearDownEditorFixtureImpl() override
  52. {
  53. QApplication::setActiveWindow(nullptr);
  54. // Regenerate this list in case any of them were deleted during the test
  55. m_spinBoxes = { m_intSpinBox.get(), m_doubleSpinBox.get(), m_doubleSpinBoxWithLineEdit.get() };
  56. for (auto spinBox : m_spinBoxes)
  57. {
  58. if (spinBox)
  59. {
  60. spinBox->setParent(nullptr);
  61. }
  62. }
  63. m_dummyWidget.reset();
  64. m_doubleSpinBoxWithLineEdit.reset();
  65. m_doubleSpinBox.reset();
  66. m_intSpinBox.reset();
  67. }
  68. QString setupTruncationTest(QString textValue)
  69. {
  70. QString retval;
  71. m_doubleSpinBoxWithLineEdit->setDecimals(7);
  72. m_doubleSpinBoxWithLineEdit->setDisplayDecimals(3);
  73. m_doubleSpinBoxWithLineEdit->setFocus();
  74. m_doubleSpinBoxWithLineEdit->GetLineEdit()->setText(textValue);
  75. m_doubleSpinBoxWithLineEdit->clearFocus();
  76. return m_doubleSpinBoxWithLineEdit->textFromValue(m_doubleSpinBoxWithLineEdit->value());
  77. }
  78. AZStd::unique_ptr<QWidget> m_dummyWidget;
  79. AZStd::unique_ptr<AzQtComponents::SpinBox> m_intSpinBox;
  80. AZStd::unique_ptr<AzQtComponents::DoubleSpinBox> m_doubleSpinBox;
  81. AZStd::unique_ptr<DoubleSpinBoxWithLineEdit> m_doubleSpinBoxWithLineEdit;
  82. AZStd::vector<QAbstractSpinBox*> m_spinBoxes;
  83. };
  84. TEST_F(SpinBoxFixture, SpinBoxesCreated)
  85. {
  86. using ::testing::Ne;
  87. EXPECT_THAT(m_intSpinBox, Ne(nullptr));
  88. EXPECT_THAT(m_doubleSpinBox, Ne(nullptr));
  89. EXPECT_THAT(m_doubleSpinBoxWithLineEdit, Ne(nullptr));
  90. }
  91. TEST_F(SpinBoxFixture, SpinBoxMousePressAndMoveRightScrollsValue)
  92. {
  93. m_doubleSpinBox->setValue(10.0);
  94. const int halfWidgetHeight = m_doubleSpinBox->height() / 2;
  95. const QPoint widgetCenterLeftBorder = m_doubleSpinBox->pos() + QPoint(1, halfWidgetHeight);
  96. // Check we have a valid window setup before moving the cursor
  97. EXPECT_TRUE(m_doubleSpinBox->window()->windowHandle() != nullptr);
  98. // Right in screen space
  99. MousePressAndMove(m_doubleSpinBox.get(), widgetCenterLeftBorder, QPoint(11, 0));
  100. // AzQtComponents::SpinBox::Config.pixelsPerStep is 10
  101. EXPECT_NEAR(m_doubleSpinBox->value(), 11.0, 0.001);
  102. }
  103. TEST_F(SpinBoxFixture, SpinBoxMousePressAndMoveLeftScrollsValue)
  104. {
  105. m_doubleSpinBox->setValue(10.0);
  106. const int halfWidgetHeight = m_doubleSpinBox->height() / 2;
  107. const QPoint widgetCenterLeftBorder = m_doubleSpinBox->pos() + QPoint(1, halfWidgetHeight);
  108. // Check we have a valid window setup before moving the cursor
  109. EXPECT_TRUE(m_doubleSpinBox->window()->windowHandle() != nullptr);
  110. // Left in screen space
  111. MousePressAndMove(m_doubleSpinBox.get(), widgetCenterLeftBorder, QPoint(-11, 0));
  112. // AzQtComponents::SpinBox::Config.pixelsPerStep is 10
  113. EXPECT_NEAR(m_doubleSpinBox->value(), 9.0, 0.001);
  114. }
  115. TEST_F(SpinBoxFixture, SpinBoxKeyboardUpAndDownArrowsChangeValue)
  116. {
  117. m_intSpinBox->setValue(5);
  118. m_intSpinBox->setFocus();
  119. QTest::keyClick(m_intSpinBox.get(), Qt::Key_Up, Qt::NoModifier);
  120. EXPECT_EQ(m_intSpinBox->value(), 6);
  121. QTest::keyClick(m_intSpinBox.get(), Qt::Key_Down, Qt::NoModifier);
  122. QTest::keyClick(m_intSpinBox.get(), Qt::Key_Down, Qt::NoModifier);
  123. EXPECT_EQ(m_intSpinBox->value(), 4);
  124. }
  125. TEST_F(SpinBoxFixture, SpinBoxChangeContentsAndEnterCommitsNewValue)
  126. {
  127. m_doubleSpinBoxWithLineEdit->setValue(10.0);
  128. m_doubleSpinBoxWithLineEdit->setFocus();
  129. m_doubleSpinBoxWithLineEdit->GetLineEdit()->setText(QString("15"));
  130. QTest::keyClick(m_doubleSpinBoxWithLineEdit.get(), Qt::Key_Enter, Qt::NoModifier);
  131. EXPECT_NEAR(m_doubleSpinBoxWithLineEdit->value(), 15.0, 0.001);
  132. }
  133. TEST_F(SpinBoxFixture, SpinBoxChangeContentsAndLoseFocusCommitsNewValue)
  134. {
  135. m_doubleSpinBoxWithLineEdit->setValue(10.0);
  136. m_doubleSpinBoxWithLineEdit->setFocus();
  137. m_doubleSpinBoxWithLineEdit->GetLineEdit()->setText(QString("15"));
  138. m_doubleSpinBoxWithLineEdit->clearFocus();
  139. EXPECT_NEAR(m_doubleSpinBoxWithLineEdit->value(), 15.0, 0.001);
  140. }
  141. TEST_F(SpinBoxFixture, SpinBoxClearContentsAndEscapeReturnsToPreviousValue)
  142. {
  143. m_doubleSpinBoxWithLineEdit->setValue(10.0);
  144. m_doubleSpinBoxWithLineEdit->setFocus();
  145. m_doubleSpinBoxWithLineEdit->GetLineEdit()->clear();
  146. QTest::keyClick(m_doubleSpinBoxWithLineEdit.get(), Qt::Key_Escape, Qt::NoModifier);
  147. EXPECT_NEAR(m_doubleSpinBoxWithLineEdit->value(), 10.0, 0.001);
  148. }
  149. TEST_F(SpinBoxFixture, SpinBoxChangeContentsAndEscapeReturnsToPreviousValue)
  150. {
  151. m_doubleSpinBoxWithLineEdit->setValue(10.0);
  152. m_doubleSpinBoxWithLineEdit->setFocus();
  153. m_doubleSpinBoxWithLineEdit->GetLineEdit()->setText(QString("15"));
  154. QTest::keyClick(m_doubleSpinBoxWithLineEdit.get(), Qt::Key_Escape, Qt::NoModifier);
  155. EXPECT_NEAR(m_doubleSpinBoxWithLineEdit->value(), 10.0, 0.001);
  156. EXPECT_TRUE(m_doubleSpinBoxWithLineEdit->GetLineEdit()->hasSelectedText());
  157. }
  158. TEST_F(SpinBoxFixture, SpinBoxSelectContentsAndEscapeKeepsFocus)
  159. {
  160. m_doubleSpinBox->setValue(10.0);
  161. m_doubleSpinBox->setFocus();
  162. m_doubleSpinBox->selectAll();
  163. QTest::keyClick(m_doubleSpinBox.get(), Qt::Key_Escape, Qt::NoModifier);
  164. EXPECT_TRUE(m_doubleSpinBox->hasFocus());
  165. QTest::keyClick(m_doubleSpinBox.get(), Qt::Key_Escape, Qt::NoModifier);
  166. EXPECT_TRUE(m_doubleSpinBox->hasFocus());
  167. }
  168. TEST_F(SpinBoxFixture, SpinBoxSuffixRemovedAndAppliedWithFocusChange)
  169. {
  170. using testing::StrEq;
  171. QLocale testLocale{ QLocale() };
  172. QString testString = "10" + QString(testLocale.decimalPoint()) + "0";
  173. m_doubleSpinBox->setSuffix("m");
  174. m_doubleSpinBox->setValue(10.0);
  175. // test internal logic (textFromValue() calls private StringValue())
  176. QString value = m_doubleSpinBox->textFromValue(10.0);
  177. EXPECT_THAT(value.toUtf8().constData(), testString);
  178. m_doubleSpinBox->setFocus();
  179. EXPECT_THAT(m_doubleSpinBox->suffix().toUtf8().constData(), StrEq(""));
  180. m_doubleSpinBox->clearFocus();
  181. EXPECT_THAT(m_doubleSpinBox->suffix().toUtf8().constData(), StrEq("m"));
  182. }
  183. // There is logic in our AzQtComponents::SpinBoxWatcher that delays processing of the end of wheel
  184. // events by 100msec, which used to result in a crash if the SpinBox happened to be deleted after
  185. // the timer was started and before it was triggered. This test was added to ensure the new handling
  186. // works correctly by no longer crashing in this scenario.
  187. TEST_F(SpinBoxFixture, SpinBoxClearDelayedWheelTimeoutAfterDelete)
  188. {
  189. // The wheel movement logic won't be triggered unless the SpinBox is focused at the start
  190. m_intSpinBox->setFocus();
  191. // Simulate the mouse wheel scrolling
  192. // The delta for the wheel changing doesn't matter, it just needs to be different
  193. auto delta = QPoint(10, 10);
  194. auto spinBox = m_intSpinBox.get();
  195. QWheelEvent wheelEventBegin(QPoint(), QPoint(), QPoint(), QPoint(), Qt::NoButton, Qt::NoModifier, Qt::ScrollBegin, false);
  196. QWheelEvent wheelEventUpdate(delta, delta, delta, delta, Qt::NoButton, Qt::NoModifier, Qt::ScrollUpdate, false);
  197. QWheelEvent wheelEventEnd(QPoint(), QPoint(), QPoint(), QPoint(), Qt::NoButton, Qt::NoModifier, Qt::ScrollEnd, false);
  198. QApplication::sendEvent(spinBox, &wheelEventBegin);
  199. QApplication::sendEvent(spinBox, &wheelEventUpdate);
  200. QApplication::sendEvent(spinBox, &wheelEventEnd);
  201. // Delete the SpinBox after triggering the mouse wheel scroll
  202. m_intSpinBox.reset();
  203. // The timeout in question is triggered 100msec after the mouse wheel has been moved
  204. // Waiting 200msec here to make sure it has been triggered
  205. QTest::qWait(200);
  206. // Verifying the SpinBox was deleted, although the true verification is that before the fix this
  207. // test would result in a crash
  208. EXPECT_TRUE(m_intSpinBox.get() == nullptr);
  209. }
  210. TEST_F(SpinBoxFixture, SpinBoxCheckHighValueTruncatesCorrectly)
  211. {
  212. QLocale testLocale{ QLocale() };
  213. QString testString = "0" + QString(testLocale.decimalPoint()) + "9999999";
  214. QString value = setupTruncationTest(testString);
  215. testString = "1" + QString(testLocale.decimalPoint()) + "0";
  216. EXPECT_TRUE(value == testString);
  217. }
  218. TEST_F(SpinBoxFixture, SpinBoxCheckLowValueTruncatesCorrectly)
  219. {
  220. QLocale testLocale{ QLocale() };
  221. QString testString = "0" + QString(testLocale.decimalPoint()) + "0000001";
  222. QString value = setupTruncationTest(testString);
  223. testString = "0" + QString(testLocale.decimalPoint()) + "0";
  224. EXPECT_TRUE(value == testString);
  225. }
  226. TEST_F(SpinBoxFixture, SpinBoxCheckBugValuesTruncatesCorrectly)
  227. {
  228. QLocale testLocale{ QLocale() };
  229. QString testString = "0" + QString(testLocale.decimalPoint()) + "12395";
  230. QString value = setupTruncationTest(testString);
  231. testString = "0" + QString(testLocale.decimalPoint()) + "124";
  232. EXPECT_TRUE(value == testString);
  233. testString = "0" + QString(testLocale.decimalPoint()) + "94496";
  234. value = setupTruncationTest(testString);
  235. testString = "0" + QString(testLocale.decimalPoint()) + "945";
  236. EXPECT_TRUE(value == testString);
  237. testString = "0" + QString(testLocale.decimalPoint()) + "0009999";
  238. value = setupTruncationTest(testString);
  239. testString = "0" + QString(testLocale.decimalPoint()) + "001";
  240. EXPECT_TRUE(value == testString);
  241. }
  242. } // namespace UnitTest