import { test, expect } from '@playwright/test'; // Force a mobile viewport even under the desktop Chromium project so these // checks validate responsive behavior instead of self-skipping. test.use({ viewport: { width: 393, height: 852 } }); test.describe('Mobile layout', () => { 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 navigation/i })).toBeVisible(); // Title text await expect(page.getByRole('banner').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', exact: true })).not.toBeVisible(); // Open drawer await page.getByRole('button', { name: /open navigation/i }).click(); await expect(drawerDialog.getByRole('link', { name: 'Proxy Hosts', exact: true })).toBeVisible(); // Close by pressing Escape await page.keyboard.press('Escape'); await expect(drawerDialog.getByRole('link', { name: 'Proxy Hosts', exact: true })).not.toBeVisible(); }); test('navigating from drawer closes it', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: /open navigation/i }).click(); const drawerDialog = page.locator('[role="dialog"]'); const drawerNavLink = drawerDialog.getByRole('link', { name: 'Proxy Hosts', exact: true }); 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 uses a DropdownMenu (three-dot button) for actions. // Open the dropdown and verify Edit and Delete menu items are present. const moreButton = page.getByRole('button', { name: /open menu/i }).first(); await expect(moreButton).toBeVisible(); await moreButton.click(); await expect(page.getByRole('menuitem', { name: /edit/i })).toBeVisible(); await expect(page.getByRole('menuitem', { name: /delete/i })).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 }); });