2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import {$insertDataTransferForRichText} from '@lexical/clipboard';
10 import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
13 $createTableNodeWithDimensions,
14 $createTableSelection,
15 } from '@lexical/table';
31 } from 'lexical/__tests__/utils';
33 import {$getElementForTableNode, TableNode} from '../../LexicalTableNode';
35 export class ClipboardDataMock {
36 getData: jest.Mock<string, [string]>;
37 setData: jest.Mock<void, [string, string]>;
40 this.getData = jest.fn();
41 this.setData = jest.fn();
45 export class ClipboardEventMock extends Event {
46 clipboardData: ClipboardDataMock;
48 constructor(type: string, options?: EventInit) {
50 this.clipboardData = new ClipboardDataMock();
54 global.document.execCommand = function execCommandMock(
61 Object.defineProperty(window, 'ClipboardEvent', {
62 value: new ClipboardEventMock('cut'),
65 const editorConfig = Object.freeze({
68 table: 'test-table-class',
72 describe('LexicalTableNode tests', () => {
75 beforeEach(async () => {
76 const {editor} = testEnv;
77 await editor.update(() => {
78 const root = $getRoot();
79 const paragraph = $createParagraphNode();
80 root.append(paragraph);
85 test('TableNode.constructor', async () => {
86 const {editor} = testEnv;
88 await editor.update(() => {
89 const tableNode = $createTableNode();
91 expect(tableNode).not.toBe(null);
94 expect(() => $createTableNode()).toThrow();
97 test('TableNode.createDOM()', async () => {
98 const {editor} = testEnv;
100 await editor.update(() => {
101 const tableNode = $createTableNode();
103 expect(tableNode.createDOM(editorConfig).outerHTML).toBe(
104 `<table class="${editorConfig.theme.table}"></table>`,
109 test('Copy table from an external source', async () => {
110 const {editor} = testEnv;
112 const dataTransfer = new DataTransferMock();
113 dataTransfer.setData(
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>',
117 await editor.update(() => {
118 const selection = $getSelection();
120 $isRangeSelection(selection),
121 'isRangeSelection(selection)',
123 $insertDataTransferForRichText(dataTransfer, selection, editor);
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>`,
132 test('Copy table from an external source like gdoc with formatting', async () => {
133 const {editor} = testEnv;
135 const dataTransfer = new DataTransferMock();
136 dataTransfer.setData(
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="{"1":2,"2":"Surface"}">Surface</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;font-style:italic;" data-sheets-value="{"1":2,"2":"MWP_WORK_LS_COMPOSER"}">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="{"1":3,"3":77349}">77349</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"Lexical"}">Lexical</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-decoration:line-through;" data-sheets-value="{"1":2,"2":"XDS_RICH_TEXT_AREA"}">XDS_RICH_TEXT_AREA</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"sdvd sdfvsfs"}" data-sheets-textstyleruns="{"1":0}{"1":5,"2":{"5":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>',
140 await editor.update(() => {
141 const selection = $getSelection();
143 $isRangeSelection(selection),
144 'isRangeSelection(selection)',
146 $insertDataTransferForRichText(dataTransfer, selection, editor);
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>`,
153 test('Cut table in the middle of a range selection', async () => {
154 const {editor} = testEnv;
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');
163 paragraph?.append(beforeText);
164 paragraph?.append(table);
165 paragraph?.append(afterText);
167 await editor.update(() => {
171 await editor.update(() => {
172 editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
175 expect(testEnv.innerHTML).toBe(`<p><br></p>`);
178 test('Cut table as last node in range selection ', async () => {
179 const {editor} = testEnv;
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);
187 paragraph?.append(beforeText);
188 paragraph?.append(table);
190 await editor.update(() => {
194 await editor.update(() => {
195 editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
198 expect(testEnv.innerHTML).toBe(`<p><br></p>`);
201 test('Cut table as first node in range selection ', async () => {
202 const {editor} = testEnv;
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');
210 paragraph?.append(table);
211 paragraph?.append(afterText);
213 await editor.update(() => {
217 await editor.update(() => {
218 editor.dispatchCommand(CUT_COMMAND, {} as ClipboardEvent);
221 expect(testEnv.innerHTML).toBe(`<p><br></p>`);
224 test('Cut table is whole selection, should remove it', async () => {
225 const {editor} = testEnv;
227 await editor.update(() => {
228 const root = $getRoot();
229 const table = $createTableNodeWithDimensions(4, 4, true);
232 await editor.update(() => {
233 const root = $getRoot();
234 const table = root.getLastChild<TableNode>();
236 const DOMTable = $getElementForTableNode(editor, table);
239 ?.getCellNodeFromCords(0, 0, DOMTable)
240 ?.getLastChild<ParagraphNode>()
241 ?.append($createTextNode('some text'));
242 const selection = $createTableSelection();
245 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
246 table?.getCellNodeFromCords(3, 3, DOMTable)?.__key || '',
248 $setSelection(selection);
249 editor.dispatchCommand(CUT_COMMAND, {
250 preventDefault: () => {},
251 stopPropagation: () => {},
252 } as ClipboardEvent);
257 expect(testEnv.innerHTML).toBe(`<p><br></p>`);
260 test('Cut subsection of table cells, should just clear contents', async () => {
261 const {editor} = testEnv;
263 await editor.update(() => {
264 const root = $getRoot();
265 const table = $createTableNodeWithDimensions(4, 4, true);
268 await editor.update(() => {
269 const root = $getRoot();
270 const table = root.getLastChild<TableNode>();
272 const DOMTable = $getElementForTableNode(editor, table);
275 ?.getCellNodeFromCords(0, 0, DOMTable)
276 ?.getLastChild<ParagraphNode>()
277 ?.append($createTextNode('some text'));
278 const selection = $createTableSelection();
281 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
282 table?.getCellNodeFromCords(2, 2, DOMTable)?.__key || '',
284 $setSelection(selection);
285 editor.dispatchCommand(CUT_COMMAND, {
286 preventDefault: () => {},
287 stopPropagation: () => {},
288 } as ClipboardEvent);
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>`,
298 test('Table plain text output validation', async () => {
299 const {editor} = testEnv;
301 await editor.update(() => {
302 const root = $getRoot();
303 const table = $createTableNodeWithDimensions(4, 4, true);
306 await editor.update(() => {
307 const root = $getRoot();
308 const table = root.getLastChild<TableNode>();
310 const DOMTable = $getElementForTableNode(editor, table);
313 ?.getCellNodeFromCords(0, 0, DOMTable)
314 ?.getLastChild<ParagraphNode>()
315 ?.append($createTextNode('1'));
317 ?.getCellNodeFromCords(1, 0, DOMTable)
318 ?.getLastChild<ParagraphNode>()
319 ?.append($createTextNode(''));
321 ?.getCellNodeFromCords(2, 0, DOMTable)
322 ?.getLastChild<ParagraphNode>()
323 ?.append($createTextNode('2'));
325 ?.getCellNodeFromCords(0, 1, DOMTable)
326 ?.getLastChild<ParagraphNode>()
327 ?.append($createTextNode('3'));
329 ?.getCellNodeFromCords(1, 1, DOMTable)
330 ?.getLastChild<ParagraphNode>()
331 ?.append($createTextNode('4'));
333 ?.getCellNodeFromCords(2, 1, DOMTable)
334 ?.getLastChild<ParagraphNode>()
335 ?.append($createTextNode(''));
336 const selection = $createTableSelection();
339 table?.getCellNodeFromCords(0, 0, DOMTable)?.__key || '',
340 table?.getCellNodeFromCords(2, 1, DOMTable)?.__key || '',
342 expect(selection.getTextContent()).toBe(`1\t\t2\n3\t4\t\n`);