diff --git a/package-lock.json b/package-lock.json index 602a0a0..75c3112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@eslint/js": "10.0.1", "@testing-library/react": "16.3.2", + "@types/node": "24.10.9", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitest/coverage-v8": "4.1.10", @@ -2026,6 +2027,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/react": { "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -4854,6 +4865,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", diff --git a/package.json b/package.json index 0f93437..c34572b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@eslint/js": "10.0.1", "@testing-library/react": "16.3.2", + "@types/node": "24.10.9", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitest/coverage-v8": "4.1.10", diff --git a/test/ssr.test.tsx b/test/ssr.test.tsx new file mode 100644 index 0000000..b562ce3 --- /dev/null +++ b/test/ssr.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment node +// +// The rest of the suite runs in jsdom, so nothing here verified the +// README's claim that the component "works out of the box in React Server +// Components environments ... and touches the DOM only inside effects". +// This file runs with no DOM at all. +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { renderToString } from 'react-dom/server'; + +import ImageEditor from '../src'; + +it('has no DOM in this environment', () => { + // Guards the guard: if jsdom leaked in, the test below proves nothing. + expect(typeof document).toBe('undefined'); + expect(typeof window).toBe('undefined'); +}); + +it('renders to a string on the server without touching the DOM', () => { + const html = renderToString( + + ); + + expect(html).toContain('id="ssr"'); +}); + +it('renders on the server with every prop set', () => { + // Effects never run on the server, so a prop that reaches the DOM during + // render rather than in an effect would throw here. + expect(() => + renderToString( + {}} + onSave={() => {}} + onCancel={() => {}} + onLoadError={() => {}} + onError={() => {}} + /> + ) + ).not.toThrow(); +}); + +// The 'use client' banner comes from tsup config, which nothing else +// checks — a bundler or config change could silently drop it and every +// existing test would still pass. dist/ exists in every CI job, because +// `npm ci` runs the `prepare` script and that builds; the skip is only for +// a local run before anyone has built. +const dist = (file: string) => resolve(__dirname, '..', 'dist', file); +const built = existsSync(dist('index.mjs')); + +it.skipIf(!built)('ships the "use client" directive in both builds', () => { + for (const file of ['index.js', 'index.mjs']) { + expect(readFileSync(dist(file), 'utf8').startsWith("'use client';")).toBe( + true + ); + } +}); diff --git a/tsconfig.json b/tsconfig.json index 0bc2c11..9ed99bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "resolveJsonModule": true, - "types": ["vitest/globals"] + "types": ["vitest/globals", "node"] } }