aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/quick/doc/how-tos/how-to-qml/tst_how-to-qml.cpp
blob: 66dcb208fc6fda40f04c918528ff36abc12c9e2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtCore/qregularexpression.h>
#include <QtTest/QtTest>
#include <QtQml/qqmlapplicationengine.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuick/private/qquickrepeater_p.h>
#include <QtQuickTemplates2/private/qquickdialog_p.h>
#include <QtQuickTemplates2/private/qquickdialogbuttonbox_p.h>
#include <QtQuickTemplates2/private/qquicklabel_p.h>
#include <QtQuickTemplates2/private/qquicktextfield_p.h>
#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
#include <QtQuickControlsTestUtils/private/dialogstestutils_p.h>

QT_BEGIN_NAMESPACE

using namespace QQuickVisualTestUtils;
using namespace QQuickControlsTestUtils;
using namespace QQuickDialogTestUtils;

// Allows us to use test macros outside of test functions.
#define RETURN_IF_FAILED(expression) \
expression; \
if (QTest::currentTestFailed()) \
    return

class tst_HowToQml : public QObject
{
    Q_OBJECT

public:
    tst_HowToQml();

private slots:
    void init();
    void activeFocusDebugging();
    void timePicker();

private:
    QScopedPointer<QPointingDevice> touchScreen = QScopedPointer<QPointingDevice>(QTest::createTouchDevice());
};

tst_HowToQml::tst_HowToQml()
{
    qputenv("QML_NO_TOUCH_COMPRESSION", "1");
}

void tst_HowToQml::init()
{
//    QTest::failOnWarning(QRegularExpression(QStringLiteral(".?")));
}

void tst_HowToQml::activeFocusDebugging()
{
    QQmlApplicationEngine engine;
    engine.loadFromModule("HowToQml", "ActiveFocusDebuggingMain");
    QCOMPARE(engine.rootObjects().size(), 1);

    auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().at(0));
    window->show();
    QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"ActiveFocusDebuggingMain\""));
    QVERIFY(QTest::qWaitForWindowActive(window));

    QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"textField1\""));
    auto *textField1 = window->findChild<QQuickTextField*>("textField1");
    QVERIFY(textField1);
    textField1->forceActiveFocus();
    QVERIFY(textField1->hasActiveFocus());

    QTest::ignoreMessage(QtDebugMsg, QRegularExpression("activeFocusItem: .*\"textField2\""));
    auto *textField2 = window->findChild<QQuickTextField*>("textField2");
    QVERIFY(textField2);
    QTest::keyClick(window, Qt::Key_Tab);
    QVERIFY(textField2->hasActiveFocus());
}

