Skip to content
Open
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
88 changes: 88 additions & 0 deletions tests/ui/explainers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// cp-j0gw.15 — plain-language standalone explainer pages structural test.
// Each explainer page must surface the same shared patterns: public-data
// badge, top-disclaimer, breadcrumb, verify-against-primary-sources callout,
// and a topic-specific anchor heading.

import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

const EXPLAINER_DIR = resolve(__dirname, '..', '..', 'web', 'explainers');

function read(name: string): string {
return readFileSync(resolve(EXPLAINER_DIR, name), 'utf8');
}

const PAGES = [
{
file: 'M01.html',
title: /M01\s*—\s*How a program is counted/,
topicAnchor: /four-step expansion/i,
sourcesAnchor: /668\.403\(d\)|fail_obbb_cip2_wageb/,
},
{
file: 'M03.html',
title: /M03\s*—\s*How the benchmark is selected/,
topicAnchor: /lowest of three/i,
sourcesAnchor: /84001\(c\)\(3\)\(B\)|668\.402\(c\)\(3\)/,
},
{
file: 'M18.html',
title: /M18\s*—\s*When program failure could spread/,
topicAnchor: /five[- ]?frame|five frames/i,
sourcesAnchor: /668\.16\(t\)|Loper Bright|State Farm/,
},
];

describe('cp-j0gw.15 — plain-language explainer pages', () => {
for (const page of PAGES) {
describe(page.file, () => {
const html = read(page.file);

it('has the public-data-badge', () => {
expect(html).toMatch(/class="public-data-badge"/);
expect(html).toMatch(/Public data only/);
});

it('has the top-disclaimer (not attorneys, not legislative analysts)', () => {
expect(html).toMatch(/class="top-disclaimer"/);
expect(html).toMatch(
/not attorneys.{0,5}not legislative analysts/i,
);
});

it('has a breadcrumb back to home and learn library', () => {
expect(html).toMatch(/href="\.\.\/index\.html"/);
expect(html).toMatch(/href="\.\.\/learn\.html"/);
});

it('has the topic-specific title and anchor', () => {
expect(html).toMatch(page.title);
expect(html).toMatch(page.topicAnchor);
});

it('has a verify-against-primary-sources callout linking to sources.html', () => {
expect(html).toMatch(/class="verify-callout"/);
expect(html).toMatch(/Verify against primary sources/);
// The callout body must link to the public-sources page.
const calloutMatch = html.match(
/<aside class="verify-callout"[\s\S]*?<\/aside>/,
);
expect(calloutMatch).not.toBeNull();
expect(calloutMatch![0]).toMatch(/href="\.\.\/sources\.html"/);
expect(calloutMatch![0]).toMatch(page.sourcesAnchor);
});

it('loads main.css and explainers.css', () => {
expect(html).toMatch(/href="\.\.\/css\/main\.css"/);
expect(html).toMatch(/href="\.\.\/css\/explainers\.css"/);
});

it('has the primary-source-reminder footer', () => {
expect(html).toMatch(
/Re-derive against primary sources before any external submission/,
);
});
});
}
});
150 changes: 150 additions & 0 deletions web/css/explainers.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* explainers.css — standalone plain-language explainer pages (cp-j0gw.15).
Mirrors learn.css patterns (public-data-badge, top-disclaimer) so each
explainer page can render correctly without depending on learn.css. */

.public-data-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #e6f4ea;
color: #14532d;
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
margin-top: 0.75rem;
}

.public-data-badge .badge-dot {
color: #16a34a;
font-size: 0.65rem;
}

@media (prefers-color-scheme: dark) {
.public-data-badge {
background: #14321b;
color: #b6e8c4;
}
}

.top-disclaimer {
margin: 1rem;
padding: 1rem 1.25rem;
background: #fff8e1;
border: 1px solid #f0c14b;
border-left: 4px solid #b07900;
border-radius: var(--radius);
color: #1a1a1a;
}

.top-disclaimer p {
margin: 0;
font-size: 0.95rem;
line-height: 1.5;
}

.top-disclaimer a {
color: #002b6c;
}

@media (prefers-color-scheme: dark) {
.top-disclaimer {
background: #2e2510;
border-color: #b07900;
color: #f4f4f4;
}
.top-disclaimer a {
color: #9ec0ff;
}
}

.explainer-page {
max-width: var(--max-content-width);
margin: 0 auto;
padding: 0 1rem 2rem;
}

.explainer-page > section {
margin-bottom: var(--space-lg);
}

.explainer-page h2 {
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-xs);
}

.explainer-page .lede {
font-size: 1.05rem;
color: var(--color-text);
}

.explainer-page .field-step {
background: var(--color-card);
border: 1px solid var(--color-border);
border-left: 4px solid var(--color-link);
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin: 0.5rem 0;
}

.explainer-page .field-step strong {
display: block;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-link);
margin-bottom: 0.25rem;
}

.verify-callout {
margin: var(--space-lg) 1rem;
padding: 1rem 1.25rem;
background: #eef4fb;
border: 1px solid #b9d2ee;
border-left: 4px solid #0046ad;
border-radius: var(--radius);
}

.verify-callout h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
border-bottom: none;
padding-bottom: 0;
}

.verify-callout p {
margin: 0;
font-size: 0.95rem;
}

.verify-callout a {
font-weight: 600;
}

@media (prefers-color-scheme: dark) {
.verify-callout {
background: #0c1e36;
border-color: #244a78;
color: #f4f4f4;
}
.verify-callout a {
color: #9ec0ff;
}
}

.explainer-nav {
display: flex;
justify-content: space-between;
gap: 1rem;
margin: var(--space-lg) 1rem;
font-size: 0.95rem;
}

.explainer-nav a {
text-decoration: none;
font-weight: 600;
}

.explainer-nav a:hover {
text-decoration: underline;
}
Loading
Loading