#include "FakeEngine.h" #include "input/KeyboardController.h" #include "layout/LayoutModel.h" #include "TestLayouts.h" #include #include using namespace Qt::StringLiterals; namespace { const TextContext Field{.field = true}; // A field with some text already: no auto-shift until a test asks for it. const TextContext MidSentence{.field = true, .hasSurroundingText = true, .textBeforeCursor = u"so"_s}; 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")); model.setVisibleSections({MainSectionId}); controller = std::make_unique(engine, model); controller->resetContext(MidSentence); // most tests type into a text field } 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(Field); 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 = Field; context.purpose = TextContext::Purpose::Password; controller->setContext(context); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); } void fieldTextDrivesCapitalization() { TextContext context = Field; 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(Field); 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(Field); // 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 keysymKeyActsOnPressAndRepeats() { KeyDef left = makeKey(KeyType::Keysym, u"←"_s); left.keysym = XKB_KEY_Left; left.repeat = true; controller->keyDown(left, 0); QCOMPARE(engine.keysyms, QList{XKB_KEY_Left}); QTRY_VERIFY_WITH_TIMEOUT(engine.keysyms.size() >= 3, 2000); // held: repeats controller->keyUp(left, KeyOutput::Primary, {}, 0); const auto sent = engine.keysyms.size(); QTest::qWait(200); QCOMPARE(engine.keysyms.size(), sent); QVERIFY(engine.output.isEmpty()); // no text } void sectionsAreComposedAroundTheMainKeys() { const qsizetype mainKeys = model.rowCount(); const qreal mainUnits = model.totalUnits(); model.setVisibleSections({MainSectionId, u"function"_s}); QCOMPARE(model.rowCount(), mainKeys + 13); QCOMPARE(model.totalUnits(), mainUnits); // above, not beside model.setVisibleSections({MainSectionId, u"navigation"_s}); QCOMPARE(model.totalUnits(), mainUnits + 3); model.setVisibleSections({u"numpad"_s}); QCOMPARE(model.rowCount(), 19); model.setVisibleSections({}); QCOMPARE(model.rowCount(), mainKeys); // never nothing } KeyDef keysymKey(quint32 keysym) { KeyDef key = makeKey(KeyType::Keysym); key.keysym = keysym; return key; } KeyDef modifierKey(const QString &name) { KeyDef key = makeKey(KeyType::Modifier); key.modifier = name; return key; } void ctrlTurnsKeysIntoShortcuts() { tap(modifierKey(u"ctrl"_s)); QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), int(KeyboardController::ShiftMode::Once)); tap(makeKey(KeyType::Character, u"c"_s)); QCOMPARE(engine.keysyms, QList{XKB_KEY_c}); QCOMPARE(engine.lastModifiers, Qt::ControlModifier); QVERIFY(engine.output.isEmpty()); // One-shot: the next key types again. tap(makeKey(KeyType::Character, u"c"_s)); QCOMPARE(engine.output, u"c"); } void heldModifierAppliesToEveryKey() { const KeyDef alt = modifierKey(u"alt"_s); controller->keyDown(alt, 0); tap(keysymKey(XKB_KEY_Tab)); tap(keysymKey(XKB_KEY_Tab)); controller->keyUp(alt, KeyOutput::Primary, {}, 9000); QCOMPARE(engine.keysyms.size(), 2); QCOMPARE(engine.lastModifiers, Qt::AltModifier); QCOMPARE(controller->modifierModes().value(u"alt"_s).toInt(), int(KeyboardController::ShiftMode::Off)); } void tappedShiftAppliesToFunctionKeys() { tap(makeKey(KeyType::Character, u"a"_s)); // mid-sentence: no auto-shift tap(makeKey(KeyType::Shift)); tap(keysymKey(XKB_KEY_F5)); QCOMPARE(engine.keysyms.last(), quint32(XKB_KEY_F5)); QCOMPARE(engine.lastModifiers, Qt::ShiftModifier); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); // used up } void autoShiftAndCapsLockDontApplyToFunctionKeys() { controller->resetContext(Field); // sentence start: auto-shift on QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); tap(keysymKey(XKB_KEY_Left)); QCOMPARE(engine.lastModifiers, Qt::NoModifier); tap(makeKey(KeyType::CapsLock)); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked); tap(keysymKey(XKB_KEY_Left)); QCOMPARE(engine.lastModifiers, Qt::NoModifier); tap(makeKey(KeyType::Character, u"a"_s)); tap(makeKey(KeyType::Character, u"b"_s)); QCOMPARE(engine.output, u"AB"); tap(makeKey(KeyType::CapsLock)); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); } void autoShiftCapitalizesLettersOnly() { KeyDef lj = makeKey(KeyType::Character, u"ľ"_s); // sk number row: Shift+ľ = 2 lj.shifted = u"2"_s; KeyDef one = makeKey(KeyType::Character, u"+"_s); one.shifted = u"1"_s; controller->resetContext(Field); // sentence start tap(one); QCOMPARE(engine.output, u"+"); // not the shifted level controller->resetContext(Field); tap(lj); QCOMPARE(engine.output, u"+Ľ"); tap(makeKey(KeyType::Shift)); // the user's shift does pick it tap(lj); QCOMPARE(engine.output, u"+Ľ2"); } void deadKeyComposesWithNextCharacter() { KeyDef acute = makeKey(KeyType::Character, u"´"_s); acute.dead = XKB_KEY_dead_acute; tap(makeKey(KeyType::Character, u"x"_s)); tap(acute); QVERIFY(engine.output == u"x"); // nothing typed yet tap(makeKey(KeyType::Character, u"e"_s)); QVERIFY(engine.output.endsWith(u"é"_s)); } void noTextRulesWithoutAField() { controller->resetContext({}); // keys go to a window, not a text field QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); const KeyDef space = makeKey(KeyType::Space); tap(makeKey(KeyType::Character, u"a"_s)); tap(space); tap(space); QCOMPARE(engine.output, u"a "); } 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"