Skip to content

Commit

Permalink
test: add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
damassi committed Nov 24, 2024
1 parent ed0585c commit 47ca53e
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 18 deletions.
6 changes: 3 additions & 3 deletions src/Components/NavBar/NavBarLoggedInActions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useContext } from "react"
import * as React from "react"
import { NavBarUserMenu } from "./Menus"
import { SystemContext } from "System/Contexts/SystemContext"
import { Dropdown, Flex, useDidMount } from "@artsy/palette"
import EnvelopeIcon from "@artsy/icons/EnvelopeIcon"
import PersonIcon from "@artsy/icons/PersonIcon"
Expand All @@ -24,6 +22,7 @@ import { ProgressiveOnboardingAlertFind } from "Components/ProgressiveOnboarding
import { extractNodes } from "Utils/extractNodes"
import { getENV } from "Utils/getENV"
import { FallbackErrorBoundary } from "System/Components/FallbackErrorBoundary"
import { useSystemContext } from "System/Hooks/useSystemContext"

/** Displays action icons for logged in users such as inbox, profile, and notifications */
export const NavBarLoggedInActions: React.FC<React.PropsWithChildren<
Expand Down Expand Up @@ -149,7 +148,7 @@ export const NavBarLoggedInActions: React.FC<React.PropsWithChildren<
}

