Skip to content

Commit a494347

Browse files
committed
ci
1 parent f5f4326 commit a494347

File tree

4 files changed

+455
-52
lines changed

4 files changed

+455
-52
lines changed

.cursor/rules/unit-testing.mdc

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
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

Comments
 (0)