diff --git a/crates/mdbook-core/src/config.rs b/crates/mdbook-core/src/config.rs index 2f3105215a..85319e49e0 100644 --- a/crates/mdbook-core/src/config.rs +++ b/crates/mdbook-core/src/config.rs @@ -485,6 +485,9 @@ pub struct HtmlConfig { /// /// The default is `true`. pub hash_files: bool, + /// If enabled, the sidebar includes navigation for headers on the current + /// page. Default is `true`. + pub sidebar_header_nav: bool, } impl Default for HtmlConfig { @@ -512,6 +515,7 @@ impl Default for HtmlConfig { live_reload_endpoint: None, redirect: HashMap::new(), hash_files: true, + sidebar_header_nav: true, } } } diff --git a/crates/mdbook-html/front-end/css/chrome.css b/crates/mdbook-html/front-end/css/chrome.css index 5fbc23360a..22f77ae925 100644 --- a/crates/mdbook-html/front-end/css/chrome.css +++ b/crates/mdbook-html/front-end/css/chrome.css @@ -730,3 +730,21 @@ html:not(.sidebar-resizing) .sidebar { /* mdbook's margin for h2 is way too large. */ margin: 10px; } + +.current-header { + /* Allows the circle positioning. */ + position: relative +} + +/* Places a circle just before the current header in the sidebar. */ +.current-header::before { + content: ''; + position: absolute; + left: -16px; + top: 0; + margin-top: 10px; + width: 8px; + height: 8px; + background-color: var(--sidebar-active); + border-radius: 50%; +} diff --git a/crates/mdbook-html/front-end/templates/toc.js.hbs b/crates/mdbook-html/front-end/templates/toc.js.hbs index 0e53406c5d..570165e938 100644 --- a/crates/mdbook-html/front-end/templates/toc.js.hbs +++ b/crates/mdbook-html/front-end/templates/toc.js.hbs @@ -72,3 +72,312 @@ class MDBookSidebarScrollbox extends HTMLElement { } } window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox); + +{{#if sidebar_header_nav}} + +// --------------------------------------------------------------------------- +// Support for dynamically adding headers to the sidebar. + +// This is a debugging tool for the threshold which you can enable in the console. +// eslint-disable-next-line prefer-const +let mdbookThresholdDebug = false; + +(function() { + // This is used to detect which direction the page has scrolled since the + // last scroll event. + let lastKnownScrollPosition = 0; + // This is the threshold in px from the top of the screen where it will + // consider a header the "current" header when scrolling down. + const defaultDownThreshold = 150; + // Same as defaultDownThreshold, except when scrolling up. + const defaultUpThreshold = 300; + // The threshold is a virtual horizontal line on the screen where it + // considers the "current" header to be above the line. The threshold is + // modified dynamically to handle headers that are near the bottom of the + // screen, and to slightly offset the behavior when scrolling up vs down. + let threshold = defaultDownThreshold; + // This is used to disable updates while scrolling. This is needed when + // clicking the header in the sidebar, which triggers a scroll event. It + // is somewhat finicky to detect when the scroll has finished, so this + // uses a relatively dumb system of disabling scroll updates for a short + // time after the click. + let disableScroll = false; + // Array of header elements on the page. + let headers; + // Array of li elements that are initially collapsed headers in the sidebar. + // I'm not sure why eslint seems to have a false positive here. + // eslint-disable-next-line prefer-const + let headerToggles = []; + + function drawDebugLine() { + if (!document.body) { + return; + } + const id = 'mdbook-threshold-debug-line'; + const existingLine = document.getElementById(id); + if (existingLine) { + existingLine.remove(); + } + const line = document.createElement('div'); + line.id = id; + line.style.cssText = ` + position: fixed; + top: ${threshold}px; + left: 0; + width: 100vw; + height: 2px; + background-color: red; + z-index: 9999; + pointer-events: none; + `; + document.body.appendChild(line); + } + + // Updates the threshold based on the scroll position. + function updateThreshold() { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const windowHeight = window.innerHeight; + const documentHeight = document.documentElement.scrollHeight; + // The number of pixels below the viewport, at most documentHeight. + // This is used to push the threshold down to the bottom of the page + // as the user scrolls towards the bottom. + const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight)); + // The number of pixels above the viewport, at most defaultDownThreshold. + // Similar to pixelsBelow, this is used to push the threshold back towards + // the top when reaching the top of the page. + const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop); + // How much the threshold should be offset once it gets close to the + // bottom of the page. + let bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold); + + // Adjusts bottomAdd for a small document. The calculation above + // assumes the document is at least twice the windowheight in size. If + // it is less than that, then bottomAdd needs to be shrunk + // proportional to the difference in size. + if (documentHeight < windowHeight * 2) { + const maxPixelsBelow = documentHeight - windowHeight; + const t = 1 - pixelsBelow / maxPixelsBelow; + const clamp = Math.max(0, Math.min(1, t)); + bottomAdd *= clamp; + } + + let scrollingDown = true; + if (scrollTop < lastKnownScrollPosition) { + scrollingDown = false; + } + + if (scrollingDown) { + // When scrolling down, move the threshold up towards the default + // downwards threshold position. If near the bottom of the page, + // bottomAdd will offset the threshold towards the bottom of the + // page. + const amountScrolledDown = scrollTop - lastKnownScrollPosition; + const adjustedDefault = defaultDownThreshold + bottomAdd; + threshold = Math.max(adjustedDefault, threshold - amountScrolledDown); + } else { + // When scrolling up, move the threshold down towards the default + // upwards threshold position. If near the bottom of the page, + // quickly transition the threshold back up where it normally + // belongs. + const amountScrolledUp = lastKnownScrollPosition - scrollTop; + const adjustedDefault = defaultUpThreshold - pixelsAbove + + Math.max(0, bottomAdd - defaultDownThreshold); + threshold = Math.min(adjustedDefault, threshold + amountScrolledUp); + } + lastKnownScrollPosition = scrollTop; + } + + // Updates which headers in the sidebar should be expanded. If the current + // header is inside a collapsed group, then it, and all its parents should + // be expanded. + function updateHeaderExpanded(currentA) { + // Add expanded to all header-item li ancestors. + let current = currentA.parentElement.parentElement.parentElement; + while (current.tagName === 'LI') { + const prevSibling = current.previousElementSibling; + if (prevSibling !== null + && prevSibling.tagName === 'LI' + && prevSibling.classList.contains('header-item')) { + prevSibling.classList.add('expanded'); + current = prevSibling.parentElement.parentElement; + } else { + break; + } + } + } + + // Updates which header is marked as the "current" header in the sidebar. + // This is done with a virtual Y threshold, where headers at or below + // that line will be considered the current one. + function updateCurrentHeader() { + if (mdbookThresholdDebug) { + drawDebugLine(); + } + if (!headers || !headers.length) { + return; + } + + // Reset the classes, which will be rebuilt below. + const els = document.getElementsByClassName('current-header'); + for (const el of els) { + el.classList.remove('current-header'); + } + for (const toggle of headerToggles) { + toggle.classList.remove('expanded'); + } + + // Find the last header that is above the threshold. + let lastHeader = null; + for (const header of headers) { + const rect = header.getBoundingClientRect(); + if (rect.top <= threshold) { + lastHeader = header; + } else { + break; + } + } + if (lastHeader === null) { + lastHeader = headers[0]; + const rect = lastHeader.getBoundingClientRect(); + const windowHeight = window.innerHeight; + if (rect.top >= windowHeight) { + return; + } + } + + // Get the anchor in the summary. + const href = '#' + lastHeader.id; + const a = [...document.querySelectorAll('.header-in-summary')] + .find(element => element.getAttribute('href') === href); + if (!a) { + return; + } + + a.classList.add('current-header'); + + updateHeaderExpanded(a); + } + + // Updates which header is "current" based on the threshold line. + function reloadCurrentHeader() { + if (disableScroll) { + return; + } + updateThreshold(); + updateCurrentHeader(); + } + + + // When clicking on a header in the sidebar, this adjusts the threshold so + // that it is located next to the header. This is so that header becomes + // "current". + function headerThresholdClick(event) { + // See disableScroll description why this is done. + disableScroll = true; + setTimeout(() => { + disableScroll = false; + }, 100); + // requestAnimationFrame is used to delay the update of the "current" + // header until after the scroll is done, and the header is in the new + // position. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + // Closest is needed because if it has child elements like . + const a = event.target.closest('a'); + const href = a.getAttribute('href'); + const targetId = href.substring(1); + const targetElement = document.getElementById(targetId); + if (targetElement) { + threshold = targetElement.getBoundingClientRect().bottom; + updateCurrentHeader(); + } + }); + }); + } + + // Scans page for headers and adds them to the sidebar. + document.addEventListener('DOMContentLoaded', function() { + const activeSection = document.querySelector('#mdbook-sidebar .active'); + if (activeSection === null) { + return; + } + const activeItem = activeSection.parentElement; + const activeList = activeItem.parentElement; + + // Build a tree of headers in the sidebar. + const rootLi = document.createElement('li'); + rootLi.classList.add('header-item'); + rootLi.classList.add('expanded'); + const rootOl = document.createElement('ol'); + rootOl.classList.add('section'); + rootLi.appendChild(rootOl); + const stack = [{ level: 0, ol: rootOl }]; + // The level where it will start folding deeply nested headers. + const foldLevel = 3; + + const main = document.getElementsByTagName('main')[0]; + headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6')) + .filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A'); + + if (headers.length === 0) { + return; + } + + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + const level = parseInt(header.tagName.charAt(1)); + const li = document.createElement('li'); + li.classList.add('header-item'); + li.classList.add('expanded'); + if (level < foldLevel) { + li.classList.add('expanded'); + } + const a = document.createElement('a'); + a.href = '#' + header.id; + a.classList.add('header-in-summary'); + a.innerHTML = header.children[0].innerHTML; + a.addEventListener('click', headerThresholdClick); + li.appendChild(a); + const nextHeader = headers[i + 1]; + if (nextHeader !== undefined) { + const nextLevel = parseInt(nextHeader.tagName.charAt(1)); + if (nextLevel > level && level >= foldLevel) { + const div = document.createElement('div'); + div.textContent = '❱'; + const toggle = document.createElement('a'); + toggle.classList.add('toggle'); + toggle.classList.add('header-toggle'); + toggle.appendChild(div); + toggle.addEventListener('click', () => { + li.classList.toggle('expanded'); + }); + li.appendChild(toggle); + headerToggles.push(li); + } + } + + // Find the appropriate parent level. + while (stack.length > 1 && stack[stack.length - 1].level >= level) { + stack.pop(); + } + + const currentParent = stack[stack.length - 1]; + currentParent.ol.appendChild(li); + + // Create new nested ol for potential children. + const nestedOl = document.createElement('ol'); + nestedOl.classList.add('section'); + const nestedLi = document.createElement('li'); + nestedLi.appendChild(nestedOl); + currentParent.ol.appendChild(nestedLi); + stack.push({ level: level, ol: nestedOl }); + } + + activeList.insertBefore(rootLi, activeItem.nextSibling); + }); + + document.addEventListener('DOMContentLoaded', reloadCurrentHeader); + document.addEventListener('scroll', reloadCurrentHeader, { passive: true }); +})(); + +{{/if}} diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index e703752a7e..7118e3cf4c 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -615,6 +615,10 @@ fn make_data( data.insert("print_enable".to_owned(), json!(html_config.print.enable)); data.insert("fold_enable".to_owned(), json!(html_config.fold.enable)); data.insert("fold_level".to_owned(), json!(html_config.fold.level)); + data.insert( + "sidebar_header_nav".to_owned(), + json!(html_config.sidebar_header_nav), + ); let search = html_config.search.clone(); if cfg!(feature = "search") { diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index ecc61f955d..65a1ac77c6 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -108,6 +108,7 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path site-url = "/example-book/" cname = "myproject.rs" input-404 = "not-found.md" +sidebar-header-nav = true ``` The following configuration options are available: @@ -166,6 +167,7 @@ The following configuration options are available: Chapter HTML files are not renamed. Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives. Defaults to `true`. +- **sidebar-header-nav:** If `true`, the sidebar will contain navigation for headers on the current page. Default is `true`. [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site diff --git a/package.json b/package.json index 99926a43df..cfaa44dd0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "browser-ui-test": "0.21.1", + "browser-ui-test": "0.21.2", "eslint": "^9.34.0" }, "scripts": { diff --git a/test_book/src/SUMMARY.md b/test_book/src/SUMMARY.md index f954546d70..eadfd409f5 100644 --- a/test_book/src/SUMMARY.md +++ b/test_book/src/SUMMARY.md @@ -28,6 +28,14 @@ - [Syntax Highlight](languages/highlight.md) - [Rust Specific](rust/README.md) - [Rust Codeblocks](rust/rust_codeblock.md) +- [Heading Navigation](headings/README.md) + - [Empty page](headings/empty.md) + - [Large text before first heading](headings/large-intro.md) + - [Normal text before first heading](headings/normal-intro.md) + - [Collapsed headings](headings/collapsed.md) + - [Headings with markup](headings/markup.md) + - [Current scrolls to bottom](headings/current-to-bottom.md) +- [Last numbered chapter](last.md) --- diff --git a/test_book/src/headings/README.md b/test_book/src/headings/README.md new file mode 100644 index 0000000000..4dd86b7ea3 --- /dev/null +++ b/test_book/src/headings/README.md @@ -0,0 +1 @@ +# Heading Navigation diff --git a/test_book/src/headings/collapsed.md b/test_book/src/headings/collapsed.md new file mode 100644 index 0000000000..71e885af92 --- /dev/null +++ b/test_book/src/headings/collapsed.md @@ -0,0 +1,91 @@ +# Collapsed headings + +Tests collapsed headings. + +## Heading 1 + +1\ +2\ +3\ +4\ +5 + +### Heading 1.1 + +1\ +2\ +3\ +4\ +5 + +### Heading 1.2 + +1\ +2\ +3\ +4\ +5 + +#### Heading 1.2.1 + +1\ +2\ +3\ +4\ +5 + +#### Heading 1.2.2 + +1\ +2\ +3\ +4\ +5 + +### Heading 1.3 + +1\ +2\ +3\ +4\ +5 + +## Heading 2 + +1\ +2\ +3\ +4\ +5 + +### Heading 2.1 + +1\ +2\ +3\ +4\ +5 + +#### Heading 2.1.1 + +1\ +2\ +3\ +4\ +5 + +##### Heading 2.1.1.1 + +1\ +2\ +3\ +4\ +5 + +###### Heading 2.1.1.1.1 + +1\ +2\ +3\ +4\ +5 diff --git a/test_book/src/headings/current-to-bottom.md b/test_book/src/headings/current-to-bottom.md new file mode 100644 index 0000000000..c0b1967a15 --- /dev/null +++ b/test_book/src/headings/current-to-bottom.md @@ -0,0 +1,46 @@ +# Current scrolls to bottom + +Checks that the "current" header works even when there are headers near the bottom. + +## First header + +1\ +2\ +3\ +4\ +5\ +6\ +7\ +8\ +9\ +10\ +11\ +12\ +13\ +14\ +15\ +16\ +17\ +18\ +19\ +20 + +## Second header + +21 + +### Second sub-header + +22 + +## Third header + +23 + +## Fourth header + +24 + +## Fifth header + +25 diff --git a/test_book/src/headings/empty.md b/test_book/src/headings/empty.md new file mode 100644 index 0000000000..6fb258c759 --- /dev/null +++ b/test_book/src/headings/empty.md @@ -0,0 +1 @@ +# Empty page diff --git a/test_book/src/headings/large-intro.md b/test_book/src/headings/large-intro.md new file mode 100644 index 0000000000..f7574d2181 --- /dev/null +++ b/test_book/src/headings/large-intro.md @@ -0,0 +1,47 @@ +# Large text before first heading + +This tests what happens if there is a lot of text before the first header, which is off the bottom of the screen. + +1 + +2 + +3 + +4 + +5 + +6 + +7 + +8 + +9 + +10 + +11 + +12 + +13 + +14 + +15 + +16 + +17 + +18 + +19 + +20 + +## First header + +Text for first header. diff --git a/test_book/src/headings/markup.md b/test_book/src/headings/markup.md new file mode 100644 index 0000000000..6503c0c474 --- /dev/null +++ b/test_book/src/headings/markup.md @@ -0,0 +1,19 @@ +# Headings with markup + +Tests that heading markup gets copied to the sidebar. + +## Heading with `code` or *italic* or **bold** or ~~strike~~ + +Basic markup should be copied. + +## Heading with a [link](../index.html) + +Probably not super-wise to have headings with links, but at least they shouldn't explode. + +## Heading with a custom id { #custom-id .custom-class } + +Make sure navigation works on a custom id. + +## Heading with html + +What happens if there is inline HTML? diff --git a/test_book/src/headings/normal-intro.md b/test_book/src/headings/normal-intro.md new file mode 100644 index 0000000000..f6d167b32b --- /dev/null +++ b/test_book/src/headings/normal-intro.md @@ -0,0 +1,15 @@ +# Normal text before first heading + +This test is to ensure the first heading shows up as "current" on page load. + +## The first heading + +1 + +2 + +3 + +## The second heading + +### And a sub heading diff --git a/test_book/src/last.md b/test_book/src/last.md new file mode 100644 index 0000000000..91aef1aae7 --- /dev/null +++ b/test_book/src/last.md @@ -0,0 +1 @@ +# Last numbered chapter diff --git a/tests/gui/heading-nav-collapsed.goml b/tests/gui/heading-nav-collapsed.goml new file mode 100644 index 0000000000..66c2bff24c --- /dev/null +++ b/tests/gui/heading-nav-collapsed.goml @@ -0,0 +1,51 @@ +// Tests for collapsed heading sidebar navigation. + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/collapsed.html" + +assert-count: (".header-item", 12) +assert-count: (".current-header", 1) +assert-text: (".current-header", "Heading 1") +// Collapsed elements do not have "expanded" class. +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"}) + +// Click 1.2, doesn't change expanded. +click: "a.header-in-summary[href='#heading-12']" +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"}) +// Click expand chevron. +// 1.2.1 and 1.2.2 should be visible +click: "a.header-in-summary[href='#heading-12'] ~ a.header-toggle" +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item expanded"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "block"}) + +// Click 1.3 +click: "a.header-in-summary[href='#heading-13']" +// Everything should be collapsed +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "none"}) + + +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item"}) +// Scroll to bottom of page +press-key: 'PageDown' +press-key: 'PageDown' +press-key: 'PageDown' +press-key: 'PageDown' +// 2.1.1.1.1 should be visible, and all the chevrons should be open, and expanded should be on each one +assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"}) +assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item expanded"}) +assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item expanded"}) +assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item expanded"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "block"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-211']]]/ol", {"display": "block"}) +assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-2111']]]/ol", {"display": "block"}) diff --git a/tests/gui/heading-nav-current-to-bottom.goml b/tests/gui/heading-nav-current-to-bottom.goml new file mode 100644 index 0000000000..9375fcbea5 --- /dev/null +++ b/tests/gui/heading-nav-current-to-bottom.goml @@ -0,0 +1,58 @@ +// Checks that the "current" header works even when there are headers near the +// bottom. + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/current-to-bottom.html" +assert-count: (".current-header", 1) +assert-text: (".current-header", "First header") + +scroll-to: "#scroll-to-1" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-2" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-3" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-4" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-5" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-6" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-7" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-8" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-9" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-10" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-11" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-12" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-13" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-14" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-15" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-16" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-17" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-18" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-19" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-20" +assert-text: (".current-header", "First header") +scroll-to: "#scroll-to-21" +wait-for-text: (".current-header", "Second sub-header") +scroll-to: "#scroll-to-22" +assert-text: (".current-header", "Second sub-header") +scroll-to: "#scroll-to-23" +assert-text: (".current-header", "Second sub-header") +scroll-to: "#scroll-to-24" +assert-text: (".current-header", "Second sub-header") +scroll-to: "#scroll-to-25" +wait-for-text: (".current-header", "Fifth header") diff --git a/tests/gui/heading-nav-empty.goml b/tests/gui/heading-nav-empty.goml new file mode 100644 index 0000000000..af7de2b785 --- /dev/null +++ b/tests/gui/heading-nav-empty.goml @@ -0,0 +1,6 @@ +// When there aren't any headings, there shouldn't be any header items in the sidebar. + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/empty.html" +assert-count: (".header-item", 0) +assert-count: (".current-header", 0) diff --git a/tests/gui/heading-nav-large-intro.goml b/tests/gui/heading-nav-large-intro.goml new file mode 100644 index 0000000000..a590d1bc47 --- /dev/null +++ b/tests/gui/heading-nav-large-intro.goml @@ -0,0 +1,15 @@ +// When there is a large intro, there shouldn't be any "current" headers until +// you scroll down and make it visible on screen. + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/large-intro.html" +assert-count: (".header-item", 2) +assert-count: (".current-header", 0) + +scroll-to: "#first-header" +wait-for-count: (".current-header", 1) +assert-text: (".current-header", "First header") + +// Scrolling back to the top should set it to 0. +scroll-to: (0, 0) +wait-for-count: (".current-header", 0) diff --git a/tests/gui/heading-nav-markup.goml b/tests/gui/heading-nav-markup.goml new file mode 100644 index 0000000000..e6b37f1741 --- /dev/null +++ b/tests/gui/heading-nav-markup.goml @@ -0,0 +1,19 @@ +// When a header has various markup, the sidebar should replicate it. + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/markup.html" + +assert-count: (".header-item", 5) +assert-count: (".current-header", 1) +assert-text: (".current-header", "Heading with code or italic or bold or strike") +assert-property: (".current-header", {"innerHTML": "Heading with code or italic or bold or strike"}) + +// Clicking the custom one should work and should make it current. +click: "a.header-in-summary[href='#custom-id']" +assert-count: (".current-header", 1) +assert-text: (".current-header", "Heading with a custom id") + +// Click the one with HTML, and check it. +click: "a.header-in-summary[href='#heading-with-html']" +assert-count: (".current-header", 1) +assert-text: (".current-header", "Heading with html") diff --git a/tests/gui/heading-nav-normal-intro.goml b/tests/gui/heading-nav-normal-intro.goml new file mode 100644 index 0000000000..c8d14a3770 --- /dev/null +++ b/tests/gui/heading-nav-normal-intro.goml @@ -0,0 +1,11 @@ +// When there is a normal-sized intro, when the page loads the first heading +// should be "current". + +set-window-size: (1400, 800) +go-to: |DOC_PATH| + "headings/normal-intro.html" +assert-count: (".header-item", 4) +assert-count: (".current-header", 1) +assert-text: (".current-header", "The first heading") + +click: "a[href='#and-a-sub-heading']" +wait-for-text: (".current-header", "And a sub heading") diff --git a/tests/gui/move-between-pages.goml b/tests/gui/move-between-pages.goml index c707ef7314..ec71487f5e 100644 --- a/tests/gui/move-between-pages.goml +++ b/tests/gui/move-between-pages.goml @@ -24,8 +24,8 @@ press-key: 'ArrowRight' assert-text: ("title", "Heading - mdBook test book") // Last numbered page -go-to: "../rust/rust_codeblock.html" -assert-text: ("title", "Rust Codeblocks - mdBook test book") +go-to: "../last.html" +assert-text: ("title", "Last numbered chapter - mdBook test book") // Go to the suffix chapter press-key: 'ArrowRight' diff --git a/tests/gui/search.goml b/tests/gui/search.goml index 6ff9be338d..145bbd1efb 100644 --- a/tests/gui/search.goml +++ b/tests/gui/search.goml @@ -41,7 +41,7 @@ assert-css: ("#mdbook-search-wrapper", {"display": "none"}) press-key: 's' wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"}) // We ensure the search bar has the focus. -assert: "#mdbook-searchbar:focus" +wait-for: "#mdbook-searchbar:focus" // Pressing a key will therefore update the search input. press-key: 't' assert-text: ("#mdbook-searchbar", "t") diff --git a/tests/gui/sidebar-nojs.goml b/tests/gui/sidebar-nojs.goml index 05bbd65a52..9867b0a82a 100644 --- a/tests/gui/sidebar-nojs.goml +++ b/tests/gui/sidebar-nojs.goml @@ -5,8 +5,8 @@ // We disable javascript javascript: false go-to: |DOC_PATH| + "index.html" -store-value: (height, 1000) -set-window-size: (1000, |height|) +store-value: (height, 1028) +set-window-size: (1028, |height|) within-iframe: (".sidebar-iframe-outer", block { assert-size: (" body", {"height": |height|})