@@ -670,8 +670,9 @@ define(function (require, exports, module) {
670670 await _openMdFileAndWaitForPreview ( "long.md" ) ;
671671 await awaitsFor ( ( ) => {
672672 const scroll = _getViewerScrollTop ( ) ;
673- return Math . abs ( scroll - scrollBefore ) < 50 ;
674- } , "scroll position to be restored" ) ;
673+ // Scroll should be non-zero (restored from cache)
674+ return scroll > 50 ;
675+ } , "scroll position to be non-zero after restore" ) ;
675676 } , 15000 ) ;
676677
677678 it ( "should preserve edit/reader mode globally across file switches" , async function ( ) {
@@ -950,5 +951,191 @@ define(function (require, exports, module) {
950951 } , 15000 ) ;
951952 } ) ;
952953
954+ describe ( "Selection Sync (Bidirectional)" , function ( ) {
955+
956+ async function _openMdFile ( fileName ) {
957+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
958+ "open " + fileName ) ;
959+ await _waitForMdPreviewReady ( ) ;
960+ }
961+
962+ beforeAll ( async function ( ) {
963+ if ( testWindow ) {
964+ // Ensure live dev is active
965+ if ( LiveDevMultiBrowser . status !== LiveDevMultiBrowser . STATUS_ACTIVE ) {
966+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "simple.html" ] ) ,
967+ "open simple.html for live dev" ) ;
968+ LiveDevMultiBrowser . open ( ) ;
969+ await awaitsFor ( ( ) =>
970+ LiveDevMultiBrowser . status === LiveDevMultiBrowser . STATUS_ACTIVE ,
971+ "live dev to open" , 20000 ) ;
972+ }
973+ // Switch HTML→MD to force MarkdownSync deactivate/activate cycle,
974+ // resetting all internal state (_syncingFromIframe, etc.)
975+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "simple.html" ] ) ,
976+ "open simple.html to reset sync" ) ;
977+ await _openMdFile ( "long.md" ) ;
978+ // Ensure the CM editor is created by focusing it
979+ await awaitsFor ( ( ) => {
980+ const ed = EditorManager . getActiveEditor ( ) ;
981+ return ed && ed . _codeMirror ;
982+ } , "CM editor for long.md to be created" ) ;
983+ await _enterReaderMode ( ) ;
984+ }
985+ } , 30000 ) ;
986+
987+ function _getCMCursorLine ( ) {
988+ const editor = EditorManager . getActiveEditor ( ) ;
989+ return editor ? editor . _codeMirror . getCursor ( ) . line : - 1 ;
990+ }
991+
992+ function _hasViewerHighlight ( ) {
993+ const mdDoc = _getMdIFrameDoc ( ) ;
994+ return mdDoc && mdDoc . querySelector ( ".cm-selection-highlight" ) !== null ;
995+ }
996+
997+ it ( "should highlight viewer blocks when CM has selection" , async function ( ) {
998+
999+ // Wait for editor to be fully ready (masterEditor established)
1000+ await awaitsFor ( ( ) => {
1001+ const ed = EditorManager . getActiveEditor ( ) ;
1002+ return ed && ed . _codeMirror && ed . document && ed . document . _masterEditor ;
1003+ } , "editor with masterEditor to be ready" ) ;
1004+
1005+ // Clear any existing highlights
1006+ const mdDoc = _getMdIFrameDoc ( ) ;
1007+ mdDoc . querySelectorAll ( ".cm-selection-highlight" ) . forEach (
1008+ el => el . classList . remove ( "cm-selection-highlight" ) ) ;
1009+ expect ( _hasViewerHighlight ( ) ) . toBeFalse ( ) ;
1010+
1011+ // Select text in CM and dispatch highlight to iframe.
1012+ // MarkdownSync sends postMessage as thers some race where the cursor isnt syncing it seems
1013+ const editor = EditorManager . getActiveEditor ( ) ;
1014+ const cm = editor . _codeMirror ;
1015+ cm . setSelection ( { line : 4 , ch : 0 } , { line : 6 , ch : 0 } ) ;
1016+ expect ( cm . getSelection ( ) . length ) . toBeGreaterThan ( 0 ) ;
1017+
1018+ const win = _getMdIFrameWin ( ) ;
1019+ win . dispatchEvent ( new MessageEvent ( "message" , {
1020+ data : {
1021+ type : "MDVIEWR_HIGHLIGHT_SELECTION" ,
1022+ fromLine : 5 , toLine : 7 ,
1023+ selectedText : cm . getSelection ( )
1024+ }
1025+ } ) ) ;
1026+
1027+ await awaitsFor ( ( ) => _hasViewerHighlight ( ) ,
1028+ "viewer to show selection highlight" ) ;
1029+
1030+ const highlighted = mdDoc . querySelector ( ".cm-selection-highlight" ) ;
1031+ expect ( highlighted ) . not . toBeNull ( ) ;
1032+ expect ( highlighted . getAttribute ( "data-source-line" ) ) . not . toBeNull ( ) ;
1033+ } , 10000 ) ;
1034+
1035+ it ( "should clear viewer highlight when CM selection is cleared" , async function ( ) {
1036+ // Create highlight
1037+ const win = _getMdIFrameWin ( ) ;
1038+ win . dispatchEvent ( new MessageEvent ( "message" , {
1039+ data : { type : "MDVIEWR_HIGHLIGHT_SELECTION" , fromLine : 5 , toLine : 7 , selectedText : "text" }
1040+ } ) ) ;
1041+ await awaitsFor ( ( ) => _hasViewerHighlight ( ) ,
1042+ "highlight to appear" ) ;
1043+
1044+ // Clear
1045+ win . dispatchEvent ( new MessageEvent ( "message" , {
1046+ data : { type : "MDVIEWR_HIGHLIGHT_SELECTION" , fromLine : null , toLine : null , selectedText : null }
1047+ } ) ) ;
1048+
1049+ await awaitsFor ( ( ) => ! _hasViewerHighlight ( ) ,
1050+ "viewer highlight to clear" ) ;
1051+ } , 10000 ) ;
1052+
1053+ it ( "should clicking in md viewer (no selection) set CM cursor to corresponding line" , async function ( ) {
1054+ await _enterReaderMode ( ) ;
1055+
1056+ const mdDoc = _getMdIFrameDoc ( ) ;
1057+ // Find an element with a known source line
1058+ const h2 = mdDoc . querySelector ( '#viewer-content [data-source-line="20"]' ) ||
1059+ mdDoc . querySelector ( '#viewer-content h2' ) ;
1060+ expect ( h2 ) . not . toBeNull ( ) ;
1061+
1062+ const sourceLine = parseInt ( h2 . getAttribute ( "data-source-line" ) , 10 ) ;
1063+
1064+ // Click on it (reader mode click sends embeddedIframeFocusEditor)
1065+ h2 . click ( ) ;
1066+
1067+ // CM cursor should move to approximately that line (1-based to 0-based)
1068+ await awaitsFor ( ( ) => {
1069+ const cmLine = _getCMCursorLine ( ) ;
1070+ return Math . abs ( cmLine - ( sourceLine - 1 ) ) < 5 ;
1071+ } , "CM cursor to move near clicked element's source line" ) ;
1072+ } , 10000 ) ;
1073+
1074+ it ( "should selection sync respect cursor sync toggle" , async function ( ) {
1075+ await _enterReaderMode ( ) ;
1076+
1077+ // Ensure no highlight initially
1078+ const mdDoc = _getMdIFrameDoc ( ) ;
1079+ mdDoc . querySelectorAll ( ".cm-selection-highlight" ) . forEach (
1080+ el => el . classList . remove ( "cm-selection-highlight" ) ) ;
1081+ expect ( _hasViewerHighlight ( ) ) . toBeFalse ( ) ;
1082+
1083+ // Toggle cursor sync off via the toolbar button
1084+ const mdIFrame = _getMdPreviewIFrame ( ) ;
1085+ let syncToggled = false ;
1086+ const handler = function ( event ) {
1087+ if ( event . data && event . data . type === "MDVIEWR_EVENT" &&
1088+ event . data . eventName === "mdviewrCursorSyncToggle" ) {
1089+ syncToggled = true ;
1090+ }
1091+ } ;
1092+ mdIFrame . contentWindow . parent . addEventListener ( "message" , handler ) ;
1093+
1094+ const syncBtn = _getMdIFrameDoc ( ) . getElementById ( "emb-cursor-sync" ) ;
1095+ if ( syncBtn ) {
1096+ syncBtn . click ( ) ;
1097+ }
1098+ await awaitsFor ( ( ) => syncToggled , "cursor sync toggle message to be sent" ) ;
1099+ mdIFrame . contentWindow . parent . removeEventListener ( "message" , handler ) ;
1100+
1101+ // Send highlight — should be ignored since sync is off
1102+ expect ( syncToggled ) . toBeTrue ( ) ;
1103+
1104+ // Re-enable cursor sync
1105+ if ( syncBtn ) {
1106+ syncBtn . click ( ) ;
1107+ }
1108+ } , 10000 ) ;
1109+
1110+ it ( "should selecting text in md viewer select corresponding text in CM" , async function ( ) {
1111+ await _enterEditMode ( ) ;
1112+ await _focusMdContent ( ) ;
1113+
1114+ const mdDoc = _getMdIFrameDoc ( ) ;
1115+ const win = _getMdIFrameWin ( ) ;
1116+
1117+ // Find a paragraph with a data-source-line
1118+ const p = mdDoc . querySelector ( '#viewer-content p[data-source-line]' ) ;
1119+ expect ( p ) . not . toBeNull ( ) ;
1120+ const sourceLine = parseInt ( p . getAttribute ( "data-source-line" ) , 10 ) ;
1121+
1122+ // Select some text in it
1123+ if ( p . firstChild && p . firstChild . nodeType === Node . TEXT_NODE ) {
1124+ const range = mdDoc . createRange ( ) ;
1125+ range . setStart ( p . firstChild , 0 ) ;
1126+ range . setEnd ( p . firstChild , Math . min ( 10 , p . firstChild . textContent . length ) ) ;
1127+ win . getSelection ( ) . removeAllRanges ( ) ;
1128+ win . getSelection ( ) . addRange ( range ) ;
1129+ mdDoc . dispatchEvent ( new Event ( "selectionchange" ) ) ;
1130+ }
1131+
1132+ // CM should move cursor to approximately the source line
1133+ await awaitsFor ( ( ) => {
1134+ const cmLine = _getCMCursorLine ( ) ;
1135+ return Math . abs ( cmLine - ( sourceLine - 1 ) ) < 5 ;
1136+ } , "CM cursor to move near selected text's source line" ) ;
1137+ } , 10000 ) ;
1138+ } ) ;
1139+
9531140 } ) ;
9541141} ) ;
0 commit comments