[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.
This commit is contained in:
Ricky
2026-01-13 15:52:53 -05:00
committed by GitHub
parent c18662405c
commit 3e1abcc8d7
16 changed files with 690 additions and 320 deletions

View File

@@ -278,6 +278,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node --version
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
@@ -445,6 +446,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
@@ -489,6 +491,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:

View File

@@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => {
if (__DEV__) {
console.warn('Hello\n in div');
}
assertConsoleWarnDev(['Hello']);
assertConsoleWarnDev(['Hello\n in div']);
});
it('passes if all warnings contain a stack', () => {
@@ -888,7 +888,11 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Good day\n in div');
console.warn('Bye\n in div');
}
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
});
it('fails if act is called without assertConsoleWarnDev', async () => {
@@ -1075,7 +1079,11 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi \n in div');
console.warn('Wow \n in div');
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
assertConsoleWarnDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
@@ -1085,9 +1093,9 @@ describe('ReactInternalTestUtils console assertions', () => {
- Expected warnings
+ Received warnings
- Hi
- Wow
- Bye
- Hi in div
- Wow in div
- Bye in div
+ Hi in div (at **)
+ Wow in div (at **)"
`);
@@ -1188,16 +1196,26 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello');
console.warn('Good day\n in div');
console.warn('Bye\n in div');
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Missing component stack for:
"Hello"
Unexpected warning(s) recorded.
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
- Expected warnings
+ Received warnings
- Hello in div
- Good day in div
- Bye in div
+ Hello
+ Good day in div (at **)
+ Bye in div (at **)"
`);
});
@@ -1207,16 +1225,26 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello\n in div');
console.warn('Good day');
console.warn('Bye\n in div');
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Missing component stack for:
"Good day"
Unexpected warning(s) recorded.
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
- Expected warnings
+ Received warnings
- Hello in div
- Good day in div
- Bye in div
+ Hello in div (at **)
+ Good day
+ Bye in div (at **)"
`);
});
@@ -1226,41 +1254,26 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Hello\n in div');
console.warn('Good day\n in div');
console.warn('Bye');
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
assertConsoleWarnDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Missing component stack for:
"Bye"
Unexpected warning(s) recorded.
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
`);
});
- Expected warnings
+ Received warnings
// @gate __DEV__
it('fails if all warnings do not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.warn('Hello');
console.warn('Good day');
console.warn('Bye');
assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Missing component stack for:
"Hello"
Missing component stack for:
"Good day"
Missing component stack for:
"Bye"
If this warning should omit a component stack, pass [log, {withoutStack: true}].
If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
- Hello in div
- Good day in div
- Bye in div
+ Hello in div (at **)
+ Good day in div (at **)
+ Bye"
`);
});
@@ -1339,12 +1352,13 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected warning(s) recorded.
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
- Expected warnings
+ Received warnings
- Hello
+ Hello in div (at **)"
`);
});
@@ -1361,16 +1375,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected warning(s) recorded.
Unexpected component stack for:
"Bye
in div (at **)"
- Expected warnings
+ Received warnings
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
`);
});
});
@@ -1382,9 +1396,9 @@ describe('ReactInternalTestUtils console assertions', () => {
console.warn('Bye\n in div');
}
assertConsoleWarnDev([
'Hello',
'Hello\n in div',
['Good day', {withoutStack: true}],
'Bye',
'Bye\n in div',
]);
});
@@ -1490,12 +1504,13 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected warning(s) recorded.
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
- Expected warnings
+ Received warnings
- Hello
+ Hello in div (at **)"
`);
});
@@ -1524,16 +1539,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected warning(s) recorded.
Unexpected component stack for:
"Bye
in div (at **)"
- Expected warnings
+ Received warnings
If this warning should include a component stack, remove {withoutStack: true} from this warning.
If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
`);
});
});
@@ -1606,13 +1621,18 @@ describe('ReactInternalTestUtils console assertions', () => {
it('fails if component stack is passed twice', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi %s%s', '\n in div', '\n in div');
assertConsoleWarnDev(['Hi']);
assertConsoleWarnDev(['Hi \n in div (at **)']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Received more than one component stack for a warning:
"Hi %s%s""
Unexpected warning(s) recorded.
- Expected warnings
+ Received warnings
Hi in div (at **)
+ in div (at **)"
`);
});
@@ -1621,16 +1641,23 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.warn('Hi %s%s', '\n in div', '\n in div');
console.warn('Bye %s%s', '\n in div', '\n in div');
assertConsoleWarnDev(['Hi', 'Bye']);
assertConsoleWarnDev([
'Hi \n in div (at **)',
'Bye \n in div (at **)',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleWarnDev(expected)
Received more than one component stack for a warning:
"Hi %s%s"
Unexpected warning(s) recorded.
Received more than one component stack for a warning:
"Bye %s%s""
- Expected warnings
+ Received warnings
Hi in div (at **)
+ in div (at **)
Bye in div (at **)
+ in div (at **)"
`);
});
@@ -1646,7 +1673,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev(['Hi', 'Bye']);
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
});
// @gate __DEV__
@@ -1661,7 +1688,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev(['Hi', 'Bye']);
assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']);
});
// @gate __DEV__
@@ -1677,7 +1704,11 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
assertConsoleWarnDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
});
it('should fail if waitFor is called before asserting', async () => {
@@ -1884,7 +1915,7 @@ describe('ReactInternalTestUtils console assertions', () => {
if (__DEV__) {
console.error('Hello\n in div');
}
assertConsoleErrorDev(['Hello']);
assertConsoleErrorDev(['Hello\n in div']);
});
it('passes if all errors contain a stack', () => {
@@ -1893,7 +1924,11 @@ describe('ReactInternalTestUtils console assertions', () => {
console.error('Good day\n in div');
console.error('Bye\n in div');
}
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
assertConsoleErrorDev([
'Hello\n in div',
'Good day\n in div',
'Bye\n in div',
]);
});
it('fails if act is called without assertConsoleErrorDev', async () => {
@@ -2080,7 +2115,11 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi \n in div');
console.error('Wow \n in div');
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
assertConsoleErrorDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
@@ -2090,9 +2129,9 @@ describe('ReactInternalTestUtils console assertions', () => {
- Expected errors
+ Received errors
- Hi
- Wow
- Bye
- Hi in div
- Wow in div
- Bye in div
+ Hi in div (at **)
+ Wow in div (at **)"
`);
@@ -2192,101 +2231,6 @@ describe('ReactInternalTestUtils console assertions', () => {
+ TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)"
`);
});
// @gate __DEV__
it('fails if only error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
assertConsoleErrorDev(['Hello']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if first error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello\n in div');
console.error('Good day\n in div');
console.error('Bye');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Bye"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if last error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
console.error('Good day\n in div');
console.error('Bye\n in div');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if middle error does not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello\n in div');
console.error('Good day');
console.error('Bye\n in div');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Good day"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('fails if all errors do not contain a stack', () => {
const message = expectToThrowFailure(() => {
console.error('Hello');
console.error('Good day');
console.error('Bye');
assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing component stack for:
"Hello"
Missing component stack for:
"Good day"
Missing component stack for:
"Bye"
If this error should omit a component stack, pass [log, {withoutStack: true}].
If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
`);
});
// @gate __DEV__
it('regression: checks entire string, not just the first letter', async () => {
@@ -2385,12 +2329,13 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected error(s) recorded.
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
- Expected errors
+ Received errors
- Hello
+ Hello in div (at **)"
`);
});
@@ -2407,16 +2352,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected error(s) recorded.
Unexpected component stack for:
"Bye
in div (at **)"
- Expected errors
+ Received errors
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
`);
});
});
@@ -2428,9 +2373,9 @@ describe('ReactInternalTestUtils console assertions', () => {
console.error('Bye\n in div');
}
assertConsoleErrorDev([
'Hello',
'Hello\n in div',
['Good day', {withoutStack: true}],
'Bye',
'Bye\n in div',
]);
});
@@ -2536,12 +2481,13 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected error(s) recorded.
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
- Expected errors
+ Received errors
- Hello
+ Hello in div (at **)"
`);
});
@@ -2570,16 +2516,16 @@ describe('ReactInternalTestUtils console assertions', () => {
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Unexpected component stack for:
"Hello
in div (at **)"
Unexpected error(s) recorded.
Unexpected component stack for:
"Bye
in div (at **)"
- Expected errors
+ Received errors
If this error should include a component stack, remove {withoutStack: true} from this error.
If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
- Hello
+ Hello in div (at **)
Good day
- Bye
+ Bye in div (at **)"
`);
});
@@ -2678,13 +2624,18 @@ describe('ReactInternalTestUtils console assertions', () => {
it('fails if component stack is passed twice', () => {
const message = expectToThrowFailure(() => {
console.error('Hi %s%s', '\n in div', '\n in div');
assertConsoleErrorDev(['Hi']);
assertConsoleErrorDev(['Hi \n in div (at **)']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Received more than one component stack for a warning:
"Hi %s%s""
Unexpected error(s) recorded.
- Expected errors
+ Received errors
Hi in div (at **)
+ in div (at **)"
`);
});
@@ -2693,16 +2644,23 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi %s%s', '\n in div', '\n in div');
console.error('Bye %s%s', '\n in div', '\n in div');
assertConsoleErrorDev(['Hi', 'Bye']);
assertConsoleErrorDev([
'Hi \n in div (at **)',
'Bye \n in div (at **)',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Received more than one component stack for a warning:
"Hi %s%s"
Unexpected error(s) recorded.
Received more than one component stack for a warning:
"Bye %s%s""
- Expected errors
+ Received errors
Hi in div (at **)
+ in div (at **)
Bye in div (at **)
+ in div (at **)"
`);
});
@@ -2711,14 +2669,14 @@ describe('ReactInternalTestUtils console assertions', () => {
const message = expectToThrowFailure(() => {
console.error('Hi \n in div');
console.error('Bye \n in div');
assertConsoleErrorDev('Hi', 'Bye');
assertConsoleErrorDev('Hi \n in div', 'Bye \n in div');
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev(['Hi', 'Bye']);
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
});
// @gate __DEV__
@@ -2733,7 +2691,7 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev(['Hi', 'Bye']);
assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']);
});
// @gate __DEV__
@@ -2749,7 +2707,133 @@ describe('ReactInternalTestUtils console assertions', () => {
Expected messages should be an array of strings but was given type "string"."
`);
assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
assertConsoleErrorDev([
'Hi \n in div',
'Wow \n in div',
'Bye \n in div',
]);
});
describe('in <stack> placeholder', () => {
// @gate __DEV__
it('fails if `in <stack>` is used for a component stack instead of an error stack', () => {
const message = expectToThrowFailure(() => {
console.error('Warning message\n in div');
assertConsoleErrorDev(['Warning message\n in <stack>']);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "Warning message
in <stack>"
Received: "Warning message
in div (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
`);
});
// @gate __DEV__
it('fails if `in <stack>` is used for multiple component stacks', () => {
const message = expectToThrowFailure(() => {
console.error('First warning\n in span');
console.error('Second warning\n in div');
assertConsoleErrorDev([
'First warning\n in <stack>',
'Second warning\n in <stack>',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "First warning
in <stack>"
Received: "First warning
in span (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)").
Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks.
Expected: "Second warning
in <stack>"
Received: "Second warning
in div (at **)"
If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")."
`);
});
it('allows `in <stack>` for actual error stack traces', () => {
// This should pass - \n in <stack> is correctly used for an error stack
console.error(new Error('Something went wrong'));
assertConsoleErrorDev(['Error: Something went wrong\n in <stack>']);
});
// @gate __DEV__
it('fails if error stack trace is present but \\n in <stack> is not expected', () => {
const message = expectToThrowFailure(() => {
console.error(new Error('Something went wrong'));
assertConsoleErrorDev(['Error: Something went wrong']);
});
expect(message).toMatch(`Unexpected error stack trace for:`);
expect(message).toMatch(`Error: Something went wrong`);
expect(message).toMatch(
'If this error should include an error stack trace, add \\n in <stack> to your expected message'
);
});
// @gate __DEV__
it('fails if `in <stack>` is expected but no stack is present', () => {
const message = expectToThrowFailure(() => {
console.error('Error: Something went wrong');
assertConsoleErrorDev([
'Error: Something went wrong\n in <stack>',
]);
});
expect(message).toMatchInlineSnapshot(`
"assertConsoleErrorDev(expected)
Missing error stack trace for:
"Error: Something went wrong"
The expected message uses \\n in <stack> but the actual error doesn't include an error stack trace.
If this error should not have an error stack trace, remove \\n in <stack> from your expected message."
`);
});
});
describe('[Environment] placeholder', () => {
// @gate __DEV__
it('expands [Server] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Server \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Server] Error: something went wrong', {withoutStack: true}],
]);
});
// @gate __DEV__
it('expands [Prerender] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Prerender] Error: something went wrong', {withoutStack: true}],
]);
});
// @gate __DEV__
it('expands [Cache] to ANSI escape sequence for server badge', () => {
const badge = '\u001b[0m\u001b[7m Cache \u001b[0m';
console.error(badge + 'Error: something went wrong');
assertConsoleErrorDev([
['[Cache] Error: something went wrong', {withoutStack: true}],
]);
});
});
it('should fail if waitFor is called before asserting', async () => {

View File

@@ -168,6 +168,53 @@ function normalizeCodeLocInfo(str) {
});
}
// Expands environment placeholders like [Server] into ANSI escape sequences.
// This allows test assertions to use a cleaner syntax like "[Server] Error:"
// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"
function expandEnvironmentPlaceholders(str) {
if (typeof str !== 'string') {
return str;
}
// [Environment] -> ANSI escape sequence for environment badge
// The format is: reset + inverse + " Environment " + reset
return str.replace(
/^\[(\w+)] /g,
(match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',
);
}
// The error stack placeholder that can be used in expected messages
const ERROR_STACK_PLACEHOLDER = '\n in <stack>';
// A marker used to protect the placeholder during normalization
const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';
// Normalizes expected messages, handling special placeholders
function normalizeExpectedMessage(str) {
if (typeof str !== 'string') {
return str;
}
// Protect the error stack placeholder from normalization
// (normalizeCodeLocInfo would add "(at **)" to it)
const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);
let result = str;
if (hasStackPlaceholder) {
result = result.replace(
ERROR_STACK_PLACEHOLDER,
ERROR_STACK_PLACEHOLDER_MARKER,
);
}
result = normalizeCodeLocInfo(result);
result = expandEnvironmentPlaceholders(result);
if (hasStackPlaceholder) {
// Restore the placeholder (remove the "(at **)" that was added)
result = result.replace(
ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',
ERROR_STACK_PLACEHOLDER,
);
}
return result;
}
function normalizeComponentStack(entry) {
if (
typeof entry[0] === 'string' &&
@@ -187,6 +234,15 @@ const isLikelyAComponentStack = message =>
message.includes('\n in ') ||
message.includes('\n at '));
// Error stack traces start with "*Error:" and contain "at" frames with file paths
// Component stacks contain "in ComponentName" patterns
// This helps validate that \n in <stack> is used correctly
const isLikelyAnErrorStackTrace = message =>
typeof message === 'string' &&
message.includes('Error:') &&
// Has "at" frames typical of error stacks (with file:line:col)
/\n\s+at .+\(.*:\d+:\d+\)/.test(message);
export function createLogAssertion(
consoleMethod,
matcherName,
@@ -236,13 +292,11 @@ export function createLogAssertion(
const withoutStack = options.withoutStack;
// Warn about invalid global withoutStack values.
if (consoleMethod === 'log' && withoutStack !== undefined) {
throwFormattedError(
`Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
);
} else if (withoutStack !== undefined && withoutStack !== true) {
// withoutStack can only have a value true.
throwFormattedError(
`The second argument must be {withoutStack: true}.` +
`\n\nInstead received ${JSON.stringify(options)}.`,
@@ -256,8 +310,11 @@ export function createLogAssertion(
const unexpectedLogs = [];
const unexpectedMissingComponentStack = [];
const unexpectedIncludingComponentStack = [];
const unexpectedMissingErrorStack = [];
const unexpectedIncludingErrorStack = [];
const logsMismatchingFormat = [];
const logsWithExtraComponentStack = [];
const stackTracePlaceholderMisuses = [];
// Loop over all the observed logs to determine:
// - Which expected logs are missing
@@ -319,11 +376,11 @@ export function createLogAssertion(
);
}
expectedMessage = normalizeCodeLocInfo(currentExpectedMessage);
expectedMessage = normalizeExpectedMessage(currentExpectedMessage);
expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
} else if (typeof expectedMessageOrArray === 'string') {
// Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray);
expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);
// withoutStack: inherit from global option - simplify when withoutStack is removed.
if (consoleMethod === 'log') {
expectedWithoutStack = true;
} else {
@@ -381,19 +438,93 @@ export function createLogAssertion(
}
// Main logic to check if log is expected, with the component stack.
if (
typeof expectedMessage === 'string' &&
(normalizedMessage === expectedMessage ||
normalizedMessage.includes(expectedMessage))
) {
// Check for exact match OR if the message matches with a component stack appended
let matchesExpectedMessage = false;
let expectsErrorStack = false;
const hasErrorStack = isLikelyAnErrorStackTrace(message);
if (typeof expectedMessage === 'string') {
if (normalizedMessage === expectedMessage) {
matchesExpectedMessage = true;
} else if (expectedMessage.includes('\n in <stack>')) {
expectsErrorStack = true;
// \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")
// NOT for React component stacks (e.g., "\n in ComponentName (at **)").
// Validate that the actual message looks like an error stack trace.
if (!hasErrorStack) {
// The actual message doesn't look like an error stack trace.
// This is likely a misuse - someone used \n in <stack> for a component stack.
stackTracePlaceholderMisuses.push({
expected: expectedMessage,
received: normalizedMessage,
});
}
const expectedMessageWithoutStack = expectedMessage.replace(
'\n in <stack>',
'',
);
if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {
// Remove the stack trace
const remainder = normalizedMessage.slice(
expectedMessageWithoutStack.length,
);
// After normalization, both error stacks and component stacks look like
// component stacks (at frames are converted to "in ... (at **)" format).
// So we check isLikelyAComponentStack for matching purposes.
if (isLikelyAComponentStack(remainder)) {
const messageWithoutStack = normalizedMessage.replace(
remainder,
'',
);
if (messageWithoutStack === expectedMessageWithoutStack) {
matchesExpectedMessage = true;
}
} else if (remainder === '') {
// \n in <stack> was expected but there's no stack at all
matchesExpectedMessage = true;
}
} else if (normalizedMessage === expectedMessageWithoutStack) {
// \n in <stack> was expected but actual has no stack at all (exact match without stack)
matchesExpectedMessage = true;
}
} else if (
hasErrorStack &&
!expectedMessage.includes('\n in <stack>') &&
normalizedMessage.startsWith(expectedMessage)
) {
matchesExpectedMessage = true;
}
}
if (matchesExpectedMessage) {
// withoutStack: Check for unexpected/missing component stacks.
// These checks can be simplified when withoutStack is removed.
if (isLikelyAComponentStack(normalizedMessage)) {
if (expectedWithoutStack === true) {
if (expectedWithoutStack === true && !hasErrorStack) {
// Only report unexpected component stack if it's not an error stack
// (error stacks look like component stacks after normalization)
unexpectedIncludingComponentStack.push(normalizedMessage);
}
} else if (expectedWithoutStack !== true) {
} else if (expectedWithoutStack !== true && !expectsErrorStack) {
unexpectedMissingComponentStack.push(normalizedMessage);
}
// Check for unexpected/missing error stacks
if (hasErrorStack && !expectsErrorStack) {
// Error stack is present but \n in <stack> was not in the expected message
unexpectedIncludingErrorStack.push(normalizedMessage);
} else if (
expectsErrorStack &&
!hasErrorStack &&
!isLikelyAComponentStack(normalizedMessage)
) {
// \n in <stack> was expected but the actual message doesn't have any stack at all
// (if it has a component stack, stackTracePlaceholderMisuses already handles it)
unexpectedMissingErrorStack.push(normalizedMessage);
}
// Found expected log, remove it from missing.
missingExpectedLogs.splice(0, 1);
} else {
@@ -422,6 +553,21 @@ export function createLogAssertion(
)}`;
}
// Wrong %s formatting is a failure.
// This is a common mistake when creating new warnings.
if (logsMismatchingFormat.length > 0) {
throwFormattedError(
logsMismatchingFormat
.map(
item =>
`Received ${item.args.length} arguments for a message with ${
item.expectedArgCount
} placeholders:\n ${printReceived(item.format)}`,
)
.join('\n\n'),
);
}
// Any unexpected warnings should be treated as a failure.
if (unexpectedLogs.length > 0) {
throwFormattedError(
@@ -466,18 +612,33 @@ export function createLogAssertion(
);
}
// Wrong %s formatting is a failure.
// This is a common mistake when creating new warnings.
if (logsMismatchingFormat.length > 0) {
// Any logs that include an error stack trace but \n in <stack> wasn't expected.
if (unexpectedIncludingErrorStack.length > 0) {
throwFormattedError(
logsMismatchingFormat
`${unexpectedIncludingErrorStack
.map(
item =>
`Received ${item.args.length} arguments for a message with ${
item.expectedArgCount
} placeholders:\n ${printReceived(item.format)}`,
stack =>
`Unexpected error stack trace for:\n ${printReceived(stack)}`,
)
.join('\n\n'),
.join(
'\n\n',
)}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +
`(e.g., "Error: message\\n in <stack>").`,
);
}
// Any logs that are missing an error stack trace when \n in <stack> was expected.
if (unexpectedMissingErrorStack.length > 0) {
throwFormattedError(
`${unexpectedMissingErrorStack
.map(
stack =>
`Missing error stack trace for:\n ${printReceived(stack)}`,
)
.join(
'\n\n',
)}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +
`\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,
);
}
@@ -496,6 +657,25 @@ export function createLogAssertion(
.join('\n\n'),
);
}
// Using \n in <stack> for component stacks is a misuse.
// \n in <stack> should only be used for JavaScript Error stack traces,
// not for React component stacks.
if (stackTracePlaceholderMisuses.length > 0) {
throwFormattedError(
`${stackTracePlaceholderMisuses
.map(
item =>
`Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +
`stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +
`Expected: ${printReceived(item.expected)}\n` +
`Received: ${printReceived(item.received)}\n\n` +
`If this ${logName()} has a component stack, include the full component stack in your expected message ` +
`(e.g., "Warning message\\n in ComponentName (at **)").`,
)
.join('\n\n')}`,
);
}
}
};
}

View File

@@ -1729,7 +1729,8 @@ describe('ReactFlight', () => {
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like Symbol.iterator are not supported.\n' +
' <... value={{}}>\n' +
' ^^^^\n',
' ^^^^\n' +
' in (at **)',
]);
});
@@ -3258,7 +3259,7 @@ describe('ReactFlight', () => {
const transport = ReactNoopFlightServer.render({
root: ReactServer.createElement(App),
});
assertConsoleErrorDev(['Error: err']);
assertConsoleErrorDev(['Error: err' + '\n in <stack>']);
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
expect(mockConsoleLog.mock.calls[0][0]).toBe('hi');

View File

@@ -734,7 +734,11 @@ describe('ReactHooksInspection', () => {
});
const results = normalizeSourceLoc(tree);
expect(results).toHaveLength(1);
expect(results[0]).toMatchInlineSnapshot(`
expect(results[0]).toMatchInlineSnapshot(
{
subHooks: [{value: expect.any(Promise)}],
},
`
{
"debugInfo": null,
"hookSource": {
@@ -759,12 +763,13 @@ describe('ReactHooksInspection', () => {
"isStateEditable": false,
"name": "Use",
"subHooks": [],
"value": Promise {},
"value": Any<Promise>,
},
],
"value": undefined,
}
`);
`,
);
});
describe('useDebugValue', () => {

View File

@@ -548,16 +548,23 @@ describe('ReactDOM', () => {
' in App (at **)',
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in blink (at **)',
' in blink (at **)\n' +
' in App2 (at **)\n' +
' in Child (at **)\n' +
' in ServerEntry (at **)',
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span)
'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in span (at **)\n' +
' in Child2 (at **)\n' +
' in App2 (at **)',
' in App2 (at **)\n' +
' in Child (at **)\n' +
' in ServerEntry (at **)',
// ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span)
'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in span (at **)\n' +
' in Child (at **)',
' in Child (at **)\n' +
' in ServerEntry (at **)',
// ReactDOM(App > div > font)
'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
' in font (at **)\n' +
@@ -775,7 +782,11 @@ describe('ReactDOM', () => {
// @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
// root of the application
assertConsoleErrorDev(['In HTML, <head> cannot be a child of <main>']);
assertConsoleErrorDev([
'In HTML, <head> cannot be a child of <main>.\nThis will cause a hydration error.\n' +
' in head (at **)\n' +
' in App (at **)',
]);
await act(() => {
root.render(<App phase={1} />);

View File

@@ -6879,9 +6879,12 @@ describe('ReactDOMFizzServer', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(finished).toBe(true);
@@ -6943,9 +6946,12 @@ describe('ReactDOMFizzServer', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(finished).toBe(true);
@@ -7007,9 +7013,12 @@ describe('ReactDOMFizzServer', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(finished).toBe(true);
@@ -7069,9 +7078,12 @@ describe('ReactDOMFizzServer', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(finished).toBe(true);
@@ -9024,7 +9036,8 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});
assertConsoleErrorDev([
'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.',
'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.' +
'\n in style (at **)',
]);
expect(getVisibleChildren(document)).toEqual(
<html>
@@ -9054,7 +9067,8 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});
assertConsoleErrorDev([
'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.',
'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.' +
'\n in style (at **)',
]);
expect(getVisibleChildren(document)).toEqual(
<html>
@@ -9085,7 +9099,8 @@ describe('ReactDOMFizzServer', () => {
pipe(writable);
});
assertConsoleErrorDev([
'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.',
'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.' +
'\n in style (at **)',
]);
expect(getVisibleChildren(document)).toEqual(
<html>

View File

@@ -3628,7 +3628,24 @@ body {
assertLog(['load stylesheet: foo']);
await waitForAll([]);
assertConsoleErrorDev([
"Hydration failed because the server rendered HTML didn't match the client.",
"Error: Hydration failed because the server rendered HTML didn't match the client. " +
'As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:\n\n' +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
'- External changing data without sending a snapshot of it along with the HTML.\n' +
'- Invalid HTML tag nesting.\n\n' +
'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
'https://react.dev/link/hydration-mismatch\n\n' +
' <html>\n' +
' <body>\n' +
' <div>\n' +
' <div>\n' +
' <Suspense fallback="loading 2...">\n' +
' <Component>\n' +
' <link>\n' +
'+ <div>' +
'\n in <stack>',
]);
jest.runAllTimers();

View File

@@ -120,11 +120,14 @@ describe('ReactDOMSrcObject', () => {
assertConsoleErrorDev([
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
'Pass it directly to <img src>, <video src> or <audio src> instead.' +
'\n in source (at **)',
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
'Pass it directly to <img src>, <video src> or <audio src> instead.' +
'\n in source (at **)',
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
'Pass it directly to <img src>, <video src> or <audio src> instead.' +
'\n in source (at **)',
]);
expect(videoRef.current.firstChild.src).not.toMatch(/^blob:/);
expect(videoRef.current.firstChild.src).toContain('[object%20Blob]'); // toString:ed

View File

@@ -110,7 +110,8 @@ describe('ReactFlushSync', () => {
assertConsoleErrorDev([
'flushSync was called from inside a lifecycle method. React ' +
'cannot flush when React is already rendering. Consider moving this ' +
'call to a scheduler task or micro task.',
'call to a scheduler task or micro task.' +
'\n in App',
]);
await waitForPaint([]);

View File

@@ -2145,7 +2145,8 @@ describe('ReactFlightDOM', () => {
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2169,9 +2170,12 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2235,7 +2239,8 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2260,9 +2265,12 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2327,7 +2335,8 @@ describe('ReactFlightDOM', () => {
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2351,9 +2360,12 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2416,7 +2428,8 @@ describe('ReactFlightDOM', () => {
pipe(flightWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2440,9 +2453,12 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2504,7 +2520,8 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2528,9 +2545,12 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2596,7 +2616,8 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2620,8 +2641,10 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2668,7 +2691,8 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
const response =
@@ -2692,7 +2716,8 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
]);
expect(shellErrors).toEqual([]);
@@ -2760,8 +2785,9 @@ describe('ReactFlightDOM', () => {
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'bam!',
'Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'Error: bam!\n in <stack>',
]);
const response =
@@ -2785,8 +2811,9 @@ describe('ReactFlightDOM', () => {
).pipe(fizzWritable);
});
assertConsoleErrorDev([
'The render was aborted by the server without a reason.',
'bam!',
'[Server] Error: The render was aborted by the server without a reason.' +
'\n in <stack>',
'[Server] Error: bam!\n in <stack>',
]);
expect(shellErrors).toEqual([]);

View File

@@ -957,7 +957,8 @@ describe('ReactFlightDOMForm', () => {
'Failed to serialize an action for progressive enhancement:\n' +
'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' +
' [<div/>]\n' +
' ^^^^^^',
' ^^^^^^' +
'\n in <stack>',
]);
// The error message was returned as JSX.
@@ -1032,7 +1033,8 @@ describe('ReactFlightDOMForm', () => {
await submitTheForm();
assertConsoleErrorDev([
'Failed to serialize an action for progressive enhancement:\n' +
'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.',
'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.' +
'\n in <stack>',
]);
expect(blob instanceof Blob).toBe(true);

View File

@@ -196,7 +196,8 @@ describe('ReactFlight', () => {
"The props of this element may help locate this element: { children: 'Free!', [key]: [Getter] }",
{withoutStack: true},
],
"TypeError: Cannot read properties of undefined (reading 'stack')",
"TypeError: Cannot read properties of undefined (reading 'stack')" +
'\n in <stack>',
]);
});
});

View File

@@ -864,6 +864,7 @@ describe('ReactChildren', () => {
});
it('warns for mapped list children without keys', async () => {
spyOnDev(console, 'error').mockImplementation(() => {});
function ComponentRenderingMappedChildren({children}) {
return (
<div>
@@ -883,13 +884,14 @@ describe('ReactChildren', () => {
</ComponentRenderingMappedChildren>,
);
});
assertConsoleErrorDev([
'Each child in a list should have a unique "key" prop.\n\n' +
'Check the render method of `ComponentRenderingMappedChildren`.' +
' See https://react.dev/link/warning-keys for more information.\n' +
' in div (at **)\n' +
' in **/ReactChildren-test.js:**:** (at **)',
]);
if (__DEV__) {
const calls = console.error.mock.calls;
console.error.mockRestore();
expect(calls.length).toBe(1);
expect(calls[0][0]).toEqual(
'Each child in a list should have a unique "key" prop.%s%s See https://react.dev/link/warning-keys for more information.',
);
}
});
it('does not warn for mapped static children without keys', async () => {

View File

@@ -349,10 +349,13 @@ describe('create-react-class-integration', () => {
});
assertConsoleErrorDev([
[
'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)',
{withoutStack: true},
],
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Component uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)' +
'\n in ReactClassComponent (at **)',
]);
expect(container.firstChild.className).toBe('foo');
});

View File

@@ -641,11 +641,21 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
'The result of getSnapshot should be cached to avoid an infinite loop',
{withoutStack: true},
],
'Error: Maximum update depth exceeded',
'The above error occurred i',
[
'Error: Maximum update depth exceeded. ' +
'This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ' +
'React limits the number of nested updates to prevent infinite loops.' +
'\n in <stack>',
{withoutStack: true},
],
'The above error occurred in the <App> component:\n\n' +
' in App (at **)\n\n' +
'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
'Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.',
]
: [
'The result of getSnapshot should be cached to avoid an infinite loop',
'The result of getSnapshot should be cached to avoid an infinite loop' +
'\n in App (at **)',
],
);
});
@@ -839,7 +849,12 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
await act(() => {
ReactDOM.hydrate(React.createElement(App, null), container);
});
assertConsoleErrorDev(['Text content did not match']);
assertConsoleErrorDev([
'Warning: Text content did not match. Server: "server" Client: "client"\n' +
' in Text (at **)\n' +
' in div (at **)\n' +
' in App (at **)',
]);
assertLog(['client', 'Passive effect: client']);
}
expect(container.textContent).toEqual('client');