// End-to-end gesture tests: real Keyboard.qml, synthetic touch events. // Set SLOPOSK_SNAPSHOT_DIR to also save what the keyboard looks like mid-gesture. #include "FakeEngine.h" #include "input/KeyboardController.h" #include "input/KeyboardSurface.h" #include "layout/LayoutModel.h" #include "TestLayouts.h" #include #include #include #include #include #include #include Q_IMPORT_QML_PLUGIN(SloposkPlugin) using namespace Qt::StringLiterals; namespace { constexpr QPoint KeyboardOffset(0, 100); // room above for popups constexpr int LongPressWaitMs = 600; } // namespace class TestKeyboardSurface : public QObject { Q_OBJECT private: FakeEngine engine; LayoutModel model; std::unique_ptr controller; QQmlEngine qml; QQuickWindow window; QQuickItem *keyboard = nullptr; QPointingDevice *touchscreen = QTest::createTouchDevice(); // Window coordinates of the center of the key labelled `label`. QPoint keyCenter(const QString &label, const QString &type = u"char"_s) { for (int i = 0; i < model.rowCount(); ++i) { const QModelIndex idx = model.index(i); if (idx.data(LayoutModel::KeyTypeRole) == type && (label.isEmpty() || idx.data(LayoutModel::LabelRole) == label)) return model.keyRect(i).center().toPoint() + KeyboardOffset; } qFatal("no key %s", qPrintable(label)); } // Qt Quick delivers touch moves on the next frame; let one pass. void settle() { QTest::qWait(50); } void snapshot(const QString &name) { settle(); const QString dir = qEnvironmentVariable("SLOPOSK_SNAPSHOT_DIR"); if (!dir.isEmpty()) window.grabWindow().save(dir + u'/' + name + u".png"_s); } private Q_SLOTS: void initTestCase() { model.setLayout(TestLayouts::build("us")); controller = std::make_unique(engine, model); QQmlComponent component(&qml, "Sloposk", "Keyboard"); keyboard = qobject_cast(component.createWithInitialProperties({ {u"model"_s, QVariant::fromValue(&model)}, {u"controller"_s, QVariant::fromValue(controller.get())}, })); QVERIFY2(keyboard, qPrintable(component.errorString())); keyboard->setParentItem(window.contentItem()); keyboard->setPosition(KeyboardOffset); keyboard->setSize({1000, 300}); window.setColor(QColor(u"#d1d3d9"_s)); window.resize(1000, 400); window.show(); QVERIFY(QTest::qWaitForWindowExposed(&window)); } void init() { engine = {}; } void tapTypesLetter() { QTest::touchEvent(&window, touchscreen).press(0, keyCenter(u"q"_s)); QTest::touchEvent(&window, touchscreen).release(0, keyCenter(u"q"_s)); QCOMPARE(engine.output, u"q"); } void flickDownTypesSecondarySymbol() { const QPoint w = keyCenter(u"w"_s); QTest::touchEvent(&window, touchscreen).press(0, w); QTest::touchEvent(&window, touchscreen).move(0, w + QPoint(0, 15)); snapshot(u"flick-halfway"_s); const int wIndex = model.keyAt(w - KeyboardOffset); QVERIFY(model.index(wIndex).data(LayoutModel::FlickProgressRole).toReal() > 0.3); QTest::touchEvent(&window, touchscreen).move(0, w + QPoint(0, 40)); QTest::touchEvent(&window, touchscreen).release(0, w + QPoint(0, 40)); QCOMPARE(engine.output, u"2"); } void longPressPicksAlternate() { const QPoint e = keyCenter(u"e"_s); const qreal cell = model.keyRect(0).width(); QTest::touchEvent(&window, touchscreen).press(0, e); QTest::qWait(LongPressWaitMs); auto *surface = qobject_cast(keyboard); QVERIFY(surface->alternates()->isOpen()); QTest::touchEvent(&window, touchscreen).move(0, e + QPoint(int(cell), 0)); snapshot(u"alternates"_s); QTest::touchEvent(&window, touchscreen).release(0, e + QPoint(int(cell), 0)); QCOMPARE(engine.output, u"è"); QVERIFY(!surface->alternates()->isOpen()); } void secondLongPressFinishesFirst() { const QPoint e = keyCenter(u"e"_s), u = keyCenter(u"u"_s); auto *surface = qobject_cast(keyboard); QTest::touchEvent(&window, touchscreen).press(0, e); QTest::qWait(LongPressWaitMs); QTest::touchEvent(&window, touchscreen).stationary(0).press(1, u); QTest::qWait(LongPressWaitMs); // u's popup opens; e's popup closes and types its selection QCOMPARE(engine.output, u"é"); QVERIFY(surface->alternates()->isOpen()); QTest::touchEvent(&window, touchscreen).release(0, e).stationary(1); QCOMPARE(engine.output, u"é"); // lifting the first finger types nothing more QVERIFY(surface->alternates()->isOpen()); QTest::touchEvent(&window, touchscreen).release(1, u); QCOMPARE(engine.output.size(), 2); QVERIFY(!surface->alternates()->isOpen()); } void secondFingerCommitsFirst() { const QPoint a = keyCenter(u"a"_s), s = keyCenter(u"s"_s); QTest::touchEvent(&window, touchscreen).press(0, a); QTest::touchEvent(&window, touchscreen).stationary(0).press(1, s); QCOMPARE(engine.output, u"a"); QTest::touchEvent(&window, touchscreen).release(0, a).stationary(1); QTest::touchEvent(&window, touchscreen).release(1, s); QCOMPARE(engine.output, u"as"); } void holdKeysKeepsEveryFingerDown() { controller->setOptions({.holdKeys = true}); const QPoint w = keyCenter(u"w"_s), e = keyCenter(u"e"_s); QTest::touchEvent(&window, touchscreen).press(0, w); QTest::touchEvent(&window, touchscreen).stationary(0).press(1, e); QTest::qWait(600); // past the long-press delay: e has accents, but no popup QCOMPARE(engine.held, (QSet{XKB_KEY_w, XKB_KEY_e})); QTest::touchEvent(&window, touchscreen).release(0, w).stationary(1); QCOMPARE(engine.held, QSet{XKB_KEY_e}); QTest::touchEvent(&window, touchscreen).release(1, e); QVERIFY(engine.held.isEmpty()); QVERIFY(engine.output.isEmpty()); // key events only, no text controller->setOptions({}); } void switchingHoldKeysMidPressCancelsTheKey() { const QPoint a = keyCenter(u"a"_s); QTest::touchEvent(&window, touchscreen).press(0, a); controller->setOptions({.holdKeys = true}); QTest::touchEvent(&window, touchscreen).release(0, a); QVERIFY(engine.output.isEmpty()); QVERIFY(engine.keyEvents.isEmpty()); QTest::touchEvent(&window, touchscreen).press(0, a); controller->setOptions({}); QTest::touchEvent(&window, touchscreen).release(0, a); QVERIFY(engine.held.isEmpty()); QVERIFY(engine.output.isEmpty()); // not typed a second time as a tap } void slideFromNumbersKeyTypesAndReturns() { const QPoint layerKey = keyCenter(u".?123"_s, u"layer"_s); QTest::touchEvent(&window, touchscreen).press(0, layerKey); QCOMPARE(model.layerName(), u"numbers"); const QPoint five = keyCenter(u"5"_s); QTest::touchEvent(&window, touchscreen).move(0, five); QTest::touchEvent(&window, touchscreen).release(0, five); QCOMPARE(engine.output, u"5"); QCOMPARE(model.layerName(), u"letters"); } void spaceTrackpadMovesCursor() { const QPoint space = keyCenter({}, u"space"_s); QTest::touchEvent(&window, touchscreen).press(0, space); QTest::qWait(LongPressWaitMs); QTest::touchEvent(&window, touchscreen).move(0, space + QPoint(45, 0)); snapshot(u"trackpad"_s); QTest::touchEvent(&window, touchscreen).release(0, space + QPoint(45, 0)); QCOMPARE(engine.output, QString()); QCOMPARE(engine.keysyms, (QList{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Right})); } void spaceSlideMovesCursorAtOnce() { const QPoint space = keyCenter({}, u"space"_s); QTest::touchEvent(&window, touchscreen).press(0, space); QTest::touchEvent(&window, touchscreen).move(0, space - QPoint(30, 0)); QVERIFY(!qobject_cast(keyboard)->isTrackpadActive()); // keys keep their labels QTest::touchEvent(&window, touchscreen).release(0, space - QPoint(30, 0)); QCOMPARE(engine.output, QString()); QCOMPARE(engine.keysyms, (QList{XKB_KEY_Left, XKB_KEY_Left})); } }; QTEST_MAIN(TestKeyboardSurface) #include "tst_keyboardsurface.moc"