feat(web): marketing Button, Nav (with mobile menu) and Footer from Figma, test-first

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx
This commit is contained in:
Claude
2026-10-12 15:00:38 +00:00
parent e314819629
commit 913d7a8832
16 changed files with 471 additions and 1 deletions
+20
View File
@@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Button } from '../../src/marketing/Button.tsx';
describe('Button', () => {
it('renders a link with its label and destination', () => {
render(<Button href="/signup">Get started</Button>);
expect(screen.getByRole('link', { name: 'Get started' })).toHaveAttribute('href', '/signup');
});
it('is the primary (inverse) pill by default', () => {
render(<Button href="/signup">Get started</Button>);
expect(screen.getByRole('link', { name: 'Get started' })).toHaveClass('btn', 'btn-primary', 'btn-md');
});
it('supports the ghost variant and the large size', () => {
render(<Button href="/product" variant="ghost" size="lg">See how it works</Button>);
expect(screen.getByRole('link', { name: 'See how it works' })).toHaveClass('btn-ghost', 'btn-lg');
});
});
+42
View File
@@ -0,0 +1,42 @@
import { render, screen, within } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Footer } from '../../src/marketing/Footer.tsx';
describe('Footer', () => {
it('shows the brand and tagline', () => {
render(<Footer />);
expect(screen.getByText('Your website, handled.')).toBeInTheDocument();
});
it('has the four link columns from the design', () => {
render(<Footer />);
for (const heading of ['Product', 'Company', 'Resources', 'Legal']) {
expect(screen.getByRole('heading', { name: heading })).toBeInTheDocument();
}
});
it('links only to pages that exist', () => {
render(<Footer />);
const nav = screen.getByRole('navigation', { name: 'Footer' });
const links = within(nav).getAllByRole('link').map((a) => [a.textContent, a.getAttribute('href')]);
expect(links).toEqual([
['Overview', '/product'],
['How it works', '/how-it-works'],
['Trust and safety', '/trust'],
['Pricing', '/pricing'],
['Docs', '/docs'],
]);
});
it('shows pages not built yet as plain text, not dead links (D-HL-17)', () => {
render(<Footer />);
for (const label of ['Changelog', 'About', 'Blog', 'Contact', 'Terms', 'Privacy']) {
expect(screen.getByText(label).closest('a')).toBeNull();
}
});
it('shows the copyright line', () => {
render(<Footer />);
expect(screen.getByText('© 2026 Wursor')).toBeInTheDocument();
});
});
+60
View File
@@ -0,0 +1,60 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { Nav } from '../../src/marketing/Nav.tsx';
describe('Nav', () => {
it('links the brand to the home page', () => {
render(<Nav />);
expect(screen.getByRole('link', { name: 'Wursor' })).toHaveAttribute('href', '/');
});
it('lists the five page links', () => {
render(<Nav />);
const primary = screen.getByRole('navigation', { name: 'Primary' });
const hrefs = within(primary).getAllByRole('link').map((a) => [a.textContent, a.getAttribute('href')]);
expect(hrefs).toEqual([
['Product', '/product'],
['How it works', '/how-it-works'],
['Trust', '/trust'],
['Pricing', '/pricing'],
['Docs', '/docs'],
]);
});
it('sends Sign in to sign-in and Get started to sign-up (PX-13)', () => {
render(<Nav />);
expect(screen.getAllByRole('link', { name: 'Sign in' })[0]).toHaveAttribute('href', '/signin');
expect(screen.getAllByRole('link', { name: 'Get started' })[0]).toHaveAttribute('href', '/signup');
});
it('keeps the mobile menu closed until the menu button is pressed', async () => {
render(<Nav />);
const button = screen.getByRole('button', { name: 'Open menu' });
expect(button).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
await userEvent.click(button);
expect(screen.getByRole('button', { name: 'Close menu' })).toHaveAttribute('aria-expanded', 'true');
const menu = screen.getByRole('navigation', { name: 'Mobile' });
expect(within(menu).getAllByRole('link').map((a) => a.textContent)).toEqual([
'Product', 'How it works', 'Trust and safety', 'Pricing', 'Docs', 'Get started',
]);
});
it('closes the mobile menu with Escape', async () => {
render(<Nav />);
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
await userEvent.keyboard('{Escape}');
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
});
it('closes the mobile menu after a link is chosen', async () => {
render(<Nav />);
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
const menu = screen.getByRole('navigation', { name: 'Mobile' });
await userEvent.click(within(menu).getByRole('link', { name: 'Pricing' }));
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
});
});