Merge pull request 'fix(footer): send each documentation link to the section its label names' (#20) from fix/footer-docs-links into main
Reviewed-on: #20
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
"check:csp": "node scripts/checkCsp.mjs",
|
||||
"play:datasafety": "node scripts/playDataSafety.mjs",
|
||||
"beta": "node scripts/beta.mjs",
|
||||
"test": "node --test test/beta.test.mjs test/legal.test.mjs",
|
||||
"test": "node --test test/beta.test.mjs test/legal.test.mjs test/footer.test.mjs",
|
||||
"brand:assets": "node scripts/buildBrandAssets.mjs",
|
||||
"screens:capture": "node scripts/captureScreens.mjs",
|
||||
"csp:hashes": "node scripts/checkCsp.mjs --reset && astro build && node scripts/checkCsp.mjs --write && astro build && node scripts/checkCsp.mjs",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { renderBrand } from '../lib/brand.mjs';
|
||||
import { legal } from '../data/legal.mjs';
|
||||
import { footerColumns } from '../data/footer.mjs';
|
||||
import platform from '../data/platform.json';
|
||||
|
||||
/**
|
||||
@@ -19,33 +20,9 @@ const brand = renderBrand(Astro);
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
heading: 'Product',
|
||||
links: [
|
||||
{ href: '/features/', label: 'Features' },
|
||||
{ href: '/architecture/', label: 'Architecture' },
|
||||
{ href: '/modules/', label: 'Modules' },
|
||||
{ href: '/app/', label: 'Android app' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Documentation',
|
||||
links: [
|
||||
{ href: '/docs/', label: 'Getting started' },
|
||||
{ href: '/docs/', label: 'Administration' },
|
||||
{ href: '/docs/', label: 'Building a module' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Project',
|
||||
links: [
|
||||
{ href: brand.giteaOrg, label: 'Source' },
|
||||
{ href: brand.discordInvite, label: 'Discord' },
|
||||
{ href: '/community/', label: 'Community' },
|
||||
],
|
||||
},
|
||||
];
|
||||
// The columns live in src/data/footer.mjs so a test can read them — see the note there,
|
||||
// and test/footer.test.mjs. The two Project links come from the mounted brand (§7).
|
||||
const columns = footerColumns(brand);
|
||||
|
||||
const isExternal = (href: string) => href.startsWith('http');
|
||||
---
|
||||
|
||||
57
src/data/footer.mjs
Normal file
57
src/data/footer.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* The footer's link columns.
|
||||
*
|
||||
* Data rather than markup, and in `src/data/` beside `legal.mjs` and `collection.mjs`, for
|
||||
* one reason: the links shipped wrong. All three entries under "Documentation" pointed at
|
||||
* `/docs/`, so the column rendered three different labels that went to the same page — and
|
||||
* every check passed, because each href resolved perfectly well. `checkLinks.mjs` asks
|
||||
* whether a link is broken; nothing asked whether a link goes where its label says.
|
||||
*
|
||||
* `test/footer.test.mjs` asks that now, which it can only do because the columns are
|
||||
* importable. That is the whole reason this file exists.
|
||||
*
|
||||
* The two Project links are brand-supplied (§7, D13), so this is a function of the rendered
|
||||
* brand rather than a constant — the mounted `brand.json` decides them, and `/beta` renders
|
||||
* per request, so they cannot be baked at build time.
|
||||
*/
|
||||
|
||||
/** Documentation sections a footer link may point into, and where each one starts. */
|
||||
export const docsEntryPoints = {
|
||||
'getting-started': '/docs/getting-started/requirements/',
|
||||
administration: '/docs/administration/configuration/',
|
||||
modules: '/docs/modules/building-a-module/',
|
||||
};
|
||||
|
||||
export function footerColumns(brand) {
|
||||
return [
|
||||
{
|
||||
heading: 'Product',
|
||||
links: [
|
||||
{ href: '/features/', label: 'Features' },
|
||||
{ href: '/architecture/', label: 'Architecture' },
|
||||
{ href: '/modules/', label: 'Modules' },
|
||||
{ href: '/app/', label: 'Android app' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Each of these lands INSIDE the section it names. The docs home — "What is Runic
|
||||
// Gateway?", the first page of Getting started — is the header's `Docs` link, so a
|
||||
// footer entry pointing there as well would be a fourth way to the same page rather
|
||||
// than a way into the section.
|
||||
heading: 'Documentation',
|
||||
links: [
|
||||
{ href: docsEntryPoints['getting-started'], label: 'Getting started' },
|
||||
{ href: docsEntryPoints.administration, label: 'Administration' },
|
||||
{ href: docsEntryPoints.modules, label: 'Building a module' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Project',
|
||||
links: [
|
||||
{ href: brand.giteaOrg, label: 'Source' },
|
||||
{ href: brand.discordInvite, label: 'Discord' },
|
||||
{ href: '/community/', label: 'Community' },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
97
test/footer.test.mjs
Normal file
97
test/footer.test.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* The footer's links, tested where a mistake is invisible to every other check.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHY THIS FILE EXISTS
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* The footer shipped with all three "Documentation" links pointing at `/docs/`. Three
|
||||
* labels — Getting started, Administration, Building a module — and one destination. It
|
||||
* reached production and stayed there through eleven checks and two test suites, because
|
||||
* none of them could see it:
|
||||
*
|
||||
* - `checkLinks.mjs` resolves every internal link against the build. `/docs/` resolves.
|
||||
* Three links to a page that exists are three valid links.
|
||||
* - `checkSidebar.mjs` compares the docs tree to the planned tree. The footer is not the
|
||||
* sidebar and was never in scope.
|
||||
* - `checkA11y.mjs` checks structure. Three correctly-marked-up links are correct markup.
|
||||
*
|
||||
* The bug is not a broken link. It is a link that goes somewhere other than where its label
|
||||
* says, which is the one property nothing was asserting. So that is what this file asserts,
|
||||
* and the reason `src/data/footer.mjs` exists at all — a column list inside an `.astro`
|
||||
* component cannot be imported by a test.
|
||||
*/
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { footerColumns, docsEntryPoints } from '../src/data/footer.mjs';
|
||||
|
||||
/** A stand-in for the rendered brand; only the two Project links read it. */
|
||||
const brand = {
|
||||
giteaOrg: 'https://gitea.example.com/Org',
|
||||
discordInvite: 'https://discord.gg/example',
|
||||
};
|
||||
|
||||
const columns = footerColumns(brand);
|
||||
const documentation = columns.find((c) => c.heading === 'Documentation');
|
||||
|
||||
test('every link in a column has its own destination', () => {
|
||||
for (const column of columns) {
|
||||
const hrefs = column.links.map((l) => l.href);
|
||||
assert.equal(
|
||||
new Set(hrefs).size,
|
||||
hrefs.length,
|
||||
`the ${column.heading} column has two links pointing at the same page: ${hrefs.join(', ')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('no two columns offer the same destination twice', () => {
|
||||
const all = columns.flatMap((c) => c.links.map((l) => l.href));
|
||||
assert.equal(new Set(all).size, all.length, `a footer destination is repeated: ${all.join(', ')}`);
|
||||
});
|
||||
|
||||
test('each documentation link lands inside the section its label names', () => {
|
||||
// The exact failure that shipped: `/docs/` under all three labels satisfies "starts with
|
||||
// /docs/" but names no section, so the prefixes below are section prefixes, not `/docs/`.
|
||||
const expected = [
|
||||
['Getting started', '/docs/getting-started/'],
|
||||
['Administration', '/docs/administration/'],
|
||||
['Building a module', '/docs/modules/'],
|
||||
];
|
||||
|
||||
for (const [label, prefix] of expected) {
|
||||
const link = documentation.links.find((l) => l.label === label);
|
||||
assert.ok(link, `the Documentation column no longer has a "${label}" link`);
|
||||
assert.ok(
|
||||
link.href.startsWith(prefix),
|
||||
`"${label}" points at ${link.href}, which is not inside ${prefix}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('no documentation link is the docs home, which the header already carries', () => {
|
||||
for (const link of documentation.links) {
|
||||
assert.notEqual(
|
||||
link.href,
|
||||
'/docs/',
|
||||
`"${link.label}" points at the docs home; the header's Docs link is that page`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every documentation entry point is a directory URL', () => {
|
||||
// Astro builds these as directories with an index.html; a missing trailing slash costs a
|
||||
// redirect on every click and reads as a broken path in the status bar.
|
||||
for (const [section, href] of Object.entries(docsEntryPoints)) {
|
||||
assert.ok(href.endsWith('/'), `the ${section} entry point (${href}) needs a trailing slash`);
|
||||
assert.ok(href.startsWith('/docs/'), `the ${section} entry point (${href}) is not under /docs/`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the brand supplies the two Project links rather than the code', () => {
|
||||
const project = columns.find((c) => c.heading === 'Project');
|
||||
const hrefs = project.links.map((l) => l.href);
|
||||
assert.ok(hrefs.includes(brand.giteaOrg), 'the Source link no longer reads brand.giteaOrg');
|
||||
assert.ok(hrefs.includes(brand.discordInvite), 'the Discord link no longer reads brand.discordInvite');
|
||||
});
|
||||
Reference in New Issue
Block a user