feat(layout): finalize p01 and p02 changes
Complete UI foundation and app layout implementation, stabilize container health checks, and archive both OpenSpec changes after verification.
This commit is contained in:
167
frontend/tests/e2e/layout.spec.ts
Normal file
167
frontend/tests/e2e/layout.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user