New Rule Proposal: no-unsettled-absence-query
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
- issue の種類
- 機能追加
- 明瞭さ
- おおむね明確
- 活発さ
- 静か
- 技術スタック
- eslint, javascript, typescript
- 領域
- testing-qa, tooling
調査の方向性
リポジトリのファイル、テスト、エントリーポイントは指定されていません。まず既存の Testing Library ESLint ルールとそのテストを確認し、次に提案されている失敗例と成功例を比較してください。新しい警告が未確定の不在アサーションを一貫して検出し、文書化された確定用の式を許可できれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Name for new rule
no-unsettled-absence-query
Description of the new rule
Asserting absence with queryBy* + .not.toBeInTheDocument() immediately after render(), before the component has settled, can produce a false positive. The element isn't there yet, not because it won't be there. The community calls this Testing Ghosts.
Testing Library's render() does flush the initial synchronous render (it's wrapped in act() internally), so for a plain component with no async behavior, checking absence right after render() is technically correct. The problem shows up when the component has async side effects like data fetching, lazy loading, state updates from useEffect, etc. In those cases the UI hasn't finished updating, and the absence check passes vacuously. Since the rule can't know whether a component has async behavior, it flags all unsettled absence assertions and lets the developer confirm intent by adding a settling expression.
This rule would enforce that an absence assertion (expect(screen.queryBy*(...)).not.toBeInTheDocument() / .not.toBeVisible()) must be preceded by a "settling" expression within the test body.
Why existing rules don't cover this:
| Existing Rule | Why it doesn't solve this |
|---|---|
prefer-presence-queries |
Enforces correct query type, but doesn't track timing relative to render |
no-await-sync-queries |
Only flags await directly on sync queries, not timing of assertions |
await-async-queries / await-async-utils |
About promise handling, not assertion ordering |
prefer-find-by |
Simplifies waitFor + getBy -> findBy, doesn't detect premature absence checks |
Related issues: #269, #411, #518 - all adjacent but none address this specific pattern.
Testing Library feature
This rule relates to the absence/presence assertion pattern described in the Appearance and Disappearance guide. Specifically, it enforces correct ordering of queryBy* absence assertions relative to async settling utilities (findBy*, waitFor, act) and sync settling queries (getBy*).
Testing Library framework(s)
All frameworks: React, Angular, Vue, Svelte, etc. The pattern applies anywhere screen.queryBy* + .not.toBeInTheDocument() is used.
What category of rule is this?
Warns about a potential error. Less valuable as an error given the call-out above of vanilla components.
Code examples
Fail:
// Absence assertion before component has settled, "Testing a Ghost"
test('shows no error', () => {
render(<AsyncComponent />);
expect(screen.queryByText('error')).not.toBeInTheDocument();
});
// Absence assertion BEFORE the awai, order matters
test('shows no error', async () => {
render(<AsyncComponent />);
expect(screen.queryByText('error')).not.toBeInTheDocument();
await screen.findByText('loaded');
});
// queryAllBy variant
test('shows no alerts', () => {
render(<AsyncComponent />);
expect(screen.queryAllByRole('alert')).not.toBeInTheDocument();
});
// Absence assertion inside waitFor - passes on first retry, still a ghost
test('shows no error', async () => {
render(<AsyncComponent />);
await waitFor(() => {
expect(screen.queryByText('error')).not.toBeInTheDocument();
});
});
Pass:
// findBy* settles the component first
test('shows no error', async () => {
render(<AsyncComponent />);
await screen.findByText('loaded');
expect(screen.queryByText('error')).not.toBeInTheDocument();
});
// waitFor settles the component first
test('shows no error', async () => {
render(<AsyncComponent />);
await waitFor(() => expect(something).toBe(true));
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
// act settles the component first
test('shows no error', async () => {
await act(() => render(<AsyncComponent />));
expect(screen.queryByText('error')).not.toBeInTheDocument();
});
// getBy* proves sync render completed
test('shows no error', () => {
render(<Component />);
screen.getByText('visible heading');
expect(screen.queryByText('error')).not.toBeInTheDocument();
});
What counts as "settled":
- Any
awaitexpression on a preceding statement — coversfindBy*,waitFor,act, custom async helpers - A
screen.getBy*/screen.getAllBy*call — proves the sync render produced expected output
Matchers checked: .not.toBeInTheDocument(), .not.toBeVisible()
Anything else?
References:
- Testing Library: Appearance and Disappearance guide
- Kent C. Dodds: Common mistakes with React Testing Library
- Gerardo Perrucci: Stop Testing Ghosts
- react-testing-library #865 (vague similarity; in the same realm with component mounting/unmounting states)
Do you want to submit a pull request to make the new rule?
I have a working implementation with full test coverage that I'm happy to submit as a PR if this proposal is accepted.
- 主要言語
- TypeScript
- スター
- 1.1k
- フォーク
- 167
- 平均マージ
- 19時間 58分
- マージ済み PR(30日)
- 2
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
testing-library/eslint-plugin-testing-library のほかの issue
-
bug triage
難易度 3/5 半日 初心者へのやさしさ 58/100
-
`no-wait-for-side-effects`: side effects not detected via `userEvent.setup()` instances and when wrapped in `void`対応中かも @AmineBenzaazaa が 8 日前に担当しました。 オープンbug triage
難易度 3/5 1〜2日 初心者へのやさしさ 76/100
testing-library/eslint-plugin-testing-library#1370 · コメント 1 件 ·
-
Rules don’t recognize @testing-library/angular/zoneless imports対応中かも @ThiloAschebrock が 90 日前に担当しました。 オープンbug
難易度 3/5 1〜2日 初心者へのやさしさ 70/100
-
prefer-presence-queries autofix leaves queryBy* in destructure while rewriting call site to getBy*, producing a ReferenceError対応中かも @j1ang3 が 96 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
-
Dependency Dashboardオープンpinned
難易度 5/5 1週間以上 初心者へのやさしさ 10/100
testing-library/eslint-plugin-testing-library の issue をすべて見る
似ている issue
-
bug require-triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
expressjs/expressjs.com#2549 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 83/100
umami-software/umami#4686 · コメント 1 件 ·
メンテナーはふだん 2 日以内に返信
-
[Bug]: Device screenshot fails with 500 on iPhone Duo (Xcode 27.2): serve-sim calls simctl without --display対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
pingdotgg/t3code#24900 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
inu-appcenter/UNIDorm-web#411 ·
メンテナーはふだん 1 日以内に返信
-
🐛 bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 2 日以内に返信