export const NavBarLoggedInActionsQueryRenderer: React.FC<React.PropsWithChildren<{}>> = () => {
const { relayEnvironment } = useContext(SystemContext)
const { relayEnvironment, user } = useSystemContext()

const isClient = useDidMount()

Expand All @@ -159,6 +158,7 @@ export const NavBarLoggedInActionsQueryRenderer: React.FC<React.PropsWithChildre
<FallbackErrorBoundary FallbackComponent={Placeholder}>
<SystemQueryRenderer<NavBarLoggedInActionsQuery>
environment={relayEnvironment}
placeholder={user ? <Placeholder /> : undefined}
query={graphql`
query NavBarLoggedInActionsQuery {
me {
Expand Down
184 changes: 184 additions & 0 deletions src/System/Router/Utils/__tests__/renderToStream.jest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { flushPromiseQueue } from "DevTools/flushPromiseQueue"
import { renderToPipeableStream, renderToStaticMarkup } from "react-dom/server"
import { ArtsyResponse } from "Server/middleware/artsyExpress"
import { ServerStyleSheet } from "styled-components"
import { renderToStream } from "System/Router/Utils/renderToStream"

jest.mock("react-dom/server", () => ({
...jest.requireActual("react-dom/server"),
renderToPipeableStream: jest.fn(),
}))

describe("renderToStream", () => {
const mockRenderToPipeableStream = renderToPipeableStream as jest.Mock

const res = ({
statusCode: 0,
setHeader: jest.fn(),
} as unknown) as ArtsyResponse

const sheet = ({
_emitSheetCSS: jest.fn(() => "mock-css"),
instance: {
clearTag: jest.fn(),
},
} as unknown) as ServerStyleSheet

afterEach(() => {
jest.clearAllMocks()
})

it("should set the response status and content type on shell ready", async () => {
const mockPipe = jest.fn()
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
setTimeout(() => {
options.onShellReady()
})

return { pipe: mockPipe, abort: mockAbort }
})

const jsx = <div>Hello World</div>
renderToStream({ jsx, sheet, res })

await flushPromiseQueue()

expect(res.statusCode).toBe(200)
expect(res.setHeader).toHaveBeenCalledWith(
"Content-Type",
"text/html; charset=utf-8"
)
expect(mockPipe).toHaveBeenCalled()
})

it("should handle onError and set didError to true", async () => {
const mockPipe = jest.fn()
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
options.onError(new Error("Test error"))

setTimeout(() => {
options.onShellReady()
})

return { pipe: mockPipe, abort: mockAbort }
})

const jsx = <div>Hello Error</div>
renderToStream({ jsx, sheet, res })

await flushPromiseQueue()

expect(res.statusCode).toBe(500)
expect(mockPipe).toHaveBeenCalled()
})

it("should handle onShellError and set didError to true", async () => {
const mockPipe = jest.fn()
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
options.onShellError(new Error("Test error"))

setTimeout(() => {
options.onShellReady()
})

return { pipe: mockPipe, abort: mockAbort }
})

const jsx = <div>Hello Error</div>
renderToStream({ jsx, sheet, res })

await flushPromiseQueue()

expect(res.statusCode).toBe(500)
expect(mockPipe).toHaveBeenCalled()
})

it("should call abort if STREAM_TIMEOUT is reached", () => {
jest.useFakeTimers()
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
return { pipe: jest.fn(), abort: mockAbort }
})

const jsx = <div>Timeout Test</div>
renderToStream({ jsx, sheet, res })

jest.advanceTimersByTime(5000)

expect(mockAbort).toHaveBeenCalled()
jest.useRealTimers()
})

it("should transform the stream and inject CSS into the HTML", async () => {
const mockPipe = jest.fn()
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
setTimeout(() => {
options.onShellReady()
})
return { pipe: mockPipe, abort: mockAbort }
})

const jsx = <div>Stream Test</div>
const stream = renderToStream({ jsx, sheet, res })

stream.write("<html><head></head><body></body></html>")

const chunks: string[] = []
stream.on("data", chunk => {
chunks.push(chunk)
})

stream.on("end", () => {
const result = chunks.join("")
expect(result).toContain(
"<html><head>mock-css</head><body></body></html>"
)
})

stream.end()
})

it("should insert the rendered JSX markup into the HTML body", async () => {
const jsx = <div id="content">JSX Markup</div>

const mockPipe = jest.fn(stream => {
stream.write(renderToStaticMarkup(jsx))
stream.end()
})
const mockAbort = jest.fn()

mockRenderToPipeableStream.mockImplementation((_, options) => {
setTimeout(() => {
options.onShellReady()
})
return { pipe: mockPipe, abort: mockAbort }
})

const stream = renderToStream({ jsx, sheet, res })

stream.write("<html><head></head><body></body></html>")

const chunks: string[] = []
stream.on("data", chunk => {
chunks.push(chunk)
})

await new Promise(resolve => {
stream.on("end", resolve)
})

const result = chunks.join("")
expect(result).toContain(
'<html><head>mock-css</head><body></body></html><div id="content">JSX Markup</div>'
)
})
})
6 changes: 1 addition & 5 deletions src/System/Router/Utils/collectAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const collectAssets = async ({
let styleTags

if (ENABLE_SSR_STREAMING) {
stream = renderToStream(jsx, sheet, res)
stream = renderToStream({ jsx, sheet, res })
} else {
html = renderToString(jsx)
styleTags = sheet.getStyleTags()
Expand All @@ -79,10 +79,6 @@ export const collectAssets = async ({
bundleScriptTags
.split("\n")
.map(script => {
/**
* In production, prefix injected script src with CDN endpoint.
* @see https://github.com/artsy/force/blob/main/src/lib/middleware/asset.ts#L23
*/
if (getENV("CDN_URL")) {
const scriptTagWithCDN = script.replace(
/src="\/assets/g,
Expand Down
18 changes: 15 additions & 3 deletions src/System/Router/Utils/renderToStream.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { ReactNode } from "react"
import { renderToPipeableStream } from "react-dom/server"
import { ArtsyResponse } from "Server/middleware/artsyExpress"
import { Transform } from "stream"
import { ServerStyleSheet } from "styled-components"

const STREAM_TIMEOUT = 5000

export function renderToStream(jsx, sheet, res: ArtsyResponse) {
interface RenderToStreamProps {
jsx: ReactNode
sheet: ServerStyleSheet
res: ArtsyResponse
}

export const renderToStream = ({
jsx,
sheet,
res,
}: RenderToStreamProps): Transform => {
let didError = false

const decoder = new TextDecoder("utf-8")
Expand Down Expand Up @@ -49,11 +61,11 @@ export function renderToStream(jsx, sheet, res: ArtsyResponse) {
const { pipe, abort } = renderToPipeableStream(jsx, {
onError: error => {
didError = true
console.error("error", error)
console.error("[renderToStream] onError:", error)
},
onShellError: error => {
didError = true
console.log("shell error", error)
console.error("[renderToStream] onShellError:", error)
},
onShellReady: () => {
res.statusCode = didError ? 500 : 200
Expand Down
9 changes: 2 additions & 7 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ initializeMiddleware(app)

const { routes, routePaths } = getRoutes()

/**
* Mount routes that will connect to global SSR router
*/
// React app routes
app.get(
routePaths,
async (req: ArtsyRequest, res: ArtsyResponse, next: NextFunction) => {
Expand All @@ -51,10 +49,7 @@ app.get(
}
)

/**
* Mount server-side Express routes
*/

// Common express routes
app
.use(appPreferencesServerRoutes)
.use(cookieConsentManagerServerRoutes)
Expand Down

0 comments on commit 47ca53e

Please sign in to comment.