Fixed in #3615940: Share one stepper widget, and coalesce a booker's clicks into one request and working, but nothing in the test suite would notice it coming back.
The race
Drupal treats an AJAX request as unfinished until every command in its answer has run: it clears its own in-progress flag in a .then() after the command queue resolves, and Drupal.Ajax.prototype.eventResponse() ignores a submit dispatched before then, silently. A booker's click that arrives while a request is out cannot ride along with it, so it is sent by that request's answer instead, which is to say by one of those very commands. It therefore aims straight into the window where it will be dropped.
On a fast machine the commands finish inside the pause before the re-send and nothing is lost. On a loaded one the click was dropped, the booker's number sat on the page with nothing held for it, and the widget believed a request was out and refused to send anything until its watchdog expired.
Why this needs a test rather than a note
This surfaced as OfferStepperTest failing about one continuous integration run in two, on merge requests that had nothing to do with the stepper, at testClickDuringTheRequestSurvives and testTypingWhileTheRequestIsOutIsKept. Those two tests do exercise the path, but only by accident of timing: they pass on a fast runner whether the bug is present or not, which is exactly why the cause took so long to find and why a retry that quietly stopped working would read as ordinary flakiness rather than as a regression.
There is a second reason a test is worth more than usual here. The correct fix needs two different questions kept apart: whether Drupal would drop a dispatch right now, which gates the dispatch, and whether an answer is still coming that will carry the gesture, which gates the gesture. Collapsing them into one is a natural-looking simplification, and it loses the gesture outright by holding it back until after the only thing that would have sent it has run. That is a comment today. It should be an assertion.
Proposed resolution
Hold the window open deliberately instead of waiting for a slow runner to do it: wrap the state command so that it returns a promise resolving after a beat, which is precisely what a loaded machine does for free, then make a click while a request is out and assert the quantity still reaches storage. Written that way the test fails against the unfixed script every time and tells the same story on any machine.
Worth covering the second question in the same class, so that collapsing the two guards fails rather than merely being discouraged: a gesture made while a request is out and completed after its answer has been applied still has to reach the server.
Remaining tasks
- Add the test against the shared queue, where the retry now lives.
- Confirm it fails with the retry removed.
Issue fork yoyaku-3616053
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
mably commented