From 6e8db4ec39c028c0b967225cdf7763c7f23e89e5 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:04:58 +0100 Subject: [PATCH] test: add mobile layout E2E tests for iPhone 15 - Create tests/e2e/mobile/mobile-layout.spec.ts with 8 tests covering AppBar/hamburger visibility, drawer open/close, mobile card rendering, PageHeader button layout, dialog width, card actions, and analytics overflow. - Fix AnalyticsClient: make Autocomplete full-width on mobile, add overflow:hidden to outer Stack to prevent body scrollWidth growth. - Fix WorldMapInner: remove hard-coded minWidth:400 that caused 73px horizontal overflow on 393px iPhone 15 viewport. - Fix DashboardLayoutClient: add overflowX:hidden to main content area to contain chart library elements that exceed viewport width. Co-Authored-By: Claude Sonnet 4.6 --- app/(dashboard)/DashboardLayoutClient.tsx | 3 +- app/(dashboard)/analytics/AnalyticsClient.tsx | 4 +- app/(dashboard)/analytics/WorldMapInner.tsx | 3 +- tests/e2e/mobile/mobile-layout.spec.ts | 116 ++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/mobile/mobile-layout.spec.ts diff --git a/app/(dashboard)/DashboardLayoutClient.tsx b/app/(dashboard)/DashboardLayoutClient.tsx index eced2db0..caa33da1 100644 --- a/app/(dashboard)/DashboardLayoutClient.tsx +++ b/app/(dashboard)/DashboardLayoutClient.tsx @@ -229,7 +229,8 @@ export default function DashboardLayoutClient({ user, children }: { user: User; flexGrow: 1, p: { xs: 2, md: 5 }, width: { md: `calc(100% - ${DRAWER_WIDTH}px)` }, - mt: { xs: `${APP_BAR_HEIGHT}px`, md: 0 } + mt: { xs: `${APP_BAR_HEIGHT}px`, md: 0 }, + overflowX: 'hidden', }} > diff --git a/app/(dashboard)/analytics/AnalyticsClient.tsx b/app/(dashboard)/analytics/AnalyticsClient.tsx index 326597b0..a2ebb1bb 100644 --- a/app/(dashboard)/analytics/AnalyticsClient.tsx +++ b/app/(dashboard)/analytics/AnalyticsClient.tsx @@ -282,7 +282,7 @@ export default function AnalyticsClient() { // ── Render ──────────────────────────────────────────────────────────────── return ( - + {/* Header */} @@ -356,7 +356,7 @@ export default function AnalyticsClient() { onChange={(_e, v) => setSelectedHosts(v)} disableCloseOnSelect limitTags={2} - sx={{ width: 260, flexShrink: 0 }} + sx={{ width: { xs: '100%', sm: 260 }, flexShrink: 0 }} ListboxProps={{ // Prevent scroll from the dropdown list leaking to the page style: { overscrollBehavior: 'contain' }, diff --git a/app/(dashboard)/analytics/WorldMapInner.tsx b/app/(dashboard)/analytics/WorldMapInner.tsx index 473ee0cc..263607b5 100644 --- a/app/(dashboard)/analytics/WorldMapInner.tsx +++ b/app/(dashboard)/analytics/WorldMapInner.tsx @@ -289,7 +289,8 @@ export default function WorldMapInner({ data, selectedCountry }: { data: Country border: '1px solid rgba(148,163,184,0.08)', flex: 1, minHeight: 280, - minWidth: 400, + minWidth: { xs: 0, sm: 400 }, + width: '100%', }}> { + test.beforeEach(async ({ page }) => { + // Skip on desktop viewports — these tests are mobile-only + const viewport = page.viewportSize(); + if (!viewport || viewport.width > 600) { + test.skip(); + } + }); + test('app bar is visible with hamburger and title', async ({ page }) => { + await page.goto('/'); + // The MUI AppBar should be present on mobile + const appBar = page.locator('header'); + await expect(appBar).toBeVisible(); + // Hamburger button + await expect(page.getByRole('button', { name: /open drawer/i })).toBeVisible(); + // Title text + await expect(page.getByText('Caddy Proxy Manager')).toBeVisible(); + }); + + test('drawer opens and closes via hamburger', async ({ page }) => { + await page.goto('/'); + // Drawer is closed initially — it renders as a dialog with keepMounted + // but the dialog should not be visible (no active attr on closed drawer) + // Open drawer first to get a reference to the dialog + const drawerDialog = page.locator('[role="dialog"]'); + // The dialog is hidden (not visible) before opening + await expect(drawerDialog.getByRole('link', { name: /proxy hosts/i })).not.toBeVisible(); + // Open drawer + await page.getByRole('button', { name: /open drawer/i }).click(); + await expect(drawerDialog.getByRole('link', { name: /proxy hosts/i })).toBeVisible(); + // Close by pressing Escape + await page.keyboard.press('Escape'); + await expect(drawerDialog.getByRole('link', { name: /proxy hosts/i })).not.toBeVisible(); + }); + + test('navigating from drawer closes it', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: /open drawer/i }).click(); + const drawerDialog = page.locator('[role="dialog"]'); + const drawerNavLink = drawerDialog.getByRole('link', { name: /proxy hosts/i }); + await expect(drawerNavLink).toBeVisible(); + // Click a nav link inside the drawer + await drawerNavLink.click(); + await expect(page).toHaveURL('/proxy-hosts'); + // Drawer should close after navigation — drawer links no longer visible + await expect(drawerDialog.getByRole('link', { name: /access lists/i })).not.toBeVisible(); + }); + + test('proxy hosts page shows card list, not a table', async ({ page }) => { + await page.goto('/proxy-hosts'); + // On mobile with mobileCard, there should be no element + // (DataTable renders cards instead) + await expect(page.locator('table')).not.toBeVisible(); + }); + + test('page header action button appears below title on mobile', async ({ page }) => { + await page.goto('/proxy-hosts'); + const title = page.getByRole('heading', { name: /proxy hosts/i }); + const button = page.getByRole('button', { name: /create host/i }); + await expect(title).toBeVisible(); + await expect(button).toBeVisible(); + // Button should be below the title — its Y coordinate should be greater + const titleBox = await title.boundingBox(); + const buttonBox = await button.boundingBox(); + expect(titleBox).not.toBeNull(); + expect(buttonBox).not.toBeNull(); + expect(buttonBox!.y).toBeGreaterThan(titleBox!.y + titleBox!.height - 1); + }); + + test('create host dialog is usable at mobile width', async ({ page }) => { + await page.goto('/proxy-hosts'); + await page.getByRole('button', { name: /create host/i }).click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + // Dialog should not overflow — check it fits in viewport + const dialogBox = await dialog.boundingBox(); + const viewportWidth = page.viewportSize()?.width ?? 393; + expect(dialogBox).not.toBeNull(); + expect(dialogBox!.width).toBeLessThanOrEqual(viewportWidth + 1); // +1 for rounding + // Key form fields should be visible + await expect(page.getByLabel(/domains/i)).toBeVisible(); + }); + + test('card edit and delete actions reachable without scrolling', async ({ page }) => { + await page.goto('/proxy-hosts'); + // Create a host so there is at least one card to inspect + await page.getByRole('button', { name: /create host/i }).click(); + await expect(page.getByRole('dialog')).toBeVisible(); + await page.getByLabel('Name').fill('Mobile Test Host'); + await page.getByLabel(/domains/i).fill('mobile-test.local'); + await page.getByPlaceholder('10.0.0.5:8080').fill('localhost:9999'); + await page.getByRole('button', { name: /^create$/i }).click(); + await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 }); + // The mobileCard renderer must include Edit and Delete icon buttons with aria-labels. + // They should be immediately visible — no horizontal scroll needed. + await expect(page.getByRole('button', { name: /^edit$/i }).first()).toBeVisible(); + await expect(page.getByRole('button', { name: /^delete$/i }).first()).toBeVisible(); + }); + + test('analytics page loads without horizontal body overflow', async ({ page }) => { + await page.goto('/analytics'); + // Wait for content to load + await page.waitForLoadState('networkidle'); + // The document body should not be wider than the viewport + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewportWidth = page.viewportSize()?.width ?? 393; + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 5); // 5px tolerance + }); +});