redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Button } from '@/components/actions/Button';
|
||||
import { ActionLink } from '@/components/actions/ActionLink';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('action components', () => {
|
||||
it('prevents duplicate interaction while loading and preserves the accessible name', () => {
|
||||
const onClick = vi.fn();
|
||||
render(
|
||||
<Button loading onClick={onClick}>
|
||||
Save reservation
|
||||
</Button>,
|
||||
);
|
||||
const button = screen.getByRole('button', { name: 'Save reservation' });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveAttribute('aria-busy', 'true');
|
||||
fireEvent.click(button);
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requires an accessible label for icon-only use through its public contract', () => {
|
||||
render(<Button icon="close" aria-label="Close panel" />);
|
||||
expect(screen.getByRole('button', { name: 'Close panel' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders unresolved navigation as a disabled non-link', () => {
|
||||
render(
|
||||
<ActionLink href="/en" disabledReason="Destination pending approval">
|
||||
Book a demo
|
||||
</ActionLink>,
|
||||
);
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Book a demo').closest('[aria-disabled="true"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Accordion } from '@/components/controls/Accordion';
|
||||
import { Tabs } from '@/components/controls/Tabs';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('selection and disclosure controls', () => {
|
||||
it('moves tab focus and selection with arrow keys', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Tabs
|
||||
label="Views"
|
||||
items={[
|
||||
{ id: 'one', label: 'One', content: 'Panel one' },
|
||||
{ id: 'two', label: 'Two', content: 'Panel two' },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
const first = screen.getByRole('tab', { name: 'One' });
|
||||
await user.click(first);
|
||||
await user.keyboard('{ArrowRight}');
|
||||
expect(screen.getByRole('tab', { name: 'Two' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('tabpanel', { name: 'Two' })).toHaveTextContent('Panel two');
|
||||
});
|
||||
|
||||
it('uses native disclosure semantics for accordion items', () => {
|
||||
render(<Accordion items={[{ id: 'faq', title: 'Question', content: 'Answer' }]} />);
|
||||
expect(screen.getByText('Question').closest('summary')).toBeInTheDocument();
|
||||
expect(screen.getByText('Answer').closest('details')).not.toHaveAttribute('open');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { canRenderProductionEvidence, Metric } from '@/components/marketing/Evidence';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('evidence governance', () => {
|
||||
it('allows only approved evidence in production mode', () => {
|
||||
expect(canRenderProductionEvidence('approved')).toBe(true);
|
||||
expect(canRenderProductionEvidence('pending-review')).toBe(false);
|
||||
expect(canRenderProductionEvidence('research-only')).toBe(false);
|
||||
expect(canRenderProductionEvidence('prohibited')).toBe(false);
|
||||
});
|
||||
|
||||
it('suppresses research-only metrics in production mode', () => {
|
||||
const { container } = render(
|
||||
<Metric
|
||||
production
|
||||
statusLabel="Research only"
|
||||
content={{ value: '24', label: 'Fixture', status: 'research-only' }}
|
||||
/>,
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders approved evidence with its source reference', () => {
|
||||
render(
|
||||
<Metric
|
||||
production
|
||||
statusLabel="Approved"
|
||||
content={{
|
||||
value: '1',
|
||||
label: 'Approved fixture',
|
||||
status: 'approved',
|
||||
sourceReference: 'SRC-001',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('SRC-001')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ErrorSummary } from '@/components/forms/ErrorSummary';
|
||||
import { FormField } from '@/components/forms/FormField';
|
||||
import { TextInput } from '@/components/forms/TextInput';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('form primitives', () => {
|
||||
it('associates labels, help, required state, and errors', () => {
|
||||
render(
|
||||
<FormField
|
||||
id="work-email"
|
||||
label="Work email"
|
||||
required
|
||||
supportingText="Use work email"
|
||||
error="Enter a valid address"
|
||||
>
|
||||
<TextInput
|
||||
id="work-email"
|
||||
type="email"
|
||||
required
|
||||
invalid
|
||||
describedBy="work-email-help work-email-error"
|
||||
/>
|
||||
</FormField>,
|
||||
);
|
||||
const input = screen.getByLabelText(/Work email/);
|
||||
expect(input).toHaveAttribute('required');
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true');
|
||||
expect(input).toHaveAccessibleDescription('Use work email Enter a valid address');
|
||||
});
|
||||
|
||||
it('links summary errors back to fields', () => {
|
||||
render(
|
||||
<ErrorSummary
|
||||
title="Correct the fields"
|
||||
errors={[{ fieldId: 'work-email', message: 'Enter a valid address' }]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('alert').querySelector('a')).toHaveAttribute('href', '#work-email');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { buildContentSecurityPolicy, createNonce } from '@/lib/security/csp';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('CSP foundation', () => {
|
||||
it('generates unpredictable nonce-shaped values', () => {
|
||||
const first = createNonce();
|
||||
const second = createNonce();
|
||||
expect(first).toMatch(/^[a-f0-9]{32}$/);
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it('allows development evaluation only in development', () => {
|
||||
expect(buildContentSecurityPolicy('abc', true)).toContain("'unsafe-eval'");
|
||||
expect(buildContentSecurityPolicy('abc', false)).not.toContain("'unsafe-eval'");
|
||||
expect(buildContentSecurityPolicy('abc', false)).toContain("'nonce-abc'");
|
||||
expect(buildContentSecurityPolicy('abc', false)).toContain("style-src 'self' 'nonce-abc'");
|
||||
expect(buildContentSecurityPolicy('abc', false)).not.toContain("'unsafe-inline'");
|
||||
expect(buildContentSecurityPolicy('abc', false)).toContain("frame-ancestors 'none'");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatDateRange,
|
||||
formatNumber,
|
||||
formatPercent,
|
||||
formatPhoneNumber,
|
||||
formatRelativeTime,
|
||||
preserveIdentifier,
|
||||
} from '@/lib/localization/format';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const instant = new Date('2026-06-25T12:00:00.000Z');
|
||||
|
||||
describe('locale-aware formatting primitives', () => {
|
||||
it('formats Gregorian dates with explicit time zones', () => {
|
||||
expect(formatDate(instant, { locale: 'en', timeZone: 'UTC' })).toContain('2026');
|
||||
expect(formatDate(instant, { locale: 'fr', timeZone: 'UTC' })).toContain('2026');
|
||||
expect(formatDate(instant, { locale: 'ar', timeZone: 'UTC' })).toContain('2026');
|
||||
});
|
||||
|
||||
it('formats ranges, numbers, percentages, and explicit currencies', () => {
|
||||
expect(
|
||||
formatDateRange(instant, new Date('2026-06-27T12:00:00.000Z'), {
|
||||
locale: 'en',
|
||||
timeZone: 'UTC',
|
||||
}),
|
||||
).toContain('2026');
|
||||
expect(formatNumber(1234.5, 'fr')).toMatch(/1.*234/);
|
||||
expect(formatPercent(0.125, 'en')).toContain('12.5');
|
||||
expect(formatCurrency(125, 'en', 'USD')).toContain('$');
|
||||
expect(() => formatCurrency(125, 'en', 'usd')).toThrow(/ISO 4217/);
|
||||
});
|
||||
|
||||
it('formats relative time and preserves atomic mixed-direction values', () => {
|
||||
expect(formatRelativeTime(-1, 'day', 'en')).toBe('yesterday');
|
||||
expect(formatPhoneNumber('+1 (202) 555-0100')).toBe('+12025550100');
|
||||
expect(preserveIdentifier('RES 2026 0042')).toBe('RES 2026 0042');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import arHomepage from '@/content/locales/ar/homepage.json';
|
||||
import arShell from '@/content/locales/ar/shell.json';
|
||||
import enHomepage from '@/content/locales/en/homepage.json';
|
||||
import enShell from '@/content/locales/en/shell.json';
|
||||
import frHomepage from '@/content/locales/fr/homepage.json';
|
||||
import frShell from '@/content/locales/fr/shell.json';
|
||||
import { buildHomepageContent } from '@/content/homepage-model';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('homepage content model', () => {
|
||||
it.each([
|
||||
['en', enHomepage, enShell],
|
||||
['fr', frHomepage, frShell],
|
||||
['ar', arHomepage, arShell],
|
||||
] as const)('builds complete %s route content', (_locale, homepage, shell) => {
|
||||
const content = buildHomepageContent(homepage, shell);
|
||||
|
||||
expect(content.hero.title).toBeTruthy();
|
||||
expect(content.trust.items).toHaveLength(4);
|
||||
expect(content.workflow.steps).toHaveLength(4);
|
||||
expect(content.modules.items).toHaveLength(6);
|
||||
expect(content.faq.items).toHaveLength(6);
|
||||
expect(content.pricing.fleetBands).toHaveLength(4);
|
||||
expect(content.pricing.plans).toHaveLength(3);
|
||||
expect(content.hero.preview.rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps unresolved conversion destinations disabled', () => {
|
||||
const content = buildHomepageContent(enHomepage, enShell);
|
||||
|
||||
expect(content.hero.primary.disabledReason).toBe(enShell.header.demoUnavailable);
|
||||
expect(content.pricing.actionDisabledReason).toBe(enShell.states.pendingBody);
|
||||
expect(content.final.secondary.disabledReason).toBe(enShell.states.pendingBody);
|
||||
});
|
||||
|
||||
it('keeps preview identifiers isolated from translated prose', () => {
|
||||
const content = buildHomepageContent(arHomepage, arShell);
|
||||
|
||||
expect(content.hero.preview.rows.map((row) => row.secondary)).toEqual([
|
||||
arHomepage.preview.plate1,
|
||||
arHomepage.preview.plate2,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { analyticsEventRegistry, validateAnalyticsEvent } from '@/lib/analytics/events';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const context = {
|
||||
routeId: 'home',
|
||||
locale: 'en',
|
||||
direction: 'ltr',
|
||||
resolvedTheme: 'light',
|
||||
source: 'hero',
|
||||
} as const;
|
||||
|
||||
describe('analytics contract', () => {
|
||||
it('accepts allowlisted non-personal demo events', () => {
|
||||
expect(validateAnalyticsEvent('demo_open', context)).toEqual({ name: 'demo_open', context });
|
||||
});
|
||||
|
||||
it('rejects unknown events and payload properties', () => {
|
||||
expect(validateAnalyticsEvent('lead_created', context)).toBeNull();
|
||||
expect(
|
||||
validateAnalyticsEvent('demo_open', { ...context, email: 'person@example.test' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('marks every registry entry as non-personal and consent-gated', () => {
|
||||
expect(analyticsEventRegistry.every((event) => !event.personalData)).toBe(true);
|
||||
expect(analyticsEventRegistry.every((event) => event.consentCategory === 'analytics')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isApplicationJson } from '@/lib/security/content-type';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('demo API content type validation', () => {
|
||||
it.each([
|
||||
'application/json',
|
||||
'application/json; charset=utf-8',
|
||||
' Application/JSON ; charset=UTF-8',
|
||||
])('accepts %s', (value) => {
|
||||
expect(isApplicationJson(value)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([null, '', 'text/plain', 'application/jsonp', 'application/json-patch+json'])(
|
||||
'rejects %s',
|
||||
(value) => {
|
||||
expect(isApplicationJson(value)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { demoLeadSchema, demoSubmissionEnvelopeSchema, fleetSizeValues } from '@/lib/demo/schema';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const validLead = {
|
||||
fullName: ' Ada Example ',
|
||||
workEmail: ' ADA@EXAMPLE.TEST ',
|
||||
company: ' Rental Test Labs ',
|
||||
fleetSize: '25-49',
|
||||
market: '',
|
||||
preferredLanguage: 'en',
|
||||
message: ' Reservation workflow ',
|
||||
idempotencyKey: '154249a8-0c3c-4ad8-bda6-91b1ffb361a8',
|
||||
source: 'hero',
|
||||
} as const;
|
||||
|
||||
describe('demo lead schema', () => {
|
||||
it('normalizes approved fields without adding sales-enrichment data', () => {
|
||||
const result = demoLeadSchema.parse(validLead);
|
||||
expect(result).toEqual({
|
||||
...validLead,
|
||||
fullName: 'Ada Example',
|
||||
workEmail: 'ada@example.test',
|
||||
company: 'Rental Test Labs',
|
||||
market: undefined,
|
||||
message: 'Reservation workflow',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown fields and unsupported fleet sizes', () => {
|
||||
expect(() => demoLeadSchema.parse({ ...validLead, phone: '+1 555 0100' })).toThrow();
|
||||
expect(() => demoLeadSchema.parse({ ...validLead, fleetSize: '5000+' })).toThrow();
|
||||
});
|
||||
|
||||
it('keeps the Phase 9 fleet ranges stable', () => {
|
||||
expect(fleetSizeValues).toEqual(['1-9', '10-24', '25-49', '50-99', '100-249', '250+']);
|
||||
});
|
||||
|
||||
it('rejects transport fields outside the approved envelope', () => {
|
||||
expect(() =>
|
||||
demoSubmissionEnvelopeSchema.parse({
|
||||
lead: validLead,
|
||||
guard: { honeypot: '', startedAtMs: 1, fingerprint: 'nope' },
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { destinationRegistry, resolveDestination } from '@/lib/integrations/destinations';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('destination registry', () => {
|
||||
it('activates only the safe local demo destination', () => {
|
||||
const registry = destinationRegistry({ demoSubmissionEnabled: true, demoMode: 'local' });
|
||||
expect(registry['book-demo'].status).toBe('active');
|
||||
expect(registry['contact-sales'].status).toBe('blocked');
|
||||
expect(registry['sign-in'].status).toBe('active');
|
||||
expect(registry.pricing.status).toBe('placeholder');
|
||||
});
|
||||
|
||||
it('fails the demo destination closed when integration is disabled', () => {
|
||||
const registry = destinationRegistry({ demoSubmissionEnabled: false, demoMode: 'blocked' });
|
||||
expect(registry['book-demo'].status).toBe('blocked');
|
||||
});
|
||||
|
||||
it('resolves approved localized placeholder routes without guessing external URLs', () => {
|
||||
expect(
|
||||
resolveDestination('privacy', 'fr', { demoSubmissionEnabled: false, demoMode: 'blocked' })
|
||||
.href,
|
||||
).toBe('/fr/confidentialite');
|
||||
expect(
|
||||
resolveDestination('sign-in', 'en', { demoSubmissionEnabled: false, demoMode: 'blocked' }).href,
|
||||
).toBe('/en/sign-in');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { containsSensitiveKey, redactSensitive } from '@/lib/demo/redaction';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('privacy-safe diagnostics', () => {
|
||||
it('redacts nested personal fields', () => {
|
||||
expect(
|
||||
redactSensitive({
|
||||
category: 'validation',
|
||||
lead: { workEmail: 'person@example.test', company: 'Example Rentals' },
|
||||
}),
|
||||
).toEqual({
|
||||
category: 'validation',
|
||||
lead: { workEmail: '[REDACTED]', company: '[REDACTED]' },
|
||||
});
|
||||
});
|
||||
|
||||
it('detects sensitive diagnostic structures', () => {
|
||||
expect(containsSensitiveKey({ context: { message: 'private' } })).toBe(true);
|
||||
expect(containsSensitiveKey({ category: 'timeout', correlationId: 'abc' })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
getDirection,
|
||||
hreflangMap,
|
||||
isLocale,
|
||||
localeFromAcceptLanguage,
|
||||
localeSwitchUrl,
|
||||
localizedPath,
|
||||
routeById,
|
||||
routeIdFromPathname,
|
||||
routeIdFromSlug,
|
||||
} from '@/lib/localization/config';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('localization foundations', () => {
|
||||
it('selects a supported locale by quality', () => {
|
||||
expect(localeFromAcceptLanguage('de-DE;q=0.9, ar;q=0.8, fr;q=0.7')).toBe('ar');
|
||||
expect(localeFromAcceptLanguage('fr-CA, en;q=0.8')).toBe('fr');
|
||||
expect(localeFromAcceptLanguage(null)).toBe('en');
|
||||
});
|
||||
|
||||
it('validates locale direction and route lookup', () => {
|
||||
expect(isLocale('fr')).toBe(true);
|
||||
expect(isLocale('de')).toBe(false);
|
||||
expect(getDirection('ar')).toBe('rtl');
|
||||
expect(routeById('home').id).toBe('home');
|
||||
});
|
||||
|
||||
it('generates and resolves localized stable route paths', () => {
|
||||
expect(localizedPath('home', 'ar')).toBe('/ar');
|
||||
expect(localizedPath('privacy', 'fr')).toBe('/fr/confidentialite');
|
||||
expect(localizedPath('sign-in', 'ar')).toBe('/ar/تسجيل-الدخول');
|
||||
expect(routeIdFromSlug('fr', 'confidentialite')).toBe('privacy');
|
||||
expect(routeIdFromPathname('/ar/%D8%A7%D9%84%D8%AE%D8%B5%D9%88%D8%B5%D9%8A%D8%A9')).toBe(
|
||||
'privacy',
|
||||
);
|
||||
expect(routeIdFromPathname('/de')).toBeNull();
|
||||
});
|
||||
|
||||
it('generates reciprocal hreflang values', () => {
|
||||
const links = hreflangMap(new URL('https://approved.test'), 'home');
|
||||
expect(links.en).toBe('https://approved.test/en');
|
||||
expect(links.ar).toBe('https://approved.test/ar');
|
||||
expect(links['x-default']).toBe('https://approved.test/en');
|
||||
});
|
||||
|
||||
it('preserves only approved campaign query values and valid hashes', () => {
|
||||
const current = new URL(
|
||||
'https://approved.test/en?utm_source=campaign&email=private%40approved.test#gibberish',
|
||||
);
|
||||
const switched = localeSwitchUrl(current, 'fr', 'home');
|
||||
expect(switched.pathname).toBe('/fr');
|
||||
expect(switched.search).toBe('?utm_source=campaign');
|
||||
expect(switched.hash).toBe('');
|
||||
|
||||
current.hash = '#workflow';
|
||||
expect(localeSwitchUrl(current, 'ar', 'home').hash).toBe('#workflow');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { PricingSection } from '@/components/homepage/sections/PricingSection';
|
||||
import enHomepage from '@/content/locales/en/homepage.json';
|
||||
import enShell from '@/content/locales/en/shell.json';
|
||||
import { buildHomepageContent } from '@/content/homepage-model';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('PricingSection', () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.dataset.theme = 'light';
|
||||
});
|
||||
|
||||
it('changes the recommended plan when the fleet band changes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const content = buildHomepageContent(enHomepage, enShell).pricing;
|
||||
const { container } = render(
|
||||
<PricingSection content={content} locale="en" homePath="/en" demoSubmissionEnabled />,
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-plan="launch"]')).toHaveAttribute(
|
||||
'data-recommended',
|
||||
'true',
|
||||
);
|
||||
|
||||
await user.click(screen.getByLabelText('26–75 vehicles'));
|
||||
|
||||
expect(container.querySelector('[data-plan="growth"]')).toHaveAttribute(
|
||||
'data-recommended',
|
||||
'true',
|
||||
);
|
||||
expect(container.querySelector('[data-plan="launch"]')).not.toHaveAttribute('data-recommended');
|
||||
});
|
||||
|
||||
it('keeps unapproved public prices out of the rendered section', () => {
|
||||
const content = buildHomepageContent(enHomepage, enShell).pricing;
|
||||
render(<PricingSection content={content} locale="en" homePath="/en" demoSubmissionEnabled />);
|
||||
|
||||
expect(screen.getAllByText('Starting price pending approval')).toHaveLength(2);
|
||||
expect(screen.getByText('Custom pricing')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { buildHomepageContent } from '@/content/homepage-model';
|
||||
import enHomepage from '@/content/locales/en/homepage.json';
|
||||
import enShell from '@/content/locales/en/shell.json';
|
||||
import { pricingCommercialConfig } from '@/content/pricing-config';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('homepage pricing configuration', () => {
|
||||
it('keeps commercial values in one guarded configuration', () => {
|
||||
expect(pricingCommercialConfig.publicPricesApproved).toBe(false);
|
||||
expect(pricingCommercialConfig.currency).toBe('USD');
|
||||
expect(pricingCommercialConfig.annualDiscountPercent).toBeNull();
|
||||
});
|
||||
|
||||
it('maps every fleet band and plan from localized content', () => {
|
||||
const pricing = buildHomepageContent(enHomepage, enShell).pricing;
|
||||
|
||||
expect(pricing.fleetBands.map((band) => band.id)).toEqual([
|
||||
'small',
|
||||
'growing',
|
||||
'scale',
|
||||
'enterprise',
|
||||
]);
|
||||
expect(pricing.plans.map((plan) => plan.id)).toEqual(['launch', 'growth', 'enterprise']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isThemePreference, resolveTheme, serverResolvedTheme } from '@/lib/theme/config';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('theme foundations', () => {
|
||||
it('accepts only approved preferences', () => {
|
||||
expect(isThemePreference('light')).toBe(true);
|
||||
expect(isThemePreference('dark')).toBe(true);
|
||||
expect(isThemePreference('system')).toBe(true);
|
||||
expect(isThemePreference('sepia')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps preference and resolved theme separate', () => {
|
||||
expect(resolveTheme('system', true)).toBe('dark');
|
||||
expect(resolveTheme('system', false)).toBe('light');
|
||||
expect(resolveTheme('light', true)).toBe('light');
|
||||
expect(serverResolvedTheme('system')).toBe('light');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user