atomantic / atomantic/PortOS

VideoTimelineEditor keyboard test flakes in full-suite runs: sync getByRole races the clip lane's commit

Open
#7,316 2 comments 0 reactions 1 assignee Claimed by @LouisDeconinck View on GitHub
effort:low good first issue in-progress model:light planner:opus-5 tests
Dominant language
JavaScript
Stars
38
Forks
31
Avg merge
22m
Merged PRs (30d)
988

Description

## What fails

`VideoTimelineEditor.keyboard.test.jsx` → "still selects the block on Enter, which the drag must not swallow" fails **only in a full-suite run** (`cd client && npx vitest run`, 960 files), with:

```
TestingLibraryElementError: Unable to find an accessible element with the role "button"
and name `Remove second.png from timeline`
❯ blockFor src/pages/VideoTimelineEditor.keyboard.test.jsx:77:4
❯ src/pages/VideoTimelineEditor.keyboard.test.jsx:117:20
```

Observed once in a local full-suite run on 2026-09-13 (959 passed / 1 failed). It does **not** reproduce:

- running the file alone (3/3 pass)
- running `src/pages` (1426 tests) three times in a row

So it is load/timing sensitive, not a deterministic failure.

## Likely cause

`renderEditor()` waits only for the "Loading project…" placeholder to disappear:

```js
const renderEditor = async () => {
render();
await waitFor(() => expect(screen.queryByText('Loading project…')).not.toBeInTheDocument());
};
```

Every test then reaches for a clip block with a **synchronous** query:

```js
const blockFor = (assetFile) => screen
.getByRole('button', { name: `Remove ${assetFile} from timeline` })
.closest('[aria-roledescription="sortable"]');
```

The suite's `getTimelineProject`, `getGalleryImages`, `listVideoHistory`, `listMusicLibrary` and `listImageGalleryPage` mocks are all separate promises. "Loading project…" clearing is not proof that the segment lane has committed its blocks, so under load the sync `getByRole` can run a tick early. The first two tests happen to `await pressKey(...)` right after, which lets a straggling microtask land; the Enter test asserts sooner, which is consistent with it being the one that failed.

## Suggested fix

Make the block lookup await its element rather than assume it is already committed — either:

- `const blockFor = async (assetFile) => (await screen.findByRole('button', { name: ... })).closest('[aria-roledescription="sortable"]')`, awaiting it at the three call sites; or
- extend `renderEditor()` to `await screen.findByRole('button', { name: /Remove .* from timeline/ })` so every test starts from a committed lane.

Prefer whichever keeps the three tests reading the same way. Do **not** paper over it with an arbitrary sleep.

## Verify

Run the whole client suite (`cd client && npx vitest run`) a few times — the file must pass in the full 960-file run, not just standalone. The three assertions the file guards (#7243: Space/arrow/Space reorder, Escape cancel, Enter still selects) must all still hold.

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.