- Theme toggle in the nav: follows the device until chosen, remembers the choice, sets data-theme before first paint (no flash). - Site search: Cmd/Ctrl+K command palette on every marketing page, opened by the shortcut, a nav Search button, or the Docs search field. Searches pages, docs topics and articles that have a page. - Docs topics move to content.ts so the page and the search index share one source. - next.config: allowedDevOrigins for private network ranges so a phone on the same Wi-Fi hydrates the dev site. Dev only. - Sun and moon icons added by hand (not Figma exports); noted in the icons README. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
import { render, screen, within } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { Nav } from '../../src/marketing/Nav.tsx';
|
|
|
|
function deviceTheme(dark: boolean) {
|
|
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
|
matches: dark && query === '(prefers-color-scheme: dark)',
|
|
media: query,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
|
|
describe('Nav', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
document.documentElement.removeAttribute('data-theme');
|
|
deviceTheme(false);
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
describe('theme toggle', () => {
|
|
it('is off on a light device', () => {
|
|
render(<Nav />);
|
|
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'false');
|
|
});
|
|
|
|
it('is on when the device is dark and nothing is saved', () => {
|
|
deviceTheme(true);
|
|
render(<Nav />);
|
|
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
|
|
});
|
|
|
|
it('is on when dark was chosen earlier, even on a light device', () => {
|
|
localStorage.setItem('wursor-theme', 'dark');
|
|
render(<Nav />);
|
|
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
|
|
});
|
|
|
|
it('switches the page to dark and remembers it', async () => {
|
|
render(<Nav />);
|
|
await userEvent.click(screen.getByRole('button', { name: 'Dark theme' }));
|
|
expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
|
|
expect(localStorage.getItem('wursor-theme')).toBe('dark');
|
|
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
|
|
});
|
|
|
|
it('switches a dark device to light', async () => {
|
|
deviceTheme(true);
|
|
render(<Nav />);
|
|
await userEvent.click(screen.getByRole('button', { name: 'Dark theme' }));
|
|
expect(document.documentElement).toHaveAttribute('data-theme', 'light');
|
|
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'false');
|
|
});
|
|
});
|
|
});
|