From 6863e12d19bc9ef3b5d98464922673ae4d13bdd3 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Wed, 24 Jun 2026 13:52:51 +0100 Subject: [PATCH 01/11] test updates and address coderabbit feedback Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index f64787d22a1..29cea47f0f6 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,7 +5,6 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; - argoVersion: string; }; export const test = base.extend({ @@ -31,33 +30,6 @@ export const test = base.extend({ await use(page); }, - //get target argocd version - argoVersion: async ({ page }, use) => { - try { - //get version - const response = await page.request.get('/api/version'); - - if (!response.ok()) { - throw new Error(`API returned status: ${response.status()}`); - } - - const data = await response.json(); - const fullVersion = data.Version || 'Unknown'; - - //extract the major.minor version (e.g., "v2.10.1" -> "2.10") - const match = fullVersion.match(/v(\d+\.\d+)/); - const version = match ? match[1] : '3.0'; - - //for debugging/CI logs - console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); - - await use(version); - } catch (error) { - console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); - await use('3.0'); // Default to 3.0 - } - }, - managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 7d6c8326e25ed77176201f1ebb9a1985276d10ad Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Fri, 26 Jun 2026 11:13:58 +0100 Subject: [PATCH 02/11] add Argocd version check and harden app health locators Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 +++++++++++++++++++++++++ test/ui-e2e/tests/resource-tree.spec.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index 29cea47f0f6..f64787d22a1 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,6 +5,7 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; + argoVersion: string; }; export const test = base.extend({ @@ -30,6 +31,33 @@ export const test = base.extend({ await use(page); }, + //get target argocd version + argoVersion: async ({ page }, use) => { + try { + //get version + const response = await page.request.get('/api/version'); + + if (!response.ok()) { + throw new Error(`API returned status: ${response.status()}`); + } + + const data = await response.json(); + const fullVersion = data.Version || 'Unknown'; + + //extract the major.minor version (e.g., "v2.10.1" -> "2.10") + const match = fullVersion.match(/v(\d+\.\d+)/); + const version = match ? match[1] : '3.0'; + + //for debugging/CI logs + console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); + + await use(version); + } catch (error) { + console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); + await use('3.0'); // Default to 3.0 + } + }, + managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); diff --git a/test/ui-e2e/tests/resource-tree.spec.ts b/test/ui-e2e/tests/resource-tree.spec.ts index 4123e396f0b..e262fa9b221 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 }) => { + test('Navigate to app details, open a Pod, and verify logs stream', async ({ page, managedApp, argoVersion }) => { test.setTimeout(120000); const appsPage = new ApplicationsPage(page); From cfc7674ff53c7a631065ebbcb39f43b07fb9d400 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Tue, 21 Jul 2026 15:05:11 +0100 Subject: [PATCH 03/11] add UI test for clean application deletion Signed-off-by: Triona Doyle --- test/ui-e2e/tests/app-deletion.spec.ts | 157 +++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/ui-e2e/tests/app-deletion.spec.ts diff --git a/test/ui-e2e/tests/app-deletion.spec.ts b/test/ui-e2e/tests/app-deletion.spec.ts new file mode 100644 index 00000000000..b1bbc0c529e --- /dev/null +++ b/test/ui-e2e/tests/app-deletion.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '@playwright/test'; +import { execSync } from 'child_process'; +import { LoginPage } from '../src/pages/LoginPage'; + +test.describe('Clean Application Deletion (Pruning)', () => { + //make app name unique for test isolation + const appName = `ui-deletion-${Date.now()}`; + //pin revision to immutable commit SHA for reproducibility + const targetCommit = '8088f4c0d970abb09e250248cc97e35623447cb5'; + + test.beforeAll(async ({}, testInfo) => { + //set timeout to 120s + testInfo.setTimeout(120000); + console.log(`\n[setup] Deploying dummy application '${appName}' via CLI...`); + + //define standard guestbook app yaml targeting openshift-gitops namespace + const appYaml = ` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ${appName} + namespace: openshift-gitops +spec: + destination: + namespace: openshift-gitops + server: https://kubernetes.default.svc + project: default + source: + path: guestbook + repoURL: https://github.com/argoproj/argocd-example-apps.git + targetRevision: ${targetCommit} + syncPolicy: + automated: + prune: true + selfHeal: true +`; + try { + //deploy dummy app via cli with process timeout + execSync(`echo "${appYaml}" | oc apply -f -`, { stdio: 'pipe', timeout: 15000 }); + + //poll until argo cd controller populates sync status (bounded loop) + let isSynced = false; + for (let i = 1; i <= 15; i++) { + try { + const syncStatus = execSync(`oc get application ${appName} -n openshift-gitops -o jsonpath='{.status.sync.status}'`, { stdio: 'pipe', timeout: 3000 }).toString().trim(); + console.log(`[setup] Checking sync status (Attempt ${i}/15): '${syncStatus || 'Initializing...'}'`); + if (syncStatus === 'Synced') { + isSynced = true; + break; + } + } catch (e) { + console.log(`[setup] Checking sync status (Attempt ${i}/15): Waiting for resource...`); + } + await new Promise(resolve => setTimeout(resolve, 3000)); + } + + if (!isSynced) { + throw new Error(`Dummy application '${appName}' never reached Synced status.`); + } + } catch (e) { + console.error('Failed to pre-deploy dummy app', e); + throw e; + } + }); + + test.afterAll(async ({}, testInfo) => { + //set hook timeout to 60s + testInfo.setTimeout(60000); + console.log('\n[teardown] Ensuring application is cleaned up...'); + + //attempt fallback cleanup if ui deletion failed or was skipped + try { + execSync(`oc delete application ${appName} -n openshift-gitops --ignore-not-found --wait=true`, { stdio: 'pipe', timeout: 15000 }); + } catch (e) { + console.warn(`[teardown] Initial cleanup command failed: ${(e as Error).message}`); + } + + //verify resource is completely absent from cluster + let isDeleted = false; + for (let i = 1; i <= 5; i++) { + try { + execSync(`oc get application ${appName} -n openshift-gitops`, { stdio: 'pipe', timeout: 2000 }); + //if command succeeds resource still exists + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch (e) { + //resource no longer exists + isDeleted = true; + break; + } + } + + if (!isDeleted) { + throw new Error(`[teardown] Cleanup verification failed: '${appName}' still exists on cluster.`); + } + }); + + test('Delete application via UI and verify cascading deletion', async ({ page }) => { + //set explicit test timeout budget + test.setTimeout(90000); + + //log into argo cd ui + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.loginViaOpenShift( + process.env.CLUSTER_USER!, + process.env.CLUSTER_PASSWORD!, + process.env.IDP || 'kube:admin' + ); + + //locate application card specifically bound to appName without broad div scanning + const appTile = page.locator('.application-tile, [class*="application-tile"], [class*="applications-list__entry"]') + .filter({ hasText: appName }); + + //ensure application tile appears on dashboard + await expect(appTile).toBeVisible({ timeout: 30000 }); + + //click delete button scoped specifically to this app card + const deleteBtn = appTile.locator('[qe-id="applications-tiles-button-delete"]'); + await deleteBtn.click(); + + //locate modal container via dialog role or confirmation prompt text + const modal = page.getByRole('dialog') + .or(page.locator('div').filter({ hasText: /to confirm the deletion/i })) + .first(); + await expect(modal).toBeVisible({ timeout: 15000 }); + + //type application name into confirmation field + const confirmInput = modal.getByRole('textbox').or(modal.locator('input')).first(); + await confirmInput.fill(appName); + + //confirm deletion + const okBtn = modal.getByRole('button', { name: /^ok$/i }).or(modal.locator('button').filter({ hasText: /^ok$/i })).first(); + await okBtn.click(); + + //assert modal closes after confirming + await expect(modal).toBeHidden({ timeout: 15000 }); + + //assert app tile disappears from ui dashboard + await expect(appTile).toBeHidden({ timeout: 30000 }); + + //verify backend cr deletion via cli directly within test block before teardown + let backendDeleted = false; + for (let i = 1; i <= 10; i++) { + try { + execSync(`oc get application ${appName} -n openshift-gitops`, { stdio: 'pipe', timeout: 2000 }); + //resource still present on cluster, wait before checking again + await new Promise(resolve => setTimeout(resolve, 2000)); + } catch (e) { + //oc command threw an error, meaning resource was deleted from kubernetes api + backendDeleted = true; + break; + } + } + + expect(backendDeleted).toBe(true); + }); +}); \ No newline at end of file From 80ced7ca19196dc14f2c7eeafb391f3d15a05266 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Wed, 24 Jun 2026 13:52:51 +0100 Subject: [PATCH 04/11] test updates and address coderabbit feedback Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index f64787d22a1..29cea47f0f6 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,7 +5,6 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; - argoVersion: string; }; export const test = base.extend({ @@ -31,33 +30,6 @@ export const test = base.extend({ await use(page); }, - //get target argocd version - argoVersion: async ({ page }, use) => { - try { - //get version - const response = await page.request.get('/api/version'); - - if (!response.ok()) { - throw new Error(`API returned status: ${response.status()}`); - } - - const data = await response.json(); - const fullVersion = data.Version || 'Unknown'; - - //extract the major.minor version (e.g., "v2.10.1" -> "2.10") - const match = fullVersion.match(/v(\d+\.\d+)/); - const version = match ? match[1] : '3.0'; - - //for debugging/CI logs - console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); - - await use(version); - } catch (error) { - console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); - await use('3.0'); // Default to 3.0 - } - }, - managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 891c18ea52869d608396e5fcf2c81156bd315e6a Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Fri, 26 Jun 2026 11:13:58 +0100 Subject: [PATCH 05/11] add Argocd version check and harden app health locators Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index 29cea47f0f6..f64787d22a1 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,6 +5,7 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; + argoVersion: string; }; export const test = base.extend({ @@ -30,6 +31,33 @@ export const test = base.extend({ await use(page); }, + //get target argocd version + argoVersion: async ({ page }, use) => { + try { + //get version + const response = await page.request.get('/api/version'); + + if (!response.ok()) { + throw new Error(`API returned status: ${response.status()}`); + } + + const data = await response.json(); + const fullVersion = data.Version || 'Unknown'; + + //extract the major.minor version (e.g., "v2.10.1" -> "2.10") + const match = fullVersion.match(/v(\d+\.\d+)/); + const version = match ? match[1] : '3.0'; + + //for debugging/CI logs + console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); + + await use(version); + } catch (error) { + console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); + await use('3.0'); // Default to 3.0 + } + }, + managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 6081821228df5f4c3e4d75367eed0e1c3e21f1e8 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Wed, 24 Jun 2026 13:52:51 +0100 Subject: [PATCH 06/11] test updates and address coderabbit feedback Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index f64787d22a1..29cea47f0f6 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,7 +5,6 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; - argoVersion: string; }; export const test = base.extend({ @@ -31,33 +30,6 @@ export const test = base.extend({ await use(page); }, - //get target argocd version - argoVersion: async ({ page }, use) => { - try { - //get version - const response = await page.request.get('/api/version'); - - if (!response.ok()) { - throw new Error(`API returned status: ${response.status()}`); - } - - const data = await response.json(); - const fullVersion = data.Version || 'Unknown'; - - //extract the major.minor version (e.g., "v2.10.1" -> "2.10") - const match = fullVersion.match(/v(\d+\.\d+)/); - const version = match ? match[1] : '3.0'; - - //for debugging/CI logs - console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); - - await use(version); - } catch (error) { - console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); - await use('3.0'); // Default to 3.0 - } - }, - managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 02ec641d0860ba6d48de966f33f607520f376eb6 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Fri, 26 Jun 2026 11:13:58 +0100 Subject: [PATCH 07/11] add Argocd version check and harden app health locators Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index 29cea47f0f6..f64787d22a1 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,6 +5,7 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; + argoVersion: string; }; export const test = base.extend({ @@ -30,6 +31,33 @@ export const test = base.extend({ await use(page); }, + //get target argocd version + argoVersion: async ({ page }, use) => { + try { + //get version + const response = await page.request.get('/api/version'); + + if (!response.ok()) { + throw new Error(`API returned status: ${response.status()}`); + } + + const data = await response.json(); + const fullVersion = data.Version || 'Unknown'; + + //extract the major.minor version (e.g., "v2.10.1" -> "2.10") + const match = fullVersion.match(/v(\d+\.\d+)/); + const version = match ? match[1] : '3.0'; + + //for debugging/CI logs + console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); + + await use(version); + } catch (error) { + console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); + await use('3.0'); // Default to 3.0 + } + }, + managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From d8c58975b11fa64a13cc81ae41cc8ffe9a3ca0b5 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Wed, 24 Jun 2026 13:52:51 +0100 Subject: [PATCH 08/11] test updates and address coderabbit feedback Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index f64787d22a1..29cea47f0f6 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,7 +5,6 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; - argoVersion: string; }; export const test = base.extend({ @@ -31,33 +30,6 @@ export const test = base.extend({ await use(page); }, - //get target argocd version - argoVersion: async ({ page }, use) => { - try { - //get version - const response = await page.request.get('/api/version'); - - if (!response.ok()) { - throw new Error(`API returned status: ${response.status()}`); - } - - const data = await response.json(); - const fullVersion = data.Version || 'Unknown'; - - //extract the major.minor version (e.g., "v2.10.1" -> "2.10") - const match = fullVersion.match(/v(\d+\.\d+)/); - const version = match ? match[1] : '3.0'; - - //for debugging/CI logs - console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); - - await use(version); - } catch (error) { - console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); - await use('3.0'); // Default to 3.0 - } - }, - managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 076cbfd6338cf42edeceab9c13e830f111a17e6a Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Fri, 26 Jun 2026 11:13:58 +0100 Subject: [PATCH 09/11] add Argocd version check and harden app health locators Signed-off-by: Triona Doyle --- test/ui-e2e/src/fixtures.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index 29cea47f0f6..f64787d22a1 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -5,6 +5,7 @@ import { ApplicationsPage } from './pages/ApplicationsPage'; //define custom fixture types type MyFixtures = { managedApp: string; + argoVersion: string; }; export const test = base.extend({ @@ -30,6 +31,33 @@ export const test = base.extend({ await use(page); }, + //get target argocd version + argoVersion: async ({ page }, use) => { + try { + //get version + const response = await page.request.get('/api/version'); + + if (!response.ok()) { + throw new Error(`API returned status: ${response.status()}`); + } + + const data = await response.json(); + const fullVersion = data.Version || 'Unknown'; + + //extract the major.minor version (e.g., "v2.10.1" -> "2.10") + const match = fullVersion.match(/v(\d+\.\d+)/); + const version = match ? match[1] : '3.0'; + + //for debugging/CI logs + console.log(`TARGETING ARGO CD VERSION: ${fullVersion}`); + + await use(version); + } catch (error) { + console.warn(`\n[warn] Failed to fetch Argo CD version from API. Defaulting to 3.0. Reason: ${error instanceof Error ? error.message : 'Unknown'}\n`); + await use('3.0'); // Default to 3.0 + } + }, + managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); From 7e58a78abed91e539b8de9a3d79aea8b9d0a1b80 Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Mon, 10 Aug 2026 12:23:53 +0100 Subject: [PATCH 10/11] address inital codeRabbit feedback Signed-off-by: Triona Doyle --- test/ui-e2e/tests/app-deletion.spec.ts | 140 +++++++++++++++++------- test/ui-e2e/tests/resource-tree.spec.ts | 2 +- 2 files changed, 102 insertions(+), 40 deletions(-) diff --git a/test/ui-e2e/tests/app-deletion.spec.ts b/test/ui-e2e/tests/app-deletion.spec.ts index b1bbc0c529e..37b7d87448c 100644 --- a/test/ui-e2e/tests/app-deletion.spec.ts +++ b/test/ui-e2e/tests/app-deletion.spec.ts @@ -1,6 +1,5 @@ -import { test, expect } from '@playwright/test'; -import { execSync } from 'child_process'; -import { LoginPage } from '../src/pages/LoginPage'; +import { test, expect } from '../src/fixtures'; +import { execFileSync } from 'child_process'; test.describe('Clean Application Deletion (Pruning)', () => { //make app name unique for test isolation @@ -8,11 +7,37 @@ test.describe('Clean Application Deletion (Pruning)', () => { //pin revision to immutable commit SHA for reproducibility const targetCommit = '8088f4c0d970abb09e250248cc97e35623447cb5'; + //returns true when the application cr is still present + const applicationExists = (): boolean => { + const out = execFileSync( + 'oc', + ['get', 'application', appName, '-n', 'openshift-gitops', '--ignore-not-found', '-o', 'name'], + { stdio: 'pipe', timeout: 5000 } + ).toString().trim(); + return out.length > 0; + }; + + //guestbook always creates deploy/svc named guestbook-ui. use --ignore-not-found -o name + //(empty = gone; real oc errors still throw). avoids instance-label lookups that fail under annotation tracking. + const remainingChildResources = (): string => { + const kinds = ['deploy', 'svc'] as const; + return kinds + .map((kind) => + execFileSync( + 'oc', + ['get', kind, 'guestbook-ui', '-n', 'openshift-gitops', '--ignore-not-found', '-o', 'name'], + { stdio: 'pipe', timeout: 5000 } + ).toString().trim() + ) + .filter(Boolean) + .join('\n'); + }; + test.beforeAll(async ({}, testInfo) => { - //set timeout to 120s - testInfo.setTimeout(120000); + //set timeout to 150s (sync poll can take ~90s after a fresh install) + testInfo.setTimeout(150000); console.log(`\n[setup] Deploying dummy application '${appName}' via CLI...`); - + //define standard guestbook app yaml targeting openshift-gitops namespace const appYaml = ` apiVersion: argoproj.io/v1alpha1 @@ -35,27 +60,58 @@ spec: selfHeal: true `; try { + //clear leftover guestbook children that can leave a new app stuck Unknown/OutOfSync + for (const kind of ['deploy', 'svc'] as const) { + execFileSync( + 'oc', + ['delete', kind, 'guestbook-ui', '-n', 'openshift-gitops', '--ignore-not-found', '--wait=false'], + { stdio: 'pipe', timeout: 15000 } + ); + } + //deploy dummy app via cli with process timeout - execSync(`echo "${appYaml}" | oc apply -f -`, { stdio: 'pipe', timeout: 15000 }); - - //poll until argo cd controller populates sync status (bounded loop) + execFileSync('oc', ['apply', '-f', '-'], { input: appYaml, stdio: 'pipe', timeout: 15000 }); + + //poll until argo cd reports synced (unknown is common while repo-server warms up) let isSynced = false; - for (let i = 1; i <= 15; i++) { + let lastSync = ''; + let lastHealth = ''; + let lastMessage = ''; + for (let i = 1; i <= 30; i++) { try { - const syncStatus = execSync(`oc get application ${appName} -n openshift-gitops -o jsonpath='{.status.sync.status}'`, { stdio: 'pipe', timeout: 3000 }).toString().trim(); - console.log(`[setup] Checking sync status (Attempt ${i}/15): '${syncStatus || 'Initializing...'}'`); - if (syncStatus === 'Synced') { + lastSync = execFileSync( + 'oc', + ['get', 'application', appName, '-n', 'openshift-gitops', '-o', 'jsonpath={.status.sync.status}'], + { stdio: 'pipe', timeout: 3000 } + ).toString().trim(); + lastHealth = execFileSync( + 'oc', + ['get', 'application', appName, '-n', 'openshift-gitops', '-o', 'jsonpath={.status.health.status}'], + { stdio: 'pipe', timeout: 3000 } + ).toString().trim(); + lastMessage = execFileSync( + 'oc', + ['get', 'application', appName, '-n', 'openshift-gitops', '-o', 'jsonpath={.status.conditions[0].message}'], + { stdio: 'pipe', timeout: 3000 } + ).toString().trim(); + console.log( + `[setup] Checking sync status (Attempt ${i}/30): sync='${lastSync || 'Initializing...'}' health='${lastHealth || '-'}'` + ); + if (lastSync === 'Synced') { isSynced = true; break; } } catch (e) { - console.log(`[setup] Checking sync status (Attempt ${i}/15): Waiting for resource...`); + console.log(`[setup] Checking sync status (Attempt ${i}/30): Waiting for resource...`); } await new Promise(resolve => setTimeout(resolve, 3000)); } if (!isSynced) { - throw new Error(`Dummy application '${appName}' never reached Synced status.`); + throw new Error( + `Dummy application '${appName}' never reached Synced status ` + + `(last sync='${lastSync || '-'}' health='${lastHealth || '-'}' message='${lastMessage || '-'}').` + ); } } catch (e) { console.error('Failed to pre-deploy dummy app', e); @@ -67,10 +123,14 @@ spec: //set hook timeout to 60s testInfo.setTimeout(60000); console.log('\n[teardown] Ensuring application is cleaned up...'); - + //attempt fallback cleanup if ui deletion failed or was skipped try { - execSync(`oc delete application ${appName} -n openshift-gitops --ignore-not-found --wait=true`, { stdio: 'pipe', timeout: 15000 }); + execFileSync( + 'oc', + ['delete', 'application', appName, '-n', 'openshift-gitops', '--ignore-not-found', '--wait=true'], + { stdio: 'pipe', timeout: 15000 } + ); } catch (e) { console.warn(`[teardown] Initial cleanup command failed: ${(e as Error).message}`); } @@ -78,15 +138,12 @@ spec: //verify resource is completely absent from cluster let isDeleted = false; for (let i = 1; i <= 5; i++) { - try { - execSync(`oc get application ${appName} -n openshift-gitops`, { stdio: 'pipe', timeout: 2000 }); - //if command succeeds resource still exists - await new Promise(resolve => setTimeout(resolve, 2000)); - } catch (e) { - //resource no longer exists + if (!applicationExists()) { isDeleted = true; break; } + //resource still exists, wait before checking again + await new Promise(resolve => setTimeout(resolve, 2000)); } if (!isDeleted) { @@ -98,15 +155,6 @@ spec: //set explicit test timeout budget test.setTimeout(90000); - //log into argo cd ui - const loginPage = new LoginPage(page); - await loginPage.goto(); - await loginPage.loginViaOpenShift( - process.env.CLUSTER_USER!, - process.env.CLUSTER_PASSWORD!, - process.env.IDP || 'kube:admin' - ); - //locate application card specifically bound to appName without broad div scanning const appTile = page.locator('.application-tile, [class*="application-tile"], [class*="applications-list__entry"]') .filter({ hasText: appName }); @@ -114,6 +162,9 @@ spec: //ensure application tile appears on dashboard await expect(appTile).toBeVisible({ timeout: 30000 }); + //confirm guestbook children exist before delete so cascade assertion is meaningful + expect(remainingChildResources()).not.toBe(''); + //click delete button scoped specifically to this app card const deleteBtn = appTile.locator('[qe-id="applications-tiles-button-delete"]'); await deleteBtn.click(); @@ -141,17 +192,28 @@ spec: //verify backend cr deletion via cli directly within test block before teardown let backendDeleted = false; for (let i = 1; i <= 10; i++) { - try { - execSync(`oc get application ${appName} -n openshift-gitops`, { stdio: 'pipe', timeout: 2000 }); - //resource still present on cluster, wait before checking again - await new Promise(resolve => setTimeout(resolve, 2000)); - } catch (e) { - //oc command threw an error, meaning resource was deleted from kubernetes api + if (!applicationExists()) { backendDeleted = true; break; } + //resource still present on cluster, wait before checking again + await new Promise(resolve => setTimeout(resolve, 2000)); } expect(backendDeleted).toBe(true); + + //verify cascading deletion removed guestbook deploy/svc + let childrenGone = false; + for (let i = 1; i <= 10; i++) { + const remaining = remainingChildResources(); + if (remaining === '') { + childrenGone = true; + break; + } + console.log(`[verify] Waiting for child resources to prune (Attempt ${i}/10): ${remaining}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + expect(childrenGone).toBe(true); }); -}); \ 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); From cde1db762ef00f2669c931ab6f814410168fb39b Mon Sep 17 00:00:00 2001 From: Triona Doyle Date: Mon, 10 Aug 2026 14:47:29 +0100 Subject: [PATCH 11/11] address further codeRabbit feedback for timeout Signed-off-by: Triona Doyle --- test/ui-e2e/tests/app-deletion.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ui-e2e/tests/app-deletion.spec.ts b/test/ui-e2e/tests/app-deletion.spec.ts index 37b7d87448c..9991611fae8 100644 --- a/test/ui-e2e/tests/app-deletion.spec.ts +++ b/test/ui-e2e/tests/app-deletion.spec.ts @@ -152,8 +152,8 @@ spec: }); test('Delete application via UI and verify cascading deletion', async ({ page }) => { - //set explicit test timeout budget - test.setTimeout(90000); + //covers ui waits plus backend/child prune polling budgets + test.setTimeout(180000); //locate application card specifically bound to appName without broad div scanning const appTile = page.locator('.application-tile, [class*="application-tile"], [class*="applications-list__entry"]')