feat: redesign homepage from flat card list to book-style pitch page
This commit is contained in:
+260
-14
@@ -2,38 +2,284 @@
|
|||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
import ArticleCard from '../components/ArticleCard.astro';
|
|
||||||
import { getReadingTime } from '../utils/reading-time';
|
import { getReadingTime } from '../utils/reading-time';
|
||||||
|
|
||||||
const posts = await getCollection('posts');
|
const posts = await getCollection('posts');
|
||||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||||
|
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
|
||||||
|
|
||||||
|
const rageSeries = [
|
||||||
|
{ title: 'Your Build Cache Is Lying to You', status: 'coming-soon' },
|
||||||
|
{ title: 'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic', status: 'coming-soon' },
|
||||||
|
{ title: 'Hooking Syscalls Without a Kernel Driver', status: 'coming-soon' },
|
||||||
|
{ title: 'TypeScript 7 Makes the Build Harness More Important', status: 'coming-soon' },
|
||||||
|
{ title: 'node_modules in a Content-Addressed Store', status: 'coming-soon' },
|
||||||
|
{ title: 'What I Learned Maintaining Lage and Why I Rewrote It', status: 'coming-soon' },
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title='Crash Test Dev'>
|
<BaseLayout title='Crash Test Dev'>
|
||||||
<NavBar slot="nav" />
|
<NavBar slot="nav" />
|
||||||
<section class="post-list" aria-label="All posts">
|
|
||||||
|
<section class="hero" aria-label="Introduction">
|
||||||
|
<h1 class="site-title">Crash Test Dev</h1>
|
||||||
|
<p class="tagline">Engineering judgment, tested against reality.</p>
|
||||||
|
<p class="byline">By Ken Hiatt</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<section class="featured" aria-label="Featured excerpt">
|
||||||
|
<p class="featured-label">From the latest</p>
|
||||||
|
<blockquote class="excerpt">
|
||||||
|
AI raises the floor dramatically. It does not flatten the ceiling. The profession that survives is the one hardest to demo without, not just hardest to do without. The professional sees second-order failures coming. The amateur with AI doesn't know there is a second order.
|
||||||
|
</blockquote>
|
||||||
|
<a class="featured-link" href={`${base}posts/ai-commoditizes-features-but-judgment-compounds/`}>
|
||||||
|
Read "AI Commoditizes Features, But Judgment Compounds"
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<section class="series" aria-label="Rage series">
|
||||||
|
<h2 class="section-heading">Rage: Rebuilding the Build System</h2>
|
||||||
|
<p class="series-description">A six-part investigation into rage, a Rust monorepo build system that fuses BuildXL's provable correctness with lage's ergonomics. From syscall sandboxing to content-addressed storage.</p>
|
||||||
|
<ol class="series-list">
|
||||||
|
{rageSeries.map((item) => (
|
||||||
|
<li class="series-item">
|
||||||
|
<span class="series-item-title">{item.title}</span>
|
||||||
|
<span class="series-item-status">Coming soon</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<section class="all-posts" aria-label="All posts">
|
||||||
|
<h2 class="section-heading">All Posts</h2>
|
||||||
{sortedPosts.map((post) => (
|
{sortedPosts.map((post) => (
|
||||||
<ArticleCard
|
<article class="post-entry">
|
||||||
title={post.data.title}
|
<a href={`${base}posts/${post.id}/`} class="post-entry-link">
|
||||||
date={post.data.date}
|
<span class="post-entry-badge">{post.data.type}</span>
|
||||||
type={post.data.type}
|
<span class="post-entry-title">{post.data.title}</span>
|
||||||
summary={post.data.summary}
|
{post.body && (
|
||||||
slug={post.id}
|
<span class="post-entry-meta">{getReadingTime(post.body)} min</span>
|
||||||
readingTime={post.body ? getReadingTime(post.body) : undefined}
|
)}
|
||||||
/>
|
</a>
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.post-list {
|
/* Layout column */
|
||||||
|
.hero,
|
||||||
|
.featured,
|
||||||
|
.series,
|
||||||
|
.all-posts {
|
||||||
max-width: var(--width-prose);
|
max-width: var(--width-prose);
|
||||||
margin-top: var(--space-6);
|
|
||||||
margin-bottom: var(--space-12);
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dividers */
|
||||||
|
.divider {
|
||||||
|
max-width: var(--width-prose);
|
||||||
|
margin: var(--space-6) auto;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero {
|
||||||
|
margin-top: var(--space-10);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .site-title {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-3xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .tagline {
|
||||||
|
font-family: var(--font-prose);
|
||||||
|
font-size: var(--type-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-prose);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .byline {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Featured excerpt */
|
||||||
|
.featured {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-label {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.excerpt {
|
||||||
|
font-family: var(--font-prose);
|
||||||
|
font-size: var(--type-lg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: var(--leading-prose);
|
||||||
|
border-left: 3px solid var(--accent-primary);
|
||||||
|
padding-left: var(--space-3);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-link {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Series */
|
||||||
|
.series {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-xl);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
margin-bottom: var(--space-1-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-description {
|
||||||
|
font-family: var(--font-prose);
|
||||||
|
font-size: var(--type-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-prose);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-list {
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: series;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: var(--space-1-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item {
|
||||||
|
counter-increment: series;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
padding: var(--space-1) var(--space-1-5);
|
||||||
|
border-radius: var(--rounded-md);
|
||||||
|
background-color: var(--surface-card);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item::before {
|
||||||
|
content: counter(series) ".";
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-right: var(--space-1-5);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item-title {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item-status {
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All posts */
|
||||||
|
.all-posts {
|
||||||
|
margin-bottom: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-posts .section-heading {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry {
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-1-5);
|
||||||
|
padding: var(--space-1-5) 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: color var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-link:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-link:hover .post-entry-title {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-badge {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-title {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-base);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
transition: color var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-entry-meta {
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -35,39 +35,45 @@ describe('Clean build output', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 2. Index page structure
|
// 2. Index page structure (book-style pitch page)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
describe('Index page', () => {
|
describe('Index page', () => {
|
||||||
const html = readDist('index.html');
|
const html = readDist('index.html');
|
||||||
|
|
||||||
test('contains two article cards', () => {
|
test('has hero section with site title', () => {
|
||||||
const cardMatches = html.match(/<article[^>]*class="card"/g);
|
assert.ok(html.includes('class="hero"') || html.includes('class=\"hero\"'),
|
||||||
assert.ok(cardMatches, 'No article cards found');
|
'Should have hero section');
|
||||||
assert.equal(cardMatches.length, 2, `Expected 2 article cards, got ${cardMatches.length}`);
|
assert.ok(html.includes('Crash Test Dev'), 'Should have site title');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card surface uses warm off-white (--surface-card)', () => {
|
test('has featured excerpt section', () => {
|
||||||
assert.ok(html.includes('var(--surface-card)'), 'Card should use --surface-card token');
|
assert.ok(html.includes('class="featured"') || html.includes('class=\"featured\"'),
|
||||||
|
'Should have featured section');
|
||||||
|
assert.ok(html.includes('class="excerpt"') || html.includes('class=\"excerpt\"'),
|
||||||
|
'Should have excerpt blockquote');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card has subtle shadow (--shadow-sm)', () => {
|
test('has series section with Rage', () => {
|
||||||
assert.ok(html.includes('var(--shadow-sm)'), 'Card should use --shadow-sm token');
|
assert.ok(html.includes('class="series"') || html.includes('class=\"series\"'),
|
||||||
|
'Should have series section');
|
||||||
|
assert.ok(html.includes('Rage'), 'Should mention Rage series');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card hover shows deeper shadow (--shadow-md)', () => {
|
test('has all-posts section with post entries', () => {
|
||||||
assert.ok(html.includes('var(--shadow-md)'), 'Card hover should use --shadow-md token');
|
assert.ok(html.includes('class="all-posts"') || html.includes('class=\"all-posts\"'),
|
||||||
|
'Should have all-posts section');
|
||||||
|
const entryMatches = html.match(/class="post-entry"/g);
|
||||||
|
assert.ok(entryMatches, 'No post entries found');
|
||||||
|
assert.ok(entryMatches.length >= 2, `Expected at least 2 post entries, got ${entryMatches.length}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card hover shows terracotta title (--accent-primary)', () => {
|
test('uses terracotta accent (--accent-primary)', () => {
|
||||||
assert.ok(
|
// Astro scoped styles compile to external CSS, check source instead
|
||||||
html.includes(':hover .title') || html.includes(':hover .title{'),
|
const source = readFileSync('src/pages/index.astro', 'utf-8');
|
||||||
'Card hover should style the title'
|
assert.ok(source.includes('var(--accent-primary)'), 'Should use terracotta accent in source');
|
||||||
);
|
|
||||||
assert.ok(html.includes('var(--accent-primary)'), 'Title hover should use terracotta accent');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('card links have correct URL format with slash separator', () => {
|
test('post links have correct URL format with slash separator', () => {
|
||||||
// The base is /crashtestdev and posts must be at /crashtestdev/posts/...
|
|
||||||
const linkRegex = /href="([^"]*posts[^"]*)"/g;
|
const linkRegex = /href="([^"]*posts[^"]*)"/g;
|
||||||
let match;
|
let match;
|
||||||
let found = false;
|
let found = false;
|
||||||
@@ -83,7 +89,7 @@ describe('Index page', () => {
|
|||||||
`URL "${url}" should not concatenate base and posts without slash`
|
`URL "${url}" should not concatenate base and posts without slash`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert.ok(found, 'Should find article card links');
|
assert.ok(found, 'Should find post links');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+88
-70
@@ -30,11 +30,6 @@ describe('Index page', () => {
|
|||||||
assert.match(content, /import\s+NavBar\s+from\s+['"].*NavBar\.astro['"]/, 'should import NavBar');
|
assert.match(content, /import\s+NavBar\s+from\s+['"].*NavBar\.astro['"]/, 'should import NavBar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should import ArticleCard', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /import\s+ArticleCard\s+from\s+['"].*ArticleCard\.astro['"]/, 'should import ArticleCard');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should import getReadingTime', () => {
|
it('should import getReadingTime', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /import\s+.*getReadingTime.*from\s+['"].*reading-time['"]/, 'should import getReadingTime');
|
assert.match(content, /import\s+.*getReadingTime.*from\s+['"].*reading-time['"]/, 'should import getReadingTime');
|
||||||
@@ -46,15 +41,89 @@ describe('Index page', () => {
|
|||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /getCollection\s*\(\s*['"]posts['"]\s*\)/, 'should call getCollection("posts")');
|
assert.match(content, /getCollection\s*\(\s*['"]posts['"]\s*\)/, 'should call getCollection("posts")');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should sort posts descending by date', () => {
|
describe('Hero section', () => {
|
||||||
|
it('should have a hero section', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /sort\s*\(/, 'should call sort');
|
assert.match(content, /class="hero"/, 'should have a section with class hero');
|
||||||
assert.match(content, /b\.data\.date\.getTime\(\)\s*-\s*a\.data\.date\.getTime\(\)/, 'should sort descending by date');
|
});
|
||||||
|
|
||||||
|
it('should have the site title Crash Test Dev as a heading', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /<h1[^>]*>Crash Test Dev<\/h1>/, 'should have h1 with site title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a tagline in prose font', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="tagline"/, 'should have a tagline element');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have an author byline', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="byline"/, 'should have a byline element');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Template', () => {
|
describe('Featured excerpt section', () => {
|
||||||
|
it('should have a featured excerpt section', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="featured"/, 'should have a section with class featured');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain an excerpt blockquote', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /<blockquote[^>]*class="excerpt"/, 'should have a blockquote with class excerpt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a link to the full post', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// The featured section should contain a link to read the full post
|
||||||
|
assert.match(content, /class="featured-link"/, 'should have a featured-link to the full post');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Series section', () => {
|
||||||
|
it('should have a series section', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="series"/, 'should have a section with class series');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mention the Rage series', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /Rage/, 'should mention Rage in the series section');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list planned posts', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="series-item"/, 'should have series items');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate coming soon items', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /coming soon/i, 'should indicate coming soon posts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('All posts section', () => {
|
||||||
|
it('should have a posts listing section below the pitch', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="all-posts"/, 'should have a section with class all-posts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map posts to compact entries', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show post titles as links', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// Each post entry should link to its page
|
||||||
|
assert.match(content, /posts\/\$\{post\.id\}/, 'should link to post pages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Layout', () => {
|
||||||
it('should use BaseLayout with title Crash Test Dev', () => {
|
it('should use BaseLayout with title Crash Test Dev', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /<BaseLayout\s+title\s*=\s*['"]Crash Test Dev['"]/, 'should have BaseLayout with title');
|
assert.match(content, /<BaseLayout\s+title\s*=\s*['"]Crash Test Dev['"]/, 'should have BaseLayout with title');
|
||||||
@@ -64,79 +133,28 @@ describe('Index page', () => {
|
|||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /<NavBar\s+slot\s*=\s*["']nav["']/, 'should have NavBar with slot="nav"');
|
assert.match(content, /<NavBar\s+slot\s*=\s*["']nav["']/, 'should have NavBar with slot="nav"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a section with class post-list and aria-label', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /<section\s+class\s*=\s*["']post-list["']/, 'should have section.post-list');
|
|
||||||
assert.match(content, /aria-label\s*=\s*["']All posts["']/, 'should have aria-label="All posts"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should map sortedPosts to ArticleCard components', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
|
|
||||||
assert.match(content, /<ArticleCard/, 'should render ArticleCard');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass title from post.data.title', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /title\s*=\s*\{post\.data\.title\}/, 'should pass title');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass date from post.data.date', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /date\s*=\s*\{post\.data\.date\}/, 'should pass date');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass type from post.data.type', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /type\s*=\s*\{post\.data\.type\}/, 'should pass type');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass summary from post.data.summary', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /summary\s*=\s*\{post\.data\.summary\}/, 'should pass summary');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass slug as post.id', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /slug\s*=\s*\{post\.id\}/, 'should pass slug as post.id');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should compute and pass readingTime from post.body', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /getReadingTime\s*\(\s*post\.body/, 'should call getReadingTime(post.body...)');
|
|
||||||
assert.match(content, /readingTime\s*=\s*\{/, 'should pass readingTime prop');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Style', () => {
|
describe('Style', () => {
|
||||||
it('should have .post-list with prose max-width', () => {
|
it('should use --width-prose for content width', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /\.post-list\s*\{/, 'should have .post-list selector');
|
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should constrain content to prose width');
|
||||||
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should have prose max-width');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have space-6 top and space-12 bottom margins', () => {
|
it('should use --font-structure for hero heading', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /margin-top\s*:\s*var\(--space-6\)/, 'should have space-6 top margin');
|
assert.match(content, /\.hero[\s\S]*?font-family\s*:\s*var\(--font-structure\)/, 'should use structure font in hero');
|
||||||
assert.match(content, /margin-bottom\s*:\s*var\(--space-12\)/, 'should have space-12 bottom margin');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have auto left and right margins', () => {
|
it('should use --font-prose for excerpt', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /margin-left\s*:\s*auto/, 'should have auto left margin');
|
assert.match(content, /\.excerpt[\s\S]*?font-family\s*:\s*var\(--font-prose\)/, 'should use prose font for excerpt');
|
||||||
assert.match(content, /margin-right\s*:\s*auto/, 'should have auto right margin');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have flex column display', () => {
|
it('should not have the old .post-list as the main layout', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /display\s*:\s*flex/, 'should have flex display');
|
// The old flat list structure should be gone
|
||||||
assert.match(content, /flex-direction\s*:\s*column/, 'should have column direction');
|
assert.doesNotMatch(content, /<ArticleCard/, 'should not use ArticleCard component');
|
||||||
});
|
|
||||||
|
|
||||||
it('should have space-2 gap', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /gap\s*:\s*var\(--space-2\)/, 'should have space-2 gap');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user