phase 16 implementation

This commit is contained in:
root
2026-06-25 20:08:39 -04:00
parent 5d017f533a
commit c43620a005
248 changed files with 18458 additions and 90 deletions
@@ -0,0 +1,15 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
for (const locale of ['en', 'fr', 'ar'] as const) {
test(`${locale} demo dialog has no detectable WCAG A/AA violations`, async ({ page }) => {
await page.goto(`/${locale}`);
await page.locator('main button[data-demo-source="hero"]').click();
await expect(page.getByRole('dialog')).toBeVisible();
const results = await new AxeBuilder({ page })
.include('dialog')
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
}
+2 -2
View File
@@ -18,11 +18,11 @@ for (const scenario of [
await expect(page.locator('main > section')).toHaveCount(12);
});
test(`${scenario.locale} unresolved actions are non-navigable`, async ({ page }) => {
test(`${scenario.locale} unresolved actions remain 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).toHaveCount(3);
await expect(disabledActions.first()).not.toHaveAttribute('href');
});
}
@@ -0,0 +1,74 @@
import { expect, test } from '@playwright/test';
const localeData = {
en: {
button: /Book a demo|Book a product demo/,
title: 'Book a product demo',
success: 'Demo request recorded',
},
fr: {
button: /Réserver une démonstration/,
title: 'Réserver une démonstration du produit',
success: 'Demande de démonstration enregistrée',
},
ar: {
button: /احجز عرضًا توضيحيًا|احجز عرض/,
title: 'احجز عرضًا توضيحيًا للمنتج',
success: 'تم تسجيل طلب العرض',
},
} as const;
for (const locale of ['en', 'fr', 'ar'] as const) {
test(`${locale} demo flow validates and succeeds through the local adapter`, async ({ page }) => {
const labels = localeData[locale];
await page.goto(`/${locale}`);
await page.locator('main button[data-demo-source="hero"]').click();
const dialog = page.getByRole('dialog', { name: labels.title });
await expect(dialog).toBeVisible();
await dialog
.getByRole('button', { name: /Request my demo|Demander ma démonstration|إرسال طلب العرض/ })
.click();
await expect(dialog.locator('[role="alert"]')).toBeVisible();
await expect(dialog.locator('#fullName')).toBeFocused();
await dialog.locator('#fullName').fill('Avery Example');
await dialog.locator('#workEmail').fill('avery@example.test');
await dialog.locator('#company').fill('Example Rental Lab');
await dialog.locator('#fleetSize').selectOption('10-24');
await dialog
.getByRole('button', { name: /Request my demo|Demander ma démonstration|إرسال طلب العرض/ })
.click();
await expect(dialog.getByRole('heading', { name: labels.success })).toBeVisible();
await expect(page).toHaveURL(new RegExp(`/${locale}$`));
});
}
test('duplicate clicks create only one request while submission is pending', async ({ page }) => {
await page.goto('/en');
await page.locator('main button[data-demo-source="hero"]').click();
const dialog = page.getByRole('dialog', { name: 'Book a product demo' });
await dialog.locator('#fullName').fill('Avery Example');
await dialog.locator('#workEmail').fill('avery@example.test');
await dialog.locator('#company').fill('Example Rental Lab');
await dialog.locator('#fleetSize').selectOption('10-24');
const submit = dialog.getByRole('button', { name: 'Request my demo' });
await submit.dblclick();
await expect(dialog.getByRole('heading', { name: 'Demo request recorded' })).toBeVisible();
});
test('demo form does not place personal data in the URL or analytics sink', async ({ page }) => {
await page.goto('/en');
await page.locator('main button[data-demo-source="hero"]').click();
const dialog = page.getByRole('dialog', { name: 'Book a product demo' });
await dialog.locator('#fullName').fill('Private Example');
await dialog.locator('#workEmail').fill('private@example.test');
await dialog.locator('#company').fill('Private Rentals');
await dialog.locator('#fleetSize').selectOption('1-9');
await dialog.getByRole('button', { name: 'Request my demo' }).click();
await expect(dialog.getByRole('heading', { name: 'Demo request recorded' })).toBeVisible();
expect(page.url()).not.toContain('private');
const analytics = await page.evaluate(() => window.__rdgAnalyticsTestSink ?? []);
expect(JSON.stringify(analytics)).not.toContain('private');
});
+97
View File
@@ -0,0 +1,97 @@
import { clearIdempotencyStoreForTests } from '@/lib/demo/idempotency';
import type { LeadDestinationAdapter } from '@/lib/demo/adapters';
import { submitDemoLead } from '@/lib/demo/service';
import type { DemoLead } from '@/lib/demo/schema';
import type { IntegrationEnvironment } from '@/lib/integrations/environment';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const environment: IntegrationEnvironment = {
NODE_ENV: 'test',
NEXT_PUBLIC_DEMO_ENABLED: true,
DEMO_SUBMISSION_MODE: 'local',
DEMO_REQUEST_TIMEOUT_MS: 250,
DEMO_IDEMPOTENCY_TTL_SECONDS: 900,
DEMO_TEST_SCENARIOS_ENABLED: true,
DEMO_LOCAL_TEST_MODE: false,
NEXT_PUBLIC_ANALYTICS_MODE: 'disabled',
};
const lead: DemoLead = {
fullName: 'Avery Example',
workEmail: 'avery@example.test',
company: 'Example Rental Lab',
fleetSize: '10-24',
preferredLanguage: 'en',
idempotencyKey: '6b4992d5-d88e-4e44-9b66-adc18fd8ee37',
source: 'hero',
};
beforeEach(clearIdempotencyStoreForTests);
describe('demo submission orchestration', () => {
it('confirms acceptance before returning success', async () => {
const adapter: LeadDestinationAdapter = {
name: 'test-success',
submitLead: vi.fn(async () => ({
ok: true as const,
externalId: 'local_test',
acceptedAt: '2026-06-25T00:00:00.000Z',
mode: 'local' as const,
})),
};
const result = await submitDemoLead(lead, { environment, adapter });
expect(result.ok).toBe(true);
expect(adapter.submitLead).toHaveBeenCalledOnce();
});
it('coalesces repeated transport with the same idempotency key', async () => {
const adapter: LeadDestinationAdapter = {
name: 'test-idempotent',
submitLead: vi.fn(async () => ({
ok: true as const,
externalId: 'local_once',
acceptedAt: '2026-06-25T00:00:00.000Z',
mode: 'local' as const,
})),
};
const [first, second] = await Promise.all([
submitDemoLead(lead, { environment, adapter }),
submitDemoLead(lead, { environment, adapter }),
]);
expect(first).toEqual(second);
expect(adapter.submitLead).toHaveBeenCalledOnce();
});
it('normalizes adapter failures without exposing vendor bodies', async () => {
const adapter: LeadDestinationAdapter = {
name: 'test-error',
submitLead: async () => ({
ok: false,
category: 'integration',
retryable: true,
code: 'normalized_failure',
}),
};
const result = await submitDemoLead(lead, { environment, adapter });
expect(result).toMatchObject({
ok: false,
category: 'integration',
formErrorCode: 'normalized_failure',
retryable: true,
});
});
it('bounds a hung integration with a timeout', async () => {
const adapter: LeadDestinationAdapter = {
name: 'test-timeout',
submitLead: (_lead, context) =>
new Promise((_resolve, reject) => {
context.signal.addEventListener('abort', () =>
reject(new DOMException('Aborted', 'AbortError')),
);
}),
};
const result = await submitDemoLead(lead, { environment, adapter });
expect(result).toMatchObject({ ok: false, category: 'timeout', retryable: true });
});
});
+30
View File
@@ -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,22 @@
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.login.status).toBe('blocked');
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('login', 'en', { demoSubmissionEnabled: false, demoMode: 'blocked' }).href,
).toBeUndefined();
});
});
+21
View File
@@ -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);
});
});
+2 -1
View File
@@ -6,8 +6,9 @@ for (const scenario of [
{ locale: 'ar', theme: 'dark', width: 390, height: 1200 },
] as const) {
test(`${scenario.locale}-${scenario.theme} component matrix`, async ({ page }) => {
test.skip(test.info().project.name !== 'chromium', 'Canonical visual baselines use Chromium.');
await page.addInitScript(
(theme) => localStorage.setItem('rdg-theme-preference', theme),
(theme) => localStorage.setItem('hpc.theme.preference', theme),
scenario.theme,
);
await page.setViewportSize({ width: scenario.width, height: scenario.height });
+24
View File
@@ -0,0 +1,24 @@
import { expect, test } from '@playwright/test';
for (const scenario of [
{ locale: 'en', theme: 'light' },
{ locale: 'en', theme: 'dark' },
{ locale: 'fr', theme: 'light' },
{ locale: 'ar', theme: 'light' },
{ locale: 'ar', theme: 'dark' },
] as const) {
test(`${scenario.locale} ${scenario.theme} demo initial and validation states`, async ({
page,
}) => {
test.skip(test.info().project.name !== 'chromium', 'Canonical visual baselines use Chromium.');
await page.emulateMedia({ colorScheme: scenario.theme });
await page.goto(`/${scenario.locale}`);
await page.locator('main button[data-demo-source="hero"]').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toHaveScreenshot(`demo-${scenario.locale}-${scenario.theme}-initial.png`);
await dialog.locator('button[type="submit"]').click();
await expect(dialog).toHaveScreenshot(
`demo-${scenario.locale}-${scenario.theme}-validation.png`,
);
});
}