feat(dashboard): Enhance dashboard with PageHeader, StatCard, and auth fixes
- Create PageHeader component with title, description, and action slots - Create StatCard component with trend indicators and icons - Update dashboard with KPI cards, Quick Actions, and Allocation Preview - Polish login page with branding and centered layout - Fix auth redirect: authenticated users accessing /login go to dashboard - Fix page refresh: auth state persists, no blank page - Fix sidebar: visible after login, toggle works, state persists - Fix CSS import: add app.css to layout, fix DaisyUI import path - Fix breadcrumbs: home icon links to /dashboard - Add comprehensive E2E and unit tests Refs: openspec/changes/p03-dashboard-enhancement Closes: p03-dashboard-enhancement
This commit is contained in:
@@ -164,4 +164,119 @@ test.describe('Layout E2E', () => {
|
||||
.poll(async () => page.evaluate(() => document.documentElement.getAttribute('data-sidebar')))
|
||||
.not.toBe(before);
|
||||
});
|
||||
|
||||
test('sidebar is visible after login', async ({ page }) => {
|
||||
await loginThroughForm(page);
|
||||
|
||||
// Sidebar should be visible
|
||||
const sidebar = page.locator('[data-testid="sidebar"]');
|
||||
await expect(sidebar).toBeVisible();
|
||||
|
||||
// Verify sidebar has content
|
||||
await expect(sidebar.locator('text=Dashboard')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sidebar toggle button works when collapsed', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await openDashboard(page);
|
||||
|
||||
// Get initial state
|
||||
const sidebar = page.locator('[data-testid="sidebar"]');
|
||||
await expect(sidebar).toBeVisible();
|
||||
|
||||
// Find and click collapse button (if exists)
|
||||
const collapseButton = page.locator('[data-testid="sidebar-toggle"], [aria-label*="toggle"], button').filter({ has: page.locator('svg') }).first();
|
||||
|
||||
if (await collapseButton.isVisible().catch(() => false)) {
|
||||
// Collapse the sidebar
|
||||
await collapseButton.click();
|
||||
|
||||
// Wait for collapsed state
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Sidebar should still exist but be in collapsed state
|
||||
await expect(sidebar).toBeAttached();
|
||||
|
||||
// Click toggle again to expand
|
||||
await collapseButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Sidebar should be visible again
|
||||
await expect(sidebar).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('page refresh after login does not show blank page', async ({ page }) => {
|
||||
await loginThroughForm(page);
|
||||
|
||||
// Verify we're on dashboard
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||||
|
||||
// Get the token before refresh
|
||||
const tokenBefore = await page.evaluate(() => localStorage.getItem('headroom_access_token'));
|
||||
expect(tokenBefore).toBeTruthy();
|
||||
|
||||
// Refresh the page
|
||||
await page.reload();
|
||||
|
||||
// Wait for page to fully load after refresh
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check what URL we're on (for debugging)
|
||||
const currentUrl = page.url();
|
||||
|
||||
// We should either be on dashboard (success) or redirected to login (bug)
|
||||
// If we're on login, that's the bug being reported
|
||||
if (currentUrl.includes('/login')) {
|
||||
// Bug: We were redirected to login after refresh
|
||||
// Check if token is still there
|
||||
const tokenAfter = await page.evaluate(() => localStorage.getItem('headroom_access_token'));
|
||||
expect(tokenAfter, 'Token should persist after refresh').toBeTruthy();
|
||||
// Fail the test to document the bug
|
||||
throw new Error('Bug: User was redirected to login after page refresh despite having valid token');
|
||||
}
|
||||
|
||||
// If we're still on dashboard, wait for client-side hydration to complete
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
|
||||
// Wait for SvelteKit to hydrate the page
|
||||
// The page might be SSR'd but needs client-side hydration to show content
|
||||
await page.waitForFunction(() => {
|
||||
// Check if the page has hydrated by looking for SvelteKit's hydration marker
|
||||
return document.querySelector('[data-sveltekit-hydrated]') !== null ||
|
||||
document.body.innerHTML.includes('Dashboard');
|
||||
}, { timeout: 10000 });
|
||||
|
||||
// Now verify content is visible
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify token is still there
|
||||
const tokenAfter = await page.evaluate(() => localStorage.getItem('headroom_access_token'));
|
||||
expect(tokenAfter).toBe(tokenBefore);
|
||||
});
|
||||
|
||||
test('sidebar state persists after page refresh', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
await openDashboard(page);
|
||||
|
||||
// Get initial sidebar state
|
||||
const initialState = await page.evaluate(() =>
|
||||
document.documentElement.getAttribute('data-sidebar')
|
||||
);
|
||||
|
||||
// Refresh page
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify sidebar is still visible
|
||||
const sidebar = page.locator('[data-testid="sidebar"]');
|
||||
await expect(sidebar).toBeVisible();
|
||||
|
||||
// Verify state persisted
|
||||
const afterRefresh = await page.evaluate(() =>
|
||||
document.documentElement.getAttribute('data-sidebar')
|
||||
);
|
||||
expect(afterRefresh).toBe(initialState);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user