Skip to content

Commit 31c680d

Browse files
Merge branch 'main' into SS/fixTwitterIcon
2 parents 90056b6 + 580d4b7 commit 31c680d

File tree

14 files changed

+405
-67
lines changed

14 files changed

+405
-67
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
### Features
44

55
- `[jest-config]` Add `defineConfig` and `mergeConfig` helpers for type-safe Jest config ([#15844](https://github.com/jestjs/jest/pull/15844))
6+
- `[jest-fake-timers]` Add `setTimerTickMode` to configure how timers advance
67

78
### Fixes
89

10+
- `[jest-reporters]` Fix issue where console output not displayed for GHA reporter even with `silent: false` option ([#15864](https://github.com/jestjs/jest/pull/15864))
911
- `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842))
1012
- `[jest-test-sequencer]` Fix issue where failed tests due to compilation errors not getting re-executed even with `--onlyFailures` CLI option ([#15851](https://github.com/jestjs/jest/pull/15851))
1113

1214
### Chore & Maintenance
1315

1416
- `[docs]` Update V30 migration guide to notify users on `jest.mock()` work with case-sensitive path ([#15849](https://github.com/jestjs/jest/pull/15849))
17+
- `[deps]` Update to sinon/fake-timers v15
1518
- Updated Twitter icon to match the latest brand guidelines.([#15868](https://github.com/jestjs/jest/pull/15869))
1619

1720
## 30.2.0

docs/JestObjectAPI.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,29 @@ This function is not available when using legacy fake timers implementation.
11261126

11271127
:::
11281128

1129+
### `jest.setTimerTickMode(mode)`
1130+
1131+
Allows configuring how fake timers advance time.
1132+
1133+
Configuration options:
1134+
1135+
```ts
1136+
type TimerTickMode =
1137+
| {mode: 'manual'}
1138+
| {mode: 'nextAsync'}
1139+
| {mode: 'interval'; delta?: number};
1140+
```
1141+
1142+
- `manual`: Timers do not advance without explicit, manual calls to the tick APIs (`jest.advanceTimersByTime(ms)`, `jest.runAllTimers()`, etc).
1143+
- `nextAsync`: The clock will continuously break the event loop, then run the next timer until the mode changes.
1144+
- `interval`: This is the same as specifying `advanceTimers: true` with an `advanceTimeDelta`. If the delta is not specified, 20 will be used by default.
1145+
1146+
:::info
1147+
1148+
This function is not available when using legacy fake timers implementation.
1149+
1150+
:::
1151+
11291152
### `jest.getRealSystemTime()`
11301153

11311154
When mocking time, `Date.now()` will also be mocked. If you for some reason need access to the real current time, you can invoke this function.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const realSetTimeout = setTimeout;
11+
12+
describe('jest.setTimerTickMode', () => {
13+
it('should not advance the clock with manual', async () => {
14+
jest.useFakeTimers();
15+
jest.setTimerTickMode({mode: 'manual'});
16+
const spy = jest.fn();
17+
setTimeout(spy, 5);
18+
await new Promise(resolve => realSetTimeout(resolve, 20));
19+
expect(spy).not.toHaveBeenCalled();
20+
});
21+
22+
it('should advance the clock to next timer with nextAsync', async () => {
23+
jest.useFakeTimers();
24+
jest.setTimerTickMode({mode: 'nextAsync'});
25+
await new Promise(resolve => setTimeout(resolve, 5000));
26+
await new Promise(resolve => setTimeout(resolve, 5000));
27+
await new Promise(resolve => setTimeout(resolve, 5000));
28+
await new Promise(resolve => setTimeout(resolve, 5000));
29+
// test should not time out
30+
});
31+
32+
it('should advance the clock in real time with delta', async () => {
33+
jest.useFakeTimers();
34+
jest.setTimerTickMode({delta: 10, mode: 'interval'});
35+
const spy = jest.fn();
36+
setTimeout(spy, 10);
37+
await new Promise(resolve => realSetTimeout(resolve, 5));
38+
expect(spy).not.toHaveBeenCalled();
39+
await new Promise(resolve => realSetTimeout(resolve, 5));
40+
expect(spy).toHaveBeenCalled();
41+
});
42+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "set-timer-tick-mode-e2e",
3+
"private": true,
4+
"version": "1.0.0"
5+
}

packages/jest-environment/src/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,25 @@ export interface Jest {
425425
* performance, time and timer APIs.
426426
*/
427427
useRealTimers(): Jest;
428+
/**
429+
* Updates the mode of advancing timers when using fake timers.
430+
*
431+
* @param config The configuration to use for advancing timers
432+
*
433+
* When the mode is 'interval', timers will be advanced automatically by the [delta]
434+
* milliseconds every [delta] milliseconds of real time. The default delta is 20 milliseconds.
435+
*
436+
* When mode is 'nextAsync', configures whether timers advance automatically to the next timer in the queue after each macrotask.
437+
* This mode differs from 'interval' in that it advances all the way to the next timer, regardless
438+
* of how far in the future that timer is scheduled (e.g. advanceTimersToNextTimerAsync).
439+
*
440+
* When mode is 'manual' (the default), timers will not advance automatically. Instead,
441+
* timers must be advanced using APIs such as `advanceTimersToNextTimer`, `advanceTimersByTime`, etc.
442+
*
443+
* @remarks
444+
* Not available when using legacy fake timers implementation.
445+
*/
446+
setTimerTickMode(
447+
config: {mode: 'manual' | 'nextAsync'} | {mode: 'interval'; delta?: number},
448+
): Jest;
428449
}

packages/jest-fake-timers/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
},
2121
"dependencies": {
2222
"@jest/types": "workspace:*",
23-
"@sinonjs/fake-timers": "^13.0.0",
23+
"@sinonjs/fake-timers": "^15.0.0",
2424
"@types/node": "*",
2525
"jest-message-util": "workspace:*",
2626
"jest-mock": "workspace:*",
2727
"jest-util": "workspace:*"
2828
},
2929
"devDependencies": {
3030
"@jest/test-utils": "workspace:*",
31-
"@types/sinonjs__fake-timers": "^8.1.5"
31+
"@types/sinonjs__fake-timers": "15.0.0"
3232
},
3333
"engines": {
3434
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"

packages/jest-fake-timers/src/__tests__/modernFakeTimers.test.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ describe('FakeTimers', () => {
7373
Date,
7474
clearInterval,
7575
clearTimeout,
76-
process: {
77-
nextTick: origNextTick,
78-
},
76+
process: {nextTick: origNextTick},
7977
setInterval,
8078
setTimeout,
8179
} as unknown as typeof globalThis;
@@ -153,9 +151,7 @@ describe('FakeTimers', () => {
153151
Date,
154152
clearInterval,
155153
clearTimeout,
156-
process: {
157-
nextTick: () => {},
158-
},
154+
process: {nextTick: () => {}},
159155
setInterval,
160156
setTimeout,
161157
} as unknown as typeof globalThis;
@@ -186,9 +182,7 @@ describe('FakeTimers', () => {
186182
Date,
187183
clearInterval,
188184
clearTimeout,
189-
process: {
190-
nextTick,
191-
},
185+
process: {nextTick},
192186
setInterval,
193187
setTimeout,
194188
} as unknown as typeof globalThis;
@@ -205,9 +199,7 @@ describe('FakeTimers', () => {
205199
Date,
206200
clearInterval,
207201
clearTimeout,
208-
process: {
209-
nextTick: () => {},
210-
},
202+
process: {nextTick: () => {}},
211203
setInterval,
212204
setTimeout,
213205
} as unknown as typeof globalThis;
@@ -231,9 +223,7 @@ describe('FakeTimers', () => {
231223
Date,
232224
clearInterval,
233225
clearTimeout,
234-
process: {
235-
nextTick: () => {},
236-
},
226+
process: {nextTick: () => {}},
237227
setInterval,
238228
setTimeout,
239229
} as unknown as typeof globalThis;
@@ -902,9 +892,7 @@ describe('FakeTimers', () => {
902892
Date,
903893
clearInterval,
904894
clearTimeout,
905-
process: {
906-
nextTick: () => {},
907-
},
895+
process: {nextTick: () => {}},
908896
setImmediate: () => {},
909897
setInterval,
910898
setTimeout,
@@ -1430,6 +1418,48 @@ describe('FakeTimers', () => {
14301418
});
14311419
});
14321420

1421+
describe('setTimerTickMode', () => {
1422+
let global: typeof globalThis;
1423+
let timers: FakeTimers;
1424+
const realSetTimeout = setTimeout;
1425+
beforeEach(() => {
1426+
global = {
1427+
Date,
1428+
Promise,
1429+
clearInterval,
1430+
clearTimeout,
1431+
process,
1432+
setInterval,
1433+
setTimeout,
1434+
} as unknown as typeof globalThis;
1435+
timers = new FakeTimers({config: makeProjectConfig(), global});
1436+
timers.useFakeTimers();
1437+
});
1438+
1439+
afterEach(() => {
1440+
timers.useRealTimers();
1441+
});
1442+
1443+
it('should advance the clock to next timer with nextAsync', async () => {
1444+
timers.setTimerTickMode({mode: 'nextAsync'});
1445+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1446+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1447+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1448+
await new Promise(resolve => global.setTimeout(resolve, 5000));
1449+
// test should not time out
1450+
});
1451+
1452+
it('should advance the clock in real time with delta', async () => {
1453+
timers.setTimerTickMode({delta: 10, mode: 'interval'});
1454+
const spy = jest.fn();
1455+
global.setTimeout(spy, 10);
1456+
await new Promise(resolve => realSetTimeout(resolve, 5));
1457+
expect(spy).not.toHaveBeenCalled();
1458+
await new Promise(resolve => realSetTimeout(resolve, 5));
1459+
expect(spy).toHaveBeenCalled();
1460+
});
1461+
});
1462+
14331463
describe('now', () => {
14341464
let timers: FakeTimers;
14351465
let fakedGlobal: typeof globalThis;

packages/jest-fake-timers/src/modernFakeTimers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default class FakeTimers {
118118

119119
runAllTicks(): void {
120120
if (this._checkFakeTimers()) {
121-
// @ts-expect-error - doesn't exist?
121+
// @ts-expect-error needs an upstream fix: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/73943
122122
this._clock.runMicrotasks();
123123
}
124124
}
@@ -156,6 +156,15 @@ export default class FakeTimers {
156156
}
157157
}
158158

159+
setTimerTickMode(tickModeConfig: {
160+
mode: 'interval' | 'manual' | 'nextAsync';
161+
delta?: number;
162+
}): void {
163+
if (this._checkFakeTimers()) {
164+
this._clock.setTickMode(tickModeConfig);
165+
}
166+
}
167+
159168
getRealSystemTime(): number {
160169
return Date.now();
161170
}

packages/jest-reporters/src/GitHubActionsReporter.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import {stripVTControlCharacters as stripAnsi} from 'util';
99
import chalk from 'chalk';
10+
import {type ConsoleBuffer, getConsoleOutput} from '@jest/console';
1011
import type {
1112
AggregatedResult,
1213
AssertionResult,
@@ -66,12 +67,14 @@ type ResultTree = {
6667
export default class GitHubActionsReporter extends BaseReporter {
6768
static readonly filename = __filename;
6869
private readonly options: {silent: boolean};
70+
private readonly globalConfig: Config.GlobalConfig;
6971

7072
constructor(
71-
_globalConfig: Config.GlobalConfig,
73+
globalConfig: Config.GlobalConfig,
7274
reporterOptions: {silent?: boolean} = {},
7375
) {
7476
super();
77+
this.globalConfig = globalConfig;
7578
this.options = {
7679
silent:
7780
typeof reporterOptions.silent === 'boolean'
@@ -90,7 +93,7 @@ export default class GitHubActionsReporter extends BaseReporter {
9093
this.printFullResult(test.context, testResult);
9194
}
9295
if (this.isLastTestSuite(aggregatedResults)) {
93-
this.printFailedTestLogs(test, aggregatedResults);
96+
this.printFailedTestLogs(test, testResult.console, aggregatedResults);
9497
}
9598
}
9699

@@ -179,7 +182,7 @@ export default class GitHubActionsReporter extends BaseReporter {
179182
testDir,
180183
results.perfStats,
181184
);
182-
this.printResultTree(resultTree);
185+
this.printResultTree(resultTree, context.config, results.console);
183186
}
184187

185188
private arrayEqual(a1: Array<any>, a2: Array<any>): boolean {
@@ -311,7 +314,11 @@ export default class GitHubActionsReporter extends BaseReporter {
311314
return node;
312315
}
313316

314-
private printResultTree(resultTree: ResultTree): void {
317+
private printResultTree(
318+
resultTree: ResultTree,
319+
config: Config.ProjectConfig,
320+
consoleLog: ConsoleBuffer | undefined,
321+
): void {
315322
let perfMs;
316323
if (resultTree.performanceInfo.slow) {
317324
perfMs = ` (${chalk.red.inverse(
@@ -324,6 +331,9 @@ export default class GitHubActionsReporter extends BaseReporter {
324331
this.startGroup(
325332
`${chalk.bold.green.inverse('PASS')} ${resultTree.name}${perfMs}`,
326333
);
334+
if (consoleLog && !this.options.silent) {
335+
this.log(getConsoleOutput(consoleLog, config, this.globalConfig));
336+
}
327337
for (const child of resultTree.children) {
328338
this.recursivePrintResultTree(child, true, 1);
329339
}
@@ -401,6 +411,7 @@ export default class GitHubActionsReporter extends BaseReporter {
401411

402412
private printFailedTestLogs(
403413
context: Test,
414+
consoleLog: ConsoleBuffer | undefined,
404415
testResults: AggregatedResult,
405416
): boolean {
406417
const rootDir = context.context.config.rootDir;
@@ -416,6 +427,15 @@ export default class GitHubActionsReporter extends BaseReporter {
416427
written = true;
417428
}
418429
this.startGroup(`Errors thrown in ${testDir}`);
430+
if (consoleLog && !this.options.silent) {
431+
this.log(
432+
getConsoleOutput(
433+
consoleLog,
434+
context.context.config,
435+
this.globalConfig,
436+
),
437+
);
438+
}
419439
this.log(result.failureMessage);
420440
this.endGroup();
421441
}

0 commit comments

Comments
 (0)