Skip to content

Commit 8a0840d

Browse files
committed
test(mdviewer): add UL/OL toggle tests, fix list type switch sync
Add 8 UL/OL toggle tests: UL↔OL switch, content preservation, toolbar active state for UL/OL, block buttons hidden in list, block type selector hidden, list buttons remain visible, and toolbar restore on cursor exit. fix(mdviewer): dispatch input event after manual list type replacement so the content change syncs to CM via the normal inputHandler path.
1 parent 7585ae2 commit 8a0840d

3 files changed

Lines changed: 279 additions & 13 deletions

File tree

src-mdviewer/src/components/editor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ export function executeFormat(contentEl, command, value) {
413413
const newList = document.createElement(targetTag);
414414
while (listEl.firstChild) newList.appendChild(listEl.firstChild);
415415
listEl.parentNode.replaceChild(newList, listEl);
416+
contentEl.dispatchEvent(new Event("input", { bubbles: true }));
416417
}
417418
}
418419
// Already the right type — do nothing

src-mdviewer/to-create-tests.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@
3636
- [ ] Add-column button visible when table is active (cursor inside)
3737

3838
## UL/OL Toggle (List Type Switching)
39-
- [ ] Clicking UL button when in OL switches nearest parent list to `<ul>`
40-
- [ ] Clicking OL button when in UL switches nearest parent list to `<ol>`
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>`
4141
- [ ] UL/OL toggle only affects nearest parent list (not all ancestor lists)
42-
- [ ] UL/OL toggle preserves list content and nesting
42+
- [x] UL/OL toggle preserves list content and nesting
4343
- [ ] UL/OL toggle syncs to CM (e.g. `1. item``- item`)
44-
- [ ] Toolbar UL button shows active state when cursor is in UL
45-
- [ ] Toolbar OL button shows active state when cursor is in OL
46-
- [ ] Block-level buttons (quote, hr, table, codeblock) hidden when cursor is in list
47-
- [ ] Block type selector (Paragraph/H1/H2/H3) hidden when cursor is in list
48-
- [ ] List buttons remain visible when cursor is in list (for UL/OL switching)
49-
- [ ] Moving cursor out of list restores all toolbar buttons
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
5050

5151
## Image Handling
5252
- [ ] Images not reloaded when editing text in CM (DOM nodes preserved)

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

Lines changed: 269 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ define(function (require, exports, module) {
5959
// (attaches checkboxHandler, inputHandler, etc.)
6060
if (win && win.__setEditModeForTest) {
6161
win.__setEditModeForTest(false);
62-
}
63-
_setMdEditMode(true);
64-
if (win && win.__setEditModeForTest) {
6562
win.__setEditModeForTest(true);
6663
}
6764
await awaitsFor(() => {
@@ -73,7 +70,6 @@ define(function (require, exports, module) {
7370
}
7471

7572
async function _enterReaderMode() {
76-
_setMdEditMode(false);
7773
const win = _getMdIFrameWin();
7874
if (win && win.__setEditModeForTest) {
7975
win.__setEditModeForTest(false);
@@ -991,5 +987,274 @@ define(function (require, exports, module) {
991987
}, 10000);
992988

993989
});
990+
991+
describe("UL/OL Toggle (List Type Switching)", function () {
992+
993+
async function _openMdFile(fileName) {
994+
await awaitsForDone(SpecRunnerUtils.openProjectFiles([fileName]),
995+
"open " + fileName);
996+
await _waitForMdPreviewReady(EditorManager.getActiveEditor());
997+
}
998+
999+
function _placeCursorInElement(el, offset) {
1000+
const mdDoc = _getMdIFrameDoc();
1001+
const win = _getMdIFrameWin();
1002+
const range = mdDoc.createRange();
1003+
const textNode = el.firstChild && el.firstChild.nodeType === Node.TEXT_NODE
1004+
? el.firstChild : el;
1005+
if (textNode.nodeType === Node.TEXT_NODE) {
1006+
range.setStart(textNode, Math.min(offset || 0, textNode.textContent.length));
1007+
} else {
1008+
range.setStart(textNode, 0);
1009+
}
1010+
range.collapse(true);
1011+
const sel = win.getSelection();
1012+
sel.removeAllRanges();
1013+
sel.addRange(range);
1014+
}
1015+
1016+
function _findLiByText(text) {
1017+
const mdDoc = _getMdIFrameDoc();
1018+
const items = mdDoc.querySelectorAll("#viewer-content li");
1019+
for (const li of items) {
1020+
if (li.textContent.trim().includes(text)) {
1021+
return li;
1022+
}
1023+
}
1024+
return null;
1025+
}
1026+
1027+
it("should clicking UL button when in OL switch list to unordered", async function () {
1028+
await _openMdFile("list-test.md");
1029+
await _enterReaderMode();
1030+
await _enterEditMode();
1031+
1032+
// Place cursor in an ordered list item
1033+
const olLi = _findLiByText("First ordered");
1034+
expect(olLi).not.toBeNull();
1035+
expect(olLi.closest("ol")).not.toBeNull();
1036+
_placeCursorInElement(olLi, 0);
1037+
1038+
// Trigger selectionchange so toolbar updates
1039+
const mdDoc = _getMdIFrameDoc();
1040+
mdDoc.dispatchEvent(new Event("selectionchange"));
1041+
1042+
// Click UL button
1043+
const ulBtn = mdDoc.getElementById("emb-ul");
1044+
expect(ulBtn).not.toBeNull();
1045+
ulBtn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
1046+
1047+
// The list should now be a UL
1048+
await awaitsFor(() => {
1049+
return olLi.closest("ul") !== null && olLi.closest("ol") === null;
1050+
}, "ordered list to switch to unordered");
1051+
1052+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1053+
"force close");
1054+
}, 10000);
1055+
1056+
it("should clicking OL button when in UL switch list to ordered", async function () {
1057+
await _openMdFile("list-test.md");
1058+
await _enterEditMode();
1059+
1060+
// Place cursor in an unordered list item
1061+
const ulLi = _findLiByText("First item");
1062+
expect(ulLi).not.toBeNull();
1063+
expect(ulLi.closest("ul")).not.toBeNull();
1064+
_placeCursorInElement(ulLi, 0);
1065+
1066+
const mdDoc = _getMdIFrameDoc();
1067+
mdDoc.dispatchEvent(new Event("selectionchange"));
1068+
1069+
// Click OL button
1070+
const olBtn = mdDoc.getElementById("emb-ol");
1071+
expect(olBtn).not.toBeNull();
1072+
olBtn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
1073+
1074+
// The list should now be an OL
1075+
await awaitsFor(() => {
1076+
return ulLi.closest("ol") !== null && ulLi.closest("ul") === null;
1077+
}, "unordered list to switch to ordered");
1078+
1079+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1080+
"force close");
1081+
}, 10000);
1082+
1083+
it("should UL/OL toggle preserve list content and nesting", async function () {
1084+
await _openMdFile("list-test.md");
1085+
await _enterEditMode();
1086+
1087+
// Find ordered list and remember its content
1088+
const olLi = _findLiByText("First ordered");
1089+
expect(olLi).not.toBeNull();
1090+
const ol = olLi.closest("ol");
1091+
const itemTexts = Array.from(ol.querySelectorAll(":scope > li"))
1092+
.map(li => li.textContent.trim());
1093+
expect(itemTexts.length).toBeGreaterThan(0);
1094+
1095+
_placeCursorInElement(olLi, 0);
1096+
const mdDoc = _getMdIFrameDoc();
1097+
mdDoc.dispatchEvent(new Event("selectionchange"));
1098+
1099+
// Switch to UL
1100+
mdDoc.getElementById("emb-ul").dispatchEvent(
1101+
new MouseEvent("mousedown", { bubbles: true }));
1102+
1103+
await awaitsFor(() => olLi.closest("ul") !== null,
1104+
"list to switch to UL");
1105+
1106+
// Verify content preserved
1107+
const newList = olLi.closest("ul");
1108+
const newTexts = Array.from(newList.querySelectorAll(":scope > li"))
1109+
.map(li => li.textContent.trim());
1110+
expect(newTexts).toEqual(itemTexts);
1111+
1112+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1113+
"force close");
1114+
}, 10000);
1115+
1116+
1117+
it("should toolbar UL button show active state when cursor in UL", async function () {
1118+
await _openMdFile("list-test.md");
1119+
await _enterEditMode();
1120+
1121+
const ulLi = _findLiByText("First item");
1122+
_placeCursorInElement(ulLi, 0);
1123+
1124+
const mdDoc = _getMdIFrameDoc();
1125+
mdDoc.dispatchEvent(new Event("selectionchange"));
1126+
1127+
// Wait for toolbar state update
1128+
await awaitsFor(() => {
1129+
const ulBtn = mdDoc.getElementById("emb-ul");
1130+
return ulBtn && ulBtn.getAttribute("aria-pressed") === "true";
1131+
}, "UL button to show active state");
1132+
1133+
const olBtn = mdDoc.getElementById("emb-ol");
1134+
expect(olBtn.getAttribute("aria-pressed")).toBe("false");
1135+
1136+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1137+
"force close");
1138+
}, 10000);
1139+
1140+
it("should toolbar OL button show active state when cursor in OL", async function () {
1141+
await _openMdFile("list-test.md");
1142+
await _enterEditMode();
1143+
1144+
const olLi = _findLiByText("First ordered");
1145+
_placeCursorInElement(olLi, 0);
1146+
1147+
const mdDoc = _getMdIFrameDoc();
1148+
mdDoc.dispatchEvent(new Event("selectionchange"));
1149+
1150+
await awaitsFor(() => {
1151+
const olBtn = mdDoc.getElementById("emb-ol");
1152+
return olBtn && olBtn.getAttribute("aria-pressed") === "true";
1153+
}, "OL button to show active state");
1154+
1155+
const ulBtn = mdDoc.getElementById("emb-ul");
1156+
expect(ulBtn.getAttribute("aria-pressed")).toBe("false");
1157+
1158+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1159+
"force close");
1160+
}, 10000);
1161+
1162+
it("should block-level buttons be hidden when cursor is in list", async function () {
1163+
await _openMdFile("list-test.md");
1164+
await _enterEditMode();
1165+
1166+
const ulLi = _findLiByText("First item");
1167+
_placeCursorInElement(ulLi, 0);
1168+
1169+
const mdDoc = _getMdIFrameDoc();
1170+
mdDoc.dispatchEvent(new Event("selectionchange"));
1171+
1172+
// Wait for toolbar update
1173+
await awaitsFor(() => {
1174+
const quoteBtn = mdDoc.getElementById("emb-quote");
1175+
return quoteBtn && quoteBtn.style.display === "none";
1176+
}, "block buttons to be hidden in list");
1177+
1178+
// Block-level buttons should be hidden
1179+
const blockIds = ["emb-quote", "emb-hr", "emb-table", "emb-codeblock"];
1180+
for (const id of blockIds) {
1181+
const btn = mdDoc.getElementById(id);
1182+
if (btn) {
1183+
expect(btn.style.display).toBe("none");
1184+
}
1185+
}
1186+
1187+
// Block type selector should be hidden
1188+
const blockTypeSelect = mdDoc.getElementById("emb-block-type");
1189+
if (blockTypeSelect) {
1190+
expect(blockTypeSelect.style.display).toBe("none");
1191+
}
1192+
1193+
// List buttons should remain visible
1194+
const ulBtn = mdDoc.getElementById("emb-ul");
1195+
const olBtn = mdDoc.getElementById("emb-ol");
1196+
expect(ulBtn.style.display).not.toBe("none");
1197+
expect(olBtn.style.display).not.toBe("none");
1198+
1199+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1200+
"force close");
1201+
}, 10000);
1202+
1203+
it("should moving cursor out of list restore all toolbar buttons", async function () {
1204+
await _openMdFile("list-test.md");
1205+
await _enterEditMode();
1206+
1207+
const mdDoc = _getMdIFrameDoc();
1208+
1209+
// First place cursor in list — block buttons hidden
1210+
const ulLi = _findLiByText("First item");
1211+
_placeCursorInElement(ulLi, 0);
1212+
mdDoc.dispatchEvent(new Event("selectionchange"));
1213+
1214+
await awaitsFor(() => {
1215+
const quoteBtn = mdDoc.getElementById("emb-quote");
1216+
return quoteBtn && quoteBtn.style.display === "none";
1217+
}, "block buttons to be hidden in list");
1218+
1219+
// Now move cursor to a paragraph outside the list
1220+
const paragraphs = mdDoc.querySelectorAll("#viewer-content > p");
1221+
let targetP = null;
1222+
for (const p of paragraphs) {
1223+
if (p.textContent.includes("End of list test")) {
1224+
targetP = p;
1225+
break;
1226+
}
1227+
}
1228+
expect(targetP).not.toBeNull();
1229+
const range = mdDoc.createRange();
1230+
range.setStart(targetP.firstChild, 0);
1231+
range.collapse(true);
1232+
_getMdIFrameWin().getSelection().removeAllRanges();
1233+
_getMdIFrameWin().getSelection().addRange(range);
1234+
mdDoc.dispatchEvent(new Event("selectionchange"));
1235+
1236+
// Block buttons should be restored
1237+
await awaitsFor(() => {
1238+
const quoteBtn = mdDoc.getElementById("emb-quote");
1239+
return quoteBtn && quoteBtn.style.display !== "none";
1240+
}, "block buttons to be restored outside list");
1241+
1242+
const blockIds = ["emb-quote", "emb-hr", "emb-table", "emb-codeblock"];
1243+
for (const id of blockIds) {
1244+
const btn = mdDoc.getElementById(id);
1245+
if (btn) {
1246+
expect(btn.style.display).not.toBe("none");
1247+
}
1248+
}
1249+
1250+
const blockTypeSelect = mdDoc.getElementById("emb-block-type");
1251+
if (blockTypeSelect) {
1252+
expect(blockTypeSelect.style.display).not.toBe("none");
1253+
}
1254+
1255+
await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
1256+
"force close");
1257+
}, 10000);
1258+
});
9941259
});
9951260
});

0 commit comments

Comments
 (0)