e2e: community-rail keyboard reorder flakes up to 58% — @dnd-kit KeyboardSensor attaches keydown a macrotask after pick-up
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
`community-rail.spec.ts:1329` fails up to 58% of runs at merge-base because @dnd-kit's KeyboardSensor attaches its keydown listener one macrotask after pick-up. Three other tests in the same file are also flaky. All pre-existing — found while auditing e2e noise for the js-to-Rust perf pack, not caused by it.
## Mechanism (source-derived, not inferred)
`@dnd-kit/core` 6.3.1, `node_modules/@dnd-kit/core/dist/core.esm.js:1154-1158`:
```js
attach() {
this.handleStart(); // SYNCHRONOUS: item is picked up
this.windowListeners.add(EventName.Resize, this.handleCancel);
this.windowListeners.add(EventName.VisibilityChange, this.handleCancel);
setTimeout(() => this.listeners.add(EventName.Keydown, this.handleKeyDown));
}
```
Pick-up is synchronous; the keydown listener attaches a macrotask later. Between them the item **is** picked up and **nothing** is listening for ArrowUp. Under main-thread contention the test's ArrowUp lands in that gap and is dropped silently — no error, no rejected promise, the reorder simply never happens, and the test waits out its 5s poll.
This is why the timing is bimodal with nothing in between: pass ~573-633ms, fail ~5.5s.
**The obvious fix does not work.** Waiting on visibility, on the drag overlay, or on picked-up state cannot close this gap, because the item is already picked up while the gap is open — the state you would wait for is *true during the bug*. The fix has to cross a macrotask boundary explicitly.
## Fix
In `desktop/tests/e2e/community-rail.spec.ts`, between the pick-up Space dispatch and the ArrowUp press:
```ts
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 0)));
```
## Evidence — four alternating arms
Base `78cbffeb64c01220e705adf0aa9690fdbd0d7a37`, isolated spec, `--project=smoke --repeat-each=60 --workers=1`, head verified, patch applied and reverted from a pristine copy between arms.
| # | arm | failures / 60 |
|---|---|---|
| 1 | unpatched | 10 |
| 2 | **patched** | **0** |
| 3 | unpatched | 35 |
| 4 | **patched** | **0** |
| 5 | unpatched | 33 |
Arms 3-5 are the reason there are five arms rather than two. My first comparison (10 vs 0) was unpaired — different sessions, different machine load — and re-running showed the *unpatched* rate is itself unstable: 10, then 35, then 33 for byte-identical code. A 10-vs-0 claim would have rested on a number that moves. The result rests on the alternating back-to-back pairs, 35 → 0 → 33, where separation is total.
P(0 failures in 60 | rate 33/60) ≈ 1e-16.
## Three more flaky tests in the same file
From a completed 480-slot arm (24 tests × repeat-each=20) at `d19c7fbfc`:
| line | test | failures |
|---|---|---|
| :716 | does not repair a remembered channel until live validation succeeds | 6 |
| :308 | keeps profile community actions available to members without invite access | 3 |
| :601 | enters a remembered channel before live validation completes | 1 |
| :1329 | keyboard reorder | 1 |
`:716` is the highest-frequency of these and has a **different** signature — live-validation repair, not drag/drop. It is not a variant of the dnd-kit mechanism and needs its own investigation. Its per-failure artifacts were lost to a disk cleanup before I extracted the exact signature, so what I can stand behind is the count and the line, not a captured failure mode. Starting point: the test drives an explicit latch seam (`__BUZZ_E2E_DEFER_NEXT_CHANNELS_READ__` / `__BUZZ_E2E_RELEASE_CHANNELS_READ__`) and asserts `{ released: 0, pending: 0 }`.
## Versions
`@dnd-kit/core` 6.3.1, `@dnd-kit/sortable` 10.0.0.
Contributor guide
Assessment
This issue has not been assessed yet.