|
1 | 1 | import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; |
2 | | -import { EditorState, TextSelection } from 'prosemirror-state'; |
| 2 | +import { AllSelection, EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; |
3 | 3 | import { CellSelection, TableMap } from 'prosemirror-tables'; |
4 | 4 | import { loadTestDataForEditorTests, initTestEditor } from '@tests/helpers/helpers.js'; |
5 | 5 | import { createTable } from './tableHelpers/createTable.js'; |
@@ -1267,6 +1267,77 @@ describe('Table commands', async () => { |
1267 | 1267 | expect(editor.state.doc.child(1).type.name).toBe('paragraph'); |
1268 | 1268 | expect(editor.state.doc.childCount).toBe(2); |
1269 | 1269 | }); |
| 1270 | + |
| 1271 | + it('does not throw when insertTable is called with a NodeSelection on a top-level block', async () => { |
| 1272 | + const { docx, media, mediaFiles, fonts } = cachedBlankDoc; |
| 1273 | + ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })); |
| 1274 | + |
| 1275 | + // Insert a documentSection (atom: true, group: 'block') to get a |
| 1276 | + // selectable top-level block node. When selected as a NodeSelection, |
| 1277 | + // $from.depth is 0 and $from.end() returns doc.content.size, which |
| 1278 | + // previously caused insertTable to compute an out-of-range offset. |
| 1279 | + const { schema } = editor.state; |
| 1280 | + const sectionNode = schema.nodes.documentSection.create(null, [schema.nodes.paragraph.create()]); |
| 1281 | + const { tr } = editor.state; |
| 1282 | + const insertPos = tr.selection.$from.before(1); |
| 1283 | + tr.insert(insertPos, sectionNode); |
| 1284 | + tr.setSelection(NodeSelection.create(tr.doc, insertPos)); |
| 1285 | + editor.view.dispatch(tr); |
| 1286 | + |
| 1287 | + expect(editor.state.selection).toBeInstanceOf(NodeSelection); |
| 1288 | + expect(editor.state.selection.$from.depth).toBe(0); |
| 1289 | + |
| 1290 | + // Inserting a table while a top-level node is selected should not throw |
| 1291 | + expect(() => editor.commands.insertTable({ rows: 2, cols: 2 })).not.toThrow(); |
| 1292 | + |
| 1293 | + // Verify a table was actually inserted |
| 1294 | + const tablePos = findTablePos(editor.state.doc); |
| 1295 | + expect(tablePos).not.toBeNull(); |
| 1296 | + |
| 1297 | + // Verify the cursor is inside the first table cell |
| 1298 | + const table = editor.state.doc.nodeAt(tablePos); |
| 1299 | + const map = TableMap.get(table); |
| 1300 | + const firstCellTextPos = tablePos + 1 + map.map[0] + 2; |
| 1301 | + |
| 1302 | + const { $from } = editor.state.selection; |
| 1303 | + expect(editor.state.selection.from).toBe(firstCellTextPos); |
| 1304 | + expect($from.parent.type.name).toBe('paragraph'); |
| 1305 | + expect($from.node($from.depth - 1).type.spec.tableRole).toBe('cell'); |
| 1306 | + }); |
| 1307 | + |
| 1308 | + it('places cursor in first cell and adds trailing paragraph when inserting table with AllSelection', async () => { |
| 1309 | + const { docx, media, mediaFiles, fonts } = cachedBlankDoc; |
| 1310 | + ({ editor } = initTestEditor({ content: docx, media, mediaFiles, fonts })); |
| 1311 | + |
| 1312 | + // Type some text so the paragraph is non-empty (simulates a real document) |
| 1313 | + editor.commands.insertContent('This is a test'); |
| 1314 | + |
| 1315 | + // Select all content (Ctrl+A equivalent) |
| 1316 | + editor.view.dispatch(editor.state.tr.setSelection(new AllSelection(editor.state.doc))); |
| 1317 | + expect(editor.state.selection).toBeInstanceOf(AllSelection); |
| 1318 | + |
| 1319 | + // Insert a table while everything is selected |
| 1320 | + editor.commands.insertTable({ rows: 2, cols: 2 }); |
| 1321 | + |
| 1322 | + // The table should be followed by a trailing separator paragraph |
| 1323 | + const doc = editor.state.doc; |
| 1324 | + const tablePos = findTablePos(doc); |
| 1325 | + expect(tablePos).not.toBeNull(); |
| 1326 | + const table = doc.nodeAt(tablePos); |
| 1327 | + const tableEndPos = tablePos + table.nodeSize; |
| 1328 | + const $afterTable = doc.resolve(tableEndPos); |
| 1329 | + const nodeAfterTable = $afterTable.nodeAfter; |
| 1330 | + expect(nodeAfterTable?.type.name).toBe('paragraph'); |
| 1331 | + |
| 1332 | + // The cursor should be in the first table cell, not the last |
| 1333 | + const map = TableMap.get(table); |
| 1334 | + const firstCellTextPos = tablePos + 1 + map.map[0] + 2; |
| 1335 | + |
| 1336 | + const { $from } = editor.state.selection; |
| 1337 | + expect(editor.state.selection.from).toBe(firstCellTextPos); |
| 1338 | + expect($from.parent.type.name).toBe('paragraph'); |
| 1339 | + expect($from.node($from.depth - 1).type.spec.tableRole).toBe('cell'); |
| 1340 | + }); |
1270 | 1341 | }); |
1271 | 1342 |
|
1272 | 1343 | describe('normalizeNewTableAttrs tblLook (SD-2086)', async () => { |
|
0 commit comments