import { expect, test } from '@playwright/test'; function createToken() { const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' }), 'utf-8').toString('base64'); const payload = Buffer.from( JSON.stringify({ sub: 'e2e', exp: Math.floor(Date.now() / 1000) + 3600 }), 'utf-8' ).toString('base64'); return `${header}.${payload}.sig`; } async function openDashboard(page: import('@playwright/test').Page) { const token = createToken(); await page.goto('/login'); await page.evaluate((accessToken) => { localStorage.setItem('headroom_access_token', accessToken); localStorage.setItem('headroom_refresh_token', accessToken); }, token); await page.goto('/dashboard'); await page.waitForURL('/dashboard'); } async function loginThroughForm(page: import('@playwright/test').Page) { await page.goto('/login'); await page.fill('input[type="email"]', 'superuser@headroom.test'); await page.fill('input[type="password"]', 'password'); await page.click('button[type="submit"]'); await page.waitForURL('/dashboard'); } test.describe('Layout E2E', () => { test.beforeEach(async ({ page }) => { await page.goto('/login'); await page.context().clearCookies(); await page.evaluate(() => localStorage.clear()); }); test('dashboard page has sidebar', async ({ page }) => { await openDashboard(page); await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); }); test('login redirects to dashboard with sidebar', async ({ page }) => { await loginThroughForm(page); await expect(page.locator('[data-testid="sidebar"]')).toBeVisible(); }); test('sidebar toggle works', async ({ page }) => { await openDashboard(page); const initial = await page.evaluate(() => document.documentElement.getAttribute('data-sidebar')); await page.click('[data-testid="sidebar-toggle"]'); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .not.toBe(initial); }); test('theme toggle works', async ({ page }) => { await openDashboard(page); const before = await page.evaluate(() => localStorage.getItem('headroom_theme') ?? 'light'); await page.click('[data-testid="theme-toggle"]'); await expect .poll(async () => page.evaluate(() => localStorage.getItem('headroom_theme'))) .not.toBe(before); }); test('month selector updates period store', async ({ page }) => { await openDashboard(page); const before = await page.evaluate(() => localStorage.getItem('headroom_selected_period')); await page.click('[data-testid="month-selector"] button'); await page.getByRole('button', { name: 'Next' }).click(); await expect .poll(async () => page.evaluate(() => localStorage.getItem('headroom_selected_period'))) .not.toBe(before); }); test('breadcrumbs reflect current route', async ({ page }) => { await openDashboard(page); await expect(page.locator('[data-testid="breadcrumbs"]')).toContainText('Dashboard'); }); test('login page has no sidebar', async ({ page }) => { await page.goto('/login'); await expect(page.locator('[data-testid="sidebar"]')).toHaveCount(0); }); test('sidebar hidden by default on mobile', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openDashboard(page); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .toBe('hidden'); }); test('hamburger shows sidebar on mobile', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openDashboard(page); await page.click('[data-testid="mobile-hamburger"]'); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .toBe('expanded'); }); test('sidebar overlays content and closes on backdrop click on mobile', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openDashboard(page); await page.click('[data-testid="mobile-hamburger"]'); await expect .poll(async () => page.evaluate(() => { const main = document.querySelector('[data-testid="layout-main"]'); if (!main) return null; return getComputedStyle(main).marginLeft; }) ) .toBe('0px'); await page.evaluate(() => { const backdrop = document.querySelector('[data-testid="sidebar-backdrop"]') as HTMLElement | null; backdrop?.click(); }); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .toBe('hidden'); }); test('all key breakpoints render expected sidebar state', async ({ page }) => { const breakpoints: Array<[number, number, 'hidden' | 'collapsed' | 'expanded']> = [ [320, 700, 'hidden'], [768, 900, 'collapsed'], [1024, 900, 'collapsed'], [1280, 900, 'expanded'] ]; for (const [width, height, expected] of breakpoints) { await page.setViewportSize({ width, height }); await page.goto('/login'); await page.evaluate(() => localStorage.setItem('headroom_sidebar_state', 'expanded')); await openDashboard(page); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .toBe(expected); } }); test('keyboard shortcut toggles sidebar', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await openDashboard(page); const before = await page.evaluate(() => document.documentElement.getAttribute('data-sidebar')); await page.keyboard.down('Control'); await page.keyboard.press('\\'); await page.keyboard.up('Control'); await expect .poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar'))) .not.toBe(before); }); });