block / block/buzz

e2e: community-rail keyboard reorder flakes up to 58% — @dnd-kit KeyboardSensor attaches keydown a macrotask after pick-up

Open
#6,007 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.