Files
thunderpad/tests/test_bindings.cpp
2026-08-12 07:39:45 -04:00

349 lines
12 KiB
C++

#include "axis.h"
#include "bindingdialog.h"
#include "button.h"
#include "inputbinding.h"
#include <QElapsedTimer>
#include <QApplication>
#include <QKeyEvent>
#include <QSignalSpy>
#include <QTest>
#include <QTextStream>
class RecorderProbe : public BindingDialog {
public:
RecorderProbe() : BindingDialog("Test control", true) {}
void press(int qtKey, int scanCode, bool repeat = false) {
QKeyEvent event(QEvent::KeyPress, qtKey, Qt::NoModifier,
static_cast<quint32>(scanCode), 0, 0, QString(), repeat);
keyPressEvent(&event);
}
void releaseKey(int qtKey, int scanCode, bool repeat = false) {
QKeyEvent event(QEvent::KeyRelease, qtKey, Qt::NoModifier,
static_cast<quint32>(scanCode), 0, 0, QString(), repeat);
keyReleaseEvent(&event);
}
void tap(int qtKey, int scanCode) {
press(qtKey, scanCode);
releaseKey(qtKey, scanCode);
}
void finish() {
press(Qt::Key_Control, 37);
press(Qt::Key_Return, 36);
}
void dispatchTap(int qtKey, int scanCode) {
QWidget *target = findChild<QPushButton *>();
QVERIFY(target);
QKeyEvent pressEvent(QEvent::KeyPress, qtKey, Qt::NoModifier,
static_cast<quint32>(scanCode), 0, 0);
QApplication::sendEvent(target, &pressEvent);
QKeyEvent releaseEvent(QEvent::KeyRelease, qtKey, Qt::NoModifier,
static_cast<quint32>(scanCode), 0, 0);
QApplication::sendEvent(target, &releaseEvent);
}
bool cancelAvailable() const {
for (QPushButton *button : findChildren<QPushButton *>()) {
if (button->text().contains("Cancel")) return button->isVisibleTo(this);
}
return false;
}
};
class ButtonProbe : public Button {
public:
ButtonProbe() : Button(0) {}
QVector<bool> plainEvents;
QVector<QPair<int, bool>> compositeEvents;
protected:
void click(bool press) override { plainEvents.append(press); }
void compositeKeyEvent(int keycode, bool press) override {
compositeEvents.append(qMakePair(keycode, press));
}
};
class AxisProbe : public Axis {
public:
AxisProbe() : Axis(0) {}
QVector<QPair<int, bool>> compositeEvents;
protected:
void compositeKeyEvent(int keycode, bool press) override {
compositeEvents.append(qMakePair(keycode, press));
}
};
class BindingTests : public QObject {
Q_OBJECT
private slots:
void parseAndSerializeSequences();
void rejectInvalidSequences_data();
void rejectInvalidSequences();
void preserveLegacySerialization();
void recorderPlainChordAndSteps();
void recorderModifiersRepeatLimitAndFinish();
void runnerOrderTimingRetriggerAndCancel();
void preservePlainButtonModes();
void axisCompositeRearmsWithoutRepeating();
};
void BindingTests::parseAndSerializeSequences() {
InputBinding binding;
QString error;
QVERIFY2(InputBinding::parseSequence("125", "64+28/49/10+14", &binding, &error), qPrintable(error));
QCOMPARE(binding.stepDelay, 125);
QCOMPARE(binding.chords.size(), 3);
QCOMPARE(binding.sequenceToken(), QString("64+28/49/10+14"));
Button button(0);
QString buttonInput("sequence 125 64+28/49");
QTextStream buttonRead(&buttonInput);
QVERIFY(button.read(buttonRead));
QString buttonOutput;
QTextStream buttonWrite(&buttonOutput);
button.write(buttonWrite);
QCOMPARE(buttonOutput, QString("\tButton 1: sequence 125 64+28/49\n"));
Axis axis(0);
QString axisInput("ZeroOne, +sequence 80 64+28/49, -sequence 250 10/11+12");
QTextStream axisRead(&axisInput);
QVERIFY2(axis.read(axisRead), qPrintable(axis.readError()));
QString axisOutput;
QTextStream axisWrite(&axisOutput);
axis.write(axisWrite);
QCOMPARE(axisOutput, QString("\tAxis 1: ZeroOne, +sequence 80 64+28/49, -sequence 250 10/11+12\n"));
Button malformedButton(0);
QString malformedButtonInput("sequence 100 10 / 11");
QTextStream malformedButtonRead(&malformedButtonInput);
QVERIFY(!malformedButton.read(malformedButtonRead));
QVERIFY(!malformedButton.readError().isEmpty());
Axis malformedAxis(0);
QString malformedAxisInput("ZeroOne, +sequence 100 10 + 11, -key 12");
QTextStream malformedAxisRead(&malformedAxisInput);
QVERIFY(!malformedAxis.read(malformedAxisRead));
QVERIFY(!malformedAxis.readError().isEmpty());
Axis mouseModeSequence(0);
QString mouseModeInput("Gradient, +sequence 100 10+11, -key 12, mouse+h");
QTextStream mouseModeRead(&mouseModeInput);
QVERIFY(!mouseModeSequence.read(mouseModeRead));
QVERIFY(mouseModeSequence.readError().contains("mouse movement"));
}
void BindingTests::rejectInvalidSequences_data() {
QTest::addColumn<QString>("delay");
QTest::addColumn<QString>("sequence");
QTest::newRow("delay-low") << "49" << "10/11";
QTest::newRow("delay-high") << "1001" << "10/11";
QTest::newRow("delay-text") << "fast" << "10/11";
QTest::newRow("empty-chord") << "100" << "10//11";
QTest::newRow("empty-key") << "100" << "10++11";
QTest::newRow("duplicate") << "100" << "10+10";
QTest::newRow("keycode-zero") << "100" << "0/11";
QTest::newRow("keycode-high") << "100" << "256/11";
QStringList tooMany;
for (int i = 0; i < 33; ++i) tooMany.append(QString::number(i + 1));
QTest::newRow("too-many") << "100" << tooMany.join('/');
}
void BindingTests::rejectInvalidSequences() {
QFETCH(QString, delay);
QFETCH(QString, sequence);
InputBinding binding;
QString error;
QVERIFY(!InputBinding::parseSequence(delay, sequence, &binding, &error));
QVERIFY(!error.isEmpty());
}
void BindingTests::preserveLegacySerialization() {
Button button(1);
QString buttonInput("rapidfire, sticky, key 42");
QTextStream buttonRead(&buttonInput);
QVERIFY(button.read(buttonRead));
QString buttonOutput;
QTextStream buttonWrite(&buttonOutput);
button.write(buttonWrite);
QCOMPARE(buttonOutput, QString("\tButton 2: rapidfire, sticky, key 42\n"));
Axis axis(2);
QString axisInput("Gradient, dZone 4000, xZone 29000, +mouse 1, -key 113");
QTextStream axisRead(&axisInput);
QVERIFY(axis.read(axisRead));
QString axisOutput;
QTextStream axisWrite(&axisOutput);
axis.write(axisWrite);
QCOMPARE(axisOutput, QString("\tAxis 3: Gradient, dZone 4000, xZone 29000, +mouse 1, -key 113\n"));
}
void BindingTests::recorderPlainChordAndSteps() {
RecorderProbe plain;
plain.tap(Qt::Key_A, 38);
plain.finish();
QVERIFY(plain.binding().isPlainKey());
QCOMPARE(plain.binding().chords.first().keycodes, QVector<int>({38}));
RecorderProbe chord;
chord.press(Qt::Key_Alt, 64);
chord.press(Qt::Key_T, 28);
chord.releaseKey(Qt::Key_T, 28);
chord.releaseKey(Qt::Key_Alt, 64);
chord.finish();
QCOMPARE(chord.binding().chords, QVector<KeyChord>({KeyChord{{64, 28}}}));
RecorderProbe steps;
steps.tap(Qt::Key_A, 38);
steps.tap(Qt::Key_B, 56);
steps.finish();
QCOMPARE(steps.binding().chords, QVector<KeyChord>({KeyChord{{38}}, KeyChord{{56}}}));
RecorderProbe tab;
tab.dispatchTap(Qt::Key_Tab, 23);
tab.finish();
QCOMPARE(tab.binding().chords, QVector<KeyChord>({KeyChord{{23}}}));
QVERIFY(tab.cancelAvailable());
}
void BindingTests::recorderModifiersRepeatLimitAndFinish() {
RecorderProbe modifier;
modifier.tap(Qt::Key_Shift, 50);
modifier.finish();
QCOMPARE(modifier.binding().chords, QVector<KeyChord>({KeyChord{{50}}}));
RecorderProbe repeat;
repeat.press(Qt::Key_A, 38);
repeat.press(Qt::Key_A, 38, true);
repeat.releaseKey(Qt::Key_A, 38, true);
repeat.releaseKey(Qt::Key_A, 38);
repeat.finish();
QCOMPARE(repeat.binding().chords, QVector<KeyChord>({KeyChord{{38}}}));
RecorderProbe separateCtrlEnter;
separateCtrlEnter.tap(Qt::Key_Control, 37);
separateCtrlEnter.tap(Qt::Key_Return, 36);
separateCtrlEnter.finish();
QCOMPARE(separateCtrlEnter.binding().chords,
QVector<KeyChord>({KeyChord{{37}}, KeyChord{{36}}}));
RecorderProbe limit;
for (int i = 0; i < InputBinding::MaximumChords; ++i) limit.tap(Qt::Key_A, 8 + i);
QCOMPARE(limit.binding().chords.size(), InputBinding::MaximumChords);
}
void BindingTests::runnerOrderTimingRetriggerAndCancel() {
InputBinding binding;
QVERIFY(InputBinding::parseSequence("80", "10+11/12+13", &binding));
SequenceRunner runner;
QVector<QPair<int, bool>> events;
QVector<qint64> times;
QElapsedTimer elapsed;
elapsed.start();
connect(&runner, &SequenceRunner::keyEvent, &runner, [&](int keycode, bool press) {
events.append(qMakePair(keycode, press));
times.append(elapsed.elapsed());
});
QSignalSpy finished(&runner, &SequenceRunner::finished);
QVERIFY(runner.start(binding));
QVERIFY(!runner.start(binding));
QVERIFY(finished.wait(300));
const QVector<QPair<int, bool>> expected = {
{10, true}, {11, true}, {11, false}, {10, false},
{12, true}, {13, true}, {13, false}, {12, false}
};
QCOMPARE(events, expected);
QVERIFY(times.at(2) >= 20 && times.at(2) <= 60);
QVERIFY(times.at(4) >= 65 && times.at(4) <= 120);
events.clear();
QVERIFY(runner.start(binding));
runner.cancel();
const QVector<QPair<int, bool>> cancelled = {
{10, true}, {11, true}, {11, false}, {10, false}
};
QCOMPARE(events, cancelled);
QVERIFY(!runner.isRunning());
}
void BindingTests::preservePlainButtonModes() {
ButtonProbe held;
held.setKey(false, 38);
held.jsevent(1);
held.jsevent(0);
QCOMPARE(held.plainEvents, QVector<bool>({true, false}));
ButtonProbe sticky;
QString stickyInput("sticky, key 38");
QTextStream stickyRead(&stickyInput);
QVERIFY(sticky.read(stickyRead));
sticky.jsevent(1);
sticky.jsevent(0);
sticky.jsevent(1);
QCOMPARE(sticky.plainEvents, QVector<bool>({true, false}));
ButtonProbe rapid;
QString rapidInput("rapidfire, key 38");
QTextStream rapidRead(&rapidInput);
QVERIFY(rapid.read(rapidRead));
rapid.jsevent(1);
rapid.timerTick(FREQ);
rapid.timerTick(FREQ + FREQ / 2);
rapid.jsevent(0);
QCOMPARE(rapid.plainEvents, QVector<bool>({true, false}));
ButtonProbe mouse;
mouse.setKey(true, 1);
QVERIFY(mouse.getBinding().isMouse());
mouse.jsevent(1);
mouse.jsevent(0);
QCOMPARE(mouse.plainEvents, QVector<bool>({true, false}));
Button composite(0);
QString compositeInput("sticky, rapidfire, sequence 100 10+11");
QTextStream compositeRead(&compositeInput);
QVERIFY(composite.read(compositeRead));
QString output;
QTextStream writer(&output);
composite.write(writer);
QVERIFY(!output.contains("sticky"));
QVERIFY(!output.contains("rapidfire"));
InputBinding sequence;
QVERIFY(InputBinding::parseSequence("100", "10+11/12", &sequence));
ButtonProbe cancelling;
cancelling.setBinding(sequence);
cancelling.jsevent(1);
cancelling.release();
const QVector<QPair<int, bool>> cancelled = {
{10, true}, {11, true}, {11, false}, {10, false}
};
QCOMPARE(cancelling.compositeEvents, cancelled);
}
void BindingTests::axisCompositeRearmsWithoutRepeating() {
AxisProbe axis;
QString input("Gradient, +sequence 50 10+11, -sequence 50 12+13");
QTextStream reader(&input);
QVERIFY(axis.read(reader));
axis.jsevent(20000);
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 4, 150);
axis.jsevent(25000);
QTest::qWait(80);
QCOMPARE(axis.compositeEvents.size(), 4);
axis.jsevent(-25000);
QTest::qWait(80);
QCOMPARE(axis.compositeEvents.size(), 4);
axis.jsevent(0);
axis.jsevent(20000);
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 8, 150);
axis.jsevent(0);
axis.jsevent(-20000);
QTRY_COMPARE_WITH_TIMEOUT(axis.compositeEvents.size(), 12, 150);
QCOMPARE(axis.compositeEvents.at(8), qMakePair(12, true));
QCOMPARE(axis.compositeEvents.at(9), qMakePair(13, true));
}
QTEST_MAIN(BindingTests)
#include "test_bindings.moc"