-
-
Notifications
You must be signed in to change notification settings - Fork 632
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c7a2731
commit e1fdfc7
Showing
11 changed files
with
524 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Override the fetch function to make it easier to test and for future use | ||
const customFetch = (...args: Parameters<typeof fetch>) => { | ||
const res = fetch(...args); | ||
return res; | ||
} | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export { customFetch as fetch }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* eslint-disable no-underscore-dangle */ | ||
/* eslint-disable import/first */ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
// Mock webpack require system for RSC | ||
window.__webpack_require__ = jest.fn(); | ||
window.__webpack_chunk_load__ = jest.fn(); | ||
|
||
import * as React from 'react'; | ||
import { enableFetchMocks } from 'jest-fetch-mock'; | ||
import { render, waitFor, screen } from '@testing-library/react'; | ||
import '@testing-library/jest-dom'; | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import { createNodeReadableStream } from './testUtils'; | ||
|
||
// const __filename = fileURLToPath(import.meta.url); | ||
// const __dirname = path.dirname(__filename); | ||
|
||
import RSCClientRoot, {resetRenderCache } from '../src/RSCClientRoot'; | ||
|
||
enableFetchMocks(); | ||
|
||
describe('RSCClientRoot', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
|
||
jest.resetModules(); | ||
resetRenderCache(); | ||
}); | ||
|
||
it('throws error when React.use is not defined', () => { | ||
jest.mock('react', () => ({ | ||
...jest.requireActual('react'), | ||
use: undefined | ||
})); | ||
|
||
expect(() => { | ||
// Re-import to trigger the check | ||
jest.requireActual('../src/RSCClientRoot'); | ||
}).toThrow('React.use is not defined'); | ||
}); | ||
|
||
const mockRSCRequest = (rscRenderingUrlPath = 'rsc-render') => { | ||
const chunksDirectory = path.join(__dirname, 'fixtures', 'rsc-payloads', 'simple-shell-with-async-component'); | ||
const chunk1 = JSON.parse(fs.readFileSync(path.join(chunksDirectory, 'chunk1.json'), 'utf8')); | ||
const chunk2 = JSON.parse(fs.readFileSync(path.join(chunksDirectory, 'chunk2.json'), 'utf8')); | ||
|
||
const { stream, push } = createNodeReadableStream(); | ||
window.fetchMock.mockResolvedValue(new Response(stream)); | ||
|
||
const props = { | ||
componentName: 'TestComponent', | ||
rscRenderingUrlPath | ||
}; | ||
|
||
const { rerender } = render(<RSCClientRoot {...props} />); | ||
|
||
return { | ||
rerender: () => rerender(<RSCClientRoot {...props} />), | ||
pushFirstChunk: () => push(JSON.stringify(chunk1)), | ||
pushSecondChunk: () => push(JSON.stringify(chunk2)), | ||
pushCustomChunk: (chunk) => push(chunk), | ||
endStream: () => push(null), | ||
} | ||
} | ||
|
||
it('fetches and caches component data', async () => { | ||
const { rerender, pushFirstChunk, pushSecondChunk, endStream } = mockRSCRequest(); | ||
|
||
expect(window.fetch).toHaveBeenCalledWith('/rsc-render/TestComponent'); | ||
expect(window.fetch).toHaveBeenCalledTimes(1); | ||
expect(screen.queryByText('StaticServerComponent')).not.toBeInTheDocument(); | ||
|
||
pushFirstChunk(); | ||
await waitFor(() => expect(screen.getByText('StaticServerComponent')).toBeInTheDocument()); | ||
expect(screen.getByText('Loading AsyncComponent...')).toBeInTheDocument(); | ||
expect(screen.queryByText('AsyncComponent')).not.toBeInTheDocument(); | ||
|
||
pushSecondChunk(); | ||
endStream(); | ||
await waitFor(() => expect(screen.getByText('AsyncComponent')).toBeInTheDocument()); | ||
expect(screen.queryByText('Loading AsyncComponent...')).not.toBeInTheDocument(); | ||
|
||
// Second render - should use cache | ||
rerender(); | ||
|
||
expect(screen.getByText('AsyncComponent')).toBeInTheDocument(); | ||
expect(window.fetch).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('replays console logs', async () => { | ||
const consoleSpy = jest.spyOn(console, 'log'); | ||
const { rerender, pushFirstChunk, pushSecondChunk, endStream } = mockRSCRequest(); | ||
|
||
pushFirstChunk(); | ||
await waitFor(() => expect(consoleSpy).toHaveBeenCalledWith('[SERVER] Console log at first chunk')); | ||
expect(consoleSpy).toHaveBeenCalledTimes(1); | ||
|
||
pushSecondChunk(); | ||
await waitFor(() => expect(consoleSpy).toHaveBeenCalledWith('[SERVER] Console log at second chunk')); | ||
endStream(); | ||
expect(consoleSpy).toHaveBeenCalledTimes(2); | ||
|
||
// On rerender, console logs should not be replayed again | ||
rerender(); | ||
expect(consoleSpy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('strips leading and trailing slashes from rscRenderingUrlPath', async () => { | ||
const { pushFirstChunk, pushSecondChunk, endStream } = mockRSCRequest('/rsc-render/'); | ||
|
||
pushFirstChunk(); | ||
pushSecondChunk(); | ||
endStream(); | ||
|
||
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('/rsc-render/TestComponent')); | ||
expect(window.fetch).toHaveBeenCalledTimes(1); | ||
|
||
await waitFor(() => expect(screen.getByText('StaticServerComponent')).toBeInTheDocument()); | ||
}); | ||
}); |
6 changes: 6 additions & 0 deletions
6
node_package/tests/fixtures/rsc-payloads/simple-shell-with-async-component/chunk1.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"html": "1:\"$Sreact.suspense\"\n0:D{\"name\":\"StaticServerComponent\",\"env\":\"Server\"}\n2:D{\"name\":\"AsyncComponent\",\"env\":\"Server\"}\n0:[\"$\",\"div\",null,{\"children\":[[\"$\",\"h1\",null,{\"children\":\"StaticServerComponent\"}],[\"$\",\"p\",null,{\"children\":\"This is a static server component\"}],[\"$\",\"$1\",null,{\"fallback\":[\"$\",\"div\",null,{\"children\":\"Loading AsyncComponent...\"}],\"children\":\"$L2\"}]]}]\n", | ||
"consoleReplayScript": "\n\u003cscript id=\"consoleReplayLog\"\u003e\nconsole.log.apply(console, [\"[SERVER] Console log at first chunk\"]);\n\u003c/script\u003e", | ||
"hasErrors": false, | ||
"isShellReady": true | ||
} |
6 changes: 6 additions & 0 deletions
6
node_package/tests/fixtures/rsc-payloads/simple-shell-with-async-component/chunk2.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"html": "2:[\"$\",\"div\",null,{\"children\":\"AsyncComponent\"}]\n", | ||
"consoleReplayScript": "\n\u003cscript id=\"consoleReplayLog\"\u003e\nconsole.log.apply(console, [\"[SERVER] Console log at second chunk\"]);\n\u003c/script\u003e", | ||
"hasErrors": false, | ||
"isShellReady": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Readable } from 'stream'; | ||
|
||
/** | ||
* Creates a Node.js Readable stream with external push capability. | ||
* Pusing a null or undefined chunk will end the stream. | ||
* @returns {{ | ||
* stream: Readable, | ||
* push: (chunk: any) => void | ||
* }} Object containing the stream and push function | ||
*/ | ||
// eslint-disable-next-line import/prefer-default-export | ||
export const createNodeReadableStream = () => { | ||
const pendingChunks = []; | ||
let pushFn; | ||
const stream = new Readable({ | ||
read() { | ||
pushFn = this.push.bind(this); | ||
if (pendingChunks.length > 0) { | ||
pushFn(pendingChunks.shift()); | ||
} | ||
}, | ||
}); | ||
|
||
const push = (chunk) => { | ||
if (pushFn) { | ||
pushFn(chunk); | ||
} else { | ||
pendingChunks.push(chunk); | ||
} | ||
}; | ||
|
||
return { stream, push }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { enableFetchMocks } from 'jest-fetch-mock'; | ||
|
||
import { Readable } from 'stream'; | ||
import { fetch } from '../src/utils'; | ||
import { createNodeReadableStream } from './testUtils'; | ||
|
||
enableFetchMocks(); | ||
|
||
describe('fetch', () => { | ||
it('streams body as ReadableStream', async () => { | ||
// create Readable stream that emits 5 chunks with 10ms delay between each chunk | ||
const { stream, push } = createNodeReadableStream(); | ||
let n = 0; | ||
const intervalId = setInterval(() => { | ||
n += 1; | ||
push(`chunk${n}`); | ||
if (n === 5) { | ||
clearInterval(intervalId); | ||
push(null); | ||
} | ||
}, 10); | ||
|
||
global.fetchMock.mockResolvedValue(new Response(stream)); | ||
|
||
await fetch('/test').then(async (response) => { | ||
console.log(response.body); | ||
const { body } = response; | ||
expect(body).toBeInstanceOf(ReadableStream); | ||
|
||
const reader = body.getReader(); | ||
const chunks = []; | ||
const decoder = new TextDecoder(); | ||
let { done, value } = await reader.read(); | ||
while (!done) { | ||
chunks.push(decoder.decode(value)); | ||
// eslint-disable-next-line no-await-in-loop | ||
({ done, value } = await reader.read()); | ||
} | ||
expect(chunks).toEqual(['chunk1', 'chunk2', 'chunk3', 'chunk4', 'chunk5']); | ||
|
||
// expect global.fetch to be called one time | ||
expect(global.fetch).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.