diff --git a/test/ui-e2e/.auth/setup.ts b/test/ui-e2e/.auth/setup.ts index 03fad54e779..8cf9728ee04 100644 --- a/test/ui-e2e/.auth/setup.ts +++ b/test/ui-e2e/.auth/setup.ts @@ -34,8 +34,8 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { if (await idpScreenText.isVisible()) { console.log(`IDP selection screen detected. Selecting provider: "${idpName}"`); - //look for the specific IDP - const idpLink = page.getByRole('link', { name: idpName, exact: true }); + //look for the specific idp link without exact matching + const idpLink = page.getByRole('link', { name: idpName }); await idpLink.waitFor({ state: 'visible', timeout: TIMEOUTS.short }); await idpLink.click(); @@ -58,10 +58,10 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { await passwordInput.fill(process.env.CLUSTER_PASSWORD); await page.getByRole('button', { name: /Log in/i }).click(); -//handle the OpenShift 4.x Welcome Tour modal if it appears + //handle the openshift welcome tour modal if it appears try { const skipTourButton = page.getByRole('button', { name: /skip tour/i }); - //wait up to 5 seconds for the modal to pop up + //wait briefly for the modal to pop up await skipTourButton.waitFor({ state: 'visible', timeout: TIMEOUTS.short }); await skipTourButton.click(); console.log('Dismissed the OpenShift Welcome Tour modal.'); diff --git a/test/ui-e2e/global.setup.ts b/test/ui-e2e/global.setup.ts index abad09b10e0..466a9e675bb 100644 --- a/test/ui-e2e/global.setup.ts +++ b/test/ui-e2e/global.setup.ts @@ -13,10 +13,10 @@ async function globalSetup() { execSync('oc delete all -l app=spring-petclinic -n openshift-gitops --wait=false', { stdio: 'ignore' }); console.log('* Cluster sanitized. Starting test suite.'); - } catch (error) { + } catch (error) { console.error('Pre-flight cleanup failed. Check your cluster connection.', error); throw error; - } + } } export default globalSetup; \ No newline at end of file diff --git a/test/ui-e2e/run-ui-tests.sh b/test/ui-e2e/run-ui-tests.sh index 87b684f40d2..0e090a3d3e0 100755 --- a/test/ui-e2e/run-ui-tests.sh +++ b/test/ui-e2e/run-ui-tests.sh @@ -73,8 +73,8 @@ if [ -z "$GITOPS_VERSION" ]; then GITOPS_VERSION="Unknown" fi -#get Argo CD version -ARGO_API_VERSION=$(curl -s -k "$ARGOCD_URL/api/version" | grep -o '"Version":"[^"]*"' | cut -d'"' -f4) +#get Argo CD version (with CodeRabbit timeout fix) +ARGO_API_VERSION=$(curl -s -k --max-time 10 "$ARGOCD_URL/api/version" | grep -o '"Version":"[^"]*"' | cut -d'"' -f4) if [ -z "$ARGO_API_VERSION" ]; then ARGO_API_VERSION="Unknown" fi @@ -86,11 +86,14 @@ echo " " # 2. Execute based on the environment if [ "$ENV" = "ci" ] || [ "$ENV" = "pipeline" ]; then echo "Running headlessly in automation ($ENV)..." - npm ci + + #coderabbit hard-fails + npm ci || { echo "Error: npm ci failed."; exit 1; } + if [ "$(uname -s)" = "Darwin" ]; then - npx playwright install chromium + npx playwright install chromium || { echo "Error: Playwright browser install failed."; exit 1; } else - npx playwright install chromium --with-deps + npx playwright install chromium --with-deps || { echo "Error: Playwright browser install failed."; exit 1; } fi #headed from args diff --git a/test/ui-e2e/src/pages/LoginPage.ts b/test/ui-e2e/src/pages/LoginPage.ts index a1b107c0ff1..8665c2d52a6 100644 --- a/test/ui-e2e/src/pages/LoginPage.ts +++ b/test/ui-e2e/src/pages/LoginPage.ts @@ -63,6 +63,6 @@ export class LoginPage { } //Success Checking make we land on the applications dashboard - await this.page.waitForURL('**/applications**', { timeout: 20000 }); + await this.page.waitForURL('**/applications**', { timeout: 30000 }); } } \ No newline at end of file diff --git a/test/ui-e2e/tests/ha-login.spec.ts b/test/ui-e2e/tests/ha-login.spec.ts new file mode 100644 index 00000000000..81634abb8a2 --- /dev/null +++ b/test/ui-e2e/tests/ha-login.spec.ts @@ -0,0 +1,131 @@ +import { test, expect } from '@playwright/test'; +import { execSync } from 'child_process'; +import { LoginPage } from '../src/pages/LoginPage'; + +//run tests together (same ha state setup) +test.describe.configure({ mode: 'serial' }); + +test.describe('HA Login Verification', () => { + + //force fresh login + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeAll(async ({}, testInfo) => { + testInfo.setTimeout(600000); //10 mins for ha rollout + + console.log('\n[setup] Enabling High Availability (HA) for Argo CD...'); + try { + //patch cr with strict timeout + execSync('oc patch argocd openshift-gitops -n openshift-gitops --type=merge -p \'{"spec":{"ha":{"enabled":true}}}\'', { stdio: 'inherit', timeout: 30000 }); + + console.log('[setup] Polling cluster for new HA deployment (this may take a few minutes)...'); + let retries = 30; + let podsReady = false; + + while (retries > 0 && !podsReady) { + try { + execSync('oc wait --for=condition=Available deployment/openshift-gitops-redis-ha-haproxy -n openshift-gitops --timeout=30s', { stdio: 'pipe' }); + podsReady = true; + } catch (e) { + console.log(`[setup] HA proxy not provisioned yet. Retrying in 10s... (${retries} attempts left)`); + await new Promise(resolve => setTimeout(resolve, 10000)); + retries--; + } + } + + if (!podsReady) { + throw new Error('HA proxy deployment never appeared or became available after polling.'); + } + + console.log('[setup] Waiting for Operator to roll out HA-aware components...'); + + //wait for rollouts + execSync('oc rollout status statefulset/openshift-gitops-redis-ha-server -n openshift-gitops --timeout=300s', { stdio: 'inherit' }); + execSync('oc rollout status deployment/openshift-gitops-server -n openshift-gitops --timeout=300s', { stdio: 'inherit' }); + execSync('oc rollout status deployment/openshift-gitops-dex-server -n openshift-gitops --timeout=300s', { stdio: 'inherit' }); + + console.log('[setup] Rollouts complete. Giving cluster time to stabilize network routes...'); + await new Promise(resolve => setTimeout(resolve, 10000)); + + console.log('[setup] HA successfully enabled and stabilized.'); + } catch (error) { + console.error('[setup] Failed to enable HA. Aborting tests.', error); + throw error; + } + }); + + test.afterAll(async ({}, testInfo) => { + testInfo.setTimeout(300000); //5 mins for teardown + + console.log('\n[teardown] Disabling High Availability (HA) to restore cluster state...'); + try { + //disable ha with strict timeout + execSync('oc patch argocd openshift-gitops -n openshift-gitops --type=merge -p \'{"spec":{"ha":{"enabled":false}}}\'', { stdio: 'inherit', timeout: 30000 }); + + //wait for rollbacks + execSync('oc wait --for=condition=Available deployment/openshift-gitops-server -n openshift-gitops --timeout=300s', { stdio: 'inherit' }); + execSync('oc rollout status deployment/openshift-gitops-dex-server -n openshift-gitops --timeout=300s', { stdio: 'inherit' }); + + //helper to independently wait for deletions and only ignore NotFound errors + const waitForDelete = (resource: string) => { + try { + execSync(`oc wait --for=delete ${resource} -n openshift-gitops --timeout=300s`, { stdio: 'pipe' }); + } catch (e: any) { + const stderr = e.stderr ? e.stderr.toString() : ''; + const message = e.message || ''; + if (!stderr.includes('NotFound') && !message.includes('NotFound')) { + throw e; //rethrow timeouts or api failures + } + } + }; + + //wait for ha components to delete independently + waitForDelete('statefulset/openshift-gitops-redis-ha-server'); + waitForDelete('deployment/openshift-gitops-redis-ha-haproxy'); + + console.log('[teardown] Cluster successfully restored to non-HA state.'); + } catch (error) { + console.error('[teardown] Failed to disable HA during cleanup! Cluster may be in a dirty state.', error); + throw error; + } + }); + + test('Local Admin Login under HA', async ({ page }) => { + test.setTimeout(120000); + + let rawOutput = execSync('oc extract secret/openshift-gitops-cluster -n openshift-gitops --keys=admin.password --to=-', { timeout: 30000 }).toString(); + const adminPassword = rawOutput.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))[0]; + + if (!adminPassword) { + throw new Error('failed to extract admin password from cluster secret'); + } + + await page.goto('/login?dex=none', { waitUntil: 'load' }); + + const userField = page.getByLabel(/username/i); + await userField.waitFor({ state: 'visible', timeout: 30000 }); + + //fill form + await userField.fill('admin'); + await page.locator('input[type="password"]').fill(adminPassword); + await page.getByRole('button', { name: /sign in/i }).click(); + + await expect(page.getByText('Applications', { exact: true }).first()).toBeVisible({ timeout: 30000 }); + }); + + test('OpenShift SSO Login under HA', async ({ page }) => { + test.setTimeout(120000); + + const loginPage = new LoginPage(page); + await loginPage.goto(); + + await loginPage.loginViaOpenShift( + process.env.CLUSTER_USER!, + process.env.CLUSTER_PASSWORD!, + process.env.IDP || 'kube:admin' + ); + + await expect(page.getByText('Applications', { exact: true }).first()).toBeVisible({ timeout: 30000 }); + }); + +}); \ No newline at end of file diff --git a/test/ui-e2e/tests/resource-tree.spec.ts b/test/ui-e2e/tests/resource-tree.spec.ts index e262fa9b221..4123e396f0b 100644 --- a/test/ui-e2e/tests/resource-tree.spec.ts +++ b/test/ui-e2e/tests/resource-tree.spec.ts @@ -6,7 +6,7 @@ test.describe('Argo CD Resource Tree and Pod Logs', () => { test.use({ storageState: '.auth/storageState.json' }); - test('Navigate to app details, open a Pod, and verify logs stream', async ({ page, managedApp, argoVersion }) => { + test('Navigate to app details, open a Pod, and verify logs stream', async ({ page, managedApp }) => { test.setTimeout(120000); const appsPage = new ApplicationsPage(page);