Browse Source

[Bug][Core] Fix for Flow Tap: fix handling of distinct taps and timer updates. (#25175)

* Flow Tap bug fix.

As reported by @amarz45 and @mwpardue, there is a bug where if two
tap-hold keys are pressed in distinct taps back to back, then Flow Tap
is not applied on the second tap-hold key, but it should be.

In a related bug reported by @NikGovorov, if a tap-hold key is held
followed by a tap of a tap-hold key, then Flow Tap updates its timer on
the release of the held tap-hold key, but it should be ignored.

The problem common to both these bugs is that I incorrectly assumed
`tapping_key` is cleared to noevent once it is released, when actually
`tapping_key` is still maintained for `TAPPING_TERM` ms after release
(for Quick Tap). This commit fixes that. Thanks to @amarz45, @mwpardue,
and @NikGovorov for reporting!

Details:

* Logic for converting the current tap-hold event to a tap is extracted
  to `flow_tap_key_if_within_term()`, which is now invoked also in the
  post-release "interfered with other tap key" case. This fixes the
  distinct taps bug.

* The Flow Tap timer is now updated at the beginning of each call to
  `process_record()`, provided that there is no unsettled tap-hold key
  at that time and that the record is not for a mod or layer switch key.
  By moving this update logic to `process_record()`, it is conceptually
  simpler and more robust.

* Unit tests extended to cover the reported scenarios.

* Fix formatting.

* Revision to fix @NikGovorov's scenario.

The issue is that when another key is pressed while a layer-tap hasn't
been settled yet, the `prev_keycode` remembers the keycode from before
the layer switched. This can then enable Flow Tap for the following key
when it shouldn't, or vice versa.

Thanks to @NikGovorov for reporting!

This commit revises Flow Tap in the following ways:

* The previous key and timer are both updated from `process_record()`.
  This is slightly later in the sequence of processing than before, and
  by this point, a just-settled layer-tap should have taken effect so
  that the keycode from the correct layer is remembered.

* The Flow Tap previous key and timer are updated now also on key
  release events, except for releases of modifiers and held layer
  switches.

* The Flow Tap previous key and timer are now updated together, for
  simplicity. This makes the logic easier to think about.

* A few additional unit tests, including @NikGovorov's scenario as
  "layer_tap_ignored_with_disabled_key_complex."
Pascal Getreuer 1 năm trước cách đây
mục cha
commit
73e2ef486a

+ 3 - 0
quantum/action.c

@@ -281,6 +281,9 @@ void process_record(keyrecord_t *record) {
     if (IS_NOEVENT(record->event)) {
         return;
     }
+#ifdef FLOW_TAP_TERM
+    flow_tap_update_last_event(record);
+#endif // FLOW_TAP_TERM
 
     if (!process_record_quantum(record)) {
 #ifndef NO_ACTION_ONESHOT

+ 73 - 32
quantum/action_tapping.c

@@ -6,6 +6,7 @@
 #include "action_tapping.h"
 #include "action_util.h"
 #include "keycode.h"
+#include "quantum_keycodes.h"
 #include "timer.h"
 
 #ifndef NO_ACTION_TAPPING
@@ -102,10 +103,10 @@ __attribute__((weak)) bool get_hold_on_other_key_press(uint16_t keycode, keyreco
 #    endif
 
 #    if defined(FLOW_TAP_TERM)
-static uint32_t last_input   = 0;
-static uint16_t prev_keycode = KC_NO;
+static uint32_t flow_tap_prev_time    = 0;
+static uint16_t flow_tap_prev_keycode = KC_NO;
 
-uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
+static bool flow_tap_key_if_within_term(keyrecord_t *record);
 #    endif // defined(FLOW_TAP_TERM)
 
 static keyrecord_t tapping_key                         = {};
@@ -157,19 +158,6 @@ void action_tapping_process(keyrecord_t record) {
         }
     }
     if (IS_EVENT(record.event)) {
-#    if defined(FLOW_TAP_TERM)
-        const uint16_t keycode = get_record_keycode(&record, false);
-        // Track the previous key press.
-        if (record.event.pressed) {
-            prev_keycode = keycode;
-        }
-        // If there is no unsettled tap-hold key, update last input time. Ignore
-        // mod keys in this update to allow for chording multiple mods for
-        // hotkeys like "Ctrl+Shift+arrow".
-        if (IS_NOEVENT(tapping_key.event) && !IS_MODIFIER_KEYCODE(keycode)) {
-            last_input = timer_read32();
-        }
-#    endif // defined(FLOW_TAP_TERM)
         ac_dprintf("\n");
     }
 }
@@ -252,22 +240,8 @@ bool process_tapping(keyrecord_t *keyp) {
             // into the "pressed" tapping key state
 
 #    if defined(FLOW_TAP_TERM)
-            const uint16_t keycode = get_record_keycode(keyp, false);
-            if (is_mt_or_lt(keycode)) {
-                const uint32_t idle_time = timer_elapsed32(last_input);
-                uint16_t       term      = get_flow_tap_term(keycode, keyp, prev_keycode);
-                if (term > 500) {
-                    term = 500;
-                }
-                if (idle_time < 500 && idle_time < term) {
-                    debug_event(keyp->event);
-                    ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", (int16_t)idle_time, term);
-                    keyp->tap.count = 1;
-                    registered_taps_add(keyp->event.key);
-                    debug_registered_taps();
-                    process_record(keyp);
-                    return true;
-                }
+            if (flow_tap_key_if_within_term(keyp)) {
+                return true;
             }
 #    endif // defined(FLOW_TAP_TERM)
 
@@ -582,6 +556,13 @@ bool process_tapping(keyrecord_t *keyp) {
                     return true;
                 } else if (is_tap_record(keyp)) {
                     // Sequential tap can be interfered with other tap key.
+#    if defined(FLOW_TAP_TERM)
+                    if (flow_tap_key_if_within_term(keyp)) {
+                        tapping_key = (keyrecord_t){0};
+                        debug_tapping_key();
+                        return true;
+                    }
+#    endif // defined(FLOW_TAP_TERM)
                     ac_dprintf("Tapping: Start with interfering other tap.\n");
                     tapping_key = *keyp;
                     waiting_buffer_scan_tap();
@@ -809,6 +790,66 @@ static void waiting_buffer_process_regular(void) {
 #    endif // CHORDAL_HOLD
 
 #    ifdef FLOW_TAP_TERM
+void flow_tap_update_last_event(keyrecord_t *record) {
+    // Don't update while a tap-hold key is unsettled.
+    if (waiting_buffer_tail != waiting_buffer_head || (tapping_key.event.pressed && tapping_key.tap.count == 0)) {
+        return;
+    }
+    const uint16_t keycode = get_record_keycode(record, false);
+    // Ignore releases of modifiers and held layer switches.
+    if (!record->event.pressed) {
+        switch (keycode) {
+            case MODIFIER_KEYCODE_RANGE:
+            case QK_MOMENTARY ... QK_MOMENTARY_MAX:
+            case QK_LAYER_TAP_TOGGLE ... QK_LAYER_TAP_TOGGLE_MAX:
+#        ifndef NO_ACTION_ONESHOT // Ignore one-shot keys.
+            case QK_ONE_SHOT_MOD ... QK_ONE_SHOT_MOD_MAX:
+            case QK_ONE_SHOT_LAYER ... QK_ONE_SHOT_LAYER_MAX:
+#        endif                  // NO_ACTION_ONESHOT
+#        ifdef TRI_LAYER_ENABLE // Ignore Tri Layer keys.
+            case QK_TRI_LAYER_LOWER:
+            case QK_TRI_LAYER_UPPER:
+#        endif // TRI_LAYER_ENABLE
+                return;
+            case QK_MODS ... QK_MODS_MAX:
+                if (QK_MODS_GET_BASIC_KEYCODE(keycode) == KC_NO) {
+                    return;
+                }
+                break;
+            case QK_MOD_TAP ... QK_MOD_TAP_MAX:
+            case QK_LAYER_TAP ... QK_LAYER_TAP_MAX:
+                if (record->tap.count == 0) {
+                    return;
+                }
+                break;
+        }
+    }
+
+    flow_tap_prev_keycode = keycode;
+    flow_tap_prev_time    = timer_read32();
+}
+
+static bool flow_tap_key_if_within_term(keyrecord_t *record) {
+    const uint16_t keycode = get_record_keycode(record, false);
+    if (is_mt_or_lt(keycode)) {
+        const uint32_t idle_time = timer_elapsed32(flow_tap_prev_time);
+        uint16_t       term      = get_flow_tap_term(keycode, record, flow_tap_prev_keycode);
+        if (term > 500) {
+            term = 500;
+        }
+        if (idle_time < 500 && idle_time < term) {
+            debug_event(record->event);
+            ac_dprintf(" within flow tap term (%u < %u) considered a tap\n", (int16_t)idle_time, term);
+            record->tap.count = 1;
+            registered_taps_add(record->event.key);
+            debug_registered_taps();
+            process_record(record);
+            return true;
+        }
+    }
+    return false;
+}
+
 // By default, enable Flow Tap for the keys in the main alphas area and Space.
 // This should work reasonably even if the layout is remapped on the host to an
 // alt layout or international layout (e.g. Dvorak or AZERTY), where these same

+ 3 - 0
quantum/action_tapping.h

@@ -166,6 +166,9 @@ bool is_flow_tap_key(uint16_t keycode);
  * @return Time in milliseconds.
  */
 uint16_t get_flow_tap_term(uint16_t keycode, keyrecord_t *record, uint16_t prev_keycode);
+
+/** Updates the Flow Tap last key and timer. */
+void flow_tap_update_last_event(keyrecord_t *record);
 #endif // FLOW_TAP_TERM
 
 #ifdef DYNAMIC_TAPPING_TERM_ENABLE

+ 1 - 0
tests/tap_hold_configurations/flow_tap/config.h

@@ -20,3 +20,4 @@
 #include "test_common.h"
 
 #define FLOW_TAP_TERM 150
+#define PERMISSIVE_HOLD

+ 330 - 1
tests/tap_hold_configurations/flow_tap/test_tap_hold.cpp

@@ -25,7 +25,169 @@ using testing::InSequence;
 
 class FlowTapTest : public TestFixture {};
 
-TEST_F(FlowTapTest, short_flow_tap_settled_as_tapped) {
+// Test an input of quick distinct taps. All should be settled as tapped.
+TEST_F(FlowTapTest, distinct_taps) {
+    TestDriver driver;
+    InSequence s;
+    auto       regular_key  = KeymapKey(0, 0, 0, KC_A);
+    auto       mod_tap_key1 = KeymapKey(0, 1, 0, SFT_T(KC_B));
+    auto       mod_tap_key2 = KeymapKey(0, 2, 0, CTL_T(KC_C));
+    auto       mod_tap_key3 = KeymapKey(0, 3, 0, ALT_T(KC_D));
+
+    set_keymap({regular_key, mod_tap_key1, mod_tap_key2, mod_tap_key3});
+
+    // Tap regular key.
+    EXPECT_REPORT(driver, (KC_A));
+    EXPECT_EMPTY_REPORT(driver);
+    tap_key(regular_key, FLOW_TAP_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap mod-tap 1.
+    EXPECT_REPORT(driver, (KC_B));
+    mod_tap_key1.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    mod_tap_key1.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap mod-tap 2.
+    EXPECT_REPORT(driver, (KC_C));
+    mod_tap_key2.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    mod_tap_key2.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap mod-tap 3.
+    EXPECT_REPORT(driver, (KC_D));
+    mod_tap_key3.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    mod_tap_key3.release();
+    idle_for(FLOW_TAP_TERM + 1); // Pause between taps.
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap mod-tap 1.
+    EXPECT_NO_REPORT(driver);
+    mod_tap_key1.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_REPORT(driver, (KC_B));
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    mod_tap_key1.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap mod-tap 2.
+    EXPECT_REPORT(driver, (KC_C));
+    mod_tap_key2.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    mod_tap_key2.release();
+    idle_for(FLOW_TAP_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+}
+
+// By default, Flow Tap is disabled when mods other than Shift and AltGr are on.
+TEST_F(FlowTapTest, hotkey_taps) {
+    TestDriver driver;
+    InSequence s;
+    auto       ctrl_key    = KeymapKey(0, 0, 0, KC_LCTL);
+    auto       shft_key    = KeymapKey(0, 1, 0, KC_LSFT);
+    auto       alt_key     = KeymapKey(0, 2, 0, KC_LALT);
+    auto       gui_key     = KeymapKey(0, 3, 0, KC_LGUI);
+    auto       regular_key = KeymapKey(0, 4, 0, KC_A);
+    auto       mod_tap_key = KeymapKey(0, 5, 0, RCTL_T(KC_B));
+
+    set_keymap({ctrl_key, shft_key, alt_key, gui_key, regular_key, mod_tap_key});
+
+    for (KeymapKey* mod_key : {&ctrl_key, &alt_key, &gui_key}) {
+        // Hold mod key.
+        EXPECT_REPORT(driver, (mod_key->code));
+        mod_key->press();
+        run_one_scan_loop();
+
+        // Tap regular key.
+        EXPECT_REPORT(driver, (mod_key->code, KC_A));
+        regular_key.press();
+        run_one_scan_loop();
+        VERIFY_AND_CLEAR(driver);
+
+        EXPECT_REPORT(driver, (mod_key->code));
+        regular_key.release();
+        run_one_scan_loop();
+        VERIFY_AND_CLEAR(driver);
+
+        // Press mod-tap, where Flow Tap is disabled due to the held mod.
+        EXPECT_REPORT(driver, (mod_key->code, KC_RCTL));
+        mod_tap_key.press();
+        idle_for(TAPPING_TERM + 1);
+        VERIFY_AND_CLEAR(driver);
+
+        // Release mod-tap.
+        EXPECT_REPORT(driver, (mod_key->code));
+        mod_tap_key.release();
+        run_one_scan_loop();
+
+        // Release mod key.
+        EXPECT_EMPTY_REPORT(driver);
+        mod_key->release();
+        run_one_scan_loop();
+        VERIFY_AND_CLEAR(driver);
+    }
+
+    // Hold Shift key.
+    EXPECT_REPORT(driver, (KC_LSFT));
+    shft_key.press();
+    run_one_scan_loop();
+
+    // Tap regular key.
+    EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+    regular_key.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_REPORT(driver, (KC_LSFT));
+    regular_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    // Press mod-tap, where Flow Tap applies to settle as tapped.
+    EXPECT_REPORT(driver, (KC_LSFT, KC_B));
+    mod_tap_key.press();
+    idle_for(TAPPING_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    // Release mod-tap.
+    EXPECT_REPORT(driver, (KC_LSFT));
+    mod_tap_key.release();
+    run_one_scan_loop();
+
+    // Release Shift key.
+    EXPECT_EMPTY_REPORT(driver);
+    shft_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+}
+
+// Test input with two mod-taps in a rolled press quickly after a regular key.
+TEST_F(FlowTapTest, rolled_press) {
     TestDriver driver;
     InSequence s;
     auto       regular_key  = KeymapKey(0, 0, 0, KC_A);
@@ -152,6 +314,53 @@ TEST_F(FlowTapTest, holding_multiple_mod_taps) {
     VERIFY_AND_CLEAR(driver);
 }
 
+TEST_F(FlowTapTest, holding_mod_tap_with_regular_mod) {
+    TestDriver driver;
+    InSequence s;
+    auto       regular_key = KeymapKey(0, 0, 0, KC_A);
+    auto       mod_key     = KeymapKey(0, 1, 0, KC_LSFT);
+    auto       mod_tap_key = KeymapKey(0, 2, 0, CTL_T(KC_C));
+
+    set_keymap({regular_key, mod_key, mod_tap_key});
+
+    // Tap regular key.
+    EXPECT_REPORT(driver, (KC_A));
+    EXPECT_EMPTY_REPORT(driver);
+    tap_key(regular_key);
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_NO_REPORT(driver);
+    idle_for(FLOW_TAP_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    // Press mod and mod-tap keys.
+    EXPECT_REPORT(driver, (KC_LSFT));
+    mod_key.press();
+    run_one_scan_loop();
+    mod_tap_key.press();
+    idle_for(TAPPING_TERM - 5); // Hold almost until tapping term.
+    VERIFY_AND_CLEAR(driver);
+
+    // Press regular key.
+    EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+    EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL, KC_A));
+    regular_key.press();
+    idle_for(10);
+    VERIFY_AND_CLEAR(driver);
+
+    // Release keys.
+    EXPECT_REPORT(driver, (KC_LSFT, KC_LCTL));
+    EXPECT_REPORT(driver, (KC_LCTL));
+    EXPECT_EMPTY_REPORT(driver);
+    regular_key.release();
+    run_one_scan_loop();
+    mod_key.release();
+    run_one_scan_loop();
+    mod_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+}
+
 TEST_F(FlowTapTest, layer_tap_key) {
     TestDriver driver;
     InSequence s;
@@ -216,6 +425,125 @@ TEST_F(FlowTapTest, layer_tap_key) {
     VERIFY_AND_CLEAR(driver);
 }
 
+TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key) {
+    TestDriver driver;
+    InSequence s;
+    auto       no_key        = KeymapKey(0, 0, 0, KC_NO);
+    auto       regular_key   = KeymapKey(1, 0, 0, KC_ESC);
+    auto       layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+    auto       mod_tap_key   = KeymapKey(0, 2, 0, CTL_T(KC_B));
+
+    set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
+
+    EXPECT_REPORT(driver, (KC_ESC));
+    EXPECT_EMPTY_REPORT(driver);
+    layer_tap_key.press();
+    idle_for(TAPPING_TERM + 1);
+    tap_key(regular_key);
+    layer_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_REPORT(driver, (KC_LCTL));
+    mod_tap_key.press();
+    idle_for(TAPPING_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    mod_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_ignored_with_disabled_key_complex) {
+    TestDriver driver;
+    InSequence s;
+    auto       regular_key1  = KeymapKey(0, 0, 0, KC_Q);
+    auto       layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_SPC));
+    auto       mod_tap_key1  = KeymapKey(0, 2, 0, CTL_T(KC_T));
+    // Place RALT_T(KC_I), where Flow Tap is enabled, in the same position on
+    // layer 0 as KC_RGHT, where Flow Tap is disabled. This tests that Flow Tap
+    // tracks the keycode from the correct layer.
+    auto mod_tap_key2 = KeymapKey(0, 3, 0, RALT_T(KC_I));
+    auto regular_key2 = KeymapKey(1, 3, 0, KC_RGHT);
+
+    set_keymap({regular_key1, layer_tap_key, mod_tap_key1, mod_tap_key2, regular_key2});
+
+    // Tap regular key 1.
+    EXPECT_REPORT(driver, (KC_Q));
+    EXPECT_EMPTY_REPORT(driver);
+    tap_key(regular_key1);
+    idle_for(FLOW_TAP_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    // Hold layer-tap key.
+    EXPECT_NO_REPORT(driver);
+    layer_tap_key.press();
+    run_one_scan_loop();
+    // idle_for(TAPPING_TERM + 1);
+    VERIFY_AND_CLEAR(driver);
+
+    // Tap regular key 2.
+    EXPECT_REPORT(driver, (KC_RGHT));
+    EXPECT_EMPTY_REPORT(driver);
+    tap_key(regular_key2);
+    VERIFY_AND_CLEAR(driver);
+
+    // Release layer-tap key.
+    EXPECT_NO_REPORT(driver);
+    layer_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    // Quickly hold mod-tap key 1.
+    EXPECT_NO_REPORT(driver);
+    mod_tap_key1.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_REPORT(driver, (KC_LCTL));
+    EXPECT_REPORT(driver, (KC_LCTL, KC_Q));
+    EXPECT_REPORT(driver, (KC_LCTL));
+    tap_key(regular_key1);
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    mod_tap_key1.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+}
+
+TEST_F(FlowTapTest, layer_tap_ignored_with_enabled_key) {
+    TestDriver driver;
+    InSequence s;
+    auto       no_key        = KeymapKey(0, 0, 0, KC_NO);
+    auto       regular_key   = KeymapKey(1, 0, 0, KC_C);
+    auto       layer_tap_key = KeymapKey(0, 1, 0, LT(1, KC_A));
+    auto       mod_tap_key   = KeymapKey(0, 2, 0, CTL_T(KC_B));
+
+    set_keymap({no_key, regular_key, layer_tap_key, mod_tap_key});
+
+    EXPECT_REPORT(driver, (KC_C));
+    EXPECT_EMPTY_REPORT(driver);
+    layer_tap_key.press();
+    idle_for(TAPPING_TERM + 1);
+    tap_key(regular_key);
+    layer_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_REPORT(driver, (KC_B));
+    mod_tap_key.press();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+
+    EXPECT_EMPTY_REPORT(driver);
+    idle_for(TAPPING_TERM + 1);
+    mod_tap_key.release();
+    run_one_scan_loop();
+    VERIFY_AND_CLEAR(driver);
+}
+
 TEST_F(FlowTapTest, combo_key) {
     TestDriver driver;
     InSequence s;
@@ -275,6 +603,7 @@ TEST_F(FlowTapTest, oneshot_mod_key) {
     // Nested press of OSM and regular keys.
     EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
     EXPECT_REPORT(driver, (KC_LSFT, KC_A));
+    EXPECT_CALL(driver, send_keyboard_mock(KeyboardReport(KC_LSFT))).Times(AnyNumber());
     EXPECT_EMPTY_REPORT(driver);
     osm_key.press();
     run_one_scan_loop();