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
12 changes: 12 additions & 0 deletions .github/workflows/site-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,15 @@ jobs:

- name: Browser smoke tests
run: npm run browser:test

- name: Lighthouse thresholds
run: npm run lighthouse

- name: Upload Lighthouse reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-reports
path: site/.lighthouseci
if-no-files-found: ignore
include-hidden-files: true
2 changes: 2 additions & 0 deletions site/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules/
dist/
playwright-report/
test-results/
output/
.lighthouseci/
1 change: 1 addition & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0b0b0d" />
<meta name="description" content="Backline is a local-first API regression ledger for backend developers." />
<link rel="canonical" href="https://backline-site-xi.vercel.app/" />
<title>Backline | API regression history</title>
</head>
<body>
Expand Down
28 changes: 28 additions & 0 deletions site/lighthouserc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* global module */

module.exports = {
ci: {
collect: {
startServerCommand: 'npm run preview -- --host 127.0.0.1',
startServerReadyPattern: 'Local',
startServerReadyTimeout: 30_000,
url: ['http://127.0.0.1:4173/'],
numberOfRuns: 3,
settings: {
preset: 'desktop',
},
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.95 }],
'categories:accessibility': ['error', { minScore: 1 }],
'categories:best-practices': ['error', { minScore: 0.95 }],
'categories:seo': ['error', { minScore: 0.95 }],
},
},
upload: {
target: 'filesystem',
outputDir: '.lighthouseci',
},
},
};
3 changes: 2 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"test": "node --test tests/content.test.mjs",
"browser:test": "playwright test"
"browser:test": "playwright test",
"lighthouse": "lhci autorun"
},
"devDependencies": {
"@axe-core/playwright": "^4.12.1",
Expand Down
1 change: 1 addition & 0 deletions site/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineConfig({
timeout: 30_000,
fullyParallel: true,
reporter: 'line',
snapshotPathTemplate: '{testDir}/__screenshots__/{arg}{ext}',
use: {
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-failure',
Expand Down
2 changes: 2 additions & 0 deletions site/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
7 changes: 3 additions & 4 deletions site/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ p { color: var(--muted); line-height: 1.65; }

.section { padding-top: 7.5rem; padding-bottom: 7.5rem; }

.section-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 0.5fr); gap: 4rem; align-items: end; margin-bottom: 2.5rem; }
.section-intro { max-width: 760px; margin-bottom: 2.5rem; }

.section-intro p:not(.eyebrow) { max-width: 390px; margin-bottom: 0.25rem; font-size: 0.95rem; }
.section-intro p:not(.eyebrow) { max-width: 560px; margin-bottom: 0.25rem; font-size: 0.95rem; }

.report-capture { width: 100%; box-shadow: 22px 24px 0 rgba(0, 0, 0, 0.18); }

Expand Down Expand Up @@ -279,11 +279,10 @@ html[data-motion="enabled"] .reveal { transform: translateY(16px); transition: t
html[data-motion="enabled"] .reveal.is-visible { transform: translateY(0); }

@media (max-width: 900px) {
.hero, .section-intro, .policy-section, .architecture-layout, .boundaries-section, .quick-start { grid-template-columns: 1fr; }
.hero, .policy-section, .architecture-layout, .boundaries-section, .quick-start { grid-template-columns: 1fr; }
.hero { gap: 3.5rem; min-height: auto; }
.hero-copy { max-width: 720px; }
.hero-capture { justify-self: start; }
.section-intro { gap: 1.25rem; }
.section-intro p:not(.eyebrow) { max-width: 560px; }
.boundaries-section { gap: 2.8rem; }
.architecture-chain { justify-content: flex-start; }
Expand Down
Binary file added site/tests/__screenshots__/landing-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/tests/__screenshots__/landing-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions site/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@ test('has no accessibility violations in the configured WCAG checks', async ({ p
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();

const requiredRules = new Set([
'aria-command-name',
'button-name',
'color-contrast',
'focus-order-semantics',
'link-name',
'scrollable-region-focusable',
]);

expect(results.violations.filter((violation) => requiredRules.has(violation.id))).toEqual([]);
expect(results.violations).toEqual([]);
});
8 changes: 7 additions & 1 deletion site/tests/content.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import assert from 'node:assert/strict';
import { readFile, stat } from 'node:fs/promises';
import test from 'node:test';

const [html, css, terminalCapture, reportCapture] = await Promise.all([
const [html, css, robots, terminalCapture, reportCapture] = await Promise.all([
readFile(new URL('../index.html', import.meta.url), 'utf8'),
readFile(new URL('../src/style.css', import.meta.url), 'utf8'),
readFile(new URL('../public/robots.txt', import.meta.url), 'utf8'),
stat(new URL('../public/demo/failed-run-diff.webp', import.meta.url)),
stat(new URL('../public/demo/markdown-report.webp', import.meta.url)),
]);
Expand All @@ -30,6 +31,7 @@ test('landing page includes accessible structure and repository links', () => {
'aria-label="Primary navigation"',
'aria-label="backline - home"',
'aria-controls="site-nav"',
'rel="canonical" href="https://backline-site-xi.vercel.app/"',
'https://github.com/ChimdumebiNebolisa/Backline',
]) {
assert.ok(html.includes(phrase), `expected page to contain ${phrase}`);
Expand All @@ -52,3 +54,7 @@ test('landing page uses authentic demo captures and a direct setup path', () =>
test('visible copy avoids typographic dash clutter', () => {
assert.doesNotMatch(html, /[\u2014\u2013]/);
});

test('crawler policy allows the landing page to be indexed', () => {
assert.equal(robots, 'User-agent: *\nAllow: /\n');
});
65 changes: 64 additions & 1 deletion site/tests/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ test('renders the landing page and primary paths on desktop', async ({ page }) =

await expect(page).toHaveTitle('Backline | API regression history');
await expect(page.getByRole('heading', { name: 'Regression history for APIs that change.' })).toBeVisible();
await expect(page.getByRole('img', { name: /failed broken-endpoint run and its diff/i })).toBeVisible();
const terminalCapture = page.getByRole('img', { name: /failed broken-endpoint run and its diff/i });
const reportCapture = page.getByRole('img', { name: /two passed and one failed check/i });

await expect(terminalCapture).toBeVisible();
await expect(terminalCapture).toHaveAttribute('src', '/demo/failed-run-diff.webp');
await expect(reportCapture).toHaveAttribute('src', '/demo/markdown-report.webp');
expect(await terminalCapture.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
expect(await reportCapture.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true);
await expect(page.locator('[aria-label="Start locally"]')).toContainText('docker compose up --build -d');
await expect(page.getByRole('link', { name: /Open setup guide/ })).toHaveAttribute(
'href',
Expand All @@ -17,6 +24,17 @@ test('renders the landing page and primary paths on desktop', async ({ page }) =
);
});

test('keeps normal-motion reveal content visible before it settles', async ({ page }) => {
await page.goto('/');

const opacities = await page.locator('[data-reveal]').evaluateAll((elements) =>
elements.map((element) => getComputedStyle(element).opacity),
);

expect(opacities).toContain('1');
expect(opacities.every((opacity) => opacity === '1')).toBe(true);
});

test('supports mobile navigation and returns focus after Escape', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
Expand All @@ -34,10 +52,55 @@ test('supports mobile navigation and returns focus after Escape', async ({ page
await expect(toggle).toBeFocused();
});

test('does not overflow horizontally at the mobile viewport', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');

const dimensions = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));

expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
});

test('honors reduced-motion preference', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/');

const scrollBehavior = await page.evaluate(() => getComputedStyle(document.documentElement).scrollBehavior);
expect(scrollBehavior).toBe('auto');

const revealState = await page.locator('[data-reveal]').evaluateAll((elements) =>
elements.map((element) => {
const style = getComputedStyle(element);
return { opacity: style.opacity, transform: style.transform };
}),
);
expect(revealState).not.toHaveLength(0);
expect(revealState.every((state) => state.opacity === '1' && state.transform === 'none')).toBe(true);
});

test('matches the desktop visual baseline', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/');

await expect(page).toHaveScreenshot('landing-desktop.png', {
animations: 'disabled',
caret: 'hide',
maxDiffPixelRatio: 0.05,
});
});

test('matches the mobile visual baseline', async ({ page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');

await expect(page).toHaveScreenshot('landing-mobile.png', {
animations: 'disabled',
caret: 'hide',
maxDiffPixelRatio: 0.05,
});
});
Loading