#include "FakeEngine.h" #include "input/KeyboardController.h" #include "layout/LayoutModel.h" #include "TestLayouts.h" #include #include using namespace Qt::StringLiterals; namespace { KeyDef makeKey(KeyType type, const QString &label = {}) { KeyDef k; k.type = type; k.label = label; return k; } } // namespace class TestController : public QObject { Q_OBJECT private: FakeEngine engine; LayoutModel model; std::unique_ptr controller; qint64 now = 0; void tap(const KeyDef &key, KeyOutput output = KeyOutput::Primary, const QString &alternate = {}) { now += 1000; // far apart: never a double tap controller->keyDown(key, now); controller->keyUp(key, output, alternate, now + 50); } private Q_SLOTS: void init() { engine = {}; model.setLayout(TestLayouts::build("us")); controller = std::make_unique(engine, model); } void typesLowercase() { tap(makeKey(KeyType::Character, u"a"_s)); QCOMPARE(engine.output, u"a"); } void shiftCapitalizesOneCharacter() { const KeyDef a = makeKey(KeyType::Character, u"a"_s); tap(makeKey(KeyType::Shift)); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); tap(a); tap(a); QCOMPARE(engine.output, u"Aa"); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); } void flickAndAlternateOutputs() { KeyDef e = makeKey(KeyType::Character, u"e"_s); e.flick = u"3"_s; tap(e, KeyOutput::Flick); tap(e, KeyOutput::Alternate, u"é"_s); QCOMPARE(engine.output, u"3é"); } void backspaceActsOnPress() { controller->keyDown(makeKey(KeyType::Backspace), 0); QCOMPARE(engine.keysyms, QList{XKB_KEY_BackSpace}); } void enterSendsReturn() { tap(makeKey(KeyType::Enter)); QCOMPARE(engine.keysyms, QList{XKB_KEY_Return}); } void layerSwitchChangesModelLayer() { KeyDef layer = makeKey(KeyType::LayerSwitch); layer.target = u"numbers"_s; controller->keyDown(layer, 0); QCOMPARE(model.layerName(), u"numbers"); } void backspaceStopsOnRelease() { const KeyDef backspace = makeKey(KeyType::Backspace); controller->keyDown(backspace, 0); controller->keyUp(backspace, KeyOutput::Primary, {}, 50); QTest::qWait(700); // past the repeat delay QCOMPARE(engine.keysyms.size(), 1); } void capitalizesAtSentenceStart() { controller->resetContext(); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); for (const char *c : {"h", "i", ".", " "}) tap(makeKey(KeyType::Character, QString::fromLatin1(c))); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); tap(makeKey(KeyType::Character, u"o"_s)); QCOMPARE(engine.output, u"Hi. O"); } void noCapitalizationInPasswordFields() { TextContext context; context.purpose = TextContext::Purpose::Password; controller->setContext(context); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); } void fieldTextDrivesCapitalization() { TextContext context; context.hasSurroundingText = true; context.textBeforeCursor = u"Done! "_s; controller->setContext(context); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); context.textBeforeCursor = u"Done, "_s; controller->setContext(context); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); } void doubleSpaceTypesPeriod() { const KeyDef space = makeKey(KeyType::Space); tap(makeKey(KeyType::Character, u"a"_s)); tap(space); tap(space); QCOMPARE(engine.output, u"a. "); QCOMPARE(engine.mismatchedReplacements, 0); // A Backspace keysym may reach the client after the commit and eat the new space. QVERIFY(engine.keysyms.isEmpty()); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); } void doubleSpacePeriodCanBeTurnedOff() { controller->setOptions({.doubleSpacePeriod = false}); const KeyDef space = makeKey(KeyType::Space); tap(makeKey(KeyType::Character, u"a"_s)); tap(space); tap(space); QCOMPARE(engine.output, u"A "); } void autoCapitalizationCanBeTurnedOff() { controller->resetContext(); controller->setOptions({.autoCapitalize = false}); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); tap(makeKey(KeyType::Character, u"a"_s)); QCOMPARE(engine.output, u"a"); } void doubleTapShiftLocksAtSentenceStart() { controller->resetContext(); // a field was focused QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift const KeyDef shift = makeKey(KeyType::Shift); controller->keyDown(shift, 5000); controller->keyUp(shift, KeyOutput::Primary, {}, 5050); controller->keyDown(shift, 5200); controller->keyUp(shift, KeyOutput::Primary, {}, 5250); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked); } void slideTypesSymbolAndSwitchesBack() { KeyDef layer = makeKey(KeyType::LayerSwitch); layer.target = u"numbers"_s; controller->keyDown(layer, 0); controller->slideReleased(makeKey(KeyType::Character, u"5"_s), 100); QCOMPARE(engine.output, u"5"); QCOMPARE(model.layerName(), u"letters"); } void trackpadMovesCursor() { controller->moveCursor({2, -1}); QCOMPARE(engine.keysyms, (QList{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Up})); } void globeRequestsNextLayout() { int requested = 0; connect(controller.get(), &KeyboardController::nextLayoutRequested, this, [&] { ++requested; }); tap(makeKey(KeyType::Globe)); QCOMPARE(requested, 1); } void dismissEmitsSignal() { int dismissed = 0; connect(controller.get(), &KeyboardController::dismissRequested, this, [&] { ++dismissed; }); tap(makeKey(KeyType::Dismiss)); QCOMPARE(dismissed, 1); } }; QTEST_GUILESS_MAIN(TestController) #include "tst_controller.moc"