@pie-lib/editable-html-tip-tap 1.2.0-next.11 → 1.2.0-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/lib/components/CharacterPicker.js +1 -0
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +9 -1
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +61 -42
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/choice.js +13 -6
- package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +8 -2
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/math.js +1 -0
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/responseArea.js +2 -3
- package/lib/extensions/responseArea.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/EditableHtml.test.jsx +35 -0
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +10 -1
- package/src/components/MenuBar.jsx +49 -23
- package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
- package/src/components/__tests__/InlineDropdown.test.jsx +149 -0
- package/src/components/__tests__/MenuBar.test.jsx +32 -0
- package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
- package/src/components/respArea/DragInTheBlank/choice.jsx +31 -4
- package/src/components/respArea/InlineDropdown.jsx +10 -1
- package/src/extensions/__tests__/math.test.js +327 -0
- package/src/extensions/__tests__/responseArea.test.js +157 -0
- package/src/extensions/math.js +1 -0
- package/src/extensions/responseArea.js +2 -7
|
@@ -184,4 +184,153 @@ describe('InlineDropdown', () => {
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
it('passes editorCallback to InlineDropdownToolbar', async () => {
|
|
189
|
+
const mockToolbarComponent = jest.fn(({ editorCallback }) => {
|
|
190
|
+
editorCallback?.({ instanceId: 'test-instance' });
|
|
191
|
+
return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const mockOptionsWithCallback = {
|
|
195
|
+
respAreaToolbar: jest.fn(() => mockToolbarComponent),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const { queryByTestId } = render(
|
|
199
|
+
<InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('stores toolbar editor instance in ref when editorCallback is called', async () => {
|
|
208
|
+
let capturedCallback;
|
|
209
|
+
const mockToolbarComponent = ({ editorCallback }) => {
|
|
210
|
+
capturedCallback = editorCallback;
|
|
211
|
+
return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const mockOptionsWithCallback = {
|
|
215
|
+
respAreaToolbar: jest.fn(() => mockToolbarComponent),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const { queryByTestId } = render(
|
|
219
|
+
<InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Verify callback exists
|
|
227
|
+
expect(capturedCallback).toBeDefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('handles click outside logic with data-toolbar-for attribute', async () => {
|
|
231
|
+
const editorWithInstanceId = {
|
|
232
|
+
...mockEditor,
|
|
233
|
+
instanceId: 'editor-123',
|
|
234
|
+
_toolbarOpened: false,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Mock the toolbar callback to set the toolbar editor instance
|
|
238
|
+
let capturedCallback;
|
|
239
|
+
const mockToolbarComponent = ({ editorCallback }) => {
|
|
240
|
+
React.useEffect(() => {
|
|
241
|
+
capturedCallback = editorCallback;
|
|
242
|
+
if (editorCallback) {
|
|
243
|
+
editorCallback({ instanceId: 'editor-123' });
|
|
244
|
+
}
|
|
245
|
+
}, [editorCallback]);
|
|
246
|
+
return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const mockOptionsWithCallback = {
|
|
250
|
+
respAreaToolbar: jest.fn(() => mockToolbarComponent),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const { container, queryByTestId } = render(
|
|
254
|
+
<InlineDropdown
|
|
255
|
+
{...defaultProps}
|
|
256
|
+
editor={editorWithInstanceId}
|
|
257
|
+
options={mockOptionsWithCallback}
|
|
258
|
+
selected={true}
|
|
259
|
+
/>,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Create an element with data-toolbar-for attribute
|
|
267
|
+
const otherToolbar = document.createElement('div');
|
|
268
|
+
otherToolbar.setAttribute('data-toolbar-for', 'editor-456');
|
|
269
|
+
document.body.appendChild(otherToolbar);
|
|
270
|
+
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
fireEvent.mouseDown(otherToolbar);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Cleanup
|
|
276
|
+
document.body.removeChild(otherToolbar);
|
|
277
|
+
expect(container).toBeInTheDocument();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('does not close when clicking inside same editor toolbar', async () => {
|
|
281
|
+
const editorWithInstanceId = {
|
|
282
|
+
...mockEditor,
|
|
283
|
+
instanceId: 'editor-123',
|
|
284
|
+
_toolbarOpened: false,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
let toolbarEditorInstance;
|
|
288
|
+
const mockToolbarComponent = ({ editorCallback }) => {
|
|
289
|
+
React.useEffect(() => {
|
|
290
|
+
if (editorCallback) {
|
|
291
|
+
editorCallback({ instanceId: 'editor-123' });
|
|
292
|
+
toolbarEditorInstance = { instanceId: 'editor-123' };
|
|
293
|
+
}
|
|
294
|
+
}, [editorCallback]);
|
|
295
|
+
return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const mockOptionsWithCallback = {
|
|
299
|
+
respAreaToolbar: jest.fn(() => mockToolbarComponent),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
render(
|
|
303
|
+
<InlineDropdown
|
|
304
|
+
{...defaultProps}
|
|
305
|
+
editor={editorWithInstanceId}
|
|
306
|
+
options={mockOptionsWithCallback}
|
|
307
|
+
selected={true}
|
|
308
|
+
/>,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(mockOptionsWithCallback.respAreaToolbar).toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('checks editor._toolbarOpened in click outside handler', async () => {
|
|
317
|
+
const editorWithToolbarOpened = {
|
|
318
|
+
...mockEditor,
|
|
319
|
+
_toolbarOpened: true,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const { queryByTestId } = render(
|
|
323
|
+
<InlineDropdown {...defaultProps} editor={editorWithToolbarOpened} selected={true} />,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// When _toolbarOpened is true, clicking outside should not close
|
|
331
|
+
fireEvent.mouseDown(document.body);
|
|
332
|
+
|
|
333
|
+
// Toolbar should still be visible
|
|
334
|
+
expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
|
|
335
|
+
});
|
|
187
336
|
});
|
|
@@ -214,4 +214,36 @@ describe('StyledMenuBar', () => {
|
|
|
214
214
|
toolbar?.dispatchEvent(event);
|
|
215
215
|
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
it('calculates hasTextSelectionInTable correctly when selection is not empty in table', () => {
|
|
219
|
+
// This test verifies the hasTextSelectionInTable state computation
|
|
220
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
221
|
+
expect(container).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('hides table manipulation buttons when text is selected in table', () => {
|
|
225
|
+
// When hasTextSelectionInTable is true, table row/column buttons should be hidden
|
|
226
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
227
|
+
// The component should render but table manipulation buttons should be conditional
|
|
228
|
+
expect(container).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('shows table manipulation buttons when no text is selected in table', () => {
|
|
232
|
+
// When hasTextSelectionInTable is false, table row/column buttons should be visible
|
|
233
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
234
|
+
expect(container).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('shows text formatting buttons regardless of table state', () => {
|
|
238
|
+
// Bold, italic, etc. should always be visible when their plugin is active
|
|
239
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['bold', 'italic', 'underline']} />);
|
|
240
|
+
const buttons = container.querySelectorAll('button');
|
|
241
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not hide text formatting buttons when in table', () => {
|
|
245
|
+
// Verify that the removal of "|| state.isTable" condition works correctly
|
|
246
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
247
|
+
expect(container).toBeInTheDocument();
|
|
248
|
+
});
|
|
217
249
|
});
|
|
@@ -33,7 +33,11 @@ const DragDrop = (props) => {
|
|
|
33
33
|
|
|
34
34
|
// console.log({nodeProps.children})
|
|
35
35
|
return (
|
|
36
|
-
<NodeViewWrapper
|
|
36
|
+
<NodeViewWrapper
|
|
37
|
+
className="drag-in-the-blank"
|
|
38
|
+
data-selected={selected}
|
|
39
|
+
style={{ display: 'inline', whiteSpace: 'normal' }}
|
|
40
|
+
>
|
|
37
41
|
<span
|
|
38
42
|
{...attributes}
|
|
39
43
|
style={{
|
|
@@ -52,6 +56,7 @@ const DragDrop = (props) => {
|
|
|
52
56
|
pos={pos}
|
|
53
57
|
value={attributes}
|
|
54
58
|
duplicates={options.duplicates}
|
|
59
|
+
selected={selected}
|
|
55
60
|
onChange={(choice) => onValueChange(editor, node, pos, choice)}
|
|
56
61
|
removeResponse={(choice) => onRemoveResponse(editor, node, choice)}
|
|
57
62
|
></DragDropTile>
|
|
@@ -15,7 +15,7 @@ const StyledContent = styled('span')(({ theme }) => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
export function BlankContent({ n, children, isDragging, isOver, dragItem, value }) {
|
|
18
|
+
export function BlankContent({ n, children, isDragging, isOver, dragItem, value, selected }) {
|
|
19
19
|
const [hoveredElementSize, setHoveredElementSize] = useState(null);
|
|
20
20
|
const elementRef = useRef(null);
|
|
21
21
|
|
|
@@ -56,15 +56,22 @@ export function BlankContent({ n, children, isDragging, isOver, dragItem, value
|
|
|
56
56
|
const hasGrip = finalLabel !== '\u00A0';
|
|
57
57
|
const isPreview = dragItem && isOver;
|
|
58
58
|
|
|
59
|
+
const borderStyle = selected
|
|
60
|
+
? `2px solid ${color.primaryDark()}`
|
|
61
|
+
: isPreview
|
|
62
|
+
? `1px solid ${color.defaults.BORDER_DARK}`
|
|
63
|
+
: `1px solid ${color.defaults.BORDER_LIGHT}`;
|
|
64
|
+
|
|
59
65
|
return (
|
|
60
66
|
<div
|
|
61
67
|
ref={elementRef}
|
|
68
|
+
className={selected ? 'selected' : undefined}
|
|
62
69
|
style={{
|
|
63
70
|
display: 'inline-flex',
|
|
64
71
|
minWidth: '178px',
|
|
65
72
|
minHeight: '36px',
|
|
66
73
|
background: isPreview ? `${color.defaults.BORDER_LIGHT}` : `${color.defaults.WHITE}`,
|
|
67
|
-
border:
|
|
74
|
+
border: borderStyle,
|
|
68
75
|
boxSizing: 'border-box',
|
|
69
76
|
borderRadius: '3px',
|
|
70
77
|
overflow: 'hidden',
|
|
@@ -104,9 +111,21 @@ BlankContent.propTypes = {
|
|
|
104
111
|
isOver: PropTypes.bool,
|
|
105
112
|
dragItem: PropTypes.object,
|
|
106
113
|
value: PropTypes.object,
|
|
114
|
+
selected: PropTypes.bool,
|
|
107
115
|
};
|
|
108
116
|
|
|
109
|
-
function DragDropChoice({
|
|
117
|
+
function DragDropChoice({
|
|
118
|
+
value,
|
|
119
|
+
disabled,
|
|
120
|
+
instanceId,
|
|
121
|
+
children,
|
|
122
|
+
n,
|
|
123
|
+
onChange,
|
|
124
|
+
removeResponse,
|
|
125
|
+
duplicates,
|
|
126
|
+
pos,
|
|
127
|
+
selected,
|
|
128
|
+
}) {
|
|
110
129
|
const {
|
|
111
130
|
attributes: dragAttributes,
|
|
112
131
|
listeners: dragListeners,
|
|
@@ -196,7 +215,14 @@ function DragDropChoice({ value, disabled, instanceId, children, n, onChange, re
|
|
|
196
215
|
};
|
|
197
216
|
|
|
198
217
|
const dragContent = (
|
|
199
|
-
<BlankContent
|
|
218
|
+
<BlankContent
|
|
219
|
+
n={n}
|
|
220
|
+
isDragging={isDragging}
|
|
221
|
+
isOver={isOver}
|
|
222
|
+
dragItem={dragItem?.data?.current}
|
|
223
|
+
value={value}
|
|
224
|
+
selected={selected}
|
|
225
|
+
>
|
|
200
226
|
{children}
|
|
201
227
|
</BlankContent>
|
|
202
228
|
);
|
|
@@ -223,6 +249,7 @@ DragDropChoice.propTypes = {
|
|
|
223
249
|
onChange: PropTypes.func.isRequired,
|
|
224
250
|
removeResponse: PropTypes.func.isRequired,
|
|
225
251
|
duplicates: PropTypes.bool,
|
|
252
|
+
selected: PropTypes.bool,
|
|
226
253
|
};
|
|
227
254
|
|
|
228
255
|
export default DragDropChoice;
|
|
@@ -12,6 +12,7 @@ const InlineDropdown = (props) => {
|
|
|
12
12
|
// Needed because items with values inside have different positioning for some reason
|
|
13
13
|
const html = value || '<div> </div>';
|
|
14
14
|
const toolbarRef = useRef(null);
|
|
15
|
+
const toolbarEditor = useRef(null);
|
|
15
16
|
const [showToolbar, setShowToolbar] = useState(false);
|
|
16
17
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
17
18
|
const InlineDropdownToolbar = options.respAreaToolbar(node, editor, () => {});
|
|
@@ -41,7 +42,11 @@ const InlineDropdown = (props) => {
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
const handleClickOutside = (event) => {
|
|
45
|
+
const insideSomeEditor = event.target.closest('[data-toolbar-for]');
|
|
46
|
+
|
|
44
47
|
if (
|
|
48
|
+
(!insideSomeEditor || insideSomeEditor.dataset.toolbarFor !== toolbarEditor.current.instanceId) &&
|
|
49
|
+
!editor._toolbarOpened &&
|
|
45
50
|
toolbarRef.current &&
|
|
46
51
|
!toolbarRef.current.contains(event.target) &&
|
|
47
52
|
!event.target.closest('[data-inline-node]')
|
|
@@ -116,7 +121,11 @@ const InlineDropdown = (props) => {
|
|
|
116
121
|
{showToolbar &&
|
|
117
122
|
ReactDOM.createPortal(
|
|
118
123
|
<div ref={toolbarRef} style={{ zIndex: 1 }}>
|
|
119
|
-
<InlineDropdownToolbar
|
|
124
|
+
<InlineDropdownToolbar
|
|
125
|
+
editorCallback={(instance) => {
|
|
126
|
+
toolbarEditor.current = instance;
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
120
129
|
</div>,
|
|
121
130
|
document.body,
|
|
122
131
|
)}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { MathNode, MathNodeView } from '../math';
|
|
4
|
+
|
|
5
|
+
jest.mock('@tiptap/react', () => ({
|
|
6
|
+
NodeViewWrapper: ({ children, ...props }) => (
|
|
7
|
+
<div data-testid="node-view-wrapper" {...props}>
|
|
8
|
+
{children}
|
|
9
|
+
</div>
|
|
10
|
+
),
|
|
11
|
+
ReactNodeViewRenderer: jest.fn((component) => component),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('react-dom', () => ({
|
|
15
|
+
...jest.requireActual('react-dom'),
|
|
16
|
+
createPortal: (node) => node,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock('@pie-lib/math-toolbar', () => {
|
|
20
|
+
const React = require('react');
|
|
21
|
+
return {
|
|
22
|
+
MathPreview: ({ latex }) => <div data-testid="math-preview">{latex}</div>,
|
|
23
|
+
MathToolbar: ({ latex, onChange, onDone }) => {
|
|
24
|
+
const [localLatex, setLocalLatex] = React.useState(latex);
|
|
25
|
+
return (
|
|
26
|
+
<div data-testid="math-toolbar">
|
|
27
|
+
<input
|
|
28
|
+
data-testid="math-input"
|
|
29
|
+
value={localLatex}
|
|
30
|
+
onChange={(e) => {
|
|
31
|
+
setLocalLatex(e.target.value);
|
|
32
|
+
onChange(e.target.value);
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
<button data-testid="done-button" onClick={() => onDone(localLatex)}>
|
|
36
|
+
Done
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
jest.mock('@pie-lib/math-rendering', () => ({
|
|
45
|
+
wrapMath: (latex, wrapper) => latex,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock('@tiptap/core', () => ({
|
|
49
|
+
Node: {
|
|
50
|
+
create: jest.fn((config) => config),
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
jest.mock('prosemirror-state', () => ({
|
|
55
|
+
Plugin: jest.fn(function (config) {
|
|
56
|
+
return config;
|
|
57
|
+
}),
|
|
58
|
+
PluginKey: jest.fn(function (key) {
|
|
59
|
+
this.key = key;
|
|
60
|
+
}),
|
|
61
|
+
TextSelection: {
|
|
62
|
+
create: jest.fn((doc, pos) => ({ type: 'text', pos })),
|
|
63
|
+
},
|
|
64
|
+
NodeSelection: {
|
|
65
|
+
create: jest.fn((doc, pos) => ({ type: 'node', pos })),
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
describe('MathNode', () => {
|
|
70
|
+
describe('configuration', () => {
|
|
71
|
+
it('has correct name', () => {
|
|
72
|
+
expect(MathNode.name).toBe('math');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('is inline', () => {
|
|
76
|
+
expect(MathNode.inline).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('is in inline group', () => {
|
|
80
|
+
expect(MathNode.group).toBe('inline');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('is atomic', () => {
|
|
84
|
+
expect(MathNode.atom).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('addAttributes', () => {
|
|
89
|
+
it('returns required attributes', () => {
|
|
90
|
+
const attributes = MathNode.addAttributes();
|
|
91
|
+
|
|
92
|
+
expect(attributes).toHaveProperty('latex');
|
|
93
|
+
expect(attributes).toHaveProperty('wrapper');
|
|
94
|
+
expect(attributes).toHaveProperty('html');
|
|
95
|
+
|
|
96
|
+
expect(attributes.latex).toEqual({ default: '' });
|
|
97
|
+
expect(attributes.wrapper).toEqual({ default: null });
|
|
98
|
+
expect(attributes.html).toEqual({ default: null });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('parseHTML', () => {
|
|
103
|
+
it('returns parsing rules for latex', () => {
|
|
104
|
+
const rules = MathNode.parseHTML();
|
|
105
|
+
|
|
106
|
+
expect(Array.isArray(rules)).toBe(true);
|
|
107
|
+
expect(rules).toHaveLength(2);
|
|
108
|
+
expect(rules[0]).toHaveProperty('tag', 'span[data-latex]');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns parsing rules for mathml', () => {
|
|
112
|
+
const rules = MathNode.parseHTML();
|
|
113
|
+
expect(rules[1]).toHaveProperty('tag', 'span[data-type="mathml"]');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('renderHTML', () => {
|
|
118
|
+
it('renders mathml when html attribute is present', () => {
|
|
119
|
+
const result = MathNode.renderHTML({
|
|
120
|
+
HTMLAttributes: {
|
|
121
|
+
html: '<math><mi>x</mi></math>',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result[0]).toBe('span');
|
|
126
|
+
expect(result[1]).toHaveProperty('data-type', 'mathml');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('renders latex when html attribute is not present', () => {
|
|
130
|
+
const result = MathNode.renderHTML({
|
|
131
|
+
HTMLAttributes: {
|
|
132
|
+
latex: 'x^2',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result[0]).toBe('span');
|
|
137
|
+
expect(result[1]).toHaveProperty('data-latex', '');
|
|
138
|
+
expect(result[1]).toHaveProperty('data-raw', 'x^2');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('addCommands', () => {
|
|
143
|
+
it('returns insertMath command', () => {
|
|
144
|
+
const commands = MathNode.addCommands();
|
|
145
|
+
|
|
146
|
+
expect(commands).toHaveProperty('insertMath');
|
|
147
|
+
expect(typeof commands.insertMath).toBe('function');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('addNodeView', () => {
|
|
152
|
+
it('returns ReactNodeViewRenderer result', () => {
|
|
153
|
+
const result = MathNode.addNodeView();
|
|
154
|
+
|
|
155
|
+
expect(result).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('MathNodeView', () => {
|
|
161
|
+
const createMockEditor = () => ({
|
|
162
|
+
state: {
|
|
163
|
+
selection: {
|
|
164
|
+
from: 0,
|
|
165
|
+
to: 1,
|
|
166
|
+
},
|
|
167
|
+
tr: {
|
|
168
|
+
setSelection: jest.fn().mockReturnThis(),
|
|
169
|
+
},
|
|
170
|
+
doc: {},
|
|
171
|
+
},
|
|
172
|
+
view: {
|
|
173
|
+
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
|
|
174
|
+
dispatch: jest.fn(),
|
|
175
|
+
},
|
|
176
|
+
commands: {
|
|
177
|
+
focus: jest.fn(),
|
|
178
|
+
},
|
|
179
|
+
instanceId: 'editor-123',
|
|
180
|
+
_toolbarOpened: false,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const mockNode = {
|
|
184
|
+
attrs: {
|
|
185
|
+
latex: 'x^2',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let defaultProps;
|
|
190
|
+
|
|
191
|
+
beforeAll(() => {
|
|
192
|
+
Object.defineProperty(document.body, 'getBoundingClientRect', {
|
|
193
|
+
value: jest.fn(() => ({ top: 0, left: 0 })),
|
|
194
|
+
configurable: true,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
jest.clearAllMocks();
|
|
200
|
+
defaultProps = {
|
|
201
|
+
node: mockNode,
|
|
202
|
+
updateAttributes: jest.fn(),
|
|
203
|
+
editor: createMockEditor(),
|
|
204
|
+
selected: false,
|
|
205
|
+
options: {},
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('renders without crashing', () => {
|
|
210
|
+
const { container } = render(<MathNodeView {...defaultProps} />);
|
|
211
|
+
expect(container).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('renders NodeViewWrapper', () => {
|
|
215
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} />);
|
|
216
|
+
expect(getByTestId('node-view-wrapper')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('displays math preview', () => {
|
|
220
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} />);
|
|
221
|
+
expect(getByTestId('math-preview')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('shows toolbar when selected', async () => {
|
|
225
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(getByTestId('math-toolbar')).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does not show toolbar when not selected', () => {
|
|
232
|
+
const { queryByTestId } = render(<MathNodeView {...defaultProps} selected={false} />);
|
|
233
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('adds data-toolbar-for attribute with editor instanceId', async () => {
|
|
237
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
238
|
+
await waitFor(() => {
|
|
239
|
+
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
240
|
+
expect(toolbar).toHaveAttribute('data-toolbar-for', 'editor-123');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('renders toolbar with correct position', async () => {
|
|
245
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
248
|
+
expect(toolbar).toHaveStyle({ position: 'absolute' });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('calls updateAttributes when latex changes', async () => {
|
|
253
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
const input = getByTestId('math-input');
|
|
256
|
+
fireEvent.change(input, { target: { value: 'y^2' } });
|
|
257
|
+
});
|
|
258
|
+
expect(defaultProps.updateAttributes).toHaveBeenCalledWith({ latex: 'y^2' });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('closes toolbar and updates attributes when done', async () => {
|
|
262
|
+
const updateAttributes = jest.fn();
|
|
263
|
+
const { getByTestId } = render(
|
|
264
|
+
<MathNodeView {...defaultProps} updateAttributes={updateAttributes} selected={true} />,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(getByTestId('done-button')).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const doneButton = getByTestId('done-button');
|
|
272
|
+
fireEvent.click(doneButton);
|
|
273
|
+
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('sets editor._toolbarOpened when toolbar is shown', async () => {
|
|
280
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(getByTestId('math-toolbar')).toBeInTheDocument();
|
|
283
|
+
expect(defaultProps.editor._toolbarOpened).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('unsets editor._toolbarOpened when toolbar is closed', async () => {
|
|
288
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
289
|
+
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(getByTestId('done-button')).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const doneButton = getByTestId('done-button');
|
|
295
|
+
fireEvent.click(doneButton);
|
|
296
|
+
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
expect(defaultProps.editor._toolbarOpened).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('closes toolbar on outside click', async () => {
|
|
303
|
+
const { queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
304
|
+
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
fireEvent.mouseDown(document.body);
|
|
310
|
+
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('renders with empty latex', () => {
|
|
317
|
+
const nodeWithEmptyLatex = { attrs: { latex: '' } };
|
|
318
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} node={nodeWithEmptyLatex} />);
|
|
319
|
+
expect(getByTestId('math-preview')).toBeInTheDocument();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('has correct styling on NodeViewWrapper', () => {
|
|
323
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} />);
|
|
324
|
+
const wrapper = getByTestId('node-view-wrapper');
|
|
325
|
+
expect(wrapper).toHaveStyle({ display: 'inline-flex', cursor: 'pointer' });
|
|
326
|
+
});
|
|
327
|
+
});
|