diff --git a/src/Components/NavBar/NavBarLoggedInActions.tsx b/src/Components/NavBar/NavBarLoggedInActions.tsx index 3e4635a9e4f..2c7383316f7 100644 --- a/src/Components/NavBar/NavBarLoggedInActions.tsx +++ b/src/Components/NavBar/NavBarLoggedInActions.tsx @@ -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" @@ -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> = () => { - const { relayEnvironment } = useContext(SystemContext) + const { relayEnvironment, user } = useSystemContext() const isClient = useDidMount() @@ -159,6 +158,7 @@ export const NavBarLoggedInActionsQueryRenderer: React.FC environment={relayEnvironment} + placeholder={user ? : undefined} query={graphql` query NavBarLoggedInActionsQuery { me { diff --git a/src/System/Router/Utils/__tests__/renderToStream.jest.tsx b/src/System/Router/Utils/__tests__/renderToStream.jest.tsx new file mode 100644 index 00000000000..a46295a70e7 --- /dev/null +++ b/src/System/Router/Utils/__tests__/renderToStream.jest.tsx @@ -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 =
Hello World
+ 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 =
Hello Error
+ 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 =
Hello Error
+ 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 =
Timeout Test
+ 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 =
Stream Test
+ const stream = renderToStream({ jsx, sheet, res }) + + stream.write("") + + const chunks: string[] = [] + stream.on("data", chunk => { + chunks.push(chunk) + }) + + stream.on("end", () => { + const result = chunks.join("") + expect(result).toContain( + "mock-css" + ) + }) + + stream.end() + }) + + it("should insert the rendered JSX markup into the HTML body", async () => { + const jsx =
JSX Markup
+ + 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("") + + 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( + 'mock-css
JSX Markup
' + ) + }) +}) diff --git a/src/System/Router/Utils/collectAssets.tsx b/src/System/Router/Utils/collectAssets.tsx index 54ff430f234..9cbe61b4fc5 100644 --- a/src/System/Router/Utils/collectAssets.tsx +++ b/src/System/Router/Utils/collectAssets.tsx @@ -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() @@ -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, diff --git a/src/System/Router/Utils/renderToStream.tsx b/src/System/Router/Utils/renderToStream.tsx index 680ca79f26c..7337adb5daf 100644 --- a/src/System/Router/Utils/renderToStream.tsx +++ b/src/System/Router/Utils/renderToStream.tsx @@ -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") @@ -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 diff --git a/src/server.ts b/src/server.ts index 4710d4007ec..2c7bbde70d4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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) => { @@ -51,10 +49,7 @@ app.get( } ) -/** - * Mount server-side Express routes - */ - +// Common express routes app .use(appPreferencesServerRoutes) .use(cookieConsentManagerServerRoutes)