void tst_HowToQml::timePicker()
{
    QQmlApplicationEngine engine;
    engine.loadFromModule("HowToQml", "TimePickerMain");
    QCOMPARE(engine.rootObjects().size(), 1);

    auto *window = qobject_cast<QQuickWindow*>(engine.rootObjects().at(0));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window));

    auto *dialog = window->findChild<QQuickDialog *>();
    QVERIFY(dialog);

    auto *timePicker = dialog->findChild<QQuickItem *>("timePicker");
    QVERIFY(timePicker);

    auto *contentContainer = timePicker->findChild<QQuickItem *>("contentContainer");
    QVERIFY(contentContainer);
    const int contentRadius = contentContainer->property("radius").toReal();

    auto *labelRepeater = timePicker->findChild<QQuickRepeater *>();
    QVERIFY(labelRepeater);

    auto *selectionArm = timePicker->findChild<QQuickItem *>("selectionArm");
    QVERIFY(selectionArm);

    auto *selectionIndicator = selectionArm->findChild<QQuickItem *>("selectionIndicator");
    QVERIFY(selectionIndicator);
    const int selectionIndicatorHeight = selectionIndicator->height();

    auto angleForValue = [](int value) -> int {
        return int((value / 60.0) * 360) % 360;
    };

    // Note that is24HourValue should be true if "value" is a 24-hour value,
    // not if the picker's is24Hour property is true.
    auto labelCenterPosForValue = [&](int value, bool is24HourValue = false) -> QPoint {
        if (value < 0 || value > 60)
            return {};

        const qreal angle = angleForValue(value);

        QTransform transform;
        // Translate to the center.
        transform.translate(contentRadius, contentRadius);
        // Rotate to the correct angle.
        transform.rotate(angle);
        // Go outward.
        const int labelDistance = is24HourValue ? selectionIndicatorHeight * 1.5 : selectionIndicatorHeight * 0.5;
        transform.translate(0, -contentRadius + labelDistance);

        const auto centerPos = transform.map(QPoint(0, 0));
        return centerPos;
    };

    enum Mode {
        Hours,
        Minutes
    };

    const int valuesPerLabelStep = 5;
    const bool TwelveHour = false;
    const bool TwentyFourHour = true;

    // Checks that all the labels are in their expected positions and that they have the correct text.
    auto verifyLabels = [&](Mode expectedMode, bool is24Hour, int callerLineNumber) {
        // When not in 24 hour mode, there are always 12 labels, regardless of whether it's showing hours or minutes.
        const int expectedLabelCount = expectedMode == Hours && is24Hour ? 24 : 12;
        QCOMPARE(labelRepeater->count(), expectedLabelCount);
        for (int i = 0; i < expectedLabelCount; ++i) {
            auto *labelDelegate = labelRepeater->itemAt(i);
            QVERIFY2(labelDelegate, qPrintable(QString::fromLatin1("Expected valid label delegate item at index %1 (caller line %2)")
                .arg(i).arg(callerLineNumber)));
            // Use the waiting variant of the macro because there are opacity animations.
            // TODO: is this causing the failure on line 224?
            QTRY_VERIFY2(qFuzzyCompare(labelDelegate->opacity(), 1.0), qPrintable(QString::fromLatin1(
                "Expected opacity of label delegate %1 at index %2 to be 1 but it's %3 (caller line %4) - QTBUG-118056: actual label delegate at this index is now %5")
                    .arg(QDebug::toString(labelDelegate)).arg(i).arg(labelDelegate->opacity()).arg(callerLineNumber).arg(QDebug::toString(labelRepeater->itemAt(i)))));

            const int expectedValue = (i * valuesPerLabelStep) % 60;
            const int actualValue = labelDelegate->property("value").toInt();
            QVERIFY2(expectedValue == actualValue, qPrintable(QString::fromLatin1(
                "Expected label's value at index %1 to be %2 but it's %3 (caller line %4)")
                .arg(i).arg(expectedValue).arg(actualValue).arg(callerLineNumber)));

            const QString expectedText = QString::number(expectedMode == Hours
                ? (i == 0 ? 12 : (i == 12 ? 0 : i)) : i * valuesPerLabelStep);
            // Use QTRY_VERIFY2 rather than QVERIFY2, because text changes are animated.
            QTRY_VERIFY2(expectedText == labelDelegate->property("text").toString(), qPrintable(QString::fromLatin1(
                "Expected label's text at index %1 to be %2 but it's %3 (caller line %4)").arg(i)
                .arg(expectedText, labelDelegate->property("text").toString(), QString::number(callerLineNumber))));
        }
    };

    auto verifySelectionIndicator = [&](int expectedValue, bool expect24HourValue, int callerLineNumber) {
        const qreal actualRotation = int(selectionArm->rotation()) % 360;
        const qreal expectedRotation = angleForValue(expectedValue);
        QVERIFY2(qFuzzyCompare(actualRotation, expectedRotation), qPrintable(QString::fromLatin1(
            "Expected selection arm's rotation to be %1 for expectedValue %2 but it's %3 (caller line %4)")
            .arg(expectedRotation).arg(expectedValue).arg(actualRotation).arg(callerLineNumber)));

        const QPoint expectedIndicatorCenterPos = labelCenterPosForValue(expectedValue, expect24HourValue);
        const QPoint actualIndicatorCenterPos = selectionIndicator->mapToItem(
            contentContainer, selectionIndicator->boundingRect().center().toPoint()).toPoint();
        const QPoint difference = actualIndicatorCenterPos - expectedIndicatorCenterPos;
        QVERIFY2(difference.x() <= 2 && difference.y() <= 2, qPrintable(QString::fromLatin1(
            "Expected selection indicator's center position to be %1 (with 2 pixels of tolerance) but it's %2 (caller line %3)")
            .arg(QDebug::toString(expectedIndicatorCenterPos), QDebug::toString(actualIndicatorCenterPos)).arg(callerLineNumber)));
    };

    auto valueForHour = [&](int hour) {
        return (hour * valuesPerLabelStep) % 60;
    };

    // Open the picker to hours mode by clicking on the label.
    auto *openDialogLabel = window->findChild<QQuickLabel *>(QString(), Qt::FindDirectChildrenOnly);
    QVERIFY(openDialogLabel);
    QCOMPARE(openDialogLabel->text(), "12:00");
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
    QTRY_COMPARE(dialog->property("opened").toBool(), true);
    // It should show hours.
    RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(0, TwelveHour, __LINE__));

    // Select the 3rd hour.
    const QPoint thirdHourPos = labelCenterPosForValue(valueForHour(3));
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, thirdHourPos));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(3), TwelveHour, __LINE__));
    QCOMPARE(timePicker->property("mode").toInt(), Hours);
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, thirdHourPos));
    QCOMPARE(timePicker->property("hours").toInt(), 3);
    QCOMPARE(timePicker->property("minutes").toInt(), 0);
    // The dialog's values shouldn't change until the dialog has been accepted.
    QCOMPARE(dialog->property("hours").toInt(), 12);
    QCOMPARE(dialog->property("minutes").toInt(), 0);
    // It should be showing minutes now.
    QCOMPARE(timePicker->property("mode").toInt(), Minutes);
    RETURN_IF_FAILED(verifyLabels(Minutes, TwelveHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(0, TwelveHour, __LINE__));
    auto *minutesLabel = dialog->findChild<QQuickLabel *>("minutesLabel");
    QVERIFY(minutesLabel);
    QCOMPARE(minutesLabel->property("dim").toBool(), false);

    // Select the 59th minute.
    const QPoint fiftyNinthMinutePos = labelCenterPosForValue(59);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, fiftyNinthMinutePos));
    RETURN_IF_FAILED(verifySelectionIndicator(59, TwelveHour, __LINE__));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, fiftyNinthMinutePos));
    QCOMPARE(timePicker->property("hours").toInt(), 3);
    QCOMPARE(timePicker->property("minutes").toInt(), 59);
    QCOMPARE(dialog->property("hours").toInt(), 12);
    QCOMPARE(dialog->property("minutes").toInt(), 0);
    // It shouldn't be closed until the OK or Cancel buttons are clicked.
    QCOMPARE(dialog->property("opened").toBool(), true);

    // Accept the dialog to make the changes.
    auto *dialogButtonBox = qobject_cast<QQuickDialogButtonBox *>(dialog->footer());
    QVERIFY(dialogButtonBox);
    auto *okButton = findDialogButton(dialogButtonBox, "OK");
    QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
    QVERIFY(clickButton(okButton));
    QTRY_COMPARE(dialog->property("visible").toBool(), false);
    QCOMPARE(dialog->property("hours").toInt(), 3);
    QCOMPARE(dialog->property("minutes").toInt(), 59);
    QCOMPARE(openDialogLabel->text(), "03:59");

    // Open it again.
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
    QTRY_COMPARE(dialog->property("opened"), true);
    RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(3), TwelveHour, __LINE__));
    // The time label should be unchanged.
    QCOMPARE(openDialogLabel->text(), "03:59");

    // Switch from hours to minutes by clicking on the minutes label.
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(minutesLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(minutesLabel));
    RETURN_IF_FAILED(verifyLabels(Minutes, TwelveHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(59, TwelveHour, __LINE__));

    // Select the 1st minute.
    const QPoint firstMinutePos = labelCenterPosForValue(1);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, firstMinutePos));
    RETURN_IF_FAILED(verifySelectionIndicator(1, TwelveHour, __LINE__));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, firstMinutePos));
    QCOMPARE(timePicker->property("hours").toInt(), 3);
    QCOMPARE(timePicker->property("minutes").toInt(), 1);
    // It shouldn't be closed until the OK or Cancel buttons are clicked.
    QCOMPARE(dialog->property("opened").toBool(), true);

    // Accept the dialog to make the changes.
    QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
    QVERIFY(clickButton(okButton));
    QTRY_COMPARE(dialog->property("visible").toBool(), false);
    QCOMPARE(dialog->property("hours").toInt(), 3);
    QCOMPARE(dialog->property("minutes").toInt(), 1);
    QCOMPARE(openDialogLabel->text(), "03:01");

    // Check that hours and minutes set programmatically on the picker and dialog are respected.
    QVERIFY(dialog->setProperty("hours", QVariant::fromValue(7)));
    QVERIFY(dialog->setProperty("minutes", QVariant::fromValue(8)));
    QCOMPARE(openDialogLabel->text(), "07:08");
    // Open the picker to hours mode by clicking on the label.
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
    QTRY_COMPARE(dialog->property("opened").toBool(), true);
    RETURN_IF_FAILED(verifyLabels(Hours, TwelveHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(7), TwelveHour, __LINE__));
    QCOMPARE(timePicker->property("hours").toInt(), 7);
    QCOMPARE(timePicker->property("minutes").toInt(), 8);

    // Check that cancelling the dialog cancels any changes.
    // Select the fourth hour.
    const QPoint fourthHourPos = labelCenterPosForValue(20);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, fourthHourPos));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(4), TwelveHour, __LINE__));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, fourthHourPos));
    QCOMPARE(timePicker->property("hours").toInt(), 4);
    QCOMPARE(timePicker->property("minutes").toInt(), 8);
    auto *cancelButton = findDialogButton(dialogButtonBox, "Cancel");
    QVERIFY(clickButton(cancelButton));
    QTRY_COMPARE(dialog->property("visible").toBool(), false);
    QCOMPARE(dialog->property("hours").toInt(), 7);
    QCOMPARE(dialog->property("minutes").toInt(), 8);

    // Test that the 24 hour mode works.
    const bool isCi = qgetenv("QTEST_ENVIRONMENT") == "ci"; // QTBUG-122679
    if (isCi)
        qDebug() << "about to set is24hour to true";
    QVERIFY(dialog->setProperty("is24Hour", QVariant::fromValue(true)));
    if (isCi)
        qDebug() << "about to open picker";
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(openDialogLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(openDialogLabel));
    QTRY_COMPARE(dialog->property("opened").toBool(), true);
    QCOMPARE(timePicker->property("mode").toInt(), Hours);
    if (isCi)
        qDebug() << "about to verify labels after switching to 24hr";
    RETURN_IF_FAILED(verifyLabels(Hours, TwentyFourHour, __LINE__));
    // TwelveHour because the selected value (7) is on the 12 hour "ring".
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(7), TwelveHour, __LINE__));
    // It should still show the old time.
    QCOMPARE(timePicker->property("hours").toInt(), 7);
    QCOMPARE(timePicker->property("minutes").toInt(), 8);
    QCOMPARE(dialog->property("hours").toInt(), 7);
    QCOMPARE(dialog->property("minutes").toInt(), 8);

    // Select the 23rd hour.
    const QPoint twentyThirdHourPos = labelCenterPosForValue(valueForHour(11), TwentyFourHour);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, twentyThirdHourPos));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(23), TwentyFourHour, __LINE__));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, twentyThirdHourPos));
    QCOMPARE(timePicker->property("hours").toInt(), 23);
    QCOMPARE(timePicker->property("minutes").toInt(), 8);
    QCOMPARE(dialog->property("hours").toInt(), 7);
    QCOMPARE(dialog->property("minutes").toInt(), 8);
    RETURN_IF_FAILED(verifyLabels(Minutes, TwentyFourHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(8, TwelveHour, __LINE__));

    // Select the 20th minute.
    const QPoint twentiethMinutePos = labelCenterPosForValue(20);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapToWindow(contentContainer, twentiethMinutePos));
    RETURN_IF_FAILED(verifySelectionIndicator(20, TwelveHour, __LINE__));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapToWindow(contentContainer, twentiethMinutePos));
    QCOMPARE(timePicker->property("hours").toInt(), 23);
    QCOMPARE(timePicker->property("minutes").toInt(), 20);

    // Go back to hours and make sure that the selection indicator is correct.
    auto *hoursLabel = dialog->findChild<QQuickLabel *>("hoursLabel");
    QVERIFY(hoursLabel);
    QTest::touchEvent(window, touchScreen.data()).press(0, mapCenterToWindow(hoursLabel));
    QTest::touchEvent(window, touchScreen.data()).release(0, mapCenterToWindow(hoursLabel));
    RETURN_IF_FAILED(verifyLabels(Hours, TwentyFourHour, __LINE__));
    RETURN_IF_FAILED(verifySelectionIndicator(valueForHour(23), TwentyFourHour, __LINE__));

    // Accept.
    QTest::ignoreMessage(QtDebugMsg, "A time was chosen - do something here!");
    QVERIFY(clickButton(okButton));
    QTRY_COMPARE(dialog->property("visible").toBool(), false);
    QCOMPARE(dialog->property("hours").toInt(), 23);
    QCOMPARE(dialog->property("minutes").toInt(), 20);
}

QT_END_NAMESPACE

QTEST_MAIN(tst_HowToQml)

#include "tst_how-to-qml.moc"