nav_tests.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. #!/usr/bin/env python
  2. import sys
  3. import unittest
  4. from mkdocs.structure.nav import get_navigation
  5. from mkdocs.structure.files import File, Files
  6. from mkdocs.structure.pages import Page
  7. from mkdocs.tests.base import dedent, load_config
  8. class SiteNavigationTests(unittest.TestCase):
  9. maxDiff = None
  10. def test_simple_nav(self):
  11. nav_cfg = [
  12. {'Home': 'index.md'},
  13. {'About': 'about.md'}
  14. ]
  15. expected = dedent("""
  16. Page(title='Home', url='/')
  17. Page(title='About', url='/about/')
  18. """)
  19. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  20. files = Files(
  21. [File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  22. for item in nav_cfg]
  23. )
  24. site_navigation = get_navigation(files, cfg)
  25. self.assertEqual(str(site_navigation).strip(), expected)
  26. self.assertEqual(len(site_navigation.items), 2)
  27. self.assertEqual(len(site_navigation.pages), 2)
  28. self.assertEqual(repr(site_navigation.homepage), "Page(title='Home', url='/')")
  29. def test_nav_no_directory_urls(self):
  30. nav_cfg = [
  31. {'Home': 'index.md'},
  32. {'About': 'about.md'}
  33. ]
  34. expected = dedent("""
  35. Page(title='Home', url='/index.html')
  36. Page(title='About', url='/about.html')
  37. """)
  38. cfg = load_config(nav=nav_cfg, use_directory_urls=False, site_url='http://example.com/')
  39. files = Files(
  40. [File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  41. for item in nav_cfg]
  42. )
  43. site_navigation = get_navigation(files, cfg)
  44. self.assertEqual(str(site_navigation).strip(), expected)
  45. self.assertEqual(len(site_navigation.items), 2)
  46. self.assertEqual(len(site_navigation.pages), 2)
  47. def test_nav_missing_page(self):
  48. nav_cfg = [
  49. {'Home': 'index.md'}
  50. ]
  51. expected = dedent("""
  52. Page(title='Home', url='/')
  53. """)
  54. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  55. files = Files([
  56. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  57. File('page_not_in_nav.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  58. ])
  59. site_navigation = get_navigation(files, cfg)
  60. self.assertEqual(str(site_navigation).strip(), expected)
  61. self.assertEqual(len(site_navigation.items), 1)
  62. self.assertEqual(len(site_navigation.pages), 1)
  63. for file in files:
  64. self.assertIsInstance(file.page, Page)
  65. def test_nav_no_title(self):
  66. nav_cfg = [
  67. 'index.md',
  68. {'About': 'about.md'}
  69. ]
  70. expected = dedent("""
  71. Page(title=[blank], url='/')
  72. Page(title='About', url='/about/')
  73. """)
  74. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  75. files = Files([
  76. File(nav_cfg[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  77. File(nav_cfg[1]['About'], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  78. ])
  79. site_navigation = get_navigation(files, cfg)
  80. self.assertEqual(str(site_navigation).strip(), expected)
  81. self.assertEqual(len(site_navigation.items), 2)
  82. self.assertEqual(len(site_navigation.pages), 2)
  83. def test_nav_external_links(self):
  84. nav_cfg = [
  85. {'Home': 'index.md'},
  86. {'Local': '/local.html'},
  87. {'External': 'http://example.com/external.html'}
  88. ]
  89. expected = dedent("""
  90. Page(title='Home', url='/')
  91. Link(title='Local', url='/local.html')
  92. Link(title='External', url='http://example.com/external.html')
  93. """)
  94. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  95. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  96. with self.assertLogs('mkdocs', level='DEBUG') as cm:
  97. site_navigation = get_navigation(files, cfg)
  98. self.assertEqual(
  99. cm.output,
  100. [
  101. "DEBUG:mkdocs.structure.nav:An absolute path to '/local.html' is included in the "
  102. "'nav' configuration, which presumably points to an external resource.",
  103. "DEBUG:mkdocs.structure.nav:An external link to 'http://example.com/external.html' "
  104. "is included in the 'nav' configuration."
  105. ]
  106. )
  107. self.assertEqual(str(site_navigation).strip(), expected)
  108. self.assertEqual(len(site_navigation.items), 3)
  109. self.assertEqual(len(site_navigation.pages), 1)
  110. def test_nav_bad_links(self):
  111. nav_cfg = [
  112. {'Home': 'index.md'},
  113. {'Missing': 'missing.html'},
  114. {'Bad External': 'example.com'}
  115. ]
  116. expected = dedent("""
  117. Page(title='Home', url='/')
  118. Link(title='Missing', url='missing.html')
  119. Link(title='Bad External', url='example.com')
  120. """)
  121. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  122. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  123. with self.assertLogs('mkdocs', level='WARNING') as cm:
  124. site_navigation = get_navigation(files, cfg)
  125. self.assertEqual(
  126. cm.output,
  127. [
  128. "WARNING:mkdocs.structure.nav:A relative path to 'missing.html' is included "
  129. "in the 'nav' configuration, which is not found in the documentation files",
  130. "WARNING:mkdocs.structure.nav:A relative path to 'example.com' is included "
  131. "in the 'nav' configuration, which is not found in the documentation files"
  132. ]
  133. )
  134. self.assertEqual(str(site_navigation).strip(), expected)
  135. self.assertEqual(len(site_navigation.items), 3)
  136. self.assertEqual(len(site_navigation.pages), 1)
  137. def test_indented_nav(self):
  138. nav_cfg = [
  139. {'Home': 'index.md'},
  140. {'API Guide': [
  141. {'Running': 'api-guide/running.md'},
  142. {'Testing': 'api-guide/testing.md'},
  143. {'Debugging': 'api-guide/debugging.md'},
  144. {'Advanced': [
  145. {'Part 1': 'api-guide/advanced/part-1.md'},
  146. ]},
  147. ]},
  148. {'About': [
  149. {'Release notes': 'about/release-notes.md'},
  150. {'License': '/license.html'}
  151. ]},
  152. {'External': 'https://example.com/'}
  153. ]
  154. expected = dedent("""
  155. Page(title='Home', url='/')
  156. Section(title='API Guide')
  157. Page(title='Running', url='/api-guide/running/')
  158. Page(title='Testing', url='/api-guide/testing/')
  159. Page(title='Debugging', url='/api-guide/debugging/')
  160. Section(title='Advanced')
  161. Page(title='Part 1', url='/api-guide/advanced/part-1/')
  162. Section(title='About')
  163. Page(title='Release notes', url='/about/release-notes/')
  164. Link(title='License', url='/license.html')
  165. Link(title='External', url='https://example.com/')
  166. """)
  167. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  168. files = Files([
  169. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  170. File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  171. File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  172. File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  173. File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  174. File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  175. ])
  176. site_navigation = get_navigation(files, cfg)
  177. self.assertEqual(str(site_navigation).strip(), expected)
  178. self.assertEqual(len(site_navigation.items), 4)
  179. self.assertEqual(len(site_navigation.pages), 6)
  180. self.assertEqual(repr(site_navigation.homepage), "Page(title='Home', url='/')")
  181. self.assertIsNone(site_navigation.items[0].parent)
  182. self.assertEqual(site_navigation.items[0].ancestors, [])
  183. self.assertIsNone(site_navigation.items[1].parent)
  184. self.assertEqual(site_navigation.items[1].ancestors, [])
  185. self.assertEqual(len(site_navigation.items[1].children), 4)
  186. self.assertEqual(repr(site_navigation.items[1].children[0].parent), "Section(title='API Guide')")
  187. self.assertEqual(site_navigation.items[1].children[0].ancestors, [site_navigation.items[1]])
  188. self.assertEqual(repr(site_navigation.items[1].children[1].parent), "Section(title='API Guide')")
  189. self.assertEqual(site_navigation.items[1].children[1].ancestors, [site_navigation.items[1]])
  190. self.assertEqual(repr(site_navigation.items[1].children[2].parent), "Section(title='API Guide')")
  191. self.assertEqual(site_navigation.items[1].children[2].ancestors, [site_navigation.items[1]])
  192. self.assertEqual(repr(site_navigation.items[1].children[3].parent), "Section(title='API Guide')")
  193. self.assertEqual(site_navigation.items[1].children[3].ancestors, [site_navigation.items[1]])
  194. self.assertEqual(len(site_navigation.items[1].children[3].children), 1)
  195. self.assertEqual(repr(site_navigation.items[1].children[3].children[0].parent), "Section(title='Advanced')")
  196. self.assertEqual(site_navigation.items[1].children[3].children[0].ancestors,
  197. [site_navigation.items[1].children[3], site_navigation.items[1]])
  198. self.assertIsNone(site_navigation.items[2].parent)
  199. self.assertEqual(len(site_navigation.items[2].children), 2)
  200. self.assertEqual(repr(site_navigation.items[2].children[0].parent), "Section(title='About')")
  201. self.assertEqual(site_navigation.items[2].children[0].ancestors, [site_navigation.items[2]])
  202. self.assertEqual(repr(site_navigation.items[2].children[1].parent), "Section(title='About')")
  203. self.assertEqual(site_navigation.items[2].children[1].ancestors, [site_navigation.items[2]])
  204. self.assertIsNone(site_navigation.items[3].parent)
  205. self.assertEqual(site_navigation.items[3].ancestors, [])
  206. self.assertIsNone(site_navigation.items[3].children)
  207. def test_nested_ungrouped_nav(self):
  208. nav_cfg = [
  209. {'Home': 'index.md'},
  210. {'Contact': 'about/contact.md'},
  211. {'License Title': 'about/sub/license.md'},
  212. ]
  213. expected = dedent("""
  214. Page(title='Home', url='/')
  215. Page(title='Contact', url='/about/contact/')
  216. Page(title='License Title', url='/about/sub/license/')
  217. """)
  218. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  219. files = Files(
  220. [File(list(item.values())[0], cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  221. for item in nav_cfg]
  222. )
  223. site_navigation = get_navigation(files, cfg)
  224. self.assertEqual(str(site_navigation).strip(), expected)
  225. self.assertEqual(len(site_navigation.items), 3)
  226. self.assertEqual(len(site_navigation.pages), 3)
  227. def test_nested_ungrouped_nav_no_titles(self):
  228. nav_cfg = [
  229. 'index.md',
  230. 'about/contact.md',
  231. 'about/sub/license.md'
  232. ]
  233. expected = dedent("""
  234. Page(title=[blank], url='/')
  235. Page(title=[blank], url='/about/contact/')
  236. Page(title=[blank], url='/about/sub/license/')
  237. """)
  238. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  239. files = Files(
  240. [File(item, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) for item in nav_cfg]
  241. )
  242. site_navigation = get_navigation(files, cfg)
  243. self.assertEqual(str(site_navigation).strip(), expected)
  244. self.assertEqual(len(site_navigation.items), 3)
  245. self.assertEqual(len(site_navigation.pages), 3)
  246. self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
  247. @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
  248. def test_nested_ungrouped_no_titles_windows(self):
  249. nav_cfg = [
  250. 'index.md',
  251. 'about\\contact.md',
  252. 'about\\sub\\license.md',
  253. ]
  254. expected = dedent("""
  255. Page(title=[blank], url='/')
  256. Page(title=[blank], url='/about/contact/')
  257. Page(title=[blank], url='/about/sub/license/')
  258. """)
  259. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  260. files = Files(
  261. [File(item, cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) for item in nav_cfg]
  262. )
  263. site_navigation = get_navigation(files, cfg)
  264. self.assertEqual(str(site_navigation).strip(), expected)
  265. self.assertEqual(len(site_navigation.items), 3)
  266. self.assertEqual(len(site_navigation.pages), 3)
  267. def test_nav_from_files(self):
  268. expected = dedent("""
  269. Page(title=[blank], url='/')
  270. Page(title=[blank], url='/about/')
  271. """)
  272. cfg = load_config(site_url='http://example.com/')
  273. files = Files([
  274. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  275. File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  276. ])
  277. site_navigation = get_navigation(files, cfg)
  278. self.assertEqual(str(site_navigation).strip(), expected)
  279. self.assertEqual(len(site_navigation.items), 2)
  280. self.assertEqual(len(site_navigation.pages), 2)
  281. self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
  282. def test_nav_from_nested_files(self):
  283. expected = dedent("""
  284. Page(title=[blank], url='/')
  285. Section(title='About')
  286. Page(title=[blank], url='/about/license/')
  287. Page(title=[blank], url='/about/release-notes/')
  288. Section(title='Api guide')
  289. Page(title=[blank], url='/api-guide/debugging/')
  290. Page(title=[blank], url='/api-guide/running/')
  291. Page(title=[blank], url='/api-guide/testing/')
  292. Section(title='Advanced')
  293. Page(title=[blank], url='/api-guide/advanced/part-1/')
  294. """)
  295. cfg = load_config(site_url='http://example.com/')
  296. files = Files([
  297. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  298. File('about/license.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  299. File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  300. File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  301. File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  302. File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  303. File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  304. ])
  305. site_navigation = get_navigation(files, cfg)
  306. self.assertEqual(str(site_navigation).strip(), expected)
  307. self.assertEqual(len(site_navigation.items), 3)
  308. self.assertEqual(len(site_navigation.pages), 7)
  309. self.assertEqual(repr(site_navigation.homepage), "Page(title=[blank], url='/')")
  310. def test_active(self):
  311. nav_cfg = [
  312. {'Home': 'index.md'},
  313. {'API Guide': [
  314. {'Running': 'api-guide/running.md'},
  315. {'Testing': 'api-guide/testing.md'},
  316. {'Debugging': 'api-guide/debugging.md'},
  317. {'Advanced': [
  318. {'Part 1': 'api-guide/advanced/part-1.md'},
  319. ]},
  320. ]},
  321. {'About': [
  322. {'Release notes': 'about/release-notes.md'},
  323. {'License': 'about/license.md'}
  324. ]}
  325. ]
  326. cfg = load_config(nav=nav_cfg, site_url='http://example.com/')
  327. files = Files([
  328. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  329. File('api-guide/running.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  330. File('api-guide/testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  331. File('api-guide/debugging.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  332. File('api-guide/advanced/part-1.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  333. File('about/release-notes.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  334. File('about/license.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  335. ])
  336. site_navigation = get_navigation(files, cfg)
  337. # Confirm nothing is active
  338. self.assertTrue(all(page.active is False for page in site_navigation.pages))
  339. self.assertTrue(all(item.active is False for item in site_navigation.items))
  340. # Activate
  341. site_navigation.items[1].children[3].children[0].active = True
  342. # Confirm ancestors are activated
  343. self.assertTrue(site_navigation.items[1].children[3].children[0].active)
  344. self.assertTrue(site_navigation.items[1].children[3].active)
  345. self.assertTrue(site_navigation.items[1].active)
  346. # Confirm non-ancestors are not activated
  347. self.assertFalse(site_navigation.items[0].active)
  348. self.assertFalse(site_navigation.items[1].children[0].active)
  349. self.assertFalse(site_navigation.items[1].children[1].active)
  350. self.assertFalse(site_navigation.items[1].children[2].active)
  351. self.assertFalse(site_navigation.items[2].active)
  352. self.assertFalse(site_navigation.items[2].children[0].active)
  353. self.assertFalse(site_navigation.items[2].children[1].active)
  354. # Deactivate
  355. site_navigation.items[1].children[3].children[0].active = False
  356. # Confirm ancestors are deactivated
  357. self.assertFalse(site_navigation.items[1].children[3].children[0].active)
  358. self.assertFalse(site_navigation.items[1].children[3].active)
  359. self.assertFalse(site_navigation.items[1].active)