153 lines
5.9 KiB
JavaScript
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.");
|