Compare commits

..

3 Commits

Author SHA1 Message Date
Chris Farhood 40c86d66d2 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 42a614a2fe 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:20 +00:00
Chris Farhood 08cff6f5f3 Add E2E test infrastructure for argocd plugin
- playwright.config.ts with authenticated test projects
- e2e/auth.setup.ts authenticates via OIDC or token
- e2e/argocd.spec.ts smoke tests for sidebar, applications list,
  and plugin settings
- scripts/deploy-e2e-headlamp.sh deploys Headlamp + argocd in headlamp-dev
- scripts/teardown-e2e-headlamp.sh cleans up after tests
- e2e.yaml uses reusable workflow from .github repo
- @playwright/test ^1.58.2 devDep added

- PRI-638

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-05 05:14:47 +00:00
12 changed files with 133 additions and 59 deletions
+1 -1
View File
@@ -18,6 +18,6 @@ jobs:
e2e:
uses: privilegedescalation/.github/.github/workflows/plugin-e2e.yaml@hugh/add-pnpm-support-plugin-e2e
with:
node-version: "22"
node-version: '22'
headlamp-version: v0.40.1
e2e-namespace: headlamp-dev
+7 -1
View File
@@ -14,4 +14,10 @@ coverage/
*.swo
*~
.vscode/
.idea/
.idea/
# E2E
e2e/.auth/
.env.e2e
playwright-report/
test-results/
+3 -3
View File
@@ -1,4 +1,4 @@
version: "0.1.2"
version: "0.1.0"
name: headlamp-argocd
displayName: ArgoCD Headlamp Plugin
createdAt: "2026-04-21T00:00:00Z"
@@ -26,8 +26,8 @@ maintainers:
provider:
name: privilegedescalation
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.2/privilegedescalation-headlamp-argocd-plugin-0.1.2.tar.gz"
headlamp/plugin/archive-checksum: sha256:e71f84913eed1fd7e2d074912e3bfa668c4b1fefcbb069731a4e4277a998ca28
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-argocd-plugin/releases/download/v0.1.0/headlamp-argocd-0.1.0.tar.gz"
headlamp/plugin/archive-checksum: "sha256:1f4df43f79b795bdf4f70e1e3aa5bacadf689ea5584fdadf92fb677faab21c2c"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/distro-compat: "in-cluster"
changes:
+50
View File
@@ -0,0 +1,50 @@
import { test, expect } from '@playwright/test';
async function waitForSidebar(page: import('@playwright/test').Page) {
const sidebar = page.getByRole('navigation', { name: 'Navigation' });
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await page.waitForLoadState('networkidle');
return sidebar;
}
test.describe('ArgoCD plugin smoke tests', () => {
test('sidebar contains ArgoCD entry', async ({ page }) => {
await page.goto('/');
const sidebar = await waitForSidebar(page);
await expect(sidebar.getByRole('button', { name: /argocd/i })).toBeVisible();
});
test('ArgoCD sidebar entry navigates to applications list', async ({ page }) => {
await page.goto('/');
const sidebar = await waitForSidebar(page);
const argocdEntry = sidebar.getByRole('button', { name: /argocd/i });
await expect(argocdEntry).toBeVisible();
await argocdEntry.click();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/argocd/);
await expect(page.getByRole('heading', { name: /argo.*cd.*applications/i })).toBeVisible();
});
test('applications list page renders', async ({ page }) => {
await page.goto('/c/main/argocd');
await waitForSidebar(page);
await expect(page.getByRole('heading', { name: /argo.*cd.*applications/i })).toBeVisible({
timeout: 15_000,
});
const hasTable = await page.locator('table').first().isVisible().catch(() => false);
const hasContent = await page.locator('text=/application|sync|health/i').first().isVisible().catch(() => false);
expect(hasTable || hasContent).toBe(true);
});
test('plugin settings page shows argocd plugin entry', async ({ page }) => {
await page.goto('/settings/plugins');
await page.waitForLoadState('networkidle');
const pluginEntry = page.locator('text=/argocd|argo.*cd/i').first();
await expect(pluginEntry).toBeVisible({ timeout: 30_000 });
});
});
+62 -12
View File
@@ -1,19 +1,69 @@
import { test as setup, request } from '@playwright/test';
import { test as setup, expect, Page } from '@playwright/test';
setup('authenticate', async ({ page }) => {
const token = process.env.HEADLAMP_TOKEN;
const url = process.env.HEADLAMP_URL;
const AUTH_STATE_PATH = 'e2e/.auth/state.json';
if (!token || !url) {
throw new Error('HEADLAMP_TOKEN and HEADLAMP_URL must be set');
async function authenticateWithOIDC(page: Page, username: string, password: string): Promise<void> {
await page.goto('/');
await page.waitForURL('**/login');
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: /sign in/i }).click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
await popup.waitForLoadState('networkidle');
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();
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();
await popup.waitForEvent('close', { timeout: 15_000 });
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
async function authenticateWithToken(page: Page, token: string): Promise<void> {
await page.goto('/');
await page.waitForURL(/\/(login|token)$/);
if (page.url().includes('/login')) {
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');
}
await page.context().addInitScript(() => {
window.localStorage.setItem('token', 'dummy-token');
await page.getByRole('textbox', { name: /id token/i }).fill(token);
await page.getByRole('button', { name: /authenticate/i }).click();
await expect(page.getByRole('navigation', { name: 'Navigation' })).toBeVisible({
timeout: 15_000,
});
}
await page.goto(url);
setup('authenticate with Headlamp', async ({ page }) => {
const username = process.env.AUTHENTIK_USERNAME;
const password = process.env.AUTHENTIK_PASSWORD;
const token = process.env.HEADLAMP_TOKEN;
const context = page.context();
await context.storageState({ path: 'e2e/.auth/state.json' });
});
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 });
});
-16
View File
@@ -1,16 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('ArgoCD Plugin E2E', () => {
test('plugin page loads', async ({ page }) => {
const url = process.env.HEADLAMP_URL;
if (!url) {
throw new Error('HEADLAMP_URL must be set');
}
await page.goto(url);
await page.waitForLoadState('networkidle');
const title = await page.title();
expect(title).toBeTruthy();
});
});
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@privilegedescalation/headlamp-argocd-plugin",
"version": "0.1.2",
"version": "0.1.0",
"description": "Headlamp plugin for ArgoCD visibility — monitors ArgoCD Applications, Rollouts, and health status",
"repository": {
"type": "git",
@@ -51,7 +51,6 @@
"@headlamp-k8s/eslint-config": "^0.6.0",
"eslint": "^8.57.0",
"jsdom": "^24.0.0",
"playwright": "^1.58.2",
"prettier": "^2.8.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+1 -1
View File
@@ -24,4 +24,4 @@ export default defineConfig({
dependencies: ['setup'],
},
],
});
});
+2 -5
View File
@@ -49,9 +49,6 @@ importers:
jsdom:
specifier: ^24.0.0
version: 24.1.3
playwright:
specifier: ^1.58.2
version: 1.59.1
prettier:
specifier: ^2.8.8
version: 2.8.8
@@ -6255,7 +6252,7 @@ snapshots:
jsdom: 24.1.3
jsonpath-plus: 10.4.0
lodash: 4.18.1
material-react-table: 2.13.3(330725fe5432f245d076f0c0dda1a7a7)
material-react-table: 2.13.3(0078ddeddc9e779fa84c03996c1db10e)
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.3))
@@ -9964,7 +9961,7 @@ snapshots:
'@types/minimatch': 3.0.5
minimatch: 3.1.5
material-react-table@2.13.3(330725fe5432f245d076f0c0dda1a7a7):
material-react-table@2.13.3(0078ddeddc9e779fa84c03996c1db10e):
dependencies:
'@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@19.2.14)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)
-4
View File
@@ -1,4 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>privilegedescalation/.github:renovate-config"]
}
+3 -11
View File
@@ -4,16 +4,9 @@
# Deploys a stock Headlamp instance with the argocd plugin loaded via
# a ConfigMap volume mount.
#
# E2E resources are deployed to the `headlamp-dev` namespace. Nothing
# persists beyond the test run — teardown cleans up all created resources.
#
# Prerequisites:
# - Plugin built (dist/ exists with plugin-main.js + package.json)
# - kubectl configured with cluster access
#
# Environment:
# E2E_NAMESPACE — namespace for E2E Headlamp (default: headlamp-dev)
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
# E2E_NAMESPACE — namespace (default: headlamp-dev)
# E2E_RELEASE — release name prefix (default: headlamp-e2e)
# HEADLAMP_VERSION — Headlamp image tag (default: latest)
set -euo pipefail
@@ -150,7 +143,6 @@ spec:
EOF
echo "Waiting for rollout..."
sleep 2
kubectl rollout status "deployment/${E2E_RELEASE}" \
-n "$E2E_NAMESPACE" --timeout=120s
@@ -187,4 +179,4 @@ else
fi
echo ""
echo "E2E deployment complete."
echo "E2E deployment complete."
+3 -3
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# teardown-e2e-headlamp.sh
#
# Tears down the dedicated E2E Headlamp instance deployed by deploy-e2e-headlamp.sh.
# Tears down the dedicated E2E Headlamp instance.
#
# Environment:
# E2E_NAMESPACE — namespace to clean up (default: headlamp-dev)
# E2E_RELEASE — release/resource name prefix (default: headlamp-e2e)
# E2E_RELEASE — release name prefix (default: headlamp-e2e)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -34,4 +34,4 @@ if [ -f "$REPO_ROOT/.env.e2e" ]; then
fi
echo ""
echo "E2E teardown complete."
echo "E2E teardown complete."