moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
import { describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach } from 'vitest';
import BrandPersonasCard from '../app/_components/BrandPersonasCard';
import { personas, type PersonaSlug } from '../app/_data/personas';
afterEach(() => {
cleanup();
});
describe('BrandPersonasCard — landing variant', () => {
it('renders 13 persona tiles + 3 filler tiles by default', () => {
render(<BrandPersonasCard />);
const tiles = screen.getAllByTestId(/^persona-tile-/);
expect(tiles).toHaveLength(13);
const fillers = screen.getAllByTestId('brand-personas-filler');
expect(fillers).toHaveLength(3);
// Fillers must be marked aria-hidden to stay out of AT navigation.
for (const filler of fillers) {
expect(filler).toHaveAttribute('aria-hidden', 'true');
}
});
it('renders correct title + role copy for all 13 personas (verbatim from locked decision)', () => {
render(<BrandPersonasCard />);
for (const persona of personas) {
const tile = screen.getByTestId(`persona-tile-${persona.slug}`);
const scope = within(tile);
expect(scope.getByText(persona.title)).toBeInTheDocument();
expect(scope.getByText(persona.role)).toBeInTheDocument();
}
});
it('fires onPersonaClick with the correct slug when a tile is clicked', () => {
const handler = vi.fn<(slug: PersonaSlug) => void>();
render(<BrandPersonasCard onPersonaClick={handler} />);
const hunterTile = screen.getByTestId('persona-tile-hunter');
const button = within(hunterTile).getByRole('button', {
name: /Waggle The Hunter bee mascot/i,
});
fireEvent.click(button);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith('hunter');
});
it('fires onTileHover with the correct slug on mouseenter', () => {
const handler = vi.fn<(slug: PersonaSlug) => void>();
render(<BrandPersonasCard onTileHover={handler} />);
const connectorTile = screen.getByTestId('persona-tile-connector');
fireEvent.mouseEnter(connectorTile);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith('connector');
});
it('renders the cta slot below the grid', () => {
render(
<BrandPersonasCard
cta={<a href="https://example.test/cta">Meet the hive</a>}
/>,
);
const ctaSlot = screen.getByTestId('brand-personas-cta');
expect(ctaSlot).toBeInTheDocument();
expect(
within(ctaSlot).getByRole('link', { name: /Meet the hive/i }),
).toHaveAttribute('href', 'https://example.test/cta');
});
it('hides filler tiles when showFillerTiles={false}', () => {
render(<BrandPersonasCard showFillerTiles={false} />);
expect(screen.queryAllByTestId('brand-personas-filler')).toHaveLength(0);
// Persona tiles remain intact — feature is filler-scoped.
expect(screen.getAllByTestId(/^persona-tile-/)).toHaveLength(13);
});
it('flips to a placeholder when a persona asset fails to load', () => {
render(<BrandPersonasCard />);
const writerTile = screen.getByTestId('persona-tile-writer');
const img = within(writerTile).getByRole('img', {
name: /Waggle The Writer bee mascot/i,
});
// Simulate the 404/onerror path that fires when an asset is missing
// (e.g., mid-regen Task #24 state before the new PNG lands).
fireEvent.error(img);
expect(writerTile).toHaveAttribute('data-placeholder', 'true');
expect(
screen.getByTestId('persona-placeholder-writer'),
).toBeInTheDocument();
// Role copy must stay readable even while the image degrades.
expect(
within(writerTile).getByText(
'Shapes the story the memory wants to tell.',
),
).toBeInTheDocument();
});
it('supports keyboard activation (Enter + Space) when onPersonaClick is provided', () => {
const handler = vi.fn<(slug: PersonaSlug) => void>();
render(<BrandPersonasCard onPersonaClick={handler} />);
const teamTile = screen.getByTestId('persona-tile-team');
const button = within(teamTile).getByRole('button');
fireEvent.keyDown(button, { key: 'Enter' });
fireEvent.keyDown(button, { key: ' ' });
// A non-activation key must not fire the handler.
fireEvent.keyDown(button, { key: 'Tab' });
expect(handler).toHaveBeenCalledTimes(2);
expect(handler).toHaveBeenNthCalledWith(1, 'team');
expect(handler).toHaveBeenNthCalledWith(2, 'team');
});
it('honours custom heading + subtitle overrides', () => {
render(
<BrandPersonasCard
heading="Internal reference"
subtitle="Brand canon for the team."
/>,
);
expect(
screen.getByRole('heading', { name: 'Internal reference' }),
).toBeInTheDocument();
expect(screen.getByText('Brand canon for the team.')).toBeInTheDocument();
});
});
describe('BrandPersonasCard — compact variant', () => {
it('renders the compact stub without errors and exposes the data-variant hook', () => {
render(<BrandPersonasCard variant="compact" />);
const stub = screen.getByTestId('brand-personas-card-compact');
expect(stub).toBeInTheDocument();
expect(stub).toHaveAttribute('data-variant', 'compact');
// Landing DOM must not leak into compact path.
expect(
screen.queryByTestId('brand-personas-card'),
).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,51 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import Pricing from '../app/_components/Pricing';
afterEach(() => {
cleanup();
window.history.pushState({}, '', '/');
});
describe('Pricing', () => {
it('uses the canonical GET checkout URL for Team checkout', () => {
render(<Pricing />);
const monthlyCta = screen.getByRole('link', {
name: 'landing.pricing.tiers.teams.cta',
});
expect(monthlyCta).toHaveAttribute(
'href',
'/api/stripe/checkout?tier=teams&billing=monthly',
);
fireEvent.click(
screen.getByRole('button', { name: /landing\.pricing\.toggle\.annual/ }),
);
const annualCta = screen.getByRole('link', {
name: 'landing.pricing.tiers.teams.cta',
});
expect(annualCta).toHaveAttribute(
'href',
'/api/stripe/checkout?tier=teams&billing=annual',
);
});
it('shows a retry path after a cancelled checkout', async () => {
window.history.pushState({}, '', '/?checkout=cancelled#pricing');
render(<Pricing />);
const notice = await screen.findByRole('status');
expect(notice).toHaveTextContent('landing.pricing.notices.cancelled');
expect(
screen.getByRole('link', {
name: 'landing.pricing.notices.retry',
}),
).toHaveAttribute(
'href',
'/api/stripe/checkout?tier=teams&billing=monthly',
);
});
});

View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const workflow = () =>
readFileSync(
join(process.cwd(), '..', '..', '.github', 'workflows', 'deploy-www.yml'),
'utf8',
);
describe('public-site deployment workflow', () => {
it('deploys the dynamic Next app with Vercel instead of GitHub Pages static artifacts', () => {
const source = workflow();
expect(source).toContain('vercel pull');
expect(source).toContain('vercel build');
expect(source).toContain('vercel deploy --prebuilt --prod');
expect(source).toContain('VERCEL_TOKEN');
expect(source).toContain('VERCEL_ORG_ID');
expect(source).toContain('VERCEL_PROJECT_ID');
expect(source).not.toContain('upload-pages-artifact');
expect(source).not.toContain('deploy-pages');
expect(source).not.toContain('apps/www/dist');
});
});

View File

@@ -0,0 +1,58 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render, screen } from '@testing-library/react';
import DownloadCTA from '../app/_components/DownloadCTA';
import DownloadPage from '../app/download/page';
import { detectOSFromUserAgent } from '../app/_lib/os-detection';
afterEach(() => {
cleanup();
});
describe('download path', () => {
it('routes public download CTAs to the controlled download page', () => {
render(<DownloadCTA section="hero" />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/download');
expect(screen.getByRole('link')).not.toHaveAttribute('target');
});
it('does not send visitors directly to an empty GitHub Releases page', () => {
render(<DownloadPage />);
expect(
screen.getByRole('heading', { name: 'Download Waggle' }),
).toBeInTheDocument();
expect(
screen.getByText('Windows and macOS installers are being prepared for the signed public release.'),
).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'View source on GitHub' }),
).toHaveAttribute('href', 'https://github.com/marolinik/waggle-os');
expect(
screen.queryByRole('link', { name: /releases/i }),
).not.toBeInTheDocument();
});
it('does not label mobile visitors as desktop operating systems', () => {
expect(
detectOSFromUserAgent(
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148',
),
).toBeNull();
expect(
detectOSFromUserAgent(
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/126.0.0.0 Mobile Safari/537.36',
),
).toBeNull();
expect(
detectOSFromUserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 Safari/605.1.15',
),
).toBe('macOS');
expect(
detectOSFromUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36',
),
).toBe('Windows');
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from 'vitest';
import type { ReactElement, ReactNode } from 'react';
vi.mock('next/font/google', () => ({
Hanken_Grotesk: () => ({ variable: '__font_hanken' }),
JetBrains_Mono: () => ({ variable: '__font_mono' }),
}));
vi.mock('next-intl/server', () => ({
getLocale: vi.fn(async () => 'en'),
getMessages: vi.fn(async () => ({})),
}));
vi.mock('@clerk/nextjs', () => ({
ClerkProvider: ({ children }: { children: ReactNode }) => children,
}));
vi.mock('@clerk/themes', () => ({
dark: {},
}));
import RootLayout from '../app/layout';
describe('RootLayout', () => {
it('keeps the progressive-enhancement js class as an intentional hydration mismatch', async () => {
const tree = (await RootLayout({
children: <main>content</main>,
})) as ReactElement<{
className: string;
suppressHydrationWarning?: boolean;
children: ReactNode;
}>;
expect(tree.type).toBe('html');
expect(tree.props.className).toBe('scroll-smooth __font_hanken __font_mono');
expect(tree.props.className).not.toContain(' js');
expect(tree.props.suppressHydrationWarning).toBe(true);
});
});

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const legalFiles = [
'terms/page.tsx',
'privacy/page.tsx',
'cookies/page.tsx',
'eu-ai-act/page.tsx',
] as const;
const launchBlockingCopy = [
/Day-0 placeholder text/i,
/\[Day-0 launch date\]/i,
/to be filled before public launch/i,
/Pro or Teams/i,
/\[to be designated/i,
] as const;
describe('legal pages', () => {
it('do not expose launch placeholders or retired tier copy', () => {
for (const file of legalFiles) {
const source = readFileSync(
join(process.cwd(), 'app', '(legal)', file),
'utf8',
);
for (const pattern of launchBlockingCopy) {
expect(source, `${file} should not match ${pattern}`).not.toMatch(
pattern,
);
}
}
});
});

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { config } from '../middleware';
describe('www Clerk middleware boundary', () => {
it('protects only identity and server-owned flows', () => {
expect(config.matcher).toEqual([
'/account(.*)',
'/sign-in(.*)',
'/sign-up(.*)',
'/(api|trpc)(.*)',
]);
});
});

View File

@@ -0,0 +1,45 @@
import '@testing-library/jest-dom';
import { vi } from 'vitest';
import type { ReactNode } from 'react';
/**
* jsdom does not implement `matchMedia`; our component queries
* `prefers-reduced-motion` via plain CSS, but consumer code using matchMedia
* (existing `Pricing` component paths, etc.) still needs a shim during tests.
*/
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
/**
* Mock next-intl for unit tests. The `t()` function returns the namespaced
* key (with ICU `{var}` placeholders interpolated), so tests can assert on
* deterministic strings without needing a real `messages/en.json` round-trip.
*
* BrandPersonasCard tests check persona-data text (from `_data/personas.ts`,
* not i18n) and passed-in prop overrides — never the default heading/
* subtitle from i18n — so this mock is safe.
*/
vi.mock('next-intl', () => ({
useTranslations: (namespace?: string) => {
return (key: string, params?: Record<string, string | number>) => {
const fullKey = namespace ? `${namespace}.${key}` : key;
if (!params) return fullKey;
return Object.entries(params).reduce(
(acc, [k, v]) => acc.replace(`{${k}}`, String(v)),
fullKey,
);
};
},
NextIntlClientProvider: ({ children }: { children: ReactNode }) => children,
}));

View File

@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const clerkMocks = vi.hoisted(() => ({
auth: vi.fn(),
getUser: vi.fn(),
updateUserMetadata: vi.fn(),
}));
const stripeMocks = vi.hoisted(() => ({
checkoutSessionsCreate: vi.fn(),
customersCreate: vi.fn(),
pricesList: vi.fn(),
}));
vi.mock('@clerk/nextjs/server', () => ({
auth: clerkMocks.auth,
clerkClient: vi.fn(async () => ({
users: {
getUser: clerkMocks.getUser,
updateUserMetadata: clerkMocks.updateUserMetadata,
},
})),
}));
vi.mock('stripe', () => ({
default: vi.fn().mockImplementation(() => ({
checkout: {
sessions: {
create: stripeMocks.checkoutSessionsCreate,
},
},
customers: {
create: stripeMocks.customersCreate,
},
prices: {
list: stripeMocks.pricesList,
},
})),
}));
import { GET } from '../app/api/stripe/checkout/route';
describe('/api/stripe/checkout', () => {
beforeEach(() => {
process.env.STRIPE_SECRET_KEY = 'sk_test_checkout';
process.env.STRIPE_PRICE_TEAMS_ANNUAL = 'price_team_annual';
clerkMocks.auth.mockResolvedValue({ userId: 'user_123' });
clerkMocks.getUser.mockResolvedValue({
publicMetadata: { stripeCustomerId: 'cus_existing' },
primaryEmailAddress: { emailAddress: 'team@example.test' },
});
stripeMocks.checkoutSessionsCreate.mockResolvedValue({
url: 'https://checkout.stripe.test/session',
});
});
afterEach(() => {
vi.clearAllMocks();
delete process.env.STRIPE_SECRET_KEY;
delete process.env.STRIPE_PRICE_TEAMS_ANNUAL;
});
it('sends cancelled checkouts back to the homepage pricing section', async () => {
const res = await GET(
new Request(
'https://waggle.example/api/stripe/checkout?tier=teams&billing=annual',
),
);
expect(res.status).toBe(303);
expect(stripeMocks.checkoutSessionsCreate).toHaveBeenCalledWith(
expect.objectContaining({
cancel_url: 'https://waggle.example/?checkout=cancelled#pricing',
}),
);
});
it('sends signed-out users to sign-in and preserves the checkout target', async () => {
clerkMocks.auth.mockResolvedValueOnce({ userId: null });
const res = await GET(
new Request(
'https://waggle.example/api/stripe/checkout?tier=teams&billing=monthly',
),
);
expect(res.status).toBe(303);
expect(res.headers.get('location')).toBe(
'https://waggle.example/sign-in?redirect_url=%2Fapi%2Fstripe%2Fcheckout%3Ftier%3Dteams%26billing%3Dmonthly',
);
expect(stripeMocks.checkoutSessionsCreate).not.toHaveBeenCalled();
});
});