@@ -192,15 +192,31 @@ define(function (require, exports, module) {
192192 mdDoc . dispatchEvent ( new Event ( "selectionchange" ) ) ;
193193 }
194194
195- async function _waitForMdPreviewReady ( ) {
195+ /**
196+ * Wait for the md preview iframe to be fully ready and synced with the given editor.
197+ * Verifies: iframe visible, bridge initialized, content rendered, suppression cleared,
198+ * and the viewer's loaded markdown matches the editor's content.
199+ * @param {Object } editor - The active Editor instance whose content should be synced to the viewer.
200+ */
201+ async function _waitForMdPreviewReady ( editor ) {
202+ const expectedSrc = editor ? editor . _codeMirror . getValue ( ) : null ;
196203 await awaitsFor ( ( ) => {
197204 const mdIFrame = _getMdPreviewIFrame ( ) ;
198205 if ( ! mdIFrame || mdIFrame . style . display === "none" ) { return false ; }
199206 if ( ! mdIFrame . src || ! mdIFrame . src . includes ( "mdViewer" ) ) { return false ; }
200- // Wait for bridge to initialize (exposes test helpers)
201207 const win = mdIFrame . contentWindow ;
202- return win && typeof win . __setEditModeForTest === "function" ;
203- } , "md preview to be ready with bridge initialized" ) ;
208+ if ( ! win || typeof win . __setEditModeForTest !== "function" ) { return false ; }
209+ if ( win . __isSuppressingContentChange && win . __isSuppressingContentChange ( ) ) { return false ; }
210+ const content = mdIFrame . contentDocument && mdIFrame . contentDocument . getElementById ( "viewer-content" ) ;
211+ if ( ! content || content . children . length === 0 ) { return false ; }
212+ if ( ! EditorManager . getActiveEditor ( ) ) { return false ; }
213+ // Verify the viewer has synced with the editor's content
214+ if ( expectedSrc ) {
215+ const viewerSrc = win . __getCurrentContent && win . __getCurrentContent ( ) ;
216+ if ( viewerSrc !== expectedSrc ) { return false ; }
217+ }
218+ return true ;
219+ } , "md preview synced with editor content" ) ;
204220 }
205221
206222 describe ( "livepreview:Markdown Editor" , function ( ) {
@@ -248,7 +264,7 @@ define(function (require, exports, module) {
248264 // Now open the test markdown file
249265 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "test-shortcuts.md" ] ) ,
250266 "open test-shortcuts.md" ) ;
251- await _waitForMdPreviewReady ( ) ;
267+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
252268 testFilePath = testFolder + "/test-shortcuts.md" ;
253269 }
254270 } , 30000 ) ;
@@ -291,38 +307,6 @@ define(function (require, exports, module) {
291307 }
292308 }
293309
294- let _tempFileCounter = 0 ;
295-
296- /**
297- * Create a fresh temp .md file with clean content, open it, and wait for
298- * the md preview to be ready. Avoids CM→iframe re-render races.
299- */
300- async function _openFreshMdFile ( content ) {
301- content = content || ORIGINAL_MD_CONTENT ;
302- _tempFileCounter ++ ;
303- const tempPath = testFolder + "/_test_temp_" + _tempFileCounter + ".md" ;
304- await SpecRunnerUtils . createTextFileAsync ( tempPath , content ) ;
305- await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "_test_temp_" + _tempFileCounter + ".md" ] ) ,
306- "open temp md file" ) ;
307- await _waitForMdPreviewReady ( ) ;
308- // Wait for viewer to have the content rendered and settled
309- await awaitsFor ( ( ) => {
310- const win = _getMdIFrameWin ( ) ;
311- const mdDoc = _getMdIFrameDoc ( ) ;
312- const el = mdDoc && mdDoc . getElementById ( "viewer-content" ) ;
313- return el && el . querySelector ( "h1, p" ) &&
314- win && win . __isSuppressingContentChange && ! win . __isSuppressingContentChange ( ) ;
315- } , "temp file content to render and settle" ) ;
316- return tempPath ;
317- }
318-
319- async function _cleanupTempFiles ( ) {
320- for ( let i = 1 ; i <= _tempFileCounter ; i ++ ) {
321- const p = testFolder + "/_test_temp_" + i + ".md" ;
322- await SpecRunnerUtils . deletePathAsync ( p , true ) ;
323- }
324- }
325-
326310 describe ( "Keyboard Shortcut Forwarding" , function ( ) {
327311
328312 function _listenForShortcut ( key ) {
@@ -581,7 +565,7 @@ define(function (require, exports, module) {
581565 async function _openMdFileAndWaitForPreview ( fileName ) {
582566 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
583567 "open " + fileName ) ;
584- await _waitForMdPreviewReady ( ) ;
568+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
585569 }
586570
587571 function _getViewerScrollTop ( ) {
@@ -722,7 +706,7 @@ define(function (require, exports, module) {
722706 // Now open an md file in the other project
723707 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "readme.md" ] ) ,
724708 "open readme.md in other project" ) ;
725- await _waitForMdPreviewReady ( ) ;
709+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
726710
727711 // Edit mode should be preserved
728712 await _assertMdEditMode ( true ) ;
@@ -761,7 +745,7 @@ define(function (require, exports, module) {
761745 await awaitsForDone ( CommandManager . execute ( Commands . FILE_LIVE_FILE_PREVIEW ) ) ;
762746 await awaitsFor ( ( ) => WorkspaceManager . isPanelVisible ( "live-preview-panel" ) ,
763747 "live preview panel to reopen" ) ;
764- await _waitForMdPreviewReady ( ) ;
748+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
765749
766750 // Verify iframe persisted (JS variable survived)
767751 const win = _getMdIFrameWin ( ) ;
@@ -936,7 +920,7 @@ define(function (require, exports, module) {
936920 async function _openMdFile ( fileName ) {
937921 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
938922 "open " + fileName ) ;
939- await _waitForMdPreviewReady ( ) ;
923+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
940924 }
941925
942926 beforeAll ( async function ( ) {
@@ -1122,7 +1106,7 @@ define(function (require, exports, module) {
11221106 async function _openMdFile ( fileName ) {
11231107 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
11241108 "open " + fileName ) ;
1125- await _waitForMdPreviewReady ( ) ;
1109+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
11261110 }
11271111
11281112 beforeAll ( async function ( ) {
@@ -1261,7 +1245,7 @@ define(function (require, exports, module) {
12611245 async function _openMdFile ( fileName ) {
12621246 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
12631247 "open " + fileName ) ;
1264- await _waitForMdPreviewReady ( ) ;
1248+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
12651249 }
12661250
12671251 beforeAll ( async function ( ) {
@@ -1366,14 +1350,91 @@ define(function (require, exports, module) {
13661350 return content && content . textContent . includes ( "Remove Me" ) ;
13671351 } , "text to still exist after link removal" ) ;
13681352 } , 10000 ) ;
1353+
1354+ it ( "should link popover allow editing link URL in viewer and sync to CM" , async function ( ) {
1355+ await _openMdFile ( "doc2.md" ) ;
1356+ await _enterEditMode ( ) ;
1357+ await _focusMdContent ( ) ;
1358+
1359+ const mdDoc = _getMdIFrameDoc ( ) ;
1360+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1361+ const link = content . querySelector ( "a[href*='test-link-doc2']" ) ;
1362+ expect ( link ) . not . toBeNull ( ) ;
1363+ const range = mdDoc . createRange ( ) ;
1364+ range . selectNodeContents ( link ) ;
1365+ range . collapse ( true ) ;
1366+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1367+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1368+ content . dispatchEvent ( new KeyboardEvent ( "keyup" , {
1369+ key : "ArrowRight" , code : "ArrowRight" , bubbles : true
1370+ } ) ) ;
1371+
1372+ await awaitsFor ( ( ) => {
1373+ const popover = mdDoc . getElementById ( "link-popover" ) ;
1374+ return popover && popover . classList . contains ( "visible" ) ;
1375+ } , "link popover to appear" ) ;
1376+
1377+ // Edit via popover
1378+ const popover = mdDoc . getElementById ( "link-popover" ) ;
1379+ popover . querySelector ( ".link-popover-edit-btn" ) . click ( ) ;
1380+ popover . querySelector ( ".link-popover-input" ) . value = "https://edited-popover.example.com" ;
1381+ popover . querySelector ( ".link-popover-confirm-btn" ) . click ( ) ;
1382+
1383+ await awaitsFor ( ( ) =>
1384+ content . querySelector ( "a[href='https://edited-popover.example.com']" ) !== null ,
1385+ "edited URL in viewer" ) ;
1386+
1387+ // Old URL should be gone
1388+ expect ( content . querySelector ( "a[href*='test-link-doc2']" ) ) . toBeNull ( ) ;
1389+
1390+ // Force close without saving
1391+ await awaitsForDone ( CommandManager . execute ( Commands . FILE_CLOSE , { _forceClose : true } ) ,
1392+ "force close doc2.md" ) ;
1393+ } , 15000 ) ;
1394+
1395+ it ( "should link popover allow removing link in viewer and sync to CM" , async function ( ) {
1396+ await _openMdFile ( "doc3.md" ) ;
1397+ await _enterEditMode ( ) ;
1398+ await _focusMdContent ( ) ;
1399+
1400+ const mdDoc = _getMdIFrameDoc ( ) ;
1401+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1402+ const link = content . querySelector ( "a[href*='remove-link-doc3']" ) ;
1403+ expect ( link ) . not . toBeNull ( ) ;
1404+ const range = mdDoc . createRange ( ) ;
1405+ range . selectNodeContents ( link ) ;
1406+ range . collapse ( true ) ;
1407+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1408+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1409+ content . dispatchEvent ( new KeyboardEvent ( "keyup" , {
1410+ key : "ArrowRight" , code : "ArrowRight" , bubbles : true
1411+ } ) ) ;
1412+
1413+ await awaitsFor ( ( ) => {
1414+ const popover = mdDoc . getElementById ( "link-popover" ) ;
1415+ return popover && popover . classList . contains ( "visible" ) ;
1416+ } , "link popover to appear" ) ;
1417+
1418+ mdDoc . getElementById ( "link-popover" ) . querySelector ( ".link-popover-unlink-btn" ) . click ( ) ;
1419+
1420+ await awaitsFor ( ( ) =>
1421+ content . querySelector ( "a[href*='remove-link-doc3']" ) === null ,
1422+ "link removed from viewer via popover" ) ;
1423+
1424+ expect ( content . textContent ) . toContain ( "Remove Link" ) ;
1425+
1426+ // Force close without saving
1427+ await awaitsForDone ( CommandManager . execute ( Commands . FILE_CLOSE , { _forceClose : true } ) ,
1428+ "force close doc3.md" ) ;
1429+ } , 15000 ) ;
13691430 } ) ;
13701431
13711432 describe ( "Empty Line Placeholder" , function ( ) {
13721433
13731434 async function _openMdFile ( fileName ) {
13741435 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
13751436 "open " + fileName ) ;
1376- await _waitForMdPreviewReady ( ) ;
1437+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
13771438 }
13781439
13791440 it ( "should empty paragraph in edit mode show hint text" , async function ( ) {
@@ -1402,8 +1463,8 @@ define(function (require, exports, module) {
14021463 } , 10000 ) ;
14031464
14041465 it ( "should hint only show in edit mode not reader mode" , async function ( ) {
1405- // Use doc3 for clean state
1406- await _openMdFile ( "doc3 .md" ) ;
1466+ // Use doc2 for clean state (not modified by previous tests)
1467+ await _openMdFile ( "doc2 .md" ) ;
14071468 await _enterReaderMode ( ) ;
14081469
14091470 const mdDoc = _getMdIFrameDoc ( ) ;
@@ -1418,7 +1479,7 @@ define(function (require, exports, module) {
14181479 async function _openMdFile ( fileName ) {
14191480 await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
14201481 "open " + fileName ) ;
1421- await _waitForMdPreviewReady ( ) ;
1482+ await _waitForMdPreviewReady ( EditorManager . getActiveEditor ( ) ) ;
14221483 }
14231484
14241485 function _isSlashMenuVisible ( ) {
0 commit comments