1+ ---
2+ description: Guidelines for writing unit tests in the Plate monorepo using Jest, React Testing Library, and Slate Hyperscript JSX.
3+ globs: packages/**/*.spec.{ts,tsx}, packages/**/*.test.{ts,tsx}
4+ alwaysApply: true
5+ ---
6+
7+ - **Testing Framework and Setup:**
8+ - Use Jest with TypeScript (`ts-jest`) and SWC transformer
9+ - Tests run in `jsdom` environment
10+ - Test files must end with `.spec.ts` or `.spec.tsx`
11+ - Global setup includes `@testing-library/jest-dom` and Slate test utils
12+ - Use `describe` blocks to group related tests
13+ - Use `it` or `test` for individual test cases
14+ - Use `beforeEach` for common setup between tests
15+ - E2E tests use Playwright (see `tooling/e2e/*.spec.ts`)
16+
17+ - **Required Imports for Slate Hyperscript JSX:**
18+ ```typescript
19+ /** @jsx jsx */
20+ import { jsx } from '@platejs/test-utils';
21+
22+ jsx; // Required to prevent removal by compiler
23+ ```
24+
25+ Or for typed JSX in docx tests:
26+ ```typescript
27+ /** @jsx jsxt */
28+ import { jsxt } from '@platejs/test-utils';
29+
30+ jsxt; // Required to prevent removal by compiler
31+ ```
32+
33+ - **Editor Creation Patterns:**
34+ ```typescript
35+ // ✅ DO: Use createPlateEditor for React components
36+ import { createPlateEditor } from '@platejs/core/react';
37+
38+ const editor = createPlateEditor({
39+ plugins: [MyPlugin],
40+ });
41+
42+ // ✅ DO: Use createEditor for pure Slate operations
43+ import { createEditor } from '@platejs/slate';
44+
45+ const editor = createEditor();
46+ editor.children = initialValue;
47+ ```
48+
49+ - **Slate Hyperscript JSX Elements:**
50+ - Common elements: `<editor>`, `<hp>`, `<htext>`, `<hh1>` through `<hh6>`
51+ - List elements: `<hul>`, `<hol>`, `<hli>`
52+ - Table elements: `<htable>`, `<htr>`, `<htd>`, `<hth>`
53+ - Inline elements: `<ha>`, `<htext bold>`, `<htext italic>`
54+ - Special elements: `<hcodeblock>`, `<hblockquote>`, `<hmention>`
55+
56+ ```typescript
57+ // ✅ DO: Use hyperscript JSX for test data
58+ const input = (
59+ <editor>
60+ <hp>
61+ <htext>Hello </htext>
62+ <htext bold>world</htext>
63+ </hp>
64+ </editor>
65+ );
66+ ```
67+
68+ - **Plugin Testing Patterns:**
69+ ```typescript
70+ // ✅ DO: Test plugin configuration
71+ const TestPlugin = createSlatePlugin({
72+ key: 'test',
73+ options: { testOption: 'value' },
74+ }).extendEditorApi(() => ({
75+ testMethod: () => 'result',
76+ }));
77+
78+ const editor = createPlateEditor({
79+ plugins: [TestPlugin],
80+ });
81+
82+ // Test API methods
83+ expect(editor.api.testMethod()).toBe('result');
84+
85+ // Test plugin retrieval
86+ const plugin = editor.getPlugin(TestPlugin);
87+ expect(plugin.options.testOption).toBe('value');
88+ ```
89+
90+ - **Transform Testing:**
91+ ```typescript
92+ // ✅ DO: Test transforms with clear initial and expected values
93+ describe('wrapNodes', () => {
94+ it('should wrap the node', () => {
95+ const initialValue = [
96+ {
97+ children: [{ text: 'test' }],
98+ type: 'paragraph',
99+ },
100+ ];
101+
102+ const expectedValue = [
103+ {
104+ children: [
105+ {
106+ children: [{ text: 'test' }],
107+ type: 'blockquote',
108+ },
109+ ],
110+ type: 'paragraph',
111+ },
112+ ];
113+
114+ const editor = createEditor();
115+ editor.children = initialValue;
116+
117+ editor.tf.wrapNodes(
118+ { children: [], type: 'blockquote' },
119+ { at: [0, 0] }
120+ );
121+
122+ expect(editor.children).toEqual(expectedValue);
123+ });
124+ });
125+ ```
126+
127+ - **Utility Function Testing:**
128+ ```typescript
129+ // ✅ DO: Test pure functions with multiple scenarios
130+ describe('isImageUrl', () => {
131+ it('should return true for image URLs', () => {
132+ expect(isImageUrl('https://example.com/image.jpg')).toBe(true);
133+ expect(isImageUrl('https://example.com/image.png')).toBe(true);
134+ });
135+
136+ it('should return false for non-image URLs', () => {
137+ expect(isImageUrl('https://example.com/file.pdf')).toBe(false);
138+ expect(isImageUrl('not-a-url')).toBe(false);
139+ });
140+ });
141+ ```
142+
143+ - **Async Testing:**
144+ ```typescript
145+ // ✅ DO: Use async/await for asynchronous tests
146+ it('should handle async operations', async () => {
147+ const result = await someAsyncFunction();
148+ expect(result).toBeDefined();
149+ });
150+ ```
151+
152+ - **Mock Usage:**
153+ ```typescript
154+ // ✅ DO: Mock external dependencies
155+ jest.mock('nanoid', () => ({
156+ nanoid: () => 'mock-id',
157+ }));
158+
159+ // ✅ DO: Spy on console methods in setup
160+ jest.spyOn(global.console, 'warn').mockImplementation(() => jest.fn());
161+
162+ // ✅ DO: Mock specific functions
163+ const mockOnChange = jest.fn();
164+ const mockOnKeyDown = jest.fn();
165+
166+ // ✅ DO: Mock complex objects
167+ jest.spyOn(JSON, 'parse').mockReturnValue(<fragment>mocked</fragment>);
168+ ```
169+
170+ - **Test Organization:**
171+ ```typescript
172+ // ✅ DO: Group related tests logically
173+ describe('FeatureName', () => {
174+ describe('when condition A', () => {
175+ it('should behavior X', () => {
176+ // test
177+ });
178+ });
179+
180+ describe('when condition B', () => {
181+ it('should behavior Y', () => {
182+ // test
183+ });
184+ });
185+ });
186+ ```
187+
188+ - **Common Test Patterns:**
189+ - Test both positive and negative cases
190+ - Test edge cases and boundary conditions
191+ - Keep tests focused on a single behavior
192+ - Use descriptive test names that explain the expected behavior
193+ - Avoid testing implementation details
194+ - Test the public API, not internal functions
195+
196+ - **React Component Testing:**
197+ ```typescript
198+ // ✅ DO: Use React Testing Library for components
199+ import { render } from '@testing-library/react';
200+
201+ it('should render component', () => {
202+ const { getByText } = render(<MyComponent />);
203+ expect(getByText('Expected Text')).toBeInTheDocument();
204+ });
205+ ```
206+
207+ - **Type Testing:**
208+ ```typescript
209+ // ✅ DO: Test TypeScript types when relevant
210+ it('should have correct types', () => {
211+ let a: SlatePluginContext<Config> = {} as any;
212+ const b = getEditorPlugin(editor, plugin);
213+ a = b; // Should compile without errors
214+ expect(a).toBeDefined();
215+ });
216+ ```
217+
218+ - **DataTransfer Testing:**
219+ ```typescript
220+ // ✅ DO: Create DataTransfer for paste/drop testing
221+ import { createDataTransfer } from '@platejs/test-utils';
222+
223+ const dataTransfer = createDataTransfer(new Map([
224+ ['text/html', '<p>Hello</p>'],
225+ ['text/plain', 'Hello']
226+ ]));
227+
228+ // Or inline:
229+ const dataTransfer = {
230+ constructor: { name: 'DataTransfer' },
231+ getData: (format: string) =>
232+ format === 'text/html' && '<p>Hello</p>',
233+ } as any;
234+
235+ editor.tf.insertData(dataTransfer);
236+ ```
237+
238+ - **Fragment and Selection Testing:**
239+ ```typescript
240+ // ✅ DO: Use cursor and selection markers in JSX
241+ const input = (
242+ <editor>
243+ <hp>
244+ test<cursor />
245+ </hp>
246+ </editor>
247+ ) as any as SlateEditor;
248+
249+ const editor = createPlateEditor({
250+ selection: input.selection,
251+ value: input.children,
252+ });
253+ ```
254+
255+ - **Plugin Configuration Testing:**
256+ ```typescript
257+ // ✅ DO: Test plugin configuration and extension
258+ const TestPlugin = BasePlugin.configure({
259+ options: { newOption: 'value' },
260+ }).extendEditorApi(() => ({
261+ newMethod: () => 'result',
262+ }));
263+
264+ // Test plugin resolution
265+ const editor = createPlateEditor({ plugins: [TestPlugin] });
266+ const resolvedPlugin = editor.plugins.test;
267+ expect(resolvedPlugin.options.newOption).toBe('value');
268+ ```
269+
270+ - **Store and Hook Testing:**
271+ ```typescript
272+ // ✅ DO: Use renderHook for testing hooks
273+ import { renderHook } from '@testing-library/react';
274+
275+ const wrapper = ({ children }: any) => (
276+ <PlateController {...props}>{children}</PlateController>
277+ );
278+
279+ const { result } = renderHook(() => useMyHook(), { wrapper });
280+ expect(result.current).toBe(expectedValue);
281+ ```
282+
283+ - **Snapshot Testing:**
284+ ```typescript
285+ // ✅ DO: Use snapshots for serialization tests
286+ const result = serializeMd(editor, { value: slateNodes });
287+ expect(result).toMatchSnapshot();
288+ ```
289+
290+ - **Editor Transform Testing:**
291+ ```typescript
292+ // ✅ DO: Test editor transforms thoroughly
293+ editor.tf.insertText('Hello');
294+ editor.tf.delete({ distance: 5, reverse: true });
295+ editor.tf.wrapNodes({ type: 'blockquote' }, { at: [0, 0] });
296+ editor.tf.insertFragment([{ type: 'p', children: [{ text: 'new' }] }]);
297+ ```
298+
299+ - **Error Testing:**
300+ ```typescript
301+ // ✅ DO: Test error conditions
302+ expect(() => {
303+ editor.api.debug.error('Test error', 'TEST_ERROR');
304+ }).toThrow(PlateError);
305+
306+ try {
307+ someFunction();
308+ } catch (error) {
309+ expect(error).toBeInstanceOf(CustomError);
310+ expect((error as CustomError).message).toBe('Expected message');
311+ }
312+ ```
313+
314+ - **Logger and Debug Testing:**
315+ ```typescript
316+ // ✅ DO: Mock loggers for debug testing
317+ const mockLogger = jest.fn();
318+ const editor = createPlateEditor({
319+ plugins: [
320+ DebugPlugin.configure({
321+ options: { logger: { log: mockLogger } as any },
322+ }),
323+ ],
324+ });
325+
326+ editor.api.debug.log('Test message');
327+ expect(mockLogger).toHaveBeenCalledWith('Test message', expect.any(String));
328+ ```
329+
330+ - **HTML Deserialization Testing:**
331+ ```typescript
332+ // ✅ DO: Test HTML deserialization
333+ import { getHtmlDocument } from '@platejs/test-utils';
334+
335+ const html = '<div>test</div>';
336+ const element = getHtmlDocument(html).body;
337+
338+ const result = deserializeHtml(editor, { element });
339+ expect(result).toEqual(expectedOutput);
340+ ```
341+
342+ - **Multiple Test Scenarios:**
343+ ```typescript
344+ // ✅ DO: Use describe blocks for different scenarios
345+ describe('when condition is true', () => {
346+ it('should behave one way', () => {});
347+ });
348+
349+ describe('when condition is false', () => {
350+ it('should behave another way', () => {});
351+ });
352+ ```
353+
354+ - **Type Testing with Generics:**
355+ ```typescript
356+ // ✅ DO: Test generic type constraints
357+ type Config = PluginConfig<'test', { option: string }>;
358+ const plugin = createTSlatePlugin<Config>({ key: 'test' });
359+
360+ // Should compile without errors
361+ const typed: SlatePluginContext<Config> = getEditorPlugin(editor, plugin);
362+ ```
363+
364+ - **Test Utilities:**
365+ - Use `createTestEditor()` for tests requiring a configured editor
366+ - Use `createPlateTestEditor()` for async testing with test harness
367+ - Use `resolvePluginTest()` for testing plugin resolution
368+ - Use `@testing-library/jest-dom` matchers like `toBeInTheDocument()`
369+ - Mock `nanoid` for consistent IDs in tests
370+ - `createDataTransfer()` for DataTransfer mocking
371+ - `getHtmlDocument()` for HTML parsing in tests
372+
373+ - **Common Patterns to Avoid:**
374+ ```typescript
375+ // ❌ DON'T: Use relative imports in tests
376+ import { myFunction } from '../src/myFunction';
377+
378+ // ✅ DO: Use package imports
379+ import { myFunction } from '@platejs/package-name';
380+
381+ // ❌ DON'T: Test implementation details
382+ expect(privateFunction).toHaveBeenCalled();
383+
384+ // ✅ DO: Test public API behavior
385+ expect(editor.api.publicMethod()).toBe(expectedResult);
386+ ```
387+
388+ - **Testing Best Practices Summary:**
389+ - Write tests that read like documentation
390+ - Focus on behavior, not implementation
391+ - Use descriptive test names: "should [expected behavior] when [condition]"
392+ - Keep tests isolated and independent
393+ - Mock external dependencies, not internal functions
394+ - Use the simplest approach that properly tests the feature
395+ - Refer to existing tests in the same package for patterns
396+
397+ Follow existing test patterns in the codebase and maintain consistency with the testing conventions.
0 commit comments