Skip to content

Commit 50f002e

Browse files
committed
feat: add deterministic async ui controls
1 parent 63b6fc1 commit 50f002e

File tree

8 files changed

+380
-2
lines changed

8 files changed

+380
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All notable changes to this project are documented in this file.
1616
- Added project-aware JS/TS runtime support for TSX, ESM `.js`, `tsconfig` path aliases, `setupFiles`, and `jsdom` environments.
1717
- Added first-party snapshots, mocks, run-diff artifacts, and watch mode for tighter agent rerun loops.
1818
- Added a lightweight DOM-oriented `jsdom` UI test layer with `render`, `screen`, `fireEvent`, `waitFor`, `cleanup`, and UI matchers for text, attributes, and document presence.
19+
- Added deterministic async UI test controls with fake timers, microtask flushing, and first-party fetch mocking for `jsdom` tests.
1920
- Added an in-repo VS Code extension scaffold for artifact-driven result viewing, reruns, and HTML report opening.
2021
- Expanded the VS Code extension scaffold with generated-review navigation for source/test/hint mappings and unresolved generation backlog.
2122
- Refreshed README, AGENTS, and supporting docs to match the current package scope, JS/TS feature set, artifact contracts, and extension surface.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ For UI-oriented `jsdom` tests, Themis also ships a lightweight DOM layer:
272272
- `fireEvent.click/change/input/submit/keyDown(...)`
273273
- `waitFor(asyncAssertion)`
274274
- `cleanup()`
275+
- `useFakeTimers()`, `advanceTimersByTime(ms)`, `runAllTimers()`, `useRealTimers()`
276+
- `flushMicrotasks()`
277+
- `mockFetch(...)`, `resetFetchMocks()`, `restoreFetch()`
275278

276279
Example:
277280

@@ -287,6 +290,30 @@ test('submits the form', async () => {
287290
});
288291
```
289292

293+
Network and async example:
294+
295+
```ts
296+
test('loads api state deterministically', async () => {
297+
useFakeTimers();
298+
const fetchMock = mockFetch({ json: { ok: true } });
299+
300+
let done = false;
301+
setTimeout(async () => {
302+
const response = await fetch('/api/status');
303+
const payload = await response.json();
304+
done = payload.ok;
305+
}, 50);
306+
307+
advanceTimersByTime(50);
308+
await flushMicrotasks();
309+
310+
expect(done).toBe(true);
311+
expect(fetchMock).toHaveBeenCalled();
312+
useRealTimers();
313+
restoreFetch();
314+
});
315+
```
316+
290317
## Intent Syntax
291318

292319
Themis supports a strict code-native intent DSL:

docs/api.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ Themis exposes a lightweight DOM-oriented helper layer for `jsdom` tests:
171171
- `fireEvent.keyDown(node, payload?)`
172172
- `waitFor(assertion, options?)`
173173
- `cleanup()`
174+
- `useFakeTimers()`
175+
- `useRealTimers()`
176+
- `advanceTimersByTime(ms)`
177+
- `runAllTimers()`
178+
- `flushMicrotasks()`
179+
- `mockFetch(handlerOrResponse)`
180+
- `resetFetchMocks()`
181+
- `restoreFetch()`
174182

175183
Supported DOM matchers:
176184

@@ -180,6 +188,14 @@ Supported DOM matchers:
180188

181189
These helpers are intentionally small and deterministic. They are designed for generated UI unit-layer tests and human-authored component tests running in Themis `jsdom` mode.
182190

191+
`mockFetch(...)` accepts either:
192+
193+
- a function `(input, init) => response`
194+
- a Response instance
195+
- a shorthand object like `{ status, headers, body }` or `{ status, json }`
196+
197+
The fake timer helpers only patch the current Themis runtime. They do not mutate system time outside the active test process.
198+
183199
## Config File (`themis.config.json`)
184200

185201
| Field | Type | Default | Notes |

globals.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
ScreenApi,
1313
SpyOn,
1414
Test,
15+
FetchMock,
16+
FlushMicrotasks,
1517
WaitFor
1618
} from './index';
1719

@@ -37,6 +39,14 @@ declare global {
3739
var fireEvent: FireEventApi;
3840
var waitFor: WaitFor;
3941
var cleanup: Cleanup;
42+
var useFakeTimers: () => void;
43+
var useRealTimers: () => void;
44+
var advanceTimersByTime: (ms: number) => void;
45+
var runAllTimers: () => void;
46+
var flushMicrotasks: FlushMicrotasks;
47+
var mockFetch: (handlerOrResponse: unknown) => FetchMock;
48+
var restoreFetch: () => void;
49+
var resetFetchMocks: () => void;
4050
}
4151

4252
export {};

index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,15 @@ export interface FireEventApi {
503503
export type Render = (input: unknown, options?: { container?: HTMLElement }) => RenderResult;
504504
export type WaitFor = <T>(assertion: () => T | Promise<T>, options?: { timeout?: number; interval?: number }) => Promise<T>;
505505
export type Cleanup = () => void;
506+
export type FetchMock = MockFunction<any[], Promise<unknown>>;
507+
export type UseFakeTimers = () => void;
508+
export type UseRealTimers = () => void;
509+
export type AdvanceTimersByTime = (ms: number) => void;
510+
export type RunAllTimers = () => void;
511+
export type FlushMicrotasks = () => Promise<void>;
512+
export type MockFetch = (handlerOrResponse: unknown) => FetchMock;
513+
export type RestoreFetch = () => void;
514+
export type ResetFetchMocks = () => void;
506515

507516
export type SuiteFn = () => void;
508517
export type TestFn = () => Awaitable<void>;

src/runtime.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,15 @@ function installGlobals(api) {
284284
'screen',
285285
'fireEvent',
286286
'waitFor',
287-
'cleanup'
287+
'cleanup',
288+
'useFakeTimers',
289+
'useRealTimers',
290+
'advanceTimersByTime',
291+
'runAllTimers',
292+
'flushMicrotasks',
293+
'mockFetch',
294+
'restoreFetch',
295+
'resetFetchMocks'
288296
];
289297
const previous = {};
290298
for (const name of names) {
@@ -312,6 +320,14 @@ function installGlobals(api) {
312320
global.fireEvent = api.fireEvent;
313321
global.waitFor = api.waitFor;
314322
global.cleanup = api.cleanup;
323+
global.useFakeTimers = api.useFakeTimers;
324+
global.useRealTimers = api.useRealTimers;
325+
global.advanceTimersByTime = api.advanceTimersByTime;
326+
global.runAllTimers = api.runAllTimers;
327+
global.flushMicrotasks = api.flushMicrotasks;
328+
global.mockFetch = api.mockFetch;
329+
global.restoreFetch = api.restoreFetch;
330+
global.resetFetchMocks = api.resetFetchMocks;
315331

316332
return previous;
317333
}

0 commit comments

Comments
 (0)