Files
computer-playbook/tests/unit/javascript/roles/web-app-docs/files/versions.test.js
T
kevinveenbirkenbachandClaude Opus 5 6eb9a59d14 feat(web-app-docs): merge infinito-sphinx and serve every release on demand
The Sphinx configuration, extensions and generators lived in the separate
infinito-sphinx repository, whose image baked a snapshot of core at build time
from a base image name core no longer publishes. Only core consumed it, and its
generators read core's role layout, so the two repositories drifted without any
CI covering the pair.

The tooling now ships in roles/web-app-docs/files/, and web-app-sphinx is
renamed to web-app-docs. The container is a small service instead of a static
image:

- it keeps a git mirror of the repository in the docs_sites volume;
- /latest/ is the last commit of the default branch, rebuilt whenever a fetch
  finds a new one while the previous build stays online;
- every release tag is selectable in the sidebar switcher and on /versions/,
  and is built the first time someone opens it, behind a progress bar;
- replicas share the volume: the one holding builder.lock fetches and builds,
  the queue and the build states are files beside the sites, so another
  replica resumes the queue when the builder dies.

The theme rewrites every page serially after the build, which took about 75
minutes for the 4544 pages; that pass now runs on the -j workers with the
theme's own function. sphinx-apidoc skips tests/, because
tests/utils/bare_bootstrap_probe.py installs an import blocker when autodoc
imports it and killed the Sphinx workers. The roles overview is generated by a
separate process, so autodoc resolves utils from the version being built, not
from main. The role pages now show galaxy_info.description instead of the
top-level key that never existed.

The Sphinx packages join the dev extra, because the mirrored unit tests of the
tooling import them. min_storage now budgets the built image, the latest site
and a running build's scratch instead of the old image's size. The Playwright
suite is split into front page, versions and on-demand build files; the last
one selects an unbuilt tag in the overview and asserts the build gets queued.

Operators must rename the web-app-sphinx inventory group to web-app-docs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:39:54 +02:00

154 lines
5.0 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const SOURCE = path.join(
__dirname,
"..", "..", "..", "..", "..", "..",
"roles", "web-app-docs", "files", "javascript", "versions.js",
);
const VERSIONS = [
{ name: "latest", built: true, state: "building", progress: 42, phase: "sphinx", log: [] },
{ name: "v14.0.0", built: true, state: "ready", progress: 100, phase: "", log: [] },
{ name: "v13.0.0", built: false, state: "failed", progress: 12, phase: "sphinx", log: ["a", "b"] },
{ name: "v11.6.0", built: false, state: "missing", progress: 0, phase: "", log: [] },
];
function element(tag, fields = {}) {
return {
tag,
children: [],
dataset: {},
value: "",
textContent: "",
listeners: {},
...fields,
append(...nodes) {
this.children.push(...nodes);
},
replaceChildren(...nodes) {
this.children = nodes;
},
addEventListener(type, handler) {
this.listeners[type] = handler;
},
};
}
function load(selectors = {}) {
let ready = null;
const window = {
reloaded: 0,
intervals: [],
location: {
href: "",
reload() {
window.reloaded += 1;
},
},
setInterval(handler, ms) {
window.intervals.push(ms);
},
};
const document = {
addEventListener: (type, handler) => {
if (type === "DOMContentLoaded") {
ready = handler;
}
},
createElement: (tag) => element(tag),
createTextNode: (text) => ({ text }),
querySelectorAll: (selector) => selectors[selector] || [],
querySelector: (selector) =>
selector
.split(",")
.map((part) => (selectors[part.trim()] || [])[0])
.find(Boolean) || null,
};
const fetch = async (url) => ({ json: async () => (url === "/api/versions" ? VERSIONS : null) });
vm.runInNewContext(fs.readFileSync(SOURCE, "utf8"), { document, window, fetch });
return { api: window.docsVersions, window, start: () => ready() };
}
test("urls keep the page when switching versions", () => {
const { api } = load();
assert.equal(api.versionUrl("v14.0.0", "docs/index"), "/v14.0.0/docs/index.html");
assert.equal(api.versionUrl("latest"), "/latest/");
});
test("labels tell built, building and failed versions apart", () => {
const { api } = load();
assert.equal(api.optionLabel(VERSIONS[1]), "v14.0.0");
assert.equal(api.optionLabel(VERSIONS[3]), "v11.6.0 (not built yet)");
assert.equal(api.describe(VERSIONS[0]), "building: sphinx");
assert.equal(api.describe(VERSIONS[2]), "failed: sphinx");
assert.equal(api.describe(VERSIONS[1]), "ready");
});
test("switcher lists every version and selects the current one", () => {
const { api } = load();
const select = element("select", { dataset: { current: "v14.0.0", page: "roles/index" } });
api.fillSwitcher(select, VERSIONS);
assert.deepEqual(
select.children.map((option) => [option.value, option.selected]),
[
["/latest/roles/index.html", false],
["/v14.0.0/roles/index.html", true],
["/v13.0.0/roles/index.html", false],
["/v11.6.0/roles/index.html", false],
],
);
});
test("overview renders one row with link, status and bar per version", () => {
const { api } = load();
const tbody = element("tbody");
api.fillOverview(tbody, VERSIONS);
const cells = tbody.children[0].children.map((cell) => cell.children[0]);
assert.equal(tbody.children.length, 4);
assert.equal(cells[0].href, "/latest/");
assert.equal(cells[1].text, "building: sphinx");
assert.equal(cells[2].value, 42);
});
test("build page shows progress and the failure log, reloads once built", () => {
const { api, window } = load();
const bar = element("progress");
const phase = element("p");
const log = element("pre");
const parts = { progress: bar, "[data-phase]": phase, pre: log };
const section = element("section", { dataset: { build: "v13.0.0" }, querySelector: (s) => parts[s] });
api.followBuild(section, VERSIONS);
assert.deepEqual([bar.value, phase.textContent, log.textContent], [12, "failed: sphinx", "a\nb"]);
assert.equal(window.reloaded, 0);
section.dataset.build = "v14.0.0";
api.followBuild(section, VERSIONS);
assert.equal(window.reloaded, 1);
});
test("start wires the switcher and polls only on overview and build pages", async () => {
const select = element("select", { dataset: { current: "latest", page: "index" } });
const sidebarOnly = load({ "select.docs-version-switcher": [select] });
sidebarOnly.start();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(select.children.length, 4);
assert.deepEqual(sidebarOnly.window.intervals, []);
select.value = "/v14.0.0/index.html";
select.listeners.change();
assert.equal(sidebarOnly.window.location.href, "/v14.0.0/index.html");
const overview = load({ "tbody[data-versions]": [element("tbody")] });
overview.start();
assert.deepEqual(overview.window.intervals, [2000]);
});