Skip to content

Commit b7e1bb9

Browse files
committed
assert console cleared before act
1 parent 99781c8 commit b7e1bb9

File tree

3 files changed

+230
-3
lines changed

3 files changed

+230
-3
lines changed

packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js

Lines changed: 178 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,8 @@ describe('ReactInternalTestUtils console mocks', () => {
308308
});
309309
});
310310

311-
// Helper methods avoids invalid toWarn().toThrow() nesting
312-
// See no-to-warn-dev-within-to-throw
313-
const expectToWarnAndToThrow = (expectBlock, expectedErrorMessage) => {
311+
// Helper method to capture assertion failure.
312+
const expectToWarnAndToThrow = expectBlock => {
314313
let caughtError;
315314
try {
316315
expectBlock();
@@ -321,6 +320,18 @@ const expectToWarnAndToThrow = (expectBlock, expectedErrorMessage) => {
321320
return stripAnsi(caughtError.message);
322321
};
323322

323+
// Helper method to capture assertion failure with act.
324+
const awaitExpectToWarnAndToThrow = async expectBlock => {
325+
let caughtError;
326+
try {
327+
await expectBlock();
328+
} catch (error) {
329+
caughtError = error;
330+
}
331+
expect(caughtError).toBeDefined();
332+
return stripAnsi(caughtError.message);
333+
};
334+
324335
describe('ReactInternalTestUtils console assertions', () => {
325336
beforeEach(() => {
326337
jest.resetAllMocks();
@@ -346,6 +357,44 @@ describe('ReactInternalTestUtils console assertions', () => {
346357
assertConsoleLogDev(['Hello', 'Good day', 'Bye']);
347358
});
348359

360+
it('fails if act is called without assertConsoleLogDev', async () => {
361+
const Yield = ({id}) => {
362+
console.log(id);
363+
return id;
364+
};
365+
366+
function App() {
367+
return (
368+
<div>
369+
<Yield id="A" />
370+
<Yield id="B" />
371+
<Yield id="C" />
372+
</div>
373+
);
374+
}
375+
376+
const root = ReactNoop.createRoot();
377+
await act(() => {
378+
root.render(<App />);
379+
});
380+
const message = await awaitExpectToWarnAndToThrow(async () => {
381+
await act(() => {
382+
root.render(<App />);
383+
});
384+
});
385+
386+
expect(message).toMatchInlineSnapshot(`
387+
"asserConsoleLogsCleared(expected)
388+
389+
console.log was called without assertConsoleLogDev:
390+
+ A
391+
+ B
392+
+ C
393+
394+
You must call one of the assertConsoleDev helpers between each act call."
395+
`);
396+
});
397+
349398
// @gate __DEV__
350399
it('fails if first expected log is not included', () => {
351400
const message = expectToWarnAndToThrow(() => {
@@ -648,6 +697,44 @@ describe('ReactInternalTestUtils console assertions', () => {
648697
assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1});
649698
});
650699

700+
it('fails if act is called without assertConsoleWarnDev', async () => {
701+
const Yield = ({id}) => {
702+
console.warn(id);
703+
return id;
704+
};
705+
706+
function App() {
707+
return (
708+
<div>
709+
<Yield id="A" />
710+
<Yield id="B" />
711+
<Yield id="C" />
712+
</div>
713+
);
714+
}
715+
716+
const root = ReactNoop.createRoot();
717+
await act(() => {
718+
root.render(<App />);
719+
});
720+
const message = await awaitExpectToWarnAndToThrow(async () => {
721+
await act(() => {
722+
root.render(<App />);
723+
});
724+
});
725+
726+
expect(message).toMatchInlineSnapshot(`
727+
"asserConsoleLogsCleared(expected)
728+
729+
console.warn was called without assertConsoleWarnDev:
730+
+ A
731+
+ B
732+
+ C
733+
734+
You must call one of the assertConsoleDev helpers between each act call."
735+
`);
736+
});
737+
651738
// @gate __DEV__
652739
it('fails if first expected warning is not included', () => {
653740
const message = expectToWarnAndToThrow(() => {
@@ -1143,6 +1230,94 @@ describe('ReactInternalTestUtils console assertions', () => {
11431230
assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1});
11441231
});
11451232

1233+
it('fails if act is called without assertConsoleErrorDev', async () => {
1234+
const Yield = ({id}) => {
1235+
console.error(id);
1236+
return id;
1237+
};
1238+
1239+
function App() {
1240+
return (
1241+
<div>
1242+
<Yield id="A" />
1243+
<Yield id="B" />
1244+
<Yield id="C" />
1245+
</div>
1246+
);
1247+
}
1248+
1249+
const root = ReactNoop.createRoot();
1250+
await act(() => {
1251+
root.render(<App />);
1252+
});
1253+
const message = await awaitExpectToWarnAndToThrow(async () => {
1254+
await act(() => {
1255+
root.render(<App />);
1256+
});
1257+
});
1258+
1259+
expect(message).toMatchInlineSnapshot(`
1260+
"asserConsoleLogsCleared(expected)
1261+
1262+
console.error was called without assertConsoleErrorDev:
1263+
+ A
1264+
+ B
1265+
+ C
1266+
1267+
You must call one of the assertConsoleDev helpers between each act call."
1268+
`);
1269+
});
1270+
1271+
it('fails if act is called without any assertConsoleDev helpers', async () => {
1272+
const Yield = ({id}) => {
1273+
console.log(id);
1274+
console.warn(id);
1275+
console.error(id);
1276+
return id;
1277+
};
1278+
1279+
function App() {
1280+
return (
1281+
<div>
1282+
<Yield id="A" />
1283+
<Yield id="B" />
1284+
<Yield id="C" />
1285+
</div>
1286+
);
1287+
}
1288+
1289+
const root = ReactNoop.createRoot();
1290+
await act(() => {
1291+
root.render(<App />);
1292+
});
1293+
const message = await awaitExpectToWarnAndToThrow(async () => {
1294+
await act(() => {
1295+
root.render(<App />);
1296+
});
1297+
});
1298+
1299+
expect(message).toMatchInlineSnapshot(`
1300+
"asserConsoleLogsCleared(expected)
1301+
1302+
console.log was called without assertConsoleLogDev:
1303+
+ A
1304+
+ B
1305+
+ C
1306+
1307+
console.warn was called without assertConsoleWarnDev:
1308+
+ A
1309+
+ B
1310+
+ C
1311+
1312+
console.error was called without assertConsoleErrorDev:
1313+
+ A
1314+
+ B
1315+
+ C
1316+
1317+
You must call one of the assertConsoleDev helpers between each act call."
1318+
`);
1319+
});
1320+
11461321
// @gate __DEV__
11471322
it('fails if first expected error is not included', () => {
11481323
const message = expectToWarnAndToThrow(() => {

packages/internal-test-utils/consoleMock.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,53 @@ export function clearErrors() {
182182
return errors;
183183
}
184184

185+
export function assertConsoleLogsCleared() {
186+
const logs = clearLogs();
187+
const warnings = clearWarnings();
188+
const errors = clearErrors();
189+
190+
if (logs.length > 0 || errors.length > 0 || warnings.length > 0) {
191+
let message = `${chalk.dim('asserConsoleLogsCleared')}(${chalk.red(
192+
'expected',
193+
)})\n`;
194+
195+
if (logs.length > 0) {
196+
message += `\nconsole.log was called without assertConsoleLogDev:\n${diff(
197+
'',
198+
logs.join('\n'),
199+
{
200+
omitAnnotationLines: true,
201+
},
202+
)}\n`;
203+
}
204+
205+
if (warnings.length > 0) {
206+
message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff(
207+
'',
208+
warnings.join('\n'),
209+
{
210+
omitAnnotationLines: true,
211+
},
212+
)}\n`;
213+
}
214+
if (errors.length > 0) {
215+
message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff(
216+
'',
217+
errors.join('\n'),
218+
{
219+
omitAnnotationLines: true,
220+
},
221+
)}\n`;
222+
}
223+
224+
message += `\nYou must call one of the assertConsoleDev helpers between each act call.`;
225+
226+
const error = Error(message);
227+
Error.captureStackTrace(error, assertConsoleLogsCleared);
228+
throw error;
229+
}
230+
}
231+
185232
function replaceComponentStack(str) {
186233
if (typeof str !== 'string') {
187234
return str;

packages/internal-test-utils/internalAct.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {Thenable} from 'shared/ReactTypes';
1919
import * as Scheduler from 'scheduler/unstable_mock';
2020

2121
import enqueueTask from './enqueueTask';
22+
import {assertConsoleLogsCleared} from './consoleMock';
2223

2324
export let actingUpdatesScopeDepth: number = 0;
2425

@@ -45,6 +46,10 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
4546
);
4647
}
4748

49+
// We require every `act` call to assert console logs
50+
// with one of the assertion helpers. Fails if not empty.
51+
assertConsoleLogsCleared();
52+
4853
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
4954
if (!jest.isMockFunction(setTimeout)) {
5055
throw Error(

0 commit comments

Comments
 (0)