Files
MeshCore/test/test_release_features.js
T

153 lines
5.9 KiB
JavaScript

"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.");