Files
sloposk/tests/tst_keyboardsurface.cpp
T
ericek111andClaude Opus 5.5 23e957660f Reword code comments in plain technical language
Comments only, no code changes. Replaces vague or personified phrasing
with explicit statements of what the code does and why, uses bullet
lists for multi-case behavior, and fixes the settings path in
WindowSettings.h (~/.config/sloposk/sloposk.conf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:58:59 +01:00

226 lines
8.6 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 "FakeEngine.h"
#include "input/KeyboardController.h"
#include "input/KeyboardSurface.h"
#include "layout/LayoutModel.h"
#include "TestLayouts.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 {
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<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()
{
model.setLayout(TestLayouts::build("us"));
controller = std::make_unique<KeyboardController>(engine, model);
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 secondLongPressFinishesFirst()
{
const QPoint e = keyCenter(u"e"_s), u = keyCenter(u"u"_s);
auto *surface = qobject_cast<KeyboardSurface *>(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<quint32>{XKB_KEY_w, XKB_KEY_e}));
QTest::touchEvent(&window, touchscreen).release(0, w).stationary(1);
QCOMPARE(engine.held, QSet<quint32>{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<quint32>{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<KeyboardSurface *>(keyboard)->isTrackpadActive()); // keys keep their labels
QTest::touchEvent(&window, touchscreen).release(0, space - QPoint(30, 0));
QCOMPARE(engine.output, QString());
QCOMPARE(engine.keysyms, (QList<quint32>{XKB_KEY_Left, XKB_KEY_Left}));
}
};
QTEST_MAIN(TestKeyboardSurface)
#include "tst_keyboardsurface.moc"