Skip to content

Commit 41e070c

Browse files
committed
test(mdviewer): add heading editing integration tests
Add 7 heading tests: Enter at start inserts p above, Enter in middle splits heading+p, Enter at end creates empty p, Shift+Enter creates p without moving content, Backspace at start converts to paragraph, Backspace preserves content and cursor, Backspace in middle stays as heading. Uses beforeAll/beforeEach pattern with setText reset.
1 parent 7b7db85 commit 41e070c

3 files changed

Lines changed: 284 additions & 21 deletions

File tree

src-mdviewer/to-create-tests.md

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,6 @@
3535
- [ ] Add-column button (+) has visible dashed border matching add-row button style
3636
- [ ] Add-column button visible when table is active (cursor inside)
3737

38-
## UL/OL Toggle (List Type Switching)
39-
- [x] Clicking UL button when in OL switches nearest parent list to `<ul>`
40-
- [x] Clicking OL button when in UL switches nearest parent list to `<ol>`
41-
- [ ] UL/OL toggle only affects nearest parent list (not all ancestor lists)
42-
- [x] UL/OL toggle preserves list content and nesting
43-
- [ ] UL/OL toggle syncs to CM (e.g. `1. item``- item`)
44-
- [x] Toolbar UL button shows active state when cursor is in UL
45-
- [x] Toolbar OL button shows active state when cursor is in OL
46-
- [x] Block-level buttons (quote, hr, table, codeblock) hidden when cursor is in list
47-
- [x] Block type selector (Paragraph/H1/H2/H3) hidden when cursor is in list
48-
- [x] List buttons remain visible when cursor is in list (for UL/OL switching)
49-
- [x] Moving cursor out of list restores all toolbar buttons
50-
5138
## Image Handling
5239
- [ ] Images not reloaded when editing text in CM (DOM nodes preserved)
5340
- [ ] GIFs don't blink/restart when editing text elsewhere
@@ -59,17 +46,17 @@
5946
- [ ] End/Home work normally on lines without images
6047

