feat: add Playwright E2E smoke tests

Follows the pattern established in headlamp-intel-gpu-plugin (PR #25):
- e2e/sealed-secrets.spec.ts: 5 smoke tests covering sidebar navigation,
  list view, sealing keys view, cross-view navigation, and plugin settings
- e2e/auth.setup.ts: shared OIDC + token auth setup
- playwright.config.ts: fail-fast if HEADLAMP_URL not set (no prod URL fallback)
- scripts/deploy-e2e-headlamp.sh: ConfigMap-based plugin injection to privilegedescalation-dev
- scripts/teardown-e2e-headlamp.sh: clean teardown of all E2E resources
This commit is contained in:
Gandalf the Greybeard
2026-03-24 23:19:20 +00:00
parent 4f474e02bc
commit a2ac69c764
8 changed files with 447 additions and 0 deletions
View File
+81
View File
@@ -0,0 +1,81 @@
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;
// Wait for the Authentik popup to fully load before interacting
await popup.waitForLoadState('domcontentloaded');
await popup.waitForLoadState('networkidle');
// Authentik step 1: fill username — wait for the form to render
const usernameField = popup.getByRole('textbox', { name: /email or username/i });
await usernameField.waitFor({ state: 'visible', timeout: 15_000 });
await usernameField.fill(username);
await popup.getByRole('button', { name: /log in/i }).click();
// Authentik step 2: fill password — wait for the next step to load
await popup.waitForLoadState('networkidle');
const passwordField = popup.getByRole('textbox', { name: /password/i });
await passwordField.waitFor({ state: 'visible', timeout: 15_000 });
await passwordField.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> {
await page.goto('/');
// Headlamp goes to /token directly when no OIDC is configured,
// or through /login when OIDC is configured
await page.waitForURL(/\/(login|token)$/);
if (page.url().includes('/login')) {
// OIDC login page — click "use a token" to reach token auth.
const useTokenBtn = page.getByRole('button', { name: /use a token/i });
await useTokenBtn.waitFor({ state: 'visible', timeout: 15_000 });
await useTokenBtn.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 });
});
+88
View File
@@ -0,0 +1,88 @@
import { test, expect } from '@playwright/test';
test.describe('Sealed Secrets plugin smoke tests', () => {
test('sidebar contains sealed-secrets entry', async ({ page }) => {
await page.goto('/');
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await expect(sidebar.getByRole('button', { name: /sealed.secrets/i })).toBeVisible();
});
test('sidebar sealed-secrets entry is clickable and navigates to list view', async ({ page }) => {
await page.goto('/');
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
const sealedSecretsEntry = sidebar.getByRole('button', { name: /sealed.secrets/i });
await expect(sealedSecretsEntry).toBeVisible();
await sealedSecretsEntry.click();
await expect(page).toHaveURL(/\/sealedsecrets/);
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible();
});
test('sealed secrets list page renders table or empty state', async ({ page }) => {
await page.goto('/c/main/sealedsecrets');
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({
timeout: 15_000,
});
// Either a populated table or an empty-state indicator must be visible
const hasTable = await page.locator('table').first().isVisible().catch(() => false);
const hasEmptyState = await page
.locator('text=/no.*sealed|no.*secret|0 item|empty/i')
.first()
.isVisible()
.catch(() => false);
expect(hasTable || hasEmptyState).toBe(true);
});
test('sealing keys page renders table or empty state', async ({ page }) => {
await page.goto('/c/main/sealedsecrets/keys');
await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible({
timeout: 15_000,
});
const hasTable = await page.locator('table').first().isVisible().catch(() => false);
const hasEmptyState = await page
.locator('text=/no.*key|0 item|empty/i')
.first()
.isVisible()
.catch(() => false);
expect(hasTable || hasEmptyState).toBe(true);
});
test('navigation between sealed-secrets views works', async ({ page }) => {
await page.goto('/c/main/sealedsecrets');
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible({
timeout: 15_000,
});
// Navigate to Sealing Keys via sidebar
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
const keysLink = sidebar.getByRole('link', { name: /sealing.key/i });
await expect(keysLink).toBeVisible();
await keysLink.click();
await expect(page).toHaveURL(/\/sealedsecrets\/keys$/);
await expect(page.getByRole('heading', { name: /sealing.key/i })).toBeVisible();
// Navigate back to All Sealed Secrets
const allSecretsLink = sidebar.getByRole('link', { name: /all sealed secrets/i });
await expect(allSecretsLink).toBeVisible();
await allSecretsLink.click();
await expect(page).toHaveURL(/\/sealedsecrets(?!\/keys)/);
await expect(page.getByRole('heading', { name: /sealed.secrets/i })).toBeVisible();
});
test('plugin settings page shows sealed-secrets plugin entry', async ({ page }) => {
await page.goto('/settings/plugins');
// Wait for plugin list to load — plugin scripts load asynchronously
const pluginEntry = page.locator('text=sealed-secrets').first();
await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
});
});