@@ -1824,6 +1824,104 @@ def test_no_broken_heading_attributes(pkg_name: str):
18241824 )
18251825
18261826
1827+ # ═══════════════════════════════════════════════════════════════════════════════
1828+ # R4: Navbar Rendering (nav)
1829+ # ═══════════════════════════════════════════════════════════════════════════════
1830+
1831+
1832+ @requires_bs4
1833+ @pytest .mark .parametrize ("pkg_name" , _RENDERED_PACKAGES )
1834+ def test_navbar_renders_with_links (pkg_name : str ):
1835+ """The navbar renders with at least one navigation link."""
1836+ index = _site_dir (pkg_name ) / "index.html"
1837+ if not index .exists ():
1838+ pytest .skip ("No index.html" )
1839+
1840+ soup = _load_html (index )
1841+ nav = soup .select_one ("nav.navbar" )
1842+ if nav is None :
1843+ pytest .fail (f"{ pkg_name } : no <nav class='navbar'> element found" )
1844+
1845+ links = nav .select ("a[href]" )
1846+ assert links , f"{ pkg_name } : navbar has no links"
1847+
1848+ # The brand/title link should be present
1849+ brand = nav .select_one (".navbar-brand, .navbar-title" )
1850+ assert brand , f"{ pkg_name } : navbar missing brand/title element"
1851+
1852+
1853+ # ═══════════════════════════════════════════════════════════════════════════════
1854+ # R4: Page Metadata / OG Tags (meta)
1855+ # ═══════════════════════════════════════════════════════════════════════════════
1856+
1857+
1858+ @requires_bs4
1859+ @pytest .mark .parametrize ("pkg_name" , _RENDERED_PACKAGES )
1860+ def test_page_has_meta_description (pkg_name : str ):
1861+ """The landing page has a <meta name='description'> tag."""
1862+ index = _site_dir (pkg_name ) / "index.html"
1863+ if not index .exists ():
1864+ pytest .skip ("No index.html" )
1865+
1866+ soup = _load_html (index )
1867+ meta = soup .select_one ("meta[name='description']" )
1868+ assert meta , f"{ pkg_name } : missing <meta name='description'>"
1869+ content = meta .get ("content" , "" ).strip ()
1870+ assert content , f"{ pkg_name } : meta description is empty"
1871+
1872+
1873+ @requires_bs4
1874+ @pytest .mark .parametrize ("pkg_name" , _RENDERED_PACKAGES )
1875+ def test_page_has_og_title (pkg_name : str ):
1876+ """The landing page has an og:title meta tag."""
1877+ index = _site_dir (pkg_name ) / "index.html"
1878+ if not index .exists ():
1879+ pytest .skip ("No index.html" )
1880+
1881+ soup = _load_html (index )
1882+ og = soup .select_one ("meta[property='og:title']" )
1883+ assert og , f"{ pkg_name } : missing <meta property='og:title'>"
1884+ content = og .get ("content" , "" ).strip ()
1885+ assert content , f"{ pkg_name } : og:title is empty"
1886+
1887+
1888+ # ═══════════════════════════════════════════════════════════════════════════════
1889+ # R4: Accessibility (a11y)
1890+ # ═══════════════════════════════════════════════════════════════════════════════
1891+
1892+
1893+ @requires_bs4
1894+ @pytest .mark .parametrize ("pkg_name" , _RENDERED_PACKAGES )
1895+ def test_heading_hierarchy_no_skips (pkg_name : str ):
1896+ """Heading levels don't skip (e.g., h2 → h4 without h3) in main content.
1897+
1898+ Allows the initial jump from h1 to any level (Quarto sidebars/layouts
1899+ commonly inject headings at varying levels before the first section).
1900+ Only checks forward-skips: going from hN to hM where M > N+1.
1901+ """
1902+ index = _site_dir (pkg_name ) / "index.html"
1903+ if not index .exists ():
1904+ pytest .skip ("No index.html" )
1905+
1906+ soup = _load_html (index )
1907+ main = soup .select_one ("main, #quarto-document-content, main.content" )
1908+ if main is None :
1909+ pytest .skip ("No main content area" )
1910+
1911+ headings = main .select ("h1, h2, h3, h4, h5, h6" )
1912+ if len (headings ) < 3 :
1913+ pytest .skip ("Too few headings to check hierarchy" )
1914+
1915+ levels = [int (h .name [1 ]) for h in headings ]
1916+ # Skip the first heading pair (h1 → h3 is common in Quarto layouts)
1917+ for i in range (2 , len (levels )):
1918+ if levels [i ] > levels [i - 1 ] + 1 :
1919+ pytest .fail (
1920+ f"{ pkg_name } : heading skip from h{ levels [i - 1 ]} to h{ levels [i ]} "
1921+ f"(headings { i } to { i + 1 } )"
1922+ )
1923+
1924+
18271925# ═══════════════════════════════════════════════════════════════════════════════
18281926# R4: Directive Stripping
18291927# ═══════════════════════════════════════════════════════════════════════════════
0 commit comments