Skip to content

Commit 2f7a06e

Browse files
committed
feat(react): enhance testing and add new dependencies
- Added @testing-library/jest-dom and @testing-library/react for improved testing capabilities. - Introduced jsdom for better DOM manipulation in tests. - Updated EmojiRandomizer component to clean up unnecessary code. - Refactored live-editing tests to improve structure and clarity. - Enhanced StoryblokLiveEditing component with better visual editor detection. This update improves the testing framework and overall code quality in the React package.
1 parent ac5296e commit 2f7a06e

File tree

8 files changed

+280
-40
lines changed

8 files changed

+280
-40
lines changed

packages/react/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"@cypress/react": "^9.0.1",
7373
"@cypress/vite-dev-server": "^6.0.3",
7474
"@storyblok/eslint-config": "workspace:*",
75+
"@testing-library/jest-dom": "^6.6.3",
76+
"@testing-library/react": "^16.3.0",
7577
"@tsconfig/recommended": "^1.0.8",
7678
"@types/node": "^22.15.18",
7779
"@types/react": "19.1.4",
@@ -81,6 +83,7 @@
8183
"eslint": "^9.26.0",
8284
"eslint-plugin-cypress": "^4.3.0",
8385
"eslint-plugin-jest": "^28.11.0",
86+
"jsdom": "^26.1.0",
8487
"react": "^19.1.0",
8588
"react-dom": "^19.1.0",
8689
"rollup-plugin-preserve-directives": "^0.4.0",

packages/react/playground/next15/src/app/components/EmojiRandomizer.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ const useIsClient = () => {
77
const [isClient, setIsClient] = useState(false);
88

99
useEffect(() => {
10-
1110
setIsClient(true);
1211
}, []);
1312

1413
return isClient;
1514
};
1615

