-
Notifications
You must be signed in to change notification settings - Fork 355
Gitops 9686 UI test app pruning #1252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
trdoyle81
wants to merge
11
commits into
redhat-developer:master
Choose a base branch
from
trdoyle81:GITOPS-9686-UI-Test-App-Pruning
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+219
−0
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
6863e12
test updates and address coderabbit feedback
7d6c832
add Argocd version check and harden app health locators
cfc7674
add UI test for clean application deletion
80ced7c
test updates and address coderabbit feedback
891c18e
add Argocd version check and harden app health locators
6081821
test updates and address coderabbit feedback
02ec641
add Argocd version check and harden app health locators
d8c5897
test updates and address coderabbit feedback
076cbfd
add Argocd version check and harden app health locators
7e58a78
address inital codeRabbit feedback
cde1db7
address further codeRabbit feedback for timeout
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| import { test, expect } from '../src/fixtures'; | ||
| import { execFileSync } from 'child_process'; | ||
|
|
||
| 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'; | ||
|
|
||
| //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 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 | ||
| 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 { | ||
| //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 | ||
| 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; | ||
| let lastSync = ''; | ||
| let lastHealth = ''; | ||
| let lastMessage = ''; | ||
| for (let i = 1; i <= 30; i++) { | ||
| try { | ||
| 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}/30): Waiting for resource...`); | ||
| } | ||
| await new Promise(resolve => setTimeout(resolve, 3000)); | ||
| } | ||
|
|
||
| if (!isSynced) { | ||
| 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); | ||
| 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 { | ||
| 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}`); | ||
| } | ||
|
|
||
| //verify resource is completely absent from cluster | ||
| let isDeleted = false; | ||
| for (let i = 1; i <= 5; i++) { | ||
| if (!applicationExists()) { | ||
| isDeleted = true; | ||
| break; | ||
| } | ||
| //resource still exists, wait before checking again | ||
| await new Promise(resolve => setTimeout(resolve, 2000)); | ||
| } | ||
|
|
||
| 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 }) => { | ||
| //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"]') | ||
| .filter({ hasText: appName }); | ||
|
|
||
| //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(); | ||
|
|
||
| //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++) { | ||
| 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); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| //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); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.