Update homepage routes to include language and mode
Build & Deploy / Build & Push Docker Image (push) Successful in 2m51s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 49s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Storefront Unit Tests (push) Successful in 39s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 40s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-02 14:10:17 -04:00
parent 56c3bcf666
commit 781512399b
51 changed files with 792 additions and 344 deletions
@@ -5,7 +5,7 @@ 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`);
await page.goto(`/${locale}/system/component-lab`);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
@@ -3,7 +3,7 @@ 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}`);
await page.goto(`/${locale}/system`);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
@@ -12,7 +12,7 @@ for (const locale of ['en', 'fr', 'ar'] as const) {
}
test('homepage keeps a coherent heading outline', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
const levels = await page
.locator('main h1, main h2, main h3')
.evaluateAll((headings) => headings.map((heading) => Number(heading.tagName.slice(1))));
@@ -3,7 +3,7 @@ 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.goto(`/${locale}/system`);
await page.locator('main button[data-demo-source="hero"]').click();
await expect(page.getByRole('dialog')).toBeVisible();
const results = await new AxeBuilder({ page })
+1 -1
View File
@@ -17,7 +17,7 @@ for (const locale of ['en', 'fr', 'ar'] as const) {
sameSite: 'Lax',
},
]);
await page.goto(`/${locale}`);
await page.goto(`/${locale}/${theme}`);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze();
@@ -2,7 +2,7 @@ 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 page.goto(`/${locale}/system/component-lab`);
await expect(page.locator('h1')).toBeVisible();
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
@@ -15,7 +15,7 @@ for (const locale of ['en', 'fr', 'ar'] as const) {
test('component controls support keyboard interaction and dialog focus return', async ({
page,
}) => {
await page.goto('/en/component-lab');
await page.goto('/en/system/component-lab');
const firstTab = page.getByRole('tab', { name: 'Reservations' });
await firstTab.focus();
await page.keyboard.press('ArrowRight');
@@ -36,7 +36,7 @@ test('component fixture reflows at narrow mobile and 200 percent zoom equivalent
page,
}) => {
await page.setViewportSize({ width: 320, height: 800 });
await page.goto('/fr/component-lab');
await page.goto('/fr/system/component-lab');
await page.evaluate(() => (document.body.style.zoom = '2'));
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
+7 -7
View File
@@ -6,7 +6,7 @@ for (const scenario of [
{ locale: 'ar', dir: 'rtl' },
] as const) {
test(`${scenario.locale} homepage assembles the approved section sequence`, async ({ page }) => {
await page.goto(`/${scenario.locale}`);
await page.goto(`/${scenario.locale}/system`);
await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
await expect(page.locator('main h1')).toHaveCount(1);
@@ -19,7 +19,7 @@ for (const scenario of [
});
test(`${scenario.locale} unresolved actions remain non-navigable`, async ({ page }) => {
await page.goto(`/${scenario.locale}`);
await page.goto(`/${scenario.locale}/system`);
const disabledActions = page.locator('main [aria-disabled="true"]');
await expect(disabledActions).toHaveCount(2);
@@ -28,22 +28,22 @@ for (const scenario of [
}
test('Arabic preview preserves left-to-right vehicle identifiers', async ({ page }) => {
await page.goto('/ar');
await page.goto('/ar/system');
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');
await page.goto('/en/system');
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);
await expect(page.locator(`header a[href="/en/system#${hash}"]`)).toHaveCount(2);
}
});
test('FAQ uses native disclosure behavior', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
const firstItem = page.locator('#faq details').first();
await expect(firstItem).toHaveAttribute('open', '');
await firstItem.locator('summary').click();
@@ -51,7 +51,7 @@ test('FAQ uses native disclosure behavior', async ({ page }) => {
});
test('pricing recommends a plan from the selected fleet band', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
await expect(page.locator('#pricing [data-plan="launch"]')).toHaveAttribute(
'data-recommended',
@@ -21,7 +21,7 @@ const localeData = {
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.goto(`/${locale}/system`);
await page.locator('main button[data-demo-source="hero"]').click();
const dialog = page.getByRole('dialog', { name: labels.title });
@@ -41,12 +41,12 @@ for (const locale of ['en', 'fr', 'ar'] as const) {
.click();
await expect(dialog.getByRole('heading', { name: labels.success })).toBeVisible();
await expect(page).toHaveURL(new RegExp(`/${locale}$`));
await expect(page).toHaveURL(new RegExp(`/${locale}/system$`));
});
}
test('duplicate clicks create only one request while submission is pending', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
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');
@@ -59,7 +59,7 @@ test('duplicate clicks create only one request while submission is pending', asy
});
test('demo form does not place personal data in the URL or analytics sink', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
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');
@@ -3,7 +3,7 @@ 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');
await page.goto('/en/system');
const trigger = page.getByRole('button', { name: 'Open navigation menu' });
await trigger.focus();
await trigger.click();
@@ -19,7 +19,7 @@ test('mobile navigation opens modally, closes with Escape, and returns focus', a
});
test('skip navigation moves focus to the main landmark', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
await page.keyboard.press('Tab');
const skip = page.getByRole('link', { name: 'Skip to main content' });
await expect(skip).toBeFocused();
@@ -32,7 +32,7 @@ test.describe('primary navigation visibility', () => {
test.use({ viewport: { width: 1024, height: 768 } });
test('keeps the complete navbar visible at common laptop widths', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
const navigation = page.getByRole('navigation', { name: 'Primary navigation' }).first();
await expect(navigation).toBeVisible();
@@ -47,7 +47,7 @@ test.describe('mobile primary navigation destinations', () => {
test.use({ viewport: { width: 390, height: 844 } });
test('keeps every navbar destination in the mobile drawer', async ({ page }) => {
await page.goto('/en');
await page.goto('/en/system');
await page.getByRole('button', { name: 'Open navigation menu' }).click();
const dialog = page.getByRole('dialog', { name: 'Primary navigation' });
@@ -3,7 +3,7 @@ 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 page.goto('/ar/dark');
await expect(page.locator('html')).toHaveAttribute('lang', 'ar');
await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
await expect(page.locator('h1')).toBeVisible();
@@ -6,7 +6,7 @@ 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}`);
await page.goto(`/${locale}/system`);
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
@@ -19,6 +19,6 @@ test('shell remains operable at a 320 CSS pixel reflow equivalent to 200 percent
page,
}) => {
await page.setViewportSize({ width: 640, height: 800 });
await page.goto('/fr');
await page.goto('/fr/system');
await expect(page.getByRole('button', { name: 'Ouvrir le menu de navigation' })).toBeVisible();
});
+7 -7
View File
@@ -6,14 +6,14 @@ for (const scenario of [
{ 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 raw = await request.get(`/${scenario.locale}/system`);
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}`);
const response = await page.goto(`/${scenario.locale}/system`);
expect(response?.status()).toBe(200);
await expect(page.locator('html')).toHaveAttribute('lang', scenario.locale);
await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
@@ -23,7 +23,7 @@ for (const scenario of [
await expect(page.locator('footer')).toBeVisible();
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
'href',
new RegExp(`/${scenario.locale}$`),
new RegExp(`/${scenario.locale}/system$`),
);
for (const language of ['en', 'fr', 'ar', 'x-default']) {
await expect(page.locator(`link[rel="alternate"][hreflang="${language}"]`)).toHaveCount(1);
@@ -41,18 +41,18 @@ test('root locale detection honors Accept-Language', async ({ browser }) => {
});
const page = await context.newPage();
await page.goto('/');
await expect(page).toHaveURL(/\/ar$/);
await expect(page).toHaveURL(/\/ar\/system$/);
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.goto('/en/system/privacy?utm_source=qa&email=drop-me#ignored');
await page.getByLabel('Language').first().selectOption('fr');
await expect(page).toHaveURL('/fr/confidentialite?utm_source=qa');
await expect(page).toHaveURL('/fr/system/confidentialite?utm_source=qa');
});
test('pending route shells are localized and noindex', async ({ page }) => {
await page.goto('/fr/confidentialite');
await page.goto('/fr/system/confidentialite');
await expect(page.locator('h1')).toContainText('Confidentialité');
await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', /noindex/);
});
@@ -3,7 +3,7 @@ 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.goto(`/${locale}/system`);
await page
.locator('main article h3')
.first()
@@ -14,7 +14,7 @@ test('explicit dark theme is present on the server-rendered root', async ({ brow
},
]);
const page = await context.newPage();
const response = await page.goto('/en');
const response = await page.goto('/en/dark');
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'] ?? '';
@@ -28,7 +28,7 @@ test('explicit dark theme is present on the server-rendered root', async ({ brow
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 page.goto('/en/system');
await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'system');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await context.close();
@@ -37,7 +37,7 @@ test('system preference resolves before interaction', async ({ browser }) => {
test('system follows operating-system changes while explicit preferences ignore them', async ({
page,
}) => {
await page.goto('/en');
await page.goto('/en/system');
const selector = page.getByLabel('Theme').first();
await selector.selectOption('system');
@@ -47,6 +47,7 @@ test('system follows operating-system changes while explicit preferences ignore
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await selector.selectOption('dark');
await expect(page).toHaveURL('/en/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');
@@ -1,5 +1,5 @@
{
"fixtureRoute": "/{locale}/component-lab",
"fixtureRoute": "/{locale}/system/component-lab",
"productionGuard": "COMPONENT_FIXTURES_ENABLED=true",
"locales": ["en", "fr", "ar"],
"themes": ["light", "dark", "system"],
@@ -19,9 +19,9 @@ describe('destination registry', () => {
expect(
resolveDestination('privacy', 'fr', { demoSubmissionEnabled: false, demoMode: 'blocked' })
.href,
).toBe('/fr/confidentialite');
).toBe('/fr/system/confidentialite');
expect(
resolveDestination('sign-in', 'en', { demoSubmissionEnabled: false, demoMode: 'blocked' }).href,
).toBe('/en/sign-in');
).toBe('/en/system/sign-in');
});
});
+11 -11
View File
@@ -27,33 +27,33 @@ describe('localization foundations', () => {
});
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(localizedPath('home', 'ar')).toBe('/ar/system');
expect(localizedPath('privacy', 'fr')).toBe('/fr/system/confidentialite');
expect(localizedPath('sign-in', 'ar')).toBe('/ar/system/تسجيل-الدخول');
expect(routeIdFromSlug('fr', 'confidentialite')).toBe('privacy');
expect(routeIdFromSlug('fr', 'privacy')).toBeNull();
expect(routeIdFromAnyLocaleSlug('privacy')).toBe('privacy');
expect(routeIdFromAnyLocaleSlug('confidentialite')).toBe('privacy');
expect(routeIdFromAnyLocaleSlug('unknown')).toBeNull();
expect(routeIdFromPathname('/ar/%D8%A7%D9%84%D8%AE%D8%B5%D9%88%D8%B5%D9%8A%D8%A9')).toBe(
'privacy',
);
expect(
routeIdFromPathname('/ar/system/%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');
expect(links.en).toBe('https://approved.test/en/system');
expect(links.ar).toBe('https://approved.test/ar/system');
expect(links['x-default']).toBe('https://approved.test/en/system');
});
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',
'https://approved.test/en/system?utm_source=campaign&email=private%40approved.test#gibberish',
);
const switched = localeSwitchUrl(current, 'fr', 'home');
expect(switched.pathname).toBe('/fr');
expect(switched.pathname).toBe('/fr/system');
expect(switched.search).toBe('?utm_source=campaign');
expect(switched.hash).toBe('');
@@ -15,7 +15,7 @@ describe('PricingSection', () => {
const user = userEvent.setup();
const content = buildHomepageContent(enHomepage, enShell).pricing;
const { container } = render(
<PricingSection content={content} locale="en" homePath="/en" demoSubmissionEnabled />,
<PricingSection content={content} locale="en" homePath="/en/system" demoSubmissionEnabled />,
);
expect(container.querySelector('[data-plan="launch"]')).toHaveAttribute(
@@ -34,7 +34,9 @@ describe('PricingSection', () => {
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 />);
render(
<PricingSection content={content} locale="en" homePath="/en/system" demoSubmissionEnabled />,
);
expect(screen.getAllByText('Starting price pending approval')).toHaveLength(2);
expect(screen.getByText('Custom pricing')).toBeInTheDocument();
+49 -2
View File
@@ -43,6 +43,15 @@ function request(input: string) {
};
}
type ProxyTestResponse = {
kind: 'next' | 'redirect';
url?: string;
};
function runProxy(proxy: (request: never) => unknown, input: ReturnType<typeof request>) {
return proxy(input as never) as ProxyTestResponse;
}
beforeEach(() => {
nextServer.next.mockClear();
nextServer.redirect.mockClear();
@@ -52,7 +61,7 @@ describe('homepage proxy app boundaries', () => {
it('does not localize storefront proxy paths', async () => {
const { proxy } = await import('@/proxy');
const response = proxy(request('https://rentaldrivego.ma/storefront/en') as never) as any;
const response = runProxy(proxy, request('https://rentaldrivego.ma/storefront/en'));
expect(response.kind).toBe('next');
expect(nextServer.redirect).not.toHaveBeenCalled();
@@ -61,7 +70,45 @@ describe('homepage proxy app boundaries', () => {
it('redirects locale-prefixed storefront paths back to the storefront proxy mount', async () => {
const { proxy } = await import('@/proxy');
const response = proxy(request('https://rentaldrivego.ma/en/storefront/en') as never) as any;
const response = runProxy(proxy, request('https://rentaldrivego.ma/en/storefront/en'));
expect(response).toMatchObject({
kind: 'redirect',
url: 'https://rentaldrivego.ma/storefront/en',
});
});
it('redirects the root path to locale and mode segments', async () => {
const { proxy } = await import('@/proxy');
const input = request('https://rentaldrivego.ma/');
input.headers.set('accept-language', 'fr;q=1,en;q=0.8');
const response = runProxy(proxy, input);
expect(response).toMatchObject({
kind: 'redirect',
url: 'https://rentaldrivego.ma/fr/system',
});
});
it('redirects legacy localized slugs to mode-aware localized slugs', async () => {
const { proxy } = await import('@/proxy');
const response = runProxy(proxy, request('https://rentaldrivego.ma/en/privacy'));
expect(response).toMatchObject({
kind: 'redirect',
url: 'https://rentaldrivego.ma/en/system/privacy',
});
});
it('redirects locale-and-mode storefront paths back to the storefront proxy mount', async () => {
const { proxy } = await import('@/proxy');
const response = runProxy(
proxy,
request('https://rentaldrivego.ma/en/dark/storefront/en'),
);
expect(response).toMatchObject({
kind: 'redirect',
@@ -12,7 +12,7 @@ for (const scenario of [
scenario.theme,
);
await page.setViewportSize({ width: scenario.width, height: scenario.height });
await page.goto(`/${scenario.locale}/component-lab`);
await page.goto(`/${scenario.locale}/${scenario.theme}/component-lab`);
await expect(page).toHaveScreenshot(
`component-system-${scenario.locale}-${scenario.theme}-${scenario.width}.png`,
{ fullPage: true },
@@ -24,7 +24,7 @@ for (const scenario of scenarios) {
sameSite: 'Lax',
},
]);
await page.goto(`/${scenario.locale}`);
await page.goto(`/${scenario.locale}/${scenario.theme}`);
await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
});
}
@@ -12,7 +12,7 @@ for (const scenario of [
}) => {
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.goto(`/${scenario.locale}/${scenario.theme}`);
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`);
@@ -31,7 +31,7 @@ for (const scenario of scenarios) {
sameSite: 'Lax',
},
]);
await page.goto(`/${scenario.locale}`);
await page.goto(`/${scenario.locale}/${scenario.theme}`);
await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
});
}
@@ -49,7 +49,7 @@ test('mobile navigation open baseline', async ({ page }) => {
sameSite: 'Lax',
},
]);
await page.goto('/ar');
await page.goto('/ar/dark');
await page.getByRole('button', { name: 'فتح قائمة التنقل' }).click();
await expect(page).toHaveScreenshot('mobile-ar-dark-navigation-open.png', { fullPage: true });
});
@@ -57,7 +57,7 @@ test('mobile navigation open baseline', async ({ page }) => {
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.goto('/en/light');
await page.keyboard.press('Tab');
await expect(page).toHaveScreenshot('desktop-en-light-focus.png', { fullPage: true });
});