@samitouri / QOS-React-2 / commits / 3e1abcc8d7

[tests] Require exact error messages in assertConsole helpers (#35497)

Requires full error message in assert helpers. Some of the error messages we asset on add a native javascript stack trace, which would be a pain to add to the messages and maintain. This PR allows you to just add `\n in <stack>` placeholder to the error message to denote a native stack trace is present in the message. --- Note: i vibe coded this so it was a pain to backtrack this to break this into a stack, I tried and gave up, sorry.

Ricky committed Jan 13, 2026 at 15:52 UTC 3e1abcc8d7083a13adf4774feb0d67ecbe4a2bc4
16 files changed +690 -320
.github/workflows/runtime_build_and_test.yml
+3
@@ -278,6 +278,7 @@ jobs:
278 if: steps.node_modules.outputs.cache-hit != 'true'
279 - run: yarn --cwd compiler install --frozen-lockfile
280 if: steps.node_modules.outputs.cache-hit != 'true'
281 + - run: node --version
282 - run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
283
284 # Hardcoded to improve parallelism
@@ -445,6 +446,7 @@ jobs:
446 merge-multiple: true
447 - name: Display structure of build
448 run: ls -R build
449 + - run: node --version
450 - run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
451
452 test_build_devtools:
@@ -489,6 +491,7 @@ jobs:
491 merge-multiple: true
492 - name: Display structure of build
493 run: ls -R build
494 + - run: node --version
495 - run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
496
497 process_artifacts_combined:
packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+309 -225
@@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => {
879 if (__DEV__) {
880 console.warn('Hello\n in div');
881 }
882 - assertConsoleWarnDev(['Hello']);
882 + assertConsoleWarnDev(['Hello\n in div']);
883 });
884
885 it('passes if all warnings contain a stack', () => {
@@ -888,7 +888,11 @@ describe('ReactInternalTestUtils console assertions', () => {
888 console.warn('Good day\n in div');
889 console.warn('Bye\n in div');
890 }
891 - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
891 + assertConsoleWarnDev([
892 + 'Hello\n in div',
893 + 'Good day\n in div',
894 + 'Bye\n in div',
895 + ]);
896 });
897
898 it('fails if act is called without assertConsoleWarnDev', async () => {
@@ -1075,7 +1079,11 @@ describe('ReactInternalTestUtils console assertions', () => {
1079 const message = expectToThrowFailure(() => {
1080 console.warn('Hi \n in div');
1081 console.warn('Wow \n in div');
1078 - assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
1082 + assertConsoleWarnDev([
1083 + 'Hi \n in div',
1084 + 'Wow \n in div',
1085 + 'Bye \n in div',
1086 + ]);
1087 });
1088 expect(message).toMatchInlineSnapshot(`
1089 "assertConsoleWarnDev(expected)
@@ -1085,9 +1093,9 @@ describe('ReactInternalTestUtils console assertions', () => {
1093 - Expected warnings
1094 + Received warnings
1095
1088 - - Hi
1089 - - Wow
1090 - - Bye
1096 + - Hi in div
1097 + - Wow in div
1098 + - Bye in div
1099 + Hi in div (at **)
1100 + Wow in div (at **)"
1101 `);
@@ -1188,16 +1196,26 @@ describe('ReactInternalTestUtils console assertions', () => {
1196 console.warn('Hello');
1197 console.warn('Good day\n in div');
1198 console.warn('Bye\n in div');
1191 - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
1199 + assertConsoleWarnDev([
1200 + 'Hello\n in div',
1201 + 'Good day\n in div',
1202 + 'Bye\n in div',
1203 + ]);
1204 });
1205 expect(message).toMatchInlineSnapshot(`
1206 "assertConsoleWarnDev(expected)
1207
1196 - Missing component stack for:
1197 - "Hello"
1208 + Unexpected warning(s) recorded.
1209
1199 - If this warning should omit a component stack, pass [log, {withoutStack: true}].
1200 - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
1210 + - Expected warnings
1211 + + Received warnings
1212 +
1213 + - Hello in div
1214 + - Good day in div
1215 + - Bye in div
1216 + + Hello
1217 + + Good day in div (at **)
1218 + + Bye in div (at **)"
1219 `);
1220 });
1221
@@ -1207,16 +1225,26 @@ describe('ReactInternalTestUtils console assertions', () => {
1225 console.warn('Hello\n in div');
1226 console.warn('Good day');
1227 console.warn('Bye\n in div');
1210 - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
1228 + assertConsoleWarnDev([
1229 + 'Hello\n in div',
1230 + 'Good day\n in div',
1231 + 'Bye\n in div',
1232 + ]);
1233 });
1234 expect(message).toMatchInlineSnapshot(`
1235 "assertConsoleWarnDev(expected)
1236
1215 - Missing component stack for:
1216 - "Good day"
1237 + Unexpected warning(s) recorded.
1238
1218 - If this warning should omit a component stack, pass [log, {withoutStack: true}].
1219 - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
1239 + - Expected warnings
1240 + + Received warnings
1241 +
1242 + - Hello in div
1243 + - Good day in div
1244 + - Bye in div
1245 + + Hello in div (at **)
1246 + + Good day
1247 + + Bye in div (at **)"
1248 `);
1249 });
1250
@@ -1226,41 +1254,26 @@ describe('ReactInternalTestUtils console assertions', () => {
1254 console.warn('Hello\n in div');
1255 console.warn('Good day\n in div');
1256 console.warn('Bye');
1229 - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
1230 - });
1231 - expect(message).toMatchInlineSnapshot(`
1232 - "assertConsoleWarnDev(expected)
1233 -
1234 - Missing component stack for:
1235 - "Bye"
1236 -
1237 - If this warning should omit a component stack, pass [log, {withoutStack: true}].
1238 - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
1239 - `);
1240 - });
1241 -
1242 - // @gate __DEV__
1243 - it('fails if all warnings do not contain a stack', () => {
1244 - const message = expectToThrowFailure(() => {
1245 - console.warn('Hello');
1246 - console.warn('Good day');
1247 - console.warn('Bye');
1248 - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
1257 + assertConsoleWarnDev([
1258 + 'Hello\n in div',
1259 + 'Good day\n in div',
1260 + 'Bye\n in div',
1261 + ]);
1262 });
1263 expect(message).toMatchInlineSnapshot(`
1264 "assertConsoleWarnDev(expected)
1265
1253 - Missing component stack for:
1254 - "Hello"
1255 -
1256 - Missing component stack for:
1257 - "Good day"
1266 + Unexpected warning(s) recorded.
1267
1259 - Missing component stack for:
1260 - "Bye"
1268 + - Expected warnings
1269 + + Received warnings
1270
1262 - If this warning should omit a component stack, pass [log, {withoutStack: true}].
1263 - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
1271 + - Hello in div
1272 + - Good day in div
1273 + - Bye in div
1274 + + Hello in div (at **)
1275 + + Good day in div (at **)
1276 + + Bye"
1277 `);
1278 });
1279
@@ -1339,12 +1352,13 @@ describe('ReactInternalTestUtils console assertions', () => {
1352 expect(message).toMatchInlineSnapshot(`
1353 "assertConsoleWarnDev(expected)
1354
1342 - Unexpected component stack for:
1343 - "Hello
1344 - in div (at **)"
1355 + Unexpected warning(s) recorded.
1356 +
1357 + - Expected warnings
1358 + + Received warnings
1359
1346 - If this warning should include a component stack, remove {withoutStack: true} from this warning.
1347 - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
1360 + - Hello
1361 + + Hello in div (at **)"
1362 `);
1363 });
1364
@@ -1361,16 +1375,16 @@ describe('ReactInternalTestUtils console assertions', () => {
1375 expect(message).toMatchInlineSnapshot(`
1376 "assertConsoleWarnDev(expected)
1377
1364 - Unexpected component stack for:
1365 - "Hello
1366 - in div (at **)"
1378 + Unexpected warning(s) recorded.
1379
1368 - Unexpected component stack for:
1369 - "Bye
1370 - in div (at **)"
1380 + - Expected warnings
1381 + + Received warnings
1382
1372 - If this warning should include a component stack, remove {withoutStack: true} from this warning.
1373 - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
1383 + - Hello
1384 + + Hello in div (at **)
1385 + Good day
1386 + - Bye
1387 + + Bye in div (at **)"
1388 `);
1389 });
1390 });
@@ -1382,9 +1396,9 @@ describe('ReactInternalTestUtils console assertions', () => {
1396 console.warn('Bye\n in div');
1397 }
1398 assertConsoleWarnDev([
1385 - 'Hello',
1399 + 'Hello\n in div',
1400 ['Good day', {withoutStack: true}],
1387 - 'Bye',
1401 + 'Bye\n in div',
1402 ]);
1403 });
1404
@@ -1490,12 +1504,13 @@ describe('ReactInternalTestUtils console assertions', () => {
1504 expect(message).toMatchInlineSnapshot(`
1505 "assertConsoleWarnDev(expected)
1506
1493 - Unexpected component stack for:
1494 - "Hello
1495 - in div (at **)"
1507 + Unexpected warning(s) recorded.
1508
1497 - If this warning should include a component stack, remove {withoutStack: true} from this warning.
1498 - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
1509 + - Expected warnings
1510 + + Received warnings
1511 +
1512 + - Hello
1513 + + Hello in div (at **)"
1514 `);
1515 });
1516
@@ -1524,16 +1539,16 @@ describe('ReactInternalTestUtils console assertions', () => {
1539 expect(message).toMatchInlineSnapshot(`
1540 "assertConsoleWarnDev(expected)
1541
1527 - Unexpected component stack for:
1528 - "Hello
1529 - in div (at **)"
1542 + Unexpected warning(s) recorded.
1543
1531 - Unexpected component stack for:
1532 - "Bye
1533 - in div (at **)"
1544 + - Expected warnings
1545 + + Received warnings
1546
1535 - If this warning should include a component stack, remove {withoutStack: true} from this warning.
1536 - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
1547 + - Hello
1548 + + Hello in div (at **)
1549 + Good day
1550 + - Bye
1551 + + Bye in div (at **)"
1552 `);
1553 });
1554 });
@@ -1606,13 +1621,18 @@ describe('ReactInternalTestUtils console assertions', () => {
1621 it('fails if component stack is passed twice', () => {
1622 const message = expectToThrowFailure(() => {
1623 console.warn('Hi %s%s', '\n in div', '\n in div');
1609 - assertConsoleWarnDev(['Hi']);
1624 + assertConsoleWarnDev(['Hi \n in div (at **)']);
1625 });
1626 expect(message).toMatchInlineSnapshot(`
1627 "assertConsoleWarnDev(expected)
1628
1614 - Received more than one component stack for a warning:
1615 - "Hi %s%s""
1629 + Unexpected warning(s) recorded.
1630 +
1631 + - Expected warnings
1632 + + Received warnings
1633 +
1634 + Hi in div (at **)
1635 + + in div (at **)"
1636 `);
1637 });
1638
@@ -1621,16 +1641,23 @@ describe('ReactInternalTestUtils console assertions', () => {
1641 const message = expectToThrowFailure(() => {
1642 console.warn('Hi %s%s', '\n in div', '\n in div');
1643 console.warn('Bye %s%s', '\n in div', '\n in div');
1624 - assertConsoleWarnDev(['Hi', 'Bye']);
1644 + assertConsoleWarnDev([
1645 + 'Hi \n in div (at **)',
1646 + 'Bye \n in div (at **)',
1647 + ]);
1648 });
1649 expect(message).toMatchInlineSnapshot(`
1650 "assertConsoleWarnDev(expected)
1651
1629 - Received more than one component stack for a warning:
1630 - "Hi %s%s"
1652 + Unexpected warning(s) recorded.
1653 +
1654 + - Expected warnings
1655 + + Received warnings
1656
1632 - Received more than one component stack for a warning:
1633 - "Bye %s%s""
1657 + Hi in div (at **)
1658 + + in div (at **)
1659 + Bye in div (at **)
1660 + + in div (at **)"
1661 `);
1662 });
1663
@@ -1646,7 +1673,7 @@ describe('ReactInternalTestUtils console assertions', () => {
1673
1674 Expected messages should be an array of strings but was given type "string"."
1675 `);
1649 - assertConsoleWarnDev(['Hi', 'Bye']);
1676 + assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
1677 });
1678
1679 // @gate __DEV__
@@ -1661,7 +1688,7 @@ describe('ReactInternalTestUtils console assertions', () => {
1688
1689 Expected messages should be an array of strings but was given type "string"."
1690 `);
1664 - assertConsoleWarnDev(['Hi', 'Bye']);
1691 + assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
1692 });
1693
1694 // @gate __DEV__
@@ -1677,7 +1704,11 @@ describe('ReactInternalTestUtils console assertions', () => {
1704
1705 Expected messages should be an array of strings but was given type "string"."
1706 `);
1680 - assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
1707 + assertConsoleWarnDev([
1708 + 'Hi \n in div',
1709 + 'Wow \n in div',
1710 + 'Bye \n in div',
1711 + ]);
1712 });
1713
1714 it('should fail if waitFor is called before asserting', async () => {
@@ -1884,7 +1915,7 @@ describe('ReactInternalTestUtils console assertions', () => {
1915 if (__DEV__) {
1916 console.error('Hello\n in div');
1917 }
1887 - assertConsoleErrorDev(['Hello']);
1918 + assertConsoleErrorDev(['Hello\n in div']);
1919 });
1920
1921 it('passes if all errors contain a stack', () => {
@@ -1893,7 +1924,11 @@ describe('ReactInternalTestUtils console assertions', () => {
1924 console.error('Good day\n in div');
1925 console.error('Bye\n in div');
1926 }
1896 - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
1927 + assertConsoleErrorDev([
1928 + 'Hello\n in div',
1929 + 'Good day\n in div',
1930 + 'Bye\n in div',
1931 + ]);
1932 });
1933
1934 it('fails if act is called without assertConsoleErrorDev', async () => {
@@ -2080,7 +2115,11 @@ describe('ReactInternalTestUtils console assertions', () => {
2115 const message = expectToThrowFailure(() => {
2116 console.error('Hi \n in div');
2117 console.error('Wow \n in div');
2083 - assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
2118 + assertConsoleErrorDev([
2119 + 'Hi \n in div',
2120 + 'Wow \n in div',
2121 + 'Bye \n in div',
2122 + ]);
2123 });
2124 expect(message).toMatchInlineSnapshot(`
2125 "assertConsoleErrorDev(expected)
@@ -2090,9 +2129,9 @@ describe('ReactInternalTestUtils console assertions', () => {
2129 - Expected errors
2130 + Received errors
2131
2093 - - Hi
2094 - - Wow
2095 - - Bye
2132 + - Hi in div
2133 + - Wow in div
2134 + - Bye in div
2135 + Hi in div (at **)
2136 + Wow in div (at **)"
2137 `);
@@ -2192,101 +2231,6 @@ describe('ReactInternalTestUtils console assertions', () => {
2231 + TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
2232 `);
2233 });
2195 - // @gate __DEV__
2196 - it('fails if only error does not contain a stack', () => {
2197 - const message = expectToThrowFailure(() => {
2198 - console.error('Hello');
2199 - assertConsoleErrorDev(['Hello']);
2200 - });
2201 - expect(message).toMatchInlineSnapshot(`
2202 - "assertConsoleErrorDev(expected)
2203 -
2204 - Missing component stack for:
2205 - "Hello"
2206 -
2207 - If this error should omit a component stack, pass [log, {withoutStack: true}].
2208 - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
2209 - `);
2210 - });
2211 -
2212 - // @gate __DEV__
2213 - it('fails if first error does not contain a stack', () => {
2214 - const message = expectToThrowFailure(() => {
2215 - console.error('Hello\n in div');
2216 - console.error('Good day\n in div');
2217 - console.error('Bye');
2218 - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
2219 - });
2220 - expect(message).toMatchInlineSnapshot(`
2221 - "assertConsoleErrorDev(expected)
2222 -
2223 - Missing component stack for:
2224 - "Bye"
2225 -
2226 - If this error should omit a component stack, pass [log, {withoutStack: true}].
2227 - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
2228 - `);
2229 - });
2230 - // @gate __DEV__
2231 - it('fails if last error does not contain a stack', () => {
2232 - const message = expectToThrowFailure(() => {
2233 - console.error('Hello');
2234 - console.error('Good day\n in div');
2235 - console.error('Bye\n in div');
2236 - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
2237 - });
2238 - expect(message).toMatchInlineSnapshot(`
2239 - "assertConsoleErrorDev(expected)
2240 -
2241 - Missing component stack for:
2242 - "Hello"
2243 -
2244 - If this error should omit a component stack, pass [log, {withoutStack: true}].
2245 - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
2246 - `);
2247 - });
2248 - // @gate __DEV__
2249 - it('fails if middle error does not contain a stack', () => {
2250 - const message = expectToThrowFailure(() => {
2251 - console.error('Hello\n in div');
2252 - console.error('Good day');
2253 - console.error('Bye\n in div');
2254 - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
2255 - });
2256 - expect(message).toMatchInlineSnapshot(`
2257 - "assertConsoleErrorDev(expected)
2258 -
2259 - Missing component stack for:
2260 - "Good day"
2261 -
2262 - If this error should omit a component stack, pass [log, {withoutStack: true}].
2263 - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
2264 - `);
2265 - });
2266 - // @gate __DEV__
2267 - it('fails if all errors do not contain a stack', () => {
2268 - const message = expectToThrowFailure(() => {
2269 - console.error('Hello');
2270 - console.error('Good day');
2271 - console.error('Bye');
2272 - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
2273 - });
2274 - expect(message).toMatchInlineSnapshot(`
2275 - "assertConsoleErrorDev(expected)
2276 -
2277 - Missing component stack for:
2278 - "Hello"
2279 -
2280 - Missing component stack for:
2281 - "Good day"
2282 -
2283 - Missing component stack for:
2284 - "Bye"
2285 -
2286 - If this error should omit a component stack, pass [log, {withoutStack: true}].
2287 - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
2288 - `);
2289 - });
2234
2235 // @gate __DEV__
2236 it('regression: checks entire string, not just the first letter', async () => {
@@ -2385,12 +2329,13 @@ describe('ReactInternalTestUtils console assertions', () => {
2329 expect(message).toMatchInlineSnapshot(`
2330 "assertConsoleErrorDev(expected)
2331
2388 - Unexpected component stack for:
2389 - "Hello
2390 - in div (at **)"
2332 + Unexpected error(s) recorded.
2333
2392 - If this error should include a component stack, remove {withoutStack: true} from this error.
2393 - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
2334 + - Expected errors
2335 + + Received errors
2336 +
2337 + - Hello
2338 + + Hello in div (at **)"
2339 `);
2340 });
2341
@@ -2407,16 +2352,16 @@ describe('ReactInternalTestUtils console assertions', () => {
2352 expect(message).toMatchInlineSnapshot(`
2353 "assertConsoleErrorDev(expected)
2354
2410 - Unexpected component stack for:
2411 - "Hello
2412 - in div (at **)"
2355 + Unexpected error(s) recorded.
2356
2414 - Unexpected component stack for:
2415 - "Bye
2416 - in div (at **)"
2357 + - Expected errors
2358 + + Received errors
2359
2418 - If this error should include a component stack, remove {withoutStack: true} from this error.
2419 - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
2360 + - Hello
2361 + + Hello in div (at **)
2362 + Good day
2363 + - Bye
2364 + + Bye in div (at **)"
2365 `);
2366 });
2367 });
@@ -2428,9 +2373,9 @@ describe('ReactInternalTestUtils console assertions', () => {
2373 console.error('Bye\n in div');
2374 }
2375 assertConsoleErrorDev([
2431 - 'Hello',
2376 + 'Hello\n in div',
2377 ['Good day', {withoutStack: true}],
2433 - 'Bye',
2378 + 'Bye\n in div',
2379 ]);
2380 });
2381
@@ -2536,12 +2481,13 @@ describe('ReactInternalTestUtils console assertions', () => {
2481 expect(message).toMatchInlineSnapshot(`
2482 "assertConsoleErrorDev(expected)
2483
2539 - Unexpected component stack for:
2540 - "Hello
2541 - in div (at **)"
2484 + Unexpected error(s) recorded.
2485 +
2486 + - Expected errors
2487 + + Received errors
2488
2543 - If this error should include a component stack, remove {withoutStack: true} from this error.
2544 - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
2489 + - Hello
2490 + + Hello in div (at **)"
2491 `);
2492 });
2493
@@ -2570,16 +2516,16 @@ describe('ReactInternalTestUtils console assertions', () => {
2516 expect(message).toMatchInlineSnapshot(`
2517 "assertConsoleErrorDev(expected)
2518
2573 - Unexpected component stack for:
2574 - "Hello
2575 - in div (at **)"
2519 + Unexpected error(s) recorded.
2520
2577 - Unexpected component stack for:
2578 - "Bye
2579 - in div (at **)"
2521 + - Expected errors
2522 + + Received errors
2523
2581 - If this error should include a component stack, remove {withoutStack: true} from this error.
2582 - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
2524 + - Hello
2525 + + Hello in div (at **)
2526 + Good day
2527 + - Bye
2528 + + Bye in div (at **)"
2529 `);
2530 });
2531
@@ -2678,13 +2624,18 @@ describe('ReactInternalTestUtils console assertions', () => {
2624 it('fails if component stack is passed twice', () => {
2625 const message = expectToThrowFailure(() => {
2626 console.error('Hi %s%s', '\n in div', '\n in div');
2681 - assertConsoleErrorDev(['Hi']);
2627 + assertConsoleErrorDev(['Hi \n in div (at **)']);
2628 });
2629 expect(message).toMatchInlineSnapshot(`
2630 "assertConsoleErrorDev(expected)
2631
2686 - Received more than one component stack for a warning:
2687 - "Hi %s%s""
2632 + Unexpected error(s) recorded.
2633 +
2634 + - Expected errors
2635 + + Received errors
2636 +
2637 + Hi in div (at **)
2638 + + in div (at **)"
2639 `);
2640 });
2641
@@ -2693,16 +2644,23 @@ describe('ReactInternalTestUtils console assertions', () => {
2644 const message = expectToThrowFailure(() => {
2645 console.error('Hi %s%s', '\n in div', '\n in div');
2646 console.error('Bye %s%s', '\n in div', '\n in div');
2696 - assertConsoleErrorDev(['Hi', 'Bye']);
2647 + assertConsoleErrorDev([
2648 + 'Hi \n in div (at **)',
2649 + 'Bye \n in div (at **)',
2650 + ]);
2651 });
2652 expect(message).toMatchInlineSnapshot(`
2653 "assertConsoleErrorDev(expected)
2654
2701 - Received more than one component stack for a warning:
2702 - "Hi %s%s"
2655 + Unexpected error(s) recorded.
2656 +
2657 + - Expected errors
2658 + + Received errors
2659
2704 - Received more than one component stack for a warning:
2705 - "Bye %s%s""
2660 + Hi in div (at **)
2661 + + in div (at **)
2662 + Bye in div (at **)
2663 + + in div (at **)"
2664 `);
2665 });
2666
@@ -2711,14 +2669,14 @@ describe('ReactInternalTestUtils console assertions', () => {
2669 const message = expectToThrowFailure(() => {
2670 console.error('Hi \n in div');
2671 console.error('Bye \n in div');
2714 - assertConsoleErrorDev('Hi', 'Bye');
2672 + assertConsoleErrorDev('Hi \n in div', 'Bye \n in div');
2673 });
2674 expect(message).toMatchInlineSnapshot(`
2675 "assertConsoleErrorDev(expected)
2676
2677 Expected messages should be an array of strings but was given type "string"."
2678 `);
2721 - assertConsoleErrorDev(['Hi', 'Bye']);
2679 + assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
2680 });
2681
2682 // @gate __DEV__
@@ -2733,7 +2691,7 @@ describe('ReactInternalTestUtils console assertions', () => {
2691
2692 Expected messages should be an array of strings but was given type "string"."
2693 `);
2736 - assertConsoleErrorDev(['Hi', 'Bye']);
2694 + assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
2695 });
2696
2697 // @gate __DEV__
@@ -2749,7 +2707,133 @@ describe('ReactInternalTestUtils console assertions', () => {
2707
2708 Expected messages should be an array of strings but was given type "string"."
2709 `);
2752 - assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
2710 + assertConsoleErrorDev([
2711 + 'Hi \n in div',
2712 + 'Wow \n in div',
2713 + 'Bye \n in div',
2714 + ]);
2715 + });
2716 +
2717 + describe('in <stack> placeholder', () => {
2718 + // @gate __DEV__
2719 + it('fails if `in <stack>` is used for a component stack instead of an error stack', () => {
2720 + const message = expectToThrowFailure(() => {
2721 + console.error('Warning message\n in div');
2722 + assertConsoleErrorDev(['Warning message\n in <stack>']);
2723 + });
2724 + expect(message).toMatchInlineSnapshot(`
2725 + "assertConsoleErrorDev(expected)
2726 +
2727 + Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
2728 +
2729 + Expected: "Warning message
2730 + in <stack>"
2731 + Received: "Warning message
2732 + in div (at **)"
2733 +
2734 + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
2735 + `);
2736 + });
2737 +
2738 + // @gate __DEV__
2739 + it('fails if `in <stack>` is used for multiple component stacks', () => {
2740 + const message = expectToThrowFailure(() => {
2741 + console.error('First warning\n in span');
2742 + console.error('Second warning\n in div');
2743 + assertConsoleErrorDev([
2744 + 'First warning\n in <stack>',
2745 + 'Second warning\n in <stack>',
2746 + ]);
2747 + });
2748 + expect(message).toMatchInlineSnapshot(`
2749 + "assertConsoleErrorDev(expected)
2750 +
2751 + Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
2752 +
2753 + Expected: "First warning
2754 + in <stack>"
2755 + Received: "First warning
2756 + in span (at **)"
2757 +
2758 + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)").
2759 +
2760 + Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
2761 +
2762 + Expected: "Second warning
2763 + in <stack>"
2764 + Received: "Second warning
2765 + in div (at **)"
2766 +
2767 + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
2768 + `);
2769 + });
2770 +
2771 + it('allows `in <stack>` for actual error stack traces', () => {
2772 + // This should pass - \n in <stack> is correctly used for an error stack
2773 + console.error(new Error('Something went wrong'));
2774 + assertConsoleErrorDev(['Error: Something went wrong\n in <stack>']);
2775 + });
2776 +
2777 + // @gate __DEV__
2778 + it('fails if error stack trace is present but \\n in <stack> is not expected', () => {
2779 + const message = expectToThrowFailure(() => {
2780 + console.error(new Error('Something went wrong'));
2781 + assertConsoleErrorDev(['Error: Something went wrong']);
2782 + });
2783 + expect(message).toMatch(`Unexpected error stack trace for:`);
2784 + expect(message).toMatch(`Error: Something went wrong`);
2785 + expect(message).toMatch(
2786 + 'If this error should include an error stack trace, add \\n in <stack> to your expected message'
2787 + );
2788 + });
2789 +
2790 + // @gate __DEV__
2791 + it('fails if `in <stack>` is expected but no stack is present', () => {
2792 + const message = expectToThrowFailure(() => {
2793 + console.error('Error: Something went wrong');
2794 + assertConsoleErrorDev([
2795 + 'Error: Something went wrong\n in <stack>',
2796 + ]);
2797 + });
2798 + expect(message).toMatchInlineSnapshot(`
2799 + "assertConsoleErrorDev(expected)
2800 +
2801 + Missing error stack trace for:
2802 + "Error: Something went wrong"
2803 +
2804 + The expected message uses \\n in <stack> but the actual error doesn't include an error stack trace.
2805 + If this error should not have an error stack trace, remove \\n in <stack> from your expected message."
2806 + `);
2807 + });
2808 + });
2809 +
2810 + describe('[Environment] placeholder', () => {
2811 + // @gate __DEV__
2812 + it('expands [Server] to ANSI escape sequence for server badge', () => {
2813 + const badge = '\u001b[0m\u001b[7m Server \u001b[0m';
2814 + console.error(badge + 'Error: something went wrong');
2815 + assertConsoleErrorDev([
2816 + ['[Server] Error: something went wrong', {withoutStack: true}],
2817 + ]);
2818 + });
2819 +
2820 + // @gate __DEV__
2821 + it('expands [Prerender] to ANSI escape sequence for server badge', () => {
2822 + const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m';
2823 + console.error(badge + 'Error: something went wrong');
2824 + assertConsoleErrorDev([
2825 + ['[Prerender] Error: something went wrong', {withoutStack: true}],
2826 + ]);
2827 + });
2828 +
2829 + // @gate __DEV__
2830 + it('expands [Cache] to ANSI escape sequence for server badge', () => {
2831 + const badge = '\u001b[0m\u001b[7m Cache \u001b[0m';
2832 + console.error(badge + 'Error: something went wrong');
2833 + assertConsoleErrorDev([
2834 + ['[Cache] Error: something went wrong', {withoutStack: true}],
2835 + ]);
2836 + });
2837 });
2838
2839 it('should fail if waitFor is called before asserting', async () => {
packages/internal-test-utils/consoleMock.js
+201 -21
@@ -168,6 +168,53 @@ function normalizeCodeLocInfo(str) {
168 });
169 }
170
171 +// Expands environment placeholders like [Server] into ANSI escape sequences.
172 +// This allows test assertions to use a cleaner syntax like "[Server] Error:"
173 +// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"
174 +function expandEnvironmentPlaceholders(str) {
175 + if (typeof str !== 'string') {
176 + return str;
177 + }
178 + // [Environment] -> ANSI escape sequence for environment badge
179 + // The format is: reset + inverse + " Environment " + reset
180 + return str.replace(
181 + /^\[(\w+)] /g,
182 + (match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',
183 + );
184 +}
185 +
186 +// The error stack placeholder that can be used in expected messages
187 +const ERROR_STACK_PLACEHOLDER = '\n in <stack>';
188 +// A marker used to protect the placeholder during normalization
189 +const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';
190 +
191 +// Normalizes expected messages, handling special placeholders
192 +function normalizeExpectedMessage(str) {
193 + if (typeof str !== 'string') {
194 + return str;
195 + }
196 + // Protect the error stack placeholder from normalization
197 + // (normalizeCodeLocInfo would add "(at **)" to it)
198 + const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);
199 + let result = str;
200 + if (hasStackPlaceholder) {
201 + result = result.replace(
202 + ERROR_STACK_PLACEHOLDER,
203 + ERROR_STACK_PLACEHOLDER_MARKER,
204 + );
205 + }
206 + result = normalizeCodeLocInfo(result);
207 + result = expandEnvironmentPlaceholders(result);
208 + if (hasStackPlaceholder) {
209 + // Restore the placeholder (remove the "(at **)" that was added)
210 + result = result.replace(
211 + ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',
212 + ERROR_STACK_PLACEHOLDER,
213 + );
214 + }
215 + return result;
216 +}
217 +
218 function normalizeComponentStack(entry) {
219 if (
220 typeof entry[0] === 'string' &&
@@ -187,6 +234,15 @@ const isLikelyAComponentStack = message =>
234 message.includes('\n in ') ||
235 message.includes('\n at '));
236
237 +// Error stack traces start with "*Error:" and contain "at" frames with file paths
238 +// Component stacks contain "in ComponentName" patterns
239 +// This helps validate that \n in <stack> is used correctly
240 +const isLikelyAnErrorStackTrace = message =>
241 + typeof message === 'string' &&
242 + message.includes('Error:') &&
243 + // Has "at" frames typical of error stacks (with file:line:col)
244 + /\n\s+at .+\(.*:\d+:\d+\)/.test(message);
245 +
246 export function createLogAssertion(
247 consoleMethod,
248 matcherName,
@@ -236,13 +292,11 @@ export function createLogAssertion(
292
293 const withoutStack = options.withoutStack;
294
239 - // Warn about invalid global withoutStack values.
295 if (consoleMethod === 'log' && withoutStack !== undefined) {
296 throwFormattedError(
297 `Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
298 );
299 } else if (withoutStack !== undefined && withoutStack !== true) {
245 - // withoutStack can only have a value true.
300 throwFormattedError(
301 `The second argument must be {withoutStack: true}.` +
302 `\n\nInstead received ${JSON.stringify(options)}.`,
@@ -256,8 +310,11 @@ export function createLogAssertion(
310 const unexpectedLogs = [];
311 const unexpectedMissingComponentStack = [];
312 const unexpectedIncludingComponentStack = [];
313 + const unexpectedMissingErrorStack = [];
314 + const unexpectedIncludingErrorStack = [];
315 const logsMismatchingFormat = [];
316 const logsWithExtraComponentStack = [];
317 + const stackTracePlaceholderMisuses = [];
318
319 // Loop over all the observed logs to determine:
320 // - Which expected logs are missing
@@ -319,11 +376,11 @@ export function createLogAssertion(
376 );
377 }
378
322 - expectedMessage = normalizeCodeLocInfo(currentExpectedMessage);
379 + expectedMessage = normalizeExpectedMessage(currentExpectedMessage);
380 expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
381 } else if (typeof expectedMessageOrArray === 'string') {
325 - // Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
326 - expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray);
382 + expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);
383 + // withoutStack: inherit from global option - simplify when withoutStack is removed.
384 if (consoleMethod === 'log') {
385 expectedWithoutStack = true;
386 } else {
@@ -381,19 +438,93 @@ export function createLogAssertion(
438 }
439
440 // Main logic to check if log is expected, with the component stack.
384 - if (
385 - typeof expectedMessage === 'string' &&
386 - (normalizedMessage === expectedMessage ||
387 - normalizedMessage.includes(expectedMessage))
388 - ) {
441 + // Check for exact match OR if the message matches with a component stack appended
442 + let matchesExpectedMessage = false;
443 + let expectsErrorStack = false;
444 + const hasErrorStack = isLikelyAnErrorStackTrace(message);
445 +
446 + if (typeof expectedMessage === 'string') {
447 + if (normalizedMessage === expectedMessage) {
448 + matchesExpectedMessage = true;
449 + } else if (expectedMessage.includes('\n in <stack>')) {
450 + expectsErrorStack = true;
451 + // \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")
452 + // NOT for React component stacks (e.g., "\n in ComponentName (at **)").
453 + // Validate that the actual message looks like an error stack trace.
454 + if (!hasErrorStack) {
455 + // The actual message doesn't look like an error stack trace.
456 + // This is likely a misuse - someone used \n in <stack> for a component stack.
457 + stackTracePlaceholderMisuses.push({
458 + expected: expectedMessage,
459 + received: normalizedMessage,
460 + });
461 + }
462 +
463 + const expectedMessageWithoutStack = expectedMessage.replace(
464 + '\n in <stack>',
465 + '',
466 + );
467 + if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {
468 + // Remove the stack trace
469 + const remainder = normalizedMessage.slice(
470 + expectedMessageWithoutStack.length,
471 + );
472 +
473 + // After normalization, both error stacks and component stacks look like
474 + // component stacks (at frames are converted to "in ... (at **)" format).
475 + // So we check isLikelyAComponentStack for matching purposes.
476 + if (isLikelyAComponentStack(remainder)) {
477 + const messageWithoutStack = normalizedMessage.replace(
478 + remainder,
479 + '',
480 + );
481 + if (messageWithoutStack === expectedMessageWithoutStack) {
482 + matchesExpectedMessage = true;
483 + }
484 + } else if (remainder === '') {
485 + // \n in <stack> was expected but there's no stack at all
486 + matchesExpectedMessage = true;
487 + }
488 + } else if (normalizedMessage === expectedMessageWithoutStack) {
489 + // \n in <stack> was expected but actual has no stack at all (exact match without stack)
490 + matchesExpectedMessage = true;
491 + }
492 + } else if (
493 + hasErrorStack &&
494 + !expectedMessage.includes('\n in <stack>') &&
495 + normalizedMessage.startsWith(expectedMessage)
496 + ) {
497 + matchesExpectedMessage = true;
498 + }
499 + }
500 +
501 + if (matchesExpectedMessage) {
502 + // withoutStack: Check for unexpected/missing component stacks.
503 + // These checks can be simplified when withoutStack is removed.
504 if (isLikelyAComponentStack(normalizedMessage)) {
390 - if (expectedWithoutStack === true) {
505 + if (expectedWithoutStack === true && !hasErrorStack) {
506 + // Only report unexpected component stack if it's not an error stack
507 + // (error stacks look like component stacks after normalization)
508 unexpectedIncludingComponentStack.push(normalizedMessage);
509 }
393 - } else if (expectedWithoutStack !== true) {
510 + } else if (expectedWithoutStack !== true && !expectsErrorStack) {
511 unexpectedMissingComponentStack.push(normalizedMessage);
512 }
513
514 + // Check for unexpected/missing error stacks
515 + if (hasErrorStack && !expectsErrorStack) {
516 + // Error stack is present but \n in <stack> was not in the expected message
517 + unexpectedIncludingErrorStack.push(normalizedMessage);
518 + } else if (
519 + expectsErrorStack &&
520 + !hasErrorStack &&
521 + !isLikelyAComponentStack(normalizedMessage)
522 + ) {
523 + // \n in <stack> was expected but the actual message doesn't have any stack at all
524 + // (if it has a component stack, stackTracePlaceholderMisuses already handles it)
525 + unexpectedMissingErrorStack.push(normalizedMessage);
526 + }
527 +
528 // Found expected log, remove it from missing.
529 missingExpectedLogs.splice(0, 1);
530 } else {
@@ -422,6 +553,21 @@ export function createLogAssertion(
553 )}`;
554 }
555
556 + // Wrong %s formatting is a failure.
557 + // This is a common mistake when creating new warnings.
558 + if (logsMismatchingFormat.length > 0) {
559 + throwFormattedError(
560 + logsMismatchingFormat
561 + .map(
562 + item =>
563 + `Received ${item.args.length} arguments for a message with ${
564 + item.expectedArgCount
565 + } placeholders:\n ${printReceived(item.format)}`,
566 + )
567 + .join('\n\n'),
568 + );
569 + }
570 +
571 // Any unexpected warnings should be treated as a failure.
572 if (unexpectedLogs.length > 0) {
573 throwFormattedError(
@@ -466,18 +612,33 @@ export function createLogAssertion(
612 );
613 }
614
469 - // Wrong %s formatting is a failure.
470 - // This is a common mistake when creating new warnings.
471 - if (logsMismatchingFormat.length > 0) {
615 + // Any logs that include an error stack trace but \n in <stack> wasn't expected.
616 + if (unexpectedIncludingErrorStack.length > 0) {
617 throwFormattedError(
473 - logsMismatchingFormat
618 + `${unexpectedIncludingErrorStack
619 .map(
475 - item =>
476 - `Received ${item.args.length} arguments for a message with ${
477 - item.expectedArgCount
478 - } placeholders:\n ${printReceived(item.format)}`,
620 + stack =>
621 + `Unexpected error stack trace for:\n ${printReceived(stack)}`,
622 )
480 - .join('\n\n'),
623 + .join(
624 + '\n\n',
625 + )}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +
626 + `(e.g., "Error: message\\n in <stack>").`,
627 + );
628 + }
629 +
630 + // Any logs that are missing an error stack trace when \n in <stack> was expected.
631 + if (unexpectedMissingErrorStack.length > 0) {
632 + throwFormattedError(
633 + `${unexpectedMissingErrorStack
634 + .map(
635 + stack =>
636 + `Missing error stack trace for:\n ${printReceived(stack)}`,
637 + )
638 + .join(
639 + '\n\n',
640 + )}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +
641 + `\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,
642 );
643 }
644
@@ -496,6 +657,25 @@ export function createLogAssertion(
657 .join('\n\n'),
658 );
659 }
660 +
661 + // Using \n in <stack> for component stacks is a misuse.
662 + // \n in <stack> should only be used for JavaScript Error stack traces,
663 + // not for React component stacks.
664 + if (stackTracePlaceholderMisuses.length > 0) {
665 + throwFormattedError(
666 + `${stackTracePlaceholderMisuses
667 + .map(
668 + item =>
669 + `Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +
670 + `stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +
671 + `Expected: ${printReceived(item.expected)}\n` +
672 + `Received: ${printReceived(item.received)}\n\n` +
673 + `If this ${logName()} has a component stack, include the full component stack in your expected message ` +
674 + `(e.g., "Warning message\\n in ComponentName (at **)").`,
675 + )
676 + .join('\n\n')}`,
677 + );
678 + }
679 }
680 };
681 }
packages/react-client/src/__tests__/ReactFlight-test.js
+3 -2
@@ -1729,7 +1729,8 @@ describe('ReactFlight', () => {
1729 'Only plain objects can be passed to Client Components from Server Components. ' +
1730 'Objects with symbol properties like Symbol.iterator are not supported.\n' +
1731 ' <... value={{}}>\n' +
1732 - ' ^^^^\n',
1732 + ' ^^^^\n' +
1733 + ' in (at **)',
1734 ]);
1735 });
1736
@@ -3258,7 +3259,7 @@ describe('ReactFlight', () => {
3259 const transport = ReactNoopFlightServer.render({
3260 root: ReactServer.createElement(App),
3261 });
3261 - assertConsoleErrorDev(['Error: err']);
3262 + assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
3263
3264 expect(mockConsoleLog).toHaveBeenCalledTimes(1);
3265 expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+8 -3
@@ -734,7 +734,11 @@ describe('ReactHooksInspection', () => {
734 });
735 const results = normalizeSourceLoc(tree);
736 expect(results).toHaveLength(1);
737 - expect(results[0]).toMatchInlineSnapshot(`
737 + expect(results[0]).toMatchInlineSnapshot(
738 + {
739 + subHooks: [{value: expect.any(Promise)}],
740 + },
741 + `
742 {
743 "debugInfo": null,
744 "hookSource": {
@@ -759,12 +763,13 @@ describe('ReactHooksInspection', () => {
763 "isStateEditable": false,
764 "name": "Use",
765 "subHooks": [],
762 - "value": Promise {},
766 + "value": Any<Promise>,
767 },
768 ],
769 "value": undefined,
770 }
767 - `);
771 + `,
772 + );
773 });
774
775 describe('useDebugValue', () => {
packages/react-dom/src/__tests__/ReactDOM-test.js
+15 -4
@@ -548,16 +548,23 @@ describe('ReactDOM', () => {
548 ' in App (at **)',
549 // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
550 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
551 - ' in blink (at **)',
551 + ' in blink (at **)\n' +
552 + ' in App2 (at **)\n' +
553 + ' in Child (at **)\n' +
554 + ' in ServerEntry (at **)',
555 // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span)
556 'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
557 ' in span (at **)\n' +
558 ' in Child2 (at **)\n' +
556 - ' in App2 (at **)',
559 + ' in App2 (at **)\n' +
560 + ' in Child (at **)\n' +
561 + ' in ServerEntry (at **)',
562 // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span)
563 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
564 ' in span (at **)\n' +
560 - ' in Child (at **)',
565 + ' in Child (at **)\n' +
566 + ' in ServerEntry (at **)',
567 +
568 // ReactDOM(App > div > font)
569 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
570 ' in font (at **)\n' +
@@ -775,7 +782,11 @@ describe('ReactDOM', () => {
782
783 // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
784 // root of the application
778 - assertConsoleErrorDev(['In HTML, <head> cannot be a child of <main>']);
785 + assertConsoleErrorDev([
786 + 'In HTML, <head> cannot be a child of <main>.\nThis will cause a hydration error.\n' +
787 + ' in head (at **)\n' +
788 + ' in App (at **)',
789 + ]);
790
791 await act(() => {
792 root.render(<App phase={1} />);
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+30 -15
@@ -6879,9 +6879,12 @@ describe('ReactDOMFizzServer', () => {
6879 });
6880
6881 assertConsoleErrorDev([
6882 - 'The render was aborted by the server without a reason.',
6883 - 'The render was aborted by the server without a reason.',
6884 - 'The render was aborted by the server without a reason.',
6882 + 'Error: The render was aborted by the server without a reason.' +
6883 + '\n in <stack>',
6884 + 'Error: The render was aborted by the server without a reason.' +
6885 + '\n in <stack>',
6886 + 'Error: The render was aborted by the server without a reason.' +
6887 + '\n in <stack>',
6888 ]);
6889
6890 expect(finished).toBe(true);
@@ -6943,9 +6946,12 @@ describe('ReactDOMFizzServer', () => {
6946 });
6947
6948 assertConsoleErrorDev([
6946 - 'The render was aborted by the server without a reason.',
6947 - 'The render was aborted by the server without a reason.',
6948 - 'The render was aborted by the server without a reason.',
6949 + 'Error: The render was aborted by the server without a reason.' +
6950 + '\n in <stack>',
6951 + 'Error: The render was aborted by the server without a reason.' +
6952 + '\n in <stack>',
6953 + 'Error: The render was aborted by the server without a reason.' +
6954 + '\n in <stack>',
6955 ]);
6956
6957 expect(finished).toBe(true);
@@ -7007,9 +7013,12 @@ describe('ReactDOMFizzServer', () => {
7013 });
7014
7015 assertConsoleErrorDev([
7010 - 'The render was aborted by the server without a reason.',
7011 - 'The render was aborted by the server without a reason.',
7012 - 'The render was aborted by the server without a reason.',
7016 + 'Error: The render was aborted by the server without a reason.' +
7017 + '\n in <stack>',
7018 + 'Error: The render was aborted by the server without a reason.' +
7019 + '\n in <stack>',
7020 + 'Error: The render was aborted by the server without a reason.' +
7021 + '\n in <stack>',
7022 ]);
7023
7024 expect(finished).toBe(true);
@@ -7069,9 +7078,12 @@ describe('ReactDOMFizzServer', () => {
7078 });
7079
7080 assertConsoleErrorDev([
7072 - 'The render was aborted by the server without a reason.',
7073 - 'The render was aborted by the server without a reason.',
7074 - 'The render was aborted by the server without a reason.',
7081 + 'Error: The render was aborted by the server without a reason.' +
7082 + '\n in <stack>',
7083 + 'Error: The render was aborted by the server without a reason.' +
7084 + '\n in <stack>',
7085 + 'Error: The render was aborted by the server without a reason.' +
7086 + '\n in <stack>',
7087 ]);
7088
7089 expect(finished).toBe(true);
@@ -9024,7 +9036,8 @@ describe('ReactDOMFizzServer', () => {
9036 pipe(writable);
9037 });
9038 assertConsoleErrorDev([
9027 - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0mR4nd0m". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "R4nd0m" that was included with this render.',
9039 + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0mR4nd0m". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "R4nd0m" that was included with this render.' +
9040 + '\n in style (at **)',
9041 ]);
9042 expect(getVisibleChildren(document)).toEqual(
9043 <html>
@@ -9054,7 +9067,8 @@ describe('ReactDOMFizzServer', () => {
9067 pipe(writable);
9068 });
9069 assertConsoleErrorDev([
9057 - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.',
9070 + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.' +
9071 + '\n in style (at **)',
9072 ]);
9073 expect(getVisibleChildren(document)).toEqual(
9074 <html>
@@ -9085,7 +9099,8 @@ describe('ReactDOMFizzServer', () => {
9099 pipe(writable);
9100 });
9101 assertConsoleErrorDev([
9088 - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.',
9102 + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.' +
9103 + '\n in style (at **)',
9104 ]);
9105 expect(getVisibleChildren(document)).toEqual(
9106 <html>
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+18 -1
@@ -3628,7 +3628,24 @@ body {
3628 assertLog(['load stylesheet: foo']);
3629 await waitForAll([]);
3630 assertConsoleErrorDev([
3631 - "Hydration failed because the server rendered HTML didn't match the client.",
3631 + "Error: Hydration failed because the server rendered HTML didn't match the client. " +
3632 + 'As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n' +
3633 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
3634 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
3635 + "- Date formatting in a user's locale which doesn't match the server.\n" +
3636 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
3637 + '- Invalid HTML tag nesting.\n\n' +
3638 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
3639 + 'https://react.dev/link/hydration-mismatch\n\n' +
3640 + ' <html>\n' +
3641 + ' <body>\n' +
3642 + ' <div>\n' +
3643 + ' <div>\n' +
3644 + ' <Suspense fallback="loading 2...">\n' +
3645 + ' <Component>\n' +
3646 + ' <link>\n' +
3647 + '+ <div>' +
3648 + '\n in <stack>',
3649 ]);
3650 jest.runAllTimers();
3651
packages/react-dom/src/__tests__/ReactDOMSrcObject-test.js
+6 -3
@@ -120,11 +120,14 @@ describe('ReactDOMSrcObject', () => {
120
121 assertConsoleErrorDev([
122 'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
123 - 'Pass it directly to <img src>, <video src> or <audio src> instead.',
123 + 'Pass it directly to <img src>, <video src> or <audio src> instead.' +
124 + '\n in source (at **)',
125 'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
125 - 'Pass it directly to <img src>, <video src> or <audio src> instead.',
126 + 'Pass it directly to <img src>, <video src> or <audio src> instead.' +
127 + '\n in source (at **)',
128 'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
127 - 'Pass it directly to <img src>, <video src> or <audio src> instead.',
129 + 'Pass it directly to <img src>, <video src> or <audio src> instead.' +
130 + '\n in source (at **)',
131 ]);
132 expect(videoRef.current.firstChild.src).not.toMatch(/^blob:/);
133 expect(videoRef.current.firstChild.src).toContain('[object%20Blob]'); // toString:ed
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+2 -1
@@ -110,7 +110,8 @@ describe('ReactFlushSync', () => {
110 assertConsoleErrorDev([
111 'flushSync was called from inside a lifecycle method. React ' +
112 'cannot flush when React is already rendering. Consider moving this ' +
113 - 'call to a scheduler task or micro task.',
113 + 'call to a scheduler task or micro task.' +
114 + '\n in App',
115 ]);
116
117 await waitForPaint([]);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+56 -29
@@ -2145,7 +2145,8 @@ describe('ReactFlightDOM', () => {
2145 pipe(flightWritable);
2146 });
2147 assertConsoleErrorDev([
2148 - 'The render was aborted by the server without a reason.',
2148 + 'Error: The render was aborted by the server without a reason.' +
2149 + '\n in <stack>',
2150 ]);
2151
2152 const response =
@@ -2169,9 +2170,12 @@ describe('ReactFlightDOM', () => {
2170 ).pipe(fizzWritable);
2171 });
2172 assertConsoleErrorDev([
2172 - 'The render was aborted by the server without a reason.',
2173 - 'The render was aborted by the server without a reason.',
2174 - 'The render was aborted by the server without a reason.',
2173 + '[Server] Error: The render was aborted by the server without a reason.' +
2174 + '\n in <stack>',
2175 + '[Server] Error: The render was aborted by the server without a reason.' +
2176 + '\n in <stack>',
2177 + '[Server] Error: The render was aborted by the server without a reason.' +
2178 + '\n in <stack>',
2179 ]);
2180
2181 expect(shellErrors).toEqual([]);
@@ -2235,7 +2239,8 @@ describe('ReactFlightDOM', () => {
2239 });
2240
2241 assertConsoleErrorDev([
2238 - 'The render was aborted by the server without a reason.',
2242 + 'Error: The render was aborted by the server without a reason.' +
2243 + '\n in <stack>',
2244 ]);
2245
2246 const response =
@@ -2260,9 +2265,12 @@ describe('ReactFlightDOM', () => {
2265 });
2266
2267 assertConsoleErrorDev([
2263 - 'The render was aborted by the server without a reason.',
2264 - 'The render was aborted by the server without a reason.',
2265 - 'The render was aborted by the server without a reason.',
2268 + '[Server] Error: The render was aborted by the server without a reason.' +
2269 + '\n in <stack>',
2270 + '[Server] Error: The render was aborted by the server without a reason.' +
2271 + '\n in <stack>',
2272 + '[Server] Error: The render was aborted by the server without a reason.' +
2273 + '\n in <stack>',
2274 ]);
2275
2276 expect(shellErrors).toEqual([]);
@@ -2327,7 +2335,8 @@ describe('ReactFlightDOM', () => {
2335 pipe(flightWritable);
2336 });
2337 assertConsoleErrorDev([
2330 - 'The render was aborted by the server without a reason.',
2338 + 'Error: The render was aborted by the server without a reason.' +
2339 + '\n in <stack>',
2340 ]);
2341
2342 const response =
@@ -2351,9 +2360,12 @@ describe('ReactFlightDOM', () => {
2360 ).pipe(fizzWritable);
2361 });
2362 assertConsoleErrorDev([
2354 - 'The render was aborted by the server without a reason.',
2355 - 'The render was aborted by the server without a reason.',
2356 - 'The render was aborted by the server without a reason.',
2363 + '[Server] Error: The render was aborted by the server without a reason.' +
2364 + '\n in <stack>',
2365 + '[Server] Error: The render was aborted by the server without a reason.' +
2366 + '\n in <stack>',
2367 + '[Server] Error: The render was aborted by the server without a reason.' +
2368 + '\n in <stack>',
2369 ]);
2370
2371 expect(shellErrors).toEqual([]);
@@ -2416,7 +2428,8 @@ describe('ReactFlightDOM', () => {
2428 pipe(flightWritable);
2429 });
2430 assertConsoleErrorDev([
2419 - 'The render was aborted by the server without a reason.',
2431 + 'Error: The render was aborted by the server without a reason.' +
2432 + '\n in <stack>',
2433 ]);
2434
2435 const response =
@@ -2440,9 +2453,12 @@ describe('ReactFlightDOM', () => {
2453 ).pipe(fizzWritable);
2454 });
2455 assertConsoleErrorDev([
2443 - 'The render was aborted by the server without a reason.',
2444 - 'The render was aborted by the server without a reason.',
2445 - 'The render was aborted by the server without a reason.',
2456 + '[Server] Error: The render was aborted by the server without a reason.' +
2457 + '\n in <stack>',
2458 + '[Server] Error: The render was aborted by the server without a reason.' +
2459 + '\n in <stack>',
2460 + '[Server] Error: The render was aborted by the server without a reason.' +
2461 + '\n in <stack>',
2462 ]);
2463
2464 expect(shellErrors).toEqual([]);
@@ -2504,7 +2520,8 @@ describe('ReactFlightDOM', () => {
2520 });
2521
2522 assertConsoleErrorDev([
2507 - 'The render was aborted by the server without a reason.',
2523 + 'Error: The render was aborted by the server without a reason.' +
2524 + '\n in <stack>',
2525 ]);
2526
2527 const response =
@@ -2528,9 +2545,12 @@ describe('ReactFlightDOM', () => {
2545 ).pipe(fizzWritable);
2546 });
2547 assertConsoleErrorDev([
2531 - 'The render was aborted by the server without a reason.',
2532 - 'The render was aborted by the server without a reason.',
2533 - 'The render was aborted by the server without a reason.',
2548 + '[Server] Error: The render was aborted by the server without a reason.' +
2549 + '\n in <stack>',
2550 + '[Server] Error: The render was aborted by the server without a reason.' +
2551 + '\n in <stack>',
2552 + '[Server] Error: The render was aborted by the server without a reason.' +
2553 + '\n in <stack>',
2554 ]);
2555
2556 expect(shellErrors).toEqual([]);
@@ -2596,7 +2616,8 @@ describe('ReactFlightDOM', () => {
2616 });
2617
2618 assertConsoleErrorDev([
2599 - 'The render was aborted by the server without a reason.',
2619 + 'Error: The render was aborted by the server without a reason.' +
2620 + '\n in <stack>',
2621 ]);
2622
2623 const response =
@@ -2620,8 +2641,10 @@ describe('ReactFlightDOM', () => {
2641 ).pipe(fizzWritable);
2642 });
2643 assertConsoleErrorDev([
2623 - 'The render was aborted by the server without a reason.',
2624 - 'The render was aborted by the server without a reason.',
2644 + '[Server] Error: The render was aborted by the server without a reason.' +
2645 + '\n in <stack>',
2646 + '[Server] Error: The render was aborted by the server without a reason.' +
2647 + '\n in <stack>',
2648 ]);
2649
2650 expect(shellErrors).toEqual([]);
@@ -2668,7 +2691,8 @@ describe('ReactFlightDOM', () => {
2691 });
2692
2693 assertConsoleErrorDev([
2671 - 'The render was aborted by the server without a reason.',
2694 + 'Error: The render was aborted by the server without a reason.' +
2695 + '\n in <stack>',
2696 ]);
2697
2698 const response =
@@ -2692,7 +2716,8 @@ describe('ReactFlightDOM', () => {
2716 ).pipe(fizzWritable);
2717 });
2718 assertConsoleErrorDev([
2695 - 'The render was aborted by the server without a reason.',
2719 + '[Server] Error: The render was aborted by the server without a reason.' +
2720 + '\n in <stack>',
2721 ]);
2722
2723 expect(shellErrors).toEqual([]);
@@ -2760,8 +2785,9 @@ describe('ReactFlightDOM', () => {
2785 });
2786
2787 assertConsoleErrorDev([
2763 - 'The render was aborted by the server without a reason.',
2764 - 'bam!',
2788 + 'Error: The render was aborted by the server without a reason.' +
2789 + '\n in <stack>',
2790 + 'Error: bam!\n in <stack>',
2791 ]);
2792
2793 const response =
@@ -2785,8 +2811,9 @@ describe('ReactFlightDOM', () => {
2811 ).pipe(fizzWritable);
2812 });
2813 assertConsoleErrorDev([
2788 - 'The render was aborted by the server without a reason.',
2789 - 'bam!',
2814 + '[Server] Error: The render was aborted by the server without a reason.' +
2815 + '\n in <stack>',
2816 + '[Server] Error: bam!\n in <stack>',
2817 ]);
2818
2819 expect(shellErrors).toEqual([]);
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+4 -2
@@ -957,7 +957,8 @@ describe('ReactFlightDOMForm', () => {
957 'Failed to serialize an action for progressive enhancement:\n' +
958 'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' +
959 ' [<div/>]\n' +
960 - ' ^^^^^^',
960 + ' ^^^^^^' +
961 + '\n in <stack>',
962 ]);
963
964 // The error message was returned as JSX.
@@ -1032,7 +1033,8 @@ describe('ReactFlightDOMForm', () => {
1033 await submitTheForm();
1034 assertConsoleErrorDev([
1035 'Failed to serialize an action for progressive enhancement:\n' +
1035 - 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.',
1036 + 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.' +
1037 + '\n in <stack>',
1038 ]);
1039
1040 expect(blob instanceof Blob).toBe(true);
packages/react-server/src/__tests__/ReactFlightServer-test.js
+2 -1
@@ -196,7 +196,8 @@ describe('ReactFlight', () => {
196 "The props of this element may help locate this element: { children: 'Free!', [key]: [Getter] }",
197 {withoutStack: true},
198 ],
199 - "TypeError: Cannot read properties of undefined (reading 'stack')",
199 + "TypeError: Cannot read properties of undefined (reading 'stack')" +
200 + '\n in <stack>',
201 ]);
202 });
203 });
packages/react/src/__tests__/ReactChildren-test.js
+9 -7
@@ -864,6 +864,7 @@ describe('ReactChildren', () => {
864 });
865
866 it('warns for mapped list children without keys', async () => {
867 + spyOnDev(console, 'error').mockImplementation(() => {});
868 function ComponentRenderingMappedChildren({children}) {
869 return (
870 <div>
@@ -883,13 +884,14 @@ describe('ReactChildren', () => {
884 </ComponentRenderingMappedChildren>,
885 );
886 });
886 - assertConsoleErrorDev([
887 - 'Each child in a list should have a unique "key" prop.\n\n' +
888 - 'Check the render method of `ComponentRenderingMappedChildren`.' +
889 - ' See https://react.dev/link/warning-keys for more information.\n' +
890 - ' in div (at **)\n' +
891 - ' in **/ReactChildren-test.js:**:** (at **)',
892 - ]);
887 + if (__DEV__) {
888 + const calls = console.error.mock.calls;
889 + console.error.mockRestore();
890 + expect(calls.length).toBe(1);
891 + expect(calls[0][0]).toEqual(
892 + 'Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.',
893 + );
894 + }
895 });
896
897 it('does not warn for mapped static children without keys', async () => {
packages/react/src/__tests__/createReactClassIntegration-test.js
+5 -2
@@ -349,10 +349,13 @@ describe('create-react-class-integration', () => {
349 });
350 assertConsoleErrorDev([
351 [
352 - 'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
352 + 'Component uses the legacy childContextTypes API which will soon be removed. ' +
353 + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)',
354 {withoutStack: true},
355 ],
355 - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
356 + 'Component uses the legacy contextTypes API which will soon be removed. ' +
357 + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)' +
358 + '\n in ReactClassComponent (at **)',
359 ]);
360 expect(container.firstChild.className).toBe('foo');
361 });
packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js
+19 -4
@@ -641,11 +641,21 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
641 'The result of getSnapshot should be cached to avoid an infinite loop',
642 {withoutStack: true},
643 ],
644 - 'Error: Maximum update depth exceeded',
645 - 'The above error occurred i',
644 + [
645 + 'Error: Maximum update depth exceeded. ' +
646 + 'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' +
647 + 'React limits the number of nested updates to prevent infinite loops.' +
648 + '\n in <stack>',
649 + {withoutStack: true},
650 + ],
651 + 'The above error occurred in the <App> component:\n\n' +
652 + ' in App (at **)\n\n' +
653 + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
654 + 'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.',
655 ]
656 : [
648 - 'The result of getSnapshot should be cached to avoid an infinite loop',
657 + 'The result of getSnapshot should be cached to avoid an infinite loop' +
658 + '\n in App (at **)',
659 ],
660 );
661 });
@@ -839,7 +849,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
849 await act(() => {
850 ReactDOM.hydrate(React.createElement(App, null), container);
851 });
842 - assertConsoleErrorDev(['Text content did not match']);
852 + assertConsoleErrorDev([
853 + 'Warning: Text content did not match. Server: "server" Client: "client"\n' +
854 + ' in Text (at **)\n' +
855 + ' in div (at **)\n' +
856 + ' in App (at **)',
857 + ]);
858 assertLog(['client', 'Passive effect: client']);
859 }
860 expect(container.textContent).toEqual('client');