summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/basic/test_keypress.cpp22
-rw-r--r--tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp64
-rw-r--r--tests/caps_word/caps_word_combo/config.h20
-rw-r--r--tests/caps_word/caps_word_combo/test.mk19
-rw-r--r--tests/caps_word/caps_word_combo/test_caps_word_combo.cpp212
-rw-r--r--tests/caps_word/test_caps_word.cpp159
-rw-r--r--tests/tap_dance/config.h19
-rw-r--r--tests/tap_dance/examples.c199
-rw-r--r--tests/tap_dance/examples.h33
-rw-r--r--tests/tap_dance/test.mk22
-rw-r--r--tests/tap_dance/test_examples.cpp318
-rw-r--r--tests/test_common/test_fixture.cpp16
-rw-r--r--tests/test_common/test_fixture.hpp7
13 files changed, 1074 insertions, 36 deletions
diff --git a/tests/basic/test_keypress.cpp b/tests/basic/test_keypress.cpp
index bb68ced557..6d5b502a00 100644
--- a/tests/basic/test_keypress.cpp
+++ b/tests/basic/test_keypress.cpp
@@ -64,11 +64,7 @@ TEST_F(KeyPress, CorrectKeysAreReportedWhenTwoKeysArePressed) {
key_b.press();
key_c.press();
- // Note that QMK only processes one key at a time
- // See issue #1476 for more information
EXPECT_REPORT(driver, (key_b.report_code));
- keyboard_task();
-
EXPECT_REPORT(driver, (key_b.report_code, key_c.report_code));
keyboard_task();
@@ -76,8 +72,6 @@ TEST_F(KeyPress, CorrectKeysAreReportedWhenTwoKeysArePressed) {
key_c.release();
// Note that the first key released is the first one in the matrix order
EXPECT_REPORT(driver, (key_c.report_code));
- keyboard_task();
-
EXPECT_EMPTY_REPORT(driver);
keyboard_task();
}
@@ -92,10 +86,7 @@ TEST_F(KeyPress, LeftShiftIsReportedCorrectly) {
key_lsft.press();
key_a.press();
- // Unfortunately modifiers are also processed in the wrong order
- // See issue #1476 for more information
EXPECT_REPORT(driver, (key_a.report_code));
- keyboard_task();
EXPECT_REPORT(driver, (key_a.report_code, key_lsft.report_code));
keyboard_task();
@@ -118,11 +109,7 @@ TEST_F(KeyPress, PressLeftShiftAndControl) {
key_lsft.press();
key_lctrl.press();
- // Unfortunately modifiers are also processed in the wrong order
- // See issue #1476 for more information
EXPECT_REPORT(driver, (key_lsft.report_code));
- keyboard_task();
-
EXPECT_REPORT(driver, (key_lsft.report_code, key_lctrl.report_code));
keyboard_task();
@@ -130,8 +117,6 @@ TEST_F(KeyPress, PressLeftShiftAndControl) {
key_lctrl.release();
EXPECT_REPORT(driver, (key_lctrl.report_code));
- keyboard_task();
-
EXPECT_EMPTY_REPORT(driver);
keyboard_task();
}
@@ -145,20 +130,13 @@ TEST_F(KeyPress, LeftAndRightShiftCanBePressedAtTheSameTime) {
key_lsft.press();
key_rsft.press();
- // Unfortunately modifiers are also processed in the wrong order
- // See issue #1476 for more information
EXPECT_REPORT(driver, (key_lsft.report_code));
- keyboard_task();
-
EXPECT_REPORT(driver, (key_lsft.report_code, key_rsft.report_code));
keyboard_task();
key_lsft.release();
key_rsft.release();
-
EXPECT_REPORT(driver, (key_rsft.report_code));
- keyboard_task();
-
EXPECT_EMPTY_REPORT(driver);
keyboard_task();
}
diff --git a/tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp b/tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp
index deb4d95766..ba21c527a6 100644
--- a/tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp
+++ b/tests/caps_word/caps_word_autoshift/test_caps_word_autoshift.cpp
@@ -19,6 +19,14 @@
#include "test_fixture.hpp"
#include "test_keymap_key.hpp"
+// Allow reports with no keys or only KC_LSFT.
+// clang-format off
+#define EXPECT_EMPTY_OR_LSFT(driver) \
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf( \
+ KeyboardReport(), \
+ KeyboardReport(KC_LSFT))))
+// clang-format on
+
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AnyOf;
@@ -39,13 +47,7 @@ TEST_F(CapsWord, AutoShiftKeys) {
KeymapKey key_spc(0, 1, 0, KC_SPC);
set_keymap({key_a, key_spc});
- // Allow any number of reports with no keys or only KC_LSFT.
- // clang-format off
- EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
- KeyboardReport(),
- KeyboardReport(KC_LSFT))))
- .Times(AnyNumber());
- // clang-format on
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "A, A, space, a".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_A));
@@ -65,6 +67,46 @@ TEST_F(CapsWord, AutoShiftKeys) {
testing::Mock::VerifyAndClearExpectations(&driver);
}
+// Test Caps Word + Auto Shift where keys A and B are rolled.
+TEST_F(CapsWord, AutoShiftRolledShiftedKeys) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_b(0, 0, 1, KC_B);
+ set_keymap({key_a, key_b});
+
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
+ { // Expect: "A, B, A, B".
+ InSequence s;
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ }
+
+ caps_word_on();
+
+ key_a.press(); // Overlapping taps: A down, B down, A up, B up.
+ run_one_scan_loop();
+ key_b.press();
+ run_one_scan_loop();
+ key_a.release();
+ run_one_scan_loop();
+ key_b.release();
+ run_one_scan_loop();
+
+ key_a.press(); // Nested taps: A down, B down, B up, A up.
+ run_one_scan_loop();
+ key_b.press();
+ run_one_scan_loop();
+ key_b.release();
+ run_one_scan_loop();
+ key_a.release();
+ run_one_scan_loop();
+
+ caps_word_off();
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
// Tests that with tap-hold keys with Retro Shift, letter keys are shifted by
// Caps Word regardless of whether they are retroshifted.
TEST_F(CapsWord, RetroShiftKeys) {
@@ -73,13 +115,7 @@ TEST_F(CapsWord, RetroShiftKeys) {
KeymapKey key_layertap_b(0, 1, 0, LT(1, KC_B));
set_keymap({key_modtap_a, key_layertap_b});
- // Allow any number of reports with no keys or only KC_LSFT.
- // clang-format off
- EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
- KeyboardReport(),
- KeyboardReport(KC_LSFT))))
- .Times(AnyNumber());
- // clang-format on
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
{ // Expect: "B, A, B, A".
InSequence s;
EXPECT_REPORT(driver, (KC_LSFT, KC_B));
diff --git a/tests/caps_word/caps_word_combo/config.h b/tests/caps_word/caps_word_combo/config.h
new file mode 100644
index 0000000000..92dbe045b2
--- /dev/null
+++ b/tests/caps_word/caps_word_combo/config.h
@@ -0,0 +1,20 @@
+// Copyright 2022 Google LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+#include "test_common.h"
+
+#define TAPPING_TERM 200
diff --git a/tests/caps_word/caps_word_combo/test.mk b/tests/caps_word/caps_word_combo/test.mk
new file mode 100644
index 0000000000..9f2e157189
--- /dev/null
+++ b/tests/caps_word/caps_word_combo/test.mk
@@ -0,0 +1,19 @@
+# Copyright 2022 Google LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+CAPS_WORD_ENABLE = yes
+COMBO_ENABLE = yes
+AUTO_SHIFT_ENABLE = yes
+
diff --git a/tests/caps_word/caps_word_combo/test_caps_word_combo.cpp b/tests/caps_word/caps_word_combo/test_caps_word_combo.cpp
new file mode 100644
index 0000000000..3a0530b854
--- /dev/null
+++ b/tests/caps_word/caps_word_combo/test_caps_word_combo.cpp
@@ -0,0 +1,212 @@
+// Copyright 2022 Google LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+// Test Caps Word + Combos, with and without Auto Shift.
+
+#include <algorithm>
+#include <numeric>
+#include <vector>
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "test_fixture.hpp"
+#include "test_keymap_key.hpp"
+
+// Allow reports with no keys or only KC_LSFT.
+// clang-format off
+#define EXPECT_EMPTY_OR_LSFT(driver) \
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf( \
+ KeyboardReport(), \
+ KeyboardReport(KC_LSFT))))
+// clang-format on
+
+using ::testing::AnyNumber;
+using ::testing::AnyOf;
+using ::testing::InSequence;
+using ::testing::TestParamInfo;
+
+extern "C" {
+// Define some combos to use for the test, including overlapping combos and
+// combos that chord tap-hold keys.
+enum combo_events { AB_COMBO, BC_COMBO, AD_COMBO, DE_COMBO, FGHI_COMBO, COMBO_LENGTH };
+uint16_t COMBO_LEN = COMBO_LENGTH;
+
+const uint16_t ab_combo[] PROGMEM = {KC_A, KC_B, COMBO_END};
+const uint16_t bc_combo[] PROGMEM = {KC_B, KC_C, COMBO_END};
+const uint16_t ad_combo[] PROGMEM = {KC_A, LCTL_T(KC_D), COMBO_END};
+const uint16_t de_combo[] PROGMEM = {LCTL_T(KC_D), LT(1, KC_E), COMBO_END};
+const uint16_t fghi_combo[] PROGMEM = {KC_F, KC_G, KC_H, KC_I, COMBO_END};
+
+// clang-format off
+combo_t key_combos[] = {
+ [AB_COMBO] = COMBO(ab_combo, KC_SPC), // KC_A + KC_B = KC_SPC
+ [BC_COMBO] = COMBO(bc_combo, KC_X), // KC_B + KC_C = KC_X
+ [AD_COMBO] = COMBO(ad_combo, KC_Y), // KC_A + LCTL_T(KC_D) = KC_Y
+ [DE_COMBO] = COMBO(de_combo, KC_Z), // LCTL_T(KC_D) + LT(1, KC_E) = KC_Z
+ [FGHI_COMBO] = COMBO(fghi_combo, KC_W) // KC_F + KC_G + KC_H + KC_I = KC_W
+};
+// clang-format on
+} // extern "C"
+
+namespace {
+
+// To test combos thorougly, we test them with pressing the chord keys with
+// a few different orders and timings.
+struct TestParams {
+ std::string name;
+ bool autoshift_on;
+
+ static const std::string& GetName(const TestParamInfo<TestParams>& info) {
+ return info.param.name;
+ }
+};
+
+class CapsWord : public ::testing::WithParamInterface<TestParams>, public TestFixture {
+ public:
+ void SetUp() override {
+ caps_word_off();
+ if (GetParam().autoshift_on) {
+ autoshift_enable();
+ } else {
+ autoshift_disable();
+ }
+ }
+};
+
+// Test pressing the keys in a combo with different orders and timings.
+TEST_P(CapsWord, SingleCombo) {
+ TestDriver driver;
+ KeymapKey key_b(0, 0, 1, KC_B);
+ KeymapKey key_c(0, 0, 2, KC_C);
+ set_keymap({key_b, key_c});
+
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_X));
+
+ caps_word_on();
+ tap_combo({key_b, key_c});
+
+ EXPECT_TRUE(is_caps_word_on());
+ caps_word_off();
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Test a longer 4-key combo.
+TEST_P(CapsWord, LongerCombo) {
+ TestDriver driver;
+ KeymapKey key_f(0, 0, 0, KC_F);
+ KeymapKey key_g(0, 0, 1, KC_G);
+ KeymapKey key_h(0, 0, 2, KC_H);
+ KeymapKey key_i(0, 0, 3, KC_I);
+ set_keymap({key_f, key_g, key_h, key_i});
+
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
+ EXPECT_REPORT(driver, (KC_LSFT, KC_W));
+
+ caps_word_on();
+ tap_combo({key_f, key_g, key_h, key_i});
+
+ EXPECT_TRUE(is_caps_word_on());
+ caps_word_off();
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Test with two overlapping combos on regular keys:
+// KC_A + KC_B = KC_SPC,
+// KC_B + KC_C = KC_X.
+TEST_P(CapsWord, ComboRegularKeys) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_b(0, 0, 1, KC_B);
+ KeymapKey key_c(0, 0, 2, KC_C);
+ KeymapKey key_1(0, 0, 3, KC_1);
+ set_keymap({key_a, key_b, key_c, key_1});
+
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
+ { // Expect: "A, B, 1, X, 1, C, space, a".
+ InSequence s;
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+ EXPECT_REPORT(driver, (KC_1));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_X));
+ EXPECT_REPORT(driver, (KC_1));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_C));
+ EXPECT_REPORT(driver, (KC_SPC));
+ EXPECT_REPORT(driver, (KC_A));
+ }
+
+ caps_word_on();
+ tap_key(key_a);
+ tap_key(key_b);
+ tap_key(key_1);
+ tap_combo({key_b, key_c}); // BC combo types "x".
+ tap_key(key_1);
+ tap_key(key_c);
+ tap_combo({key_a, key_b}); // AB combo types space.
+ tap_key(key_a);
+
+ EXPECT_FALSE(is_caps_word_on());
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// Test where combo chords involve tap-hold keys:
+// KC_A + LCTL_T(KC_D) = KC_Y,
+// LCTL_T(KC_D) + LT(1, KC_E) = KC_Z,
+TEST_P(CapsWord, ComboModTapKey) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_modtap_d(0, 0, 1, LCTL_T(KC_D));
+ KeymapKey key_layertap_e(0, 0, 2, LT(1, KC_E));
+ set_keymap({key_a, key_modtap_d, key_layertap_e});
+
+ EXPECT_EMPTY_OR_LSFT(driver).Times(AnyNumber());
+ { // Expect: "A, D, E, Y, Z".
+ InSequence s;
+ EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_D));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_E));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_Y));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_Z));
+ }
+
+ caps_word_on();
+ tap_key(key_a);
+ tap_key(key_modtap_d);
+ tap_key(key_layertap_e);
+ tap_combo({key_a, key_modtap_d}); // AD combo types "y".
+ tap_combo({key_modtap_d, key_layertap_e}); // DE combo types "z".
+
+ EXPECT_TRUE(is_caps_word_on());
+ caps_word_off();
+
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+// clang-format off
+INSTANTIATE_TEST_CASE_P(
+ Combos,
+ CapsWord,
+ ::testing::Values(
+ TestParams{"AutoshiftDisabled", false},
+ TestParams{"AutoshiftEnabled", true}
+ ),
+ TestParams::GetName
+ );
+// clang-format on
+
+} // namespace
diff --git a/tests/caps_word/test_caps_word.cpp b/tests/caps_word/test_caps_word.cpp
index 0af4b0175d..3f59ed3744 100644
--- a/tests/caps_word/test_caps_word.cpp
+++ b/tests/caps_word/test_caps_word.cpp
@@ -25,10 +25,47 @@ using ::testing::AnyOf;
using ::testing::InSequence;
using ::testing::TestParamInfo;
+namespace {
+
+bool press_user_default(uint16_t keycode) {
+ switch (keycode) {
+ // Keycodes that continue Caps Word, with shift applied.
+ case KC_A ... KC_Z:
+ case KC_MINS:
+ add_weak_mods(MOD_BIT(KC_LSFT)); // Apply shift to next key.
+ return true;
+
+ // Keycodes that continue Caps Word, without shifting.
+ case KC_1 ... KC_0:
+ case KC_BSPC:
+ case KC_DEL:
+ case KC_UNDS:
+ return true;
+
+ default:
+ return false; // Deactivate Caps Word.
+ }
+}
+
+uint16_t passed_keycode;
+bool press_user_save_passed_keycode(uint16_t keycode) {
+ passed_keycode = keycode;
+ return true;
+}
+
+bool (*press_user_fun)(uint16_t) = press_user_default;
+
+extern "C" {
+bool caps_word_press_user(uint16_t keycode) {
+ return press_user_fun(keycode);
+}
+} // extern "C"
+
class CapsWord : public TestFixture {
public:
void SetUp() override {
caps_word_off();
+ press_user_fun = press_user_default;
}
};
@@ -226,6 +263,126 @@ TEST_F(CapsWord, ShiftsAltGrSymbols) {
testing::Mock::VerifyAndClearExpectations(&driver);
}
+// Tests typing "AltGr + A" using a mod-tap key.
+TEST_F(CapsWord, ShiftsModTapAltGrSymbols) {
+ TestDriver driver;
+ KeymapKey key_a(0, 0, 0, KC_A);
+ KeymapKey key_altgr_t(0, 1, 0, RALT_T(KC_B));
+ set_keymap({key_a, key_altgr_t});
+
+ // Allow any number of reports with no keys or only modifiers.
+ // clang-format off
+ EXPECT_CALL(driver, send_keyboard_mock(AnyOf(
+ KeyboardReport(),
+ KeyboardReport(KC_RALT),
+ KeyboardReport(KC_LSFT, KC_RALT))))
+ .Times(AnyNumber());
+ // Expect "Shift + AltGr + A".
+ EXPECT_REPORT(driver, (KC_LSFT, KC_RALT, KC_A));
+ // clang-format on
+
+ // Turn on Caps Word and type "AltGr + A".
+ caps_word_on();
+
+ key_altgr_t.press();
+ idle_for(TAPPING_TERM + 1);
+ tap_key(key_a);
+ run_one_scan_loop();
+ key_altgr_t.release();
+
+ EXPECT_TRUE(is_caps_word_on());
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+struct CapsWordPressUserParams {
+ std::string name;
+ uint16_t keycode;
+ uint16_t delay_ms;
+ uint16_t expected_passed_keycode;
+ bool continues_caps_word;
+
+ static const std::string& GetName(const TestParamInfo<CapsWordPressUserParams>& info) {
+ return info.param.name;
+ }
+};
+
+class CapsWordPressUser : public ::testing::WithParamInterface<CapsWordPressUserParams>, public CapsWord {
+ void SetUp() override {
+ caps_word_on();
+ passed_keycode = KC_NO;
+ press_user_fun = press_user_save_passed_keycode;
+ }
+};
+
+// Tests keycodes passed to caps_word_press_user() function for various keys.
+TEST_P(CapsWordPressUser, KeyCode) {
+ TestDriver driver;
+ KeymapKey key(0, 0, 0, GetParam().keycode);
+ set_keymap({key});
+
+ EXPECT_ANY_REPORT(driver).Times(AnyNumber());
+ tap_key(key, GetParam().delay_ms);
+
+ EXPECT_EQ(passed_keycode, GetParam().expected_passed_keycode);
+ EXPECT_EQ(is_caps_word_on(), GetParam().continues_caps_word);
+ clear_oneshot_mods();
+ testing::Mock::VerifyAndClearExpectations(&driver);
+}
+
+const uint16_t LT_1_KC_A = LT(1, KC_A);
+// clang-format off
+INSTANTIATE_TEST_CASE_P(
+ PressUser,
+ CapsWordPressUser,
+ ::testing::Values(
+ CapsWordPressUserParams{
+ "KC_A", KC_A, 1, KC_A, true},
+ CapsWordPressUserParams{
+ "KC_HASH", KC_HASH, 1, KC_HASH, true},
+ CapsWordPressUserParams{
+ "KC_LSFT", KC_LSFT, 1, KC_LSFT, true},
+ CapsWordPressUserParams{
+ "KC_RSFT", KC_RSFT, 1, KC_RSFT, true},
+ CapsWordPressUserParams{
+ "LSFT_T_tapped", LSFT_T(KC_A), 1, KC_A, true},
+ CapsWordPressUserParams{
+ "LSFT_T_held", LSFT_T(KC_A), TAPPING_TERM + 1, KC_LSFT, true},
+ CapsWordPressUserParams{
+ "RSFT_T_held", RSFT_T(KC_A), TAPPING_TERM + 1, KC_RSFT, true},
+ CapsWordPressUserParams{
+ "RSA_T_held", RSA_T(KC_A), TAPPING_TERM + 1, RSFT(KC_RALT), true},
+ // Holding a mod-tap other than Shift or AltGr stops Caps Word.
+ CapsWordPressUserParams{
+ "LCTL_T_held", LCTL_T(KC_A), TAPPING_TERM + 1, KC_NO, false},
+ CapsWordPressUserParams{
+ "LALT_T_held", LALT_T(KC_A), TAPPING_TERM + 1, KC_NO, false},
+ CapsWordPressUserParams{
+ "LGUI_T_held", LGUI_T(KC_A), TAPPING_TERM + 1, KC_NO, false},
+ // Layer keys are ignored and continue Caps Word.
+ CapsWordPressUserParams{
+ "MO", MO(1), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "TO", TO(1), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "TG", TG(1), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "TT", TT(1), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "OSL", OSL(1), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "LT_held", LT_1_KC_A, TAPPING_TERM + 1, KC_NO, true},
+ // AltGr keys are ignored and continue Caps Word.
+ CapsWordPressUserParams{
+ "KC_RALT", KC_RALT, 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "OSM_MOD_RALT", OSM(MOD_RALT), 1, KC_NO, true},
+ CapsWordPressUserParams{
+ "RALT_T_held", RALT_T(KC_A), TAPPING_TERM + 1, KC_NO, true}
+ ),
+ CapsWordPressUserParams::GetName
+ );
+// clang-format on
+
struct CapsWordBothShiftsParams {
std::string name;
uint16_t left_shift_keycode;
@@ -435,3 +592,5 @@ INSTANTIATE_TEST_CASE_P(
CapsWordDoubleTapShiftParams::GetName
);
// clang-format on
+
+} // namespace
diff --git a/tests/tap_dance/config.h b/tests/tap_dance/config.h
new file mode 100644
index 0000000000..6aada3efd3
--- /dev/null
+++ b/tests/tap_dance/config.h
@@ -0,0 +1,19 @@
+/* Copyright 2022 Jouke Witteveen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "test_common.h"
diff --git a/tests/tap_dance/examples.c b/tests/tap_dance/examples.c
new file mode 100644
index 0000000000..4a5be41b08
--- /dev/null
+++ b/tests/tap_dance/examples.c
@@ -0,0 +1,199 @@
+/* Copyright 2022 Jouke Witteveen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "quantum.h"
+#include "examples.h"
+
+// Example code from the tap dance documentation, adapted for testing
+
+// clang-format off
+
+// Example 1
+
+void dance_egg(qk_tap_dance_state_t *state, void *user_data) {
+ if (state->count >= 100) {
+ // SEND_STRING("Safety dance!");
+ tap_code(KC_C);
+ reset_tap_dance(state);
+ }
+}
+
+
+// Example 2
+
+void dance_flsh_each(qk_tap_dance_state_t *state, void *user_data) {
+ switch (state->count) {
+ case 1:
+ register_code(KC_3);
+ break;
+ case 2:
+ register_code(KC_2);
+ break;
+ case 3:
+ register_code(KC_1);
+ break;
+ case 4:
+ unregister_code(KC_3);
+ // wait_ms(50);
+ unregister_code(KC_2);
+ // wait_ms(50);
+ unregister_code(KC_1);
+ }
+}
+
+void dance_flsh_finished(qk_tap_dance_state_t *state, void *user_data) {
+ if (state->count >= 4) {
+ // reset_keyboard();
+ tap_code(KC_R);
+ }
+}
+
+void dance_flsh_reset(qk_tap_dance_state_t *state, void *user_data) {
+ unregister_code(KC_1);
+ // wait_ms(50);
+ unregister_code(KC_2);
+ // wait_ms(50);
+ unregister_code(KC_3);
+}
+
+
+// Example 3
+
+typedef struct {
+ uint16_t tap;
+ uint16_t hold;
+ uint16_t held;
+} tap_dance_tap_hold_t;
+
+bool process_record_user(uint16_t keycode, keyrecord_t *record) {
+ qk_tap_dance_action_t *action;
+
+ switch (keycode) {
+ case TD(CT_CLN):
+ action = &tap_dance_actions[TD_INDEX(keycode)];
+ if (!record->event.pressed && action->state.count && !action->state.finished) {
+ tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)action->user_data;
+ tap_code16(tap_hold->tap);
+ }
+ }
+ return true;
+}
+
+void tap_dance_tap_hold_finished(qk_tap_dance_state_t *state, void *user_data) {
+ tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)user_data;
+
+ if (state->pressed) {
+ if (state->count == 1
+#ifndef PERMISSIVE_HOLD
+ && !state->interrupted
+#endif
+ ) {
+ register_code16(tap_hold->hold);
+ tap_hold->held = tap_hold->hold;
+ } else {
+ register_code16(tap_hold->tap);
+ tap_hold->held = tap_hold->tap;
+ }
+ }
+}
+
+void tap_dance_tap_hold_reset(qk_tap_dance_state_t *state, void *user_data) {
+ tap_dance_tap_hold_t *tap_hold = (tap_dance_tap_hold_t *)user_data;
+
+ if (tap_hold->held) {
+ unregister_code16(tap_hold->held);
+ tap_hold->held = 0;
+ }
+}
+
+#define ACTION_TAP_DANCE_TAP_HOLD(tap, hold) \
+ { .fn = {NULL, tap_dance_tap_hold_finished, tap_dance_tap_hold_reset}, .user_data = (void *)&((tap_dance_tap_hold_t){tap, hold, 0}), }
+
+
+// Example 4
+
+typedef enum {
+ TD_NONE,
+ TD_UNKNOWN,
+ TD_SINGLE_TAP,
+ TD_SINGLE_HOLD,
+ TD_DOUBLE_TAP,
+ TD_DOUBLE_HOLD,
+ TD_DOUBLE_SINGLE_TAP,
+ TD_TRIPLE_TAP,
+ TD_TRIPLE_HOLD
+} td_state_t;
+
+typedef struct {
+ bool is_press_action;
+ td_state_t state;
+} td_tap_t;
+
+td_state_t cur_dance(qk_tap_dance_state_t *state) {
+ if (state->count == 1) {
+ if (state->interrupted || !state->pressed) return TD_SINGLE_TAP;
+ else return TD_SINGLE_HOLD;
+ } else if (state->count == 2) {
+ if (state->interrupted) return TD_DOUBLE_SINGLE_TAP;
+ else if (state->pressed) return TD_DOUBLE_HOLD;
+ else return TD_DOUBLE_TAP;
+ }
+
+ if (state->count == 3) {
+ if (state->interrupted || !state->pressed) return TD_TRIPLE_TAP;
+ else return TD_TRIPLE_HOLD;
+ } else return TD_UNKNOWN;
+}
+
+static td_tap_t xtap_state = {
+ .is_press_action = true,
+ .state = TD_NONE
+};
+
+void x_finished(qk_tap_dance_state_t *state, void *user_data) {
+ xtap_state.state = cur_dance(state);
+ switch (xtap_state.state) {
+ case TD_SINGLE_TAP: register_code(KC_X); break;
+ case TD_SINGLE_HOLD: register_code(KC_LCTL); break;
+ case TD_DOUBLE_TAP: register_code(KC_ESC); break;
+ case TD_DOUBLE_HOLD: register_code(KC_LALT); break;
+ case TD_DOUBLE_SINGLE_TAP: tap_code(KC_X); register_code(KC_X);
+ default: break; // Not present in documentation
+ }
+}
+
+void x_reset(qk_tap_dance_state_t *state, void *user_data) {
+ switch (xtap_state.state) {
+ case TD_SINGLE_TAP: unregister_code(KC_X); break;
+ case TD_SINGLE_HOLD: unregister_code(KC_LCTL); break;
+ case TD_DOUBLE_TAP: unregister_code(KC_ESC); break;
+ case TD_DOUBLE_HOLD: unregister_code(KC_LALT);
+ case TD_DOUBLE_SINGLE_TAP: unregister_code(KC_X);
+ default: break; // Not present in documentation
+ }
+ xtap_state.state = TD_NONE;
+}
+
+
+qk_tap_dance_action_t tap_dance_actions[] = {
+ [TD_ESC_CAPS] = ACTION_TAP_DANCE_DOUBLE(KC_ESC, KC_CAPS),
+ [CT_EGG] = ACTION_TAP_DANCE_FN(dance_egg),
+ [CT_FLSH] = ACTION_TAP_DANCE_FN_ADVANCED(dance_flsh_each, dance_flsh_finished, dance_flsh_reset),
+ [CT_CLN] = ACTION_TAP_DANCE_TAP_HOLD(KC_COLN, KC_SCLN),
+ [X_CTL] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, x_finished, x_reset)
+};
+
+// clang-format on
diff --git a/tests/tap_dance/examples.h b/tests/tap_dance/examples.h
new file mode 100644
index 0000000000..2622af6b2f
--- /dev/null
+++ b/tests/tap_dance/examples.h
@@ -0,0 +1,33 @@
+/* Copyright 2022 Jouke Witteveen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum {
+ TD_ESC_CAPS,
+ CT_EGG,
+ CT_FLSH,
+ CT_CLN,
+ X_CTL,
+};
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/tests/tap_dance/test.mk b/tests/tap_dance/test.mk
new file mode 100644
index 0000000000..041d9b4dc9
--- /dev/null
+++ b/tests/tap_dance/test.mk
@@ -0,0 +1,22 @@
+# Copyright 2022 Jouke Witteveen
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# --------------------------------------------------------------------------------
+# Keep this file, even if it is empty, as a marker that this folder contains tests
+# --------------------------------------------------------------------------------
+
+TAP_DANCE_ENABLE = yes
+
+SRC += examples.c
diff --git a/tests/tap_dance/test_examples.cpp b/tests/tap_dance/test_examples.cpp
new file mode 100644
index 0000000000..6dabc45513
--- /dev/null
+++ b/tests/tap_dance/test_examples.cpp
@@ -0,0 +1,318 @@
+/* Copyright 2022 Jouke Witteveen
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "keyboard_report_util.hpp"
+#include "keycode.h"
+#include "test_common.hpp"
+#include "action_tapping.h"
+#include "test_keymap_key.hpp"
+#include "examples.h"
+
+using testing::_;
+using testing::InSequence;
+
+class TapDance : public TestFixture {};
+
+TEST_F(TapDance, DoubleTap) {
+ TestDriver driver;
+ InSequence s;
+ auto key_esc_caps = KeymapKey{0, 1, 0, TD(TD_ESC_CAPS)};
+
+ set_keymap({key_esc_caps});
+
+ /* The tap dance key does nothing on the first press */
+ key_esc_caps.press();
+ run_one_scan_loop();
+ key_esc_caps.release();
+ EXPECT_NO_REPORT(driver);
+
+ /* We get the key press and the release on timeout */
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Double tap gets us the second key */
+ tap_key(key_esc_caps);
+ EXPECT_NO_REPORT(driver);
+ key_esc_caps.press();
+ EXPECT_REPORT(driver, (KC_CAPS));
+ run_one_scan_loop();
+ key_esc_caps.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, DoubleTapWithMod) {
+ TestDriver driver;
+ InSequence s;
+ auto key_esc_caps = KeymapKey{0, 1, 0, TD(TD_ESC_CAPS)};
+ auto key_shift = KeymapKey{0, 2, 0, KC_LSFT};
+
+ set_keymap({key_esc_caps, key_shift});
+
+ /* The tap dance key does nothing on the first press */
+ key_shift.press();
+ EXPECT_REPORT(driver, (KC_LSFT));
+ run_one_scan_loop();
+ key_esc_caps.press();
+ run_one_scan_loop();
+
+ key_esc_caps.release();
+ key_shift.release();
+ EXPECT_EMPTY_REPORT(driver);
+
+ /* We get the key press and the release */
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_ESC));
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Double tap gets us the second key */
+ key_shift.press();
+ EXPECT_REPORT(driver, (KC_LSFT));
+ run_one_scan_loop();
+ tap_key(key_esc_caps);
+ EXPECT_NO_REPORT(driver);
+ key_shift.release();
+ key_esc_caps.press();
+ EXPECT_REPORT(driver, (KC_LSFT, KC_CAPS));
+ EXPECT_REPORT(driver, (KC_CAPS));
+ run_one_scan_loop();
+ key_esc_caps.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, DoubleTapInterrupted) {
+ TestDriver driver;
+ InSequence s;
+ auto key_esc_caps = KeymapKey{0, 1, 0, TD(TD_ESC_CAPS)};
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({key_esc_caps, regular_key});
+
+ /* Interrupted double tap */
+ tap_key(key_esc_caps);
+ regular_key.press();
+ /* Immediate tap of the first key */
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
+ /* Followed by the interrupting key */
+ EXPECT_REPORT(driver, (KC_A));
+ run_one_scan_loop();
+ regular_key.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Second tap after being interrupted acts as a single tap */
+ key_esc_caps.press();
+ run_one_scan_loop();
+ key_esc_caps.release();
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, DanceFn) {
+ TestDriver driver;
+ InSequence s;
+ auto key_egg = KeymapKey(0, 1, 0, TD(CT_EGG));
+
+ set_keymap({key_egg});
+
+ /* 99 taps do nothing */
+ for (int i = 0; i < 99; i++) {
+ run_one_scan_loop();
+ key_egg.press();
+ run_one_scan_loop();
+ key_egg.release();
+ }
+ idle_for(TAPPING_TERM);
+ EXPECT_NO_REPORT(driver);
+ run_one_scan_loop();
+
+ /* 100 taps trigger the action */
+ for (int i = 0; i < 100; i++) {
+ run_one_scan_loop();
+ key_egg.press();
+ run_one_scan_loop();
+ key_egg.release();
+ }
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* 250 taps act the same as 100 taps */
+ /* Taps are counted in an uint8_t, so the count overflows after 255 taps */
+ for (int i = 0; i < 250; i++) {
+ run_one_scan_loop();
+ key_egg.press();
+ run_one_scan_loop();
+ key_egg.release();
+ }
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_C));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, DanceFnAdvanced) {
+ TestDriver driver;
+ InSequence s;
+ auto key_flsh = KeymapKey(0, 1, 0, TD(CT_FLSH));
+
+ set_keymap({key_flsh});
+
+ /* Three taps don't trigger a reset */
+ EXPECT_REPORT(driver, (KC_3));
+ EXPECT_REPORT(driver, (KC_3, KC_2));
+ EXPECT_REPORT(driver, (KC_3, KC_2, KC_1));
+ for (int i = 0; i < 3; i++) {
+ run_one_scan_loop();
+ key_flsh.press();
+ run_one_scan_loop();
+ key_flsh.release();
+ }
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_3, KC_2));
+ EXPECT_REPORT(driver, (KC_3));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Four taps trigger a reset */
+ EXPECT_REPORT(driver, (KC_3));
+ EXPECT_REPORT(driver, (KC_3, KC_2));
+ EXPECT_REPORT(driver, (KC_3, KC_2, KC_1));
+ EXPECT_REPORT(driver, (KC_2, KC_1));
+ EXPECT_REPORT(driver, (KC_1));
+ EXPECT_EMPTY_REPORT(driver);
+ for (int i = 0; i < 4; i++) {
+ run_one_scan_loop();
+ key_flsh.press();
+ run_one_scan_loop();
+ key_flsh.release();
+ }
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_R));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, TapHold) {
+ TestDriver driver;
+ InSequence s;
+ auto key_cln = KeymapKey{0, 1, 0, TD(CT_CLN)};
+
+ set_keymap({key_cln});
+
+ /* Short taps fire on release */
+ key_cln.press();
+ run_one_scan_loop();
+ key_cln.release();
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_SCLN));
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Holds immediate following a tap apply to the tap key */
+ key_cln.press();
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_REPORT(driver, (KC_LSFT, KC_SCLN));
+ idle_for(TAPPING_TERM * 2);
+ key_cln.release();
+ EXPECT_REPORT(driver, (KC_LSFT));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Holds trigger the hold key */
+ key_cln.press();
+ idle_for(TAPPING_TERM);
+ run_one_scan_loop();
+ EXPECT_REPORT(driver, (KC_SCLN));
+ run_one_scan_loop();
+ key_cln.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
+
+TEST_F(TapDance, QuadFunction) {
+ TestDriver driver;
+ InSequence s;
+ auto key_quad = KeymapKey{0, 1, 0, TD(X_CTL)};
+ auto regular_key = KeymapKey(0, 2, 0, KC_A);
+
+ set_keymap({key_quad, regular_key});
+
+ /* Single tap */
+ key_quad.press();
+ run_one_scan_loop();
+ key_quad.release();
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_X));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Single hold */
+ key_quad.press();
+ run_one_scan_loop();
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_LCTL));
+ run_one_scan_loop();
+ key_quad.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Double tap */
+ tap_key(key_quad);
+ key_quad.press();
+ run_one_scan_loop();
+ key_quad.release();
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_ESC));
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Double tap and hold */
+ tap_key(key_quad);
+ key_quad.press();
+ run_one_scan_loop();
+ idle_for(TAPPING_TERM);
+ EXPECT_REPORT(driver, (KC_LALT));
+ run_one_scan_loop();
+ key_quad.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+
+ /* Double single tap */
+ tap_key(key_quad);
+ tap_key(key_quad);
+ regular_key.press();
+ EXPECT_REPORT(driver, (KC_X));
+ EXPECT_EMPTY_REPORT(driver);
+ EXPECT_REPORT(driver, (KC_X));
+ EXPECT_EMPTY_REPORT(driver);
+ EXPECT_REPORT(driver, (KC_A));
+ run_one_scan_loop();
+ regular_key.release();
+ EXPECT_EMPTY_REPORT(driver);
+ run_one_scan_loop();
+}
diff --git a/tests/test_common/test_fixture.cpp b/tests/test_common/test_fixture.cpp
index 5fc6964054..44694cd390 100644
--- a/tests/test_common/test_fixture.cpp
+++ b/tests/test_common/test_fixture.cpp
@@ -108,6 +108,22 @@ void TestFixture::tap_key(KeymapKey key, unsigned delay_ms) {
run_one_scan_loop();
}
+void TestFixture::tap_combo(const std::vector<KeymapKey>& chord_keys, unsigned delay_ms) {
+ for (KeymapKey key : chord_keys) { // Press each key.
+ key.press();
+ run_one_scan_loop();
+ }
+
+ if (delay_ms > 1) {
+ idle_for(delay_ms - 1);
+ }
+
+ for (KeymapKey key : chord_keys) { // Release each key.
+ key.release();
+ run_one_scan_loop();
+ }
+}
+
void TestFixture::set_keymap(std::initializer_list<KeymapKey> keys) {
this->keymap.clear();
for (auto& key : keys) {
diff --git a/tests/test_common/test_fixture.hpp b/tests/test_common/test_fixture.hpp
index 81906f76c7..2590acd006 100644
--- a/tests/test_common/test_fixture.hpp
+++ b/tests/test_common/test_fixture.hpp
@@ -53,6 +53,13 @@ class TestFixture : public testing::Test {
}
}
+ /**
+ * @brief Taps a combo with `delay_ms` delay between press and release.
+ *
+ * Example: `tap_combo({key_a, key_b})` to tap the chord A + B.
+ */
+ void tap_combo(const std::vector<KeymapKey>& chord_keys, unsigned delay_ms = 1);
+
void run_one_scan_loop();
void idle_for(unsigned ms);