Skip to content

Commit

Permalink
Test showing mismatches after suspending force fallbacks to be shown
Browse files Browse the repository at this point in the history
  • Loading branch information
rickhanlonii committed Feb 10, 2024
1 parent 374fd68 commit 9708487
Showing 1 changed file with 321 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,327 @@ describe('ReactDOMServerPartialHydration', () => {
}
});

it('does not show a fallback if mismatch is after suspending', 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;
const promise = new Promise(resolvePromise => {
resolve = () => {
suspend = false;
resolvePromise();
};
});
function Child() {
if (suspend) {
Scheduler.log('Suspend');
throw promise;
} else {
Scheduler.log('Hello');
return 'Hello';
}
}
function Component({shouldMismatch}) {
Scheduler.log('Component');
if (shouldMismatch && client) {
return <article>Mismatch</article>;
}
return <div>Component</div>;
}
function Fallback() {
Scheduler.log('Fallback');
return 'Loading...';
}
function App() {
return (
<Suspense fallback={<Fallback />}>
<Child />
<Component shouldMismatch={true} />
</Suspense>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Hello', 'Component']);

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

suspend = true;
client = true;

ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.log(error.message);
},
});
await waitForAll(['Suspend']);
jest.runAllTimers();

// !! Unchanged, continue showing server content while suspended.
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><!--/$-->',
);

suspend = false;
resolve();
await promise;
await waitForAll([
// first pass, mismatches at end
'Hello',
'Component',
'Hello',
'Component',
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
]);
jest.runAllTimers();

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

it('does show a fallback if mismatch is before suspending', 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;
const promise = new Promise(resolvePromise => {
resolve = () => {
suspend = false;
resolvePromise();
};
});
function Child() {
if (suspend) {
Scheduler.log('Suspend');
throw promise;
} else {
Scheduler.log('Hello');
return 'Hello';
}
}
function Component({shouldMismatch}) {
Scheduler.log('Component');
if (shouldMismatch && client) {
return <article>Mismatch</article>;
}
return <div>Component</div>;
}
function Fallback() {
Scheduler.log('Fallback');
return 'Loading...';
}
function App() {
return (
<Suspense fallback={<Fallback />}>
<Component shouldMismatch={true} />
<Child />
</Suspense>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Component', 'Hello']);

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

suspend = true;
client = true;

ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.log(error.message);
},
});
await waitForAll([
'Component',
'Component',
'Suspend',
'Fallback',
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
]);
jest.runAllTimers();

// !! Client switches to suspense fallback.
expect(container.innerHTML).toBe('Loading...');

suspend = false;
resolve();
await promise;
await waitForAll([
// first pass, mismatches at end
'Component',
'Hello',
]);
jest.runAllTimers();

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

it('does show a fallback if mismatch is before suspending in a child', 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;
const promise = new Promise(resolvePromise => {
resolve = () => {
suspend = false;
resolvePromise();
};
});
function Child() {
if (suspend) {
Scheduler.log('Suspend');
throw promise;
} else {
Scheduler.log('Hello');
return 'Hello';
}
}
function Component({shouldMismatch}) {
Scheduler.log('Component');
if (shouldMismatch && client) {
return <article>Mismatch</article>;
}
return <div>Component</div>;
}
function Fallback() {
Scheduler.log('Fallback');
return 'Loading...';
}
function App() {
return (
<Suspense fallback={<Fallback />}>
<Component shouldMismatch={true} />
<div>
<Child />
</div>
</Suspense>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Component', 'Hello']);

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

suspend = true;
client = true;

ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.log(error.message);
},
});
await waitForAll([
'Component',
'Component',
'Suspend',
'Fallback',
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
]);
jest.runAllTimers();

// !! Client switches to suspense fallback.
expect(container.innerHTML).toBe('Loading...');

suspend = false;
resolve();
await promise;
await waitForAll([
// first pass, mismatches at end
'Component',
'Hello',
]);
jest.runAllTimers();

// Client rendered - suspense comment nodes removed.
expect(container.innerHTML).toBe(
'<article>Mismatch</article><div>Hello</div>',
);
if (__DEV__) {
const secondToLastCall =
mockError.mock.calls[mockError.mock.calls.length - 2];
expect(secondToLastCall).toEqual([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
'article',
'section',
'\n' +
' in article (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 () => {
let suspend = false;
let resolve;
Expand Down

0 comments on commit 9708487

Please sign in to comment.