Compare commits

..

3 Commits

Author SHA1 Message Date
Chris Farhood 14f4b4d17d fix(e2e): add waitForSidebar helper and networkidle waits for reliability
Add waitForSidebar helper function with explicit sidebar visibility wait
and networkidle state to ensure page is fully loaded before assertions.
This addresses flaky E2E tests where elements were not consistently
found due to timing issues during page transitions.
2026-05-05 06:50:21 +00:00
Chris Farhood af07e46930 Fix E2E workflow: use pnpm-capable reusable workflow branch
The reusable plugin-e2e.yaml@main lacks pnpm support. Switching to
the PR branch that has pnpm detector, Corepack setup, and pnpm commands.

Will revert to @main once PR #141 merges.

- PRI-619 E2E fix

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 06:10:21 +00:00
Chris Farhood c9858dd4e1 Add E2E test infrastructure for kube-vip plugin
Scaffolded via e2e-scaffold.sh (proactive improvement).
- playwright.config.ts, e2e/auth.setup.ts, e2e/kube-vip.spec.ts
- scripts/deploy-e2e-headlamp.sh, scripts/teardown-e2e-headlamp.sh
- .github/workflows/e2e.yaml uses reusable workflow
- @playwright/test ^1.58.2 devDep

- PRI-641

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 05:14:54 +00:00
7 changed files with 20 additions and 50 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
e2e:
uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@main
uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e
with:
node-version: '22'
headlamp-version: v0.40.1
+6
View File
@@ -5,3 +5,9 @@ dist/
.env
.env.local
.eslintcache
# E2E
e2e/.auth/
.env.e2e
playwright-report/
test-results/
+2 -3
View File
@@ -24,14 +24,14 @@ test.describe('kube-vip plugin smoke tests', () => {
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/kube-vip/);
await expect(page.getByRole('heading', { name: /kube.vip/i }).first()).toBeVisible();
await expect(page.getByRole('heading', { name: /kube.vip/i })).toBeVisible();
});
test('kube-vip page renders content', async ({ page }) => {
await page.goto('/c/main/kube-vip');
await waitForSidebar(page);
await expect(page.getByRole('heading', { name: /kube.vip/i }).first()).toBeVisible({
await expect(page.getByRole('heading', { name: /kube.vip/i })).toBeVisible({
timeout: 15_000,
});
@@ -43,7 +43,6 @@ test.describe('kube-vip plugin smoke tests', () => {
test('plugin settings page shows kube-vip plugin entry', async ({ page }) => {
await page.goto('/settings/plugins');
await page.waitForLoadState('networkidle');
await page.waitForSelector('table, [class*="PluginList"], [class*="plugin"]', { timeout: 10_000 }).catch(() => {});
const pluginEntry = page.locator('text=/kube.vip/i').first();
await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
+4 -3
View File
@@ -24,7 +24,8 @@
"format:check": "prettier --check src/",
"test": "vitest run",
"test:watch": "vitest",
"e2e": "playwright test"
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
},
"peerDependencies": {
"react": "^18.0.0",
@@ -40,7 +41,6 @@
"@headlamp-k8s/eslint-config": "^0.6.0",
"@kinvolk/headlamp-plugin": "^0.13.0",
"@mui/material": "^5.15.14",
"@playwright/test": "^1.59.1",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
@@ -54,6 +54,7 @@
"react-dom": "^18.3.1",
"react-router-dom": "^5.3.0",
"typescript": "~5.6.2",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"@playwright/test": "^1.58.2"
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ importers:
specifier: ^5.15.14
version: 5.18.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@playwright/test':
specifier: ^1.59.1
specifier: ^1.58.2
version: 1.59.1
'@testing-library/jest-dom':
specifier: ^6.4.8
@@ -6212,7 +6212,7 @@ snapshots:
material-react-table: 2.13.3(93149b7a28d7dcf9399e2d03ebc8c990)
monaco-editor: 0.52.2
msw: 2.4.9(typescript@5.6.2)
msw-storybook-addon: 2.0.3(msw@2.4.9(typescript@5.6.2))
msw-storybook-addon: 2.0.3(msw@2.4.9(typescript@5.6.3))
notistack: 3.0.2(csstype@3.2.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
path-browserify: 1.0.1
prettier: 2.8.8
@@ -10113,7 +10113,7 @@ snapshots:
ms@2.1.3: {}
msw-storybook-addon@2.0.3(msw@2.4.9(typescript@5.6.2)):
msw-storybook-addon@2.0.3(msw@2.4.9(typescript@5.6.3)):
dependencies:
is-node-process: 1.2.0
msw: 2.4.9(typescript@5.6.2)
+3 -39
View File
@@ -118,7 +118,7 @@ metadata:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
spec:
type: LoadBalancer
type: ClusterIP
selector:
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: ${E2E_RELEASE}
@@ -150,51 +150,15 @@ done
echo ""
echo "E2E Headlamp is ready at: ${SVC_URL}"
echo ""
echo "Getting LoadBalancer IP for Headlamp service..."
LB_IP=""
ATTEMPTS=0
MAX_ATTEMPTS=24
while [ -z "${LB_IP}" ] || [ "${LB_IP}" = "<pending>" ]; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
echo "ERROR: LoadBalancer IP not assigned after $((MAX_ATTEMPTS * 5))s" >&2
exit 1
fi
LB_IP=$(kubectl get svc "${E2E_RELEASE}" -n "$E2E_NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
if [ -z "${LB_IP}" ] || [ "${LB_IP}" = "<pending>" ]; then
LB_IP=""
echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] LoadBalancer IP not yet assigned, retrying in 5s..."
sleep 5
fi
done
echo " LoadBalancer IP: ${LB_IP}"
echo ""
echo "Waiting for Headlamp at http://${LB_IP}:80 to be reachable..."
ATTEMPTS=0
MAX_ATTEMPTS=24
until curl -sf --max-time 5 "http://${LB_IP}:80" -o /dev/null 2>/dev/null; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ "$ATTEMPTS" -ge "$MAX_ATTEMPTS" ]; then
echo "ERROR: http://${LB_IP}:80 not reachable after $((MAX_ATTEMPTS * 5))s" >&2
exit 1
fi
echo " [${ATTEMPTS}/${MAX_ATTEMPTS}] LoadBalancer not yet reachable, retrying in 5s..."
sleep 5
done
echo ""
echo "Headlamp is ready at http://${LB_IP}:80"
echo ""
echo "Creating service account token for E2E auth..."
kubectl create serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
TOKEN=$(kubectl create token headlamp-e2e-test -n "$E2E_NAMESPACE" --duration=1h 2>/dev/null || echo "")
if [ -n "$TOKEN" ]; then
echo "HEADLAMP_URL=http://${LB_IP}:80" > "$REPO_ROOT/.env.e2e"
echo "HEADLAMP_URL=${SVC_URL}" > "$REPO_ROOT/.env.e2e"
echo "HEADLAMP_TOKEN=${TOKEN}" >> "$REPO_ROOT/.env.e2e"
echo "Wrote .env.e2e with HEADLAMP_URL=http://${LB_IP}:80 and HEADLAMP_TOKEN"
echo "Wrote .env.e2e with HEADLAMP_URL and HEADLAMP_TOKEN"
else
echo " WARNING: Could not generate token."
fi
+1 -1
View File
@@ -22,7 +22,7 @@ echo "Cleaning up test service account..."
kubectl delete serviceaccount headlamp-e2e-test -n "$E2E_NAMESPACE" --ignore-not-found
if [ -f "$REPO_ROOT/.env.e2e" ]; then
rm -f "$REPO_ROOT/.env.e2e"
rm "$REPO_ROOT/.env.e2e"
echo "Removed .env.e2e"
fi