@@ -970,21 +970,12 @@ define(function (require, exports, module) {
970970 el => el . classList . remove ( "cm-selection-highlight" ) ) ;
971971 expect ( _hasViewerHighlight ( ) ) . toBeFalse ( ) ;
972972
973- // Select text in CM and dispatch highlight to iframe.
974- // MarkdownSync sends postMessage as thers some race where the cursor isnt syncing it seems
973+ // Select text in CM — MarkdownSync's cursorActivity handler
974+ // debounces and sends MDVIEWR_HIGHLIGHT_SELECTION to the iframe
975975 const editor = EditorManager . getActiveEditor ( ) ;
976976 editor . setSelection ( { line : 4 , ch : 0 } , { line : 6 , ch : 0 } ) ;
977977 expect ( editor . getSelectedText ( ) . length ) . toBeGreaterThan ( 0 ) ;
978978
979- const win = _getMdIFrameWin ( ) ;
980- win . dispatchEvent ( new MessageEvent ( "message" , {
981- data : {
982- type : "MDVIEWR_HIGHLIGHT_SELECTION" ,
983- fromLine : 5 , toLine : 7 ,
984- selectedText : editor . getSelectedText ( )
985- }
986- } ) ) ;
987-
988979 await awaitsFor ( ( ) => _hasViewerHighlight ( ) ,
989980 "viewer to show selection highlight" ) ;
990981
@@ -994,18 +985,14 @@ define(function (require, exports, module) {
994985 } , 10000 ) ;
995986
996987 it ( "should clear viewer highlight when CM selection is cleared" , async function ( ) {
997- // Create highlight
998- const win = _getMdIFrameWin ( ) ;
999- win . dispatchEvent ( new MessageEvent ( "message" , {
1000- data : { type : "MDVIEWR_HIGHLIGHT_SELECTION" , fromLine : 5 , toLine : 7 , selectedText : "text" }
1001- } ) ) ;
988+ // Create highlight by selecting in CM
989+ const editor = EditorManager . getActiveEditor ( ) ;
990+ editor . setSelection ( { line : 4 , ch : 0 } , { line : 6 , ch : 0 } ) ;
1002991 await awaitsFor ( ( ) => _hasViewerHighlight ( ) ,
1003992 "highlight to appear" ) ;
1004993
1005- // Clear
1006- win . dispatchEvent ( new MessageEvent ( "message" , {
1007- data : { type : "MDVIEWR_HIGHLIGHT_SELECTION" , fromLine : null , toLine : null , selectedText : null }
1008- } ) ) ;
994+ // Clear selection in CM — should clear viewer highlight
995+ editor . setCursorPos ( 0 , 0 ) ;
1009996
1010997 await awaitsFor ( ( ) => ! _hasViewerHighlight ( ) ,
1011998 "viewer highlight to clear" ) ;
@@ -1096,6 +1083,270 @@ define(function (require, exports, module) {
10961083 return Math . abs ( cmLine - ( sourceLine - 1 ) ) < 5 ;
10971084 } , "CM cursor to move near selected text's source line" ) ;
10981085 } , 10000 ) ;
1086+
1087+ it ( "should cursor sync toggle state preserve across file switch and mode toggle" , async function ( ) {
1088+ await _enterReaderMode ( ) ;
1089+
1090+ // Disable cursor sync
1091+ const syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1092+ expect ( syncBtn ) . not . toBeNull ( ) ;
1093+ syncBtn . click ( ) ;
1094+ await awaitsFor ( ( ) => ! syncBtn . classList . contains ( "active" ) ,
1095+ "cursor sync button to become inactive" ) ;
1096+ expect ( syncBtn . getAttribute ( "aria-pressed" ) ) . toBe ( "false" ) ;
1097+
1098+ // Switch to another file — toolbar re-renders
1099+ await _openMdFile ( "doc1.md" ) ;
1100+ const syncBtnAfterSwitch = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1101+ expect ( syncBtnAfterSwitch ) . not . toBeNull ( ) ;
1102+ expect ( syncBtnAfterSwitch . classList . contains ( "active" ) ) . toBeFalse ( ) ;
1103+ expect ( syncBtnAfterSwitch . getAttribute ( "aria-pressed" ) ) . toBe ( "false" ) ;
1104+
1105+ // Toggle to edit mode — toolbar re-renders again
1106+ await _enterEditMode ( ) ;
1107+ const syncBtnAfterMode = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1108+ expect ( syncBtnAfterMode ) . not . toBeNull ( ) ;
1109+ expect ( syncBtnAfterMode . classList . contains ( "active" ) ) . toBeFalse ( ) ;
1110+ expect ( syncBtnAfterMode . getAttribute ( "aria-pressed" ) ) . toBe ( "false" ) ;
1111+
1112+ // Re-enable cursor sync and verify it persists across switch
1113+ syncBtnAfterMode . click ( ) ;
1114+ await awaitsFor ( ( ) => syncBtnAfterMode . classList . contains ( "active" ) ,
1115+ "cursor sync button to become active again" ) ;
1116+
1117+ await _openMdFile ( "long.md" ) ;
1118+ const syncBtnFinal = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1119+ expect ( syncBtnFinal . classList . contains ( "active" ) ) . toBeTrue ( ) ;
1120+ expect ( syncBtnFinal . getAttribute ( "aria-pressed" ) ) . toBe ( "true" ) ;
1121+
1122+ // Force close doc1
1123+ await awaitsForDone ( CommandManager . execute ( Commands . FILE_CLOSE , { _forceClose : true } ) ,
1124+ "force close" ) ;
1125+ } , 15000 ) ;
1126+
1127+ it ( "should content sync still work when cursor sync is disabled" , async function ( ) {
1128+ await _openMdFile ( "long.md" ) ;
1129+ await _enterEditMode ( ) ;
1130+
1131+ // Disable cursor sync
1132+ const syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1133+ syncBtn . click ( ) ;
1134+ await awaitsFor ( ( ) => ! syncBtn . classList . contains ( "active" ) ,
1135+ "cursor sync to be disabled" ) ;
1136+
1137+ // Edit content in CM — should still sync to viewer
1138+ const editor = EditorManager . getActiveEditor ( ) ;
1139+ const originalText = editor . document . getText ( ) ;
1140+ editor . document . setText ( "# Sync Test\n\nContent sync works even without cursor sync.\n" ) ;
1141+
1142+ await awaitsFor ( ( ) => {
1143+ const mdDoc = _getMdIFrameDoc ( ) ;
1144+ const h1 = mdDoc && mdDoc . querySelector ( "#viewer-content h1" ) ;
1145+ return h1 && h1 . textContent . includes ( "Sync Test" ) ;
1146+ } , "viewer to update with new content despite cursor sync off" ) ;
1147+
1148+ // Restore original content
1149+ editor . document . setText ( originalText ) ;
1150+ await awaitsFor ( ( ) => {
1151+ const mdDoc = _getMdIFrameDoc ( ) ;
1152+ const h1 = mdDoc && mdDoc . querySelector ( "#viewer-content h1" ) ;
1153+ return h1 && ! h1 . textContent . includes ( "Sync Test" ) ;
1154+ } , "viewer to restore original content" ) ;
1155+
1156+ // Re-enable cursor sync
1157+ syncBtn . click ( ) ;
1158+ await awaitsFor ( ( ) => syncBtn . classList . contains ( "active" ) ,
1159+ "cursor sync to be re-enabled" ) ;
1160+ } , 10000 ) ;
1161+
1162+ it ( "should cursor sync toggle work in both reader and edit mode" , async function ( ) {
1163+ await _openMdFile ( "long.md" ) ;
1164+
1165+ // Test in reader mode
1166+ await _enterReaderMode ( ) ;
1167+ let syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1168+ expect ( syncBtn ) . not . toBeNull ( ) ;
1169+ expect ( syncBtn . classList . contains ( "active" ) ) . toBeTrue ( ) ;
1170+
1171+ syncBtn . click ( ) ;
1172+ await awaitsFor ( ( ) => ! syncBtn . classList . contains ( "active" ) ,
1173+ "cursor sync to toggle off in reader mode" ) ;
1174+ expect ( syncBtn . getAttribute ( "aria-pressed" ) ) . toBe ( "false" ) ;
1175+
1176+ syncBtn . click ( ) ;
1177+ await awaitsFor ( ( ) => syncBtn . classList . contains ( "active" ) ,
1178+ "cursor sync to toggle on in reader mode" ) ;
1179+ expect ( syncBtn . getAttribute ( "aria-pressed" ) ) . toBe ( "true" ) ;
1180+
1181+ // Test in edit mode
1182+ await _enterEditMode ( ) ;
1183+ syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1184+ expect ( syncBtn ) . not . toBeNull ( ) ;
1185+ expect ( syncBtn . classList . contains ( "active" ) ) . toBeTrue ( ) ;
1186+
1187+ syncBtn . click ( ) ;
1188+ await awaitsFor ( ( ) => ! syncBtn . classList . contains ( "active" ) ,
1189+ "cursor sync to toggle off in edit mode" ) ;
1190+ expect ( syncBtn . getAttribute ( "aria-pressed" ) ) . toBe ( "false" ) ;
1191+
1192+ syncBtn . click ( ) ;
1193+ await awaitsFor ( ( ) => syncBtn . classList . contains ( "active" ) ,
1194+ "cursor sync to toggle on in edit mode" ) ;
1195+ expect ( syncBtn . getAttribute ( "aria-pressed" ) ) . toBe ( "true" ) ;
1196+ } , 10000 ) ;
1197+
1198+ it ( "should disabling cursor sync in reader mode prevent CM cursor move on click" , async function ( ) {
1199+ await _openMdFile ( "long.md" ) ;
1200+ await _enterReaderMode ( ) ;
1201+
1202+ // Set CM cursor to line 0 as baseline
1203+ const editor = EditorManager . getActiveEditor ( ) ;
1204+ editor . setCursorPos ( 0 , 0 ) ;
1205+ expect ( _getCMCursorLine ( ) ) . toBe ( 0 ) ;
1206+
1207+ // Disable cursor sync
1208+ const syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1209+ syncBtn . click ( ) ;
1210+ await awaitsFor ( ( ) => ! syncBtn . classList . contains ( "active" ) ,
1211+ "cursor sync to be disabled" ) ;
1212+
1213+ // Click a paragraph lower in the document
1214+ const mdDoc = _getMdIFrameDoc ( ) ;
1215+ const paragraphs = mdDoc . querySelectorAll ( '#viewer-content p[data-source-line]' ) ;
1216+ let targetP = null ;
1217+ for ( const p of paragraphs ) {
1218+ const srcLine = parseInt ( p . getAttribute ( "data-source-line" ) , 10 ) ;
1219+ if ( srcLine > 10 ) {
1220+ targetP = p ;
1221+ break ;
1222+ }
1223+ }
1224+ expect ( targetP ) . not . toBeNull ( ) ;
1225+
1226+ // Click directly on the element — bridge.js click handler sends
1227+ // embeddedIframeFocusEditor which MarkdownSync should ignore (sync off)
1228+ targetP . click ( ) ;
1229+
1230+ // Cursor should still be at 0 — the click while sync was off had no effect.
1231+ // Re-enable cursor sync first (re-query btn in case toolbar re-rendered).
1232+ const syncBtnAfter = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1233+ syncBtnAfter . click ( ) ;
1234+ await awaitsFor ( ( ) => syncBtnAfter . classList . contains ( "active" ) ,
1235+ "cursor sync to be re-enabled" ) ;
1236+ expect ( _getCMCursorLine ( ) ) . toBe ( 0 ) ;
1237+ } , 10000 ) ;
1238+
1239+ it ( "should changing CM cursor position scroll md viewer accordingly" , async function ( ) {
1240+ await _openMdFile ( "long.md" ) ;
1241+ await _enterReaderMode ( ) ;
1242+
1243+ const mdDoc = _getMdIFrameDoc ( ) ;
1244+ const viewer = mdDoc . querySelector ( ".app-viewer" ) ;
1245+ const editor = EditorManager . getActiveEditor ( ) ;
1246+
1247+ // Set cursor to line 0 — viewer should scroll to top
1248+ editor . setCursorPos ( 0 , 0 ) ;
1249+ await awaitsFor ( ( ) => viewer . scrollTop < 50 ,
1250+ "viewer to scroll near top when CM cursor at line 0" ) ;
1251+ const topScroll = viewer . scrollTop ;
1252+
1253+ // Set cursor to last line — viewer should scroll down
1254+ const lastLine = editor . lineCount ( ) - 1 ;
1255+ editor . setCursorPos ( lastLine , 0 ) ;
1256+ await awaitsFor ( ( ) => viewer . scrollTop > topScroll + 100 ,
1257+ "viewer to scroll down when CM cursor moves to last line" ) ;
1258+ } , 10000 ) ;
1259+
1260+ it ( "should edit to reader switch re-render with fresh data-source-line attrs" , async function ( ) {
1261+ await _openMdFile ( "long.md" ) ;
1262+ await _enterEditMode ( ) ;
1263+ await _focusMdContent ( ) ;
1264+
1265+ // Add a new heading in edit mode via CM
1266+ const editor = EditorManager . getActiveEditor ( ) ;
1267+ const originalText = editor . document . getText ( ) ;
1268+ editor . document . setText ( "# Original Heading\n\n## Added In Edit\n\nSome new paragraph.\n\n" + originalText ) ;
1269+
1270+ await awaitsFor ( ( ) => {
1271+ const mdDoc = _getMdIFrameDoc ( ) ;
1272+ const h2 = mdDoc && mdDoc . querySelector ( '#viewer-content h2' ) ;
1273+ return h2 && h2 . textContent . includes ( "Added In Edit" ) ;
1274+ } , "new heading to appear in viewer" ) ;
1275+
1276+ // Switch to reader mode — should re-render from CM content
1277+ await _enterReaderMode ( ) ;
1278+
1279+ // Verify data-source-line attributes are present and refreshed
1280+ const mdDoc = _getMdIFrameDoc ( ) ;
1281+ const elements = mdDoc . querySelectorAll ( '#viewer-content [data-source-line]' ) ;
1282+ expect ( elements . length ) . toBeGreaterThan ( 0 ) ;
1283+
1284+ // The new heading should have a data-source-line attribute
1285+ const addedH2 = mdDoc . querySelector ( '#viewer-content h2' ) ;
1286+ expect ( addedH2 ) . not . toBeNull ( ) ;
1287+ expect ( addedH2 . textContent ) . toContain ( "Added In Edit" ) ;
1288+ expect ( addedH2 . hasAttribute ( "data-source-line" ) ) . toBeTrue ( ) ;
1289+
1290+ // Restore original content
1291+ editor . document . setText ( originalText ) ;
1292+ await awaitsFor ( ( ) => {
1293+ const h2 = _getMdIFrameDoc ( ) . querySelector ( '#viewer-content h2' ) ;
1294+ return ! h2 || ! h2 . textContent . includes ( "Added In Edit" ) ;
1295+ } , "viewer to restore after content reset" ) ;
1296+ } , 15000 ) ;
1297+
1298+ it ( "should cursor sync work on newly edited elements after edit to reader switch" , async function ( ) {
1299+ await _openMdFile ( "long.md" ) ;
1300+ await _enterEditMode ( ) ;
1301+ await _focusMdContent ( ) ;
1302+
1303+ // Add a distinctive paragraph at the top
1304+ const editor = EditorManager . getActiveEditor ( ) ;
1305+ const originalText = editor . document . getText ( ) ;
1306+ const newContent = "# Top Heading\n\nNewly added paragraph for sync test.\n\n" + originalText ;
1307+ editor . document . setText ( newContent ) ;
1308+
1309+ await awaitsFor ( ( ) => {
1310+ const mdDoc = _getMdIFrameDoc ( ) ;
1311+ const p = mdDoc && mdDoc . querySelector ( '#viewer-content p' ) ;
1312+ return p && p . textContent . includes ( "Newly added paragraph" ) ;
1313+ } , "new paragraph to render" ) ;
1314+
1315+ // Switch to reader mode
1316+ await _enterReaderMode ( ) ;
1317+
1318+ // Find the new paragraph and verify it has a source line for sync
1319+ const mdDoc = _getMdIFrameDoc ( ) ;
1320+ const paragraphs = mdDoc . querySelectorAll ( '#viewer-content p[data-source-line]' ) ;
1321+ let newP = null ;
1322+ for ( const p of paragraphs ) {
1323+ if ( p . textContent . includes ( "Newly added paragraph" ) ) {
1324+ newP = p ;
1325+ break ;
1326+ }
1327+ }
1328+ expect ( newP ) . not . toBeNull ( ) ;
1329+ const sourceLine = parseInt ( newP . getAttribute ( "data-source-line" ) , 10 ) ;
1330+ expect ( sourceLine ) . toBeGreaterThan ( 0 ) ;
1331+
1332+ // Move CM cursor far away so we can verify the click actually moves it
1333+ const editor2 = EditorManager . getActiveEditor ( ) ;
1334+ const farLine = editor2 . lineCount ( ) - 1 ;
1335+ editor2 . setCursorPos ( farLine , 0 ) ;
1336+ expect ( Math . abs ( _getCMCursorLine ( ) - ( sourceLine - 1 ) ) ) . toBeGreaterThan ( 3 ) ;
1337+
1338+ // Click the new paragraph directly — bridge.js click handler
1339+ // sends embeddedIframeFocusEditor, MarkdownSync scrolls CM
1340+ newP . click ( ) ;
1341+
1342+ await awaitsFor ( ( ) => {
1343+ const cmLine = _getCMCursorLine ( ) ;
1344+ return Math . abs ( cmLine - ( sourceLine - 1 ) ) < 3 ;
1345+ } , "CM cursor to move to newly edited element's source line" ) ;
1346+
1347+ // Restore
1348+ editor . document . setText ( originalText ) ;
1349+ } , 15000 ) ;
10991350 } ) ;
11001351
11011352 describe ( "Toolbar & UI" , function ( ) {
0 commit comments