]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.tsx
b11b99490b681ff63805c53dad5c23b282cafc01
[bookstack] / resources / js / wysiwyg / lexical / table / __tests__ / unit / LexicalTableNode.test.tsx
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import {$insertDataTransferForRichText} from '@lexical/clipboard';
10 import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
11 import {
12   $createTableNode,
13   $createTableNodeWithDimensions,
14   $createTableSelection,
15 } from '@lexical/table';
16 import {
17   $createParagraphNode,
18   $createTextNode,
19   $getRoot,
20   $getSelection,
21   $isRangeSelection,
22   $selectAll,
23   $setSelection,
24   CUT_COMMAND,
25   ParagraphNode,
26 } from 'lexical';
27 import {
28   DataTransferMock,
29   initializeUnitTest,
30   invariant,
31 } from 'lexical/src/__tests__/utils';
32
33 import {$getElementForTableNode, TableNode} from '../../LexicalTableNode';
34
35 export class ClipboardDataMock {
36   getData: jest.Mock<string, [string]>;
37   setData: jest.Mock<void, [string, string]>;
38
39   constructor() {
40     this.getData = jest.fn();
41     this.setData = jest.fn();
42   }
43 }
44
45 export class ClipboardEventMock extends Event {
46   clipboardData: ClipboardDataMock;
47
48   constructor(type: string, options?: EventInit) {
49     super(type, options);
50     this.clipboardData = new ClipboardDataMock();
51   }
52 }
53
54 global.document.execCommand = function execCommandMock(
55   commandId: string,
56   showUI?: boolean,
57   value?: string,
58 ): boolean {
59   return true;
60 };
61 Object.defineProperty(window, 'ClipboardEvent', {
62   value: new ClipboardEventMock('cut'),
63 });
64
65 const editorConfig = Object.freeze({
66   namespace: '',
67   theme: {
68     table: 'test-table-class',
69   },
70 });
71
72 describe('LexicalTableNode tests', () => {
73   initializeUnitTest(
74     (testEnv) => {
75       beforeEach(async () => {
76         const {editor} = testEnv;
77         await editor.update(() => {
78           const root = $getRoot();
79           const paragraph = $createParagraphNode();
80           root.append(paragraph);
81           paragraph.select();
82         });
83       });
84
85       test('TableNode.constructor', async () => {
86         const {editor} = testEnv;
87
88         await editor.update(() => {
89           const tableNode = $createTableNode();
90
91           expect(tableNode).not.toBe(null);
92         });
93
94         expect(() => $createTableNode()).toThrow();
95       });
96
97       test('TableNode.createDOM()', async () => {
98         const {editor} = testEnv;
99
100         await editor.update(() => {
101           const tableNode = $createTableNode();
102
103           expect(tableNode.createDOM(editorConfig).outerHTML).toBe(
104             `<table class="${editorConfig.theme.table}"></table>`,
105           );
106         });
107       });
108
109       test('Copy table from an external source', async () => {
110         const {editor} = testEnv;
111
112         const dataTransfer = new DataTransferMock();
113         dataTransfer.setData(
114           'text/html',
115           '<html><body><meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-16a69100-7fff-6cb9-b829-cb1def16a58d"><div dir="ltr" style="margin-left:0pt;" align="left"><table style="border:none;border-collapse:collapse;table-layout:fixed;width:468pt"><colgroup><col /><col /></colgroup><tbody><tr style="height:22.015pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Hello there</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">General Kenobi!</span></p></td></tr><tr style="height:22.015pt"><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Lexical is nice</span></p></td><td style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><br /></td></tr></tbody></table></div></b><!--EndFragment--></body></html>',
116         );
117         await editor.update(() => {
118           const selection = $getSelection();
119           invariant(
120             $isRangeSelection(selection),
121             'isRangeSelection(selection)',
122           );
123           $insertDataTransferForRichText(dataTransfer, selection, editor);
124         });
125         // Make sure paragraph is inserted inside empty cells
126         const emptyCell = '<td><p><br></p></td>';
127         expect(testEnv.innerHTML).toBe(
128           `<table><tr><td><p dir="ltr"><span data-lexical-text="true">Hello there</span></p></td><td><p dir="ltr"><span data-lexical-text="true">General Kenobi!</span></p></td></tr><tr><td><p dir="ltr"><span data-lexical-text="true">Lexical is nice</span></p></td>${emptyCell}</tr></table>`,
129         );
130       });
131
132       test('Copy table from an external source like gdoc with formatting', async () => {
133         const {editor} = testEnv;
134
135         const dataTransfer = new DataTransferMock();
136         dataTransfer.setData(
137           'text/html',
138           '<google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none" data-sheets-root="1"><colgroup><col width="100"/><col width="189"/><col width="171"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-weight:bold;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Surface&quot;}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;MWP_WORK_LS_COMPOSER&quot;}">MWP_WORK_LS_COMPOSER</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:underline;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;Lexical&quot;}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;XDS_RICH_TEXT_AREA&quot;}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;sdvd sdfvsfs&quot;}" data-sheets-textstyleruns="{&quot;1&quot;:0}{&quot;1&quot;:5,&quot;2&quot;:{&quot;5&quot;:1}}"><span style="font-size:10pt;font-family:Arial;font-style:normal;">sdvd </span><span style="font-size:10pt;font-family:Arial;font-weight:bold;font-style:normal;">sdfvsfs</span></td></tr></tbody></table>',
139         );
140         await editor.update(() => {
141           const selection = $getSelection();
142           invariant(
143             $isRangeSelection(selection),
144             'isRangeSelection(selection)',
145           );
146           $insertDataTransferForRichText(dataTransfer, selection, editor);
147         });
148         expect(testEnv.innerHTML).toBe(
149           `<table><tr style="height: 21px;"><td><p dir="ltr"><strong data-lexical-text="true">Surface</strong></p></td><td><p dir="ltr"><em data-lexical-text="true">MWP_WORK_LS_COMPOSER</em></p></td><td><p style="text-align: right;"><span data-lexical-text="true">77349</span></p></td></tr><tr style="height: 21px;"><td><p dir="ltr"><span data-lexical-text="true">Lexical</span></p></td><td><p dir="ltr"><span data-lexical-text="true">XDS_RICH_TEXT_AREA</span></p></td><td><p dir="ltr"><span data-lexical-text="true">sdvd </span><strong data-lexical-text="true">sdfvsfs</strong></p></td></tr></table>`,
150         );
151       });
152
153       test('Cut table in the middle of a range selection', async () => {
154         const {editor} = testEnv;
155
156         await editor.update(() => {
157           const root = $getRoot();
158           const paragraph = root.getFirstChild<ParagraphNode>();
159           const beforeText = $createTextNode('text before the table');
160           const table = $createTableNodeWithDimensions(4, 4, true);
161           const afterText = $createTextNode('text after the table');
162
163           paragraph?.append(beforeText);
164           paragraph?.append(table);
165           paragraph?.append(afterText);
166         });
167         await editor.update(() => {
168           editor.focus();
169           $selectAll();
170         });
171         await editor.update(() => {
172           editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
173         });
174
175         expect(testEnv.innerHTML).toBe(`<p><br></p>`);
176       });
177
178       test('Cut table as last node in range selection ', async () => {
179         const {editor} = testEnv;
180
181         await editor.update(() => {
182           const root = $getRoot();
183           const paragraph = root.getFirstChild<ParagraphNode>();
184           const beforeText = $createTextNode('text before the table');
185           const table = $createTableNodeWithDimensions(4, 4, true);
186
187           paragraph?.append(beforeText);
188           paragraph?.append(table);
189         });
190         await editor.update(() => {
191           editor.focus();
192           $selectAll();
193         });
194         await editor.update(() => {
195           editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
196         });
197
198         expect(testEnv.innerHTML).toBe(`<p><br></p>`);
199       });
200
201       test('Cut table as first node in range selection ', async () => {
202         const {editor} = testEnv;
203
204         await editor.update(() => {
205           const root = $getRoot();
206           const paragraph = root.getFirstChild<ParagraphNode>();
207           const table = $createTableNodeWithDimensions(4, 4, true);
208           const afterText = $createTextNode('text after the table');
209
210           paragraph?.append(table);
211           paragraph?.append(afterText);
212         });
213         await editor.update(() => {
214           editor.focus();
215           $selectAll();
216         });
217         await editor.update(() => {
218           editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
219         });
220
221         expect(testEnv.innerHTML).toBe(`<p><br></p>`);
222       });
223
224       test('Cut table is whole selection, should remove it', async () => {
225         const {editor} = testEnv;
226
227         await editor.update(() => {
228           const root = $getRoot();
229           const table = $createTableNodeWithDimensions(4, 4, true);
230           root.append(table);
231         });
232         await editor.update(() => {
233           const root = $getRoot();
234           const table = root.getLastChild<TableNode>();
235           if (table) {
236             const DOMTable = $getElementForTableNode(editor, table);
237             if (DOMTable) {
238               table
239                 ?.getCellNodeFromCords(0, 0, DOMTable)
240                 ?.getLastChild<ParagraphNode>()
241                 ?.append($createTextNode('some text'));
242               const selection = $createTableSelection();
243               selection.set(
244                 table.__key,
245                 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
246                 table?.getCellNodeFromCords(3, 3, DOMTable)?.__key || '',
247               );
248               $setSelection(selection);
249               editor.dispatchCommand(CUT_COMMAND, {
250                 preventDefault: () => {},
251                 stopPropagation: () => {},
252               } as ClipboardEvent);
253             }
254           }
255         });
256
257         expect(testEnv.innerHTML).toBe(`<p><br></p>`);
258       });
259
260       test('Cut subsection of table cells, should just clear contents', async () => {
261         const {editor} = testEnv;
262
263         await editor.update(() => {
264           const root = $getRoot();
265           const table = $createTableNodeWithDimensions(4, 4, true);
266           root.append(table);
267         });
268         await editor.update(() => {
269           const root = $getRoot();
270           const table = root.getLastChild<TableNode>();
271           if (table) {
272             const DOMTable = $getElementForTableNode(editor, table);
273             if (DOMTable) {
274               table
275                 ?.getCellNodeFromCords(0, 0, DOMTable)
276                 ?.getLastChild<ParagraphNode>()
277                 ?.append($createTextNode('some text'));
278               const selection = $createTableSelection();
279               selection.set(
280                 table.__key,
281                 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
282                 table?.getCellNodeFromCords(2, 2, DOMTable)?.__key || '',
283               );
284               $setSelection(selection);
285               editor.dispatchCommand(CUT_COMMAND, {
286                 preventDefault: () => {},
287                 stopPropagation: () => {},
288               } as ClipboardEvent);
289             }
290           }
291         });
292
293         expect(testEnv.innerHTML).toBe(
294           `<p><br></p><table><tr><th><p><br></p></th><th><p><br></p></th><th><p><br></p></th><th><p><br></p></th></tr><tr><th><p><br></p></th><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr><tr><th><p><br></p></th><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr><tr><th><p><br></p></th><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr></table>`,
295         );
296       });
297
298       test('Table plain text output validation', async () => {
299         const {editor} = testEnv;
300
301         await editor.update(() => {
302           const root = $getRoot();
303           const table = $createTableNodeWithDimensions(4, 4, true);
304           root.append(table);
305         });
306         await editor.update(() => {
307           const root = $getRoot();
308           const table = root.getLastChild<TableNode>();
309           if (table) {
310             const DOMTable = $getElementForTableNode(editor, table);
311             if (DOMTable) {
312               table
313                 ?.getCellNodeFromCords(0, 0, DOMTable)
314                 ?.getLastChild<ParagraphNode>()
315                 ?.append($createTextNode('1'));
316               table
317                 ?.getCellNodeFromCords(1, 0, DOMTable)
318                 ?.getLastChild<ParagraphNode>()
319                 ?.append($createTextNode(''));
320               table
321                 ?.getCellNodeFromCords(2, 0, DOMTable)
322                 ?.getLastChild<ParagraphNode>()
323                 ?.append($createTextNode('2'));
324               table
325                 ?.getCellNodeFromCords(0, 1, DOMTable)
326                 ?.getLastChild<ParagraphNode>()
327                 ?.append($createTextNode('3'));
328               table
329                 ?.getCellNodeFromCords(1, 1, DOMTable)
330                 ?.getLastChild<ParagraphNode>()
331                 ?.append($createTextNode('4'));
332               table
333                 ?.getCellNodeFromCords(2, 1, DOMTable)
334                 ?.getLastChild<ParagraphNode>()
335                 ?.append($createTextNode(''));
336               const selection = $createTableSelection();
337               selection.set(
338                 table.__key,
339                 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
340                 table?.getCellNodeFromCords(2, 1, DOMTable)?.__key || '',
341               );
342               expect(selection.getTextContent()).toBe(`1\t\t2\n3\t4\t\n`);
343             }
344           }
345         });
346       });
347     },
348     undefined,
349     <TablePlugin />,
350   );
351 });