Included in 1.18.0.1 compared with mainline MeshCore 1.18.0, checked 24 October 2026. This includes earlier Cascade additions and extensions to existing mainline features. Availability depends on the board and firmware role; some features require compatible clients or default off.
diff --git a/mkdocs.yml b/mkdocs.yml index 558851cec..2c3b9014c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -132,6 +132,7 @@ extra_css: - _stylesheets/esp32_partition_catalog.css?v=20261001-1 extra_javascript: + - _javascript/release_features.js?v=20261024-1 - _javascript/hosted_doc_links.js?v=20261007-1 - _javascript/notification_builder.js?v=20261006-channel9 - https://unpkg.com/leaflet@1.9.4/dist/leaflet.js diff --git a/test/test_release_features.js b/test/test_release_features.js new file mode 100644 index 000000000..82ccd728f --- /dev/null +++ b/test/test_release_features.js @@ -0,0 +1,152 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const vm = require("node:vm"); +const { openReleaseFeatures, followReleaseFeaturesLink } = require("../docs/_javascript/release_features.js"); + +const page = "https://mikecarper.github.io/MeshCore/releases/1.18.0.1/"; +const script = fs.readFileSync(require.resolve("../docs/_javascript/release_features.js"), "utf8"); + +function fixture(hash = "", readyState = "complete") { + const documentHandlers = new Map(); + const windowHandlers = new Map(); + let navigation; + let element; + const location = { href: page + hash, hash }; + const doc = { + baseURI: page, + readyState, + getElementById(id) { + assert.equal(id, "extra-features"); + return element; + }, + addEventListener(name, callback, options) { + if (name === "DOMContentLoaded") assert.equal(options.once, true); + assert.equal(documentHandlers.has(name), false, "handlers are installed once per page"); + documentHandlers.set(name, callback); + } + }; + function replaceDetails(tagName = "DETAILS") { + element = { tagName, open: false, scrolls: 0, scrollIntoView(options) { + assert.equal(this.open, true, "expand before scrolling"); + assert.equal(options.block, "start"); + this.scrolls++; + } }; + return element; + } + replaceDetails(); + return { + doc, location, replaceDetails, + get details() { return element; }, + removeDetails() { element = null; }, + boot() { + vm.runInNewContext(script, { + URL, + window: { location, addEventListener(name, callback) { windowHandlers.set(name, callback); } }, + document: doc, + document$: { subscribe(callback) { navigation = callback; } } + }); + }, + ready() { documentHandlers.get("DOMContentLoaded")(); }, + navigate(hash) { location.hash = hash; location.href = page + hash; windowHandlers.get("hashchange")(); }, + materialNavigate() { navigation(); }, + click(event) { documentHandlers.get("click")(event); } + }; +} + +function click(href, properties = {}, attributes = {}) { + const values = { href, ...attributes }; + const link = { + getAttribute(name) { return values[name] ?? null; }, + hasAttribute(name) { return Object.hasOwn(values, name); } + }; + return { + button: 0, + target: { closest(selector) { assert.equal(selector, "a[href]"); return link; } }, + preventDefault() { assert.fail("normal link navigation must be preserved"); }, + ...properties + }; +} + +// An initial fragment must work both before and after DOM readiness. +const loading = fixture("#extra-features", "loading"); +loading.boot(); +assert.equal(loading.details.open, false); +loading.ready(); +assert.equal(loading.details.open, true); +assert.equal(loading.details.scrolls, 1); +const ready = fixture("#extra-features"); +ready.boot(); +assert.equal(ready.details.open, true); + +// Unrelated hashes do not expand, including after an instant-navigation update. +const ordinary = fixture("#install"); +ordinary.boot(); +ordinary.materialNavigate(); +assert.equal(ordinary.details.open, false); +assert.equal(ordinary.details.scrolls, 0); +ordinary.navigate("#extra-features"); +assert.equal(ordinary.details.open, true); +const replacement = ordinary.replaceDetails(); +ordinary.materialNavigate(); +assert.equal(replacement.open, true); +assert.equal(replacement.scrolls, 1); + +// Clicking the already-selected fragment reopens a manually closed list. +replacement.open = false; +ordinary.click(click("#extra-features")); +assert.equal(replacement.open, true); +assert.equal(replacement.scrolls, 2); +replacement.open = false; +ordinary.click(click(page + "#extra-features")); +assert.equal(replacement.open, true); + +// Same-page clicks work without depending on a future hashchange event. +const directClick = fixture(); +directClick.boot(); +directClick.click(click("#extra-features")); +assert.equal(directClick.details.open, true); +assert.equal(directClick.location.hash, "", "the browser owns URL navigation"); + +// Preserve normal browser actions and links to other pages or sites. +const rejected = [ + click("#install"), + click("#extra-features", { defaultPrevented: true }), + ...["ctrlKey", "metaKey", "shiftKey", "altKey"].map(key => click("#extra-features", { [key]: true })), + click("#extra-features", { button: 1 }), + click("#extra-features", { button: 2 }), + click("#extra-features", {}, { target: "_blank" }), + click("#extra-features", {}, { target: "another-window" }), + click("#extra-features", {}, { download: "features.html" }), + click("https://example.org/MeshCore/releases/1.18.0.1/#extra-features"), + click("../1.18.0/#extra-features"), + click("?different=1#extra-features"), + click("mailto:example@example.org#extra-features"), + { button: 0, target: null }, + { button: 0, target: { closest() { return null; } } } +]; +for (const event of rejected) { + const node = ordinary.replaceDetails(); + assert.equal(followReleaseFeaturesLink(event, ordinary.doc, ordinary.location), false); + assert.equal(node.open, false); + assert.equal(node.scrolls, 0); +} + +// Text nodes inside links, explicit same-frame targets and a base URL are supported. +const textClick = click("#extra-features", {}, { target: "_self" }); +textClick.target = { parentElement: textClick.target }; +assert.equal(followReleaseFeaturesLink(textClick, ordinary.doc, ordinary.location), true); +ordinary.doc.baseURI = "https://example.org/"; +const untouched = ordinary.replaceDetails(); +assert.equal(followReleaseFeaturesLink(click("#extra-features"), ordinary.doc, ordinary.location), false); +assert.equal(untouched.open, false); + +// Missing sections and an accidentally reused ID are harmless. +ordinary.removeDetails(); +assert.equal(openReleaseFeatures(ordinary.doc, "#extra-features"), false); +ordinary.replaceDetails("DIV"); +assert.equal(openReleaseFeatures(ordinary.doc, "#extra-features"), false); +assert.equal(openReleaseFeatures(ordinary.doc, "#extra-features-other"), false); + +console.log("Release feature links: startup, hash, repeat click, navigation and browser-action guards passed.");