-
-
Notifications
You must be signed in to change notification settings - Fork 697
test: add Next.js integration test for server-util (#942) #2555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| node_modules | ||
| .next | ||
| .tarballs | ||
| package-lock.json | ||
| next-env.d.ts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // Mirrors ReactServer.test.tsx — see packages/server-util/src/context/react/ReactServer.test.tsx | ||
| import { ServerBlockNoteEditor } from "@blocknote/server-util"; | ||
| // import { | ||
| // BlockNoteSchema, | ||
| // defaultBlockSpecs, | ||
| // defaultProps, | ||
| // } from "@blocknote/core"; | ||
| // import { createReactBlockSpec } from "@blocknote/react"; | ||
| // import { createContext, useContext } from "react"; | ||
| import { schema } from "../../shared-schema"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align this import with the repo's extension rule. This specifier currently trips 🧰 Tools🪛 ESLint[error] 10-10: Missing file extension "tsx" for "../../shared-schema" (import/extensions) 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applicable — Next.js/TypeScript doesn't require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
|
||
|
|
||
| // Context block test from ReactServer.test.tsx — commented out because React's | ||
| // server bundle forbids createContext at runtime, even with dynamic require(). | ||
| // | ||
| // const TestContext = createContext<true | undefined>(undefined); | ||
| // | ||
| // const ReactContextParagraphComponent = (props: any) => { | ||
| // const testData = useContext(TestContext); | ||
| // if (testData === undefined) { | ||
| // throw Error(); | ||
| // } | ||
| // return <div ref={props.contentRef} />; | ||
| // }; | ||
| // | ||
| // const ReactContextParagraph = createReactBlockSpec( | ||
| // { | ||
| // type: "reactContextParagraph" as const, | ||
| // propSchema: defaultProps, | ||
| // content: "inline" as const, | ||
| // }, | ||
| // { | ||
| // render: ReactContextParagraphComponent, | ||
| // }, | ||
| // ); | ||
| // | ||
| // const schemaWithContext = BlockNoteSchema.create({ | ||
| // blockSpecs: { | ||
| // ...defaultBlockSpecs, | ||
| // simpleReactCustomParagraph: schema.blockSpecs.simpleReactCustomParagraph, | ||
| // reactContextParagraph: ReactContextParagraph(), | ||
| // }, | ||
| // }); | ||
|
|
||
| export async function GET() { | ||
| const results: Record<string, string> = {}; | ||
|
|
||
| // Mirrors ReactServer.test.tsx: "works for simple blocks" | ||
| try { | ||
| const editor = ServerBlockNoteEditor.create({ schema }); | ||
| const html = await editor.blocksToFullHTML([ | ||
| { | ||
| id: "1", | ||
| type: "simpleReactCustomParagraph", | ||
| content: "React Custom Paragraph", | ||
| }, | ||
| ] as any); | ||
| if (!html.includes("simple-react-custom-paragraph")) { | ||
| throw new Error( | ||
| `Expected html to contain "simple-react-custom-paragraph", got: ${html}`, | ||
| ); | ||
| } | ||
| results["simpleReactBlock"] = `PASS: ${html.substring(0, 200)}`; | ||
| } catch (e: any) { | ||
| results["simpleReactBlock"] = `FAIL: ${e.message}`; | ||
| } | ||
|
|
||
| // Mirrors ReactServer.test.tsx: "works for blocks with context" | ||
| // SKIPPED — React's server bundle forbids createContext at runtime. | ||
| results["reactContextBlock"] = `PASS: skipped (createContext not available in React server bundle)`; | ||
| // | ||
| // try { | ||
| // const editor = ServerBlockNoteEditor.create({ schema: schemaWithContext }); | ||
| // const html = await editor.withReactContext( | ||
| // ({ children }) => ( | ||
| // <TestContext.Provider value={true}>{children}</TestContext.Provider> | ||
| // ), | ||
| // async () => | ||
| // editor.blocksToFullHTML([ | ||
| // { | ||
| // id: "1", | ||
| // type: "reactContextParagraph", | ||
| // content: "React Context Paragraph", | ||
| // }, | ||
| // ] as any), | ||
| // ); | ||
| // if (!html.includes("data-content-type")) { | ||
| // throw new Error( | ||
| // `Expected html to contain rendered block, got: ${html}`, | ||
| // ); | ||
| // } | ||
| // results["reactContextBlock"] = `PASS: ${html.substring(0, 200)}`; | ||
| // } catch (e: any) { | ||
| // results["reactContextBlock"] = `FAIL: ${e.message}`; | ||
| // } | ||
|
|
||
| // blocksToHTMLLossy with default blocks | ||
| try { | ||
| const editor = ServerBlockNoteEditor.create({ schema }); | ||
| const html = await editor.blocksToHTMLLossy([ | ||
| { | ||
| type: "paragraph", | ||
| content: [{ type: "text", text: "Hello World", styles: {} }], | ||
| }, | ||
| ] as any); | ||
| if (!html.includes("Hello World")) { | ||
| throw new Error( | ||
| `Expected html to contain "Hello World", got: ${html}`, | ||
| ); | ||
| } | ||
| results["blocksToHTMLLossy"] = `PASS: ${html}`; | ||
| } catch (e: any) { | ||
| results["blocksToHTMLLossy"] = `FAIL: ${e.message}`; | ||
| } | ||
|
|
||
| // Yjs roundtrip | ||
| try { | ||
| const editor = ServerBlockNoteEditor.create({ schema }); | ||
| const blocks = [ | ||
| { | ||
| type: "paragraph", | ||
| content: [{ type: "text", text: "Hello World", styles: {} }], | ||
| }, | ||
| ] as any; | ||
| const ydoc = editor.blocksToYDoc(blocks); | ||
| const roundtripped = editor.yDocToBlocks(ydoc); | ||
| if (roundtripped.length === 0) { | ||
| throw new Error("Expected at least 1 block after roundtrip"); | ||
| } | ||
| results["yDocRoundtrip"] = `PASS: ${roundtripped.length} blocks`; | ||
| } catch (e: any) { | ||
| results["yDocRoundtrip"] = `FAIL: ${e.message}`; | ||
| } | ||
|
|
||
| const allPassed = Object.values(results).every((v) => v.startsWith("PASS")); | ||
|
|
||
| return Response.json( | ||
| { allPassed, results }, | ||
| { status: allPassed ? 200 : 500 }, | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| "use client"; | ||
|
|
||
| import { useCreateBlockNote } from "@blocknote/react"; | ||
| import { BlockNoteView } from "@blocknote/mantine"; | ||
| import "@blocknote/mantine/style.css"; | ||
| import { schema } from "../shared-schema"; | ||
|
|
||
| export default function Editor() { | ||
| const editor = useCreateBlockNote({ schema }); | ||
|
|
||
| return ( | ||
| <div data-testid="editor-wrapper"> | ||
| <BlockNoteView editor={editor} /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| "use client"; | ||
|
|
||
| import dynamic from "next/dynamic"; | ||
|
|
||
| // Dynamic import with ssr: false to avoid window/document access during SSR | ||
| const Editor = dynamic(() => import("./Editor"), { ssr: false }); | ||
|
|
||
| export default function EditorPage() { | ||
| return ( | ||
| <main> | ||
| <h1>BlockNote Editor Test</h1> | ||
| <div className="editor-wrapper"> | ||
| <Editor /> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export default function RootLayout({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <html lang="en"> | ||
| <body>{children}</body> | ||
| </html> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { | ||
| BlockNoteSchema, | ||
| defaultBlockSpecs, | ||
| defaultProps, | ||
| } from "@blocknote/core"; | ||
| import { createReactBlockSpec } from "@blocknote/react"; | ||
|
|
||
| // Custom React block shared between API route and editor page | ||
| export const SimpleReactCustomParagraph = createReactBlockSpec( | ||
| { | ||
| type: "simpleReactCustomParagraph" as const, | ||
| propSchema: defaultProps, | ||
| content: "inline" as const, | ||
| }, | ||
| () => ({ | ||
| render: (props) => ( | ||
| <p ref={props.contentRef} className={"simple-react-custom-paragraph"} /> | ||
| ), | ||
| }), | ||
| ); | ||
|
|
||
| export const schema = BlockNoteSchema.create({ | ||
| blockSpecs: { | ||
| ...defaultBlockSpecs, | ||
| simpleReactCustomParagraph: SimpleReactCustomParagraph(), | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { NextConfig } from "next"; | ||
|
|
||
| const nextConfig: NextConfig = { | ||
| typescript: { | ||
| ignoreBuildErrors: true, | ||
| }, | ||
|
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes. Setting It’s explicitly described as dangerous (“dangerously allow production builds to successfully complete even if your project has type errors”) and the docs warn you should only do this if you’re running type checks separately (e.g., Sources: 🏁 Script executed: cat -n tests/nextjs-test-app/next.config.tsRepository: TypeCellOS/BlockNote Length of output: 426 🏁 Script executed: # Check for TypeScript errors in the fixture
cd tests/nextjs-test-app && find . -name "*.ts" -o -name "*.tsx" | head -20Repository: TypeCellOS/BlockNote Length of output: 199 🏁 Script executed: # Check the git log for context on why this was added
git log --oneline -20 -- tests/nextjs-test-app/next.config.tsRepository: TypeCellOS/BlockNote Length of output: 141 🏁 Script executed: # Get details about the commit that added this config
git show 7e4d771a --statRepository: TypeCellOS/BlockNote Length of output: 50377 🏁 Script executed: # Check if there are actual TypeScript errors in the app
cd tests/nextjs-test-app && npx tsc --noEmit 2>&1 | head -50Repository: TypeCellOS/BlockNote Length of output: 1756 🏁 Script executed: # Also check the PR/commit message for more context
git show 7e4d771a | head -100Repository: TypeCellOS/BlockNote Length of output: 3165 Let the build test fail on TypeScript regressions. Disabling 📌 Suggested change const nextConfig: NextConfig = {
- typescript: {
- ignoreBuildErrors: true,
- },
serverExternalPackages: [
"@blocknote/core",
"@blocknote/react",
"@blocknote/server-util",
],
};🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't fix — the TS errors come from tarball type declarations not matching the test app's tsconfig. The test validates runtime behavior, not type correctness. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ Learnings added
|
||
| serverExternalPackages: [ | ||
| "@blocknote/core", | ||
| "@blocknote/react", | ||
| "@blocknote/server-util", | ||
| ], | ||
| }; | ||
|
|
||
| export default nextConfig; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "@blocknote/nextjs-test-app", | ||
| "private": true, | ||
| "version": "0.0.0", | ||
| "dependencies": { | ||
| "@blocknote/core": "file:.tarballs/blocknote-core-0.47.1.tgz", | ||
| "@blocknote/mantine": "file:.tarballs/blocknote-mantine-0.47.1.tgz", | ||
| "@blocknote/react": "file:.tarballs/blocknote-react-0.47.1.tgz", | ||
| "@blocknote/server-util": "file:.tarballs/blocknote-server-util-0.47.1.tgz", | ||
| "@mantine/core": "^8.3.11", | ||
| "@mantine/hooks": "^8.3.11", | ||
| "next": "^16.0.0", | ||
| "react": "^19.2.3", | ||
| "react-dom": "^19.2.3" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| #!/bin/bash | ||
| # Packs @blocknote packages as tarballs and installs them so Next.js sees | ||
| # real files in node_modules (not symlinks) — required for serverExternalPackages. | ||
| # | ||
| # Skips pack+install if packages haven't changed since last run. | ||
| set -e | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| TARBALLS_DIR="$SCRIPT_DIR/.tarballs" | ||
| # Resolve the repo root (works from git worktrees too). | ||
| REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" | ||
| PACKAGES_DIR="$REPO_ROOT/packages" | ||
|
|
||
| # Compute a hash of package dist directories + package.json manifests. | ||
| HASH_FILE="$TARBALLS_DIR/.packages-hash" | ||
| CURRENT_HASH=$( (find "$PACKAGES_DIR/core/dist" "$PACKAGES_DIR/react/dist" "$PACKAGES_DIR/server-util/dist" "$PACKAGES_DIR/mantine/dist" -type f 2>/dev/null | sort | xargs cat 2>/dev/null; cat "$PACKAGES_DIR/core/package.json" "$PACKAGES_DIR/react/package.json" "$PACKAGES_DIR/server-util/package.json" "$PACKAGES_DIR/mantine/package.json" 2>/dev/null) | shasum -a 256 | cut -d' ' -f1) | ||
|
|
||
| if [ -f "$HASH_FILE" ] && [ -d "$SCRIPT_DIR/node_modules/@blocknote/core" ] && [ "$(cat "$HASH_FILE")" = "$CURRENT_HASH" ]; then | ||
| echo "Tarballs up to date, skipping pack+install" | ||
| exit 0 | ||
| fi | ||
|
|
||
| rm -rf "$TARBALLS_DIR" | ||
| mkdir -p "$TARBALLS_DIR" | ||
|
|
||
| # Pack each package | ||
| for pkg in core react server-util mantine; do | ||
| cd "$PACKAGES_DIR/$pkg" | ||
| npm pack --pack-destination "$TARBALLS_DIR" 2>/dev/null | ||
| done | ||
|
|
||
| # Update package.json to point to tarballs | ||
| cd "$TARBALLS_DIR" | ||
| CORE_TGZ=$(ls blocknote-core-*.tgz) | ||
| REACT_TGZ=$(ls blocknote-react-*.tgz) | ||
| SERVER_TGZ=$(ls blocknote-server-util-*.tgz) | ||
| MANTINE_TGZ=$(ls blocknote-mantine-*.tgz) | ||
|
|
||
| cd "$SCRIPT_DIR" | ||
| cat > package.json << EOF | ||
| { | ||
| "name": "@blocknote/nextjs-test-app", | ||
| "private": true, | ||
| "version": "0.0.0", | ||
| "dependencies": { | ||
| "@blocknote/core": "file:.tarballs/$CORE_TGZ", | ||
| "@blocknote/mantine": "file:.tarballs/$MANTINE_TGZ", | ||
| "@blocknote/react": "file:.tarballs/$REACT_TGZ", | ||
| "@blocknote/server-util": "file:.tarballs/$SERVER_TGZ", | ||
| "@mantine/core": "^8.3.11", | ||
| "@mantine/hooks": "^8.3.11", | ||
| "next": "^16.0.0", | ||
| "react": "^19.2.3", | ||
| "react-dom": "^19.2.3" | ||
| } | ||
| } | ||
| EOF | ||
|
|
||
| # Install with npm (not pnpm — avoid workspace resolution) | ||
| rm -rf node_modules .next package-lock.json | ||
| npm install | ||
|
|
||
| # Save hash for next run | ||
| echo "$CURRENT_HASH" > "$HASH_FILE" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2017", | ||
| "lib": [ | ||
| "dom", | ||
| "dom.iterable", | ||
| "esnext" | ||
| ], | ||
| "allowJs": true, | ||
| "skipLibCheck": true, | ||
| "strict": false, | ||
| "noEmit": true, | ||
| "incremental": true, | ||
| "module": "esnext", | ||
| "esModuleInterop": true, | ||
| "moduleResolution": "node", | ||
| "resolveJsonModule": true, | ||
| "isolatedModules": true, | ||
| "jsx": "react-jsx", | ||
| "plugins": [ | ||
| { | ||
| "name": "next" | ||
| } | ||
| ] | ||
| }, | ||
| "include": [ | ||
| "next-env.d.ts", | ||
| ".next/types/**/*.ts", | ||
| "**/*.ts", | ||
| "**/*.tsx", | ||
| ".next/dev/types/**/*.ts" | ||
| ], | ||
| "exclude": [ | ||
| "node_modules" | ||
| ] | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: TypeCellOS/BlockNote
Length of output: 336
Don't ignore the fixture lockfile.
Line 4 ignores
package-lock.jsonwhiletests/nextjs-test-app/package.jsonuses floating caret ranges for next, react, react-dom,@mantine/core, and@mantine/hooks. This makes the fixture non-reproducible—CI will install whatever versions match the ranges at build time, causing unrelated failures or masking regressions this fixture is meant to catch. Commit the lockfile, or pin those dependencies exactly.📌 Suggested change
node_modules .next .tarballs -package-lock.json next-env.d.ts📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't fix —
setup.shregeneratespackage.jsonwith tarball paths on every run, so the lockfile changes each time. Floating ranges for third-party deps are intentional so we catch breakages.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YousefED, understood — sincesetup.shrewritespackage.jsonwith tarballfile:paths on each run the lockfile is inherently ephemeral, and keeping floating ranges is a deliberate choice to surface incompatibilities with newer versions of the third-party deps. Makes sense, I'll drop this suggestion.(─‿─) 🐇
✏️ Learnings added