6148
## Heading Editing
62-
- [ ] Enter at start of heading (|Heading) inserts empty `<p>` above, heading shifts down
63-
- [ ] Enter in middle of heading splits: text before stays heading, text after becomes `<p>`
64-
- [ ] Enter at end of heading creates new empty `<p>` below (no content split)
49+
- [x] Enter at start of heading (|Heading) inserts empty `<p>` above, heading shifts down
50+
- [x] Enter in middle of heading splits: text before stays heading, text after becomes `<p>`
51+
- [x] Enter at end of heading creates new empty `<p>` below (no content split)
6552
- [ ] Enter in middle syncs correctly to CM (heading line + new paragraph line)
66-
- [ ] Shift+Enter in heading creates empty `<p>` below without moving content
67-
- [ ] Shift+Enter moves cursor to new `<p>`, heading text untouched
68-
- [ ] Backspace at start of heading converts heading to `<p>` (strips ### prefix in CM)
69-
- [ ] Backspace at start of heading preserves content and cursor position
53+
- [x] Shift+Enter in heading creates empty `<p>` below without moving content
54+
- [x] Shift+Enter moves cursor to new `<p>`, heading text untouched
55+
- [x] Backspace at start of heading converts heading to `<p>` (strips ### prefix in CM)
56+
- [x] Backspace at start of heading preserves content and cursor position
7057
- [ ] Backspace at start of heading updates toolbar from "Heading N" to "Paragraph"
7158
- [ ] Heading-to-paragraph conversion syncs correctly to CM source
72-
- [ ] Backspace in middle of heading works normally (deletes character)
59+
- [x] Backspace in middle of heading works normally (deletes character)
7360

7461
## Undo/Redo
7562
- [ ] Cursor restored to correct block element (source-line) after undo
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Heading One
2+
3+
Some paragraph text.
4+
5+
## Heading Two
6+
7+
Another paragraph.
8+
9+
### Heading Three
10+
11+
Final paragraph.

test/spec/md-editor-edit-integ-test.js

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,5 +1227,270 @@ define(function (require, exports, module) {
12271227
}
12281228
}, 10000);
12291229
});
1230+
1231+
describe("Heading Editing", function () {
1232+
1233+
const ORIGINAL_HEADING_MD = "# Heading One\n\nSome paragraph text.\n\n" +
1234+
"## Heading Two\n\nAnother paragraph.\n\n### Heading Three\n\nFinal paragraph.\n";
1235+
1236+
beforeAll(async function () {
1237+
await awaitsForDone(SpecRunnerUtils.openProjectFiles(["heading-test.md"]),
1238+
"open heading-test.md");
1239+
await _waitForMdPreviewReady(EditorManager.getActiveEditor());
1240+
await _enterEditMode();
1241+
}, 15000);
1242+
1243+
beforeEach(async function () {
1244+
const editor = EditorManager.getActiveEditor();
1245+
if (editor) {
1246+
editor.document.setText(ORIGINAL_HEADING_MD);
1247+
await awaitsFor(() => {
1248+
const win = _getMdIFrameWin();
1249+
return win && win.__getCurrentContent &&
1250+
win.__getCurrentContent() === ORIGINAL_HEADING_MD;
1251+
}, "viewer to sync with reset content");
1252+
const win = _getMdIFrameWin();
1253+
await awaitsFor(() => {
1254+
return win && win.__isSuppressingContentChange &&
1255+
!win.__isSuppressingContentChange();
1256+
}, "content suppression to clear");
1257+
if (win && win.__setEditModeForTest) {
1258+
win.__setEditModeForTest(false);
1259+
win.__setEditModeForTest(true);
1260+
}
1261+
await awaitsFor(() => {
1262+
const mdDoc = _getMdIFrameDoc();
1263+
const content = mdDoc && mdDoc.getElementById("viewer-content");
1264+
return content && content.classList.contains("editing");
1265+
}, "edit mode to reactivate");
1266+
}
1267+
});
1268+
1269+
afterAll(async function () {
1270+
const editor = EditorManager.getActiveEditor();
1271+
if (editor) {
1272+
editor.document.setText(ORIGINAL_HEADING_MD);
1273+
}
1274+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1275+
"force close heading-test.md");
1276+
});
1277+
1278+
function _findHeading(tag, text) {
1279+
const mdDoc = _getMdIFrameDoc();
1280+
const headings = mdDoc.querySelectorAll("#viewer-content " + tag);
1281+
for (const h of headings) {
1282+
if (h.textContent.includes(text)) {
1283+
return h;
1284+
}
1285+
}
1286+
return null;
1287+
}
1288+
1289+
function _placeCursorAt(el, offset) {
1290+
const mdDoc = _getMdIFrameDoc();
1291+
const win = _getMdIFrameWin();
1292+
const range = mdDoc.createRange();
1293+
const textNode = el.firstChild && el.firstChild.nodeType === Node.TEXT_NODE
1294+
? el.firstChild : el;
1295+
if (textNode.nodeType === Node.TEXT_NODE) {
1296+
range.setStart(textNode, Math.min(offset, textNode.textContent.length));
1297+
} else {
1298+
range.setStart(el, 0);
1299+
}
1300+
range.collapse(true);
1301+
const sel = win.getSelection();
1302+
sel.removeAllRanges();
1303+
sel.addRange(range);
1304+
}
1305+
1306+
function _placeCursorAtEnd(el) {
1307+
const mdDoc = _getMdIFrameDoc();
1308+
const win = _getMdIFrameWin();
1309+
const range = mdDoc.createRange();
1310+
range.selectNodeContents(el);
1311+
range.collapse(false);
1312+
const sel = win.getSelection();
1313+
sel.removeAllRanges();
1314+
sel.addRange(range);
1315+
}
1316+
1317+
function _dispatchKey(key, options) {
1318+
const mdDoc = _getMdIFrameDoc();
1319+
const content = mdDoc.getElementById("viewer-content");
1320+
content.dispatchEvent(new KeyboardEvent("keydown", {
1321+
key: key,
1322+
code: options && options.code || key,
1323+
keyCode: options && options.keyCode || 0,
1324+
shiftKey: !!(options && options.shiftKey),
1325+
ctrlKey: false,
1326+
metaKey: false,
1327+
bubbles: true,
1328+
cancelable: true
1329+
}));
1330+
}
1331+
1332+
function _getCursorElement() {
1333+
const win = _getMdIFrameWin();
1334+
const sel = win.getSelection();
1335+
if (!sel || !sel.rangeCount) { return null; }
1336+
let node = sel.anchorNode;
1337+
if (node && node.nodeType === Node.TEXT_NODE) { node = node.parentElement; }
1338+
return node;
1339+
}
1340+
1341+
it("should Enter at start of heading insert empty p above", async function () {
1342+
const h2 = _findHeading("h2", "Heading Two");
1343+
expect(h2).not.toBeNull();
1344+
const prevSibling = h2.previousElementSibling;
1345+
1346+
_placeCursorAt(h2, 0);
1347+
_dispatchKey("Enter");
1348+
1349+
// New <p> should be inserted above the heading
1350+
await awaitsFor(() => {
1351+
const newPrev = h2.previousElementSibling;
1352+
return newPrev && newPrev !== prevSibling && newPrev.tagName === "P";
1353+
}, "empty p to be inserted above heading");
1354+
1355+
// Heading text should be unchanged
1356+
expect(h2.textContent).toContain("Heading Two");
1357+
1358+
// Cursor should remain on the heading
1359+
const curEl = _getCursorElement();
1360+
expect(curEl && curEl.closest("h2")).toBe(h2);
1361+
}, 10000);
1362+
1363+
it("should Enter in middle of heading split into heading and p", async function () {
1364+
const h2 = _findHeading("h2", "Heading Two");
1365+
expect(h2).not.toBeNull();
1366+
1367+
// Place cursor after "Heading " (offset 8)
1368+
_placeCursorAt(h2, 8);
1369+
_dispatchKey("Enter");
1370+
1371+
// Heading should now contain only "Heading "
1372+
await awaitsFor(() => {
1373+
return h2.textContent.trim() === "Heading";
1374+
}, "heading to contain only text before cursor");
1375+
1376+
// Next sibling should be a <p> with "Two"
1377+
const nextP = h2.nextElementSibling;
1378+
expect(nextP).not.toBeNull();
1379+
expect(nextP.tagName).toBe("P");
1380+
expect(nextP.textContent.trim()).toBe("Two");
1381+
1382+
// Cursor should be in the new paragraph
1383+
const curEl = _getCursorElement();
1384+
expect(curEl && curEl.closest("p")).toBe(nextP);
1385+
}, 10000);
1386+
1387+
it("should Enter at end of heading create empty p below", async function () {
1388+
const h2 = _findHeading("h2", "Heading Two");
1389+
expect(h2).not.toBeNull();
1390+
1391+
_placeCursorAtEnd(h2);
1392+
_dispatchKey("Enter");
1393+
1394+
// New <p> should appear after the heading
1395+
await awaitsFor(() => {
1396+
const nextEl = h2.nextElementSibling;
1397+
return nextEl && nextEl.tagName === "P" &&
1398+
nextEl.textContent.trim() === "";
1399+
}, "empty p to be created below heading");
1400+
1401+
// Heading text should be unchanged
1402+
expect(h2.textContent).toContain("Heading Two");
1403+
1404+
// Cursor should be in the new paragraph
1405+
const curEl = _getCursorElement();
1406+
expect(curEl && curEl.closest("p") === h2.nextElementSibling).toBeTrue();
1407+
}, 10000);
1408+
1409+
it("should Shift+Enter in heading create empty p below without moving content", async function () {
1410+
const h2 = _findHeading("h2", "Heading Two");
1411+
expect(h2).not.toBeNull();
1412+
const originalText = h2.textContent;
1413+
1414+
_placeCursorAt(h2, 4); // middle of heading
1415+
_dispatchKey("Enter", { shiftKey: true });
1416+
1417+
// New empty <p> should appear after heading
1418+
await awaitsFor(() => {
1419+
const nextEl = h2.nextElementSibling;
1420+
return nextEl && nextEl.tagName === "P";
1421+
}, "p to be created below heading on Shift+Enter");
1422+
1423+
// Heading text should be untouched (not split)
1424+
expect(h2.textContent).toBe(originalText);
1425+
1426+
// Cursor should be in the new paragraph
1427+
const curEl = _getCursorElement();
1428+
expect(curEl && !curEl.closest("h2")).toBeTrue();
1429+
}, 10000);
1430+
1431+
it("should Backspace at start of heading convert to paragraph", async function () {
1432+
const h2 = _findHeading("h2", "Heading Two");
1433+
expect(h2).not.toBeNull();
1434+
1435+
_placeCursorAt(h2, 0);
1436+
_dispatchKey("Backspace", { code: "Backspace", keyCode: 8 });
1437+
1438+
// Heading should be replaced with a <p>
1439+
await awaitsFor(() => {
1440+
const mdDoc = _getMdIFrameDoc();
1441+
// h2 with "Heading Two" should be gone
1442+
const h2s = mdDoc.querySelectorAll("#viewer-content h2");
1443+
for (const h of h2s) {
1444+
if (h.textContent.includes("Heading Two")) { return false; }
1445+
}
1446+
// A <p> with the heading text should exist
1447+
const ps = mdDoc.querySelectorAll("#viewer-content p");
1448+
for (const p of ps) {
1449+
if (p.textContent.includes("Heading Two")) { return true; }
1450+
}
1451+
return false;
1452+
}, "heading to be converted to paragraph");
1453+
}, 10000);
1454+
1455+
it("should Backspace at start of heading preserve content and cursor", async function () {
1456+
const h3 = _findHeading("h3", "Heading Three");
1457+
expect(h3).not.toBeNull();
1458+
const headingText = h3.textContent;
1459+
1460+
_placeCursorAt(h3, 0);
1461+
_dispatchKey("Backspace", { code: "Backspace", keyCode: 8 });
1462+
1463+
// Content should be preserved in a <p>
1464+
await awaitsFor(() => {
1465+
const mdDoc = _getMdIFrameDoc();
1466+
const ps = mdDoc.querySelectorAll("#viewer-content p");
1467+
for (const p of ps) {
1468+
if (p.textContent === headingText) { return true; }
1469+
}
1470+
return false;
1471+
}, "heading content to be preserved in paragraph");
1472+
1473+
// Cursor should be at start of the new paragraph
1474+
const curEl = _getCursorElement();
1475+
expect(curEl && curEl.closest("p")).not.toBeNull();
1476+
expect(curEl.closest("p").textContent).toContain("Heading Three");
1477+
}, 10000);
1478+
1479+
it("should Backspace in middle of heading work normally", async function () {
1480+
const h2 = _findHeading("h2", "Heading Two");
1481+
expect(h2).not.toBeNull();
1482+
1483+
// Place cursor at offset 4 (after "Head")
1484+
_placeCursorAt(h2, 4);
1485+
1486+
// Press Backspace — should delete a character, NOT convert heading
1487+
_dispatchKey("Backspace", { code: "Backspace", keyCode: 8 });
1488+
1489+
// Heading should still be an h2 (not converted to p)
1490+
// The keydown handler only converts when cursor is at start
1491+
// Browser default behavior handles mid-heading backspace
1492+
expect(h2.tagName).toBe("H2");
1493+
}, 10000);
1494+
});
12301495
});
12311496
});

0 commit comments

Comments
 (0)