feat(discovery): added built-in test timers rule to defaults
- Added a new built-in TTSR rule `ts-no-test-timers.md` that flags `Bun.sleep`, `setTimeout`, and `setInterval` usage in `*.test.ts` files. - Registered `ts-no-test-timers` in the built-in rules index so it ships with default discovery providers. - Updated builtin-defaults tests to enforce rule-name uniqueness and verify the new rule only matches in `*.test.ts` scopes.
This commit is contained in:
@@ -20,6 +20,7 @@ import tsNoAny from "./ts-no-any.md" with { type: "text" };
|
||||
import tsNoDeprecatedLeftovers from "./ts-no-deprecated-leftovers.md" with { type: "text" };
|
||||
import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
|
||||
import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
|
||||
import tsNoTestTimers from "./ts-no-test-timers.md" with { type: "text" };
|
||||
import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
|
||||
import tsPromiseWithResolvers from "./ts-promise-with-resolvers.md" with { type: "text" };
|
||||
import tsRedundantClearGuard from "./ts-redundant-clear-guard.md" with { type: "text" };
|
||||
@@ -45,6 +46,7 @@ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
|
||||
{ name: "ts-no-deprecated-leftovers", content: tsNoDeprecatedLeftovers },
|
||||
{ name: "ts-no-dynamic-import", content: tsNoDynamicImport },
|
||||
{ name: "ts-no-return-type", content: tsNoReturnType },
|
||||
{ name: "ts-no-test-timers", content: tsNoTestTimers },
|
||||
{ name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
|
||||
{ name: "ts-promise-with-resolvers", content: tsPromiseWithResolvers },
|
||||
{ name: "ts-redundant-clear-guard", content: tsRedundantClearGuard },
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: Do not use real timers (Bun.sleep, setTimeout, setInterval) in tests — drive time with fake timers instead
|
||||
condition:
|
||||
- "Bun\\.sleep\\("
|
||||
- "\\bsetInterval\\("
|
||||
- "\\bsetTimeout\\("
|
||||
scope: "tool:edit(*.test.ts), tool:write(*.test.ts)"
|
||||
interruptMode: never
|
||||
---
|
||||
|
||||
**Do not reach for real wall-clock timers in test files.** `Bun.sleep(...)`, `setTimeout(...)`, and `setInterval(...)` tie a test's duration to real time: they slow the suite on every run, and any delay tuned to "long enough" eventually races on a loaded machine and flakes.
|
||||
|
||||
## Why it's wrong
|
||||
|
||||
- Real delays add fixed latency to every invocation; CI pays it on every run.
|
||||
- A sleep sized to mask a race is a guess — the race resurfaces under load.
|
||||
- A fixed wait hides *what* you are waiting for, so a failure points at a timeout instead of the real cause.
|
||||
|
||||
## Avoid
|
||||
|
||||
```typescript
|
||||
test("debounce fires once", async () => {
|
||||
const fn = debounce(handler, 100);
|
||||
fn();
|
||||
await Bun.sleep(150); // real delay — slow and timing-dependent
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
Drive time deterministically with fake timers:
|
||||
|
||||
```typescript
|
||||
import { expect, test, vi } from "bun:test";
|
||||
|
||||
test("debounce fires once", () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = debounce(handler, 100);
|
||||
fn();
|
||||
vi.advanceTimersByTime(150); // advance the clock, no real wait
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
When the code under test resolves a promise or emits an event, await that signal directly instead of guessing a duration:
|
||||
|
||||
```typescript
|
||||
await once(emitter, "done"); // await the real event
|
||||
const value = await pending; // await the promise the code already exposes
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
|
||||
An integration test that deliberately exercises real timer behavior against the platform clock may need a genuine delay. Keep it rare, and add a short comment naming why deterministic time control will not work.
|
||||
Reference in New Issue
Block a user