Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 63 additions & 0 deletions test/ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ImageEditor image="https://example.com/photo.jpg" editorId="ssr" />
);

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(
<ImageEditor
image="https://example.com/photo.jpg"
options={{ theme: 'dark', locale: 'fr', projectId: 1234 }}
scriptUrl="https://cdn.example.com/embed.js"
minHeight="80vh"
style={{ borderRadius: 8 }}
onLoad={() => {}}
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
);
}
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"types": ["vitest/globals"]
"types": ["vitest/globals", "node"]
}
}
Loading