feat: add Playwright E2E smoke tests and fix empty namespace crash

Fix getNamespaces() to skip cluster-scoped resources (Namespace: "")
that caused Router.createRouteURL to throw TypeError on the Namespaces
page. Add Playwright E2E smoke tests with Authentik OIDC auth for CI
and K8s token fallback for local dev. Add Gitea Actions E2E workflow,
vitest unit test infrastructure, and test-utils fixtures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 18:53:40 -05:00
parent 2a85f2a3d1
commit 186f9ef380
19 changed files with 1264 additions and 33 deletions
+67
View File
@@ -0,0 +1,67 @@
import { test as setup, expect, Page } from '@playwright/test';
const AUTH_STATE_PATH = 'e2e/.auth/state.json';
async function authenticateWithOIDC(page: Page, username: string, password: string): Promise<void> {
// Navigate to login — Headlamp redirects / to /c/main/login
await page.goto('/');
await page.waitForURL('**/login');
// Click "Sign In" and capture the Authentik popup
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: /sign in/i }).click();
const popup = await popupPromise;
// Authentik step 1: fill username
await popup.getByRole('textbox', { name: /email or username/i }).fill(username);
await popup.getByRole('button', { name: /log in/i }).click();
// Authentik step 2: fill password
await popup.getByRole('textbox', { name: /password/i }).fill(password);
await popup.getByRole('button', { name: /continue|log in/i }).click();
// Wait for the popup to close (Authentik redirects back, Headlamp processes callback)
await popup.waitForEvent('close', { timeout: 15_000 });
// Original page should now be authenticated — wait for sidebar
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
async function authenticateWithToken(page: Page, token: string): Promise<void> {
// Navigate to login — Headlamp redirects / to /c/main/login
await page.goto('/');
await page.waitForURL('**/login');
// Click the token auth option
await page.getByRole('button', { name: /use a token/i }).click();
await page.waitForURL('**/token');
// Fill the "ID token" field and submit
await page.getByRole('textbox', { name: /id token/i }).fill(token);
await page.getByRole('button', { name: /authenticate/i }).click();
// Wait for the main UI to load
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
setup('authenticate with Headlamp', async ({ page }) => {
const username = process.env.AUTHENTIK_USERNAME;
const password = process.env.AUTHENTIK_PASSWORD;
const token = process.env.HEADLAMP_TOKEN;
if (username && password) {
await authenticateWithOIDC(page, username, password);
} else if (token) {
await authenticateWithToken(page, token);
} else {
throw new Error(
'Set AUTHENTIK_USERNAME + AUTHENTIK_PASSWORD for OIDC auth, or HEADLAMP_TOKEN for token auth'
);
}
await page.context().storageState({ path: AUTH_STATE_PATH });
});