@@ -1256,5 +1256,301 @@ define(function (require, exports, module) {
12561256 } , 10000 ) ;
12571257 } ) ;
12581258
1259+ describe ( "Links & Format Bar" , function ( ) {
1260+
1261+ async function _openMdFile ( fileName ) {
1262+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
1263+ "open " + fileName ) ;
1264+ await _waitForMdPreviewReady ( ) ;
1265+ }
1266+
1267+ beforeAll ( async function ( ) {
1268+ if ( testWindow ) {
1269+ if ( LiveDevMultiBrowser . status !== LiveDevMultiBrowser . STATUS_ACTIVE ) {
1270+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ "simple.html" ] ) ,
1271+ "open simple.html for live dev" ) ;
1272+ LiveDevMultiBrowser . open ( ) ;
1273+ await awaitsFor ( ( ) =>
1274+ LiveDevMultiBrowser . status === LiveDevMultiBrowser . STATUS_ACTIVE ,
1275+ "live dev to open" , 20000 ) ;
1276+ }
1277+ }
1278+ } , 30000 ) ;
1279+
1280+ it ( "should format bar and link popover elements exist in edit mode" , async function ( ) {
1281+ await _openMdFile ( "doc1.md" ) ;
1282+ await _enterEditMode ( ) ;
1283+
1284+ const mdDoc = _getMdIFrameDoc ( ) ;
1285+ const bar = mdDoc . getElementById ( "format-bar" ) ;
1286+ expect ( bar ) . not . toBeNull ( ) ;
1287+ expect ( bar . querySelector ( "#fb-bold" ) ) . not . toBeNull ( ) ;
1288+ expect ( bar . querySelector ( "#fb-italic" ) ) . not . toBeNull ( ) ;
1289+ expect ( bar . querySelector ( "#fb-underline" ) ) . not . toBeNull ( ) ;
1290+ expect ( bar . querySelector ( "#fb-link" ) ) . not . toBeNull ( ) ;
1291+
1292+ const popover = mdDoc . getElementById ( "link-popover" ) ;
1293+ expect ( popover ) . not . toBeNull ( ) ;
1294+ } , 10000 ) ;
1295+
1296+ it ( "should adding a link in CM show it in md viewer" , async function ( ) {
1297+ await _openMdFile ( "doc2.md" ) ;
1298+ await _enterEditMode ( ) ;
1299+
1300+ const cm = EditorManager . getActiveEditor ( ) . _codeMirror ;
1301+ const lastLine = cm . lastLine ( ) ;
1302+ cm . replaceRange ( "\n\n[CM Link](https://cm-link-test.example.com)\n" ,
1303+ { line : lastLine , ch : cm . getLine ( lastLine ) . length } ) ;
1304+
1305+ const mdDoc = _getMdIFrameDoc ( ) ;
1306+ await awaitsFor ( ( ) => {
1307+ const link = mdDoc . querySelector ( '#viewer-content a[href="https://cm-link-test.example.com"]' ) ;
1308+ return link && link . textContent . includes ( "CM Link" ) ;
1309+ } , "link from CM to appear in viewer with correct text" ) ;
1310+ } , 10000 ) ;
1311+
1312+ it ( "should editing link URL in CM update it in md viewer" , async function ( ) {
1313+ await _openMdFile ( "doc2.md" ) ;
1314+ await _enterEditMode ( ) ;
1315+
1316+ const cm = EditorManager . getActiveEditor ( ) . _codeMirror ;
1317+ const val = cm . getValue ( ) ;
1318+
1319+ // Add a link
1320+ cm . replaceRange ( "\n[Old Link](https://old-url.example.com)\n" ,
1321+ { line : cm . lastLine ( ) , ch : cm . getLine ( cm . lastLine ( ) ) . length } ) ;
1322+
1323+ const mdDoc = _getMdIFrameDoc ( ) ;
1324+ await awaitsFor ( ( ) =>
1325+ mdDoc . querySelector ( '#viewer-content a[href="https://old-url.example.com"]' ) !== null ,
1326+ "old link to appear in viewer" ) ;
1327+
1328+ // Change the URL in CM
1329+ const cmVal = cm . getValue ( ) ;
1330+ cm . setValue ( cmVal . replace ( "https://old-url.example.com" , "https://new-url.example.com" ) ) ;
1331+
1332+ await awaitsFor ( ( ) =>
1333+ mdDoc . querySelector ( '#viewer-content a[href="https://new-url.example.com"]' ) !== null ,
1334+ "updated link URL to appear in viewer" ) ;
1335+
1336+ // Old URL should be gone
1337+ expect ( mdDoc . querySelector ( '#viewer-content a[href="https://old-url.example.com"]' ) ) . toBeNull ( ) ;
1338+ } , 10000 ) ;
1339+
1340+ it ( "should removing link markup in CM remove link from md viewer" , async function ( ) {
1341+ await _openMdFile ( "doc3.md" ) ;
1342+ await _enterEditMode ( ) ;
1343+
1344+ const cm = EditorManager . getActiveEditor ( ) . _codeMirror ;
1345+
1346+ // Add a link
1347+ cm . replaceRange ( "\n[Remove Me](https://remove-cm.example.com)\n" ,
1348+ { line : cm . lastLine ( ) , ch : cm . getLine ( cm . lastLine ( ) ) . length } ) ;
1349+
1350+ const mdDoc = _getMdIFrameDoc ( ) ;
1351+ await awaitsFor ( ( ) =>
1352+ mdDoc . querySelector ( '#viewer-content a[href="https://remove-cm.example.com"]' ) !== null ,
1353+ "link to appear" ) ;
1354+
1355+ // Remove the link markup — replace [text](url) with just text
1356+ const cmVal = cm . getValue ( ) ;
1357+ cm . setValue ( cmVal . replace ( "[Remove Me](https://remove-cm.example.com)" , "Remove Me" ) ) ;
1358+
1359+ await awaitsFor ( ( ) =>
1360+ mdDoc . querySelector ( '#viewer-content a[href="https://remove-cm.example.com"]' ) === null ,
1361+ "link to be removed from viewer" ) ;
1362+
1363+ // Text should still exist
1364+ await awaitsFor ( ( ) => {
1365+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1366+ return content && content . textContent . includes ( "Remove Me" ) ;
1367+ } , "text to still exist after link removal" ) ;
1368+ } , 10000 ) ;
1369+ } ) ;
1370+
1371+ describe ( "Empty Line Placeholder" , function ( ) {
1372+
1373+ async function _openMdFile ( fileName ) {
1374+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
1375+ "open " + fileName ) ;
1376+ await _waitForMdPreviewReady ( ) ;
1377+ }
1378+
1379+ it ( "should empty paragraph in edit mode show hint text" , async function ( ) {
1380+ await _openMdFile ( "doc1.md" ) ;
1381+ await _enterEditMode ( ) ;
1382+ await _focusMdContent ( ) ;
1383+
1384+ const mdDoc = _getMdIFrameDoc ( ) ;
1385+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1386+
1387+ // Create an empty paragraph by pressing Enter at end
1388+ const lastP = content . querySelector ( "p:last-of-type" ) ;
1389+ if ( lastP ) {
1390+ const range = mdDoc . createRange ( ) ;
1391+ range . selectNodeContents ( lastP ) ;
1392+ range . collapse ( false ) ;
1393+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1394+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1395+ mdDoc . execCommand ( "insertParagraph" ) ;
1396+ }
1397+
1398+ // The new empty paragraph should have the hint class
1399+ await awaitsFor ( ( ) => {
1400+ return content . querySelector ( ".cursor-empty-hint" ) !== null ;
1401+ } , "empty line hint to appear" ) ;
1402+ } , 10000 ) ;
1403+
1404+ it ( "should hint only show in edit mode not reader mode" , async function ( ) {
1405+ // Use doc3 for clean state
1406+ await _openMdFile ( "doc3.md" ) ;
1407+ await _enterReaderMode ( ) ;
1408+
1409+ const mdDoc = _getMdIFrameDoc ( ) ;
1410+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1411+ // No hints in reader mode
1412+ expect ( content . querySelector ( ".cursor-empty-hint" ) ) . toBeNull ( ) ;
1413+ } , 10000 ) ;
1414+ } ) ;
1415+
1416+ describe ( "Slash Menu" , function ( ) {
1417+
1418+ async function _openMdFile ( fileName ) {
1419+ await awaitsForDone ( SpecRunnerUtils . openProjectFiles ( [ fileName ] ) ,
1420+ "open " + fileName ) ;
1421+ await _waitForMdPreviewReady ( ) ;
1422+ }
1423+
1424+ function _isSlashMenuVisible ( ) {
1425+ const mdDoc = _getMdIFrameDoc ( ) ;
1426+ const anchor = mdDoc && mdDoc . getElementById ( "slash-menu-anchor" ) ;
1427+ return anchor && anchor . classList . contains ( "visible" ) ;
1428+ }
1429+
1430+ it ( "should slash menu appear when typing / at start of line" , async function ( ) {
1431+ await _openMdFile ( "doc1.md" ) ;
1432+ await _enterEditMode ( ) ;
1433+ await _focusMdContent ( ) ;
1434+
1435+ const mdDoc = _getMdIFrameDoc ( ) ;
1436+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1437+
1438+ // Create a new empty paragraph
1439+ const lastP = content . querySelector ( "p:last-of-type" ) ;
1440+ if ( lastP ) {
1441+ const range = mdDoc . createRange ( ) ;
1442+ range . selectNodeContents ( lastP ) ;
1443+ range . collapse ( false ) ;
1444+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1445+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1446+ mdDoc . execCommand ( "insertParagraph" ) ;
1447+ }
1448+
1449+ // Type "/" to trigger slash menu
1450+ mdDoc . execCommand ( "insertText" , false , "/" ) ;
1451+ content . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
1452+
1453+ await awaitsFor ( ( ) => _isSlashMenuVisible ( ) ,
1454+ "slash menu to appear after typing /" ) ;
1455+ } , 10000 ) ;
1456+
1457+ it ( "should typing after / filter menu items to show only matches" , async function ( ) {
1458+ await _openMdFile ( "doc3.md" ) ;
1459+ await _enterEditMode ( ) ;
1460+ await _focusMdContent ( ) ;
1461+
1462+ const mdDoc = _getMdIFrameDoc ( ) ;
1463+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1464+
1465+ // Dismiss any leftover slash menu
1466+ if ( _isSlashMenuVisible ( ) ) {
1467+ content . dispatchEvent ( new KeyboardEvent ( "keydown" , {
1468+ key : "Escape" , code : "Escape" , keyCode : 27 ,
1469+ bubbles : true , cancelable : true
1470+ } ) ) ;
1471+ await awaitsFor ( ( ) => ! _isSlashMenuVisible ( ) , "old slash menu to dismiss" ) ;
1472+ }
1473+
1474+ // Place cursor at end of last paragraph, create new line, type /
1475+ const lastP = content . querySelector ( "p:last-of-type" ) ;
1476+ if ( lastP ) {
1477+ const range = mdDoc . createRange ( ) ;
1478+ range . selectNodeContents ( lastP ) ;
1479+ range . collapse ( false ) ;
1480+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1481+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1482+ mdDoc . execCommand ( "insertParagraph" ) ;
1483+ }
1484+
1485+ // Type / to open menu
1486+ mdDoc . execCommand ( "insertText" , false , "/" ) ;
1487+ content . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
1488+ await awaitsFor ( ( ) => _isSlashMenuVisible ( ) , "slash menu to appear" ) ;
1489+
1490+ // Get total items count
1491+ const anchor = mdDoc . getElementById ( "slash-menu-anchor" ) ;
1492+ const totalCount = anchor . querySelectorAll ( ".slash-menu-item" ) . length ;
1493+
1494+ // Now type "image" character by character to filter
1495+ for ( const ch of "image" ) {
1496+ mdDoc . execCommand ( "insertText" , false , ch ) ;
1497+ content . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
1498+ }
1499+
1500+ await awaitsFor ( ( ) => {
1501+ const items = anchor . querySelectorAll ( ".slash-menu-item" ) ;
1502+ // Filtered count should be less than total and items should contain "image"
1503+ return items . length > 0 && items . length < totalCount ;
1504+ } , "slash menu to filter to image items" ) ;
1505+
1506+ // Verify remaining items contain "image"
1507+ const filtered = anchor . querySelectorAll ( ".slash-menu-item" ) ;
1508+ for ( const item of filtered ) {
1509+ expect ( item . textContent . toLowerCase ( ) ) . toContain ( "image" ) ;
1510+ }
1511+
1512+ // Dismiss
1513+ content . dispatchEvent ( new KeyboardEvent ( "keydown" , {
1514+ key : "Escape" , code : "Escape" , keyCode : 27 ,
1515+ bubbles : true , cancelable : true
1516+ } ) ) ;
1517+ } , 10000 ) ;
1518+
1519+ it ( "should Escape dismiss slash menu" , async function ( ) {
1520+ // Open slash menu
1521+ if ( ! _isSlashMenuVisible ( ) ) {
1522+ await _openMdFile ( "doc1.md" ) ;
1523+ await _enterEditMode ( ) ;
1524+ await _focusMdContent ( ) ;
1525+
1526+ const mdDoc = _getMdIFrameDoc ( ) ;
1527+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1528+ const lastP = content . querySelector ( "p:last-of-type" ) ;
1529+ if ( lastP ) {
1530+ const range = mdDoc . createRange ( ) ;
1531+ range . selectNodeContents ( lastP ) ;
1532+ range . collapse ( false ) ;
1533+ _getMdIFrameWin ( ) . getSelection ( ) . removeAllRanges ( ) ;
1534+ _getMdIFrameWin ( ) . getSelection ( ) . addRange ( range ) ;
1535+ mdDoc . execCommand ( "insertParagraph" ) ;
1536+ }
1537+ mdDoc . execCommand ( "insertText" , false , "/" ) ;
1538+ content . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
1539+ await awaitsFor ( ( ) => _isSlashMenuVisible ( ) , "slash menu to appear" ) ;
1540+ }
1541+
1542+ // Dispatch Escape on the content element (where keydown is listened)
1543+ const mdDoc = _getMdIFrameDoc ( ) ;
1544+ const content = mdDoc . getElementById ( "viewer-content" ) ;
1545+ content . dispatchEvent ( new KeyboardEvent ( "keydown" , {
1546+ key : "Escape" , code : "Escape" , keyCode : 27 ,
1547+ bubbles : true , cancelable : true
1548+ } ) ) ;
1549+
1550+ await awaitsFor ( ( ) => ! _isSlashMenuVisible ( ) ,
1551+ "slash menu to dismiss on Escape" ) ;
1552+ } , 10000 ) ;
1553+ } ) ;
1554+
12591555 } ) ;
12601556} ) ;
0 commit comments