Files
cloudflare__vinext/tests/server-inserted-html-context.test.ts
Stephen Zhou c17d6941be chore: migrate to vite plus (#535)
* chore: migrate to vite plus

* Disable typeAware and typeCheck

* Update CI

* Fix CI

* Fix test

* Clean

* Run test with vp

* Try revert

* react: false In test

* Fix test

* Revert "Try revert"

This reverts commit 009da10473.

* Update

* Update

* Try revert ci changes

* revert

* Run vp migrate

* Disable typeAware and typeCheck for now

* Better resolve for test

* Use vp dev instead of vite

* Update expect

* Fix NormalizeManifestModuleId

* Try increase timeout

* Update to use vp

* Try new check

* Bring back npx vp

* Migrate CI

* Make next-intl resolvable

* Update

* Update

* Update
2026-03-15 10:50:13 +00:00

191 lines
7.2 KiB
TypeScript

/**
* Tests for ServerInsertedHTMLContext — the React Context that CSS-in-JS libraries
* (Apollo Client, styled-components, emotion) use to register HTML injection
* callbacks during SSR via useContext().
*
* These tests verify the actual integration pattern used by libraries, not just
* structural properties of the context object.
*/
import { describe, it, expect, beforeEach } from "vite-plus/test";
import * as React from "react";
import { renderToString } from "react-dom/server";
describe("ServerInsertedHTMLContext", () => {
beforeEach(async () => {
const { clearServerInsertedHTML } = await import("../packages/vinext/src/shims/navigation.js");
clearServerInsertedHTML();
});
it("is exported from next/navigation shim", async () => {
const mod = await import("../packages/vinext/src/shims/navigation.js");
expect(mod.ServerInsertedHTMLContext).toBeDefined();
});
it("is a valid React.Context with Provider and Consumer", async () => {
const { ServerInsertedHTMLContext } =
await import("../packages/vinext/src/shims/navigation.js");
expect(ServerInsertedHTMLContext).not.toBeNull();
expect(ServerInsertedHTMLContext).toHaveProperty("Provider");
expect(ServerInsertedHTMLContext).toHaveProperty("Consumer");
});
it("has null as default value (no Provider)", async () => {
const { ServerInsertedHTMLContext } =
await import("../packages/vinext/src/shims/navigation.js");
// Without a Provider, useContext returns the default value (null).
// This is correct — Apollo checks for null and throws a clear error
// if used outside the App Router.
expect((ServerInsertedHTMLContext as any)._currentValue).toBeNull();
});
it("provides a callback registration function when wrapped with Provider", async () => {
const { ServerInsertedHTMLContext } =
await import("../packages/vinext/src/shims/navigation.js");
let contextValue: unknown = "not-set";
// Component that reads the context — simulates what Apollo does
function ContextReader() {
contextValue = React.useContext(ServerInsertedHTMLContext!);
return React.createElement("div", null, "test");
}
// Simulate the SSR pipeline: Provider wraps the tree with a registration function
const addCallback = (_cb: () => unknown) => {
/* registration function */
};
const tree = React.createElement(
ServerInsertedHTMLContext!.Provider,
{ value: addCallback },
React.createElement(ContextReader),
);
renderToString(tree);
expect(contextValue).toBe(addCallback);
expect(typeof contextValue).toBe("function");
});
it("Apollo Client pattern: useContext returns a usable registration function", async () => {
const { ServerInsertedHTMLContext, useServerInsertedHTML, flushServerInsertedHTML } =
await import("../packages/vinext/src/shims/navigation.js");
// Simulate Apollo's actual usage pattern:
// const insertHtml = useContext(ServerInsertedHTMLContext);
// if (!insertHtml) throw new Error("...");
// insertHtml(() => <style>...</style>);
let apolloError: Error | null = null;
function ApolloSSRComponent() {
const insertHtml = React.useContext(ServerInsertedHTMLContext!);
if (!insertHtml) {
apolloError = new Error(
"The SSR build of ApolloNextAppProvider cannot be used outside of the Next App Router!",
);
return React.createElement("div", null, "error");
}
// Register a style injection callback (what Apollo does for SSR)
insertHtml(() => "<style>.apollo-ssr { color: red; }</style>");
return React.createElement("div", null, "apollo-content");
}
// Wrap with Provider (simulates what handleSsr does)
const tree = React.createElement(
ServerInsertedHTMLContext!.Provider,
{ value: useServerInsertedHTML },
React.createElement(ApolloSSRComponent),
);
const html = renderToString(tree);
// Apollo should NOT throw
expect(apolloError).toBeNull();
expect(html).toContain("apollo-content");
// The callback should have been registered via useServerInsertedHTML
const flushed = flushServerInsertedHTML();
expect(flushed).toHaveLength(1);
expect(flushed[0]).toBe("<style>.apollo-ssr { color: red; }</style>");
});
it("works alongside direct useServerInsertedHTML calls", async () => {
const { ServerInsertedHTMLContext, useServerInsertedHTML, flushServerInsertedHTML } =
await import("../packages/vinext/src/shims/navigation.js");
// Component using direct useServerInsertedHTML (styled-components pattern)
function StyledComponentsRegistry({ children }: { children: React.ReactNode }) {
useServerInsertedHTML(() => "<style>.sc-1 { display: block; }</style>");
return React.createElement(React.Fragment, null, children);
}
// Component using useContext (Apollo pattern)
function ApolloRegistry() {
const insertHtml = React.useContext(ServerInsertedHTMLContext!);
if (insertHtml) {
insertHtml(() => "<style>.apollo { font-weight: bold; }</style>");
}
return React.createElement("div", null, "app");
}
const tree = React.createElement(
ServerInsertedHTMLContext!.Provider,
{ value: useServerInsertedHTML },
React.createElement(StyledComponentsRegistry, null, React.createElement(ApolloRegistry)),
);
renderToString(tree);
// Both callbacks should be in the same array
const flushed = flushServerInsertedHTML();
expect(flushed).toHaveLength(2);
expect(flushed[0]).toContain(".sc-1");
expect(flushed[1]).toContain(".apollo");
});
it("returns null without Provider (Apollo throws clear error)", async () => {
const { ServerInsertedHTMLContext } =
await import("../packages/vinext/src/shims/navigation.js");
let contextValue: unknown = "not-set";
function ComponentWithoutProvider() {
contextValue = React.useContext(ServerInsertedHTMLContext!);
return React.createElement("div", null, "no-provider");
}
// Render WITHOUT Provider — simulates using outside App Router
renderToString(React.createElement(ComponentWithoutProvider));
// Context value should be null (the default)
expect(contextValue).toBeNull();
});
it("supports multiple callback registrations from context", async () => {
const { ServerInsertedHTMLContext, useServerInsertedHTML, flushServerInsertedHTML } =
await import("../packages/vinext/src/shims/navigation.js");
function MultiCallbackComponent() {
const insertHtml = React.useContext(ServerInsertedHTMLContext!);
if (insertHtml) {
insertHtml(() => "<style>.first {}</style>");
insertHtml(() => "<style>.second {}</style>");
insertHtml(() => "<style>.third {}</style>");
}
return React.createElement("div", null, "multi");
}
const tree = React.createElement(
ServerInsertedHTMLContext!.Provider,
{ value: useServerInsertedHTML },
React.createElement(MultiCallbackComponent),
);
renderToString(tree);
const flushed = flushServerInsertedHTML();
expect(flushed).toHaveLength(3);
expect(flushed[0]).toContain(".first");
expect(flushed[1]).toContain(".second");
expect(flushed[2]).toContain(".third");
});
});