17-
1816
interface EmojiRandomizerProps {
1917
blok: SbBlokData & {
2018
label?: string;
@@ -25,7 +23,6 @@ interface EmojiRandomizerProps {
2523
* A component that displays a label and a random emoji that changes on click
2624
*/
2725
const EmojiRandomizer: FC<EmojiRandomizerProps> = ({ blok }) => {
28-
2926
// List of fun emojis to randomly choose from
3027
const emojis = ['😊', '🎉', '🚀', '✨', '🌈', '🎨', '🎸', '🎮', '🍕', '🌺'];
3128

packages/react/playground/next15/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
ISbStoriesParams,
33
StoryblokClient,
44
} from '@storyblok/react/rsc';
5-
import { MarkTypes, StoryblokLiveEditing, StoryblokRichText, StoryblokServerComponent } from '@storyblok/react/rsc';
5+
import { StoryblokLiveEditing, StoryblokServerComponent } from '@storyblok/react/rsc';
66
import { getStoryblokApi } from '@/lib/storyblok';
77
import Link from 'next/link';
88

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,112 @@
1-
import { describe, expect, it, vi } from 'vitest';
2-
import StoryblokLiveEditing from '../rsc/live-editing';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { render } from '@testing-library/react';
3+
import React from 'react';
34
import type { ISbStoryData } from '@storyblok/js';
45

5-
// Mock dependencies
6+
// Import after mocks
7+
import StoryblokLiveEditing from '../rsc/live-editing';
8+
import { isBridgeLoaded, isVisualEditor } from '../utils';
9+
import { loadStoryblokBridge } from '@storyblok/js';
10+
11+
// Mock dependencies - need to define functions inline to avoid hoisting issues
612
vi.mock('../rsc/live-edit-update-action', () => ({
713
liveEditUpdateAction: vi.fn(),
814
}));
915

1016
vi.mock('@storyblok/js', () => ({
1117
registerStoryblokBridge: vi.fn(),
18+
loadStoryblokBridge: vi.fn(() => Promise.resolve()),
1219
}));
1320

14-
// Mock window for testing
15-
const originalWindow = globalThis.window;
16-
beforeEach(() => {
17-
// Reset the window mock
18-
vi.stubGlobal('window', {
19-
storyblokRegisterEvent: undefined,
20-
location: {
21-
search: '',
22-
pathname: '/',
23-
},
21+
vi.mock('../utils', () => ({
22+
isVisualEditor: vi.fn(() => false),
23+
isBridgeLoaded: vi.fn(() => false),
24+
}));
25+
26+
describe('storyblokLiveEditing', () => {
27+
beforeEach(() => {
28+
vi.clearAllMocks();
29+
vi.mocked(isVisualEditor).mockReturnValue(false);
30+
vi.mocked(isBridgeLoaded).mockReturnValue(false);
31+
32+
// Mock window
33+
vi.stubGlobal('window', {
34+
storyblokRegisterEvent: undefined,
35+
location: {
36+
search: '',
37+
pathname: '/',
38+
},
39+
self: {},
40+
top: {},
41+
});
2442
});
25-
});
2643

27-
afterEach(() => {
28-
// Restore original window
29-
vi.stubGlobal('window', originalWindow);
30-
});
44+
afterEach(() => {
45+
vi.unstubAllGlobals();
46+
});
3147

32-
describe('storyblokLiveEditing', () => {
3348
it('should return null when not in visual editor', () => {
34-
const component = StoryblokLiveEditing({
35-
story: { uuid: '123', id: 456 } as ISbStoryData,
36-
bridgeOptions: {},
37-
});
38-
expect(component).toBeNull();
49+
vi.mocked(isVisualEditor).mockReturnValue(false);
50+
51+
const { container } = render(
52+
React.createElement(StoryblokLiveEditing, {
53+
story: { uuid: '123', id: 456 } as ISbStoryData,
54+
bridgeOptions: {},
55+
}),
56+
);
57+
58+
expect(container.firstChild).toBeNull();
59+
expect(loadStoryblokBridge).not.toHaveBeenCalled();
3960
});
4061

41-
it('should pass story id to useEffect dependencies', () => {
62+
it('should load bridge and register when in visual editor', async () => {
63+
vi.mocked(isVisualEditor).mockReturnValue(true);
64+
vi.mocked(isBridgeLoaded).mockReturnValue(false);
65+
4266
const story = { id: 789, uuid: 'test-uuid' } as ISbStoryData;
4367

44-
// This test is minimal because we can't easily test React hooks in unit tests
45-
// Instead, we just verify the component doesn't throw errors with valid props
46-
const renderFn = () => {
47-
StoryblokLiveEditing({ story, bridgeOptions: { resolveRelations: ['test.relation'] } });
48-
};
68+
const { container } = render(
69+
React.createElement(StoryblokLiveEditing, {
70+
story,
71+
bridgeOptions: { resolveRelations: ['test.relation'] },
72+
}),
73+
);
74+
75+
// Component should return null but load bridge
76+
expect(container.firstChild).toBeNull();
4977

50-
expect(renderFn).not.toThrow();
78+
// Wait for async bridge loading
79+
await vi.waitFor(() => {
80+
expect(loadStoryblokBridge).toHaveBeenCalled();
81+
});
5182
});
5283

5384
it('should handle null story gracefully', () => {
54-
// @ts-expect-error - Testing invalid input
55-
const renderFn = () => StoryblokLiveEditing({ story: null, bridgeOptions: {} });
85+
vi.mocked(isVisualEditor).mockReturnValue(false);
86+
87+
const { container } = render(
88+
React.createElement(StoryblokLiveEditing, {
89+
// @ts-expect-error - Testing invalid input
90+
story: null,
91+
bridgeOptions: {},
92+
}),
93+
);
94+
95+
expect(container.firstChild).toBeNull();
96+
});
97+
98+
it('should not load bridge when already loaded', () => {
99+
vi.mocked(isVisualEditor).mockReturnValue(true);
100+
vi.mocked(isBridgeLoaded).mockReturnValue(true);
101+
102+
render(
103+
React.createElement(StoryblokLiveEditing, {
104+
story: { id: 123, uuid: 'test' } as ISbStoryData,
105+
bridgeOptions: {},
106+
}),
107+
);
56108

57-
// Should not throw an error with null story
58-
expect(renderFn).not.toThrow();
59-
expect(renderFn()).toBeNull();
109+
// Since bridge is already loaded, we should not call loadStoryblokBridge
110+
expect(loadStoryblokBridge).not.toHaveBeenCalled();
60111
});
61112
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import '@testing-library/jest-dom';
2+
import { vi } from 'vitest';
3+
4+
// Mock the browser environment
5+
Object.defineProperty(window, 'location', {
6+
value: {
7+
href: 'http://localhost:3000',
8+
origin: 'http://localhost:3000',
9+
protocol: 'http:',
10+
hostname: 'localhost',
11+
port: '3000',
12+
pathname: '/',
13+
search: '',
14+
hash: '',
15+
assign: vi.fn(),
16+
replace: vi.fn(),
17+
reload: vi.fn(),
18+
},
19+
writable: true,
20+
});
21+
22+
Object.defineProperty(window, 'self', {
23+
value: window,
24+
writable: true,
25+
});
26+
27+
Object.defineProperty(window, 'top', {
28+
value: window,
29+
writable: true,
30+
});
31+
32+
// Mock console methods to reduce noise in tests
33+
globalThis.console = {
34+
...console,
35+
warn: vi.fn(),
36+
error: vi.fn(),
37+
};

packages/react/src/rsc/live-editing.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { liveEditUpdateAction } from './live-edit-update-action';
66
import { isBridgeLoaded, isVisualEditor } from '../utils';
77

88
const StoryblokLiveEditing = ({ story = null, bridgeOptions = {} }: { story: ISbStoryData; bridgeOptions?: StoryblokBridgeConfigV2 }) => {
9+
const inVisualEditor = isVisualEditor();
10+
11+
if (!inVisualEditor) {
12+
return null;
13+
}
14+
915
const storyId = story?.id ?? 0;
1016
useEffect(() => {
1117
(async () => {

packages/react/vitest.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference types="vitest" />
2+
import { defineConfig } from 'vite';
3+
import react from '@vitejs/plugin-react';
4+
5+
export default defineConfig({
6+
plugins: [react()],
7+
test: {
8+
environment: 'jsdom',
9+
setupFiles: ['./src/__tests__/setup.ts'],
10+
globals: true,
11+
},
12+
});

0 commit comments

Comments
 (0)