Files
sloposk/tests/tst_keyboardsurface.cpp
T
ericek111andClaude Opus 5.5 2b5edeb365 Gestures: flick, long-press alternates, trackpad, layer slide, rollover
- PointerTracker: per-finger gesture state machine (pure, unit-tested)
- AlternatesPicker + AlternatesPopup.qml: long-press choices, finger selects
- Space long-press trackpad moves the cursor with arrow keysyms
- Slide from .?123 onto a symbol types it and switches back
- New finger commits pending taps (iOS rollover)
- Flick animation: secondary symbol slides down as the finger moves
- Transparent popup margin with an input region that passes clicks through
- tst_keyboardsurface: end-to-end gesture tests on the real QML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:55:49 +02:00

178 lines
6.1 KiB
C++

// 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 "engine/InputEngine.h"
#include "input/KeyboardController.h"
#include "input/KeyboardSurface.h"
#include "layout/LayoutModel.h"
#include "layout/LayoutRepository.h"
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
#include <QQuickItem>
#include <QQuickWindow>
#include <QTest>
#include <xkbcommon/xkbcommon-keysyms.h>
Q_IMPORT_QML_PLUGIN(SloposkPlugin)
using namespace Qt::StringLiterals;
namespace {
class FakeEngine : public InputEngine
{
public:
void commitText(const QString &text) override { output += text; }
void sendKeysym(quint32 keysym) override { keysyms.append(keysym); }
QString output;
QList<quint32> keysyms;
};
constexpr QPoint KeyboardOffset(0, 100); // room above for popups
constexpr int LongPressWaitMs = 600;
} // namespace
class TestKeyboardSurface : public QObject
{
Q_OBJECT
private:
FakeEngine engine;
LayoutRepository repo;
LayoutModel model;
std::unique_ptr<KeyboardController> 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()
{
Q_INIT_RESOURCE(layouts);
repo.loadDirectory(u":/layouts"_s);
repo.finalize(u"en_US"_s);
controller = std::make_unique<KeyboardController>(engine, model, repo);
QQmlComponent component(&qml, "Sloposk", "Keyboard");
keyboard = qobject_cast<QQuickItem *>(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<KeyboardSurface *>(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 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 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<quint32>{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Right}));
}
};
QTEST_MAIN(TestKeyboardSurface)
#include "tst_keyboardsurface.moc"