Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions scripts/a11y-routes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import site from '../site.config.json' with { type: 'json' };

/**
* Which routes the Lighthouse gate audits.
*
* Derived from site.config.json rather than typed out, because a hand-kept copy
* of the route inventory drifts: /code-signing-policy had to be added in a
* follow-up commit, and /helper shipped with no audit at all. Adding a page to
* the inventory now adds it to the audit, which is the only behaviour that
* cannot fall behind.
*/

/** In the inventory, but not an HTML page a Lighthouse run means anything for. */
const NOT_A_PAGE = new Set(['/rss.xml']);

/**
* Routes built from a template, where auditing every instance would audit the
* same markup over and over. One stands for the family: the accessibility of
* the 104th post is the accessibility of the first.
*/
const TEMPLATE_REPRESENTATIVES = ['/blog/the-tool-goes-to-the-data', '/blog/engine/postgresql'];

export const a11yRoutes = [...site.routes.filter((route) => !NOT_A_PAGE.has(route)), ...TEMPLATE_REPRESENTATIVES];
33 changes: 2 additions & 31 deletions scripts/lighthouse-gate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { existsSync, statSync } from 'node:fs';
import { extname, join, resolve as resolvePath, sep } from 'node:path';
import { launch } from 'chrome-launcher';
import lighthouse from 'lighthouse';
import { a11yRoutes } from './a11y-routes.mjs';

const args = process.argv.slice(2);
const flag = (name, fallback) => {
Expand All @@ -32,37 +33,7 @@ const flag = (name, fallback) => {

const MIN = Number(flag('min', 95));
const DIST = 'dist';
const ROUTES = args.includes('--route')
? args.filter((a, i) => args[i - 1] === '--route')
: [
'/',
'/blog',
'/blog/the-tool-goes-to-the-data',
// The engine archives are one template over seventeen routes, so one
// stands for all of them the way the post above stands for 104.
'/blog/engine/postgresql',
'/faq',
'/get-started',
// Every standalone content page is audited. /playground earns its slot
// twice over: it is the only route with an interactive surface built by
// script at runtime, so it is the one axe-core findings can appear on
// without anyone touching a template.
'/playground',
'/databases',
'/features',
'/open-source',
'/supporters',
'/deploy',
'/docker-compose',
'/libredb-database',
'/security',
'/code-signing-policy',
'/helper',
'/support',
'/platform',
'/compare',
'/privacy-policy',
];
const ROUTES = args.includes('--route') ? args.filter((a, i) => args[i - 1] === '--route') : a11yRoutes;

if (!existsSync(join(DIST, 'index.html'))) {
console.error(`No ${DIST}/index.html — run \`bun run build\` first.`);
Expand Down
21 changes: 9 additions & 12 deletions src/components/home/FlowBand.astro
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,15 @@ const modes = ['out', 'in'] as const;
}

/* "Tool → data" is the working direction: green wire, Studio lands, specs light */
.flow__card[data-switch-active='in'] .diag__line {
background-image: repeating-linear-gradient(90deg, var(--success-text) 0 5px, transparent 5px 10px);
animation-name: lbDashXR;
.flow__card[data-switch-active='in'] .diag {
--diag-dash-color: var(--success-text);
}
/* Running the dashes backwards is a horizontal idea. Below 560px the wire is
drawn top-to-bottom, so it keeps the shared vertical animation instead. */
@media (min-width: 561px) {
.flow__card[data-switch-active='in'] .diag {
--diag-dash-anim: lbDashXR;
}
}
.flow__card[data-switch-active='in'] .diag__chip {
color: var(--success-text);
Expand All @@ -180,15 +186,6 @@ const modes = ['out', 'in'] as const;
color: var(--success-text);
}

/* The stacked layout turns the wire vertical, but the rule above is what sets
this state's animation and it outranks the one in diagram.css, so the axis
has to be restated here or the green wire stops moving. */
@media (max-width: 560px) {
.flow__card[data-switch-active='in'] .diag__line {
background-image: repeating-linear-gradient(180deg, var(--success-text) 0 5px, transparent 5px 10px);
animation-name: lbDashY;
}
}
.flow__card[data-switch-active='in'] .flow__spec {
color: var(--text-brand);
border-color: var(--primary-600);
Expand Down
10 changes: 2 additions & 8 deletions src/components/home/WhySection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ import { why } from '../../data/home';
}

/* step 04 is the one that works: the wire turns green and Studio lands */
.why__card[data-switch-active='3'] .diag__line {
background-image: repeating-linear-gradient(90deg, var(--success-text) 0 5px, transparent 5px 10px);
.why__card[data-switch-active='3'] .diag {
--diag-dash-color: var(--success-text);
}
.why__card[data-switch-active='3'] .diag__chip {
color: var(--success-text);
Expand All @@ -178,12 +178,6 @@ import { why } from '../../data/home';
color: var(--success-text);
}

@media (max-width: 560px) {
.why__card[data-switch-active='3'] .diag__line {
background-image: repeating-linear-gradient(180deg, var(--success-text) 0 5px, transparent 5px 10px);
}
}

.why__status {
font: var(--weight-regular) 11.5px / 1.5 var(--font-mono);
color: var(--text-tertiary);
Expand Down
19 changes: 15 additions & 4 deletions src/styles/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
arrangement, so the base rules live here; each section keeps only its own
state overrides in its scoped <style>.
-------------------------------------------------------------------------- */
/* The wire's axis is one decision, not two: the gradient angle and the keyframes
that scroll it have to agree, and when they disagreed the wire sat still while
looking alive. It is decided here, on the container, and inherited. A state
that only recolours the wire sets --diag-dash-color and touches nothing else. */
.diag {
--diag-dash-angle: 90deg;
--diag-dash-anim: lbDashX;
--diag-dash-color: var(--error-text);
display: flex;
align-items: stretch;
}
Expand Down Expand Up @@ -64,8 +71,12 @@
top: 50%;
height: 2px;
margin-top: -1px;
background-image: repeating-linear-gradient(90deg, var(--error-text) 0 5px, transparent 5px 10px);
animation: lbDashX 1s linear infinite;
background-image: repeating-linear-gradient(
var(--diag-dash-angle),
var(--diag-dash-color) 0 5px,
transparent 5px 10px
);
animation: var(--diag-dash-anim) 1s linear infinite;
}
.diag__chip {
position: absolute;
Expand Down Expand Up @@ -126,6 +137,8 @@

@media (max-width: 560px) {
.diag {
--diag-dash-angle: 180deg;
--diag-dash-anim: lbDashY;
flex-direction: column;
gap: var(--space-04);
}
Expand All @@ -144,7 +157,5 @@
width: 2px;
height: auto;
margin: 0 0 0 -1px;
background-image: repeating-linear-gradient(180deg, var(--error-text) 0 5px, transparent 5px 10px);
animation-name: lbDashY;
}
}
22 changes: 22 additions & 0 deletions tests/dist-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,25 @@ describe('hero product tour', () => {
expect(button?.getAttribute('aria-label')).toBeTruthy();
});
});

/**
* The diagram wire is drawn by a dashed gradient and scrolled by keyframes, and
* the two have to agree on an axis. They were stated together in six places —
* base, mobile, and a green override per section per breakpoint — and one of the
* six was missed: the working state animated along X while drawn along Y, so the
* wire looked alive and sat still. The axis is now set once on .diag and
* inherited; a state that recolours the wire must not restate the gradient.
*/
describe('the diagram wire states its axis once', () => {
it('defines the dashed gradient in exactly one rule', () => {
const gradients = styles().match(/repeating-linear-gradient\(var\(--diag-dash-angle\)/g) ?? [];
expect(gradients.length, 'the wire gradient is defined more than once').toBe(1);
});

it('leaves no state override spelling the gradient out again', () => {
// A literal angle next to a dash colour is the old shape coming back.
const literals =
styles().match(/repeating-linear-gradient\((?:90|180)deg,\s*var\(--(?:error|success)-text\)/g) ?? [];
expect(literals, 'a state restates the gradient instead of setting --diag-dash-color').toEqual([]);
});
});
42 changes: 42 additions & 0 deletions tests/domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,45 @@ describe('the test and production deploys stay separate', () => {
expect(netlify).toMatch(/NETLIFY_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NETLIFY_AUTH_TOKEN\s*\}\}/);
});
});

