Wait for shortcut bindings in work item shortcuts spec

What this fixes

spec/features/work_items/detail/shortcuts_work_item_spec.rb fails intermittently on master. The failures pass on automatic retry, so they never block a pipeline and never open a [Test] issue, but they cost a 30s Capybara timeout plus a retried job each time.

Related to #622982

That work item reports 20,167 pass-on-retry flakes over 2026-05-24 to 2026-08-24, ~99% of them FrozenError. The FrozenError half is already fixed by !249637 (merged) (merged 2026-08-12). This MR addresses what is left.

Root cause

Keyboard shortcut handlers are bound through addShortcutsExtension (app/assets/javascripts/behaviors/shortcuts/index.js), which resolves a dynamic import of the shortcuts bundle. work_item_detail.vue calls it from mounted().

wait_for_requests only reads window.pendingRequests, window.pendingApolloRequests and window.pendingRailsUJSRequests (spec/support/helpers/wait_for_requests.rb). A dynamic chunk import is none of those. So the spec can send m after wait_for_requests returns but before Mousetrap has a binding for it. The key reaches no handler, ShortcutsWorkItem.openSidebarDropdown never runs, nothing is logged, and the next Capybara finder burns its full 30 seconds.

r is a second instance of the same shape: it is bound when the notes app mounts (work_item_notes.vue), which lands after wait_for_requests sees the last request settle.

Evidence

Pass-on-retry flakes are recorded in the rspec:flaky-tests-report artifact rspec/retried_tests_report.txt. Scanning 988 master pipelines from 2026-08-13 to 2026-08-25 (561 with a valid report) found 41 failing master jobs, all rspec system pg17 N/32, all green on retry:

Example Jobs Assertion that timed out
pressing m opens milestones dropdown 16 .gl-new-dropdown-panel in work-item-milestone (line 31)
pressing a opens assignee dropdown 8 work-item-assignees (line 43)
pressing e starts editing mode 7 [data-testid="work-item-title-input"] (line 49)
pressing r focuses main comment field 7 .js-main-target-form .js-gfm-input:focus (line 58)
pressing l opens labels dropdown 6 work-item-labels (line 37)

Sample jobs: 16040354558 (m), 16034850096 (l), 16017253769 (a), 16035521396 (e), 15990777699 (m + r). Most recent hit: 16101742913, 2026-08-25.

The Capybara HTML snapshots all show the same thing. The page is fully rendered. The milestone widget is present in read-only state, with its Edit button carrying shortcut-sidebar-dropdown-toggle and title="Change milestone <kbd>m</kbd>". No dropdown panel inside any sidebar widget. No alert, no spinner, no skeleton. gon.keyboard_shortcuts_enabled=true. No browser console errors and no infra errors in any trace. The keypress simply had no effect.

What changed

  1. behaviors/shortcuts/index.js sets document.body.dataset.shortcutsReady once an extension is bound. This makes an async boundary observable instead of invisible.
  2. wait_for_keyboard_shortcuts added to WaitForRequests, which is already included in every feature spec.
  3. The spec calls it, and waits for the note to render so the r handler is bound.
  4. A jest spec covers the new marker.

No timeout bumps, no sleep, no :retry metadata.

Verification

The natural flake did not reproduce locally: 3 full runs plus 12 instrumented runs, zero failures. Instrumentation shows why. Locally the bindings land at 2.6-4.0s while wait_for_requests returns at 16.5-19.7s, so this machine has about 13 seconds of slack that a loaded CI runner does not.

So the mechanism was confirmed by forcing the bundle to resolve 20 seconds late, which puts the keypress inside the window CI hits by chance:

Scenario Result
Spec as-is, binding forced late 5 of 8 fail: m, l, a, e, and . (Web IDE)
Same delay, spec waits for bindings 0 of 8 fail

The forced-failure text matched the CI failures down to the XPath shape (.../MAIN[1]/DIV[5]/.../SECTION[3]).

After the fix, with the artificial delay removed: 3 consecutive full runs green (seeds 34271, 7051, 35349). RuboCop clean, ESLint clean, jest passes.

What this does not do

It does not explain why the residual rate stepped up on 2026-08-18. There were zero hits in 230 valid master reports from 08-13 to 08-17, then a steady 10-15% per pipeline from 08-18. The tightest SHA boundary lands on a REST-API-only commit, so that boundary is noise. Two shortcut-area changes sit in the window (5298faebc098, ref-counted Mousetrap pause, 08-13; 2629a2c3314b, work item detail panel layout, 08-14), but there is no evidence tying either to the failures, and gon.keyboard_shortcuts_enabled=true in the snapshots rules out the pause-on-disabled path. The race is structural and older than 08-18; something that week most likely narrowed the timing margin rather than creating the race. The fix closes the race either way, but if the rate stays flat after this lands, that is the thread to pull.

Also worth flagging: ee/spec/features/projects/quality/shortcuts_test_case_spec.rb started appearing in the retried reports the same week, with a related signature. The new helper applies there too, but it is out of scope here.

Merge request reports

Loading
Loading