init project

This commit is contained in:
root
2026-06-25 19:06:59 -04:00
parent c5dfee6efb
commit 5d017f533a
349 changed files with 31330 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
for (const locale of ['en', 'fr', 'ar'] as const) {
test(`${locale} component fixture has no detectable accessibility violations`, async ({
page,
}) => {
await page.goto(`/${locale}/component-lab`);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
}
+24
View File
@@ -0,0 +1,24 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
for (const locale of ['en', 'fr', 'ar'] as const) {
test(`${locale} homepage has no detectable WCAG A/AA violations`, async ({ page }) => {
await page.goto(`/${locale}`);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}
test('homepage keeps a coherent heading outline', async ({ page }) => {
await page.goto('/en');
const levels = await page
.locator('main h1, main h2, main h3')
.evaluateAll((headings) => headings.map((heading) => Number(heading.tagName.slice(1))));
expect(levels[0]).toBe(1);
for (let index = 1; index < levels.length; index += 1) {
expect((levels[index] ?? 1) - (levels[index - 1] ?? 1)).toBeLessThanOrEqual(1);
}
});
+27
View File
@@ -0,0 +1,27 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
for (const locale of ['en', 'fr', 'ar'] as const) {
for (const theme of ['light', 'dark'] as const) {
test(`${locale} ${theme} shell has no detectable WCAG A/AA violations`, async ({
page,
context,
}) => {
await context.addCookies([
{
name: 'hpc-theme',
value: theme,
domain: '127.0.0.1',
path: '/',
secure: false,
sameSite: 'Lax',
},
]);
await page.goto(`/${locale}`);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}
}
+45
View File
@@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test';
for (const locale of ['en', 'fr', 'ar'] as const) {
test(`${locale} component fixture renders without horizontal overflow`, async ({ page }) => {
await page.goto(`/${locale}/component-lab`);
await expect(page.locator('h1')).toBeVisible();
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(overflow).toBe(false);
await expect(page.locator('html')).toHaveAttribute('dir', locale === 'ar' ? 'rtl' : 'ltr');
});
}
test('component controls support keyboard interaction and dialog focus return', async ({
page,
}) => {
await page.goto('/en/component-lab');
const firstTab = page.getByRole('tab', { name: 'Reservations' });
await firstTab.focus();
await page.keyboard.press('ArrowRight');
await expect(page.getByRole('tab', { name: 'Fleet availability' })).toHaveAttribute(
'aria-selected',
'true',
);
const trigger = page.getByRole('button', { name: 'Open dialog' });
await trigger.click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toBeHidden();
await expect(trigger).toBeFocused();
});
test('component fixture reflows at narrow mobile and 200 percent zoom equivalent', async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 800 });
await page.goto('/fr/component-lab');
await page.evaluate(() => (document.body.style.zoom = '2'));
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
);
expect(overflow).toBe(false);
});
+51
View File
@@ -0,0 +1,51 @@
import { expect, test } from '@playwright/test';
for (const scenario of [
{ locale: 'en', dir: 'ltr' },
{ locale: 'fr', dir: 'ltr' },
{ locale: 'ar', dir: 'rtl' },
] as const) {
test(`${scenario.locale} homepage assembles the approved section sequence`, async ({ page }) => {
await page.goto(`/${scenario.locale}`);
await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
await expect(page.locator('main h1')).toHaveCount(1);
await expect(page.locator('#product')).toBeVisible();
await expect(page.locator('#workflow')).toBeVisible();
await expect(page.locator('#modules')).toBeVisible();
await expect(page.locator('#pricing')).toBeVisible();
await expect(page.locator('#faq')).toBeVisible();
await expect(page.locator('main > section')).toHaveCount(12);
});
test(`${scenario.locale} unresolved actions are non-navigable`, async ({ page }) => {
await page.goto(`/${scenario.locale}`);
const disabledActions = page.locator('main [aria-disabled="true"]');
await expect(disabledActions).toHaveCount(5);
await expect(disabledActions.first()).not.toHaveAttribute('href');
});
}
test('Arabic preview preserves left-to-right vehicle identifiers', async ({ page }) => {
await page.goto('/ar');
const identifiers = page.locator('[role="table"] bdi');
await expect(identifiers).toHaveCount(2);
await expect(identifiers.first()).toHaveAttribute('dir', 'ltr');
});
test('homepage navigation targets resolve to real sections', async ({ page }) => {
await page.goto('/en');
for (const hash of ['product', 'workflow', 'modules', 'pricing', 'faq']) {
await expect(page.locator(`#${hash}`)).toHaveCount(1);
await expect(page.locator(`header a[href="/en#${hash}"]`)).toHaveCount(2);
}
});
test('FAQ uses native disclosure behavior', async ({ page }) => {
await page.goto('/en');
const firstItem = page.locator('#faq details').first();
await expect(firstItem).toHaveAttribute('open', '');
await firstItem.locator('summary').click();
await expect(firstItem).not.toHaveAttribute('open', '');
});
+28
View File
@@ -0,0 +1,28 @@
import { expect, test } from '@playwright/test';
test.use({ viewport: { width: 390, height: 844 } });
test('mobile navigation opens modally, closes with Escape, and returns focus', async ({ page }) => {
await page.goto('/en');
const trigger = page.getByRole('button', { name: 'Open navigation menu' });
await trigger.focus();
await trigger.click();
const dialog = page.getByRole('dialog', { name: 'Primary navigation' });
await expect(dialog).toBeVisible();
await expect(page.locator('body')).toHaveAttribute('data-navigation-open', 'true');
await expect(page.getByRole('button', { name: 'Close navigation menu' })).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).not.toBeVisible();
await expect(trigger).toBeFocused();
});
test('skip navigation moves focus to the main landmark', async ({ page }) => {
await page.goto('/en');
await page.keyboard.press('Tab');
const skip = page.getByRole('link', { name: 'Skip to main content' });
await expect(skip).toBeFocused();
await skip.press('Enter');
await expect(page.locator('#main-content')).toBeFocused();
});
+12
View File
@@ -0,0 +1,12 @@
import { expect, test } from '@playwright/test';
test('server shell remains readable with JavaScript disabled', async ({ browser }) => {
const context = await browser.newContext({ javaScriptEnabled: false, colorScheme: 'dark' });
const page = await context.newPage();
await page.goto('/ar');
await expect(page.locator('html')).toHaveAttribute('lang', 'ar');
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
await context.close();
});
+24
View File
@@ -0,0 +1,24 @@
import { expect, test } from '@playwright/test';
const widths = [320, 390, 768, 1024, 1440];
for (const locale of ['en', 'fr', 'ar'] as const) {
for (const width of widths) {
test(`${locale} has no horizontal overflow at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto(`/${locale}`);
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(overflow).toBeLessThanOrEqual(1);
});
}
}
test('shell remains operable at a 320 CSS pixel reflow equivalent to 200 percent zoom', async ({
page,
}) => {
await page.setViewportSize({ width: 640, height: 800 });
await page.goto('/fr');
await expect(page.getByRole('button', { name: 'Ouvrir le menu de navigation' })).toBeVisible();
});
+58
View File
@@ -0,0 +1,58 @@
import { expect, test } from '@playwright/test';
for (const scenario of [
{ locale: 'en', dir: 'ltr', title: /RentalDriveGo/ },
{ locale: 'fr', dir: 'ltr', title: /RentalDriveGo/ },
{ locale: 'ar', dir: 'rtl', title: /RentalDriveGo/ },
] as const) {
test(`${scenario.locale} shell is server rendered`, async ({ page, request }) => {
const raw = await request.get(`/${scenario.locale}`);
const html = await raw.text();
expect(raw.status()).toBe(200);
expect(html).toMatch(
new RegExp(`<html[^>]*lang=\"${scenario.locale}\"[^>]*dir=\"${scenario.dir}\"`),
);
const response = await page.goto(`/${scenario.locale}`);
expect(response?.status()).toBe(200);
await expect(page.locator('html')).toHaveAttribute('lang', scenario.locale);
await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
await expect(page).toHaveTitle(scenario.title);
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('header')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
'href',
new RegExp(`/${scenario.locale}$`),
);
for (const language of ['en', 'fr', 'ar', 'x-default']) {
await expect(page.locator(`link[rel="alternate"][hreflang="${language}"]`)).toHaveCount(1);
}
await expect(page.locator('meta[property="og:locale"]')).toHaveAttribute(
'content',
scenario.locale,
);
});
}
test('root locale detection honors Accept-Language', async ({ browser }) => {
const context = await browser.newContext({
extraHTTPHeaders: { 'Accept-Language': 'ar;q=1,en;q=0.8' },
});
const page = await context.newPage();
await page.goto('/');
await expect(page).toHaveURL(/\/ar$/);
await context.close();
});
test('locale switching keeps the semantic route and approved campaign state', async ({ page }) => {
await page.goto('/en/privacy?utm_source=qa&email=drop-me#ignored');
await page.getByLabel('Language').first().selectOption('fr');
await expect(page).toHaveURL('/fr/confidentialite?utm_source=qa');
});
test('pending route shells are localized and noindex', async ({ page }) => {
await page.goto('/fr/confidentialite');
await expect(page.locator('h1')).toContainText('Confidentialité');
await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', /noindex/);
});
+20
View File
@@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test';
for (const locale of ['fr', 'ar'] as const) {
test(`${locale} shell tolerates long content expansion`, async ({ page }) => {
await page.setViewportSize({ width: 390, height: 900 });
await page.goto(`/${locale}`);
await page
.locator('main article h3')
.first()
.evaluate((element) => {
element.textContent = [element.textContent, element.textContent, element.textContent]
.filter(Boolean)
.join(' ');
});
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(overflow).toBeLessThanOrEqual(1);
});
}
+58
View File
@@ -0,0 +1,58 @@
import { expect, test } from '@playwright/test';
test('explicit dark theme is present on the server-rendered root', async ({ browser, baseURL }) => {
const origin = new URL(baseURL ?? 'http://127.0.0.1:3000');
const context = await browser.newContext();
await context.addCookies([
{
name: 'hpc-theme',
value: 'dark',
domain: origin.hostname,
path: '/',
secure: false,
sameSite: 'Lax',
},
]);
const page = await context.newPage();
const response = await page.goto('/en');
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'dark');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
const csp = response?.headers()['content-security-policy'] ?? '';
const nonce = await page.locator('#theme-bootstrap').getAttribute('nonce');
expect(nonce).toBeTruthy();
expect(csp).toContain(`'nonce-${nonce}'`);
await context.close();
});
test('system preference resolves before interaction', async ({ browser }) => {
const context = await browser.newContext({ colorScheme: 'dark' });
const page = await context.newPage();
await page.goto('/en');
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'system');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await context.close();
});
test('system follows operating-system changes while explicit preferences ignore them', async ({
page,
}) => {
await page.goto('/en');
const selector = page.getByLabel('Theme').first();
await selector.selectOption('system');
await page.emulateMedia({ colorScheme: 'dark' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.emulateMedia({ colorScheme: 'light' });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await selector.selectOption('dark');
await page.emulateMedia({ colorScheme: 'light' });
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'dark');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
expect(await page.evaluate(() => window.localStorage.getItem('hpc.theme.preference'))).toBe(
'dark',
);
expect(await page.context().cookies()).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'hpc-theme', value: 'dark' })]),
);
});
+5
View File
@@ -0,0 +1,5 @@
# Phase 11 foundation fixtures
Phase 10 did not approve a component-workbench dependency. The repository therefore uses the live localized shell plus the matrix in `foundation-matrix.json` as the approved equivalent fixture strategy.
Playwright functional, accessibility, responsive, and visual suites consume the same production routes and components. This avoids a parallel story-only rendering path that could pass while production quietly fails, a trick software is distressingly good at.
+9
View File
@@ -0,0 +1,9 @@
{
"fixtureRoute": "/{locale}/component-lab",
"productionGuard": "COMPONENT_FIXTURES_ENABLED=true",
"locales": ["en", "fr", "ar"],
"themes": ["light", "dark", "system"],
"widths": [320, 390, 768, 1024, 1440],
"states": ["default", "focus-visible", "disabled", "loading", "error", "long-content", "rtl"],
"contentPolicy": "Fictional deterministic development data only; no production claims, customers, personal data, or live operations."
}
+22
View File
@@ -0,0 +1,22 @@
{
"locales": ["en", "fr", "ar"],
"directions": ["ltr", "rtl"],
"themes": ["light", "dark", "system"],
"viewports": [
{ "name": "mobile", "width": 390, "height": 844 },
{ "name": "tablet", "width": 1024, "height": 900 },
{ "name": "desktop", "width": 1440, "height": 1000 }
],
"shell_states": [
"default",
"mobile-navigation-open",
"keyboard-focus",
"loading",
"error",
"not-found",
"pending-destination",
"long-text",
"reduced-motion"
],
"coverage_source": ["tests/browser", "tests/a11y", "tests/visual"]
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
@@ -0,0 +1,16 @@
import { Button } from '@/components/actions/Button';
import type { MetricContent } from '@/components/marketing/Evidence';
void Button;
const approvedMetric: MetricContent = {
value: '1',
label: 'Fixture',
status: 'approved',
sourceReference: 'SRC-001',
};
void approvedMetric;
// @ts-expect-error icon-only buttons require an accessible label.
const invalidIconButton = <Button icon="close" />;
void invalidIconButton;
+35
View File
@@ -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();
});
});
+31
View File
@@ -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');
});
});
+39
View File
@@ -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();
});
});
+41
View File
@@ -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');
});
});
+20
View File
@@ -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'");
});
});
+40
View File
@@ -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');
});
});
+42
View File
@@ -0,0 +1,42 @@
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.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.action.disabledReason).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,
]);
});
});
+58
View File
@@ -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('login', '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');
});
});
+18
View File
@@ -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');
});
});
+10
View File
@@ -0,0 +1,10 @@
# Visual baselines
The Playwright scenarios define the Phase 11 locale, theme, viewport, drawer, and focus-state matrix.
Canonical PNG snapshots must be generated with the pinned Chromium build in an unblocked environment:
```bash
pnpm test:visual:update --project=chromium
```
No fabricated or manually composed screenshots are stored here. The local execution environment blocked browser installation and loopback browser navigation, so baseline approval remains an explicit release gate in the Phase 11 issue register.
@@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test';
for (const scenario of [
{ locale: 'en', theme: 'light', width: 1440, height: 1200 },
{ locale: 'fr', theme: 'dark', width: 1024, height: 1200 },
{ locale: 'ar', theme: 'dark', width: 390, height: 1200 },
] as const) {
test(`${scenario.locale}-${scenario.theme} component matrix`, async ({ page }) => {
await page.addInitScript(
(theme) => localStorage.setItem('rdg-theme-preference', theme),
scenario.theme,
);
await page.setViewportSize({ width: scenario.width, height: scenario.height });
await page.goto(`/${scenario.locale}/component-lab`);
await expect(page).toHaveScreenshot(
`component-system-${scenario.locale}-${scenario.theme}-${scenario.width}.png`,
{ fullPage: true },
);
});
}
+30
View File
@@ -0,0 +1,30 @@
import { expect, test } from '@playwright/test';
const scenarios = [
{ name: 'homepage-en-light-desktop', locale: 'en', theme: 'light', width: 1440, height: 1000 },
{ name: 'homepage-en-dark-mobile', locale: 'en', theme: 'dark', width: 390, height: 844 },
{ name: 'homepage-fr-light-tablet', locale: 'fr', theme: 'light', width: 1024, height: 900 },
{ name: 'homepage-fr-dark-mobile', locale: 'fr', theme: 'dark', width: 390, height: 844 },
{ name: 'homepage-ar-light-desktop', locale: 'ar', theme: 'light', width: 1440, height: 1000 },
{ name: 'homepage-ar-dark-mobile', locale: 'ar', theme: 'dark', width: 390, height: 844 },
{ name: 'homepage-en-light-320', locale: 'en', theme: 'light', width: 320, height: 800 },
] as const;
for (const scenario of scenarios) {
test(`${scenario.name} baseline`, async ({ page, context }) => {
test.skip(test.info().project.name !== 'chromium');
await page.setViewportSize({ width: scenario.width, height: scenario.height });
await context.addCookies([
{
name: 'hpc-theme',
value: scenario.theme,
domain: '127.0.0.1',
path: '/',
secure: false,
sameSite: 'Lax',
},
]);
await page.goto(`/${scenario.locale}`);
await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
});
}
+63
View File
@@ -0,0 +1,63 @@
import { expect, test } from '@playwright/test';
const scenarios = [
{
name: 'desktop-en-light',
locale: 'en',
theme: 'light',
viewport: { width: 1440, height: 1000 },
},
{ name: 'desktop-en-dark', locale: 'en', theme: 'dark', viewport: { width: 1440, height: 1000 } },
{ name: 'tablet-fr-light', locale: 'fr', theme: 'light', viewport: { width: 1024, height: 900 } },
{ name: 'tablet-fr-dark', locale: 'fr', theme: 'dark', viewport: { width: 1024, height: 900 } },
{ name: 'mobile-ar-light', locale: 'ar', theme: 'light', viewport: { width: 390, height: 844 } },
{ name: 'mobile-ar-dark', locale: 'ar', theme: 'dark', viewport: { width: 390, height: 844 } },
] as const;
for (const scenario of scenarios) {
test(`${scenario.name} visual baseline`, async ({ page, context }) => {
test.skip(
test.info().project.name !== 'chromium',
'Canonical visual baselines use Chromium; other projects run functional coverage.',
);
await page.setViewportSize(scenario.viewport);
await context.addCookies([
{
name: 'hpc-theme',
value: scenario.theme,
domain: '127.0.0.1',
path: '/',
secure: false,
sameSite: 'Lax',
},
]);
await page.goto(`/${scenario.locale}`);
await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
});
}
test('mobile navigation open baseline', async ({ page }) => {
test.skip(test.info().project.name !== 'chromium');
await page.setViewportSize({ width: 390, height: 844 });
await page.context().addCookies([
{
name: 'hpc-theme',
value: 'dark',
domain: '127.0.0.1',
path: '/',
secure: false,
sameSite: 'Lax',
},
]);
await page.goto('/ar');
await page.getByRole('button', { name: 'فتح قائمة التنقل' }).click();
await expect(page).toHaveScreenshot('mobile-ar-dark-navigation-open.png', { fullPage: true });
});
test('keyboard focus baseline', async ({ page }) => {
test.skip(test.info().project.name !== 'chromium');
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/en');
await page.keyboard.press('Tab');
await expect(page).toHaveScreenshot('desktop-en-light-focus.png', { fullPage: true });
});