/**
* The Lighthouse gate audits a list of routes, and that list used to be typed
* out by hand. That is the same second-copy-of-a-list this file exists to
* forbid, and it failed the same way: /code-signing-policy had to be added in a
* follow-up commit, and /helper plus seventeen engine archives shipped in
* v1.3.0 with no audit at all, because adding a page to site.config.json does
* not add it to a list that lives somewhere else.
*
* The list is now derived, so these assert the derivation against dist rather
* than against another literal.
*/
describe('the accessibility gate audits what the site actually builds', () => {
const built: string[] = [];
const walk = (dir: string, prefix = '') => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) walk(`${dir}/${entry.name}`, `${prefix}/${entry.name}`);
else if (entry.name === 'index.html') built.push(prefix || '/');
else if (entry.name === '404.html') built.push('/404');
}
};
walk('dist');

it('covers every standalone page, so a new one cannot ship unaudited', async () => {
const { a11yRoutes } = await import('../scripts/a11y-routes.mjs');
// Blog posts and engine archives are templates over many routes; they are
// covered by a representative below, not one entry per post.
const standalone = built.filter((r) => !r.startsWith('/blog/') && !redirectPaths.includes(r));
const missing = standalone.filter((r) => !a11yRoutes.includes(r));
expect(missing, 'these pages build but are never audited').toEqual([]);
});

it('audits one representative of each templated family', async () => {
const { a11yRoutes } = await import('../scripts/a11y-routes.mjs');
for (const family of ['/blog/', '/blog/engine/']) {
const audited = a11yRoutes.filter((r: string) => r.startsWith(family) && r !== family.replace(/\/$/, ''));
expect(audited.length, `no ${family} page is audited`).toBeGreaterThan(0);
// A representative that no longer builds audits nothing.
for (const route of audited) expect(built, `${route} is audited but not built`).toContain(route);
}
});
});
Loading