Skip to content

Commit

Permalink
Add back warning with component stack on Hydration mismatch (#23241)
Browse files Browse the repository at this point in the history
* add back warning

* wrapper errorMock in __DEV__ flag

* lint
  • Loading branch information
salazarm committed Feb 7, 2022
1 parent cd4eb11 commit 1dece52
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 125 deletions.
18 changes: 14 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1740,8 +1740,13 @@ describe('ReactDOMFizzServer', () => {
'The server HTML was replaced with client content',
]);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
[
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
' in div (at **)\n' +
' in App (at **)',
],
{withoutStack: 1},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
} else {
Expand Down Expand Up @@ -1833,8 +1838,13 @@ describe('ReactDOMFizzServer', () => {
'The server HTML was replaced with client content',
]);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
[
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
' in div (at **)\n' +
' in App (at **)',
],
{withoutStack: 1},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ let SuspenseList;
let act;
let IdleEventPriority;

function normalizeCodeLocInfo(strOrErr) {
if (strOrErr && strOrErr.replace) {
return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
});
}
return strOrErr;
}

function dispatchMouseEvent(to, from) {
if (!to) {
to = null;
Expand Down Expand Up @@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {

// @gate enableClientRenderFallbackOnHydrationMismatch
it('falls back to client rendering boundary on mismatch', async () => {
// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
let client = false;
let suspend = false;
let resolve;
Expand Down Expand Up @@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
</Suspense>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(Scheduler).toHaveYielded([
'Hello',
'Component',
'Component',
'Component',
'Component',
]);
try {
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(Scheduler).toHaveYielded([
'Hello',
'Component',
'Component',
'Component',
'Component',
]);

expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);

suspend = true;
client = true;
suspend = true;
client = true;

ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
expect(Scheduler).toFlushAndYield([
'Suspend',
'Component',
'Component',
'Component',
'Component',
]);
jest.runAllTimers();
ReactDOM.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
expect(Scheduler).toFlushAndYield([
'Suspend',
'Component',
'Component',
'Component',
'Component',
]);
jest.runAllTimers();

// Unchanged
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);
// Unchanged
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);

suspend = false;
resolve();
await promise;
suspend = false;
resolve();
await promise;
expect(Scheduler).toFlushAndYield([
// first pass, mismatches at end
'Hello',
'Component',
'Component',
'Component',
'Component',

// second pass as client render
'Hello',
'Component',
'Component',
'Component',
'Component',

// Hydration mismatch is logged
'An error occurred during hydration. The server HTML was replaced with client content',
]);

expect(Scheduler).toFlushAndYield([
// first pass, mismatches at end
'Hello',
'Component',
'Component',
'Component',
'Component',

// second pass as client render
'Hello',
'Component',
'Component',
'Component',
'Component',

// Hydration mismatch is logged
'An error occurred during hydration. The server HTML was replaced with client content',
]);
// Client rendered - suspense comment nodes removed
expect(container.innerHTML).toBe(
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
);

// Client rendered - suspense comment nodes removed
expect(container.innerHTML).toBe(
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
);
if (__DEV__) {
expect(mockError.mock.calls[0]).toEqual([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
'div',
'div',
'\n' +
' in div (at **)\n' +
' in Component (at **)\n' +
' in Suspense (at **)\n' +
' in App (at **)',
]);
}
} finally {
console.error = originalConsoleError;
}
});

it('calls the hydration callbacks after hydration or deletion', async () => {
Expand Down Expand Up @@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
});

it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
spyOnDev(console, 'error');
const ref = React.createRef();
function App({hasB}) {
return (
<div>
<Suspense fallback="Loading...">
<Suspender />
<span ref={ref}>A</span>
{hasB ? <span>B</span> : null}
</Suspense>
<div>Sibling</div>
</div>
);
}
// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};

const ref = React.createRef();
let shouldSuspend = false;
let resolve;
const promise = new Promise(res => {
Expand All @@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
}
return <></>;
}
function App({hasB}) {
return (
<div>
<Suspense fallback="Loading...">
<Suspender />
<span ref={ref}>A</span>
{hasB ? <span>B</span> : null}
</Suspense>
<div>Sibling</div>
</div>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);

const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);

const container = document.createElement('div');
container.innerHTML = finalHTML;
const container = document.createElement('div');
container.innerHTML = finalHTML;

const span = container.getElementsByTagName('span')[0];
const span = container.getElementsByTagName('span')[0];

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);

shouldSuspend = true;
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
});
shouldSuspend = true;
act(() => {
ReactDOM.hydrateRoot(container, <App hasB={false} />);
});

// await expect(async () => {
resolve();
await promise;
Scheduler.unstable_flushAll();
await null;
jest.runAllTimers();
// }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
resolve();
await promise;
Scheduler.unstable_flushAll();
await null;
jest.runAllTimers();

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(ref.current).not.toBe(span);
} else {
expect(ref.current).toBe(span);
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(ref.current).not.toBe(span);
} else {
expect(ref.current).toBe(span);
}
if (__DEV__) {
expect(mockError).toHaveBeenCalledWith(
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
'span',
'div',
'\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
' in App (at **)',
);
}
} finally {
console.error = originalConsoleError;
}
});

Expand Down Expand Up @@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
});
});
}).toErrorDev(
'Warning: An error occurred during hydration. ' +
'The server HTML was replaced with client content in <div>.',
{withoutStack: true},
[
'Warning: An error occurred during hydration. ' +
'The server HTML was replaced with client content in <div>.',
'Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
' in span (at **)\n' +
' in App (at **)',
],
{withoutStack: 1},
);
expect(Scheduler).toHaveYielded([
'Log recoverable error: An error occurred during hydration. The server ' +
Expand Down
Loading

0 comments on commit 1dece52

Please sign in to comment.